Application and content localization can be tricky, but fortunately, Kentico Xperience 13.0 makes it easy ๐ค!
Using Xperience's Content Tree-based routing, we let the platform generate our URLs, inserting the culture code as part of the path.
When our application handles a localized URL, assuming we use the CultureInfo.CurrentCulture
(or more likely IPageRetriever
) to retrieve our Page's content, we'll be sure display the localized content ๐ช๐ฟ!
In this post I want to talk about a different scenario...
How do we make sure that any XHR requests made to an API, running in our Xperience application, are also localized, working with and returning the correct content for the current page ๐ค?
๐ What Will We Learn?
- What is Localization?
- How does Xperience set the Page's request culture?
- How do our custom API endpoints find the request culture?
- What tools can we use to pass the Page's culture on to the API requests?
๐ What is Localization?
Good ol' Wikipedia says that localization "is the process of adapting a product's translation to a specific country or region."
When we "localize" a response to an HTTP request for a web page, we are making sure that the content returned with that response is correct for the country or region associated with that request. This is often simplified to be the language associated with the request.
For the rest of this post I'm going to use this simplified definition of localization and refer to the language, but there are plenty of other bits of content that need to be localized, like number and date formatting.
Also, the .NET docs have a great discussion on Globalization (building an app so it can support multiple languages) and Localization (adding support for a specific language(s)).
๐ How does Xperience Figure Out a Request's Culture?
Xperience, like any globalized platform/website, needs to decide on a culture by looking at the incoming HTTP request, because that culture determines the localization of the Page.
There's a lot of places this information could be stored, but the most common are cookies ๐ช or the URL itself.
Xperience's Content Tree-based routing let's developers pick from 2 options:
- Using the host:
https://site.es/products
- Spanish language top level domain - Using the path:
https://site.com/es-MX/products
- Mexican Spanish language path prefix
The scenario I'm interested in is the latter because it supports having 2 cultures on the same domain.
When looking for the culture in the URL path, Xperience uses route constraints and tokens to pull the culture value out. It has a specific URL pattern, using the path prefix (https://stuff/en-US/more-stuff
) which makes it easy to pinpoint the culture ๐ง.
In the Xperience ASP.NET Core integration, Xperience has some middleware ๐ค that looks at the incoming request context and figures out what culture to associate with it (based on the settings in the application).
Once it figures out the culture for the request URL, it then sets the culture for the thread/context:
CultureInfo.CurrentCulture = requestCulture.Culture;
CultureInfo.CurrentUICulture = requestCulture.UICulture;
๐งญ Finding the Request Culture in API Endpoints
When routing to an ASP.NET Controller or View associated with a Page in the content tree, Xperience does all the traffic management.
But our custom API endpoints aren't associated with anything in the content tree, which means Xperience is hands off ๐!
Let's assume we have a content tree route to a Product Page that is handled by Xperience and we are visiting the (Mexican) Spanish language version of that Page:
GET https://site.com/es-MX/product/apples
We rendered the product id for "apples" into the Razor View for this page, and we use that value to make an XHR request back to our API:
@model ProductViewModel
<h1>@Model.Name</h1> @* This will be "Manzanas" *@
<script>
const productId = @Model.ProductId;
fetch(`/api/products/${productId}`)
.then(resp => resp.json())
.then(resp => console.log(resp));
</script>
Here's the code that makes up our ProductsApiController
:
[ApiController]
[Route("api/[controller]")]
public class ProductsApiController : ControllerBase
{
[HttpGet("{productId:int}")]
public ActionResult GetProduct(int productId)
{
var culture = CultureInfo.CurrentCulture.Name;
// ...
}
}
Our original Razor View and HTML document were rendered using the Spanish language content and the URL this page is loaded from has the /es-MX
path prefix.
When this GetProduct
method is called from the XHR made by our JavaScript fetch()
request, what will the value of culture
be ๐ค๐?
...
It's a trick question ๐คฆ๐ฝโโ๏ธ! I haven't told you what the default culture is for our site.
Since there's no culture code in the XHR request's URL, Xperience will associate it with whatever the default culture is for the site that the domain is associated with ๐ฎ.
In my specific use-case, it's English. This means that the culture
variable will have the value en-US
.
How can we get that to be es-MX
so that any data we retrieve from the Xperience database and return from our API endpoint will be localized to the Page's culture ๐คท๐ผโโ๏ธ?
๐ค๐ป๐ค๐ค๐พ Passing the Culture
There are many ways to pass the culture information in a URL and they would all work - really all we need to do is get the culture code to be a parameter (probably query string) that our GetProduct()
method can directly access.
Then we can the CultureInfo
constructor and the code pattern that Xperience already uses to set the culture on the current thread ๐๐พ:
public ActionResult GetProduct(int productId, [FromQuery] string culture)
{
var cultureInfo = CultureInfo.GetCultureInfo(culture);
CultureInfo.CurrentCulture = cultureInfo;
CultureInfo.CurrentUICulture = cultureInfo;
// ...
}
The problem with this approach is we are now cluttering up our API action method with assigning the thread culture and we have this weird query string parameter we have to remember to include in all our other API requests and action methods.
We also don't have access to the correct culture until our Controller action method executes, which means Action filters and anything else that executes before the action are using our default culture ๐ฃ.
A better approach is to use the same feature that Xperience relies on for parsing out and assigning a culture to a Page's HTTP request.
We just need to make sure our API URL includes a special {culture}
token in the route definition:
[ApiController]
[Route("api/{culture}/[controller]")] // <- new route token
public class ProductsApiController : ControllerBase
{
[HttpGet("{productId:int}")]
public ActionResult GetProduct(int productId)
{
var culture = CultureInfo.CurrentCulture.Name;
// ^ culture will have the correct value now
}
}
And, then include that in the fetch()
request URL (we will source the culture value from CultureInfo.CurrentCulture.Name
which is correctly set when rendering our Page):
<script>
const productId = @Model.ProductId;
const culture = '@CultureInfo.CurrentCulture.Name';
fetch(`/api/${culture}/products/${productId}`)
.then(resp => resp.json())
.then(resp => console.log(resp));
</script>
I think this is a much nicer solution ๐๐ฝ!
Our API URLs match the same culture path prefix pattern that our Page URLs have and we don't have to worry about extra culture related values and logic in our API action methods ๐๐ผ.
๐ง Conclusion
Globalization and localization are important features when they're needed, but they tend to create new problems that need solutions, especially if we're used to not worrying about other languages (like me!).
Kentico Xperience supports localization as a first class feature and, for the most part, handles it all for us ๐.
However, when we start to come up with clever technical solutions for our application requirements, we need to be sure to carry all the existing features forward.
Fortunately, culture-aware XHR requests can be enabled in an elegant and simple way with Kentico Xperience 13.0 sites.
As always, thanks for reading ๐!
References
- .NET Documentation on Localization and Globalization
- Xperience 13.0 Documentation on Setting up Multilingual Websites
We've put together a list over on Kentico's GitHub account of developer resources. Go check it out!
If you are looking for additional Kentico content, checkout the Kentico or Xperience tags here on DEV.
Or my Kentico Xperience blog series, like:
Top comments (0)