feat(augur): real i18n keys — replace T constants with $_('augur.*')

The M1–M6 build shipped strings in `T = {} as const` constants — clean
enough to keep the i18n-hardcoded baseline at zero, but never the
real plan. This commit closes the loop:

  - Add per-locale namespace `augur` with five bundles
    (de / en / fr / it / es). DE + EN are translated; FR/IT/ES mirror
    DE for parity until proper translation lands. Same staging that
    other modules use.
  - Replace every `T.x` with `$_('augur.section.x')` across 8 svelte
    files + 2 routes. Drop the const blocks where they were the only
    reason a script tag had untyped state.
  - Existing `KIND_LABELS[k].de` / `VIBE_LABELS` / etc. stay — they
    serve dual-mode (web + tools.ts), and the module convention is
    nested-locale records rather than svelte-i18n keys for those.

Validators:
  - i18n-parity:    36 namespaces × 5 locales — 2852 keys aligned
  - i18n-keys:      315 missing (== baseline) — augur adds zero
  - i18n-hardcoded: augur not flagged

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-25 18:17:07 +02:00
parent 303058d406
commit c5ff7e1d33
14 changed files with 899 additions and 237 deletions

View file

@ -0,0 +1,152 @@
{
"app": {
"title": "Augur",
"tagline": "Zeichen sammeln, Muster lesen."
},
"mode": {
"witness": "Witness",
"oracle": "Oracle",
"witnessHint": "Zeichen sammeln",
"oracleHint": "Muster lesen"
},
"witness": {
"searchPlaceholder": "Suche nach Quelle, Aussage, Deutung ...",
"newOpen": "+ neu",
"newClose": "× schliessen",
"emptyAll": "Noch keine Zeichen erfasst. Sammle erst — auswerten kommt spaeter.",
"emptyFiltered": "Keine passenden Zeichen.",
"openOnly": "nur offene",
"dueOnly": "nur faellige",
"openHint": "noch offen"
},
"due": {
"single": "Zeichen wartet auf Aufloesung",
"plural": "Zeichen warten auf Aufloesung",
"hide": "verbergen",
"show": "oeffnen",
"yes": "ja",
"partly": "teils",
"no": "nein",
"overdue": "Tage faellig",
"dueToday": "heute"
},
"oracle": {
"title": "Oracle",
"subtitle": "Was deine Daten zurueck sagen.",
"yearRecapLink": "Jahresrueckblick →",
"coldStart": "Noch zu wenig aufgeloeste Zeichen.",
"coldStartHint": "Sammle und loese mindestens",
"coldStartUnit": "Zeichen auf, dann zeigt das Orakel verlaessliche Zahlen.",
"statTotal": "gesammelt",
"statResolved": "aufgeloest",
"statOpen": "offen",
"statHitRate": "Trefferquote",
"statBrier": "Brier-Score",
"statBrierBaseline": "Baseline 0.25 (50/50)",
"sourceTitle": "Forecaster in deinem Leben",
"sourceSub": "Wer / was war wie oft richtig?",
"sourceCol": "Quelle",
"sourceN": "n",
"sourceHit": "Treffer",
"sourceBrier": "Brier",
"sourceMix": "Verteilung",
"vibeTitle": "Stimmt dein Bauchgefuehl?",
"vibeSub": "Treffer pro Stimmung — gut, raetselhaft, schlecht.",
"vibeNoData": "noch keine Daten",
"vibeHit": "Treffer",
"vibeDir": "Richtung",
"corrTitle": "Was um deine Zeichen herum passiert",
"corrSub": "Korrelation, nicht Kausalitaet — gefunden in deinen eigenen Daten.",
"corrEmpty": "Noch keine belastbaren Muster. Logge weiter — Mood und Sleep werden dazu gerechnet.",
"corrAfter": "In den 3 Tagen danach",
"corrMoodLevel": "lag dein Mood-Level bei",
"corrSleepQuality": "lag deine Schlaf-Qualitaet bei",
"corrSleepDuration": "lag deine Schlafdauer bei",
"corrVsBaseline": "Baseline:"
},
"detail": {
"source": "Quelle",
"claim": "Aussage",
"felt": "Eigene Deutung",
"expected": "Erwartetes Ergebnis",
"expectedBy": "Bis",
"probability": "Wahrscheinlichkeit",
"tags": "Tags",
"captured": "Erfasst",
"resolved": "Aufgeloest",
"outcomeNote": "Wie es kam",
"resolvePrompt": "Hat sich das bewahrheitet?",
"resolveYes": "eingetreten",
"resolvePartly": "teilweise",
"resolveNo": "nicht eingetreten",
"resolveReopen": "erneut oeffnen",
"actionEdit": "bearbeiten",
"actionArchive": "archivieren",
"actionDelete": "loeschen",
"actionCancel": "abbrechen",
"notePlaceholder": "Optionale Notiz: wie genau ist es gekommen?",
"confirmDelete": "Diesen Eintrag wirklich loeschen?",
"visibility": "Sichtbarkeit"
},
"form": {
"kind": "Art",
"source": "Quelle",
"category": "Kategorie",
"claim": "Was sagt das Zeichen?",
"vibe": "Stimmung",
"feltMeaning": "Eigene Deutung (optional)",
"expectedOutcome": "Was sollte konkret passieren? (optional)",
"expectedBy": "Bis wann? (optional)",
"probability": "Wahrscheinlichkeit (optional, 0-100%)",
"tags": "Tags (komma-getrennt)",
"encounteredAt": "Wann erlebt?",
"sourcePlaceholder": "z. B. schwarze Katze, Glueckskeks, Bauchgefuehl",
"claimPlaceholder": "z. B. heute kommt eine gute Nachricht",
"feltPlaceholder": "Was es fuer mich bedeutet ...",
"expectedPlaceholder": "z. B. Job-Zusage bis Freitag",
"tagsPlaceholder": "arbeit, naturzeichen ...",
"submitCreate": "+ erfassen",
"submitUpdate": "speichern",
"cancel": "abbrechen",
"more": "+ Prognose & Tags"
},
"recap": {
"title": "Jahresrueckblick",
"yearTotal": "Zeichen",
"yearResolved": "aufgeloest",
"yearHitRate": "Trefferquote",
"emptyYear": "In diesem Jahr noch keine Zeichen erfasst.",
"distKind": "Nach Art",
"distVibe": "Nach Stimmung",
"distOutcome": "Nach Ergebnis",
"bestSource": "Bester Forecaster",
"worstSource": "Unzuverlaessigster Forecaster",
"topSources": "Meistgenutzte Quellen",
"mostFulfilled": "Eingetretene Zeichen",
"mostSurprising": "Ueberraschungen — wo dein Gefuehl danebenlag",
"none": "—",
"hitOf": "von",
"matches": "Treffer"
},
"oracleHint": {
"titleLive": "Was deine Daten zurueck sagen",
"titleSnapshot": "Was das Orakel damals sagte",
"matchCount": "aehnliche Zeichen",
"hit": "eingetreten",
"partly": "teilweise",
"no": "nicht eingetreten"
},
"route": {
"detailFallbackTitle": "Augur",
"detailRouteTitle": "Zeichen",
"loading": "laedt ...",
"notFound": "Eintrag nicht gefunden.",
"backLink": "← zurueck",
"recapInvalid": "Ungueltiges Jahr.",
"recapBack": "← zurueck"
},
"common": {
"all": "alle",
"brierUnknown": ""
}
}

View file

