/** * Zentrale Übersetzungen (i18n-ähnlich). * Quelle: CMS translation_bundle (API, Middleware setzt Astro.locals.translations). * Wird in Layout geladen und über TranslationProvider/Svelte-Context bereitgestellt. * * Wichtig für client:load-Inseln: useTranslate() hat dort keinen Context und beim SSR kein window, * daher fehlen Übersetzungen (es erscheint der Key). Lösung: Übersetzungen als Prop von der * Astro-Seite übergeben und t(translations, key, replacements) nutzen. Siehe .cursor/rules/translations-i18n.mdc. */ import { getContext } from "svelte"; export type Translations = Record; /** 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; /** 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}}}`; }); } /** Liest Übersetzungen aus window oder aus script#astro-translations (Fallback für Inseln). */ function getTranslationsFromWindow(): Translations | null { if (typeof window === "undefined") return null; const w = window as unknown as { __TRANSLATIONS__?: Translations }; if (w.__TRANSLATIONS__ && typeof w.__TRANSLATIONS__ === "object") return w.__TRANSLATIONS__; try { const el = document.getElementById("astro-translations"); const json = el?.getAttribute("data-translations") ?? (el as HTMLScriptElement | null)?.textContent?.trim(); if (json) { const parsed = JSON.parse(json) as Translations; w.__TRANSLATIONS__ = parsed; return parsed; } } catch { /* ignore */ } return null; } /** * t-Funktion aus Svelte-Context oder aus window.__TRANSLATIONS__. * Nur zuverlässig innerhalb des TranslationProvider-Baums. Für client:load-Inseln: * Übersetzungen als Prop übergeben und statische t(translations, key) nutzen (siehe Regel translations-i18n). */ export function useTranslate(): TFunction { const fromContext = getContext(T_CONTEXT_KEY); if (fromContext) return fromContext; const warnedKeys = new Set(); return (key: string, replacements?: TranslationReplacements): string => { const translations = getTranslationsFromWindow(); const raw = translations?.[key] ?? key; if ( typeof import.meta.env !== "undefined" && import.meta.env.DEV && raw === key && key.length > 0 && !warnedKeys.has(key) ) { warnedKeys.add(key); console.warn( `[translations] Key "${key}" in Insel nicht gefunden (kein Context/window). ` + `Übersetzungen als Prop von der Astro-Seite übergeben und t(translations, key) nutzen. Siehe .cursor/rules/translations-i18n.mdc`, ); } return replacePlaceholders(raw, 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", ] 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 }; /** * Platzhalter im CMS-Text: {{name}} wird durch t(key, { name: "Wert" }) ersetzt. * Beispiel im CMS: "Hallo {{user}}, du hast {{count}} Einträge." * Aufruf: t(T.xyz, { user: "Max", count: 5 }) */ /** * 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). * Nutzbar in Astro und in Svelte-Inseln: Übersetzungen von der Astro-Seite als Prop übergeben, * dann in der Komponente z. B. `t(translations ?? null, T.xy, { count: 5 })` (siehe BlogOverview.svelte). */ 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); }