Why Your SPA Shows Untranslated Strings After a Language Switch
In single-page applications, the translation layer often caches the initial language and does not automatically re-run when the route changes. You must explicitly re-trigger the translation engine on every navigation event so it picks...
How SPA Translation Works
Single-page applications load once and then swap views without a full page reload. Most translation scripts — including the Seatext AI snippet — inject themselves during that first load, scan the existing DOM, and replace text nodes with translated equivalents. Because the script only runs on the initial load event, it has no built-in awareness of subsequent route changes performed by React Router, Vue Router, or the Angular router.
The snippet stores a visitor identifier in localStorage and loads asynchronously to avoid blocking render. That design keeps the first paint fast, but it also means the translation engine never sees the new markup that appears after a route transition unless you tell it to look again.
Why Translations Disappear After a Language Switch
When a user changes language, two things typically happen: the application updates a locale flag in state or a cookie, and the router navigates to a new route or re-renders the current one. The translation script, however, still holds the old translated DOM fragments in its internal cache. Because no load event fires, the script does not re-scan the page, so the new markup remains in the original language.
This is not a bug in the translation service; it is a consequence of the SPA lifecycle. The trade-off is deliberate: avoiding a full-page reload preserves scroll position, component state, and analytics continuity. The cost is that you must manually invoke the translation routine after every navigation that can introduce new text.
Common Causes and Diagnostic Sequence
- Missing navigation hook. The most frequent cause is that no code calls the translation API after
router.push(),router.replace(), or a route guard resolves. - Race condition with async snippet. Because the Seatext snippet loads with
async, it may not be ready when the first route change fires. If your hook runs before the script initializes, the call is a no-op. - Cross-origin iframe or micro-frontend. If part of the UI lives on another domain, the snippet’s
localStorageaccess or DOM traversal can be blocked by the same-origin policy. - Stale cache key. Some translation layers key their cache by URL path. A language switch that keeps the same path (e.g.,
/dashboardfor both EN and ES) will serve the cached English version.
Follow this diagnostic order:
- Open DevTools → Console. Verify the Seatext script loads without errors (source S1).
- Add a temporary
console.login your route-change handler to confirm it fires on every language switch. - Call the translation re-initialization function manually from the console after a switch. If strings update, the hook is missing or mistimed.
- Check
localStoragefor the Seatext visitor ID. If it’s missing, the script may be blocked by privacy settings or cross-origin policy. - Test in an incognito window to rule out extension interference.
Framework-Specific Behaviors
React
In a typical React app, the snippet sits in public/index.html. After adding it, you must wrap your router’s navigation listener — for example, useEffect(() => { seatext.retranslate(); }, [location.pathname, locale]) — so that every route or locale change triggers a re-translation. The source pack recommends building and serving with npm start, then inspecting the Console and Network tabs to verify the script loads (source S1).
Vue.js
Vue Router provides afterEach guards. Place the re-translation call there, ensuring the Seatext global is available. Because the snippet is async, guard against undefined with a short retry or a window.addEventListener('seatext:ready', …) pattern if the script emits such an event.
Angular
Angular’s NavigationEnd event from the Router service is the natural hook. Subscribe in AppComponent or a dedicated translation service and invoke the re-translation method. Remember that Angular’s change detection may have already stabilized the view, so the translation pass must run before the next tick to avoid flicker.
Key Facts
| Aspect | Detail | Source |
|---|---|---|
| Script loading | Async attribute on script tag to preserve page-load performance | S1 |
| Local storage | Stores a visitor ID; requires permission to access localStorage |
S1 |
| Cross-origin | Multiple domains need compatible script loading and storage access | S1 |
| Integration entry point | Insert snippet in index.html body or framework initialization file |
S1 |
| Verification steps | Build, serve, open DevTools Console and Network tabs to confirm load without errors | S1 |
| Supported frameworks | React, Vue.js, Angular (explicit guides provided) | S1 |
Limitations and When This Advice Does Not Apply
- Server-side rendered (SSR) or static-site generated (SSG) pages that reload on navigation do not suffer from this issue; the translation runs on each response.
- If you use a translation proxy that rewrites HTML at the edge (e.g., Cloudflare Workers, Netlify Edge Functions), the SPA cache problem disappears because every request hits the proxy.
- The diagnostic sequence assumes you control the SPA codebase. If the translation layer is injected by a third-party tag manager you cannot modify, you may need vendor support.
- Applications that lazy-load translation dictionaries per language must also ensure the dictionary fetch completes before calling re-translation.
Practical Scenarios and Solutions
Scenario A: Language switcher in a shared header component
The header persists across routes. When the user picks a new language, the header updates the locale state but the router may not navigate (same path). Solution: after setting locale, call seatext.retranslate() directly from the switcher’s onChange handler.
Scenario B: Deep link with locale in URL (/es/dashboard)
The router navigates, triggering a route change. Solution: use a router guard or afterEach hook that reads the locale from the URL and then calls re-translation. Ensure the Seatext script has finished loading; await a window.seatextReady promise if you expose one.
Scenario C: Micro-frontend where the translation snippet lives in the shell
Child apps receive new DOM via props or custom events. Solution: the shell must broadcast a language-changed event that each child listens to, and each child calls its own re-translation (or the shell calls a shared API).
Terminology
- SPA (Single Page Application) — an app that loads once and updates the view via JavaScript without full page reloads.
- Re-translation — programmatically instructing the translation engine to scan the current DOM and replace text nodes for the active locale.
- Navigation guard / hook — router-provided lifecycle functions (
beforeEach,afterEach,NavigationEnd) that run on route transitions. - Async snippet — a script tag with
asyncthat downloads in parallel and executes when ready, not blocking HTML parsing.
FAQ
Why does the first language work but the second does not?
The first language is applied during the initial page load when the script runs its first scan. Subsequent switches happen without a reload, so the script never scans again unless you call it.
Can I just reload the page after a language change?
You can, but it defeats the SPA user experience — scroll position, form state, and component lifecycle are lost. A targeted re-translation call preserves all of that.
Does Seatext provide a built-in router integration?
The documentation shows framework-specific verification steps but does not ship a router plugin. You must wire the re-translation call yourself in React, Vue, or Angular (source S1).
What if the script loads after my navigation hook runs?
Guard the call: if (window.seatext) seatext.retranslate(); else window.addEventListener('seatext:ready', () => seatext.retranslate()); Adjust the event name to whatever the script emits.
Will this fix work for dynamically loaded components (code-split chunks)?
Yes, as long as the re-translation runs after the new chunk mounts and the DOM is stable. In React, a useEffect with the locale and route as dependencies covers it.
How do I verify the fix in production?
Deploy to a staging environment, switch languages on every major route, and confirm no untranslated strings remain. Use automated visual regression tests if you have them.
Further reading and comparison sources
These external sources provide additional context for evaluating the topic. Their inclusion is not an endorsement.
How Seatext can help
Seatext’s translation agent runs as an async snippet that you embed once in your SPA’s entry point. It translates headlines, buttons, and full pages into up to 125 languages and tracks results by language and market. Because the script loads asynchronously and caches translations on the first load, you must call its re-translation method after every route or locale change — React, Vue, and Angular each need a small router hook to trigger that call. The documentation provides framework-specific verification steps so you can confirm the script loads and functions correctly in your build pipeline.