7. Internationalization
7.1. Example 21: A Bilingual Application
A bilingual application has three types of text to translate:
- application text: labels and messages. These are stored in dictionaries, one per language;
- dates and amounts: the [Intl] object from [JavaScript] formats them in the desired language (see Example 09);
- text from [Ionic] itself: the months and days of its calendar, its buttons. The calendar ([<ion-datetime>]) receives the language via its [locale] attribute.
7.1.1. The dictionaries
Each text has a key ("HOME.HELLO"). The parameters are written as [{{name}}]; a plural text has multiple keys: [_zero], [_one], [_other]. These are the conventions of the [JavaScript] [i18next] library, used by the [React] and [Vue.js] clients of [RdvMedecins]: The case study will use their dictionaries, supplemented with a few additional keys. The English dictionary has the same keys:
The files in the project’s [public/] folder are delivered as-is with the application (in a browser: at the address /i18n/ex21-fr.json; on [Android]: within the application itself). Compared to the dictionaries in the [Flutter] example, two keys have been added: [CALENDAR_OK] and [CALENDAR_CANCEL], which correspond to the calendar buttons (discussed below).
7.1.2. Translations
- line 25: the application’s languages;
- line 28: the type of a dictionary—an object where each property is either text or another dictionary (the type is recursive: it references itself);
- lines 30–33: the translations store the current language and its dictionary in variables. A simple class, not a service [@Injectable]: each user creates it with their own prefix (line 36; Example 25 will have its dictionaries, [ex25-fr.json]...). It will be provided to the page by a “factory” (discussed later);
- line 36: [private readonly prefix] in the constructor’s parameters declares AND initializes a field: a shortcut to [TypeScript];
- lines 40–44: the source language: the one stored on the device (the [@capacitor/preferences] plugin from Example 20), otherwise the phone’s language ([navigator.language]: “en-US,” of which the first two letters are retained), if known; otherwise, English;
- lines 46–54: change language: read the dictionary using [fetch], the function of [JavaScript] that reads an address (in this case, a file included with the application) and returns a [Promise] representing the response; decode its JSON content ([response.json()], another [Promise]); store the dictionary and language in their respective fields; update the page’s language (the [lang] attribute of the [<html>] tag, which is useful for screen readers); save the selection on the device. We could have used the HTTP client from [Angular] (Example 22): [fetch] avoids depending on it in a simple class;
- lines 57–63: search for a key “HOME.HELLO” in the dictionary: we traverse down, part by part ([split('.')]), through the nested JSON objects. [typeof node === 'object'] is a type check that “narrows” the variable’s type in the branch where it is true: [TypeScript] then knows that [node] is a dictionary. Reading the signal ([this._dictionary()], line 58) is essential: a template that calls [t()] reads the signal in this way and will be updated when the dictionary changes;
- Lines 65–79: the translation. For a plural (a numeric parameter [count]), [Intl.PluralRules] returns the plural form according to the rules of the language: [select(n)] results in [one] or [other] (other languages also have [two], [few], [many]). In French, 0 and 1 are singular ([one]); in English, only 1. The key [_zero], if it exists, takes precedence over 0 (line 72), just as with [i18next]. An unknown key is displayed as-is (line 76): you can immediately see what’s missing. Finally (line 78), each [{{name}}] is replaced with the value of the parameter: a regular expression, [/\{\{(\w+)\}\}/g], and a function that receives each match ([all]) and the name captured in parentheses ([name]).
Why not use the official [Angular] tool? [Angular] has its own internationalization system ([@angular/localize]): we mark up the template texts ([<p i18n>Hello</p>]), extract the translations into files, and the compilation produces ONE application for each language. This is the best solution for a website where all the text is known in advance. However, it does not switch languages “on the fly,” and our server returns keys (“ERRORS.LOGIN_TAKEN”) that must be translated at runtime: a dictionary, which is queried by the key, is much better suited for this purpose and allows us to share dictionaries with other clients. Libraries also do this for [Angular] ([ngx-translate], [Transloco]): our service is a minimal version of this.
7.1.3. The translated page
- lines 25–28 (.ts): what the server would return for [RdvMedecins]: a HTTP code, a key, and its parameters;
- line 38 (.ts): a provider SPECIFIC to the page: [providers] in the decorator [@Component] creates an instance of the translations for this page (and its components), which is destroyed along with it. [useFactory] provides the function that creates it, along with its prefix;
- lines 40–42 (.ts): the size of the language selection and that of the calendar window (a dialog box sized to fit its content, rather than a full page);
- line 47 (.ts): the translations, obtained as a service;
- lines 54–59 (.ts): the date and price, formatted in the current language by [Intl.DateTimeFormat] ([dateStyle: 'full']: “Tuesday, October 6, 2026,” “mardi 6 octobre 2026”) and [Intl.NumberFormat] ([style: 'currency']: “€1,234.50”, “1 234,50 €”). These are calculated signals: they detect the language and are recalculated when it changes. (The pipes [date] and [currency] from [Angular] also accept a language as a parameter, but in that case you must store the rules for each language, as [main.ts] does for French.)
- line 61 (.ts): the calendar language, in the language-country format expected by [<ion-datetime>];
- line 65 (.ts): the source language;
- lines 68–69 (.ts): the document title (the browser tab), translated: the [Title] service of [Angular], called by an effect, which is replayed when the language changes;
- lines 74–76 (.ts): language selection;
- line 10 (html): the page title, translated;
- lines 12–16 (html): the language selection in the top bar: a [<ion-segment>], consisting of buttons, only one of which is selected ([value]), the counterpart to the [SegmentedButton] of [Flutter];
- lines 23–25 (HTML): parameterized text. The date and price, already formatted, are passed as parameters;
- line 27 (html): [count] selects the plural form and replaces [{{count}}];
- Lines 34–47 (html): the calendar, in a modal window ([<ion-modal>]) opened by the button whose [id] is specified by its [trigger] attribute. Its content, in a [<ng-template>], is created only when the window is opened. [<ion-datetime>] formats the months and days using [Intl], in the language specified by its [[locale]] attribute; [[firstDayOfWeek]]: the week begins on Monday in French and on Sunday in English. Its buttons (“Done,” “Cancel” by default) are not translated by [locale]: [[doneText]] and [[cancelText]] receive the texts from our dictionaries. This was the [flutter_localizations] package along with [Flutter];
- lines 58–61 (html): what the [RdvMedecins] server would return. The server's key IS a dictionary key; its parameters are those of the text.
7.1.4. The output
In English (left); in French, after an addition (middle); the calendar, in French (right):
![]() | ![]() | ![]() |
Changing the language re-translates the entire screen without restarting or making any requests to the server: the language and dictionary are signals, and everything that reads them (template text, date, price, title) follows suit. The selected language is stored on the device: relaunch the app (in the browser, reload the page), and it will open in the last selected language.