@ -0,0 +1,152 @@
{
"app": {
"title": "Augur",
"tagline": "Collect signs, read patterns."
},
"mode": {
"witness": "Witness",
"oracle": "Oracle",
"witnessHint": "collect signs",
"oracleHint": "read patterns"
},
"witness": {
"searchPlaceholder": "Search source, claim, meaning ...",
"newOpen": "+ new",
"newClose": "× close",
"emptyAll": "No signs captured yet. Collect first — analysis comes later.",
"emptyFiltered": "No matching signs.",
"openOnly": "open only",
"dueOnly": "due only",
"openHint": "still open"
},
"due": {
"single": "sign waiting for resolution",
"plural": "signs waiting for resolution",
"hide": "hide",
"show": "show",
"yes": "yes",
"partly": "partly",
"no": "no",
"overdue": "days overdue",
"dueToday": "today"
},
"oracle": {
"title": "Oracle",
"subtitle": "What your data says back.",
"yearRecapLink": "Year in review →",
"coldStart": "Not enough resolved signs yet.",
"coldStartHint": "Collect and resolve at least",
"coldStartUnit": "signs, then the oracle shows reliable numbers.",
"statTotal": "collected",
"statResolved": "resolved",
"statOpen": "open",
"statHitRate": "hit rate",
"statBrier": "Brier score",
"statBrierBaseline": "Baseline 0.25 (50/50)",
"sourceTitle": "Forecasters in your life",
"sourceSub": "Who / what was right how often?",
"sourceCol": "Source",
"sourceN": "n",
"sourceHit": "Hit",
"sourceBrier": "Brier",
"sourceMix": "Distribution",
"vibeTitle": "Does your gut feeling check out?",
"vibeSub": "Hits per vibe — good, mysterious, bad.",
"vibeNoData": "no data yet",
"vibeHit": "Hit",
"vibeDir": "Direction",
"corrTitle": "What happens around your signs",
"corrSub": "Correlation, not causation — found in your own data.",
"corrEmpty": "No reliable patterns yet. Keep logging — mood and sleep are factored in.",
"corrAfter": "In the 3 days after",
"corrMoodLevel": "your mood level was",
"corrSleepQuality": "your sleep quality was",
"corrSleepDuration": "your sleep duration was",
"corrVsBaseline": "Baseline:"
},
"detail": {
"source": "Source",
"claim": "Claim",
"felt": "Your interpretation",
"expected": "Expected outcome",
"expectedBy": "By",
"probability": "Probability",
"tags": "Tags",
"captured": "Captured",
"resolved": "Resolved",
"outcomeNote": "How it went",
"resolvePrompt": "Did it come true?",
"resolveYes": "fulfilled",
"resolvePartly": "partly",
"resolveNo": "not fulfilled",
"resolveReopen": "reopen",
"actionEdit": "edit",
"actionArchive": "archive",
"actionDelete": "delete",
"actionCancel": "cancel",
"notePlaceholder": "Optional note: how exactly did it play out?",
"confirmDelete": "Really delete this entry?",
"visibility": "Visibility"
},
"form": {
"kind": "Kind",
"source": "Source",
"category": "Category",
"claim": "What does the sign say?",
"vibe": "Vibe",
"feltMeaning": "Your interpretation (optional)",
"expectedOutcome": "What concretely should happen? (optional)",
"expectedBy": "By when? (optional)",
"probability": "Probability (optional, 0-100%)",
"tags": "Tags (comma-separated)",
"encounteredAt": "When experienced?",
"sourcePlaceholder": "e.g. black cat, fortune cookie, gut feeling",
"claimPlaceholder": "e.g. good news will arrive today",
"feltPlaceholder": "What it means to me ...",
"expectedPlaceholder": "e.g. job offer by Friday",
"tagsPlaceholder": "work, omens ...",
"submitCreate": "+ capture",
"submitUpdate": "save",
"cancel": "cancel",
"more": "+ Prediction & Tags"
},
"recap": {
"title": "Year in review",
"yearTotal": "signs",
"yearResolved": "resolved",
"yearHitRate": "hit rate",
"emptyYear": "No signs captured this year.",
"distKind": "By kind",
"distVibe": "By vibe",
"distOutcome": "By outcome",
"bestSource": "Best forecaster",
"worstSource": "Most unreliable forecaster",
"topSources": "Most-used sources",
"mostFulfilled": "Fulfilled signs",
"mostSurprising": "Surprises — where your gut got it wrong",
"none": "—",
"hitOf": "of",
"matches": "Hits"
},
"oracleHint": {
"titleLive": "What your data says back",
"titleSnapshot": "What the oracle said then",
"matchCount": "similar signs",
"hit": "fulfilled",
"partly": "partly",
"no": "not fulfilled"
},
"route": {
"detailFallbackTitle": "Augur",
"detailRouteTitle": "Sign",
"loading": "loading ...",
"notFound": "Entry not found.",
"backLink": "← back",
"recapInvalid": "Invalid year.",
"recapBack": "← back"
},
"common": {
"all": "all",
"brierUnknown": ""
}
}

View file

@ -0,0 +1,152 @@
{
"app": {
"title": "Augur",
"tagline": "Zeichen sammeln, Muster lesen."
},
"mode": {
"witness": "Witness",
"oracle": "Oracle",
"witnessHint": "Zeichen sammeln",
"oracleHint": "Muster lesen"
},
"witness": {
"searchPlaceholder": "Suche nach Quelle, Aussage, Deutung ...",
"newOpen": "+ neu",
"newClose": "× schliessen",
"emptyAll": "Noch keine Zeichen erfasst. Sammle erst — auswerten kommt spaeter.",
"emptyFiltered": "Keine passenden Zeichen.",
"openOnly": "nur offene",
"dueOnly": "nur faellige",
"openHint": "noch offen"
},
"due": {
"single": "Zeichen wartet auf Aufloesung",
"plural": "Zeichen warten auf Aufloesung",
"hide": "verbergen",
"show": "oeffnen",
"yes": "ja",
"partly": "teils",
"no": "nein",
"overdue": "Tage faellig",
"dueToday": "heute"
},
"oracle": {
"title": "Oracle",
"subtitle": "Was deine Daten zurueck sagen.",
"yearRecapLink": "Jahresrueckblick →",
"coldStart": "Noch zu wenig aufgeloeste Zeichen.",
"coldStartHint": "Sammle und loese mindestens",
"coldStartUnit": "Zeichen auf, dann zeigt das Orakel verlaessliche Zahlen.",
"statTotal": "gesammelt",
"statResolved": "aufgeloest",
"statOpen": "offen",
"statHitRate": "Trefferquote",
"statBrier": "Brier-Score",
"statBrierBaseline": "Baseline 0.25 (50/50)",
"sourceTitle": "Forecaster in deinem Leben",
"sourceSub": "Wer / was war wie oft richtig?",
"sourceCol": "Quelle",
"sourceN": "n",
"sourceHit": "Treffer",
"sourceBrier": "Brier",
"sourceMix": "Verteilung",
"vibeTitle": "Stimmt dein Bauchgefuehl?",
"vibeSub": "Treffer pro Stimmung — gut, raetselhaft, schlecht.",
"vibeNoData": "noch keine Daten",
"vibeHit": "Treffer",
"vibeDir": "Richtung",
"corrTitle": "Was um deine Zeichen herum passiert",
"corrSub": "Korrelation, nicht Kausalitaet — gefunden in deinen eigenen Daten.",
"corrEmpty": "Noch keine belastbaren Muster. Logge weiter — Mood und Sleep werden dazu gerechnet.",
"corrAfter": "In den 3 Tagen danach",
"corrMoodLevel": "lag dein Mood-Level bei",
"corrSleepQuality": "lag deine Schlaf-Qualitaet bei",
"corrSleepDuration": "lag deine Schlafdauer bei",
"corrVsBaseline": "Baseline:"
},
"detail": {
"source": "Quelle",
"claim": "Aussage",
"felt": "Eigene Deutung",
"expected": "Erwartetes Ergebnis",
"expectedBy": "Bis",
"probability": "Wahrscheinlichkeit",
"tags": "Tags",
"captured": "Erfasst",
"resolved": "Aufgeloest",
"outcomeNote": "Wie es kam",
"resolvePrompt": "Hat sich das bewahrheitet?",
"resolveYes": "eingetreten",
"resolvePartly": "teilweise",
"resolveNo": "nicht eingetreten",
"resolveReopen": "erneut oeffnen",
"actionEdit": "bearbeiten",
"actionArchive": "archivieren",
"actionDelete": "loeschen",
"actionCancel": "abbrechen",
"notePlaceholder": "Optionale Notiz: wie genau ist es gekommen?",
"confirmDelete": "Diesen Eintrag wirklich loeschen?",
"visibility": "Sichtbarkeit"
},
"form": {
"kind": "Art",
"source": "Quelle",
"category": "Kategorie",
"claim": "Was sagt das Zeichen?",
"vibe": "Stimmung",
"feltMeaning": "Eigene Deutung (optional)",
"expectedOutcome": "Was sollte konkret passieren? (optional)",
"expectedBy": "Bis wann? (optional)",
"probability": "Wahrscheinlichkeit (optional, 0-100%)",
"tags": "Tags (komma-getrennt)",
"encounteredAt": "Wann erlebt?",
"sourcePlaceholder": "z. B. schwarze Katze, Glueckskeks, Bauchgefuehl",
"claimPlaceholder": "z. B. heute kommt eine gute Nachricht",
"feltPlaceholder": "Was es fuer mich bedeutet ...",
"expectedPlaceholder": "z. B. Job-Zusage bis Freitag",
"tagsPlaceholder": "arbeit, naturzeichen ...",
"submitCreate": "+ erfassen",
"submitUpdate": "speichern",
"cancel": "abbrechen",
"more": "+ Prognose & Tags"
},
"recap": {
"title": "Jahresrueckblick",
"yearTotal": "Zeichen",
"yearResolved": "aufgeloest",
"yearHitRate": "Trefferquote",
"emptyYear": "In diesem Jahr noch keine Zeichen erfasst.",
"distKind": "Nach Art",
"distVibe": "Nach Stimmung",
"distOutcome": "Nach Ergebnis",
"bestSource": "Bester Forecaster",
"worstSource": "Unzuverlaessigster Forecaster",
"topSources": "Meistgenutzte Quellen",
"mostFulfilled": "Eingetretene Zeichen",
"mostSurprising": "Ueberraschungen — wo dein Gefuehl danebenlag",
"none": "—",
"hitOf": "von",
"matches": "Treffer"
},
"oracleHint": {
"titleLive": "Was deine Daten zurueck sagen",
"titleSnapshot": "Was das Orakel damals sagte",
"matchCount": "aehnliche Zeichen",
"hit": "eingetreten",
"partly": "teilweise",
"no": "nicht eingetreten"
},
"route": {
"detailFallbackTitle": "Augur",
"detailRouteTitle": "Zeichen",
"loading": "laedt ...",
"notFound": "Eintrag nicht gefunden.",
"backLink": "← zurueck",
"recapInvalid": "Ungueltiges Jahr.",
"recapBack": "← zurueck"
},
"common": {
"all": "alle",
"brierUnknown": ""
}
}

