refactor: $app/stores → $app/state, legacy stores → runes

Codemod-driven migration of 55 .svelte files from the deprecated
$app/stores module to the rune-based $app/state ($page.x → page.x,
no auto-subscription wrapper). Two custom writable() stores converted
to .svelte.ts factory functions matching the existing theme store
pattern, with consumers updated to use .value getters and the explicit
.set() method.

UserHeader.svelte's login link now guards page.url.search behind
the browser flag — search-param access throws during prerender, and
this defensive change unblocks future prerender adoption on any page
that includes the header.
This commit is contained in:
2026-04-29 22:31:16 +02:00
parent e5d218820b
commit 3cd2a678a6
62 changed files with 255 additions and 178 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
<script lang="ts">
import { browser } from '$app/environment';
import { enhance } from '$app/forms';
import { page } from '$app/stores';
import { page } from '$app/state';
import Heart from '@lucide/svelte/icons/heart';
let { recipeId, isFavorite = $bindable(false), isLoggedIn = false } = $props<{ recipeId: string, isFavorite?: boolean, isLoggedIn?: boolean }>();
const recipeLang = $derived($page.url.pathname.split('/')[1] || 'rezepte');
const recipeLang = $derived(page.url.pathname.split('/')[1] || 'rezepte');
let isLoading = $state(false);
+9 -9
View File
@@ -1,8 +1,8 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { recipeTranslationStore } from '$lib/stores/recipeTranslation';
import { languageStore } from '$lib/stores/language';
import { page } from '$app/state';
import { recipeTranslationStore } from '$lib/stores/recipeTranslation.svelte';
import { languageStore } from '$lib/stores/language.svelte';
import { convertFitnessPath } from '$lib/js/fitnessI18n';
import { convertCospendPath } from '$lib/js/cospendI18n';
import { onMount } from 'svelte';
@@ -10,7 +10,7 @@
let { lang = undefined }: { lang?: 'de' | 'en' | 'la' } = $props();
// Use prop for display if provided (SSR-safe), otherwise fall back to store
const displayLang = $derived(lang ?? $languageStore);
const displayLang = $derived(lang ?? languageStore.value);
let currentPath = $state('');
let langButton: HTMLButtonElement;
@@ -33,14 +33,14 @@
};
// Whether the current page is a faith route (show LA option)
const faithPath = $derived(currentPath || $page.url.pathname);
const faithPath = $derived(currentPath || page.url.pathname);
const isFaithRoute = $derived(
faithPath.startsWith('/glaube') || faithPath.startsWith('/faith') || faithPath.startsWith('/fides')
);
$effect(() => {
// Update current language and path when page changes (reactive to browser navigation)
const path = $page.url.pathname;
const path = page.url.pathname;
currentPath = path;
if (path.startsWith('/recipes') || path.startsWith('/faith')) {
@@ -87,7 +87,7 @@
// Compute target paths for each language (used as href for no-JS)
function computeTargetPath(targetLang: 'de' | 'en' | 'la'): string {
const path = currentPath || $page.url.pathname;
const path = currentPath || page.url.pathname;
if (path.startsWith('/glaube') || path.startsWith('/faith') || path.startsWith('/fides')) {
return convertFaithPath(path, targetLang);
@@ -102,7 +102,7 @@
}
// Use translated recipe slugs from page data when available (works during SSR)
const pageData = $page.data;
const pageData = page.data;
if (targetLang === 'en' && path.startsWith('/rezepte')) {
if (pageData?.englishShortName) {
return `/recipes/${pageData.englishShortName}`;
@@ -171,7 +171,7 @@
}
// If we have recipe translation data from store, use the correct short names
const recipeData = $recipeTranslationStore;
const recipeData = recipeTranslationStore.value;
if (recipeData) {
if (lang === 'en' && recipeData.englishShortName) {
await goto(`/recipes/${recipeData.englishShortName}`);
+3 -3
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { page } from '$app/stores';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import ErrorView from './ErrorView.svelte';
import { getErrorTitle, getErrorDescription, errorLabels, pick } from '$lib/js/errorStrings';
@@ -18,8 +18,8 @@
let { sectionHref, sectionLabel, isEnglish: isEnglishProp, extraActions }: Props = $props();
let status = $derived($page.status);
let error = $derived($page.error as any);
let status = $derived(page.status);
let error = $derived(page.error as any);
let bibleQuote = $derived(error?.bibleQuote);
let detectedEnglish = $derived(error?.lang === 'en');
let isEnglish = $derived(isEnglishProp ?? detectedEnglish);
+4 -3
View File
@@ -1,7 +1,8 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { onMount } from "svelte";
import { page } from '$app/stores';
import { page } from '$app/state';
import { browser } from '$app/environment';
import LogIn from '@lucide/svelte/icons/log-in';
let { user, recipeLang = 'rezepte', lang = 'de' } = $props();
@@ -157,7 +158,7 @@
<li><a href={resolve('/[recipeLang=recipeLang]/administration', { recipeLang })}>Administration</a></li>
{/if}
<li><a href="https://sso.bocken.org/if/user/#/settings" >Einstellungen</a></li>
<li><a href={`${resolve('/logout')}?callbackUrl=${encodeURIComponent(getLogoutCallbackUrl($page.url.pathname))}`}>Log Out</a></li>
<li><a href={`${resolve('/logout')}?callbackUrl=${encodeURIComponent(getLogoutCallbackUrl(page.url.pathname))}`}>Log Out</a></li>
</ul>
</div>
</div>
@@ -165,7 +166,7 @@
{:else}
<a
class="entry login-link"
href={`${resolve('/login')}?callbackUrl=${encodeURIComponent($page.url.pathname + $page.url.search)}`}
href={`${resolve('/login')}?callbackUrl=${encodeURIComponent(page.url.pathname + (browser ? page.url.search : ''))}`}
aria-label={lang === 'de' ? 'Anmelden' : 'Login'}
title={lang === 'de' ? 'Anmelden' : 'Login'}
>
@@ -1,11 +1,11 @@
<script>
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { page } from '$app/state';
import ProfilePicture from './ProfilePicture.svelte';
import { formatCurrency } from '$lib/utils/formatters';
import { detectCospendLang, locale, t } from '$lib/js/cospendI18n';
const lang = $derived(detectCospendLang($page.url.pathname));
const lang = $derived(detectCospendLang(page.url.pathname));
const loc = $derived(locale(lang));
/**
@@ -1,11 +1,11 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { page } from '$app/state';
import ProfilePicture from './ProfilePicture.svelte';
import { formatCurrency as formatCurrencyUtil } from '$lib/utils/formatters';
import { detectCospendLang, locale, t } from '$lib/js/cospendI18n';
const lang = $derived(detectCospendLang($page.url.pathname));
const lang = $derived(detectCospendLang(page.url.pathname));
const loc = $derived(locale(lang));
let { initialBalance = null, initialDebtData = null } = $props<{ initialBalance?: any, initialDebtData?: any }>();
@@ -2,7 +2,7 @@
import { resolve } from '$app/paths';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { page } from '$app/state';
import ProfilePicture from './ProfilePicture.svelte';
import EditButton from '$lib/components/EditButton.svelte';
import { getCategoryEmoji } from '$lib/utils/categories';
@@ -13,9 +13,9 @@
let { paymentId, onclose, onpaymentDeleted } = $props();
// Get session from page store
let session = $derived($page.data?.session);
let session = $derived(page.data?.session);
const lang = $derived(detectCospendLang($page.url.pathname));
const lang = $derived(detectCospendLang(page.url.pathname));
const root = $derived(cospendRoot(lang));
const loc = $derived(locale(lang));
@@ -1,9 +1,9 @@
<script>
import ProfilePicture from './ProfilePicture.svelte';
import { page } from '$app/stores';
import { page } from '$app/state';
import { detectCospendLang, t } from '$lib/js/cospendI18n';
const lang = $derived(detectCospendLang($page.url.pathname));
const lang = $derived(detectCospendLang(page.url.pathname));
let {
splitMethod = $bindable('equal'),
@@ -1,12 +1,12 @@
<script>
import { resolve } from '$app/paths';
import { page } from '$app/stores';
import { page } from '$app/state';
import { getEnrichedExerciseById } from '$lib/data/exercisedb';
import { detectFitnessLang, fitnessSlugs } from '$lib/js/fitnessI18n';
let { exerciseId, plain = false } = $props();
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const exercise = $derived(getEnrichedExerciseById(exerciseId, lang));
const sl = $derived(fitnessSlugs(lang));
</script>
@@ -9,10 +9,10 @@
import PersonStanding from '@lucide/svelte/icons/person-standing';
import Shapes from '@lucide/svelte/icons/shapes';
import Weight from '@lucide/svelte/icons/weight';
import { page } from '$app/stores';
import { page } from '$app/state';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const isEn = $derived(lang === 'en');
/**
+2 -2
View File
@@ -1,6 +1,6 @@
<script>
import { resolve } from '$app/paths';
import { page } from '$app/stores';
import { page } from '$app/state';
import { browser } from '$app/environment';
import { untrack } from 'svelte';
import Heart from '@lucide/svelte/icons/heart';
@@ -50,7 +50,7 @@
initialResults = undefined,
} = $props();
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const s = $derived(fitnessSlugs(lang));
const isEn = $derived(lang === 'en');
const btnLabel = $derived(confirmLabel ?? t('log_food', lang));
@@ -1,6 +1,6 @@
<script>
import { detectFitnessLang } from '$lib/js/fitnessI18n';
import { page } from '$app/stores';
import { page } from '$app/state';
import Beef from '@lucide/svelte/icons/beef';
import Droplet from '@lucide/svelte/icons/droplet';
import Wheat from '@lucide/svelte/icons/wheat';
@@ -31,7 +31,7 @@
showDetailRows = true,
} = $props();
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const isEn = $derived(lang === 'en');
const macroPercent = $derived.by(() => {
@@ -1,5 +1,5 @@
<script>
import { page } from '$app/stores';
import { page } from '$app/state';
import { onMount } from 'svelte';
import { detectFitnessLang } from '$lib/js/fitnessI18n';
import frontSvgRaw from '$lib/assets/muscle-front.svg?raw';
@@ -13,7 +13,7 @@
/** @type {{ data?: { totals?: Record<string, MuscleTotals> } | null }} */
let { data } = $props();
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const isEn = $derived(lang === 'en');
/** @type {Record<string, MuscleTotals>} */
const totals = $derived(data?.totals ?? {});
@@ -1,6 +1,6 @@
<script>
import { resolve } from '$app/paths';
import { page } from '$app/stores';
import { page } from '$app/state';
import { getExerciseById, getExerciseMetrics } from '$lib/data/exercises';
import Clock from '@lucide/svelte/icons/clock';
import Weight from '@lucide/svelte/icons/weight';
@@ -10,7 +10,7 @@
import Flame from '@lucide/svelte/icons/flame';
import { detectFitnessLang, fitnessSlugs } from '$lib/js/fitnessI18n';
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const sl = $derived(fitnessSlugs(lang));
/**
+2 -2
View File
@@ -5,10 +5,10 @@
import Square from '@lucide/svelte/icons/square';
import { METRIC_LABELS } from '$lib/data/exercises';
import RestTimer from './RestTimer.svelte';
import { page } from '$app/stores';
import { page } from '$app/state';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
/**
* @type {{
@@ -2,10 +2,10 @@
import { getExerciseById } from '$lib/data/exercises';
import EllipsisVertical from '@lucide/svelte/icons/ellipsis-vertical';
import MapPin from '@lucide/svelte/icons/map-pin';
import { page } from '$app/stores';
import { page } from '$app/state';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
/**
* @type {{
+2 -2
View File
@@ -4,10 +4,10 @@ import Play from '@lucide/svelte/icons/play';
import Pause from '@lucide/svelte/icons/pause';
import ChevronRight from '@lucide/svelte/icons/chevron-right';
import SyncIndicator from '$lib/components/fitness/SyncIndicator.svelte';
import { page } from '$app/stores';
import { page } from '$app/state';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
let { href, elapsed = '0:00', paused = false, syncStatus = 'idle', onPauseToggle,
restSeconds = 0, restTotal = 0, onRestAdjust = null, onRestSkip = null } = $props();
@@ -1,7 +1,7 @@
<script>
import { browser } from '$app/environment';
import { enhance } from '$app/forms';
import { page } from '$app/stores';
import { page } from '$app/state';
let { item, multiplier = 1, yeastId = 0, lang = 'de' } = $props();
@@ -11,7 +11,7 @@
: 'Zwischen Frischhefe und Trockenhefe wechseln');
// Get all current URL parameters to preserve state
const currentParams = $derived(browser ? new URLSearchParams(window.location.search) : $page.url.searchParams);
const currentParams = $derived(browser ? new URLSearchParams(window.location.search) : page.url.searchParams);
/** @param {Event} event */
function toggleHefe(event) {
@@ -2,7 +2,7 @@
import { onMount } from 'svelte';
import { onNavigate } from "$app/navigation";
import { browser } from '$app/environment';
import { page } from '$app/stores';
import { page } from '$app/state';
import HefeSwapper from './HefeSwapper.svelte';
import NutritionSummary from './NutritionSummary.svelte';
import AddToFoodLogButton from './AddToFoodLogButton.svelte';
@@ -272,7 +272,7 @@ const yeastIds = $derived.by(() => {
});
// Get all current URL parameters to preserve state in multiplier forms
const currentParams = $derived(browser ? new URLSearchParams(window.location.search) : $page.url.searchParams);
const currentParams = $derived(browser ? new URLSearchParams(window.location.search) : page.url.searchParams);
// Progressive enhancement - use JS if available
onMount(() => {
+12
View File
@@ -0,0 +1,12 @@
export type Language = 'de' | 'en';
function createLanguage() {
let value = $state<Language>('de');
return {
get value() { return value; },
set: (v: Language) => { value = v; }
};
}
export const languageStore = createLanguage();
-27
View File
@@ -1,27 +0,0 @@
import { writable } from 'svelte/store';
type Language = 'de' | 'en';
function createLanguageStore() {
const { subscribe, set } = writable<Language>('de');
return {
subscribe,
set,
init: () => {
if (typeof window !== 'undefined') {
const path = window.location.pathname;
if (path.startsWith('/recipes') || path.startsWith('/faith')) {
set('en');
} else if (path.startsWith('/rezepte') || path.startsWith('/glaube')) {
set('de');
} else {
const preferredLanguage = localStorage.getItem('preferredLanguage');
set(preferredLanguage === 'en' ? 'en' : 'de');
}
}
}
};
}
export const languageStore = createLanguageStore();
@@ -0,0 +1,16 @@
export interface RecipeTranslationData {
germanShortName: string;
englishShortName?: string;
hasEnglishTranslation: boolean;
}
function createRecipeTranslation() {
let value = $state<RecipeTranslationData | null>(null);
return {
get value() { return value; },
set: (v: RecipeTranslationData | null) => { value = v; }
};
}
export const recipeTranslationStore = createRecipeTranslation();
-9
View File
@@ -1,9 +0,0 @@
import { writable } from 'svelte/store';
interface RecipeTranslationData {
germanShortName: string;
englishShortName?: string;
hasEnglishTranslation: boolean;
}
export const recipeTranslationStore = writable<RecipeTranslationData | null>(null);