Englisch als zweite Sprache fuer die oeffentlichen Seiten
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled

Hub und Bot-Produktseite lassen sich jetzt auf Englisch umstellen. Die Wahl
haengt am Browser (Deutsch nur, wenn der Browser es will) und bleibt im
localStorage stehen.

- src/i18n.jsx: Context mit t(), tOr(), Datums- und Zahlenformat der Sprache.
  Keine Bibliothek, die Woerterbuecher werden mitgebaut — kein Nachlade-Blitzer.
- Platzhalter im %s-Stil, dieselbe Schreibweise wie in den Bot-Texten.
- Umschalter in der Navigation, zusaetzlich im Schubladen-Menue: .auth ist auf
  schmalen Schirmen ausgeblendet, sonst waere der Wechsel dort unerreichbar.
- Datums- und Zahlenformat folgen mit (31.07.2026 / 31/07/2026, 45.231 / 45,231).
- Die Funktionsliste kommt weiter deutsch vom Server; englische Modulnamen
  liegen unter feat.<id> im Woerterbuch. Fehlt einer, bleibt der Servertext
  stehen — ein neues Modul verschwindet nie von der Seite.

Das Config-Panel und die Discord-Texte bleiben deutsch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 22:41:32 +02:00
co-authored by Claude Fable 5
parent 570e45fe60
commit 5360c8229f
19 changed files with 1084 additions and 329 deletions
+29 -26
View File
@@ -3,8 +3,9 @@ import { apiGet, apiPost } from '../api.js';
import { Markdown } from '../markdown.jsx';
import { SkeletonCards } from '../components/Skeleton.jsx';
import { IconThumbsUp } from '../icons.jsx';
import { useT, useFmt } from '../i18n.jsx';
const dueFmt = new Intl.DateTimeFormat('de-DE', { month: 'long', year: 'numeric' });
const ZIEL = { month: 'long', year: 'numeric' };
/** Milestone-Gruppe bestimmen: Fertig / In Arbeit / Geplant */
function groupOf(m) {
@@ -13,12 +14,14 @@ function groupOf(m) {
}
const GROUPS = [
{ id: 'active', tag: '// In Arbeit', empty: 'Gerade nichts in Arbeit.' },
{ id: 'planned', tag: '// Geplant', empty: null },
{ id: 'done', tag: '// Fertig', empty: null },
{ id: 'active', tag: 'roadmap.active', empty: 'roadmap.nothingActive' },
{ id: 'planned', tag: 'roadmap.planned', empty: null },
{ id: 'done', tag: 'roadmap.done', empty: null },
];
function Milestone({ m }) {
const { t } = useT();
const { datum } = useFmt();
const total = m.open + m.closed;
const pct = total > 0 ? Math.round((m.closed / total) * 100) : m.state === 'closed' ? 100 : 0;
return (
@@ -26,7 +29,7 @@ function Milestone({ m }) {
<div className="card-meta">
<span className="milestone-title">{m.title}</span>
{m.due_on && m.state !== 'closed' && (
<span className="card-rel">Ziel: {dueFmt.format(new Date(m.due_on))}</span>
<span className="card-rel">{t('roadmap.due', datum(m.due_on, ZIEL))}</span>
)}
</div>
{m.description && (
@@ -39,7 +42,7 @@ function Milestone({ m }) {
</div>
<div className="progress-info">
<span>{pct}%</span>
{total > 0 && <span>{m.closed} / {total} Aufgaben</span>}
{total > 0 && <span>{t('roadmap.tasks', m.closed, total)}</span>}
</div>
</article>
);
@@ -47,6 +50,7 @@ function Milestone({ m }) {
/** GitHub-Style-Heatmap: 52 Wochen × 7 Tage */
function Heatmap({ days }) {
const { t } = useT();
const byDay = new Map(days.map((d) => [d.day, d.n]));
const max = Math.max(1, ...days.map((d) => d.n));
const today = new Date();
@@ -67,7 +71,7 @@ function Heatmap({ days }) {
<span
key={d}
className={`hm-cell hm-${level}`}
title={`${key}: ${n} Commit${n === 1 ? '' : 's'}`}
title={n === 1 ? t('roadmap.commitOn', key) : t('roadmap.commitsOn', key, n)}
/>
);
}
@@ -76,13 +80,14 @@ function Heatmap({ days }) {
const total = days.reduce((sum, d) => sum + d.n, 0);
return (
<div className="roadmap-group">
<h2 className="settings-title">// Aktivität — {total} Commits in 12 Monaten</h2>
<h2 className="settings-title">{t('roadmap.activity', total)}</h2>
<div className="hm-wrap">{weeks}</div>
</div>
);
}
export default function Roadmap() {
const { t } = useT();
const [data, setData] = useState(null);
const [wishes, setWishes] = useState([]);
const [canVote, setCanVote] = useState(false);
@@ -105,20 +110,20 @@ export default function Roadmap() {
? w
: { ...w, voted: res.voted, score: w.score + (res.voted ? 1 : -1) }));
} catch (e) {
if (e.status === 403) setSubmitMsg('✗ Voten dürfen nur Discord-Member.');
if (e.status === 403) setSubmitMsg(t('roadmap.voteMembersOnly'));
}
}
async function submitWish() {
const text = idea.trim();
if (text.length < 5) { setSubmitMsg('✗ Etwas mehr Text bitte.'); return; }
if (text.length < 5) { setSubmitMsg(t('roadmap.tooShort')); return; }
try {
await apiPost('/api/wishes', { idea: text });
setIdea('');
setSubmitMsg('✓ Wunsch eingereicht — er steht jetzt auch im Discord zur Abstimmung!');
setSubmitMsg(t('roadmap.submitted'));
apiGet('/api/wishes').then((d) => setWishes(d.wishes)).catch(() => {});
} catch (e) {
setSubmitMsg(e.status === 403 ? '✗ Einreichen dürfen nur Discord-Member.' : '✗ Konnte den Wunsch nicht einreichen.');
setSubmitMsg(e.status === 403 ? t('roadmap.submitMembersOnly') : t('roadmap.submitFailed'));
}
}
@@ -128,32 +133,30 @@ export default function Roadmap() {
<div className="page-bg-text">ROADMAP</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">// Wohin die Reise geht</div>
<h1 className="page-title">Roadmap</h1>
<p className="page-subtitle">
Meilensteine direkt aus der Entwicklung live aus dem Issue-Tracker.
</p>
<div className="page-tag">{t('roadmap.tag')}</div>
<h1 className="page-title">{t('nav.roadmap')}</h1>
<p className="page-subtitle">{t('roadmap.subtitle')}</p>
</div>
</section>
<section className="content">
{error && <p className="notice">Roadmap konnte nicht geladen werden.</p>}
{error && <p className="notice">{t('roadmap.error')}</p>}
{!error && !data && <SkeletonCards n={3} />}
{data?.milestones.length === 0 && (
<p className="notice">Noch keine Meilensteine angelegt.</p>
<p className="notice">{t('roadmap.noMilestones')}</p>
)}
{heatmap && heatmap.length > 0 && <Heatmap days={heatmap} />}
{(wishes.length > 0 || canVote) && (
<div className="roadmap-group">
<h2 className="settings-title">// Community-Wünsche</h2>
<h2 className="settings-title">{t('roadmap.wishes')}</h2>
{wishes.map((w, i) => (
<div className="wish-row" key={w.message_id ?? `${w.created_at}-${i}`}>
{canVote && w.message_id ? (
<button
className={`wish-vote with-icon${w.voted ? ' voted' : ''}`}
title={w.voted ? 'Vote zurücknehmen' : 'Dafür stimmen'}
title={w.voted ? t('roadmap.unvote') : t('roadmap.vote')}
onClick={() => vote(w.message_id)}
>
<IconThumbsUp size={14} /> {w.score}
@@ -168,16 +171,16 @@ export default function Roadmap() {
{canVote ? (
<div className="wish-form">
<input
type="text" maxLength={500} placeholder="Deine Idee für den Server …"
type="text" maxLength={500} placeholder={t('roadmap.ideaPlaceholder')}
value={idea}
onChange={(e) => setIdea(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') submitWish(); }}
/>
<button className="btn btn-save" onClick={submitWish}>Einreichen</button>
<button className="btn btn-save" onClick={submitWish}>{t('roadmap.submit')}</button>
</div>
) : (
<p className="notice" style={{ padding: '.6rem 0 0' }}>
Eigene Idee? Im Discord <code>/wunsch</code> benutzen oder hier einloggen und direkt abstimmen.
{t('roadmap.hint').split('%s')[0]}<code>/wunsch</code>{t('roadmap.hint').split('%s')[1]}
</p>
)}
{submitMsg && <p className="notice" style={{ padding: '.6rem 0 0' }}>{submitMsg}</p>}
@@ -189,8 +192,8 @@ export default function Roadmap() {
if (items.length === 0 && !empty) return null;
return (
<div className="roadmap-group" key={id}>
<h2 className="settings-title">{tag}</h2>
{items.length === 0 && <p className="notice">{empty}</p>}
<h2 className="settings-title">{t(tag)}</h2>
{items.length === 0 && <p className="notice">{t(empty)}</p>}
{items.map((m) => (
<Milestone m={m} key={m.title} />
))}