Internationalization (i18n)
This step consists in proposing the website in several languages.
Final result
The result of this step is as follows:
The website is now available in English and French. You can change the language via a link in the navivation bar
Please note the text "item" on the Cart page which is more complex to handle (styling + plural)
- Vite App
- Nextjs App
Configuration
You specify the languages supported by the website via the configuration file src/settings.ts
:
export default {
i18n: {
locales: [
{
locale: 'en',
path: '/en',
},
{
locale: 'fr',
path: '/fr',
},
],
defaultLocale: 'en',
},
...
} as AppSettings;
Translations
The translations are stored in the folders public/locales/{locale}/{namespace}.json (in JSON format)
onekijs
supports splitting translations into multiple files that are called namespaces.
By convention, the default namespace is named common and is automatically loaded.
It's a good practice to put everything that is common to the whole application in this file (e.g: the text in the navigation bar).
For small applications, it's common to put all translations only in this file.
My-App
├─ public
│ ├─ locales
│ │ └─ en
│ │ │ └─ common.json // locales in english
│ │ └─ fr
│ │ │ └─ common.json // locales in french
Translation files
Let's create these translation files for our application
English
{
"Phone XL": "Phone XL",
"Phone Mini": "Phone Mini",
"Phone Invalid": "Phone Invalid",
"A large phone with one of the best screens": "A large phone with one of the best screens",
"A great phone with one of the best cameras": "A great phone with one of the best cameras",
"The product has been shared!": "The product has been shared!",
"You will be notified when the product goes on sale": "You will be notified when the product goes on sale",
"Product added succesfully to the cart!": "Product added succesfully to the cart!",
"Notify me": "Notify me",
"Share": "Share",
"Description": "Description",
"Product Details": "Product Details",
"Buy": "Buy",
"Loading": "Loading",
"Empty the cart": "Empty the cart",
"An error occured while cleaning the cart": "An error occured while cleaning the cart",
"There is no item in the shopping cart !": "There is no item in the shopping cart !",
"Buy another products": "Buy another products",
"Use any username / password (no check performed)": "Use any username / password (no check performed)",
"Username": "Username",
"Password": "Password",
"Submit": "Submit",
"Submitting": "Submitting",
"Error": "Error",
"Logging out": "Logging out",
"An error occured": "An error occured",
"stacktrace": "stacktrace",
"My Store": "My Store",
"Log out": "Log out",
"Checkout": "Checkout"
}
French
{
"Phone XL": "Téléphone XL",
"Phone Mini": "Téléphone Mini",
"Phone Invalid": "Téléphone Invalide",
"A large phone with one of the best screens": "Un grand téléphone avec l'un des meilleurs écran",
"A great phone with one of the best cameras": "Un bon téléphone avec l'un des meilleurs appareil photo",
"The product has been shared!": "Ce produit a été partagé!",
"You will be notified when the product goes on sale": "Vous serez notifié quand le produit sera disponible à la vente",
"Product added succesfully to the cart!": "Le produit a été ajouté au panier avec succès!",
"Notify me": "Me notifier",
"Share": "Partager",
"Description": "Description",
"Product Details": "Détails du produit",
"Buy": "Acheter",
"Loading": "Chargement",
"Empty the cart": "Vider le panier",
"An error occured while cleaning the cart": "Une erreur est survenue pendant le nettoyage du panier",
"There is no item in the shopping cart !": "Il n'y a aucun éléments dans le panier !",
"Buy another products": "Acheter d'autres produits",
"Use any username / password (no check performed)": "Utilisez n'importe quel identifiant / mot de passe (pas de check effectué)",
"Username": "Identifiant",
"Password": "Mot de passe",
"Submit": "Envoyer",
"Submitting": "Envoi",
"Error": "Erreur",
"Logging out": "Déconnexion",
"An error occured": "Une erreur est survenue",
"stacktrace": "stacktrace",
"My Store": "Mon Magasin",
"Log out": "Déconnexion",
"Checkout": "Panier"
}
Update of the application
useTranslation hook
To display a string (or even a component) in the right locale, onekijs
offers the useTranslation
hook. This hook returns
- a component
<T />
to translate something inside a JSX - a function
t()
to translate a typescript string
It often happens that the element to be translated is not a simple string.
Let's update the Cart page to use this hook and complexe string to be translated
- Vite App
- Nextjs App
loading...
loading...
useI18nService hook
onekijs
offers a service providing some i18n related helper methods like switching between locales.
The service can be instancied via the useI18nService
hook.
Also note that the useTranslation
hook returns the active locale as the third parameter.
Let's add a link to the <Navbar />
component to select the appropriate locale (English or French) and to hightlight the active one.
- Vite App
- Nextjs App
loading...
loading...
Now a user can display the application in english or in french
Advanced use cases
Complex string
It often happens that the element to be translated is not a simple string. To illustrate this, let's update the Cart component like this:
const Cart: FC<CartOptions> = ({ cart }) => {
const [T] = useTranslation();
return (
<div>
<h3>
<T>Cart</T>
</h3>
{cart.products.map((item, index) => (
<div key={`item-${index}`} className="cart-item">
<span>
<T>{item.name}</T>
</span>
<span>
<T>{currency(item.price)}</T>
</span>
</div>
))}
{cart.products.length === 0 && (
<h4>
<T>
There is <u>no item</u> in the shopping cart !
</T>
</h4>
)}
<p>
<Link href="/products">
<T>Buy another products</T>
</Link>
</p>
</div>
);
};
export default Cart;
There is no item in the shopping cart !
in locales/en/common.json is no more valid. To become valid, the key must be upated like this:{
...
"There is <1>no item</1> in the shopping cart !": "There is <1>no item</1> in the shopping cart !",
...
}
{
...
"There is <1>no item</1> in the shopping cart !": "Il n'y a <1>aucun éléments</1> dans le panier !",
...
}
Plural and variables
Plurals are essential when dealing with internationalization.
Let's update the Cart
component to add a plural and also a variable
const Cart: FC<CartOptions> = ({ cart }) => {
const nbItems = cart.products.length;
const [T] = useTranslation();
return (
<div>
<h3>
<T>Cart</T>
</h3>
{cart.products.map((item, index) => (
<div key={`item-${index}`} className="cart-item">
<span>
<T>{item.name}</T>
</span>
<span>
<T>{currency(item.price)}</T>
</span>
</div>
))}
{nbItems === 0 && (
<h4>
<T>
There is <u>no item</u> in the shopping cart !
</T>
</h4>
)}
{nbItems > 0 && (
<h4>
<T count={nbItems}>
There is <u>{{ nbItems }} item</u> in the shopping cart !
</T>
</h4>
)}
<p>
<Link href="/products">
<T>Buy another products</T>
</Link>
</p>
</div>
);
};
export default Cart;
Update the translation file to support plural:
{
...
"There is <1>{{nbItems}} item</1> in the shopping cart !": "There is <1>{{nbItems}} item</1> in the shopping cart !",
"plural::There is <1>{{nbItems}} item</1> in the shopping cart !": "There are <1>{{nbItems}} items</1> in the shopping cart !",
...
}
{
...
"There is <1>{{nbItems}} item</1> in the shopping cart !": "Il y a <1>{{nbItems}} élement</1> in the shopping cart !",
"plural::There is <1>{{nbItems}} item</1> in the shopping cart !": "Il y a <1>{{nbItems}} éléments</1> in the shopping cart !",
...
}
The following text is displayed if there is one element in the cart:
There is 1 item in the shopping cart !
The following text is displayed if there are two or more elements in the cart:
There are 2 items in the shopping cart !
Next step
In the next step, we present how to build powerful forms with onekijs
.