Englisch als zweite Sprache fuer die oeffentlichen Seiten
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:
@@ -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} />
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user