Zum Inhalt springen
12. Oktober 202611 min readGuides

Übersetzungen ohne Redeploy aktualisieren: Laden zur Laufzeit, Caching, und warum Nutzer alte Texte sehen

Um Übersetzungen ohne Redeploy zu aktualisieren, holen Sie sie aus dem Build heraus: Die App lädt ihre Übersetzungsdateien zur Laufzeit von einer URL (Ihrem eigenen Server, einem Object-Storage-Bucket oder einem Übersetzungs-CDN), eine Textänderung ist also eine Dateiänderung unter dieser URL statt eines neuen Releases. Mit i18next ist das ein Backend-Plugin statt gebündelter resources. Wie schnell Nutzer eine Änderung sehen, entscheidet dann das Caching: Jeder Browser-Cache, jeder Service Worker, jede localStorage-Schicht und jedes CDN im Pfad kann die alte Datei weiter ausliefern, bis sein eigener Ablauf erreicht ist.

Dieser Beitrag behandelt die drei Wege, auf denen Teams Übersetzungsupdates heute handhaben, die Caches hinter "Ich habe deployt und der Text hat sich nicht geändert", die Fälle, in denen Bundling die bessere Wahl bleibt, und wie das in Locize funktioniert.

Kernfakten
  • Gebündelte Übersetzungen verhalten sich wie Code: Wird Übersetzungs-JSON ins Bundle importiert oder i18next als resources übergeben, braucht jede Textänderung einen Build und ein Deploy, oder ein App-Store-Release.
  • Laden zur Laufzeit entkoppelt Texte von Releases: i18next lädt Namespaces über ein Backend-Plugin, und i18next-http-backend holt sie von jeder URL, die Sie konfigurieren.
  • Veraltete Texte sind fast immer ein Cache: der HTTP-Cache des Browsers (Cache-Control max-age), Service-Worker-Caches (die nie von selbst ablaufen), localStorage-Cache-Backends (bei i18next-localstorage-backend standardmässig 7 Tage) und CDN-Edges.
  • Gebündelte Ressourcen schalten das Backend ab: i18next lädt nur dann über ein Backend, wenn beim Init keine resources übergeben werden oder partialBundledLanguages aktiviert ist.
  • Bundling bleibt für manche Apps richtig: Offline-first-Apps, vorgerenderte Seiten, native App-Binaries und vertrauliche Texte.

Warum eine Textänderung meistens ein Release bedeutet

Die meisten Apps starten mit Übersetzungen im Repository, neben dem Code, ins Bundle importiert und i18next beim Init übergeben:

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 }
  }
})

Das ist einfach, schnell und funktioniert offline. Es bedeutet aber auch, dass der Bundler Übersetzungen genau wie Code behandelt: Sie landen in einer JavaScript-Datei, und der einzige Weg, ein einzelnes Wort zu ändern, ist ein neuer Build. Eine Übersetzerin korrigiert einen Tippfehler, ein Entwickler holt die Datei, öffnet einen Pull Request, wartet auf die CI und deployt. Bei nativen Mobile-Apps kommt noch ein App-Store-Review dazu. Der Release-Zyklus Ihrer Texte ist fest mit dem Release-Zyklus Ihres Codes verschweisst, also das Gegenteil dessen, was Continuous Localization braucht.

Dieselbe Konfiguration versteckt eine zweite Falle. Laut den i18next-Konfigurationsoptionen sind resources für Setups ohne Backend gedacht, und erst partialBundledLanguages: true erlaubt gebündelte Ressourcen und ein Backend nebeneinander. Teams, die später ein Backend-Plugin hinzufügen und die gebündelten Ressourcen stehen lassen, erhalten ein Backend, das nie aufgerufen wird: einer der klassischen Gründe für "Übersetzungen werden nach dem Deploy nicht aktualisiert". Selbst mit partialBundledLanguages wird eine Sprache und ein Namespace, die bereits aus dem Bundle im Store liegen, nicht erneut geladen, ausser Sie laden sie explizit mit i18next.reloadResources() neu.

Wie Teams das heute handhaben

1. Gebündelte Dateien plus Redeploy

