i18n(comments): all UI strings through translations
Pulls every German string out of Comments.svelte, PostCard.svelte and CommentCountBadge.svelte and routes it through `useTranslate()` + the `T.*` key registry. New keys (`comments_*`, `post_overview` count aria, `comments_jump_to`) are registered in `lib/translations.ts`. Default DE values land in `cms_content/windwiderstand/de/translation_bundle/app.json5` (committed separately) so the CMS-loaded bundle keeps the same look without each component falling back to its raw key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,9 @@
|
|||||||
import Icon from "@iconify/svelte";
|
import Icon from "@iconify/svelte";
|
||||||
import "$lib/iconify-offline";
|
import "$lib/iconify-offline";
|
||||||
import { env } from "$env/dynamic/public";
|
import { env } from "$env/dynamic/public";
|
||||||
|
import { useTranslate, T } from "$lib/translations";
|
||||||
|
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
let {
|
let {
|
||||||
pageId,
|
pageId,
|
||||||
@@ -54,7 +57,7 @@
|
|||||||
<a
|
<a
|
||||||
href="#{targetId}"
|
href="#{targetId}"
|
||||||
onclick={onClick}
|
onclick={onClick}
|
||||||
aria-label="Zu Kommentaren springen"
|
aria-label={t(T.comments_jump_to)}
|
||||||
class="no-underline! inline-flex items-center gap-1 text-xs font-medium transition-colors {cls}"
|
class="no-underline! inline-flex items-center gap-1 text-xs font-medium transition-colors {cls}"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:comment-outline" class="size-4" />
|
<Icon icon="mdi:comment-outline" class="size-4" />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import Icon from "@iconify/svelte";
|
import Icon from "@iconify/svelte";
|
||||||
import "$lib/iconify-offline";
|
import "$lib/iconify-offline";
|
||||||
import { env } from "$env/dynamic/public";
|
import { env } from "$env/dynamic/public";
|
||||||
|
import { useTranslate, T } from "$lib/translations";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
pageId,
|
pageId,
|
||||||
@@ -15,6 +16,8 @@
|
|||||||
bodyMax?: number;
|
bodyMax?: number;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
type Status = "approved" | "pending" | "spam";
|
type Status = "approved" | "pending" | "spam";
|
||||||
|
|
||||||
type PublicComment = {
|
type PublicComment = {
|
||||||
@@ -187,7 +190,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteComment(id: string) {
|
async function deleteComment(id: string) {
|
||||||
if (!confirm("Kommentar löschen?")) return;
|
if (!confirm(t(T.comments_confirm_delete))) return;
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`${apiUrl()}`.replace(`/${pageId}`, `/${pageId}/${id}`), {
|
const r = await fetch(`${apiUrl()}`.replace(`/${pageId}`, `/${pageId}/${id}`), {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
@@ -310,12 +313,12 @@
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="rounded bg-zinc-800 px-3 py-1 font-medium text-white hover:bg-zinc-700"
|
class="rounded bg-zinc-800 px-3 py-1 font-medium text-white hover:bg-zinc-700"
|
||||||
>Speichern</button>
|
>{t(T.comments_save)}</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={cancelEdit}
|
onclick={cancelEdit}
|
||||||
class="text-zinc-500 hover:text-zinc-900"
|
class="text-zinc-500 hover:text-zinc-900"
|
||||||
>Abbrechen</button>
|
>{t(T.comments_cancel)}</button>
|
||||||
<span class="ml-auto text-zinc-400 tabular-nums">{editBody.length} / {bodyMax}</span>
|
<span class="ml-auto text-zinc-400 tabular-nums">{editBody.length} / {bodyMax}</span>
|
||||||
</div>
|
</div>
|
||||||
{#if editError}<p class="text-red-700 text-xs mt-1">{editError}</p>{/if}
|
{#if editError}<p class="text-red-700 text-xs mt-1">{editError}</p>{/if}
|
||||||
@@ -330,7 +333,7 @@
|
|||||||
class="text-xs text-zinc-600 hover:text-zinc-900 inline-flex items-center gap-1"
|
class="text-xs text-zinc-600 hover:text-zinc-900 inline-flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:pencil-outline" class="size-3" />
|
<Icon icon="mdi:pencil-outline" class="size-3" />
|
||||||
Bearbeiten
|
{t(T.comments_edit)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -338,7 +341,7 @@
|
|||||||
class="text-xs text-zinc-600 hover:text-red-700 inline-flex items-center gap-1"
|
class="text-xs text-zinc-600 hover:text-red-700 inline-flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:trash-can-outline" class="size-3" />
|
<Icon icon="mdi:trash-can-outline" class="size-3" />
|
||||||
Löschen
|
{t(T.comments_delete)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -349,7 +352,7 @@
|
|||||||
<div class="flex flex-wrap items-baseline gap-2 text-sm">
|
<div class="flex flex-wrap items-baseline gap-2 text-sm">
|
||||||
<strong class="font-medium text-zinc-900">{c.author}</strong>
|
<strong class="font-medium text-zinc-900">{c.author}</strong>
|
||||||
{#if c.can_edit}
|
{#if c.can_edit}
|
||||||
<span class="text-[10px] uppercase tracking-wide text-zinc-400">du</span>
|
<span class="text-[10px] uppercase tracking-wide text-zinc-400">{t(T.comments_self_marker)}</span>
|
||||||
{/if}
|
{/if}
|
||||||
<time
|
<time
|
||||||
class="text-zinc-500 text-xs"
|
class="text-zinc-500 text-xs"
|
||||||
@@ -357,12 +360,12 @@
|
|||||||
title={fmtAbs(c.created_at)}
|
title={fmtAbs(c.created_at)}
|
||||||
>{fmtRel(c.created_at)}</time>
|
>{fmtRel(c.created_at)}</time>
|
||||||
{#if c.updated_at}
|
{#if c.updated_at}
|
||||||
<span class="text-zinc-400 text-xs" title={fmtAbs(c.updated_at)}>(bearbeitet)</span>
|
<span class="text-zinc-400 text-xs" title={fmtAbs(c.updated_at)}>{t(T.comments_edited)}</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if c.status === "pending"}
|
{#if c.status === "pending"}
|
||||||
<span class="ml-auto inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-amber-800">
|
<span class="ml-auto inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-amber-800">
|
||||||
<Icon icon="mdi:clock-outline" class="size-3" />
|
<Icon icon="mdi:clock-outline" class="size-3" />
|
||||||
wartet
|
{t(T.comments_pending_short)}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -370,7 +373,7 @@
|
|||||||
|
|
||||||
<div class="comments {cls}">
|
<div class="comments {cls}">
|
||||||
{#if loading && comments.length === 0}
|
{#if loading && comments.length === 0}
|
||||||
<ul class="space-y-4 list-none p-0" aria-busy="true" aria-label="Lade Kommentare">
|
<ul class="space-y-4 list-none p-0" aria-busy="true" aria-label={t(T.comments_loading)}>
|
||||||
{#each Array(2) as _, i (i)}
|
{#each Array(2) as _, i (i)}
|
||||||
<li class="flex gap-3 list-none!">
|
<li class="flex gap-3 list-none!">
|
||||||
<div class="size-9 shrink-0 rounded-full bg-zinc-200 animate-pulse"></div>
|
<div class="size-9 shrink-0 rounded-full bg-zinc-200 animate-pulse"></div>
|
||||||
@@ -386,20 +389,20 @@
|
|||||||
<div class="flex items-start gap-2 rounded border border-red-200 bg-red-50 p-3 text-sm text-red-800">
|
<div class="flex items-start gap-2 rounded border border-red-200 bg-red-50 p-3 text-sm text-red-800">
|
||||||
<Icon icon="mdi:alert-circle-outline" class="size-5 shrink-0 mt-0.5" />
|
<Icon icon="mdi:alert-circle-outline" class="size-5 shrink-0 mt-0.5" />
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="font-medium">Kommentare konnten nicht geladen werden.</p>
|
<p class="font-medium">{t(T.comments_load_error_title)}</p>
|
||||||
<p class="mt-1 text-xs text-red-700">{loadError}</p>
|
<p class="mt-1 text-xs text-red-700">{loadError}</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={loadComments}
|
onclick={loadComments}
|
||||||
class="text-xs text-red-800 underline hover:no-underline"
|
class="text-xs text-red-800 underline hover:no-underline"
|
||||||
>Erneut versuchen</button>
|
>{t(T.comments_retry)}</button>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#if topLevel.length === 0}
|
{#if topLevel.length === 0}
|
||||||
<div class="flex flex-col items-center gap-2 py-8 text-center">
|
<div class="flex flex-col items-center gap-2 py-8 text-center">
|
||||||
<Icon icon="mdi:comment-text-outline" class="size-10 text-zinc-300" />
|
<Icon icon="mdi:comment-text-outline" class="size-10 text-zinc-300" />
|
||||||
<p class="text-sm text-zinc-500">Noch keine Kommentare. Sei der erste.</p>
|
<p class="text-sm text-zinc-500">{t(T.comments_empty)}</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<ul class="space-y-5 mb-8 list-none p-0">
|
<ul class="space-y-5 mb-8 list-none p-0">
|
||||||
@@ -423,10 +426,12 @@
|
|||||||
>
|
>
|
||||||
<Icon icon="mdi:reply" class="size-3" />
|
<Icon icon="mdi:reply" class="size-3" />
|
||||||
{replyTo === c.id
|
{replyTo === c.id
|
||||||
? "Abbrechen"
|
? t(T.comments_cancel)
|
||||||
: replies.length > 0
|
: replies.length === 0
|
||||||
? `${replies.length} ${replies.length === 1 ? 'Antwort' : 'Antworten'}`
|
? t(T.comments_reply)
|
||||||
: "Antworten"}
|
: replies.length === 1
|
||||||
|
? t(T.comments_reply_count_one)
|
||||||
|
: t(T.comments_reply_count_other, { count: replies.length })}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -435,7 +440,7 @@
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={replyAuthor}
|
bind:value={replyAuthor}
|
||||||
placeholder="Dein Name"
|
placeholder={t(T.comments_name_placeholder)}
|
||||||
required
|
required
|
||||||
maxlength="80"
|
maxlength="80"
|
||||||
class="w-full p-2 border border-zinc-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-zinc-400"
|
class="w-full p-2 border border-zinc-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-zinc-400"
|
||||||
@@ -444,7 +449,7 @@
|
|||||||
bind:value={replyBody}
|
bind:value={replyBody}
|
||||||
use:autosize
|
use:autosize
|
||||||
onkeydown={submitOnCmdEnter}
|
onkeydown={submitOnCmdEnter}
|
||||||
placeholder="Deine Antwort"
|
placeholder={t(T.comments_reply_placeholder)}
|
||||||
required
|
required
|
||||||
rows="2"
|
rows="2"
|
||||||
maxlength={bodyMax}
|
maxlength={bodyMax}
|
||||||
@@ -456,7 +461,7 @@
|
|||||||
type="submit"
|
type="submit"
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
class="px-3 py-1.5 bg-zinc-800 text-white rounded text-sm font-medium hover:bg-zinc-700 disabled:opacity-50"
|
class="px-3 py-1.5 bg-zinc-800 text-white rounded text-sm font-medium hover:bg-zinc-700 disabled:opacity-50"
|
||||||
>Antwort senden</button>
|
>{t(T.comments_reply_send)}</button>
|
||||||
<span class="ml-auto text-xs text-zinc-400 tabular-nums">{replyBody.length} / {bodyMax}</span>
|
<span class="ml-auto text-xs text-zinc-400 tabular-nums">{replyBody.length} / {bodyMax}</span>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -486,11 +491,11 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<form onsubmit={onSubmit} class="border-t border-zinc-200 pt-5 space-y-3">
|
<form onsubmit={onSubmit} class="border-t border-zinc-200 pt-5 space-y-3">
|
||||||
<h3 class="font-medium text-sm text-zinc-900">Kommentar schreiben</h3>
|
<h3 class="font-medium text-sm text-zinc-900">{t(T.comments_form_title)}</h3>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={author}
|
bind:value={author}
|
||||||
placeholder="Dein Name"
|
placeholder={t(T.comments_name_placeholder)}
|
||||||
required
|
required
|
||||||
maxlength="80"
|
maxlength="80"
|
||||||
class="w-full p-2 border border-zinc-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-zinc-400"
|
class="w-full p-2 border border-zinc-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-zinc-400"
|
||||||
@@ -499,7 +504,7 @@
|
|||||||
bind:value={body}
|
bind:value={body}
|
||||||
use:autosize
|
use:autosize
|
||||||
onkeydown={submitOnCmdEnter}
|
onkeydown={submitOnCmdEnter}
|
||||||
placeholder="Schreibe etwas Konstruktives. Cmd/Ctrl+Enter sendet."
|
placeholder={t(T.comments_body_placeholder)}
|
||||||
required
|
required
|
||||||
rows="3"
|
rows="3"
|
||||||
maxlength={bodyMax}
|
maxlength={bodyMax}
|
||||||
@@ -522,19 +527,21 @@
|
|||||||
disabled={submitting || !body.trim() || !author.trim()}
|
disabled={submitting || !body.trim() || !author.trim()}
|
||||||
class="px-4 py-2 bg-zinc-800 text-white rounded text-sm font-medium hover:bg-zinc-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
class="px-4 py-2 bg-zinc-800 text-white rounded text-sm font-medium hover:bg-zinc-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{submitting ? "Sende …" : "Senden"}
|
{submitting ? t(T.comments_sending) : t(T.comments_send)}
|
||||||
</button>
|
</button>
|
||||||
{#if requireApproval}
|
{#if requireApproval}
|
||||||
<span class="text-zinc-500 inline-flex items-center gap-1">
|
<span class="text-zinc-500 inline-flex items-center gap-1">
|
||||||
<Icon icon="mdi:shield-check-outline" class="size-3.5" />
|
<Icon icon="mdi:shield-check-outline" class="size-3.5" />
|
||||||
Wird vor Veröffentlichung geprüft.
|
{t(T.comments_approval_hint)}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="ml-auto text-zinc-400 tabular-nums">{body.length} / {bodyMax}</span>
|
<span class="ml-auto text-zinc-400 tabular-nums">{body.length} / {bodyMax}</span>
|
||||||
</div>
|
</div>
|
||||||
{#if totalPending > 0}
|
{#if totalPending > 0}
|
||||||
<p class="text-xs text-zinc-500 italic">
|
<p class="text-xs text-zinc-500 italic">
|
||||||
{totalPending} {totalPending === 1 ? 'Kommentar wartet' : 'Kommentare warten'} noch auf Freigabe (nur für dich sichtbar).
|
{totalPending === 1
|
||||||
|
? t(T.comments_pending_summary_one)
|
||||||
|
: t(T.comments_pending_summary_other, { count: totalPending })}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
import EventBadges from "./EventBadges.svelte";
|
import EventBadges from "./EventBadges.svelte";
|
||||||
import RustyImage from "./RustyImage.svelte";
|
import RustyImage from "./RustyImage.svelte";
|
||||||
import Card from "./Card.svelte";
|
import Card from "./Card.svelte";
|
||||||
|
import { useTranslate, T } from "$lib/translations";
|
||||||
|
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
type PostWithImage = PostEntry & {
|
type PostWithImage = PostEntry & {
|
||||||
_resolvedImageUrl?: string;
|
_resolvedImageUrl?: string;
|
||||||
@@ -98,8 +101,8 @@
|
|||||||
<div class="absolute top-2 left-2">
|
<div class="absolute top-2 left-2">
|
||||||
<span
|
<span
|
||||||
class="inline-flex items-center gap-1 rounded-full bg-white/90 px-2 py-1 text-[.65rem] font-semibold tabular-nums text-zinc-700 shadow-sm"
|
class="inline-flex items-center gap-1 rounded-full bg-white/90 px-2 py-1 text-[.65rem] font-semibold tabular-nums text-zinc-700 shadow-sm"
|
||||||
aria-label="{commentCount} Kommentare"
|
aria-label={t(T.comments_count_aria, { count: commentCount })}
|
||||||
title="{commentCount} Kommentare"
|
title={t(T.comments_count_aria, { count: commentCount })}
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:comment-outline" class="size-3" />
|
<Icon icon="mdi:comment-outline" class="size-3" />
|
||||||
{commentCount}
|
{commentCount}
|
||||||
|
|||||||
@@ -133,6 +133,35 @@ const TRANSLATION_KEYS = [
|
|||||||
"post_map_activate",
|
"post_map_activate",
|
||||||
"post_map_open_osm",
|
"post_map_open_osm",
|
||||||
"post_comments",
|
"post_comments",
|
||||||
|
// Comment plugin (Comments.svelte / PostCard count pill / CommentCountBadge).
|
||||||
|
"comments_loading",
|
||||||
|
"comments_load_error_title",
|
||||||
|
"comments_retry",
|
||||||
|
"comments_empty",
|
||||||
|
"comments_edit",
|
||||||
|
"comments_delete",
|
||||||
|
"comments_save",
|
||||||
|
"comments_cancel",
|
||||||
|
"comments_confirm_delete",
|
||||||
|
"comments_reply",
|
||||||
|
"comments_reply_count_one",
|
||||||
|
"comments_reply_count_other",
|
||||||
|
"comments_reply_send",
|
||||||
|
"comments_send",
|
||||||
|
"comments_sending",
|
||||||
|
"comments_form_title",
|
||||||
|
"comments_name_placeholder",
|
||||||
|
"comments_body_placeholder",
|
||||||
|
"comments_reply_placeholder",
|
||||||
|
"comments_approval_hint",
|
||||||
|
"comments_edited",
|
||||||
|
"comments_pending_short",
|
||||||
|
"comments_pending_full",
|
||||||
|
"comments_self_marker",
|
||||||
|
"comments_pending_summary_one",
|
||||||
|
"comments_pending_summary_other",
|
||||||
|
"comments_count_aria",
|
||||||
|
"comments_jump_to",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type TranslationKey = (typeof TRANSLATION_KEYS)[number];
|
export type TranslationKey = (typeof TRANSLATION_KEYS)[number];
|
||||||
|
|||||||
Reference in New Issue
Block a user