View file

@ -0,0 +1,152 @@
{
"app": {
"title": "Augur",
"tagline": "Zeichen sammeln, Muster lesen."
},
"mode": {
"witness": "Witness",
"oracle": "Oracle",
"witnessHint": "Zeichen sammeln",
"oracleHint": "Muster lesen"
},
"witness": {
"searchPlaceholder": "Suche nach Quelle, Aussage, Deutung ...",
"newOpen": "+ neu",
"newClose": "× schliessen",
"emptyAll": "Noch keine Zeichen erfasst. Sammle erst — auswerten kommt spaeter.",
"emptyFiltered": "Keine passenden Zeichen.",
"openOnly": "nur offene",
"dueOnly": "nur faellige",
"openHint": "noch offen"
},
"due": {
"single": "Zeichen wartet auf Aufloesung",
"plural": "Zeichen warten auf Aufloesung",
"hide": "verbergen",
"show": "oeffnen",
"yes": "ja",
"partly": "teils",
"no": "nein",
"overdue": "Tage faellig",
"dueToday": "heute"
},
"oracle": {
"title": "Oracle",
"subtitle": "Was deine Daten zurueck sagen.",
"yearRecapLink": "Jahresrueckblick →",
"coldStart": "Noch zu wenig aufgeloeste Zeichen.",
"coldStartHint": "Sammle und loese mindestens",
"coldStartUnit": "Zeichen auf, dann zeigt das Orakel verlaessliche Zahlen.",
"statTotal": "gesammelt",
"statResolved": "aufgeloest",
"statOpen": "offen",
"statHitRate": "Trefferquote",
"statBrier": "Brier-Score",
"statBrierBaseline": "Baseline 0.25 (50/50)",
"sourceTitle": "Forecaster in deinem Leben",
"sourceSub": "Wer / was war wie oft richtig?",
"sourceCol": "Quelle",
"sourceN": "n",
"sourceHit": "Treffer",
"sourceBrier": "Brier",
"sourceMix": "Verteilung",
"vibeTitle": "Stimmt dein Bauchgefuehl?",
"vibeSub": "Treffer pro Stimmung — gut, raetselhaft, schlecht.",
"vibeNoData": "noch keine Daten",
"vibeHit": "Treffer",
"vibeDir": "Richtung",
"corrTitle": "Was um deine Zeichen herum passiert",
"corrSub": "Korrelation, nicht Kausalitaet — gefunden in deinen eigenen Daten.",
"corrEmpty": "Noch keine belastbaren Muster. Logge weiter — Mood und Sleep werden dazu gerechnet.",
"corrAfter": "In den 3 Tagen danach",
"corrMoodLevel": "lag dein Mood-Level bei",
"corrSleepQuality": "lag deine Schlaf-Qualitaet bei",
"corrSleepDuration": "lag deine Schlafdauer bei",
"corrVsBaseline": "Baseline:"
},
"detail": {
"source": "Quelle",
"claim": "Aussage",
"felt": "Eigene Deutung",
"expected": "Erwartetes Ergebnis",
"expectedBy": "Bis",
"probability": "Wahrscheinlichkeit",
"tags": "Tags",
"captured": "Erfasst",
"resolved": "Aufgeloest",
"outcomeNote": "Wie es kam",
"resolvePrompt": "Hat sich das bewahrheitet?",
"resolveYes": "eingetreten",
"resolvePartly": "teilweise",
"resolveNo": "nicht eingetreten",
"resolveReopen": "erneut oeffnen",
"actionEdit": "bearbeiten",
"actionArchive": "archivieren",
"actionDelete": "loeschen",
"actionCancel": "abbrechen",
"notePlaceholder": "Optionale Notiz: wie genau ist es gekommen?",
"confirmDelete": "Diesen Eintrag wirklich loeschen?",
"visibility": "Sichtbarkeit"
},
"form": {
"kind": "Art",
"source": "Quelle",
"category": "Kategorie",
"claim": "Was sagt das Zeichen?",
"vibe": "Stimmung",
"feltMeaning": "Eigene Deutung (optional)",
"expectedOutcome": "Was sollte konkret passieren? (optional)",
"expectedBy": "Bis wann? (optional)",
"probability": "Wahrscheinlichkeit (optional, 0-100%)",
"tags": "Tags (komma-getrennt)",
"encounteredAt": "Wann erlebt?",
"sourcePlaceholder": "z. B. schwarze Katze, Glueckskeks, Bauchgefuehl",
"claimPlaceholder": "z. B. heute kommt eine gute Nachricht",
"feltPlaceholder": "Was es fuer mich bedeutet ...",
"expectedPlaceholder": "z. B. Job-Zusage bis Freitag",
"tagsPlaceholder": "arbeit, naturzeichen ...",
"submitCreate": "+ erfassen",
"submitUpdate": "speichern",
"cancel": "abbrechen",
"more": "+ Prognose & Tags"
},
"recap": {
"title": "Jahresrueckblick",
"yearTotal": "Zeichen",
"yearResolved": "aufgeloest",
"yearHitRate": "Trefferquote",
"emptyYear": "In diesem Jahr noch keine Zeichen erfasst.",
"distKind": "Nach Art",
"distVibe": "Nach Stimmung",
"distOutcome": "Nach Ergebnis",
"bestSource": "Bester Forecaster",
"worstSource": "Unzuverlaessigster Forecaster",
"topSources": "Meistgenutzte Quellen",
"mostFulfilled": "Eingetretene Zeichen",
"mostSurprising": "Ueberraschungen — wo dein Gefuehl danebenlag",
"none": "—",
"hitOf": "von",
"matches": "Treffer"
},
"oracleHint": {
"titleLive": "Was deine Daten zurueck sagen",
"titleSnapshot": "Was das Orakel damals sagte",
"matchCount": "aehnliche Zeichen",
"hit": "eingetreten",
"partly": "teilweise",
"no": "nicht eingetreten"
},
"route": {
"detailFallbackTitle": "Augur",
"detailRouteTitle": "Zeichen",
"loading": "laedt ...",
"notFound": "Eintrag nicht gefunden.",
"backLink": "← zurueck",
"recapInvalid": "Ungueltiges Jahr.",
"recapBack": "← zurueck"
},
"common": {
"all": "alle",
"brierUnknown": ""
}
}

