mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
* fix(i18n,ux): package A — missing keys, chart labels, query invalidation, admin error UX Addresses bugs surfaced in issues #73 and #74 plus drift found during audit. - i18n: add 21 missing translation keys (targets.* namespace was only partially populated after the English-first migration; common.reset and settings.configured were referenced but absent), plus common helpers (copy, copyDetails, retry, reportIssue) and admin.settingsSaveError. - charts: rename points7Label/30Label/90Label from cryptic "7P/30P/90P" to "7 pts / 30 pts / 90 pts" in EN and "7 Pkt / 30 Pkt / 90 Pkt" in DE. - queries: add src/lib/query-keys.ts as a single source of truth with measurement/mood/medication-dependent key sets. Wire them into measurement-form/list, mood-form/list, medication-form/card, intake-history-list, intake-timeline, medications page, and Withings sync in settings. Fixes dashboard not auto-refreshing after adding a measurement (old code only invalidated ["measurements"], not ["analytics"] / ["insights"] / ["insights","targets"]). - admin: useUpdateSettings + updateUser mutations now surface real error messages via sonner toasts and invoke getApiErrorMessage() so saves fail loudly instead of silently. Success also gets a toast. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(i18n): package B — locale-aware formatters throughout Removes the hard-coded de-DE locale that lingered from the German-first era and makes every number, date, and time locale-aware. English stays the default per user preference; German remains selectable. New: - src/lib/format-locale.ts exposes makeFormatters(locale) + a cached Europe/Berlin timezone. Covers number, integer, percent, date, dateShort, dateTime, time, dateWithWeekday, monthShort. Also exports parseLocaleFromAcceptLanguage() for server code. - useFormatters() hook in @/lib/i18n/context resolves from the active locale, memoized per render. Migrated (all Intl.NumberFormat("de-DE") / toLocaleString("de-DE") / Intl.DateTimeFormat("de-DE") call sites): - src/app/page.tsx (dashboard) - src/app/insights/page.tsx - src/app/admin/page.tsx - src/app/settings/page.tsx (Withings last-sync, doctor-report caller) - src/components/charts/trend-card.tsx (+ i18n "7d"/"30d" short labels) - src/components/charts/health-chart.tsx - src/components/medications/medication-card.tsx - src/components/medications/medication-form.tsx - src/components/measurements/measurement-list.tsx - src/components/mood/mood-list.tsx - src/components/insights/insight-status-card.tsx (also removed 5 German hard-coded strings with TODO: i18n markers — now translated) - src/components/insights/insight-advisor-card.tsx (same — 10+ strings) Server: reminder-check / reminder-worker / umami-test switched from "de-DE" to neutral en-GB/en-US where the string is machine-compared rather than user-facing. Bug-report GitHub issue titles use ISO-8601 UTC now — the repo is English-speaking. Next.js metadata: - OpenGraph locale from "de_DE" → "en_US" with "de_DE" as alternateLocale. - Root layout now reads healthlog-locale cookie (falls back to Accept-Language) and sets <html lang> server-side, avoiding an EN-flashes-then-DE hydration gap and fixing SEO/a11y. - I18nProvider mirrors locale to a 1-year SameSite=Lax cookie so the next SSR pass is correct. Doctor-report PDF: - generateDoctorReportPDF(data, { t, locale }) now accepts injected translator + locale. All section titles, column headers, BMI / BP classifications, mood labels, and the footer disclaimer are i18n-driven. File name slug switches between gesundheitsbericht / health-report. - 40+ new doctorReport.* keys in EN + DE. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ux): package C — typed passkey errors, bug-report gating, error boundary Addresses issue #73 points 1, 5, 6 and lays groundwork for copy-paste error reporting requested during review. Passkey registration / login (#73.1): - src/lib/passkey-errors.ts maps DOMException.name + SimpleWebAuthn code to specific i18n keys instead of swallowing every failure as "cancelled". Distinguishes NotAllowed/InvalidState/NotSupported/ Security/Abort errors and falls back to a diagnostic message. - settings passkey + login passkey flows both use describePasskeyError now, so origin mismatches and previous-registration collisions show actionable copy. - 5 new passkey.* i18n keys in EN + DE. Bug report (#73.5): - New GET /api/bugreport/status reports {configured, hasToken, hasRepo, isAdmin}. - /bugreport page now queries status first. When not configured it shows a notice (different copy for admins vs. regular users) with a direct link to the admin panel. Form is hidden, not silently broken. - When POST /api/bugreport can't create issues it returns 503 instead of 500 with the same user-facing message. - 4 new bugreport.* i18n keys. Global error UX: - src/components/error-details.tsx — reusable panel with retry + copy diagnostics (message, digest, stack (trimmed), URL, user-agent, request-id, locale, timestamp) + report-issue link. Clipboard fallback prompt covers insecure-context cases. - src/app/error.tsx uses it. - src/app/global-error.tsx is provider-free (no i18n dependency) since it replaces the root layout on catastrophic failures — hardcoded English + Dracula palette so it still looks like HealthLog. - 2 new errorBoundary.* keys and 4 common.* keys (copy, copyDetails, retry, reportIssue). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(query-keys): collapse repeated invalidation blocks into invalidateKeys helper Eleven mutation handlers repeated `Promise.all(keys.map(k => invalidate({queryKey: k})))` verbatim. Adds `invalidateKeys(qc, keys)` to the existing query-keys factory and adopts it everywhere, including replacing the local `invalidateMedicationRelated` helper that medication-form.tsx had grown. Also fixes the import-sandwich that helper introduced. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: small simplifications in i18n/UX changes - passkey-errors: drop the typeof-name dance now that error.name is already string after the instanceof Error narrow - doctor-report-pdf: replace nested gender ternary with a small lookup map; clarify unitFor's null-as-translation-marker convention - dashboard page: route the analytics queries through the new queryKeys factory so future invalidation stays in one place - health-chart: add missing fmt dependency to trendInfo useMemo (introduced when formatSigned was switched to the locale-aware formatter) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ai): capture upstream response body in OpenAI errors A production insights burst at 12:19 UTC failed with "OpenAI request failed (500)". The 500 was the upstream OpenAI HTTP status (confirmed via logs), but we had no way to tell from logs whether it was an OpenAI incident, a model-not-found, or a response_format rejection because the response body was thrown away. Now the error message includes the first 500 chars of the response body and structured fields (httpStatus, upstream, model) that Wide Event logging can attach. No behavior change for the happy path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(review): address code-review + security findings Review round produced five real issues — fix them all, plus three nits that were cheap enough to bundle. Must-fix: - security: error-details.tsx + global-error.tsx were copying window.location.href into the clipboard, which on OAuth callback routes (/api/withings/*, password reset) would leak code/state tokens into GitHub issues. Strip to pathname + searchParamCount. - fe: query-keys.invalidateKeys now uses Promise.allSettled, so a transient invalidate failure doesn't abort the chain and no unhandled rejection leaks from void invalidateKeys(...) call sites. - fe: resolveInitialLocale in app/layout.tsx wraps cookies()/headers() in try/catch — a dynamic-server error in the root layout now degrades to the "en" default instead of crashing into global-error. - be: reminder-worker + reminder-check parseTimeToMinutes normalize "24:00" → "00:00" (some ICU builds emit that for midnight under en-GB). Prevents a 1440-minute comparison against schedule windows. - security: openai-client error body excerpt redacts sk-* / Bearer tokens before logging, and moves the excerpt off Error.message into a structured field (bodyExcerpt) so log transports can filter it. - i18n: /api/withings/connect German error message → English. - i18n: /insights/page.tsx hardcoded "Fehler beim Laden" → t(...) Hardening: - /api/bugreport/status collapses to {configured, isAdmin} and adds a 30/min rate limit per user. Client interface updated. - i18n/context.tsx: I18nProvider's initializer now reads the cookie (matches SSR) before falling back to localStorage or browser detection — eliminates the locale-flash hydration class. - format.ts legacy helpers also read cookie first, matching SSR. Tests: - new src/lib/__tests__/format-locale.test.ts — 9 cases covering makeFormatters, parseLocaleFromAcceptLanguage, resolveIntlLocale - new passkey-errors.test.ts — 8 cases covering every DOMException name and SimpleWebAuthn code we map - new query-keys.test.ts — bundles + invalidateKeys allSettled semantics (incl. partial-failure test) 118 → 142 tests. All pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(v1.2): thresholds, glucose, dashboard customization, built-in feedback, multi-provider AI (#76) * build(deps): bump @prisma/adapter-pg from 7.6.0 to 7.7.0 (#60) Bumps [@prisma/adapter-pg](https://github.com/prisma/prisma/tree/HEAD/packages/adapter-pg) from 7.6.0 to 7.7.0. - [Release notes](https://github.com/prisma/prisma/releases) - [Commits](https://github.com/prisma/prisma/commits/7.7.0/packages/adapter-pg) --- updated-dependencies: - dependency-name: "@prisma/adapter-pg" dependency-version: 7.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump prisma from 7.6.0 to 7.7.0 (#61) Bumps [prisma](https://github.com/prisma/prisma/tree/HEAD/packages/cli) from 7.6.0 to 7.7.0. - [Release notes](https://github.com/prisma/prisma/releases) - [Commits](https://github.com/prisma/prisma/commits/7.7.0/packages/cli) --- updated-dependencies: - dependency-name: prisma dependency-version: 7.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump eslint-config-next from 16.2.2 to 16.2.3 (#62) Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 16.2.2 to 16.2.3. - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](https://github.com/vercel/next.js/commits/v16.2.3/packages/eslint-config-next) --- updated-dependencies: - dependency-name: eslint-config-next dependency-version: 16.2.3 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump next from 16.2.2 to 16.2.3 (#65) Bumps [next](https://github.com/vercel/next.js) from 16.2.2 to 16.2.3. - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](https://github.com/vercel/next.js/compare/v16.2.2...v16.2.3) --- updated-dependencies: - dependency-name: next dependency-version: 16.2.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump @types/node from 25.5.2 to 25.6.0 (#66) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.5.2 to 25.6.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 25.6.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump dotenv from 17.4.0 to 17.4.1 (#67) Bumps [dotenv](https://github.com/motdotla/dotenv) from 17.4.0 to 17.4.1. - [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md) - [Commits](https://github.com/motdotla/dotenv/compare/v17.4.0...v17.4.1) --- updated-dependencies: - dependency-name: dotenv dependency-version: 17.4.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump @tanstack/react-query from 5.96.2 to 5.99.0 (#70) Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.96.2 to 5.99.0. - [Release notes](https://github.com/TanStack/query/releases) - [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md) - [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.99.0/packages/react-query) --- updated-dependencies: - dependency-name: "@tanstack/react-query" dependency-version: 5.99.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump typescript from 5.9.3 to 6.0.2 (#64) Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.3 to 6.0.2. - [Release notes](https://github.com/microsoft/TypeScript/releases) - [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v6.0.2) --- updated-dependencies: - dependency-name: typescript dependency-version: 6.0.2 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump prettier from 3.8.1 to 3.8.2 (#68) Bumps [prettier](https://github.com/prettier/prettier) from 3.8.1 to 3.8.2. - [Release notes](https://github.com/prettier/prettier/releases) - [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/prettier/compare/3.8.1...3.8.2) --- updated-dependencies: - dependency-name: prettier dependency-version: 3.8.2 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump react from 19.2.4 to 19.2.5 (#63) Bumps [react](https://github.com/facebook/react/tree/HEAD/packages/react) from 19.2.4 to 19.2.5. - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/v19.2.5/packages/react) --- updated-dependencies: - dependency-name: react dependency-version: 19.2.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump vitest from 4.1.2 to 4.1.4 (#69) Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 4.1.2 to 4.1.4. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.4/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 4.1.4 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump @prisma/client from 7.6.0 to 7.7.0 (#71) Bumps [@prisma/client](https://github.com/prisma/prisma/tree/HEAD/packages/client) from 7.6.0 to 7.7.0. - [Release notes](https://github.com/prisma/prisma/releases) - [Commits](https://github.com/prisma/prisma/commits/7.7.0/packages/client) --- updated-dependencies: - dependency-name: "@prisma/client" dependency-version: 7.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump shadcn from 4.1.2 to 4.2.0 (#72) Bumps [shadcn](https://github.com/shadcn-ui/ui/tree/HEAD/packages/shadcn) from 4.1.2 to 4.2.0. - [Release notes](https://github.com/shadcn-ui/ui/releases) - [Changelog](https://github.com/shadcn-ui/ui/blob/main/packages/shadcn/CHANGELOG.md) - [Commits](https://github.com/shadcn-ui/ui/commits/shadcn@4.2.0/packages/shadcn) --- updated-dependencies: - dependency-name: shadcn dependency-version: 4.2.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat(thresholds): foundation + settings UI (v1.2) Introduces per-user health threshold overrides with an auto-default / user-override pattern. Default computation stays evidence-based (WHO, ESC/ESH 2018, AHA, AASM, ADA 2024, DGIM/DDG S3). Overrides live in User.thresholdsJson; any missing metric falls through to the default so new users need zero configuration. Schema (migration 0021): - New enums: glucose_context (FASTING/POSTPRANDIAL/RANDOM/BEDTIME), feedback_category, feedback_status. - measurement_type gains BLOOD_GLUCOSE; Measurement gets optional glucose_context (enforced via CHECK constraint — only set for glucose). - User gains thresholds_json, dashboard_widgets_json, glucose_unit, ai_provider, ai_model, ai_base_url, ai_anthropic_key_encrypted, ai_local_key_encrypted (stubs for the upcoming features). - New Feedback table for the built-in feedback system (next commit). Backend: - src/lib/analytics/effective-range.ts is the single source of truth: getEffectiveRange(metric, profile, overrides) returns the resolved range + the pristine default + physiological bounds. Every chart, band, insight prompt, and doctor-report section should call this. - Supports 11 metrics incl. four glucose contexts with ADA/DGIM ranges. - src/lib/validations/thresholds.ts — Zod schema with per-metric bounds and min<max refinement. - GET/PUT/DELETE /api/user/thresholds with full audit logging (before/ after) so medical-ish data mutations are traceable. UI: - src/components/settings/thresholds-section.tsx — clean per-metric row with Auto/Override Switch, inline min/max, reset-per-row + reset-all, and a "this looks outside guideline ranges" warning banner that only shows when the override is >30% off the default. - New Personalization section in /settings. - 25 new i18n keys (EN + DE) in a `thresholds.*` namespace. Tests: 8 new cases covering default computation, override application, missing profile gracefully returning null, glucose-context defaults, and per-metric bounds. 150/150 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(glucose): wire BLOOD_GLUCOSE + context through the write path Extends measurement validation and the POST /api/measurements route to accept blood-glucose samples with a required `glucoseContext` (FASTING / POSTPRANDIAL / RANDOM / BEDTIME). - Zod schema adds BLOOD_GLUCOSE to the type enum and glucoseContextEnum. - refine(): BLOOD_GLUCOSE requires a context; every other type must not set one — mirrored by the Postgres CHECK constraint from migration 0021 so callers bypassing validation still can't write inconsistent rows. - VALUE_RANGES gains BLOOD_GLUCOSE (20-800 mg/dL) — covers severe hypo through severe hyperglycemia. - getUnitForType(BLOOD_GLUCOSE) returns "mg/dL" (canonical storage). Display in mmol/L is a UI-level conversion per user preference. - Error messages translated to English (removed two lingering German refine messages while here). Measurement form + charts + targets UI follow in the next commits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(i18n): localize AI insight prompts (en/de) Each prompt builder (base-system, blood-pressure, bmi, general-status, medication-compliance, mood, pulse, weight) now accepts a Locale and returns the system + user prompt either fully in English or fully in German. Stable contract keys (classification enum optimal/gut/grenzwertig /erhoht/kritisch, confidence niedrig/mittel/hoch) are preserved verbatim so the response parser keeps working — only the natural-language sections are translated. The prompt explicitly tells the model to emit classificationLabel / summary / recommendations in the user's language. Wires the locale through every status generator (general / blood-pressure / bmi / weight / pulse / mood / medication-compliance) into the prompt calls and adds a small server-side resolver (src/lib/i18n/server-locale.ts: explicit override > healthlog-locale or userLocale cookie > User.locale > Accept-Language header > default English) used by all /api/insights/*-status routes plus /api/insights/generate. The auditLog cache key already contains the locale suffix, so cached entries stay disjoint per language. The reminder worker continues to fall back to "de" when User.locale is null, matching its prior behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(glucose): UI + unit conversion helpers - src/lib/glucose.ts — mg/dL ↔ mmol/L conversion (factor 18.0182 per DGIM/DDG), resolveGlucoseUnit() defaulting to mg/dL, thresholdMetricForContext() mapping each context to the matching effective-range key. - Measurement form gains a "Blood glucose" option (mg/dL) with a context selector (fasting / postprandial / random / bedtime). The glucoseContext field is only sent for BLOOD_GLUCOSE — the Zod refinement and the Postgres CHECK constraint both enforce that. - 10 new i18n keys per locale (measurements.typeBloodGlucose, glucoseContext*, glucoseUnit, glucoseUnitHint). - 6 new tests covering conversion roundtrip, unit resolution defaults, and context → threshold-metric mapping. Charts, target cards, and doctor-PDF row for glucose follow in the next commits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(feedback): built-in feedback system (no GitHub required) Implements the feedback-system design from the v1.2 spec. The submit button on /bugreport now always works — even when no admin has configured a GitHub repository. Feedback is stored in the Feedback table (migration 0021) with an admin inbox for triage. Endpoints: - POST /api/feedback — user submits. Auth-gated, rate-limited 5/hour. Category (BUG / FEATURE_REQUEST / QUESTION / OTHER), subject, description, optional screenshot (≤5 MB, base64), optional metadata (locale, URL path, userAgent). - GET /api/feedback — user's own history (paginated). - GET /api/admin/feedback — admin inbox with status filter and counts-by-status for tab badges. - PATCH /api/admin/feedback/[id] — update status / admin note. - DELETE /api/admin/feedback/[id] — soft-archive. Every mutation writes to AuditLog with before/after details. UI: - /bugreport rewritten: category dropdown, optional subject, always submits. Informational banner explains whether the server has GitHub escalation available or keeps feedback internal-only. - Legacy "not configured" blocker is gone — the form is never hidden. - 8 new bugreport.* i18n keys per locale. The old POST /api/bugreport route and /api/bugreport/status endpoint are left intact for backwards compatibility; they'll be removed in v1.3 once no one depends on them. Admin inbox UI lands in the next commit alongside the dashboard and multi-provider work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(telegram): localize bot UI per User.locale The Telegram webhook now resolves the user's locale (User.locale, falling back to "de" so existing bots stay German) and runs every reply, inline- keyboard label and callback toast through a small server-side translator. Adds src/lib/i18n/server-translator.ts (getServerTranslator) — reads the same messages/{en,de}.json bundles the client uses, with the same "missing key falls back to English, then to the raw key" semantics, and the same {param} interpolation. Importing the client useTranslations() hook into a server route would have pulled React in, so this mirrors just the parts the bot needs. Adds the telegram.* namespace to messages/en.json and messages/de.json covering: button labels, callback toasts, the /help body, /add prompt, medication-not-found / inactive errors, snooze/skip/confirm confirmations and the cancel button. The "genommen" / "taken" intake keyword and the "hi/hallo/hey/moin/hello" greeting list are accepted regardless of UI locale to keep backwards compatibility for existing users. Also adds 6 vitest cases for getServerTranslator (basic resolution, locale switching, parameter interpolation, English fallback, raw-key fallback, exposed locale). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(dashboard): user-customizable widget layout Implements the dashboard customization spec: users choose which widgets appear on / and in what order. Auto-default keeps the current v1.1 behavior so nothing changes without opt-in. Backend: - src/lib/dashboard-layout.ts — single source of truth with DEFAULT_DASHBOARD_LAYOUT (10 widgets incl. the new sleep/steps/glucose ones that were previously hard-conditional), a version field for future schema evolution, and resolveDashboardLayout() that merges persisted state with defaults so new widgets auto-appear (invisible by default) when the schema bumps. - GET / PUT / DELETE /api/dashboard/widgets with strict Zod validation (version literal, widget-id enum, order range). - Persisted in User.dashboardWidgetsJson (column added in 0021). UI: - src/components/settings/dashboard-layout-section.tsx — mounted under the new Personalization section alongside Thresholds. Local edit buffer so toggling/reordering doesn't fire a network call per click; single "Save" commits. Each widget has Up/Down buttons + visibility toggle, plus a reset-to-defaults button at the section header. - 11 new dashboard.* i18n keys per locale. Dashboard page integration is staged — widgets currently still render per the old data-conditional rules. Wiring the saved layout through /app/page.tsx is a follow-up so this commit stays focused on the plumbing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(auth): locale-aware password-strength feedback Completes the zxcvbn i18n migration by wiring the new getZxcvbnTranslations(locale) helper into the client PasswordStrength component and removing the obsolete zxcvbn-de helper. - src/components/ui/password-strength.tsx reads the active locale from useTranslations() and memoizes the translator per locale — so changing language instantly retranslates open warnings/suggestions. - src/lib/zxcvbn-de.ts deleted. Every consumer now routes through zxcvbn-i18n.ts which carries both EN and DE mappings. - server-side checkPasswordStrength (already refactored earlier in this session) continues to accept locale; clients can pass it from the session. No test impact — 162 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(dashboard): replace setState-in-effect with draft-overlay pattern The dashboard-layout-section.tsx initial implementation synced server state into local state via useEffect, which trips the project's strict react-hooks/set-state-in-effect rule. Rewrote using a nullable draft pattern: `layout` is derived as `draft ?? remote`, and only user edits set the draft. Save/Reset clear the draft so the server copy wins again. Cancel also clears instead of re-seeding from server. No behavior change, no new tests — the component contract stays the same. Lint back to baseline (2 errors / 7 warnings, both pre-existing). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v1.2 README + CHANGELOG (thresholds, glucose, dashboard, feedback, multi-provider AI) - CHANGELOG 1.2.0 entry covering 5 new features + locale polish - README Key Features expanded: glucose, custom thresholds, dashboard customization, multi-provider AI, built-in feedback - Doctor Report description updated for locale-aware output Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(targets): glucose cards per context with effective range + unit conversion Render one target card per logged glucose context (FASTING, POSTPRANDIAL, RANDOM, BEDTIME) on /targets, sourcing the threshold band from getEffectiveRange() so user overrides apply consistently. The targets API returns glucose values canonical mg/dL; the page converts to the user's glucoseUnit preference via convertGlucose() before display. - Extend /api/insights/targets to include per-context glucose summaries (latest, 30-day average, range, classification) and surface glucoseUnit in the profile envelope. - Expose glucoseUnit on /api/auth/me and AuthUser type. - Targets page resolves the i18n label key, applies unit conversion to current/avg/range, and uses Droplet iconography. - Add targets.glucoseFasting/Postprandial/Random/Bedtime keys to en + de. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(security): address H1 (CSP nonce), H3 (SSRF), M1+M2 (XSS) From the v1.2 full-repo security audit: - **H1 / CWE-79** src/proxy.ts: CSP nonce was base64 of the UUID *string* form, which gives ~122 bits and a structured pattern (hyphens at fixed offsets). Replaced with 16 random bytes → proper 128-bit nonce. - **H3 / CWE-918** src/app/api/admin/ai-settings/route.ts: the admin AI base URL only required "https://". A compromised admin cookie could point it at attacker.example and exfiltrate the bearer key on the next insight call. Now validates with URL parser, requires https://, and only accepts hosts on a small allowlist (OpenAI, Anthropic, Google AI, Mistral, Groq, OpenRouter). Ops can extend via `ADMIN_AI_BASE_URL_ALLOWLIST` env var. - **M1 / CWE-79 (stored, off-platform)** src/app/api/bugreport/route.ts: bug-report description flowed to GitHub with only backtick/asterisk stripping. Now strips HTML tags entirely, neutralises dangerous markdown link schemes (javascript:/data:/vbscript:/file:), and wraps the whole body in a fenced code block so residual markdown renders literally. - **M2 / CWE-79** Zod schemas for feedback + bugreport screenshot now require a base64 PNG/JPEG/WEBP/GIF data URL via regex. SVG deliberately excluded — it can carry inline JS that would execute when the admin inbox renders the `<img>` preview. Medium rate-limiter semantics (audit M3) left as-is: the fixed-window behaviour is acceptable for a single-tenant PWA and matches what ops actually want. Remaining M4–M6 + L1–L7 are tracked for v1.3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(dashboard): glucose TrendCards + chart label support Render one TrendCard per logged glucose context on the dashboard when the user has the "glucose" widget enabled in their layout. Cards respect the user's glucoseUnit preference via convertGlucose(); data sources from the existing /api/analytics endpoint, which now emits a glucoseByContext map alongside the summaries object. - /api/analytics: include BLOOD_GLUCOSE in the type whitelist (so total counts include glucose) and add glucoseByContext containing a summarize() result per context. - HealthChart: wire BLOOD_GLUCOSE into BASE_TYPE_LABEL_KEYS so the legend shows the localized "Blood glucose" label when the chart is invoked for that type. - Dashboard page: fetch the dashboard layout, gate glucose cards on widget visibility AND data presence, render Droplet-icon TrendCards. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(i18n): bugreport.subtitle no longer promises a GitHub issue The visual QA pass flagged that bugreport.subtitle still said "a GitHub issue will be created automatically" even though the banner below the heading correctly explains the submission is stored locally (built-in feedback system, GitHub is only an optional escalation). Neutral copy now that matches the actual behaviour in both locales. * feat(ai): user-level provider selection + test endpoint + UI Adds the missing pieces for the multi-provider AI MVP that pair with the AnthropicClient and LocalOpenAICompatibleClient already on disk: * /api/user/ai-provider (GET + PATCH): user can pick OPENAI / ANTHROPIC / LOCAL / CHATGPT_OAUTH and store provider, model, baseUrl and per-provider API keys. Anthropic + local keys go through the existing AES-256-GCM crypto helper. Provider value is whitelisted. * /api/ai/test (POST): rate-limited (5/min/user) round-trip that resolves the user's provider and asks for a tiny JSON ack so the UI surfaces real "does this config work?" feedback instead of a blind Save. * settings/page.tsx: appends a per-user override card under the existing AI Insights section. Provider dropdown drives conditional fields (Anthropic key for ANTHROPIC, baseUrl + optional bearer for LOCAL, model for all), then Save / Test buttons. * provider.test.ts: extended to cover the four new cases (ANTHROPIC with key, ANTHROPIC fallback, LOCAL, OPENAI routes through admin key, CHATGPT_OAUTH routes through Codex). Switched to resetAllMocks() in beforeEach to flush mockResolvedValueOnce queues between cases — the prior clearAllMocks() leaked them. * local-client.test.ts: full happy / no-auth / bearer-auth / structured error / empty-content coverage mirroring the openai-client suite. typecheck + tests green; lint stays at the pre-existing 2 errors / 7 warnings baseline (no new findings introduced). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: integrate late agent additions (glucose PDF row + i18n) * test(glucose): cover display-unit conversion edge cases Lock in the precision contract for the targets/dashboard/PDF surfaces: canonical mg/dL stays as integers, mmol/L collapses to 1-decimal, and the ADA diabetes thresholds (70/99/126) round-trip to their published SI values (3.9 / 5.5 / 7.0 mmol/L). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(i18n): eliminate hydration flash by threading SSR-resolved locale to I18nProvider Layout already resolves the initial locale from cookie + Accept-Language and sets <html lang>. Until now the client I18nProvider re-detected locale at mount via getSavedLocale() / detectSystemLocale(), causing a flash where "Loading…" (EN) rendered first, then flipped to "Laden…" (DE) after hydration. Pass initialLocale down layout -> Providers -> I18nProvider so useState seeds the same locale the server used. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(admin/feedback): return 409 when GitHub issue already published Spec requires 409 Conflict (not 200 with flag) when the feedback row already has gitHubIssueUrl set. Drops the alreadyPublished response field in favour of a clear error status. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(crypto,dashboard): L4 hard-fail short ENCRYPTION_KEY in prod + dashboard WIP from parallel agent * refactor(effective-range): collapse duplicated glucose case arms Extract glucoseRange() helper and toDate() normalizer. Use METRIC_BOUNDS keys as the source of truth for getAllEffectiveRanges() metric list so additions stay in one place. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(dashboard-layout-section): drop redundant setLayout alias Remove the trivial setLayout wrapper and hoist the per-row label key lookup so it runs once per widget instead of twice (span + aria-label). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(security): MED-1 SSRF on user AI baseUrl + MED-2 publish-to-GitHub race Final-audit blockers before v1.2 merge. MED-1 — SSRF via user-level LOCAL AI baseUrl Authenticated user could set aiBaseUrl to http://169.254.169.254/, http://localhost:9090/metrics, or any internal host and get response bodies echoed back via the bodyExcerpt error field. Route the value through the existing isPublicUrl() helper (exported from lib/validations/notifications.ts). Ops can opt-in to private hosts on a specific instance via ALLOW_LOCAL_AI_PRIVATE_HOSTS=true — that's how self-hosted Ollama / LM Studio stays usable without exposing the SSRF surface by default. MED-2 — Race in POST /api/admin/feedback/[id]/github Two concurrent admin clicks both saw gitHubIssueUrl=null and both created duplicate GitHub issues. Atomic claim now: a single updateMany({ where: { id, gitHubIssueUrl: null }, data: PENDING }) — only the caller whose count=1 proceeds. PENDING is released on failure so retries work. Tests + typecheck green. Baseline lint unchanged. * fix: i18n completeness + UX polish (closes #73, #74) (#75) * fix(i18n,ux): package A — missing keys, chart labels, query invalidation, admin error UX Addresses bugs surfaced in issues #73 and #74 plus drift found during audit. - i18n: add 21 missing translation keys (targets.* namespace was only partially populated after the English-first migration; common.reset and settings.configured were referenced but absent), plus common helpers (copy, copyDetails, retry, reportIssue) and admin.settingsSaveError. - charts: rename points7Label/30Label/90Label from cryptic "7P/30P/90P" to "7 pts / 30 pts / 90 pts" in EN and "7 Pkt / 30 Pkt / 90 Pkt" in DE. - queries: add src/lib/query-keys.ts as a single source of truth with measurement/mood/medication-dependent key sets. Wire them into measurement-form/list, mood-form/list, medication-form/card, intake-history-list, intake-timeline, medications page, and Withings sync in settings. Fixes dashboard not auto-refreshing after adding a measurement (old code only invalidated ["measurements"], not ["analytics"] / ["insights"] / ["insights","targets"]). - admin: useUpdateSettings + updateUser mutations now surface real error messages via sonner toasts and invoke getApiErrorMessage() so saves fail loudly instead of silently. Success also gets a toast. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(i18n): package B — locale-aware formatters throughout Removes the hard-coded de-DE locale that lingered from the German-first era and makes every number, date, and time locale-aware. English stays the default per user preference; German remains selectable. New: - src/lib/format-locale.ts exposes makeFormatters(locale) + a cached Europe/Berlin timezone. Covers number, integer, percent, date, dateShort, dateTime, time, dateWithWeekday, monthShort. Also exports parseLocaleFromAcceptLanguage() for server code. - useFormatters() hook in @/lib/i18n/context resolves from the active locale, memoized per render. Migrated (all Intl.NumberFormat("de-DE") / toLocaleString("de-DE") / Intl.DateTimeFormat("de-DE") call sites): - src/app/page.tsx (dashboard) - src/app/insights/page.tsx - src/app/admin/page.tsx - src/app/settings/page.tsx (Withings last-sync, doctor-report caller) - src/components/charts/trend-card.tsx (+ i18n "7d"/"30d" short labels) - src/components/charts/health-chart.tsx - src/components/medications/medication-card.tsx - src/components/medications/medication-form.tsx - src/components/measurements/measurement-list.tsx - src/components/mood/mood-list.tsx - src/components/insights/insight-status-card.tsx (also removed 5 German hard-coded strings with TODO: i18n markers — now translated) - src/components/insights/insight-advisor-card.tsx (same — 10+ strings) Server: reminder-check / reminder-worker / umami-test switched from "de-DE" to neutral en-GB/en-US where the string is machine-compared rather than user-facing. Bug-report GitHub issue titles use ISO-8601 UTC now — the repo is English-speaking. Next.js metadata: - OpenGraph locale from "de_DE" → "en_US" with "de_DE" as alternateLocale. - Root layout now reads healthlog-locale cookie (falls back to Accept-Language) and sets <html lang> server-side, avoiding an EN-flashes-then-DE hydration gap and fixing SEO/a11y. - I18nProvider mirrors locale to a 1-year SameSite=Lax cookie so the next SSR pass is correct. Doctor-report PDF: - generateDoctorReportPDF(data, { t, locale }) now accepts injected translator + locale. All section titles, column headers, BMI / BP classifications, mood labels, and the footer disclaimer are i18n-driven. File name slug switches between gesundheitsbericht / health-report. - 40+ new doctorReport.* keys in EN + DE. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ux): package C — typed passkey errors, bug-report gating, error boundary Addresses issue #73 points 1, 5, 6 and lays groundwork for copy-paste error reporting requested during review. Passkey registration / login (#73.1): - src/lib/passkey-errors.ts maps DOMException.name + SimpleWebAuthn code to specific i18n keys instead of swallowing every failure as "cancelled". Distinguishes NotAllowed/InvalidState/NotSupported/ Security/Abort errors and falls back to a diagnostic message. - settings passkey + login passkey flows both use describePasskeyError now, so origin mismatches and previous-registration collisions show actionable copy. - 5 new passkey.* i18n keys in EN + DE. Bug report (#73.5): - New GET /api/bugreport/status reports {configured, hasToken, hasRepo, isAdmin}. - /bugreport page now queries status first. When not configured it shows a notice (different copy for admins vs. regular users) with a direct link to the admin panel. Form is hidden, not silently broken. - When POST /api/bugreport can't create issues it returns 503 instead of 500 with the same user-facing message. - 4 new bugreport.* i18n keys. Global error UX: - src/components/error-details.tsx — reusable panel with retry + copy diagnostics (message, digest, stack (trimmed), URL, user-agent, request-id, locale, timestamp) + report-issue link. Clipboard fallback prompt covers insecure-context cases. - src/app/error.tsx uses it. - src/app/global-error.tsx is provider-free (no i18n dependency) since it replaces the root layout on catastrophic failures — hardcoded English + Dracula palette so it still looks like HealthLog. - 2 new errorBoundary.* keys and 4 common.* keys (copy, copyDetails, retry, reportIssue). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(query-keys): collapse repeated invalidation blocks into invalidateKeys helper Eleven mutation handlers repeated `Promise.all(keys.map(k => invalidate({queryKey: k})))` verbatim. Adds `invalidateKeys(qc, keys)` to the existing query-keys factory and adopts it everywhere, including replacing the local `invalidateMedicationRelated` helper that medication-form.tsx had grown. Also fixes the import-sandwich that helper introduced. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: small simplifications in i18n/UX changes - passkey-errors: drop the typeof-name dance now that error.name is already string after the instanceof Error narrow - doctor-report-pdf: replace nested gender ternary with a small lookup map; clarify unitFor's null-as-translation-marker convention - dashboard page: route the analytics queries through the new queryKeys factory so future invalidation stays in one place - health-chart: add missing fmt dependency to trendInfo useMemo (introduced when formatSigned was switched to the locale-aware formatter) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ai): capture upstream response body in OpenAI errors A production insights burst at 12:19 UTC failed with "OpenAI request failed (500)". The 500 was the upstream OpenAI HTTP status (confirmed via logs), but we had no way to tell from logs whether it was an OpenAI incident, a model-not-found, or a response_format rejection because the response body was thrown away. Now the error message includes the first 500 chars of the response body and structured fields (httpStatus, upstream, model) that Wide Event logging can attach. No behavior change for the happy path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(review): address code-review + security findings Review round produced five real issues — fix them all, plus three nits that were cheap enough to bundle. Must-fix: - security: error-details.tsx + global-error.tsx were copying window.location.href into the clipboard, which on OAuth callback routes (/api/withings/*, password reset) would leak code/state tokens into GitHub issues. Strip to pathname + searchParamCount. - fe: query-keys.invalidateKeys now uses Promise.allSettled, so a transient invalidate failure doesn't abort the chain and no unhandled rejection leaks from void invalidateKeys(...) call sites. - fe: resolveInitialLocale in app/layout.tsx wraps cookies()/headers() in try/catch — a dynamic-server error in the root layout now degrades to the "en" default instead of crashing into global-error. - be: reminder-worker + reminder-check parseTimeToMinutes normalize "24:00" → "00:00" (some ICU builds emit that for midnight under en-GB). Prevents a 1440-minute comparison against schedule windows. - security: openai-client error body excerpt redacts sk-* / Bearer tokens before logging, and moves the excerpt off Error.message into a structured field (bodyExcerpt) so log transports can filter it. - i18n: /api/withings/connect German error message → English. - i18n: /insights/page.tsx hardcoded "Fehler beim Laden" → t(...) Hardening: - /api/bugreport/status collapses to {configured, isAdmin} and adds a 30/min rate limit per user. Client interface updated. - i18n/context.tsx: I18nProvider's initializer now reads the cookie (matches SSR) before falling back to localStorage or browser detection — eliminates the locale-flash hydration class. - format.ts legacy helpers also read cookie first, matching SSR. Tests: - new src/lib/__tests__/format-locale.test.ts — 9 cases covering makeFormatters, parseLocaleFromAcceptLanguage, resolveIntlLocale - new passkey-errors.test.ts — 8 cases covering every DOMException name and SimpleWebAuthn code we map - new query-keys.test.ts — bundles + invalidateKeys allSettled semantics (incl. partial-failure test) 118 → 142 tests. All pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Marc-André Bombeck <marc@bombeck.io> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: restore Link import lost in merge * fix(bugreport): remove stale loading + not-configured blocks lost in merge The v1.2 built-in feedback refactor made status informational only — form always renders. Main's merge re-introduced the pre-refactor blocker blocks referencing undefined statusLoading/AlertCircle and broke the build. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Marc-André Bombeck <marc@bombeck.io> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Marc-André Bombeck <marc@bombeck.io> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1654 lines
70 KiB
JSON
1654 lines
70 KiB
JSON
{
|
||
"common": {
|
||
"save": "Save",
|
||
"cancel": "Cancel",
|
||
"delete": "Delete",
|
||
"edit": "Edit",
|
||
"moreOptions": "More options",
|
||
"create": "Create",
|
||
"close": "Close",
|
||
"loading": "Loading...",
|
||
"error": "Error",
|
||
"success": "Success",
|
||
"saved": "Saved",
|
||
"confirm": "Confirm",
|
||
"back": "Back",
|
||
"next": "Next",
|
||
"yes": "Yes",
|
||
"no": "No",
|
||
"search": "Search",
|
||
"filter": "Filter",
|
||
"noData": "No data available",
|
||
"optional": "Optional",
|
||
"required": "Required",
|
||
"all": "All",
|
||
"active": "Active",
|
||
"inactive": "Inactive",
|
||
"enabled": "Enabled",
|
||
"disabled": "Disabled",
|
||
"networkError": "Network error",
|
||
"unknownError": "Unknown error",
|
||
"notLoggedIn": "Not logged in",
|
||
"noAccess": "Access denied",
|
||
"copied": "Copied!",
|
||
"or": "or",
|
||
"unknown": "Unknown",
|
||
"replace": "Replace",
|
||
"add": "Add",
|
||
"import": "Import",
|
||
"test": "Test",
|
||
"reset": "Reset",
|
||
"copy": "Copy",
|
||
"copyDetails": "Copy details",
|
||
"retry": "Retry",
|
||
"reportIssue": "Report issue"
|
||
},
|
||
"doctorReport": {
|
||
"title": "Health Report",
|
||
"subtitle": "HealthLog — Personal Health Report",
|
||
"patient": "Patient",
|
||
"dateOfBirth": "Date of birth",
|
||
"gender": "Gender",
|
||
"genderMale": "Male",
|
||
"genderFemale": "Female",
|
||
"genderOther": "Non-binary",
|
||
"height": "Height",
|
||
"period": "Reporting period",
|
||
"createdOn": "Created on",
|
||
"vitalsTitle": "Vital signs — Summary",
|
||
"colParameter": "Parameter",
|
||
"colCurrent": "Current",
|
||
"colAvgPeriod": "Avg. period",
|
||
"colMin": "Min",
|
||
"colMax": "Max",
|
||
"colN": "n",
|
||
"bpClassificationTitle": "Blood pressure — ESC/ESH classification (2018)",
|
||
"avgBp": "Average blood pressure",
|
||
"classification": "Classification",
|
||
"bmiTitle": "Body Mass Index (BMI)",
|
||
"bmiRow": "BMI: {bmi} kg/m² — Classification: {class} (WHO)",
|
||
"complianceTitle": "Medication compliance",
|
||
"colMedication": "Medication",
|
||
"colTaken": "Taken",
|
||
"colSkipped": "Skipped",
|
||
"colMissed": "Missed",
|
||
"colTotal": "Total",
|
||
"colComplianceRate": "Compliance rate",
|
||
"moodTitle": "Mood trajectory",
|
||
"moodSummary": "Average: {avg} / 5.0 · Entries: {count} · Range: {min}–{max}",
|
||
"colMood": "Mood",
|
||
"colCount": "Count",
|
||
"colShare": "Share",
|
||
"footerDisclaimer1": "This report was generated automatically from self-tracked data and is intended to support",
|
||
"footerDisclaimer2": "the doctor–patient conversation. It does not replace a medical diagnosis. Correlations do not imply causation.",
|
||
"footerSource": "Source: HealthLog | Timezone: Europe/Berlin | Created: {timestamp}",
|
||
"typeWeight": "Body weight",
|
||
"typeBpSys": "Systolic blood pressure",
|
||
"typeBpDia": "Diastolic blood pressure",
|
||
"typePulse": "Resting pulse",
|
||
"typeBodyFat": "Body fat",
|
||
"typeSleep": "Sleep duration",
|
||
"typeSteps": "Activity (steps)",
|
||
"unitSteps": "steps",
|
||
"bmiUnderweight": "Underweight",
|
||
"bmiNormal": "Normal weight",
|
||
"bmiOverweight": "Overweight (pre-obese)",
|
||
"bmiObeseGrade1": "Obesity grade I",
|
||
"bmiObeseGrade2": "Obesity grade II",
|
||
"bmiObeseGrade3": "Obesity grade III",
|
||
"bpOptimal": "Optimal",
|
||
"bpNormal": "Normal",
|
||
"bpHighNormal": "High-normal",
|
||
"bpHypertensionGrade1": "Hypertension grade 1",
|
||
"bpHypertensionGrade2": "Hypertension grade 2",
|
||
"bpHypertensionGrade3": "Hypertension grade 3",
|
||
"moodAwful": "Very bad",
|
||
"moodBad": "Bad",
|
||
"moodNeutral": "Neutral",
|
||
"moodGood": "Good",
|
||
"moodGreat": "Very good",
|
||
"typeGlucoseFasting": "Glucose (fasting)",
|
||
"typeGlucosePostprandial": "Glucose (postprandial)",
|
||
"typeGlucoseRandom": "Glucose (random)",
|
||
"typeGlucoseBedtime": "Glucose (bedtime)",
|
||
"glucoseClassificationTitle": "Blood glucose — reference ranges (ADA 2024 / DDG)",
|
||
"glucoseRow": "{label}: avg {avg} {unit} (target {rangeMin}–{rangeMax}) — {class}",
|
||
"glucoseInTarget": "in target",
|
||
"glucoseBelowTarget": "below target",
|
||
"glucoseAboveTarget": "above target"
|
||
},
|
||
"errorBoundary": {
|
||
"title": "Something went wrong",
|
||
"description": "An unexpected error occurred. You can retry, copy the technical details, or open a bug report."
|
||
},
|
||
"nav": {
|
||
"home": "Home",
|
||
"dashboard": "Dashboard",
|
||
"measurements": "Measurements",
|
||
"medications": "Medications",
|
||
"charts": "History",
|
||
"insights": "Insights",
|
||
"targets": "Targets",
|
||
"achievements": "Achievements",
|
||
"settings": "Settings",
|
||
"bugreport": "Bug Report",
|
||
"admin": "Admin Console",
|
||
"logout": "Sign out",
|
||
"login": "Sign in",
|
||
"notifications": "Notifications",
|
||
"theme": "Appearance",
|
||
"themeSystem": "System",
|
||
"themeDark": "Dark",
|
||
"themeLight": "Light",
|
||
"collapseSidebar": "Collapse",
|
||
"userMenu": "User menu",
|
||
"mood": "Mood",
|
||
"skipToContent": "Skip to content",
|
||
"sidebar": "Sidebar",
|
||
"mainNavigation": "Main navigation",
|
||
"mobileNavigation": "Mobile navigation",
|
||
"loadingScreen": "Loading…"
|
||
},
|
||
"auth": {
|
||
"login": "Sign in",
|
||
"register": "Sign up",
|
||
"loginTitle": "Sign In",
|
||
"registerTitle": "Sign Up",
|
||
"username": "Username",
|
||
"password": "Password",
|
||
"email": "Email address",
|
||
"emailPlaceholder": "name@example.com",
|
||
"emailOrUsername": "Email address or username",
|
||
"emailOrUsernamePlaceholder": "name@example.com or username",
|
||
"loginWithPasskey": "Sign in with Passkey",
|
||
"loginWithPassword": "Sign in with password",
|
||
"noAccount": "Don't have an account?",
|
||
"hasAccount": "Already have an account?",
|
||
"registerDisabled": "Registration is disabled.",
|
||
"loginFailed": "Login failed",
|
||
"passkeyFailed": "Passkey authentication cancelled or failed",
|
||
"registerFailed": "Registration failed",
|
||
"createAccount": "Create account",
|
||
"newAccount": "Create new account",
|
||
"backToPasskey": "Back to Passkey",
|
||
"passkeySetupLater": "You can set up passkeys at any time in Settings.",
|
||
"passwordMinLength": "Min. 12 characters",
|
||
"passwordTooShort": "Password must be at least {minLength} characters long.",
|
||
"passwordStrength": "Password strength",
|
||
"strengthWeak": "Very weak",
|
||
"strengthFair": "Weak",
|
||
"strengthGood": "Fair",
|
||
"strengthStrong": "Strong",
|
||
"strengthVeryStrong": "Very strong"
|
||
},
|
||
"dashboard": {
|
||
"title": "Dashboard",
|
||
"subtitle": "Your health overview",
|
||
"greeting": {
|
||
"morning": "Good morning",
|
||
"day": "Hello",
|
||
"evening": "Good evening"
|
||
},
|
||
"welcomeBack": "{greeting}, welcome back.",
|
||
"welcomeBackWithName": "{greeting} {name}, welcome back.",
|
||
"weight": "Weight",
|
||
"bloodPressure": "Blood Pressure",
|
||
"bloodPressureSys": "Blood Pressure (Sys)",
|
||
"bloodPressureDia": "Blood Pressure (Dia)",
|
||
"pulse": "Pulse",
|
||
"bodyFat": "Body Fat",
|
||
"bpInTarget": "BP in Target",
|
||
"days30Short": "30d",
|
||
"medications": "Medications",
|
||
"lastMeasurement": "Last measurement",
|
||
"noMeasurements": "No measurements yet",
|
||
"trend": "Trend",
|
||
"trendUp": "rising",
|
||
"trendDown": "falling",
|
||
"trendStable": "stable",
|
||
"quickEntry": "Quick entry",
|
||
"addMeasurement": "Add measurement",
|
||
"compliance7d": "Compliance (7 days)",
|
||
"complianceGood": "Good",
|
||
"complianceLow": "Low",
|
||
"mood": "Mood",
|
||
"customizeTitle": "Dashboard layout",
|
||
"customizeSubtitle": "Choose which cards appear on your dashboard and in what order. Defaults work out of the box.",
|
||
"layoutReset": "Reset to defaults",
|
||
"layoutSaveSuccess": "Dashboard layout saved",
|
||
"layoutSaveError": "Could not save dashboard layout",
|
||
"layoutResetSuccess": "Reset to default layout",
|
||
"layoutUsingDefaults": "Using default layout",
|
||
"layoutCustomized": "Custom layout active",
|
||
"moveUp": "Move up",
|
||
"moveDown": "Move down"
|
||
},
|
||
"measurements": {
|
||
"title": "Measurements",
|
||
"subtitle": "Weight, blood pressure, pulse & more",
|
||
"addMeasurement": "Add",
|
||
"editMeasurement": "Edit measurement",
|
||
"type": "Type",
|
||
"value": "Value",
|
||
"unit": "Unit",
|
||
"date": "Date",
|
||
"source": "Source",
|
||
"notes": "Notes",
|
||
"typeWeight": "Weight",
|
||
"typeBloodPressure": "Blood Pressure",
|
||
"typeBpSys": "Sys",
|
||
"typeBpDia": "Dia",
|
||
"typePulse": "Pulse",
|
||
"typeBodyFat": "Body Fat",
|
||
"typeSleep": "Sleep",
|
||
"typeSteps": "Steps",
|
||
"typeBloodGlucose": "Blood glucose",
|
||
"glucoseContext": "Context",
|
||
"glucoseContextFasting": "Fasting (≥8h without food)",
|
||
"glucoseContextPostprandial": "Post-meal (2h after eating)",
|
||
"glucoseContextRandom": "Random / casual",
|
||
"glucoseContextBedtime": "Before bed",
|
||
"glucoseUnit": "Glucose display unit",
|
||
"glucoseUnitHint": "Stored canonically in mg/dL; display unit is your preference.",
|
||
"unitHours": "hours",
|
||
"unitSteps": "steps",
|
||
"sourceManual": "Manual",
|
||
"sourceWithings": "Withings",
|
||
"sourceImport": "Import",
|
||
"systolicLabel": "Systolic (mmHg)",
|
||
"diastolicLabel": "Diastolic (mmHg)",
|
||
"pulseLabel": "Pulse (bpm)",
|
||
"valueWithUnit": "Value ({unit})",
|
||
"timestamp": "Time",
|
||
"notesPlaceholder": "e.g. after a meal",
|
||
"formReset": "Reset form",
|
||
"saveError": "Error saving",
|
||
"deleteError": "Error deleting",
|
||
"invalidValue": "Please enter a valid value.",
|
||
"invalidTimestamp": "Please select a valid time.",
|
||
"loginRequired": "Please sign in to view measurements.",
|
||
"allTypes": "All types",
|
||
"measurementCount": "{count} measurements",
|
||
"noMeasurements": "No measurements found",
|
||
"comment": "Comment",
|
||
"actions": "Actions",
|
||
"deleteConfirmTitle": "Delete measurement?",
|
||
"deleteConfirmDescription": "This measurement will be permanently deleted.",
|
||
"pageInfo": "Page {page} of {total}",
|
||
"showing": "Showing",
|
||
"of": "of",
|
||
"entries": "entries"
|
||
},
|
||
"mood": {
|
||
"title": "Mood",
|
||
"subtitle": "Mood tracker & entries",
|
||
"addEntry": "Add",
|
||
"editEntry": "Edit entry",
|
||
"moodLevel": "Mood",
|
||
"tags": "Tags",
|
||
"tagsPlaceholder": "e.g. Exercise, Work, Family",
|
||
"tagsHelp": "Comma-separated",
|
||
"timestamp": "Time",
|
||
"date": "Date",
|
||
"source": "Source",
|
||
"levelSuperGut": "Amazing",
|
||
"levelGut": "Good",
|
||
"levelOkay": "Okay",
|
||
"levelSchlecht": "Bad",
|
||
"levelLausig": "Terrible",
|
||
"sourceManual": "Manual",
|
||
"sourceMoodlog": "moodLog",
|
||
"sourceWeb": "Web",
|
||
"sourceTelegram": "Telegram",
|
||
"sourceDaylio": "Daylio",
|
||
"allMoods": "All moods",
|
||
"entryCount": "{count} entries",
|
||
"noEntries": "No mood entries found",
|
||
"actions": "Actions",
|
||
"formReset": "Reset form",
|
||
"saveError": "Error saving",
|
||
"deleteError": "Error deleting",
|
||
"invalidTimestamp": "Please select a valid time.",
|
||
"deleteConfirmTitle": "Delete entry?",
|
||
"deleteConfirmDescription": "This mood entry will be permanently deleted.",
|
||
"pageInfo": "Page {page} of {total}",
|
||
"loginRequired": "Please sign in to view mood entries."
|
||
},
|
||
"medications": {
|
||
"title": "Medications",
|
||
"subtitle": "Medications, schedules & intake",
|
||
"loginRequired": "Please sign in to manage medications.",
|
||
"addMedication": "Add",
|
||
"editMedication": "Edit medication",
|
||
"newMedication": "New medication",
|
||
"firstMedication": "Add first medication",
|
||
"noMedicationsYet": "No medications added yet",
|
||
"loadFailed": "Could not load medications",
|
||
"retryLoad": "Retry",
|
||
"name": "Name",
|
||
"dose": "Dose",
|
||
"doseAmount": "Amount",
|
||
"doseUnit": "Unit",
|
||
"category": "Category",
|
||
"schedule": "Schedule",
|
||
"scheduleFrom": "From",
|
||
"scheduleTo": "To",
|
||
"scheduleLabel": "Label",
|
||
"scheduleDays": "Days",
|
||
"scheduleDaily": "Daily",
|
||
"active": "Active",
|
||
"inactive": "Paused",
|
||
"intakeHistory": "Intake history",
|
||
"showIntakeHistory": "Show intake history",
|
||
"hideIntakeHistory": "Hide intake history",
|
||
"markTaken": "Taken",
|
||
"markSkipped": "Skipped",
|
||
"deleteConfirm": "Delete medication?",
|
||
"deleteDescription": "The medication and all intake records will be permanently deleted.",
|
||
"noMedications": "No medications found.",
|
||
"compliance": "Compliance",
|
||
"import": "Import",
|
||
"importIntakes": "Import intakes",
|
||
"importDescription": "Paste a JSON array with intake data. Expected format:",
|
||
"importUploadFile": "Upload JSON file",
|
||
"importPaste": "Paste JSON here...",
|
||
"importSelected": "Selected: {name}",
|
||
"importFileLoaded": "File \"{name}\" loaded",
|
||
"importInvalidJson": "File does not contain valid JSON",
|
||
"importNoArray": "No array found",
|
||
"importResult": "{imported} intakes imported",
|
||
"importDuplicatesSkipped": "{count} duplicates skipped",
|
||
"importInvalidSkipped": "{count} invalid entries skipped",
|
||
"importFailed": "Import failed",
|
||
"importInvalidFormat": "Invalid JSON format",
|
||
"apiAccess": "API access",
|
||
"apiEndpointTitle": "API Endpoint: {name}",
|
||
"apiEndpointDescription": "Use this endpoint to record intakes via API (e.g., iPhone Shortcut or cron job).",
|
||
"apiEndpointActive": "API endpoint active",
|
||
"apiEndpointActivated": "API endpoint activated",
|
||
"apiEndpointDeactivated": "API endpoint deactivated",
|
||
"apiTokenCount": "Active ({count} tokens)",
|
||
"apiToken": "API Token",
|
||
"apiTokenActiveHint": "Endpoint is active. Existing tokens remain valid but cannot be shown in plain text again.",
|
||
"apiTokenActivateHint": "Activate the endpoint to create a new token.",
|
||
"apiTokenOnceHint": "The token is only shown in plain text once at creation.",
|
||
"requestExample": "Request example",
|
||
"statusLoadFailed": "Could not load status",
|
||
"changeFailed": "Change failed",
|
||
"categoryBloodPressure": "Blood Pressure",
|
||
"categoryCholesterol": "Cholesterol",
|
||
"categoryDiabetes": "Diabetes",
|
||
"categoryVitamin": "Vitamins",
|
||
"categoryThyroid": "Thyroid",
|
||
"categoryHormone": "Hormones",
|
||
"categorySupplement": "Supplement",
|
||
"categoryPainRelief": "Pain Relief",
|
||
"categoryAllergy": "Allergy",
|
||
"categoryDigestive": "Gastrointestinal",
|
||
"categorySkin": "Skincare",
|
||
"categorySleepAid": "Sleep Aid",
|
||
"categoryOther": "Other",
|
||
"daysSun": "Sun",
|
||
"daysMon": "Mon",
|
||
"daysTue": "Tue",
|
||
"daysWed": "Wed",
|
||
"daysThu": "Thu",
|
||
"daysFri": "Fri",
|
||
"daysSat": "Sat",
|
||
"intakeHistoryTitle": "Intake History",
|
||
"newIntake": "Add",
|
||
"editIntake": "Edit Intake",
|
||
"intakeScheduledFor": "Scheduled",
|
||
"intakeTakenAt": "Taken at",
|
||
"intakeStatus": "Status",
|
||
"intakeSource": "Source",
|
||
"intakeStatusTaken": "Taken",
|
||
"intakeStatusSkipped": "Skipped",
|
||
"intakeStatusMissed": "Missed",
|
||
"intakeDeleteConfirm": "Delete intake record?",
|
||
"intakeDeleteDescription": "This intake record will be permanently deleted.",
|
||
"intakeCount": "{count} intakes",
|
||
"viewHistory": "History",
|
||
"today": "Today",
|
||
"yesterday": "Yesterday",
|
||
"tomorrow": "Tomorrow",
|
||
"withoutNotification": "Without notification",
|
||
"pausedSince": "Paused since",
|
||
"takeNow": "Take now",
|
||
"overdue": "Overdue",
|
||
"veryOverdue": "Very overdue",
|
||
"compliance7d": "7-day compliance",
|
||
"compliance30d": "30-day compliance",
|
||
"lastIntake": "Last intake:",
|
||
"nextIntake": "Next intake:",
|
||
"dayStreak": "day streak",
|
||
"taken": "Taken",
|
||
"skipped": "Skipped",
|
||
"weekdaySunday": "Sunday",
|
||
"weekdayMonday": "Monday",
|
||
"weekdayTuesday": "Tuesday",
|
||
"weekdayWednesday": "Wednesday",
|
||
"weekdayThursday": "Thursday",
|
||
"weekdayFriday": "Friday",
|
||
"weekdaySaturday": "Saturday",
|
||
"weekdaySundayPlural": "Sundays",
|
||
"weekdayMondayPlural": "Mondays",
|
||
"weekdayTuesdayPlural": "Tuesdays",
|
||
"weekdayWednesdayPlural": "Wednesdays",
|
||
"weekdayThursdayPlural": "Thursdays",
|
||
"weekdayFridayPlural": "Fridays",
|
||
"weekdaySaturdayPlural": "Saturdays",
|
||
"formType": "Type",
|
||
"formDose": "Dose",
|
||
"formUnit": "Unit",
|
||
"formSchedule": "Schedule",
|
||
"newSchedule": "New schedule",
|
||
"removeSchedule": "Remove schedule",
|
||
"advancedEditing": "Advanced editing",
|
||
"advancedEditingHide": "Hide advanced editing",
|
||
"scheduleInterval": "Interval",
|
||
"scheduleEveryWeek": "every week",
|
||
"scheduleEveryNWeeks": "every {n} weeks",
|
||
"scheduleIntervalPrefix": "Interval:",
|
||
"nextSchedulePrefix": "Next schedule:",
|
||
"nextScheduleDaily": "daily",
|
||
"importIntakesAction": "Import intakes",
|
||
"apiEndpointAction": "API endpoint",
|
||
"pause": "Pause",
|
||
"activate": "Activate",
|
||
"disableNotifications": "Disable notifications",
|
||
"enableNotifications": "Enable notifications",
|
||
"purgeRecords": "Delete all records",
|
||
"formReset": "Reset form",
|
||
"createMedication": "Create medication",
|
||
"deleteConfirmDescription": "The medication and all intake history will be permanently deleted.",
|
||
"purgeConfirmTitle": "Delete all records?",
|
||
"purgeConfirmDescription": "All intake records for this medication will be permanently deleted. The medication itself will remain and start without history data.",
|
||
"saveError": "Error saving",
|
||
"deleteError": "Error deleting",
|
||
"purgeError": "Error deleting records",
|
||
"invalidTimestamp": "Please select a valid time.",
|
||
"noIntakesYet": "No intakes recorded yet",
|
||
"noIntakesForMedication": "No intakes yet for",
|
||
"deleteIntakeAriaLabel": "Delete intake",
|
||
"actions": "Actions",
|
||
"pageInfo": "Page {page} of {total}",
|
||
"loginRequiredIntakes": "Please sign in to view intakes.",
|
||
"clear": "Clear",
|
||
"phaseConfig": "Reminder phases",
|
||
"phaseConfigDescription": "Configure timing for reminders.",
|
||
"phaseGreen": "Green",
|
||
"phaseYellow": "Yellow",
|
||
"phaseOrange": "Orange",
|
||
"phaseRed": "Red",
|
||
"phaseMinutes": "Min",
|
||
"phasePercent": "%",
|
||
"phaseBeforeEnd": "before end",
|
||
"phaseAfterEnd": "after end",
|
||
"phaseResetDefaults": "Reset to defaults",
|
||
"phaseSaved": "Reminder phases saved",
|
||
"phaseReset": "Reset to defaults",
|
||
"sourceWeb": "Website",
|
||
"sourceApi": "External API",
|
||
"sourceReminder": "Telegram / Reminder",
|
||
"sourceImport": "CSV Import",
|
||
"unitDrops": "Drops",
|
||
"unitTablets": "Tablet(s)",
|
||
"unitCapsules": "Capsule(s)",
|
||
"unitPieces": "Piece(s)",
|
||
"namePlaceholder": "e.g. Ibuprofen",
|
||
"dosePlaceholder": "e.g. 400",
|
||
"labelPlaceholder": "e.g. Morning",
|
||
"defaultDose": "Default",
|
||
"dayDeactivate": "Deactivate {day}",
|
||
"dayActivate": "Activate {day}"
|
||
},
|
||
"charts": {
|
||
"title": "History",
|
||
"subtitle": "Visualization of your health data",
|
||
"timeRange": "Time range",
|
||
"days7": "7 days",
|
||
"days30": "30 days",
|
||
"days90": "90 days",
|
||
"days365": "1 year",
|
||
"daysAll": "All",
|
||
"weight": "Weight",
|
||
"bloodPressure": "Blood Pressure",
|
||
"pulse": "Pulse",
|
||
"bodyFat": "Body Fat",
|
||
"sleep": "Sleep",
|
||
"steps": "Steps",
|
||
"compliance": "Compliance",
|
||
"noData": "No data in selected time range",
|
||
"movingAverage": "Moving average",
|
||
"target": "Target range",
|
||
"points7Label": "7 pts",
|
||
"points7Title": "Last 7 data points",
|
||
"points30Label": "30 pts",
|
||
"points30Title": "Last 30 data points",
|
||
"points90Label": "90 pts",
|
||
"points90Title": "Last 90 data points",
|
||
"pointsAllLabel": "All",
|
||
"pointsAllTitle": "All available data points",
|
||
"systolic": "Systolic",
|
||
"diastolic": "Diastolic",
|
||
"movingAverage7d": "7d avg",
|
||
"avg7dShort": "7d",
|
||
"avg30dShort": "30d",
|
||
"trend": "Trend",
|
||
"targetRanges": "Target ranges",
|
||
"perWeek": "/week",
|
||
"selectMedication": "Select medication",
|
||
"noComplianceData": "No compliance data available",
|
||
"history": "History",
|
||
"complianceDays90": "Compliance (90 days)",
|
||
"colorGreen": "Green:",
|
||
"colorOrange": "Orange:",
|
||
"colorRed": "Red:",
|
||
"mood": "Mood",
|
||
"moodScore": "Mood Score",
|
||
"moodMA": "7-Day Average",
|
||
"moodLabel1": "Awful",
|
||
"moodLabel2": "Bad",
|
||
"moodLabel3": "Okay",
|
||
"moodLabel4": "Good",
|
||
"moodLabel5": "Great",
|
||
"weekdays": {
|
||
"mon": "Mon",
|
||
"wed": "Wed",
|
||
"fri": "Fri",
|
||
"sun": "Sun"
|
||
},
|
||
"months": {
|
||
"jan": "Jan",
|
||
"feb": "Feb",
|
||
"mar": "Mar",
|
||
"apr": "Apr",
|
||
"may": "May",
|
||
"jun": "Jun",
|
||
"jul": "Jul",
|
||
"aug": "Aug",
|
||
"sep": "Sep",
|
||
"oct": "Oct",
|
||
"nov": "Nov",
|
||
"dec": "Dec"
|
||
},
|
||
"heatmapOnTime": "on time",
|
||
"heatmapLate": "late",
|
||
"heatmapVeryLate": "very late",
|
||
"legendOnTime": "On time",
|
||
"legendLate": "Late",
|
||
"legendVeryLate": "Very late",
|
||
"legendMissed": "Missed"
|
||
},
|
||
"insights": {
|
||
"aiAnalysisTitle": "AI Health Analysis",
|
||
"noAnalysisYet": "No analysis yet.",
|
||
"noProviderConfigured": "AI provider not configured.",
|
||
"startAnalysis": "Start analysis",
|
||
"refreshAnalysis": "Refresh analysis",
|
||
"keyTakeaway": "Key Takeaway",
|
||
"findingsTitle": "Findings",
|
||
"correlationsTitle": "Correlations",
|
||
"recommendationsTitle": "Recommendations",
|
||
"dataFoundation": "Data coverage",
|
||
"dataGaps": "Data gaps:",
|
||
"lastUpdated": "Last updated",
|
||
"confidenceHoch": "High",
|
||
"confidenceMittel": "Medium",
|
||
"confidenceGering": "Low",
|
||
"loadError": "Failed to load insights",
|
||
"generationFailed": "The analysis couldn't be generated. Please try again.",
|
||
"title": "Insights",
|
||
"subtitle": "AI-powered analysis of your health data",
|
||
"overviewSubtitle": "Trends, risks, progress at a glance",
|
||
"navGeneral": "General",
|
||
"navBloodPressure": "Blood Pressure",
|
||
"navWeight": "Weight",
|
||
"navPulse": "Pulse",
|
||
"navMood": "Mood",
|
||
"navMedication": "Medication",
|
||
"navBmi": "BMI",
|
||
"measurementsOverDays": "{count} measurements over {days} days",
|
||
"healthStatus": "Health Status",
|
||
"noWeightData": "No weight data or height not set.",
|
||
"notEnoughBpData": "Not enough blood pressure data.",
|
||
"bpTargetRange": "BP Target Range",
|
||
"inTargetRange": "in target range",
|
||
"bpTarget": "Target: {range}",
|
||
"noDobSet": "Date of birth not set.",
|
||
"avg30dEsc": "30d average · ESC/ESH 2018",
|
||
"whoClassification": "WHO Classification",
|
||
"correlations": "Correlations",
|
||
"weightVsBp": "Weight vs. Blood Pressure (Systolic)",
|
||
"strengthStrong": "Strong correlation",
|
||
"strengthModerate": "Moderate correlation",
|
||
"strengthWeak": "Weak correlation",
|
||
"strengthNone": "No correlation",
|
||
"weightBpStrong": "There is a strong correlation between weight and blood pressure.",
|
||
"weightBpModerate": "There is a moderate correlation between weight and blood pressure.",
|
||
"weightBpWeak": "There is a weak correlation between weight and blood pressure.",
|
||
"weightBpNone": "No discernible correlation between weight and blood pressure.",
|
||
"notEnoughCorrelationData": "Not enough data for correlation analysis.",
|
||
"minCorrelationData": "At least 5 time-matched weight and blood pressure measurements needed.",
|
||
"bpMedContinuity": "BP Medication Continuity vs. Blood Pressure (Systolic)",
|
||
"continuityLabel": "Continuity",
|
||
"continuityBpLabel": "BP Medication Continuity (%)",
|
||
"weightKgLabel": "Weight (kg)",
|
||
"bpMedNegative": "Negative correlation: higher continuity tends to be associated with lower systolic blood pressure.",
|
||
"bpMedPositive": "Positive correlation: higher continuity tends to be associated with higher systolic blood pressure in your data.",
|
||
"bpMedNone": "No discernible correlation in your data.",
|
||
"bpMedCount": "{count} BP medications",
|
||
"notEnoughMedData": "Not enough data for this analysis.",
|
||
"notEnoughMedDataHint": "Tag medications as blood pressure medications and record sufficient intakes + blood pressure measurements.",
|
||
"medicationCompliance": "Medication Compliance",
|
||
"dayStreak": "{count} day streak",
|
||
"compliance7d": "7-day compliance",
|
||
"compliance30d": "30-day compliance",
|
||
"taken": "taken",
|
||
"skipped": "skipped",
|
||
"missed": "missed",
|
||
"complianceTrend": "Compliance Trend",
|
||
"generalStatusTitle": "General Health Status",
|
||
"generalStatusLoading": "Generating general status...",
|
||
"generalStatusNoKey": "The general status cannot be generated because no OpenAI key is configured.",
|
||
"generalStatusUnavailable": "The general status is currently unavailable.",
|
||
"generalStatusBadge": {
|
||
"good": "Good",
|
||
"watch": "Watch",
|
||
"critical": "Critical"
|
||
},
|
||
"assessmentTitle": "Assessment",
|
||
"bloodPressureSectionTitle": "Blood Pressure",
|
||
"bloodPressureMedicationListTitle": "Blood Pressure Medication & Compliance",
|
||
"bloodPressureMedicationListEmpty": "No active blood pressure medication configured.",
|
||
"bloodPressureStatusLoading": "Generating blood pressure summary...",
|
||
"bloodPressureStatusNoKey": "The blood pressure summary cannot be generated because no OpenAI key is configured.",
|
||
"bloodPressureStatusUnavailable": "The blood pressure summary is currently unavailable.",
|
||
"weightSectionTitle": "Weight",
|
||
"weightStatusLoading": "Generating weight summary...",
|
||
"weightStatusNoKey": "The weight summary cannot be generated because no OpenAI key is configured.",
|
||
"weightStatusUnavailable": "The weight summary is currently unavailable.",
|
||
"pulseSectionTitle": "Pulse",
|
||
"pulseStatusLoading": "Generating pulse summary...",
|
||
"pulseStatusNoKey": "The pulse summary cannot be generated because no OpenAI key is configured.",
|
||
"pulseStatusUnavailable": "The pulse summary is currently unavailable.",
|
||
"bmiSectionTitle": "BMI",
|
||
"bmiStatusLoading": "Generating BMI summary...",
|
||
"bmiStatusNoKey": "The BMI summary cannot be generated because no OpenAI key is configured.",
|
||
"bmiStatusUnavailable": "The BMI summary is currently unavailable.",
|
||
"medicationComplianceStatusLoading": "Generating medication compliance text...",
|
||
"medicationComplianceStatusNoKey": "The medication compliance text cannot be generated because no OpenAI key is configured.",
|
||
"medicationComplianceStatusUnavailable": "The medication compliance text is currently unavailable.",
|
||
"complianceNoData": "No compliance data available.",
|
||
"aiInsights": "AI Insights",
|
||
"configureApiKey": "Configure an OpenAI API key in Settings to get AI-powered insights.",
|
||
"alertsTitle": "Notes & Recommendations",
|
||
"showLess": "Show less",
|
||
"showMore": "+{count} more",
|
||
"generate": "Generate new analysis",
|
||
"generating": "Generating analysis...",
|
||
"lastGenerated": "Last analysis",
|
||
"noInsights": "No analysis available yet.",
|
||
"rateLimit": "Please wait before generating a new analysis.",
|
||
"noApiKey": "Please add an OpenAI API key in Settings first.",
|
||
"sendRawData": "Send raw data",
|
||
"minMoodCorrelationData": "At least 5 temporally matched mood and measurement data points required.",
|
||
"moodBpModerate": "There is a moderate correlation between mood and blood pressure.",
|
||
"moodBpNone": "No detectable correlation between mood and blood pressure.",
|
||
"moodBpStrong": "There is a strong correlation between mood and blood pressure.",
|
||
"moodBpWeak": "There is a weak correlation between mood and blood pressure.",
|
||
"moodScoreLabel": "Mood (avg)",
|
||
"moodSectionTitle": "Mood",
|
||
"moodStatusLoading": "Generating mood analysis...",
|
||
"moodStatusNoKey": "Mood summary cannot be generated because no OpenAI API key is configured.",
|
||
"moodStatusUnavailable": "Mood analysis unavailable",
|
||
"moodVsBp": "Mood vs. Blood Pressure (Systolic)",
|
||
"moodVsPulse": "Mood vs. Pulse",
|
||
"moodVsWeight": "Mood vs. Weight",
|
||
"moodWeightModerate": "There is a moderate correlation between mood and weight.",
|
||
"moodWeightNone": "No detectable correlation between mood and weight.",
|
||
"moodWeightStrong": "There is a strong correlation between mood and weight.",
|
||
"moodWeightWeak": "There is a weak correlation between mood and weight.",
|
||
"notEnoughMoodCorrelationData": "Not enough mood data for correlation analysis.",
|
||
"refreshButton": "Refresh",
|
||
"generateButton": "Generate",
|
||
"analyzing": "Analyzing health data...",
|
||
"confidence": "Confidence",
|
||
"confidence_niedrig": "low",
|
||
"confidence_mittel": "medium",
|
||
"confidence_hoch": "high",
|
||
"cached": "Cached",
|
||
"sectionChanged": "What has changed",
|
||
"sectionStable": "What is stable",
|
||
"sectionDrivers": "Possible correlations",
|
||
"sectionNextSteps": "Next steps",
|
||
"generatePrompt": "Click \"Generate\" for AI-powered insights about your health data."
|
||
},
|
||
"thresholds": {
|
||
"title": "Personal thresholds",
|
||
"subtitle": "Override the computed target range for any metric. Defaults stay in effect until you set a value here.",
|
||
"defaultLabel": "Default",
|
||
"overrideLabel": "Your override",
|
||
"autoModeLabel": "Auto",
|
||
"overrideModeLabel": "Override",
|
||
"resetAction": "Reset to default",
|
||
"resetAllAction": "Reset all to default",
|
||
"minLabel": "Min",
|
||
"maxLabel": "Max",
|
||
"unitSuffix": "({unit})",
|
||
"outOfBoundsHint": "Allowed range: {min}–{max} {unit}",
|
||
"overrideWarning": "Overriding with values outside guideline ranges may produce unhealthy targets. Only do this on doctor's advice.",
|
||
"saveSuccess": "Thresholds updated",
|
||
"saveError": "Could not save thresholds",
|
||
"resetSuccess": "Reset to default",
|
||
"sourceOverride": "User override",
|
||
"metricWeight": "Weight",
|
||
"metricBpSys": "Blood pressure (systolic)",
|
||
"metricBpDia": "Blood pressure (diastolic)",
|
||
"metricPulse": "Resting pulse",
|
||
"metricBodyFat": "Body fat",
|
||
"metricSleep": "Sleep duration",
|
||
"metricSteps": "Daily steps",
|
||
"metricGlucoseFasting": "Glucose — fasting",
|
||
"metricGlucosePostprandial": "Glucose — post-meal",
|
||
"metricGlucoseRandom": "Glucose — random",
|
||
"metricGlucoseBedtime": "Glucose — bedtime",
|
||
"unsetExplanation": "No override — using computed default.",
|
||
"loadError": "Could not load thresholds"
|
||
},
|
||
"targets": {
|
||
"title": "Targets",
|
||
"subtitle": "Personal reference ranges based on your profile",
|
||
"introText": "These targets are personal reference ranges derived from your profile and established medical guidelines. They help you quickly classify your key health values and detect changes early. The ranges are calculated individually for you, for example based on age, sex, and height. For each value, you can immediately see whether you are within the target range or above or below it. This gives you a compact overview of which targets you are currently meeting well and where improvement is still possible.",
|
||
"current": "Current",
|
||
"average30d": "30-day average:",
|
||
"targetRange": "Target range",
|
||
"classification": "Rating",
|
||
"source": "Source",
|
||
"sourceLabel": "Source: {source}",
|
||
"trend": "Trend",
|
||
"weight": "Weight",
|
||
"bloodPressure": "Blood Pressure",
|
||
"pulse": "Resting Pulse",
|
||
"sleep": "Sleep Duration",
|
||
"bmi": "BMI",
|
||
"bodyFat": "Body Fat",
|
||
"steps": "Steps/Day",
|
||
"compliance": "Compliance",
|
||
"compliancePerMedication": "Per medication",
|
||
"diastolic": "Diastolic",
|
||
"noProfile": "Please complete your profile to get personalized targets.",
|
||
"profileIncomplete": "Profile incomplete",
|
||
"profileIncompleteHeightAge": "Add your height and date of birth in your profile to get personalized target ranges.",
|
||
"profileIncompleteHeight": "Add your height in your profile to calculate weight target range.",
|
||
"profileIncompleteAge": "Add your date of birth in your profile to get age-based target ranges.",
|
||
"noMeasurementData": "No measurement data available yet.",
|
||
"noMeasurementYet": "No measurement available yet",
|
||
"currentValue": "Current: {value} {unit}",
|
||
"targetRangeValue": "Target range: {min}–{max} {unit}",
|
||
"inTarget": "In target range",
|
||
"belowTarget": "{delta} {unit} below target",
|
||
"aboveTarget": "{delta} {unit} above target",
|
||
"loadError": "Error loading data",
|
||
"classificationOptimal": "Optimal",
|
||
"classificationNormal": "Normal",
|
||
"classificationElevated": "Elevated",
|
||
"classificationHigh": "High",
|
||
"classificationLow": "Low",
|
||
"glucoseFasting": "Glucose — fasting",
|
||
"glucosePostprandial": "Glucose — post-meal",
|
||
"glucoseRandom": "Glucose — random",
|
||
"glucoseBedtime": "Glucose — bedtime"
|
||
},
|
||
"onboarding": {
|
||
"welcome": "Welcome to HealthLog!",
|
||
"setupDescription": "Set up your profile to get started. You can change this later in Settings.",
|
||
"completeSetup": "Complete setup",
|
||
"skip": "Skip",
|
||
"medicationsTitle": "Do you take medications?",
|
||
"medicationsDescription": "Add your medications to enable reminders and compliance tracking.",
|
||
"medNamePlaceholder": "Name",
|
||
"medDosePlaceholder": "Dose",
|
||
"notificationsTitle": "Reminders",
|
||
"notificationsDescription": "HealthLog can remind you about your medications. Configure this later in Settings.",
|
||
"telegramHint": "Reminders via Telegram bot with snooze buttons",
|
||
"webPushHint": "Browser notifications directly to your device",
|
||
"notificationsLater": "You can set this up anytime in Settings.",
|
||
"targetsTitle": "Target Values",
|
||
"targetsDescription": "Set optional target values for blood pressure and weight.",
|
||
"targetSys": "Systolic (mmHg)",
|
||
"targetDia": "Diastolic (mmHg)",
|
||
"targetWeight": "Target weight (kg)"
|
||
},
|
||
"settings": {
|
||
"title": "Settings",
|
||
"subtitle": "Profile, Security & Connections",
|
||
"sectionsTitle": "Settings",
|
||
"categoryGeneral": "General",
|
||
"categoryNotifications": "Notifications",
|
||
"categoryAi": "AI",
|
||
"categoryPersonalization": "Personalization",
|
||
"categoryIntegration": "Integration",
|
||
"categoryApi": "API",
|
||
"profile": "Profile",
|
||
"security": "Security",
|
||
"language": "Language",
|
||
"languageDescription": "Choose the display language of the application.",
|
||
"languageSystem": "System (automatic)",
|
||
"username": "Username",
|
||
"height": "Height (cm)",
|
||
"dateOfBirth": "Date of birth",
|
||
"dateOfBirthHint": "Used for automatic blood pressure target calculations.",
|
||
"gender": "Gender",
|
||
"genderNone": "Not specified",
|
||
"genderMale": "Male",
|
||
"genderFemale": "Female",
|
||
"genderHint": "Used for gender-specific target values.",
|
||
"profileSaved": "Profile saved",
|
||
"changePassword": "Change password",
|
||
"changePasswordDescription": "Replace your current password with a new one.",
|
||
"passwordReset": "Password reset",
|
||
"currentPassword": "Current password",
|
||
"newPassword": "New password",
|
||
"confirmNewPassword": "Confirm new password",
|
||
"passwordMismatch": "New passwords do not match",
|
||
"passwordUpdated": "Password updated successfully",
|
||
"passkeys": "Passkeys",
|
||
"registeredPasskeys": "Registered Passkeys",
|
||
"passkeysDescription": "Passkeys allow secure login without a password.",
|
||
"passkeyName": "Name",
|
||
"passkeyDevice": "Device",
|
||
"passkeyBackup": "Backup",
|
||
"passkeyCreated": "Created",
|
||
"passkeyActions": "Actions",
|
||
"noPasskeys": "No passkeys registered.",
|
||
"addPasskey": "Add passkey",
|
||
"passkeyAdded": "Passkey added successfully!",
|
||
"passkeyOptionsError": "Could not load passkey options",
|
||
"passkeyRegistrationFailed": "Passkey registration failed",
|
||
"passkeyRegistrationCancelled": "Passkey registration cancelled",
|
||
"passkeyNotSupported": "Your browser or device doesn't support passkeys yet. Try a different browser or device.",
|
||
"passkeyAlreadyRegistered": "This device is already registered as a passkey for your account.",
|
||
"passkeySecurityBlocked": "Your browser blocked the passkey request for security reasons. Make sure you're on a trusted HTTPS origin and try again.",
|
||
"passkeyTimeout": "The passkey prompt timed out. Please try again.",
|
||
"passkeyUnknownError": "Passkey registration failed: {message}",
|
||
"deletePasskey": "Delete passkey?",
|
||
"deletePasskeyDescription": "The passkey will be permanently deleted.",
|
||
"singleDevice": "Single device",
|
||
"multiDevice": "Multi-device",
|
||
"backedUp": "backed up",
|
||
"telegram": "Telegram Notifications",
|
||
"telegramDescription": "Get reminders for missed medication intake via Telegram.",
|
||
"telegramSaved": "Telegram settings saved",
|
||
"botToken": "Bot Token",
|
||
"chatId": "Chat ID",
|
||
"enableNotifications": "Enable notifications",
|
||
"testMessage": "Test message",
|
||
"testSent": "Test message sent!",
|
||
"telegramStep1": "1. Create a bot via @BotFather in Telegram and copy the token.",
|
||
"telegramStep2": "2. Send /start to your bot to activate the chat.",
|
||
"telegramStep3": "3. Find your Chat ID via @userinfobot or the Bot API.",
|
||
"ntfy": "ntfy",
|
||
"ntfyDescription": "Notifications via ntfy (self-hosted or ntfy.sh).",
|
||
"ntfyEnable": "Enable ntfy",
|
||
"ntfyServer": "Server URL",
|
||
"ntfyTopic": "Topic",
|
||
"ntfyAuthToken": "Auth Token (optional)",
|
||
"ntfyAuthTokenHint": "Only needed for private topics with access control.",
|
||
"webPush": "Browser Push",
|
||
"webPushDescription": "Receive notifications directly in your browser, even when HealthLog is not open.",
|
||
"webPushNotSupported": "Your browser does not support push notifications.",
|
||
"webPushDenied": "Push notifications are blocked. Allow them in your browser settings.",
|
||
"webPushNotConfigured": "Web Push is not configured on the server.",
|
||
"webPushSubscribe": "Enable push",
|
||
"webPushUnsubscribe": "Disable push",
|
||
"webPushSubscribed": "Push notifications enabled!",
|
||
"webPushUnsubscribed": "Push notifications disabled.",
|
||
"webPushSubscribeFailed": "Activation failed",
|
||
"webPushActive": "Active",
|
||
"kiInsights": "AI Insights (OpenAI)",
|
||
"kiInsightsDescription": "Optional: Save your OpenAI key for daily, automatic AI evaluations in Insights.",
|
||
"kiInsightsBenefitsTitle": "Benefits with OpenAI key",
|
||
"kiInsightsBenefit1": "Daily summaries for general status, blood pressure, weight, pulse, BMI, and medication compliance.",
|
||
"kiInsightsBenefit2": "Better trend and correlation interpretation based on your latest 30 daily measurement points per area.",
|
||
"kiInsightsBenefit3": "Automatic background refresh so the Insights page always shows up-to-date assessments.",
|
||
"createApiKeyHint": "Create your API key at:",
|
||
"createApiKeyLinkLabel": "platform.openai.com/api-keys",
|
||
"apiKey": "API Key",
|
||
"apiKeyStored": "Key stored — enter new one to replace",
|
||
"apiKeySaved": "API key saved",
|
||
"apiKeyRemoved": "API key removed",
|
||
"codexConnected": "ChatGPT connected successfully! AI insights are now active.",
|
||
"codexDisconnected": "ChatGPT connection removed.",
|
||
"codexConnectionFailed": "ChatGPT connection failed. Please try again.",
|
||
"rawData": "Send raw data",
|
||
"rawDataOnDescription": "Aggregated metrics plus anonymized raw points from the last 30 days are sent. This includes per-metric measurements (for example weight, blood pressure, pulse) with time context so AI can detect patterns, outliers, and correlations more reliably. Name, email, and direct account identifiers are not transmitted.",
|
||
"rawDataOffDescription": "Only summarized metrics are sent (for example averages, trends, minimum/maximum). No individual points and no exact time context. This is more privacy-preserving, but AI can be less precise for short-term patterns and fluctuations.",
|
||
"rawDataWarning": "Raw mode enabled: AI receives additional anonymized points from the last 30 days for higher analysis accuracy.",
|
||
"regenerateInsights": "Regenerate reports",
|
||
"regenerateSuccess": "Reports regenerated successfully",
|
||
"regenerateRateLimit": "Please wait — max 2 analyses per hour.",
|
||
"lastGeneratedAt": "Last generated",
|
||
"withings": "Withings",
|
||
"withingsDescription": "Connect your Withings scale and blood pressure monitors.",
|
||
"withingsCredentials": "API Credentials",
|
||
"withingsClientId": "Client ID",
|
||
"withingsClientSecret": "Client Secret",
|
||
"withingsCredentialsSaved": "Credentials saved",
|
||
"withingsCredentialsSavedPlaceholder": "Saved — enter new to replace",
|
||
"withingsCredentialsSavedPlaceholderSecret": "Saved — enter new to replace",
|
||
"withingsSaveCredentials": "Save credentials",
|
||
"withingsConnected": "Connected",
|
||
"configured": "Configured",
|
||
"withingsTokenExpired": "Token expired",
|
||
"withingsLastSync": "Last sync",
|
||
"withingsSync": "Sync now",
|
||
"withingsFullSync": "Sync all data",
|
||
"withingsFullSyncTitle": "Full synchronization?",
|
||
"withingsFullSyncDescription": "All available Withings data will be fully synchronized. This may take some time depending on your history.",
|
||
"withingsSyncResult": "{count} measurements synchronized",
|
||
"withingsFullSyncResult": "{count} measurements fully synchronized",
|
||
"withingsSyncFailed": "Sync failed",
|
||
"withingsSynchronize": "Synchronize",
|
||
"withingsDisconnect": "Disconnect",
|
||
"withingsDisconnectTitle": "Disconnect Withings?",
|
||
"withingsDisconnectDescription": "The connection to Withings will be disconnected. Previously synced data will be preserved.",
|
||
"withingsConnect": "Connect with Withings",
|
||
"withingsNoCredentials": "Please enter your API credentials above to connect Withings.",
|
||
"apiTokens": "API Tokens",
|
||
"apiTokensDescription": "Create API tokens for external medication intake (e.g., Shortcuts, automations).",
|
||
"tokenName": "Token name",
|
||
"tokenNamePlaceholder": "Token name (e.g., iPhone Shortcut)",
|
||
"tokenCreated": "Token created — copy it now! It won't be shown again.",
|
||
"tokenRevoke": "Revoke token?",
|
||
"tokenRevokeDescription": "The token will be permanently deactivated. Applications using it will lose access.",
|
||
"tokenRevoked": "Revoked",
|
||
"tokenExpired": "Expired",
|
||
"tokenActive": "Active",
|
||
"activeTokensTitle": "Active tokens",
|
||
"revokedTokensTitle": "Revoked tokens ({count})",
|
||
"tokenTableName": "Name",
|
||
"tokenTablePermissions": "Permissions",
|
||
"tokenTableStatus": "Status",
|
||
"tokenTableCreated": "Created",
|
||
"tokenTableLastUsed": "Last used",
|
||
"tokenTableActions": "Actions",
|
||
"noActiveTokens": "No active tokens available.",
|
||
"tokenNeverUsed": "Never",
|
||
"tokenLastUsed": "Last used:",
|
||
"tokenCreatedAt": "Created:",
|
||
"apiEndpoint": "API endpoint for medication intake:",
|
||
"apiEndpointsTitle": "API endpoints",
|
||
"apiEndpointsDescription": "Overview of currently available external endpoint(s) for token-based ingestion.",
|
||
"apiEndpointMethod": "Method",
|
||
"apiEndpointPath": "Path",
|
||
"apiEndpointAuth": "Authentication",
|
||
"apiEndpointExample": "Body example",
|
||
"export": "Data Export",
|
||
"exportDescription": "Export all your health data.",
|
||
"exportJson": "JSON Export",
|
||
"exportCsv": "CSV Export",
|
||
"doctorReport": "Doctor Report (PDF)",
|
||
"auditLog": "Activity Log",
|
||
"auditLogHidden": "Log hidden.",
|
||
"auditLogShowing": "Showing last {count} of {total} entries",
|
||
"auditNoActivities": "No activities found.",
|
||
"auditAction": "Action",
|
||
"auditCategory": "Category",
|
||
"auditIp": "IP",
|
||
"auditLocation": "Location",
|
||
"auditTimestamp": "Time",
|
||
"auditOther": "Other",
|
||
"auditCategoryAuth": "Auth",
|
||
"auditCategoryData": "Data",
|
||
"auditCategoryIntegration": "Integration",
|
||
"auditCategoryAI": "AI",
|
||
"auditCategoryAdmin": "Admin",
|
||
"auditActionAuthRegister": "Registration",
|
||
"auditActionAuthLogin": "Login",
|
||
"auditActionAuthLoginPasskey": "Passkey login",
|
||
"auditActionAuthLoginPassword": "Password login",
|
||
"auditActionAuthLoginFailed": "Failed login",
|
||
"auditActionAuthLogout": "Logout",
|
||
"auditActionAuthPasskeyRegister": "Passkey added",
|
||
"auditActionAuthPasskeyDelete": "Passkey deleted",
|
||
"auditActionAuthPasswordChange": "Password changed",
|
||
"auditActionMeasurementCreate": "Measurement created",
|
||
"auditActionMeasurementUpdate": "Measurement updated",
|
||
"auditActionMeasurementDelete": "Measurement deleted",
|
||
"auditActionMedicationCreate": "Medication created",
|
||
"auditActionMedicationUpdate": "Medication updated",
|
||
"auditActionMedicationDelete": "Medication deleted",
|
||
"auditActionMedicationIntake": "Intake recorded",
|
||
"auditActionMedicationIngestExternal": "External intake",
|
||
"auditActionWithingsConnect": "Withings connected",
|
||
"auditActionWithingsDisconnect": "Withings disconnected",
|
||
"auditActionExportDownload": "Export downloaded",
|
||
"auditActionInsightsGenerate": "AI insight generated",
|
||
"auditActionAdminUserUpdate": "Admin: User updated",
|
||
"auditActionAdminUserResetPassword": "Admin: Password reset",
|
||
"auditActionAdminSettingsUpdate": "Admin: Settings changed",
|
||
"collapse": "Collapse",
|
||
"expand": "Expand",
|
||
"dangerZoneTitle": "Danger Zone",
|
||
"dangerZone": "Delete All Data",
|
||
"dangerZoneDescription": "Deletes all your health data and integrations. Your user account will be preserved.",
|
||
"dangerZoneConfirm": "Delete all data?",
|
||
"dangerZoneConfirmDescription": "This will permanently delete all your health data and integrations. Your user account will be preserved.",
|
||
"dangerZoneSuccess": "All personal data has been deleted",
|
||
"dangerZoneDeleteFailed": "Deletion failed",
|
||
"finalDelete": "Delete permanently",
|
||
"saved": "Saved",
|
||
"savingError": "Error saving",
|
||
"adminAreaTitle": "Admin area",
|
||
"adminAreaDescription": "System-wide settings and user management are available in the admin console.",
|
||
"openAdminConsole": "Open admin console",
|
||
"moodLogTitle": "moodLog",
|
||
"moodLogDescription": "Import mood data from moodLog",
|
||
"moodLogUrl": "moodLog URL",
|
||
"moodLogUrlPlaceholder": "https://mood.example.com",
|
||
"moodLogApiKey": "API Key",
|
||
"moodLogApiKeyPlaceholder": "ml_...",
|
||
"moodLogWebhookSecret": "Webhook Secret",
|
||
"moodLogWebhookSecretHelp": "Enter this secret in moodLog as webhook secret",
|
||
"moodLogSync": "Start sync",
|
||
"moodLogFullSync": "Full sync",
|
||
"moodLogFullSyncTitle": "Full sync?",
|
||
"moodLogFullSyncDescription": "All available mood data will be fully synchronized.",
|
||
"moodLogDisconnect": "Disconnect",
|
||
"moodLogDisconnectTitle": "Disconnect moodLog?",
|
||
"moodLogDisconnectDescription": "The connection will be removed and all imported mood data will be deleted.",
|
||
"moodLogLastSync": "Last sync",
|
||
"moodLogEntries": "Entries",
|
||
"moodLogSyncResult": "{count} entries synchronized",
|
||
"moodLogSyncFailed": "Sync failed",
|
||
"moodLogSaved": "moodLog connection saved",
|
||
"moodLogDisconnected": "moodLog disconnected"
|
||
},
|
||
"admin": {
|
||
"title": "Administration",
|
||
"subtitle": "System status, users, and settings",
|
||
"settingsSaveError": "Failed to save settings",
|
||
"settingsSaveValidationError": "Please check the highlighted fields",
|
||
"sectionsTitle": "Admin",
|
||
"systemStatus": "System Status",
|
||
"version": "Version",
|
||
"database": "Database",
|
||
"databaseConnected": "Connected",
|
||
"databaseError": "Error",
|
||
"gitCommit": "Git Commit",
|
||
"startedAt": "Started",
|
||
"users": "Users",
|
||
"measurementsCount": "Measurements",
|
||
"activeTokens": "Active API Tokens",
|
||
"activeSessions": "Active Sessions",
|
||
"loadingStatus": "Loading status...",
|
||
"workerStatus": "Job Worker",
|
||
"workerRunning": "Running",
|
||
"workerStopped": "Stopped",
|
||
"lastReminderCheck": "Last Reminder Check",
|
||
"appSettings": "Application Settings",
|
||
"registrationEnabled": "Allow registration",
|
||
"registrationEnabledDescription": "When disabled, new users cannot register.",
|
||
"defaultLanguage": "Default language",
|
||
"defaultLanguageDescription": "Used for new users and as fallback.",
|
||
"servicesGlobal": "Services (global)",
|
||
"servicesGlobalDescription": "Disabled services are not available to any user.",
|
||
"telegramGlobal": "Allow Telegram notifications system-wide.",
|
||
"ntfyGlobal": "Allow ntfy notifications system-wide.",
|
||
"webPushGlobal": "Allow browser push notifications system-wide.",
|
||
"apiGlobal": "Allow API endpoints and API tokens system-wide.",
|
||
"moodLogGlobal": "Allow moodLog integration system-wide.",
|
||
"sectionGeneral": "General",
|
||
"sectionIntegrations": "Integrations",
|
||
"sectionMedication": "Medications",
|
||
"sectionManagement": "Management",
|
||
"umamiTitle": "Umami",
|
||
"umamiDescription": "Web analytics with Umami",
|
||
"glitchtipTitle": "GlitchTip",
|
||
"glitchtipDescription": "Error tracking with GlitchTip",
|
||
"webPushVapidTitle": "Web Push VAPID",
|
||
"monitoring": "Monitoring (Umami/Glitchtip)",
|
||
"monitoringDescription": "Configuration for website analytics and error tracking.",
|
||
"umamiEnabled": "Enable Umami tracking.",
|
||
"umamiScriptUrl": "Umami script URL",
|
||
"umamiScriptUrlPlaceholder": "https://analytics.example.com/script.js",
|
||
"umamiWebsiteId": "Umami website ID",
|
||
"umamiWebsiteIdPlaceholder": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||
"glitchtipEnabled": "Enable Glitchtip error tracking.",
|
||
"glitchtipDsn": "Glitchtip DSN",
|
||
"glitchtipDsnPlaceholder": "https://<public-key>@glitchtip.example.com/<project-id>",
|
||
"glitchtipEnvironment": "Glitchtip environment",
|
||
"glitchtipEnvironmentPlaceholder": "production",
|
||
"webPushVapid": "Web Push (VAPID)",
|
||
"webPushVapidDescription": "Configure VAPID keys and subject here so browser push works without environment variables.",
|
||
"webPushVapidPublicKey": "VAPID public key",
|
||
"webPushVapidPublicKeyPlaceholder": "BElx... (Base64URL)",
|
||
"webPushVapidPrivateKey": "VAPID private key",
|
||
"webPushVapidPrivateKeyPlaceholder": "Optional: enter a new private key",
|
||
"webPushVapidSubject": "VAPID subject",
|
||
"webPushVapidSubjectPlaceholder": "mailto:you@example.com",
|
||
"configured": "Configured",
|
||
"notConfigured": "Not configured",
|
||
"monitoringTestUmami": "Send Umami test",
|
||
"monitoringTestGlitchtip": "Send Glitchtip test",
|
||
"monitoringTestSuccess": "Test event sent",
|
||
"monitoringTestFailed": "Monitoring test failed",
|
||
"monitoringSave": "Save monitoring",
|
||
"bugReportGithub": "Bug report (GitHub Issues)",
|
||
"bugReportGithubDescription": "Configure GitHub issue token and repository for incoming bug reports.",
|
||
"bugReportRepo": "Repository",
|
||
"bugReportRepoPlaceholder": "owner/repo",
|
||
"bugReportToken": "Issue token",
|
||
"bugReportTokenPlaceholder": "ghp_...",
|
||
"bugReportConfigured": "Bug report is configured.",
|
||
"bugReportNotConfigured": "Bug report is not fully configured yet.",
|
||
"bugReportSave": "Save bug report settings",
|
||
"userManagement": "User Management",
|
||
"userEmail": "Email",
|
||
"userRole": "Role",
|
||
"userPasskeys": "Passkeys",
|
||
"userCreated": "Created",
|
||
"userActions": "Actions",
|
||
"editUser": "Edit user",
|
||
"editUserTitle": "Edit user: {name}",
|
||
"demoteToUser": "Demote to user",
|
||
"promoteToAdmin": "Promote to admin",
|
||
"toUser": "→ User",
|
||
"toAdmin": "→ Admin",
|
||
"ownRoleUnchangeable": "Cannot change own role",
|
||
"lastAdminError": "Cannot demote the last admin",
|
||
"resetPassword": "Reset password",
|
||
"resetPasswordTitle": "Reset password: {name}",
|
||
"newPassword": "New password",
|
||
"newPasswordPlaceholder": "At least 12 characters",
|
||
"passwordReset": "Password has been reset",
|
||
"reset": "Reset",
|
||
"loadingUsers": "Loading users...",
|
||
"apiTokens": "API Tokens",
|
||
"tokenUser": "User",
|
||
"tokenName": "Name",
|
||
"tokenPermissions": "Permissions",
|
||
"tokenStatus": "Status",
|
||
"tokenLastUsed": "Last used",
|
||
"tokenCreated": "Created",
|
||
"tokenNeverUsed": "Never",
|
||
"noTokens": "No API tokens found.",
|
||
"loginOverview": "Login Overview",
|
||
"allAuthEvents": "All auth events",
|
||
"failedOnly": "Failed only",
|
||
"noEntries": "No entries found.",
|
||
"showingEntries": "Showing {count} of {total} auth entries",
|
||
"status": "Status",
|
||
"action": "Action",
|
||
"ip": "IP",
|
||
"location": "Location",
|
||
"timestamp": "Time",
|
||
"dangerZone": "Danger Zone",
|
||
"deleteAllData": "Delete all data globally",
|
||
"deleteAllDescription": "Deletes all measurement, medication, integration, and log data for all users. User accounts will be preserved.",
|
||
"deleteAllConfirm": "Really delete all data?",
|
||
"deleteAllConfirmDescription": "This will permanently delete all measurements, medications, intake records, and integration data for all users. User accounts will be preserved.",
|
||
"deleteButton": "Delete all data",
|
||
"finalDelete": "Delete permanently",
|
||
"deletedResult": "Deleted: {measurements} measurements, {medications} medications, {intakeEvents} intakes",
|
||
"authRegister": "Registration",
|
||
"authLogin": "Login",
|
||
"authLoginPasskey": "Passkey login",
|
||
"authLoginPassword": "Password login",
|
||
"authLoginFailed": "Failed",
|
||
"authLogout": "Logout",
|
||
"authPasskeyRegister": "Passkey added",
|
||
"authPasskeyDelete": "Passkey deleted",
|
||
"medicationReminders": "Medication Reminders",
|
||
"medicationRemindersDescription": "Thresholds for reminder badge color transitions on medication cards.",
|
||
"reminderLateMinutes": "Late threshold (minutes)",
|
||
"reminderLateMinutesDescription": "Minutes after window end until the badge changes from yellow to orange.",
|
||
"reminderMissedMinutes": "Missed threshold (minutes)",
|
||
"reminderMissedMinutesDescription": "Minutes after window end until the intake is considered missed.",
|
||
"reminderThresholdsSave": "Save thresholds",
|
||
"notificationTest": "Test Notifications",
|
||
"notificationTestSend": "Send test notification",
|
||
"notificationTestSuccess": "Test notification sent",
|
||
"notificationTestFailed": "Failed to send test notification",
|
||
"reminderCheckRun": "Run reminder check",
|
||
"reminderCheckSuccess": "Reminder check completed",
|
||
"reminderCheckFailed": "Failed to run reminder check",
|
||
"reminderCheckResults": "Results",
|
||
"reminderCheckNotifOn": "Notifications on",
|
||
"reminderCheckNotifOff": "Notifications off",
|
||
"reminderCheckEventsToday": "Events today",
|
||
"reminderCheckNotifSent": "Sent",
|
||
"feedback": {
|
||
"title": "Feedback Inbox",
|
||
"description": "User-submitted feedback, bug reports, and feature requests.",
|
||
"tabOpen": "Open",
|
||
"tabAcknowledged": "Acknowledged",
|
||
"tabResolved": "Resolved",
|
||
"tabArchived": "Archived",
|
||
"subject": "Subject",
|
||
"category": "Category",
|
||
"user": "User",
|
||
"createdAt": "Submitted",
|
||
"actions": "Actions",
|
||
"details": "Details",
|
||
"noEntries": "No feedback in this category.",
|
||
"loading": "Loading feedback...",
|
||
"anonymous": "anonymous",
|
||
"categoryBug": "Bug",
|
||
"categoryFeature": "Feature",
|
||
"categoryQuestion": "Question",
|
||
"categoryOther": "Other",
|
||
"statusOpen": "Open",
|
||
"statusAcknowledged": "Acknowledged",
|
||
"statusResolved": "Resolved",
|
||
"statusArchived": "Archived",
|
||
"dialogTitle": "Feedback details",
|
||
"metadataHeading": "Metadata",
|
||
"metaUrl": "URL",
|
||
"metaLocale": "Locale",
|
||
"metaUserAgent": "User agent",
|
||
"metaAppVersion": "App version",
|
||
"screenshotHeading": "Screenshot",
|
||
"adminNote": "Admin note",
|
||
"adminNotePlaceholder": "Internal note (not visible to user)",
|
||
"saveNote": "Save note",
|
||
"noteSaved": "Note saved",
|
||
"actionAcknowledge": "Acknowledge",
|
||
"actionResolve": "Resolve",
|
||
"actionArchive": "Archive",
|
||
"actionPublishGithub": "Publish to GitHub",
|
||
"publishedToGithub": "Published to GitHub",
|
||
"viewIssue": "View issue",
|
||
"publishSuccess": "Issue created",
|
||
"publishFailed": "GitHub publish failed",
|
||
"updateFailed": "Update failed",
|
||
"archiveConfirm": "Archive this feedback entry?",
|
||
"submittedBy": "Submitted by",
|
||
"sectionTitle": "Feedback"
|
||
}
|
||
},
|
||
"achievements": {
|
||
"title": "Achievements",
|
||
"subtitle": "Unlock achievements from feature rollout onward through consistent health trends and medication habits.",
|
||
"loginRequired": "Please sign in to view achievements.",
|
||
"points": "Points",
|
||
"unlocked": "Unlocked",
|
||
"nextGoal": "Next goal",
|
||
"allCompleted": "All achievements unlocked!",
|
||
"completed": "Completed",
|
||
"inProgress": "In progress",
|
||
"progress": "Progress",
|
||
"goalReached": "Goal reached",
|
||
"remaining": "{count} remaining",
|
||
"remainingUnlocks": "{count} still unlockable",
|
||
"noneUnlockedYet": "No achievements unlocked yet.",
|
||
"pointsValue": "{points} points",
|
||
"metricCount": "{count}",
|
||
"metricDays": "{count} days",
|
||
"metricPercent": "{count}%",
|
||
"badges": {
|
||
"intakeTotal1": {
|
||
"title": "First intake",
|
||
"description": "Record your first taken medication event."
|
||
},
|
||
"intakeTotal10": {
|
||
"title": "Intake starter",
|
||
"description": "Record 10 taken medication events."
|
||
},
|
||
"intakeTotal50": {
|
||
"title": "Intake routine",
|
||
"description": "Record 50 taken medication events."
|
||
},
|
||
"intakeTotal150": {
|
||
"title": "Intake expert",
|
||
"description": "Record 150 taken medication events."
|
||
},
|
||
"intakeTotal300": {
|
||
"title": "Intake legend",
|
||
"description": "Record 300 taken medication events."
|
||
},
|
||
"overIntake1": {
|
||
"title": "Idiot",
|
||
"description": "Take medication at least once more than planned."
|
||
},
|
||
"skippedIntake1": {
|
||
"title": "Lazy Boy",
|
||
"description": "Skip at least one planned intake."
|
||
},
|
||
"passkeyCreated1": {
|
||
"title": "Passkey set up",
|
||
"description": "Create your first passkey."
|
||
},
|
||
"passkeyLogin1": {
|
||
"title": "Passkey login",
|
||
"description": "Sign in at least once with a passkey."
|
||
},
|
||
"passwordLogin1": {
|
||
"title": "Old school",
|
||
"description": "Sign in at least once with username and password."
|
||
},
|
||
"bugReport1": {
|
||
"title": "Bug hunter",
|
||
"description": "Submit a bug report."
|
||
},
|
||
"loginStreak7": {
|
||
"title": "Login streak 7d",
|
||
"description": "Sign in on 7 consecutive days."
|
||
},
|
||
"loginStreak30": {
|
||
"title": "Login streak 30d",
|
||
"description": "Sign in on 30 consecutive days."
|
||
},
|
||
"onTimePerfect1": {
|
||
"title": "On-time 1d",
|
||
"description": "Take all medications on time for 1 consecutive days."
|
||
},
|
||
"compliance801": {
|
||
"title": "80% adherence · 1d",
|
||
"description": "Reach at least 80% 30-day adherence for 1 consecutive days."
|
||
},
|
||
"bmiGreen1": {
|
||
"title": "BMI green · 1d",
|
||
"description": "Keep your BMI in the normal range for 1 consecutive days."
|
||
},
|
||
"bpGreen1": {
|
||
"title": "BP green · 1d",
|
||
"description": "Keep your blood pressure in the normal range for 1 consecutive days."
|
||
},
|
||
"pulseGreen1": {
|
||
"title": "Pulse green · 1d",
|
||
"description": "Keep your resting pulse in the normal range for 1 consecutive days."
|
||
},
|
||
"onTimePerfect7": {
|
||
"title": "On-time 7d",
|
||
"description": "Take all medications on time for 7 consecutive days."
|
||
},
|
||
"compliance807": {
|
||
"title": "80% adherence · 7d",
|
||
"description": "Reach at least 80% 30-day adherence for 7 consecutive days."
|
||
},
|
||
"bmiGreen7": {
|
||
"title": "BMI green · 7d",
|
||
"description": "Keep your BMI in the normal range for 7 consecutive days."
|
||
},
|
||
"bpGreen7": {
|
||
"title": "BP green · 7d",
|
||
"description": "Keep your blood pressure in the normal range for 7 consecutive days."
|
||
},
|
||
"pulseGreen7": {
|
||
"title": "Pulse green · 7d",
|
||
"description": "Keep your resting pulse in the normal range for 7 consecutive days."
|
||
},
|
||
"onTimePerfect30": {
|
||
"title": "On-time 30d",
|
||
"description": "Take all medications on time for 30 consecutive days."
|
||
},
|
||
"compliance8030": {
|
||
"title": "80% adherence · 30d",
|
||
"description": "Reach at least 80% 30-day adherence for 30 consecutive days."
|
||
},
|
||
"bmiGreen30": {
|
||
"title": "BMI green · 30d",
|
||
"description": "Keep your BMI in the normal range for 30 consecutive days."
|
||
},
|
||
"bpGreen30": {
|
||
"title": "BP green · 30d",
|
||
"description": "Keep your blood pressure in the normal range for 30 consecutive days."
|
||
},
|
||
"pulseGreen30": {
|
||
"title": "Pulse green · 30d",
|
||
"description": "Keep your resting pulse in the normal range for 30 consecutive days."
|
||
},
|
||
"onTimePerfect180": {
|
||
"title": "On-time 180d",
|
||
"description": "Take all medications on time for 180 consecutive days."
|
||
},
|
||
"compliance80180": {
|
||
"title": "80% adherence · 180d",
|
||
"description": "Reach at least 80% 30-day adherence for 180 consecutive days."
|
||
},
|
||
"bmiGreen180": {
|
||
"title": "BMI green · 180d",
|
||
"description": "Keep your BMI in the normal range for 180 consecutive days."
|
||
},
|
||
"bpGreen180": {
|
||
"title": "BP green · 180d",
|
||
"description": "Keep your blood pressure in the normal range for 180 consecutive days."
|
||
},
|
||
"pulseGreen180": {
|
||
"title": "Pulse green · 180d",
|
||
"description": "Keep your resting pulse in the normal range for 180 consecutive days."
|
||
},
|
||
"onTimePerfect360": {
|
||
"title": "On-time 360d",
|
||
"description": "Take all medications on time for 360 consecutive days."
|
||
},
|
||
"compliance80360": {
|
||
"title": "80% adherence · 360d",
|
||
"description": "Reach at least 80% 30-day adherence for 360 consecutive days."
|
||
},
|
||
"bmiGreen360": {
|
||
"title": "BMI green · 360d",
|
||
"description": "Keep your BMI in the normal range for 360 consecutive days."
|
||
},
|
||
"bpGreen360": {
|
||
"title": "BP green · 360d",
|
||
"description": "Keep your blood pressure in the normal range for 360 consecutive days."
|
||
},
|
||
"pulseGreen360": {
|
||
"title": "Pulse green · 360d",
|
||
"description": "Keep your resting pulse in the normal range for 360 consecutive days."
|
||
}
|
||
},
|
||
"nextProgressLabel": "Progress to unlock",
|
||
"completedOn": "Completed on {date}"
|
||
},
|
||
"bugreport": {
|
||
"title": "Bug Report",
|
||
"subtitle": "Send a bug report, feature request, or question. The server administrator will see your submission.",
|
||
"loginRequired": "Please sign in to report a bug.",
|
||
"bugTitle": "Title",
|
||
"bugTitlePlaceholder": "Brief description of the problem",
|
||
"description": "Description",
|
||
"descriptionPlaceholder": "Describe the bug in detail. What did you do? What happened? What did you expect?",
|
||
"screenshot": "Screenshot (optional)",
|
||
"screenshotPreview": "Screenshot preview",
|
||
"attachScreenshot": "Attach screenshot",
|
||
"imageOnly": "Only image files allowed",
|
||
"tooLarge": "Screenshot is too large (max 5 MB)",
|
||
"submit": "Send",
|
||
"success": "Thank you! Your bug report has been submitted successfully.",
|
||
"viewOnGithub": "View on GitHub",
|
||
"errorCreating": "Error creating report",
|
||
"notConfiguredTitle": "Bug reports not available yet",
|
||
"notConfiguredUser": "Bug reporting is disabled on this server. Please contact your administrator.",
|
||
"notConfiguredAdmin": "Bug reporting requires a GitHub integration (issue token + repository). Configure it in the admin panel under Settings → Bug reports.",
|
||
"openAdminSettings": "Open admin settings",
|
||
"category": "Type",
|
||
"categoryBug": "Bug report",
|
||
"categoryFeature": "Feature request",
|
||
"categoryQuestion": "Question",
|
||
"categoryOther": "Other feedback",
|
||
"bugTitlePlaceholder": "Short summary (optional)",
|
||
"githubEscalationNote": "Your feedback is stored locally. If the admin enables GitHub integration, it can be promoted to a public issue on request.",
|
||
"internalOnlyNote": "Your feedback is stored locally and only visible to the administrator of this server."
|
||
},
|
||
"notifications": {
|
||
"title": "Notifications",
|
||
"subtitle": "Choose which notifications you want to receive through which channel.",
|
||
"noChannels": "Configure a notification channel in Settings first.",
|
||
"goToSettings": "Go to Settings",
|
||
"channelDisabled": "Disabled",
|
||
"eventMedicationReminder": "Medication Reminder",
|
||
"eventMedicationReminderDesc": "Reminder when an intake is due.",
|
||
"eventMeasurementAnomaly": "Anomalous Measurement",
|
||
"eventMeasurementAnomalyDesc": "Warning for unusual measurements (e.g., high blood pressure).",
|
||
"eventComplianceLow": "Low Compliance",
|
||
"eventComplianceLowDesc": "Notification when medication compliance drops below a threshold.",
|
||
"eventWithingsSyncFailed": "Withings Sync Failed",
|
||
"eventWithingsSyncFailedDesc": "Alert when the automatic Withings data sync fails.",
|
||
"eventSystemAlert": "System Alert",
|
||
"eventSystemAlertDesc": "Important system messages and administrative notices."
|
||
},
|
||
"format": {
|
||
"dateShort": "{month}/{day}/{year}",
|
||
"timeShort": "{hours}:{minutes}",
|
||
"dateTime": "{month}/{day}/{year}, {hours}:{minutes}"
|
||
},
|
||
"classifications": {
|
||
"bmi": {
|
||
"Underweight": "Underweight",
|
||
"Normal": "Normal",
|
||
"Overweight": "Overweight",
|
||
"ObesityGradeI": "Obesity Grade I",
|
||
"ObesityGradeII": "Obesity Grade II",
|
||
"ObesityGradeIII": "Obesity Grade III"
|
||
},
|
||
"bp": {
|
||
"Optimal": "Optimal",
|
||
"Normal": "Normal",
|
||
"HighNormal": "High-normal",
|
||
"HypertensionGrade1": "Hypertension Grade 1",
|
||
"HypertensionGrade2": "Hypertension Grade 2",
|
||
"HypertensionGrade3": "Hypertension Grade 3"
|
||
},
|
||
"pulse": {
|
||
"Bradycardia": "Bradycardia",
|
||
"Normal": "Normal",
|
||
"Elevated": "Elevated",
|
||
"Tachycardia": "Tachycardia"
|
||
},
|
||
"pulseTarget": {
|
||
"SignificantlyLow": "Significantly low",
|
||
"SlightlyLow": "Slightly low",
|
||
"OnTarget": "On target",
|
||
"SlightlyElevated": "Slightly elevated",
|
||
"SignificantlyElevated": "Significantly elevated"
|
||
},
|
||
"sleep": {
|
||
"FarTooShort": "Far too short",
|
||
"TooShort": "Too short",
|
||
"OnTarget": "On target",
|
||
"SlightlyLong": "Slightly long",
|
||
"FarTooLong": "Far too long"
|
||
},
|
||
"bodyFat": {
|
||
"Essential": "Essential",
|
||
"Athletic": "Athletic",
|
||
"Fitness": "Fitness",
|
||
"Average": "Average",
|
||
"Obese": "Obese"
|
||
},
|
||
"steps": {
|
||
"VeryLow": "Very low",
|
||
"LowActive": "Low active",
|
||
"ModeratelyActive": "Moderately active",
|
||
"Active": "Active",
|
||
"VeryActive": "Very active"
|
||
},
|
||
"general": {
|
||
"Good": "Good",
|
||
"Moderate": "Moderate",
|
||
"Low": "Low",
|
||
"VeryGood": "Very good",
|
||
"VeryStable": "Very stable",
|
||
"Stable": "Stable",
|
||
"Fluctuating": "Fluctuating"
|
||
},
|
||
"alerts": {
|
||
"bmiBelowNormalTitle": "BMI below normal range",
|
||
"bmiBelowNormalMessage": "Your BMI of {bmi} is below the normal range (18.5–24.9).",
|
||
"bmiOverweightTitle": "BMI in overweight range",
|
||
"bmiOverweightMessage": "Your BMI of {bmi} is in the overweight range. Regular exercise and a balanced diet can help.",
|
||
"bmiObesityTitle": "BMI in obesity range",
|
||
"bmiObesityMessage": "Your BMI of {bmi} is in the obesity range.",
|
||
"bmiNormalTitle": "BMI in normal range",
|
||
"bmiNormalMessage": "Your BMI of {bmi} is in the healthy range.",
|
||
"bpDangerTitle": "Blood pressure significantly elevated",
|
||
"bpDangerMessage": "Your average blood pressure ({sys}/{dia}) is classified as \"{category}\".",
|
||
"bpWarningTitle": "Blood pressure slightly elevated",
|
||
"bpWarningMessage": "Your average blood pressure ({sys}/{dia}) is in the \"{category}\" range.",
|
||
"bpTargetGoodTitle": "Good blood pressure target adherence",
|
||
"bpTargetGoodMessage": "{pct}% of your readings are within the target range.",
|
||
"bpTargetBadTitle": "Blood pressure often outside target range",
|
||
"bpTargetBadMessage": "Only {pct}% of your readings are within the target range.",
|
||
"weightIncreasingTitle": "Weight increasing",
|
||
"weightIncreasingMessage": "Your weight is increasing by approx. {change} kg per week over the last 30 days.",
|
||
"weightDecreasingTitle": "Weight decreasing",
|
||
"weightDecreasingMessage": "Your weight is decreasing by approx. {change} kg per week over the last 30 days.",
|
||
"pulseOutliersTitle": "Pulse outliers detected",
|
||
"pulseOutliersMessage": "{count} unusual pulse readings in the last 30 days.",
|
||
"complianceLowTitle": "Low compliance: {name}",
|
||
"complianceLowMessage": "Your 7-day compliance for {name} is only {pct}%. Regular intake is important.",
|
||
"complianceGoodTitle": "Excellent compliance: {name}",
|
||
"complianceGoodMessage": "{pct}% compliance over the last 7 days. Keep it up!"
|
||
}
|
||
},
|
||
"targets": {
|
||
"title": "Targets",
|
||
"introText": "Your current values compared to recommended target ranges. Guideline sources are linked on each card.",
|
||
"weight": "Weight",
|
||
"bloodPressure": "Blood pressure",
|
||
"bloodPressureOnTarget": "Blood pressure on target",
|
||
"restingPulse": "Resting pulse",
|
||
"sleepDuration": "Sleep duration",
|
||
"bmi": "BMI",
|
||
"bodyFat": "Body fat",
|
||
"stepsPerDay": "Steps/day",
|
||
"medicationCompliance": "Medication compliance",
|
||
"mood": "Mood",
|
||
"moodStability": "Mood stability",
|
||
"steps": "steps",
|
||
"average30d": "30-day average:",
|
||
"currentValue": "Current: {value} {unit}",
|
||
"targetRangeValue": "Target: {min}–{max} {unit}",
|
||
"sourceLabel": "Source: {source}",
|
||
"inTarget": "Within target range",
|
||
"aboveTarget": "{delta} {unit} above target",
|
||
"belowTarget": "{delta} {unit} below target",
|
||
"diastolic": "Diastolic",
|
||
"noMeasurementYet": "No measurements yet",
|
||
"noMeasurementData": "No measurement data available. Add some measurements to see your targets.",
|
||
"profileIncomplete": "Profile incomplete",
|
||
"profileIncompleteHeight": "Add your height in profile settings to enable BMI targets.",
|
||
"profileIncompleteAge": "Add your date of birth in profile settings for age-specific targets.",
|
||
"profileIncompleteHeightAge": "Add your height and date of birth in profile settings for personalized targets.",
|
||
"compliancePerMedication": "Compliance per medication",
|
||
"loadError": "Failed to load targets"
|
||
},
|
||
"passwordStrength": {
|
||
"veryWeak": "Very weak",
|
||
"weak": "Weak",
|
||
"acceptable": "Acceptable",
|
||
"strong": "Strong",
|
||
"veryStrong": "Very strong",
|
||
"tooShort": "Too short",
|
||
"characters": "characters",
|
||
"minLength": "At least {count} characters required."
|
||
},
|
||
"telegram": {
|
||
"errorInvalidAction": "Invalid action.",
|
||
"errorMedicationNotFound": "Medication not found.",
|
||
"errorMedicationInactive": "Medication not found or inactive.",
|
||
"errorUnknownAction": "Unknown action.",
|
||
"snoozeOneHour": "1 hour",
|
||
"snoozeThreeHours": "3 hours",
|
||
"snoozedFor": "{name} snoozed for {duration}.",
|
||
"skipped": "{name} skipped.",
|
||
"alreadyRecorded": "{name} was already recorded.",
|
||
"recordedAsTaken": "{name} recorded as taken.",
|
||
"confirmed": "{name} confirmed.",
|
||
"genericConfirmed": "Confirmed.",
|
||
"cancelled": "Cancelled.",
|
||
"noActiveMedications": "No active medications found.",
|
||
"whichMedication": "<b>Which medication did you take?</b>",
|
||
"cancelButton": "Cancel",
|
||
"askMedicationName": "Please specify a medication, e.g. <b>taken Ramipril</b>.",
|
||
"medicationNotFoundExact": "Medication not found. Please send the name exactly as in HealthLog.",
|
||
"helpHeader": "<b>Available commands:</b>",
|
||
"helpBody": "/help — show this help\n/add — record an intake\n/start — start the bot\n\n<b>Text commands:</b>\ntaken <name> — confirm an intake\n\n<b>Buttons in reminders:</b>\n• Taken — confirm intake\n• 🕐 1h / 🕐 3h — postpone reminder\n• ⏭ Skip — skip intake\n• ✓ Confirm — confirm a missed intake"
|
||
}
|
||
}
|