Language detection strategy in server-side rendered JavaScript
Building a web app that requires handling different languages is a good opportunity to sharpen your skills in software architecture, especially while using server-side rendered JavaScript. This article will show you briefly (with simplified code snippets) how we at Onefootball serve our users relevant content in their own language.
Here’s an illustration of the request’s journey and every main operation involved in our language detection strategy.
Node server
As you can see, the first part is pretty straightforward: the user makes a request to “onefootball.com/\*”, and it is received in our node server, which is using express.
Note: “\*” is used to denote a wildcard path.
Middlewares
If this request is asking for a specific view instead of a static asset, it will go through the following middlewares related to our language strategy: initDataObject, assignLanguage and translationsRedirect.
1. initDataObject
This middleware initializes an object in the request, which is passed and read by the next middlewares.
export function initOFData(req: OFReq, _: Response, next: NextFunction) { req.OFData = req.OFData || {}; next();}
2. assignLanguage
Here is where all the magic happens. This middleware is in charge of extracting and assigning the UI language from the request.
First, we try to get our locale from the path found in the request. This function checks if the string after the first “/” is a valid and supported locale. In this case, the user is requesting “onefootball.com/es” or “onefootball.com/en/\*”. If this operation returns `undefined` and the path is pointing somewhere other than “onefootball.com/”, we immediately return 404. We do this because all of our supported URLs must contain the locale just after the root domain.
Then we set the property `UILang` in our OFData object (the object was generated in our previous middleware) to the locale found. However, as this could be `undefined` because the user could going to “onefootball.com/”, we need to find the locale using another strategy.
The most exciting logic happens at this point. First, we check if the request contains a cookie set by us called UILang. If the cookie isn’t present, we try to match the `accept-language` headers with our supported languages. While parsing these headers we check them against our supported languages and regions. We assign `UILang` property the most convenient language that we support. For example, if a user has set the preferred languages in their browser’s language settings like this: `1. Turkish (preferred) 2. Brazilian Portuguese 3. Korean`. We will assign Brazilian Portuguese. Finally, if our system doesn’t support any of the user’s preferred languages we will assign our default language, which is English.
Here is how this middleware is coded:
export function assignUILang(
req: OFReq, res: express.Response, next: express.NextFunction) { const localeFromPath = getLocaleFromPath(req.path);
if (req.path !== ‘/’ && localeFromPath === undefined) { send404(res); // unsupported language in URL return; } if (req.OFData) { req.OFData.UILang = localeFromPath || getLocale(req); } next();}
function getLocale(req: express.Request): Locale { if (isSupportedLocale(req.cookies.UILang)) { return req.cookies.UILang; } if (req.headers[‘accept-language’]) { // parser comes from ‘accept-language-parser’ library for (const language of parser.parse(req.headers[‘accept-language’])) { const supportedLanguage = parseLanguage(language); if (supportedLanguage) { return supportedLanguage.locale; } } } return DEFAULT_LOCALE;}
function parseLanguage({ code, region}: parser.Language): SupportedLanguage | undefined { if (!isSupportedLanguageCode(code)) { return undefined; } const languageConfig = environment.languages.config[code]; return ( (region && languageConfig.regions[region.toLowerCase()]) || languageConfig.default );}
function isSupportedLanguageCode(code: string): code is SupportedLanguageCode { …}
3. translationsRedirect
So far the content has not been sent to the user. The reason is that we need to redirect the user to a valid URL containing a supported language in its path if “onefootball.com/” was requested. This middleware is as easy as the following:
if (requestedLang !== undefined && req.path === ‘/’) {
// temporary redirect res.redirect(302, `${req.path}${requestedLang}`); res.end(); return;}
Javascript server-side engine and Setting “lang” attribute in <html> tag
After performing our language detection strategy we generate the HTML response that we are sending to our user using the JavaScript engine, Angular Universal in our case. Just before we send the response, we assign the lang attribute to the <html> tag `lang=”${requestedLang}”`. SEO rocks!!!
Browser
Once the user has got the HTML response the usual happens. We serve the JavaScript files and once JavaScript is loaded the web application is fully interactive. Finally, as soon as the client-side app is loaded, the first thing we do is set the UILang cookie in our user’s browser. This is done so that we avoid performing the entire language detection strategy if the user requests “onefootball.com/” again.
P.S. It is worth noting that on every language change we perform a request to our server. This simplifies our lives as we don’t need to worry about updating the state of every operation that relies on the current locale and we can optimise locale-specific data served to the client.
Conclusion
Having a proper language detection strategy with server-side rendering is vital to every multilingual application. Once this is set up properly you can forget about this code (assuming every corner case has been considered). It is very important to state that even though this technical implementation is fairly easy to put together, the most important part of the strategy is the planning job done beforehand (Thanks Jim). Having defined with your project manager all the supported languages, locales and regions is crucial for having a reliable and extendable local detection strategy.
Acknowledgements
I would like to thank my entire Consumer Web team at Onefootball for this amazing experience. Big shout to our engineers Julia Rozenthal, Eugene Stativka, Valentyn Kremeshnyi and Andrea Dessì.