15 mars 2023
Internationaliser avec Next
8 minutes de lecture
L'internationalisation des applications est devenue incontournable, utiliser correctement les outils de traduction l'est aussi. Petite mise au point des méthodes existantes, leurs avantages, leurs inconvénients et leurs implémentations avec l'outil i18next.
Next traduction intégré
La traduction d'une application se fait à travers plusieurs fichiers contenant des clés de traduction similaires. Ces clés sont utilisées pour récupérer la version traduite d'un texte, ainsi le fait de changer de modifier la traduction d'une clé n'implique pas de changement dans les champs de texte qui utilisent cette clé.
Depuis sa v10, Next possède un système de traduction intégré i18n. On peut donc configurer i18n dans le fichier de configuration next.config.js
et ainsi avoir accès à ces informations dans chaque utilisation de getServerSideProps
et getStaticProps
// next.config.js
/** @type {import("next").NextConfig} */
const nextConfig = {
reactStrictMode: true,
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr'],
},
}
module.exports = nextConfig
Cette solution permet de récupérer la locale définie au sein des fonctions getStaticProps
et getServerSideProps
, de la transmettre à travers les props afin que la page puisse récupérer le fichier de traduction correspondant.
Si cette méthode ne nécessite aucune librairie, elle reste très limitée en termes de possibilités et oblige à importer à la main les fichiers de traduction dans chaque page.
i18next et plus si affinité
Abordons maintenant un outil qui va venir compléter la partie i18n intégrée à Next : la librairie i18next
, enrobée par react-i18next
, elle-même enrobée par next-i18next
.
-
i18next : permet de récupérer les traductions depuis un fichier de traduction, une API ou une base de données. C'est notre noyau.
-
react-i18next : permet d'implémenter i18next dans React, notamment en proposant des hooks et des composants.
-
next-i18next : permet d'utiliser les fonctionnalités de i18next et react-i18next dans Next, en proposant des méthodes adaptées pour le SSR et le SSG.
Avec ce triptyque nous avons toutes les cartes en main pour disposer à notre guise de nos fichiers de traduction.
Chacun apporte une couche de possibilités adaptée au framework utilisé. Derrière tout ça se cache en réalité une instance i18next, décorée par une variété d'options, utilisée dans un Provider top-level (_app.tsx dans le cas de Next) et accessible par des hooks.
Installation
Pour préparer l'installation, je vous invite à aller jeter un coup d'œil au README next-i18next, mais revenez vite j'en ai pas fini avec vous.
Sous le capot
À ce stade, nous avons une application avec une traduction fonctionnelle, certes mais des points restent à éclaircir, passons au crible le fonctionnement actuel avant de préparer nos alternatives.
Locales et namespaces
Le principe de traduction repose sur une combinaison de différents langages et différents namespaces, le langage évolue en fonction de sa valeur par défaut, du navigateur, ou de la route (/fr
donnera une page en français par exemple). Pour le namespace, il est par défaut à common
, mais il peut être intéressant d'avoir un namespace par page voire même par composant (header et footer par exemple) pour gagner en clarté dans les fichiers de traduction.
Attention toutefois, certaines APIs de traduction n'intègrent pas ce principe de namespace.
Pour utiliser un namespace différent de celui par défaut, il faut l'ajouter dans la méthode serverSideTranslations(locale!, ["about", "common"])
et dans le hook useTranslation(["about", "common"])
.
Les backends i18next
Les backends définissent la manière de récupérer les fichiers de traduction, dans i18next ils ont la forme de plugin: ici on parlera des backends i18next-fs-backend
et i18next-http-backend
.
i18next-fs-backend
Sous le capot next-i18next
utilise i18next-fs-backend
par défaut, c'est-à-dire qu'il récupère les fichiers de traduction depuis le système de fichiers. Ce comportement par défaut empêche la récupération des traductions côté client.
Autrement dit les traductions sont chargées au build time et ne peuvent être modifiées. Pour importer des traductions distantes il faudra donc exécuter un script en prebuild pour modifier les fichiers locaux de traduction.
Voici un exemple de script permettant d'écrire un fichier de traduction depuis une API :
// script/import-translations.js
require('dotenv').config()
const { writeFile } = require('fs')
const importTranslations = (locales) =>
fetch(`${process.env.API_URL}/api/export/locale/${locales}.json&key=${process.env.API_KEY}`)
.then((res) => res.json())
.then((res) => {
if (!res.error) {
writeFile(`public/locales/${locales}/common.json`, JSON.stringify(res), (error) => {
if (error) {
throw error
}
})
}
})
importTranslations('fr')
importTranslations('en')
Dans certains cas, il sera nécessaire d'exporter de nouvelles clés à l'API avant de récupérer les traductions, pour ce faire voici un script d'export permettant d'exporter les nouvelles clés :
// script/export-translations.js
const { readFile } = require('fs')
const exportTranslations = (locale) => {
readFile(`./public/locales/${locale}/common.json`, (err, data) => {
if (err) {
throw err
} else {
fetch(`${process.env.API_URL}/api/import/json?key=${process.env.API_KEY}&locale=${locale}`, {
method: 'POST',
body: data.toString(),
})
}
})
}
Vous l'aurez compris cette méthode par défaut ... a des défauts. Notamment le fait que chaque modification d'un fichier de traduction nécessite un nouveau build de l'application pour être prise en compte.
i18next-http-backend
Des alternatives existent, avec le plugin i18next-http-backend
. Ce backend utilise le protocole http pour récupérer les fichiers de traductions, facilitant ainsi nos appels à une API de traduction ou encore la modification de nos fichiers de traductions à chaud, avec la possibilité de récupérer les traductions côté client.
yarn add i18next-http-backend
Une fois le plugin installé, il faut modifier la configuration dans le fichier next-i18next.config.js
:
// next-i18next.config.js
/** @type {import('next-i18next').UserConfig} */
const HttpBackend = require('i18next-http-backend/cjs')
const isBrowser = typeof window !== 'undefined'
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr'],
},
backend: isBrowser
? {
loadPath: '/locales/{{lng}}/{{ns}}.json',
// Dans le cas d'une API de traduction
/*crossDomain: true,
requestOptions: {
mode: "no-cors",
},*/
}
: {},
debug: true,
serializeConfig: false,
use: isBrowser ? [HttpBackend] : [],
}
Les vérifications isBrowser
nous permettent de garder le comportement précédent pendant le build time, mais d'avoir un comportement différent lors de l'exécution côté client.
Dans le fichier _app
faites la modification suivante :
// _app.tsx
import { appWithTranslation } from 'next-i18next'
import i18n from '@/next-i18next.config'
import type { AppProps } from 'next/app'
const App = ({ Component, pageProps }: AppProps) => {
return <Component {...pageProps} />
}
export default appWithTranslation(App, i18n)
L'ajout de la configuration dans la fonction appWithTranslation
est obligatoire dès lors que l'on sort du comportement par défaut de next-i18next, c'est à dire l'utilisation d'un backend différent de i18next-fs-backend
.
Pour récupérer les traductions côté client il faut modifier le getStaticProps dans les pages concernées :
// pages/index.tsx
export default function Main() {
const { t, i18n } = useTranslation(['common'], {
bindI18n: 'languageChanged loaded',
})
useEffect(() => {
i18n.reloadResources(i18n.resolvedLanguage, ['common'])
}, [i18n])
return <span>{t('content.title')}</span>
}
export const getStaticProps: GetStaticProps = async ({ locale }) => ({
props: {
...(await serverSideTranslations(locale!, ['common'])),
},
})
On peut même, dans le cas de la traduction côté client, enlever la fonction getStaticProps en observant l'objet ready
présent dans useTranslation()
à la place, cela permet de gérer le chargement des traductions directement dans le composant :
// pages/index.tsx
export default function Main() {
const { t, ready } = useTranslation()
return <span>{ready && t('content.title')}</span>
}
Génération des clés
Localement
Évitez de générer les clés de traduction à la main dans les fichiers JSON. Utilisez i18next-parser
pour le faire automatiquement dans tous les langages supportés.
yarn add -D i18next-parser
Créez le fichier de configuration i18next-parser.config.js
:
// i18next-parser.config.js
module.exports = {
defaultNamespace: 'common',
locales: ['en', 'fr'],
output: 'public/locales/$LOCALE/$NAMESPACE.json',
}
Ajoutez le script dans package.json
:
"extract-translations": "i18next 'src/**/*.{js,jsx,ts,tsx}' 'pages/**/*.{js,jsx,ts,tsx}' [-oc]"
Lancez-le après avoir utilisé une nouvelle clé de traduction dans votre code, elle sera automatiquement ajoutée dans vos fichiers de traduction.
Avec une API
Une autre solution pour générer automatiquement les clés est d'utiliser la propriété addPath
, qui permet de préciser un endpoint d'API POST permettant de pousser les clés manquantes dans les fichiers de traduction.
La propriété saveMissing
permet de forcer l'ajout de nouvelles clés
// next-i18next.config.js
[...]
backend: {
addPath: '/api/locales/{{lng}}/{{ns}}'
}
saveMissing: true
[...]
Bonus
En environnement de développement
Dans le cas d'une traduction server-side, la modification d'un fichier de traduction nécessite un nouveau build de l'application pour être prise en compte. Cela peut être fastidieux pendant la phase de développement.
Pour que les modifications soient prises en compte sans avoir à relancer le build, il faut ajouter reloadOnPrerender : true
dans le fichier next-i18next.config.js
.
// next-i18next.config.js
[...]
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr'],
},
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
debug: true,
serializeConfig: false,
reloadOnPrerender: true,
}
Locale detection
Par défaut next-i18next détecte la langue du navigateur et la met dans l'url. Pour éviter cela il faut ajouter localeDetection : false
à votre objet i18n dans next.config.js
:
// next.config.js
[...]
i18n: {
...i18n,
localeDetection: false,
}
[...]
Un peu de lecture
Pour completer cette petite présentation, voici le répertoire du plugin i18next-http-backend avec des détails sur les configurations existantes.
Piste d'améliorations
Certains cas de figure n'ont pas été abordé ici, comme par exemple le fait de récupérer les fichiers de traduction au build time grâce au backend HTTP plutôt qu'avec un script séparé comme présenté ci-dessus. Cette méthode pourrait être utile dans le cas d'un système de traduction totalement distant sans fichier locaux.
Conclusion
En fonction du besoin, il existe différentes méthodes pour récupérer vos fichiers de traduction, que ce soit pour une application SSG, SSR ou CSR vous trouverez forcément votre bonheur avec i18next.
Recommandations
N'utilisez pas l'internationalisation comme un CMS (Content Management System). Si vous avez besoin de modifier du contenu dans une seule langue, utilisez un CMS dédié.
Plusieurs librairies de traduction existent, ainsi que plusieurs TMS (Translation Management System). Certains TMS ont des plugins pour une ou plusieurs librairies de traduction. Dans ce cas, il faudra donc favoriser des librairies de traduction qui ont des plugins pour votre TMS.
Il est important de bien définir le besoin pour choisir quelle méthode de traduction utiliser. Pour cibler le besoin, il faut se demander quelle sera la fréquence de mise à jour des traductions. Un fréquence élevée impliquera une traduction client-side, une fréquence faible impliquera plutôt une traduction server-side.
La méthode la plus simple est de faire une traduction server-side, c'est la méthode qui demandera le moins de configuration. Accompagné des scripts permettant de récupérer les traductions du TMS et de les ajouter dans les fichiers de traduction, vous pouvez rapidement utiliser un TMS et traduire votre application.