Aller au contenu
12 octobre 202613 min readGuides

Mettre à jour les traductions sans redéployer : chargement à l'exécution, cache, et pourquoi les utilisateurs voient d'anciens textes

Pour mettre à jour les traductions sans redéployer, sortez-les du build : l'application charge ses fichiers de traduction à l'exécution depuis une URL (votre propre serveur, un bucket de stockage objet ou un CDN de traductions), si bien que modifier un texte revient à modifier un fichier à cette URL plutôt qu'à livrer une nouvelle version. Avec i18next, c'est un plugin backend à la place de resources embarquées. La vitesse à laquelle les utilisateurs voient un changement dépend ensuite du cache : chaque cache navigateur, service worker, couche localStorage et CDN sur le chemin peut continuer à servir l'ancien fichier jusqu'à sa propre expiration.

Ce billet présente les trois façons dont les équipes gèrent aujourd'hui les mises à jour de traductions, les caches derrière « j'ai déployé et le texte n'a pas changé », les cas où le bundling reste le meilleur choix, et le fonctionnement dans Locize.

Faits clés
  • Les traductions embarquées se comportent comme du code : si le JSON de traduction est importé dans le bundle ou passé à i18next comme resources, chaque changement de texte exige un build et un déploiement, ou une nouvelle version sur le store.
  • Le chargement à l'exécution découple les textes des releases : i18next charge les namespaces via un plugin backend, et i18next-http-backend les récupère depuis n'importe quelle URL que vous configurez.
  • Des textes périmés viennent presque toujours d'un cache : le cache HTTP du navigateur (Cache-Control max-age), les caches de service worker (qui n'expirent jamais d'eux-mêmes), les backends de cache localStorage (7 jours par défaut dans i18next-localstorage-backend) et les nœuds du CDN.
  • Les ressources embarquées désactivent le backend : i18next ne charge via un backend que si l'option resources n'est pas passée à l'initialisation, ou si partialBundledLanguages est activé.
  • Le bundling reste pertinent pour certaines applications : applications offline-first, pages pré-rendues, binaires d'applications natives et textes confidentiels.

Pourquoi modifier un texte implique en général une release

La plupart des applications démarrent avec les traductions dans le dépôt, à côté du code, importées dans le bundle et passées à i18next à l'initialisation :

import i18next from 'i18next'
import en from './locales/en/common.json'
import de from './locales/de/common.json'

i18next.init({
  lng: 'en',
  fallbackLng: 'en',
  defaultNS: 'common',
  resources: {
    en: { common: en },
    de: { common: de }
  }
})

C'est simple, rapide et cela fonctionne hors ligne. Mais cela signifie aussi que le bundler traite les traductions exactement comme du code : elles finissent dans un fichier JavaScript, et la seule façon de changer un seul mot est un nouveau build. Une traductrice corrige une coquille, un développeur récupère le fichier, ouvre une pull request, attend la CI et déploie. Sur mobile natif, il faut ajouter la revue de la boutique d'applications. Le cycle de release de vos textes est soudé à celui de votre code, soit l'inverse de ce dont a besoin la localisation continue.

La même configuration cache un second piège. D'après les options de configuration d'i18next, resources est prévu pour les configurations sans backend, et c'est partialBundledLanguages: true qui permet de combiner ressources embarquées et backend. Les équipes qui ajoutent un plugin backend plus tard en laissant les ressources embarquées en place obtiennent un backend qui n'est jamais appelé : l'une des raisons classiques des « traductions qui ne se mettent pas à jour après un déploiement ». Même avec partialBundledLanguages, une langue et un namespace déjà présents dans le store grâce au bundle ne sont pas rechargés, sauf si vous le demandez explicitement avec i18next.reloadResources().

Comment les équipes s'y prennent aujourd'hui

1. Fichiers embarqués et redéploiement

L'approche par défaut. Les fichiers de traduction vivent dans le dépôt, un script ou un job de CI récupère les nouvelles traductions là où travaillent les traducteurs, et le prochain déploiement les livre. Les avantages : aucune requête à l'exécution, le hors-ligne dès le premier lancement, et des textes versionnés avec le code. Le coût : chaque correction attend une release, et le rythme de release de votre plateforme la plus lente devient le rythme de mise à jour de vos textes. La façon dont ce passage de relais entre en collision avec le déploiement continu est décrite dans comment les workflows de traduction par lots cassent le déploiement continu.

