Files
windwiderstand.de/src/lib/translations.ts
T

187 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
}