diff --git a/README.md b/README.md index 577053e..8dd7422 100755 --- a/README.md +++ b/README.md @@ -96,7 +96,8 @@ Port 3210 rather than 3000, so it never fights another dev server for the port. A standard Next.js app, Vercel, Netlify and Cloudflare Pages all work with no configuration. - `/` is the app -- `/landing` is the public page describing it +- `/landing` is the public page describing it, in English, with `/landing/de`, + `/landing/sl` and `/landing/pl` alongside it - `/about` is the in-app about page | Branch | URL | Purpose | diff --git a/document/01-product.md b/document/01-product.md index 24b7ddb..3107c19 100755 --- a/document/01-product.md +++ b/document/01-product.md @@ -71,7 +71,10 @@ Each of these was considered and rejected. Do not add them without asking. ## Surfaces - `/` is the app itself -- `/landing` is the public marketing page, rendered outside the app shell +- `/landing` is the public marketing page, rendered outside the app shell, with + `/landing/de`, `/landing/sl` and `/landing/pl` beside it. Routed rather than + translated in the browser, because it renders on the server. See + [05-decisions.md](05-decisions.md). - `/about` is an in-app tab with the version and changelog - Android ships the same code inside a Capacitor WebView - iOS is served by the PWA; there is no native iOS app diff --git a/document/05-decisions.md b/document/05-decisions.md index 41c44fe..7d8e4ac 100755 --- a/document/05-decisions.md +++ b/document/05-decisions.md @@ -319,31 +319,47 @@ the fold. That happened in testing. See [06-traps.md](06-traps.md). to no file. This is why `ButtonLink` exists: `window.location.href = "/plan"` silently does nothing inside the APK. -## The public page stays in English - -Every screen inside the app is translated. `/landing` is not, and that is a -decision rather than an omission. - -The language is chosen in the app and kept in a store in the browser. Every -translated screen is a client component that reads it. `/landing` is a server -component: it exports `metadata`, fetches its own statistics on the server, and -ships no state, which is what lets it render instantly on a phone that has -never opened the app before. A server render happens before any browser has -said which language it wants, so there is nothing for `useLang` to read. - -Making it a client component would translate it and cost the thing it is for. -The alternative that actually works is routed locales, `/landing/de` and the -rest, each rendered on the server for a language known from the URL, with -`hreflang` so a search engine indexes all four. That is a piece of work with a -routing decision, a metadata decision and a canonical-URL decision in it. It -belongs on its own, not appended to the end of a run of wiring PRs. - -Until then the public page is English, and the language picker in the app is -what a person who wants Slovenian will find on their first screen after it. - -The same applies to the `metadata` export in `src/app/layout.tsx`, the page -title and description a search engine and a browser tab show. It is computed on -the server for the same reason and belongs to the same piece of work. +## The public page is routed, the app is not + +`/landing` is English and `/landing/de`, `/landing/sl` and `/landing/pl` are the +rest. All four are prerendered, name each other in `hreflang`, and carry a +canonical pointing at themselves; `x-default` points at English. + +English keeps the bare path rather than moving to `/landing/en`. Every link to +this page that exists in the world points at `/landing`, and a second URL +holding the same page is a duplicate a search engine has to be told to ignore. + +The page reads its strings with `translate(lang, key)` rather than `useLang`, +because it renders on the server before any browser has said which language it +wants. That is the same fact that made this a piece of work rather than a +translation pass, and the reasoning it replaces is below. + +`lang` sits on `
` rather than on ``. The root layout owns `` +and is shared by the whole app, so varying it would mean two root layouts and a +route group around every other page. A screen reader switches voice at the +element that carries `lang`, which is what the attribute is for. + +**The app's own `metadata` in `src/app/layout.tsx` is still English**, and that +is not an oversight. It is the title a browser tab and a search engine show for +`/`, `/plan`, `/stock` and the rest, which are client screens whose language +lives in a store in the browser. Routing those would mean `/sl/plan` and a +locale segment through the entire app, for a title nobody links to. The public +page was worth routing because it is the one page a stranger arrives at. + +### What this replaced, and why the wait was right + +For four cycles the answer was that `/landing` stays English. The reasoning +then: it is a server component, a server render happens before any browser has +said which language it wants, and making it a client component would translate +it and cost the thing it is for, which is arriving instantly on a phone that has +never opened the app. + +That was correct and the conclusion drawn from it was wrong only in timing. The +answer was never "client component"; it was routed locales, and that is a piece +of work with a routing decision, a metadata decision and a canonical-URL +decision in it. It did not belong appended to the end of a run of wiring PRs, +and it is better done as one thing that builds, exports and is checked in the +emitted HTML than as a rushed sixth item. ## A name stays, a description translates diff --git a/src/app/landing/Landing.tsx b/src/app/landing/Landing.tsx new file mode 100644 index 0000000..8aeead0 --- /dev/null +++ b/src/app/landing/Landing.tsx @@ -0,0 +1,623 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { + ArrowRight, + Calculator, + Code2, + Download, + Droplet, + FileUp, + FlaskConical, + LineChart, + Lock, + MessageCircle, + Plus, + ShieldAlert, + Smartphone, + Syringe, + Users, + WifiOff, +} from "lucide-react"; + +import { PEPTIDES } from "@/lib/data/peptides"; +import { LAB_MARKERS } from "@/lib/data/labs"; +import { CURRENT_VERSION, GITHUB_URL } from "@/lib/changelog"; +import { getLandingStats } from "@/lib/landingStats"; +import { translate, type Lang, type TranslationKey } from "@/lib/i18n"; +import { splitSlots } from "@/lib/i18n/rich"; +import { ThemeToggle } from "@/components/ThemeToggle"; +import { Reveal } from "./Reveal"; +import { + CurveArt, + LabsArt, + PrivacyArt, + ReconArt, + RotationArt, + StackArt, + StockArt, +} from "./visuals"; + +/** + * The public page, in one language. + * + * Every other screen in this app is a client component that reads the language + * out of a store in the browser. This one cannot: it renders on the server, + * before any browser has said which language it wants, which is what lets it + * arrive instantly on a phone that has never opened the app. So the language + * comes from the URL and is passed in, and the strings are read with + * `translate` rather than with `useLang`. + * + * English lives at /landing and the rest at /landing/de, /sl and /pl. English + * keeps the bare path rather than gaining a /landing/en of its own: every link + * that exists in the world today points at /landing, and a second URL holding + * the same page is a duplicate a search engine has to be told to ignore. + */ + +const DISCORD_URL = "https://discord.gg/NTfnwSxxr"; +const APK_URL = "https://github.com/ArunNGun/Bench/releases/latest"; + +/** Read from the library itself, so the page cannot overstate what is in it. */ +const COMPOUND_COUNT = PEPTIDES.length; +const MARKER_COUNT = LAB_MARKERS.length; + +/** Which languages the page is written in, and where each one lives. */ +export const LANDING_LANGS: { lang: Lang; path: string; label: string }[] = [ + { lang: "en", path: "/landing", label: "English" }, + { lang: "de", path: "/landing/de", label: "Deutsch" }, + { lang: "sl", path: "/landing/sl", label: "Slovenščina" }, + { lang: "pl", path: "/landing/pl", label: "Polski" }, +]; + +/** + * Title, description and the four addresses this page has. + * + * `hreflang` is the whole point of routing the page rather than translating it + * in the browser: it tells a search engine that these four URLs are one page in + * four languages rather than four pages competing with each other, and it can + * only be said in the markup of a page that was rendered knowing its language. + * `x-default` points at English, which is where a reader we have no language + * for should land. + * + * The paths are relative. This app is self-hosted by whoever wants it, so there + * is no one absolute origin to write down, and a wrong one would be worse than + * none. + */ +export function landingMetadata(lang: Lang): Metadata { + const languages = Object.fromEntries( + LANDING_LANGS.map(({ lang: l, path }) => [l, path])); + + return { + title: translate(lang, "lp_meta_title"), + description: translate(lang, "lp_meta_description"), + alternates: { + canonical: LANDING_LANGS.find((l) => l.lang === lang)?.path ?? "/landing", + languages: { ...languages, "x-default": "/landing" }, + }, + }; +} + +const FEATURES: { + icon: typeof Calculator; + eyebrow: TranslationKey; + title: TranslationKey; + body: TranslationKey; + art: React.ReactNode; +}[] = [ + { + icon: Calculator, + eyebrow: "lp_f_recon_eyebrow", + title: "lp_f_recon_title", + body: "lp_f_recon_body", + art: , + }, + { + icon: LineChart, + eyebrow: "lp_f_levels_eyebrow", + title: "lp_f_levels_title", + body: "lp_f_levels_body", + art: , + }, + { + icon: FlaskConical, + eyebrow: "lp_f_stock_eyebrow", + title: "lp_f_stock_title", + body: "lp_f_stock_body", + art: , + }, + { + icon: Droplet, + eyebrow: "lp_f_labs_eyebrow", + title: "lp_f_labs_title", + body: "lp_f_labs_body", + art: , + }, + { + icon: ShieldAlert, + eyebrow: "lp_f_safety_eyebrow", + title: "lp_f_safety_title", + body: "lp_f_safety_body", + art: , + }, + { + icon: Syringe, + eyebrow: "lp_f_rotation_eyebrow", + title: "lp_f_rotation_title", + body: "lp_f_rotation_body", + art: , + }, +]; + +const ALSO: { icon: typeof WifiOff; title: TranslationKey; body: TranslationKey }[] = [ + { icon: WifiOff, title: "lp_also_offline_title", body: "lp_also_offline_body" }, + { icon: FileUp, title: "lp_also_import_title", body: "lp_also_import_body" }, + { icon: Download, title: "lp_also_backup_title", body: "lp_also_backup_body" }, + { icon: Plus, title: "lp_also_own_title", body: "lp_also_own_body" }, + { icon: Users, title: "lp_also_profiles_title", body: "lp_also_profiles_body" }, + { icon: Lock, title: "lp_also_updates_title", body: "lp_also_updates_body" }, +]; + +function Cta({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} + +/** The same generated icon the launcher and the home screen use. */ +function Logo({ size = 40 }: { size?: number }) { + return ( + /* eslint-disable-next-line @next/next/no-img-element -- a static icon, nothing for the image pipeline to do */ + + ); +} + +/** + * The language row, plain links rather than a picker. + * + * A dropdown would need script, and this page is deliberately readable without + * it. Links also give a search engine the four addresses in the markup, which + * is half of what the `hreflang` tags are saying in the head. + */ +function LangBar({ lang }: { lang: Lang }) { + return ( + + {LANDING_LANGS.map(({ lang: l, path, label }) => + l === lang ? ( + + {label} + + ) : ( + + {label} + + ))} + + ); +} + +export async function Landing({ lang }: { lang: Lang }) { + const t = (key: TranslationKey, vars?: Record) => + translate(lang, key, vars); + + const { apkDownloads, pageViews } = await getLandingStats(); + const totalUsers = (apkDownloads ?? 0) + (pageViews ?? 0) || null; + + return ( + /* + `lang` here rather than on ``, which belongs to the root layout and + is shared by the whole app. A screen reader switches voice at this + element, which is the behaviour the attribute exists for, and a crawler + reads the subtree as this language. Moving it to `` would mean two + root layouts and a route group around every other page, which is a lot of + restructuring to say the same thing. + */ +
+ {/* + The reveal and hero animations both start from opacity 0 and are + released by script. With script off, nothing would release them and the + page would be an empty canvas, so opt out of the whole scheme up front. + */} + + + + + {/* Hero */} +
+
+ + {/* Stat band */} +
+ +
+ {[ + { n: String(COMPOUND_COUNT), l: t("lp_stat_compounds") }, + { n: String(MARKER_COUNT), l: t("lp_stat_markers") }, + { n: "962", l: t("lp_stat_tests") }, + { n: "0", l: t("lp_stat_bytes") }, + ].map(({ n, l }) => ( +
+

+ {n} +

+

+ {l} +

+
+ ))} + {(totalUsers != null || (apkDownloads != null && apkDownloads > 0)) && ( + <> + {totalUsers != null && ( +
+

+ {totalUsers.toLocaleString()} +

+

+ + + + + {t("lp_stat_users")} +

+
+ )} + {apkDownloads != null && apkDownloads > 0 && ( +
+

+ {apkDownloads.toLocaleString()} +

+

+ {t("lp_stat_downloads")} +

+
+ )} + + )} +
+
+
+ + {/* Features, alternating so the eye has somewhere to go */} +
+ {FEATURES.map(({ icon: Icon, eyebrow, title, body, art }, i) => ( +
+ + + {t(eyebrow)} + +

+ + {t(title)} +

+

+ {t(body, { n: MARKER_COUNT })} +

+
+ + +
+ {art} +
+
+
+ ))} +
+ + {/* Privacy */} +
+ +
+
+ + {t("lp_privacy_badge")} + +

+ {t("lp_privacy_title")} +

+

+ {t("lp_privacy_1")} +

+

+ {t("lp_privacy_2")} +

+

+ {t("lp_privacy_3")} +

+
+ +
+
+
+ + {/* Everything else */} +
+ +

+ {t("lp_also_title")} +

+
+
+ {ALSO.map(({ icon: Icon, title, body }, i) => ( + +
+ + + +

{t(title)}

+

{t(body)}

+
+
+ ))} +
+
+ + {/* Sourcing and limits */} +
+ +
+

+ {t("lp_sources_title")} +

+

+ {t("lp_sources_1")} +

+

+ {t("lp_sources_2")} +

+
+
+ + +
+

+ {t("lp_notwhat_title")} +

+

+ {t("lp_notwhat_1")} +

+

+ {t("lp_notwhat_2")} +

+
+
+
+ + {/* Close */} +
+ +
+ +

+ {t("lp_close_title")} +

+

+ {t("lp_close_body")} +

+
+ {t("lp_cta_open")} + + + {t("lp_close_discord")} + +
+
+
+
+ +
+
+ + + {/* The builder's name is a name, so it is a slot rather than copy. */} + + {splitSlots(t("lp_footer_built", { version: CURRENT_VERSION }), ["name"]).map( + (part, i) => + "text" in part ? ( + {part.text} + ) : ( + + Arun + + ))} + + + + + {t("lp_nav_about")} + + + GitHub + + + Discord + + + {t("lp_footer_apk")} + + + {t("lp_nav_open")} + + +
+
+ {/* Two names and two links inside one sentence, so the sentence stays whole. */} + {splitSlots(t("lp_footer_icon"), ["author", "site"]).map((part, i) => + "text" in part ? ( + {part.text} + ) : part.slot === "author" ? ( + + Ricardo Ruiz + + ) : ( + + Flaticon + + ))} +
+
+
+ ); +} diff --git a/src/app/landing/[lang]/page.tsx b/src/app/landing/[lang]/page.tsx new file mode 100644 index 0000000..eea3318 --- /dev/null +++ b/src/app/landing/[lang]/page.tsx @@ -0,0 +1,36 @@ +import { notFound } from "next/navigation"; +import { Landing, LANDING_LANGS, landingMetadata } from "../Landing"; +import type { Lang } from "@/lib/i18n"; + +/** + * The public page in a language that is not English. + * + * Three static pages rather than a route that renders anything it is handed. + * `dynamicParams = false` is what makes /landing/fr a 404 instead of an attempt + * to render a language the dictionary does not carry, and it is also what the + * static export needs: with no server to render on demand, the set of pages has + * to be known when the folder is built. + */ +export const dynamicParams = false; + +const OTHERS = LANDING_LANGS.filter((l) => l.lang !== "en"); + +export function generateStaticParams() { + return OTHERS.map(({ lang }) => ({ lang })); +} + +function known(lang: string): Lang | null { + return OTHERS.find((l) => l.lang === lang)?.lang ?? null; +} + +export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) { + const { lang } = await params; + return landingMetadata(known(lang) ?? "en"); +} + +export default async function LandingInLanguage({ params }: { params: Promise<{ lang: string }> }) { + const { lang } = await params; + const one = known(lang); + if (!one) notFound(); + return ; +} diff --git a/src/app/landing/page.tsx b/src/app/landing/page.tsx index 538ae52..f4bc577 100755 --- a/src/app/landing/page.tsx +++ b/src/app/landing/page.tsx @@ -1,561 +1,16 @@ -import type { Metadata } from "next"; -import Link from "next/link"; -import { - ArrowRight, - Calculator, - Code2, - Download, - Droplet, - FileUp, - FlaskConical, - LineChart, - Lock, - MessageCircle, - Plus, - ShieldAlert, - Smartphone, - Syringe, - Users, - WifiOff, -} from "lucide-react"; - -const DISCORD_URL = "https://discord.gg/NTfnwSxxr"; -import { PEPTIDES } from "@/lib/data/peptides"; -import { LAB_MARKERS } from "@/lib/data/labs"; -import { CURRENT_VERSION, GITHUB_URL } from "@/lib/changelog"; -import { getLandingStats } from "@/lib/landingStats"; -import { ThemeToggle } from "@/components/ThemeToggle"; -import { Reveal } from "./Reveal"; -import { - CurveArt, - LabsArt, - PrivacyArt, - ReconArt, - RotationArt, - StackArt, - StockArt, -} from "./visuals"; - -export const metadata: Metadata = { - title: "Bench, a private peptide, HGH and anabolic tracker", - description: - "Track protocols, doses, stock and bloodwork with arithmetic you can trust. No account, no server, and everything stays on your device unless you choose to run your own.", -}; - -/** Read from the library itself, so the page cannot overstate what is in it. */ -const COMPOUND_COUNT = PEPTIDES.length; -const MARKER_COUNT = LAB_MARKERS.length; - -const FEATURES = [ - { - icon: Calculator, - eyebrow: "Reconstitution", - title: "It draws the syringe you are actually holding", - body: "Tell it the vial and the dose you want, and it tells you what to add and how far to draw, on a barrel rendered to real graduations. A U-40 barrel is never quietly read as U-100. That one mistake delivers two and a half times the intended dose, so the scale is an explicit choice and the reading on the other scale sits right beside it.", - art: , - }, - { - icon: LineChart, - eyebrow: "Levels", - title: "See what is circulating, not just what you took", - body: "Every logged dose feeds a curve built from published half-lives, so you can see a trough before you feel it. Blends are split into their components and each one modelled on its own half-life, because averaging a blend into a single line describes nothing that is actually in you.", - art: , - }, - { - icon: FlaskConical, - eyebrow: "Stock", - title: "Know what is left, and what it cost", - body: "Vials move from sealed through reconstituted to empty, counted in mass rather than volume, so a dose drawn from a sealed vial still depletes it. Beyond-use dates, doses remaining and cost per dose are all derived rather than typed in and forgotten.", - art: , - }, - { - icon: Droplet, - eyebrow: "Bloodwork", - title: "Results on the same axis as your doses", - body: `${MARKER_COUNT} markers charted against your dose history, so a change lines up with whatever caused it. The app suggests the ones your own compounds make worth watching: lipase on a GLP-1, haematocrit on an androgen, liver enzymes on an oral. Reference ranges come from your own report, because they belong to the lab that ran the sample.`, - art: , - }, - { - icon: ShieldAlert, - eyebrow: "Safety", - title: "Warnings you have not learned to ignore", - body: "Checks run across everything active at once: two compounds on the same receptor, the same compound arriving from two protocols, two oral steroids sharing one liver. They are built on receptor classes rather than names, so a deliberate pairing like CJC-1295 with ipamorelin stays silent. A warning you dismiss on reflex is worse than no warning at all.", - art: , - }, - { - icon: Syringe, - eyebrow: "Rotation", - title: "A site that has had time to recover", - body: "Pin the sites a protocol uses and the app suggests the next one, keeping recently used tissue resting. Titration ladders advance on schedule, and adherence is reported honestly rather than flattering you.", - art: , - }, -]; - -const ALSO = [ - { - icon: WifiOff, - title: "Works offline, installs like an app", - body: "Add it to your home screen on iPhone or Android and it opens straight from cache, with no connection needed.", - }, - { - icon: FileUp, - title: "Bring your history with you", - body: "Import CSV, TSV, JSON or a spreadsheet. Shotsy is recognised by name, anything else is read from its column headers, and you see a preview before a single row is written.", - }, - { - icon: Download, - title: "Backups you control", - body: "Export the lot to a file whenever you like. On Android it also writes rotating backups into Documents, which survive clearing the app data.", - }, - { - icon: Plus, - title: "Your own compounds", - body: "Running something the library does not carry? Add it from any picker. It behaves like a built-in everywhere, and is never dressed up as researched.", - }, - { - icon: Users, - title: "More than one person", - body: "Separate profiles keep two people sharing one device from ever seeing each other's numbers.", - }, - { - icon: Lock, - title: "Updates that leave data alone", - body: "A new version asks before it installs, and migrations are tested in both directions, so a backup from any older version restores intact.", - }, -]; - -function Cta({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ); -} - -const APK_URL = "https://github.com/ArunNGun/Bench/releases/latest"; - -function ApkButton() { - return ( - - - Download for Android - - ); -} - -/** The same generated icon the launcher and the home screen use. */ -function Logo({ size = 40 }: { size?: number }) { - return ( - /* eslint-disable-next-line @next/next/no-img-element -- a static icon, nothing for the image pipeline to do */ - - ); -} - -export default async function LandingPage() { - const { apkDownloads, pageViews } = await getLandingStats(); - const totalUsers = (apkDownloads ?? 0) + (pageViews ?? 0) || null; - return ( -
- {/* - The reveal and hero animations both start from opacity 0 and are - released by script. With script off, nothing would release them and the - page would be an empty canvas, so opt out of the whole scheme up front. - */} - - - - - {/* Hero */} -
-
- - {/* Stat band */} -
- -
- {[ - { n: String(COMPOUND_COUNT), l: "compounds, every figure cited" }, - { n: String(MARKER_COUNT), l: "blood markers you can chart" }, - { n: "962", l: "tests on the arithmetic" }, - { n: "0", l: "bytes of your data leave the device" }, - ].map(({ n, l }) => ( -
-

- {n} -

-

- {l} -

-
- ))} - {(totalUsers != null || (apkDownloads != null && apkDownloads > 0)) && ( - <> - {totalUsers != null && ( -
-

- {totalUsers.toLocaleString()} -

-

- - - - - users -

-
- )} - {apkDownloads != null && apkDownloads > 0 && ( -
-

- {apkDownloads.toLocaleString()} -

-

- APK downloads -

-
- )} - - )} -
-
-
- - {/* Features, alternating so the eye has somewhere to go */} -
- {FEATURES.map(({ icon: Icon, eyebrow, title, body, art }, i) => ( -
- - - {eyebrow} - -

- - {title} -

-

{body}

-
- - -
- {art} -
-
-
- ))} -
- - {/* Privacy */} -
- -
-
- - Privacy - -

- There is no backend to leak. -

-

- There is no account to create and nothing is uploaded. Everything you enter lives in - your browser, on your device. We run no server, so there is none of yours on ours. -

-

- If you want the same data on a phone and a laptop, you can point the app at a sync - server you host yourself. It is off until you set it up, it takes a server address you - own, and what goes up is encrypted in your browser first, so even that machine holds - something it cannot read. -

-

- That is a real guarantee and a real risk in the same breath. Nobody can read your data, - and nobody can recover it for you either, so the app nudges you to keep an exported - copy somewhere safe. -

-
- -
-
-
- - {/* Everything else */} -
- -

- And the rest of it -

-
-
- {ALSO.map(({ icon: Icon, title, body }, i) => ( - -
- - - -

{title}

-

{body}

-
-
- ))} -
-
- - {/* Sourcing and limits */} -
- -
-

- Where the numbers come from -

-

- Prescribing labels first, then published trials, then registries, and every entry links - its sources. Community practice is included where it is genuinely what people do, and it - is tagged as exactly that so it never passes for an approved figure. -

-

- Where a half-life has never been measured in humans, the app says so and draws no curve - rather than inventing one that looks authoritative. -

-
-
- - -
-

- What this is not -

-

- Not medical advice, not a prescription, and not a recommendation to use anything in it. - Most of these compounds are not approved medicines anywhere, and material sold for - research use has no regulated identity, purity or sterility behind it. -

-

- The app can check your arithmetic. It cannot check what is in your vial, and it is no - substitute for a doctor who knows your history. -

-
-
-
- - {/* Close */} -
- -
- -

- Nothing to sign up for. Open it and start. -

-

- Already tracking somewhere else? Import your history and the app will build a protocol - from the pattern of your doses. -

-
- Open the app - - - Join the community - -
-
-
-
- - -
- ); +import { Landing, landingMetadata } from "./Landing"; + +/** + * The public page in English, at /landing. + * + * English keeps the bare path rather than moving to /landing/en. Every link to + * this page that exists in the world points here, and a second URL holding the + * same page is a duplicate that a search engine has to be told to ignore. The + * other three languages live at /landing/de, /sl and /pl, and all four name + * each other in `hreflang`. + */ +export const metadata = landingMetadata("en"); + +export default function LandingPage() { + return ; } diff --git a/src/lib/i18n/translations.ts b/src/lib/i18n/translations.ts index f5ac83e..ec0d1de 100644 --- a/src/lib/i18n/translations.ts +++ b/src/lib/i18n/translations.ts @@ -1329,6 +1329,78 @@ export const TRANSLATIONS = { now_vial_label: "{name} vial", now_all_past_date: "**{name}** has {doses} left, and every one of them is past its date.", account_signed_in_on: "Signed in as **{user}** on {url}.", + + // ── The public page ─────────────────────────────────────────────────────── + // Reached at /landing in English and /landing/de, /sl, /pl in the rest. + // Unlike every other screen this one is rendered on the server, before any + // browser has said which language it wants, so the language comes from the + // URL and these are read with `translate` rather than `useLang`. + lp_meta_title: "Bench, a private peptide, HGH and anabolic tracker", + lp_meta_description: "Track protocols, doses, stock and bloodwork with arithmetic you can trust. No account, no server, and everything stays on your device unless you choose to run your own.", + lp_language: "Language", + lp_nav_about: "About", + lp_nav_open: "Open app", + lp_hero_badge: "No account, no server, no analytics", + lp_hero_title_1: "Get the arithmetic right.", + lp_hero_title_2: "Keep the data yours.", + lp_hero_body: "A tracker for peptide, growth hormone and anabolic protocols. Doses, reconstitution, stock, bloodwork and outcomes, all held in your browser on your own device and sent nowhere.", + lp_cta_open: "Open the app", + lp_cta_apk: "Download for Android", + lp_hero_note: "Free, nothing to sign up for. Works offline once loaded.", + lp_stat_compounds: "compounds, every figure cited", + lp_stat_markers: "blood markers you can chart", + lp_stat_tests: "tests on the arithmetic", + lp_stat_bytes: "bytes of your data leave the device", + lp_stat_users: "users", + lp_stat_downloads: "APK downloads", + lp_f_recon_eyebrow: "Reconstitution", + lp_f_recon_title: "It draws the syringe you are actually holding", + lp_f_recon_body: "Tell it the vial and the dose you want, and it tells you what to add and how far to draw, on a barrel rendered to real graduations. A U-40 barrel is never quietly read as U-100. That one mistake delivers two and a half times the intended dose, so the scale is an explicit choice and the reading on the other scale sits right beside it.", + lp_f_levels_eyebrow: "Levels", + lp_f_levels_title: "See what is circulating, not just what you took", + lp_f_levels_body: "Every logged dose feeds a curve built from published half-lives, so you can see a trough before you feel it. Blends are split into their components and each one modelled on its own half-life, because averaging a blend into a single line describes nothing that is actually in you.", + lp_f_stock_eyebrow: "Stock", + lp_f_stock_title: "Know what is left, and what it cost", + lp_f_stock_body: "Vials move from sealed through reconstituted to empty, counted in mass rather than volume, so a dose drawn from a sealed vial still depletes it. Beyond-use dates, doses remaining and cost per dose are all derived rather than typed in and forgotten.", + lp_f_labs_eyebrow: "Bloodwork", + lp_f_labs_title: "Results on the same axis as your doses", + lp_f_labs_body: "{n} markers charted against your dose history, so a change lines up with whatever caused it. The app suggests the ones your own compounds make worth watching: lipase on a GLP-1, haematocrit on an androgen, liver enzymes on an oral. Reference ranges come from your own report, because they belong to the lab that ran the sample.", + lp_f_safety_eyebrow: "Safety", + lp_f_safety_title: "Warnings you have not learned to ignore", + lp_f_safety_body: "Checks run across everything active at once: two compounds on the same receptor, the same compound arriving from two protocols, two oral steroids sharing one liver. They are built on receptor classes rather than names, so a deliberate pairing like CJC-1295 with ipamorelin stays silent. A warning you dismiss on reflex is worse than no warning at all.", + lp_f_rotation_eyebrow: "Rotation", + lp_f_rotation_title: "A site that has had time to recover", + lp_f_rotation_body: "Pin the sites a protocol uses and the app suggests the next one, keeping recently used tissue resting. Titration ladders advance on schedule, and adherence is reported honestly rather than flattering you.", + lp_privacy_badge: "Privacy", + lp_privacy_title: "There is no backend to leak.", + lp_privacy_1: "There is no account to create and nothing is uploaded. Everything you enter lives in your browser, on your device. We run no server, so there is none of yours on ours.", + lp_privacy_2: "If you want the same data on a phone and a laptop, you can point the app at a sync server you host yourself. It is off until you set it up, it takes a server address you own, and what goes up is encrypted in your browser first, so even that machine holds something it cannot read.", + lp_privacy_3: "That is a real guarantee and a real risk in the same breath. Nobody can read your data, and nobody can recover it for you either, so the app nudges you to keep an exported copy somewhere safe.", + lp_also_title: "And the rest of it", + lp_also_offline_title: "Works offline, installs like an app", + lp_also_offline_body: "Add it to your home screen on iPhone or Android and it opens straight from cache, with no connection needed.", + lp_also_import_title: "Bring your history with you", + lp_also_import_body: "Import CSV, TSV, JSON or a spreadsheet. Shotsy is recognised by name, anything else is read from its column headers, and you see a preview before a single row is written.", + lp_also_backup_title: "Backups you control", + lp_also_backup_body: "Export the lot to a file whenever you like. On Android it also writes rotating backups into Documents, which survive clearing the app data.", + lp_also_own_title: "Your own compounds", + lp_also_own_body: "Running something the library does not carry? Add it from any picker. It behaves like a built-in everywhere, and is never dressed up as researched.", + lp_also_profiles_title: "More than one person", + lp_also_profiles_body: "Separate profiles keep two people sharing one device from ever seeing each other's numbers.", + lp_also_updates_title: "Updates that leave data alone", + lp_also_updates_body: "A new version asks before it installs, and migrations are tested in both directions, so a backup from any older version restores intact.", + lp_sources_title: "Where the numbers come from", + lp_sources_1: "Prescribing labels first, then published trials, then registries, and every entry links its sources. Community practice is included where it is genuinely what people do, and it is tagged as exactly that so it never passes for an approved figure.", + lp_sources_2: "Where a half-life has never been measured in humans, the app says so and draws no curve rather than inventing one that looks authoritative.", + lp_notwhat_title: "What this is not", + lp_notwhat_1: "Not medical advice, not a prescription, and not a recommendation to use anything in it. Most of these compounds are not approved medicines anywhere, and material sold for research use has no regulated identity, purity or sterility behind it.", + lp_notwhat_2: "The app can check your arithmetic. It cannot check what is in your vial, and it is no substitute for a doctor who knows your history.", + lp_close_title: "Nothing to sign up for. Open it and start.", + lp_close_body: "Already tracking somewhere else? Import your history and the app will build a protocol from the pattern of your doses.", + lp_close_discord: "Join the community", + lp_footer_built: "Bench v{version}, built by {name}", + lp_footer_apk: "Android APK", + lp_footer_icon: "App icon by {author} on {site}.", }, de: { @@ -2630,6 +2702,73 @@ export const TRANSLATIONS = { now_vial_label: "{name}-Ampulle", now_all_past_date: "**{name}** hat noch {doses}, und jede davon ist über dem Datum.", account_signed_in_on: "Angemeldet als **{user}** auf {url}.", + + lp_meta_title: "Bench, ein privater Tracker für Peptide, HGH und Anabolika", + lp_meta_description: "Protokolle, Dosen, Vorrat und Blutwerte mit einer Rechnung, der du trauen kannst. Kein Konto, kein Server, und alles bleibt auf deinem Gerät, solange du nicht selbst einen betreibst.", + lp_language: "Sprache", + lp_nav_about: "Info", + lp_nav_open: "App öffnen", + lp_hero_badge: "Kein Konto, kein Server, keine Analytics", + lp_hero_title_1: "Rechne richtig.", + lp_hero_title_2: "Behalte deine Daten.", + lp_hero_body: "Ein Tracker für Peptid-, Wachstumshormon- und Anabolikaprotokolle. Dosen, Rekonstitution, Vorrat, Blutwerte und Ergebnisse, alles im Browser auf deinem eigenen Gerät und nirgendwohin gesendet.", + lp_cta_open: "App öffnen", + lp_cta_apk: "Für Android laden", + lp_hero_note: "Kostenlos, keine Anmeldung. Läuft offline, sobald es einmal geladen ist.", + lp_stat_compounds: "Substanzen, jede Zahl mit Quelle", + lp_stat_markers: "Blutwerte zum Auswerten", + lp_stat_tests: "Tests auf der Rechnung", + lp_stat_bytes: "Bytes deiner Daten verlassen das Gerät", + lp_stat_users: "Nutzer", + lp_stat_downloads: "APK-Downloads", + lp_f_recon_eyebrow: "Rekonstitution", + lp_f_recon_title: "Es zeichnet die Spritze, die du in der Hand hältst", + lp_f_recon_body: "Sag ihm die Ampulle und die gewünschte Dosis, und es sagt dir, wie viel hinein muss und wie weit du aufziehst, auf einem Zylinder mit echter Skalierung. Ein U-40-Zylinder wird nie stillschweigend als U-100 gelesen. Dieser eine Fehler liefert das Zweieinhalbfache der gemeinten Dosis, deshalb ist die Skala eine bewusste Wahl und der Wert auf der anderen steht direkt daneben.", + lp_f_levels_eyebrow: "Spiegel", + lp_f_levels_title: "Sieh, was zirkuliert, nicht nur was du genommen hast", + lp_f_levels_body: "Jede eingetragene Dosis speist eine Kurve aus publizierten Halbwertszeiten, so siehst du ein Tal, bevor du es spürst. Blends werden in ihre Bestandteile zerlegt und jeder auf seiner eigenen Halbwertszeit modelliert, denn ein Blend als eine einzige Linie beschreibt nichts, was tatsächlich in dir ist.", + lp_f_stock_eyebrow: "Vorrat", + lp_f_stock_title: "Wisse, was übrig ist und was es gekostet hat", + lp_f_stock_body: "Ampullen gehen von versiegelt über rekonstituiert bis leer, gezählt in Masse statt in Volumen, sodass eine aus einer versiegelten Ampulle gezogene Dosis sie trotzdem leert. Aufbrauchfristen, verbleibende Dosen und Kosten pro Dosis werden hergeleitet statt einmal eingetippt und vergessen.", + lp_f_labs_eyebrow: "Blutwerte", + lp_f_labs_title: "Ergebnisse auf derselben Achse wie deine Dosen", + lp_f_labs_body: "{n} Marker gegen deine Dosishistorie aufgetragen, damit eine Veränderung zu dem passt, was sie ausgelöst hat. Die App schlägt die vor, die deine eigenen Substanzen beobachtenswert machen: Lipase bei einem GLP-1, Hämatokrit bei einem Androgen, Leberwerte bei einem Oralen. Referenzbereiche kommen aus deinem eigenen Befund, denn sie gehören dem Labor, das die Probe gemessen hat.", + lp_f_safety_eyebrow: "Sicherheit", + lp_f_safety_title: "Warnungen, die du dir noch nicht abgewöhnt hast", + lp_f_safety_body: "Geprüft wird über alles Aktive zugleich: zwei Substanzen am selben Rezeptor, dieselbe Substanz aus zwei Protokollen, zwei orale Steroide an einer Leber. Die Prüfungen bauen auf Rezeptorklassen statt auf Namen, deshalb bleibt eine bewusste Kombination wie CJC-1295 mit Ipamorelin still. Eine Warnung, die man reflexhaft wegklickt, ist schlimmer als gar keine.", + lp_f_rotation_eyebrow: "Rotation", + lp_f_rotation_title: "Eine Stelle, die sich erholen konnte", + lp_f_rotation_body: "Lege die Stellen fest, die ein Protokoll nutzt, und die App schlägt die nächste vor, damit zuletzt benutztes Gewebe ruht. Titrationsleitern rücken nach Plan vor, und die Adhärenz wird ehrlich berichtet statt schmeichelhaft.", + lp_privacy_badge: "Privatsphäre", + lp_privacy_title: "Es gibt kein Backend, aus dem etwas laufen könnte.", + lp_privacy_1: "Es gibt kein Konto anzulegen und nichts wird hochgeladen. Alles, was du einträgst, liegt in deinem Browser, auf deinem Gerät. Wir betreiben keinen Server, also liegt von dir nichts auf unserem.", + lp_privacy_2: "Willst du dieselben Daten auf Handy und Laptop, kannst du die App auf einen Sync-Server richten, den du selbst betreibst. Er ist aus, bis du ihn einrichtest, er nimmt eine Adresse, die dir gehört, und was hochgeht, wird vorher im Browser verschlüsselt, sodass selbst diese Maschine etwas hält, das sie nicht lesen kann.", + lp_privacy_3: "Das ist eine echte Garantie und im selben Atemzug ein echtes Risiko. Niemand kann deine Daten lesen, und niemand kann sie dir auch wiederherstellen, deshalb erinnert dich die App daran, eine exportierte Kopie sicher aufzubewahren.", + lp_also_title: "Und der Rest", + lp_also_offline_title: "Läuft offline, installiert sich wie eine App", + lp_also_offline_body: "Leg es auf iPhone oder Android auf den Startbildschirm, dann öffnet es direkt aus dem Cache, ganz ohne Verbindung.", + lp_also_import_title: "Nimm deine Historie mit", + lp_also_import_body: "Importiere CSV, TSV, JSON oder eine Tabelle. Shotsy wird am Namen erkannt, alles andere über die Spaltenköpfe gelesen, und du siehst eine Vorschau, bevor eine einzige Zeile geschrieben wird.", + lp_also_backup_title: "Backups, die dir gehören", + lp_also_backup_body: "Exportiere jederzeit alles in eine Datei. Auf Android schreibt es zusätzlich rotierende Backups nach Dokumente, die das Löschen der App-Daten überleben.", + lp_also_own_title: "Deine eigenen Substanzen", + lp_also_own_body: "Du nimmst etwas, das die Bibliothek nicht führt? Füge es aus jeder Auswahl hinzu. Es verhält sich überall wie ein eingebautes und wird nie als recherchiert ausgegeben.", + lp_also_profiles_title: "Mehr als eine Person", + lp_also_profiles_body: "Getrennte Profile sorgen dafür, dass zwei Menschen an einem Gerät nie die Zahlen des anderen sehen.", + lp_also_updates_title: "Updates, die Daten in Ruhe lassen", + lp_also_updates_body: "Eine neue Version fragt, bevor sie sich installiert, und Migrationen werden in beide Richtungen getestet, sodass ein Backup aus jeder älteren Version unversehrt zurückkommt.", + lp_sources_title: "Woher die Zahlen kommen", + lp_sources_1: "Zuerst Fachinformationen, dann publizierte Studien, dann Register, und jeder Eintrag verlinkt seine Quellen. Community-Praxis ist dort enthalten, wo sie wirklich das ist, was Leute tun, und sie ist genau so gekennzeichnet, damit sie nie als zugelassene Zahl durchgeht.", + lp_sources_2: "Wo eine Halbwertszeit nie am Menschen gemessen wurde, sagt die App das und zeichnet keine Kurve, statt eine zu erfinden, die autoritativ aussieht.", + lp_notwhat_title: "Was das nicht ist", + lp_notwhat_1: "Keine medizinische Beratung, kein Rezept und keine Empfehlung, irgendetwas davon zu nehmen. Die meisten dieser Substanzen sind nirgends zugelassene Arzneimittel, und Material, das für Forschungszwecke verkauft wird, hat keine regulierte Identität, Reinheit oder Sterilität hinter sich.", + lp_notwhat_2: "Die App kann deine Rechnung prüfen. Sie kann nicht prüfen, was in deiner Ampulle ist, und sie ersetzt keine Ärztin, die deine Geschichte kennt.", + lp_close_title: "Nichts anzumelden. Öffnen und loslegen.", + lp_close_body: "Trackst du schon woanders? Importiere deine Historie, und die App baut aus dem Muster deiner Dosen ein Protokoll.", + lp_close_discord: "Zur Community", + lp_footer_built: "Bench v{version}, gebaut von {name}", + lp_footer_apk: "Android-APK", + lp_footer_icon: "App-Icon von {author} auf {site}.", }, sl: { @@ -4023,6 +4162,73 @@ export const TRANSLATIONS = { now_vial_label: "vialka {name}", now_all_past_date: "**{name}** ima še {doses}, a vse so čez rok.", account_signed_in_on: "Prijavljen kot **{user}** na {url}.", + + lp_meta_title: "Bench, zasebni sledilnik peptidov, HGH in anabolikov", + lp_meta_description: "Spremljaj načrte, doze, zalogo in krvne slike z računom, ki mu lahko zaupaš. Brez računa, brez strežnika, in vse ostane na tvoji napravi, dokler si ne postaviš svojega.", + lp_language: "Jezik", + lp_nav_about: "O Benchu", + lp_nav_open: "Odpri aplikacijo", + lp_hero_badge: "Brez računa, brez strežnika, brez analitike", + lp_hero_title_1: "Račun naj bo pravilen.", + lp_hero_title_2: "Podatki naj ostanejo tvoji.", + lp_hero_body: "Sledilnik za načrte s peptidi, rastnim hormonom in anaboliki. Doze, rekonstitucija, zaloga, krvne slike in izidi, vse v tvojem brskalniku na tvoji napravi in poslano nikamor.", + lp_cta_open: "Odpri aplikacijo", + lp_cta_apk: "Prenesi za Android", + lp_hero_note: "Brezplačno, nič za prijaviti. Ko se enkrat naloži, deluje brez povezave.", + lp_stat_compounds: "spojin, vsaka številka z virom", + lp_stat_markers: "krvnih označevalcev za prikaz", + lp_stat_tests: "testov na računu", + lp_stat_bytes: "bajtov tvojih podatkov zapusti napravo", + lp_stat_users: "uporabnikov", + lp_stat_downloads: "prenosov APK", + lp_f_recon_eyebrow: "Rekonstitucija", + lp_f_recon_title: "Nariše brizgo, ki jo res držiš v roki", + lp_f_recon_body: "Povej mu vialko in dozo, ki jo hočeš, in pove ti, koliko dodati in do kam potegniti, na valju z resničnimi črticami. Valj U-40 nikoli ni tiho prebran kot U-100. Ta ena napaka dostavi dvainpolkrat večjo dozo od namenjene, zato je lestvica izrecna izbira, odčitek na drugi pa stoji tik ob njej.", + lp_f_levels_eyebrow: "Nivoji", + lp_f_levels_title: "Poglej, kaj kroži, ne le kaj si vzel", + lp_f_levels_body: "Vsaka zabeležena doza hrani krivuljo iz objavljenih razpolovnih dob, tako da dolino vidiš, preden jo začutiš. Mešanice so razstavljene na sestavine in vsaka modelirana po svoji razpolovni dobi, ker mešanica, povprečena v eno črto, ne opisuje ničesar, kar je v tebi.", + lp_f_stock_eyebrow: "Zaloga", + lp_f_stock_title: "Vedi, kaj je ostalo in koliko je stalo", + lp_f_stock_body: "Vialke gredo od zaprte prek rekonstituirane do prazne, štete po masi in ne po prostornini, zato doza iz zaprte vialke to vialko vseeno izprazni. Datumi varne uporabe, preostale doze in cena na dozo so izračunani, ne enkrat vpisani in pozabljeni.", + lp_f_labs_eyebrow: "Krvne slike", + lp_f_labs_title: "Izvidi na isti osi kot tvoje doze", + lp_f_labs_body: "{n} označevalcev, narisanih ob tvoji zgodovini doz, da se sprememba poravna s tem, kar jo je povzročilo. Aplikacija predlaga tiste, ki jih tvoje spojine naredijo vredne spremljanja: lipazo pri GLP-1, hematokrit pri androgenu, jetrne encime pri oralnem. Referenčna območja pridejo iz tvojega izvida, ker pripadajo laboratoriju, ki je vzorec izmeril.", + lp_f_safety_eyebrow: "Varnost", + lp_f_safety_title: "Opozorila, ki se jih še nisi navadil prezreti", + lp_f_safety_body: "Preverjanje teče čez vse aktivno hkrati: dve spojini na istem receptorju, ista spojina iz dveh načrtov, dva oralna steroida na enih jetrih. Zgrajeno je na razredih receptorjev in ne na imenih, zato namerna kombinacija, kot je CJC-1295 z ipamorelinom, ostane tiha. Opozorilo, ki ga odkljukaš po refleksu, je slabše od nobenega.", + lp_f_rotation_eyebrow: "Rotacija", + lp_f_rotation_title: "Mesto, ki je imelo čas za počitek", + lp_f_rotation_body: "Pripni mesta, ki jih načrt uporablja, in aplikacija predlaga naslednje, tako da nedavno uporabljeno tkivo počiva. Titracijske lestve napredujejo po urniku, dosledno jemanje pa je poročano pošteno in ne laskavo.", + lp_privacy_badge: "Zasebnost", + lp_privacy_title: "Ni zaledja, iz katerega bi kaj ušlo.", + lp_privacy_1: "Ni računa za ustvariti in nič se ne nalaga nikamor. Vse, kar vpišeš, živi v tvojem brskalniku, na tvoji napravi. Strežnika ne poganjamo, torej na našem ni ničesar tvojega.", + lp_privacy_2: "Če hočeš iste podatke na telefonu in na prenosniku, lahko aplikacijo usmeriš na sinhronizacijski strežnik, ki ga gostiš sam. Izklopljen je, dokler ga ne nastaviš, vzame naslov, ki je tvoj, in kar gre gor, je prej šifrirano v brskalniku, tako da tudi tisti stroj hrani nekaj, česar ne more prebrati.", + lp_privacy_3: "To je resnično jamstvo in resnično tveganje v isti sapi. Tvojih podatkov ne more prebrati nihče, in nihče ti jih tudi ne more obnoviti, zato te aplikacija priganja, da izvoženo kopijo hraniš na varnem.", + lp_also_title: "In vse ostalo", + lp_also_offline_title: "Deluje brez povezave, namesti se kot aplikacija", + lp_also_offline_body: "Dodaj jo na domači zaslon na iPhonu ali Androidu in odpre se naravnost iz predpomnilnika, brez povezave.", + lp_also_import_title: "Vzemi svojo zgodovino s sabo", + lp_also_import_body: "Uvozi CSV, TSV, JSON ali preglednico. Shotsy je prepoznan po imenu, vse drugo prebrano iz glav stolpcev, in predogled vidiš, preden je zapisana ena sama vrstica.", + lp_also_backup_title: "Varnostne kopije, ki jih obvladuješ", + lp_also_backup_body: "Vse izvozi v datoteko, kadarkoli hočeš. Na Androidu piše tudi krožeče kopije v Dokumente, ki preživijo brisanje podatkov aplikacije.", + lp_also_own_title: "Tvoje lastne spojine", + lp_also_own_body: "Jemlješ nekaj, česar knjižnica nima? Dodaj iz kateregakoli izbirnika. Povsod se obnaša kot vgrajena in nikoli ni našemljena v raziskano.", + lp_also_profiles_title: "Več kot ena oseba", + lp_also_profiles_body: "Ločeni profili poskrbijo, da dva človeka na eni napravi nikoli ne vidita številk drug drugega.", + lp_also_updates_title: "Posodobitve, ki pustijo podatke pri miru", + lp_also_updates_body: "Nova različica vpraša, preden se namesti, migracije pa so preizkušene v obe smeri, tako da se kopija iz katerekoli starejše različice povrne nedotaknjena.", + lp_sources_title: "Od kod so številke", + lp_sources_1: "Najprej odobrena navodila, nato objavljene raziskave, nato registri, in vsak vnos povezuje svoje vire. Praksa skupnosti je vključena tam, kjer je res to, kar ljudje počnejo, in je označena natanko kot taka, da nikoli ne gre za odobreno številko.", + lp_sources_2: "Kjer razpolovna doba pri ljudeh ni bila nikoli izmerjena, aplikacija to pove in ne nariše krivulje, namesto da bi si izmislila tako, ki izgleda verodostojno.", + lp_notwhat_title: "Kaj to ni", + lp_notwhat_1: "Ni zdravniški nasvet, ni recept in ni priporočilo, da karkoli od tega uporabiš. Večina teh spojin ni odobreno zdravilo nikjer, material, prodan za raziskovalno rabo, pa nima za sabo urejene identitete, čistosti ali sterilnosti.", + lp_notwhat_2: "Aplikacija lahko preveri tvoj račun. Ne more preveriti, kaj je v tvoji vialki, in ni nadomestilo za zdravnika, ki pozna tvojo zgodovino.", + lp_close_title: "Nič za prijaviti. Odpri in začni.", + lp_close_body: "Že spremljaš kje drugje? Uvozi svojo zgodovino in aplikacija bo iz vzorca tvojih doz sestavila načrt.", + lp_close_discord: "Pridruži se skupnosti", + lp_footer_built: "Bench v{version}, zgradil {name}", + lp_footer_apk: "Android APK", + lp_footer_icon: "Ikona aplikacije: {author} na {site}.", }, pl: { @@ -5416,6 +5622,73 @@ export const TRANSLATIONS = { now_vial_label: "fiolka {name}", now_all_past_date: "**{name}** ma jeszcze {doses}, a każda z nich jest po terminie.", account_signed_in_on: "Zalogowano jako **{user}** na {url}.", + + lp_meta_title: "Bench, prywatny tracker peptydów, HGH i anabolików", + lp_meta_description: "Prowadź protokoły, dawki, zapas i badania krwi z obliczeniami, którym możesz ufać. Bez konta, bez serwera, wszystko zostaje na twoim urządzeniu, chyba że postawisz własny.", + lp_language: "Język", + lp_nav_about: "O aplikacji", + lp_nav_open: "Otwórz aplikację", + lp_hero_badge: "Bez konta, bez serwera, bez analityki", + lp_hero_title_1: "Policz to dobrze.", + lp_hero_title_2: "Zatrzymaj swoje dane.", + lp_hero_body: "Tracker protokołów peptydowych, hormonu wzrostu i anabolików. Dawki, rekonstytucja, zapas, badania krwi i efekty, wszystko trzymane w przeglądarce na twoim urządzeniu i wysyłane donikąd.", + lp_cta_open: "Otwórz aplikację", + lp_cta_apk: "Pobierz na Androida", + lp_hero_note: "Za darmo, nie ma się do czego zapisywać. Po pierwszym załadowaniu działa offline.", + lp_stat_compounds: "związków, każda liczba ze źródłem", + lp_stat_markers: "markerów krwi na wykresie", + lp_stat_tests: "testów na obliczeniach", + lp_stat_bytes: "bajtów twoich danych opuszcza urządzenie", + lp_stat_users: "użytkowników", + lp_stat_downloads: "pobrań APK", + lp_f_recon_eyebrow: "Rekonstytucja", + lp_f_recon_title: "Rysuje strzykawkę, którą naprawdę trzymasz", + lp_f_recon_body: "Podaj fiolkę i dawkę, a aplikacja powie, ile dodać i dokąd pociągnąć, na cylindrze z prawdziwą podziałką. Cylinder U-40 nigdy nie zostanie po cichu odczytany jako U-100. Ten jeden błąd podaje dwa i pół raza większą dawkę, więc skala jest świadomym wyborem, a odczyt z drugiej stoi tuż obok.", + lp_f_levels_eyebrow: "Poziomy", + lp_f_levels_title: "Zobacz, co krąży, a nie tylko co wziąłeś", + lp_f_levels_body: "Każda zapisana dawka zasila krzywą z opublikowanych okresów półtrwania, więc dolinę widzisz, zanim ją poczujesz. Mieszanki są rozbijane na składniki i każdy modelowany na własnym okresie półtrwania, bo mieszanka uśredniona do jednej linii nie opisuje niczego, co faktycznie jest w tobie.", + lp_f_stock_eyebrow: "Zapas", + lp_f_stock_title: "Wiedz, co zostało i ile kosztowało", + lp_f_stock_body: "Fiolki idą od zaplombowanej przez zrekonstytuowaną do pustej, liczone w masie, nie w objętości, więc dawka pobrana z zaplombowanej fiolki i tak ją uszczupla. Terminy przydatności, pozostałe dawki i koszt dawki są wyliczane, a nie wpisane raz i zapomniane.", + lp_f_labs_eyebrow: "Badania krwi", + lp_f_labs_title: "Wyniki na tej samej osi co dawki", + lp_f_labs_body: "{n} markerów naniesionych na historię dawek, żeby zmiana zgadzała się z tym, co ją wywołało. Aplikacja podpowiada te, które twoje własne związki czynią wartymi obserwacji: lipazę przy GLP-1, hematokryt przy androgenie, enzymy wątrobowe przy doustnym. Zakresy referencyjne pochodzą z twojego wyniku, bo należą do laboratorium, które zmierzyło próbkę.", + lp_f_safety_eyebrow: "Bezpieczeństwo", + lp_f_safety_title: "Ostrzeżenia, których nie nauczyłeś się ignorować", + lp_f_safety_body: "Sprawdzenia biegną przez wszystko aktywne naraz: dwa związki na tym samym receptorze, ten sam związek z dwóch protokołów, dwa doustne sterydy na jednej wątrobie. Opierają się na klasach receptorów, a nie na nazwach, więc świadome połączenie w rodzaju CJC-1295 z ipamoreliną milczy. Ostrzeżenie odklikiwane odruchowo jest gorsze niż żadne.", + lp_f_rotation_eyebrow: "Rotacja", + lp_f_rotation_title: "Miejsce, które miało czas odpocząć", + lp_f_rotation_body: "Przypnij miejsca, których używa protokół, a aplikacja podpowie następne, zostawiając niedawno użytą tkankę w spokoju. Drabinki titracji idą w górę zgodnie z planem, a regularność jest raportowana uczciwie, a nie pochlebnie.", + lp_privacy_badge: "Prywatność", + lp_privacy_title: "Nie ma backendu, z którego coś mogłoby wyciec.", + lp_privacy_1: "Nie ma konta do założenia i nic nie jest wysyłane. Wszystko, co wpiszesz, żyje w twojej przeglądarce, na twoim urządzeniu. Nie prowadzimy serwera, więc na naszym nie ma nic twojego.", + lp_privacy_2: "Jeśli chcesz tych samych danych na telefonie i na laptopie, możesz wskazać aplikacji serwer synchronizacji, który hostujesz sam. Jest wyłączony, dopóki go nie ustawisz, przyjmuje adres, który należy do ciebie, a to, co idzie w górę, jest najpierw szyfrowane w przeglądarce, więc nawet ta maszyna trzyma coś, czego nie umie odczytać.", + lp_privacy_3: "To prawdziwa gwarancja i prawdziwe ryzyko jednym tchem. Nikt nie odczyta twoich danych i nikt ci ich też nie odzyska, dlatego aplikacja przypomina, żeby trzymać wyeksportowaną kopię w bezpiecznym miejscu.", + lp_also_title: "I cała reszta", + lp_also_offline_title: "Działa offline, instaluje się jak aplikacja", + lp_also_offline_body: "Dodaj na ekran główny na iPhonie albo Androidzie, a otworzy się prosto z pamięci podręcznej, bez połączenia.", + lp_also_import_title: "Zabierz historię ze sobą", + lp_also_import_body: "Zaimportuj CSV, TSV, JSON albo arkusz. Shotsy jest rozpoznawany po nazwie, reszta czytana z nagłówków kolumn, a podgląd widzisz, zanim zapisze się choć jeden wiersz.", + lp_also_backup_title: "Kopie, nad którymi masz kontrolę", + lp_also_backup_body: "Wyeksportuj całość do pliku, kiedy chcesz. Na Androidzie aplikacja pisze też rotujące kopie do Dokumentów, które przeżywają wyczyszczenie danych.", + lp_also_own_title: "Twoje własne związki", + lp_also_own_body: "Bierzesz coś, czego nie ma w bibliotece? Dodaj z dowolnego wyboru. Wszędzie zachowuje się jak wbudowany i nigdy nie udaje przebadanego.", + lp_also_profiles_title: "Więcej niż jedna osoba", + lp_also_profiles_body: "Osobne profile sprawiają, że dwie osoby na jednym urządzeniu nigdy nie zobaczą swoich liczb nawzajem.", + lp_also_updates_title: "Aktualizacje, które nie ruszają danych", + lp_also_updates_body: "Nowa wersja pyta, zanim się zainstaluje, a migracje są testowane w obie strony, więc kopia z dowolnej starszej wersji wraca w całości.", + lp_sources_title: "Skąd są te liczby", + lp_sources_1: "Najpierw charakterystyki produktów, potem opublikowane badania, potem rejestry, a każdy wpis linkuje swoje źródła. Praktyka społeczności jest uwzględniona tam, gdzie naprawdę tak się robi, i jest oznaczona dokładnie jako taka, żeby nigdy nie uchodziła za liczbę zatwierdzoną.", + lp_sources_2: "Tam, gdzie okresu półtrwania nigdy nie zmierzono u ludzi, aplikacja to mówi i nie rysuje krzywej, zamiast wymyślać taką, która wygląda wiarygodnie.", + lp_notwhat_title: "Czym to nie jest", + lp_notwhat_1: "To nie porada medyczna, nie recepta i nie zachęta, żeby cokolwiek z tego brać. Większość tych związków nigdzie nie jest dopuszczonym lekiem, a materiał sprzedawany do celów badawczych nie ma za sobą żadnej regulowanej tożsamości, czystości ani jałowości.", + lp_notwhat_2: "Aplikacja sprawdzi twoje obliczenia. Nie sprawdzi, co jest w fiolce, i nie zastąpi lekarza, który zna twoją historię.", + lp_close_title: "Nie ma się do czego zapisywać. Otwórz i zacznij.", + lp_close_body: "Prowadzisz to już gdzie indziej? Zaimportuj historię, a aplikacja zbuduje protokół z wzoru twoich dawek.", + lp_close_discord: "Dołącz do społeczności", + lp_footer_built: "Bench v{version}, zbudował {name}", + lp_footer_apk: "APK na Androida", + lp_footer_icon: "Ikona aplikacji: {author} na {site}.", }, } satisfies Record>;