From ee60d5fda3421c2e49c541ee6d37d9acf3addf67 Mon Sep 17 00:00:00 2001 From: elpatron Date: Sun, 31 May 2026 11:25:10 +0200 Subject: [PATCH] fix(dev): veralteten PWA-Cache bereinigen, damit i18n-Labels laden MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stale Service-Worker-Precache konnte Vite-Module und Locale-Bundles überlagern, sodass Kompass-Dial-Texte als Roh-i18n-Keys erschienen. Co-authored-by: Cursor --- client/src/hooks/usePwaUpdate.ts | 2 +- client/src/i18n/courseDialKeys.test.ts | 25 +++++++++++++++++++++ client/src/i18n/index.ts | 19 +++++++++++----- client/src/main.tsx | 31 +++++++++++++++++++------- client/vite.config.ts | 3 +++ 5 files changed, 65 insertions(+), 15 deletions(-) create mode 100644 client/src/i18n/courseDialKeys.test.ts diff --git a/client/src/hooks/usePwaUpdate.ts b/client/src/hooks/usePwaUpdate.ts index ec1193b..47d5ac4 100644 --- a/client/src/hooks/usePwaUpdate.ts +++ b/client/src/hooks/usePwaUpdate.ts @@ -48,7 +48,7 @@ export function usePwaUpdate() { needRefresh: [needRefresh, setNeedRefresh], updateServiceWorker } = useRegisterSW({ - immediate: true, + immediate: !import.meta.env.DEV, onNeedReload() { clearUpdateSuppression() setNeedRefresh(false) diff --git a/client/src/i18n/courseDialKeys.test.ts b/client/src/i18n/courseDialKeys.test.ts new file mode 100644 index 0000000..89080ae --- /dev/null +++ b/client/src/i18n/courseDialKeys.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest' +import deJson from './locales/de.json' +import enJson from './locales/en.json' + +const resources = { + de: { translation: deJson.translation }, + en: { translation: enJson.translation } +} + +describe('course dial i18n keys', () => { + it.each([ + 'logs.event_course_section', + 'logs.course_tab_mgk', + 'logs.course_tab_rwk', + 'logs.course_dial_hint', + 'logs.course_step_fine', + 'logs.wind_mode_cardinal' + ])('resolves %s in de and en bundles', async (key) => { + const { default: i18n } = await import('i18next') + await i18n.init({ lng: 'de', resources, defaultNS: 'translation' }) + expect(i18n.t(key)).not.toBe(key) + await i18n.changeLanguage('en') + expect(i18n.t(key)).not.toBe(key) + }) +}) diff --git a/client/src/i18n/index.ts b/client/src/i18n/index.ts index 62d306d..41a6aec 100644 --- a/client/src/i18n/index.ts +++ b/client/src/i18n/index.ts @@ -1,19 +1,26 @@ import i18n from 'i18next' import { initReactI18next } from 'react-i18next' import LanguageDetector from 'i18next-browser-languagedetector' -import enTranslation from './locales/en.json' -import deTranslation from './locales/de.json' +import enJson from './locales/en.json' +import deJson from './locales/de.json' import { initSeo } from '../utils/seo.js' +/** JSON files wrap strings in `translation` — register that namespace explicitly. */ +const resources = { + en: { translation: enJson.translation }, + de: { translation: deJson.translation } +} + i18n .use(LanguageDetector) .use(initReactI18next) .init({ - resources: { - en: enTranslation, - de: deTranslation - }, + resources, + defaultNS: 'translation', fallbackLng: 'en', + supportedLngs: ['de', 'en'], + nonExplicitSupportedLngs: true, + load: 'languageOnly', interpolation: { escapeValue: false // React already escapes values (prevents XSS) }, diff --git a/client/src/main.tsx b/client/src/main.tsx index 665bf30..0b22ae9 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -3,14 +3,29 @@ import { createRoot } from 'react-dom/client' import 'leaflet/dist/leaflet.css' import './themes.css' import './index.css' -import App from './App.tsx' -import './i18n' import { applyAppearanceToDocument } from './services/appearance.ts' -applyAppearanceToDocument() +/** Stale PWA precache on localhost can shadow Vite dev modules and old locale bundles. */ +async function clearDevServiceWorkerCaches(): Promise { + if (!import.meta.env.DEV || !('serviceWorker' in navigator)) return + const regs = await navigator.serviceWorker.getRegistrations() + await Promise.all(regs.map((r) => r.unregister())) + if ('caches' in window) { + const keys = await caches.keys() + await Promise.all(keys.map((k) => caches.delete(k))) + } +} -createRoot(document.getElementById('root')!).render( - - - , -) +async function bootstrap(): Promise { + await clearDevServiceWorkerCaches() + await import('./i18n') + const { default: App } = await import('./App.tsx') + applyAppearanceToDocument() + createRoot(document.getElementById('root')!).render( + + + , + ) +} + +void bootstrap() diff --git a/client/vite.config.ts b/client/vite.config.ts index 4058dbd..bfd71f2 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -47,6 +47,9 @@ export default defineConfig({ srcDir: 'src', filename: 'sw.ts', registerType: 'prompt', + devOptions: { + enabled: false + }, includeAssets: ['favicon.ico', 'logo.png'], injectManifest: { globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2,webmanifest}']