View file

@ -0,0 +1,152 @@
{
"app": {
"title": "Augur",
"tagline": "Zeichen sammeln, Muster lesen."
},
"mode": {
"witness": "Witness",
"oracle": "Oracle",
"witnessHint": "Zeichen sammeln",
"oracleHint": "Muster lesen"
},
"witness": {
"searchPlaceholder": "Suche nach Quelle, Aussage, Deutung ...",
"newOpen": "+ neu",
"newClose": "× schliessen",
"emptyAll": "Noch keine Zeichen erfasst. Sammle erst — auswerten kommt spaeter.",
"emptyFiltered": "Keine passenden Zeichen.",
"openOnly": "nur offene",
"dueOnly": "nur faellige",
"openHint": "noch offen"
},
"due": {
"single": "Zeichen wartet auf Aufloesung",
"plural": "Zeichen warten auf Aufloesung",
"hide": "verbergen",
"show": "oeffnen",
"yes": "ja",
"partly": "teils",
"no": "nein",
"overdue": "Tage faellig",
"dueToday": "heute"
},
"oracle": {
"title": "Oracle",
"subtitle": "Was deine Daten zurueck sagen.",
"yearRecapLink": "Jahresrueckblick →",
"coldStart": "Noch zu wenig aufgeloeste Zeichen.",
"coldStartHint": "Sammle und loese mindestens",
"coldStartUnit": "Zeichen auf, dann zeigt das Orakel verlaessliche Zahlen.",
"statTotal": "gesammelt",
"statResolved": "aufgeloest",
"statOpen": "offen",
"statHitRate": "Trefferquote",
"statBrier": "Brier-Score",
"statBrierBaseline": "Baseline 0.25 (50/50)",
"sourceTitle": "Forecaster in deinem Leben",
"sourceSub": "Wer / was war wie oft richtig?",
"sourceCol": "Quelle",
"sourceN": "n",
"sourceHit": "Treffer",
"sourceBrier": "Brier",
"sourceMix": "Verteilung",
"vibeTitle": "Stimmt dein Bauchgefuehl?",
"vibeSub": "Treffer pro Stimmung — gut, raetselhaft, schlecht.",
"vibeNoData": "noch keine Daten",
"vibeHit": "Treffer",
"vibeDir": "Richtung",
"corrTitle": "Was um deine Zeichen herum passiert",
"corrSub": "Korrelation, nicht Kausalitaet — gefunden in deinen eigenen Daten.",
"corrEmpty": "Noch keine belastbaren Muster. Logge weiter — Mood und Sleep werden dazu gerechnet.",
"corrAfter": "In den 3 Tagen danach",
"corrMoodLevel": "lag dein Mood-Level bei",
"corrSleepQuality": "lag deine Schlaf-Qualitaet bei",
"corrSleepDuration": "lag deine Schlafdauer bei",
"corrVsBaseline": "Baseline:"
},
"detail": {
"source": "Quelle",
"claim": "Aussage",
"felt": "Eigene Deutung",
"expected": "Erwartetes Ergebnis",
"expectedBy": "Bis",
"probability": "Wahrscheinlichkeit",
"tags": "Tags",
"captured": "Erfasst",
"resolved": "Aufgeloest",
"outcomeNote": "Wie es kam",
"resolvePrompt": "Hat sich das bewahrheitet?",
"resolveYes": "eingetreten",
"resolvePartly": "teilweise",
"resolveNo": "nicht eingetreten",
"resolveReopen": "erneut oeffnen",
"actionEdit": "bearbeiten",
"actionArchive": "archivieren",
"actionDelete": "loeschen",
"actionCancel": "abbrechen",
"notePlaceholder": "Optionale Notiz: wie genau ist es gekommen?",
"confirmDelete": "Diesen Eintrag wirklich loeschen?",
"visibility": "Sichtbarkeit"
},
"form": {
"kind": "Art",
"source": "Quelle",
"category": "Kategorie",
"claim": "Was sagt das Zeichen?",
"vibe": "Stimmung",
"feltMeaning": "Eigene Deutung (optional)",
"expectedOutcome": "Was sollte konkret passieren? (optional)",
"expectedBy": "Bis wann? (optional)",
"probability": "Wahrscheinlichkeit (optional, 0-100%)",
"tags": "Tags (komma-getrennt)",
"encounteredAt": "Wann erlebt?",
"sourcePlaceholder": "z. B. schwarze Katze, Glueckskeks, Bauchgefuehl",
"claimPlaceholder": "z. B. heute kommt eine gute Nachricht",
"feltPlaceholder": "Was es fuer mich bedeutet ...",
"expectedPlaceholder": "z. B. Job-Zusage bis Freitag",
"tagsPlaceholder": "arbeit, naturzeichen ...",
"submitCreate": "+ erfassen",
"submitUpdate": "speichern",
"cancel": "abbrechen",
"more": "+ Prognose & Tags"
},
"recap": {
"title": "Jahresrueckblick",
"yearTotal": "Zeichen",
"yearResolved": "aufgeloest",
"yearHitRate": "Trefferquote",
"emptyYear": "In diesem Jahr noch keine Zeichen erfasst.",
"distKind": "Nach Art",
"distVibe": "Nach Stimmung",
"distOutcome": "Nach Ergebnis",
"bestSource": "Bester Forecaster",
"worstSource": "Unzuverlaessigster Forecaster",
"topSources": "Meistgenutzte Quellen",
"mostFulfilled": "Eingetretene Zeichen",
"mostSurprising": "Ueberraschungen — wo dein Gefuehl danebenlag",
"none": "—",
"hitOf": "von",
"matches": "Treffer"
},
"oracleHint": {
"titleLive": "Was deine Daten zurueck sagen",
"titleSnapshot": "Was das Orakel damals sagte",
"matchCount": "aehnliche Zeichen",
"hit": "eingetreten",
"partly": "teilweise",
"no": "nicht eingetreten"
},
"route": {
"detailFallbackTitle": "Augur",
"detailRouteTitle": "Zeichen",
"loading": "laedt ...",
"notFound": "Eintrag nicht gefunden.",
"backLink": "← zurueck",
"recapInvalid": "Ungueltiges Jahr.",
"recapBack": "← zurueck"
},
"common": {
"all": "alle",
"brierUnknown": ""
}
}

View file

