- {EXTRAS.map(([Icon, name, desc]) => (
-
+ {EXTRAS.map(([Icon, key]) => (
+
-
{name}
-
{desc}
+
{t(key)}
+
{t(`${key}.text`)}
))}
@@ -104,17 +95,13 @@ export default function BotFeatures() {
{data && (
-
- Gebaut, weil drei Bots mit je eigenem Abo für eine Community
- weder übersichtlich noch günstig sind. Der Quelltext liegt offen,
- der Betrieb kostet einen Container.
-
+
{t('features.outro')}
- Befehle ansehen
+ {t('bot.viewCommands')}
- Zur Übersicht
+ {t('features.overview')}
diff --git a/frontend/src/pages/BotLanding.jsx b/frontend/src/pages/BotLanding.jsx
index 226b597..1092efa 100644
--- a/frontend/src/pages/BotLanding.jsx
+++ b/frontend/src/pages/BotLanding.jsx
@@ -3,24 +3,26 @@
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { apiGet } from '../api.js';
+import { useT } from '../i18n.jsx';
import {
IconDevlog, IconLevel, IconRoles, IconServer, IconSupport, IconGiveaway,
IconBirthday, IconShield, IconMagic, IconKey, IconArrowRight, IconBot,
} from '../icons.jsx';
const HIGHLIGHTS = [
- [IconDevlog, 'Devlogs & Feeds', 'Entwicklungs-Berichte landen als gebrandetes Embed im Discord und werden gleichzeitig durchsuchbar archiviert. Commits und Releases aus Gitea genauso.'],
- [IconServer, 'Server-Monitor', 'Über 300 Spiele per gamedig, dazu FiveM und einfache Erreichbarkeits-Checks. Ein Live-Embed pro Server, Alarm bei Ausfall, Spielerzahl in der Bot-Präsenz.'],
- [IconLevel, 'Level & Aktivität', 'XP fürs Mitreden mit Rollen-Belohnungen, öffentliche Bestenliste und Aktivitäts-Statistik des Servers.'],
- [IconRoles, 'Rollen-Menüs', 'Selbstbedienung per Knopfdruck — im Discord und im Browser. Farben pro Knopf, wahlweise exklusiv.'],
- [IconSupport, 'Support & Kontakt', 'Tickets als private Threads mit Gesprächsprotokoll, dazu Modmail: eine Direktnachricht an den Bot landet beim Team.'],
- [IconShield, 'Moderation', 'Verwarnungen mit Verlauf, Auszeiten, Aufräumen — alles im Mod-Log, dazu Protokoll für Beitritte, Namens- und Rollenwechsel.'],
- [IconGiveaway, 'Verlosungen', 'Teilnahme per Knopf, automatische Ziehung, Neuauslosung wenn sich niemand meldet.'],
- [IconBirthday, 'Geburtstage', 'Wer mag, trägt seinen Tag ein — der Bot gratuliert morgens und vergibt eine Rolle für den Tag.'],
- [IconMagic, 'Willkommens-Karten', 'Neue Mitglieder bekommen ein gerendertes Bild mit Avatar und Mitglieds-Nummer.'],
+ [IconDevlog, 'bot.hl.devlogs'],
+ [IconServer, 'bot.hl.monitor'],
+ [IconLevel, 'bot.hl.level'],
+ [IconRoles, 'bot.hl.roles'],
+ [IconSupport, 'bot.hl.support'],
+ [IconShield, 'bot.hl.mod'],
+ [IconGiveaway, 'bot.hl.giveaway'],
+ [IconBirthday, 'bot.hl.birthday'],
+ [IconMagic, 'bot.hl.welcome'],
];
export default function BotLanding() {
+ const { t } = useT();
const [status, setStatus] = useState(null);
const [hub, setHub] = useState(null);
const [featureCount, setFeatureCount] = useState(null);
@@ -40,21 +42,17 @@ export default function BotLanding() {
BOT
-
// Selbst gebaut, keine Paywall
+
{t('bot.tag')}
- Ein Discord-Bot,
der alles macht
+ {t('bot.title1')}
{t('bot.title2')}
-
- Devlogs, Level, Moderation, Server-Monitoring, Verlosungen, Tickets —
- was sonst drei Bots mit Abo-Modell erledigen, läuft hier in einem,
- mit eigenem Webinterface.
-
+
{t('bot.subtitle')}
- Alle Funktionen
+ {t('bot.allFeatures')}
- Befehle ansehen
+ {t('bot.viewCommands')}
@@ -65,62 +63,56 @@ export default function BotLanding() {
{featureCount && (
{featureCount}
- Funktionen, einzeln schaltbar
+ {t('bot.statFeatures')}
)}
16
- Slash-Befehle
+ {t('bot.statCommands')}
300+
- Spiele im Monitor
+ {t('bot.statGames')}
0 €
- Kosten, keine Premium-Stufen
+ {t('bot.statCost')}
- {HIGHLIGHTS.map(([Icon, title, text]) => (
-
+ {HIGHLIGHTS.map(([Icon, key]) => (
+
- {title}
- {text}
+ {t(key)}
+ {t(`${key}.text`)}
))}
-
// Für Entwickler
+
{t('bot.forDevs')}
- API & Single Sign-On
-
- Eigene Skripte posten über die API v1 mit Bearer-Schlüsseln und Bereichs-Rechten.
- Andere Dienste nutzen den Discord-Login des Bots mit und bekommen die Rollen
- gleich mitgeliefert.
-
+ {t('bot.dev.api')}
+ {t('bot.dev.api.text')}
- Läuft überall
-
- Node und SQLite in einem Container, ein Volume für die Daten.
- Nächtliche Sicherungen inklusive — von der Datenbank und allen Repositories.
-
+ {t('bot.dev.host')}
+ {t('bot.dev.host.text')}
{hub && (
- Der Bot läuft für die D4RKST3R-Community
- {status?.members ? ` mit ${status.members} Mitgliedern` : ''} —
- {' '}zum Community-Hub.
+ {status?.members
+ ? t('bot.runsForWith', status.members)
+ : t('bot.runsFor')} —
+ {' '}{t('bot.toHub')}.
)}
diff --git a/frontend/src/pages/Changelog.jsx b/frontend/src/pages/Changelog.jsx
index a441a20..7404817 100644
--- a/frontend/src/pages/Changelog.jsx
+++ b/frontend/src/pages/Changelog.jsx
@@ -5,12 +5,13 @@ import EmptyState from '../components/EmptyState.jsx';
import { SkeletonCards } from '../components/Skeleton.jsx';
import { Markdown } from '../markdown.jsx';
import ToTop from '../components/ToTop.jsx';
+import { useT, useFmt } from '../i18n.jsx';
-const dateFmt = new Intl.DateTimeFormat('de-DE', {
- day: '2-digit', month: 'long', year: 'numeric',
-});
+const DATUM = { day: '2-digit', month: 'long', year: 'numeric' };
export default function Changelog() {
+ const { t } = useT();
+ const { datum } = useFmt();
const [data, setData] = useState(null);
const [page, setPage] = useState(1);
const [error, setError] = useState(false);
@@ -29,21 +30,21 @@ export default function Changelog() {
CHANGELOG
-
// Releases
-
Changelog
-
Alle Versionen und Release-Notes.
+
{t('changelog.tag')}
+
{t('nav.changelog')}
+
{t('changelog.subtitle')}
- {error && Changelog konnte nicht geladen werden.
}
+ {error && {t('changelog.error')}
}
{!error && !data && }
{data?.total === 0 && (
)}
@@ -52,17 +53,17 @@ export default function Changelog() {
{r.tag}
- {r.prerelease ? Pre-Release : null}
+ {r.prerelease ? {t('changelog.prerelease')} : null}
{r.repo.split('/').pop()}
- {dateFmt.format(new Date(r.published_at))}
+ {datum(r.published_at, DATUM)}
{r.name && r.name !== r.tag && {r.name}
}
-
+
{r.url && (
)}
@@ -71,13 +72,13 @@ export default function Changelog() {
{pages > 1 && (
{String(page).padStart(2, '0')} / {String(pages).padStart(2, '0')}
)}
diff --git a/frontend/src/pages/DevlogDetail.jsx b/frontend/src/pages/DevlogDetail.jsx
index e46c188..d184fc2 100644
--- a/frontend/src/pages/DevlogDetail.jsx
+++ b/frontend/src/pages/DevlogDetail.jsx
@@ -8,13 +8,14 @@ import { Markdown } from '../markdown.jsx';
import Lightbox from '../components/Lightbox.jsx';
import { SkeletonCards } from '../components/Skeleton.jsx';
import { splitContent } from './Devlogs.jsx';
+import { useT, useFmt } from '../i18n.jsx';
-const dateFmt = new Intl.DateTimeFormat('de-DE', {
- weekday: 'long', day: '2-digit', month: 'long', year: 'numeric',
-});
+const DATUM = { weekday: 'long', day: '2-digit', month: 'long', year: 'numeric' };
export default function DevlogDetail() {
const { id } = useParams();
+ const { t } = useT();
+ const { datum } = useFmt();
const [item, setItem] = useState(null);
const [error, setError] = useState(false);
const [lightbox, setLightbox] = useState(null);
@@ -43,16 +44,16 @@ export default function DevlogDetail() {
DEVLOG
-
// Devlog
-
{item ? dateFmt.format(new Date(item.posted_at)) : 'Devlog'}
- {parts?.project &&
Projekt: {parts.project}
}
+
{t('devlog.tag')}
+
{item ? datum(item.posted_at, DATUM) : t('nav.devlog')}
+ {parts?.project &&
{t('devlog.project', parts.project)}
}
{error && (
- Devlog nicht gefunden — zurück zum Archiv.
+ {t('devlog.notFound')} {t('devlog.backToArchive')}.
)}
{!error && !item && }
@@ -67,7 +68,7 @@ export default function DevlogDetail() {
@@ -80,8 +81,8 @@ export default function DevlogDetail() {
- Alle Devlogs
-
+ {t('devlog.allDevlogs')}
+
{lightbox !== null && (
diff --git a/frontend/src/pages/Devlogs.jsx b/frontend/src/pages/Devlogs.jsx
index 18a75b9..196449e 100644
--- a/frontend/src/pages/Devlogs.jsx
+++ b/frontend/src/pages/Devlogs.jsx
@@ -5,20 +5,19 @@ import { Markdown } from '../markdown.jsx';
import ToTop from '../components/ToTop.jsx';
import Lightbox from '../components/Lightbox.jsx';
import { SkeletonCards } from '../components/Skeleton.jsx';
+import { useT, useFmt } from '../i18n.jsx';
-const dateFmt = new Intl.DateTimeFormat('de-DE', {
- weekday: 'short', day: '2-digit', month: '2-digit', year: 'numeric',
-});
-const monthFmt = new Intl.DateTimeFormat('de-DE', { month: 'long', year: 'numeric' });
+const DATUM = { weekday: 'short', day: '2-digit', month: '2-digit', year: 'numeric' };
+const MONAT = { month: 'long', year: 'numeric' };
/** Relative Angabe wie „vor 3 Tagen" für die Karten-Ecke */
-function relative(iso) {
+function relative(iso, t) {
const days = Math.floor((Date.now() - new Date(iso).getTime()) / 86400000);
- if (days <= 0) return 'heute';
- if (days === 1) return 'gestern';
- if (days < 30) return `vor ${days} Tagen`;
+ if (days <= 0) return t('common.today');
+ if (days === 1) return t('common.yesterday');
+ if (days < 30) return t('common.daysAgo', days);
const months = Math.floor(days / 30);
- return months === 1 ? 'vor 1 Monat' : `vor ${months} Monaten`;
+ return months === 1 ? t('common.monthAgo') : t('common.monthsAgo', months);
}
/**
@@ -45,6 +44,8 @@ export function splitContent(raw) {
}
export default function Devlogs({ me }) {
+ const { t } = useT();
+ const { datum } = useFmt();
const [data, setData] = useState(null);
const [page, setPage] = useState(1);
const [error, setError] = useState(false);
@@ -83,12 +84,12 @@ export default function Devlogs({ me }) {
// Admin: Devlog aus dem Archiv entfernen (löscht auch die lokalen Bilder)
async function remove(id) {
- if (!window.confirm('Dieses Devlog aus dem Archiv löschen?')) return;
+ if (!window.confirm(t('devlogs.confirmDelete'))) return;
try {
await apiDelete(`/api/devlogs/${id}`);
load();
} catch {
- window.alert('Löschen fehlgeschlagen.');
+ window.alert(t('devlogs.deleteFailed'));
}
}
@@ -103,11 +104,9 @@ export default function Devlogs({ me }) {
DEVLOG
-
// Archiv
-
Devlog
-
- Entwicklungs-Updates, frisch aus dem Editor — automatisch archiviert.
-
+
{t('devlogs.tag')}
+
{t('nav.devlog')}
+
{t('devlogs.subtitle')}
@@ -118,7 +117,7 @@ export default function Devlogs({ me }) {
className={`dl-filter${project === '' ? ' active' : ''}`}
onClick={() => { setProject(''); setPage(1); }}
>
- Alle
{projects.reduce((s, p) => s + p.n, 0)}
+ {t('devlogs.all')}
{projects.reduce((s, p) => s + p.n, 0)}
{projects.map((p) => (
Devlogs konnten nicht geladen werden.
{q
- ? `Keine Treffer für „${q}".`
+ ? t('devlogs.noHits', q)
: project
- ? `Noch keine Devlogs für ${project}.`
- : 'Noch keine Devlogs archiviert.'}
+ ? t('devlogs.noneForProject', project)
+ : t('devlogs.none')}
)}
@@ -159,7 +158,7 @@ export default function Devlogs({ me }) {
))}
diff --git a/frontend/src/pages/Galerie.jsx b/frontend/src/pages/Galerie.jsx
index 63154e9..32b77e7 100644
--- a/frontend/src/pages/Galerie.jsx
+++ b/frontend/src/pages/Galerie.jsx
@@ -4,10 +4,13 @@ import { IconChevronLeft, IconChevronRight, IconGallery } from '../icons.jsx';
import EmptyState from '../components/EmptyState.jsx';
import Lightbox from '../components/Lightbox.jsx';
import { SkeletonGrid } from '../components/Skeleton.jsx';
+import { useT, useFmt } from '../i18n.jsx';
-const dateFmt = new Intl.DateTimeFormat('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
+const DATUM = { day: '2-digit', month: '2-digit', year: 'numeric' };
export default function Galerie() {
+ const { t } = useT();
+ const { datum } = useFmt();
const [data, setData] = useState(null);
const [page, setPage] = useState(1);
const [error, setError] = useState(false);
@@ -30,21 +33,21 @@ export default function Galerie() {