i18n: paraglide setup mirroring played/plaza, de unprefixed + /en/ prefixed
- project.inlang/settings.json with locales [de, en], baseLocale de.
- messages/{de,en}.json hold the strings (tagline, blog labels, etc.).
- vite.config.ts: paraglideVitePlugin compiles to src/paraglide/
(gitignored) on dev/build. Strategy: url → cookie →
preferredLanguage → baseLocale. URL pattern keeps German at /:path
(unprefixed) and English at /en/:path so existing URLs stay valid.
- server.tsx wraps the handler in paraglideMiddleware so AsyncLocalStorage
carries the per-request locale into SSR.
- router.tsx adds rewrite { input: deLocalizeUrl, output: localizeUrl }
so route matching stays locale-agnostic — the router sees /blog while
URLs show /en/blog.
- cms.ts narrows getLocale()'s union back to the de|en pair the CMS
supports, used as the default for find/findGlobal calls.
- Components/routes switch to m.foo() for user-visible strings; date
formatting picks de-DE / en-GB from getLocale().
- Root html lang reads getLocale() so the document language flips per
request.
Known minor: TanStack Start's { title } meta entry still serves the base
locale value (og:title and the description meta are localized correctly).
Will track separately.
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import type { Post } from "@/lib/cms";
|
||||
|
||||
const dateFmt = new Intl.DateTimeFormat("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
});
|
||||
import { getLocale } from "@/paraglide/runtime";
|
||||
|
||||
export default function PostCard({ post }: { post: Post }) {
|
||||
const locale = getLocale();
|
||||
const dateFmt = new Intl.DateTimeFormat(locale === "en" ? "en-GB" : "de-DE", {
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
return (
|
||||
<Link
|
||||
to="/blog/$slug"
|
||||
|
||||
+19
-13
@@ -3,6 +3,7 @@
|
||||
|
||||
import { PayloadSDK } from "@payloadcms/sdk";
|
||||
import type { Config, Page, Post } from "@unom/cms/payload-types";
|
||||
import { getLocale, type Locale } from "@/paraglide/runtime";
|
||||
|
||||
const CMS_URL = "https://cms.unom.io";
|
||||
|
||||
@@ -10,52 +11,57 @@ export const cmsClient = new PayloadSDK<Config>({
|
||||
baseURL: `${CMS_URL}/api`,
|
||||
});
|
||||
|
||||
// The CMS only stores content for the locales declared in its localization
|
||||
// config; getLocale() on the website can return any locale paraglide knows
|
||||
// about. Narrow to the set the CMS supports.
|
||||
function cmsLocale(locale?: Locale): "de" | "en" {
|
||||
const l = locale ?? getLocale();
|
||||
return l === "en" ? "en" : "de";
|
||||
}
|
||||
|
||||
export async function findPageBySlug(
|
||||
slug: string,
|
||||
locale: "de" | "en" = "de",
|
||||
locale?: Locale,
|
||||
): Promise<Page | null> {
|
||||
const res = await cmsClient.find({
|
||||
collection: "pages",
|
||||
where: { slug: { equals: slug } },
|
||||
limit: 1,
|
||||
locale,
|
||||
locale: cmsLocale(locale),
|
||||
depth: 1,
|
||||
});
|
||||
return res.docs[0] ?? null;
|
||||
}
|
||||
|
||||
export async function findPosts(
|
||||
locale: "de" | "en" = "de",
|
||||
limit = 50,
|
||||
) {
|
||||
export async function findPosts(locale?: Locale, limit = 50) {
|
||||
return cmsClient.find({
|
||||
collection: "posts",
|
||||
limit,
|
||||
sort: "-createdAt",
|
||||
locale,
|
||||
locale: cmsLocale(locale),
|
||||
});
|
||||
}
|
||||
|
||||
export async function findPostBySlug(
|
||||
slug: string,
|
||||
locale: "de" | "en" = "de",
|
||||
locale?: Locale,
|
||||
): Promise<Post | null> {
|
||||
const res = await cmsClient.find({
|
||||
collection: "posts",
|
||||
where: { slug: { equals: slug } },
|
||||
limit: 1,
|
||||
locale,
|
||||
locale: cmsLocale(locale),
|
||||
depth: 1,
|
||||
});
|
||||
return res.docs[0] ?? null;
|
||||
}
|
||||
|
||||
export async function findFooter(locale: "de" | "en" = "de") {
|
||||
return cmsClient.findGlobal({ slug: "footer", locale });
|
||||
export async function findFooter(locale?: Locale) {
|
||||
return cmsClient.findGlobal({ slug: "footer", locale: cmsLocale(locale) });
|
||||
}
|
||||
|
||||
export async function findHeader(locale: "de" | "en" = "de") {
|
||||
return cmsClient.findGlobal({ slug: "header", locale });
|
||||
export async function findHeader(locale?: Locale) {
|
||||
return cmsClient.findGlobal({ slug: "header", locale: cmsLocale(locale) });
|
||||
}
|
||||
|
||||
export type { Page, Post, Footer, Header } from "@unom/cms/payload-types";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
|
||||
import { deLocalizeUrl, localizeUrl } from "./paraglide/runtime.js";
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
|
||||
export function getRouter() {
|
||||
@@ -6,6 +7,13 @@ export function getRouter() {
|
||||
routeTree,
|
||||
scrollRestoration: true,
|
||||
defaultPreload: "intent",
|
||||
// Keep route matching locale-agnostic: the router sees `/blog`, not
|
||||
// `/en/blog`. On every navigation paraglide writes the locale prefix
|
||||
// back into the URL according to the current locale.
|
||||
rewrite: {
|
||||
input: ({ url }) => deLocalizeUrl(url),
|
||||
output: ({ url }) => localizeUrl(url),
|
||||
},
|
||||
});
|
||||
|
||||
return router;
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
import "@fontsource/inter";
|
||||
import Layout from "@/components/Layout";
|
||||
import { findFooter, type Footer } from "@/lib/cms";
|
||||
import { m } from "@/paraglide/messages";
|
||||
import { getLocale } from "@/paraglide/runtime";
|
||||
import appCss from "../styles/globals.css?url";
|
||||
|
||||
const SITE_URL = "https://unom.io";
|
||||
@@ -26,13 +28,10 @@ export const Route = createRootRoute({
|
||||
meta: [
|
||||
{ charSet: "utf-8" },
|
||||
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
||||
{ name: "description", content: "Kreative Webentwicklung aus Rottweil" },
|
||||
{ title: "unom - Kreative Webentwicklung" },
|
||||
{ property: "og:title", content: "unom - Kreative Webentwicklung" },
|
||||
{
|
||||
property: "og:description",
|
||||
content: "Kreative Webentwicklung aus Rottweil",
|
||||
},
|
||||
{ name: "description", content: m.site_description() },
|
||||
{ title: m.home_title() },
|
||||
{ property: "og:title", content: m.home_title() },
|
||||
{ property: "og:description", content: m.site_description() },
|
||||
{ property: "og:url", content: SITE_URL },
|
||||
{ property: "og:type", content: "website" },
|
||||
],
|
||||
@@ -53,7 +52,7 @@ export const Route = createRootRoute({
|
||||
|
||||
function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="de">
|
||||
<html lang={getLocale()} suppressHydrationWarning>
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import PostCard from "@/components/PostCard";
|
||||
import Section from "@/components/Section";
|
||||
import { findPosts } from "@/lib/cms";
|
||||
import { m } from "@/paraglide/messages";
|
||||
|
||||
export const Route = createFileRoute("/blog/")({
|
||||
loader: async () => {
|
||||
@@ -11,11 +12,8 @@ export const Route = createFileRoute("/blog/")({
|
||||
component: BlogIndex,
|
||||
head: () => ({
|
||||
meta: [
|
||||
{ title: "unom - Blog" },
|
||||
{
|
||||
name: "description",
|
||||
content: "Notizen, Gedanken und Tutorials aus der Werkstatt von unom.",
|
||||
},
|
||||
{ title: `${m.site_name()} - ${m.blog_title()}` },
|
||||
{ name: "description", content: m.blog_meta_description() },
|
||||
],
|
||||
}),
|
||||
});
|
||||
@@ -25,9 +23,9 @@ function BlogIndex() {
|
||||
|
||||
return (
|
||||
<Section className="pt-height-header">
|
||||
<h1>Blog</h1>
|
||||
<h1>{m.blog_title()}</h1>
|
||||
{posts.length === 0 ? (
|
||||
<p>Noch keine Beiträge.</p>
|
||||
<p>{m.blog_empty()}</p>
|
||||
) : (
|
||||
<div className="grid gap-6 md:grid-cols-2 mt-6">
|
||||
{posts.map((post) => (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import LogoQuadBG from "@/components/LogoQuadBG";
|
||||
import bgDark from "@/assets/unom_Logo_5_Dark.webp";
|
||||
import { m } from "@/paraglide/messages";
|
||||
|
||||
export default function Landing() {
|
||||
return (
|
||||
@@ -11,7 +12,6 @@ export default function Landing() {
|
||||
height={2160}
|
||||
alt="Ein 3D Rendering des unom Logos"
|
||||
/>
|
||||
{/* dim layer so the tagline sits on a calmer field */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-neutral/0 via-neutral/0 to-neutral/80" />
|
||||
|
||||
<div className="relative h-full flex flex-col items-center justify-center gap-10 px-section-main-x text-center">
|
||||
@@ -19,11 +19,10 @@ export default function Landing() {
|
||||
<LogoQuadBG />
|
||||
</div>
|
||||
<p className="max-w-md text-lg md:text-xl text-main/90">
|
||||
Kreative Webentwicklung aus Rottweil.
|
||||
{m.site_tagline()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* subtle scroll hint */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute bottom-6 left-1/2 -translate-x-1/2 text-secondary/60 text-sm tracking-wide"
|
||||
|
||||
+5
-1
@@ -1,7 +1,11 @@
|
||||
import handler from "@tanstack/react-start/server-entry";
|
||||
import { paraglideMiddleware } from "./paraglide/server.js";
|
||||
|
||||
export default {
|
||||
async fetch(req: Request): Promise<Response> {
|
||||
return handler.fetch(req);
|
||||
// paraglide reads the URL prefix / cookie / Accept-Language and binds the
|
||||
// detected locale into AsyncLocalStorage for the duration of the request,
|
||||
// so any `getLocale()` calls during SSR see the right value.
|
||||
return paraglideMiddleware(req, () => handler.fetch(req));
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user