Catalin Ciubotaru avatar

User preferred language with Angular Universal

The user speaks a different language.

The Problem

Now that you are an expert in Angular Universal (assuming you read this), you wanna make things better. You want to make sure that the user gets the app (or website, however you wanna call your brain child) in their own preferred language, if possible.

For example: you support English 🇺🇸 and French 🇫🇷, but you default to English and allow the user to change it if needed. While this works, it would be even nicer to figure out what the user wants, and give them that from the get-go if supported.

Disclaimer

Now, your Product Owner does not know about this(let’s call him Robin for example). So, you don’t wanna bother Robin with these details, and you’re not even sure he would prioritise this over the pile of other stuff that needs to be built, yesterday. Long story short: you wanna build this, and you want to build it fast, under the radar.

The Solution

The solution has 3 parts:

  • The Contract for the service that will figure out the language
  • The service that figures out the language on the Browser
  • The service that figures out the language on the Server

Part 1

The contract needs to be an abstract class because Angular Dependency Injection does not work with interfaces. This is pretty straight forward:

import { LanguageShortName } from '../models/language-short-name';

export abstract class LanguageService {
readonly SUPPORTED_LANGUAGES: LanguageShortName[] = ['nl', 'en', 'sv', 'de'];

public abstract getLanguage(defaultLanguage: LanguageShortName): LanguageShortName;
}

Now, this is the Abstract class. It also has a list of supported languages and it needs implementations for the getLanguage method.

Part 2

The Browser implementation. Here, we need to ask the browser, what is the user preferred language, and see if we support that. If not, use some default value. This would look something like this:

import { LanguageService, LanguageShortName } from 'somewhere';

@Injectable()
export class WebLanguageService extends LanguageService {
constructor() {
super();
}

getLanguage(defaultLanguage: LanguageShortName): LanguageShortName {
const deviceLanguage = window?.navigator.language?.substring(0,2) as LanguageShortName;

return this.SUPPORTED_LANGUAGES.includes(deviceLanguage) ? deviceLanguage : defaultLanguage;
}
}

This one uses the window object to ask what is the browser language. We only use the first 2 characters. Next, we check if this language is supported, if not, we return the default language.

Small note: the default language is an input for this method. This way, the place that needs it, has full control.

Part 3

The Server implementation. This is where things get tricky, or so I thought. Since on the server we don’t really have a browser, we can’t get the user preferred language from there. What can we do though?

After a bit of (soul)searching I found that the browser adds a header named accept-language to every request it makes. This header has as value a comma separated list of all the user preferred languages, in the preferred order. VERY USEFUL! Armed with that knowledge, we can implement the Server version of the language service. It looks something like this:

import { Inject, Injectable } from '@angular/core';
import { LanguageService, LanguageShortName } from 'somewhere';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { Request } from 'express';

@Injectable()
export class ServerLanguageService extends LanguageService {
constructor(@Inject(REQUEST) private request: Request) {
super();
}

getLanguage(defaultLanguage: LanguageShortName): LanguageShortName {
const preferredLanguage = this.request.headers['accept-language']
?.split(',')?.[0]
?.substring(0, 2) as LanguageShortName;

return this.SUPPORTED_LANGUAGES.includes(preferredLanguage)
? preferredLanguage
: defaultLanguage;
}
}

As you can see, here we inject the Request, get the accept-language header, split it, get first value badabing, badabum and Bob’s your uncle(who’s Bob?). You have the user preferred language on the server.

Part 4

Yes, there is a part 4. Tying everything together. The only thing we need to do now is make sure the correct service is provided on the correct platform. Something like this:

@NgModule({
imports: [
...
],
declarations: [
...
],
// Providers that are different between the Server and the Browser
providers: [
...
{ provide: LanguageService, useClass: WebLanguageService },
],
bootstrap: [AppComponent]
})
export class AppModule {}
@NgModule({
imports: [
// The AppServerModule should import your AppModule followed
// by the ServerModule from @angular/platform-server.
AppModule,
ServerModule,
ServerTransferStateModule,
NoopAnimationsModule
],
providers: [
...
{ provide: LanguageService, useClass: ServerLanguageService }
],
// Since the bootstrapped component is not inherited from your
// imported AppModule, it needs to be repeated here.
bootstrap: [AppComponent]
})
export class AppServerModule {}

Of course, this is the stripped down version of the AppModule and the AppServerModule, but you get the idea…(if not, let me know and I’ll try to explain how this works in more detail).

Small notes

  • You can of course make the service not need a default language, and use the first item in the SUPPORTED_LANGUAGES array.
  • Everything that’s common between the implementation should live in the Abstract Language Service. Great place for that!
  • When you need the language service, don’t inject the WebLanguageService, or the ServerLanguageService. Always inject the LanguageService because Angular will figure out which implementation to use. Something like this:
@Injectable({
providedIn: 'root'
})
export class CarsService {
const userLanguage: LanguageShortName;

constructor(
private languageService: LanguageService,
) {
this.userLanguage = this.languageService.getLanguage('en');
}
}

Want more?

Let me know if this is not clear, you need more examples, more use cases etc.

Also feel free to flag what’s wrong with my approach, that’s how we get better at this.

Be kind to each other! 🧡

Over and out

My Twitter avatar
Catalin Ciubotaru 🚀

Fresh out of the oven! #Angular #AngularUniversal

Dec 13, 2021
11 people are talking about this

Wanna read more?

Updates delivered to your inbox!

A periodic update about my life, recent blog posts, how-tos, and discoveries.

No spam - unsubscribe at any time!