Soluling home   Document home

Sports, a multilingual application that uses multilingual data

Sports, a multilingual application that uses multilingual data

In this sample, we are going to write a Sports application. It shows a list of sports. The application is a multilingual web application that consumes multilingual data. It means that the data in the database and the user interface of the client application have been localized. In addition, the API is locale-aware and the API also uses machine translation to translate the DB content to the language required by the user. The application consists of a database, an API, and several client applications (using different technologies). The application is cross-platform using the following technologies:

This sample covers most localization concepts you might need when implementing your applications. We recommend you to view the source code and to learn how they have been written. The source code contains useful comments. Soluling supports all parts of the applications: datbase, API, and clients.

DatabaseDatabase

Source code: <data-dir>\Samples\ASP.NET\Core\WebAPI\SportAPI

For each sport, there are some properties such as name, origin, shorts description, amount of players, and so on. Properties such as the name of the sport are language-specific. For example, ice hockey is ice hockey in English but jääkiekko in Finnish. This is why we are going to need a data structure where some properties can be repeated in different languages. We need a database table to store language-independent properties and another table to store language-specific properties. The language-specific table has a one-to-many relationship to the language-specific table. Because we use Entity Framework Code First, we don't need to specify the tables. Instead, we define data models. The language-independent sport model (i.e., object) is

public class Sport
{
  [Display(Name = "Id")]
  public int Id { get; set; }
  
  [Required]
  [Display(Name = "Olympic")]
  public Olympic Olympic { get; set; }
  
  [Display(Name = "Field players")]
  public int? FieldPlayers { get; set; }

  [Display(Name = "Goalie")]
  public bool? Goalie { get; set; }
  
  public List<SportLanguage> Languages { get; set; }
}

The class contains the primary key, status in the Olympic Games, the number of players, and a flag that tell if a goalkeeper is used. In addition, it contains a list of language-specific properties. One for each language. The language-specific sport model is

public class SportLanguage
{
  [Display(Name = "Id")]
  public int Id { get; set; }
  
  [Required]
  [Display(Name = "Language")]
  public string Language { get; set; }
  
  [Required]
  [Display(Name = "Name")]
  public string Name { get; set; }
  
  [Required]
  [Display(Name = "Origin")]
  public string Origin { get; set; }
  
  [Required]
  [Display(Name = "Description")]
  public string Description { get; set; }

  [Display(Name = "Machine translated")]
  public bool MachineTranslated { get; set; } = false;

  public int SportId { get; set; }
}

In addition to the primary key, the object contains language property. It contains the IETF language tag (Wikipedia) that specifies the language that is used in other properties. Finally, SportId is a property that contains a foreign key to the language-independent part of the sport. Once we run the database code for the first time, Entity Framework creates the SQL Server database for us and even creates the tables we need.

Database

We use a seed code to populate the initial database. Seeded sports are soccer, ice hockey, basketball, and alpine skiing. The language-independent table looks like this

Table

The language specific table looks like this after seeding it with English and Finnish.

Rows

We can easily add more language without needing to change the schema of the database or the model.

Learn more about database localization.

APIAPI

Source code: <data-dir>\Samples\ASP.NET\Core\WebAPI\SportAPI

The next step is to implement the API for our database. Our API is a CRUD API. However, we want to make the API itself language aware. This means that we need to implement a way that we can tell API that we want the sports data in a specific language. A convenient way to do this is to use HTTP's Accept-Language header property. This is the property were HTTP clients (e.g., browser) indicate the language they want to browse the resource. ASP.NET Core has built-in support to read the attribute. We just need to enable it in Startup.cs.

app.UseRequestLocalization(options);

This gives us three different options to pass the language id. The first is to use the HTTP header. The second is to pass the language as a URL parameter. For example, if we need to get data in Finnish, we just add a culture parameter with Finnish language code: http://localhost:53783/sports?culture=fi. The third option is to use cookies. Let's see the language id passing in action. If we get the first sport using the API, we will see this

API item

As you can see, the API returns the sport object that contains all the language specific-parts the object has. Because our database contains soccer in English and Finnish, we will get an array that contains first English and then Finnish. This is the order where they exist in the database. If we pass the language parameter, the order might change. Passing English won't change the order because English was already the first one. However, passing the Finnish language code will change the order.

API items

Only the order of the language-specific parts has changed. The id of the English part is still 1, and Finnish part 2. The reason we want to put the desired language first is that it gets easier to consume the API. The client passes the language code and will get a response where the data in that language is first. If the sport database does not yet have data in that language, then the API uses Microsoft Translator API to machine translate the English (or some other if English does not exists) to the target language and saves the new language part to the databases. View the source code to see how the machine translation API works. The following code moves the selected language part to the top of the list.

sport.MoveDefaultLanguateToTop(Language);

Adding a new sport is a bit more complex than in a single language database. If we add a new sport, then we need to POST a sport object with at least one language-specific part to http://localhost:53783/sports. Using Curl, the command is this.

curl http://localhost:53783/sports -X POST -d "@Waterpolo.json" --header "Content-Type: application/json"

Content of Waterpolo.json file is

{
  "olympic": "Summer",
  "fieldPlayers": 6,
  "goalie": true,
  "languages":
  [
    {
      "language": "en",
      "name": "Water polo",
      "origin": "England",
      "description": "Water polo is a competitive team sport played in the water between two team"
    }
  ]
}

If the sport already exists and we want to add a new language-specific part, then we will POST a sport language.

curl http://localhost:53783/sports/5 -X POST -d "@Waterpolo_fi.json" --header "Content-Type: application/json"

