Module: Multilingual content
4 of 6 Pages
Handle multilingual URLs
Offering your content to visitors in multiple languages is an important step in reaching a broader audience.
If you followed the first part of the Multilingual content mini-series, your website can now display content in multiple languages, and it’s time to make switching between those languages seamless for your visitors.
In the second part of the series, you’ll enhance the user experience by handling multilingual URLs and implementing a simple language selector that ties it all together.
Multilingual content series
This is the second and last part of the Set up multilingual mini-series. Using a concrete example, the mini-series demonstrates best practices and necessary steps you need to perform in order to present your website content in multiple languages in a user-friendly way.
If you have followed along with the previous part of the series, you now:
- understand the basic terminology related to multilingual support in Xperience by Kentico.
- have your home page and cookie consents in two language versions: English and Spanish.
Continue with this series and learn how to handle navigating and working with URLs for multilingual pages. To tie everything together, you will implement a simple language selector control for the visitor, similar to the gif below:
Prerequisites
To follow along, you should have the following:
- running instance of Xperience by Kentico version 29.6.1 or higher (for guidance, follow Install a specific version of Xperience by Kentico)
- a website channel set up with at least one page displaying a content item stored in the content hub (for guidance, follow our Kickstart series)
- content items and pages existing in at least two different languages (this first part of the Multilingual series uses English and Spanish as an example)
- some version of routing implemented in your application (a link navigating from one page to another is sufficient)
Alternatively, feel free to take a look at the finished branch of our Training guides repository to see the completed implementation of both multilingual URL handling and a language selector. All the code samples below are sourced from the repository.
Handle URLs and routing
Setting up multiple languages in Xperience by Kentico affects URL generation for web pages. For any non-primary language, the URL has to contain the language code (e.g., ~/es/page).
As a developer, you must ensure the links across your site work properly, preserving the language context, as visitors navigate your site.
If you have followed along with the first part of this mini-series or are running the app from the finished branch in our repository, your site contains a Tracking consent banner and a Cookie policy page that look similar to the screenshot below:
Let’s look at an example of routing on the banner. It includes a Configure cookies link. This link is supposed to navigate the visitor to the Cookie policy page so they can set their preferences with more granularity. However, right now, it always navigates the visitor to the Cookie policy page in the primary language, regardless of previous context. Navigate to the TrackingConsent.cshtml file to find out why:
In the TrackingConsent view, the Configure cookies anchor tag points to a URL composed of BaseUrl
and the relative RedirectUrl
.
...
<a href="@string.Format("{0}{1}", Model.BaseUrl, Model.RedirectUrl)" class="xpcookiebanner__cta" data-tracking-code="CookieBar_More">
@Model.ConfigureMessage
</a>
...
Look inside the TrackingConsentViewComponent.cs and notice that the BaseUrl
property is populated by the GetBaseUrl
method of HttpRequestService
. This is a problem, as the GetBaseUrl
method returns the site’s base URL without a language codename.
You need to define a new method that will consider the current request’s language and add relevant language code to the base URL.
To handle preserving language thread in routing, Xperience by Kentico provides an option to define a custom language name key in the RouteValueDictionary object of the request:
Define a new constant in TrainingGuides.Web/Features/Shared/Helpers/ApplicationConstants.cs.
C#ApplicationConstants.csnamespace TrainingGuides.Web.Features.Shared.Helpers; internal static class ApplicationConstants { public const string LANGUAGE_KEY = "language"; }
In your Program.cs file, locate the
UseWebPageRouting
method call that enables content tree-based routing. Pass in your custom language key:C#Program.cs... builder.Services.AddKentico(async features => { ... // Pass in the constant as a custom LanguageNameRouteValuesKey using WebPageRoutingOptions object. features.UseWebPageRouting(new WebPageRoutingOptions { LanguageNameRouteValuesKey = ApplicationConstants.LANGUAGE_KEY }); }); ...
A custom language code name in the request can come in handy in several scenarios. In the next step of this section, we utilize it to manually extract the language from the request URL.
Another example would be invoking a custom action from the context of the page served by the router.
Read more about LanguageNameRouteValuesKey and its usage in our documentation.
Add a new
GetBaseUrlWithLanguage
public method declaration into the IHttpRequestService interface (in TrainingGuides.Web/Features/Shared/Services).Implement the
GetBaseUrlWithLanguage
method in the HttpRequestService class:C#HttpRequestService.cs... private HttpRequest RetrieveCurrentRequest() => httpContextAccessor?.HttpContext?.Request ?? throw new NullReferenceException("Unable to retrieve current request context."); ... /// <summary> /// Retrieves Base URL from the current request context. If current site is in a language variant, returns language with the base URL as well /// </summary> /// <returns>The base URL in current language variant. (e.g. website.com or website.com/es)</returns> public string GetBaseUrlWithLanguage() { // Use httpContextAccessor (already injected in the Service using DI) to access current request context. var currentRequest = RetrieveCurrentRequest(); //Access the language of the request using the LANGUAGE_KEY constant defined in steps 1. and 2. string language = (string?)currentRequest.RouteValues[ApplicationConstants.LANGUAGE_KEY] ?? string.Empty; // Parse the current request URL to find out if it contains the language code. // No language code in the URL means the request language is the primary language. var webPageUrlPathList = ((string?)currentRequest.RouteValues[WEB_PAGE_URL_PATHS])?.Split('/').ToList() ?? []; bool notPrimaryLanguage = webPageUrlPathList.Contains(language); // Call existing private GetBaseUrl method to get the base URL. // If the language of the current request is not primary, add language code to the URL. var url = new UriBuilder(GetBaseUrl(currentRequest)) { Path = notPrimaryLanguage ? $"/{language}" : string.Empty }; return url.ToString(); } ...
Notice that our example appends the language to the base URL only when it does not equal the primary language.
If you navigate to a URL with a language code of the primary language, the system will NOT redirect you to a URL without a language code. Instead, it returns an HTTP 404 (Not found) error.
For example, if the primary language of your channel is English, http://localhost:53415/en/page will return HTTP 404.
The
GetBaseUrlWithLanguage
method in this code sample depends onKentico.WebPageUrlPaths
in the route values, and may not work outside the context of Xperience’s Content tree-based router.You can find an example of an overload that operates outside this context in the member registration widget guide.
Now, let’s propagate a new property to the view.
Navigate to the ~/Features/DataProtection/ViewComponents/TrackingConsent folder and add a newBaseUrlWithLanguage
property intoTrackingConsentViewModel
.C#TrackingConsentViewModel.csusing Microsoft.AspNetCore.Html; namespace TrainingGuides.Web.Features.DataProtection.ViewComponents.TrackingConsent; public class TrackingConsentViewModel { public bool CookieAccepted { get; set; } public bool IsAgreed { get; set; } public HtmlString CookieHeaderHtml { get; set; } = HtmlString.Empty; public HtmlString CookieMessageHtml { get; set; } = HtmlString.Empty; public string AcceptMessage { get; set; } = string.Empty; public string RedirectUrl { get; set; } = string.Empty; public string ConfigureMessage { get; set; } = string.Empty; public string ConsentMapping { get; set; } = string.Empty; public string BaseUrl { get; set; } = string.Empty; // new property to hold base URL plus language, if applicable public string BaseUrlWithLanguage { get; set; } = string.Empty; }
Keep the original
BaseUrl
property. The Tracking consent banner still needs this one to POST to thecookiebannersubmit
endpoint when the visitor clicks the submit button.Populate
BaseUrlWithLanguage
in the TrackingConsentViewComponent.C#TrackingConsentViewComponent.cs... public async Task<IViewComponentResult> InvokeAsync() { ... if (consents.Count() > 0) { ... var consentModel = new TrackingConsentViewModel { ... BaseUrl = httpRequestService.GetBaseUrl(), // Populate new property to hold base URL with language (if applicable). BaseUrlWithLanguage = httpRequestService.GetBaseUrlWithLanguage() }; // Display a view with tracking consent information and actions return View("~/Features/DataProtection/ViewComponents/TrackingConsent/TrackingConsent.cshtml", consentModel); } return Content(string.Empty); } ...
Finally, change the property used to populate the link in the TrackingConsent view from
Model.BaseUrl
toModel.BaseUrlWithLanguage
.cshtmlTrackingConsent.cshtml... <a href="@string.Format("{0}{1}", Model.BaseUrlWithLanguage, Model.RedirectUrl)" class="xpcookiebanner__cta" data-tracking-code="CookieBar_More"> @Model.ConfigureMessage </a> ...
Build and run your solution. When visitors click the Configure cookies link, they stay within the context of the current language.