feat: i18n

This commit is contained in:
2026-01-20 20:56:18 +01:00
parent 0f13d67f10
commit a31f28c834
17 changed files with 1390 additions and 55 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "https://inlang.com/schema/message-format-plugin",
"hello_world": "Hallo Welt",
"pfadi": "Pfadi"
}
+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "https://inlang.com/schema/message-format-plugin",
"hello_world": "Hello World",
"pfadi": "Scouts"
}
+2
View File
@@ -27,6 +27,8 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@fontsource/outfit": "^5.0.13", "@fontsource/outfit": "^5.0.13",
"@inlang/paraglide-js": "^1.11.0",
"@inlang/paraglide-sveltekit": "^0.16.1",
"@ts-stack/markdown": "^1.5.0", "@ts-stack/markdown": "^1.5.0",
"mime-types": "^3.0.2", "mime-types": "^3.0.2",
"svelte-preprocess": "^6.0.2", "svelte-preprocess": "^6.0.2",
+1216 -5
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
{
"$schema": "https://inlang.com/schema/project-settings",
"sourceLanguageTag": "de",
"languageTags": [
"de",
"en"
],
"modules": [
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@latest/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js"
],
"plugin.inlang.messageFormat": {
"pathPattern": "./messages/{languageTag}.json"
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="%paraglide.lang%" dir="%paraglide.textDirection%">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" /> <link rel="icon" href="%sveltekit.assets%/favicon.png" />
+3
View File
@@ -0,0 +1,3 @@
import { i18n } from "$lib/i18n"
export const handle = i18n.handle()
+11
View File
@@ -0,0 +1,11 @@
import { createI18n } from "@inlang/paraglide-sveltekit"
import * as runtime from "$lib/paraglide/runtime"
export const i18n = createI18n(runtime, {
pathnames: {
"/about": {
en: "/about",
de: "/ueber-uns"
}
}
})
+28 -3
View File
@@ -54,8 +54,8 @@ export async function getCoverImage(slug: string): Promise<string | null> {
}); });
} }
export async function getPages() { export async function getPages(lang: string = 'de') {
return withCache("pages", async () => { return withCache(`pages:${lang}`, async () => {
try { try {
const items = await client.getDirectoryContents("/"); const items = await client.getDirectoryContents("/");
@@ -71,7 +71,32 @@ export async function getPages() {
const pages = await Promise.all( const pages = await Promise.all(
directories.map(async (item: any) => { directories.map(async (item: any) => {
const slug = item.basename; const slug = item.basename;
const title = slug.charAt(0).toUpperCase() + slug.slice(1); const baseName = slug.charAt(0).toUpperCase() + slug.slice(1);
const filename = lang === 'de' ? `${baseName}.md` : `${baseName}.${lang}.md`;
const path = `/${slug}/${filename}`;
let title = baseName;
try {
const content = await getPageContent(path);
const headerMatch = content.match(/^#{1,6}\s+(.+)$/m);
if (headerMatch) {
title = headerMatch[1];
}
} catch (e) {
// Fallback to German if English is missing
if (lang !== 'de') {
try {
const fallbackContent = await getPageContent(`/${slug}/${baseName}.md`);
const headerMatch = fallbackContent.match(/^#{1,6}\s+(.+)$/m);
if (headerMatch) {
title = headerMatch[1];
}
} catch (innerE) {
console.warn(`Could not fetch fallback title for ${slug}`);
}
}
}
const coverImage = await getCoverImage(slug); const coverImage = await getCoverImage(slug);
return { return {
+5
View File
@@ -0,0 +1,5 @@
import type { ParamMatcher } from '@sveltejs/kit';
export const match: ParamMatcher = (param) => {
return ['de', 'en'].includes(param);
};
+2 -2
View File
@@ -1,8 +1,8 @@
import type { LayoutServerLoad } from './$types'; import type { LayoutServerLoad } from './$types';
import { getPages } from '$lib/server/nextcloud'; import { getPages } from '$lib/server/nextcloud';
export const load: LayoutServerLoad = async () => { export const load: LayoutServerLoad = async ({ params }) => {
const pages = await getPages(); const pages = await getPages(params.lang);
return { return {
pages pages
}; };
+24 -1
View File
@@ -1,11 +1,34 @@
<script lang="ts"> <script lang="ts">
import "../app.css"; import "../app.css";
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import { ParaglideJS } from '@inlang/paraglide-sveltekit';
import { i18n } from '$lib/i18n';
import { languageTag } from '$lib/paraglide/runtime';
let { children }: { children?: Snippet } = $props(); let { children }: { children?: Snippet } = $props();
</script> </script>
<header class="fixed z-50 mix-blend-difference p-1"> <ParaglideJS {i18n}>
<header class="fixed z-50 mix-blend-difference p-1 w-full flex justify-between items-start">
<a href="/" class="text-white text-2xl">schreifuchs.ch</a> <a href="/" class="text-white text-2xl">schreifuchs.ch</a>
<nav class="flex gap-2 p-2">
<a
href="/"
hreflang="de"
data-no-translate
class="text-white hover:underline {languageTag() === 'de' ? 'font-bold' : ''}"
>
DE
</a>
<a
href="/en"
hreflang="en"
data-no-translate
class="text-white hover:underline {languageTag() === 'en' ? 'font-bold' : ''}"
>
EN
</a>
</nav>
</header> </header>
{@render children?.()} {@render children?.()}
</ParaglideJS>
@@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
import ImageLinkTile from "$lib/components/ImageLinkTile.svelte"; import ImageLinkTile from "$lib/components/ImageLinkTile.svelte";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import { i18n } from "$lib/i18n";
import * as m from "$lib/paraglide/messages";
// Import local images to preserve existing design // Import local images to preserve existing design
import mouse from "$lib/images/home/mouse.webp"; import mouse from "$lib/images/home/mouse.webp";
@@ -8,7 +10,6 @@
import scouts from "$lib/images/home/scouts.webp"; import scouts from "$lib/images/home/scouts.webp";
import console_image from "$lib/images/home/console.webp"; import console_image from "$lib/images/home/console.webp";
import lauch from "$lib/images/home/lauch.webp"; import lauch from "$lib/images/home/lauch.webp";
import monitor from "$lib/images/informatik/monitor.webp"; // Using monitor for Informatik if console is not preferred
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
@@ -42,7 +43,7 @@
<!-- Static/External Links --> <!-- Static/External Links -->
<ImageLinkTile src={scouts} href="https://pfadifrisco.ch/" target="_blank"> <ImageLinkTile src={scouts} href="https://pfadifrisco.ch/" target="_blank">
<h2 class="text-3xl font-bold text-white hover:underline text-shadow-lg"> <h2 class="text-3xl font-bold text-white hover:underline text-shadow-lg">
Pfadi {m.pfadi()}
</h2> </h2>
</ImageLinkTile> </ImageLinkTile>
</main> </main>
@@ -0,0 +1,49 @@
import { getCoverImage, getPageContent } from "$lib/server/nextcloud";
import type { PageServerLoad } from "./$types";
import { error } from "@sveltejs/kit";
export const load: PageServerLoad = async ({ params }) => {
const { slug, lang } = params;
const language = lang || 'de';
// Capitalize the first letter to match the convention: fotos -> Fotos.md
const baseName = slug.charAt(0).toUpperCase() + slug.slice(1);
const filename = language === 'de' ? `${baseName}.md` : `${baseName}.${language}.md`;
const path = `/${slug}/${filename}`;
try {
let rawContent;
try {
rawContent = await getPageContent(path);
} catch (e) {
if (language !== 'de') {
console.warn(`Localized content ${filename} not found, falling back to German.`);
rawContent = await getPageContent(`/${slug}/${baseName}.md`);
} else {
throw e;
}
}
// Extract the first header (e.g., # Title or ## Title)
const headerMatch = rawContent.match(/^#{1,6}\s+(.+)$/m);
const title = headerMatch ? headerMatch[1] : baseName;
// Remove the first header from content so it's not rendered twice
const content = headerMatch
? rawContent.replace(headerMatch[0], "").trim()
: rawContent;
const coverImage = await getCoverImage(slug);
return {
content,
title,
slug,
coverImage,
lang: language
};
} catch (e) {
console.error(`Error fetching ${filename} from Nextcloud`, e);
throw error(404, "Page not found");
}
};
-27
View File
@@ -1,27 +0,0 @@
import { getCoverImage, getPageContent } from "$lib/server/nextcloud";
import type { PageServerLoad } from "./$types";
import { error } from "@sveltejs/kit";
export const load: PageServerLoad = async ({ params }) => {
const { slug } = params;
// Capitalize the first letter to match the convention: fotos -> Fotos.md
const filename = slug.charAt(0).toUpperCase() + slug.slice(1) + ".md";
const path = `/${slug}/${filename}`;
try {
const [content, coverImage] = await Promise.all([
getPageContent(path),
getCoverImage(slug)
]);
return {
content,
title: slug.charAt(0).toUpperCase() + slug.slice(1),
slug,
coverImage
};
} catch (e) {
console.error(`Error fetching ${filename} from Nextcloud`, e);
throw error(404, "Page not found");
}
};
+8 -1
View File
@@ -1,6 +1,13 @@
import { paraglide } from '@inlang/paraglide-sveltekit/vite';
import { sveltekit } from '@sveltejs/kit/vite'; import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
export default defineConfig({ export default defineConfig({
plugins: [sveltekit()] plugins: [
paraglide({
project: './project.inlang',
outdir: './src/lib/paraglide'
}),
sveltekit()
]
}); });