Initial SvelteKit frontend port of windwiderstand.de
Deploy / verify (push) Failing after 32s
Deploy / deploy (push) Has been skipped

Full parity with Astro site: content rows, post/tag routes, pagination,
event badges + OSM map, comments, Live-Search via /api/search-index,
CMS image proxy, RSS, sitemap.

Deploy: Dockerfile + docker-compose.prod.yml + Gitea Actions workflow
to build + push to git.pm86.de registry and ssh-deploy to Contabo.
This commit is contained in:
Peter Meier
2026-04-17 22:01:30 +02:00
parent 852cef0980
commit 2fef91a548
137 changed files with 28585 additions and 0 deletions
+153
View File
@@ -0,0 +1,153 @@
/**
* Zentrale Übersetzungen (i18n-ähnlich).
* Quelle: CMS translation_bundle (API, hooks.server.ts setzt event.locals.translations).
* Wird in +layout.server.ts geladen und über TranslationProvider/Svelte-Context bereitgestellt.
*/
import { getContext } from "svelte";
export type Translations = Record<string, string>;
/** Context-Key für die t-Funktion (von TranslationProvider gesetzt). */
export const T_CONTEXT_KEY = Symbol("translations.t");
/** Platzhalter für Ersetzung: z. B. { name: "Max" } ersetzt {{name}} im Übersetzungstext. */
export type TranslationReplacements = Record<string, string | number>;
/** t(key) oder t(key, { placeholder: value }) mit optionalen Ersetzungen für {{placeholder}}. */
export type TFunction = (
key: string,
replacements?: TranslationReplacements,
) => string;
/**
* Ersetzt {{name}} im Text durch replacements[name]; fehlende Keys bleiben als {{name}}.
*/
export function replacePlaceholders(
text: string,
replacements?: TranslationReplacements | null,
): string {
if (!replacements || typeof text !== "string") return text;
return text.replace(/\{\{(\w+)\}\}/g, (_, name: string) => {
const val = replacements[name];
return val !== undefined && val !== null ? String(val) : `{{${name}}}`;
});
}
/**
* t-Funktion aus Svelte-Context holen.
* Zuverlässig innerhalb des TranslationProvider-Baums.
*/
export function useTranslate(): TFunction {
const fromContext = getContext<TFunction | undefined>(T_CONTEXT_KEY);
if (fromContext) return fromContext;
return (key: string, replacements?: TranslationReplacements): string => {
return replacePlaceholders(key, replacements);
};
}
/** Bekannte Übersetzungs-Keys nur hier eintragen, Nutzung über T.xy (Autocomplete, keine Tippfehler). */
const TRANSLATION_KEYS = [
"searchable_text_help",
"searchable_text_help_aria",
"searchable_text_placeholder",
"searchable_text_search_aria",
"searchable_text_clear_search",
"searchable_text_tag_all",
"searchable_text_no_results",
"searchable_text_count_all",
"searchable_text_count_filtered",
"searchable_text_copy_aria",
"searchable_text_copy_button",
"searchable_text_copied",
"searchable_text_untitled",
"footer_copyright",
"nav_start",
"nav_posts",
"nav_home",
"header_search_open",
"header_search_close",
"header_menu_open",
"header_menu_close",
"header_nav_aria",
"header_overlay_close",
"header_social_aria",
"site_name_fallback",
"pagination_prev",
"pagination_next",
"blog_tag_all",
"blog_filter_aria",
"blog_count",
"page_404_title",
"page_404_text",
"page_404_back",
"posts_title",
"posts_subtitle",
"posts_tag_title",
"posts_tag_subtitle",
"posts_cms_error",
"iframe_activate_aria",
"iframe_missing",
"image_gallery_swipe_aria",
"image_gallery_prev_aria",
"image_gallery_next_aria",
"image_gallery_position_aria",
"image_gallery_slide_aria",
"image_gallery_empty",
"youtube_missing",
"youtube_title_fallback",
"calendar_prev_month",
"calendar_next_month",
"calendar_weekday_mo",
"calendar_weekday_di",
"calendar_weekday_mi",
"calendar_weekday_do",
"calendar_weekday_fr",
"calendar_weekday_sa",
"calendar_weekday_so",
"calendar_more_info",
"calendar_no_events",
"calendar_time_suffix",
"calendar_select_day",
"calendar_next_events",
"calendar_show_more",
"calendar_show_less",
"post_overview_all",
"blog_search_label",
"blog_search_placeholder",
"blog_search_clear",
"blog_year_label",
"blog_year_all",
"blog_upcoming_events",
"blog_results_count",
"blog_results_for",
"blog_results_in_year",
"blog_filter_reset",
"blog_no_results",
"blog_rss_subscribe",
"post_map",
"post_map_activate",
"post_map_open_osm",
"post_comments",
] as const;
export type TranslationKey = (typeof TRANSLATION_KEYS)[number];
/** Keys als Objekt: T.searchable_text_help → "searchable_text_help" (für t(T.xy)). */
export const T: { [K in TranslationKey]: K } = Object.fromEntries(
TRANSLATION_KEYS.map((k) => [k, k]),
) as { [K in TranslationKey]: K };
/**
* Gibt den übersetzten Text für key zurück, optional mit Ersetzung von {{placeholder}}.
* Fallback: key selbst (damit fehlende Einträge im CMS erkennbar sind).
*/
export function t(
translations: Translations | null | undefined,
key: string,
replacements?: TranslationReplacements,
): string {
if (!key) return key;
const raw = translations?.[key] ?? key;
return replacePlaceholders(raw, replacements);
}