2. Chargement à l'exécution depuis votre serveur ou un stockage objet

L'application récupère ses fichiers de traduction au démarrage depuis une URL que vous contrôlez : un dossier statique sur votre serveur web, un bucket de stockage, n'importe quel hôte. Avec i18next, c'est i18next-http-backend :

import i18next from 'i18next'
import HttpBackend from 'i18next-http-backend'

i18next
  .use(HttpBackend)
  .init({
    fallbackLng: 'en',
    ns: ['common'],
    defaultNS: 'common',
    backend: {
      // any URL: your web server, a storage bucket, a CDN
      loadPath: 'https://translations.example.com/{{lng}}/{{ns}}.json'
    }
  })

Les composants React, Vue ou React Native continuent d'appeler t() comme avant ; seule la source des textes change. Un changement de texte est désormais un upload, sans aucun build. Ce que vous prenez en charge en échange : le pipeline qui copie les traductions de l'outil des traducteurs vers cet hôte, les en-têtes cross-origin, la disponibilité de cet hôte, et les en-têtes de cache de ces fichiers, sujet de la section suivante.

3. Un CDN alimenté par votre système de gestion des traductions

Le système de gestion des traductions (TMS) publie lui-même les fichiers sur un CDN, et l'application les charge depuis là. Téléchargement, commit et upload disparaissent de votre pipeline : une traduction enregistrée et publiée est le fichier en production. Vous choisissez toujours un plugin backend et vous composez toujours avec le cache, mais l'hébergement, la distribution mondiale et l'étape de publication ne sont plus à votre charge. La place de cette approche dans un projet i18next qui grandit est décrite dans i18next in production.

Embarqué + redéploiementVotre serveur ou stockageCDN alimenté par le TMS
Un changement de texte demandeBuild et déploiement (ou release sur le store)Un upload de fichierUne publication dans le TMS
Affichage hors ligne au premier lancementOuiSeulement avec un repli embarquéSeulement avec un repli embarqué
Vous exploitezRien de plusHébergement, pipeline d'upload, en-têtes de cacheConfiguration du backend et réglages de cache
Origine des textes périmésAnciens builds encore en serviceCaches entre l'hôte et l'utilisateurCaches entre le CDN et l'utilisateur

Comment le cache décide du moment où les utilisateurs voient le changement

Dès que les traductions sont chargées à l'exécution, la question n'est plus « avons-nous déployé ? » mais « quel cache détient encore l'ancien fichier ? ». Le cache HTTP est piloté par l'en-tête de réponse Cache-Control :

  • max-age=N : la réponse reste fraîche pendant N secondes après sa génération ; tant qu'elle est fraîche, un cache peut la réutiliser sans interroger le serveur.
  • no-cache : les caches peuvent stocker la réponse, mais doivent la valider auprès du serveur avant chaque réutilisation. Si le fichier n'a pas changé, c'est une requête conditionnelle peu coûteuse.
  • no-store : aucun cache ne doit stocker la réponse.
  • s-maxage : une durée de fraîcheur distincte pour les caches partagés comme les CDN ; elle y remplace max-age et est ignorée par le navigateur.
  • immutable : la réponse ne changera pas tant qu'elle est fraîche, le navigateur saute donc la revalidation.

Noms de fichiers hachés : parfaits pour les bundles, malcommodes pour les textes chargés à l'exécution

Pour les ressources statiques, MDN décrit le motif de cache-busting : mettre une version ou un hash dans l'URL, ne jamais modifier le fichier, le mettre en cache pendant un an et le marquer immutable. C'est ainsi que les bundlers livrent le JavaScript, et c'est pourquoi les traductions embarquées ne sont jamais périmées une fois le nouveau bundle chargé.

Pour les traductions chargées à l'exécution, c'est l'inverse. Si le nom du fichier contient un hash, l'application doit apprendre le nouveau nom, et l'endroit où elle l'apprend est généralement le prochain déploiement. Il en va de même pour l'option queryStringParams d'i18next-http-backend (par exemple { v: '1.3.5' }) : la chaîne de version vit dans votre code. Les traductions chargées à l'exécution se trouvent donc en général à des URL stables avec un max-age court, ou avec no-cache et revalidation, en échangeant quelques requêtes de plus contre la fraîcheur. Si vous tenez malgré tout à un cache long, faites d'abord charger à l'application un petit manifeste non mis en cache qui indique la version actuelle des traductions.

Les couches entre un changement et l'utilisateur

CoucheCe qui conserve l'ancien texteComment faire passer le nouveau texte
Cache HTTP du navigateurLa réponse JSON, pendant son max-ageUn max-age court, ou no-cache avec revalidation. i18next-http-backend transmet requestOptions à fetch, et cache: 'no-cache' y force le navigateur à revalider même une entrée fraîche (un CDN placé devant peut tout de même répondre depuis son propre cache)
Service workerLes entrées de la Cache API, mises à jour uniquement si votre code le fait, et qui n'expirent jamais d'elles-mêmesExclure les URL de traduction du precaching, ou les charger en network-first
Cache localStoragei18next-localstorage-backend sert un namespace mis en cache jusqu'à expirationTime (par défaut : 7 jours)Une expiration plus courte, une nouvelle valeur de defaultVersion ou versions, ou le mode de rafraîchissement du chained backend (ci-dessous)
Nœud du CDNLa copie en périphérie, pendant son max-age, son s-maxage ou le TTL propre au CDNUn TTL court, ou une purge à la publication
Serveur de longue duréeL'instance i18next qui a chargé les fichiers au démarragereloadInterval d'i18next-http-backend : une heure par défaut hors navigateur, désactivé dans le navigateur. Réutiliser une seule instance par processus
HTML pré-renduLe texte rendu dans le HTML au moment du buildUn nouveau build, ou une revalidation comme l'ISR de Next.js
Rendu ReactRien : le store contient le nouveau texte, mais aucun composant ne s'est re-rendureact-i18next re-rend par défaut sur languageChanged ; définissez bindI18nStore: 'added' pour re-rendre quand des ressources sont ajoutées

Une configuration qui s'affiche instantanément depuis un cache local tout en récupérant les changements pendant la même visite combine i18next-chained-backend et son mode refreshAndUpdateStore : il sert la copie en cache, récupère la version fraîche depuis le backend suivant, met à jour le cache et l'ajoute au store :

import i18next from 'i18next'
import { initReactI18next } from 'react-i18next'
import ChainedBackend from 'i18next-chained-backend'
import LocalStorageBackend from 'i18next-localstorage-backend'
import HttpBackend from 'i18next-http-backend'

i18next
  .use(ChainedBackend)
  .use(initReactI18next)
  .init({
    fallbackLng: 'en',
    backend: {
      backends: [LocalStorageBackend, HttpBackend],
      backendOptions: [
        { expirationTime: 7 * 24 * 60 * 60 * 1000 }, // the default: 7 days
        { loadPath: 'https://translations.example.com/{{lng}}/{{ns}}.json' }
      ],
      // serve the cached copy first, then fetch the fresh one and update the store
      cacheHitMode: 'refreshAndUpdateStore'
    },
    react: {
      bindI18nStore: 'added' // re-render when refreshed resources land in the store
    }
  })