Content of Waterpolo_fi.json file is

{
  "language": "fi",
  "name": "Vesipallo",
  "origin": "Englanti",
  "description": "Vesipallo on vesialtaassa pelattava pallopeli"
}

The above samples use, Curl but in the real world, you would use the HTTP class of your programming language. For testing purposes, Curl (Wikipedia) or Postman (homepage) are excellent tools. We wrote our first client application in Angular, so we used TypeScript and HttpClient object.

API also implements a help endpoint, Controllers\HelpController.cs. Its purpose is to provide instructions about other endpoints. This is why it returns HTML instead of JSON. The returned HTML contains text that has been localized. The localized resource files locate at Resources\Controllers.HelpController.*.resx. The help controller uses IStringLocalizer to access localized strings.

AngularAngular application

Source code: <data-dir>\Samples\Angular\Sport

Our first client is a typical Angular application. It uses Material, and consumes our sports API. The application follows a typical Angular design patterns. However, because the application has been localized and it uses localized data, we need to pay extra attention when getting, inserting, and updating data.

The client applications uses following additional libraries:

Library Description How to install
Material Material Design user interface components for Angular. Follow instatuction in Getting Started.

The first issue we need to solve is how we are going to pass the locale information. Before we can pass the locale id we need to get it. Fortunately this is pretty simple in Angular. We can get LOCALE_ID injectable token that contains the locale id of the current language. We inject it to our service that is used to access the API.

constructor(private http: HttpClient, @Inject(LOCALE_ID) private locale: string)
{
}

Once we have the id we can pass it to each HTTP calls.

get getOptions(): any
{
  return { headers: new HttpHeaders({'Accept-Language': this.locale})};
}

This property returns the header we can pass with GET calls.

return this.http.get<any[]>(URL, this.getOptions)...

We only need to pass the Accept-Language to GET calls.

API payloads contain JSON. HttpClient automatically converts them to generic JSON object. They are convenient because conversion is automatic. However they are without any type information or custom methods. It is better to use strongly typed TypeScript objects instead of JSON objects. Unfortunately conversion from JSON payload to TypeScript objects is not automatic but we need to do the conversion manually. For that we added a static deserialize method in the Sport object.

Once installed we implement a method into SportsService.

private jsonToSport(json: any): Sport
{
  return Sport.deserialize(json);
}

The method converts JSON object to Sport.

getSports(): Observable<Sport[]>
{
  return this.http.get<any[]>(URL, this.getOptions)
    .map((sports) =>
    {
      let result: Sport[] = [];
      for (let index in sports) 
        result.push(this.jsonToSport(sports[index]));
      return result;
    });
}

The sport parameter in map function is any[] so it is untyped JavaScript object array. We create a typed Sport[] array and take each untyped sport, convert it typed and add them to the array.

The application has been localized to several languages, and the application selects the active language based on the Accept-Language HTTP header sent by the browser.

ReactReact application

Source code: <data-dir>\Samples\React\sport

Our second client is written in React and consumes our sport API. The application follows a typical React design patterns.

The client applications uses following additional libraries:

Library Description How to install
Bootstrap React Bootstrap Follow instatuction in Getting Started.
React Intl React Intl Follow instatuction in GitHub page.

The application has been localized to several languages, and the application selects the active language based on the Accept-Language HTTP header sent by the browser.

VueVue client application

Source code: <data-dir>\Samples\Vue\sport

Our third client is written in Vue and consumes our sport API. The application follows a typical Vue design patterns.

The client applications uses following additional libraries:

Library Description How to install
Bootstrap Bootstrap Vue Follow instatuction in Getting Started.
Vue I18n Vue I18n Follow instatuction in Getting Started.

The application has been localized to several languages, and the application selects the active language based on the Accept-Language HTTP header sent by the browser.

Shared .NET code

We use .NET in API and several clients such as Blazor, ASP.NET, desktop and Xamarin. Because they all use .NET and we use C# as a programming language we can share great deal of code. For the shared code we have SportLibrary that is a .NET Standard library. It contains our sport models Sport and SportLanguage in Sport.cs and SportService service to access the API in SportService.cs. This makes each .NET based client pretty thin containing only the UI to show and edit sports.

BlazorBlazor WebAssembly client application

Source code: <data-dir>\Samples\Blazor\SportWasm

The application has been localized to several languages, and the application selects the active language based on the Accept-Language HTTP header sent by the browser.

BlazorBlazor Server client application

Source code: <data-dir>\Samples\Blazor\SportServer

The application has been localized to several languages, and the application selects the active language based on the Accept-Language HTTP header sent by the browser.

ASP.NETASP.NET Razor Pages client application

Source code: <data-dir>\Samples\ASP.NET\Core\RazorPages\Sport

The application has been localized to several languages, and the application selects the active language based on the Accept-Language HTTP header sent by the browser.

ASP.NETASP.NET MVC client application

Source code: <data-dir>\Samples\ASP.NET\Core\MVC\Sport

The application has been localized to several languages, and the application selects the active language based on the Accept-Language HTTP header sent by the browser.

Summary

The are dozens of technology stacks you can use to build something like this. You can use Node.js, Go, Java, Python, etc. to write the API. Instead of SQL Server, you can use pretty much any database. You can access the database directly using SQL, or you can use any other ORM, such as Dapper. We already implemented several clients and planning to write even more like Xamarin, iOS, Android, React Native, Svelte, and FireMonkey. The first client we wrote was Angular. There is a reason for that. Angular is the only major client-side JavaScript library/framework that has built-in support for localization, does not use verbose syntax to mark strings to be localized, and has a string extraction tool.