@ -8,6 +8,7 @@
<script lang="ts">
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { _ } from 'svelte-i18n';
import WitnessView from './views/WitnessView.svelte';
import OracleView from './views/OracleView.svelte';
import type { ViewProps } from '$lib/app-registry';
@ -16,13 +17,6 @@
type Mode = 'witness' | 'oracle';
const T = {
witness: 'Witness',
oracle: 'Oracle',
witnessHint: 'Zeichen sammeln',
oracleHint: 'Muster lesen',
} as const;
const mode = $derived<Mode>(
page.url.searchParams.get('mode') === 'oracle' ? 'oracle' : 'witness'
);
@ -45,8 +39,8 @@
aria-selected={mode === 'witness'}
onclick={() => setMode('witness')}
>
<span class="label">{T.witness}</span>
<span class="hint">{T.witnessHint}</span>
<span class="label">{$_('augur.mode.witness')}</span>
<span class="hint">{$_('augur.mode.witnessHint')}</span>
</button>
<button
type="button"
@ -56,8 +50,8 @@
aria-selected={mode === 'oracle'}
onclick={() => setMode('oracle')}
>
<span class="label">{T.oracle}</span>
<span class="hint">{T.oracleHint}</span>
<span class="label">{$_('augur.mode.oracle')}</span>
<span class="hint">{$_('augur.mode.oracleHint')}</span>
</button>
</div>

View file

@ -12,6 +12,7 @@
-->
<script lang="ts">
import { goto } from '$app/navigation';
import { _ } from 'svelte-i18n';
import { augurStore } from '../stores/entries.svelte';
import { reminderDate, daysUntilDue } from '../lib/reminders';
import type { AugurEntry, AugurOutcome } from '../types';
@ -20,23 +21,11 @@
let expanded = $state(false);
const T = {
single: 'Zeichen wartet auf Aufloesung',
plural: 'Zeichen warten auf Aufloesung',
hide: 'verbergen',
show: 'oeffnen',
yes: 'ja',
partly: 'teils',
no: 'nein',
overdue: 'Tage faellig',
dueToday: 'heute',
} as const;
function dueLabel(e: AugurEntry): string {
const d = daysUntilDue(e);
if (d == null) return '';
if (d === 0) return T.dueToday;
if (d < 0) return `${-d} ${T.overdue}`;
if (d === 0) return $_('augur.due.dueToday');
if (d < 0) return `${-d} ${$_('augur.due.overdue')}`;
return reminderDate(e) ?? '';
}
@ -51,9 +40,9 @@
<span class="dot"></span>
<span class="text">
<strong>{entries.length}</strong>
{entries.length === 1 ? T.single : T.plural}
{entries.length === 1 ? $_('augur.due.single') : $_('augur.due.plural')}
</span>
<span class="toggle">{expanded ? T.hide : T.show}</span>
<span class="toggle">{expanded ? $_('augur.due.hide') : $_('augur.due.show')}</span>
</button>
{#if expanded}
@ -70,7 +59,7 @@
type="button"
class="qb yes"
onclick={() => quickResolve(entry.id, 'fulfilled')}
title={T.yes}
title={$_('augur.due.yes')}
>
</button>
@ -78,7 +67,7 @@
type="button"
class="qb partly"
onclick={() => quickResolve(entry.id, 'partly')}
title={T.partly}
title={$_('augur.due.partly')}
>
~
</button>
@ -86,7 +75,7 @@
type="button"
class="qb no"
onclick={() => quickResolve(entry.id, 'not-fulfilled')}
title={T.no}
title={$_('augur.due.no')}
>
</button>

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { augurStore } from '../stores/entries.svelte';
import { useAllAugurEntries } from '../queries';
import LivingOracleHint from './LivingOracleHint.svelte';
@ -26,28 +27,6 @@
const history = $derived(history$.value);
let oracleReflection = $state<string | null>(null);
const T = {
kind: 'Art',
source: 'Quelle',
category: 'Kategorie',
claim: 'Was sagt das Zeichen?',
vibe: 'Stimmung',
feltMeaning: 'Eigene Deutung (optional)',
expectedOutcome: 'Was sollte konkret passieren? (optional)',
expectedBy: 'Bis wann? (optional)',
probability: 'Wahrscheinlichkeit (optional, 0-100%)',
tags: 'Tags (komma-getrennt)',
encounteredAt: 'Wann erlebt?',
sourcePlaceholder: 'z. B. schwarze Katze, Glueckskeks, Bauchgefuehl',
claimPlaceholder: 'z. B. heute kommt eine gute Nachricht',
feltPlaceholder: 'Was es fuer mich bedeutet ...',
expectedPlaceholder: 'z. B. Job-Zusage bis Freitag',
tagsPlaceholder: 'arbeit, naturzeichen ...',
submitCreate: '+ erfassen',
submitUpdate: 'speichern',
cancel: 'abbrechen',
} as const;
/* svelte-ignore state_referenced_locally */
let kind = $state<AugurKind>(initial?.kind ?? 'hunch');
/* svelte-ignore state_referenced_locally */
@ -143,7 +122,7 @@
{#if mode === 'create'}
<div class="row">
<label class="field">
<span>{T.kind}</span>
<span>{$_('augur.form.kind')}</span>
<select bind:value={kind}>
{#each KIND_ORDER as k (k)}
<option value={k}>{KIND_LABELS[k].de}</option>
@ -151,7 +130,7 @@
</select>
</label>
<label class="field">
<span>{T.encounteredAt}</span>
<span>{$_('augur.form.encounteredAt')}</span>
<input type="date" bind:value={encounteredAt} />
</label>
</div>
@ -159,11 +138,11 @@
<div class="row">
<label class="field grow">
<span>{T.source}</span>
<input bind:value={source} placeholder={T.sourcePlaceholder} required />
<span>{$_('augur.form.source')}</span>
<input bind:value={source} placeholder={$_('augur.form.sourcePlaceholder')} required />
</label>
<label class="field">
<span>{T.category}</span>
<span>{$_('augur.form.category')}</span>
<select bind:value={sourceCategory}>
{#each CATEGORY_ORDER as c (c)}
<option value={c}>{SOURCE_CATEGORY_LABELS[c].de}</option>
@ -173,13 +152,14 @@
</div>
<label class="field">
<span>{T.claim}</span>
<textarea bind:value={claim} placeholder={T.claimPlaceholder} rows="2" required></textarea>
<span>{$_('augur.form.claim')}</span>
<textarea bind:value={claim} placeholder={$_('augur.form.claimPlaceholder')} rows="2" required
></textarea>
</label>
<div class="row">
<label class="field grow">
<span>{T.vibe}</span>
<span>{$_('augur.form.vibe')}</span>
<div class="vibe-row">
{#each VIBE_ORDER as v (v)}
<button
@ -196,8 +176,9 @@
</div>
<label class="field">
<span>{T.feltMeaning}</span>
<textarea bind:value={feltMeaning} placeholder={T.feltPlaceholder} rows="2"></textarea>
<span>{$_('augur.form.feltMeaning')}</span>
<textarea bind:value={feltMeaning} placeholder={$_('augur.form.feltPlaceholder')} rows="2"
></textarea>
</label>
{#if mode === 'create'}
@ -216,18 +197,18 @@
{/if}
<details class="more">
<summary>+ Prognose & Tags</summary>
<summary>{$_('augur.form.more')}</summary>
<label class="field">
<span>{T.expectedOutcome}</span>
<input bind:value={expectedOutcome} placeholder={T.expectedPlaceholder} />
<span>{$_('augur.form.expectedOutcome')}</span>
<input bind:value={expectedOutcome} placeholder={$_('augur.form.expectedPlaceholder')} />
</label>
<div class="row">
<label class="field">
<span>{T.expectedBy}</span>
<span>{$_('augur.form.expectedBy')}</span>
<input type="date" bind:value={expectedBy} />
</label>
<label class="field">
<span>{T.probability}</span>
<span>{$_('augur.form.probability')}</span>
<input
type="number"
min="0"
@ -239,19 +220,19 @@
</label>
</div>
<label class="field">
<span>{T.tags}</span>
<input bind:value={tagsText} placeholder={T.tagsPlaceholder} />
<span>{$_('augur.form.tags')}</span>
<input bind:value={tagsText} placeholder={$_('augur.form.tagsPlaceholder')} />
</label>
</details>
<div class="actions">
{#if onclose}
<button type="button" class="btn ghost" onclick={() => onclose?.()} disabled={saving}>
{T.cancel}
{$_('augur.form.cancel')}
</button>
{/if}
<button type="submit" class="btn primary" disabled={saving}>
{mode === 'edit' ? T.submitUpdate : T.submitCreate}
{mode === 'edit' ? $_('augur.form.submitUpdate') : $_('augur.form.submitCreate')}
</button>
</div>
</form>

View file

@ -14,6 +14,7 @@
*at the time*, alongside what actually happened.
-->
<script lang="ts">
import { _ } from 'svelte-i18n';
import {
findMatches,
fingerprint,
@ -50,15 +51,6 @@
let props: Props = $props();
const T = {
title: 'Was deine Daten zurueck sagen',
titleSnapshot: 'Was das Orakel damals sagte',
matchCount: 'aehnliche Zeichen',
hit: 'eingetreten',
partly: 'teilweise',
no: 'nicht eingetreten',
} as const;
const liveResult = $derived.by(() => {
if (props.mode === 'snapshot') return null;
const fp: Fingerprint | null = fingerprint(props.input);
@ -80,7 +72,7 @@
<aside class="hint snapshot">
<header>
<span class="dot"></span>
<span class="title">{T.titleSnapshot}</span>
<span class="title">{$_('augur.oracleHint.titleSnapshot')}</span>
</header>
<p class="text">{props.snapshot}</p>
</aside>
@ -89,18 +81,30 @@
<aside class="hint live">
<header>
<span class="dot"></span>
<span class="title">{T.title}</span>
<span class="title">{$_('augur.oracleHint.titleLive')}</span>
</header>
<p class="text">{liveResult.text}</p>
<div class="bars">
{#if liveResult.set.fulfilled > 0}
<span class="bar yes" style:flex={liveResult.set.fulfilled} title={T.hit}></span>
<span
class="bar yes"
style:flex={liveResult.set.fulfilled}
title={$_('augur.oracleHint.hit')}
></span>
{/if}
{#if liveResult.set.partly > 0}
<span class="bar partly" style:flex={liveResult.set.partly} title={T.partly}></span>
<span
class="bar partly"
style:flex={liveResult.set.partly}
title={$_('augur.oracleHint.partly')}
></span>
{/if}
{#if liveResult.set.notFulfilled > 0}
<span class="bar no" style:flex={liveResult.set.notFulfilled} title={T.no}></span>
<span
class="bar no"
style:flex={liveResult.set.notFulfilled}
title={$_('augur.oracleHint.no')}
></span>
{/if}
</div>
</aside>

View file

@ -15,6 +15,7 @@
needs the mood/sleep query layer pulled in. Not in this PR.
-->
<script lang="ts">
import { _ } from 'svelte-i18n';
import { useAllAugurEntries } from '../queries';
import {
calibrationPerSource,
@ -26,43 +27,8 @@
import { useMoodByDate, useSleepByDate } from '../lib/signal-bridge.svelte';
import { KIND_LABELS, SOURCE_CATEGORY_LABELS, VIBE_LABELS, VIBE_COLORS } from '../types';
const T = {
title: 'Oracle',
subtitle: 'Was deine Daten zurueck sagen.',
coldStart: 'Noch zu wenig aufgeloeste Zeichen.',
coldStartHint: 'Sammle und loese mindestens',
coldStartUnit: 'Zeichen auf, dann zeigt das Orakel verlaessliche Zahlen.',
statTotal: 'gesammelt',
statResolved: 'aufgeloest',
statOpen: 'offen',
statHitRate: 'Trefferquote',
statBrier: 'Brier-Score',
statBrierBaseline: 'Baseline 0.25 (50/50)',
sourceTitle: 'Forecaster in deinem Leben',
sourceSub: 'Wer / was war wie oft richtig?',
sourceCol: 'Quelle',
sourceN: 'n',
sourceHit: 'Treffer',
sourceBrier: 'Brier',
sourceMix: 'Verteilung',
vibeTitle: 'Stimmt dein Bauchgefuehl?',
vibeSub: 'Treffer pro Stimmung — gut, raetselhaft, schlecht.',
vibeNoData: 'noch keine Daten',
vibeHit: 'Treffer',
vibeDir: 'Richtung',
brierUnknown: '',
corrTitle: 'Was um deine Zeichen herum passiert',
corrSub: 'Korrelation, nicht Kausalitaet — gefunden in deinen eigenen Daten.',
corrEmpty:
'Noch keine belastbaren Muster. Logge weiter — Mood und Sleep werden dazu gerechnet.',
corrAfter: 'In den 3 Tagen danach',
corrMoodLevel: 'lag dein Mood-Level bei',
corrSleepQuality: 'lag deine Schlaf-Qualitaet bei',
corrSleepDuration: 'lag deine Schlafdauer bei',
corrVsBaseline: 'Baseline:',
corrUnits: { min: 'min', score: '/10', score5: '/5' },
yearRecapLink: 'Jahresrueckblick →',
} as const;
// Metric units stay as constants — they're not translated, just symbolic.
const METRIC_UNITS = { min: 'min', score: '/10', score5: '/5' } as const;
const currentYear = new Date().getFullYear();
@ -82,22 +48,22 @@
function metricLabel(f: CorrelationFinding): string {
switch (f.metric) {
case 'mood-level':
return T.corrMoodLevel;
return $_('augur.oracle.corrMoodLevel');
case 'sleep-quality':
return T.corrSleepQuality;
return $_('augur.oracle.corrSleepQuality');
case 'sleep-duration':
return T.corrSleepDuration;
return $_('augur.oracle.corrSleepDuration');
}
}
function metricUnit(f: CorrelationFinding): string {
switch (f.metric) {
case 'mood-level':
return T.corrUnits.score;
return METRIC_UNITS.score;
case 'sleep-quality':
return T.corrUnits.score5;
return METRIC_UNITS.score5;
case 'sleep-duration':
return T.corrUnits.min;
return METRIC_UNITS.min;
}
}
@ -117,12 +83,12 @@
}
function pct(value: number | null): string {
if (value == null) return T.brierUnknown;
if (value == null) return $_('augur.common.brierUnknown');
return `${Math.round(value * 100)}%`;
}
function brier(value: number | null): string {
if (value == null) return T.brierUnknown;
if (value == null) return $_('augur.common.brierUnknown');
return value.toFixed(3);
}
@ -132,42 +98,42 @@
<div class="oracle">
<header class="head">
<div>
<h2>{T.title}</h2>
<p class="sub">{T.subtitle}</p>
<h2>{$_('augur.oracle.title')}</h2>
<p class="sub">{$_('augur.oracle.subtitle')}</p>
</div>
<a class="recap-link" href="/augur/recap/{currentYear}">{T.yearRecapLink}</a>
<a class="recap-link" href="/augur/recap/{currentYear}">{$_('augur.oracle.yearRecapLink')}</a>
</header>
<section class="stats">
<div class="stat">
<span class="stat-num">{stats.total}</span>
<span class="stat-label">{T.statTotal}</span>
<span class="stat-label">{$_('augur.oracle.statTotal')}</span>
</div>
<div class="stat">
<span class="stat-num">{stats.resolved}</span>
<span class="stat-label">{T.statResolved}</span>
<span class="stat-label">{$_('augur.oracle.statResolved')}</span>
</div>
<div class="stat">
<span class="stat-num">{stats.open}</span>
<span class="stat-label">{T.statOpen}</span>
<span class="stat-label">{$_('augur.oracle.statOpen')}</span>
</div>
<div class="stat highlight">
<span class="stat-num">{pct(stats.hitRate)}</span>
<span class="stat-label">{T.statHitRate}</span>
<span class="stat-label">{$_('augur.oracle.statHitRate')}</span>
</div>
<div class="stat" title={T.statBrierBaseline}>
<div class="stat" title={$_('augur.oracle.statBrierBaseline')}>
<span class="stat-num">{brier(stats.brier)}</span>
<span class="stat-label">{T.statBrier} ({stats.brierN})</span>
<span class="stat-label">{$_('augur.oracle.statBrier')} ({stats.brierN})</span>
</div>
</section>
{#if isColdStart}
<section class="cold-start">
<p>{T.coldStart}</p>
<p>{$_('augur.oracle.coldStart')}</p>
<p class="hint">
{T.coldStartHint}
{$_('augur.oracle.coldStartHint')}
<strong>{ORACLE_COLD_START_MIN}</strong>
{T.coldStartUnit}
{$_('augur.oracle.coldStartUnit')}
</p>
<div class="progress">
<div
@ -179,20 +145,20 @@
{:else}
<section class="block">
<header class="block-head">
<h3>{T.sourceTitle}</h3>
<p>{T.sourceSub}</p>
<h3>{$_('augur.oracle.sourceTitle')}</h3>
<p>{$_('augur.oracle.sourceSub')}</p>
</header>
{#if sourceRows.length === 0}
<p class="empty">{T.vibeNoData}</p>
<p class="empty">{$_('augur.oracle.vibeNoData')}</p>
{:else}
<table class="ranked">
<thead>
<tr>
<th>{T.sourceCol}</th>
<th class="num">{T.sourceN}</th>
<th class="num">{T.sourceHit}</th>
<th class="num">{T.sourceBrier}</th>
<th class="mix">{T.sourceMix}</th>
<th>{$_('augur.oracle.sourceCol')}</th>
<th class="num">{$_('augur.oracle.sourceN')}</th>
<th class="num">{$_('augur.oracle.sourceHit')}</th>
<th class="num">{$_('augur.oracle.sourceBrier')}</th>
<th class="mix">{$_('augur.oracle.sourceMix')}</th>
</tr>
</thead>
<tbody>
@ -229,11 +195,11 @@
<section class="block">
<header class="block-head">
<h3>{T.corrTitle}</h3>
<p>{T.corrSub}</p>
<h3>{$_('augur.oracle.corrTitle')}</h3>
<p>{$_('augur.oracle.corrSub')}</p>
</header>
{#if correlations.length === 0}
<p class="empty">{T.corrEmpty}</p>
<p class="empty">{$_('augur.oracle.corrEmpty')}</p>
{:else}
<ul class="corr-list">
{#each correlations.slice(0, 6) as f (f.dimension + f.bucket + f.metric + f.windowDays)}
@ -247,10 +213,10 @@
<span class="corr-n">n={f.n}</span>
</div>
<p class="corr-text">
{T.corrAfter}
{$_('augur.oracle.corrAfter')}
{bucketLabel(f).toLowerCase()}-Zeichen {metricLabel(f)}
<strong>{fmt(f.bucketMean, f.metric)}{metricUnit(f)}</strong>
{T.corrVsBaseline}
{$_('augur.oracle.corrVsBaseline')}
{fmt(f.baseline, f.metric)}{metricUnit(f)}.
</p>
</li>
@ -261,21 +227,21 @@
<section class="block">
<header class="block-head">
<h3>{T.vibeTitle}</h3>
<p>{T.vibeSub}</p>
<h3>{$_('augur.oracle.vibeTitle')}</h3>
<p>{$_('augur.oracle.vibeSub')}</p>
</header>
<div class="vibe-grid">
{#each vibeRows as row (row.vibe)}
<div class="vibe-card" style:--vibe-color={VIBE_COLORS[row.vibe]}>
<div class="vibe-label">{VIBE_LABELS[row.vibe].de}</div>
{#if row.n === 0}
<div class="vibe-empty">{T.vibeNoData}</div>
<div class="vibe-empty">{$_('augur.oracle.vibeNoData')}</div>
{:else}
<div class="vibe-rate">{pct(row.hitRate)}</div>
<div class="vibe-meta">{T.vibeHit} (n={row.n})</div>
<div class="vibe-meta">{$_('augur.oracle.vibeHit')} (n={row.n})</div>
{#if row.directionalHitRate != null}
<div class="vibe-rate small">{pct(row.directionalHitRate)}</div>
<div class="vibe-meta">{T.vibeDir}</div>
<div class="vibe-meta">{$_('augur.oracle.vibeDir')}</div>
{/if}
{/if}
</div>

View file

@ -9,6 +9,7 @@
-->
<script lang="ts">
import { goto } from '$app/navigation';
import { _ } from 'svelte-i18n';
import KindTabs from '../components/KindTabs.svelte';
import EntryCard from '../components/EntryCard.svelte';
import EntryForm from '../components/EntryForm.svelte';
@ -22,17 +23,6 @@
import { isDue } from '../lib/reminders';
import type { AugurEntry, AugurKind } from '../types';
const T = {
searchPlaceholder: 'Suche nach Quelle, Aussage, Deutung ...',
newOpen: '+ neu',
newClose: '× schliessen',
emptyAll: 'Noch keine Zeichen erfasst. Sammle erst — auswerten kommt spaeter.',
emptyFiltered: 'Keine passenden Zeichen.',
openOnly: 'nur offene',
dueOnly: 'nur faellige',
openHint: 'noch offen',
} as const;
let entries$ = useAllAugurEntries();
let entries = $derived(entries$.value);
@ -75,7 +65,7 @@
type="search"
class="search"
bind:value={searchQuery}
placeholder={T.searchPlaceholder}
placeholder={$_('augur.witness.searchPlaceholder')}
/>
<button
type="button"
@ -84,7 +74,7 @@
onclick={() => (showCreate = !showCreate)}
aria-expanded={showCreate}
>
{showCreate ? T.newClose : T.newOpen}
{showCreate ? $_('augur.witness.newClose') : $_('augur.witness.newOpen')}
</button>
</div>
@ -99,14 +89,14 @@
<div class="filter-row">
<label class="open-toggle">
<input type="checkbox" bind:checked={showOpenOnly} />
<span>{T.openOnly}</span>
<span>{$_('augur.witness.openOnly')}</span>
{#if unresolvedCount > 0}
<span class="badge open">{unresolvedCount} {T.openHint}</span>
<span class="badge open">{unresolvedCount} {$_('augur.witness.openHint')}</span>
{/if}
</label>
<label class="open-toggle">
<input type="checkbox" bind:checked={showDueOnly} />
<span>{T.dueOnly}</span>
<span>{$_('augur.witness.dueOnly')}</span>
{#if dueEntries.length > 0}
<span class="badge due">{dueEntries.length}</span>
{/if}
@ -116,7 +106,7 @@
{#if filtered.length === 0}
<p class="empty">
{entries.length === 0 ? T.emptyAll : T.emptyFiltered}
{entries.length === 0 ? $_('augur.witness.emptyAll') : $_('augur.witness.emptyFiltered')}
</p>
{:else}
<ul class="grid">

View file

@ -15,6 +15,7 @@
-->
<script lang="ts">
import { goto } from '$app/navigation';
import { _ } from 'svelte-i18n';
import { useAllAugurEntries } from '../queries';
import { buildYearRecap } from '../lib/year-recap';
import {
@ -29,31 +30,12 @@
let { year }: { year: number } = $props();
const T = {
title: 'Jahresrueckblick',
yearTotal: 'Zeichen',
yearResolved: 'aufgeloest',
yearHitRate: 'Trefferquote',
emptyYear: 'In diesem Jahr noch keine Zeichen erfasst.',
distKind: 'Nach Art',
distVibe: 'Nach Stimmung',
distOutcome: 'Nach Ergebnis',
bestSource: 'Bester Forecaster',
worstSource: 'Unzuverlaessigster Forecaster',
topSources: 'Meistgenutzte Quellen',
mostFulfilled: 'Eingetretene Zeichen',
mostSurprising: 'Ueberraschungen — wo dein Gefuehl danebenlag',
none: '—',
hitOf: 'von',
matches: 'Treffer',
} as const;
const entries$ = useAllAugurEntries();
const entries = $derived(entries$.value);
const recap = $derived(buildYearRecap(entries, year));
function pct(v: number | null): string {
if (v == null) return T.none;
if (v == null) return $_('augur.recap.none');
return `${Math.round(v * 100)}%`;
}
@ -65,30 +47,30 @@
<div class="recap">
<header class="head">
<div class="year">{year}</div>
<h2>{T.title}</h2>
<h2>{$_('augur.recap.title')}</h2>
</header>
{#if recap.total === 0}
<p class="empty">{T.emptyYear}</p>
<p class="empty">{$_('augur.recap.emptyYear')}</p>
{:else}
<section class="headline">
<div class="big-stat">
<span class="num">{recap.total}</span>
<span class="lbl">{T.yearTotal}</span>
<span class="lbl">{$_('augur.recap.yearTotal')}</span>
</div>
<div class="big-stat">
<span class="num">{recap.resolved}</span>
<span class="lbl">{T.yearResolved}</span>
<span class="lbl">{$_('augur.recap.yearResolved')}</span>
</div>
<div class="big-stat highlight">
<span class="num">{pct(recap.hitRate)}</span>
<span class="lbl">{T.yearHitRate}</span>
<span class="lbl">{$_('augur.recap.yearHitRate')}</span>
</div>
</section>
<section class="dist-row">
<div class="dist">
<h4>{T.distKind}</h4>
<h4>{$_('augur.recap.distKind')}</h4>
<ul>
{#each Object.entries(recap.byKind) as [k, n] (k)}
<li>
@ -99,7 +81,7 @@
</ul>
</div>
<div class="dist">
<h4>{T.distVibe}</h4>
<h4>{$_('augur.recap.distVibe')}</h4>
<ul>
{#each Object.entries(recap.byVibe) as [v, n] (v)}
<li>
@ -112,7 +94,7 @@
</ul>
</div>
<div class="dist">
<h4>{T.distOutcome}</h4>
<h4>{$_('augur.recap.distOutcome')}</h4>
<ul>
{#each Object.entries(recap.byOutcome) as [o, n] (o)}
<li>
@ -126,34 +108,42 @@
<section class="forecaster-row">
<div class="forecaster best">
<h4>{T.bestSource}</h4>
<h4>{$_('augur.recap.bestSource')}</h4>
{#if recap.bestSource}
<div class="fc-name">
{SOURCE_CATEGORY_LABELS[recap.bestSource.sourceCategory].de}
</div>
<div class="fc-num">{pct(recap.bestSource.hitRate)}</div>
<div class="fc-meta">{recap.bestSource.fulfilled} {T.hitOf} {recap.bestSource.n}</div>
<div class="fc-meta">
{recap.bestSource.fulfilled}
{$_('augur.recap.hitOf')}
{recap.bestSource.n}
</div>
{:else}
<p class="fc-empty">{T.none}</p>
<p class="fc-empty">{$_('augur.recap.none')}</p>
{/if}
</div>
<div class="forecaster worst">
<h4>{T.worstSource}</h4>
<h4>{$_('augur.recap.worstSource')}</h4>
{#if recap.worstSource}
<div class="fc-name">
{SOURCE_CATEGORY_LABELS[recap.worstSource.sourceCategory].de}
</div>
<div class="fc-num">{pct(recap.worstSource.hitRate)}</div>
<div class="fc-meta">{recap.worstSource.fulfilled} {T.hitOf} {recap.worstSource.n}</div>
<div class="fc-meta">
{recap.worstSource.fulfilled}
{$_('augur.recap.hitOf')}
{recap.worstSource.n}
</div>
{:else}
<p class="fc-empty">{T.none}</p>
<p class="fc-empty">{$_('augur.recap.none')}</p>
{/if}
</div>
</section>
{#if recap.topCategories.length > 0}
<section class="block">
<h3>{T.topSources}</h3>
<h3>{$_('augur.recap.topSources')}</h3>
<ul class="ranked-list">
{#each recap.topCategories as cat (cat.category)}
<li>
@ -173,7 +163,7 @@
{#if recap.mostFulfilled.length > 0}
<section class="block">
<h3>{T.mostFulfilled}</h3>
<h3>{$_('augur.recap.mostFulfilled')}</h3>
<div class="card-grid">
{#each recap.mostFulfilled as entry (entry.id)}
<EntryCard {entry} onclick={openEntry} />
@ -184,7 +174,7 @@
{#if recap.mostSurprising.length > 0}
<section class="block">
<h3>{T.mostSurprising}</h3>
<h3>{$_('augur.recap.mostSurprising')}</h3>
<div class="card-grid">
{#each recap.mostSurprising as entry (entry.id)}
<EntryCard {entry} onclick={openEntry} />

View file

@ -1,32 +1,25 @@
<script lang="ts">
import { page } from '$app/state';
import { _ } from 'svelte-i18n';
import DetailView from '$lib/modules/augur/views/DetailView.svelte';
import { useAllAugurEntries } from '$lib/modules/augur/queries';
import { RoutePage } from '$lib/components/shell';
const entries$ = useAllAugurEntries();
const entry = $derived(entries$.value.find((e) => e.id === page.params.id));
const T = {
fallbackTitle: 'Augur',
routeTitle: 'Zeichen',
loading: 'laedt ...',
notFound: 'Eintrag nicht gefunden.',
backLink: '← zurueck',
} as const;
</script>
<svelte:head>
<title>{entry?.source ?? T.fallbackTitle} - Mana</title>
<title>{entry?.source ?? $_('augur.route.detailFallbackTitle')} - Mana</title>
</svelte:head>
<RoutePage appId="augur" backHref="/augur" title={T.routeTitle}>
<RoutePage appId="augur" backHref="/augur" title={$_('augur.route.detailRouteTitle')}>
{#if entries$.loading}
<p class="state">{T.loading}</p>
<p class="state">{$_('augur.route.loading')}</p>
{:else if !entry}
<div class="state">
<p>{T.notFound}</p>
<a href="/augur">{T.backLink}</a>
<p>{$_('augur.route.notFound')}</p>
<a href="/augur">{$_('augur.route.backLink')}</a>
</div>
{:else}
<DetailView {entry} />

View file

@ -1,14 +1,9 @@
<script lang="ts">
import { page } from '$app/state';
import { _ } from 'svelte-i18n';
import YearRecapView from '$lib/modules/augur/views/YearRecapView.svelte';
import { RoutePage } from '$lib/components/shell';
const T = {
title: 'Jahresrueckblick',
invalid: 'Ungueltiges Jahr.',
back: '← zurueck',
} as const;
const year = $derived.by(() => {
const raw = page.params.year;
if (!raw) return null;
@ -19,14 +14,14 @@
</script>
<svelte:head>
<title>{year ?? T.title} - Augur - Mana</title>
<title>{year ?? $_('augur.recap.title')} - Augur - Mana</title>
</svelte:head>
<RoutePage appId="augur" backHref="/augur?mode=oracle" title={T.title}>
<RoutePage appId="augur" backHref="/augur?mode=oracle" title={$_('augur.recap.title')}>
{#if year == null}
<div class="state">
<p>{T.invalid}</p>
<a href="/augur">{T.back}</a>
<p>{$_('augur.route.recapInvalid')}</p>
<a href="/augur">{$_('augur.route.recapBack')}</a>
</div>
{:else}
<YearRecapView {year} />