faith: add bilingual routes /glaube ↔ /faith

Add language toggle support for faith pages similar to recipes.
Routes now work in both German and English:
- /glaube ↔ /faith
- /glaube/gebete ↔ /faith/prayers
- /glaube/rosenkranz ↔ /faith/rosary
- /glaube/angelus ↔ /faith/angelus
This commit is contained in:
2026-02-02 16:15:47 +01:00
parent 87d5e9cbc0
commit 1a5117e8d0
16 changed files with 322 additions and 131 deletions

View File

@@ -9,14 +9,20 @@
let langButton: HTMLButtonElement;
let langOptions: HTMLDivElement;
// Faith subroute mappings
const faithSubroutes: Record<string, Record<string, string>> = {
en: { gebete: 'prayers', rosenkranz: 'rosary', angelus: 'angelus' },
de: { prayers: 'gebete', rosary: 'rosenkranz', angelus: 'angelus' }
};
$effect(() => {
// Update current language and path when page changes (reactive to browser navigation)
const path = $page.url.pathname;
currentPath = path;
if (path.startsWith('/recipes')) {
if (path.startsWith('/recipes') || path.startsWith('/faith')) {
languageStore.set('en');
} else if (path.startsWith('/rezepte')) {
} else if (path.startsWith('/rezepte') || path.startsWith('/glaube')) {
languageStore.set('de');
} else if (path === '/') {
// On main page, read from localStorage
@@ -33,6 +39,24 @@
}
}
function convertFaithPath(path: string, targetLang: 'de' | 'en'): string {
// Extract the current base and subroute
const faithMatch = path.match(/^\/(glaube|faith)(\/(.+))?$/);
if (!faithMatch) return path;
const targetBase = targetLang === 'en' ? 'faith' : 'glaube';
const subroute = faithMatch[3]; // e.g., "gebete", "rosenkranz", "angelus"
if (!subroute) {
// Main faith page
return `/${targetBase}`;
}
// Convert subroute
const convertedSubroute = faithSubroutes[targetLang][subroute] || subroute;
return `/${targetBase}/${convertedSubroute}`;
}
async function switchLanguage(lang: 'de' | 'en') {
// Update the shared language store immediately
languageStore.set(lang);
@@ -51,6 +75,13 @@
return;
}
// Handle faith pages
if (path.startsWith('/glaube') || path.startsWith('/faith')) {
const newPath = convertFaithPath(path, lang);
await goto(newPath);
return;
}
// If we have recipe translation data from store, use the correct short names
const recipeData = $recipeTranslationStore;
if (recipeData) {
@@ -76,7 +107,7 @@
} else if (lang === 'de' && path.startsWith('/recipes')) {
newPath = path.replace('/recipes', '/rezepte');
} else if (!path.startsWith('/rezepte') && !path.startsWith('/recipes')) {
// On other pages (glaube, cospend, etc), go to recipe home
// On other pages (cospend, etc), go to recipe home
newPath = lang === 'en' ? '/recipes' : '/rezepte';
}

5
src/params/faithLang.ts Normal file
View File

@@ -0,0 +1,5 @@
import type { ParamMatcher } from '@sveltejs/kit';
export const match: ParamMatcher = (param) => {
return param === 'faith' || param === 'glaube';
};

View File

@@ -0,0 +1,5 @@
import type { ParamMatcher } from '@sveltejs/kit';
export const match: ParamMatcher = (param) => {
return param === 'prayers' || param === 'gebete';
};

5
src/params/rosaryLang.ts Normal file
View File

@@ -0,0 +1,5 @@
import type { ParamMatcher } from '@sveltejs/kit';
export const match: ParamMatcher = (param) => {
return param === 'rosary' || param === 'rosenkranz';
};

View File

@@ -6,6 +6,7 @@
let lang = $state<'de' | 'en'>('de');
let recipesUrl = $state('/rezepte');
let faithUrl = $state('/glaube');
onMount(() => {
// Check localStorage for preferred language
@@ -13,15 +14,18 @@
if (preferredLanguage === 'en') {
lang = 'en';
recipesUrl = '/recipes';
faithUrl = '/faith';
} else {
lang = 'de';
recipesUrl = '/rezepte';
faithUrl = '/glaube';
}
// Listen for language changes from UserHeader
const handleLanguageChange = (e: CustomEvent) => {
lang = e.detail.lang;
recipesUrl = lang === 'en' ? '/recipes' : '/rezepte';
faithUrl = lang === 'en' ? '/faith' : '/glaube';
};
window.addEventListener('languagechange', handleLanguageChange as EventListener);
@@ -181,7 +185,7 @@ section h2{
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M335.5 4l288 160c15.4 8.6 21 28.1 12.4 43.5s-28.1 21-43.5 12.4L320 68.6 47.5 220c-15.4 8.6-34.9 3-43.5-12.4s-3-34.9 12.4-43.5L304.5 4c9.7-5.4 21.4-5.4 31.1 0zM320 160a40 40 0 1 1 0 80 40 40 0 1 1 0-80zM144 256a40 40 0 1 1 0 80 40 40 0 1 1 0-80zm312 40a40 40 0 1 1 80 0 40 40 0 1 1 -80 0zM226.9 491.4L200 441.5V480c0 17.7-14.3 32-32 32H120c-17.7 0-32-14.3-32-32V441.5L61.1 491.4c-6.3 11.7-20.8 16-32.5 9.8s-16-20.8-9.8-32.5l37.9-70.3c15.3-28.5 45.1-46.3 77.5-46.3h19.5c16.3 0 31.9 4.5 45.4 12.6l33.6-62.3c15.3-28.5 45.1-46.3 77.5-46.3h19.5c32.4 0 62.1 17.8 77.5 46.3l33.6 62.3c13.5-8.1 29.1-12.6 45.4-12.6h19.5c32.4 0 62.1 17.8 77.5 46.3l37.9 70.3c6.3 11.7 1.9 26.2-9.8 32.5s-26.2 1.9-32.5-9.8L552 441.5V480c0 17.7-14.3 32-32 32H472c-17.7 0-32-14.3-32-32V441.5l-26.9 49.9c-6.3 11.7-20.8 16-32.5 9.8s-16-20.8-9.8-32.5l36.3-67.5c-1.7-1.7-3.2-3.6-4.3-5.8L376 345.5V400c0 17.7-14.3 32-32 32H296c-17.7 0-32-14.3-32-32V345.5l-26.9 49.9c-1.2 2.2-2.6 4.1-4.3 5.8l36.3 67.5c6.3 11.7 1.9 26.2-9.8 32.5s-26.2 1.9-32.5-9.8z"/></svg>
<h3>{labels.familyTree}</h3>
</a>
<a href=glaube>
<a href={faithUrl}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M351.2 4.8c3.2-2 6.6-3.3 10-4.1c4.7-1 9.6-.9 14.1 .1c7.7 1.8 14.8 6.5 19.4 13.6L514.6 194.2c8.8 13.1 13.4 28.6 13.4 44.4v73.5c0 6.9 4.4 13 10.9 15.2l79.2 26.4C631.2 358 640 370.2 640 384v96c0 9.9-4.6 19.3-12.5 25.4s-18.1 8.1-27.7 5.5L431 465.9c-56-14.9-95-65.7-95-123.7V224c0-17.7 14.3-32 32-32s32 14.3 32 32v80c0 8.8 7.2 16 16 16s16-7.2 16-16V219.1c0-7-1.8-13.8-5.3-19.8L340.3 48.1c-1.7-3-2.9-6.1-3.6-9.3c-1-4.7-1-9.6 .1-14.1c1.9-8 6.8-15.2 14.3-19.9zm-62.4 0c7.5 4.6 12.4 11.9 14.3 19.9c1.1 4.6 1.2 9.4 .1 14.1c-.7 3.2-1.9 6.3-3.6 9.3L213.3 199.3c-3.5 6-5.3 12.9-5.3 19.8V304c0 8.8 7.2 16 16 16s16-7.2 16-16V224c0-17.7 14.3-32 32-32s32 14.3 32 32V342.3c0 58-39 108.7-95 123.7l-168.7 45c-9.6 2.6-19.9 .5-27.7-5.5S0 490 0 480V384c0-13.8 8.8-26 21.9-30.4l79.2-26.4c6.5-2.2 10.9-8.3 10.9-15.2V238.5c0-15.8 4.7-31.2 13.4-44.4L245.2 14.5c4.6-7.1 11.7-11.8 19.4-13.6c4.6-1.1 9.4-1.2 14.1-.1c3.5 .8 6.9 2.1 10 4.1z"/></svg>
<h3>{labels.faith}</h3>
</a>

View File

@@ -0,0 +1,17 @@
import type { LayoutServerLoad } from "./$types"
import { error } from "@sveltejs/kit";
export const load : LayoutServerLoad = async ({locals, params}) => {
// Validate faithLang parameter
if (params.faithLang !== 'glaube' && params.faithLang !== 'faith') {
throw error(404, 'Not found');
}
const lang = params.faithLang === 'faith' ? 'en' : 'de';
return {
session: await locals.auth(),
lang,
faithLang: params.faithLang
}
};

View File

@@ -0,0 +1,47 @@
<script>
import { page } from '$app/stores';
import Header from '$lib/components/Header.svelte'
import UserHeader from '$lib/components/UserHeader.svelte';
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
let { data, children } = $props();
const isEnglish = $derived(data.lang === 'en');
const prayersPath = $derived(isEnglish ? 'prayers' : 'gebete');
const rosaryPath = $derived(isEnglish ? 'rosary' : 'rosenkranz');
const labels = $derived({
prayers: isEnglish ? 'Prayers' : 'Gebete',
rosary: isEnglish ? 'Rosary' : 'Rosenkranz'
});
function isActive(path) {
const currentPath = $page.url.pathname;
// Check if current path starts with the link path
return currentPath.startsWith(path);
}
</script>
<svelte:head>
<link rel="preload" href="/fonts/crosses.woff2" as="font" type="font/woff2" crossorigin>
</svelte:head>
<Header>
{#snippet links()}
<ul class=site_header>
<li><a href="/{data.faithLang}/{prayersPath}" class:active={isActive(`/${data.faithLang}/${prayersPath}`)}>{labels.prayers}</a></li>
<li><a href="/{data.faithLang}/{rosaryPath}" class:active={isActive(`/${data.faithLang}/${rosaryPath}`)}>{labels.rosary}</a></li>
</ul>
{/snippet}
{#snippet language_selector_mobile()}
<LanguageSelector />
{/snippet}
{#snippet language_selector_desktop()}
<LanguageSelector />
{/snippet}
{#snippet right_side()}
<UserHeader user={data.session?.user}></UserHeader>
{/snippet}
{@render children()}
</Header>

View File

@@ -1,10 +1,24 @@
<script>
import LinksGrid from '$lib/components/LinksGrid.svelte';
let { data } = $props();
const isEnglish = $derived(data.lang === 'en');
const prayersPath = $derived(isEnglish ? 'prayers' : 'gebete');
const rosaryPath = $derived(isEnglish ? 'rosary' : 'rosenkranz');
const labels = $derived({
title: isEnglish ? 'Faith' : 'Glaube',
description: isEnglish
? 'Here you will find some prayers and an interactive rosary for the Catholic faith. A focus on Latin and the Tridentine rite will be noticeable.'
: 'Hier findet man einige Gebete und einen interaktiven Rosenkranz zum katholischen Glauben. Ein Fokus auf Latein und den tridentinischen Ritus wird zu bemerken sein.',
prayers: isEnglish ? 'Prayers' : 'Gebete',
rosary: isEnglish ? 'Rosary' : 'Rosenkranz'
});
</script>
<svelte:head>
<title>Glaube - Bocken</title>
<meta name="description" content="Gebete und ein interaktiver Rosenkranz zum katholischen Glauben." />
<title>{labels.title} - Bocken</title>
<meta name="description" content={labels.description} />
</svelte:head>
<style>
h1{
@@ -19,18 +33,17 @@
</style>
<h1>Glaube</h1>
<h1>{labels.title}</h1>
<p>
Hier findet man einige Gebete und einen interaktiven Rosenkranz zum katholischen Glauben.
Ein Fokus auf Latein und den tridentinischen Ritus wird zu bemerken sein.
{labels.description}
</p>
<LinksGrid>
<a href="/glaube/gebete">
<a href="/{data.faithLang}/{prayersPath}">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M351.2 4.8c3.2-2 6.6-3.3 10-4.1c4.7-1 9.6-.9 14.1 .1c7.7 1.8 14.8 6.5 19.4 13.6L514.6 194.2c8.8 13.1 13.4 28.6 13.4 44.4v73.5c0 6.9 4.4 13 10.9 15.2l79.2 26.4C631.2 358 640 370.2 640 384v96c0 9.9-4.6 19.3-12.5 25.4s-18.1 8.1-27.7 5.5L431 465.9c-56-14.9-95-65.7-95-123.7V224c0-17.7 14.3-32 32-32s32 14.3 32 32v80c0 8.8 7.2 16 16 16s16-7.2 16-16V219.1c0-7-1.8-13.8-5.3-19.8L340.3 48.1c-1.7-3-2.9-6.1-3.6-9.3c-1-4.7-1-9.6 .1-14.1c1.9-8 6.8-15.2 14.3-19.9zm-62.4 0c7.5 4.6 12.4 11.9 14.3 19.9c1.1 4.6 1.2 9.4 .1 14.1c-.7 3.2-1.9 6.3-3.6 9.3L213.3 199.3c-3.5 6-5.3 12.9-5.3 19.8V304c0 8.8 7.2 16 16 16s16-7.2 16-16V224c0-17.7 14.3-32 32-32s32 14.3 32 32V342.3c0 58-39 108.7-95 123.7l-168.7 45c-9.6 2.6-19.9 .5-27.7-5.5S0 490 0 480V384c0-13.8 8.8-26 21.9-30.4l79.2-26.4c6.5-2.2 10.9-8.3 10.9-15.2V238.5c0-15.8 4.7-31.2 13.4-44.4L245.2 14.5c4.6-7.1 11.7-11.8 19.4-13.6c4.6-1.1 9.4-1.2 14.1-.1c3.5 .8 6.9 2.1 10 4.1z"/></svg>
<h3>Gebete</h3>
<h3>{labels.prayers}</h3>
</a>
<a href=/glaube/rosenkranz >
<a href="/{data.faithLang}/{rosaryPath}">
<svg viewBox="0 0 512 512">
<g>
<path class="st0" d="M241.251,145.056c-39.203-17.423-91.472,17.423-104.54,60.982c-13.068,43.558,8.712,117.608,65.337,143.742
@@ -56,6 +69,6 @@
C251.417,184.249,273.196,208.938,257.227,213.294z"/>
</g>
</svg>
<h3>Rosenkranz</h3>
<h3>{labels.rosary}</h3>
</a>
</LinksGrid>

View File

@@ -0,0 +1,182 @@
<script>
import { createLanguageContext } from "$lib/contexts/languageContext.js";
import LanguageToggle from "$lib/components/LanguageToggle.svelte";
import Prayer from '$lib/components/prayers/Prayer.svelte';
import Angelus from "$lib/components/prayers/Angelus.svelte";
import AveMaria from '$lib/components/prayers/AveMaria.svelte';
import "$lib/css/christ.css";
import "$lib/css/rosenkranz.css";
// Create language context for prayer components
createLanguageContext();
</script>
<svelte:head>
<title>Angelus - The Angel of the Lord</title>
<meta name="description" content="Pray the Angelus prayer in Latin, German, and English" />
</svelte:head>
<div class="angelus-page">
<div class="header">
<h1>Angelus</h1>
<div class="controls">
<LanguageToggle />
</div>
</div>
<div class="prayers-content">
<div class="prayer-section">
<Prayer>
<!-- First Versicle and Response -->
<p>
<v lang="la"><i>℣.</i> Angelus Domini nuntiavit Mariae.</v>
<v lang="de"><i>℣.</i> Der Engel des Herrn brachte Maria die Botschaft</v>
<v lang="en"><i>℣.</i> The Angel of the Lord declared unto Mary.</v>
<v lang="la"><i>℟.</i> Et concepit de Spiritu Sancto.</v>
<v lang="de"><i>℟.</i> und sie empfing vom Heiligen Geist.</v>
<v lang="en"><i>℟.</i> And she conceived of the Holy Spirit.</v>
</p>
</Prayer>
</div>
</div>
<div class="prayer-content">
<AveMaria />
</div>
<div class="prayer-content">
<Prayer>
<!-- Second Versicle and Response -->
<p>
<v lang="la"><i>℣.</i> Ecce ancilla Domini,</v>
<v lang="de"><i>℣.</i> Maria sprach: Siehe, ich bin die Magd des Herrn</v>
<v lang="en"><i>℣.</i> Behold the handmaid of the Lord.</v>
<v lang="la"><i>℟.</i> Fiat mihi secundum verbum tuum.</v>
<v lang="de"><i>℟.</i> mir geschehe nach Deinem Wort.</v>
<v lang="en"><i>℟.</i> Be it done unto me according to thy word.</v>
</p>
</Prayer>
</div>
<div class="prayer-content">
<AveMaria />
</div>
<div class="prayer-content">
<Prayer>
<!-- Third Versicle and Response -->
<p>
<v lang="la"><i>℣.</i> Et Verbum caro factum est,</v>
<v lang="de"><i>℣.</i> Und das Wort ist Fleisch geworden</v>
<v lang="en"><i>℣.</i> And the Word was made flesh.</v>
<v lang="la"><i>℟.</i> Et habitavit in nobis.</v>
<v lang="de"><i>℟.</i> und hat unter uns gewohnt.</v>
<v lang="en"><i>℟.</i> And dwelt among us.</v>
</p>
</Prayer>
</div>
<div class="prayer-content">
<Prayer>
<!-- Fourth Versicle and Response -->
<p>
<v lang="la"><i>℣.</i> Ora pro nobis, sancta Dei Genetrix,</v>
<v lang="de"><i>℣.</i> Bitte für uns Heilige Gottesmutter</v>
<v lang="en"><i>℣.</i> Pray for us, O holy Mother of God.</v>
<v lang="la"><i>℟.</i> Ut digni efficiamur promissionibus Christi.</v>
<v lang="de"><i>℟.</i> auf dass wir würdig werden der Verheißungen Christi.</v>
<v lang="en"><i>℟.</i> That we may be made worthy of the promises of Christ.</v>
</p>
</Prayer>
</div>
<div class="prayer-content">
<Prayer>
<!-- Closing Prayer -->
<p>
<v lang="la"><i>℣.</i> Oremus.</v>
<v lang="de"><i>℣.</i> Lasset uns beten.</v>
<v lang="en"><i>℣.</i> Let us pray:</v>
</p>
<p>
<v lang="la">
Gratiam tuam, quaesumus, Domine, mentibus nostris infunde; ut qui, Angelo nuntiante,
Christi Filii tui incarnationem cognovimus, per passionem eius et crucem ad
resurrectionis gloriam perducamur. Per eumdem Christum Dominum nostrum. Amen.
</v>
<v lang="de">
Allmächtiger Gott, gieße deine Gnade in unsere Herzen ein. Durch die Botschaft des
Engels haben wir die Menschwerdung Christi, deines Sohnes, erkannt. Lass uns durch
sein Leiden und Kreuz zur Herrlichkeit der Auferstehung gelangen. Darum bitten wir
durch Christus, unseren Herrn. Amen.
</v>
<v lang="en">
Pour forth, we beseech Thee, O Lord, Thy grace into our hearts, that we to whom the
Incarnation of Christ Thy Son was made known by the message of an angel, may by His
Passion and Cross be brought to the glory of His Resurrection. Through the same Christ
Our Lord. Amen.
</v>
</p>
</Prayer>
</div>
</div>
<style>
.angelus-page {
max-width: 800px;
margin: 0 auto;
padding: 2rem 1rem;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
flex-wrap: wrap;
gap: 1rem;
}
h1 {
color: var(--nord6);
margin: 0;
}
@media (prefers-color-scheme: light) {
h1 {
color: black;
}
}
.controls {
display: flex;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
}
@media (max-width: 600px) {
.header {
flex-direction: column;
align-items: flex-start;
}
.prayer-content {
padding: 1rem;
}
}
.prayer-section {
scroll-snap-align: start;
padding: 2rem;
margin-bottom: 2rem;
background: var(--accent-dark);
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
position: relative;
}
.prayers-content {
scroll-snap-type: y proximity;
max-width: 700px;
}
</style>

View File

@@ -1,7 +0,0 @@
import type { LayoutServerLoad } from "./$types"
export const load : LayoutServerLoad = (async ({locals}) => {
return {
session: await locals.auth(),
}
});

View File

@@ -1,29 +0,0 @@
<script>
import { page } from '$app/stores';
import Header from '$lib/components/Header.svelte'
import UserHeader from '$lib/components/UserHeader.svelte';
let { data, children } = $props();
function isActive(path) {
const currentPath = $page.url.pathname;
// Check if current path starts with the link path
return currentPath.startsWith(path);
}
</script>
<svelte:head>
<link rel="preload" href="/fonts/crosses.woff2" as="font" type="font/woff2" crossorigin>
</svelte:head>
<Header>
{#snippet links()}
<ul class=site_header>
<li><a href="/glaube/gebete" class:active={isActive('/glaube/gebete')}>Gebete</a></li>
<li><a href="/glaube/rosenkranz" class:active={isActive('/glaube/rosenkranz')}>Rosenkranz</a></li>
</ul>
{/snippet}
{#snippet right_side()}
<UserHeader user={data.session?.user}></UserHeader>
{/snippet}
{@render children()}
</Header>

View File

@@ -1,82 +0,0 @@
<script>
import { createLanguageContext } from "$lib/contexts/languageContext.js";
import LanguageToggle from "$lib/components/LanguageToggle.svelte";
import LanguageSelector from "$lib/components/LanguageSelector.svelte";
import Angelus from "$lib/components/prayers/Angelus.svelte";
import "$lib/css/christ.css";
// Create language context for prayer components
createLanguageContext();
</script>
<svelte:head>
<title>Angelus - The Angel of the Lord</title>
<meta name="description" content="Pray the Angelus prayer in Latin, German, and English" />
</svelte:head>
<div class="angelus-page">
<div class="header">
<h1>Angelus</h1>
<div class="controls">
<LanguageSelector />
<LanguageToggle />
</div>
</div>
<div class="prayer-content">
<Angelus />
</div>
</div>
<style>
.angelus-page {
max-width: 800px;
margin: 0 auto;
padding: 2rem 1rem;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
flex-wrap: wrap;
gap: 1rem;
}
h1 {
color: var(--nord6);
margin: 0;
}
@media (prefers-color-scheme: light) {
h1 {
color: black;
}
}
.controls {
display: flex;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
}
.prayer-content {
background-color: var(--accent-dark);
padding: 2rem;
border-radius: 0.5rem;
box-shadow: 0 0 1em rgba(0, 0, 0, 0.5);
}
@media (max-width: 600px) {
.header {
flex-direction: column;
align-items: flex-start;
}
.prayer-content {
padding: 1rem;
}
}
</style>