Pour les applications qui restent ouvertes des heures (tableaux de bord, outils d'administration), appelez i18next.reloadResources() à intervalle régulier ou quand l'onglet redevient visible ; avec bindI18nStore: 'added', le texte rechargé apparaît sans recharger la page.

Déboguer « les traductions ne se mettent pas à jour »

  1. Le fichier de traduction est-il seulement demandé ? Regardez l'onglet réseau. Aucune requête signifie que les textes viennent du bundle (cherchez resources à l'initialisation), d'un service worker ou du localStorage.
  2. Est-il servi depuis un cache ? « Memory cache » ou « disk cache » signifie que le navigateur a servi sa propre copie sans rien demander. Une réponse 304 signifie que le navigateur a revalidé et que le CDN ou le serveur d'origine a répondu « inchangé » : un texte périmé après un 304 se trouve donc en amont. Les en-têtes de réponse Cache-Control et Age montrent ce que fait le CDN.
  3. Le fichier lui-même est-il à jour ? Ouvrez directement l'URL du fichier et comparez-la avec la source. S'il est déjà ancien à cet endroit, le problème vient de l'étape de publication ou d'upload, pas de l'application.
  4. Que stocke le navigateur ? Dans les outils de développement, regardez les entrées localStorage avec le préfixe i18next_res_ (la valeur par défaut d'i18next-localstorage-backend) et le Cache Storage.
  5. Côté serveur : le processus a-t-il été redémarré, et l'instance i18next se recharge-t-elle à intervalle régulier ? Une nouvelle instance par requête télécharge les traductions à chaque requête, et chaque instance garde son propre minuteur de rechargement actif (reloadInterval vaut une heure par défaut hors navigateur) : créez donc l'instance une seule fois par processus et réutilisez-la.
  6. Le store a le texte, l'écran non : c'est une question de liaison du rendu, pas de cache. Voir bindI18nStore ci-dessus.

Si vous voyez des clés ou des textes de repli plutôt que d'anciens textes, la cause est une traduction manquante et non périmée : voir missing translations.

Quand le bundling est le bon choix

Le chargement à l'exécution n'est pas automatiquement meilleur. Embarquez les traductions quand :

  • l'application est offline-first. Une traduction chargée à l'exécution exige un réseau au premier lancement. Si l'application doit s'afficher avant d'avoir jamais été en ligne, embarquez au moins la langue par défaut. L'approche hybride est un chained backend avec le réseau en premier et des ressources embarquées en repli.
  • les pages sont pré-rendues (SSG, export statique). Le texte rendu au build fait partie du HTML. Remplacer un fichier sur un CDN ne change pas un HTML déjà généré ; la page a besoin d'un nouveau build ou d'une revalidation (l'ISR de Next.js, que l'export statique ne prend pas en charge). Charger des textes plus récents côté client après l'hydratation signifie que les utilisateurs peuvent voir le texte changer après l'affichage de la page. Pour des pages de contenu reconstruites de toute façon à chaque modification, le bundling est plus simple.
  • les textes sont livrés dans le binaire d'une application native. Les ressources de chaînes iOS et Android sont compilées dans l'application, donc les modifier implique un nouveau build. Les App Review Guidelines d'Apple interdisent de télécharger du code qui modifie les fonctionnalités d'une application (2.5.2). Les traductions sont des données, pas du code, c'est pourquoi les applications React Native peuvent les charger à l'exécution. Mais la règle 2.3.1 exige aussi que les fonctionnalités de l'application soient claires pour l'App Review ; si vos textes décrivent des fonctionnalités, des prix ou des conditions, les livrer avec le build validé garantit que les deux restent identiques.
  • les textes sont confidentiels. Un fichier à une URL publique peut être lu par quiconque la trouve : noms de produits non annoncés, outils internes, formulations propres à un client. Embarquez-les, ou hébergez-les vous-même derrière une authentification. Une clé livrée dans une application navigateur est visible par chaque utilisateur ; les téléchargements protégés par clé protègent donc les récupérations côté serveur et au build, pas un frontend public.
  • les textes doivent changer en même temps que le code. Une nouvelle clé utilisée par du nouveau code doit exister avant que ce code n'atteigne les utilisateurs. Avec le chargement à l'exécution, publiez les nouvelles clés avant le déploiement ou en même temps, sinon les utilisateurs voient des textes de repli.

Comment cela fonctionne dans Locize

Locize, créé par l'équipe derrière i18next, correspond à la troisième option ci-dessus :

  • Publié sur un CDN par défaut. Les traductions sont publiées sur le CDN de Locize sauf si vous le désactivez, et les applications web et React Native les chargent à l'exécution via i18next ; un changement publié ne demande aucun redéploiement :
import i18next from 'i18next'
import LocizeBackend from 'i18next-locize-backend'

i18next
  .use(LocizeBackend)
  .init({
    fallbackLng: 'en',
    backend: {
      projectId: '[PROJECTID]',
      version: 'latest', // a dedicated production version needs a plan with several versions
      cdnType: 'standard' // or 'pro', matching your project's CDN type
    }
  })
  • Deux types de CDN, deux modèles de cache. Le CDN Standard (BunnyCDN, la valeur par défaut des nouveaux projets) est uniquement public, avec une durée de cache fixe d'une heure. Le CDN Pro (Amazon CloudFront, à partir de l'offre Growth) permet de définir le Cache-Control max-age par version, prend en charge la publication privée qui exige une clé API, et conserve des sauvegardes des namespaces publiés. Voir CDN types: Standard vs Pro et caching.
  • Développement et production restent séparés. Sur le CDN Pro, la documentation sur le cache recommande de désactiver le cache sur les versions de développement, et une version de production dédiée sans publication automatique, publiée manuellement ou via l'API ou la CLI, avec un max-age faible mais non nul. Pendant le développement sur le CDN Standard, noCache: true dans i18next-locize-backend (ou ?cache=no dans l'URL) contourne le cache.
  • Le bundling reste possible. Téléchargez les traductions avec la CLI, l'API ou la GitHub Action locize/download et embarquez-les, ou combinez les deux avec un repli embarqué. Les applications natives iOS et Android suivent cette voie : leurs fichiers de chaînes sont synchronisés au build, car Locize n'a pas de SDK natif over-the-air. Pour des textes confidentiels, désactivez la publication automatique et embarquez les traductions, ou utilisez la publication privée.