Der Standard. Die Übersetzungsdateien liegen im Repo, ein Skript oder ein CI-Job holt neue Übersetzungen von dort, wo Übersetzer arbeiten, und das nächste Deploy liefert sie aus. Der Vorteil: kein Request zur Laufzeit, Offline-Unterstützung ab dem ersten Start und Texte, die zusammen mit dem Code versioniert sind. Der Preis: Jede Korrektur wartet auf ein Release, und der Release-Rhythmus Ihrer langsamsten Plattform wird zum Update-Rhythmus Ihrer Texte. Wie diese Übergabe mit Continuous Deployment kollidiert, beschreibt wie Batch-Übersetzungsworkflows Continuous Deployment brechen.

2. Laden zur Laufzeit vom eigenen Server oder aus dem Object Storage

Die App holt ihre Übersetzungsdateien beim Start von einer URL, die Sie kontrollieren: ein statischer Ordner auf Ihrem Webserver, ein Storage-Bucket, ein beliebiger Host. Mit i18next ist das 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'
    }
  })

Komponenten in React, Vue oder React Native rufen weiterhin t() auf wie bisher; nur die Quelle der Texte ändert sich. Eine Textänderung ist jetzt ein Upload, ganz ohne Build. Was Sie dafür übernehmen: die Pipeline, die Übersetzungen von dort, wo Übersetzer arbeiten, auf diesen Host kopiert, Cross-Origin-Header, die Verfügbarkeit dieses Hosts und die Cache-Header auf diesen Dateien, um die es im nächsten Abschnitt geht.

3. Ein CDN, das Ihr Translation-Management-System befüllt

Das Translation-Management-System (TMS) veröffentlicht die Dateien selbst auf einem CDN, und die App lädt sie von dort. Download, Commit und Upload verschwinden aus Ihrer Pipeline: Eine gespeicherte und veröffentlichte Übersetzung ist die Live-Datei. Sie wählen weiterhin ein Backend-Plugin und leben weiterhin mit Caching, aber Hosting, globale Verteilung und der Publish-Schritt sind nicht mehr Ihre Aufgabe. Wie das in ein wachsendes i18next-Setup passt, beschreibt i18next in production.

Gebündelt + RedeployEigener Server oder StorageCDN aus dem TMS
Eine Textänderung brauchtBuild und Deploy (oder Store-Release)Einen Datei-UploadEin Publish im TMS
Rendert offline beim ersten StartJaNur mit gebündeltem FallbackNur mit gebündeltem Fallback
Sie betreibenNichts zusätzlichHosting, Upload-Pipeline, Cache-HeaderBackend-Konfiguration und Cache-Einstellungen
Woher veraltete Texte kommenAlte Builds, die noch laufenCaches zwischen Host und NutzerCaches zwischen CDN und Nutzer

Wie Caching entscheidet, wann Nutzer die Änderung sehen

Sobald Übersetzungen zur Laufzeit geladen werden, lautet die Frage nicht mehr "Haben wir deployt?", sondern "Welcher Cache hält noch die alte Datei?". HTTP-Caching wird über den Cache-Control-Response-Header gesteuert:

  • max-age=N: Die Antwort bleibt N Sekunden nach ihrer Erzeugung frisch; solange sie frisch ist, darf ein Cache sie wiederverwenden, ohne den Server zu fragen.
  • no-cache: Caches dürfen die Antwort speichern, müssen sie aber vor jeder Wiederverwendung beim Server validieren. Hat sich die Datei nicht geändert, ist das ein günstiger bedingter Request.
  • no-store: Kein Cache darf die Antwort speichern.
  • s-maxage: eine eigene Frischedauer für geteilte Caches wie CDNs; sie überschreibt dort max-age und wird vom Browser ignoriert.
  • immutable: Die Antwort ändert sich nicht, solange sie frisch ist, der Browser überspringt also die Revalidierung.

Gehashte Dateinamen: perfekt für Bundles, unpraktisch für Laufzeit-Texte

Für statische Assets beschreibt MDN das Cache-Busting-Muster: eine Version oder einen Hash in die URL schreiben, die Datei nie verändern, ein Jahr lang cachen und als immutable markieren. So liefern Bundler JavaScript aus, und deshalb sind gebündelte Übersetzungen nie veraltet, sobald das neue Bundle geladen ist.

