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 "$lib/iconify-offline";
|
||||
import { env } from "$env/dynamic/public";
|
||||
import { useTranslate, T } from "$lib/translations";
|
||||
|
||||
const t = useTranslate();
|
||||
|
||||
let {
|
||||
pageId,
|
||||
@@ -54,7 +57,7 @@
|
||||
<a
|
||||
href="#{targetId}"
|
||||
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}"
|
||||
>
|
||||
<Icon icon="mdi:comment-outline" class="size-4" />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import Icon from "@iconify/svelte";
|
||||
import "$lib/iconify-offline";
|
||||
import { env } from "$env/dynamic/public";
|
||||
import { useTranslate, T } from "$lib/translations";
|
||||
|
||||
let {
|
||||
pageId,
|
||||
@@ -15,6 +16,8 @@
|
||||
bodyMax?: number;
|
||||
} = $props();
|
||||
|
||||
const t = useTranslate();
|
||||
|
||||
type Status = "approved" | "pending" | "spam";
|
||||
|
||||
type PublicComment = {
|
||||
@@ -187,7 +190,7 @@
|
||||
}
|
||||
|
||||
async function deleteComment(id: string) {
|
||||
if (!confirm("Kommentar löschen?")) return;
|
||||
if (!confirm(t(T.comments_confirm_delete))) return;
|
||||
try {
|
||||
const r = await fetch(`${apiUrl()}`.replace(`/${pageId}`, `/${pageId}/${id}`), {
|
||||
method: "DELETE",
|
||||
@@ -310,12 +313,12 @@
|
||||
<button
|
||||
type="submit"
|
||||
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
|
||||
type="button"
|
||||
onclick={cancelEdit}
|
||||
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>
|
||||
</div>
|
||||
{#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"
|
||||
>
|
||||
<Icon icon="mdi:pencil-outline" class="size-3" />
|
||||
Bearbeiten
|
||||
{t(T.comments_edit)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -338,7 +341,7 @@
|
||||
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" />
|
||||
Löschen
|
||||
{t(T.comments_delete)}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -349,7 +352,7 @@
|
||||
<div class="flex flex-wrap items-baseline gap-2 text-sm">
|
||||
<strong class="font-medium text-zinc-900">{c.author}</strong>
|
||||
{#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}
|
||||
<time
|
||||
class="text-zinc-500 text-xs"
|
||||
@@ -357,12 +360,12 @@
|
||||
title={fmtAbs(c.created_at)}
|
||||
>{fmtRel(c.created_at)}</time>
|
||||
{#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 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">
|
||||
<Icon icon="mdi:clock-outline" class="size-3" />
|
||||
wartet
|
||||
{t(T.comments_pending_short)}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -370,7 +373,7 @@
|
||||
|
||||
<div class="comments {cls}">
|
||||
{#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)}
|
||||
<li class="flex gap-3 list-none!">
|
||||
<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">
|
||||
<Icon icon="mdi:alert-circle-outline" class="size-5 shrink-0 mt-0.5" />
|
||||
<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>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onclick={loadComments}
|
||||
class="text-xs text-red-800 underline hover:no-underline"
|
||||
>Erneut versuchen</button>
|
||||
>{t(T.comments_retry)}</button>
|
||||
</div>
|
||||
{:else}
|
||||
{#if topLevel.length === 0}
|
||||
<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" />
|
||||
<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>
|
||||
{:else}
|
||||
<ul class="space-y-5 mb-8 list-none p-0">
|
||||
@@ -423,10 +426,12 @@
|
||||
>
|
||||
<Icon icon="mdi:reply" class="size-3" />
|
||||
{replyTo === c.id
|
||||
? "Abbrechen"
|
||||
: replies.length > 0
|
||||
? `${replies.length} ${replies.length === 1 ? 'Antwort' : 'Antworten'}`
|
||||
: "Antworten"}
|
||||
? t(T.comments_cancel)
|
||||
: replies.length === 0
|
||||
? t(T.comments_reply)
|
||||
: replies.length === 1
|
||||
? t(T.comments_reply_count_one)
|
||||
: t(T.comments_reply_count_other, { count: replies.length })}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -435,7 +440,7 @@
|
||||
<input
|
||||
type="text"
|
||||
bind:value={replyAuthor}
|
||||
placeholder="Dein Name"
|
||||
placeholder={t(T.comments_name_placeholder)}
|
||||
required
|
||||
maxlength="80"
|
||||
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}
|
||||
use:autosize
|
||||
onkeydown={submitOnCmdEnter}
|
||||
placeholder="Deine Antwort"
|
||||
placeholder={t(T.comments_reply_placeholder)}
|
||||
required
|
||||
rows="2"
|
||||
maxlength={bodyMax}
|
||||
@@ -456,7 +461,7 @@
|
||||
type="submit"
|
||||
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"
|
||||
>Antwort senden</button>
|
||||
>{t(T.comments_reply_send)}</button>
|
||||
<span class="ml-auto text-xs text-zinc-400 tabular-nums">{replyBody.length} / {bodyMax}</span>
|
||||
</div>
|
||||
</form>
|
||||
@@ -486,11 +491,11 @@
|
||||
{/if}
|
||||
|
||||
<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
|
||||
type="text"
|
||||
bind:value={author}
|
||||
placeholder="Dein Name"
|
||||
placeholder={t(T.comments_name_placeholder)}
|
||||
required
|
||||
maxlength="80"
|
||||
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}
|
||||
use:autosize
|
||||
onkeydown={submitOnCmdEnter}
|
||||
placeholder="Schreibe etwas Konstruktives. Cmd/Ctrl+Enter sendet."
|
||||
placeholder={t(T.comments_body_placeholder)}
|
||||
required
|
||||
rows="3"
|
||||
maxlength={bodyMax}
|
||||
@@ -522,19 +527,21 @@
|
||||
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"
|
||||
>
|
||||
{submitting ? "Sende …" : "Senden"}
|
||||
{submitting ? t(T.comments_sending) : t(T.comments_send)}
|
||||
</button>
|
||||
{#if requireApproval}
|
||||
<span class="text-zinc-500 inline-flex items-center gap-1">
|
||||
<Icon icon="mdi:shield-check-outline" class="size-3.5" />
|
||||
Wird vor Veröffentlichung geprüft.
|
||||
{t(T.comments_approval_hint)}
|
||||
</span>
|
||||
{/if}
|
||||
<span class="ml-auto text-zinc-400 tabular-nums">{body.length} / {bodyMax}</span>
|
||||
</div>
|
||||
{#if totalPending > 0}
|
||||
<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>
|
||||
{/if}
|
||||
</form>
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
import EventBadges from "./EventBadges.svelte";
|
||||
import RustyImage from "./RustyImage.svelte";
|
||||
import Card from "./Card.svelte";
|
||||
import { useTranslate, T } from "$lib/translations";
|
||||
|
||||
const t = useTranslate();
|
||||
|
||||
type PostWithImage = PostEntry & {
|
||||
_resolvedImageUrl?: string;
|
||||
@@ -98,8 +101,8 @@
|
||||
<div class="absolute top-2 left-2">
|
||||
<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"
|
||||
aria-label="{commentCount} Kommentare"
|
||||
title="{commentCount} Kommentare"
|
||||
aria-label={t(T.comments_count_aria, { count: commentCount })}
|
||||
title={t(T.comments_count_aria, { count: commentCount })}
|
||||
>
|
||||
<Icon icon="mdi:comment-outline" class="size-3" />
|
||||
{commentCount}
|
||||
|
||||
@@ -133,6 +133,35 @@ const TRANSLATION_KEYS = [
|
||||
"post_map_activate",
|
||||
"post_map_open_osm",
|
||||
"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;
|
||||
|
||||
export type TranslationKey = (typeof TRANSLATION_KEYS)[number];
|
||||
|
||||
Reference in New Issue
Block a user