Pour voir la boucle de bout en bout : créez un projet, pointez i18next-locize-backend dessus, modifiez un texte dans l'éditeur, publiez, puis rechargez votre application. La vue d'ensemble, livrer les textes indépendamment du code, se trouve sur la page continuous localization.

Questions fréquentes

Comment mettre à jour les traductions sans redéployer mon application ? Chargez les traductions à l'exécution depuis une URL (votre serveur, un bucket de stockage ou un CDN alimenté par le TMS) au lieu de les compiler dans le build. Avec i18next, utilisez un plugin backend comme i18next-http-backend et ne passez pas de resources embarquées à l'initialisation. Un changement de texte devient un changement de fichier, sous réserve des caches intermédiaires.

Pourquoi mes traductions i18next ne se mettent-elles pas à jour après un déploiement ? Le plus souvent à cause de ressources embarquées qui désactivent le backend, d'un cache qui sert encore l'ancien fichier (navigateur, service worker, backend localStorage, nœud du CDN), d'une instance serveur de longue durée qui a chargé les fichiers au démarrage, ou de composants React qui ne se sont pas re-rendus après la mise à jour du store.

Comment charger les traductions depuis un CDN à l'exécution ? Pointez un backend i18next vers le motif d'URL du CDN, gardez une URL stable, choisissez un max-age adapté à la fraîcheur nécessaire des textes, et embarquez une langue de repli si l'application doit s'afficher hors ligne.

Combien de temps faut-il pour que les utilisateurs voient une traduction modifiée ? Aussi longtemps que le permet le cache le plus lent du chemin : le max-age du navigateur, la durée de vie dans le CDN, une expiration localStorage ou une entrée de service worker que votre code n'a pas remplacée. Les applications ouvertes voient le changement au prochain chargement du namespace.

Quand faut-il embarquer les traductions plutôt que les charger à l'exécution ? Pour les applications offline-first, les pages pré-rendues, les binaires d'applications natives soumis à la revue du store, et les textes confidentiels. Un repli embarqué combiné au chargement à l'exécution couvre l'essentiel des cas intermédiaires.

Puis-je modifier les textes d'une application mobile sans nouvelle version sur le store ? Avec React Native et i18next, oui : les traductions se chargent à l'exécution comme sur le web. Les ressources de chaînes natives iOS et Android sont compilées dans le binaire ; sans SDK over-the-air (Locize n'en propose pas), les modifier implique donc un nouveau build et une nouvelle version sur le store.

Fatigué de gérer vos traductions à la main ?

Locize est le backend de gestion de traductions créé par l'équipe i18next : diffusion CDN, traduction IA, édition in-context, sans redéploiement.

Démarrez votre essai gratuit de 14 jours