diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 5c640a6..fd17899 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -17,12 +17,15 @@ import Impressum from './pages/Impressum.jsx'; import Datenschutz from './pages/Datenschutz.jsx'; import Seite from './pages/Seite.jsx'; import { IconChevronDown, IconMenu, IconX, IconLock } from './icons.jsx'; +import { useT, LangSwitch } from './i18n.jsx'; +// Beschriftung erst beim Rendern uebersetzen — der Pfad bleibt deutsch, +// damit geteilte Links weiter funktionieren. const COMMUNITY_LINKS = [ - ['/galerie', 'Galerie'], - ['/level', 'Level'], - ['/events', 'Events'], - ['/changelog', 'Changelog'], + ['/galerie', 'nav.gallery'], + ['/level', 'nav.level'], + ['/events', 'nav.events'], + ['/changelog', 'nav.changelog'], ]; /** Dropdown in der Navbar (schließt bei Klick daneben und bei Navigation) */ @@ -86,6 +89,7 @@ export default function App() { const [scrolled, setScrolled] = useState(false); const [drawer, setDrawer] = useState(false); const location = useLocation(); + const { t } = useT(); const communityActive = COMMUNITY_LINKS.some(([to]) => location.pathname.startsWith(to)); // Drawer bei Seitenwechsel schließen @@ -125,21 +129,22 @@ export default function App() {
+ {me.loading ? null : me.user ? ( } > - Profil - {me.admin && Commits} - Logout + {t('nav.profile')} + {me.admin && {t('nav.commits')}} + {t('nav.logout')} ) : ( - Login + {t('nav.login')} )}
- {drawer && ( )} @@ -190,15 +200,14 @@ export default function App() {
- Der Login ist Mitgliedern des Discord-Servers vorbehalten — - tritt erst dem Server bei und logg dich dann nochmal ein. + {t('gate.text')} {invite && ( - Zum Discord + {t('gate.join')} )} - +
)} @@ -229,8 +238,8 @@ export default function App() { git.d4rkst3r.de // - Impressum - Datenschutz + {t('footer.imprint')} + {t('footer.privacy')} ); diff --git a/frontend/src/BotApp.jsx b/frontend/src/BotApp.jsx index ac255b1..7073c25 100644 --- a/frontend/src/BotApp.jsx +++ b/frontend/src/BotApp.jsx @@ -10,6 +10,7 @@ import Settings from './pages/Settings.jsx'; import Impressum from './pages/Impressum.jsx'; import Datenschutz from './pages/Datenschutz.jsx'; import { IconMenu, IconX } from './icons.jsx'; +import { useT, LangSwitch } from './i18n.jsx'; export default function BotApp() { const [me, setMe] = useState({ user: null, admin: false, scopes: [], loading: true }); @@ -17,6 +18,7 @@ export default function BotApp() { const [drawer, setDrawer] = useState(false); const [hub, setHub] = useState(null); const location = useLocation(); + const { t } = useT(); useEffect(() => { setDrawer(false); }, [location.pathname]); @@ -43,40 +45,46 @@ export default function BotApp() {
+ {me.loading ? null : me.user ? ( {me.user.avatar && } {me.user.username} - + ) : ( - Anmelden + {t('nav.signIn')} )}
- {drawer && ( )} @@ -96,11 +104,11 @@ export default function BotApp() { D4RKBOT. // - Quellcode + {t('footer.source')} // - Impressum - Datenschutz + {t('footer.imprint')} + {t('footer.privacy')} ); diff --git a/frontend/src/i18n.jsx b/frontend/src/i18n.jsx new file mode 100644 index 0000000..74821f4 --- /dev/null +++ b/frontend/src/i18n.jsx @@ -0,0 +1,119 @@ +// Zweisprachigkeit für die öffentlichen Seiten (Hub + Bot-Produktseite). +// +// Bewusst ohne Bibliothek: ein Wörterbuch pro Sprache, ein Context, fertig. +// Die Wörterbücher werden mitgebaut, es gibt also keinen Nachlade-Blitzer beim +// ersten Rendern. Das Config-Panel bleibt deutsch — dort steht der Owner davor. +import { createContext, useContext, useEffect, useState } from 'react'; +import de from './locales/de.js'; +import en from './locales/en.js'; + +const WOERTERBUCH = { de, en }; +export const SPRACHEN = [['de', 'DE'], ['en', 'EN']]; +const SPEICHER_KEY = 'd4rk_lang'; + +/** Gespeicherte Wahl schlägt Browsersprache. Deutsch nur, wenn der Browser + * es auch will — sonst Englisch, das versteht der Rest der Welt eher. */ +function ermitteln() { + try { + const gewaehlt = localStorage.getItem(SPEICHER_KEY); + if (WOERTERBUCH[gewaehlt]) return gewaehlt; + } catch { + // Privater Modus ohne localStorage — dann eben jedes Mal neu erkennen + } + const browser = (navigator.languages?.[0] ?? navigator.language ?? '').toLowerCase(); + return browser.startsWith('de') ? 'de' : 'en'; +} + +const LangContext = createContext({ + lang: 'de', setLang: () => {}, t: (k) => k, tOr: (k, f) => f, +}); + +export function LangProvider({ children }) { + const [lang, setLangState] = useState(ermitteln); + + useEffect(() => { + document.documentElement.lang = lang; + }, [lang]); + + const setLang = (neu) => { + if (!WOERTERBUCH[neu]) return; + setLangState(neu); + try { + localStorage.setItem(SPEICHER_KEY, neu); + } catch { + // nicht schlimm, dann gilt die Wahl nur für diesen Besuch + } + }; + + // %s-Platzhalter der Reihe nach ersetzen — dasselbe Format wie in den + // Bot-Texten, damit man nicht zwei Schreibweisen im Kopf haben muss. + const t = (key, ...args) => { + const roh = WOERTERBUCH[lang]?.[key] ?? WOERTERBUCH.de[key]; + if (roh == null) return key; + if (typeof roh !== 'string' || args.length === 0) return roh; + let i = 0; + return roh.replace(/%s/g, () => String(args[i++] ?? '')); + }; + + // Für Inhalte, die der Server deutsch liefert (Modul-Namen etwa): gibt es + // keine Übersetzung, bleibt der Servertext stehen statt eines nackten + // Schlüssels. So verschwindet eine neue Funktion nicht von der Seite. + const tOr = (key, fallback) => { + const roh = WOERTERBUCH[lang]?.[key]; + return roh == null ? fallback : roh; + }; + + return ( + + {children} + + ); +} + +export function useT() { + return useContext(LangContext); +} + +// Intl-Formatter sind teuer zu bauen und werden in Listen oft gebraucht — +// einmal pro Sprache und Optionen reicht. +const formatCache = new Map(); +function formatter(tag, opts) { + const key = tag + JSON.stringify(opts); + let f = formatCache.get(key); + if (!f) { + f = new Intl.DateTimeFormat(tag, opts); + formatCache.set(key, f); + } + return f; +} + +/** Datums- und Zahlenformat der aktiven Sprache */ +export function useFmt() { + const { lang } = useT(); + // en-GB statt en-US: Tag vor Monat, wie es der Rest der Seite auch macht + const tag = lang === 'de' ? 'de-DE' : 'en-GB'; + return { + tag, + datum: (wert, opts) => formatter(tag, opts).format(new Date(wert)), + zahl: (n) => Number(n ?? 0).toLocaleString(tag), + }; +} + +/** Umschalter für die Navigation */ +export function LangSwitch() { + const { lang, setLang } = useT(); + return ( +
+ {SPRACHEN.map(([id, label]) => ( + + ))} +
+ ); +} diff --git a/frontend/src/locales/de.js b/frontend/src/locales/de.js new file mode 100644 index 0000000..3f08c14 --- /dev/null +++ b/frontend/src/locales/de.js @@ -0,0 +1,258 @@ +// Deutsche Texte der öffentlichen Seiten. Diese Datei ist die Referenz: +// Was hier steht, muss es auch in en.js geben — fehlt ein Schlüssel dort, +// fällt die Anzeige auf Deutsch zurück statt auf den nackten Schlüssel. +export default { + // ── Navigation & Rahmen ─────────────────────────── + 'nav.devlog': 'Devlog', + 'nav.roadmap': 'Roadmap', + 'nav.server': 'Server', + 'nav.community': 'Community', + 'nav.gallery': 'Galerie', + 'nav.level': 'Level', + 'nav.events': 'Events', + 'nav.changelog': 'Changelog', + 'nav.config': 'Config', + 'nav.profile': 'Profil', + 'nav.commits': 'Commits', + 'nav.login': 'Login', + 'nav.loginDiscord': 'Login mit Discord', + 'nav.logout': 'Logout', + 'nav.features': 'Funktionen', + 'nav.commands': 'Befehle', + 'nav.dashboard': 'Dashboard', + 'nav.signIn': 'Anmelden', + 'nav.signOut': 'Abmelden', + 'nav.menu': 'Menü', + 'footer.source': 'Quellcode', + 'footer.imprint': 'Impressum', + 'footer.privacy': 'Datenschutz', + 'gate.text': 'Der Login ist Mitgliedern des Discord-Servers vorbehalten — tritt erst dem Server bei und logg dich dann nochmal ein.', + 'gate.join': 'Zum Discord', + 'gate.hide': 'Ausblenden', + + // ── Allgemein ───────────────────────────────────── + 'common.loading': 'Lade …', + 'common.newer': 'Neuere', + 'common.older': 'Ältere', + 'common.readDevlogs': 'Devlogs lesen', + 'common.toRoadmap': 'Zur Roadmap', + 'common.today': 'heute', + 'common.yesterday': 'gestern', + 'common.daysAgo': 'vor %s Tagen', + 'common.monthAgo': 'vor 1 Monat', + 'common.monthsAgo': 'vor %s Monaten', + + // ── Startseite (Hub) ────────────────────────────── + 'home.tag': '// Community-Hub', + 'home.welcome': 'Willkommen', + 'home.at': 'bei', + 'home.subtitle': 'Devlogs, Roadmap, Server-Status und alles aus der Community — an einem Ort, direkt aus dem Discord.', + 'home.discord': 'Zum Discord', + 'home.readDevlogs': 'Devlogs lesen', + 'home.members': 'Member im Discord', + 'home.serversOnline': 'Server online', + 'home.playersNow': 'Spieler gerade drauf', + 'home.services': '// Dienste', + 'home.latestDevlog': '// Neuestes Devlog', + 'home.more': 'Weiterlesen', + 'home.tile.devlog': 'Entwicklungs-Updates, frisch aus dem Editor', + 'home.tile.roadmap': 'Meilensteine + Community-Wünsche zum Abstimmen', + 'home.tile.server': 'Live-Status aller Game-Server mit Verlauf', + 'home.tile.gallery': 'Screenshots aus der Community', + 'home.tile.level': 'XP-Bestenliste — wer redet am meisten mit?', + 'home.tile.events': 'Playtests, Streams und was sonst ansteht', + + // ── Devlogs ─────────────────────────────────────── + 'devlogs.tag': '// Archiv', + 'devlogs.subtitle': 'Entwicklungs-Updates, frisch aus dem Editor — automatisch archiviert.', + 'devlogs.all': 'Alle', + 'devlogs.search': 'Devlogs durchsuchen …', + 'devlogs.hits': '%s Treffer', + 'devlogs.error': 'Devlogs konnten nicht geladen werden.', + 'devlogs.noHits': 'Keine Treffer für „%s".', + 'devlogs.noneForProject': 'Noch keine Devlogs für %s.', + 'devlogs.none': 'Noch keine Devlogs archiviert.', + 'devlogs.permalink': 'Direktlink zu diesem Devlog', + 'devlogs.delete': 'Aus dem Archiv löschen', + 'devlogs.confirmDelete': 'Dieses Devlog aus dem Archiv löschen?', + 'devlogs.deleteFailed': 'Löschen fehlgeschlagen.', + 'devlogs.screenshot': 'Devlog-Screenshot', + + // ── Devlog-Permalink ────────────────────────────── + 'devlog.tag': '// Devlog', + 'devlog.project': 'Projekt: %s', + 'devlog.notFound': 'Devlog nicht gefunden —', + 'devlog.backToArchive': 'zurück zum Archiv', + 'devlog.allDevlogs': 'Alle Devlogs', + 'devlog.copyLink': 'Link kopieren', + 'devlog.linkCopied': 'Link kopiert', + + // ── Roadmap ─────────────────────────────────────── + 'roadmap.tag': '// Wohin die Reise geht', + 'roadmap.subtitle': 'Meilensteine direkt aus der Entwicklung — live aus dem Issue-Tracker.', + 'roadmap.error': 'Roadmap konnte nicht geladen werden.', + 'roadmap.noMilestones': 'Noch keine Meilensteine angelegt.', + 'roadmap.active': '// In Arbeit', + 'roadmap.planned': '// Geplant', + 'roadmap.done': '// Fertig', + 'roadmap.nothingActive': 'Gerade nichts in Arbeit.', + 'roadmap.due': 'Ziel: %s', + 'roadmap.tasks': '%s / %s Aufgaben', + 'roadmap.activity': '// Aktivität — %s Commits in 12 Monaten', + 'roadmap.commitsOn': '%s: %s Commits', + 'roadmap.commitOn': '%s: 1 Commit', + 'roadmap.wishes': '// Community-Wünsche', + 'roadmap.unvote': 'Vote zurücknehmen', + 'roadmap.vote': 'Dafür stimmen', + 'roadmap.ideaPlaceholder': 'Deine Idee für den Server …', + 'roadmap.submit': 'Einreichen', + 'roadmap.hint': 'Eigene Idee? Im Discord %s benutzen — oder hier einloggen und direkt abstimmen.', + 'roadmap.voteMembersOnly': '✗ Voten dürfen nur Discord-Member.', + 'roadmap.tooShort': '✗ Etwas mehr Text bitte.', + 'roadmap.submitted': '✓ Wunsch eingereicht — er steht jetzt auch im Discord zur Abstimmung!', + 'roadmap.submitMembersOnly': '✗ Einreichen dürfen nur Discord-Member.', + 'roadmap.submitFailed': '✗ Konnte den Wunsch nicht einreichen.', + + // ── Server ──────────────────────────────────────── + 'server.tag': '// Live-Status', + 'server.subtitle': 'Alle Game-Server auf einen Blick — aktualisiert alle 2 Minuten.', + 'server.error': 'Server-Status konnte nicht geladen werden.', + 'server.none': 'Noch keine Server eingetragen.', + 'server.online': 'Online', + 'server.offline': 'Offline', + 'server.waiting': 'Warte auf Check', + 'server.players24h': 'Spielerzahl · letzte 24h', + 'server.peak': 'Peak', + 'server.uptime': 'Uptime', + 'server.connect': 'Connect', + 'server.copyAddress': 'Adresse kopieren', + 'server.copied': 'Kopiert!', + 'server.checkedAt': 'Zuletzt geprüft: %s Uhr', + 'server.serversOnline': 'Server online', + 'server.playersNow': 'Spieler gerade drauf', + + // ── Galerie ─────────────────────────────────────── + 'gallery.tag': '// Community', + 'gallery.subtitle': 'Screenshots aus der Community — direkt aus dem Discord.', + 'gallery.error': 'Galerie konnte nicht geladen werden.', + 'gallery.emptyTitle': 'Noch keine Screenshots', + 'gallery.emptyText': 'Alles, was im Screenshot-Kanal landet, erscheint automatisch hier — mitsamt Name und Datum.', + 'gallery.shotBy': 'Screenshot von %s', + + // ── Level ───────────────────────────────────────── + 'level.tag': '// Bestenliste', + 'level.subtitle': 'XP gibt’s fürs Mitreden im Discord — hier stehen die Aktivsten. %s zeigt deinen Platz.', + 'level.error': 'Bestenliste konnte nicht geladen werden.', + 'level.emptyTitle': 'Noch keine XP vergeben', + 'level.emptyText': 'XP gibt es fürs Mitreden im Discord. Sobald geschrieben wird, füllt sich die Bestenliste von selbst.', + 'level.lv': 'Lv. %s', + 'level.activity': '// Server-Aktivität — %s Nachrichten in 30 Tagen · %s Member', + 'level.barTitle': '%s: %s Nachrichten · +%s/−%s Member', + + // ── Events ──────────────────────────────────────── + 'events.tag': '// Termine', + 'events.subtitle': 'Playtests, Streams & Community-Abende — direkt aus dem Discord.', + 'events.error': 'Events konnten nicht geladen werden.', + 'events.emptyTitle': 'Noch keine Events geplant', + 'events.emptyText': 'Playtests, Streams und Community-Runden kündigen wir hier an — und im Discord bekommst du eine Erinnerung.', + 'events.tbd': 'Termin folgt', + 'events.interested': '%s interessiert', + 'events.viewInDiscord': 'Im Discord ansehen', + + // ── Changelog ───────────────────────────────────── + 'changelog.tag': '// Releases', + 'changelog.subtitle': 'Alle Versionen und Release-Notes.', + 'changelog.error': 'Changelog konnte nicht geladen werden.', + 'changelog.emptyTitle': 'Noch keine Releases', + 'changelog.emptyText': 'Sobald eine Version veröffentlicht wird, steht sie hier mit allen Änderungen. Bis dahin steht der Fortschritt im Devlog.', + 'changelog.prerelease': 'Pre-Release', + 'changelog.noNotes': '*Keine Release-Notes.*', + 'changelog.viewInGitea': 'Release in Gitea ansehen', + + // ── Bot-Produktseite: Start ─────────────────────── + 'bot.tag': '// Selbst gebaut, keine Paywall', + 'bot.title1': 'Ein Discord-Bot,', + 'bot.title2': 'der alles macht', + 'bot.subtitle': 'Devlogs, Level, Moderation, Server-Monitoring, Verlosungen, Tickets — was sonst drei Bots mit Abo-Modell erledigen, läuft hier in einem, mit eigenem Webinterface.', + 'bot.allFeatures': 'Alle Funktionen', + 'bot.viewCommands': 'Befehle ansehen', + 'bot.statFeatures': 'Funktionen, einzeln schaltbar', + 'bot.statCommands': 'Slash-Befehle', + 'bot.statGames': 'Spiele im Monitor', + 'bot.statCost': 'Kosten, keine Premium-Stufen', + 'bot.whatItDoes': '// Was er kann', + 'bot.forDevs': '// Für Entwickler', + 'bot.runsFor': 'Der Bot läuft für die D4RKST3R-Community', + 'bot.runsForWith': 'Der Bot läuft für die D4RKST3R-Community mit %s Mitgliedern', + 'bot.toHub': 'zum Community-Hub', + + 'bot.hl.devlogs': 'Devlogs & Feeds', + 'bot.hl.devlogs.text': 'Entwicklungs-Berichte landen als gebrandetes Embed im Discord und werden gleichzeitig durchsuchbar archiviert. Commits und Releases aus Gitea genauso.', + 'bot.hl.monitor': 'Server-Monitor', + 'bot.hl.monitor.text': 'Ü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.', + 'bot.hl.level': 'Level & Aktivität', + 'bot.hl.level.text': 'XP fürs Mitreden mit Rollen-Belohnungen, öffentliche Bestenliste und Aktivitäts-Statistik des Servers.', + 'bot.hl.roles': 'Rollen-Menüs', + 'bot.hl.roles.text': 'Selbstbedienung per Knopfdruck — im Discord und im Browser. Farben pro Knopf, wahlweise exklusiv.', + 'bot.hl.support': 'Support & Kontakt', + 'bot.hl.support.text': 'Tickets als private Threads mit Gesprächsprotokoll, dazu Modmail: eine Direktnachricht an den Bot landet beim Team.', + 'bot.hl.mod': 'Moderation', + 'bot.hl.mod.text': 'Verwarnungen mit Verlauf, Auszeiten, Aufräumen — alles im Mod-Log, dazu Protokoll für Beitritte, Namens- und Rollenwechsel.', + 'bot.hl.giveaway': 'Verlosungen', + 'bot.hl.giveaway.text': 'Teilnahme per Knopf, automatische Ziehung, Neuauslosung wenn sich niemand meldet.', + 'bot.hl.birthday': 'Geburtstage', + 'bot.hl.birthday.text': 'Wer mag, trägt seinen Tag ein — der Bot gratuliert morgens und vergibt eine Rolle für den Tag.', + 'bot.hl.welcome': 'Willkommens-Karten', + 'bot.hl.welcome.text': 'Neue Mitglieder bekommen ein gerendertes Bild mit Avatar und Mitglieds-Nummer.', + 'bot.dev.api': 'API & Single Sign-On', + 'bot.dev.api.text': '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.', + 'bot.dev.host': 'Läuft überall', + 'bot.dev.host.text': 'Node und SQLite in einem Container, ein Volume für die Daten. Nächtliche Sicherungen inklusive — von der Datenbank und allen Repositories.', + + // ── Bot-Produktseite: Funktionen ────────────────── + 'features.tag': '// Alles drin, nichts hinter einer Paywall', + 'features.title': 'Funktionen', + 'features.subtitleN': '%s Funktionen — jede einzeln ein- und ausschaltbar, jede im Browser einstellbar.', + 'features.subtitle': 'Jede Funktion einzeln ein- und ausschaltbar, jede im Browser einstellbar.', + 'features.extras': '// Drumherum', + 'features.outro': '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.', + 'features.overview': 'Zur Übersicht', + 'features.x.brand': 'Webinterface im eigenen Look', + 'features.x.brand.text': 'Jede Funktion wird im Browser eingestellt — keine Konfigurationsdatei, kein Neustart. Farben, Bot-Avatar und Banner gehören dazu, und andere Dienste können denselben Look mitbenutzen.', + 'features.x.texts': 'Texte selbst schreiben', + 'features.x.texts.text': 'Begrüßung, Level-Ansage, Ticket-Texte — alles, was der Bot nach außen schreibt, lässt sich im Panel ändern, mit Platzhaltern für Name, Zahl und Link.', + 'features.x.api': 'API & Single Sign-On', + 'features.x.api.text': 'Eigene Skripte posten über die API v1 mit Bearer-Schlüsseln und Bereichs-Rechten. Andere Dienste nutzen den Discord-Login mit und bekommen die Rollen gleich mitgeliefert.', + 'features.x.pages': 'Eigene Seiten', + 'features.x.pages.text': 'Regeln, Über uns, FAQ — in Markdown geschrieben, mit einem Klick veröffentlicht und automatisch im Menü.', + 'features.x.team': 'Rechte fürs Team', + 'features.x.team.text': 'Team-Mitglieder bekommen gezielten Zugriff auf einzelne Bereiche. Wer was geändert hat, steht im Protokoll.', + 'features.x.host': 'Läuft überall', + 'features.x.host.text': 'Node und SQLite in einem Container, ein Volume für die Daten. Nächtliche Sicherungen inklusive — von der Datenbank und allen Repositories.', + + // ── Bot-Produktseite: Befehle ───────────────────── + 'commands.tag': '// Referenz', + 'commands.title': 'Befehle', + 'commands.subtitle': 'Alle Slash-Befehle im Überblick. Vieles läuft zusätzlich über Knöpfe direkt in den Nachrichten.', + 'commands.note': 'Moderations- und Einrichtungs-Befehle sind Mitgliedern mit den passenden Discord-Rechten vorbehalten.', + 'commands.g.everyone': 'Für alle', + 'commands.g.mod': 'Moderation', + 'commands.g.setup': 'Einrichtung', + 'commands.rank': 'Zeigt deinen Rang, deine XP und den Fortschritt zum nächsten Level.', + 'commands.wunsch': 'Reicht einen Feature-Wunsch ein — landet im Voting-Kanal und auf der Roadmap.', + 'commands.bug': 'Meldet einen Fehler als Issue im Repository. Wird er geschlossen, bekommst du eine Nachricht.', + 'commands.bdaySet': 'Trägt deinen Geburtstag ein. Der Bot gratuliert morgens und vergibt eine Rolle für den Tag.', + 'commands.bdayDel': 'Trägt ihn wieder aus.', + 'commands.remind': 'Erinnert dich per Direktnachricht — etwa „2h" oder „30m".', + 'commands.tag': 'Ruft einen gespeicherten Textbaustein ab, mit Vorschlägen beim Tippen.', + 'commands.ping': 'Lebenszeichen des Bots.', + 'commands.warn': 'Verwarnt jemanden, schickt eine Nachricht und schreibt es ins Protokoll.', + 'commands.warns': 'Zeigt die Verwarnungs-Historie.', + 'commands.timeout': 'Setzt eine Auszeit.', + 'commands.purge': 'Löscht mehrere Nachrichten auf einmal.', + 'commands.giveaway': 'Startet eine Verlosung mit Teilnahme-Knopf und automatischer Ziehung.', + 'commands.ticketSetup': 'Postet den Knopf, über den Support-Tickets als private Threads entstehen.', + 'commands.playtesterSetup': 'Postet den Bewerbungs-Knopf für das Playtester-Programm.', + 'commands.devlogBackfill': 'Holt ältere Devlog-Nachrichten nachträglich ins Archiv.', + 'commands.galerieBackfill': 'Dasselbe für den Screenshot-Kanal.', +}; diff --git a/frontend/src/locales/en.js b/frontend/src/locales/en.js new file mode 100644 index 0000000..5405edb --- /dev/null +++ b/frontend/src/locales/en.js @@ -0,0 +1,339 @@ +// English texts for the public pages. Mirrors de.js key for key — a missing +// key here falls back to the German string, never to a bare key. +export default { + // ── Navigation & chrome ─────────────────────────── + 'nav.devlog': 'Devlog', + 'nav.roadmap': 'Roadmap', + 'nav.server': 'Servers', + 'nav.community': 'Community', + 'nav.gallery': 'Gallery', + 'nav.level': 'Levels', + 'nav.events': 'Events', + 'nav.changelog': 'Changelog', + 'nav.config': 'Config', + 'nav.profile': 'Profile', + 'nav.commits': 'Commits', + 'nav.login': 'Log in', + 'nav.loginDiscord': 'Log in with Discord', + 'nav.logout': 'Log out', + 'nav.features': 'Features', + 'nav.commands': 'Commands', + 'nav.dashboard': 'Dashboard', + 'nav.signIn': 'Sign in', + 'nav.signOut': 'Sign out', + 'nav.menu': 'Menu', + 'footer.source': 'Source', + 'footer.imprint': 'Imprint', + 'footer.privacy': 'Privacy', + 'gate.text': 'Logging in is for members of the Discord server — join the server first, then log in again.', + 'gate.join': 'Join Discord', + 'gate.hide': 'Hide', + + // ── Shared ──────────────────────────────────────── + 'common.loading': 'Loading …', + 'common.newer': 'Newer', + 'common.older': 'Older', + 'common.readDevlogs': 'Read devlogs', + 'common.toRoadmap': 'To the roadmap', + 'common.today': 'today', + 'common.yesterday': 'yesterday', + 'common.daysAgo': '%s days ago', + 'common.monthAgo': '1 month ago', + 'common.monthsAgo': '%s months ago', + + // ── Home (hub) ──────────────────────────────────── + 'home.tag': '// Community hub', + 'home.welcome': 'Welcome', + 'home.at': 'to', + 'home.subtitle': 'Devlogs, roadmap, server status and everything from the community — in one place, straight out of Discord.', + 'home.discord': 'Join Discord', + 'home.readDevlogs': 'Read devlogs', + 'home.members': 'members on Discord', + 'home.serversOnline': 'servers online', + 'home.playersNow': 'players right now', + 'home.services': '// Services', + 'home.latestDevlog': '// Latest devlog', + 'home.more': 'Read more', + 'home.tile.devlog': 'Development updates, fresh from the editor', + 'home.tile.roadmap': 'Milestones plus community wishes to vote on', + 'home.tile.server': 'Live status of every game server, with history', + 'home.tile.gallery': 'Screenshots from the community', + 'home.tile.level': 'XP leaderboard — who talks the most?', + 'home.tile.events': 'Playtests, streams and whatever else is coming up', + + // ── Devlogs ─────────────────────────────────────── + 'devlogs.tag': '// Archive', + 'devlogs.subtitle': 'Development updates, fresh from the editor — archived automatically.', + 'devlogs.all': 'All', + 'devlogs.search': 'Search devlogs …', + 'devlogs.hits': '%s results', + 'devlogs.error': 'Devlogs could not be loaded.', + 'devlogs.noHits': 'No results for “%s”.', + 'devlogs.noneForProject': 'No devlogs for %s yet.', + 'devlogs.none': 'No devlogs archived yet.', + 'devlogs.permalink': 'Direct link to this devlog', + 'devlogs.delete': 'Remove from the archive', + 'devlogs.confirmDelete': 'Remove this devlog from the archive?', + 'devlogs.deleteFailed': 'Deleting failed.', + 'devlogs.screenshot': 'Devlog screenshot', + + // ── Devlog permalink ────────────────────────────── + 'devlog.tag': '// Devlog', + 'devlog.project': 'Project: %s', + 'devlog.notFound': 'Devlog not found —', + 'devlog.backToArchive': 'back to the archive', + 'devlog.allDevlogs': 'All devlogs', + 'devlog.copyLink': 'Copy link', + 'devlog.linkCopied': 'Link copied', + + // ── Roadmap ─────────────────────────────────────── + 'roadmap.tag': '// Where this is heading', + 'roadmap.subtitle': 'Milestones straight from development — live from the issue tracker.', + 'roadmap.error': 'The roadmap could not be loaded.', + 'roadmap.noMilestones': 'No milestones yet.', + 'roadmap.active': '// In progress', + 'roadmap.planned': '// Planned', + 'roadmap.done': '// Done', + 'roadmap.nothingActive': 'Nothing in progress right now.', + 'roadmap.due': 'Due: %s', + 'roadmap.tasks': '%s / %s tasks', + 'roadmap.activity': '// Activity — %s commits in 12 months', + 'roadmap.commitsOn': '%s: %s commits', + 'roadmap.commitOn': '%s: 1 commit', + 'roadmap.wishes': '// Community wishes', + 'roadmap.unvote': 'Take back your vote', + 'roadmap.vote': 'Vote for this', + 'roadmap.ideaPlaceholder': 'Your idea for the server …', + 'roadmap.submit': 'Submit', + 'roadmap.hint': 'Got an idea? Use %s on Discord — or log in here and vote right away.', + 'roadmap.voteMembersOnly': '✗ Only Discord members can vote.', + 'roadmap.tooShort': '✗ A little more text, please.', + 'roadmap.submitted': '✓ Wish submitted — it is up for a vote on Discord too!', + 'roadmap.submitMembersOnly': '✗ Only Discord members can submit.', + 'roadmap.submitFailed': '✗ Could not submit the wish.', + + // ── Servers ─────────────────────────────────────── + 'server.tag': '// Live status', + 'server.subtitle': 'Every game server at a glance — refreshed every 2 minutes.', + 'server.error': 'The server status could not be loaded.', + 'server.none': 'No servers added yet.', + 'server.online': 'Online', + 'server.offline': 'Offline', + 'server.waiting': 'Waiting for check', + 'server.players24h': 'Player count · last 24h', + 'server.peak': 'Peak', + 'server.uptime': 'Uptime', + 'server.connect': 'Connect', + 'server.copyAddress': 'Copy address', + 'server.copied': 'Copied!', + 'server.checkedAt': 'Last checked: %s', + 'server.serversOnline': 'servers online', + 'server.playersNow': 'players right now', + + // ── Gallery ─────────────────────────────────────── + 'gallery.tag': '// Community', + 'gallery.subtitle': 'Screenshots from the community — straight out of Discord.', + 'gallery.error': 'The gallery could not be loaded.', + 'gallery.emptyTitle': 'No screenshots yet', + 'gallery.emptyText': 'Anything posted in the screenshot channel shows up here automatically — name and date included.', + 'gallery.shotBy': 'Screenshot by %s', + + // ── Levels ──────────────────────────────────────── + 'level.tag': '// Leaderboard', + 'level.subtitle': 'XP comes from joining the conversation on Discord — here are the most active. %s shows your rank.', + 'level.error': 'The leaderboard could not be loaded.', + 'level.emptyTitle': 'No XP awarded yet', + 'level.emptyText': 'XP comes from joining the conversation on Discord. As soon as people start writing, the leaderboard fills itself.', + 'level.lv': 'Lv. %s', + 'level.activity': '// Server activity — %s messages in 30 days · %s members', + 'level.barTitle': '%s: %s messages · +%s/−%s members', + + // ── Events ──────────────────────────────────────── + 'events.tag': '// Dates', + 'events.subtitle': 'Playtests, streams & community nights — straight out of Discord.', + 'events.error': 'Events could not be loaded.', + 'events.emptyTitle': 'No events planned yet', + 'events.emptyText': 'We announce playtests, streams and community rounds here — and Discord will remind you.', + 'events.tbd': 'Date to be announced', + 'events.interested': '%s interested', + 'events.viewInDiscord': 'View on Discord', + + // ── Changelog ───────────────────────────────────── + 'changelog.tag': '// Releases', + 'changelog.subtitle': 'Every version and its release notes.', + 'changelog.error': 'The changelog could not be loaded.', + 'changelog.emptyTitle': 'No releases yet', + 'changelog.emptyText': 'As soon as a version ships, it shows up here with all its changes. Until then, the devlog has the progress.', + 'changelog.prerelease': 'Pre-release', + 'changelog.noNotes': '*No release notes.*', + 'changelog.viewInGitea': 'View release on Gitea', + + // ── Bot product page: landing ───────────────────── + 'bot.tag': '// Self-built, no paywall', + 'bot.title1': 'A Discord bot', + 'bot.title2': 'that does it all', + 'bot.subtitle': 'Devlogs, levels, moderation, server monitoring, giveaways, tickets — what usually takes three subscription bots runs here in one, with its own web interface.', + 'bot.allFeatures': 'All features', + 'bot.viewCommands': 'View commands', + 'bot.statFeatures': 'features, each one switchable', + 'bot.statCommands': 'slash commands', + 'bot.statGames': 'games in the monitor', + 'bot.statCost': 'cost, no premium tiers', + 'bot.whatItDoes': '// What it does', + 'bot.forDevs': '// For developers', + 'bot.runsFor': 'The bot runs for the D4RKST3R community', + 'bot.runsForWith': 'The bot runs for the D4RKST3R community with %s members', + 'bot.toHub': 'to the community hub', + + 'bot.hl.devlogs': 'Devlogs & feeds', + 'bot.hl.devlogs.text': 'Development reports land in Discord as a branded embed and get archived searchably at the same time. Commits and releases from Gitea, too.', + 'bot.hl.monitor': 'Server monitor', + 'bot.hl.monitor.text': 'Over 300 games via gamedig, plus FiveM and plain reachability checks. One live embed per server, an alert when one goes down, player count in the bot presence.', + 'bot.hl.level': 'Levels & activity', + 'bot.hl.level.text': 'XP for joining the conversation with role rewards, a public leaderboard and activity stats for the server.', + 'bot.hl.roles': 'Role menus', + 'bot.hl.roles.text': 'Self-service at the press of a button — on Discord and in the browser. A colour per button, exclusive if you want.', + 'bot.hl.support': 'Support & contact', + 'bot.hl.support.text': 'Tickets as private threads with a transcript, plus modmail: a DM to the bot reaches the team.', + 'bot.hl.mod': 'Moderation', + 'bot.hl.mod.text': 'Warnings with history, timeouts, cleanup — all in the mod log, plus a record of joins, name changes and role changes.', + 'bot.hl.giveaway': 'Giveaways', + 'bot.hl.giveaway.text': 'Enter with a button, automatic draw, reroll when nobody answers.', + 'bot.hl.birthday': 'Birthdays', + 'bot.hl.birthday.text': 'Add your day if you like — the bot congratulates you in the morning and hands out a role for the day.', + 'bot.hl.welcome': 'Welcome cards', + 'bot.hl.welcome.text': 'New members get a rendered image with their avatar and member number.', + 'bot.dev.api': 'API & single sign-on', + 'bot.dev.api.text': 'Your own scripts post through API v1 with bearer keys and scoped permissions. Other services reuse the bot’s Discord login and get the roles delivered with it.', + 'bot.dev.host': 'Runs anywhere', + 'bot.dev.host.text': 'Node and SQLite in one container, one volume for the data. Nightly backups included — of the database and every repository.', + + // ── Bot product page: features ──────────────────── + 'features.tag': '// Everything included, nothing behind a paywall', + 'features.title': 'Features', + 'features.subtitleN': '%s features — each one switchable, each one configurable in the browser.', + 'features.subtitle': 'Every feature switchable on its own, every one configurable in the browser.', + 'features.extras': '// Around it', + 'features.outro': 'Built because three bots with three subscriptions for one community is neither tidy nor cheap. The source is open, running it costs one container.', + 'features.overview': 'Back to the overview', + 'features.x.brand': 'Web interface in your own look', + 'features.x.brand.text': 'Every feature is set in the browser — no config file, no restart. Colours, bot avatar and banner included, and other services can share the same look.', + 'features.x.texts': 'Write the texts yourself', + 'features.x.texts.text': 'Welcome message, level announcement, ticket texts — everything the bot says out loud can be changed in the panel, with placeholders for name, number and link.', + 'features.x.api': 'API & single sign-on', + 'features.x.api.text': 'Your own scripts post through API v1 with bearer keys and scoped permissions. Other services reuse the Discord login and get the roles delivered with it.', + 'features.x.pages': 'Your own pages', + 'features.x.pages.text': 'Rules, about us, FAQ — written in Markdown, published with one click and added to the menu automatically.', + 'features.x.team': 'Permissions for the team', + 'features.x.team.text': 'Team members get targeted access to individual areas. Who changed what is in the log.', + 'features.x.host': 'Runs anywhere', + 'features.x.host.text': 'Node and SQLite in one container, one volume for the data. Nightly backups included — of the database and every repository.', + + // ── Module names for the features page ──────────── + // Keyed by module id from src/modules.js. Anything missing here keeps the + // German text the server sends, so a new module never vanishes. + 'fgroup.inhalte': 'Content & feeds', + 'fgroup.community': 'Community', + 'fgroup.moderation': 'Moderation & support', + 'fgroup.server': 'Server & tech', + + 'feat.devlogs': 'Devlogs', + 'feat.devlogs.desc': 'Post development reports as a branded embed and archive them searchably.', + 'feat.commit_feed': 'Commit feed', + 'feat.commit_feed.desc': 'Pushes from Gitea land in the channel as an embed. Archiving happens regardless.', + 'feat.releases': 'Release announcements', + 'feat.releases.desc': 'New releases get announced and land in the public changelog.', + 'feat.weekly_recap': 'Weekly recap', + 'feat.weekly_recap.desc': 'A summary of the week, Sundays at 8 pm.', + 'feat.devlog_threads': 'Devlog threads', + 'feat.devlog_threads.desc': 'A discussion thread opens under every devlog.', + 'feat.social': 'Twitch & YouTube', + 'feat.social.desc': 'Announces live streams and new videos.', + + 'feat.levels': 'Level system', + 'feat.levels.desc': 'XP for joining the conversation, role rewards and a public leaderboard.', + 'feat.starboard': 'Starboard', + 'feat.starboard.desc': 'Messages with enough stars land in a best-of channel.', + 'feat.gallery': 'Screenshot gallery', + 'feat.gallery.desc': 'Images from one channel show up in the public gallery.', + 'feat.voting': 'Feature wishes', + 'feat.voting.desc': 'Submit wishes and vote on them — on Discord and on the roadmap.', + 'feat.giveaways': 'Giveaways', + 'feat.giveaways.desc': 'Enter with a button, automatic draw, reroll available.', + 'feat.birthdays': 'Birthdays', + 'feat.birthdays.desc': 'Congratulations on the big day, with a role for the day if you like.', + 'feat.events': 'Event announcements', + 'feat.events.desc': 'New Discord events get announced and show up on the events page.', + 'feat.playtester': 'Playtester programme', + 'feat.playtester.desc': 'Application button, role and participant list.', + 'feat.alpha_keys': 'Alpha keys', + 'feat.alpha_keys.desc': 'A pool of keys handed out by direct message.', + 'feat.role_menus': 'Role menus', + 'feat.role_menus.desc': 'Self-service roles at the press of a button — on Discord and in the profile.', + 'feat.applications': 'Applications', + 'feat.applications.desc': 'Forms as input dialogs, review with accept and reject.', + + 'feat.moderation': 'Moderation', + 'feat.moderation.desc': 'Warnings, timeouts, cleanup — with history and a log.', + 'feat.modlog': 'Audit log', + 'feat.modlog.desc': 'Deleted and edited messages, joins, name and role changes.', + 'feat.modmail': 'Modmail', + 'feat.modmail.desc': 'Direct messages to the bot reach the team as a thread.', + 'feat.tickets': 'Tickets', + 'feat.tickets.desc': 'Private threads at the press of a button, with a transcript on close.', + 'feat.welcome': 'Welcome cards', + 'feat.welcome.desc': 'New members are greeted with a rendered image.', + 'feat.autorole': 'Auto role', + 'feat.autorole.desc': 'New members automatically get a starting role.', + 'feat.sticky_roles': 'Sticky roles', + 'feat.sticky_roles.desc': 'Leave the server and come back — your roles come back with you.', + 'feat.bug_reports': 'Bug reports', + 'feat.bug_reports.desc': 'Reports become issues in the repository, with a reply when they close.', + + 'feat.server_monitor': 'Server monitor', + 'feat.server_monitor.desc': 'Live status of the game servers with history and downtime alerts.', + 'feat.watchdog': 'Reachability watchdog', + 'feat.watchdog.desc': 'Checks the addresses you enter and reports outages by direct message.', + 'feat.temp_voice': 'Temporary voice channels', + 'feat.temp_voice.desc': 'Joining the hub creates your own channel with a control panel.', + 'feat.triggers': 'Auto replies', + 'feat.triggers.desc': 'The bot answers to keywords you set.', + 'feat.reminders': 'Reminders', + 'feat.reminders.desc': 'Members can have themselves reminded by direct message.', + 'feat.scheduled_posts': 'Scheduled posts', + 'feat.scheduled_posts.desc': 'Messages at fixed times, once or recurring.', + 'feat.tags': 'Text snippets', + 'feat.tags.desc': 'Call up saved texts with a command.', + 'feat.backups': 'Database backup', + 'feat.backups.desc': 'A nightly backup kept for 14 days.', + 'feat.repo_backups': 'Repository backup', + 'feat.repo_backups.desc': 'Back up every Gitea repository as a bundle.', + 'feat.member_gate': 'Members-only login', + 'feat.member_gate.desc': 'Only people on the Discord server can sign in on the web.', + + // ── Bot product page: commands ──────────────────── + 'commands.tag': '// Reference', + 'commands.title': 'Commands', + 'commands.subtitle': 'Every slash command at a glance. A lot also runs through buttons right inside the messages.', + 'commands.note': 'Moderation and setup commands are reserved for members with the matching Discord permissions.', + 'commands.g.everyone': 'For everyone', + 'commands.g.mod': 'Moderation', + 'commands.g.setup': 'Setup', + 'commands.rank': 'Shows your rank, your XP and the progress to the next level.', + 'commands.wunsch': 'Submits a feature wish — lands in the voting channel and on the roadmap.', + 'commands.bug': 'Reports a bug as an issue in the repository. When it is closed, you get a message.', + 'commands.bdaySet': 'Saves your birthday. The bot congratulates you in the morning and hands out a role for the day.', + 'commands.bdayDel': 'Removes it again.', + 'commands.remind': 'Reminds you by direct message — something like “2h” or “30m”.', + 'commands.tag': 'Fetches a saved text snippet, with suggestions as you type.', + 'commands.ping': 'A sign of life from the bot.', + 'commands.warn': 'Warns someone, sends them a message and writes it to the log.', + 'commands.warns': 'Shows the warning history.', + 'commands.timeout': 'Puts someone in timeout.', + 'commands.purge': 'Deletes several messages at once.', + 'commands.giveaway': 'Starts a giveaway with an entry button and an automatic draw.', + 'commands.ticketSetup': 'Posts the button that turns support tickets into private threads.', + 'commands.playtesterSetup': 'Posts the application button for the playtester programme.', + 'commands.devlogBackfill': 'Pulls older devlog messages into the archive after the fact.', + 'commands.galerieBackfill': 'The same for the screenshot channel.', +}; diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 11a6edb..142c203 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -4,6 +4,7 @@ import { BrowserRouter } from 'react-router-dom'; import App from './App.jsx'; import BotApp from './BotApp.jsx'; import { IS_BOT } from './site.js'; +import { LangProvider } from './i18n.jsx'; import '@fontsource/bebas-neue'; import '@fontsource/barlow-condensed/300.css'; import '@fontsource/barlow-condensed/400.css'; @@ -16,7 +17,9 @@ import './style.css'; ReactDOM.createRoot(document.getElementById('root')).render( - {IS_BOT ? : } + + {IS_BOT ? : } + ); diff --git a/frontend/src/pages/BotCommands.jsx b/frontend/src/pages/BotCommands.jsx index 3885a9b..63a219a 100644 --- a/frontend/src/pages/BotCommands.jsx +++ b/frontend/src/pages/BotCommands.jsx @@ -1,34 +1,37 @@ // Übersicht aller Slash-Befehle des Bots. import { useEffect } from 'react'; import { IconLock } from '../icons.jsx'; +import { useT } from '../i18n.jsx'; +// Befehlsname bleibt wie er ist — nur die Erklärung wird übersetzt. const GROUPS = [ - ['Für alle', [ - ['/rank', 'Zeigt deinen Rang, deine XP und den Fortschritt zum nächsten Level.'], - ['/wunsch idee:…', 'Reicht einen Feature-Wunsch ein — landet im Voting-Kanal und auf der Roadmap.'], - ['/bug titel:… beschreibung:…', 'Meldet einen Fehler als Issue im Repository. Wird er geschlossen, bekommst du eine Nachricht.'], - ['/geburtstag setzen tag:… monat:…', 'Trägt deinen Geburtstag ein. Der Bot gratuliert morgens und vergibt eine Rolle für den Tag.'], - ['/geburtstag entfernen', 'Trägt ihn wieder aus.'], - ['/remind dauer:… text:…', 'Erinnert dich per Direktnachricht — etwa „2h" oder „30m".'], - ['/tag name:…', 'Ruft einen gespeicherten Textbaustein ab, mit Vorschlägen beim Tippen.'], - ['/ping', 'Lebenszeichen des Bots.'], + ['commands.g.everyone', false, [ + ['/rank', 'commands.rank'], + ['/wunsch idee:…', 'commands.wunsch'], + ['/bug titel:… beschreibung:…', 'commands.bug'], + ['/geburtstag setzen tag:… monat:…', 'commands.bdaySet'], + ['/geburtstag entfernen', 'commands.bdayDel'], + ['/remind dauer:… text:…', 'commands.remind'], + ['/tag name:…', 'commands.tag'], + ['/ping', 'commands.ping'], ]], - ['Moderation', [ - ['/warn user:… grund:…', 'Verwarnt jemanden, schickt eine Nachricht und schreibt es ins Protokoll.'], - ['/warns user:…', 'Zeigt die Verwarnungs-Historie.'], - ['/timeout user:… dauer:…', 'Setzt eine Auszeit.'], - ['/purge anzahl:…', 'Löscht mehrere Nachrichten auf einmal.'], + ['commands.g.mod', true, [ + ['/warn user:… grund:…', 'commands.warn'], + ['/warns user:…', 'commands.warns'], + ['/timeout user:… dauer:…', 'commands.timeout'], + ['/purge anzahl:…', 'commands.purge'], ]], - ['Einrichtung', [ - ['/giveaway preis:… dauer:… gewinner:…', 'Startet eine Verlosung mit Teilnahme-Knopf und automatischer Ziehung.'], - ['/ticket-setup', 'Postet den Knopf, über den Support-Tickets als private Threads entstehen.'], - ['/playtester-setup', 'Postet den Bewerbungs-Knopf für das Playtester-Programm.'], - ['/devlog-backfill', 'Holt ältere Devlog-Nachrichten nachträglich ins Archiv.'], - ['/galerie-backfill', 'Dasselbe für den Screenshot-Kanal.'], + ['commands.g.setup', true, [ + ['/giveaway preis:… dauer:… gewinner:…', 'commands.giveaway'], + ['/ticket-setup', 'commands.ticketSetup'], + ['/playtester-setup', 'commands.playtesterSetup'], + ['/devlog-backfill', 'commands.devlogBackfill'], + ['/galerie-backfill', 'commands.galerieBackfill'], ]], ]; export default function BotCommands() { + const { t } = useT(); useEffect(() => { window.scrollTo(0, 0); }, []); return ( @@ -37,37 +40,30 @@ export default function BotCommands() {
BEFEHLE