Für Übersetzungen, die zur Laufzeit geladen werden, wirkt es umgekehrt. Enthält der Dateiname einen Hash, muss die App den neuen Namen erfahren, und die Stelle, von der sie ihn erfährt, ist meistens das nächste Deploy. Dasselbe gilt für die Option queryStringParams von i18next-http-backend (zum Beispiel { v: '1.3.5' }): Der Versionsstring lebt in Ihrem Code. Laufzeit-Übersetzungen liegen deshalb meistens unter stabilen URLs mit kurzem max-age, oder mit no-cache und Revalidierung, und tauschen ein paar zusätzliche Requests gegen Aktualität. Wenn Sie trotzdem lange cachen möchten, lassen Sie die App zuerst ein kleines, ungecachtes Manifest laden, das die aktuelle Übersetzungsversion nennt.

Die Schichten zwischen einer Änderung und dem Nutzer

SchichtWas den alten Text hältWie der neue Text durchkommt
HTTP-Cache des BrowsersDie JSON-Antwort, für ihr max-ageEin kurzes max-age, oder no-cache mit Revalidierung. i18next-http-backend gibt requestOptions an fetch weiter, und cache: 'no-cache' lässt den Browser dort auch einen frischen Eintrag revalidieren (ein vorgelagertes CDN kann trotzdem aus seinem eigenen Cache antworten)
Service WorkerEinträge in der Cache API, die nur aktualisiert werden, wenn Ihr Code sie aktualisiert, und nie von selbst ablaufenÜbersetzungs-URLs vom Precaching ausnehmen oder network-first laden
localStorage-Cachei18next-localstorage-backend liefert einen gecachten Namespace bis zur expirationTime (Standard: 7 Tage)Eine kürzere Ablaufzeit, ein neuer Wert für defaultVersion oder versions, oder der Refresh-Modus des Chained Backends (siehe unten)
CDN-EdgeDie Edge-Kopie, für ihr max-age, s-maxage oder die eigene TTL des CDNEine kurze TTL, oder ein Purge beim Publish
Langlebiger ServerDie i18next-Instanz, die die Dateien beim Start geladen hatreloadInterval von i18next-http-backend: ausserhalb des Browsers standardmässig eine Stunde, im Browser aus. Eine Instanz pro Prozess wiederverwenden
Vorgerendertes HTMLText, der beim Build ins HTML gerendert wurdeEin neuer Build, oder Revalidierung wie Next.js ISR
React-RenderingNichts: Der Store hat den neuen Text, aber keine Komponente hat neu gerendertreact-i18next rendert standardmässig bei languageChanged neu; setzen Sie bindI18nStore: 'added', damit neu hinzugefügte Ressourcen ein Re-Rendering auslösen

Ein Setup, das sofort aus einem lokalen Cache rendert und Änderungen trotzdem noch im selben Besuch übernimmt, kombiniert i18next-chained-backend mit dessen Modus refreshAndUpdateStore: Er liefert die gecachte Kopie, holt die frische vom nächsten Backend, aktualisiert den Cache und fügt sie dem Store hinzu:

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
    }
  })

Für Apps, die stundenlang offen bleiben (Dashboards, Admin-Tools), rufen Sie i18next.reloadResources() in einem Intervall auf oder dann, wenn der Tab wieder sichtbar wird; mit bindI18nStore: 'added' erscheint der neu geladene Text ohne Neuladen der Seite.

Debugging: "Übersetzungen werden nicht aktualisiert"

  1. Wird die Übersetzungsdatei überhaupt angefragt? Prüfen Sie den Netzwerk-Tab. Kein Request heisst: Die Texte kommen aus dem Bundle (suchen Sie nach resources beim Init), aus einem Service Worker oder aus dem localStorage.
  2. Kommt sie aus einem Cache? "Memory cache" oder "disk cache" heisst: Der Browser hat seine eigene Kopie geliefert, ohne nachzufragen. Eine 304 heisst: Der Browser hat revalidiert, und das CDN oder der Ursprungsserver hat "unverändert" geantwortet; ein veralteter Text nach einer 304 liegt also weiter oben in der Kette. Die Response-Header Cache-Control und Age zeigen, was das CDN macht.
  3. Ist die Datei selbst aktuell? Öffnen Sie die Datei-URL direkt und vergleichen Sie sie mit der Quelle. Ist sie schon dort alt, liegt das Problem beim Publish- oder Upload-Schritt, nicht in der App.
  4. Was speichert der Browser? Schauen Sie in den Entwicklertools nach localStorage-Einträgen mit dem Präfix i18next_res_ (der Standard von i18next-localstorage-backend) und in den Cache Storage.
  5. Auf dem Server: Wurde der Prozess neu gestartet, und lädt die i18next-Instanz in einem Intervall neu? Eine neue Instanz pro Request lädt die Übersetzungen bei jedem Request herunter, und jede Instanz lässt ihren eigenen Reload-Timer weiterlaufen (reloadInterval ist ausserhalb des Browsers standardmässig eine Stunde); erstellen Sie die Instanz also einmal pro Prozess und verwenden Sie sie wieder.
  6. Der Store hat den Text, der Bildschirm nicht: Das ist ein Render-Binding, kein Cache. Siehe bindI18nStore oben.

