Initial commit: Astro 6 static blog site
All checks were successful
Deploy / deploy (push) Successful in 49s
All checks were successful
Deploy / deploy (push) Successful in 49s
- German (default) and English i18n support - Categories and tags - Blog posts with hero images - Dark/light theme switcher - View Transitions removed to fix reload ghost images - Webmentions integration - RSS feeds per locale Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
commit
5bb63bacf5
95 changed files with 12199 additions and 0 deletions
134
src/i18n/ui.ts
Normal file
134
src/i18n/ui.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import { DEFAULT_LOCALE, type Locale, LOCALES } from '~/consts';
|
||||
|
||||
export const ui = {
|
||||
de: {
|
||||
'nav.home': 'Start',
|
||||
'nav.about': 'Über mich',
|
||||
'nav.categories': 'Kategorien',
|
||||
'nav.tags': 'Schlagwörter',
|
||||
'post.lastUpdated': 'Zuletzt aktualisiert am',
|
||||
'post.category': 'Kategorie',
|
||||
'post.tags': 'Schlagwörter',
|
||||
'post.translationAvailable': 'Dieser Beitrag ist auch auf Englisch verfügbar:',
|
||||
'post.translationLink': 'Englische Version lesen',
|
||||
'categories.title': 'Kategorien',
|
||||
'categories.description': 'Alle Kategorien im Überblick.',
|
||||
'category.postsIn': 'Beiträge in',
|
||||
'category.noPosts': 'Noch keine Beiträge in dieser Kategorie.',
|
||||
'tags.title': 'Schlagwörter',
|
||||
'tags.description': 'Alle Schlagwörter im Überblick.',
|
||||
'tag.postsTagged': 'Beiträge mit',
|
||||
'tag.noPosts': 'Noch keine Beiträge mit diesem Stichwort.',
|
||||
'footer.contact': 'Kontakt',
|
||||
'footer.imprint': 'Impressum',
|
||||
'footer.privacy': 'Datenschutz',
|
||||
'webmentions.title': 'Reaktionen',
|
||||
'webmentions.like': 'Like',
|
||||
'webmentions.likes': 'Likes',
|
||||
'webmentions.repost': 'Repost',
|
||||
'webmentions.reposts': 'Reposts',
|
||||
'webmentions.replies': 'Antworten',
|
||||
'webmentions.mentions': 'Erwähnungen',
|
||||
'lang.de': 'Deutsch',
|
||||
'lang.en': 'English',
|
||||
},
|
||||
en: {
|
||||
'nav.home': 'Home',
|
||||
'nav.about': 'About',
|
||||
'nav.categories': 'Categories',
|
||||
'nav.tags': 'Tags',
|
||||
'post.lastUpdated': 'Last updated on',
|
||||
'post.category': 'Category',
|
||||
'post.tags': 'Tags',
|
||||
'post.translationAvailable': 'This post is also available in German:',
|
||||
'post.translationLink': 'Read the German version',
|
||||
'categories.title': 'Categories',
|
||||
'categories.description': 'All categories at a glance.',
|
||||
'category.postsIn': 'Posts in',
|
||||
'category.noPosts': 'No posts in this category yet.',
|
||||
'tags.title': 'Tags',
|
||||
'tags.description': 'All tags at a glance.',
|
||||
'tag.postsTagged': 'Posts tagged',
|
||||
'tag.noPosts': 'No posts with this tag yet.',
|
||||
'footer.contact': 'Contact',
|
||||
'footer.imprint': 'Imprint',
|
||||
'footer.privacy': 'Privacy',
|
||||
'webmentions.title': 'Reactions',
|
||||
'webmentions.like': 'Like',
|
||||
'webmentions.likes': 'Likes',
|
||||
'webmentions.repost': 'Repost',
|
||||
'webmentions.reposts': 'Reposts',
|
||||
'webmentions.replies': 'Replies',
|
||||
'webmentions.mentions': 'Mentions',
|
||||
'lang.de': 'Deutsch',
|
||||
'lang.en': 'English',
|
||||
},
|
||||
} as const satisfies Record<Locale, Record<string, string>>;
|
||||
|
||||
export type UIKey = keyof (typeof ui)['de'];
|
||||
|
||||
export function t(locale: Locale, key: UIKey): string {
|
||||
return ui[locale][key];
|
||||
}
|
||||
|
||||
export function isLocale(value: string | undefined): value is Locale {
|
||||
return !!value && (LOCALES as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
export function getLocaleFromUrl(url: URL): Locale {
|
||||
const seg = url.pathname.split('/').filter(Boolean)[0];
|
||||
return isLocale(seg) ? seg : DEFAULT_LOCALE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a URL for a route within a given locale. `path` is the route without
|
||||
* any language prefix, e.g. "/" or "/about".
|
||||
*/
|
||||
export function localizePath(path: string, locale: Locale): string {
|
||||
const normalized = path.startsWith('/') ? path : `/${path}`;
|
||||
if (locale === DEFAULT_LOCALE) return normalized;
|
||||
if (normalized === '/') return `/${locale}/`;
|
||||
return `/${locale}${normalized}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Segments whose URL slug differs per locale. The first segment of any
|
||||
* non-prefixed pathname is translated through this map when switching.
|
||||
*/
|
||||
const LOCALIZED_SEGMENTS: Record<Locale, Record<string, string>> = {
|
||||
de: {
|
||||
category: 'kategorie',
|
||||
categories: 'kategorien',
|
||||
about: 'ueber-mich',
|
||||
tag: 'schlagwort',
|
||||
tags: 'schlagwoerter',
|
||||
contact: 'kontakt',
|
||||
imprint: 'impressum',
|
||||
'privacy-policy': 'datenschutz',
|
||||
},
|
||||
en: {
|
||||
kategorie: 'category',
|
||||
kategorien: 'categories',
|
||||
'ueber-mich': 'about',
|
||||
schlagwort: 'tag',
|
||||
schlagwoerter: 'tags',
|
||||
kontakt: 'contact',
|
||||
impressum: 'imprint',
|
||||
datenschutz: 'privacy-policy',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Swap the locale of the current pathname, preserving the rest of the route
|
||||
* and translating known per-locale URL segments (e.g. `kategorie` ↔ `category`).
|
||||
*/
|
||||
export function switchLocalePath(pathname: string, target: Locale): string {
|
||||
const parts = pathname.split('/').filter(Boolean);
|
||||
if (parts.length > 0 && isLocale(parts[0])) parts.shift();
|
||||
if (parts.length > 0) {
|
||||
const translated = LOCALIZED_SEGMENTS[target][parts[0]];
|
||||
if (translated) parts[0] = translated;
|
||||
}
|
||||
const rest = parts.length ? `/${parts.join('/')}` : '/';
|
||||
return localizePath(rest === '/' ? '/' : rest, target);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue