Update project configuration and enhance translation support. Added caching mechanism for CMS responses with a new script, updated .gitignore to include cache files, and introduced translation handling in various components. Enhanced package.json with new scripts for cache warming and deployment. Added new translation-related components and improved existing ones for better localization support.
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* 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<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}}}`;
|
||||
});
|
||||
}
|
||||
|
||||
/** 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<TFunction | undefined>(T_CONTEXT_KEY);
|
||||
if (fromContext) return fromContext;
|
||||
const warnedKeys = new Set<string>();
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user