Sehen Sie Keys oder Fallback-Texte statt alter Texte, ist die Ursache eine fehlende Übersetzung, keine veraltete: siehe missing translations.

Wann Bundling die richtige Wahl ist

Laden zur Laufzeit ist nicht automatisch besser. Bündeln Sie, wenn:

  • die App offline-first ist. Eine zur Laufzeit geladene Übersetzung braucht beim ersten Start ein Netzwerk. Muss die App rendern, bevor sie je online war, bündeln Sie mindestens die Standardsprache. Der Hybrid ist ein Chained Backend mit dem Netzwerk zuerst und gebündelten Ressourcen als Fallback.
  • Seiten vorgerendert werden (SSG, Static Export). Beim Build gerenderter Text ist Teil des HTML. Eine ersetzte Datei auf einem CDN ändert kein bereits vorhandenes HTML; die Seite braucht einen neuen Build oder eine Revalidierung (Next.js ISR, das ein Static Export nicht unterstützt). Frischere Texte nach der Hydration im Client zu laden heisst, dass Nutzer sehen können, wie sich der Text nach dem Erscheinen der Seite ändert. Für Content-Seiten, die bei jeder Änderung ohnehin neu gebaut werden, ist Bundling einfacher.
  • Texte in einem nativen App-Binary ausgeliefert werden. iOS- und Android-String-Ressourcen werden in die App kompiliert, eine Änderung bedeutet also einen neuen Build. Die App Review Guidelines von Apple schliessen das Herunterladen von Code aus, der die Funktionen einer App ändert (2.5.2). Übersetzungen sind Daten, kein Code, deshalb können React-Native-Apps sie zur Laufzeit laden. Richtlinie 2.3.1 verlangt aber auch, dass die Funktionalität der App für das App Review klar ist; beschreiben Ihre Texte Funktionen, Preise oder Bedingungen, hält die Auslieferung mit dem geprüften Build beides identisch.
  • Texte vertraulich sind. Eine Datei unter einer öffentlichen URL kann jeder lesen, der sie findet: unveröffentlichte Produktnamen, interne Tools, kundenspezifische Formulierungen. Bündeln Sie, oder hosten Sie selbst hinter einer Authentifizierung. Ein Key, der in einer Browser-App ausgeliefert wird, ist für jeden Nutzer sichtbar; Key-geschützte Downloads schützen also serverseitige und Build-Zeit-Abrufe, kein öffentliches Frontend.
  • Texte sich atomar mit dem Code ändern müssen. Ein neuer Key, den neuer Code verwendet, muss existieren, bevor dieser Code die Nutzer erreicht. Beim Laden zur Laufzeit veröffentlichen Sie neue Keys vor oder zusammen mit dem Deploy, sonst sehen Nutzer Fallbacks.

Wie das in Locize funktioniert

Locize, gebaut vom Team hinter i18next, ist die dritte Option oben:

  • Standardmässig auf einem CDN veröffentlicht. Übersetzungen werden auf dem Locize-CDN veröffentlicht, sofern Sie das nicht abschalten, und Web- sowie React-Native-Apps laden sie zur Laufzeit über i18next; eine veröffentlichte Änderung braucht kein Redeploy:
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
    }
  })
  • Zwei CDN-Typen, zwei Caching-Modelle. Das Standard-CDN (BunnyCDN, der Standard für neue Projekte) ist nur öffentlich, mit einer fixen Cache-Dauer von einer Stunde. Das Pro-CDN (Amazon CloudFront, ab dem Growth-Plan) erlaubt ein Cache-Control max-age pro Version, unterstützt privates Publizieren mit API-Key und bewahrt Backups veröffentlichter Namespaces auf. Siehe CDN types: Standard vs Pro und Caching.
  • Entwicklung und Produktion bleiben getrennt. Auf dem Pro-CDN empfiehlt die Caching-Dokumentation keinen Cache für Entwicklungsversionen und eine eigene Produktionsversion ohne Auto-Publish, manuell oder über API oder CLI veröffentlicht, mit einem niedrigen max-age, das nicht null ist. Während der Entwicklung gegen das Standard-CDN umgeht noCache: true in i18next-locize-backend (oder ?cache=no an der URL) den Cache.
  • Bundling bleibt eine Option. Laden Sie Übersetzungen mit der CLI, der API oder der GitHub Action locize/download herunter und bündeln Sie sie, oder kombinieren Sie beides mit einem gebündelten Fallback. Native iOS- und Android-Apps gehen diesen Weg: Ihre String-Dateien werden beim Build synchronisiert, denn Locize hat kein natives Over-the-Air-SDK. Für vertrauliche Texte schalten Sie Auto-Publish ab und bündeln, oder Sie nutzen privates Publizieren.

Um den Kreislauf einmal komplett zu sehen: Erstellen Sie ein Projekt, richten Sie i18next-locize-backend darauf aus, ändern Sie einen Text im Editor, veröffentlichen Sie ihn und laden Sie Ihre App neu. Das grössere Bild, Texte unabhängig vom Code auszuliefern, zeigt die Seite zu Continuous Localization.

Häufig gestellte Fragen

Wie aktualisiere ich Übersetzungen, ohne meine App neu zu deployen? Laden Sie Übersetzungen zur Laufzeit von einer URL (eigener Server, Storage-Bucket oder CDN aus dem TMS), statt sie in den Build zu kompilieren. Mit i18next nutzen Sie ein Backend-Plugin wie i18next-http-backend und übergeben beim Init keine gebündelten resources. Eine Textänderung wird zur Dateiänderung, vorbehaltlich der Caches dazwischen.

Warum werden meine i18next-Übersetzungen nach einem Deploy nicht aktualisiert? Meistens wegen gebündelter Ressourcen, die das Backend abschalten, eines Caches, der noch die alte Datei liefert (Browser, Service Worker, localStorage-Backend, CDN-Edge), einer langlebigen Server-Instanz, die die Dateien beim Start geladen hat, oder React-Komponenten, die nach der Store-Änderung nicht neu gerendert haben.

Wie lade ich Übersetzungen zur Laufzeit von einem CDN? Richten Sie ein i18next-Backend auf das URL-Muster des CDN aus, halten Sie die URL stabil, wählen Sie ein max-age passend zur nötigen Aktualität der Texte, und bündeln Sie eine Fallback-Sprache, falls die App offline rendern muss.

Wie lange dauert es, bis Nutzer eine geänderte Übersetzung sehen? So lange, wie der langsamste Cache im Pfad es zulässt: das max-age des Browsers, die Lebensdauer im CDN, eine localStorage-Ablaufzeit oder ein Service-Worker-Eintrag, den Ihr Code nicht ersetzt hat. Offene Apps sehen die Änderung beim nächsten Laden des Namespace.

Wann sollte ich Übersetzungen bündeln statt sie zur Laufzeit zu laden? Bei Offline-first-Apps, vorgerenderten Seiten, nativen App-Binaries, die durch das Store-Review gehen, und vertraulichen Texten. Ein gebündelter Fallback kombiniert mit Laden zur Laufzeit deckt den Grossteil des Mittelfelds ab.

Kann ich die Texte einer Mobile-App ohne App-Store-Release ändern? Mit React Native und i18next ja: Übersetzungen werden wie im Web zur Laufzeit geladen. Native iOS- und Android-String-Ressourcen werden ins Binary kompiliert; ohne ein Over-the-Air-SDK (Locize hat keines) bedeutet eine Änderung also einen neuen Build und ein Store-Release.

Übersetzungen noch von Hand verwalten?

Locize ist das Translation-Management-Backend vom i18next-Team: CDN-Auslieferung, KI-Übersetzung, In-Context-Editing, keine Redeploys.

14 Tage kostenlos testen