From 3626f833ba5e01302177ce29bc30c13765e9c462 Mon Sep 17 00:00:00 2001 From: Dan Anderson Date: Mon, 7 Sep 2026 14:16:56 -0400 Subject: [PATCH 01/10] Fix localization amount safety, currency preferences, and routes --- apps/web/scripts/i18n-catalog.mjs | 69 +++++ apps/web/scripts/i18n-derive-zh.mjs | 19 +- apps/web/scripts/i18n-draft.mjs | 2 + apps/web/scripts/i18n-extract.mjs | 27 +- .../[asset]/_components/launch-stats.tsx | 12 +- .../app/[lang]/_components/market-pulse.tsx | 2 +- apps/web/src/app/[lang]/create/layout.tsx | 26 ++ apps/web/src/app/[lang]/create/page.tsx | 1 + .../[lang]/dispense/_components/bridge.tsx | 1 + apps/web/src/app/[lang]/home/page.tsx | 6 +- apps/web/src/app/[lang]/launches/page.tsx | 6 +- apps/web/src/app/[lang]/layout.tsx | 7 +- .../src/app/[lang]/profile/[address]/page.tsx | 17 +- .../[lang]/profile/_components/orders-tab.tsx | 5 +- .../[lang]/swap/_components/swap-settings.tsx | 3 +- apps/web/src/app/sitemap.ts | 14 +- .../web/src/components/address-hover-card.tsx | 5 +- apps/web/src/components/amount-input.tsx | 75 +++--- apps/web/src/components/language-switch.tsx | 8 +- apps/web/src/components/site-footer.tsx | 10 +- apps/web/src/lib/currency.ts | 159 +++++++----- apps/web/src/lib/format.ts | 5 +- apps/web/src/locales/_source.json | 35 ++- apps/web/src/locales/es.json | 14 +- apps/web/src/locales/es.status.json | 18 +- apps/web/src/locales/fr.json | 14 +- apps/web/src/locales/fr.status.json | 18 +- apps/web/src/locales/ja.json | 14 +- apps/web/src/locales/ja.status.json | 18 +- apps/web/src/locales/ko.json | 14 +- apps/web/src/locales/ko.status.json | 18 +- apps/web/src/locales/notes.json | 12 +- apps/web/src/locales/pt.json | 14 +- apps/web/src/locales/pt.status.json | 18 +- apps/web/src/locales/ru.json | 14 +- apps/web/src/locales/ru.status.json | 17 +- apps/web/src/locales/uk.json | 14 +- apps/web/src/locales/uk.status.json | 17 +- apps/web/src/locales/zh-hk.json | 14 +- apps/web/src/locales/zh-hk.status.json | 17 +- apps/web/src/locales/zh-tw.json | 14 +- apps/web/src/locales/zh-tw.status.json | 17 +- apps/web/src/locales/zh.json | 14 +- apps/web/src/locales/zh.status.json | 18 +- tests/compose-amount-safety.test.ts | 195 ++++++++------ tests/currency.test.ts | 242 ++++++++++++++++++ tests/i18n-catalog.test.ts | 60 +++++ tests/i18n-rich.test.tsx | 15 ++ tests/localized-routes.test.ts | 93 +++++++ tests/number-locale.test.ts | 45 +--- tests/trade-settings.test.tsx | 41 +++ vitest.config.mts | 2 +- 52 files changed, 1138 insertions(+), 397 deletions(-) create mode 100644 apps/web/scripts/i18n-catalog.mjs create mode 100644 apps/web/src/app/[lang]/create/layout.tsx create mode 100644 tests/currency.test.ts create mode 100644 tests/i18n-catalog.test.ts create mode 100644 tests/localized-routes.test.ts create mode 100644 tests/trade-settings.test.tsx diff --git a/apps/web/scripts/i18n-catalog.mjs b/apps/web/scripts/i18n-catalog.mjs new file mode 100644 index 00000000..a04863cc --- /dev/null +++ b/apps/web/scripts/i18n-catalog.mjs @@ -0,0 +1,69 @@ +import ts from "typescript"; + +/** Static alternatives at a translation call, including singular/plural + * branches. Reading syntax also decodes escapes and ignores comments. */ +function literals(node) { + if (!node) return []; + if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return [node.text]; + if (ts.isConditionalExpression(node)) return [...literals(node.whenTrue), ...literals(node.whenFalse)]; + if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node)) { + return literals(node.expression); + } + return []; +} + +export function collectMessages(source, filename = "source.tsx") { + const file = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true, + filename.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS); + const keys = new Set(); + const indirect = []; + function visit(node) { + if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) { + const name = node.expression.text; + const args = node.arguments; + const isRich = name === "rich" && args[0] && ts.isIdentifier(args[0]) && args[0].text === "t"; + if (name === "t" || name === "msg" || isRich) { + const text = args[isRich ? 1 : 0]; + const values = literals(text); + // t(text, "context"), t(text, vars, "context"), or + // rich(t, text, nodes, "context") match the runtime API. + const contexts = isRich ? literals(args[3]) : name === "t" + ? (literals(args[1]).length ? literals(args[1]) : literals(args[2])) : []; + for (const value of values) { + if (!value.trim()) continue; + for (const context of contexts.length ? contexts : [undefined]) { + keys.add(context ? `${value}@@${context}` : value); + } + } + if (name === "t" && text && !values.length) indirect.push(text.getText(file)); + } + } + ts.forEachChild(node, visit); + } + visit(file); + return { keys: [...keys], indirect }; +} + +/** A reviewer removes a key from status.machine after checking it. Never + * replace that regional wording, or guess that an untracked edit is a draft. + * Keep regional-only entries too; extraction reports obsolete keys for review. */ +export function mergeDerivedMessages(derived, existing = {}, status = { machine: [] }) { + const previousMachine = new Set(status.machine ?? []); + const machine = new Set(previousMachine); + const messages = { ...existing }; + for (const [key, value] of Object.entries(derived)) { + if (Object.hasOwn(existing, key) && !machine.has(key)) continue; + messages[key] = value; + machine.add(key); + } + return { + messages: Object.fromEntries(Object.entries(messages).sort(([a], [b]) => a.localeCompare(b))), + status: { + ...status, + machine: [ + ...[...previousMachine].filter((key) => Object.hasOwn(messages, key)), + ...[...machine].filter((key) => !previousMachine.has(key)).sort(), + ], + }, + }; +} diff --git a/apps/web/scripts/i18n-derive-zh.mjs b/apps/web/scripts/i18n-derive-zh.mjs index 7bd6ab31..c4ed3aed 100644 --- a/apps/web/scripts/i18n-derive-zh.mjs +++ b/apps/web/scripts/i18n-derive-zh.mjs @@ -2,9 +2,14 @@ // Derive zh-tw and zh-hk from the Simplified draft with OpenCC's regional // phrase tables, then apply the glossary's own renderings where OpenCC // disagrees, plus a few known OpenCC quirks. Writes both locale files and -// their status lists. -import { readFileSync, writeFileSync } from "node:fs"; +// their status lists. Reviewed regional wording is kept: a reviewer removes +// its key from status.machine, as in the other translation tools. +import { existsSync, readFileSync, writeFileSync } from "node:fs"; import * as OpenCC from "opencc-js"; +// eslint-disable-next-line no-restricted-imports -- Node scripts cannot resolve the app's @/ alias. +import { mergeDerivedMessages } from "./i18n-catalog.mjs"; + +const read = (path, fallback) => existsSync(path) ? JSON.parse(readFileSync(path, "utf8")) : fallback; const zh = JSON.parse(readFileSync("src/locales/zh.json", "utf8")); const glossary = JSON.parse(readFileSync("src/locales/glossary.json", "utf8")).terms; @@ -47,8 +52,10 @@ for (const [variant, to] of [["zh-tw", "twp"], ["zh-hk", "hk"]]) { for (const [from, to2] of [...QUIRKS[variant], ...overrides]) s = s.split(from).join(to2); out[key] = s; } - const sorted = Object.fromEntries(Object.entries(out).sort(([a], [b]) => a.localeCompare(b))); - writeFileSync(`src/locales/${variant}.json`, JSON.stringify(sorted, null, 2) + "\n"); - writeFileSync(`src/locales/${variant}.status.json`, JSON.stringify({ machine: Object.keys(sorted) }, null, 2) + "\n"); - console.log(variant, Object.keys(sorted).length, "entries; glossary overrides:", overrides.map(([a, b]) => `${a}→${b}`).join(" ")); + const messagesPath = `src/locales/${variant}.json`; + const statusPath = `src/locales/${variant}.status.json`; + const merged = mergeDerivedMessages(out, read(messagesPath, {}), read(statusPath, { machine: [] })); + writeFileSync(messagesPath, JSON.stringify(merged.messages, null, 2) + "\n"); + writeFileSync(statusPath, JSON.stringify(merged.status, null, 2) + "\n"); + console.log(variant, Object.keys(merged.messages).length, "entries; glossary overrides:", overrides.map(([a, b]) => `${a}→${b}`).join(" ")); } diff --git a/apps/web/scripts/i18n-draft.mjs b/apps/web/scripts/i18n-draft.mjs index d35e178f..e06fbabd 100644 --- a/apps/web/scripts/i18n-draft.mjs +++ b/apps/web/scripts/i18n-draft.mjs @@ -29,6 +29,8 @@ const LANGUAGE = { "zh-hk": "Traditional Chinese (Hong Kong)", es: "Spanish", fr: "French", + ru: "Russian", + uk: "Ukrainian", pt: "Brazilian Portuguese", de: "German", it: "Italian", diff --git a/apps/web/scripts/i18n-extract.mjs b/apps/web/scripts/i18n-extract.mjs index 4b3b0d47..4933393b 100644 --- a/apps/web/scripts/i18n-extract.mjs +++ b/apps/web/scripts/i18n-extract.mjs @@ -9,16 +9,13 @@ // node scripts/i18n-extract.mjs --check # exit 1 if a locale has gaps import { readdirSync, readFileSync, statSync, writeFileSync, existsSync } from "node:fs"; import { join, relative } from "node:path"; +// eslint-disable-next-line no-restricted-imports -- Node scripts cannot resolve the app's @/ alias. +import { collectMessages } from "./i18n-catalog.mjs"; const ROOT = new URL("../src", import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1"); const LOCALES_DIR = join(ROOT, "locales"); const check = process.argv.includes("--check"); -// t("text"), t('text'), t(`text`) with no ${}, optionally followed by -// `, {vars}` or `, "context"`. Multi-line calls are matched because the -// argument is the first thing after the paren. -const CALL = /\b(?:t\(|msg\(|rich\(\s*t\s*,)\s*(?:"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'|`([^`$]*)`)\s*(?:,\s*(?:"([^"]*)"|'([^']*)'))?/g; - function* files(dir) { for (const name of readdirSync(dir)) { const path = join(dir, name); @@ -31,17 +28,17 @@ function* files(dir) { } const found = new Map(); // key -> [files] +const indirect = []; for (const file of files(ROOT)) { const src = readFileSync(file, "utf8"); - for (const m of src.matchAll(CALL)) { - const text = (m[1] ?? m[2] ?? m[3]).replace(/\\(["'`])/g, "$1"); - const context = m[4] ?? m[5]; - if (!text.trim()) continue; - const key = context ? `${text}@@${context}` : text; + const rel = relative(ROOT, file).replace(/\\/g, "/"); + const messages = collectMessages(src, file); + for (const key of messages.keys) { const list = found.get(key) ?? []; - list.push(relative(ROOT, file).replace(/\\/g, "/")); + list.push(rel); found.set(key, list); } + for (const value of messages.indirect) indirect.push(`${rel}: t(${value})`); } const keys = [...found.keys()].sort((a, b) => a.localeCompare(b)); @@ -57,14 +54,8 @@ console.log(`source: ${keys.length} strings (${keys.length - previous >= 0 ? "+" // A t(x) whose argument is not a literal is translating a string defined // elsewhere — fine when that definition is wrapped in msg(), invisible to // this scan when it is not. List them so a bare label array cannot hide. -const INDIRECT = /\bt\(\s*([A-Za-z_$][\w$.]*)\s*\)/g; -const indirect = []; -for (const file of files(ROOT)) { - const src = readFileSync(file, "utf8"); - for (const m of src.matchAll(INDIRECT)) indirect.push(`${relative(ROOT, file).replace(/\\/g, "/")}: t(${m[1]})`); -} if (indirect.length && !check) { - console.log(`indirect: ${indirect.length} t(identifier) calls — their labels must be msg()-marked where defined:`); + console.log(`indirect: ${indirect.length} t(expression) calls — their labels must be msg()-marked where defined:`); for (const line of indirect) console.log(` ${line}`); } diff --git a/apps/web/src/app/[lang]/[asset]/_components/launch-stats.tsx b/apps/web/src/app/[lang]/[asset]/_components/launch-stats.tsx index 7ba11a43..f3f8a7cc 100644 --- a/apps/web/src/app/[lang]/[asset]/_components/launch-stats.tsx +++ b/apps/web/src/app/[lang]/[asset]/_components/launch-stats.tsx @@ -5,7 +5,7 @@ import { fetchJson } from "@/lib/client"; import { useDenomination, setDenomination } from "@/lib/denomination"; import { fromSats } from "@/lib/format"; import type { Raw } from "@/lib/numeric"; -import { useFiat } from "@/lib/currency"; +import { useFiat, useFxRate } from "@/lib/currency"; import { useT } from "@/lib/i18n/client"; import { useNumbers } from "@/lib/i18n/numbers"; import { @@ -22,6 +22,7 @@ export function TermsStrip({ xcpUsd }: { xcpUsd: number | null }) { const num = useNumbers(); const t = useT(); const usd = useFiat(); + const { code } = useFxRate(); const denom = useDenomination(); const usdMode = denom === "USD" && !!xcpUsd; const rate = xcpUsd ?? 0; @@ -88,12 +89,12 @@ export function TermsStrip({ xcpUsd }: { xcpUsd: number | null }) { )} @@ -174,18 +175,19 @@ export function ParticipantsStat({ participants }: { participants: number }) { * their own rate's availability); this component only handles placement. */ export function DenomToggle({ visibleOn }: { visibleOn: "mobile" | "desktop" }) { const t = useT(); + const { code } = useFxRate(); const denom = useDenomination(); const usdMode = denom === "USD"; return ( ); } diff --git a/apps/web/src/app/[lang]/_components/market-pulse.tsx b/apps/web/src/app/[lang]/_components/market-pulse.tsx index 4680c277..3847517c 100644 --- a/apps/web/src/app/[lang]/_components/market-pulse.tsx +++ b/apps/web/src/app/[lang]/_components/market-pulse.tsx @@ -309,7 +309,7 @@ function MarketModal({ {isBtc ? t("Bitcoin Price") : t("XCP Price")} - {isBtc ? t("Bitcoin market price in USD") : t("Counterparty market price in USD")} + {isBtc ? t("Bitcoin market price in {currency}", { currency: code }) : t("Counterparty market price in {currency}", { currency: code })}
diff --git a/apps/web/src/app/[lang]/create/layout.tsx b/apps/web/src/app/[lang]/create/layout.tsx new file mode 100644 index 00000000..01ac69f1 --- /dev/null +++ b/apps/web/src/app/[lang]/create/layout.tsx @@ -0,0 +1,26 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; +import { isLocale } from "@/lib/i18n/locales"; +import { localeAlternates } from "@/lib/i18n/seo"; +import { getMessages } from "@/lib/i18n/server"; +import { makeT } from "@/lib/i18n/t"; + +// The form is a Client Component; its route metadata stays on the server. +export async function generateMetadata({ + params, +}: { + params: Promise<{ lang: string }>; +}): Promise { + const { lang } = await params; + const locale = isLocale(lang) ? lang : "en"; + const t = makeT(await getMessages(locale)); + return { + title: `${t("Launch a token")} — xcp.fun`, + description: t("Name, image, description. Everything else is the standard."), + alternates: localeAlternates(locale, "/create"), + }; +} + +export default function CreateLayout({ children }: { children: ReactNode }) { + return children; +} diff --git a/apps/web/src/app/[lang]/create/page.tsx b/apps/web/src/app/[lang]/create/page.tsx index 66c9d1a3..9ced4763 100644 --- a/apps/web/src/app/[lang]/create/page.tsx +++ b/apps/web/src/app/[lang]/create/page.tsx @@ -976,6 +976,7 @@ function ScheduleGear({ > }) { + const { lang } = await params; + redirect(localePath(isLocale(lang) ? lang : "en", "/profile")); } diff --git a/apps/web/src/app/[lang]/launches/page.tsx b/apps/web/src/app/[lang]/launches/page.tsx index c002f99a..65d93ddf 100644 --- a/apps/web/src/app/[lang]/launches/page.tsx +++ b/apps/web/src/app/[lang]/launches/page.tsx @@ -1,6 +1,8 @@ import { redirect } from "next/navigation"; +import { isLocale, localePath } from "@/lib/i18n/locales"; /** The launches list is a tab on the profile now; the old URL still works. */ -export default function LaunchesPage() { - redirect("/profile"); +export default async function LaunchesPage({ params }: { params: Promise<{ lang: string }> }) { + const { lang } = await params; + redirect(localePath(isLocale(lang) ? lang : "en", "/profile")); } diff --git a/apps/web/src/app/[lang]/layout.tsx b/apps/web/src/app/[lang]/layout.tsx index 4e2d0833..f4ffa04c 100644 --- a/apps/web/src/app/[lang]/layout.tsx +++ b/apps/web/src/app/[lang]/layout.tsx @@ -11,7 +11,7 @@ import { SessionProvider } from "@/providers/session-context"; import { SwrProvider } from "@/providers/swr-provider"; import { LocaleProvider } from "@/lib/i18n/client"; import { isLocale, LOCALE_INFO, LOCALES } from "@/lib/i18n/locales"; -import { localeAlternates, openGraphLocale } from "@/lib/i18n/seo"; +import { openGraphLocale } from "@/lib/i18n/seo"; import { getMessages, isMachineDrafted } from "@/lib/i18n/server"; import { makeT } from "@/lib/i18n/t"; import { METADATA_ORIGIN } from "@/lib/metadata"; @@ -42,9 +42,8 @@ export async function generateMetadata({ description: t( "Trustless token launches on Counterparty. All-or-nothing mints, liquidity locked by consensus, no platform custody.", ), - // Pages that name their own path override this with their own set; - // this is the homepage's, and the fallback for anything that does not. - alternates: localeAlternates(locale, "/"), + // Canonical and language alternatives belong to each page. A homepage + // fallback here would incorrectly identify any page without its own. // No title or description here on purpose: a page's own then flow into // og:title and og:description, while the site-level facts are inherited. openGraph: { type: "website", siteName: "xcp.fun", ...openGraphLocale(locale) }, diff --git a/apps/web/src/app/[lang]/profile/[address]/page.tsx b/apps/web/src/app/[lang]/profile/[address]/page.tsx index bfdca038..a95425fb 100644 --- a/apps/web/src/app/[lang]/profile/[address]/page.tsx +++ b/apps/web/src/app/[lang]/profile/[address]/page.tsx @@ -1,17 +1,26 @@ +import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { shortAddress } from "@/lib/format"; +import { isLocale } from "@/lib/i18n/locales"; +import { localeAlternates } from "@/lib/i18n/seo"; +import { getMessages } from "@/lib/i18n/server"; +import { makeT } from "@/lib/i18n/t"; import { BTC_ADDRESS_REGEX } from "@xcp/wallet-sdk"; import { ProfileView } from "@/app/[lang]/profile/_components/profile-view"; export async function generateMetadata({ params, }: { - params: Promise<{ address: string }>; -}) { - const { address } = await params; + params: Promise<{ address: string; lang: string }>; +}): Promise { + const { address, lang } = await params; + if (!BTC_ADDRESS_REGEX.test(address)) notFound(); + const locale = isLocale(lang) ? lang : "en"; + const t = makeT(await getMessages(locale)); return { title: `${shortAddress(address)} — xcp.fun`, - description: `Positions, open mints, history, and launches for ${address} on xcp.fun.`, + description: t("Positions, open mints, history, and launches for {address} on xcp.fun.", { address }), + alternates: localeAlternates(locale, `/profile/${address}`), }; } diff --git a/apps/web/src/app/[lang]/profile/_components/orders-tab.tsx b/apps/web/src/app/[lang]/profile/_components/orders-tab.tsx index d328d8a1..7b4016fb 100644 --- a/apps/web/src/app/[lang]/profile/_components/orders-tab.tsx +++ b/apps/web/src/app/[lang]/profile/_components/orders-tab.tsx @@ -1,6 +1,7 @@ "use client"; import useSWR from "swr"; +import { LazyLink } from "@/components/lazy-link"; import { COUNTERPARTY_API_BASE } from "@/lib/constants"; import { fetchJson } from "@/lib/client"; import { isBusy } from "@/hooks/use-busy"; @@ -80,9 +81,9 @@ export function OrdersTab({ {buying ? t("Buy") : t("Sell")} {" "} - + {token} - {" "} + {" "} {num.commasRaw(tokens)} @ {num.price(ratio(xcp, tokens))} ) : ( diff --git a/apps/web/src/app/[lang]/swap/_components/swap-settings.tsx b/apps/web/src/app/[lang]/swap/_components/swap-settings.tsx index eca51f8f..065db834 100644 --- a/apps/web/src/app/[lang]/swap/_components/swap-settings.tsx +++ b/apps/web/src/app/[lang]/swap/_components/swap-settings.tsx @@ -117,7 +117,7 @@ export function SwapSettingsProvider({ children }: { children: ReactNode }) { 5000, Math.max(1, Math.round(parseFloat(customExpiration)) || MARKET_EXPIRATION), ); - const customFee = Math.min(Math.round(parseFloat(customFeeRate) || 0), 500); + const customFee = Math.min(parseFloat(customFeeRate) || 0, 500); const lqCustomSlip = Math.min(parseFloat(lqCustomSlippage) || 0, 50); const lqSlippage = lqCustomSlip > 0 ? lqCustomSlip : lqSlippagePreset; return { @@ -254,6 +254,7 @@ export function SwapSettingsGear() { > [0], path: string) => { return `${METADATA_ORIGIN}${p === "/" ? "" : p}`; }; -function entry(path: string, priority: number): MetadataRoute.Sitemap[number] { +function entries(path: string, priority: number): MetadataRoute.Sitemap { const languages: Record = {}; for (const l of LOCALES) languages[LOCALE_INFO[l].tag] = absolute(l, path); languages["x-default"] = absolute("en", path); - return { - url: absolute("en", path), + return LOCALES.map((locale) => ({ + url: absolute(locale, path), priority, alternates: { languages }, - }; + })); } export default async function sitemap(): Promise { - const entries = STATIC_PATHS.map((p) => entry(p, p === "/" ? 1 : 0.6)); + const urls = STATIC_PATHS.flatMap((p) => entries(p, p === "/" ? 1 : 0.6)); // Every conforming launch, whatever its phase: a refunded launch's page is // still its record. The index is the site's own, so an outage here means a // shorter sitemap rather than a failed one. try { const index = await fetchSearchIndex(); - for (const row of index ?? []) entries.push(entry(`/${row.asset}`, 0.8)); + for (const row of index ?? []) urls.push(...entries(`/${row.asset}`, 0.8)); } catch { // Static pages only, this time. } - return entries; + return urls; } diff --git a/apps/web/src/components/address-hover-card.tsx b/apps/web/src/components/address-hover-card.tsx index 7e8722e4..6ce23082 100644 --- a/apps/web/src/components/address-hover-card.tsx +++ b/apps/web/src/components/address-hover-card.tsx @@ -185,7 +185,7 @@ export function IssuerChips({ ? t("first launch") : data.priorCapped ? t("{n}+ launches", { n: num.commas(data.prior) }) - : t("{ordinal} launch", { ordinal: ordinal(data.prior + 1) })} + : t("Launch #{n}", { n: num.commas(data.prior + 1) })} , ]; if (data.judged > 0) { @@ -240,9 +240,6 @@ export function IssuerChips({ /* ---------- issuer identity ---------- */ -const ordinal = (n: number) => - `${n}${["th", "st", "nd", "rd"][n % 100 > 10 && n % 100 < 14 ? 0 : Math.min(n % 10, 4) % 4] ?? "th"}`; - interface Reputation { track_record?: { score?: number; tier?: string }; } diff --git a/apps/web/src/components/amount-input.tsx b/apps/web/src/components/amount-input.tsx index 85b6223f..351f8085 100644 --- a/apps/web/src/components/amount-input.tsx +++ b/apps/web/src/components/amount-input.tsx @@ -4,50 +4,18 @@ * The industry-consensus money input (every major DEX ships this shape): * type="text" + inputMode="decimal" — never type="number", which accepts * e/+/-, renders spinners, and blanks invalid intermediate states. State is - * the raw string so trailing "5." survives; invalid keystrokes are rejected - * before entering state so the cursor never jumps. State is always a period - * decimal, whatever the page's language writes — the field is a machine - * amount on its way to a bigint, not a display. + * the raw string so trailing "5." survives. Every locale uses Counterparty + * notation here: ASCII digits and at most eight places after a period. + * Reject unsupported input instead of removing characters or guessing at + * grouping: a valid-looking result can represent a different amount. */ -const AMOUNT_REGEX = /^\d*\.?\d*$/; +const AMOUNT_REGEX = /^\d*(?:\.\d{0,8})?$/; +const MAX_AMOUNT_LENGTH = 26; -/** A whole number written with group separators, in either convention. */ -const GROUPED = /^\d{1,3}(?:\.\d{3})+$|^\d{1,3}(?:,\d{3})+$/; - -/** - * Which of "." and "," the writer meant as the decimal mark. - * - * A person on a Portuguese or French page types "0,5", and pastes figures - * grouped the way their language groups them: "1.234,56" there, "1,234.56" - * here. Both marks appear in both roles, so the string decides rather than - * the locale — the LAST of the two distinct marks is the decimal one and - * anything before it groups. A mark that repeats can only be grouping, and - * a lone mark is the decimal, which is what lets a typed "0," become "0." - * mid-keystroke. - */ -export function normalizeMarks(raw: string): string { - const lastDot = raw.lastIndexOf("."); - const lastComma = raw.lastIndexOf(","); - if (lastDot >= 0 && lastComma >= 0) { - const dotIsDecimal = lastDot > lastComma; - const group = dotIsDecimal ? /,/g : /\./g; - const at = dotIsDecimal ? lastDot : lastComma; - return `${raw.slice(0, at).replace(group, "")}.${raw.slice(at + 1).replace(group, "")}`; - } - // The same mark twice reads as grouping, but only when the groups are - // actually groups — "1.234.567" is a pasted number, "1.2.3" is a slip of - // the finger and falls through to the cleanup below. - if (GROUPED.test(raw)) return raw.replace(/[.,]/g, ""); - return raw.replace(/,/g, "."); -} - -export function sanitizeAmount(raw: string): string | null { - const s = normalizeMarks(raw); - if (s === "" || AMOUNT_REGEX.test(s)) return s === "." ? "0." : s; - // Paste path: strip whatever is left that is neither digit nor mark. - const cleaned = s.replace(/[^\d.]/g, "").replace(/(\..*)\./g, "$1"); - if (cleaned !== "" && AMOUNT_REGEX.test(cleaned)) return cleaned; - return null; +/** Raw satoshi fields use zero places; token-unit fields use eight. */ +export function sanitizeAmount(raw: string, decimals: 0 | 8 = 8): string | null { + if (raw.length > MAX_AMOUNT_LENGTH || !(decimals === 0 ? /^\d*$/ : AMOUNT_REGEX).test(raw)) return null; + return raw === "." ? "0." : raw; } export function AmountInput({ @@ -59,6 +27,7 @@ export function AmountInput({ className, style, disabled, + decimals = 8, }: { value: string; onChange: (value: string) => void; @@ -69,21 +38,37 @@ export function AmountInput({ /** For transient visual states (e.g. graying a derived value while stale). */ style?: React.CSSProperties; disabled?: boolean; + decimals?: 0 | 8; }) { return ( { - const next = sanitizeAmount(e.target.value); + // A regional decimal keyboard can emit a comma. Translate that one + // keystroke to our period notation; pasted/dropped text must already + // use the plain format, since "1,234" could be a grouped integer. + const event = e.nativeEvent as InputEvent; + const decimalKey = event.inputType === "insertText" && event.data === ","; + const raw = decimalKey ? e.target.value.replace(",", ".") : e.target.value; + // Validate length here too. Native maxLength truncates pasted text + // before onChange, potentially removing significant digits. + const next = sanitizeAmount(raw, decimals); if (next !== null) onChange(next); }} + onPaste={(e) => { + // Text inputs strip line breaks before onChange. Inspect the original + // clipboard text so "1\n234" cannot silently become "1234". + if (sanitizeAmount(e.clipboardData.getData("text"), decimals) === null) e.preventDefault(); + }} + onDrop={(e) => { + if (sanitizeAmount(e.dataTransfer.getData("text"), decimals) === null) e.preventDefault(); + }} placeholder={placeholder} aria-label={ariaLabel} className={className} diff --git a/apps/web/src/components/language-switch.tsx b/apps/web/src/components/language-switch.tsx index 3f756c2f..42177711 100644 --- a/apps/web/src/components/language-switch.tsx +++ b/apps/web/src/components/language-switch.tsx @@ -36,10 +36,10 @@ export const MENU_ITEM = * flags are countries, and Chinese alone spans three. Choosing navigates to * the same path under the other prefix, so nothing about the page is lost. * - * Currency rides along because the two are one decision to most people: - * the language you read in sets the currency you see, unless you say - * otherwise (see lib/currency). It is a submenu rather than a second list - * so thirty currencies never swallow two languages. + * Currency has its own choice: Auto uses the browser's regional preference, + * and an explicit currency stays selected when the page language changes + * (see lib/currency). It is a submenu rather than a second list so thirty + * currencies never swallow the language choices. */ export function LanguageSwitch({ compact = false }: { compact?: boolean }) { const t = useT(); diff --git a/apps/web/src/components/site-footer.tsx b/apps/web/src/components/site-footer.tsx index 2a9a7fc5..6b1ba5ce 100644 --- a/apps/web/src/components/site-footer.tsx +++ b/apps/web/src/components/site-footer.tsx @@ -20,6 +20,7 @@ import { LOCALE_INFO, LOCALES, type Locale, localePath, splitLocale } from "@/li */ export function SiteFooter() { const t = useT(); + const pathname = usePathname(); const machine = useMachineDrafted(); const { code, date } = useCurrency(); return ( @@ -52,7 +53,14 @@ export function SiteFooter() { {" · "} {t("Translated automatically")} {" · "} - + rememberLocale("en")} + className="underline underline-offset-2" + > English diff --git a/apps/web/src/lib/currency.ts b/apps/web/src/lib/currency.ts index c0556ab4..a8c6a49d 100644 --- a/apps/web/src/lib/currency.ts +++ b/apps/web/src/lib/currency.ts @@ -5,7 +5,6 @@ import { trackEvent } from "@/lib/analytics"; import { fetchFxRates } from "@/lib/api/launchpad-api"; import { fiat } from "@/lib/format"; import { useLocale } from "@/lib/i18n/client"; -import { type Locale, splitLocale } from "@/lib/i18n/locales"; /** * Which currency the site's fiat figures are shown in. @@ -81,9 +80,9 @@ export interface CurrencyState { const PREF_KEY = "xcpfun:currency:v1"; const FX_KEY = "xcpfun:fx:v1"; -const EVENT = "xcpfun:currency"; /** The ECB publishes once a business day; twice a day is plenty. */ const FX_MAX_AGE_MS = 12 * 60 * 60 * 1000; +const FX_RETRY_MS = 60 * 1000; const SERVER: CurrencyState = { code: "USD", @@ -98,28 +97,11 @@ function isCurrency(value: unknown): value is Currency { } /** - * What the page and the browser imply: Japan, Hong Kong, Korea, Brazil, - * and the euro for French pages. - * - * Dollars are the default for everyone. The markets the site goes out of - * its way to meet in their own currency are the ones it speaks the language - * of AND whose currency the ECB quotes: yen for Japan, Hong Kong dollars for - * Hong Kong, won for Korea, reais for Brazil. French pages default to euros - * because France and Belgium are most of their readers; a Swiss or Québécois - * reader picks CHF or CAD from the menu. Spanish readers span a dozen - * currencies, so they get dollars, which Latin America prices in anyway. Taiwan reads its own - * page but keeps US dollars — the ECB does - * not quote TWD, and Taiwanese traders price in USDT anyway — and Simplified - * Chinese readers are scattered across the mainland, Singapore and Malaysia, - * where no single currency is right, so they get dollars too. First the page - * itself: a visitor reading the Japanese site sees yen, because the language - * you read in and the currency you think in are one decision to most people. - * Then the browser's language — `maximize()` fills in the likely region for - * a bare "ja", so it resolves like "ja-JP" — and the timezone, which says - * where the machine thinks it is. Any one is enough, and an explicit currency - * choice in the menu overrides all of them. + * Auto uses the browser's language and timezone, with dollars as the + * fallback. The page's language is independent: reading a translation + * never changes the currency. An explicit choice, including USD, overrides + * detection. `maximize()` gives a bare browser tag such as "ja" its region. */ -const PAGE_CURRENCY: Partial> = { ja: "JPY", "zh-hk": "HKD", ko: "KRW", pt: "BRL", fr: "EUR" }; const REGION_CURRENCY: Record = { JP: "JPY", HK: "HKD", MO: "HKD", KR: "KRW", BR: "BRL", FR: "EUR", BE: "EUR", LU: "EUR", MC: "EUR" }; const TIMEZONE_CURRENCY: Record = { "Asia/Tokyo": "JPY", @@ -134,10 +116,6 @@ const TIMEZONE_CURRENCY: Record = { function detect(): Currency { if (typeof navigator === "undefined") return "USD"; - if (typeof location !== "undefined") { - const fromPage = PAGE_CURRENCY[splitLocale(location.pathname).locale]; - if (fromPage) return fromPage; - } const tags = navigator.languages?.length ? navigator.languages : [navigator.language]; for (const tag of tags) { try { @@ -161,13 +139,17 @@ function detect(): Currency { return "USD"; } +let preference: Currency | null | undefined; + function readPreference(): Currency | null { + if (preference !== undefined) return preference; try { const stored = localStorage.getItem(PREF_KEY); - return isCurrency(stored) ? stored : null; + preference = isCurrency(stored) ? stored : null; } catch { - return null; + preference = null; } + return preference; } interface StoredFx { @@ -176,16 +158,20 @@ interface StoredFx { rates: Record; } -function readFx(): StoredFx | null { +let fx: StoredFx | null | undefined; + +function parseFx(raw: string | null): StoredFx | null { try { - const raw = localStorage.getItem(FX_KEY); if (!raw) return null; const parsed = JSON.parse(raw) as Partial; if ( typeof parsed.fetchedAt !== "number" || + !Number.isFinite(parsed.fetchedAt) || + parsed.fetchedAt > Date.now() || typeof parsed.date !== "string" || typeof parsed.rates !== "object" || - parsed.rates === null + parsed.rates === null || + Array.isArray(parsed.rates) ) { return null; } @@ -195,8 +181,21 @@ function readFx(): StoredFx | null { } } +function readFx(): StoredFx | null { + if (fx !== undefined) return fx; + try { + fx = parseFx(localStorage.getItem(FX_KEY)); + } catch { + fx = null; + } + return fx; +} + let snapshot: CurrencyState | null = null; let fetching = false; +let retryAt = 0; +let refreshTimer: ReturnType | null = null; +const subscribers = new Set<() => void>(); function compute(): CurrencyState { const preference = readPreference(); @@ -205,54 +204,68 @@ function compute(): CurrencyState { if (code === "USD") return { code, auto: preference === null, detected, rate: 1, date: null }; const fx = readFx(); const rate = fx?.rates[code]; + const usableRate = typeof rate === "number" && Number.isFinite(rate) && rate > 0 ? rate : null; return { code, auto: preference === null, detected, - rate: typeof rate === "number" && rate > 0 ? rate : null, - date: fx?.date ?? null, + rate: usableRate, + date: usableRate === null ? null : fx?.date ?? null, }; } -/** Fetch the table when a non-dollar currency has none, or one older than a - * trading day. Never for dollars, and never twice for one table: a fresh - * table that lacks the chosen code leaves that code in dollars until the - * next refresh, rather than refetching on every render — a fetch that - * resolves invalidates the snapshot, which re-renders, which would ask - * again, forever. */ -function ensureRates(state: CurrencyState) { - if (state.code === "USD" || fetching) return; - const fx = readFx(); - if (fx && Date.now() - fx.fetchedAt < FX_MAX_AGE_MS) return; +function clearRefreshTimer() { + if (refreshTimer !== null) clearTimeout(refreshTimer); + refreshTimer = null; +} + +/** One timer and request for the whole store, started by subscriptions, not + * renders. A fresh table missing a currency still waits for its TTL. Failed + * requests keep any usable rate and retry with a delay, including in a tab + * that stays open overnight. */ +function refreshRates() { + clearRefreshTimer(); + if (subscribers.size === 0 || read().code === "USD" || fetching) return; + const table = readFx(); + const dueAt = Math.max(table ? table.fetchedAt + FX_MAX_AGE_MS : 0, retryAt); + const delay = dueAt - Date.now(); + if (delay > 0) { + refreshTimer = setTimeout(refreshRates, delay); + return; + } fetching = true; void fetchFxRates() .then((result) => { - if (!result) return; + if (!result) { + retryAt = Date.now() + FX_RETRY_MS; + return; + } + fx = { fetchedAt: Date.now(), date: result.date, rates: result.rates }; + retryAt = 0; try { - localStorage.setItem( - FX_KEY, - JSON.stringify({ fetchedAt: Date.now(), date: result.date, rates: result.rates }), - ); + localStorage.setItem(FX_KEY, JSON.stringify(fx)); } catch { - // Private mode: the rate still applies for this page via the snapshot. + // The in-memory table still applies when persistence is unavailable. } invalidate(); }) + .catch(() => { + retryAt = Date.now() + FX_RETRY_MS; + }) .finally(() => { fetching = false; + refreshRates(); }); } function invalidate() { snapshot = null; - window.dispatchEvent(new Event(EVENT)); + subscribers.forEach((onChange) => onChange()); + refreshRates(); } function read(): CurrencyState { - if (!snapshot) { - snapshot = compute(); - ensureRates(snapshot); - } + if (!snapshot) snapshot = compute(); return snapshot; } @@ -260,23 +273,39 @@ function readServer(): CurrencyState { return SERVER; } +function onStorage(e: StorageEvent) { + if (e.key !== PREF_KEY && e.key !== FX_KEY && e.key !== null) return; + if (e.key === PREF_KEY || e.key === null) { + preference = isCurrency(e.newValue) ? e.newValue : null; + } + if (e.key === FX_KEY || e.key === null) fx = parseFx(e.newValue); + invalidate(); +} + function subscribe(onChange: () => void) { - const onStorage = (e: StorageEvent) => { - if (e.key === PREF_KEY || e.key === FX_KEY) { - snapshot = null; - onChange(); - } - }; - window.addEventListener(EVENT, onChange); - window.addEventListener("storage", onStorage); + subscribers.add(onChange); + if (subscribers.size === 1) { + window.addEventListener("storage", onStorage); + window.addEventListener("focus", refreshRates); + window.addEventListener("online", refreshRates); + window.addEventListener("languagechange", invalidate); + } + refreshRates(); return () => { - window.removeEventListener(EVENT, onChange); - window.removeEventListener("storage", onStorage); + subscribers.delete(onChange); + if (subscribers.size === 0) { + clearRefreshTimer(); + window.removeEventListener("storage", onStorage); + window.removeEventListener("focus", refreshRates); + window.removeEventListener("online", refreshRates); + window.removeEventListener("languagechange", invalidate); + } }; } /** An explicit choice, or "auto" to go back to what the browser says. */ export function setCurrency(next: Currency | "auto") { + preference = next === "auto" ? null : next; try { if (next === "auto") localStorage.removeItem(PREF_KEY); else localStorage.setItem(PREF_KEY, next); diff --git a/apps/web/src/lib/format.ts b/apps/web/src/lib/format.ts index 2b62cd42..cfdf3973 100644 --- a/apps/web/src/lib/format.ts +++ b/apps/web/src/lib/format.ts @@ -231,7 +231,10 @@ export function fiat(n: number, code: string, locale = "en"): string { } if (n >= 1000) return wrap(compact(n, locale)); if (n >= 100 || (n >= 1 && minor === 0)) return wrap(String(Math.round(n))); - if (n >= 1) return wrap(n.toFixed(2)); + if (n >= 1) return wrap(n.toLocaleString(intlLocale(locale), { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })); return wrap(n.toLocaleString(intlLocale(locale), { maximumSignificantDigits: 2 })); } diff --git a/apps/web/src/locales/_source.json b/apps/web/src/locales/_source.json index 3a039691..c252ae48 100644 --- a/apps/web/src/locales/_source.json +++ b/apps/web/src/locales/_source.json @@ -432,9 +432,6 @@ "{name} is yours — this launch reuses your registered name (no registration fee). If the launch fails, the name locks at zero supply forever.": [ "app/[lang]/create/page.tsx" ], - "{ordinal} launch": [ - "components/address-hover-card.tsx" - ], "{pct} held by dumpers": [ "app/[lang]/research/_components/live-behavior-dashboard.tsx" ], @@ -952,7 +949,7 @@ "app/[lang]/dispense/_components/bridge.tsx", "app/[lang]/swap/_components/swap-settings.tsx" ], - "Bitcoin market price in USD": [ + "Bitcoin market price in {currency}": [ "app/[lang]/_components/market-pulse.tsx" ], "Bitcoin Price": [ @@ -1367,7 +1364,7 @@ "Counterparty": [ "app/[lang]/_components/market-pulse.tsx" ], - "Counterparty market price in USD": [ + "Counterparty market price in {currency}": [ "app/[lang]/_components/market-pulse.tsx" ], "Counterparty's canonical unspendable burn address": [ @@ -1805,6 +1802,12 @@ "Holding": [ "app/[lang]/profile/_components/positions-tab.tsx" ], + "Holds {count} token": [ + "components/address-hover-card.tsx" + ], + "Holds {count} tokens": [ + "components/address-hover-card.tsx" + ], "hour": [ "app/[lang]/[asset]/_components/price-chart.tsx" ], @@ -1913,6 +1916,12 @@ "Is the {mult}× opening premium a price guarantee?": [ "app/[lang]/faq/page.tsx" ], + "Issued {count} token": [ + "components/address-hover-card.tsx" + ], + "Issued {count} tokens": [ + "components/address-hover-card.tsx" + ], "It can still sell until {when}, then the remaining {amount} returns automatically.": [ "app/[lang]/dispense/_components/bridge.tsx" ], @@ -1955,7 +1964,11 @@ "Launch {name}": [ "app/[lang]/create/page.tsx" ], + "Launch #{n}": [ + "components/address-hover-card.tsx" + ], "Launch a token": [ + "app/[lang]/create/layout.tsx", "app/[lang]/create/page.tsx" ], "Launch dynamics": [ @@ -2335,6 +2348,7 @@ "app/[lang]/create/page.tsx" ], "Name, image, description. Everything else is the standard.": [ + "app/[lang]/create/layout.tsx", "app/[lang]/create/page.tsx" ], "named assets only, divisible": [ @@ -2632,8 +2646,7 @@ "app/[lang]/dispense/_components/bridge.tsx" ], "open": [ - "app/[lang]/[asset]/_components/scheduled-pulse.tsx", - "app/[lang]/rewards/page.tsx" + "app/[lang]/[asset]/_components/scheduled-pulse.tsx" ], "Open": [ "app/[lang]/activity/_components/activity-view.tsx" @@ -2653,6 +2666,9 @@ "Open XCP price": [ "app/[lang]/_components/market-pulse.tsx" ], + "open@@bounty": [ + "app/[lang]/rewards/page.tsx" + ], "open@@pool slider": [ "app/[lang]/faq/_components/explainer.tsx" ], @@ -2836,6 +2852,9 @@ "Positions you fully exited, with the profit or loss actually taken. A dash means the focused mint-and-trade history does not explain the live zero balance, so no P&L is claimed. Basis is average-cost, so a partial sale realizes its share and leaves the rest with the tokens still held.": [ "app/[lang]/profile/_components/history-tab.tsx" ], + "Positions, open mints, history, and launches for {address} on xcp.fun.": [ + "app/[lang]/profile/[address]/page.tsx" + ], "Premine / commission": [ "app/[lang]/faq/page.tsx" ], @@ -3173,7 +3192,7 @@ "Share unavailable": [ "app/[lang]/profile/_components/pools-tab.tsx" ], - "Show amounts in US dollars": [ + "Show amounts in {currency}": [ "app/[lang]/[asset]/_components/launch-stats.tsx" ], "Show amounts in XCP": [ diff --git a/apps/web/src/locales/es.json b/apps/web/src/locales/es.json index 6362aa78..a466c892 100644 --- a/apps/web/src/locales/es.json +++ b/apps/web/src/locales/es.json @@ -137,7 +137,6 @@ "{name} is available (0.5 XCP registration fee applies — {link}).": "{name} está disponible (aplica una comisión de registro de 0.5 XCP — {link}).", "{name} is scheduled.": "{name} quedó programado.", "{name} is yours — this launch reuses your registered name (no registration fee). If the launch fails, the name locks at zero supply forever.": "{name} es tuyo — este lanzamiento reutiliza tu nombre registrado (sin comisión de registro). Si el lanzamiento falla, el nombre queda bloqueado con suministro cero para siempre.", - "{ordinal} launch": "{ordinal} lanzamiento", "{pct} held by dumpers": "{pct} en manos de dumpers", "{pct} of supply": "{pct} del suministro", "{pct}% filled": "{pct}% ejecutada", @@ -302,7 +301,7 @@ "binary": "binaria", "Bitcoin": "Bitcoin", "Bitcoin fee rate in sats per vbyte": "Tasa de comisión de Bitcoin en sats por vbyte", - "Bitcoin market price in USD": "Precio de mercado de Bitcoin en USD", + "Bitcoin market price in {currency}": "Precio de mercado de Bitcoin en {currency}", "Bitcoin Price": "Precio de Bitcoin", "Bitcoin tx fee": "Comisión de minería de Bitcoin", "block": "bloque", @@ -428,7 +427,7 @@ "Couldn't reach the mint index—try again shortly.": "No se pudo acceder al índice de acuñaciones — inténtalo de nuevo en un momento.", "Couldn't refresh this page. Showing the last version that loaded.": "No se pudo actualizar esta página. Se muestra la última versión que cargó.", "Counterparty": "Counterparty", - "Counterparty market price in USD": "Precio de mercado de Counterparty en USD", + "Counterparty market price in {currency}": "Precio de mercado de Counterparty en {currency}", "Counterparty's canonical unspendable burn address": "Dirección de quema canónica de Counterparty, imposible de gastar", "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.": "La orden del DEX de Counterparty es la única primitiva de negociación: el emparejamiento pasa por el pool siempre que el precio del pool supere al libro de órdenes. Una {market} es una orden al precio de salida cotizado por el enrutador — se ejecuta de inmediato contra el pool y el libro al mejor precio. Una {limit} espera en el libro, y el pool la ejecuta automáticamente si su precio llega a cruzar el tuyo.", "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.": "Los conteos usan direcciones únicas. Conservó, movió y vendió son resultados excluyentes para los acuñadores de cada lanzamiento graduado. Conservó significa que no vendió y mantiene un saldo relevante; movió, que no vendió y ya no tiene un saldo relevante; vendió, que hizo al menos una venta en el pool o en el libro de órdenes registrada por xcp.fun.", @@ -563,6 +562,8 @@ "Holders": "Holders", "Holders:": "Holders:", "Holding": "Tenencia", + "Holds {count} token": "Tiene {count} tipo de token", + "Holds {count} tokens": "Tiene {count} tipos de tokens", "hour": "hora", "Hourly": "por hora", "how it works": "cómo funciona", @@ -597,6 +598,8 @@ "Invert rate": "Invertir tasa", "is": "es", "Is the {mult}× opening premium a price guarantee?": "¿La prima de apertura de {mult}× es una garantía de precio?", + "Issued {count} token": "Emitió {count} tipo de token", + "Issued {count} tokens": "Emitió {count} tipos de tokens", "It can still sell until {when}, then the remaining {amount} returns automatically.": "Todavía puede vender hasta {when}; después, los {amount} restantes regresan automáticamente.", "It ceases to exist — permanently. At the deadline, consensus destroys every minted token out of protocol escrow (they never touched anyone's wallet), refunds every minter's XCP, and closes the asset locked at zero supply. Nothing lingers in any wallet: no frozen tokens, no dust — minters end bit-identical to never having participated, minus one miner fee. The name itself becomes a tombstone: registered forever, mintable never. Worth weighing if you launch with a name you already own — a failed launch buries the name with the launch.": "Deja de existir — para siempre. Al vencer el plazo, el consenso destruye todos los tokens acuñados desde la custodia del protocolo (nunca tocaron la billetera de nadie), reembolsa el XCP de cada acuñador y cierra el activo bloqueado con suministro cero. No queda nada en ninguna billetera: ni tokens congelados ni polvo — los acuñadores terminan exactamente igual que si nunca hubieran participado, menos una comisión de minería. El nombre se convierte en una lápida: registrado para siempre, acuñable nunca. Vale la pena pensarlo si lanzas con un nombre que ya es tuyo — un lanzamiento fallido entierra el nombre junto con el lanzamiento.", "it has circulating supply — every unit must be destroyed first": "tiene suministro en circulación — primero hay que destruir cada unidad", @@ -610,6 +613,7 @@ "latest launch {age}": "último lanzamiento {age}", "Launch": "Lanzamiento", "Launch {name}": "Lanzar {name}", + "Launch #{n}": "Lanzamiento n.º {n}", "Launch a token": "Lanzar un token", "Launch dynamics": "Dinámica de lanzamientos", "Launch dynamics — xcp.fun": "Dinámica de lanzamientos — xcp.fun", @@ -828,6 +832,7 @@ "Open menu": "Abrir menú", "open right now": "abiertos ahora mismo", "Open XCP price": "Abrir precio de XCP", + "open@@bounty": "disponible", "open@@pool slider": "apertura", "Opening the graveyard…": "Abriendo el cementerio…", "opens": "abre", @@ -886,6 +891,7 @@ "Portfolio value over time, currently {value}": "Valor del portafolio a lo largo del tiempo, actualmente {value}", "Positions": "Posiciones", "Positions you fully exited, with the profit or loss actually taken. A dash means the focused mint-and-trade history does not explain the live zero balance, so no P&L is claimed. Basis is average-cost, so a partial sale realizes its share and leaves the rest with the tokens still held.": "Posiciones que cerraste por completo, con la ganancia o pérdida realmente obtenida. Un guion significa que el historial de acuñaciones y operaciones no explica el saldo cero actual, así que no se afirma ningún PnL. La base es el costo promedio, de modo que una venta parcial realiza su parte y deja el resto con los tokens que aún conservas.", + "Positions, open mints, history, and launches for {address} on xcp.fun.": "Posiciones, minteos abiertos, historial y lanzamientos de {address} en xcp.fun.", "Premine / commission": "Preminado / comisión", "Preparing inscription…": "Preparando la inscripción…", "Price": "Precio", @@ -989,7 +995,7 @@ "Share on X": "Compartir en X", "Share this launch": "Compartir este lanzamiento", "Share unavailable": "Participación no disponible", - "Show amounts in US dollars": "Mostrar montos en dólares", + "Show amounts in {currency}": "Mostrar montos en {currency}", "Show amounts in XCP": "Mostrar montos en XCP", "Show less": "Ver menos", "Show more": "Ver más", diff --git a/apps/web/src/locales/es.status.json b/apps/web/src/locales/es.status.json index 9f7a996e..4c75b7b4 100644 --- a/apps/web/src/locales/es.status.json +++ b/apps/web/src/locales/es.status.json @@ -89,7 +89,6 @@ "Bitcoin", "Bitcoin Price", "Bitcoin fee rate in sats per vbyte", - "Bitcoin market price in USD", "Bitcoin tx fee", "Block {n}", "Bought", @@ -179,13 +178,11 @@ "Couldn't reach the mint index—try again shortly.", "Couldn't refresh this page. Showing the last version that loaded.", "Counterparty", - "Counterparty market price in USD", "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.", "Counterparty's canonical unspendable burn address", "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.", "Covered", "Create", - "Create a launch", "Created", "Creator", "Creator bought — block {n}", @@ -627,7 +624,6 @@ "Share on X", "Share this launch", "Share unavailable", - "Show amounts in US dollars", "Show amounts in XCP", "Show less", "Show more", @@ -849,7 +845,6 @@ "cash P/L (XCP-equivalent)", "checking…", "claimed", - "closes", "closing", "collections with a minter in them", "committed", @@ -1126,7 +1121,6 @@ "{n}w ago", "{n}y ago", "{n}× apart", - "{ordinal} launch", "{pct} held by dumpers", "{pct} of supply", "{pct}% filled", @@ -1201,6 +1195,16 @@ "◆ Pool", "✓ Graduated — pool created with {xcp} XCP{usd} + 31M tokens, LP burned.", "✓ broadcast", - "💀 RIP" + "💀 RIP", + "Bitcoin market price in {currency}", + "Counterparty market price in {currency}", + "Holds {count} token", + "Holds {count} tokens", + "Issued {count} token", + "Issued {count} tokens", + "Launch #{n}", + "Positions, open mints, history, and launches for {address} on xcp.fun.", + "Show amounts in {currency}", + "open@@bounty" ] } diff --git a/apps/web/src/locales/fr.json b/apps/web/src/locales/fr.json index 801bbf1a..cb35de36 100644 --- a/apps/web/src/locales/fr.json +++ b/apps/web/src/locales/fr.json @@ -137,7 +137,6 @@ "{name} is available (0.5 XCP registration fee applies — {link}).": "{name} est disponible (des frais d'enregistrement de 0,5 XCP s'appliquent — {link}).", "{name} is scheduled.": "{name} est programmé.", "{name} is yours — this launch reuses your registered name (no registration fee). If the launch fails, the name locks at zero supply forever.": "{name} vous appartient — ce lancement réutilise votre nom enregistré (sans frais d'enregistrement). Si le lancement échoue, le nom est verrouillé à offre nulle pour toujours.", - "{ordinal} launch": "{ordinal} lancement", "{pct} held by dumpers": "{pct} détenus par des dumpers", "{pct} of supply": "{pct} de l'offre", "{pct}% filled": "{pct} % exécuté", @@ -302,7 +301,7 @@ "binary": "binaire", "Bitcoin": "Bitcoin", "Bitcoin fee rate in sats per vbyte": "Taux de frais Bitcoin en sats par vbyte", - "Bitcoin market price in USD": "Prix de marché de Bitcoin en USD", + "Bitcoin market price in {currency}": "Prix de marché de Bitcoin en {currency}", "Bitcoin Price": "Prix de Bitcoin", "Bitcoin tx fee": "Frais de minage Bitcoin", "block": "bloc", @@ -428,7 +427,7 @@ "Couldn't reach the mint index—try again shortly.": "Impossible d'atteindre l'index des mints — réessayez dans un instant.", "Couldn't refresh this page. Showing the last version that loaded.": "Impossible d'actualiser cette page. Affichage de la dernière version chargée.", "Counterparty": "Counterparty", - "Counterparty market price in USD": "Prix de marché de Counterparty en USD", + "Counterparty market price in {currency}": "Prix de marché de Counterparty en {currency}", "Counterparty's canonical unspendable burn address": "Adresse de brûlage canonique et indépensable de Counterparty", "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.": "L'ordre DEX de Counterparty est l'unique primitive de trading : l'appariement passe par le pool dès que le prix du pool bat le carnet d'ordres. Un {market} est un ordre au montant de sortie coté par le routeur — il s'exécute immédiatement contre le pool et le carnet, au meilleur prix. Un {limit} attend dans le carnet, et le pool l'exécute automatiquement si son prix vient à croiser le vôtre.", "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.": "Les décomptes utilisent des adresses uniques. Conservé, déplacé et vendu sont des issues exclusives pour les minteurs de chaque lancement gradué. Conservé signifie aucune vente et un solde actuel significatif ; déplacé, aucune vente et aucun solde actuel significatif ; vendu, au moins une vente dans le pool ou le carnet d'ordres enregistrée par xcp.fun.", @@ -563,6 +562,8 @@ "Holders": "Détenteurs", "Holders:": "Détenteurs :", "Holding": "En portefeuille", + "Holds {count} token": "Détient {count} type de token", + "Holds {count} tokens": "Détient {count} types de tokens", "hour": "heure", "Hourly": "Horaire", "how it works": "comment ça marche", @@ -597,6 +598,8 @@ "Invert rate": "Inverser le taux", "is": "est", "Is the {mult}× opening premium a price guarantee?": "La prime d'ouverture de {mult}× est-elle une garantie de prix ?", + "Issued {count} token": "A émis {count} type de token", + "Issued {count} tokens": "A émis {count} types de tokens", "It can still sell until {when}, then the remaining {amount} returns automatically.": "Il peut encore vendre jusqu'à {when}, puis les {amount} restants reviennent automatiquement.", "It ceases to exist — permanently. At the deadline, consensus destroys every minted token out of protocol escrow (they never touched anyone's wallet), refunds every minter's XCP, and closes the asset locked at zero supply. Nothing lingers in any wallet: no frozen tokens, no dust — minters end bit-identical to never having participated, minus one miner fee. The name itself becomes a tombstone: registered forever, mintable never. Worth weighing if you launch with a name you already own — a failed launch buries the name with the launch.": "Il cesse d'exister — définitivement. À l'échéance, le consensus détruit tous les tokens mintés depuis le séquestre du protocole (ils n'ont jamais touché le portefeuille de qui que ce soit), rembourse les XCP de chaque minteur et ferme l'actif, verrouillé à offre nulle. Rien ne traîne dans aucun portefeuille : pas de tokens gelés, pas de poussière — les minteurs terminent exactement comme s'ils n'avaient jamais participé, moins des frais de minage. Le nom lui-même devient une pierre tombale : enregistré pour toujours, mintable plus jamais. À peser si vous lancez avec un nom que vous possédez déjà — un lancement raté enterre le nom avec le lancement.", "it has circulating supply — every unit must be destroyed first": "il a une offre en circulation — chaque unité doit d'abord être détruite", @@ -610,6 +613,7 @@ "latest launch {age}": "dernier lancement {age}", "Launch": "Lancement", "Launch {name}": "Lancer {name}", + "Launch #{n}": "Lancement n° {n}", "Launch a token": "Lancer un token", "Launch dynamics": "Dynamique des lancements", "Launch dynamics — xcp.fun": "Dynamique des lancements — xcp.fun", @@ -828,6 +832,7 @@ "Open menu": "Ouvrir le menu", "open right now": "ouverts en ce moment", "Open XCP price": "Ouvrir le prix de XCP", + "open@@bounty": "disponible", "open@@pool slider": "ouverture", "Opening the graveyard…": "Ouverture du cimetière…", "opens": "ouvre", @@ -886,6 +891,7 @@ "Portfolio value over time, currently {value}": "Valeur du portefeuille dans le temps, actuellement {value}", "Positions": "Positions", "Positions you fully exited, with the profit or loss actually taken. A dash means the focused mint-and-trade history does not explain the live zero balance, so no P&L is claimed. Basis is average-cost, so a partial sale realizes its share and leaves the rest with the tokens still held.": "Positions que vous avez entièrement clôturées, avec le gain ou la perte réellement encaissé. Un tiret signifie que l'historique de mints et d'échanges n'explique pas le solde nul actuel, donc aucun G/P n'est affirmé. La base est le coût moyen : une vente partielle réalise sa part et laisse le reste avec les tokens encore détenus.", + "Positions, open mints, history, and launches for {address} on xcp.fun.": "Positions, mints en cours, historique et lancements de {address} sur xcp.fun.", "Premine / commission": "Prémine / commission", "Preparing inscription…": "Préparation de l'inscription…", "Price": "Prix", @@ -989,7 +995,7 @@ "Share on X": "Partager sur X", "Share this launch": "Partager ce lancement", "Share unavailable": "Part indisponible", - "Show amounts in US dollars": "Afficher les montants en dollars US", + "Show amounts in {currency}": "Afficher les montants en {currency}", "Show amounts in XCP": "Afficher les montants en XCP", "Show less": "Voir moins", "Show more": "Voir plus", diff --git a/apps/web/src/locales/fr.status.json b/apps/web/src/locales/fr.status.json index 9f7a996e..4c75b7b4 100644 --- a/apps/web/src/locales/fr.status.json +++ b/apps/web/src/locales/fr.status.json @@ -89,7 +89,6 @@ "Bitcoin", "Bitcoin Price", "Bitcoin fee rate in sats per vbyte", - "Bitcoin market price in USD", "Bitcoin tx fee", "Block {n}", "Bought", @@ -179,13 +178,11 @@ "Couldn't reach the mint index—try again shortly.", "Couldn't refresh this page. Showing the last version that loaded.", "Counterparty", - "Counterparty market price in USD", "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.", "Counterparty's canonical unspendable burn address", "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.", "Covered", "Create", - "Create a launch", "Created", "Creator", "Creator bought — block {n}", @@ -627,7 +624,6 @@ "Share on X", "Share this launch", "Share unavailable", - "Show amounts in US dollars", "Show amounts in XCP", "Show less", "Show more", @@ -849,7 +845,6 @@ "cash P/L (XCP-equivalent)", "checking…", "claimed", - "closes", "closing", "collections with a minter in them", "committed", @@ -1126,7 +1121,6 @@ "{n}w ago", "{n}y ago", "{n}× apart", - "{ordinal} launch", "{pct} held by dumpers", "{pct} of supply", "{pct}% filled", @@ -1201,6 +1195,16 @@ "◆ Pool", "✓ Graduated — pool created with {xcp} XCP{usd} + 31M tokens, LP burned.", "✓ broadcast", - "💀 RIP" + "💀 RIP", + "Bitcoin market price in {currency}", + "Counterparty market price in {currency}", + "Holds {count} token", + "Holds {count} tokens", + "Issued {count} token", + "Issued {count} tokens", + "Launch #{n}", + "Positions, open mints, history, and launches for {address} on xcp.fun.", + "Show amounts in {currency}", + "open@@bounty" ] } diff --git a/apps/web/src/locales/ja.json b/apps/web/src/locales/ja.json index b8d2482a..415d8b60 100644 --- a/apps/web/src/locales/ja.json +++ b/apps/web/src/locales/ja.json @@ -137,7 +137,6 @@ "{name} is available (0.5 XCP registration fee applies — {link}).": "{name} は利用可能です(0.5 XCP の登録手数料がかかります — {link})。", "{name} is scheduled.": "{name} を予約しました。", "{name} is yours — this launch reuses your registered name (no registration fee). If the launch fails, the name locks at zero supply forever.": "{name} はあなたのものです — このローンチは登録済みの名前を再利用します(登録手数料なし)。ローンチが失敗すると、名前は発行量ゼロで永久にロックされます。", - "{ordinal} launch": "{ordinal} 番目のローンチ", "{pct} held by dumpers": "{pct} をダンパーが保有", "{pct} of supply": "発行量の {pct}", "{pct}% filled": "{pct}% 約定", @@ -302,7 +301,7 @@ "binary": "二択", "Bitcoin": "Bitcoin", "Bitcoin fee rate in sats per vbyte": "Bitcoin 手数料率(sats/vbyte)", - "Bitcoin market price in USD": "Bitcoin の USD 建て市場価格", + "Bitcoin market price in {currency}": "Bitcoin の {currency} 建て市場価格", "Bitcoin Price": "Bitcoin 価格", "Bitcoin tx fee": "Bitcoin トランザクション手数料", "block": "ブロック", @@ -428,7 +427,7 @@ "Couldn't reach the mint index—try again shortly.": "ミント一覧に接続できませんでした — しばらくしてからもう一度お試しください。", "Couldn't refresh this page. Showing the last version that loaded.": "ページを更新できませんでした。最後に読み込めた内容を表示しています。", "Counterparty": "Counterparty", - "Counterparty market price in USD": "Counterparty (XCP) の USD 建て市場価格", + "Counterparty market price in {currency}": "Counterparty (XCP) の {currency} 建て市場価格", "Counterparty's canonical unspendable burn address": "Counterparty の正規バーンアドレス(使用不可)", "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.": "Counterparty の DEX 注文が唯一の取引プリミティブです: プールの価格が板より良いときは常にプール経由でマッチングされます。{market}はルーターの見積もり出力での注文で、プールと板から最良価格で即時に約定します。{limit}は板で待機し、プールの価格があなたの価格を越えれば自動的に約定します。", "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.": "数値はユニークアドレスで数えます。保有、移動、売却は、各卒業ローンチのミンターについて互いに排他的な結果です。保有は売却がなく意味のある現在残高があること、移動は売却がなく意味のある現在残高もないこと、売却は xcp.fun が捕捉したプールまたは板での売却が少なくとも 1 回あることを意味します。", @@ -563,6 +562,8 @@ "Holders": "ホルダー", "Holders:": "ホルダー:", "Holding": "保有中", + "Holds {count} token": "保有トークン:{count}種類", + "Holds {count} tokens": "保有トークン:{count}種類", "hour": "時間", "Hourly": "時間足", "how it works": "仕組み", @@ -597,6 +598,8 @@ "Invert rate": "レートを反転", "is": "そのまま", "Is the {mult}× opening premium a price guarantee?": "{mult}× のオープニングプレミアムは価格の保証ですか?", + "Issued {count} token": "発行トークン:{count}種類", + "Issued {count} tokens": "発行トークン:{count}種類", "It can still sell until {when}, then the remaining {amount} returns automatically.": "{when} までは販売を続け、その後残りの {amount} は自動的に戻ります。", "It ceases to exist — permanently. At the deadline, consensus destroys every minted token out of protocol escrow (they never touched anyone's wallet), refunds every minter's XCP, and closes the asset locked at zero supply. Nothing lingers in any wallet: no frozen tokens, no dust — minters end bit-identical to never having participated, minus one miner fee. The name itself becomes a tombstone: registered forever, mintable never. Worth weighing if you launch with a name you already own — a failed launch buries the name with the launch.": "永久に消滅します。締切時点で、コンセンサスはプロトコルのエスクローからミント済みトークンをすべて破棄し(誰のウォレットにも触れていません)、全ミンターの XCP を返金し、アセットを発行量ゼロでロックして閉じます。どのウォレットにも何も残りません: 凍結トークンもダストもなく、ミンターはトランザクション手数料 1 回分を除いて参加しなかった状態とビット単位で同じになります。名前そのものは墓標になります: 永久に登録され、二度とミントできません。すでに所有している名前でローンチするなら考慮に値します — 失敗したローンチは名前ごと葬ります。", "it has circulating supply — every unit must be destroyed first": "流通供給があります — まず全単位を破棄する必要があります", @@ -610,6 +613,7 @@ "latest launch {age}": "最新ローンチ {age}", "Launch": "ローンチ", "Launch {name}": "{name} をローンチ", + "Launch #{n}": "{n}回目のローンチ", "Launch a token": "トークンをローンチ", "Launch dynamics": "ローンチダイナミクス", "Launch dynamics — xcp.fun": "ローンチダイナミクス — xcp.fun", @@ -828,6 +832,7 @@ "Open menu": "メニューを開く", "open right now": "現在ミント中", "Open XCP price": "XCP 価格を開く", + "open@@bounty": "未獲得", "open@@pool slider": "オープン", "Opening the graveyard…": "墓場を開いています…", "opens": "開始", @@ -886,6 +891,7 @@ "Portfolio value over time, currently {value}": "ポートフォリオ価値の推移、現在 {value}", "Positions": "ポジション", "Positions you fully exited, with the profit or loss actually taken. A dash means the focused mint-and-trade history does not explain the live zero balance, so no P&L is claimed. Basis is average-cost, so a partial sale realizes its share and leaves the rest with the tokens still held.": "完全に手仕舞いしたポジションと、実際に確定した損益。ダッシュは、ミント・取引の履歴だけでは残高ゼロを説明できないため損益を主張していないことを意味します。取得原価は平均法で、一部売却はその分だけ実現し、残りは保有中のトークンに引き継がれます。", + "Positions, open mints, history, and launches for {address} on xcp.fun.": "xcp.funでの{address}のポジション、進行中のミント、履歴、ローンチ。", "Premine / commission": "プレマイン / コミッション", "Preparing inscription…": "刻印を準備中…", "Price": "価格", @@ -989,7 +995,7 @@ "Share on X": "X でシェア", "Share this launch": "このローンチをシェア", "Share unavailable": "持分を取得できません", - "Show amounts in US dollars": "金額を米ドルで表示", + "Show amounts in {currency}": "金額を{currency}で表示", "Show amounts in XCP": "金額を XCP で表示", "Show less": "閉じる", "Show more": "もっと見る", diff --git a/apps/web/src/locales/ja.status.json b/apps/web/src/locales/ja.status.json index ba59177c..0a10fa00 100644 --- a/apps/web/src/locales/ja.status.json +++ b/apps/web/src/locales/ja.status.json @@ -89,7 +89,6 @@ "Bitcoin", "Bitcoin Price", "Bitcoin fee rate in sats per vbyte", - "Bitcoin market price in USD", "Bitcoin tx fee", "Block {n}", "Bought", @@ -179,13 +178,11 @@ "Couldn't reach the mint index—try again shortly.", "Couldn't refresh this page. Showing the last version that loaded.", "Counterparty", - "Counterparty market price in USD", "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.", "Counterparty's canonical unspendable burn address", "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.", "Covered", "Create", - "Create a launch", "Created", "Creator", "Creator bought — block {n}", @@ -626,7 +623,6 @@ "Share on X", "Share this launch", "Share unavailable", - "Show amounts in US dollars", "Show amounts in XCP", "Show less", "Show more", @@ -848,7 +844,6 @@ "cash P/L (XCP-equivalent)", "checking…", "claimed", - "closes", "closing", "collections with a minter in them", "committed", @@ -1125,7 +1120,6 @@ "{n}w ago", "{n}y ago", "{n}× apart", - "{ordinal} launch", "{pct} held by dumpers", "{pct} of supply", "{pct}% filled", @@ -1200,6 +1194,16 @@ "◆ Pool", "✓ Graduated — pool created with {xcp} XCP{usd} + 31M tokens, LP burned.", "✓ broadcast", - "💀 RIP" + "💀 RIP", + "Bitcoin market price in {currency}", + "Counterparty market price in {currency}", + "Holds {count} token", + "Holds {count} tokens", + "Issued {count} token", + "Issued {count} tokens", + "Launch #{n}", + "Positions, open mints, history, and launches for {address} on xcp.fun.", + "Show amounts in {currency}", + "open@@bounty" ] } diff --git a/apps/web/src/locales/ko.json b/apps/web/src/locales/ko.json index 980d2d70..965836a9 100644 --- a/apps/web/src/locales/ko.json +++ b/apps/web/src/locales/ko.json @@ -137,7 +137,6 @@ "{name} is available (0.5 XCP registration fee applies — {link}).": "{name} 이름은 사용 가능합니다 (0.5 XCP 등록 수수료 적용 — {link}).", "{name} is scheduled.": "{name} 런칭이 예정되었습니다.", "{name} is yours — this launch reuses your registered name (no registration fee). If the launch fails, the name locks at zero supply forever.": "{name} 이름은 본인 소유입니다 — 이 런칭은 등록된 이름을 재사용합니다 (등록 수수료 없음). 런칭이 실패하면 이 이름은 공급량 0으로 영구히 잠깁니다.", - "{ordinal} launch": "{ordinal}번째 런칭", "{pct} held by dumpers": "덤퍼 보유 {pct}", "{pct} of supply": "공급량의 {pct}", "{pct}% filled": "{pct}% 체결", @@ -302,7 +301,7 @@ "binary": "둘 중 하나", "Bitcoin": "비트코인", "Bitcoin fee rate in sats per vbyte": "비트코인 수수료율 (sats/vbyte)", - "Bitcoin market price in USD": "비트코인 시장 가격 (USD)", + "Bitcoin market price in {currency}": "비트코인 시장 가격 ({currency})", "Bitcoin Price": "비트코인 가격", "Bitcoin tx fee": "비트코인 채굴 수수료", "block": "블록", @@ -428,7 +427,7 @@ "Couldn't reach the mint index—try again shortly.": "민팅 인덱스에 연결할 수 없습니다 — 잠시 후 다시 시도하세요.", "Couldn't refresh this page. Showing the last version that loaded.": "이 페이지를 새로 고칠 수 없습니다. 마지막으로 불러온 버전을 표시합니다.", "Counterparty": "Counterparty", - "Counterparty market price in USD": "Counterparty 시장 가격 (USD)", + "Counterparty market price in {currency}": "Counterparty 시장 가격 ({currency})", "Counterparty's canonical unspendable burn address": "Counterparty의 공식 소각 주소 — 누구도 쓸 수 없는 주소", "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.": "Counterparty의 DEX 주문이 유일한 거래 기본 단위입니다: 풀 가격이 호가창보다 좋을 때마다 매칭은 풀을 거칩니다. {market}은 라우터가 제시한 산출량으로 내는 주문으로, 풀과 호가창에서 최적가로 즉시 체결됩니다. {limit}은 호가창에 대기하며, 풀 가격이 지정한 가격을 넘어서면 자동으로 체결됩니다.", "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.": "집계는 고유 주소 기준입니다. 보유, 이동, 매도는 졸업한 각 런칭의 민터에 대해 상호 배타적인 결과입니다. 보유는 매도 없이 의미 있는 현재 잔액이 있는 경우, 이동은 매도 없이 의미 있는 잔액도 없는 경우, 매도는 xcp.fun이 포착한 풀 또는 호가창 매도가 1건 이상 있는 경우입니다.", @@ -563,6 +562,8 @@ "Holders": "홀더", "Holders:": "홀더:", "Holding": "보유 중", + "Holds {count} token": "보유 토큰 {count}종", + "Holds {count} tokens": "보유 토큰 {count}종", "hour": "시간", "Hourly": "시간봉", "how it works": "작동 방식", @@ -597,6 +598,8 @@ "Invert rate": "비율 뒤집기", "is": "곧", "Is the {mult}× opening premium a price guarantee?": "{mult}배 오프닝 프리미엄은 가격 보장인가요?", + "Issued {count} token": "발행 토큰 {count}종", + "Issued {count} tokens": "발행 토큰 {count}종", "It can still sell until {when}, then the remaining {amount} returns automatically.": "{when}까지는 계속 판매되며, 그 뒤 남은 {amount}는 자동으로 돌아옵니다.", "It ceases to exist — permanently. At the deadline, consensus destroys every minted token out of protocol escrow (they never touched anyone's wallet), refunds every minter's XCP, and closes the asset locked at zero supply. Nothing lingers in any wallet: no frozen tokens, no dust — minters end bit-identical to never having participated, minus one miner fee. The name itself becomes a tombstone: registered forever, mintable never. Worth weighing if you launch with a name you already own — a failed launch buries the name with the launch.": "영구히 사라집니다. 기한이 되면 합의가 프로토콜 에스크로에서 민팅된 모든 토큰을 파기하고 (누구의 지갑에도 닿은 적이 없습니다), 모든 민터의 XCP를 환불하며, 자산을 공급량 0으로 잠근 채 종료합니다. 어떤 지갑에도 남는 것이 없습니다: 동결된 토큰도, 먼지도 없습니다 — 민터는 채굴 수수료 한 번을 제외하면 참여하지 않았던 것과 비트 단위로 동일한 상태가 됩니다. 이름 자체는 묘비가 됩니다: 영원히 등록되고, 다시는 민팅할 수 없습니다. 이미 소유한 이름으로 런칭한다면 따져볼 만합니다 — 실패한 런칭은 이름을 런칭과 함께 묻어버립니다.", "it has circulating supply — every unit must be destroyed first": "유통 공급량이 있습니다 — 먼저 전량을 파기해야 합니다", @@ -610,6 +613,7 @@ "latest launch {age}": "최근 런칭 {age}", "Launch": "런칭", "Launch {name}": "{name} 런칭", + "Launch #{n}": "{n}번째 런칭", "Launch a token": "토큰 런칭", "Launch dynamics": "런칭 동향", "Launch dynamics — xcp.fun": "런칭 동향 — xcp.fun", @@ -828,6 +832,7 @@ "Open menu": "메뉴 열기", "open right now": "현재 진행 중", "Open XCP price": "XCP 가격 열기", + "open@@bounty": "미수령", "open@@pool slider": "오픈", "Opening the graveyard…": "묘지를 여는 중…", "opens": "시작", @@ -886,6 +891,7 @@ "Portfolio value over time, currently {value}": "시간에 따른 포트폴리오 가치, 현재 {value}", "Positions": "포지션", "Positions you fully exited, with the profit or loss actually taken. A dash means the focused mint-and-trade history does not explain the live zero balance, so no P&L is claimed. Basis is average-cost, so a partial sale realizes its share and leaves the rest with the tokens still held.": "완전히 청산한 포지션과 실제로 확정된 손익입니다. 대시(—)는 인덱싱된 민팅·거래 기록으로 현재의 0 잔액을 설명할 수 없어 손익을 제시하지 않는다는 뜻입니다. 원가는 평균법이므로 일부 매도는 그 몫만 실현하고 나머지는 아직 보유 중인 토큰에 남습니다.", + "Positions, open mints, history, and launches for {address} on xcp.fun.": "xcp.fun에서 {address}의 포지션, 진행 중인 민팅, 내역 및 런칭.", "Premine / commission": "프리마인 / 커미션", "Preparing inscription…": "인스크립션 준비 중…", "Price": "가격", @@ -989,7 +995,7 @@ "Share on X": "X에 공유", "Share this launch": "이 런칭 공유하기", "Share unavailable": "지분 확인 불가", - "Show amounts in US dollars": "금액을 미국 달러로 표시", + "Show amounts in {currency}": "금액을 {currency}로 표시", "Show amounts in XCP": "금액을 XCP로 표시", "Show less": "접기", "Show more": "더 보기", diff --git a/apps/web/src/locales/ko.status.json b/apps/web/src/locales/ko.status.json index 9f7a996e..4c75b7b4 100644 --- a/apps/web/src/locales/ko.status.json +++ b/apps/web/src/locales/ko.status.json @@ -89,7 +89,6 @@ "Bitcoin", "Bitcoin Price", "Bitcoin fee rate in sats per vbyte", - "Bitcoin market price in USD", "Bitcoin tx fee", "Block {n}", "Bought", @@ -179,13 +178,11 @@ "Couldn't reach the mint index—try again shortly.", "Couldn't refresh this page. Showing the last version that loaded.", "Counterparty", - "Counterparty market price in USD", "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.", "Counterparty's canonical unspendable burn address", "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.", "Covered", "Create", - "Create a launch", "Created", "Creator", "Creator bought — block {n}", @@ -627,7 +624,6 @@ "Share on X", "Share this launch", "Share unavailable", - "Show amounts in US dollars", "Show amounts in XCP", "Show less", "Show more", @@ -849,7 +845,6 @@ "cash P/L (XCP-equivalent)", "checking…", "claimed", - "closes", "closing", "collections with a minter in them", "committed", @@ -1126,7 +1121,6 @@ "{n}w ago", "{n}y ago", "{n}× apart", - "{ordinal} launch", "{pct} held by dumpers", "{pct} of supply", "{pct}% filled", @@ -1201,6 +1195,16 @@ "◆ Pool", "✓ Graduated — pool created with {xcp} XCP{usd} + 31M tokens, LP burned.", "✓ broadcast", - "💀 RIP" + "💀 RIP", + "Bitcoin market price in {currency}", + "Counterparty market price in {currency}", + "Holds {count} token", + "Holds {count} tokens", + "Issued {count} token", + "Issued {count} tokens", + "Launch #{n}", + "Positions, open mints, history, and launches for {address} on xcp.fun.", + "Show amounts in {currency}", + "open@@bounty" ] } diff --git a/apps/web/src/locales/notes.json b/apps/web/src/locales/notes.json index 16ea164b..c52be71d 100644 --- a/apps/web/src/locales/notes.json +++ b/apps/web/src/locales/notes.json @@ -65,5 +65,15 @@ "liquidity": "Tab beside swap: add or remove pool liquidity. Lowercase; the tab capitalises it in CSS.", "Filled": "An order status: the whole order executed. Not \"full\".", "inscription": "Chip on a launch whose image is inscribed on-chain, linking to the ordinals record. Lowercase, inside a pill beside an arrow.", - "Dismiss": "Accessible name for the × that closes an error banner." + "Dismiss": "Accessible name for the × that closes an error banner.", + "Holds {count} token": "Address hover card: count of distinct token types held by this address, not token units or a balance. The count may be an em dash while loading.", + "Holds {count} tokens": "Address hover card: count of distinct token types held by this address, not token units or a balance. The count may be an em dash while loading.", + "Issued {count} token": "Address hover card: count of distinct assets ever issued by this address, not token units or issuance transactions. The count may end in + when the result is capped, or be an em dash while loading.", + "Issued {count} tokens": "Address hover card: count of distinct assets ever issued by this address, not token units or issuance transactions. The count may end in + when the result is capped, or be an em dash while loading.", + "open@@bounty": "Rewards badge: the bounty has not been claimed yet and remains available. Not a market opening time or an open order.", + "Launch #{n}": "Issuer chip: this is the creator’s nth launch, not the launch’s rank across the whole site. The placeholder is a plain localized number; any ordinal grammar belongs in the translated sentence.", + "Positions, open mints, history, and launches for {address} on xcp.fun.": "Address profile search description. Open mints are ongoing token sales the address has participated in. Keep the address and xcp.fun unchanged.", + "Show amounts in {currency}": "Chart or stats control: show fiat amounts in the separately selected display currency. The placeholder is an ISO currency code such as USD, EUR, or JPY and must remain unchanged.", + "Bitcoin market price in {currency}": "Market price tooltip in the selected display currency. The placeholder is an ISO currency code such as USD, EUR, or JPY, independent of the page language.", + "Counterparty market price in {currency}": "Market price tooltip in the selected display currency. The placeholder is an ISO currency code such as USD, EUR, or JPY, independent of the page language." } diff --git a/apps/web/src/locales/pt.json b/apps/web/src/locales/pt.json index f0637f94..f0ccbb66 100644 --- a/apps/web/src/locales/pt.json +++ b/apps/web/src/locales/pt.json @@ -137,7 +137,6 @@ "{name} is available (0.5 XCP registration fee applies — {link}).": "{name} está disponível (há uma taxa de registro de 0,5 XCP — {link}).", "{name} is scheduled.": "{name} foi agendado.", "{name} is yours — this launch reuses your registered name (no registration fee). If the launch fails, the name locks at zero supply forever.": "{name} é seu — este lançamento reutiliza seu nome registrado (sem taxa de registro). Se o lançamento falhar, o nome fica travado com fornecimento zero para sempre.", - "{ordinal} launch": "{ordinal} lançamento", "{pct} held by dumpers": "{pct} nas mãos de dumpers", "{pct} of supply": "{pct} do fornecimento", "{pct}% filled": "{pct}% executada", @@ -302,7 +301,7 @@ "binary": "binária", "Bitcoin": "Bitcoin", "Bitcoin fee rate in sats per vbyte": "Taxa do Bitcoin em sats por vbyte", - "Bitcoin market price in USD": "Preço de mercado do Bitcoin em USD", + "Bitcoin market price in {currency}": "Preço de mercado do Bitcoin em {currency}", "Bitcoin Price": "Preço do Bitcoin", "Bitcoin tx fee": "Taxa de mineração do Bitcoin", "block": "bloco", @@ -428,7 +427,7 @@ "Couldn't reach the mint index—try again shortly.": "Não foi possível acessar o índice de mints — tente de novo em instantes.", "Couldn't refresh this page. Showing the last version that loaded.": "Não foi possível atualizar esta página. Mostrando a última versão que carregou.", "Counterparty": "Counterparty", - "Counterparty market price in USD": "Preço de mercado do Counterparty em USD", + "Counterparty market price in {currency}": "Preço de mercado do Counterparty em {currency}", "Counterparty's canonical unspendable burn address": "Endereço de queima canônico e não gastável do Counterparty", "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.": "A ordem da DEX do Counterparty é a única primitiva de negociação: o casamento passa pelo pool sempre que o preço do pool supera o livro de ofertas. Uma {market} é uma ordem no valor de saída cotado pelo roteador — ela executa na hora contra o pool e o livro ao melhor preço. Uma {limit} fica no livro, e o pool a executa automaticamente se o preço dele um dia cruzar o seu.", "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.": "As contagens usam endereços únicos. Manteve, moveu e vendeu são resultados excludentes para os minters de cada lançamento graduado. Manteve significa que não vendeu e tem um saldo relevante; moveu, que não vendeu e não tem mais um saldo relevante; vendeu, que fez pelo menos uma venda no pool ou no livro de ofertas registrada pelo xcp.fun.", @@ -563,6 +562,8 @@ "Holders": "Holders", "Holders:": "Holders:", "Holding": "Em carteira", + "Holds {count} token": "Detém {count} tipo de token", + "Holds {count} tokens": "Detém {count} tipos de tokens", "hour": "hora", "Hourly": "Por hora", "how it works": "como funciona", @@ -597,6 +598,8 @@ "Invert rate": "Inverter cotação", "is": "é", "Is the {mult}× opening premium a price guarantee?": "O prêmio de abertura de {mult}× é uma garantia de preço?", + "Issued {count} token": "Emitiu {count} tipo de token", + "Issued {count} tokens": "Emitiu {count} tipos de tokens", "It can still sell until {when}, then the remaining {amount} returns automatically.": "Ele ainda pode vender até {when}; depois, os {amount} restantes voltam automaticamente.", "It ceases to exist — permanently. At the deadline, consensus destroys every minted token out of protocol escrow (they never touched anyone's wallet), refunds every minter's XCP, and closes the asset locked at zero supply. Nothing lingers in any wallet: no frozen tokens, no dust — minters end bit-identical to never having participated, minus one miner fee. The name itself becomes a tombstone: registered forever, mintable never. Worth weighing if you launch with a name you already own — a failed launch buries the name with the launch.": "Ele deixa de existir — para sempre. No prazo final, o consenso destrói todos os tokens mintados da custódia do protocolo (eles nunca tocaram a carteira de ninguém), reembolsa o XCP de cada minter e fecha o ativo travado com fornecimento zero. Nada sobra em carteira nenhuma: nem tokens congelados, nem poeira — os minters terminam exatamente como se nunca tivessem participado, menos uma taxa de mineração. O nome em si vira uma lápide: registrado para sempre, mintável nunca. Vale pesar isso se você lançar com um nome que já é seu — um lançamento fracassado enterra o nome junto.", "it has circulating supply — every unit must be destroyed first": "ele tem fornecimento em circulação — cada unidade precisa ser destruída antes", @@ -610,6 +613,7 @@ "latest launch {age}": "último lançamento {age}", "Launch": "Lançamento", "Launch {name}": "Lançar {name}", + "Launch #{n}": "Lançamento n.º {n}", "Launch a token": "Lançar um token", "Launch dynamics": "Dinâmica dos lançamentos", "Launch dynamics — xcp.fun": "Dinâmica dos lançamentos — xcp.fun", @@ -828,6 +832,7 @@ "Open menu": "Abrir menu", "open right now": "abertos neste momento", "Open XCP price": "Abrir preço do XCP", + "open@@bounty": "disponível", "open@@pool slider": "abertura", "Opening the graveyard…": "Abrindo o cemitério…", "opens": "abre", @@ -886,6 +891,7 @@ "Portfolio value over time, currently {value}": "Valor da carteira ao longo do tempo, atualmente {value}", "Positions": "Posições", "Positions you fully exited, with the profit or loss actually taken. A dash means the focused mint-and-trade history does not explain the live zero balance, so no P&L is claimed. Basis is average-cost, so a partial sale realizes its share and leaves the rest with the tokens still held.": "Posições que você encerrou por completo, com o lucro ou prejuízo realmente obtido. Um traço significa que o histórico de mints e negociações não explica o saldo zero atual, então nenhum L/P é afirmado. A base é o custo médio, de modo que uma venda parcial realiza a sua parte e deixa o resto com os tokens que você ainda mantém.", + "Positions, open mints, history, and launches for {address} on xcp.fun.": "Posições, mints abertos, histórico e lançamentos de {address} no xcp.fun.", "Premine / commission": "Pré-mineração / comissão", "Preparing inscription…": "Preparando a inscrição…", "Price": "Preço", @@ -989,7 +995,7 @@ "Share on X": "Compartilhar no X", "Share this launch": "Compartilhar este lançamento", "Share unavailable": "Participação indisponível", - "Show amounts in US dollars": "Mostrar valores em dólares", + "Show amounts in {currency}": "Mostrar valores em {currency}", "Show amounts in XCP": "Mostrar valores em XCP", "Show less": "Ver menos", "Show more": "Ver mais", diff --git a/apps/web/src/locales/pt.status.json b/apps/web/src/locales/pt.status.json index 9f7a996e..4c75b7b4 100644 --- a/apps/web/src/locales/pt.status.json +++ b/apps/web/src/locales/pt.status.json @@ -89,7 +89,6 @@ "Bitcoin", "Bitcoin Price", "Bitcoin fee rate in sats per vbyte", - "Bitcoin market price in USD", "Bitcoin tx fee", "Block {n}", "Bought", @@ -179,13 +178,11 @@ "Couldn't reach the mint index—try again shortly.", "Couldn't refresh this page. Showing the last version that loaded.", "Counterparty", - "Counterparty market price in USD", "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.", "Counterparty's canonical unspendable burn address", "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.", "Covered", "Create", - "Create a launch", "Created", "Creator", "Creator bought — block {n}", @@ -627,7 +624,6 @@ "Share on X", "Share this launch", "Share unavailable", - "Show amounts in US dollars", "Show amounts in XCP", "Show less", "Show more", @@ -849,7 +845,6 @@ "cash P/L (XCP-equivalent)", "checking…", "claimed", - "closes", "closing", "collections with a minter in them", "committed", @@ -1126,7 +1121,6 @@ "{n}w ago", "{n}y ago", "{n}× apart", - "{ordinal} launch", "{pct} held by dumpers", "{pct} of supply", "{pct}% filled", @@ -1201,6 +1195,16 @@ "◆ Pool", "✓ Graduated — pool created with {xcp} XCP{usd} + 31M tokens, LP burned.", "✓ broadcast", - "💀 RIP" + "💀 RIP", + "Bitcoin market price in {currency}", + "Counterparty market price in {currency}", + "Holds {count} token", + "Holds {count} tokens", + "Issued {count} token", + "Issued {count} tokens", + "Launch #{n}", + "Positions, open mints, history, and launches for {address} on xcp.fun.", + "Show amounts in {currency}", + "open@@bounty" ] } diff --git a/apps/web/src/locales/ru.json b/apps/web/src/locales/ru.json index 76e1fab2..26eda45c 100644 --- a/apps/web/src/locales/ru.json +++ b/apps/web/src/locales/ru.json @@ -137,7 +137,6 @@ "{name} is available (0.5 XCP registration fee applies — {link}).": "{name} свободно (взимается комиссия за регистрацию 0,5 XCP — {link}).", "{name} is scheduled.": "{name} запланирован.", "{name} is yours — this launch reuses your registered name (no registration fee). If the launch fails, the name locks at zero supply forever.": "{name} принадлежит вам — этот запуск использует ваше зарегистрированное имя (без комиссии за регистрацию). Если запуск провалится, имя навсегда заблокируется с нулевым предложением.", - "{ordinal} launch": "{ordinal} запуск", "{pct} held by dumpers": "{pct} у дамперов", "{pct} of supply": "{pct} предложения", "{pct}% filled": "исполнено {pct} %", @@ -302,7 +301,7 @@ "binary": "бинарен", "Bitcoin": "Биткоин", "Bitcoin fee rate in sats per vbyte": "Ставка комиссии Bitcoin в сатоши за vbyte", - "Bitcoin market price in USD": "Рыночная цена биткоина в USD", + "Bitcoin market price in {currency}": "Рыночная цена биткоина в {currency}", "Bitcoin Price": "Цена биткоина", "Bitcoin tx fee": "Комиссия майнеров Bitcoin", "block": "блок", @@ -428,7 +427,7 @@ "Couldn't reach the mint index—try again shortly.": "Не удалось получить индекс минтов — попробуйте чуть позже.", "Couldn't refresh this page. Showing the last version that loaded.": "Не удалось обновить страницу. Показана последняя загруженная версия.", "Counterparty": "Counterparty", - "Counterparty market price in USD": "Рыночная цена Counterparty в USD", + "Counterparty market price in {currency}": "Рыночная цена Counterparty в {currency}", "Counterparty's canonical unspendable burn address": "Канонический адрес сжигания Counterparty, с которого невозможно потратить", "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.": "Ордер DEX Counterparty — единственный торговый примитив: сведение идёт через пул всякий раз, когда цена пула лучше книги ордеров. {market} — это ордер по выходу, рассчитанному маршрутизатором: он немедленно исполняется из пула и книги по лучшей цене. {limit} лежит в книге, и пул исполнит его автоматически, если его цена когда-нибудь пересечёт вашу.", "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.": "Подсчёт ведётся по уникальным адресам. «Держат», «переместили» и «продали» — взаимоисключающие исходы для минтеров каждого выпустившегося запуска. «Держат» — продаж не было и текущий баланс существенный; «переместили» — продаж не было и существенного баланса нет; «продали» — хотя бы одна продажа в пуле или в книге ордеров, зафиксированная xcp.fun.", @@ -563,6 +562,8 @@ "Holders": "Холдеры", "Holders:": "Холдеры:", "Holding": "В портфеле", + "Holds {count} token": "Типов токенов в портфеле: {count}", + "Holds {count} tokens": "Типов токенов в портфеле: {count}", "hour": "час", "Hourly": "Часовые", "how it works": "как это работает", @@ -597,6 +598,8 @@ "Invert rate": "Перевернуть курс", "is": "и есть", "Is the {mult}× opening premium a price guarantee?": "Стартовая премия {mult}× — это гарантия цены?", + "Issued {count} token": "Выпущено типов токенов: {count}", + "Issued {count} tokens": "Выпущено типов токенов: {count}", "It can still sell until {when}, then the remaining {amount} returns automatically.": "Он может продавать до {when}, затем остаток {amount} вернётся автоматически.", "It ceases to exist — permanently. At the deadline, consensus destroys every minted token out of protocol escrow (they never touched anyone's wallet), refunds every minter's XCP, and closes the asset locked at zero supply. Nothing lingers in any wallet: no frozen tokens, no dust — minters end bit-identical to never having participated, minus one miner fee. The name itself becomes a tombstone: registered forever, mintable never. Worth weighing if you launch with a name you already own — a failed launch buries the name with the launch.": "Он перестаёт существовать — навсегда. В дедлайн консенсус уничтожает все заминченные токены из эскроу протокола (они никогда не касались ничьего кошелька), возвращает XCP каждому минтеру и закрывает актив, заблокированный с нулевым предложением. Ни в одном кошельке ничего не остаётся: ни замороженных токенов, ни пыли — минтеры оказываются бит в бит в том же положении, как если бы не участвовали, минус одна комиссия майнеров. Само имя становится надгробием: зарегистрировано навсегда, минтить нельзя никогда. Стоит взвесить, если запускаетесь с именем, которым уже владеете: провальный запуск хоронит имя вместе с собой.", "it has circulating supply — every unit must be destroyed first": "у него есть предложение в обращении — сначала нужно уничтожить каждую единицу", @@ -610,6 +613,7 @@ "latest launch {age}": "последний запуск {age}", "Launch": "Запуск", "Launch {name}": "Запустить {name}", + "Launch #{n}": "Запуск №{n}", "Launch a token": "Запустить токен", "Launch dynamics": "Динамика запусков", "Launch dynamics — xcp.fun": "Динамика запусков — xcp.fun", @@ -828,6 +832,7 @@ "Open menu": "Открыть меню", "open right now": "открыты сейчас", "Open XCP price": "Открыть цену XCP", + "open@@bounty": "доступна", "open@@pool slider": "открытие", "Opening the graveyard…": "Открываем кладбище…", "opens": "откроется", @@ -886,6 +891,7 @@ "Portfolio value over time, currently {value}": "Стоимость портфеля во времени, сейчас {value}", "Positions": "Позиции", "Positions you fully exited, with the profit or loss actually taken. A dash means the focused mint-and-trade history does not explain the live zero balance, so no P&L is claimed. Basis is average-cost, so a partial sale realizes its share and leaves the rest with the tokens still held.": "Полностью закрытые позиции с фактически полученной прибылью или убытком. Прочерк означает, что история минтов и сделок не объясняет текущий нулевой баланс, поэтому П/У не заявляется. Базис — средняя стоимость: частичная продажа реализует свою долю, а остальное остаётся с ещё удерживаемыми токенами.", + "Positions, open mints, history, and launches for {address} on xcp.fun.": "Позиции, открытые минты, история и запуски {address} на xcp.fun.", "Premine / commission": "Премайн / комиссия", "Preparing inscription…": "Подготовка записи…", "Price": "Цена", @@ -989,7 +995,7 @@ "Share on X": "Поделиться в X", "Share this launch": "Поделиться этим запуском", "Share unavailable": "Доля недоступна", - "Show amounts in US dollars": "Показывать суммы в долларах США", + "Show amounts in {currency}": "Показывать суммы в {currency}", "Show amounts in XCP": "Показывать суммы в XCP", "Show less": "Свернуть", "Show more": "Показать ещё", diff --git a/apps/web/src/locales/ru.status.json b/apps/web/src/locales/ru.status.json index cb695cc0..e43f622a 100644 --- a/apps/web/src/locales/ru.status.json +++ b/apps/web/src/locales/ru.status.json @@ -89,7 +89,6 @@ "Bitcoin", "Bitcoin Price", "Bitcoin fee rate in sats per vbyte", - "Bitcoin market price in USD", "Bitcoin tx fee", "Block {n}", "Bought", @@ -179,13 +178,11 @@ "Couldn't reach the mint index—try again shortly.", "Couldn't refresh this page. Showing the last version that loaded.", "Counterparty", - "Counterparty market price in USD", "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.", "Counterparty's canonical unspendable burn address", "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.", "Covered", "Create", - "Create a launch", "Created", "Creator", "Creator bought — block {n}", @@ -628,7 +625,6 @@ "Share on X", "Share this launch", "Share unavailable", - "Show amounts in US dollars", "Show amounts in XCP", "Show less", "Show more", @@ -1127,7 +1123,6 @@ "{n}w ago", "{n}y ago", "{n}× apart", - "{ordinal} launch", "{pct} held by dumpers", "{pct} of supply", "{pct}% filled", @@ -1202,6 +1197,16 @@ "◆ Pool", "✓ Graduated — pool created with {xcp} XCP{usd} + 31M tokens, LP burned.", "✓ broadcast", - "💀 RIP" + "💀 RIP", + "Bitcoin market price in {currency}", + "Counterparty market price in {currency}", + "Holds {count} token", + "Holds {count} tokens", + "Issued {count} token", + "Issued {count} tokens", + "Launch #{n}", + "Positions, open mints, history, and launches for {address} on xcp.fun.", + "Show amounts in {currency}", + "open@@bounty" ] } diff --git a/apps/web/src/locales/uk.json b/apps/web/src/locales/uk.json index 9af43c27..401f08a6 100644 --- a/apps/web/src/locales/uk.json +++ b/apps/web/src/locales/uk.json @@ -137,7 +137,6 @@ "{name} is available (0.5 XCP registration fee applies — {link}).": "Ім'я {name} вільне (діє реєстраційна комісія 0,5 XCP — {link}).", "{name} is scheduled.": "Запуск {name} заплановано.", "{name} is yours — this launch reuses your registered name (no registration fee). If the launch fails, the name locks at zero supply forever.": "Ім'я {name} належить вам — цей запуск повторно використовує вже зареєстроване ім'я (без реєстраційної комісії). Якщо запуск провалиться, ім'я назавжди заблокується з нульовою пропозицією.", - "{ordinal} launch": "{ordinal} запуск", "{pct} held by dumpers": "{pct} у дамперів", "{pct} of supply": "{pct} пропозиції", "{pct}% filled": "виконано {pct}%", @@ -302,7 +301,7 @@ "binary": "бінарний", "Bitcoin": "Біткоїн", "Bitcoin fee rate in sats per vbyte": "Ставка комісії Bitcoin у sats за vbyte", - "Bitcoin market price in USD": "Ринкова ціна біткоїна в USD", + "Bitcoin market price in {currency}": "Ринкова ціна біткоїна в {currency}", "Bitcoin Price": "Ціна біткоїна", "Bitcoin tx fee": "Комісія майнерів Bitcoin", "block": "блок", @@ -428,7 +427,7 @@ "Couldn't reach the mint index—try again shortly.": "Не вдалося отримати індекс мінтів — спробуйте ще раз трохи згодом.", "Couldn't refresh this page. Showing the last version that loaded.": "Не вдалося оновити сторінку. Показано останню завантажену версію.", "Counterparty": "Counterparty", - "Counterparty market price in USD": "Ринкова ціна Counterparty в USD", + "Counterparty market price in {currency}": "Ринкова ціна Counterparty в {currency}", "Counterparty's canonical unspendable burn address": "Канонічна адреса спалювання Counterparty — з неї неможливо витратити", "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.": "DEX-ордер Counterparty — єдиний торговий примітив: зведення йде через пул щоразу, коли ціна пулу краща за книгу ордерів. {market} — це ордер за котируванням маршрутизатора: він одразу виконується з пулу та книги за найкращою ціною. {limit} лишається в книзі, і пул виконує його автоматично, якщо його ціна колись перетне вашу.", "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.": "Підрахунок ведеться за унікальними адресами. «Тримали», «перемістили» і «продали» — взаємовиключні результати для мінтерів кожного запуску, що випустився. «Тримали» — продажу не було й поточний баланс суттєвий; «перемістили» — продажу не було й суттєвого балансу немає; «продали» — принаймні один продаж у пулі або в книзі ордерів, зафіксований xcp.fun.", @@ -563,6 +562,8 @@ "Holders": "Холдери", "Holders:": "Холдери:", "Holding": "У портфелі", + "Holds {count} token": "Типів токенів у портфелі: {count}", + "Holds {count} tokens": "Типів токенів у портфелі: {count}", "hour": "година", "Hourly": "Годинні", "how it works": "як це працює", @@ -597,6 +598,8 @@ "Invert rate": "Обернути курс", "is": "і є", "Is the {mult}× opening premium a price guarantee?": "Чи є стартова премія {mult}× гарантією ціни?", + "Issued {count} token": "Випущено типів токенів: {count}", + "Issued {count} tokens": "Випущено типів токенів: {count}", "It can still sell until {when}, then the remaining {amount} returns automatically.": "Він ще може продавати до {when}, після чого решта {amount} повернеться автоматично.", "It ceases to exist — permanently. At the deadline, consensus destroys every minted token out of protocol escrow (they never touched anyone's wallet), refunds every minter's XCP, and closes the asset locked at zero supply. Nothing lingers in any wallet: no frozen tokens, no dust — minters end bit-identical to never having participated, minus one miner fee. The name itself becomes a tombstone: registered forever, mintable never. Worth weighing if you launch with a name you already own — a failed launch buries the name with the launch.": "Він припиняє існувати — назавжди. На дедлайні консенсус знищує кожен змінтований токен з ескроу протоколу (вони ніколи не торкалися чийогось гаманця), повертає XCP кожному мінтеру й закриває актив, заблокований із нульовою пропозицією. Ніщо не лишається в жодному гаманці: ні заморожених токенів, ні пилу — мінтери опиняються біт-у-біт у тому ж стані, ніби ніколи не брали участі, мінус одна комісія майнерів. Саме ім'я стає надгробком: зареєстроване назавжди, мінтити — ніколи. Варто зважити, якщо запускаєтеся з іменем, яким уже володієте: провальний запуск ховає ім'я разом із собою.", "it has circulating supply — every unit must be destroyed first": "у нього є пропозиція в обігу — спершу потрібно знищити кожну одиницю", @@ -610,6 +613,7 @@ "latest launch {age}": "останній запуск {age}", "Launch": "Запуск", "Launch {name}": "Запустити {name}", + "Launch #{n}": "Запуск №{n}", "Launch a token": "Запустити токен", "Launch dynamics": "Динаміка запусків", "Launch dynamics — xcp.fun": "Динаміка запусків — xcp.fun", @@ -828,6 +832,7 @@ "Open menu": "Відкрити меню", "open right now": "відкриті зараз", "Open XCP price": "Відкрити ціну XCP", + "open@@bounty": "доступна", "open@@pool slider": "відкриття", "Opening the graveyard…": "Відкриваємо цвинтар…", "opens": "відкриється", @@ -886,6 +891,7 @@ "Portfolio value over time, currently {value}": "Вартість портфеля в часі, зараз {value}", "Positions": "Позиції", "Positions you fully exited, with the profit or loss actually taken. A dash means the focused mint-and-trade history does not explain the live zero balance, so no P&L is claimed. Basis is average-cost, so a partial sale realizes its share and leaves the rest with the tokens still held.": "Позиції, з яких ви повністю вийшли, із фактично зафіксованим прибутком або збитком. Тире означає, що історія мінтів і угод не пояснює поточний нульовий баланс, тому PnL не стверджується. Базис — середня собівартість, тож частковий продаж реалізує свою частку, а решта лишається з токенами, які ви ще тримаєте.", + "Positions, open mints, history, and launches for {address} on xcp.fun.": "Позиції, відкриті мінти, історія та запуски {address} на xcp.fun.", "Premine / commission": "Премайн / комісія", "Preparing inscription…": "Підготовка інскрипції…", "Price": "Ціна", @@ -989,7 +995,7 @@ "Share on X": "Поділитися в X", "Share this launch": "Поділитися цим запуском", "Share unavailable": "Частка недоступна", - "Show amounts in US dollars": "Показувати суми в доларах США", + "Show amounts in {currency}": "Показувати суми в {currency}", "Show amounts in XCP": "Показувати суми в XCP", "Show less": "Згорнути", "Show more": "Розгорнути", diff --git a/apps/web/src/locales/uk.status.json b/apps/web/src/locales/uk.status.json index cb695cc0..e43f622a 100644 --- a/apps/web/src/locales/uk.status.json +++ b/apps/web/src/locales/uk.status.json @@ -89,7 +89,6 @@ "Bitcoin", "Bitcoin Price", "Bitcoin fee rate in sats per vbyte", - "Bitcoin market price in USD", "Bitcoin tx fee", "Block {n}", "Bought", @@ -179,13 +178,11 @@ "Couldn't reach the mint index—try again shortly.", "Couldn't refresh this page. Showing the last version that loaded.", "Counterparty", - "Counterparty market price in USD", "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.", "Counterparty's canonical unspendable burn address", "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.", "Covered", "Create", - "Create a launch", "Created", "Creator", "Creator bought — block {n}", @@ -628,7 +625,6 @@ "Share on X", "Share this launch", "Share unavailable", - "Show amounts in US dollars", "Show amounts in XCP", "Show less", "Show more", @@ -1127,7 +1123,6 @@ "{n}w ago", "{n}y ago", "{n}× apart", - "{ordinal} launch", "{pct} held by dumpers", "{pct} of supply", "{pct}% filled", @@ -1202,6 +1197,16 @@ "◆ Pool", "✓ Graduated — pool created with {xcp} XCP{usd} + 31M tokens, LP burned.", "✓ broadcast", - "💀 RIP" + "💀 RIP", + "Bitcoin market price in {currency}", + "Counterparty market price in {currency}", + "Holds {count} token", + "Holds {count} tokens", + "Issued {count} token", + "Issued {count} tokens", + "Launch #{n}", + "Positions, open mints, history, and launches for {address} on xcp.fun.", + "Show amounts in {currency}", + "open@@bounty" ] } diff --git a/apps/web/src/locales/zh-hk.json b/apps/web/src/locales/zh-hk.json index e88c73de..87878786 100644 --- a/apps/web/src/locales/zh-hk.json +++ b/apps/web/src/locales/zh-hk.json @@ -137,7 +137,6 @@ "{name} is available (0.5 XCP registration fee applies — {link}).": "{name} 可用(需支付 0.5 XCP 註冊費 — {link})。", "{name} is scheduled.": "{name} 已排定發射。", "{name} is yours — this launch reuses your registered name (no registration fee). If the launch fails, the name locks at zero supply forever.": "{name} 是你的 — 本次發射複用你已註冊的名稱(無需註冊費)。如果發射失敗,該名稱將以零供應量永久鎖定。", - "{ordinal} launch": "第 {ordinal} 次發射", "{pct} held by dumpers": "{pct} 由砸盤者持有", "{pct} of supply": "佔供應量 {pct}", "{pct}% filled": "已成交 {pct}%", @@ -302,7 +301,7 @@ "binary": "非此即彼", "Bitcoin": "比特幣", "Bitcoin fee rate in sats per vbyte": "比特幣費率(sat/vB)", - "Bitcoin market price in USD": "比特幣的美元市場價格", + "Bitcoin market price in {currency}": "比特幣以 {currency} 計價的市場價格", "Bitcoin Price": "比特幣價格", "Bitcoin tx fee": "比特幣礦工費", "block": "區塊", @@ -428,7 +427,7 @@ "Couldn't reach the mint index—try again shortly.": "無法連接鑄造索引 — 請稍後重試。", "Couldn't refresh this page. Showing the last version that loaded.": "無法刷新此頁面。正在顯示上次加載的版本。", "Counterparty": "Counterparty", - "Counterparty market price in USD": "Counterparty (XCP) 的美元市場價格", + "Counterparty market price in {currency}": "Counterparty (XCP) 以 {currency} 計價的市場價格", "Counterparty's canonical unspendable burn address": "Counterparty 標準的不可花費銷毀地址", "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.": "Counterparty 的 DEX 訂單是唯一的交易原語:只要流動性池的價格優於訂單簿,撮合就會經由池子進行。{market}是按路由器報價輸出下的訂單 — 它會立即以最優價格從池子和訂單簿成交。{limit}掛在訂單簿上,一旦池子價格越過你的價格,就會自動成交。", "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.": "計數按獨立地址。持有、轉出、賣出是每個畢業發射的鑄造者互斥的三種結果。持有指沒有賣出且當前餘額可觀;轉出指沒有賣出且當前餘額不可觀;賣出指 xcp.fun 記錄到至少一筆池子或訂單簿賣出。", @@ -563,6 +562,8 @@ "Holders": "持有者", "Holders:": "持有者:", "Holding": "持有中", + "Holds {count} token": "持有 {count} 種代幣", + "Holds {count} tokens": "持有 {count} 種代幣", "hour": "小時", "Hourly": "小時線", "how it works": "運作機制", @@ -597,6 +598,8 @@ "Invert rate": "切換計價方向", "is": "就是", "Is the {mult}× opening premium a price guarantee?": "{mult}× 的開盤溢價是價格保證嗎?", + "Issued {count} token": "發行了 {count} 種代幣", + "Issued {count} tokens": "發行了 {count} 種代幣", "It can still sell until {when}, then the remaining {amount} returns automatically.": "在 {when} 之前仍可繼續賣出,之後剩餘的 {amount} 會自動退回。", "It ceases to exist — permanently. At the deadline, consensus destroys every minted token out of protocol escrow (they never touched anyone's wallet), refunds every minter's XCP, and closes the asset locked at zero supply. Nothing lingers in any wallet: no frozen tokens, no dust — minters end bit-identical to never having participated, minus one miner fee. The name itself becomes a tombstone: registered forever, mintable never. Worth weighing if you launch with a name you already own — a failed launch buries the name with the launch.": "它會永久消失。到達截止期時,共識會從協議託管中銷毀所有已鑄造的代幣(它們從未進入過任何人的錢包),退回每位鑄造者的 XCP,並把資產以零供應量鎖定關閉。任何錢包裏都不會留下任何東西:沒有凍結的代幣,沒有粉塵 — 鑄造者的狀態和從未參與過完全一樣,只少了一筆礦工費。名稱本身則變成一塊墓碑:永久註冊,永不可鑄。如果你打算用一個自己已擁有的名稱來發射,這一點值得權衡 — 發射失敗會把名稱和發射一起埋葬。", "it has circulating supply — every unit must be destroyed first": "它仍有流通供應量 — 必須先銷毀每一枚", @@ -610,6 +613,7 @@ "latest launch {age}": "最近一次發射 {age}", "Launch": "發射", "Launch {name}": "發射 {name}", + "Launch #{n}": "第 {n} 次發射", "Launch a token": "發射代幣", "Launch dynamics": "發射動態", "Launch dynamics — xcp.fun": "發射動態 — xcp.fun", @@ -828,6 +832,7 @@ "Open menu": "打開選單", "open right now": "當前進行中", "Open XCP price": "打開 XCP 價格", + "open@@bounty": "待領取", "open@@pool slider": "開盤", "Opening the graveyard…": "正在打開墓地…", "opens": "開放", @@ -886,6 +891,7 @@ "Portfolio value over time, currently {value}": "組合價值隨時間的變化,當前為 {value}", "Positions": "倉位", "Positions you fully exited, with the profit or loss actually taken. A dash means the focused mint-and-trade history does not explain the live zero balance, so no P&L is claimed. Basis is average-cost, so a partial sale realizes its share and leaves the rest with the tokens still held.": "你已完全退出的倉位,以及實際落袋的盈虧。短橫線表示已索引的鑄造與交易歷史無法解釋當前的零餘額,因此不給出盈虧。成本按平均成本法計算,部分賣出只實現其對應份額,其餘成本留在仍持有的代幣上。", + "Positions, open mints, history, and launches for {address} on xcp.fun.": "{address} 在 xcp.fun 上的倉位、進行中的鑄造、歷史記錄和發射。", "Premine / commission": "預挖 / 佣金", "Preparing inscription…": "正在準備銘刻…", "Price": "價格", @@ -989,7 +995,7 @@ "Share on X": "分享到 X", "Share this launch": "分享這次發射", "Share unavailable": "無法獲取份額", - "Show amounts in US dollars": "以美元顯示金額", + "Show amounts in {currency}": "以 {currency} 顯示金額", "Show amounts in XCP": "以 XCP 顯示金額", "Show less": "收起", "Show more": "展開更多", diff --git a/apps/web/src/locales/zh-hk.status.json b/apps/web/src/locales/zh-hk.status.json index 243062d8..72fa2c47 100644 --- a/apps/web/src/locales/zh-hk.status.json +++ b/apps/web/src/locales/zh-hk.status.json @@ -138,7 +138,6 @@ "{name} is available (0.5 XCP registration fee applies — {link}).", "{name} is scheduled.", "{name} is yours — this launch reuses your registered name (no registration fee). If the launch fails, the name locks at zero supply forever.", - "{ordinal} launch", "{pct} held by dumpers", "{pct} of supply", "{pct}% filled", @@ -302,7 +301,6 @@ "binary", "Bitcoin", "Bitcoin fee rate in sats per vbyte", - "Bitcoin market price in USD", "Bitcoin Price", "Bitcoin tx fee", "block", @@ -425,13 +423,11 @@ "Couldn't reach the mint index—try again shortly.", "Couldn't refresh this page. Showing the last version that loaded.", "Counterparty", - "Counterparty market price in USD", "Counterparty's canonical unspendable burn address", "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.", "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.", "Covered", "Create", - "Create a launch", "Created", "Creator", "Creator bought — block {n}", @@ -979,7 +975,6 @@ "Share on X", "Share this launch", "Share unavailable", - "Show amounts in US dollars", "Show amounts in XCP", "Show less", "Show more", @@ -1201,6 +1196,16 @@ "Your wallet will ask once per route, in order — each popup is one route, nothing more.", "Your xcp.fun profile", "yours — reused, no fee", - "zero existing supply" + "zero existing supply", + "Bitcoin market price in {currency}", + "Counterparty market price in {currency}", + "Holds {count} token", + "Holds {count} tokens", + "Issued {count} token", + "Issued {count} tokens", + "Launch #{n}", + "Positions, open mints, history, and launches for {address} on xcp.fun.", + "Show amounts in {currency}", + "open@@bounty" ] } diff --git a/apps/web/src/locales/zh-tw.json b/apps/web/src/locales/zh-tw.json index 6bf6d9cf..666d46a3 100644 --- a/apps/web/src/locales/zh-tw.json +++ b/apps/web/src/locales/zh-tw.json @@ -137,7 +137,6 @@ "{name} is available (0.5 XCP registration fee applies — {link}).": "{name} 可用(需支付 0.5 XCP 註冊費 — {link})。", "{name} is scheduled.": "{name} 已排定發射。", "{name} is yours — this launch reuses your registered name (no registration fee). If the launch fails, the name locks at zero supply forever.": "{name} 是你的 — 本次發射複用你已註冊的名稱(無需註冊費)。如果發射失敗,該名稱將以零供應量永久鎖定。", - "{ordinal} launch": "第 {ordinal} 次發射", "{pct} held by dumpers": "{pct} 由砸盤者持有", "{pct} of supply": "佔供應量 {pct}", "{pct}% filled": "已成交 {pct}%", @@ -302,7 +301,7 @@ "binary": "非此即彼", "Bitcoin": "比特幣", "Bitcoin fee rate in sats per vbyte": "比特幣費率(sat/vB)", - "Bitcoin market price in USD": "比特幣的美元市場價格", + "Bitcoin market price in {currency}": "比特幣以 {currency} 計價的市場價格", "Bitcoin Price": "比特幣價格", "Bitcoin tx fee": "比特幣礦工費", "block": "區塊", @@ -428,7 +427,7 @@ "Couldn't reach the mint index—try again shortly.": "無法連接鑄造索引 — 請稍後重試。", "Couldn't refresh this page. Showing the last version that loaded.": "無法重新整理此頁面。正在顯示上次載入的版本。", "Counterparty": "Counterparty", - "Counterparty market price in USD": "Counterparty (XCP) 的美元市場價格", + "Counterparty market price in {currency}": "Counterparty (XCP) 以 {currency} 計價的市場價格", "Counterparty's canonical unspendable burn address": "Counterparty 標準的不可花費銷毀地址", "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.": "Counterparty 的 DEX 訂單是唯一的交易原語:只要流動性池的價格優於訂單簿,撮合就會經由池子進行。{market}是按路由器報價輸出下的訂單 — 它會立即以最優價格從池子和訂單簿成交。{limit}掛在訂單簿上,一旦池子價格越過你的價格,就會自動成交。", "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.": "計數按獨立地址。持有、轉出、賣出是每個畢業發射的鑄造者互斥的三種結果。持有指沒有賣出且當前餘額可觀;轉出指沒有賣出且當前餘額不可觀;賣出指 xcp.fun 記錄到至少一筆池子或訂單簿賣出。", @@ -563,6 +562,8 @@ "Holders": "持有者", "Holders:": "持有者:", "Holding": "持有中", + "Holds {count} token": "持有 {count} 種代幣", + "Holds {count} tokens": "持有 {count} 種代幣", "hour": "小時", "Hourly": "小時線", "how it works": "運作機制", @@ -597,6 +598,8 @@ "Invert rate": "切換計價方向", "is": "就是", "Is the {mult}× opening premium a price guarantee?": "{mult}× 的開盤溢價是價格保證嗎?", + "Issued {count} token": "發行了 {count} 種代幣", + "Issued {count} tokens": "發行了 {count} 種代幣", "It can still sell until {when}, then the remaining {amount} returns automatically.": "在 {when} 之前仍可繼續賣出,之後剩餘的 {amount} 會自動退回。", "It ceases to exist — permanently. At the deadline, consensus destroys every minted token out of protocol escrow (they never touched anyone's wallet), refunds every minter's XCP, and closes the asset locked at zero supply. Nothing lingers in any wallet: no frozen tokens, no dust — minters end bit-identical to never having participated, minus one miner fee. The name itself becomes a tombstone: registered forever, mintable never. Worth weighing if you launch with a name you already own — a failed launch buries the name with the launch.": "它會永久消失。到達截止期時,共識會從協議託管中銷毀所有已鑄造的代幣(它們從未進入過任何人的錢包),退回每位鑄造者的 XCP,並把資產以零供應量鎖定關閉。任何錢包裡都不會留下任何東西:沒有凍結的代幣,沒有粉塵 — 鑄造者的狀態和從未參與過完全一樣,只少了一筆礦工費。名稱本身則變成一塊墓碑:永久註冊,永不可鑄。如果你打算用一個自己已擁有的名稱來發射,這一點值得權衡 — 發射失敗會把名稱和發射一起埋葬。", "it has circulating supply — every unit must be destroyed first": "它仍有流通供應量 — 必須先銷毀每一枚", @@ -610,6 +613,7 @@ "latest launch {age}": "最近一次發射 {age}", "Launch": "發射", "Launch {name}": "發射 {name}", + "Launch #{n}": "第 {n} 次發射", "Launch a token": "發射代幣", "Launch dynamics": "發射動態", "Launch dynamics — xcp.fun": "發射動態 — xcp.fun", @@ -828,6 +832,7 @@ "Open menu": "開啟選單", "open right now": "當前進行中", "Open XCP price": "開啟 XCP 價格", + "open@@bounty": "待領取", "open@@pool slider": "開盤", "Opening the graveyard…": "正在開啟墓地…", "opens": "開放", @@ -886,6 +891,7 @@ "Portfolio value over time, currently {value}": "組合價值隨時間的變化,當前為 {value}", "Positions": "倉位", "Positions you fully exited, with the profit or loss actually taken. A dash means the focused mint-and-trade history does not explain the live zero balance, so no P&L is claimed. Basis is average-cost, so a partial sale realizes its share and leaves the rest with the tokens still held.": "你已完全退出的倉位,以及實際落袋的盈虧。短橫線表示已索引的鑄造與交易歷史無法解釋當前的零餘額,因此不給出盈虧。成本按平均成本法計算,部分賣出只實現其對應份額,其餘成本留在仍持有的代幣上。", + "Positions, open mints, history, and launches for {address} on xcp.fun.": "{address} 在 xcp.fun 上的倉位、進行中的鑄造、歷史紀錄和發射。", "Premine / commission": "預挖 / 佣金", "Preparing inscription…": "正在準備銘刻…", "Price": "價格", @@ -989,7 +995,7 @@ "Share on X": "分享到 X", "Share this launch": "分享這次發射", "Share unavailable": "無法獲取份額", - "Show amounts in US dollars": "以美元顯示金額", + "Show amounts in {currency}": "以 {currency} 顯示金額", "Show amounts in XCP": "以 XCP 顯示金額", "Show less": "收起", "Show more": "展開更多", diff --git a/apps/web/src/locales/zh-tw.status.json b/apps/web/src/locales/zh-tw.status.json index 243062d8..72fa2c47 100644 --- a/apps/web/src/locales/zh-tw.status.json +++ b/apps/web/src/locales/zh-tw.status.json @@ -138,7 +138,6 @@ "{name} is available (0.5 XCP registration fee applies — {link}).", "{name} is scheduled.", "{name} is yours — this launch reuses your registered name (no registration fee). If the launch fails, the name locks at zero supply forever.", - "{ordinal} launch", "{pct} held by dumpers", "{pct} of supply", "{pct}% filled", @@ -302,7 +301,6 @@ "binary", "Bitcoin", "Bitcoin fee rate in sats per vbyte", - "Bitcoin market price in USD", "Bitcoin Price", "Bitcoin tx fee", "block", @@ -425,13 +423,11 @@ "Couldn't reach the mint index—try again shortly.", "Couldn't refresh this page. Showing the last version that loaded.", "Counterparty", - "Counterparty market price in USD", "Counterparty's canonical unspendable burn address", "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.", "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.", "Covered", "Create", - "Create a launch", "Created", "Creator", "Creator bought — block {n}", @@ -979,7 +975,6 @@ "Share on X", "Share this launch", "Share unavailable", - "Show amounts in US dollars", "Show amounts in XCP", "Show less", "Show more", @@ -1201,6 +1196,16 @@ "Your wallet will ask once per route, in order — each popup is one route, nothing more.", "Your xcp.fun profile", "yours — reused, no fee", - "zero existing supply" + "zero existing supply", + "Bitcoin market price in {currency}", + "Counterparty market price in {currency}", + "Holds {count} token", + "Holds {count} tokens", + "Issued {count} token", + "Issued {count} tokens", + "Launch #{n}", + "Positions, open mints, history, and launches for {address} on xcp.fun.", + "Show amounts in {currency}", + "open@@bounty" ] } diff --git a/apps/web/src/locales/zh.json b/apps/web/src/locales/zh.json index a89cba40..c93034ed 100644 --- a/apps/web/src/locales/zh.json +++ b/apps/web/src/locales/zh.json @@ -137,7 +137,6 @@ "{name} is available (0.5 XCP registration fee applies — {link}).": "{name} 可用(需支付 0.5 XCP 注册费 — {link})。", "{name} is scheduled.": "{name} 已排定发射。", "{name} is yours — this launch reuses your registered name (no registration fee). If the launch fails, the name locks at zero supply forever.": "{name} 是你的 — 本次发射复用你已注册的名称(无需注册费)。如果发射失败,该名称将以零供应量永久锁定。", - "{ordinal} launch": "第 {ordinal} 次发射", "{pct} held by dumpers": "{pct} 由砸盘者持有", "{pct} of supply": "占供应量 {pct}", "{pct}% filled": "已成交 {pct}%", @@ -302,7 +301,7 @@ "binary": "非此即彼", "Bitcoin": "比特币", "Bitcoin fee rate in sats per vbyte": "比特币费率(sat/vB)", - "Bitcoin market price in USD": "比特币的美元市场价格", + "Bitcoin market price in {currency}": "比特币以 {currency} 计价的市场价格", "Bitcoin Price": "比特币价格", "Bitcoin tx fee": "比特币矿工费", "block": "区块", @@ -428,7 +427,7 @@ "Couldn't reach the mint index—try again shortly.": "无法连接铸造索引 — 请稍后重试。", "Couldn't refresh this page. Showing the last version that loaded.": "无法刷新此页面。正在显示上次加载的版本。", "Counterparty": "Counterparty", - "Counterparty market price in USD": "Counterparty (XCP) 的美元市场价格", + "Counterparty market price in {currency}": "Counterparty (XCP) 以 {currency} 计价的市场价格", "Counterparty's canonical unspendable burn address": "Counterparty 标准的不可花费销毁地址", "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.": "Counterparty 的 DEX 订单是唯一的交易原语:只要流动性池的价格优于订单簿,撮合就会经由池子进行。{market}是按路由器报价输出下的订单 — 它会立即以最优价格从池子和订单簿成交。{limit}挂在订单簿上,一旦池子价格越过你的价格,就会自动成交。", "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.": "计数按独立地址。持有、转出、卖出是每个毕业发射的铸造者互斥的三种结果。持有指没有卖出且当前余额可观;转出指没有卖出且当前余额不可观;卖出指 xcp.fun 记录到至少一笔池子或订单簿卖出。", @@ -563,6 +562,8 @@ "Holders": "持有者", "Holders:": "持有者:", "Holding": "持有中", + "Holds {count} token": "持有 {count} 种代币", + "Holds {count} tokens": "持有 {count} 种代币", "hour": "小时", "Hourly": "小时线", "how it works": "运作机制", @@ -597,6 +598,8 @@ "Invert rate": "切换计价方向", "is": "就是", "Is the {mult}× opening premium a price guarantee?": "{mult}× 的开盘溢价是价格保证吗?", + "Issued {count} token": "发行了 {count} 种代币", + "Issued {count} tokens": "发行了 {count} 种代币", "It can still sell until {when}, then the remaining {amount} returns automatically.": "在 {when} 之前仍可继续卖出,之后剩余的 {amount} 会自动退回。", "It ceases to exist — permanently. At the deadline, consensus destroys every minted token out of protocol escrow (they never touched anyone's wallet), refunds every minter's XCP, and closes the asset locked at zero supply. Nothing lingers in any wallet: no frozen tokens, no dust — minters end bit-identical to never having participated, minus one miner fee. The name itself becomes a tombstone: registered forever, mintable never. Worth weighing if you launch with a name you already own — a failed launch buries the name with the launch.": "它会永久消失。到达截止期时,共识会从协议托管中销毁所有已铸造的代币(它们从未进入过任何人的钱包),退回每位铸造者的 XCP,并把资产以零供应量锁定关闭。任何钱包里都不会留下任何东西:没有冻结的代币,没有粉尘 — 铸造者的状态和从未参与过完全一样,只少了一笔矿工费。名称本身则变成一块墓碑:永久注册,永不可铸。如果你打算用一个自己已拥有的名称来发射,这一点值得权衡 — 发射失败会把名称和发射一起埋葬。", "it has circulating supply — every unit must be destroyed first": "它仍有流通供应量 — 必须先销毁每一枚", @@ -610,6 +613,7 @@ "latest launch {age}": "最近一次发射 {age}", "Launch": "发射", "Launch {name}": "发射 {name}", + "Launch #{n}": "第 {n} 次发射", "Launch a token": "发射代币", "Launch dynamics": "发射动态", "Launch dynamics — xcp.fun": "发射动态 — xcp.fun", @@ -828,6 +832,7 @@ "Open menu": "打开菜单", "open right now": "当前进行中", "Open XCP price": "打开 XCP 价格", + "open@@bounty": "待领取", "open@@pool slider": "开盘", "Opening the graveyard…": "正在打开墓地…", "opens": "开放", @@ -886,6 +891,7 @@ "Portfolio value over time, currently {value}": "组合价值随时间的变化,当前为 {value}", "Positions": "仓位", "Positions you fully exited, with the profit or loss actually taken. A dash means the focused mint-and-trade history does not explain the live zero balance, so no P&L is claimed. Basis is average-cost, so a partial sale realizes its share and leaves the rest with the tokens still held.": "你已完全退出的仓位,以及实际落袋的盈亏。短横线表示已索引的铸造与交易历史无法解释当前的零余额,因此不给出盈亏。成本按平均成本法计算,部分卖出只实现其对应份额,其余成本留在仍持有的代币上。", + "Positions, open mints, history, and launches for {address} on xcp.fun.": "{address} 在 xcp.fun 上的仓位、进行中的铸造、历史记录和发射。", "Premine / commission": "预挖 / 佣金", "Preparing inscription…": "正在准备铭刻…", "Price": "价格", @@ -989,7 +995,7 @@ "Share on X": "分享到 X", "Share this launch": "分享这次发射", "Share unavailable": "无法获取份额", - "Show amounts in US dollars": "以美元显示金额", + "Show amounts in {currency}": "以 {currency} 显示金额", "Show amounts in XCP": "以 XCP 显示金额", "Show less": "收起", "Show more": "展开更多", diff --git a/apps/web/src/locales/zh.status.json b/apps/web/src/locales/zh.status.json index 9f7a996e..4c75b7b4 100644 --- a/apps/web/src/locales/zh.status.json +++ b/apps/web/src/locales/zh.status.json @@ -89,7 +89,6 @@ "Bitcoin", "Bitcoin Price", "Bitcoin fee rate in sats per vbyte", - "Bitcoin market price in USD", "Bitcoin tx fee", "Block {n}", "Bought", @@ -179,13 +178,11 @@ "Couldn't reach the mint index—try again shortly.", "Couldn't refresh this page. Showing the last version that loaded.", "Counterparty", - "Counterparty market price in USD", "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.", "Counterparty's canonical unspendable burn address", "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.", "Covered", "Create", - "Create a launch", "Created", "Creator", "Creator bought — block {n}", @@ -627,7 +624,6 @@ "Share on X", "Share this launch", "Share unavailable", - "Show amounts in US dollars", "Show amounts in XCP", "Show less", "Show more", @@ -849,7 +845,6 @@ "cash P/L (XCP-equivalent)", "checking…", "claimed", - "closes", "closing", "collections with a minter in them", "committed", @@ -1126,7 +1121,6 @@ "{n}w ago", "{n}y ago", "{n}× apart", - "{ordinal} launch", "{pct} held by dumpers", "{pct} of supply", "{pct}% filled", @@ -1201,6 +1195,16 @@ "◆ Pool", "✓ Graduated — pool created with {xcp} XCP{usd} + 31M tokens, LP burned.", "✓ broadcast", - "💀 RIP" + "💀 RIP", + "Bitcoin market price in {currency}", + "Counterparty market price in {currency}", + "Holds {count} token", + "Holds {count} tokens", + "Issued {count} token", + "Issued {count} tokens", + "Launch #{n}", + "Positions, open mints, history, and launches for {address} on xcp.fun.", + "Show amounts in {currency}", + "open@@bounty" ] } diff --git a/tests/compose-amount-safety.test.ts b/tests/compose-amount-safety.test.ts index 8e336c88..7bd264e2 100644 --- a/tests/compose-amount-safety.test.ts +++ b/tests/compose-amount-safety.test.ts @@ -1,96 +1,141 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { composeAndBroadcast } from "@xcp/wallet-sdk"; import { parseUnitsToRaw } from "../apps/web/src/lib/numeric"; -import { normalizeMarks, sanitizeAmount } from "../apps/web/src/components/amount-input"; -import { commas, fixed } from "../apps/web/src/lib/format"; +import { AmountInput, sanitizeAmount } from "../apps/web/src/components/amount-input"; /** - * A number someone types must compose as the number they typed. - * - * Not a hundred times it and not a hundredth. Everything else on the page is - * written in the reader's language — a French page groups with spaces and - * marks the decimal with a comma — and the danger of doing that well is that - * a display string can find its way into a field that a parser reads back. - * Then the amount that gets signed is not the amount that was agreed to. - * - * Two rules keep those apart, and these are the tests for them. - * - * 1. What reaches a parser is plain: digits, at most one period, no - * grouping, no language. `parseUnitsToRaw` enforces it and returns null - * on anything else, so a localized string FAILS rather than being - * quietly reinterpreted. - * 2. What a person types is normalized to that shape at the input, so - * typing a comma for a decimal is understood rather than dropped. + * Contract verified against local Counterparty Core 67e10db3: + * api/apiserver.py prepare_args parses quantities with int(str_arg). + * api/compose.py declares order/fairmint/pool quantities as raw integers; + * config.UNIT is 100_000_000. sat_per_vbyte is a separate float. + * These tests reach the installed wallet SDK's actual compose query builder. + * The mocked HTTP error stops before signing or broadcasting anything. */ +afterEach(() => { vi.unstubAllGlobals(); }); -describe("the parser refuses anything a language wrote", () => { - it("takes the plain forms", () => { - expect(parseUnitsToRaw("1.5")).toBe(150_000_000n); - expect(parseUnitsToRaw("1234.56")).toBe(123_456_000_000n); - expect(parseUnitsToRaw("0.00000001")).toBe(1n); - expect(parseUnitsToRaw("21000000")).toBe(2_100_000_000_000_000n); +function enter(value: string, inputType = "insertText", previous = "", decimals: 0 | 8 = 8, data = value.slice(-1)) { + const onChange = vi.fn(); + const input = AmountInput({ value: previous, onChange, decimals }); + input.props.onChange({ target: { value }, nativeEvent: { inputType, data } }); + return { value: onChange.mock.calls[0]?.[0] as string | undefined, onChange }; +} + +function typeAmount(typed: string, decimals: 0 | 8 = 8): string { + let value = ""; + for (const char of typed) { + const next = enter(value + char, "insertText", value, decimals); + expect(next.onChange, `keystroke ${JSON.stringify(char)} after ${value}`).toHaveBeenCalledOnce(); + value = next.value!; + } + return value; +} + +describe("an accepted amount retains its value", () => { + it.each([ + ["0.001", 100_000n], + ["1.234", 123_400_000n], + ["0.00000001", 1n], + ["100000000.00000001", 10_000_000_000_000_001n], + ] as const)("types %s without treating its third decimal as grouping", (typed, raw) => { + expect(typeAmount(typed)).toBe(typed); + expect(parseUnitsToRaw(typeAmount(typed))).toBe(raw); }); - /** - * The property that matters. A grouped or comma-decimal string is not a - * number this accepts, so there is no reading of it to get wrong: it is - * null, and every caller already treats null as "no amount yet". - */ - it("returns null rather than a wrong number for every localized form", () => { - const localized = [ - "1,5", // French, Portuguese, Russian: one and a half - "1234,56", - "1 234,56", // grouped with a space - "1.234,56", // grouped with a period - "1,234.56", // grouped with a comma - "1 234,56", // the no-break space CLDR actually emits - "1 234,56", // and the narrow one French uses - ]; - for (const input of localized) expect(parseUnitsToRaw(input)).toBeNull(); + it("normalizes a decimal keyboard comma before it reaches state", () => { + expect(typeAmount("0,001")).toBe("0.001"); + expect(parseUnitsToRaw(typeAmount("0,00000001"))).toBe(1n); + expect(typeAmount("1,234")).toBe("1.234"); + expect(enter("1,234", "insertText", "", 8, "1,234").onChange).not.toHaveBeenCalled(); }); - it("in particular never turns a decimal comma into a hundredfold", () => { - // The failure this file exists to prevent, stated as the assertion. - expect(parseUnitsToRaw("1234,56")).not.toBe(parseUnitsToRaw("123456")); - expect(parseUnitsToRaw("1234,56")).toBeNull(); + it("keeps editable decimal states", () => { + expect(sanitizeAmount("")).toBe(""); + expect(sanitizeAmount(".")).toBe("0."); + expect(sanitizeAmount("5.")).toBe("5."); + expect(sanitizeAmount(".5")).toBe(".5"); }); -}); -describe("the input normalizes to that shape before anything parses it", () => { - it("reads a typed decimal comma as a decimal", () => { - expect(sanitizeAmount("0,5")).toBe("0.5"); - expect(parseUnitsToRaw(sanitizeAmount("0,5")!)).toBe(50_000_000n); + it.each([ + "1,234", "1,234.56", "1.234,56", "1.234.567", "1,234,567", + "1 234.56", "1\u00a0234,56", "1\u202f234,56", "0,5", " 1.5 ", + "-1", "+1", "1e5", "1e-8", "NaN", "Infinity", "$12", "12 XCP", "1.2.3", "1.5", "١.٥", + ])("rejects ambiguous or unsupported paste %s", (text) => { + expect(sanitizeAmount(text)).toBeNull(); + for (const inputType of ["insertFromPaste", "insertFromDrop"]) { + expect(enter(text, inputType, "2").onChange).not.toHaveBeenCalled(); + } }); - it("reads a pasted grouped number in either convention", () => { - expect(normalizeMarks("1,234.56")).toBe("1234.56"); - expect(normalizeMarks("1.234,56")).toBe("1234.56"); - expect(parseUnitsToRaw(normalizeMarks("1.234,56"))).toBe(123_456_000_000n); + it("accepts exact plain paste and rejects a ninth decimal", () => { + expect(enter("100000000.00000001", "insertFromPaste").value).toBe("100000000.00000001"); + expect(enter("0.000000001", "insertText", "0.00000000").onChange).not.toHaveBeenCalled(); + expect(sanitizeAmount("1.999999999")).toBeNull(); }); - it("hands the parser something it accepts, for every shape a person types", () => { - for (const typed of ["0,5", "0.5", "1.234,56", "1,234.56", "100", "0.00000001"]) { - const value = sanitizeAmount(typed); - expect(value).not.toBeNull(); - expect(parseUnitsToRaw(value!)).not.toBeNull(); - } + it("rejects an overlong paste as a whole instead of letting the browser truncate its value", () => { + const input = AmountInput({ value: "2", onChange: vi.fn() }); + expect(input.props.maxLength).toBeUndefined(); + const text = `${"0".repeat(26)}1`; + expect(parseUnitsToRaw(text)).toBe(100_000_000n); + expect(enter(text, "insertFromPaste", "2").onChange).not.toHaveBeenCalled(); }); -}); -describe("display formatting never produces a composable amount", () => { - /** - * The guard against the two being confused later. A display formatter's - * output is not something the parser accepts, in any locale that writes - * numbers differently — so wiring one into a field cannot silently work. - */ - it("is rejected by the parser wherever the language differs from English", () => { - for (const locale of ["pt", "fr", "ru", "uk"]) { - expect(parseUnitsToRaw(commas(1234.56, locale))).toBeNull(); - expect(parseUnitsToRaw(fixed(1234.56, 2, locale))).toBeNull(); - } + it.each(["1\n234", "1\r234", "1\r\n234", "1,234", "1.234,56", "1e5"])( + "rejects the original clipboard/drop text %j before the browser can sanitize it", (text) => { + const input = AmountInput({ value: "2", onChange: vi.fn() }); + const preventPaste = vi.fn(); + const preventDrop = vi.fn(); + input.props.onPaste({ clipboardData: { getData: () => text }, preventDefault: preventPaste }); + input.props.onDrop({ dataTransfer: { getData: () => text }, preventDefault: preventDrop }); + expect(preventPaste).toHaveBeenCalledOnce(); + expect(preventDrop).toHaveBeenCalledOnce(); + }, + ); + + it("lets a plain decimal paste reach the normal exact-input validation", () => { + const input = AmountInput({ value: "", onChange: vi.fn() }); + const preventDefault = vi.fn(); + input.props.onPaste({ clipboardData: { getData: () => "0.00000001" }, preventDefault }); + expect(preventDefault).not.toHaveBeenCalled(); + expect(enter("0.00000001", "insertFromPaste").value).toBe("0.00000001"); }); - it("and English display grouping is refused too, rather than half-read", () => { - expect(commas(1234.56, "en")).toBe("1,234.56"); - expect(parseUnitsToRaw(commas(1234.56, "en"))).toBeNull(); + it("keeps raw satoshi fields integral", () => { + expect(typeAmount("1234", 0)).toBe("1234"); + expect(parseUnitsToRaw(typeAmount("1234", 0), 0)).toBe(1234n); + expect(sanitizeAmount("1.5", 0)).toBeNull(); + expect(enter("1,", "insertText", "1", 0).onChange).not.toHaveBeenCalled(); + }); +}); + +describe("the actual compose HTTP parameters", () => { + it.each([ + ["0.001", "100000"], + ["0.00000001", "1"], + ["100000000.00000001", "10000000000000001"], + ])("sends the exact raw digits for %s, with fractional fees separate", async (typed, expected) => { + const fetch = vi.fn(async () => new Response(JSON.stringify({ error: "test boundary reached" }), { status: 400 })); + vi.stubGlobal("fetch", fetch); + const signer = { + address: "1CounterpartyXXXXXXXXXXXXXXXUWLpVr", + signTransaction: vi.fn(), + broadcastTransaction: vi.fn(), + } as unknown as Parameters[0]; + await expect(composeAndBroadcast(signer, "order", { + give_asset: "XCP", + give_quantity: parseUnitsToRaw(typeAmount(typed))!, + get_asset: "TOKEN", + get_quantity: 1n, + expiration: 100, + fee_required: 0, + }, { feeRate: 0.1 })).rejects.toThrow("test boundary reached"); + expect(fetch).toHaveBeenCalledOnce(); + const url = new URL(String(fetch.mock.calls[0]![0])); + expect(url.pathname).toContain("/compose/order"); + expect(url.searchParams.get("give_quantity")).toBe(expected); + expect(url.searchParams.get("get_quantity")).toBe("1"); + expect(url.searchParams.get("sat_per_vbyte")).toBe("0.1"); + expect(signer.signTransaction).not.toHaveBeenCalled(); + expect(signer.broadcastTransaction).not.toHaveBeenCalled(); }); }); diff --git a/tests/currency.test.ts b/tests/currency.test.ts new file mode 100644 index 00000000..ce827eac --- /dev/null +++ b/tests/currency.test.ts @@ -0,0 +1,242 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { CurrencyState } from "@/lib/currency"; + +const mocked = vi.hoisted(() => ({ + fetchFxRates: vi.fn(), + subscribe: null as ((listener: () => void) => () => void) | null, + readServer: null as (() => CurrencyState) | null, +})); + +// Exercise the actual external store without a DOM renderer. Mounting and +// unmounting below use the subscription React receives; reads remain separate +// so a render cannot accidentally conceal a request or retry loop. +vi.mock("react", () => ({ + useCallback: (callback: unknown) => callback, + useSyncExternalStore: ( + subscribe: (listener: () => void) => () => void, + read: () => CurrencyState, + readServer: () => CurrencyState, + ) => { + mocked.subscribe = subscribe; + mocked.readServer = readServer; + return read(); + }, +})); +vi.mock("@/lib/analytics", () => ({ trackEvent: vi.fn() })); +vi.mock("@/lib/api/launchpad-api", () => ({ fetchFxRates: mocked.fetchFxRates })); +vi.mock("@/lib/i18n/client", () => ({ useLocale: () => "en" })); + +const PREF_KEY = "xcpfun:currency:v1"; +const FX_KEY = "xcpfun:fx:v1"; +const HALF_DAY = 12 * 60 * 60 * 1000; +const rates = (jpy = 150) => ({ base: "USD", date: "2026-09-07", rates: { JPY: jpy, EUR: 0.9 } }); +let data: Map; +let storage: { getItem: ReturnType; setItem: ReturnType; removeItem: ReturnType }; +let browser: { languages: string[]; language: string }; +let page: { pathname: string }; +let cleanups: (() => void)[]; + +beforeEach(() => { + vi.resetModules(); + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-09-07T12:00:00Z")); + mocked.fetchFxRates.mockReset().mockResolvedValue(rates()); + mocked.subscribe = null; + mocked.readServer = null; + data = new Map(); + storage = { + getItem: vi.fn((key: string) => data.get(key) ?? null), + setItem: vi.fn((key: string, value: string) => { data.set(key, value); }), + removeItem: vi.fn((key: string) => { data.delete(key); }), + }; + browser = { languages: ["en-US"], language: "en-US" }; + page = { pathname: "/" }; + cleanups = []; + vi.stubGlobal("window", new EventTarget()); + vi.stubGlobal("navigator", browser); + vi.stubGlobal("location", page); + vi.stubGlobal("localStorage", storage); + vi.spyOn(Intl.DateTimeFormat.prototype, "resolvedOptions").mockReturnValue({ timeZone: "America/New_York" } as Intl.ResolvedDateTimeFormatOptions); +}); + +afterEach(() => { + cleanups.forEach((cleanup) => cleanup()); + vi.clearAllTimers(); + vi.useRealTimers(); + vi.restoreAllMocks(); + vi.unstubAllGlobals(); +}); + +async function load() { + return import("@/lib/currency"); +} + +function mount(api: Awaited>) { + api.useCurrency(); + const changed = vi.fn(() => { api.useCurrency(); }); + const cleanup = mocked.subscribe!(changed); + cleanups.push(cleanup); + return { changed, cleanup }; +} + +function cached(jpy: number, age = 0) { + return JSON.stringify({ fetchedAt: Date.now() - age, date: "2026-09-04", rates: { JPY: jpy } }); +} + +function storageEvent(key: string | null, newValue: string | null) { + const event = Object.assign(new Event("storage"), { key, newValue }); + window.dispatchEvent(event); +} + +describe("currency preferences", () => { + it("keeps page language independent of Auto and preserves explicit USD", async () => { + page.pathname = "/ja"; + const api = await load(); + mount(api); + expect(api.useCurrency()).toMatchObject({ code: "USD", auto: true }); + expect(mocked.fetchFxRates).not.toHaveBeenCalled(); + + api.setCurrency("USD"); + browser.languages = ["ja-JP"]; + page.pathname = "/fr"; + window.dispatchEvent(new Event("languagechange")); + expect(api.useCurrency()).toMatchObject({ code: "USD", auto: false, detected: "JPY" }); + expect(mocked.fetchFxRates).not.toHaveBeenCalled(); + + api.setCurrency("auto"); + await vi.advanceTimersByTimeAsync(0); + expect(api.useCurrency()).toMatchObject({ code: "JPY", auto: true, rate: 150 }); + }); + + it("uses currency choices and fetched rates when storage is blocked", async () => { + const blocked = () => { throw new Error("SecurityError"); }; + storage.getItem.mockImplementation(blocked); + storage.setItem.mockImplementation(blocked); + storage.removeItem.mockImplementation(blocked); + const api = await load(); + mount(api); + api.setCurrency("JPY"); + await vi.advanceTimersByTimeAsync(0); + expect(api.useCurrency()).toMatchObject({ code: "JPY", auto: false, rate: 150 }); + expect(api.useFiat()(2)).toBe("¥300"); + window.dispatchEvent(new Event("languagechange")); + expect(api.useFxRate()).toEqual({ code: "JPY", rate: 150 }); + api.setCurrency("auto"); + expect(api.useCurrency()).toMatchObject({ code: "USD", auto: true }); + }); + + it("retains an unsaved preference when reads work but the storage quota is full", async () => { + data.set(PREF_KEY, "USD"); + storage.setItem.mockImplementation(() => { throw new Error("QuotaExceededError"); }); + const api = await load(); + mount(api); + api.setCurrency("JPY"); + await vi.advanceTimersByTimeAsync(0); + window.dispatchEvent(new Event("languagechange")); + expect(data.get(PREF_KEY)).toBe("USD"); + expect(api.useCurrency()).toMatchObject({ code: "JPY", auto: false, rate: 150 }); + }); + + it("applies cross-tab choices, FX updates, and storage.clear", async () => { + const api = await load(); + const { changed } = mount(api); + storageEvent(FX_KEY, cached(140)); + storageEvent(PREF_KEY, "JPY"); + expect(api.useFxRate()).toEqual({ code: "JPY", rate: 140 }); + storageEvent(FX_KEY, cached(155)); + expect(api.useFxRate()).toEqual({ code: "JPY", rate: 155 }); + storageEvent(null, null); + expect(api.useCurrency()).toMatchObject({ code: "USD", auto: true }); + expect(changed).toHaveBeenCalledTimes(4); + expect(mocked.fetchFxRates).not.toHaveBeenCalled(); + }); +}); + +describe("FX refresh lifecycle", () => { + it("fetches once for many subscribers, never during render, and waits on an incomplete fresh table", async () => { + data.set(PREF_KEY, "JPY"); + mocked.fetchFxRates.mockResolvedValue({ base: "USD", date: "2026-09-07", rates: { EUR: 0.9 } }); + const api = await load(); + api.useCurrency(); + expect(mocked.fetchFxRates).not.toHaveBeenCalled(); + for (let i = 0; i < 10; i++) mount(api); + await vi.advanceTimersByTimeAsync(0); + for (let i = 0; i < 100; i++) api.useCurrency(); + expect(mocked.fetchFxRates).toHaveBeenCalledTimes(1); + expect(api.useFxRate()).toEqual({ code: "USD", rate: 1 }); + expect(api.useCurrency().date).toBeNull(); + expect(vi.getTimerCount()).toBe(1); + await vi.advanceTimersByTimeAsync(HALF_DAY - 1); + expect(mocked.fetchFxRates).toHaveBeenCalledTimes(1); + }); + + it("refreshes a persisted rate when an open tab reaches the 12-hour TTL", async () => { + data.set(PREF_KEY, "JPY"); + data.set(FX_KEY, cached(140)); + const api = await load(); + mount(api); + expect(api.useFxRate()).toEqual({ code: "JPY", rate: 140 }); + expect(mocked.fetchFxRates).not.toHaveBeenCalled(); + await vi.advanceTimersByTimeAsync(HALF_DAY); + expect(mocked.fetchFxRates).toHaveBeenCalledTimes(1); + expect(api.useFxRate()).toEqual({ code: "JPY", rate: 150 }); + expect(vi.getTimerCount()).toBe(1); + }); + + it("rechecks an overdue rate on focus when background timers have not run", async () => { + data.set(PREF_KEY, "JPY"); + data.set(FX_KEY, cached(140)); + const api = await load(); + mount(api); + vi.setSystemTime(Date.now() + HALF_DAY + 1); + window.dispatchEvent(new Event("focus")); + await vi.advanceTimersByTimeAsync(0); + expect(mocked.fetchFxRates).toHaveBeenCalledTimes(1); + expect(api.useFxRate()).toEqual({ code: "JPY", rate: 150 }); + }); + + it("recovers from a first-load failure after a bounded retry delay", async () => { + data.set(PREF_KEY, "JPY"); + mocked.fetchFxRates.mockResolvedValueOnce(null); + const api = await load(); + mount(api); + await vi.advanceTimersByTimeAsync(0); + expect(api.useFxRate()).toEqual({ code: "USD", rate: 1 }); + for (let i = 0; i < 100; i++) api.useCurrency(); + window.dispatchEvent(new Event("online")); + await vi.advanceTimersByTimeAsync(59_999); + expect(mocked.fetchFxRates).toHaveBeenCalledTimes(1); + await vi.advanceTimersByTimeAsync(1); + expect(mocked.fetchFxRates).toHaveBeenCalledTimes(2); + expect(api.useFxRate()).toEqual({ code: "JPY", rate: 150 }); + }); + + it("keeps a usable old rate through a failed refresh and replaces it on retry", async () => { + data.set(PREF_KEY, "JPY"); + data.set(FX_KEY, cached(140, HALF_DAY)); + mocked.fetchFxRates.mockRejectedValueOnce(new Error("offline")); + const api = await load(); + mount(api); + await vi.advanceTimersByTimeAsync(0); + expect(api.useCurrency()).toMatchObject({ rate: 140, date: "2026-09-04" }); + await vi.advanceTimersByTimeAsync(60_000); + expect(api.useCurrency()).toMatchObject({ rate: 150, date: "2026-09-07" }); + }); + + it("stops background refreshes with no subscribers and keeps SSR in dollars", async () => { + data.set(PREF_KEY, "JPY"); + const api = await load(); + const { cleanup } = mount(api); + cleanup(); + await vi.advanceTimersByTimeAsync(0); + expect(vi.getTimerCount()).toBe(0); + expect(mocked.readServer!()).toMatchObject({ code: "USD", rate: 1 }); + await vi.advanceTimersByTimeAsync(HALF_DAY); + expect(mocked.fetchFxRates).toHaveBeenCalledTimes(1); + mount(api); + await vi.advanceTimersByTimeAsync(0); + expect(mocked.fetchFxRates).toHaveBeenCalledTimes(2); + api.setCurrency("USD"); + expect(vi.getTimerCount()).toBe(0); + }); +}); diff --git a/tests/i18n-catalog.test.ts b/tests/i18n-catalog.test.ts new file mode 100644 index 00000000..db8c1eeb --- /dev/null +++ b/tests/i18n-catalog.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from "vitest"; +import { collectMessages, mergeDerivedMessages } from "../apps/web/scripts/i18n-catalog.mjs"; + +describe("translation extraction", () => { + it("collects conditional rich text and the runtime context argument positions", () => { + const { keys } = collectMessages(` + rich(t, held === 1 ? "Holds {count} token" : "Holds {count} tokens", { count: {held} }); + rich(t, issued?.count === 1 && !issued.capped ? "Issued {count} token" : "Issued {count} tokens", { count: {issued.count} }); + t("open", undefined, "bounty"); + t("All", "window"); + t("{n} open", { n: count }, "orders"); + rich(t, "Open {link}", { link: link }, "action"); + `); + expect(keys.sort()).toEqual([ + "Holds {count} token", "Holds {count} tokens", "Issued {count} token", "Issued {count} tokens", + "open@@bounty", "All@@window", "{n} open@@orders", "Open {link}@@action", + ].sort()); + }); + + it("reads the same escaped strings as JavaScript, ignores comments, and reports indirect labels", () => { + const { keys, indirect } = collectMessages(String.raw` + // t("Comment only") + /* rich(t, "Documentation only", {}) */ + msg("Saved\nlabel"); + t("Quote: \"ok\""); + t ("Whitespace before paren"); + t(count === 1 ? "One" : (count === 0 ? "None" : "Many")); + t(item.label, { n: count }); + `); + expect(keys.sort()).toEqual(["Saved\nlabel", 'Quote: "ok"', "Whitespace before paren", "One", "None", "Many"].sort()); + expect(indirect).toEqual(["item.label"]); + }); +}); + +describe("Traditional Chinese draft regeneration", () => { + it("keeps reviewed regional edits and status metadata while refreshing machine drafts", () => { + const existing = { reviewed: "台灣用語", machine: "Old draft", regional: "香港用語" }; + const status = { machine: ["machine", "removed"], reviewer: "regional review" }; + const result = mergeDerivedMessages( + { reviewed: "Automatic replacement", machine: "New draft", added: "New entry" }, + existing, + status, + ); + expect(result.messages).toEqual({ reviewed: "台灣用語", machine: "New draft", regional: "香港用語", added: "New entry" }); + expect(result.status).toEqual({ machine: ["machine", "added"], reviewer: "regional review" }); + expect(existing.reviewed).toBe("台灣用語"); + expect(status.machine).toEqual(["machine", "removed"]); + }); + + it("treats existing entries without a status record as reviewed and new entries as drafts", () => { + expect(mergeDerivedMessages({ old: "Generated", added: "New" }, { old: "Edited" })).toEqual({ + messages: { old: "Edited", added: "New" }, + status: { machine: ["added"] }, + }); + expect(mergeDerivedMessages({ first: "Draft" })).toEqual({ + messages: { first: "Draft" }, + status: { machine: ["first"] }, + }); + }); +}); diff --git a/tests/i18n-rich.test.tsx b/tests/i18n-rich.test.tsx index 83f31917..e574f0b0 100644 --- a/tests/i18n-rich.test.tsx +++ b/tests/i18n-rich.test.tsx @@ -2,6 +2,7 @@ import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it } from "vitest"; import { rich } from "../apps/web/src/lib/i18n/rich"; import { makeT } from "../apps/web/src/lib/i18n/t"; +import ja from "../apps/web/src/locales/ja.json"; describe("rich", () => { const t = makeT({ "Call {compose} once per {n} launches.": "{n} 回のローンチごとに {compose} を一度呼びます。" }); @@ -19,4 +20,18 @@ describe("rich", () => { ); expect(html).toBe("Call compose once per 3 launches."); }); + + it("translates the address card's conditional asset count and creator launch number", () => { + const japanese = makeT(ja); + for (const count of [1, 2]) { + const html = renderToStaticMarkup( + <>{rich(japanese, count === 1 ? "Holds {count} token" : "Holds {count} tokens", { + count: {count}, + })}, + ); + expect(html).toBe(`保有トークン:${count}種類`); + } + expect(japanese("Launch #{n}", { n: 2 })).toBe("2回目のローンチ"); + expect(japanese("open", undefined, "bounty")).toBe("未獲得"); + }); }); diff --git a/tests/localized-routes.test.ts b/tests/localized-routes.test.ts new file mode 100644 index 00000000..4ff6329c --- /dev/null +++ b/tests/localized-routes.test.ts @@ -0,0 +1,93 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { LOCALES, LOCALE_INFO, localePath } from "@/lib/i18n/locales"; +import { localeAlternates } from "@/lib/i18n/seo"; + +vi.mock("next/root-params", () => ({ lang: async () => "en" })); +vi.mock("@opennextjs/cloudflare", () => ({ getCloudflareContext: vi.fn() })); +vi.mock("@/app/[lang]/profile/_components/profile-view", () => ({ ProfileView: () => null })); +vi.mock("@/lib/api/launchpad-api", () => ({ fetchSearchIndex: vi.fn() })); + +import { generateMetadata as createMetadata } from "@/app/[lang]/create/layout"; +import { generateMetadata as profileMetadata } from "@/app/[lang]/profile/[address]/page"; +import HomePage from "@/app/[lang]/home/page"; +import LaunchesPage from "@/app/[lang]/launches/page"; +import sitemap from "@/app/sitemap"; +import { fetchSearchIndex } from "@/lib/api/launchpad-api"; + +const ADDRESS = "1CounterpartyXXXXXXXXXXXXXXXUWLpVr"; + +describe("localized page metadata", () => { + it.each(LOCALES)("identifies the create page and address profile in %s", async (lang) => { + const create = await createMetadata({ params: Promise.resolve({ lang }) }); + const profile = await profileMetadata({ params: Promise.resolve({ lang, address: ADDRESS }) }); + for (const [metadata, path] of [ + [create, "/create"], + [profile, `/profile/${ADDRESS}`], + ] as const) { + expect(metadata.alternates?.canonical).toBe(localePath(lang, path)); + expect(metadata.alternates?.languages).toEqual({ + ...Object.fromEntries(LOCALES.map((locale) => [LOCALE_INFO[locale].tag, localePath(locale, path)])), + "x-default": path, + }); + } + }); + + it("uses the create page's translated copy in link previews", async () => { + const metadata = await createMetadata({ params: Promise.resolve({ lang: "ja" }) }); + expect(metadata.title).toContain("ローンチ"); + expect(metadata.description).not.toContain("Name, image, description."); + }); + + it("does not advertise an invalid address as a canonical profile", async () => { + await expect(profileMetadata({ + params: Promise.resolve({ lang: "ja", address: "invalid-address" }), + })).rejects.toThrow("NEXT_HTTP_ERROR_FALLBACK;404"); + }); +}); + +describe("legacy profile redirects", () => { + it.each(LOCALES)("keeps the %s locale for both old routes", async (lang) => { + for (const page of [HomePage, LaunchesPage]) { + await expect(page({ params: Promise.resolve({ lang }) })).rejects.toMatchObject({ + digest: `NEXT_REDIRECT;replace;${localePath(lang, "/profile")};307;`, + }); + } + }); +}); + +describe("multilingual sitemap", () => { + beforeEach(() => { + vi.mocked(fetchSearchIndex).mockReset(); + }); + + it("lists each language URL and gives it the same reciprocal alternatives as its page", async () => { + vi.mocked(fetchSearchIndex).mockResolvedValue([{ asset: "PEPE" }] as Awaited>); + const rows = await sitemap(); + const urls = new Set(rows.map((row) => row.url)); + expect(urls.size).toBe(rows.length); + + for (const path of ["/", "/create", "/PEPE"]) { + for (const locale of LOCALES) { + const localized = localePath(locale, path); + const url = `https://xcp.fun${localized === "/" ? "" : localized}`; + const row = rows.find((candidate) => candidate.url === url); + expect(row, url).toBeDefined(); + const pageAlternates = localeAlternates(locale, path).languages!; + expect(row!.alternates?.languages).toEqual(Object.fromEntries( + Object.entries(pageAlternates).map(([tag, route]) => [tag, `https://xcp.fun${route === "/" ? "" : route}`]), + )); + for (const alternate of Object.values(row!.alternates!.languages!)) { + expect(urls.has(String(alternate)), `missing sitemap URL ${alternate}`).toBe(true); + } + } + } + }); + + it("keeps all static language versions when the asset index is unavailable", async () => { + vi.mocked(fetchSearchIndex).mockRejectedValue(new Error("index unavailable")); + const rows = await sitemap(); + for (const locale of LOCALES) { + expect(rows.some((row) => row.url === `https://xcp.fun${localePath(locale, "/create")}`)).toBe(true); + } + }); +}); diff --git a/tests/number-locale.test.ts b/tests/number-locale.test.ts index e31530ee..6b7b94d6 100644 --- a/tests/number-locale.test.ts +++ b/tests/number-locale.test.ts @@ -9,14 +9,11 @@ import { percent, price, } from "../apps/web/src/lib/format"; -import { normalizeMarks, sanitizeAmount } from "../apps/web/src/components/amount-input"; import { LOCALES } from "../apps/web/src/lib/i18n/locales"; /** - * A number is not language-neutral. These lock the two things that broke in - * production before the formatters took a locale: a French page grouping in - * English commas, and a Brazilian typing "0,5" into an amount field and - * getting zero. + * Display formatting follows the locale. Input-to-compose preservation is + * tested separately in compose-amount-safety.test.ts. */ describe("grouping and decimal marks follow the page", () => { @@ -122,34 +119,14 @@ describe("bindNumbers", () => { }); }); -describe("amount fields accept either decimal mark", () => { - it("normalizes a typed comma so a Brazilian does not send zero", () => { - expect(sanitizeAmount("0,5")).toBe("0.5"); - expect(sanitizeAmount("0,")).toBe("0."); - expect(sanitizeAmount("12,345678")).toBe("12.345678"); - }); - - it("reads a pasted grouped number in either convention", () => { - expect(normalizeMarks("1,234.56")).toBe("1234.56"); - expect(normalizeMarks("1.234,56")).toBe("1234.56"); - expect(normalizeMarks("1 234,56".replace(/ /g, ""))).toBe("1234.56"); - expect(sanitizeAmount("1.234.567")).toBe("1234567"); - expect(sanitizeAmount("1,234,567")).toBe("1234567"); - }); - - it("still lets a period amount through untouched", () => { - expect(sanitizeAmount("")).toBe(""); - expect(sanitizeAmount("5.")).toBe("5."); - expect(sanitizeAmount(".5")).toBe(".5"); - expect(sanitizeAmount("100")).toBe("100"); - }); - - it("treats a mistyped second mark as a slip, not as grouping", () => { - expect(sanitizeAmount("1.2.3")).toBe("1.23"); - }); - - it("rejects what is not a number at all", () => { - expect(sanitizeAmount("abc")).toBe(null); - expect(sanitizeAmount("1e5")).toBe("15"); +describe("small fiat amounts", () => { + it("uses the same decimal mark below and above one unit", () => { + for (const locale of ["fr", "pt", "ru", "uk"]) { + expect(fiat(12.34, "EUR", locale)).toContain("12,34"); + expect(fiat(1, "EUR", locale)).toContain("1,00"); + expect(fiat(0.12, "EUR", locale)).toContain("0,12"); + } + expect(fiat(12.34, "USD", "en")).toBe("$12.34"); + expect(fiat(12.34, "JPY", "ja")).toBe("¥12"); }); }); diff --git a/tests/trade-settings.test.tsx b/tests/trade-settings.test.tsx new file mode 100644 index 00000000..0ab3d6ac --- /dev/null +++ b/tests/trade-settings.test.tsx @@ -0,0 +1,41 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { SWRConfig } from "swr"; +import { describe, expect, it, vi } from "vitest"; +import { + SwapSettingsProvider, + useSwapSettings, +} from "../apps/web/src/app/[lang]/swap/_components/swap-settings"; + +const snapshot = vi.hoisted(() => ({ customFeeRate: "" })); + +// Supply the stored string at the external-store boundary while rendering +// the actual provider and consuming its derived fee through the actual hook. +vi.mock("@/app/[lang]/swap/_lib/trade-settings-store", async (importOriginal) => { + const store = await importOriginal(); + return { + ...store, + readSettingsServer: () => ({ ...store.SETTINGS_DEFAULTS, ...snapshot }), + }; +}); + +function FeeConsumer() { + const { customFee } = useSwapSettings(); + return {customFee}; +} + +describe("trade settings fee rate", () => { + it.each([ + ["0.1", "0.1"], + ["1.56", "1.56"], + ["", "0"], + ["600", "500"], + ])("exposes the stored %s sat/vB rate as %s", (typed, expected) => { + snapshot.customFeeRate = typed; + const html = renderToStaticMarkup( + new Map(), revalidateOnMount: false }}> + + , + ); + expect(html).toBe(`${expected}`); + }); +}); diff --git a/vitest.config.mts b/vitest.config.mts index 62cb724c..34fbbe81 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -35,6 +35,6 @@ export default defineConfig({ }, test: { environment: "node", - include: ["tests/**/*.test.ts"], + include: ["tests/**/*.test.{ts,tsx}"], }, }); From 9180b42799d2fc25813798b031ad35cd79ad47c9 Mon Sep 17 00:00:00 2001 From: Dan Anderson Date: Mon, 7 Sep 2026 16:27:46 -0400 Subject: [PATCH 02/10] Preserve exact compose intent across forms and display preferences --- apps/web/package.json | 2 +- .../[lang]/[asset]/_components/mint-panel.tsx | 81 ++-- .../[asset]/_components/price-chart.tsx | 11 +- .../[asset]/_components/trade-panel.tsx | 132 +++---- .../app/[lang]/_components/market-pulse.tsx | 11 +- apps/web/src/app/[lang]/create/page.tsx | 18 +- .../[lang]/dispense/_components/bridge.tsx | 76 ++-- .../dispense/_lib/use-dispense-router.ts | 122 +++++- .../rewards/_components/rewards-content.tsx | 359 ++++++++++++++++++ apps/web/src/app/[lang]/rewards/page.tsx | 345 +---------------- .../[lang]/stats/_components/communities.tsx | 11 +- .../stats/_components/stats-content.tsx | 307 +++++++++++++++ apps/web/src/app/[lang]/stats/page.tsx | 307 +-------------- .../swap/_components/liquidity-widget.tsx | 244 ++++++------ .../[lang]/swap/_components/swap-settings.tsx | 56 ++- .../[lang]/swap/_components/swap-widget.tsx | 145 ++++--- apps/web/src/components/amount-input.tsx | 118 +++--- apps/web/src/components/compose-error.tsx | 39 ++ apps/web/src/components/number-preference.tsx | 24 ++ apps/web/src/components/site-footer.tsx | 2 + apps/web/src/components/ui/error-banner.tsx | 6 +- apps/web/src/components/ui/well.tsx | 2 +- apps/web/src/lib/amount-draft.ts | 33 ++ apps/web/src/lib/currency.ts | 4 +- apps/web/src/lib/format.ts | 4 +- apps/web/src/lib/i18n/locales.ts | 8 +- apps/web/src/lib/i18n/number-locales.ts | 13 + apps/web/src/lib/i18n/numbers.ts | 4 +- apps/web/src/lib/inscribe-launch.ts | 48 ++- apps/web/src/lib/inscriber/transactions.ts | 4 +- apps/web/src/lib/limit-amounts.ts | 15 + apps/web/src/lib/number-preference.ts | 54 +++ apps/web/src/lib/quote-validation.ts | 54 +++ apps/web/src/lib/transaction-verification.ts | 40 ++ apps/web/src/locales/_source.json | 242 +++++++----- apps/web/src/locales/es.json | 16 + apps/web/src/locales/es.status.json | 18 +- apps/web/src/locales/fr.json | 16 + apps/web/src/locales/fr.status.json | 18 +- apps/web/src/locales/ja.json | 16 + apps/web/src/locales/ja.status.json | 18 +- apps/web/src/locales/ko.json | 16 + apps/web/src/locales/ko.status.json | 18 +- apps/web/src/locales/pt.json | 16 + apps/web/src/locales/pt.status.json | 18 +- apps/web/src/locales/ru.json | 16 + apps/web/src/locales/ru.status.json | 18 +- apps/web/src/locales/uk.json | 16 + apps/web/src/locales/uk.status.json | 18 +- apps/web/src/locales/zh-hk.json | 16 + apps/web/src/locales/zh-hk.status.json | 18 +- apps/web/src/locales/zh-tw.json | 16 + apps/web/src/locales/zh-tw.status.json | 18 +- apps/web/src/locales/zh.json | 16 + apps/web/src/locales/zh.status.json | 18 +- docs/amount-and-localization-contract.md | 45 +++ package-lock.json | 79 +++- package.json | 1 + tests/compose-amount-safety.test.ts | 141 ------- tests/compose-amount-safety.test.tsx | 140 +++++++ tests/dispense-router-intent.test.tsx | 110 ++++++ tests/dispense-router.test.ts | 120 +++++- tests/historical-currency.test.tsx | 45 +++ tests/inscription-safety.test.ts | 151 ++++++++ tests/limit-amounts.test.ts | 34 ++ tests/presentation-preferences.test.tsx | 82 ++++ tests/quote-validation.test.ts | 24 ++ tests/submitted-pending-intent.test.tsx | 198 ++++++++++ tests/swap-intent.test.tsx | 99 +++++ tests/trade-settings.test.tsx | 14 +- vitest.config.mts | 10 +- 71 files changed, 3196 insertions(+), 1378 deletions(-) create mode 100644 apps/web/src/app/[lang]/rewards/_components/rewards-content.tsx create mode 100644 apps/web/src/app/[lang]/stats/_components/stats-content.tsx create mode 100644 apps/web/src/components/compose-error.tsx create mode 100644 apps/web/src/components/number-preference.tsx create mode 100644 apps/web/src/lib/amount-draft.ts create mode 100644 apps/web/src/lib/i18n/number-locales.ts create mode 100644 apps/web/src/lib/limit-amounts.ts create mode 100644 apps/web/src/lib/number-preference.ts create mode 100644 apps/web/src/lib/quote-validation.ts create mode 100644 apps/web/src/lib/transaction-verification.ts create mode 100644 docs/amount-and-localization-contract.md delete mode 100644 tests/compose-amount-safety.test.ts create mode 100644 tests/compose-amount-safety.test.tsx create mode 100644 tests/dispense-router-intent.test.tsx create mode 100644 tests/historical-currency.test.tsx create mode 100644 tests/inscription-safety.test.ts create mode 100644 tests/limit-amounts.test.ts create mode 100644 tests/presentation-preferences.test.tsx create mode 100644 tests/quote-validation.test.ts create mode 100644 tests/submitted-pending-intent.test.tsx create mode 100644 tests/swap-intent.test.tsx diff --git a/apps/web/package.json b/apps/web/package.json index 6b86b947..3f0d1463 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -19,7 +19,7 @@ "@noble/hashes": "^1.8.0", "@scure/base": "^2.2.0", "@scure/btc-signer": "^2.2.0", - "@xcp/wallet-sdk": "github:XCP/wallet-sdk#v0.4.1", + "@xcp/wallet-sdk": "github:XCP/wallet-sdk#79f190308d4b60cc3ec9023beada21e0ee776213", "babel-plugin-react-compiler": "^1.0.0", "bignumber.js": "^9.3.1", "next": "16.3.4", diff --git a/apps/web/src/app/[lang]/[asset]/_components/mint-panel.tsx b/apps/web/src/app/[lang]/[asset]/_components/mint-panel.tsx index 6e915859..0d65e577 100644 --- a/apps/web/src/app/[lang]/[asset]/_components/mint-panel.tsx +++ b/apps/web/src/app/[lang]/[asset]/_components/mint-panel.tsx @@ -1,7 +1,12 @@ "use client"; +import { ComposeError } from "@/components/compose-error"; + +import { AmountInput } from "@/components/amount-input"; +import { parseBoundedSetting } from "@/lib/amount-draft"; + import { LazyLink } from "@/components/lazy-link"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import useSWR from "swr"; import { AssetChip } from "@/components/asset-chip"; import { ConnectButton } from "@/components/connect-button"; @@ -52,6 +57,10 @@ export function MintPanel({ const usdFmt = useFiat(); const { address, status: walletStatus } = useWallet(); const compose = useCompose(); + const submittedMint = useRef<{ + pending: Omit[0], "txid">; + usd: number | null; + } | null>(null); const [tokens, setTokens] = useState("10000"); // The conforming fairminter for this ticker, read once and shared by both @@ -128,7 +137,8 @@ export function MintPanel({ const addressCapped = alreadyMintedRaw !== undefined && alreadyMintedRaw !== null && addressLots === 0; - const typedTokens = parseFloat(tokens) || 0; + const tokenDraft = parseBoundedSetting(tokens, maxLots * TOKENS_PER_LOT, 0, 0); + const typedTokens = tokenDraft.value ?? 0; const lots = Math.max(0, Math.min(maxLots, Math.floor(typedTokens / TOKENS_PER_LOT))); const mintTokens = lots * TOKENS_PER_LOT; const adjusted = typedTokens > 0 && mintTokens !== typedTokens; @@ -155,35 +165,20 @@ export function MintPanel({ const busy = isBusy(compose.status); useEffect(() => { - if (compose.status === "confirmed") { - registerPending({ - txid: compose.txid, - kind: "mint", - label: t("Mint {amount} {asset}", { amount: num.commas(mintTokens), asset }), - address: address ?? undefined, - spends: [{ asset: "XCP", raw: costRaw.toString() }], - }); - // The XCP escrowed, valued at the rate the panel just quoted. - trackTx(compose.txid, "mint", xcpUsd ? costXcp * xcpUsd : null); + if (compose.status === "error") submittedMint.current = null; + if (compose.status === "confirmed" && submittedMint.current) { + const submitted = submittedMint.current; + submittedMint.current = null; + registerPending({ ...submitted.pending, txid: compose.txid }); + trackTx(compose.txid, "mint", submitted.usd); } - }, [ - compose.status, - compose.txid, - mintTokens, - asset, - address, - costRaw, - costXcp, - xcpUsd, - t, - num, - ]); + }, [compose.status, compose.txid]); // A balance that could not be read does not block the mint — see // useSpendableBalance's balanceUnavailable. Only a read still in flight // holds the button, and only briefly. const balanceSettled = xcpBalance !== undefined || balanceUnavailable; - const ready = balanceSettled && lots > 0 && !busy && !insufficient; + const ready = tokenDraft.valid && !tokenDraft.empty && !adjusted && balanceSettled && lots > 0 && !busy && !insufficient; const buttonLabel = busy ? compose.status === "composing" ? t("Composing…") @@ -249,18 +244,14 @@ export function MintPanel({ } > - {/* Integer-only, so the input can carry real digit grouping — in the - page's own separator, since every non-digit is stripped straight - back out on the way into state. Decimal AmountInputs cannot do - this: there a comma has to stay available as a decimal point. */} - 0 ? num.commas(typedTokens) : tokens} - onChange={(e) => setTokens(e.target.value.replace(/[^0-9]/g, ""))} + @@ -348,19 +339,29 @@ export function MintPanel({
{compose.status === "error" && ( - {compose.error} + )} {walletStatus !== "connected" ? ( ) : ( + onClick={() => { + if (!ready || submittedMint.current) return; + submittedMint.current = { + pending: { + kind: "mint", + label: t("Mint {amount} {asset}", { amount: num.commas(mintTokens), asset }), + address: address ?? undefined, + spends: [{ asset: "XCP", raw: costRaw.toString() }], + }, + usd: xcpUsd ? costXcp * xcpUsd : null, + }; compose.composeFairmint({ asset, quantity: lots * XCP69.QUANTITY_BY_PRICE, - }) - } + }); + }} > {buttonLabel} diff --git a/apps/web/src/app/[lang]/[asset]/_components/price-chart.tsx b/apps/web/src/app/[lang]/[asset]/_components/price-chart.tsx index ba02ca74..b9c84d30 100644 --- a/apps/web/src/app/[lang]/[asset]/_components/price-chart.tsx +++ b/apps/web/src/app/[lang]/[asset]/_components/price-chart.tsx @@ -5,8 +5,8 @@ import useSWR from "swr"; import type { ChartCandle } from "@/lib/api/launchpad-api"; import type { ChartResolution } from "@/lib/candles"; import { monthDay } from "@/lib/chain-time"; -import { fromSats } from "@/lib/format"; -import { useFiat, useFxRate } from "@/lib/currency"; +import { fromSats, fiat } from "@/lib/format"; +import { useNumberLocale } from "@/lib/number-preference"; import { useT } from "@/lib/i18n/client"; import { type Numbers, useNumbers } from "@/lib/i18n/numbers"; import { big } from "@/lib/numeric"; @@ -122,8 +122,11 @@ export function PriceChart({ }) { const num = useNumbers(); const t = useT(); - const usd = useFiat(); - const { code } = useFxRate(); + // Historical rates are USD. Today's FX preference cannot rewrite that + // history; only separators follow the independent number preference. + const numberLocale = useNumberLocale(); + const usd = (value: number) => fiat(value, "USD", numberLocale); + const code = "USD"; const svgRef = useRef(null); const [hover, setHover] = useState(null); const [range, setRange] = useState(() => defaultRange(candles)); diff --git a/apps/web/src/app/[lang]/[asset]/_components/trade-panel.tsx b/apps/web/src/app/[lang]/[asset]/_components/trade-panel.tsx index b9c3a658..c9627514 100644 --- a/apps/web/src/app/[lang]/[asset]/_components/trade-panel.tsx +++ b/apps/web/src/app/[lang]/[asset]/_components/trade-panel.tsx @@ -1,6 +1,12 @@ "use client"; -import { useEffect, useState } from "react"; +import { ComposeError } from "@/components/compose-error"; + +import { COUNTERPARTY_MAX_INT, rawToInput } from "@xcp/wallet-sdk/amounts"; +import { parseAmountRaw } from "@/lib/amount-draft"; +import { deriveLimitAmounts } from "@/lib/limit-amounts"; + +import { useEffect, useRef, useState } from "react"; import useSWR from "swr"; import { AmountInput } from "@/components/amount-input"; import { AssetChip } from "@/components/asset-chip"; @@ -17,11 +23,9 @@ import { useT } from "@/lib/i18n/client"; import { useNumbers } from "@/lib/i18n/numbers"; import { approx, - parseUnitsToRaw, percentOf, type Raw, ratio, - SATS_PER_UNIT, SATS, } from "@/lib/numeric"; import { trackTx } from "@/lib/analytics"; @@ -51,7 +55,6 @@ const ORDER_VBYTES = 250; * three near-identical buttons cost more attention than they saved. */ const PRICE_PRESETS = [10]; const fmtPriceInput = (x: number) => x.toFixed(8).replace(/\.?0+$/, ""); -const fmtAmount = (n: number) => n.toFixed(8).replace(/\.?0+$/, ""); interface PoolInfo { asset_a: string; @@ -88,6 +91,10 @@ export function TradePanel({ const usdFmt = useFiat(); const { address, status: walletStatus } = useWallet(); const compose = useCompose(); + const submittedTrade = useRef<{ + pending: Omit[0], "txid">; + usd: number | null; + } | null>(null); const [sideState, setSideState] = useState<"buy" | "sell">("buy"); const side = sideProp ?? sideState; const [limitPrice, setLimitPrice] = useState(""); // XCP per token @@ -97,8 +104,8 @@ export function TradePanel({ const [totalStr, setTotalStr] = useState(""); // XCP const [editField, setEditField] = useState<"amount" | "total">("amount"); - const { customFee, medianFeeRate, limitExpiration } = useSwapSettings(); - const feeRate = customFee > 0 ? customFee : (medianFeeRate ?? null); + const { customFee, medianFeeRate, limitExpiration, limitSettingsValid } = useSwapSettings(); + const feeRate = customFee ?? medianFeeRate ?? null; const { data: btcUsd } = useSWR( "btc-usd", fetchBtcUsd, @@ -162,62 +169,41 @@ export function TradePanel({ const busy = isBusy(compose.status); - const limitPriceNum = parseFloat(limitPrice) || 0; - // Exact integers end to end: price at 8 places is itself an integer, so - // Amount × Price ÷ 1e8 rounds nothing, and these two values ARE the - // give/get quantities consensus enforces. - const priceExact = parseUnitsToRaw(limitPrice) ?? 0n; - const typedAmountExact = parseUnitsToRaw(amountStr) ?? 0n; - const typedTotalExact = parseUnitsToRaw(totalStr) ?? 0n; - const limitAmountExact = - editField === "amount" - ? typedAmountExact - : priceExact > 0n - ? (typedTotalExact * SATS_PER_UNIT) / priceExact - : 0n; - const limitTotalExact = - editField === "total" - ? typedTotalExact - : (limitAmountExact * priceExact) / SATS_PER_UNIT; + const limitPriceNum = approx(parseAmountRaw(limitPrice) ?? 0n) / SATS; + // Quantize only the derived leg, in the direction that preserves the + // user's maximum buy price or minimum sell price. + const priceExact = parseAmountRaw(limitPrice) ?? 0n; + const typedAmountExact = parseAmountRaw(amountStr) ?? 0n; + const typedTotalExact = parseAmountRaw(totalStr) ?? 0n; + const limitAmounts = deriveLimitAmounts(side, editField, + editField === "amount" ? typedAmountExact : typedTotalExact, priceExact); + const limitAmountExact = limitAmounts?.amount ?? 0n; + const limitTotalExact = limitAmounts?.total ?? 0n; const limitAmountRaw = approx(limitAmountExact); const limitTotalRaw = approx(limitTotalExact); const giveAsset = side === "buy" ? "XCP" : asset; const giveRaw = side === "buy" ? limitTotalExact : limitAmountExact; useEffect(() => { - if (compose.status === "confirmed") { - registerPending({ - txid: compose.txid, - kind: "order", - label: - side === "buy" - ? t("Buy {asset} — limit order", { asset }) - : t("Sell {asset} — limit order", { asset }), - address: address ?? undefined, - spends: [{ asset: giveAsset, raw: giveRaw.toString() }], - }); + if (compose.status === "error") submittedTrade.current = null; + if (compose.status === "confirmed" && submittedTrade.current) { + const submitted = submittedTrade.current; + submittedTrade.current = null; + registerPending({ ...submitted.pending, txid: compose.txid }); + trackTx(compose.txid, "limit order", submitted.usd); } - }, [ - compose.status, - compose.txid, - side, - asset, - address, - giveAsset, - giveRaw, - t, - ]); + }, [compose.status, compose.txid]); // The give side must be covered: XCP (Total) for a buy, tokens for a sell. const insufficientToken = side === "sell" && tokenBalance !== undefined && limitAmountRaw > 0 && - limitAmountRaw > tokenBalance; + limitAmountExact > tokenBalance; const insufficientXcp = side === "buy" && xcpBalance !== undefined && limitTotalRaw > 0 && - limitTotalRaw > xcpBalance; + limitTotalExact > xcpBalance; const insufficient = insufficientToken || insufficientXcp; const spendBalance = side === "buy" ? xcpBalance : tokenBalance; // A failed read does not hold the order — see useSpendableBalance's @@ -226,19 +212,8 @@ export function TradePanel({ spendBalance !== undefined || (side === "buy" ? xcpBalanceUnavailable : tokenBalanceUnavailable); - // A limit order is a stated intent, not a fill — but placing one is the - // conversion this surface exists for, and its XCP total is what the user - // committed. Placed here because limitTotalRaw isn't known any earlier. - useEffect(() => { - if (compose.status === "confirmed") { - trackTx( - compose.txid, - "limit order", - xcpUsd && limitTotalRaw > 0 ? (limitTotalRaw / SATS) * xcpUsd : null, - ); - } - }, [compose.status, compose.txid, limitTotalRaw, xcpUsd]); const limitReady = + limitSettingsValid && limitAmounts !== null && limitPriceNum > 0 && limitAmountRaw > 0 && limitTotalRaw > 0 && @@ -309,7 +284,19 @@ export function TradePanel({ : null; const submitLimit = () => { - if (!limitReady) return; + if (!limitReady || submittedTrade.current) return; + // Keep the submitted account, side and exact debit through later edits. + submittedTrade.current = { + pending: { + kind: "order", + label: side === "buy" + ? t("Buy {asset} — limit order", { asset }) + : t("Sell {asset} — limit order", { asset }), + address: address ?? undefined, + spends: [{ asset: giveAsset, raw: giveRaw.toString() }], + }, + usd: xcpUsd && limitTotalRaw > 0 ? (limitTotalRaw / SATS) * xcpUsd : null, + }; compose.composeOrder( side === "buy" ? { @@ -318,7 +305,7 @@ export function TradePanel({ get_asset: asset, get_quantity: limitAmountExact, expiration: limitExpiration, - fee_rate: customFee > 0 ? customFee : undefined, + fee_rate: customFee ?? undefined, } : { give_asset: asset, @@ -326,7 +313,7 @@ export function TradePanel({ get_asset: "XCP", get_quantity: limitTotalExact, expiration: limitExpiration, - fee_rate: customFee > 0 ? customFee : undefined, + fee_rate: customFee ?? undefined, }, ); }; @@ -460,12 +447,13 @@ export function TradePanel({ balance; buying, % of the tokens your XCP affords at the current price. */} {(() => { - const basisPrice = limitPriceNum > 0 ? limitPriceNum : (spot ?? 0); + const basisPrice = priceExact; const maxAffordableRaw = side === "sell" ? (tokenBalance ?? 0n) - : basisPrice > 0 - ? BigInt(Math.floor(approx(xcpBalance ?? 0n) / basisPrice)) + : basisPrice > 0n + ? ((xcpBalance ?? 0n) * 100000000n / basisPrice > COUNTERPARTY_MAX_INT + ? COUNTERPARTY_MAX_INT : (xcpBalance ?? 0n) * 100000000n / basisPrice) : 0n; return ( <> @@ -483,9 +471,7 @@ export function TradePanel({ onClick={() => { setEditField("amount"); setAmountStr( - fmtAmount( - approx(percentOf(maxAffordableRaw, p)) / SATS, - ), + rawToInput(percentOf(maxAffordableRaw, p), 8), ); }} className="rounded-md border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 px-1.5 py-0.5 text-[10px] font-medium text-gray-500 dark:text-gray-400 transition-colors hover:border-purple-400 dark:hover:border-purple-500 hover:text-purple-600 dark:hover:text-purple-400 active:scale-95" @@ -515,7 +501,7 @@ export function TradePanel({ }`} onClick={() => { setEditField("amount"); - setAmountStr(fmtAmount(approx(tokenBalance) / SATS)); + setAmountStr(rawToInput(tokenBalance, 8)); }} > {t("Balance:")} {num.commasRaw(tokenBalance)} @@ -530,7 +516,7 @@ export function TradePanel({ editField === "amount" ? amountStr : limitAmountRaw > 0 - ? fmtAmount(limitAmountRaw / SATS) + ? rawToInput(limitAmountExact, 8) : "" } onChange={(v) => { @@ -567,7 +553,7 @@ export function TradePanel({ }`} onClick={() => { setEditField("total"); - setTotalStr(fmtAmount(approx(xcpBalance) / SATS)); + setTotalStr(rawToInput(xcpBalance, 8)); }} > {t("Balance:")} {num.commasRaw(xcpBalance)} @@ -582,7 +568,7 @@ export function TradePanel({ editField === "total" ? totalStr : limitTotalRaw > 0 - ? fmtAmount(limitTotalRaw / SATS) + ? rawToInput(limitTotalExact, 8) : "" } onChange={(v) => { @@ -636,7 +622,7 @@ export function TradePanel({ {feeRate !== null && (
{t("TX fee")}
-
0 ? "font-medium text-purple-600 dark:text-purple-400" : ""}> +
{num.satsPerVb(feeRate)} sat/vB {btcUsd != null && ( @@ -653,7 +639,7 @@ export function TradePanel({
{compose.status === "error" && ( - {compose.error} + )} {walletStatus !== "connected" ? ( diff --git a/apps/web/src/app/[lang]/_components/market-pulse.tsx b/apps/web/src/app/[lang]/_components/market-pulse.tsx index 3847517c..41456507 100644 --- a/apps/web/src/app/[lang]/_components/market-pulse.tsx +++ b/apps/web/src/app/[lang]/_components/market-pulse.tsx @@ -10,7 +10,8 @@ import { monthDay } from "@/lib/chain-time"; import { priceChangePercent } from "@/lib/market"; import { approx } from "@/lib/numeric"; import { useFxRate } from "@/lib/currency"; -import { useT, useLocale } from "@/lib/i18n/client"; +import { useT } from "@/lib/i18n/client"; +import { useNumberLocale } from "@/lib/number-preference"; import { type Numbers, useNumbers } from "@/lib/i18n/numbers"; type Market = "btc" | "xcp"; @@ -152,7 +153,7 @@ function TickerButton({ const t = useT(); const num = useNumbers(); const { code, rate } = useFxRate(); - const locale = useLocale(); + const locale = useNumberLocale(); const isBtc = market === "btc"; return (
) : ( - +

USD

)}
@@ -479,7 +480,7 @@ function MarketLineChart({ market, points }: { market: Market; points: PricePoin const end = monthDay(points[points.length - 1]!.timestamp, num.intl); return ( - + diff --git a/apps/web/src/app/[lang]/create/page.tsx b/apps/web/src/app/[lang]/create/page.tsx index 9ced4763..b8f14280 100644 --- a/apps/web/src/app/[lang]/create/page.tsx +++ b/apps/web/src/app/[lang]/create/page.tsx @@ -1,5 +1,9 @@ "use client"; +import { ComposeError } from "@/components/compose-error"; + +import { parseBoundedSetting } from "@/lib/amount-draft"; + import { LazyLink } from "@/components/lazy-link"; import { useEffect, useState } from "react"; import useSWR from "swr"; @@ -267,7 +271,7 @@ export default function CreatePage() { // clears it. const registrationFeeXcp = nameCheck === "owned" ? 0 : REGISTRATION_FEE_XCP; - const customBlockNum = Math.round(parseFloat(customBlockInput)) || 0; + const customBlockNum = parseBoundedSetting(customBlockInput, 0x7fffffff, 1, 0).value ?? 0; const minCustomBlock = blockHeight !== undefined ? blockHeight + CUSTOM_FLOOR_BLOCKS : undefined; const scheduleValid = @@ -747,13 +751,13 @@ export default function CreatePage() {
{uploadError && ( -

+

{uploadError} -

+
)} {compose.status === "error" && (

- {compose.error} +

)} @@ -812,7 +816,7 @@ function PreviewCard({ }) { const num = useNumbers(); const t = useT(); - const customBlockNum = Math.round(parseFloat(customBlockInput)) || 0; + const customBlockNum = parseBoundedSetting(customBlockInput, 0x7fffffff, 1, 0).value ?? 0; const priceXcp = XCP69.PRICE / SATS; const lot = XCP69.QUANTITY_BY_PRICE / SATS; const targetXcp = fromSats(XCP69_RAISE_SATS); @@ -921,7 +925,7 @@ function ScheduleGear({ }) { const num = useNumbers(); const t = useT(); - const customNum = Math.round(parseFloat(customBlock)) || 0; + const customNum = parseBoundedSetting(customBlock, 0x7fffffff, 1, 0).value ?? 0; const minBlock = blockHeight !== undefined ? blockHeight + CUSTOM_FLOOR_BLOCKS : undefined; const tooSoon = @@ -977,6 +981,8 @@ function ScheduleGear({ { setFlips((f) => f + 1); @@ -136,6 +140,7 @@ export function XcpBridge({ onFlip={flip} flips={flips} customFee={customFee} + feeValid={feeDraft.valid} /> ) : ( )}
@@ -193,18 +199,20 @@ function DispenseSettingsGear() { // Not rounded: a typed 1.5 sat/vB is a rate Counterparty accepts and prices // from, so rounding it to 2 would overrule the number the user chose in the // one place they went out of their way to choose it. - const customFee = Math.min(parseFloat(settings.customFeeRate) || 0, 500); + const feeDraft = parseBoundedSetting(settings.customFeeRate, 500); + const customFee = feeDraft.value; return ( - 0} label={t("Dispense settings")}> +
{t("TX fee")} 0 ? "border-purple-600 bg-purple-50 dark:bg-purple-950/40" : "border-gray-200 dark:border-gray-800" + customFee !== null ? "border-purple-600 bg-purple-50 dark:bg-purple-950/40" : "border-gray-200 dark:border-gray-800" }`} > updateSettings({ customFeeRate: v })} placeholder={medianFeeRate ? String(medianFeeRate) : "…"} ariaLabel={t("Bitcoin fee rate in sats per vbyte")} @@ -231,6 +239,7 @@ function LoadCard({ onFlip, flips, customFee, + feeValid, hiddenCount, }: { dispensers: Dispenser[]; @@ -238,7 +247,8 @@ function LoadCard({ xcpUsd: number | null; onFlip: () => void; flips: number; - customFee: number; + customFee: number | null; + feeValid: boolean; hiddenCount: number; }) { const num = useNumbers(); @@ -294,12 +304,9 @@ function LoadCard({ .sort((a, b) => b - a) .slice(0, MAX_LEGS) .reduce((s, c) => s + c, 0); - // Typed amounts become raw units through the same parser every other input - // in this app uses, rather than parseFloat. It truncates past 8 decimals - // instead of rounding, which is the right direction for a field that - // decides what to send: never more than the user wrote. - const typedXcpRaw = parseUnitsToRaw(xcpAmount) ?? 0n; - const typedBtcSatsRaw = parseUnitsToRaw(btcAmount) ?? 0n; + // Preserve the exact draft; the XCP side requires whole-token lots. + const typedXcpRaw = parseWholeTokenRaw(xcpAmount) ?? 0n; + const typedBtcSatsRaw = parseAmountRaw(btcAmount) ?? 0n; // BTC side floors against cheapest-first fill, as the protocol prices a // payment (get_must_give floors — overpay is kept). Approximate inverse; // the plan below recomputes exactly from the unit count. @@ -607,6 +614,8 @@ function LoadCard({ } > 0 ? String(snapped) : "" } @@ -739,14 +748,15 @@ function LoadCard({
)} { + if (busy || n === 0 || !feeValid) return; if (plan.length > 1 && !armed) { setArmed(true); return; } setArmed(false); - router.start(plan, customFee > 0 ? customFee : undefined); + router.start(plan, customFee ?? undefined); }} > {buttonLabel} @@ -783,13 +793,15 @@ function UnloadCard({ onFlip, flips, customFee, + feeValid, }: { dispensers: Dispenser[]; btcUsd: number | null; xcpUsd: number | null; onFlip: () => void; flips: number; - customFee: number; + customFee: number | null; + feeValid: boolean; }) { const num = useNumbers(); const t = useT(); @@ -833,19 +845,20 @@ function UnloadCard({ // A price field in sats is a whole number of satoshi, so it parses as an // integer rather than through the 8-decimal unit parser the amount fields - // use — `parseUnitsToRaw(v, 0)` is that same parser told this field has no + // use — `parseAmountRaw(v, 0)` is that same parser told this field has no // decimals, which keeps one parser for every input in the app. // Defaults to the undercut rather than the USD market rate. The USD rate was // never a price anyone could sell at here — the book sits well above it — so // an untouched field that meant "market" was proposing a price that would // have jumped the whole queue by 30%. The floor is the neutral opening // position: the cheapest place that actually vends. - const priceSats = approx(parseUnitsToRaw(price, 0) ?? 0n) || (undercutSats ?? 0); + const priceDraft = parseBoundedSetting(price, 2_100_000_000_000_000, 1, 0); + const priceSats = priceDraft.empty ? (undercutSats ?? 0) : (priceDraft.value ?? 0); // Whole XCP only: these dispensers vend 1 XCP at a time, so a fractional // remainder could never vend — it would just sit until close. Truncated in // raw units, so the escrow is exact and the fraction is dropped rather than // rounded up into an amount the wallet does not hold. - const typedEscrowRaw = parseUnitsToRaw(escrow) ?? 0n; + const typedEscrowRaw = parseWholeTokenRaw(escrow) ?? 0n; const escrowRawBig = (typedEscrowRaw / SATS_PER_UNIT) * SATS_PER_UNIT; const wholeEscrow = approx(typedEscrowRaw / SATS_PER_UNIT); const escrowRaw = approx(escrowRawBig); @@ -863,6 +876,7 @@ function UnloadCard({ const busy = isBusy(compose.status); const ready = + feeValid && priceDraft.valid && typedEscrowRaw === escrowRawBig && balance !== undefined && escrowRaw >= SATS && priceSats > 0 && @@ -880,17 +894,19 @@ function UnloadCard({ const { data: medianFeeRate } = useSWR("btc-feerate", fetchFeeRate, { refreshInterval: 30_000, }); - const sellFeeRate = customFee > 0 ? customFee : (medianFeeRate ?? null); + const sellFeeRate = customFee ?? medianFeeRate ?? null; - const openDispenser = () => + const openDispenser = () => { + if (!ready) return; compose.composeDispenser({ asset: "XCP", give_quantity: SATS, // 1 XCP per vend — matches the load list's filter - escrow_quantity: escrowRaw, + escrow_quantity: escrowRawBig, mainchainrate: priceSats, status: 0, - fee_rate: customFee > 0 ? customFee : undefined, + fee_rate: customFee ?? undefined, }); + }; const close = () => compose.composeDispenser({ @@ -955,7 +971,7 @@ function UnloadCard({ })}

{compose.status === "error" && ( - {compose.error} + )} @@ -1132,6 +1150,8 @@ function UnloadCard({ } >
{t("TX fee")}
-
0 ? "font-medium text-purple-600 dark:text-purple-400" : ""}> +
{num.satsPerVb(sellFeeRate)} sat/vB {btcUsd !== null && ( @@ -1199,7 +1219,7 @@ function UnloadCard({
{compose.status === "error" && ( - {compose.error} + )} {walletStatus !== "connected" ? ( @@ -1216,7 +1236,7 @@ function UnloadCard({ open={dispensers} yourPriceSats={priceSats} yourEscrowXcp={escrowRaw / SATS} - active={escrowRaw > 0 || (parseUnitsToRaw(price, 0) ?? 0n) > 0n} + active={escrowRaw > 0 || (parseAmountRaw(price, 0) ?? 0n) > 0n} onPick={(sats) => setPrice(String(sats))} />
diff --git a/apps/web/src/app/[lang]/dispense/_lib/use-dispense-router.ts b/apps/web/src/app/[lang]/dispense/_lib/use-dispense-router.ts index 4672ad92..60095da9 100644 --- a/apps/web/src/app/[lang]/dispense/_lib/use-dispense-router.ts +++ b/apps/web/src/app/[lang]/dispense/_lib/use-dispense-router.ts @@ -1,6 +1,9 @@ "use client"; -import { useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; +import { hex } from "@scure/base"; +import type { Transaction } from "@scure/btc-signer"; +import { parseRawInteger } from "@xcp/wallet-sdk/amounts"; import { useT } from "@/lib/i18n/client"; import type { T } from "@/lib/i18n/t"; import type { Dispenser } from "@/lib/api/counterparty"; @@ -9,6 +12,7 @@ import { shortAddress } from "@/lib/format"; import { trackTx } from "@/lib/analytics"; import { fetchFeeRate, + addressScriptPubKey, ownTransactionOutputs, parseTxInputs, pendingChangeInputs, @@ -16,10 +20,14 @@ import { registerBroadcast, registerPending, relayingFetch, + serializeComposeParams, + serializeFeeRate, + WalletSdkError, withAddressTransactionLock, } from "@xcp/wallet-sdk"; import { useWallet } from "@/lib/wallet/wallet-context"; import { COUNTERPARTY_API_BASE } from "@/lib/constants"; +import { assertSameTransaction, readPsbt, readTransaction } from "@/lib/transaction-verification"; /** * Multi-dispense router: fills one load across up to MAX_LEGS dispensers as @@ -45,6 +53,7 @@ export const MAX_LEGS = 3; const FEE_VBYTES = 300; /** Breather between legs: lets the wallet's popup monitor (5s close-grace) settle. */ const INTER_LEG_DELAY_MS = 1200; +const MAX_BTC_SATS = 2_100_000_000_000_000n; export interface PlannedLeg { dispenser: Dispenser; @@ -77,6 +86,46 @@ interface Utxo { value: number; } +/** This bridge routes one whole XCP per vend, with an exact BTC payment. */ +function paymentForLeg(leg: PlannedLeg): bigint { + const units = parseRawInteger(leg.units, { min: 1n }); + const rate = parseRawInteger(leg.dispenser.satoshirate, { min: 1n, max: MAX_BTC_SATS }); + const payment = parseRawInteger(leg.btcSats, { min: 1n, max: MAX_BTC_SATS }); + if (parseRawInteger(leg.dispenser.give_quantity) !== 100_000_000n || units * rate !== payment) { + throw new WalletSdkError("invalid_argument", "Dispenser payment does not match the reviewed XCP quantity"); + } + return payment; +} + +function validateDispenseTransaction(raw: string, address: string, leg: PlannedLeg): Transaction { + const payment = paymentForLeg(leg); + const transaction = readTransaction(raw); + const destination = addressScriptPubKey(leg.dispenser.source); + const change = addressScriptPubKey(address); + if (destination === change) throw new WalletSdkError("invalid_argument", "Source and dispenser must differ"); + let payments = 0; + for (let i = 0; i < transaction.outputsLength; i++) { + const output = transaction.getOutput(i); + const script = hex.encode(output.script!); + if (script === destination) { + if (output.amount !== payment) throw new WalletSdkError("transaction_mismatch", "Dispenser payment amount changed"); + payments++; + } else if (script !== change && !(output.script?.[0] === 0x6a && output.amount === 0n)) { + throw new WalletSdkError("transaction_mismatch", "Unexpected dispense transaction output"); + } + } + if (payments !== 1) throw new WalletSdkError("transaction_mismatch", "Missing or repeated dispenser payment"); + return transaction; +} + +/** Verify both Core's payment and the wallet's returned envelope before broadcast. */ +export async function signDispenseLeg(raw: string, address: string, leg: PlannedLeg, sign: (raw: string) => Promise): Promise { + const expected = validateDispenseTransaction(raw, address, leg); + const signed = await sign(raw); + assertSameTransaction(expected, readTransaction(signed)); + return signed; +} + /** * The coins this load will be paid from. * @@ -102,10 +151,10 @@ export async function composeLeg( opts: { inputsSet?: string; excludeUtxos?: string[]; allowUnconfirmed?: boolean }, t: T, ): Promise { - const qp = new URLSearchParams({ + const qp = serializeComposeParams("dispense", { dispenser: leg.dispenser.source, - quantity: String(leg.btcSats), - sat_per_vbyte: String(feeRate), + quantity: paymentForLeg(leg), + sat_per_vbyte: serializeFeeRate(feeRate), exclude_utxos_with_balances: "true", verbose: "true", }); @@ -113,7 +162,7 @@ export async function composeLeg( if (opts.excludeUtxos?.length) qp.set("exclude_utxos", opts.excludeUtxos.join(",")); if (opts.allowUnconfirmed) qp.set("allow_unconfirmed_inputs", "true"); - const url = `${COUNTERPARTY_API_BASE}/addresses/${address}/compose/dispense?${qp}`; + const url = `${COUNTERPARTY_API_BASE}/addresses/${encodeURIComponent(address)}/compose/dispense?${qp}`; // A person is waiting on this request, so use the same unbudgeted relay // protection as the shared compose pipeline. The direct Counterparty host // can surface a CORS-hidden rate limit as the browser's raw "Failed to @@ -123,31 +172,44 @@ export async function composeLeg( if (!res.ok || data.error) { throw new Error(data.error?.description ?? data.error ?? t("Compose failed ({status})", { status: res.status })); } - return data.result.rawtransaction as string; + const raw = data.result?.rawtransaction; + const transaction = validateDispenseTransaction(raw, address, leg); + if (data.result.psbt) { + assertSameTransaction(transaction, readPsbt(data.result.psbt)); + } + return raw; } /** Live re-check of one dispenser right before composing its leg. Returns * the reason it can no longer run, already in the visitor's language. */ -async function preflightLeg(leg: PlannedLeg, t: T): Promise { +export async function preflightLeg(leg: PlannedLeg, t: T): Promise { try { + paymentForLeg(leg); const res = await relayingFetch( - `${COUNTERPARTY_API_BASE}/addresses/${leg.dispenser.source}/dispensers`, + `${COUNTERPARTY_API_BASE}/addresses/${encodeURIComponent(leg.dispenser.source)}/dispensers`, 10_000, ); + if (!res.ok) throw new Error(`HTTP ${res.status}`); const rows: { asset: string; status: number; give_remaining: number; + give_quantity: number; satoshirate: number; - }[] = res.ok ? ((await res.json()).result ?? []) : []; + tx_hash: string; + oracle_address: string | null; + }[] = (await res.json()).result ?? []; const live = rows.find((r) => r.asset === "XCP"); if (!live || live.status !== 0) return t("route just closed"); - if (live.satoshirate !== leg.dispenser.satoshirate) return t("route price changed"); - if (live.give_remaining < leg.units * leg.dispenser.give_quantity) + if (live.tx_hash !== leg.dispenser.tx_hash || live.oracle_address !== null + || parseRawInteger(live.satoshirate) !== parseRawInteger(leg.dispenser.satoshirate) + || parseRawInteger(live.give_quantity) !== parseRawInteger(leg.dispenser.give_quantity)) return t("route price changed"); + if (parseRawInteger(live.give_remaining) < parseRawInteger(leg.units) * parseRawInteger(leg.dispenser.give_quantity)) return t("route no longer has enough left"); return null; } catch { - return null; // can't verify — compose-time validation still applies + // Core only checks the current dispenser, not the quantity the site quoted. + return t("Could not refresh the quote. Try again."); } } @@ -159,6 +221,9 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); export function useDispenseRouter(btcUsd?: number | null) { const t = useT(); const { address, signTransaction, broadcastTransaction } = useWallet(); + const currentAddress = useRef(address); + useEffect(() => { currentAddress.current = address; }, [address]); + const planAddress = useRef(null); const [legs, setLegs] = useState([]); const [phase, setPhase] = useState("idle"); const [planError, setPlanError] = useState(null); @@ -166,6 +231,7 @@ export function useDispenseRouter(btcUsd?: number | null) { const chainModeRef = useRef(false); const feeRateRef = useRef(3); const runningRef = useRef(false); + const planningRef = useRef(false); const sync = () => setLegs([...legsRef.current]); const patch = (i: number, p: Partial) => { @@ -176,6 +242,11 @@ export function useDispenseRouter(btcUsd?: number | null) { /** Run legs from startIdx to completion (or first hard stop). */ const run = async (startIdx: number) => { if (!address || runningRef.current) return; + if (planAddress.current !== address) { + setPlanError("Wallet address changed. Reconnect the account that started this load."); + setPhase("partial"); + return; + } runningRef.current = true; setPhase("running"); try { @@ -213,13 +284,16 @@ export function useDispenseRouter(btcUsd?: number | null) { if (i > startIdx) await sleep(INTER_LEG_DELAY_MS); try { + if (currentAddress.current !== address) throw new WalletSdkError("invalid_argument", "Wallet address changed before signing"); + // Even a cached transaction must still buy the reviewed XCP lot. + // Core cannot enforce a previous quote on an ordinary BTC payment. + const stale = await preflightLeg(leg, t); + if (stale) throw new Error(t("Skipped: {reason}", { reason: stale })); // Compose (unless retrying an already-composed hex — same hex // re-sign is deduped by the wallet, so reuse it). let rawHex = leg.rawHex; if (!rawHex) { patch(i, { status: "composing", error: null }); - const stale = await preflightLeg(leg, t); - if (stale) throw new Error(t("Skipped: {reason}", { reason: stale })); const chainedInputs = chainModeRef.current && !leg.utxoAssigned ? pendingChangeInputs(address) @@ -240,7 +314,13 @@ export function useDispenseRouter(btcUsd?: number | null) { const inputs = parseTxInputs(rawHex); patch(i, { status: "signing", error: null }); - const signedHex = await signTransaction(rawHex); + if (currentAddress.current !== address) throw new WalletSdkError("invalid_argument", "Wallet address changed before signing"); + const signedHex = await signDispenseLeg(rawHex, address, leg, signTransaction); + // A wallet popup can stay open for minutes; re-check before the + // irreversible BTC broadcast, then preserve the exact signed bytes. + const moved = await preflightLeg(leg, t); + if (moved) throw new Error(t("Skipped: {reason}", { reason: moved })); + if (currentAddress.current !== address) throw new WalletSdkError("invalid_argument", "Wallet address changed before broadcast"); patch(i, { status: "broadcasting" }); const txid = await broadcastTransaction(signedHex); @@ -289,7 +369,9 @@ export function useDispenseRouter(btcUsd?: number | null) { }; const start = async (planned: PlannedLeg[], feeRateOverride?: number) => { - if (!address || planned.length === 0 || runningRef.current) return; + if (!address || planned.length === 0 || runningRef.current || planningRef.current) return; + planningRef.current = true; + planAddress.current = address; setPlanError(null); setPhase("running"); legsRef.current = planned.map((p) => ({ @@ -303,7 +385,9 @@ export function useDispenseRouter(btcUsd?: number | null) { sync(); try { + for (const leg of planned) paymentForLeg(leg); feeRateRef.current = feeRateOverride ?? (await fetchFeeRate()); + serializeFeeRate(feeRateRef.current); const knownSpent = new Set(recentlySpentUtxos(address)); const utxos = (await fetchConfirmedUtxos(address)).filter( (utxo) => !knownSpent.has(`${utxo.txid}:${utxo.vout}`), @@ -357,18 +441,20 @@ export function useDispenseRouter(btcUsd?: number | null) { } catch (e) { setPlanError(e instanceof Error ? e.message : "Planning failed"); setPhase(legsRef.current.some((l) => l.status === "done") ? "partial" : "idle"); + } finally { + planningRef.current = false; } }; /** Retry a failed leg (and continue any legs after it). */ const retry = (idx: number) => { - if (runningRef.current) return; + if (runningRef.current || planningRef.current) return; patch(idx, { status: "pending", error: null }); run(idx); }; const reset = () => { - if (runningRef.current) return; + if (runningRef.current || planningRef.current) return; legsRef.current = []; chainModeRef.current = false; setLegs([]); diff --git a/apps/web/src/app/[lang]/rewards/_components/rewards-content.tsx b/apps/web/src/app/[lang]/rewards/_components/rewards-content.tsx new file mode 100644 index 00000000..f6a73e3b --- /dev/null +++ b/apps/web/src/app/[lang]/rewards/_components/rewards-content.tsx @@ -0,0 +1,359 @@ +"use client"; + +import type { Numbers } from "@/lib/i18n/numbers"; +import { type T } from "@/lib/i18n/t"; +import { rich } from "@/lib/i18n/rich"; +import { LazyLink } from "@/components/lazy-link"; +import type { fetchPool } from "@/lib/api/counterparty"; +import type { + fetchLaunchPage, + fetchLaunchStats, + fetchMinterEarnings, + fetchRewardBatches, +} from "@/lib/api/launchpad-api"; +import type { fetchBtcUsd, fetchXcpUsd } from "@/lib/api/price"; +import { shortAddress } from "@/lib/format"; +import { ratio } from "@/lib/numeric"; +import { LABEL } from "@/components/ui/tokens"; +import { TokenImage } from "@/components/token-image"; +import { XCP69_MIN_PARTICIPANTS, XCP69_RAISE_SATS } from "@/lib/xcp69"; +import { + BOUNTIES, + FALLBACK_MINT_FEE_SATS, + MINT_CAP, + MINTS_PER_MINT, + MINTS_PRICE_XCP, + SATS_PER_XCP, +} from "@/lib/rewards"; +import { EarnersTable } from "@/app/[lang]/rewards/_components/earners-table"; +import { useNumbers } from "@/lib/i18n/numbers"; +import { useT } from "@/lib/i18n/client"; + +const raiseXcp = XCP69_RAISE_SATS / 1e8; + +export function RewardsContent({ stats, earners, graduates, mintsPool, xcpUsd, btcUsd, rewardBatches }: { + stats: Awaited>; + earners: Awaited>; + graduates: Awaited>; + mintsPool: Awaited>; + xcpUsd: Awaited>; + btcUsd: Awaited>; + rewardBatches: Awaited>; +}) { + const num = useNumbers(); + const t = useT(); // The chain keeps minting after this programme ends; the progress meter is + // programme progress, so it stops at its cap rather than saying 10,001 of + // 10,000 later. + const mintsSoFar = Math.min(MINT_CAP, stats?.activity.mints ?? 0); + const graduated = stats?.counts.graduated ?? 0; + const remaining = Math.max(0, MINT_CAP - mintsSoFar); + + // XCP per MINTS from live reserves (both sides are 8-decimal raw, so the + // raw ratio needs no scale correction). reserve_a/b follow the pool's own + // asset order, so pick the XCP side by name rather than by position. + const livePrice = mintsPool + ? mintsPool.asset_a === "XCP" + ? ratio(mintsPool.reserve_a, mintsPool.reserve_b) + : ratio(mintsPool.reserve_b, mintsPool.reserve_a) + : null; + const mintsPriceXcp = livePrice && livePrice > 0 ? livePrice : MINTS_PRICE_XCP; + const rewardXcp = MINTS_PER_MINT * mintsPriceXcp; + + // Sats per XCP from the same feeds the rest of the site prices with; the + // measured constant stands in only when a feed is down. + const satsPerXcp = + btcUsd && xcpUsd && xcpUsd > 0 ? (xcpUsd / btcUsd) * 1e8 : SATS_PER_XCP; + const feeSamples = stats?.activity.fee_samples ?? 0; + const measuredFee = stats?.activity.median_fee_sats ?? 0; + const typicalMintFeeSats = measuredFee > 0 ? measuredFee : FALLBACK_MINT_FEE_SATS; + const feeXcp = typicalMintFeeSats / satsPerXcp; + + return ( +
+ {/* No page title — the chip that brought you here already said XCP + Rewards, and the bounty section opens the page. */} + {/* ---------------- the bounty ---------------- */} +
+

{t("The graduation bounty")}

+

+ {graduated === 0 + ? t("No XCP-69 launch has graduated yet. The first three to do it earn a bounty.") + : graduated === 1 + ? t("One bounty is claimed — two are still open for the next launches to graduate.") + : graduated === 2 + ? t("Two bounties are claimed — one is still open for the next launch to graduate.") + : t("All three bounties have been claimed.")} +

+ + r.fm.asset) ?? []} /> + +

+ {rich( + t, + "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses, at which point the pool is created and its liquidity is burned. A launch that misses its target refunds every satoshi by consensus and does not count. {link}", + { + xcp: num.commas(raiseXcp), + n: XCP69_MIN_PARTICIPANTS, + link: ( + + {t("How that works")} + + ), + }, + )} +

+
+ + {/* ---------------- the ongoing reward ---------------- */} +
+
+ +

+ {t("{n} MINTS for every mint", { n: num.commas(MINTS_PER_MINT) })} +

+
+

+ {t( + "Mint any XCP-69 launch and earn {n} MINTS. It doesn't matter which launch, and it doesn't matter how much you mint — one transaction, one reward (valid for the first {cap} mint transactions).", + { n: num.commas(MINTS_PER_MINT), cap: num.commas(MINT_CAP) }, + )} +

+ +
+ + + 0 + ? t("~{xcp} XCP · {n}-mint median", { xcp: num.fixed(feeXcp, 2), n: num.commas(feeSamples) }) + : t("~{xcp} XCP · estimate", { xcp: num.fixed(feeXcp, 2) }) + } + /> + +
+ +
+
+ {t("Mints so far")} + + {t("{n} of {cap}", { n: num.commas(mintsSoFar), cap: num.commas(MINT_CAP) })} + +
+
+
+
+

+ {t("{n} still to claim", { n: num.commas(remaining) })} +

+
+
+ + {rewardBatches.length > 0 && ( +
+

{t("Distributions")}

+

+ {t("Completed and confirming reward batches, linked to the transactions that sent them.")} +

+
+ {rewardBatches.map((batch) => { + const fullyLinked = batch.sentRecipientCount === batch.recipientCount; + const confirmed = + fullyLinked && batch.transactions.every((tx) => tx.status === "confirmed"); + const state = confirmed ? t("Confirmed") : fullyLinked ? t("Confirming") : t("Partially sent"); + return ( +
+
+
+

+ {t("Mints {from}–{to}", { + from: num.commas(batch.firstMintNumber), + to: num.commas(batch.cutoffMintNumber), + })} +

+

+ {t("{recipients} recipients · {mints} mint transactions", { + recipients: num.commas(batch.recipientCount), + mints: num.commas(batch.eligibleMints), + })} +

+
+
+

+ {num.commasRaw(batch.sentQuantity)} {batch.asset} +

+ {!fullyLinked && ( +

+ {t("of {n} in the batch", { n: num.commasRaw(batch.totalQuantity) })} +

+ )} + + {state} + +
+
+ +
+ ); + })} +
+
+ )} + + {/* ---------------- who has earned what ---------------- */} +
+

{t("Earned so far")}

+

+ {t("Every mint on a conforming launch, counted. Ranked by mints, because that is the unit the reward is paid in.")} +

+ + +
+ + {/* ---------------- the small print, in the site's FAQ grammar ---------------- */} +
+

{t("FAQ")}

+
+ + {t( + "The first fairminter ever created on Counterparty — block 866,297, before any other, and it minted out free to 1,376 addresses. The supply is 100,000,000, locked forever; no more can ever be issued. The live MINTS/XCP pool prices the reward: right now 1 MINTS trades at {price} XCP, so {n} MINTS is {worth} XCP.", + { price: num.price(mintsPriceXcp), n: num.commas(MINTS_PER_MINT), worth: num.fixed(rewardXcp, 2) }, + )} + + + {t( + "The Bitcoin fee you pay is per transaction, so the reward is too. Minting one lot and minting the full 1% cost you the same fee and earn the same {n} MINTS — there is nothing to gain by splitting a mint into smaller pieces.", + { n: num.commas(MINTS_PER_MINT) }, + )} + + + {t( + "Rewards accrue as you mint and are sent in batches. Once yours is sent, its transaction appears on your profile. Lifetime earned is an all-time total, not your wallet balance.", + )} + + + {t( + "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.", + )} + +
+
+
+ ); +} + +/** + * The three bounties as a podium. + * + * Ordered 2nd–1st–3rd left to right, the way a real podium stands, with the + * step heights carrying the prize sizes. It reads as a race rather than a + * price list — which is the point while bounties are open: the whole thing + * is an invitation to take the next step. + */ +function Podium({ t, num, graduated, winners }: { t: T; num: Numbers; graduated: number; winners: string[] }) { + // Visual order, not rank order. + const layout = [ + { i: 1, height: "h-20", accent: "from-gray-300 dark:from-gray-700 to-gray-200 dark:to-gray-800", ring: "ring-gray-300 dark:ring-gray-700" }, + { i: 0, height: "h-28", accent: "from-amber-300 dark:from-amber-700 to-amber-200 dark:to-amber-800", ring: "ring-amber-400" }, + { i: 2, height: "h-14", accent: "from-orange-300/70 dark:from-orange-700/70 to-orange-200/70 dark:to-orange-800/70", ring: "ring-orange-300" }, + ]; + + return ( +
+ {layout.map(({ i, height, accent, ring }) => { + const b = BOUNTIES[i]!; + const claimed = graduated > i; + const winner = winners[i]; + return ( +
+
+ {i + 1} +
+
+ {num.commas(b.xcp)}{" "} + XCP +
+ {/* The step. Height encodes the prize; the label sits inside it. */} +
+ {claimed ? ( + winner ? ( + + + {winner} + + ) : ( + {t("claimed")} + ) + ) : ( + + {t("open", undefined, "bounty")} + + )} +
+
+ ); + })} +
+ ); +} + +function Stat({ label, value, hint }: { label: string; value: string; hint: string }) { + return ( +
+
{label}
+
{value}
+
{hint}
+
+ ); +} + +function Faq({ + q, + open = false, + children, +}: { + q: string; + open?: boolean; + children: React.ReactNode; +}) { + return ( +
+ {q} +

{children}

+
+ ); +} diff --git a/apps/web/src/app/[lang]/rewards/page.tsx b/apps/web/src/app/[lang]/rewards/page.tsx index 48283bb0..71c3f594 100644 --- a/apps/web/src/app/[lang]/rewards/page.tsx +++ b/apps/web/src/app/[lang]/rewards/page.tsx @@ -1,11 +1,8 @@ import { isLocale } from "@/lib/i18n/locales"; import { localeAlternates } from "@/lib/i18n/seo"; -import type { Numbers } from "@/lib/i18n/numbers"; -import { getMessages, getNumbers, getT } from "@/lib/i18n/server"; -import { makeT, msg, type T } from "@/lib/i18n/t"; -import { rich } from "@/lib/i18n/rich"; +import { getMessages } from "@/lib/i18n/server"; +import { makeT, msg } from "@/lib/i18n/t"; import type { Metadata } from "next"; -import { LazyLink } from "@/components/lazy-link"; import { fetchBlockHeight, fetchPool } from "@/lib/api/counterparty"; import { fetchLaunchPage, @@ -14,20 +11,7 @@ import { fetchRewardBatches, } from "@/lib/api/launchpad-api"; import { fetchBtcUsd, fetchXcpUsd } from "@/lib/api/price"; -import { shortAddress } from "@/lib/format"; -import { ratio } from "@/lib/numeric"; -import { LABEL } from "@/components/ui/tokens"; -import { TokenImage } from "@/components/token-image"; -import { XCP69_MIN_PARTICIPANTS, XCP69_RAISE_SATS } from "@/lib/xcp69"; -import { - BOUNTIES, - FALLBACK_MINT_FEE_SATS, - MINT_CAP, - MINTS_PER_MINT, - MINTS_PRICE_XCP, - SATS_PER_XCP, -} from "@/lib/rewards"; -import { EarnersTable } from "@/app/[lang]/rewards/_components/earners-table"; +import { RewardsContent } from "@/app/[lang]/rewards/_components/rewards-content"; const PAGE_METADATA: Metadata = { title: msg("XCP Rewards — xcp.fun"), @@ -55,15 +39,7 @@ export async function generateMetadata({ export const revalidate = 60; -const raiseXcp = XCP69_RAISE_SATS / 1e8; - -/** - * Not in the nav — the header's green rewards chip is the one pointer to - * this page, beside the mempool chip. - */ export default async function RewardsPage() { - const num = await getNumbers(); - const t = await getT(); const height = await fetchBlockHeight().catch(() => 0); const [stats, earners, graduates, mintsPool, xcpUsd, btcUsd, rewardBatches] = await Promise.all([ fetchLaunchStats(height).catch(() => null), @@ -77,319 +53,6 @@ export default async function RewardsPage() { fetchBtcUsd().catch(() => null), fetchRewardBatches().catch(() => []), ]); - // The chain keeps minting after this programme ends; the progress meter is - // programme progress, so it stops at its cap rather than saying 10,001 of - // 10,000 later. - const mintsSoFar = Math.min(MINT_CAP, stats?.activity.mints ?? 0); - const graduated = stats?.counts.graduated ?? 0; - const remaining = Math.max(0, MINT_CAP - mintsSoFar); - - // XCP per MINTS from live reserves (both sides are 8-decimal raw, so the - // raw ratio needs no scale correction). reserve_a/b follow the pool's own - // asset order, so pick the XCP side by name rather than by position. - const livePrice = mintsPool - ? mintsPool.asset_a === "XCP" - ? ratio(mintsPool.reserve_a, mintsPool.reserve_b) - : ratio(mintsPool.reserve_b, mintsPool.reserve_a) - : null; - const mintsPriceXcp = livePrice && livePrice > 0 ? livePrice : MINTS_PRICE_XCP; - const rewardXcp = MINTS_PER_MINT * mintsPriceXcp; - - // Sats per XCP from the same feeds the rest of the site prices with; the - // measured constant stands in only when a feed is down. - const satsPerXcp = - btcUsd && xcpUsd && xcpUsd > 0 ? (xcpUsd / btcUsd) * 1e8 : SATS_PER_XCP; - const feeSamples = stats?.activity.fee_samples ?? 0; - const measuredFee = stats?.activity.median_fee_sats ?? 0; - const typicalMintFeeSats = measuredFee > 0 ? measuredFee : FALLBACK_MINT_FEE_SATS; - const feeXcp = typicalMintFeeSats / satsPerXcp; - - return ( -
- {/* No page title — the chip that brought you here already said XCP - Rewards, and the bounty section opens the page. */} - {/* ---------------- the bounty ---------------- */} -
-

{t("The graduation bounty")}

-

- {graduated === 0 - ? t("No XCP-69 launch has graduated yet. The first three to do it earn a bounty.") - : graduated === 1 - ? t("One bounty is claimed — two are still open for the next launches to graduate.") - : graduated === 2 - ? t("Two bounties are claimed — one is still open for the next launch to graduate.") - : t("All three bounties have been claimed.")} -

- - r.fm.asset) ?? []} /> - -

- {rich( - t, - "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses, at which point the pool is created and its liquidity is burned. A launch that misses its target refunds every satoshi by consensus and does not count. {link}", - { - xcp: num.commas(raiseXcp), - n: XCP69_MIN_PARTICIPANTS, - link: ( - - {t("How that works")} - - ), - }, - )} -

-
- - {/* ---------------- the ongoing reward ---------------- */} -
-
- -

- {t("{n} MINTS for every mint", { n: num.commas(MINTS_PER_MINT) })} -

-
-

- {t( - "Mint any XCP-69 launch and earn {n} MINTS. It doesn't matter which launch, and it doesn't matter how much you mint — one transaction, one reward (valid for the first {cap} mint transactions).", - { n: num.commas(MINTS_PER_MINT), cap: num.commas(MINT_CAP) }, - )} -

- -
- - - 0 - ? t("~{xcp} XCP · {n}-mint median", { xcp: num.fixed(feeXcp, 2), n: num.commas(feeSamples) }) - : t("~{xcp} XCP · estimate", { xcp: num.fixed(feeXcp, 2) }) - } - /> - -
- -
-
- {t("Mints so far")} - - {t("{n} of {cap}", { n: num.commas(mintsSoFar), cap: num.commas(MINT_CAP) })} - -
-
-
-
-

- {t("{n} still to claim", { n: num.commas(remaining) })} -

-
-
- - {rewardBatches.length > 0 && ( -
-

{t("Distributions")}

-

- {t("Completed and confirming reward batches, linked to the transactions that sent them.")} -

-
- {rewardBatches.map((batch) => { - const fullyLinked = batch.sentRecipientCount === batch.recipientCount; - const confirmed = - fullyLinked && batch.transactions.every((tx) => tx.status === "confirmed"); - const state = confirmed ? t("Confirmed") : fullyLinked ? t("Confirming") : t("Partially sent"); - return ( -
-
-
-

- {t("Mints {from}–{to}", { - from: num.commas(batch.firstMintNumber), - to: num.commas(batch.cutoffMintNumber), - })} -

-

- {t("{recipients} recipients · {mints} mint transactions", { - recipients: num.commas(batch.recipientCount), - mints: num.commas(batch.eligibleMints), - })} -

-
-
-

- {num.commasRaw(batch.sentQuantity)} {batch.asset} -

- {!fullyLinked && ( -

- {t("of {n} in the batch", { n: num.commasRaw(batch.totalQuantity) })} -

- )} - - {state} - -
-
- -
- ); - })} -
-
- )} - {/* ---------------- who has earned what ---------------- */} -
-

{t("Earned so far")}

-

- {t("Every mint on a conforming launch, counted. Ranked by mints, because that is the unit the reward is paid in.")} -

- - -
- - {/* ---------------- the small print, in the site's FAQ grammar ---------------- */} -
-

{t("FAQ")}

-
- - {t( - "The first fairminter ever created on Counterparty — block 866,297, before any other, and it minted out free to 1,376 addresses. The supply is 100,000,000, locked forever; no more can ever be issued. The live MINTS/XCP pool prices the reward: right now 1 MINTS trades at {price} XCP, so {n} MINTS is {worth} XCP.", - { price: num.price(mintsPriceXcp), n: num.commas(MINTS_PER_MINT), worth: num.fixed(rewardXcp, 2) }, - )} - - - {t( - "The Bitcoin fee you pay is per transaction, so the reward is too. Minting one lot and minting the full 1% cost you the same fee and earn the same {n} MINTS — there is nothing to gain by splitting a mint into smaller pieces.", - { n: num.commas(MINTS_PER_MINT) }, - )} - - - {t( - "Rewards accrue as you mint and are sent in batches. Once yours is sent, its transaction appears on your profile. Lifetime earned is an all-time total, not your wallet balance.", - )} - - - {t( - "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.", - )} - -
-
-
- ); -} - -/** - * The three bounties as a podium. - * - * Ordered 2nd–1st–3rd left to right, the way a real podium stands, with the - * step heights carrying the prize sizes. It reads as a race rather than a - * price list — which is the point while bounties are open: the whole thing - * is an invitation to take the next step. - */ -function Podium({ t, num, graduated, winners }: { t: T; num: Numbers; graduated: number; winners: string[] }) { - // Visual order, not rank order. - const layout = [ - { i: 1, height: "h-20", accent: "from-gray-300 dark:from-gray-700 to-gray-200 dark:to-gray-800", ring: "ring-gray-300 dark:ring-gray-700" }, - { i: 0, height: "h-28", accent: "from-amber-300 dark:from-amber-700 to-amber-200 dark:to-amber-800", ring: "ring-amber-400" }, - { i: 2, height: "h-14", accent: "from-orange-300/70 dark:from-orange-700/70 to-orange-200/70 dark:to-orange-800/70", ring: "ring-orange-300" }, - ]; - - return ( -
- {layout.map(({ i, height, accent, ring }) => { - const b = BOUNTIES[i]!; - const claimed = graduated > i; - const winner = winners[i]; - return ( -
-
- {i + 1} -
-
- {num.commas(b.xcp)}{" "} - XCP -
- {/* The step. Height encodes the prize; the label sits inside it. */} -
- {claimed ? ( - winner ? ( - - - {winner} - - ) : ( - {t("claimed")} - ) - ) : ( - - {t("open", undefined, "bounty")} - - )} -
-
- ); - })} -
- ); -} - -function Stat({ label, value, hint }: { label: string; value: string; hint: string }) { - return ( -
-
{label}
-
{value}
-
{hint}
-
- ); -} - -function Faq({ - q, - open = false, - children, -}: { - q: string; - open?: boolean; - children: React.ReactNode; -}) { - return ( -
- {q} -

{children}

-
- ); + return ; } diff --git a/apps/web/src/app/[lang]/stats/_components/communities.tsx b/apps/web/src/app/[lang]/stats/_components/communities.tsx index 91207006..775cdf56 100644 --- a/apps/web/src/app/[lang]/stats/_components/communities.tsx +++ b/apps/web/src/app/[lang]/stats/_components/communities.tsx @@ -1,7 +1,10 @@ +"use client"; + import { CollectionChip } from "@/components/collection-chip"; import { collectionByTag } from "@/lib/collections"; import type { Communities } from "@/lib/api/launchpad-api"; -import { getNumbers, getT } from "@/lib/i18n/server"; +import { useNumbers } from "@/lib/i18n/numbers"; +import { useT } from "@/lib/i18n/client"; import { big } from "@/lib/numeric"; import { LABEL } from "@/components/ui/tokens"; import { Stat } from "@/app/[lang]/stats/_components/stat"; @@ -21,9 +24,9 @@ function percent(part: string | undefined, whole: string | undefined): number | * collection: creators, collectors, the share of all minters that is either, * and the share of all XCP minted that its members put in. */ -export async function CommunitiesSection({ data }: { data: Communities }) { - const num = await getNumbers(); - const t = await getT(); +export function CommunitiesSection({ data }: { data: Communities }) { + const num = useNumbers(); + const t = useT(); const rows = data.communities.filter((row) => row.members > 0); if (rows.length === 0 || data.minters === 0) return null; const widest = rows[0]?.members ?? 1; diff --git a/apps/web/src/app/[lang]/stats/_components/stats-content.tsx b/apps/web/src/app/[lang]/stats/_components/stats-content.tsx new file mode 100644 index 00000000..00b2ed79 --- /dev/null +++ b/apps/web/src/app/[lang]/stats/_components/stats-content.tsx @@ -0,0 +1,307 @@ +"use client"; + +import { rich } from "@/lib/i18n/rich"; +import { LazyLink } from "@/components/lazy-link"; +import type { fetchCommunities, fetchLaunchStats } from "@/lib/api/launchpad-api"; +import { CommunitiesSection } from "@/app/[lang]/stats/_components/communities"; +import { Stat } from "@/app/[lang]/stats/_components/stat"; +import type { fetchXcpUsd, fetchXcpUsdHistory } from "@/lib/api/price"; +import { fromSats } from "@/lib/format"; +import { Fiat } from "@/components/fiat"; +import { historicalUsdAt } from "@/lib/market"; +import { LABEL } from "@/components/ui/tokens"; +import { useNumbers } from "@/lib/i18n/numbers"; +import { useT } from "@/lib/i18n/client"; + +const WINDOW_DAYS = 28; + +export function StatsContent({ height, stats, xcpUsd, xcpUsdHistory, communities }: { + height: number; + stats: Awaited>; + xcpUsd: Awaited>; + xcpUsdHistory: Awaited>; + communities: Awaited>; +}) { + const num = useNumbers(); + const t = useT(); + if (!stats) { + return ( +

+ {t("Stats are unavailable right now.")} +

+ ); + } + + const { counts, total, activity, daily, blocks_per_bucket } = stats; + const settled = counts.graduated + counts.refunded; + const activeXcp = fromSats(activity.active_xcp); + const poolXcp = fromSats(stats.markets?.pool_xcp ?? 0); + const marketCapXcp = fromSats(stats.markets?.market_cap_xcp ?? 0); + const tradeXcp = fromSats(stats.markets?.trade_xcp ?? 0); + const historicalTradeUsd = (stats.markets?.trade_daily ?? []).reduce( + (sum, day) => { + const rate = historicalUsdAt(xcpUsdHistory, day.time); + return rate === null ? sum : sum + fromSats(day.xcp) * rate; + }, + 0, + ); + + // Fill the window so quiet days read as quiet rather than as missing. The + // bucket is `block / 144`, so the newest bucket is the one the tip is in. + const newest = Math.floor(height / blocks_per_bucket); + const byBucket = new Map(daily.map((d) => [d.bucket, d])); + const series = Array.from({ length: WINDOW_DAYS }, (_, i) => { + const bucket = newest - (WINDOW_DAYS - 1 - i); + return { bucket, daysAgo: newest - bucket, n: byBucket.get(bucket)?.n ?? 0 }; + }); + const peak = series.reduce((m, d) => Math.max(m, d.n), 0); + const windowTotal = series.reduce((sum, d) => sum + d.n, 0); + const refundsByBucket = new Map((stats.refunds_daily ?? []).map((d) => [d.bucket, d])); + const refundSeries = Array.from({ length: WINDOW_DAYS }, (_, i) => { + const bucket = newest - (WINDOW_DAYS - 1 - i); + const refund = refundsByBucket.get(bucket); + return { + bucket, + daysAgo: newest - bucket, + n: refund?.n ?? 0, + xcp: fromSats(refund?.xcp ?? 0), + }; + }); + const refundPeak = refundSeries.reduce((m, d) => Math.max(m, d.n), 0); + const refundTotal = refundSeries.reduce((sum, d) => sum + d.n, 0); + + // "about today" or "about 3d ago" — the bar tooltips' when-clause. + const about = (daysAgo: number) => + daysAgo === 0 ? t("today") : t("{n}d ago", { n: daysAgo }); + + return ( +
+
+

{t("Stats")}

+

+ {t("Every launch that conforms to XCP-69, and what has actually been minted.")} +

+
+ + {/* Two columns on a phone, three above it. The tiles are ordered for the + phone with `order-*` and released back to source order at `sm`, where + three-across already pairs them sensibly. On two columns the pairs + are what matter: market cap beside the pools backing it, mints beside + the minters who made them, and the two money-in-motion figures — XCP + committed to open mints, XCP that has actually traded — side by side + rather than split across the whole grid. */} +
+ }) + : t("graduated coins") + } + mobileHint={ + xcpUsd + ? rich(t, "≈ {amount} · graduated", { amount: }) + : t("graduated") + } + /> + }) + : t("in pools") + } + /> + 0 + ? rich(t, "≈ {amount} traded, all time", { amount: }) + : t("traded, all time") + } + /> + + + }) + : t("committed to open mints") + } + mobileHint={ + xcpUsd + ? rich(t, "≈ {amount} committed", { amount: }) + : t("committed") + } + /> +
+ +
+
+

{t("Minting activity")}

+ + {t("{n} in the last {days} days", { n: num.commas(windowTotal), days: WINDOW_DAYS })} + +
+ + {peak === 0 ? ( +

{t("No mints in this window.")}

+ ) : ( + <> +
+ {series.map((d) => ( +
+ ))} +
+
+ {t("{n}d ago", { n: WINDOW_DAYS })} + {t("now")} +
+ + )} +

+ {t( + "Grouped by 144 blocks — a Bitcoin day. Exact against the chain, and approximate against a wall clock, which is the right way round for a chain this page is describing.", + )} +

+
+ +
+

{t("Launches by phase")}

+
+ + + + +
+
+ + +
+

{t("Of the launches that finished")}

+ {settled === 0 ? ( +

{t("None have finished yet — nothing to score.")}

+ ) : ( + <> + {/* Both shares of one bar: the bar is every SETTLED launch, so the + widths compare against each other rather than against a total + that includes launches still in flight. */} +
+
+
+
+
+ + {t("{n} graduated · {pct}%", { + n: counts.graduated, + pct: ((counts.graduated / settled) * 100).toFixed(0), + })} + + + {t("{n} refunded · {pct}%", { + n: counts.refunded, + pct: ((counts.refunded / settled) * 100).toFixed(0), + })} + +
+ + )} +

+ {t( + "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.", + )}{" "} + + {t("How that works")} + +

+ +
+
+

{t("Refund activity")}

+ + {t("{n} in the last {days} days", { n: num.commas(refundTotal), days: WINDOW_DAYS })} + +
+ {refundPeak === 0 ? ( +

{t("No launches refunded in this window.")}

+ ) : ( + <> +
+ {refundSeries.map((d) => ( +
+ ))} +
+
+ {t("{n}d ago", { n: WINDOW_DAYS })} + {t("now")} +
+ + )} +
+
+ + {communities && } + +

+ {total === 1 + ? t("{n} conforming launch · chain tip {height}", { n: num.commas(total), height: num.commas(height) }) + : t("{n} conforming launches · chain tip {height}", { n: num.commas(total), height: num.commas(height) })} +

+
+ ); +} diff --git a/apps/web/src/app/[lang]/stats/page.tsx b/apps/web/src/app/[lang]/stats/page.tsx index 6b7c947c..2a12f349 100644 --- a/apps/web/src/app/[lang]/stats/page.tsx +++ b/apps/web/src/app/[lang]/stats/page.tsx @@ -1,19 +1,12 @@ import { isLocale } from "@/lib/i18n/locales"; import { localeAlternates } from "@/lib/i18n/seo"; -import { getMessages, getNumbers, getT } from "@/lib/i18n/server"; +import { getMessages } from "@/lib/i18n/server"; import { makeT, msg } from "@/lib/i18n/t"; -import { rich } from "@/lib/i18n/rich"; import type { Metadata } from "next"; -import { LazyLink } from "@/components/lazy-link"; import { fetchBlockHeight } from "@/lib/api/counterparty"; import { fetchCommunities, fetchLaunchStats } from "@/lib/api/launchpad-api"; -import { CommunitiesSection } from "@/app/[lang]/stats/_components/communities"; -import { Stat } from "@/app/[lang]/stats/_components/stat"; import { fetchXcpUsd, fetchXcpUsdHistory } from "@/lib/api/price"; -import { fromSats } from "@/lib/format"; -import { Fiat } from "@/components/fiat"; -import { historicalUsdAt } from "@/lib/market"; -import { LABEL } from "@/components/ui/tokens"; +import { StatsContent } from "@/app/[lang]/stats/_components/stats-content"; const PAGE_METADATA: Metadata = { title: msg("Stats — xcp.fun"), @@ -40,22 +33,7 @@ export async function generateMetadata({ export const revalidate = 60; -/** How many ~daily buckets the chart shows. Matches the window apps/api - * returns; anything longer stops being "lately". */ -const WINDOW_DAYS = 28; - -/** - * The scoreboard, and the one place refunded launches are counted. - * - * They were a "Graveyard" section on the front page, which gave failures the - * same weight as live launches — a front page should show what's happening. - * The number still matters, and arguably matters more here: all-or-nothing - * only means something if the refunds are visible somewhere, and hiding them - * entirely would be the dishonest way to remove that section. - */ export default async function StatsPage() { - const num = await getNumbers(); - const t = await getT(); const height = await fetchBlockHeight(); const [stats, xcpUsd, xcpUsdHistory, communities] = await Promise.all([ fetchLaunchStats(height), @@ -64,284 +42,5 @@ export default async function StatsPage() { fetchCommunities(), ]); - if (!stats) { - return ( -

- {t("Stats are unavailable right now.")} -

- ); - } - - const { counts, total, activity, daily, blocks_per_bucket } = stats; - const settled = counts.graduated + counts.refunded; - const activeXcp = fromSats(activity.active_xcp); - const poolXcp = fromSats(stats.markets?.pool_xcp ?? 0); - const marketCapXcp = fromSats(stats.markets?.market_cap_xcp ?? 0); - const tradeXcp = fromSats(stats.markets?.trade_xcp ?? 0); - const historicalTradeUsd = (stats.markets?.trade_daily ?? []).reduce( - (sum, day) => { - const rate = historicalUsdAt(xcpUsdHistory, day.time); - return rate === null ? sum : sum + fromSats(day.xcp) * rate; - }, - 0, - ); - - // Fill the window so quiet days read as quiet rather than as missing. The - // bucket is `block / 144`, so the newest bucket is the one the tip is in. - const newest = Math.floor(height / blocks_per_bucket); - const byBucket = new Map(daily.map((d) => [d.bucket, d])); - const series = Array.from({ length: WINDOW_DAYS }, (_, i) => { - const bucket = newest - (WINDOW_DAYS - 1 - i); - return { bucket, daysAgo: newest - bucket, n: byBucket.get(bucket)?.n ?? 0 }; - }); - const peak = series.reduce((m, d) => Math.max(m, d.n), 0); - const windowTotal = series.reduce((sum, d) => sum + d.n, 0); - const refundsByBucket = new Map((stats.refunds_daily ?? []).map((d) => [d.bucket, d])); - const refundSeries = Array.from({ length: WINDOW_DAYS }, (_, i) => { - const bucket = newest - (WINDOW_DAYS - 1 - i); - const refund = refundsByBucket.get(bucket); - return { - bucket, - daysAgo: newest - bucket, - n: refund?.n ?? 0, - xcp: fromSats(refund?.xcp ?? 0), - }; - }); - const refundPeak = refundSeries.reduce((m, d) => Math.max(m, d.n), 0); - const refundTotal = refundSeries.reduce((sum, d) => sum + d.n, 0); - - // "about today" or "about 3d ago" — the bar tooltips' when-clause. - const about = (daysAgo: number) => - daysAgo === 0 ? t("today") : t("{n}d ago", { n: daysAgo }); - - return ( -
-
-

{t("Stats")}

-

- {t("Every launch that conforms to XCP-69, and what has actually been minted.")} -

-
- - {/* Two columns on a phone, three above it. The tiles are ordered for the - phone with `order-*` and released back to source order at `sm`, where - three-across already pairs them sensibly. On two columns the pairs - are what matter: market cap beside the pools backing it, mints beside - the minters who made them, and the two money-in-motion figures — XCP - committed to open mints, XCP that has actually traded — side by side - rather than split across the whole grid. */} -
- }) - : t("graduated coins") - } - mobileHint={ - xcpUsd - ? rich(t, "≈ {amount} · graduated", { amount: }) - : t("graduated") - } - /> - }) - : t("in pools") - } - /> - 0 - ? rich(t, "≈ {amount} traded, all time", { amount: }) - : t("traded, all time") - } - /> - - - }) - : t("committed to open mints") - } - mobileHint={ - xcpUsd - ? rich(t, "≈ {amount} committed", { amount: }) - : t("committed") - } - /> -
- -
-
-

{t("Minting activity")}

- - {t("{n} in the last {days} days", { n: num.commas(windowTotal), days: WINDOW_DAYS })} - -
- - {peak === 0 ? ( -

{t("No mints in this window.")}

- ) : ( - <> -
- {series.map((d) => ( -
- ))} -
-
- {t("{n}d ago", { n: WINDOW_DAYS })} - {t("now")} -
- - )} -

- {t( - "Grouped by 144 blocks — a Bitcoin day. Exact against the chain, and approximate against a wall clock, which is the right way round for a chain this page is describing.", - )} -

-
- -
-

{t("Launches by phase")}

-
- - - - -
-
- - -
-

{t("Of the launches that finished")}

- {settled === 0 ? ( -

{t("None have finished yet — nothing to score.")}

- ) : ( - <> - {/* Both shares of one bar: the bar is every SETTLED launch, so the - widths compare against each other rather than against a total - that includes launches still in flight. */} -
-
-
-
-
- - {t("{n} graduated · {pct}%", { - n: counts.graduated, - pct: ((counts.graduated / settled) * 100).toFixed(0), - })} - - - {t("{n} refunded · {pct}%", { - n: counts.refunded, - pct: ((counts.refunded / settled) * 100).toFixed(0), - })} - -
- - )} -

- {t( - "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.", - )}{" "} - - {t("How that works")} - -

- -
-
-

{t("Refund activity")}

- - {t("{n} in the last {days} days", { n: num.commas(refundTotal), days: WINDOW_DAYS })} - -
- {refundPeak === 0 ? ( -

{t("No launches refunded in this window.")}

- ) : ( - <> -
- {refundSeries.map((d) => ( -
- ))} -
-
- {t("{n}d ago", { n: WINDOW_DAYS })} - {t("now")} -
- - )} -
-
- - {communities && } - -

- {total === 1 - ? t("{n} conforming launch · chain tip {height}", { n: num.commas(total), height: num.commas(height) }) - : t("{n} conforming launches · chain tip {height}", { n: num.commas(total), height: num.commas(height) })} -

-
- ); + return ; } diff --git a/apps/web/src/app/[lang]/swap/_components/liquidity-widget.tsx b/apps/web/src/app/[lang]/swap/_components/liquidity-widget.tsx index 2f17eaaf..f6b83614 100644 --- a/apps/web/src/app/[lang]/swap/_components/liquidity-widget.tsx +++ b/apps/web/src/app/[lang]/swap/_components/liquidity-widget.tsx @@ -1,6 +1,12 @@ "use client"; -import { useEffect, useState } from "react"; +import { ComposeError } from "@/components/compose-error"; + +import { parseRawInteger, rawToInput } from "@xcp/wallet-sdk/amounts"; +import { parseAmountRaw } from "@/lib/amount-draft"; +import { validateDepositQuote, validateWithdrawQuote, type DepositQuote, type WithdrawQuote } from "@/lib/quote-validation"; + +import { useEffect, useRef, useState } from "react"; import useSWR from "swr"; import { AmountInput } from "@/components/amount-input"; import { AssetChip } from "@/components/asset-chip"; @@ -21,7 +27,6 @@ import { big, maxRaw, minRaw, - parseUnitsToRaw, percentOf, type Raw, ratio, @@ -43,7 +48,6 @@ import { defaultTradeAsset } from "@/lib/trade-selection"; /** Pool tx size for the TX-fee estimate; true size known after compose. */ const POOL_VBYTES = 250; const PRESETS = [25, 50, 75, 100] as const; -const fmtAmount = (n: number) => n.toFixed(8).replace(/\.?0+$/, ""); interface PoolInfo { asset_a: string; @@ -53,24 +57,6 @@ interface PoolInfo { lp_asset: string; } -interface DepositQuote { - first_deposit: boolean; - asset_a: string; - asset_b: string; - quantity_a_required: Raw | null; - quantity_b_required: Raw | null; - quantity_minted_estimate: Raw | null; -} - -interface WithdrawQuote { - pool_exists: boolean; - asset_a?: string; - asset_b?: string; - quantity_a_estimate?: Raw; - quantity_b_estimate?: Raw; - supply?: Raw; -} - /** * Liquidity on top of the locked floor: the launch LP is burned forever; * anything YOU add mints LP to your address, earns the 50 bps swap fee, @@ -89,8 +75,14 @@ export function LiquidityWidget({ const usdFmt = useFiat(); const { address, status: walletStatus } = useWallet(); const compose = useCompose(); + const submittedTrade = useRef<{ + pending: Omit[0], "txid">; + action: string; + usd: number | null; + } | null>(null); const [asset, setAsset] = useState(() => defaultTradeAsset(assets)); const [tab, setTab] = useState<"add" | "remove">("add"); + const [submittedAction, setSubmittedAction] = useState<"add" | "remove" | null>(null); // Bidirectional add: edit either leg and the other derives at the pool // ratio — consensus clamps every deposit to the current ratio, so you // don't get to pick one (if you don't like the price, place an order). @@ -100,9 +92,11 @@ export function LiquidityWidget({ const [rateInverted, setRateInverted] = useState(false); const [pct, setPct] = useState(25); // remove tab const [selectorOpen, setSelectorOpen] = useState(false); + const [quoteRefreshError, setQuoteRefreshError] = useState(false); + const [refreshing, setRefreshing] = useState(false); - const { lqSlippage, customFee, medianFeeRate } = useSwapSettings(); - const feeRate = customFee > 0 ? customFee : (medianFeeRate ?? null); + const { lqSlippage, customFee, medianFeeRate, liquiditySettingsValid } = useSwapSettings(); + const feeRate = customFee ?? medianFeeRate ?? null; const { data: btcUsd } = useSWR( "btc-usd", fetchBtcUsd, @@ -116,24 +110,25 @@ export function LiquidityWidget({ ); const amount = editSide === "token" ? tokenAmount : xcpAmount; - // Parse the typed digits exactly; the double beside it feeds UI and the - // quote URL only. - const amountExact = parseUnitsToRaw(amount) ?? 0n; + // Quotes and compose retain raw integers; doubles are display only. + const amountExact = parseAmountRaw(amount) ?? 0n; const amountRaw = approx(amountExact); - const debouncedRaw = useDebounced(amountRaw, 250); + const debouncedRaw = useDebounced(amountExact.toString(), 250); // The quote's `quantity` is the FIRST asset in the URL path (verified in // counterparty-core queries.get_pool_quote_deposit) — flip the pair to // quote by whichever side is being edited. Response assets are canonical. - const { data: depositQuote, isValidating: depFetching } = useSWR( - tab === "add" && asset && debouncedRaw > 0 + const depositUrl = tab === "add" && liquiditySettingsValid && asset && amountExact > 0n && amountExact.toString() === debouncedRaw ? `${COUNTERPARTY_API_BASE}/pools/${ editSide === "token" ? `${asset}/XCP` : `XCP/${asset}` }/quote/deposit?quantity=${debouncedRaw}` - : null, - (url: string) => fetchJson(url).then((d) => d.result), - { refreshInterval: 60_000, keepPreviousData: true }, + : null; + const { data: depositQuote, isValidating: depFetching, mutate: mutateDeposit } = useSWR( + depositUrl, + (url: string) => fetchJson(url).then((d) => validateDepositQuote(d.result, + editSide === "token" ? asset : "XCP", editSide === "token" ? "XCP" : asset, BigInt(debouncedRaw))), + { refreshInterval: 60_000 }, ); - const depStale = depFetching || amountRaw !== debouncedRaw; + const depStale = depFetching || amountExact.toString() !== debouncedRaw; const depTokenRaw: Raw = depositQuote ? (depositQuote.asset_a === asset ? depositQuote.quantity_a_required @@ -161,13 +156,13 @@ export function LiquidityWidget({ } = useSpendableBalance(address, "XCP", "liquidity-xcp"); // Congestion-priced XCP gas for pool ops — usually 0, but never hardcode. - const { data: gasFee } = useSWR( + const { data: gasFee } = useSWR( address ? `${COUNTERPARTY_API_BASE}/addresses/${address}/compose/${ tab === "add" ? "pooldeposit" : "poolwithdraw" }/estimatexcpfees` : null, - (url: string) => fetchJson(url).then((d) => Number(d.result) || 0), + (url: string) => fetchJson(url).then((d) => parseRawInteger(d.result)), { refreshInterval: 60_000 }, ); @@ -217,14 +212,15 @@ export function LiquidityWidget({ ? num.percent(x / 100, { digits: 2, minDigits: 2 }) : `<${num.percent(0.01 / 100, { digits: 2, minDigits: 2 })}`; const lpToRemove = percentOf(lpBalance ?? 0, pct); - const debouncedLp = useDebounced(approx(lpToRemove), 250); + const debouncedLp = useDebounced(lpToRemove.toString(), 250); - const { data: withdrawQuote } = useSWR( - tab === "remove" && asset && debouncedLp > 0 + const withdrawalUrl = tab === "remove" && liquiditySettingsValid && asset && lpToRemove > 0n && lpToRemove.toString() === debouncedLp ? `${COUNTERPARTY_API_BASE}/pools/${asset}/XCP/quote/withdraw?quantity=${debouncedLp}` - : null, - (url: string) => fetchJson(url).then((d) => d.result), - { refreshInterval: 60_000, keepPreviousData: true }, + : null; + const { data: withdrawQuote, isValidating: withdrawalFetching } = useSWR( + withdrawalUrl, + (url: string) => fetchJson(url).then((d) => validateWithdrawQuote(d.result, asset, "XCP", BigInt(debouncedLp))), + { refreshInterval: 60_000 }, ); const outTokenRaw: Raw = withdrawQuote ? (withdrawQuote.asset_a === asset @@ -237,47 +233,19 @@ export function LiquidityWidget({ : withdrawQuote.quantity_b_estimate) ?? 0 : 0; - const busy = isBusy(compose.status); + const busy = isBusy(compose.status) || refreshing; useEffect(() => { - if (compose.status === "confirmed") { - registerPending({ - txid: compose.txid, - kind: "pool", - label: - tab === "add" - ? t("Add {asset}/XCP liquidity", { asset }) - : t("Remove {asset}/XCP liquidity", { asset }), - address: address ?? undefined, - spends: - tab === "add" - ? [ - { asset, raw: big(depTokenRaw).toString() }, - { - asset: "XCP", - raw: (big(depXcpRaw) + big(gasFee ?? 0)).toString(), - }, - ] - : pool?.lp_asset - ? [{ asset: pool.lp_asset, raw: lpToRemove.toString() }] - : undefined, - }); + if (compose.status === "error") submittedTrade.current = null; + if (compose.status === "confirmed" && submittedTrade.current) { + const submitted = submittedTrade.current; + submittedTrade.current = null; + registerPending({ ...submitted.pending, txid: compose.txid }); + trackTx(compose.txid, submitted.action, submitted.usd); } - }, [ - t, - compose.status, - compose.txid, - tab, - asset, - address, - depTokenRaw, - depXcpRaw, - gasFee, - pool?.lp_asset, - lpToRemove, - ]); - - const needTokenRaw = editSide === "token" ? amountRaw : depTokenNum; + }, [compose.status, compose.txid]); + + const needTokenRaw = editSide === "token" ? amountExact : big(depTokenRaw); const insufficientToken = tokenBalance !== undefined && needTokenRaw > 0 && @@ -286,7 +254,7 @@ export function LiquidityWidget({ const insufficientXcp = xcpBalance !== undefined && depXcpNum > 0 && - depXcpNum + (gasFee ?? 0) > xcpBalance; + big(depXcpRaw) + big(gasFee ?? 0) > xcpBalance; // Failed balance reads do not hold the deposit — see useSpendableBalance's // balanceUnavailable. Only a read still in flight does, and briefly. @@ -294,6 +262,7 @@ export function LiquidityWidget({ (tokenBalance !== undefined || tokenBalanceUnavailable) && (xcpBalance !== undefined || xcpBalanceUnavailable); const addReady = + liquiditySettingsValid && gasFee !== undefined && !depStale && tab === "add" && amountRaw > 0 && depTokenNum > 0 && @@ -304,13 +273,50 @@ export function LiquidityWidget({ !insufficientXcp && !depositQuote?.first_deposit; const removeReady = + liquiditySettingsValid && gasFee !== undefined && withdrawQuote !== undefined && !withdrawalFetching && lpToRemove.toString() === debouncedLp && tab === "remove" && lpBalance !== undefined && lpToRemove > 0n && !busy; - const submitAdd = () => { - if (!addReady || !depositQuote) return; + const intentKey = JSON.stringify([asset, address, tab, editSide, amount, pct, lqSlippage, customFee, liquiditySettingsValid, pool?.lp_asset]); + const currentIntent = useRef(intentKey); + useEffect(() => { currentIntent.current = intentKey; }, [intentKey]); + + const submitAdd = async () => { + if (!addReady || !depositQuote || !depositUrl) return; + const submittedIntent = intentKey; + setQuoteRefreshError(false); + setRefreshing(true); + try { + const fresh = validateDepositQuote((await fetchJson(depositUrl)).result, + editSide === "token" ? asset : "XCP", editSide === "token" ? "XCP" : asset, amountExact); + if (currentIntent.current !== submittedIntent) return; + // The paired spend is part of the review. Show a changed quote before + // accepting it, and never weaken the displayed minimum LP quantity. + if (fresh.first_deposit || big(fresh.quantity_a_required) !== big(depositQuote.quantity_a_required) + || big(fresh.quantity_b_required) !== big(depositQuote.quantity_b_required) + || big(fresh.quantity_minted_estimate) < reduceByPercent(depositQuote.quantity_minted_estimate, lqSlippage)) { + await mutateDeposit(fresh, { revalidate: false }); + setQuoteRefreshError(true); + return; + } + if (submittedTrade.current) return; + setSubmittedAction("add"); + // Freeze both debits and the protocol gas estimate before signing. + submittedTrade.current = { + pending: { + kind: "pool", + label: t("Add {asset}/XCP liquidity", { asset }), + address: address ?? undefined, + spends: [ + { asset, raw: big(depTokenRaw).toString() }, + { asset: "XCP", raw: (big(depXcpRaw) + big(gasFee ?? 0)).toString() }, + ], + }, + action: "liquidity added", + usd: xcpUsd && depXcpNum > 0 ? (depXcpNum / SATS) * xcpUsd * 2 : null, + }; compose.composePoolDeposit({ asset_a: asset, asset_b: "XCP", @@ -320,19 +326,50 @@ export function LiquidityWidget({ depositQuote.quantity_minted_estimate, lqSlippage, ), - fee_rate: customFee > 0 ? customFee : undefined, + fee_rate: customFee ?? undefined, }); + } catch { setQuoteRefreshError(true); } + finally { setRefreshing(false); } }; - const submitRemove = () => { - if (!removeReady || !pool) return; + const submitRemove = async () => { + if (!removeReady || !pool || !withdrawQuote || !withdrawalUrl) return; + const submittedIntent = intentKey; + setQuoteRefreshError(false); + setRefreshing(true); + try { + const fresh = validateWithdrawQuote((await fetchJson(withdrawalUrl)).result, asset, "XCP", lpToRemove); + if (currentIntent.current !== submittedIntent) return; + if (fresh.asset_a !== pool.asset_a || fresh.asset_b !== pool.asset_b + || big(fresh.quantity_a_estimate) < reduceByPercent(withdrawQuote.quantity_a_estimate, lqSlippage) + || big(fresh.quantity_b_estimate) < reduceByPercent(withdrawQuote.quantity_b_estimate, lqSlippage)) { + setQuoteRefreshError(true); + return; + } + if (submittedTrade.current) return; + setSubmittedAction("remove"); + submittedTrade.current = { + pending: { + kind: "pool", + label: t("Remove {asset}/XCP liquidity", { asset }), + address: address ?? undefined, + spends: [ + { asset: pool.lp_asset, raw: lpToRemove.toString() }, + ...(big(gasFee ?? 0) > 0n ? [{ asset: "XCP", raw: big(gasFee ?? 0).toString() }] : []), + ], + }, + action: "liquidity removed", + usd: xcpUsd && approx(outXcpRaw) > 0 ? (approx(outXcpRaw) / SATS) * xcpUsd * 2 : null, + }; compose.composePoolWithdraw({ lp_asset: pool.lp_asset, quantity: lpToRemove, min_quantity_a: reduceByPercent(withdrawQuote?.quantity_a_estimate, lqSlippage), min_quantity_b: reduceByPercent(withdrawQuote?.quantity_b_estimate, lqSlippage), - fee_rate: customFee > 0 ? customFee : undefined, + fee_rate: customFee ?? undefined, }); + } catch { setQuoteRefreshError(true); } + finally { setRefreshing(false); } }; const addLabel = busy @@ -356,25 +393,10 @@ export function LiquidityWidget({ // Both legs are worth the same by construction; USD comes off the XCP leg. const legUsd = xcpUsd && depXcpNum > 0 ? (depXcpNum / SATS) * xcpUsd : null; - // Placed after legUsd for the value; the add and remove sides are separate - // events because they mean opposite things about a pool's depth. - useEffect(() => { - if (compose.status === "confirmed") { - const withdrawUsd = - xcpUsd && approx(outXcpRaw) > 0 ? (approx(outXcpRaw) / SATS) * xcpUsd * 2 : null; - trackTx( - compose.txid, - tab === "add" ? "liquidity added" : "liquidity removed", - // A deposit is both legs; legUsd is one of two equal legs. - tab === "add" ? (legUsd === null ? null : legUsd * 2) : withdrawUsd, - ); - } - }, [compose.status, compose.txid, tab, legUsd, outXcpRaw, xcpUsd]); - const txFeeRow = feeRate !== null && (
{t("TX fee")}
-
0 ? "font-medium text-purple-600 dark:text-purple-400" : ""}> +
{num.satsPerVb(feeRate)} sat/vB {btcUsd != null && ( @@ -428,7 +450,7 @@ export function LiquidityWidget({ onClick={() => { setEditSide("token"); setTokenAmount( - fmtAmount(approx(percentOf(maxDepositRaw, p)) / SATS), + rawToInput(percentOf(maxDepositRaw, p), 8), ); }} className="rounded-md border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 px-1.5 py-0.5 text-[10px] font-medium text-gray-500 dark:text-gray-400 transition-colors hover:border-purple-400 dark:hover:border-purple-500 hover:text-purple-600 dark:hover:text-purple-400 active:scale-95" @@ -457,7 +479,7 @@ export function LiquidityWidget({ }`} onClick={() => { setEditSide("token"); - setTokenAmount(fmtAmount(approx(maxDepositRaw) / SATS)); + setTokenAmount(rawToInput(maxDepositRaw, 8)); }} > {t("Balance: {n}", { n: num.commasRaw(tokenBalance) })} @@ -472,7 +494,7 @@ export function LiquidityWidget({ editSide === "token" ? tokenAmount : depTokenNum > 0 - ? fmtAmount(depTokenNum / SATS) + ? rawToInput(depTokenRaw, 8) : "" } onChange={(v) => { @@ -511,8 +533,7 @@ export function LiquidityWidget({ // the token side through the ratio, minus gas. setEditSide("xcp"); setXcpAmount( - fmtAmount( - approx( + rawToInput( minRaw( maxRaw(0n, big(xcpBalance) - big(gasFee ?? 0)), reserveToken > 0n @@ -520,7 +541,7 @@ export function LiquidityWidget({ reserveToken : big(xcpBalance), ), - ) / SATS, + 8, ), ); }} @@ -537,7 +558,7 @@ export function LiquidityWidget({ editSide === "xcp" ? xcpAmount : depXcpNum > 0 - ? fmtAmount(depXcpNum / SATS) + ? rawToInput(depXcpRaw, 8) : "" } onChange={(v) => { @@ -710,8 +731,9 @@ export function LiquidityWidget({ )}
+ {quoteRefreshError && setQuoteRefreshError(false)}>{t("Quote changed or could not be refreshed. Review the amounts and try again.")}} {compose.status === "error" && ( - {compose.error} + )} {walletStatus !== "connected" ? ( @@ -742,7 +764,7 @@ export function LiquidityWidget({
- {tab === "add" + {submittedAction === "add" ? rich(t, "Deposit broadcast — {tx}", { tx: }) : rich(t, "Withdrawal broadcast — {tx}", { tx: })} diff --git a/apps/web/src/app/[lang]/swap/_components/swap-settings.tsx b/apps/web/src/app/[lang]/swap/_components/swap-settings.tsx index 065db834..5e3d837e 100644 --- a/apps/web/src/app/[lang]/swap/_components/swap-settings.tsx +++ b/apps/web/src/app/[lang]/swap/_components/swap-settings.tsx @@ -10,6 +10,7 @@ import { } from "react"; import useSWR from "swr"; import { AmountInput } from "@/components/amount-input"; +import { parseBoundedSetting } from "@/lib/amount-draft"; import { GearPopover } from "@/components/ui/popover"; import { useT } from "@/lib/i18n/client"; import { useNumbers } from "@/lib/i18n/numbers"; @@ -68,7 +69,10 @@ interface SwapSettingsValue { customSlip: number; slippage: number; expiration: number; - customFee: number; + customFee: number | null; + swapSettingsValid: boolean; + limitSettingsValid: boolean; + liquiditySettingsValid: boolean; lqCustomSlip: number; lqSlippage: number; } @@ -107,19 +111,21 @@ export function SwapSettingsProvider({ children }: { children: ReactNode }) { lqCustomSlippage, limitExpiration, } = stored; - const customSlip = Math.min(parseFloat(customSlippage) || 0, 50); + const slipDraft = parseBoundedSetting(customSlippage, 50); + const expirationDraft = parseBoundedSetting(customExpiration, 5000, 1, 0); + const feeDraft = parseBoundedSetting(customFeeRate, 500); + const lqSlipDraft = parseBoundedSetting(lqCustomSlippage, 50); + const customSlip = slipDraft.value ?? 0; const slippage = slippageAuto ? autoValue - : customSlip > 0 + : !slipDraft.empty ? customSlip : slippagePreset; - const expiration = Math.min( - 5000, - Math.max(1, Math.round(parseFloat(customExpiration)) || MARKET_EXPIRATION), - ); - const customFee = Math.min(parseFloat(customFeeRate) || 0, 500); - const lqCustomSlip = Math.min(parseFloat(lqCustomSlippage) || 0, 50); - const lqSlippage = lqCustomSlip > 0 ? lqCustomSlip : lqSlippagePreset; + const expiration = expirationDraft.value ?? MARKET_EXPIRATION; + const customFee = feeDraft.value; + const lqCustomSlip = lqSlipDraft.value ?? 0; + const lqSlippage = !lqSlipDraft.empty ? lqCustomSlip : lqSlippagePreset; + const limitSettingsValid = feeDraft.valid && Number.isInteger(limitExpiration) && limitExpiration >= 1 && limitExpiration <= 5000; return { slippageAuto, setSlippageAuto: (v) => updateSettings({ slippageAuto: v }), @@ -144,6 +150,9 @@ export function SwapSettingsProvider({ children }: { children: ReactNode }) { slippage, expiration, customFee, + swapSettingsValid: feeDraft.valid && expirationDraft.valid && (slippageAuto || slipDraft.valid), + limitSettingsValid, + liquiditySettingsValid: feeDraft.valid && lqSlipDraft.valid, lqCustomSlip, lqSlippage, }; @@ -164,7 +173,7 @@ export function SwapSettingsGear() { active={ !s.slippageAuto || s.expiration !== MARKET_EXPIRATION || - s.customFee > 0 + s.customFee !== null } label={t("Swap settings")} > @@ -194,7 +203,7 @@ export function SwapSettingsGear() { s.setCustomSlippage(""); }} className={`flex-1 rounded-lg border px-2 py-1.5 text-xs font-medium transition-colors ${ - !s.slippageAuto && s.slippage === p && s.customSlip === 0 + !s.slippageAuto && s.slippage === p && s.customSlippage === "" ? "border-purple-600 bg-purple-50 dark:bg-purple-950/40 text-purple-700 dark:text-purple-300" : "border-gray-200 dark:border-gray-800 text-gray-600 dark:text-gray-400 hover:border-gray-300 dark:hover:border-gray-700" }`} @@ -204,13 +213,14 @@ export function SwapSettingsGear() { ))}
0 + !s.slippageAuto && s.customSlippage !== "" ? "border-purple-600 bg-purple-50 dark:bg-purple-950/40" : "border-gray-200 dark:border-gray-800" }`} > { s.setCustomSlippage(v); if (v.trim() !== "") s.setSlippageAuto(false); @@ -255,6 +265,8 @@ export function SwapSettingsGear() { {t("TX fee")} 0 ? "border-purple-600 bg-purple-50 dark:bg-purple-950/40" : "border-gray-200 dark:border-gray-800" + s.customFee !== null ? "border-purple-600 bg-purple-50 dark:bg-purple-950/40" : "border-gray-200 dark:border-gray-800" }`} > 0} + active={s.limitExpiration !== 1000 || s.customFee !== null} label={t("Limit order settings")} >
{t("Expiration")}
@@ -323,11 +336,12 @@ export function LimitSettingsGear() { {t("TX fee")} 0 ? "border-purple-600 bg-purple-50 dark:bg-purple-950/40" : "border-gray-200 dark:border-gray-800" + s.customFee !== null ? "border-purple-600 bg-purple-50 dark:bg-purple-950/40" : "border-gray-200 dark:border-gray-800" }`} > 0 || s.customFee > 0} + active={s.lqCustomSlippage !== "" || s.customFee !== null} label={t("Liquidity settings")} >
{t("Max slippage")}
@@ -367,7 +381,7 @@ export function LiquiditySettingsGear() { s.setLqCustomSlippage(""); }} className={`flex-1 rounded-lg border px-2 py-1.5 text-xs font-medium transition-colors ${ - s.lqSlippage === p && s.lqCustomSlip === 0 + s.lqSlippage === p && s.lqCustomSlippage === "" ? "border-purple-600 bg-purple-50 dark:bg-purple-950/40 text-purple-700 dark:text-purple-300" : "border-gray-200 dark:border-gray-800 text-gray-600 dark:text-gray-400 hover:border-gray-300 dark:hover:border-gray-700" }`} @@ -377,13 +391,14 @@ export function LiquiditySettingsGear() { ))}
0 + s.lqCustomSlippage !== "" ? "border-purple-600 bg-purple-50 dark:bg-purple-950/40" : "border-gray-200 dark:border-gray-800" }`} > {t("TX fee")} 0 ? "border-purple-600 bg-purple-50 dark:bg-purple-950/40" : "border-gray-200 dark:border-gray-800" + s.customFee !== null ? "border-purple-600 bg-purple-50 dark:bg-purple-950/40" : "border-gray-200 dark:border-gray-800" }`} > n.toFixed(8).replace(/\.?0+$/, ""); export function SwapWidget({ assets, @@ -103,6 +107,11 @@ export function SwapWidget({ const { code } = useFxRate(); const { address, status: walletStatus } = useWallet(); const compose = useCompose(); + const submittedTrade = useRef<{ + pending: Omit[0], "txid">; + action: string; + usd: number | null; + } | null>(null); const [giveAsset, setGiveAsset] = useState("XCP"); const [getAsset, setGetAsset] = useState(() => defaultTradeAsset(assets)); const [amount, setAmount] = useState(""); @@ -110,6 +119,9 @@ export function SwapWidget({ const [rateInverted, setRateInverted] = useState(false); const [flips, setFlips] = useState(0); const [priceMoved, setPriceMoved] = useState(false); + const [quoteRefreshError, setQuoteRefreshError] = useState(false); + const [refreshing, setRefreshing] = useState(false); + const submitting = useRef(false); const [lastQuoteAt, setLastQuoteAt] = useState(null); // Settings live beside the mode tabs; the widget consumes the values @@ -117,9 +129,9 @@ export function SwapWidget({ const { slippage, slippageAuto, - customSlip, expiration, customFee, + swapSettingsValid, medianFeeRate, setAutoValue, } = useSwapSettings(); @@ -132,7 +144,7 @@ export function SwapWidget({ fetchBtcUsd, { refreshInterval: 60_000 }, ); - const feeRate = customFee > 0 ? customFee : (medianFeeRate ?? null); + const feeRate = customFee ?? medianFeeRate ?? null; const action = giveAsset === "XCP" ? "buy" : getAsset === "XCP" ? "sell" : "swap"; @@ -144,12 +156,12 @@ export function SwapWidget({ ]; // Parse the typed digits exactly (a full XCP-69 bag is 10^16 raw, past // double precision); the double beside it feeds UI-only paths. - const amountExact = parseUnitsToRaw(amount) ?? 0n; + const amountExact = parseAmountRaw(amount) ?? 0n; const amountRaw = approx(amountExact); - const debouncedRaw = useDebounced(amountRaw, 250); + const debouncedRaw = useDebounced(amountExact.toString(), 250); const quoteUrl = - giveAsset && getAsset && giveAsset !== getAsset && debouncedRaw > 0 + swapSettingsValid && amountExact > 0n && amountExact.toString() === debouncedRaw && giveAsset && getAsset && giveAsset !== getAsset ? `${COUNTERPARTY_API_BASE}/pools/${encodeURIComponent(giveAsset)}/${encodeURIComponent(getAsset)}/quote?quantity=${debouncedRaw}` : null; const { @@ -159,7 +171,7 @@ export function SwapWidget({ mutate: mutateQuote, } = useSWR( quoteUrl, - (url: string) => fetchJson(url).then((d) => d.result), + (url: string) => fetchJson(url).then((d) => validateSwapQuote(d.result)), { refreshInterval: QUOTE_REFRESH_MS, onSuccess: () => setLastQuoteAt(Date.now()), @@ -257,7 +269,7 @@ export function SwapWidget({ { refreshInterval: 60_000 }, ); - const staleQuote = isValidating || amountRaw !== debouncedRaw; + const staleQuote = isValidating || amountExact.toString() !== debouncedRaw; const outRaw: Raw = quote && amountRaw > 0 ? quote.estimated_output : 0; const out = approx(outRaw) / SATS; const amountHuman = amountRaw / SATS; @@ -265,8 +277,8 @@ export function SwapWidget({ const minReceivedRaw = reduceByPercent(outRaw, slippage); const impact = quote?.price_impact ?? 0; const insufficient = - effBalance !== undefined && amountRaw > 0 && amountRaw > effBalance; - const busy = isBusy(compose.status); + effBalance !== undefined && amountExact > 0n && amountExact > effBalance; + const busy = isBusy(compose.status) || refreshing; // The quote once the pending orders have gone first, as a fraction of the // quote against the confirmed state. Null while the inputs are loading or @@ -334,36 +346,20 @@ export function SwapWidget({ }, [neededSlippage, setAutoValue]); useEffect(() => { - if (compose.status === "confirmed") { - const label = - action === "buy" - ? t("Buy {asset} — market order", { asset: getAsset }) - : action === "sell" - ? t("Sell {asset} — market order", { asset: giveAsset }) - : t("Swap {give} for {get} — market order", { give: giveAsset, get: getAsset }); - registerPending({ - txid: compose.txid, - kind: "order", - label, - address: address ?? undefined, - spends: [{ asset: giveAsset, raw: amountExact.toString() }], - }); + if (compose.status === "error") submittedTrade.current = null; + if (compose.status === "confirmed" && submittedTrade.current) { + const submitted = submittedTrade.current; + submittedTrade.current = null; + registerPending({ ...submitted.pending, txid: compose.txid }); + trackTx(compose.txid, submitted.action, submitted.usd); } - }, [ - t, - compose.status, - compose.txid, - action, - giveAsset, - getAsset, - amountExact, - address, - ]); + }, [compose.status, compose.txid]); // A failed balance read does not hold the trade — see useSpendableBalance's // balanceUnavailable. Only a read still in flight does, and briefly. const balanceSettled = effBalance !== undefined || balanceUnavailable; const ready = + swapSettingsValid && balanceSettled && amountRaw > 0 && approx(outRaw) > 0 && @@ -379,15 +375,6 @@ export function SwapWidget({ const giveUsd = tradeUsd; const getUsd = tradeUsd; - // Reported here rather than alongside registerPending above, because the - // trade's USD value isn't computed until this point. trackTx dedupes on the - // txid, so this effect re-running as the rate refreshes costs nothing. - useEffect(() => { - if (compose.status === "confirmed") { - trackTx(compose.txid, action, tradeUsd); - } - }, [compose.status, compose.txid, action, tradeUsd]); - // Rate line: 1 = , tap to invert. const rate = out > 0 && amountHuman > 0 ? out / amountHuman : null; const rateText = @@ -414,34 +401,65 @@ export function SwapWidget({ : null; const rateBaseUsd = rateInverted ? getUnitUsd : giveUnitUsd; + const intentKey = JSON.stringify([amount, giveAsset, getAsset, address, slippage, expiration, customFee, swapSettingsValid]); + const currentIntent = useRef(intentKey); + useEffect(() => { currentIntent.current = intentKey; }, [intentKey]); + const submit = async () => { - if (!ready || !quote || !quoteUrl) return; + if (!ready || !quote || !quoteUrl || submitting.current) return; + submitting.current = true; + setRefreshing(true); + const submittedIntent = intentKey; + setQuoteRefreshError(false); let fresh = quote; try { - fresh = (await fetchJson(quoteUrl)).result as Quote; + fresh = validateSwapQuote((await fetchJson(quoteUrl)).result as Quote); + if (currentIntent.current !== submittedIntent) return; + parseRawInteger(fresh.estimated_output, { min: 1n }); mutateQuote(fresh, { revalidate: false }); + // eslint-disable-next-line react-hooks/purity -- This runs after a user's click and awaited quote fetch, never during render. setLastQuoteAt(Date.now()); - if (ratio(fresh.estimated_output, quote.estimated_output) < 0.99) { + if (big(fresh.estimated_output) < minReceivedRaw) { setPriceMoved(true); return; } - } catch { - // fall back to the polled quote - } setPriceMoved(false); - compose.composeOrder({ + if (submittedTrade.current) return; + // The form and wallet can change while signing. Reserve only the spend + // submitted to this compose call, and consume it once on confirmation. + submittedTrade.current = { + pending: { + kind: "order", + label: action === "buy" + ? t("Buy {asset} — market order", { asset: getAsset }) + : action === "sell" + ? t("Sell {asset} — market order", { asset: giveAsset }) + : t("Swap {give} for {get} — market order", { give: giveAsset, get: getAsset }), + address: address ?? undefined, + spends: [{ asset: giveAsset, raw: amountExact.toString() }], + }, + action, + usd: tradeUsd, + }; + await compose.composeOrder({ give_asset: giveAsset, give_quantity: amountExact, get_asset: getAsset, - get_quantity: reduceByPercent(fresh.estimated_output, slippage), + get_quantity: maxRaw(minReceivedRaw, reduceByPercent(fresh.estimated_output, slippage)), expiration, - fee_rate: customFee > 0 ? customFee : undefined, + fee_rate: customFee ?? undefined, }); + } catch { + setQuoteRefreshError(true); + } finally { + submitting.current = false; + setRefreshing(false); + } }; const flip = () => { setFlips((f) => f + 1); - if (out > 0) setAmount(fmtAmount(out)); + if (out > 0) setAmount(rawToInput(outRaw, 8)); setGiveAsset(getAsset); setGetAsset(giveAsset); setRateInverted(false); @@ -484,7 +502,7 @@ export function SwapWidget({ key={p} type="button" onClick={() => - setAmount(fmtAmount(approx(percentOf(effBalance, p)) / SATS)) + setAmount(rawToInput(percentOf(effBalance, p), 8)) } className="rounded-md border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 px-1.5 py-0.5 text-[10px] font-medium text-gray-500 dark:text-gray-400 transition-colors hover:border-purple-400 dark:hover:border-purple-500 hover:text-purple-600 dark:hover:text-purple-400 active:scale-95" > @@ -514,7 +532,7 @@ export function SwapWidget({ + +
; +} diff --git a/apps/web/src/components/number-preference.tsx b/apps/web/src/components/number-preference.tsx new file mode 100644 index 00000000..c8074cb8 --- /dev/null +++ b/apps/web/src/components/number-preference.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { useT } from "@/lib/i18n/client"; +import { LOCALES, LOCALE_INFO } from "@/lib/i18n/locales"; +import { NUMBER_LOCALE_VARIANTS } from "@/lib/i18n/number-locales"; +import { setNumberLocale, useNumberPreference, type NumberLocale } from "@/lib/number-preference"; + +export function NumberPreference() { + const t = useT(); + const { locale, auto } = useNumberPreference(); + return ; +} diff --git a/apps/web/src/components/site-footer.tsx b/apps/web/src/components/site-footer.tsx index 6b1ba5ce..7e7456a7 100644 --- a/apps/web/src/components/site-footer.tsx +++ b/apps/web/src/components/site-footer.tsx @@ -2,6 +2,7 @@ import { usePathname, useRouter } from "next/navigation"; import { Globe, rememberLocale } from "@/components/language-switch"; +import { NumberPreference } from "@/components/number-preference"; import { LazyLink } from "@/components/lazy-link"; import { CURRENCIES, type Currency, setCurrency, useCurrency } from "@/lib/currency"; import { useLocale, useMachineDrafted, useT } from "@/lib/i18n/client"; @@ -27,6 +28,7 @@ export function SiteFooter() {
+ {/* The reference pages, a second time. They are in the header too, but the header is the one row that every language competes for, and on a phone they live behind a burger. A footer link costs diff --git a/apps/web/src/components/ui/error-banner.tsx b/apps/web/src/components/ui/error-banner.tsx index 76b56b39..b53a802a 100644 --- a/apps/web/src/components/ui/error-banner.tsx +++ b/apps/web/src/components/ui/error-banner.tsx @@ -18,10 +18,10 @@ export function ErrorBanner({ const t = useT(); if (!children) return null; return ( -

- {children} +

{children}
{onDismiss && (
); } diff --git a/apps/web/src/components/ui/well.tsx b/apps/web/src/components/ui/well.tsx index 244af708..8067d76d 100644 --- a/apps/web/src/components/ui/well.tsx +++ b/apps/web/src/components/ui/well.tsx @@ -48,7 +48,7 @@ export function Well({
{layout === "row" ? (
- {children} +
{children}
{chip}
) : ( diff --git a/apps/web/src/lib/amount-draft.ts b/apps/web/src/lib/amount-draft.ts new file mode 100644 index 00000000..de80b8bb --- /dev/null +++ b/apps/web/src/lib/amount-draft.ts @@ -0,0 +1,33 @@ +import { COUNTERPARTY_MAX_INT, parseAmountDraft } from "@xcp/wallet-sdk/amounts"; + +/** User drafts never go through the arithmetic parser, which intentionally rounds. */ +export function parseAmountRaw(draft: string, decimals: 0 | 8 = 8): bigint | null { + const result = parseAmountDraft(draft, { decimals, minRaw: 0n, maxRaw: COUNTERPARTY_MAX_INT }); + return result.status === "valid" ? result.raw : null; +} + +/** A divisible token sold in whole-token lots still uses eight raw places. */ +export function parseWholeTokenRaw(draft: string): bigint | null { + const result = parseAmountDraft(draft, { decimals: 0, maxRaw: COUNTERPARTY_MAX_INT / 100_000_000n }); + return result.status === "valid" ? result.raw * 100_000_000n : null; +} + +/** Only bounded, validated settings become doubles; token quantities stay bigint. */ +export function parseBoundedSetting(draft: string, max: number, min = 0, decimals: 0 | 8 = 8) { + const result = parseAmountDraft(draft, { + decimals, + minRaw: BigInt(Math.round(min * 10 ** decimals)), + maxRaw: BigInt(Math.round(max * 10 ** decimals)), + }); + return { + empty: draft === "", + valid: draft === "" || result.status === "valid", + value: result.status === "valid" ? Number(result.canonical) : null, + }; +} + +/** Single-line HTML inputs remove control characters before change events. + * Keep pasted/dropped controls visible as escapes, so they remain invalid. */ +export function visibleDraft(text: string): string { + return text.replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t"); +} diff --git a/apps/web/src/lib/currency.ts b/apps/web/src/lib/currency.ts index a8c6a49d..0404ac73 100644 --- a/apps/web/src/lib/currency.ts +++ b/apps/web/src/lib/currency.ts @@ -4,7 +4,7 @@ import { useCallback, useSyncExternalStore } from "react"; import { trackEvent } from "@/lib/analytics"; import { fetchFxRates } from "@/lib/api/launchpad-api"; import { fiat } from "@/lib/format"; -import { useLocale } from "@/lib/i18n/client"; +import { useNumberLocale } from "@/lib/number-preference"; /** * Which currency the site's fiat figures are shown in. @@ -332,7 +332,7 @@ export function useCurrency(): CurrencyState { */ export function useFiat(): (usd: number) => string { const { code, rate } = useFxRate(); - const locale = useLocale(); + const locale = useNumberLocale(); return useCallback((usd: number) => fiat(usd * rate, code, locale), [code, rate, locale]); } diff --git a/apps/web/src/lib/format.ts b/apps/web/src/lib/format.ts index cfdf3973..e1ae1f51 100644 --- a/apps/web/src/lib/format.ts +++ b/apps/web/src/lib/format.ts @@ -8,6 +8,7 @@ import { SATS, } from "@/lib/numeric"; import { isLocale, LOCALE_INFO } from "@/lib/i18n/locales"; +import { isNumberLocale } from "@/lib/i18n/number-locales"; import { makeT, type T } from "@/lib/i18n/t"; /** `t` for callers that have no locale: the English source text itself. */ @@ -116,7 +117,7 @@ export function price(n: number, locale = "en"): string { * whole — 2, not 2.00. */ export function satsPerVb(n: number, locale = "en"): string { - return n.toLocaleString(intlLocale(locale), { maximumFractionDigits: 2 }); + return n.toLocaleString(intlLocale(locale), { maximumFractionDigits: 8 }); } /** @@ -171,6 +172,7 @@ const SHAPES = new Map(); * conventions — Japan's fullwidth ¥ and 万/億 groupings, Hong Kong's HK$ — * and the English ones for everything else, so a page never mixes two. */ export function intlLocale(locale: string): string { + if (!isLocale(locale) && isNumberLocale(locale)) return locale; return isLocale(locale) ? LOCALE_INFO[locale].intl : "en-US"; } diff --git a/apps/web/src/lib/i18n/locales.ts b/apps/web/src/lib/i18n/locales.ts index 5eaabeb3..dead4c3a 100644 --- a/apps/web/src/lib/i18n/locales.ts +++ b/apps/web/src/lib/i18n/locales.ts @@ -17,13 +17,13 @@ * region in vocabulary, not only in characters — Taiwan writes 軟體 and 網路 * where Hong Kong writes 軟件 and 網絡 — so `zh-tw` and `zh-hk` are separate * files, derived from the Simplified draft with OpenCC's regional phrase - * tables and then checked by hand. + * tables. Per-key status records which wording still needs human review. * * Spanish is one locale for the whole language. The site's Spanish readers * are in the Americas — Mexico, Venezuela, Argentina — so the draft is - * Latin American in vocabulary (billetera, not cartera) and neutral enough - * that Spain reads it without noticing. Korean is one locale and one - * currency, and counts in 만/억 like Japanese and Chinese. + * Latin American in vocabulary. Keep a shared base until reviewed regional + * wording warrants overrides; Spain's number formatting is independently + * selectable. Korean uses 만/억 compact display. Fiat is a separate choice. * * Portuguese is Brazilian (`pt`, tagged pt-BR): Brazil is where the site's * Portuguese readers are, and Portugal reads Brazilian fine. French is one diff --git a/apps/web/src/lib/i18n/number-locales.ts b/apps/web/src/lib/i18n/number-locales.ts new file mode 100644 index 00000000..be5329f8 --- /dev/null +++ b/apps/web/src/lib/i18n/number-locales.ts @@ -0,0 +1,13 @@ +import { isLocale, type Locale } from "@/lib/i18n/locales"; + +/** Number conventions can be offered without adding an interface catalog. */ +export const NUMBER_LOCALE_VARIANTS = [ + { locale: "de-DE", native: "Deutsch (Deutschland)" }, + { locale: "es-ES", native: "Español (España)" }, + { locale: "es-VE", native: "Español (Venezuela)" }, +] as const; + +export type NumberLocale = Locale | (typeof NUMBER_LOCALE_VARIANTS)[number]["locale"]; +export function isNumberLocale(value: unknown): value is NumberLocale { + return isLocale(value) || NUMBER_LOCALE_VARIANTS.some(entry => entry.locale === value); +} diff --git a/apps/web/src/lib/i18n/numbers.ts b/apps/web/src/lib/i18n/numbers.ts index 69b0d0fa..bd9cd1c4 100644 --- a/apps/web/src/lib/i18n/numbers.ts +++ b/apps/web/src/lib/i18n/numbers.ts @@ -2,7 +2,7 @@ import { useMemo } from "react"; import { bindNumbers, type Numbers } from "@/lib/format"; -import { useLocale } from "@/lib/i18n/client"; +import { useNumberLocale } from "@/lib/number-preference"; export type { Numbers }; @@ -14,6 +14,6 @@ export type { Numbers }; * cannot be imported from the server. This file is only the hook. */ export function useNumbers(): Numbers { - const locale = useLocale(); + const locale = useNumberLocale(); return useMemo(() => bindNumbers(locale), [locale]); } diff --git a/apps/web/src/lib/inscribe-launch.ts b/apps/web/src/lib/inscribe-launch.ts index d056e690..f9b45fb6 100644 --- a/apps/web/src/lib/inscribe-launch.ts +++ b/apps/web/src/lib/inscribe-launch.ts @@ -10,6 +10,9 @@ import { } from "@/lib/inscriber"; import { prepareFairminterInscriptionPsbt } from "@/lib/inscriber/fairminter"; import { COUNTERPARTY_API_BASE } from "@/lib/constants"; +import { WalletSdkError } from "@xcp/wallet-sdk"; +import { parseRawInteger } from "@xcp/wallet-sdk/amounts"; +import { assertSameTransaction, readPsbt, readTransaction } from "@/lib/transaction-verification"; const ELECTRS_API_BASE = "https://api.counterparty.io:3000"; @@ -37,21 +40,48 @@ export function taprootPubkey(address: string): Uint8Array { return decoded.pubkey; } +/** Core queries.get_utxo_balances filters positive balances. Only a complete + * successful empty page is proof of no confirmed Counterparty balances. */ +async function hasCounterpartyBalances(utxo: Utxo): Promise { + const cp = await fetch(`${COUNTERPARTY_API_BASE}/utxos/${utxo.txid}:${utxo.vout}/balances?limit=1`); + if (!cp.ok) throw new WalletSdkError("network", `Could not verify funding UTXO balances: HTTP ${cp.status}`); + const data = await cp.json(); + if (data.error || !Array.isArray(data.result) || (data.result.length === 0 && data.next_cursor !== null)) { + throw new WalletSdkError("invalid_response", "Could not verify funding UTXO balances"); + } + return data.result.length > 0; +} + +/** The witness prevout must agree with the exact, hash-verified parent bytes. */ +async function verifyFundingPrevout(utxo: Utxo, address: string): Promise { + const response = await fetch(`${ELECTRS_API_BASE}/tx/${utxo.txid}/hex`); + if (!response.ok) throw new WalletSdkError("network", `Could not verify funding transaction: HTTP ${response.status}`); + const parent = readTransaction(await response.text()); + const output = utxo.vout < parent.outputsLength ? parent.getOutput(utxo.vout) : undefined; + if (parent.id !== utxo.txid || output?.amount !== BigInt(utxo.value) + || !output.script || hexCodec.encode(output.script) !== hexCodec.encode(addressToScriptPubKey(address))) { + throw new WalletSdkError("transaction_mismatch", "Funding UTXO does not match its parent transaction"); + } +} + async function pickFundingUtxo(address: string, minValue: number): Promise { const res = await fetch(`${ELECTRS_API_BASE}/address/${address}/utxo`); if (!res.ok) throw new Error("Could not fetch UTXOs"); const utxos: Utxo[] = await res.json(); + if (!Array.isArray(utxos)) throw new WalletSdkError("invalid_response", "Invalid funding UTXO list"); + for (const utxo of utxos) { + if (!/^[0-9a-f]{64}$/.test(utxo.txid) || !Number.isSafeInteger(utxo.vout) || utxo.vout < 0 + || typeof utxo.status?.confirmed !== "boolean") throw new WalletSdkError("invalid_response", "Invalid funding UTXO"); + parseRawInteger(utxo.value, { min: 1n, max: 2_100_000_000_000_000n }); + } const candidates = utxos .filter((u) => u.status.confirmed && u.value >= minValue) .sort((a, b) => b.value - a.value); for (const utxo of candidates) { // Never spend a UTXO carrying Counterparty balances as plain fuel. - const cp = await fetch(`${COUNTERPARTY_API_BASE}/utxos/${utxo.txid}:${utxo.vout}?verbose=true`); - if (cp.ok) { - const data = await cp.json(); - if (Array.isArray(data.result) ? data.result.length > 0 : data.result) continue; - } + if (await hasCounterpartyBalances(utxo)) continue; + await verifyFundingPrevout(utxo, address); return utxo; } throw new Error(`No spendable UTXO with at least ${minValue} sats (asset-bearing UTXOs are skipped)`); @@ -117,9 +147,15 @@ export async function inscribeLaunch(opts: { revealScript: hexCodec.encode(prepared.revealScript), tapInternalKey: hexCodec.encode(prepared.tapInternalKey), }); + assertSameTransaction(readPsbt(commit.psbtHex), readPsbt(signedCommit)); const commitRawTx = finalizeSignedPsbt(signedCommit); + assertSameTransaction(readPsbt(commit.psbtHex), readTransaction(commitRawTx)); const commitTxid = txidFromRawTx(commitRawTx); + // Recheck after a potentially long wallet approval, before spending fuel. + if (await hasCounterpartyBalances(fundingUtxo)) { + throw new WalletSdkError("invalid_argument", "Funding UTXO now carries Counterparty balances"); + } onStep("broadcast-commit"); await opts.broadcast(commitRawTx); @@ -138,7 +174,9 @@ export async function inscribeLaunch(opts: { onStep("sign-reveal"); const signedReveal = await opts.signPsbt(reveal.psbtHex, { [address]: [0] }); + assertSameTransaction(readPsbt(reveal.psbtHex), readPsbt(signedReveal)); const revealRawTx = finalizeSignedPsbt(signedReveal); + assertSameTransaction(readPsbt(reveal.psbtHex), readTransaction(revealRawTx)); onStep("broadcast-reveal"); const revealTxid = await opts.broadcast(revealRawTx); diff --git a/apps/web/src/lib/inscriber/transactions.ts b/apps/web/src/lib/inscriber/transactions.ts index b81a0cd5..5519fafa 100644 --- a/apps/web/src/lib/inscriber/transactions.ts +++ b/apps/web/src/lib/inscriber/transactions.ts @@ -82,7 +82,9 @@ export function prepareCommit( const tapInternalKey = btc.TAPROOT_UNSPENDABLE_KEY const commitOutput = btc.p2tr(tapInternalKey, { script, leafVersion: 0xc0 }, NETWORK, true) const revealWeight = estimateRevealWeight(script.length, !!data.parentInscriptionId) - const revealBaseFee = revealWeight.vsize * feeRate + // Generated BTC fees round up to whole satoshis; the chosen sat/vB rate + // remains fractional. Otherwise 0.1 sat/vB can create a fractional output. + const revealBaseFee = Math.ceil(revealWeight.vsize * feeRate) const revealFeePadding = feePadding(revealBaseFee) const revealFundedFee = revealBaseFee + revealFeePadding diff --git a/apps/web/src/lib/limit-amounts.ts b/apps/web/src/lib/limit-amounts.ts new file mode 100644 index 00000000..467b55e0 --- /dev/null +++ b/apps/web/src/lib/limit-amounts.ts @@ -0,0 +1,15 @@ +import { COUNTERPARTY_MAX_INT } from "@xcp/wallet-sdk/amounts"; + +const UNIT = 100_000_000n; +const ceilDivide = (a: bigint, b: bigint) => (a + b - 1n) / b; + +/** Quantize the derived leg without weakening the user's limit price. */ +export function deriveLimitAmounts(side: "buy" | "sell", edited: "amount" | "total", value: bigint, price: bigint) { + if (value <= 0n || price <= 0n) return null; + const amount = edited === "amount" ? value + : side === "buy" ? ceilDivide(value * UNIT, price) : value * UNIT / price; + const total = edited === "total" ? value + : side === "sell" ? ceilDivide(value * price, UNIT) : value * price / UNIT; + if (amount <= 0n || total <= 0n || amount > COUNTERPARTY_MAX_INT || total > COUNTERPARTY_MAX_INT) return null; + return { amount, total }; +} diff --git a/apps/web/src/lib/number-preference.ts b/apps/web/src/lib/number-preference.ts new file mode 100644 index 00000000..2a8a30ae --- /dev/null +++ b/apps/web/src/lib/number-preference.ts @@ -0,0 +1,54 @@ +"use client"; + +import { useSyncExternalStore } from "react"; +import { useLocale } from "@/lib/i18n/client"; +import { isNumberLocale, type NumberLocale } from "@/lib/i18n/number-locales"; +export { isNumberLocale, type NumberLocale } from "@/lib/i18n/number-locales"; + +export const NUMBER_PREF_KEY = "xcpfun:number-locale:v1"; +const listeners = new Set<() => void>(); +let memory: NumberLocale | null | undefined; + +function read(): NumberLocale | null { + if (memory !== undefined) return memory; + try { + const saved = localStorage.getItem(NUMBER_PREF_KEY); + memory = isNumberLocale(saved) ? saved : null; + } catch { memory = null; } + return memory; +} +function changed() { for (const listener of listeners) listener(); } +function onStorage(event: StorageEvent) { + if (event.key === NUMBER_PREF_KEY || event.key === null) { + memory = undefined; + changed(); + } +} +function subscribe(listener: () => void) { + listeners.add(listener); + if (listeners.size === 1) window.addEventListener("storage", onStorage); + return () => { + listeners.delete(listener); + if (!listeners.size) window.removeEventListener("storage", onStorage); + }; +} +const readServer = () => null; + +export function setNumberLocale(value: NumberLocale | "auto") { + if (value !== "auto" && !isNumberLocale(value)) return; + memory = value === "auto" ? null : value; + try { + if (memory === null) localStorage.removeItem(NUMBER_PREF_KEY); + else localStorage.setItem(NUMBER_PREF_KEY, memory); + } catch { /* The preference still works for this session. */ } + changed(); +} + +/** This preference is presentation only. Transaction parsers never read it. */ +export function useNumberPreference() { + const language = useLocale(); + const explicit = useSyncExternalStore(subscribe, read, readServer); + return { locale: explicit ?? language, auto: explicit === null }; +} + +export function useNumberLocale(): NumberLocale { return useNumberPreference().locale; } diff --git a/apps/web/src/lib/quote-validation.ts b/apps/web/src/lib/quote-validation.ts new file mode 100644 index 00000000..d77287d8 --- /dev/null +++ b/apps/web/src/lib/quote-validation.ts @@ -0,0 +1,54 @@ +import { parseRawInteger } from "@xcp/wallet-sdk/amounts"; +import type { Raw } from "@/lib/numeric"; + +export interface DepositQuote { + first_deposit: boolean; + asset_a: string; + asset_b: string; + quantity_a_required: Raw | null; + quantity_b_required: Raw | null; + quantity_minted_estimate: Raw | null; +} +export interface WithdrawQuote { + pool_exists: boolean; + asset_a?: string; + asset_b?: string; + quantity?: Raw; + quantity_a_estimate?: Raw; + quantity_b_estimate?: Raw; + supply?: Raw; +} + +function requirePair(a: unknown, b: unknown, first: string, second: string) { + if (!((a === first && b === second) || (a === second && b === first))) { + throw new Error("quote_asset_mismatch"); + } +} + +/** Core quotes canonicalize the pair, but quantity always names URL asset1. */ +export function validateDepositQuote(q: DepositQuote, first: string, second: string, quantity: bigint): DepositQuote { + requirePair(q.asset_a, q.asset_b, first, second); + if (q.first_deposit === true) return q; + if (q.first_deposit !== false) throw new Error("quote_invalid"); + const a = parseRawInteger(q.quantity_a_required!, { min: 1n }); + const b = parseRawInteger(q.quantity_b_required!, { min: 1n }); + parseRawInteger(q.quantity_minted_estimate!, { min: 1n }); + if ((q.asset_a === first ? a : b) !== quantity) throw new Error("quote_quantity_mismatch"); + return q; +} + +export function validateWithdrawQuote(q: WithdrawQuote, first: string, second: string, quantity: bigint): WithdrawQuote { + if (q.pool_exists !== true) throw new Error("quote_pool_unavailable"); + requirePair(q.asset_a, q.asset_b, first, second); + if (parseRawInteger(q.quantity!) !== quantity) throw new Error("quote_quantity_mismatch"); + parseRawInteger(q.quantity_a_estimate!); + parseRawInteger(q.quantity_b_estimate!); + if (parseRawInteger(q.supply!, { min: 1n }) < quantity) throw new Error("quote_quantity_mismatch"); + return q; +} + +export function validateSwapQuote(q: T): T { + parseRawInteger(q.estimated_output); + if (!Number.isFinite(q.price_impact)) throw new Error("quote_invalid"); + return q; +} diff --git a/apps/web/src/lib/transaction-verification.ts b/apps/web/src/lib/transaction-verification.ts new file mode 100644 index 00000000..8be8779b --- /dev/null +++ b/apps/web/src/lib/transaction-verification.ts @@ -0,0 +1,40 @@ +import { base64, hex } from "@scure/base"; +import { Transaction } from "@scure/btc-signer"; +import { WalletSdkError } from "@xcp/wallet-sdk"; + +export const TX_VERIFICATION_OPTIONS = { + allowUnknownInputs: true, + allowUnknownOutputs: true, + allowLegacyWitnessUtxo: true, + disableScriptCheck: true, +} as const; + +export function readTransaction(raw: string): Transaction { + try { + const transaction = Transaction.fromRaw(hex.decode(raw), TX_VERIFICATION_OPTIONS); + if (!transaction.inputsLength || !transaction.outputsLength) throw new Error("Empty transaction"); + return transaction; + } catch (cause) { + throw new WalletSdkError("invalid_response", "Invalid transaction bytes", { cause }); + } +} + +export function readPsbt(encoded: string): Transaction { + try { + const bytes = /^[0-9a-f]+$/i.test(encoded) && encoded.length % 2 === 0 + ? hex.decode(encoded) : base64.decode(encoded); + const transaction = Transaction.fromPSBT(bytes, TX_VERIFICATION_OPTIONS); + if (!transaction.inputsLength || !transaction.outputsLength) throw new Error("Empty transaction"); + return transaction; + } catch (cause) { + throw new WalletSdkError("invalid_response", "Invalid PSBT bytes", { cause }); + } +} + +/** Bitcoin-envelope check, matching the SDK pipeline's internal verifier. + * Only signatures may change. This does not decode a Counterparty message. */ +export function assertSameTransaction(expected: Transaction, actual: Transaction): void { + if (hex.encode(expected.unsignedTx) !== hex.encode(actual.unsignedTx)) { + throw new WalletSdkError("transaction_mismatch", "Transaction inputs, outputs or amounts changed"); + } +} diff --git a/apps/web/src/locales/_source.json b/apps/web/src/locales/_source.json index c252ae48..6a323f96 100644 --- a/apps/web/src/locales/_source.json +++ b/apps/web/src/locales/_source.json @@ -199,10 +199,10 @@ "components/pending-dock.tsx" ], "{n} conforming launch · chain tip {height}": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "{n} conforming launches · chain tip {height}": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "{n} fill": [ "components/address-hover-card.tsx" @@ -211,7 +211,7 @@ "components/address-hover-card.tsx" ], "{n} graduated · {pct}%": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "{n} held": [ "app/[lang]/research/_components/live-behavior-dashboard.tsx" @@ -223,7 +223,7 @@ "app/[lang]/[asset]/_components/launch-view.tsx" ], "{n} in the last {days} days": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "{n} launch": [ "app/[lang]/profile/_components/rewards-card.tsx", @@ -238,7 +238,7 @@ "components/address-hover-card.tsx" ], "{n} mint · about {when}": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "{n} minted": [ "app/[lang]/[asset]/_components/launch-view.tsx" @@ -260,10 +260,10 @@ "components/address-hover-card.tsx" ], "{n} mints · about {when}": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "{n} MINTS for every mint": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "{n} MINTS per mint · all-time program total, not your wallet balance.": [ "app/[lang]/profile/_components/rewards-card.tsx" @@ -272,7 +272,7 @@ "app/[lang]/research/_components/live-behavior-dashboard.tsx" ], "{n} of {cap}": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "{n} of {min} minters": [ "app/[lang]/_components/launch-sections.tsx" @@ -293,16 +293,16 @@ "app/[lang]/research/_components/live-behavior-dashboard.tsx" ], "{n} refund · {xcp} XCP returned · about {when}": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "{n} refunded · {pct}%": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "{n} refunded hidden": [ "app/[lang]/_components/launch-search.tsx" ], "{n} refunds · {xcp} XCP returned · about {when}": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "{n} routes → {n} wallet signatures": [ "app/[lang]/dispense/_components/bridge.tsx" @@ -320,7 +320,7 @@ "app/[lang]/research/_components/live-behavior-dashboard.tsx" ], "{n} still to claim": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "{n} tokens": [ "app/[lang]/[asset]/_components/mint-panel.tsx", @@ -396,7 +396,7 @@ "lib/format.ts" ], "{n}d ago": [ - "app/[lang]/stats/page.tsx", + "app/[lang]/stats/_components/stats-content.tsx", "lib/chain-time.ts" ], "{n}h": [ @@ -480,7 +480,7 @@ "app/[lang]/docs/page.tsx" ], "{recipients} recipients · {mints} mint transactions": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "{resolve} All-or-nothing at the 69M soft cap. The soft cap equals the entire public sale, so reaching it {is} selling out — there is no partial success. Sell out and the pool seeds; miss the deadline and the protocol refunds every minter automatically and destroys the escrowed supply. Resolution happens at end-of-block even on a hard-cap fill, so nobody can trade the pool in the transaction that creates it.": [ "app/[lang]/docs/page.tsx" @@ -587,10 +587,10 @@ "app/[lang]/[asset]/_components/trade-panel.tsx" ], "~{xcp} XCP · {n}-mint median": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "~{xcp} XCP · estimate": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "~1 day": [ "app/[lang]/create/page.tsx", @@ -615,22 +615,22 @@ "app/[lang]/dispense/_components/bridge.tsx" ], "≈ {amount} · graduated": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "≈ {amount} · graduated coins": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "≈ {amount} committed": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "≈ {amount} committed to open mints": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "≈ {amount} in pools": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "≈ {amount} traded, all time": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "≈ {eta} from now — an estimate, not a guarantee; block time isn't exact.": [ "app/[lang]/create/page.tsx" @@ -715,7 +715,7 @@ "app/[lang]/[asset]/_components/activity-tabs.tsx" ], "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "A resting order refunds in full at expiry — the price you set is enforced by the order itself.": [ "app/[lang]/swap/_components/swap-settings.tsx" @@ -751,7 +751,7 @@ "app/[lang]/profile/_components/pools-tab.tsx" ], "Active escrow": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "Activity": [ "app/[lang]/profile/_components/profile-view.tsx", @@ -802,7 +802,7 @@ "app/[lang]/docs/page.tsx" ], "All three bounties have been claimed.": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "All-time": [ "app/[lang]/_components/launch-sections.tsx" @@ -862,7 +862,7 @@ "app/[lang]/faq/page.tsx" ], "announced, not open": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "Any future block, {floor} — tighter than the presets go. Under ~2 hours pays double the network fee.": [ "app/[lang]/create/page.tsx" @@ -895,7 +895,7 @@ "app/[lang]/dispense/_components/bridge.tsx" ], "at the live pool price": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "Auto": [ "app/[lang]/swap/_components/swap-settings.tsx" @@ -1149,6 +1149,9 @@ "Change in {currency} over the last 24 hours": [ "app/[lang]/_components/launch-sections.tsx" ], + "Check the amount, units, and decimal places.": [ + "components/compose-error.tsx" + ], "Checking availability…": [ "app/[lang]/create/page.tsx" ], @@ -1175,7 +1178,7 @@ "app/[lang]/swap/_components/swap-widget.tsx" ], "claimed": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "Clear done": [ "components/pending-dock.tsx" @@ -1210,13 +1213,13 @@ "app/[lang]/stats/_components/communities.tsx" ], "committed": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "Committed": [ "app/[lang]/rewards/_components/earners-table.tsx" ], "committed to open mints": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "Communities": [ "app/[lang]/stats/_components/communities.tsx" @@ -1225,7 +1228,7 @@ "app/[lang]/stats/_components/communities.tsx" ], "Completed and confirming reward batches, linked to the transactions that sent them.": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "Compose failed ({status})": [ "app/[lang]/dispense/_lib/use-dispense-router.ts" @@ -1264,7 +1267,7 @@ "components/pending-dock.tsx" ], "Confirmed": [ - "app/[lang]/rewards/page.tsx", + "app/[lang]/rewards/_components/rewards-content.tsx", "components/pending-dock.tsx" ], "Confirmed — resting on the book with {amount} {asset} escrowed.": [ @@ -1281,7 +1284,7 @@ ], "Confirming": [ "app/[lang]/profile/_components/rewards-tab.tsx", - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "Conformance": [ "app/[lang]/docs/page.tsx" @@ -1313,6 +1316,7 @@ ], "Copied": [ "app/[lang]/profile/_components/profile-view.tsx", + "components/compose-error.tsx", "components/header-wallet.tsx" ], "Copied ✓": [ @@ -1325,6 +1329,9 @@ "Copy as Markdown": [ "app/[lang]/docs/_components/copy-button.tsx" ], + "Copy diagnostics": [ + "components/compose-error.tsx" + ], "Copy failed": [ "app/[lang]/docs/_components/copy-button.tsx" ], @@ -1343,6 +1350,10 @@ "Corrections": [ "app/[lang]/research/page.tsx" ], + "Could not refresh the quote. Try again.": [ + "app/[lang]/dispense/_lib/use-dispense-router.ts", + "app/[lang]/swap/_components/swap-widget.tsx" + ], "Couldn't check the {give}/{get} pool. Quotes may still use the order book.": [ "app/[lang]/swap/_components/swap-widget.tsx" ], @@ -1377,7 +1388,7 @@ "app/[lang]/research/page.tsx" ], "Covered": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "Create": [ "app/[lang]/_components/home-toolbar.tsx" @@ -1480,13 +1491,13 @@ "app/[lang]/dispense/_components/bridge.tsx" ], "distinct addresses": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "Distribution": [ "app/[lang]/profile/_components/rewards-tab.tsx" ], "Distributions": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "Docs": [ "app/[lang]/docs/page.tsx", @@ -1500,7 +1511,7 @@ "app/[lang]/faq/page.tsx" ], "Doesn't minting cost me the XCP?": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "Done": [ "app/[lang]/create/page.tsx", @@ -1531,7 +1542,7 @@ "app/[lang]/rewards/_components/earners-table.tsx" ], "Earned so far": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "Edit token info": [ "app/[lang]/[asset]/_components/edit-panel.tsx" @@ -1539,9 +1550,15 @@ "Editing is locked for this session": [ "app/[lang]/[asset]/_components/edit-panel.tsx" ], + "Enter a multiple of {n}.": [ + "app/[lang]/[asset]/_components/mint-panel.tsx" + ], "Enter a price": [ "app/[lang]/[asset]/_components/trade-panel.tsx" ], + "Enter a value from {min} to {max}.": [ + "components/amount-input.tsx" + ], "Enter an amount": [ "app/[lang]/[asset]/_components/mint-panel.tsx", "app/[lang]/[asset]/_components/trade-panel.tsx", @@ -1565,13 +1582,13 @@ "app/[lang]/activity/_components/activity-view.tsx" ], "Every launch that conforms to XCP-69, and what has actually been minted.": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "Every LP token is burned — this liquidity can never be withdrawn.": [ "app/[lang]/[asset]/_components/activity-tabs.tsx" ], "Every mint on a conforming launch, counted. Ranked by mints, because that is the unit the reward is paid in.": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "Every minted token dumped in one trade — the pool still quotes a bid with {left} XCP left, and sellers exit at {mult}× mint ({paid}% of what they paid). Those {left} XCP can never be withdrawn: every launch is a permanent sink for XCP itself.": [ "app/[lang]/faq/_components/explainer.tsx" @@ -1657,7 +1674,7 @@ ], "FAQ": [ "app/[lang]/faq/page.tsx", - "app/[lang]/rewards/page.tsx", + "app/[lang]/rewards/_components/rewards-content.tsx", "components/site-footer.tsx", "components/site-header.tsx" ], @@ -1705,6 +1722,9 @@ "Floor: {n}": [ "app/[lang]/dispense/_components/bridge.tsx" ], + "Follow language": [ + "components/number-preference.tsx" + ], "Full 1M bags sold before yours": [ "app/[lang]/research/_components/exit-race-simulator.tsx" ], @@ -1729,7 +1749,7 @@ "graduated": [ "app/[lang]/[asset]/_components/launch-view.tsx", "app/[lang]/_components/launch-search.tsx", - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "Graduated": [ "app/[lang]/[asset]/_components/launch-chrome.tsx", @@ -1737,16 +1757,16 @@ "app/[lang]/_components/launch-sections.tsx", "app/[lang]/activity/_components/activity-view.tsx", "app/[lang]/profile/_components/launches-tab.tsx", - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "graduated coins": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.": [ "app/[lang]/docs/page.tsx" ], "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses, at which point the pool is created and its liquidity is burned. A launch that misses its target refunds every satoshi by consensus and does not count. {link}": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "Graduation": [ "app/[lang]/docs/page.tsx" @@ -1764,7 +1784,7 @@ "app/[lang]/_components/launch-sections.tsx" ], "Grouped by 144 blocks — a Bitcoin day. Exact against the chain, and approximate against a wall clock, which is the right way round for a chain this page is describing.": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "guaranteed by consensus, not this website": [ "app/[lang]/faq/page.tsx" @@ -1833,8 +1853,8 @@ "app/[lang]/docs/page.tsx" ], "How that works": [ - "app/[lang]/rewards/page.tsx", - "app/[lang]/stats/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx", + "app/[lang]/stats/_components/stats-content.tsx" ], "How XCP-69 works": [ "app/[lang]/research/page.tsx" @@ -1867,7 +1887,7 @@ "app/[lang]/[asset]/_components/launch-view.tsx" ], "in pools": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "In the mempool — matching runs the moment the block lands.": [ "components/order-tracker.tsx" @@ -1998,7 +2018,7 @@ "app/[lang]/rewards/_components/earners-table.tsx" ], "Launches by phase": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "Launches use the {fairminter} message (ID 90) with the pool fields {poolQuantity} and {lpAsset} set. Mints are ordinary fairmint messages in whole-lot multiples of {quantityByPrice}.": [ "app/[lang]/docs/page.tsx" @@ -2150,7 +2170,7 @@ "app/[lang]/_components/launch-sections.tsx", "app/[lang]/docs/page.tsx", "app/[lang]/faq/_components/explainer.tsx", - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "market order": [ "app/[lang]/docs/page.tsx" @@ -2217,7 +2237,7 @@ "app/[lang]/[asset]/_components/mint-panel.tsx" ], "Mint any XCP-69 launch and earn {n} MINTS. It doesn't matter which launch, and it doesn't matter how much you mint — one transaction, one reward (valid for the first {cap} mint transactions).": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "Mint broadcast": [ "app/[lang]/[asset]/_components/mint-panel.tsx" @@ -2233,7 +2253,7 @@ "app/[lang]/faq/_components/explainer.tsx" ], "mint transactions": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "Mint window": [ "app/[lang]/faq/page.tsx" @@ -2268,7 +2288,7 @@ "Minters": [ "app/[lang]/_components/launch-sections.tsx", "app/[lang]/research/_components/live-behavior-dashboard.tsx", - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "minting": [ "app/[lang]/_components/launch-search.tsx" @@ -2280,10 +2300,10 @@ "app/[lang]/activity/_components/activity-view.tsx", "app/[lang]/profile/_components/launches-tab.tsx", "app/[lang]/profile/_components/profile-view.tsx", - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "Minting activity": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "minting is live": [ "app/[lang]/[asset]/_components/scheduled-pulse.tsx" @@ -2317,11 +2337,11 @@ "app/[lang]/profile/_components/minting-tab.tsx", "app/[lang]/profile/_components/rewards-tab.tsx", "app/[lang]/rewards/_components/earners-table.tsx", - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "Mints {from}–{to}": [ "app/[lang]/profile/_components/rewards-tab.tsx", - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "Mints must be whole-lot multiples of {quantityByPrice}, within the per-transaction cap, and within the address's remaining allowance — a partially used cap can be topped up across multiple transactions. The minter needs the XCP {onBalance}; a funded BTC wallet with no XCP will compose-fail with {error}.": [ "app/[lang]/docs/page.tsx" @@ -2333,13 +2353,13 @@ "app/[lang]/faq/page.tsx" ], "Mints so far": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "Mints, refunds, and pool or order-book fills on XCP-69 launches. “Other” reconciles those events to the live balance and can represent a send, receive, burn, or liquidity movement. An open mint shows what you've committed — that XCP is escrowed by consensus and comes back automatically if the launch misses its soft cap.": [ "app/[lang]/profile/_components/activity-tab.tsx" ], "missed the cap, paid back": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "More than one wallet is installed. Pick the one to connect with.": [ "components/connect-button.tsx" @@ -2387,7 +2407,7 @@ "app/[lang]/dispense/_components/bridge.tsx" ], "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.": [ "app/[lang]/faq/page.tsx" @@ -2421,7 +2441,7 @@ "app/[lang]/mempool/_components/mempool-view.tsx" ], "No launches refunded in this window.": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "No launches yet.": [ "app/[lang]/activity/_components/activity-view.tsx" @@ -2442,7 +2462,7 @@ "app/[lang]/rewards/_components/earners-table.tsx" ], "No mints in this window.": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "No mints yet — be the first.": [ "app/[lang]/[asset]/_components/activity-tabs.tsx" @@ -2511,7 +2531,7 @@ "app/[lang]/profile/_components/minting-tab.tsx" ], "No XCP-69 launch has graduated yet. The first three to do it earn a bounty.": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "No XCP-69 tokens have been sent to the Counterparty burn address yet.": [ "app/[lang]/activity/_components/activity-view.tsx" @@ -2529,7 +2549,7 @@ "app/[lang]/create/page.tsx" ], "None have finished yet — nothing to score.": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "Not enough history to chart yet.": [ "app/[lang]/profile/_components/portfolio-chart.tsx" @@ -2576,14 +2596,17 @@ ], "now": [ "app/[lang]/[asset]/_components/price-chart.tsx", - "app/[lang]/stats/page.tsx", + "app/[lang]/stats/_components/stats-content.tsx", "lib/format.ts" ], + "Number format": [ + "components/number-preference.tsx" + ], "Observed address behavior, not identity or intent. Not investment advice.": [ "app/[lang]/research/page.tsx" ], "of {n} in the batch": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "of {n} minters, {pct}%": [ "app/[lang]/stats/_components/communities.tsx" @@ -2598,10 +2621,10 @@ "app/[lang]/[asset]/_components/mint-panel.tsx" ], "Of the launches that finished": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "of the typical mint fee": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "of which {amount} realized": [ "app/[lang]/profile/_components/positions-tab.tsx" @@ -2628,7 +2651,7 @@ "app/[lang]/docs/page.tsx" ], "One bounty is claimed — two are still open for the next launches to graduate.": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "One fixed parameter set for token launches on Counterparty: the launch sells out and liquidity locks forever, or everyone is refunded. Zero creator take, enforced by consensus.": [ "app/[lang]/faq/page.tsx" @@ -2640,7 +2663,7 @@ "app/[lang]/dispense/_components/bridge.tsx" ], "one transaction": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "One wallet confirmation per route — keep this tab open.": [ "app/[lang]/dispense/_components/bridge.tsx" @@ -2661,13 +2684,13 @@ "components/site-header.tsx" ], "open right now": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "Open XCP price": [ "app/[lang]/_components/market-pulse.tsx" ], "open@@bounty": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "open@@pool slider": [ "app/[lang]/faq/_components/explainer.tsx" @@ -2750,7 +2773,7 @@ "components/pending-dock.tsx" ], "Partially sent": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "Partially. The 10 XCP per-address cap makes a fake crowd cost at least 69 funded addresses — it raises the price of the act, it cannot prevent it. Sybil-resistant in cost, not in principle. The mint tape, address histories, and holder spread are all public, so a manufactured crowd has to fake those too.": [ "app/[lang]/faq/page.tsx" @@ -2772,7 +2795,7 @@ "app/[lang]/[asset]/_components/launch-stats.tsx" ], "Per mint": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "Per-address cap": [ "app/[lang]/faq/page.tsx" @@ -2919,6 +2942,9 @@ "Queue": [ "app/[lang]/dispense/_components/bridge.tsx" ], + "Quote changed or could not be refreshed. Review the amounts and try again.": [ + "app/[lang]/swap/_components/liquidity-widget.tsx" + ], "Quote moved — the numbers above are updated. Press again to swap at the new price.": [ "app/[lang]/swap/_components/swap-widget.tsx" ], @@ -2973,6 +2999,9 @@ "Recommended": [ "components/connect-button.tsx" ], + "Reconnect your wallet and try again.": [ + "components/compose-error.tsx" + ], "redeployed": [ "app/[lang]/research/_components/live-behavior-dashboard.tsx" ], @@ -2988,7 +3017,7 @@ "app/[lang]/_components/market-pulse.tsx" ], "Refund activity": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "refunded": [ "app/[lang]/_components/launch-search.tsx", @@ -2999,7 +3028,7 @@ "app/[lang]/activity/_components/activity-view.tsx", "app/[lang]/profile/_components/activity-tab.tsx", "app/[lang]/profile/_components/launches-tab.tsx", - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "Refunds are not a support process. They are automatic protocol behavior: at the deadline, every minter's XCP is returned and the escrowed supply is destroyed in the same block. Failed launches move to the graveyard, where their history is preserved — the mint tape, the participant count, and the on-chain proof: a destruction record tagged {tag}.": [ "app/[lang]/docs/page.tsx" @@ -3025,6 +3054,9 @@ "Represented": [ "app/[lang]/stats/_components/communities.tsx" ], + "Request declined in wallet.": [ + "components/compose-error.tsx" + ], "Resolve.": [ "app/[lang]/docs/page.tsx" ], @@ -3039,7 +3071,7 @@ "components/rewards-chip.tsx" ], "Rewards accrue as you mint and are sent in batches. Once yours is sent, its transaction appears on your profile. Lifetime earned is an all-time total, not your wallet balance.": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "rewritten": [ "app/[lang]/docs/page.tsx" @@ -3092,7 +3124,7 @@ "app/[lang]/_components/launch-sections.tsx", "app/[lang]/activity/_components/activity-view.tsx", "app/[lang]/profile/_components/launches-tab.tsx", - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "Search asset name": [ "app/[lang]/_components/launch-search.tsx" @@ -3163,7 +3195,7 @@ "app/[lang]/research/_components/live-behavior-dashboard.tsx" ], "Send": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "Settles when it confirms — usually the next block.": [ "app/[lang]/swap/_components/liquidity-widget.tsx" @@ -3246,7 +3278,7 @@ "app/[lang]/[asset]/_components/launch-view.tsx" ], "sold out, pool locked": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "Something went wrong": [ "app/[lang]/[asset]/_components/edit-panel.tsx", @@ -3268,7 +3300,7 @@ "app/[lang]/faq/page.tsx" ], "Stats": [ - "app/[lang]/stats/page.tsx", + "app/[lang]/stats/_components/stats-content.tsx", "components/site-footer.tsx", "components/site-header.tsx" ], @@ -3276,7 +3308,7 @@ "app/[lang]/stats/page.tsx" ], "Stats are unavailable right now.": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "Status": [ "app/[lang]/activity/_components/activity-view.tsx" @@ -3343,6 +3375,9 @@ "Target start block": [ "app/[lang]/create/page.tsx" ], + "Technical details": [ + "components/compose-error.tsx" + ], "Telegram": [ "app/[lang]/create/page.tsx" ], @@ -3356,7 +3391,7 @@ "app/[lang]/activity/_components/activity-view.tsx" ], "The Bitcoin fee you pay is per transaction, so the reward is too. Minting one lot and minting the full 1% cost you the same fee and earn the same {n} MINTS — there is nothing to gain by splitting a mint into smaller pieces.": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "The Bitcoin miner fee. Default tracks the next-block median.": [ "app/[lang]/swap/_components/swap-settings.tsx" @@ -3386,7 +3421,7 @@ "app/[lang]/[asset]/_components/activity-tabs.tsx" ], "The first fairminter ever created on Counterparty — block 866,297, before any other, and it minted out free to 1,376 addresses. The supply is 100,000,000, locked forever; no more can ever be issued. The live MINTS/XCP pool prices the reward: right now 1 MINTS trades at {price} XCP, so {n} MINTS is {worth} XCP.": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "The first XCP-69 launch in history hasn't happened yet. All-or-nothing mints, at least {n} participants required, every raised XCP locked into the pool forever — enforced by consensus, not by this website.": [ "app/[lang]/page.tsx" @@ -3395,7 +3430,7 @@ "app/[lang]/docs/page.tsx" ], "The graduation bounty": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "The graveyard is unavailable right now.": [ "app/[lang]/graveyard/_components/graveyard-list.tsx" @@ -3421,12 +3456,18 @@ "The sale line sits at 69M of the 100M supply; the other 31M is reserved for the pool and only ever exists if the line is crossed — 69 + 31 = 100, locked, nowhere else for supply to be. Below the line, every escrowed satoshi refunds at the deadline: there is no partial launch to be bag-held. The {perAddress} XCP per-address cap means crossing takes at least {n} distinct addresses — more if people mint below the cap. A floor on the crowd, not a count.": [ "app/[lang]/faq/_components/explainer.tsx" ], + "The service is busy or unavailable. Try again shortly.": [ + "components/compose-error.tsx" + ], "The standard removes the rug pull and the premine. It does not remove risk, and we won't pretend otherwise:": [ "app/[lang]/docs/page.tsx" ], "The timing clauses are the two deliberate inequalities. Consensus does not require a future start — a launch confirming late simply opens instantly — so the pre-announcement guarantee lives here: {startBlock} must exceed the confirmation block. Without that clause, a creator could broadcast a nominal 1,000-block sale late, confirm just before its own deadline, and run a near-instant insider mint behind thousand-block metadata. And on the fairminter row the window check relaxes to {lte} once closed because core rewrites the deadline on early sell-out — for closed launches this site restores exact equality from the immutable {event} event (see the trap in Reading state). Everything else is exact equality on the row.": [ "app/[lang]/docs/page.tsx" ], + "The transaction did not match the verified request. It was blocked.": [ + "components/compose-error.tsx" + ], "The whole standard, hands-on — starting, like every launch, with the mint.": [ "app/[lang]/faq/page.tsx" ], @@ -3455,7 +3496,7 @@ "app/[lang]/profile/_components/minting-tab.tsx" ], "today": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "token": [ "app/[lang]/create/page.tsx" @@ -3513,13 +3554,13 @@ "app/[lang]/faq/_components/explainer.tsx" ], "Trade volume": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "Trade.": [ "app/[lang]/docs/page.tsx" ], "traded, all time": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "Trader": [ "app/[lang]/activity/_components/activity-view.tsx" @@ -3550,7 +3591,7 @@ "app/[lang]/_components/market-pulse.tsx" ], "Two bounties are claimed — one is still open for the next launch to graduate.": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": [ "app/[lang]/faq/_components/explainer.tsx" @@ -3617,6 +3658,12 @@ "Upload token image": [ "app/[lang]/create/page.tsx" ], + "Use digits and a dot, with up to {n} decimal places.": [ + "components/amount-input.tsx" + ], + "Use whole numbers only.": [ + "components/amount-input.tsx" + ], "Value": [ "app/[lang]/profile/_components/positions-tab.tsx" ], @@ -3675,7 +3722,7 @@ "app/[lang]/faq/page.tsx" ], "What is MINTS?": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "What is this?": [ "app/[lang]/create/page.tsx" @@ -3694,7 +3741,7 @@ "app/[lang]/profile/_components/activity-tab.tsx" ], "When do I get paid?": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "Where do I get the XCP to mint with?": [ "app/[lang]/faq/page.tsx" @@ -3709,7 +3756,7 @@ "app/[lang]/[asset]/_components/launch-view.tsx" ], "Why per transaction, not per token?": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "window closes {n}": [ "app/[lang]/[asset]/_components/scheduled-pulse.tsx" @@ -3728,7 +3775,7 @@ "app/[lang]/swap/_components/liquidity-widget.tsx" ], "Worth": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "X profile": [ "app/[lang]/[asset]/_components/edit-panel.tsx", @@ -3747,7 +3794,7 @@ "app/[lang]/_components/market-pulse.tsx" ], "XCP in pools": [ - "app/[lang]/stats/page.tsx" + "app/[lang]/stats/_components/stats-content.tsx" ], "XCP in the pool": [ "app/[lang]/faq/_components/explainer.tsx" @@ -3857,7 +3904,7 @@ "app/[lang]/[asset]/_components/activity-tabs.tsx" ], "Your fee": [ - "app/[lang]/rewards/page.tsx" + "app/[lang]/rewards/_components/rewards-content.tsx" ], "Your lifetime earnings": [ "app/[lang]/profile/_components/rewards-card.tsx" @@ -3874,6 +3921,9 @@ "Your profile": [ "components/header-wallet.tsx" ], + "Your wallet could not complete this request. Check the details.": [ + "components/compose-error.tsx" + ], "Your wallet supplied a connection signature that didn't verify, so editing is locked for this session. Disconnect and reconnect to try again.": [ "app/[lang]/profile/_components/profile-view.tsx" ], diff --git a/apps/web/src/locales/es.json b/apps/web/src/locales/es.json index a466c892..c6e81b3a 100644 --- a/apps/web/src/locales/es.json +++ b/apps/web/src/locales/es.json @@ -362,6 +362,7 @@ "Chain:": "Cadena:", "Change in {currency} over the last {window}, since the pool opened": "Cambio en {currency} en {window}, desde que abrió el pool", "Change in {currency} over the last 24 hours": "Cambio en {currency} en las últimas 24 horas", + "Check the amount, units, and decimal places.": "Revisa la cantidad, las unidades y los decimales.", "Checking availability…": "Comprobando disponibilidad…", "Checking balance…": "Verificando saldo…", "checking…": "comprobando…", @@ -414,12 +415,14 @@ "Copied ✓": "Copiado ✓", "Copy address": "Copiar dirección", "Copy as Markdown": "Copiar como Markdown", + "Copy diagnostics": "Copiar diagnóstico", "Copy failed": "No se pudo copiar", "Copy issuer address": "Copiar dirección del emisor", "Copy link": "Copiar enlace", "Copy the link, or post it straight to X.": "Copia el enlace o publícalo directo en X.", "Core has no on-chain standard marker, so conformance is a predicate: exact equality against the standard's fixed raw values. This is the actual function this site runs — a launch either passes it or is not XCP-69.": "Core no tiene ningún marcador del estándar en la cadena, así que la conformidad es un predicado: igualdad exacta contra los valores brutos fijos del estándar. Esta es la función real que corre este sitio — un lanzamiento la pasa o no es XCP-69.", "Corrections": "Correcciones", + "Could not refresh the quote. Try again.": "No se pudo actualizar la cotización. Inténtalo de nuevo.", "Couldn't check the {give}/{get} pool. Quotes may still use the order book.": "No se pudo consultar el pool {give}/{get}. Las cotizaciones aún pueden usar el libro de órdenes.", "Couldn't confirm this signature. Updating your wallet and reconnecting usually sorts it.": "No se pudo verificar esta firma. Actualizar la billetera y volver a conectar suele resolverlo.", "Couldn't load page {n}. Showing the last page that loaded.": "No se pudo cargar la página {n}. Se muestra la última página cargada.", @@ -481,7 +484,9 @@ "Earned so far": "Ganado hasta ahora", "Edit token info": "Editar info del token", "Editing is locked for this session": "La edición está bloqueada en esta sesión", + "Enter a multiple of {n}.": "Ingresa un múltiplo de {n}.", "Enter a price": "Ingresa un precio", + "Enter a value from {min} to {max}.": "Ingresa un valor entre {min} y {max}.", "Enter an amount": "Ingresa una cantidad", "Enter opens the first result · Esc closes": "Enter abre el primer resultado · Esc cierra", "escrowed": "en custodia", @@ -533,6 +538,7 @@ "Floor": "Piso", "Floor price": "Precio piso", "Floor: {n}": "Piso: {n}", + "Follow language": "Seguir el idioma", "Full 1M bags sold before yours": "Bolsas completas de 1M vendidas antes que la tuya", "Full specification with raw compose values and the conformance predicate: {spec}. Conformance is checked field-by-field on-chain — this site lists only launches that match exactly.": "Especificación completa con los valores brutos de compose y el predicado de conformidad: {spec}. La conformidad se verifica campo por campo en la cadena — este sitio solo lista lanzamientos que coinciden exactamente.", "full XCP refund, automatic": "reembolso completo en XCP, automático", @@ -803,6 +809,7 @@ "Nothing resting on the book right now.": "No hay nada en espera en el libro ahora mismo.", "Nothing to show.": "Nada que mostrar.", "now": "ahora", + "Number format": "Formato numérico", "Observed address behavior, not identity or intent. Not investment advice.": "Comportamiento observado de direcciones, no identidad ni intención. No es asesoría de inversión.", "of {n} in the batch": "de {n} en el lote", "of {n} minters, {pct}%": "de {n} acuñadores, {pct}%", @@ -911,6 +918,7 @@ "Public sale": "Venta pública", "public sale · 69M": "venta pública · 69M", "Queue": "Fila", + "Quote changed or could not be refreshed. Review the amounts and try again.": "La cotización cambió o no se pudo actualizar. Revisa las cantidades e inténtalo de nuevo.", "Quote moved — the numbers above are updated. Press again to swap at the new price.": "La cotización cambió — los números de arriba ya están actualizados. Presiona de nuevo para intercambiar al nuevo precio.", "Quote refresh countdown": "Cuenta regresiva para actualizar la cotización", "Quote returns and prices in": "Mostrar rendimientos y precios en", @@ -928,6 +936,7 @@ "Realized PnL": "PnL realizado", "Recently confirmed": "Confirmadas recientemente", "Recommended": "Recomendada", + "Reconnect your wallet and try again.": "Vuelve a conectar tu billetera e inténtalo de nuevo.", "redeployed": "reinvertidos", "Reference launch": "Lanzamiento de referencia", "Refresh": "Actualizar", @@ -943,6 +952,7 @@ "Repeat dumpers": "Dumpers reincidentes", "Replace image": "Reemplazar imagen", "Represented": "Representadas", + "Request declined in wallet.": "Solicitud rechazada en la billetera.", "Resolve.": "Resolver.", "Retry": "Reintentar", "Reward": "Recompensa", @@ -1041,6 +1051,7 @@ "Takes effect when it confirms. {tx}": "Surte efecto al confirmarse. {tx}", "Target": "Meta", "Target start block": "Bloque de inicio objetivo", + "Technical details": "Detalles técnicos", "Telegram": "Telegram", "Ten percent above the cheapest open dispenser": "Diez por ciento por encima del dispensador abierto más barato", "The {mult}× opening premium is structural, not a price guarantee.": "La prima de apertura de {mult}× es estructural, no una garantía de precio.", @@ -1067,8 +1078,10 @@ "The per-address cap is sybil-resistant in cost only, not in principle.": "El tope por dirección es resistente a Sybil solo en costo, no en principio.", "The pool opens with 690 XCP against 31M tokens: 69/31 ≈ {mult}× the mint price. Every minter is structurally in profit at open, and the pool — not later buyers — absorbs early exits.": "El pool abre con 690 XCP contra 31M de tokens: 69/31 ≈ {mult}× el precio de acuñación. Cada acuñador está estructuralmente en ganancia al abrir, y el pool — no los compradores posteriores — absorbe las salidas tempranas.", "The sale line sits at 69M of the 100M supply; the other 31M is reserved for the pool and only ever exists if the line is crossed — 69 + 31 = 100, locked, nowhere else for supply to be. Below the line, every escrowed satoshi refunds at the deadline: there is no partial launch to be bag-held. The {perAddress} XCP per-address cap means crossing takes at least {n} distinct addresses — more if people mint below the cap. A floor on the crowd, not a count.": "La línea de venta está en 69M de los 100M de suministro; los otros 31M están reservados para el pool y solo existen si se cruza la línea — 69 + 31 = 100, bloqueado, no hay otro lugar donde pueda estar el suministro. Por debajo de la línea, cada satoshi en custodia se reembolsa al vencer el plazo: no hay lanzamiento parcial con el que quedarse colgado. El tope de {perAddress} XCP por dirección hace que cruzarla requiera al menos {n} direcciones distintas — más si la gente acuña por debajo del tope. Un piso para la multitud, no un conteo.", + "The service is busy or unavailable. Try again shortly.": "El servicio está ocupado o no está disponible. Inténtalo de nuevo en un momento.", "The standard removes the rug pull and the premine. It does not remove risk, and we won't pretend otherwise:": "El estándar elimina el rug pull y el preminado. No elimina el riesgo, y no vamos a fingir lo contrario:", "The timing clauses are the two deliberate inequalities. Consensus does not require a future start — a launch confirming late simply opens instantly — so the pre-announcement guarantee lives here: {startBlock} must exceed the confirmation block. Without that clause, a creator could broadcast a nominal 1,000-block sale late, confirm just before its own deadline, and run a near-instant insider mint behind thousand-block metadata. And on the fairminter row the window check relaxes to {lte} once closed because core rewrites the deadline on early sell-out — for closed launches this site restores exact equality from the immutable {event} event (see the trap in Reading state). Everything else is exact equality on the row.": "Las cláusulas de tiempo son las dos desigualdades deliberadas. El consenso no exige un inicio futuro — un lanzamiento que se confirma tarde simplemente abre al instante — así que la garantía del anuncio previo vive aquí: {startBlock} debe ser mayor que el bloque de confirmación. Sin esa cláusula, un creador podría transmitir tarde una venta nominal de 1,000 bloques, confirmarla justo antes de su propio plazo y ejecutar una acuñación interna casi instantánea escondida tras metadatos de mil bloques. Y en la fila del fairminter la verificación de la ventana se relaja a {lte} una vez cerrado, porque core reescribe el plazo cuando se agota antes de tiempo — para los lanzamientos cerrados este sitio restaura la igualdad exacta a partir del evento inmutable {event} (ver la trampa en Leer el estado). Todo lo demás es igualdad exacta sobre la fila.", + "The transaction did not match the verified request. It was blocked.": "La transacción no coincidía con la solicitud verificada y se bloqueó.", "The whole standard, hands-on — starting, like every launch, with the mint.": "Todo el estándar, con las manos en la masa — empezando, como cada lanzamiento, por la acuñación.", "The XCP-69 standard": "El estándar XCP-69", "There is no fee-split table because there are no fees to split. Every satoshi of XCP paid by minters goes into the pool. The only costs anywhere in the system are Bitcoin transaction fees, the 0.5 XCP asset-name registration fee for named assets, and the protocol's pooldeposit gas fee debited at creation — costs paid to the network, not to us or to the creator.": "No hay tabla de reparto de comisiones porque no hay comisiones que repartir. Cada satoshi de XCP que pagan los acuñadores va al pool. Los únicos costos en todo el sistema son las comisiones de transacción de Bitcoin, la comisión de registro de nombre de 0.5 XCP para activos con nombre y la comisión de gas del pooldeposit del protocolo, debitada al crear — costos que se pagan a la red, no a nosotros ni al creador.", @@ -1128,6 +1141,8 @@ "Upcoming": "Próximo", "Upload failed": "No se pudo subir", "Upload token image": "Subir la imagen del token", + "Use digits and a dot, with up to {n} decimal places.": "Usa dígitos y un punto, con un máximo de {n} decimales.", + "Use whole numbers only.": "Usa solo números enteros.", "Value": "Valor", "Value and underlying amounts are what withdrawing the LP tokens now would return. XCP value is available when XCP is one side of the pair. LP profit and loss is not estimated.": "El valor y los montos subyacentes son lo que devolvería retirar los tokens LP ahora. El valor en XCP está disponible cuando XCP es uno de los lados del par. No se estima la ganancia o pérdida del LP.", "Vends": "Vende", @@ -1212,6 +1227,7 @@ "Your position: {position} · {pct} of the pool": "Tu posición: {position} · {pct} del pool", "Your positions, history, and launches. Every graduated XCP-69 launch trades against XCP, so a position's value is a pool price away from a dollar figure.": "Tus posiciones, historial y lanzamientos. Todo lanzamiento XCP-69 graduado se opera contra XCP, así que el valor de una posición está a un precio de pool de una cifra en dólares.", "Your profile": "Tu perfil", + "Your wallet could not complete this request. Check the details.": "Tu billetera no pudo completar esta solicitud. Revisa los detalles.", "Your wallet supplied a connection signature that didn't verify, so editing is locked for this session. Disconnect and reconnect to try again.": "Tu billetera entregó una firma de conexión que no se pudo verificar, así que la edición queda bloqueada en esta sesión. Desconéctate y vuelve a conectarte para intentarlo de nuevo.", "Your wallet supplied a connection signature that didn't verify, so we can't confirm this address is really yours. Disconnect and reconnect to try again — nothing else on the site is affected.": "Tu billetera entregó una firma de conexión que no se pudo verificar, así que no podemos confirmar que esta dirección sea realmente tuya. Desconéctala y vuelve a conectarla para intentarlo de nuevo — nada más en el sitio se ve afectado.", "Your wallet will ask once per route, in order — each popup is one route, nothing more.": "Tu billetera te pedirá confirmar una vez por ruta, en orden — cada ventana es una ruta, nada más.", diff --git a/apps/web/src/locales/es.status.json b/apps/web/src/locales/es.status.json index 4c75b7b4..44f3b5b5 100644 --- a/apps/web/src/locales/es.status.json +++ b/apps/web/src/locales/es.status.json @@ -1205,6 +1205,22 @@ "Launch #{n}", "Positions, open mints, history, and launches for {address} on xcp.fun.", "Show amounts in {currency}", - "open@@bounty" + "open@@bounty", + "Check the amount, units, and decimal places.", + "Copy diagnostics", + "Could not refresh the quote. Try again.", + "Enter a multiple of {n}.", + "Enter a value from {min} to {max}.", + "Follow language", + "Number format", + "Quote changed or could not be refreshed. Review the amounts and try again.", + "Reconnect your wallet and try again.", + "Request declined in wallet.", + "Technical details", + "The service is busy or unavailable. Try again shortly.", + "The transaction did not match the verified request. It was blocked.", + "Use digits and a dot, with up to {n} decimal places.", + "Use whole numbers only.", + "Your wallet could not complete this request. Check the details." ] } diff --git a/apps/web/src/locales/fr.json b/apps/web/src/locales/fr.json index cb35de36..71f1b550 100644 --- a/apps/web/src/locales/fr.json +++ b/apps/web/src/locales/fr.json @@ -362,6 +362,7 @@ "Chain:": "Chaîne :", "Change in {currency} over the last {window}, since the pool opened": "Variation en {currency} sur {window}, depuis l'ouverture du pool", "Change in {currency} over the last 24 hours": "Variation en {currency} sur les dernières 24 heures", + "Check the amount, units, and decimal places.": "Vérifiez le montant, les unités et les décimales.", "Checking availability…": "Vérification de la disponibilité…", "Checking balance…": "Vérification du solde…", "checking…": "vérification…", @@ -414,12 +415,14 @@ "Copied ✓": "Copié ✓", "Copy address": "Copier l'adresse", "Copy as Markdown": "Copier en Markdown", + "Copy diagnostics": "Copier le diagnostic", "Copy failed": "Échec de la copie", "Copy issuer address": "Copier l'adresse de l'émetteur", "Copy link": "Copier le lien", "Copy the link, or post it straight to X.": "Copiez le lien, ou publiez-le directement sur X.", "Core has no on-chain standard marker, so conformance is a predicate: exact equality against the standard's fixed raw values. This is the actual function this site runs — a launch either passes it or is not XCP-69.": "Core n'a aucun marqueur du standard on-chain, la conformité est donc un prédicat : l'égalité exacte avec les valeurs brutes fixes du standard. C'est la fonction réelle que ce site exécute — un lancement la passe, ou n'est pas XCP-69.", "Corrections": "Corrections", + "Could not refresh the quote. Try again.": "Impossible d’actualiser la cotation. Réessayez.", "Couldn't check the {give}/{get} pool. Quotes may still use the order book.": "Impossible de vérifier le pool {give}/{get}. Les cotations peuvent encore passer par le carnet d'ordres.", "Couldn't confirm this signature. Updating your wallet and reconnecting usually sorts it.": "Impossible de vérifier cette signature. Mettre à jour votre portefeuille puis vous reconnecter règle généralement le problème.", "Couldn't load page {n}. Showing the last page that loaded.": "Impossible de charger la page {n}. Affichage de la dernière page chargée.", @@ -481,7 +484,9 @@ "Earned so far": "Gagné jusqu'ici", "Edit token info": "Modifier les infos du token", "Editing is locked for this session": "La modification est verrouillée pour cette session", + "Enter a multiple of {n}.": "Saisissez un multiple de {n}.", "Enter a price": "Saisissez un prix", + "Enter a value from {min} to {max}.": "Saisissez une valeur comprise entre {min} et {max}.", "Enter an amount": "Saisissez un montant", "Enter opens the first result · Esc closes": "Entrée ouvre le premier résultat · Échap ferme", "escrowed": "sous séquestre", @@ -533,6 +538,7 @@ "Floor": "Plancher", "Floor price": "Prix plancher", "Floor: {n}": "Plancher : {n}", + "Follow language": "Suivre la langue", "Full 1M bags sold before yours": "Sacs complets de 1M vendus avant le vôtre", "Full specification with raw compose values and the conformance predicate: {spec}. Conformance is checked field-by-field on-chain — this site lists only launches that match exactly.": "Spécification complète avec les valeurs brutes de compose et le prédicat de conformité : {spec}. La conformité est vérifiée champ par champ on-chain — ce site ne liste que les lancements qui correspondent exactement.", "full XCP refund, automatic": "remboursement intégral en XCP, automatique", @@ -803,6 +809,7 @@ "Nothing resting on the book right now.": "Rien en attente dans le carnet pour le moment.", "Nothing to show.": "Rien à afficher.", "now": "maintenant", + "Number format": "Format des nombres", "Observed address behavior, not identity or intent. Not investment advice.": "Comportement observé des adresses, pas leur identité ni leur intention. Ceci n'est pas un conseil en investissement.", "of {n} in the batch": "sur {n} dans le lot", "of {n} minters, {pct}%": "sur {n} minteurs, {pct} %", @@ -911,6 +918,7 @@ "Public sale": "Vente publique", "public sale · 69M": "vente publique · 69M", "Queue": "File d'attente", + "Quote changed or could not be refreshed. Review the amounts and try again.": "La cotation a changé ou n’a pas pu être actualisée. Vérifiez les montants et réessayez.", "Quote moved — the numbers above are updated. Press again to swap at the new price.": "La cotation a bougé — les chiffres ci-dessus sont à jour. Appuyez à nouveau pour échanger au nouveau prix.", "Quote refresh countdown": "Compte à rebours avant actualisation de la cotation", "Quote returns and prices in": "Afficher les performances et les prix en", @@ -928,6 +936,7 @@ "Realized PnL": "PnL réalisé", "Recently confirmed": "Confirmées récemment", "Recommended": "Recommandé", + "Reconnect your wallet and try again.": "Reconnectez votre portefeuille et réessayez.", "redeployed": "réinvestis", "Reference launch": "Lancement de référence", "Refresh": "Actualiser", @@ -943,6 +952,7 @@ "Repeat dumpers": "Dumpers récidivistes", "Replace image": "Remplacer l'image", "Represented": "Représentées", + "Request declined in wallet.": "Demande refusée dans le portefeuille.", "Resolve.": "Résoudre.", "Retry": "Réessayer", "Reward": "Récompense", @@ -1041,6 +1051,7 @@ "Takes effect when it confirms. {tx}": "Prend effet à la confirmation. {tx}", "Target": "Objectif", "Target start block": "Bloc de départ visé", + "Technical details": "Détails techniques", "Telegram": "Telegram", "Ten percent above the cheapest open dispenser": "Dix pour cent au-dessus du distributeur ouvert le moins cher", "The {mult}× opening premium is structural, not a price guarantee.": "La prime d'ouverture de {mult}× est structurelle, pas une garantie de prix.", @@ -1067,8 +1078,10 @@ "The per-address cap is sybil-resistant in cost only, not in principle.": "Le plafond par adresse résiste aux Sybil par le coût seulement, pas par principe.", "The pool opens with 690 XCP against 31M tokens: 69/31 ≈ {mult}× the mint price. Every minter is structurally in profit at open, and the pool — not later buyers — absorbs early exits.": "Le pool ouvre avec 690 XCP contre 31M de tokens : 69/31 ≈ {mult}× le prix de mint. Chaque minteur est structurellement en profit à l'ouverture, et c'est le pool — pas les acheteurs suivants — qui absorbe les sorties précoces.", "The sale line sits at 69M of the 100M supply; the other 31M is reserved for the pool and only ever exists if the line is crossed — 69 + 31 = 100, locked, nowhere else for supply to be. Below the line, every escrowed satoshi refunds at the deadline: there is no partial launch to be bag-held. The {perAddress} XCP per-address cap means crossing takes at least {n} distinct addresses — more if people mint below the cap. A floor on the crowd, not a count.": "La ligne de vente se situe à 69M sur les 100M d'offre ; les 31M restants sont réservés au pool et n'existent que si la ligne est franchie — 69 + 31 = 100, verrouillés, l'offre ne peut être nulle part ailleurs. Sous la ligne, chaque satoshi sous séquestre est remboursé à l'échéance : il n'y a pas de lancement partiel avec lequel rester coincé. Le plafond de {perAddress} XCP par adresse fait que franchir la ligne demande au moins {n} adresses distinctes — davantage si les gens mintent sous le plafond. Un plancher pour la foule, pas un décompte.", + "The service is busy or unavailable. Try again shortly.": "Le service est occupé ou indisponible. Réessayez dans un instant.", "The standard removes the rug pull and the premine. It does not remove risk, and we won't pretend otherwise:": "Le standard supprime le rug pull et la prémine. Il ne supprime pas le risque, et nous ne ferons pas semblant du contraire :", "The timing clauses are the two deliberate inequalities. Consensus does not require a future start — a launch confirming late simply opens instantly — so the pre-announcement guarantee lives here: {startBlock} must exceed the confirmation block. Without that clause, a creator could broadcast a nominal 1,000-block sale late, confirm just before its own deadline, and run a near-instant insider mint behind thousand-block metadata. And on the fairminter row the window check relaxes to {lte} once closed because core rewrites the deadline on early sell-out — for closed launches this site restores exact equality from the immutable {event} event (see the trap in Reading state). Everything else is exact equality on the row.": "Les clauses de calendrier sont les deux inégalités délibérées. Le consensus n'exige pas un début futur — un lancement confirmé en retard ouvre simplement sur-le-champ — la garantie d'annonce préalable vit donc ici : {startBlock} doit dépasser le bloc de confirmation. Sans cette clause, un créateur pourrait diffuser en retard une vente nominale de 1 000 blocs, la confirmer juste avant sa propre échéance et exécuter un mint d'initié quasi instantané derrière des métadonnées à mille blocs. Et sur la ligne du fairminter, la vérification de la fenêtre se relâche en {lte} une fois fermé, parce que core réécrit l'échéance en cas de vente anticipée — pour les lancements fermés, ce site rétablit l'égalité exacte à partir de l'événement immuable {event} (voir le piège dans Lire l'état). Tout le reste est une égalité exacte sur la ligne.", + "The transaction did not match the verified request. It was blocked.": "La transaction ne correspondait pas à la demande vérifiée et a été bloquée.", "The whole standard, hands-on — starting, like every launch, with the mint.": "Tout le standard, en pratique — en commençant, comme chaque lancement, par le mint.", "The XCP-69 standard": "Le standard XCP-69", "There is no fee-split table because there are no fees to split. Every satoshi of XCP paid by minters goes into the pool. The only costs anywhere in the system are Bitcoin transaction fees, the 0.5 XCP asset-name registration fee for named assets, and the protocol's pooldeposit gas fee debited at creation — costs paid to the network, not to us or to the creator.": "Il n'y a pas de tableau de partage des frais parce qu'il n'y a pas de frais à partager. Chaque satoshi de XCP payé par les minteurs va dans le pool. Les seuls coûts dans tout le système sont les frais de transaction Bitcoin, les frais d'enregistrement de nom d'actif de 0,5 XCP pour les actifs nommés, et les frais de gas du pooldeposit du protocole, débités à la création — des coûts payés au réseau, pas à nous ni au créateur.", @@ -1128,6 +1141,8 @@ "Upcoming": "À venir", "Upload failed": "Échec du téléversement", "Upload token image": "Téléverser l'image du token", + "Use digits and a dot, with up to {n} decimal places.": "Utilisez des chiffres et un point, avec au maximum {n} décimales.", + "Use whole numbers only.": "Utilisez uniquement des nombres entiers.", "Value": "Valeur", "Value and underlying amounts are what withdrawing the LP tokens now would return. XCP value is available when XCP is one side of the pair. LP profit and loss is not estimated.": "La valeur et les montants sous-jacents sont ce que rendrait le retrait des tokens LP maintenant. La valeur en XCP est disponible quand XCP est l'un des côtés de la paire. Le gain ou la perte sur la LP n'est pas estimé.", "Vends": "Vend", @@ -1212,6 +1227,7 @@ "Your position: {position} · {pct} of the pool": "Votre position : {position} · {pct} du pool", "Your positions, history, and launches. Every graduated XCP-69 launch trades against XCP, so a position's value is a pool price away from a dollar figure.": "Vos positions, votre historique et vos lancements. Chaque lancement XCP-69 gradué s'échange contre XCP, la valeur d'une position n'est donc qu'à un prix de pool d'un montant en dollars.", "Your profile": "Votre profil", + "Your wallet could not complete this request. Check the details.": "Votre portefeuille n’a pas pu terminer cette demande. Vérifiez les détails.", "Your wallet supplied a connection signature that didn't verify, so editing is locked for this session. Disconnect and reconnect to try again.": "Votre portefeuille a fourni une signature de connexion qui n'a pas pu être vérifiée, la modification est donc verrouillée pour cette session. Déconnectez-vous puis reconnectez-vous pour réessayer.", "Your wallet supplied a connection signature that didn't verify, so we can't confirm this address is really yours. Disconnect and reconnect to try again — nothing else on the site is affected.": "Votre portefeuille a fourni une signature de connexion qui n'a pas pu être vérifiée, nous ne pouvons donc pas confirmer que cette adresse est bien la vôtre. Déconnectez-vous et reconnectez-vous pour réessayer — rien d'autre sur le site n'est affecté.", "Your wallet will ask once per route, in order — each popup is one route, nothing more.": "Votre portefeuille vous sollicitera une fois par route, dans l'ordre — chaque fenêtre correspond à une route, rien de plus.", diff --git a/apps/web/src/locales/fr.status.json b/apps/web/src/locales/fr.status.json index 4c75b7b4..44f3b5b5 100644 --- a/apps/web/src/locales/fr.status.json +++ b/apps/web/src/locales/fr.status.json @@ -1205,6 +1205,22 @@ "Launch #{n}", "Positions, open mints, history, and launches for {address} on xcp.fun.", "Show amounts in {currency}", - "open@@bounty" + "open@@bounty", + "Check the amount, units, and decimal places.", + "Copy diagnostics", + "Could not refresh the quote. Try again.", + "Enter a multiple of {n}.", + "Enter a value from {min} to {max}.", + "Follow language", + "Number format", + "Quote changed or could not be refreshed. Review the amounts and try again.", + "Reconnect your wallet and try again.", + "Request declined in wallet.", + "Technical details", + "The service is busy or unavailable. Try again shortly.", + "The transaction did not match the verified request. It was blocked.", + "Use digits and a dot, with up to {n} decimal places.", + "Use whole numbers only.", + "Your wallet could not complete this request. Check the details." ] } diff --git a/apps/web/src/locales/ja.json b/apps/web/src/locales/ja.json index 415d8b60..89eb52c7 100644 --- a/apps/web/src/locales/ja.json +++ b/apps/web/src/locales/ja.json @@ -362,6 +362,7 @@ "Chain:": "チェーン:", "Change in {currency} over the last {window}, since the pool opened": "プール開設からの {window} 間の {currency} 建て変動", "Change in {currency} over the last 24 hours": "過去 24 時間の {currency} 建て変動", + "Check the amount, units, and decimal places.": "数量、単位、小数点以下の桁数を確認してください。", "Checking availability…": "空き状況を確認中…", "Checking balance…": "残高を確認中…", "checking…": "確認中…", @@ -414,12 +415,14 @@ "Copied ✓": "コピーしました ✓", "Copy address": "アドレスをコピー", "Copy as Markdown": "Markdown としてコピー", + "Copy diagnostics": "診断情報をコピー", "Copy failed": "コピーに失敗しました", "Copy issuer address": "発行者アドレスをコピー", "Copy link": "リンクをコピー", "Copy the link, or post it straight to X.": "リンクをコピーするか、そのまま X に投稿。", "Core has no on-chain standard marker, so conformance is a predicate: exact equality against the standard's fixed raw values. This is the actual function this site runs — a launch either passes it or is not XCP-69.": "Core にはオンチェーンの標準マーカーがないため、準拠は述語で判定します: 標準の固定された生の値との厳密な等価性です。これはこのサイトが実際に実行している関数そのもので、ローンチはこれを通るか、XCP-69 ではないかのどちらかです。", "Corrections": "訂正", + "Could not refresh the quote. Try again.": "見積もりを更新できませんでした。もう一度お試しください。", "Couldn't check the {give}/{get} pool. Quotes may still use the order book.": "{give}/{get} プールを確認できませんでした。見積もりは板を使う場合があります。", "Couldn't confirm this signature. Updating your wallet and reconnecting usually sorts it.": "署名を確認できませんでした。ウォレットを更新して再接続すると解決することが多いです。", "Couldn't load page {n}. Showing the last page that loaded.": "ページ {n} を読み込めませんでした。最後に読み込めたページを表示しています。", @@ -481,7 +484,9 @@ "Earned so far": "これまでの獲得", "Edit token info": "トークン情報を編集", "Editing is locked for this session": "このセッションでは編集がロックされています", + "Enter a multiple of {n}.": "{n}の倍数を入力してください。", "Enter a price": "価格を入力", + "Enter a value from {min} to {max}.": "{min}から{max}の値を入力してください。", "Enter an amount": "数量を入力", "Enter opens the first result · Esc closes": "Enter で先頭の結果を開く · Esc で閉じる", "escrowed": "エスクロー中", @@ -533,6 +538,7 @@ "Floor": "フロア", "Floor price": "フロア価格", "Floor: {n}": "フロア: {n}", + "Follow language": "言語に合わせる", "Full 1M bags sold before yours": "自分より先に売られたフル 1M バッグ", "Full specification with raw compose values and the conformance predicate: {spec}. Conformance is checked field-by-field on-chain — this site lists only launches that match exactly.": "生の compose 値と準拠述語を含む完全な仕様: {spec}。準拠はオンチェーンでフィールドごとに検査されます — このサイトは完全に一致するローンチだけを掲載します。", "full XCP refund, automatic": "XCP 全額返金、自動", @@ -803,6 +809,7 @@ "Nothing resting on the book right now.": "現在、板に待機中の注文はありません。", "Nothing to show.": "表示するものはありません。", "now": "現在", + "Number format": "数値の表示形式", "Observed address behavior, not identity or intent. Not investment advice.": "観測されたアドレスの行動であって、身元や意図ではありません。投資助言ではありません。", "of {n} in the batch": "バッチ内 {n} のうち", "of {n} minters, {pct}%": "{n} 人のミンター中、{pct}%", @@ -911,6 +918,7 @@ "Public sale": "公開販売", "public sale · 69M": "公開販売 · 69M", "Queue": "キュー", + "Quote changed or could not be refreshed. Review the amounts and try again.": "見積もりが変わったか、更新できませんでした。数量を確認して、もう一度お試しください。", "Quote moved — the numbers above are updated. Press again to swap at the new price.": "見積もりが変わりました — 上の数値は更新済みです。新しい価格でスワップするにはもう一度押してください。", "Quote refresh countdown": "見積もり更新までのカウントダウン", "Quote returns and prices in": "リターンと価格の表示通貨", @@ -928,6 +936,7 @@ "Realized PnL": "実現損益", "Recently confirmed": "最近承認", "Recommended": "推奨", + "Reconnect your wallet and try again.": "ウォレットを再接続して、もう一度お試しください。", "redeployed": "再投入", "Reference launch": "リファレンスローンチ", "Refresh": "更新", @@ -943,6 +952,7 @@ "Repeat dumpers": "常習ダンパー", "Replace image": "画像を差し替え", "Represented": "参加コミュニティ", + "Request declined in wallet.": "ウォレットでリクエストが拒否されました。", "Resolve.": "確定。", "Retry": "再試行", "Reward": "リワード", @@ -1041,6 +1051,7 @@ "Takes effect when it confirms. {tx}": "承認時に有効になります。{tx}", "Target": "目標額", "Target start block": "目標開始ブロック", + "Technical details": "技術情報", "Telegram": "Telegram", "Ten percent above the cheapest open dispenser": "最安の稼働中自販機より 10% 高く", "The {mult}× opening premium is structural, not a price guarantee.": "{mult}× のオープニングプレミアムは構造上の結果であり、価格の保証ではありません。", @@ -1067,8 +1078,10 @@ "The per-address cap is sybil-resistant in cost only, not in principle.": "アドレスごとの上限はコスト面でのみシビル耐性があり、原理的にはありません。", "The pool opens with 690 XCP against 31M tokens: 69/31 ≈ {mult}× the mint price. Every minter is structurally in profit at open, and the pool — not later buyers — absorbs early exits.": "プールは 690 XCP 対 31M トークンでオープンします: 69/31 ≈ ミント価格の {mult}×。すべてのミンターは構造上オープン時点で含み益があり、早期の売り抜けは後の買い手ではなくプールが吸収します。", "The sale line sits at 69M of the 100M supply; the other 31M is reserved for the pool and only ever exists if the line is crossed — 69 + 31 = 100, locked, nowhere else for supply to be. Below the line, every escrowed satoshi refunds at the deadline: there is no partial launch to be bag-held. The {perAddress} XCP per-address cap means crossing takes at least {n} distinct addresses — more if people mint below the cap. A floor on the crowd, not a count.": "販売の線は発行量 100M のうち 69M の位置にあります。残りの 31M はプール用に確保され、線を越えた場合にしか存在しません — 69 + 31 = 100 でロックされ、供給が他に行く場所はありません。線に届かなければ、エスクローされた satoshi はすべて締切時に返金されます: 抱え込まされる中途半端なローンチはありません。アドレスごとに {perAddress} XCP の上限があるため、線を越えるには少なくとも {n} の異なるアドレスが必要です — 上限未満でミントする人がいればもっと必要です。群衆の下限であって、人数ではありません。", + "The service is busy or unavailable. Try again shortly.": "サービスが混雑しているか、利用できません。しばらくしてからお試しください。", "The standard removes the rug pull and the premine. It does not remove risk, and we won't pretend otherwise:": "この標準はラグプルとプレマインを排除します。リスクは排除しませんし、そう装うつもりもありません:", "The timing clauses are the two deliberate inequalities. Consensus does not require a future start — a launch confirming late simply opens instantly — so the pre-announcement guarantee lives here: {startBlock} must exceed the confirmation block. Without that clause, a creator could broadcast a nominal 1,000-block sale late, confirm just before its own deadline, and run a near-instant insider mint behind thousand-block metadata. And on the fairminter row the window check relaxes to {lte} once closed because core rewrites the deadline on early sell-out — for closed launches this site restores exact equality from the immutable {event} event (see the trap in Reading state). Everything else is exact equality on the row.": "タイミングの条項は意図的な 2 つの不等式です。コンセンサスは未来の開始を要求しません — 遅れて承認されたローンチは単に即座に開くだけです — ので、事前告知の保証はここにあります: {startBlock} は承認ブロックより大きくなければなりません。この条項がなければ、作成者は名目上 1,000 ブロックの販売を遅くブロードキャストし、自らの締切直前に承認させ、千ブロックのメタデータの裏でほぼ即時のインサイダーミントを行えます。また、フェアミンター行の期間チェックは終了後には {lte} に緩みます。core が早期完売時に締切を書き換えるためで、終了済みローンチについてこのサイトは不変の {event} イベントから厳密な等価性を復元します(「状態の読み取り」の落とし穴を参照)。それ以外はすべて行に対する厳密な等価性です。", + "The transaction did not match the verified request. It was blocked.": "トランザクションが検証済みのリクエストと一致しなかったため、処理を停止しました。", "The whole standard, hands-on — starting, like every launch, with the mint.": "標準の全体を、手を動かしながら — すべてのローンチと同じく、ミントから始めます。", "The XCP-69 standard": "XCP-69 標準", "There is no fee-split table because there are no fees to split. Every satoshi of XCP paid by minters goes into the pool. The only costs anywhere in the system are Bitcoin transaction fees, the 0.5 XCP asset-name registration fee for named assets, and the protocol's pooldeposit gas fee debited at creation — costs paid to the network, not to us or to the creator.": "手数料の分配表はありません。分配する手数料がないからです。ミンターが支払った XCP は 1 satoshi 残らずプールに入ります。システム内で発生する費用は、Bitcoin のトランザクション手数料、名前付きアセットの 0.5 XCP 名前登録手数料、そして作成時に引き落とされるプロトコルの pooldeposit ガス手数料だけです — いずれもネットワークに支払われ、私たちや作成者には支払われません。", @@ -1128,6 +1141,8 @@ "Upcoming": "近日", "Upload failed": "アップロードに失敗しました", "Upload token image": "トークン画像をアップロード", + "Use digits and a dot, with up to {n} decimal places.": "半角の数字とドットを使い、小数点以下は{n}桁以内で入力してください。", + "Use whole numbers only.": "整数のみを入力してください。", "Value": "価値", "Value and underlying amounts are what withdrawing the LP tokens now would return. XCP value is available when XCP is one side of the pair. LP profit and loss is not estimated.": "価値と内訳の数量は、今 LP トークンを引き出した場合に戻る量です。XCP 建ての価値はペアの一方が XCP の場合に表示されます。LP の損益は推定していません。", "Vends": "販売中", @@ -1212,6 +1227,7 @@ "Your position: {position} · {pct} of the pool": "あなたのポジション: {position} · プールの {pct}", "Your positions, history, and launches. Every graduated XCP-69 launch trades against XCP, so a position's value is a pool price away from a dollar figure.": "あなたのポジション、履歴、ローンチ。卒業した XCP-69 ローンチはすべて XCP 建てで取引されるため、ポジションの価値はプール価格ひとつでドル換算できます。", "Your profile": "マイプロフィール", + "Your wallet could not complete this request. Check the details.": "ウォレットでこのリクエストを完了できませんでした。詳細を確認してください。", "Your wallet supplied a connection signature that didn't verify, so editing is locked for this session. Disconnect and reconnect to try again.": "ウォレットが提示した接続署名を検証できなかったため、このセッションでは編集がロックされています。切断して再接続してみてください。", "Your wallet supplied a connection signature that didn't verify, so we can't confirm this address is really yours. Disconnect and reconnect to try again — nothing else on the site is affected.": "ウォレットが提示した接続署名を検証できなかったため、このアドレスが本当にあなたのものか確認できません。切断して再接続してみてください — サイトの他の機能には影響ありません。", "Your wallet will ask once per route, in order — each popup is one route, nothing more.": "ウォレットはルートごとに順番に 1 回ずつ確認を求めます — ポップアップ 1 つが 1 ルートで、それ以上はありません。", diff --git a/apps/web/src/locales/ja.status.json b/apps/web/src/locales/ja.status.json index 0a10fa00..99a3a395 100644 --- a/apps/web/src/locales/ja.status.json +++ b/apps/web/src/locales/ja.status.json @@ -1204,6 +1204,22 @@ "Launch #{n}", "Positions, open mints, history, and launches for {address} on xcp.fun.", "Show amounts in {currency}", - "open@@bounty" + "open@@bounty", + "Check the amount, units, and decimal places.", + "Copy diagnostics", + "Could not refresh the quote. Try again.", + "Enter a multiple of {n}.", + "Enter a value from {min} to {max}.", + "Follow language", + "Number format", + "Quote changed or could not be refreshed. Review the amounts and try again.", + "Reconnect your wallet and try again.", + "Request declined in wallet.", + "Technical details", + "The service is busy or unavailable. Try again shortly.", + "The transaction did not match the verified request. It was blocked.", + "Use digits and a dot, with up to {n} decimal places.", + "Use whole numbers only.", + "Your wallet could not complete this request. Check the details." ] } diff --git a/apps/web/src/locales/ko.json b/apps/web/src/locales/ko.json index 965836a9..8902be22 100644 --- a/apps/web/src/locales/ko.json +++ b/apps/web/src/locales/ko.json @@ -362,6 +362,7 @@ "Chain:": "체인:", "Change in {currency} over the last {window}, since the pool opened": "풀 개장 이후 최근 {window} 동안의 {currency} 변동", "Change in {currency} over the last 24 hours": "최근 24시간 동안의 {currency} 변동", + "Check the amount, units, and decimal places.": "수량, 단위, 소수 자릿수를 확인하세요.", "Checking availability…": "사용 가능 여부 확인 중…", "Checking balance…": "잔액 확인 중…", "checking…": "확인 중…", @@ -414,12 +415,14 @@ "Copied ✓": "복사됨 ✓", "Copy address": "주소 복사", "Copy as Markdown": "Markdown으로 복사", + "Copy diagnostics": "진단 정보 복사", "Copy failed": "복사 실패", "Copy issuer address": "발행자 주소 복사", "Copy link": "링크 복사", "Copy the link, or post it straight to X.": "링크를 복사하거나 X에 바로 올리세요.", "Core has no on-chain standard marker, so conformance is a predicate: exact equality against the standard's fixed raw values. This is the actual function this site runs — a launch either passes it or is not XCP-69.": "Core에는 온체인 표준 표식이 없으므로 표준 준수는 하나의 술어입니다: 표준의 고정된 원시 값과의 정확한 일치. 이것이 이 사이트가 실제로 실행하는 함수이며, 런칭은 이를 통과하거나 XCP-69가 아니거나 둘 중 하나입니다.", "Corrections": "정정", + "Could not refresh the quote. Try again.": "견적을 새로고침하지 못했습니다. 다시 시도하세요.", "Couldn't check the {give}/{get} pool. Quotes may still use the order book.": "{give}/{get} 풀을 확인하지 못했습니다. 견적은 호가창을 사용할 수 있습니다.", "Couldn't confirm this signature. Updating your wallet and reconnecting usually sorts it.": "이 서명을 확인하지 못했습니다. 지갑을 업데이트하고 다시 연결하면 대부분 해결됩니다.", "Couldn't load page {n}. Showing the last page that loaded.": "{n}페이지를 불러오지 못했습니다. 마지막으로 불러온 페이지를 표시합니다.", @@ -481,7 +484,9 @@ "Earned so far": "현재까지 획득", "Edit token info": "토큰 정보 수정", "Editing is locked for this session": "이 세션에서는 수정이 잠겨 있습니다", + "Enter a multiple of {n}.": "{n}의 배수를 입력하세요.", "Enter a price": "가격을 입력하세요", + "Enter a value from {min} to {max}.": "{min}에서 {max} 사이의 값을 입력하세요.", "Enter an amount": "수량을 입력하세요", "Enter opens the first result · Esc closes": "Enter로 첫 결과 열기 · Esc로 닫기", "escrowed": "에스크로 중", @@ -533,6 +538,7 @@ "Floor": "최저가", "Floor price": "최저가", "Floor: {n}": "최저가: {n}", + "Follow language": "언어 설정 따르기", "Full 1M bags sold before yours": "내 앞에 매도된 1M 최대 물량 수", "Full specification with raw compose values and the conformance predicate: {spec}. Conformance is checked field-by-field on-chain — this site lists only launches that match exactly.": "원시 compose 값과 표준 준수 술어를 담은 전체 명세: {spec}. 표준 준수는 온체인에서 필드 단위로 검사됩니다 — 이 사이트는 정확히 일치하는 런칭만 표시합니다.", "full XCP refund, automatic": "XCP 전액 자동 환불", @@ -803,6 +809,7 @@ "Nothing resting on the book right now.": "현재 호가창에 대기 주문이 없습니다.", "Nothing to show.": "표시할 내용이 없습니다.", "now": "현재", + "Number format": "숫자 형식", "Observed address behavior, not identity or intent. Not investment advice.": "관측된 주소의 행동이며 신원이나 의도가 아닙니다. 투자 조언이 아닙니다.", "of {n} in the batch": "배치 {n} 중", "of {n} minters, {pct}%": "민터 {n}명 중, {pct}%", @@ -911,6 +918,7 @@ "Public sale": "공개 판매", "public sale · 69M": "공개 판매 · 6,900만", "Queue": "대기열", + "Quote changed or could not be refreshed. Review the amounts and try again.": "견적이 변경되었거나 새로고침하지 못했습니다. 수량을 확인한 후 다시 시도하세요.", "Quote moved — the numbers above are updated. Press again to swap at the new price.": "견적이 바뀌었습니다 — 위의 숫자는 업데이트되었습니다. 새 가격으로 스왑하려면 다시 누르세요.", "Quote refresh countdown": "견적 갱신 카운트다운", "Quote returns and prices in": "수익률과 가격 표시 통화", @@ -928,6 +936,7 @@ "Realized PnL": "실현 손익", "Recently confirmed": "최근 확인됨", "Recommended": "권장", + "Reconnect your wallet and try again.": "지갑을 다시 연결한 후 시도하세요.", "redeployed": "재투입", "Reference launch": "참조 런칭", "Refresh": "새로 고침", @@ -943,6 +952,7 @@ "Repeat dumpers": "반복 덤퍼", "Replace image": "이미지 교체", "Represented": "참여 커뮤니티", + "Request declined in wallet.": "지갑에서 요청이 거절되었습니다.", "Resolve.": "확정.", "Retry": "다시 시도", "Reward": "보상", @@ -1041,6 +1051,7 @@ "Takes effect when it confirms. {tx}": "확인되면 적용됩니다. {tx}", "Target": "목표", "Target start block": "목표 시작 블록", + "Technical details": "기술 세부 정보", "Telegram": "Telegram", "Ten percent above the cheapest open dispenser": "가장 싼 운영 중 자판기보다 10% 높게", "The {mult}× opening premium is structural, not a price guarantee.": "{mult}배 오프닝 프리미엄은 구조적인 것이지 가격 보장이 아닙니다.", @@ -1067,8 +1078,10 @@ "The per-address cap is sybil-resistant in cost only, not in principle.": "주소당 상한은 원리가 아니라 비용 면에서만 시빌 저항적입니다.", "The pool opens with 690 XCP against 31M tokens: 69/31 ≈ {mult}× the mint price. Every minter is structurally in profit at open, and the pool — not later buyers — absorbs early exits.": "풀은 3,100만 토큰에 690 XCP로 열립니다: 69/31 ≈ 민팅가의 {mult}배. 모든 민터는 구조적으로 오픈 시점에 수익 상태이며, 이른 매도는 나중 매수자가 아니라 풀이 흡수합니다.", "The sale line sits at 69M of the 100M supply; the other 31M is reserved for the pool and only ever exists if the line is crossed — 69 + 31 = 100, locked, nowhere else for supply to be. Below the line, every escrowed satoshi refunds at the deadline: there is no partial launch to be bag-held. The {perAddress} XCP per-address cap means crossing takes at least {n} distinct addresses — more if people mint below the cap. A floor on the crowd, not a count.": "판매선은 공급량 1억 중 6,900만에 있습니다. 나머지 3,100만은 풀에 예약되어 있고 선을 넘었을 때만 존재합니다 — 69 + 31 = 100, 잠김, 공급량이 갈 다른 곳은 없습니다. 선 아래에서는 에스크로된 모든 사토시가 기한에 환불됩니다: 떠안게 되는 반쪽짜리 런칭은 없습니다. 주소당 {perAddress} XCP 상한 때문에 선을 넘으려면 최소 {n}개의 서로 다른 주소가 필요합니다 — 상한 아래로 민팅하는 사람이 있으면 더 필요합니다. 군중의 하한이지 정확한 수는 아닙니다.", + "The service is busy or unavailable. Try again shortly.": "서비스가 혼잡하거나 이용할 수 없습니다. 잠시 후 다시 시도하세요.", "The standard removes the rug pull and the premine. It does not remove risk, and we won't pretend otherwise:": "이 표준은 러그풀과 프리마인을 없앱니다. 위험을 없애지는 않으며, 그런 척하지도 않겠습니다:", "The timing clauses are the two deliberate inequalities. Consensus does not require a future start — a launch confirming late simply opens instantly — so the pre-announcement guarantee lives here: {startBlock} must exceed the confirmation block. Without that clause, a creator could broadcast a nominal 1,000-block sale late, confirm just before its own deadline, and run a near-instant insider mint behind thousand-block metadata. And on the fairminter row the window check relaxes to {lte} once closed because core rewrites the deadline on early sell-out — for closed launches this site restores exact equality from the immutable {event} event (see the trap in Reading state). Everything else is exact equality on the row.": "시점 조항은 의도된 두 개의 부등식입니다. 합의는 미래의 시작을 요구하지 않습니다 — 늦게 확인된 런칭은 그냥 즉시 열립니다 — 그래서 사전 공지 보장은 여기에 있습니다: {startBlock}은 확인 블록보다 커야 합니다. 이 조항이 없다면 제작자는 명목상 1,000블록짜리 판매를 늦게 브로드캐스트하고, 자신의 기한 직전에 확인시킨 뒤, 천 블록짜리 메타데이터 뒤에서 거의 즉시 내부자 민팅을 돌릴 수 있습니다. 또한 페어민터 행의 기간 검사는 종료 후 {lte}로 완화됩니다. core가 조기 완판 시 기한을 다시 쓰기 때문이며, 종료된 런칭에 대해 이 사이트는 불변의 {event} 이벤트로부터 정확한 일치를 복원합니다 (상태 읽기의 함정 참조). 나머지는 전부 행에 대한 정확한 일치입니다.", + "The transaction did not match the verified request. It was blocked.": "트랜잭션이 검증된 요청과 일치하지 않아 차단되었습니다.", "The whole standard, hands-on — starting, like every launch, with the mint.": "표준 전체를 직접 만져봅니다 — 모든 런칭이 그렇듯, 민팅부터 시작합니다.", "The XCP-69 standard": "XCP-69 표준", "There is no fee-split table because there are no fees to split. Every satoshi of XCP paid by minters goes into the pool. The only costs anywhere in the system are Bitcoin transaction fees, the 0.5 XCP asset-name registration fee for named assets, and the protocol's pooldeposit gas fee debited at creation — costs paid to the network, not to us or to the creator.": "수수료 분배표는 없습니다. 분배할 수수료가 없기 때문입니다. 민터가 지불한 XCP는 사토시 하나까지 전부 풀로 들어갑니다. 시스템 전체에서 발생하는 비용은 비트코인 트랜잭션 수수료, 이름 있는 자산의 0.5 XCP 이름 등록 수수료, 그리고 생성 시 차감되는 프로토콜의 pooldeposit 가스 수수료뿐입니다 — 우리나 제작자가 아니라 네트워크에 지불되는 비용입니다.", @@ -1128,6 +1141,8 @@ "Upcoming": "곧 시작", "Upload failed": "업로드 실패", "Upload token image": "토큰 이미지 업로드", + "Use digits and a dot, with up to {n} decimal places.": "반각 숫자와 점을 사용하고 소수점 이하는 최대 {n}자리까지 입력하세요.", + "Use whole numbers only.": "정수만 입력하세요.", "Value": "가치", "Value and underlying amounts are what withdrawing the LP tokens now would return. XCP value is available when XCP is one side of the pair. LP profit and loss is not estimated.": "가치와 기초 수량은 지금 LP 토큰을 인출하면 돌려받을 자산입니다. XCP 가치는 페어의 한쪽이 XCP일 때 표시됩니다. LP 손익은 추정하지 않습니다.", "Vends": "판매 단위", @@ -1212,6 +1227,7 @@ "Your position: {position} · {pct} of the pool": "내 포지션: {position} · 풀의 {pct}", "Your positions, history, and launches. Every graduated XCP-69 launch trades against XCP, so a position's value is a pool price away from a dollar figure.": "내 포지션, 기록, 런칭입니다. 졸업한 모든 XCP-69 런칭은 XCP 대비로 거래되므로 포지션 가치는 풀 가격 하나만 거치면 달러 금액이 됩니다.", "Your profile": "내 프로필", + "Your wallet could not complete this request. Check the details.": "지갑에서 이 요청을 완료하지 못했습니다. 세부 정보를 확인하세요.", "Your wallet supplied a connection signature that didn't verify, so editing is locked for this session. Disconnect and reconnect to try again.": "지갑이 제공한 연결 서명이 검증되지 않아 이번 세션에서는 편집이 잠겼습니다. 연결을 해제하고 다시 연결해 보세요.", "Your wallet supplied a connection signature that didn't verify, so we can't confirm this address is really yours. Disconnect and reconnect to try again — nothing else on the site is affected.": "지갑이 보낸 연결 서명이 검증되지 않아 이 주소가 정말 본인 것인지 확인할 수 없습니다. 연결을 끊고 다시 연결해 보세요 — 사이트의 다른 기능에는 영향이 없습니다.", "Your wallet will ask once per route, in order — each popup is one route, nothing more.": "지갑이 경로마다 순서대로 한 번씩 확인을 요청합니다 — 팝업 하나가 경로 하나이며, 그 이상은 없습니다.", diff --git a/apps/web/src/locales/ko.status.json b/apps/web/src/locales/ko.status.json index 4c75b7b4..44f3b5b5 100644 --- a/apps/web/src/locales/ko.status.json +++ b/apps/web/src/locales/ko.status.json @@ -1205,6 +1205,22 @@ "Launch #{n}", "Positions, open mints, history, and launches for {address} on xcp.fun.", "Show amounts in {currency}", - "open@@bounty" + "open@@bounty", + "Check the amount, units, and decimal places.", + "Copy diagnostics", + "Could not refresh the quote. Try again.", + "Enter a multiple of {n}.", + "Enter a value from {min} to {max}.", + "Follow language", + "Number format", + "Quote changed or could not be refreshed. Review the amounts and try again.", + "Reconnect your wallet and try again.", + "Request declined in wallet.", + "Technical details", + "The service is busy or unavailable. Try again shortly.", + "The transaction did not match the verified request. It was blocked.", + "Use digits and a dot, with up to {n} decimal places.", + "Use whole numbers only.", + "Your wallet could not complete this request. Check the details." ] } diff --git a/apps/web/src/locales/pt.json b/apps/web/src/locales/pt.json index f0ccbb66..f8aaffc0 100644 --- a/apps/web/src/locales/pt.json +++ b/apps/web/src/locales/pt.json @@ -362,6 +362,7 @@ "Chain:": "Chain:", "Change in {currency} over the last {window}, since the pool opened": "Variação em {currency} em {window}, desde que o pool abriu", "Change in {currency} over the last 24 hours": "Variação em {currency} nas últimas 24 horas", + "Check the amount, units, and decimal places.": "Confira a quantidade, as unidades e as casas decimais.", "Checking availability…": "Verificando disponibilidade…", "Checking balance…": "Verificando saldo…", "checking…": "verificando…", @@ -414,12 +415,14 @@ "Copied ✓": "Copiado ✓", "Copy address": "Copiar endereço", "Copy as Markdown": "Copiar como Markdown", + "Copy diagnostics": "Copiar diagnóstico", "Copy failed": "Não foi possível copiar", "Copy issuer address": "Copiar endereço do emissor", "Copy link": "Copiar link", "Copy the link, or post it straight to X.": "Copie o link ou poste direto no X.", "Core has no on-chain standard marker, so conformance is a predicate: exact equality against the standard's fixed raw values. This is the actual function this site runs — a launch either passes it or is not XCP-69.": "O Core não tem nenhum marcador do padrão on-chain, então a conformidade é um predicado: igualdade exata com os valores brutos fixos do padrão. Esta é a função real que este site roda — um lançamento passa por ela ou não é XCP-69.", "Corrections": "Correções", + "Could not refresh the quote. Try again.": "Não foi possível atualizar a cotação. Tente novamente.", "Couldn't check the {give}/{get} pool. Quotes may still use the order book.": "Não foi possível consultar o pool {give}/{get}. As cotações ainda podem usar o livro de ofertas.", "Couldn't confirm this signature. Updating your wallet and reconnecting usually sorts it.": "Não foi possível verificar esta assinatura. Atualizar a carteira e reconectar costuma resolver.", "Couldn't load page {n}. Showing the last page that loaded.": "Não foi possível carregar a página {n}. Mostrando a última página carregada.", @@ -481,7 +484,9 @@ "Earned so far": "Ganho até agora", "Edit token info": "Editar informações do token", "Editing is locked for this session": "A edição está bloqueada nesta sessão", + "Enter a multiple of {n}.": "Insira um múltiplo de {n}.", "Enter a price": "Informe um preço", + "Enter a value from {min} to {max}.": "Insira um valor entre {min} e {max}.", "Enter an amount": "Informe uma quantidade", "Enter opens the first result · Esc closes": "Enter abre o primeiro resultado · Esc fecha", "escrowed": "em custódia", @@ -533,6 +538,7 @@ "Floor": "Piso", "Floor price": "Preço mínimo", "Floor: {n}": "Piso: {n}", + "Follow language": "Seguir o idioma", "Full 1M bags sold before yours": "Bolsas completas de 1M vendidas antes da sua", "Full specification with raw compose values and the conformance predicate: {spec}. Conformance is checked field-by-field on-chain — this site lists only launches that match exactly.": "Especificação completa com os valores brutos de compose e o predicado de conformidade: {spec}. A conformidade é verificada campo a campo on-chain — este site só lista lançamentos que batem exatamente.", "full XCP refund, automatic": "reembolso total em XCP, automático", @@ -803,6 +809,7 @@ "Nothing resting on the book right now.": "Não há nada em espera no livro neste momento.", "Nothing to show.": "Nada para mostrar.", "now": "agora", + "Number format": "Formato numérico", "Observed address behavior, not identity or intent. Not investment advice.": "Comportamento observado de endereços, não identidade nem intenção. Não é recomendação de investimento.", "of {n} in the batch": "de {n} no lote", "of {n} minters, {pct}%": "de {n} minters, {pct}%", @@ -911,6 +918,7 @@ "Public sale": "Venda pública", "public sale · 69M": "venda pública · 69M", "Queue": "Fila", + "Quote changed or could not be refreshed. Review the amounts and try again.": "A cotação mudou ou não pôde ser atualizada. Confira as quantidades e tente novamente.", "Quote moved — the numbers above are updated. Press again to swap at the new price.": "A cotação mudou — os números acima já estão atualizados. Pressione de novo para trocar pelo novo preço.", "Quote refresh countdown": "Contagem regressiva para atualizar a cotação", "Quote returns and prices in": "Mostrar rendimentos e preços em", @@ -928,6 +936,7 @@ "Realized PnL": "PnL realizado", "Recently confirmed": "Confirmadas recentemente", "Recommended": "Recomendada", + "Reconnect your wallet and try again.": "Reconecte sua carteira e tente novamente.", "redeployed": "reinvestidos", "Reference launch": "Lançamento de referência", "Refresh": "Atualizar", @@ -943,6 +952,7 @@ "Repeat dumpers": "Dumpers reincidentes", "Replace image": "Substituir imagem", "Represented": "Representadas", + "Request declined in wallet.": "Solicitação recusada na carteira.", "Resolve.": "Resolver.", "Retry": "Tentar de novo", "Reward": "Recompensa", @@ -1041,6 +1051,7 @@ "Takes effect when it confirms. {tx}": "Passa a valer ao confirmar. {tx}", "Target": "Meta", "Target start block": "Bloco de início desejado", + "Technical details": "Detalhes técnicos", "Telegram": "Telegram", "Ten percent above the cheapest open dispenser": "Dez por cento acima do dispenser aberto mais barato", "The {mult}× opening premium is structural, not a price guarantee.": "O prêmio de abertura de {mult}× é estrutural, não uma garantia de preço.", @@ -1067,8 +1078,10 @@ "The per-address cap is sybil-resistant in cost only, not in principle.": "O limite por endereço é resistente a Sybil só no custo, não no princípio.", "The pool opens with 690 XCP against 31M tokens: 69/31 ≈ {mult}× the mint price. Every minter is structurally in profit at open, and the pool — not later buyers — absorbs early exits.": "O pool abre com 690 XCP contra 31M de tokens: 69/31 ≈ {mult}× o preço de mint. Todo minter está estruturalmente no lucro na abertura, e o pool — não os compradores posteriores — absorve as saídas antecipadas.", "The sale line sits at 69M of the 100M supply; the other 31M is reserved for the pool and only ever exists if the line is crossed — 69 + 31 = 100, locked, nowhere else for supply to be. Below the line, every escrowed satoshi refunds at the deadline: there is no partial launch to be bag-held. The {perAddress} XCP per-address cap means crossing takes at least {n} distinct addresses — more if people mint below the cap. A floor on the crowd, not a count.": "A linha de venda fica em 69M dos 100M de fornecimento; os outros 31M são reservados para o pool e só existem se a linha for cruzada — 69 + 31 = 100, travado, sem nenhum outro lugar onde o fornecimento possa estar. Abaixo da linha, cada satoshi em custódia é reembolsado no prazo final: não existe lançamento parcial para ficar segurando. O limite de {perAddress} XCP por endereço faz com que cruzar a linha exija pelo menos {n} endereços distintos — mais, se as pessoas mintarem abaixo do limite. Um piso para a multidão, não uma contagem.", + "The service is busy or unavailable. Try again shortly.": "O serviço está ocupado ou indisponível. Tente novamente em instantes.", "The standard removes the rug pull and the premine. It does not remove risk, and we won't pretend otherwise:": "O padrão elimina o rug pull e a pré-mineração. Ele não elimina o risco, e não vamos fingir o contrário:", "The timing clauses are the two deliberate inequalities. Consensus does not require a future start — a launch confirming late simply opens instantly — so the pre-announcement guarantee lives here: {startBlock} must exceed the confirmation block. Without that clause, a creator could broadcast a nominal 1,000-block sale late, confirm just before its own deadline, and run a near-instant insider mint behind thousand-block metadata. And on the fairminter row the window check relaxes to {lte} once closed because core rewrites the deadline on early sell-out — for closed launches this site restores exact equality from the immutable {event} event (see the trap in Reading state). Everything else is exact equality on the row.": "As cláusulas de tempo são as duas desigualdades deliberadas. O consenso não exige um início futuro — um lançamento que confirma tarde simplesmente abre na hora — então a garantia do anúncio prévio mora aqui: {startBlock} precisa ser maior que o bloco de confirmação. Sem essa cláusula, um criador poderia transmitir tarde uma venda nominal de 1.000 blocos, confirmar pouco antes do próprio prazo e rodar um mint interno quase instantâneo escondido atrás de metadados de mil blocos. E na linha do fairminter a verificação da janela relaxa para {lte} depois de fechado, porque o core reescreve o prazo quando esgota antes da hora — para lançamentos fechados este site restaura a igualdade exata a partir do evento imutável {event} (veja a pegadinha em Lendo o estado). Todo o resto é igualdade exata na linha.", + "The transaction did not match the verified request. It was blocked.": "A transação não correspondia à solicitação verificada e foi bloqueada.", "The whole standard, hands-on — starting, like every launch, with the mint.": "O padrão inteiro, na prática — começando, como todo lançamento, pelo mint.", "The XCP-69 standard": "O padrão XCP-69", "There is no fee-split table because there are no fees to split. Every satoshi of XCP paid by minters goes into the pool. The only costs anywhere in the system are Bitcoin transaction fees, the 0.5 XCP asset-name registration fee for named assets, and the protocol's pooldeposit gas fee debited at creation — costs paid to the network, not to us or to the creator.": "Não existe tabela de divisão de taxas porque não há taxas para dividir. Cada satoshi de XCP pago pelos minters vai para o pool. Os únicos custos em todo o sistema são as taxas de transação do Bitcoin, a taxa de registro de nome de 0,5 XCP para ativos nomeados e a taxa de gas do pooldeposit do protocolo, debitada na criação — custos pagos à rede, não a nós nem ao criador.", @@ -1128,6 +1141,8 @@ "Upcoming": "Em breve", "Upload failed": "Falha no envio", "Upload token image": "Enviar a imagem do token", + "Use digits and a dot, with up to {n} decimal places.": "Use dígitos e um ponto, com até {n} casas decimais.", + "Use whole numbers only.": "Use apenas números inteiros.", "Value": "Valor", "Value and underlying amounts are what withdrawing the LP tokens now would return. XCP value is available when XCP is one side of the pair. LP profit and loss is not estimated.": "O valor e os montantes subjacentes são o que retirar os tokens LP agora devolveria. O valor em XCP está disponível quando XCP é um dos lados do par. O lucro ou prejuízo da LP não é estimado.", "Vends": "Vende", @@ -1212,6 +1227,7 @@ "Your position: {position} · {pct} of the pool": "Sua posição: {position} · {pct} do pool", "Your positions, history, and launches. Every graduated XCP-69 launch trades against XCP, so a position's value is a pool price away from a dollar figure.": "Suas posições, histórico e lançamentos. Todo lançamento XCP-69 graduado é negociado contra XCP, então o valor de uma posição está a um preço de pool de distância de um valor em dólares.", "Your profile": "Seu perfil", + "Your wallet could not complete this request. Check the details.": "Sua carteira não conseguiu concluir esta solicitação. Confira os detalhes.", "Your wallet supplied a connection signature that didn't verify, so editing is locked for this session. Disconnect and reconnect to try again.": "Sua carteira forneceu uma assinatura de conexão que não pôde ser verificada, então a edição está bloqueada nesta sessão. Desconecte e conecte de novo para tentar outra vez.", "Your wallet supplied a connection signature that didn't verify, so we can't confirm this address is really yours. Disconnect and reconnect to try again — nothing else on the site is affected.": "Sua carteira forneceu uma assinatura de conexão que não pôde ser verificada, então não conseguimos confirmar que este endereço é realmente seu. Desconecte e reconecte para tentar de novo — nada mais no site é afetado.", "Your wallet will ask once per route, in order — each popup is one route, nothing more.": "Sua carteira vai pedir confirmação uma vez por rota, em ordem — cada janela é uma rota, nada mais.", diff --git a/apps/web/src/locales/pt.status.json b/apps/web/src/locales/pt.status.json index 4c75b7b4..44f3b5b5 100644 --- a/apps/web/src/locales/pt.status.json +++ b/apps/web/src/locales/pt.status.json @@ -1205,6 +1205,22 @@ "Launch #{n}", "Positions, open mints, history, and launches for {address} on xcp.fun.", "Show amounts in {currency}", - "open@@bounty" + "open@@bounty", + "Check the amount, units, and decimal places.", + "Copy diagnostics", + "Could not refresh the quote. Try again.", + "Enter a multiple of {n}.", + "Enter a value from {min} to {max}.", + "Follow language", + "Number format", + "Quote changed or could not be refreshed. Review the amounts and try again.", + "Reconnect your wallet and try again.", + "Request declined in wallet.", + "Technical details", + "The service is busy or unavailable. Try again shortly.", + "The transaction did not match the verified request. It was blocked.", + "Use digits and a dot, with up to {n} decimal places.", + "Use whole numbers only.", + "Your wallet could not complete this request. Check the details." ] } diff --git a/apps/web/src/locales/ru.json b/apps/web/src/locales/ru.json index 26eda45c..ec0a1a8f 100644 --- a/apps/web/src/locales/ru.json +++ b/apps/web/src/locales/ru.json @@ -362,6 +362,7 @@ "Chain:": "Блокчейн:", "Change in {currency} over the last {window}, since the pool opened": "Изменение в {currency} за {window} с момента открытия пула", "Change in {currency} over the last 24 hours": "Изменение в {currency} за последние 24 часа", + "Check the amount, units, and decimal places.": "Проверьте сумму, единицы и количество знаков после точки.", "Checking availability…": "Проверяем доступность…", "Checking balance…": "Проверяем баланс…", "checking…": "проверяем…", @@ -414,12 +415,14 @@ "Copied ✓": "Скопировано ✓", "Copy address": "Скопировать адрес", "Copy as Markdown": "Скопировать как Markdown", + "Copy diagnostics": "Копировать диагностику", "Copy failed": "Не удалось скопировать", "Copy issuer address": "Скопировать адрес эмитента", "Copy link": "Копировать ссылку", "Copy the link, or post it straight to X.": "Скопируйте ссылку или сразу опубликуйте в X.", "Core has no on-chain standard marker, so conformance is a predicate: exact equality against the standard's fixed raw values. This is the actual function this site runs — a launch either passes it or is not XCP-69.": "В Core нет маркера стандарта в блокчейне, поэтому соответствие — это предикат: точное равенство фиксированным сырым значениям стандарта. Именно эту функцию выполняет сайт — запуск либо проходит её, либо не является XCP-69.", "Corrections": "Уточнения", + "Could not refresh the quote. Try again.": "Не удалось обновить котировку. Повторите попытку.", "Couldn't check the {give}/{get} pool. Quotes may still use the order book.": "Не удалось проверить пул {give}/{get}. Котировки всё же могут использовать книгу ордеров.", "Couldn't confirm this signature. Updating your wallet and reconnecting usually sorts it.": "Не удалось подтвердить подпись. Обычно помогает обновить кошелёк и подключиться заново.", "Couldn't load page {n}. Showing the last page that loaded.": "Не удалось загрузить страницу {n}. Показана последняя загруженная.", @@ -481,7 +484,9 @@ "Earned so far": "Заработано на данный момент", "Edit token info": "Изменить информацию о токене", "Editing is locked for this session": "Редактирование заблокировано для этой сессии", + "Enter a multiple of {n}.": "Введите значение, кратное {n}.", "Enter a price": "Введите цену", + "Enter a value from {min} to {max}.": "Введите значение от {min} до {max}.", "Enter an amount": "Введите сумму", "Enter opens the first result · Esc closes": "Enter открывает первый результат · Esc закрывает", "escrowed": "в эскроу", @@ -533,6 +538,7 @@ "Floor": "Минимум", "Floor price": "Минимальная цена", "Floor: {n}": "Минимум: {n}", + "Follow language": "Как в выбранном языке", "Full 1M bags sold before yours": "Полных мешков по 1M, проданных раньше вашего", "Full specification with raw compose values and the conformance predicate: {spec}. Conformance is checked field-by-field on-chain — this site lists only launches that match exactly.": "Полная спецификация с сырыми значениями compose и предикатом соответствия: {spec}. Соответствие проверяется по каждому полю в блокчейне — на этом сайте перечислены только запуски с точным совпадением.", "full XCP refund, automatic": "полный возврат XCP, автоматически", @@ -803,6 +809,7 @@ "Nothing resting on the book right now.": "Сейчас в книге нет ожидающих ордеров.", "Nothing to show.": "Показывать нечего.", "now": "сейчас", + "Number format": "Формат чисел", "Observed address behavior, not identity or intent. Not investment advice.": "Наблюдаемое поведение адресов, а не личности или намерения. Не является инвестиционной рекомендацией.", "of {n} in the batch": "из {n} в партии", "of {n} minters, {pct}%": "из {n} минтеров, {pct} %", @@ -911,6 +918,7 @@ "Public sale": "Публичная продажа", "public sale · 69M": "публичная продажа · 69M", "Queue": "Очередь", + "Quote changed or could not be refreshed. Review the amounts and try again.": "Котировка изменилась или не обновилась. Проверьте суммы и повторите попытку.", "Quote moved — the numbers above are updated. Press again to swap at the new price.": "Котировка изменилась — цифры выше обновлены. Нажмите ещё раз, чтобы обменять по новой цене.", "Quote refresh countdown": "Отсчёт до обновления котировки", "Quote returns and prices in": "Показывать доходность и цены в", @@ -928,6 +936,7 @@ "Realized PnL": "Реализованный PnL", "Recently confirmed": "Недавно подтверждено", "Recommended": "Рекомендуем", + "Reconnect your wallet and try again.": "Подключите кошелёк заново и повторите попытку.", "redeployed": "вложено снова", "Reference launch": "Эталонный запуск", "Refresh": "Обновить", @@ -943,6 +952,7 @@ "Repeat dumpers": "Повторные дамперы", "Replace image": "Заменить изображение", "Represented": "Представлены", + "Request declined in wallet.": "Запрос отклонён в кошельке.", "Resolve.": "Разрешение.", "Retry": "Повторить", "Reward": "Награда", @@ -1041,6 +1051,7 @@ "Takes effect when it confirms. {tx}": "Вступит в силу после подтверждения. {tx}", "Target": "Цель", "Target start block": "Целевой стартовый блок", + "Technical details": "Технические сведения", "Telegram": "Telegram", "Ten percent above the cheapest open dispenser": "На десять процентов выше самого дешёвого открытого автомата", "The {mult}× opening premium is structural, not a price guarantee.": "Стартовая премия {mult}× — следствие структуры, а не гарантия цены.", @@ -1067,8 +1078,10 @@ "The per-address cap is sybil-resistant in cost only, not in principle.": "Лимит на адрес устойчив к сибил-атаке только по стоимости, а не в принципе.", "The pool opens with 690 XCP against 31M tokens: 69/31 ≈ {mult}× the mint price. Every minter is structurally in profit at open, and the pool — not later buyers — absorbs early exits.": "Пул открывается с 690 XCP против 31M токенов: 69/31 ≈ {mult}× от цены минта. Каждый минтер структурно в плюсе на открытии, а ранние выходы поглощает пул — а не более поздние покупатели.", "The sale line sits at 69M of the 100M supply; the other 31M is reserved for the pool and only ever exists if the line is crossed — 69 + 31 = 100, locked, nowhere else for supply to be. Below the line, every escrowed satoshi refunds at the deadline: there is no partial launch to be bag-held. The {perAddress} XCP per-address cap means crossing takes at least {n} distinct addresses — more if people mint below the cap. A floor on the crowd, not a count.": "Линия продажи стоит на 69M из 100M предложения; остальные 31M зарезервированы для пула и существуют, только если линия пересечена — 69 + 31 = 100, заблокировано, предложению больше негде быть. Ниже линии каждый сатоши из эскроу возвращается в дедлайн: частичного запуска, с которым можно застрять, не бывает. Лимит {perAddress} XCP на адрес означает, что для пересечения нужно минимум {n} разных адресов — больше, если люди минтят меньше лимита. Нижняя граница толпы, а не её размер.", + "The service is busy or unavailable. Try again shortly.": "Сервис занят или недоступен. Повторите попытку немного позже.", "The standard removes the rug pull and the premine. It does not remove risk, and we won't pretend otherwise:": "Стандарт убирает рагпулл и премайн. Риск он не убирает, и мы не будем делать вид, что это не так:", "The timing clauses are the two deliberate inequalities. Consensus does not require a future start — a launch confirming late simply opens instantly — so the pre-announcement guarantee lives here: {startBlock} must exceed the confirmation block. Without that clause, a creator could broadcast a nominal 1,000-block sale late, confirm just before its own deadline, and run a near-instant insider mint behind thousand-block metadata. And on the fairminter row the window check relaxes to {lte} once closed because core rewrites the deadline on early sell-out — for closed launches this site restores exact equality from the immutable {event} event (see the trap in Reading state). Everything else is exact equality on the row.": "Условия по времени — два намеренных неравенства. Консенсус не требует будущего старта: запуск, подтверждённый с опозданием, просто открывается сразу, — поэтому гарантия предварительного объявления живёт здесь: {startBlock} должен быть больше блока подтверждения. Без этого условия создатель мог бы транслировать номинально тысячеблочную продажу с опозданием, подтвердить её перед самым дедлайном и провести почти мгновенный инсайдерский минт под прикрытием метаданных о тысяче блоков. А в записи fairminter проверка окна ослабляется до {lte} после закрытия, потому что core перезаписывает дедлайн при ранней распродаже — для закрытых запусков этот сайт восстанавливает точное равенство из неизменяемого события {event} (см. ловушку в разделе «Чтение состояния»). Всё остальное — точное равенство по записи.", + "The transaction did not match the verified request. It was blocked.": "Транзакция не соответствовала проверенному запросу и была заблокирована.", "The whole standard, hands-on — starting, like every launch, with the mint.": "Весь стандарт своими руками — начиная, как и каждый запуск, с минта.", "The XCP-69 standard": "Стандарт XCP-69", "There is no fee-split table because there are no fees to split. Every satoshi of XCP paid by minters goes into the pool. The only costs anywhere in the system are Bitcoin transaction fees, the 0.5 XCP asset-name registration fee for named assets, and the protocol's pooldeposit gas fee debited at creation — costs paid to the network, not to us or to the creator.": "Таблицы распределения комиссий нет, потому что распределять нечего. Каждый сатоши XCP, уплаченный минтерами, уходит в пул. Единственные расходы во всей системе — комиссии за биткоин-транзакции, комиссия 0,5 XCP за регистрацию имени для именованных активов и комиссия протокола pooldeposit за газ, списываемая при создании, — расходы в пользу сети, а не нас или создателя.", @@ -1128,6 +1141,8 @@ "Upcoming": "Скоро", "Upload failed": "Не удалось загрузить", "Upload token image": "Загрузить изображение токена", + "Use digits and a dot, with up to {n} decimal places.": "Используйте цифры и точку, не более {n} знаков после точки.", + "Use whole numbers only.": "Используйте только целые числа.", "Value": "Стоимость", "Value and underlying amounts are what withdrawing the LP tokens now would return. XCP value is available when XCP is one side of the pair. LP profit and loss is not estimated.": "Стоимость и базовые объёмы — это то, что вернул бы вывод LP-токенов прямо сейчас. Стоимость в XCP доступна, когда XCP — одна из сторон пары. Прибыль и убыток по LP не оцениваются.", "Vends": "Продаёт", @@ -1212,6 +1227,7 @@ "Your position: {position} · {pct} of the pool": "Ваша позиция: {position} · {pct} пула", "Your positions, history, and launches. Every graduated XCP-69 launch trades against XCP, so a position's value is a pool price away from a dollar figure.": "Ваши позиции, история и запуски. Каждый выпустившийся запуск XCP-69 торгуется к XCP, поэтому от стоимости позиции до суммы в долларах — одна цена пула.", "Your profile": "Ваш профиль", + "Your wallet could not complete this request. Check the details.": "Кошелёк не смог выполнить этот запрос. Проверьте подробности.", "Your wallet supplied a connection signature that didn't verify, so editing is locked for this session. Disconnect and reconnect to try again.": "Кошелёк передал подпись подключения, которую не удалось проверить, поэтому редактирование заблокировано на эту сессию. Отключитесь и подключитесь снова, чтобы попробовать ещё раз.", "Your wallet supplied a connection signature that didn't verify, so we can't confirm this address is really yours. Disconnect and reconnect to try again — nothing else on the site is affected.": "Ваш кошелёк передал подпись подключения, которая не прошла проверку, поэтому мы не можем подтвердить, что этот адрес действительно ваш. Отключитесь и подключитесь снова — на остальную работу сайта это не влияет.", "Your wallet will ask once per route, in order — each popup is one route, nothing more.": "Кошелёк спросит один раз на маршрут, по порядку — каждое окно — это один маршрут, не больше.", diff --git a/apps/web/src/locales/ru.status.json b/apps/web/src/locales/ru.status.json index e43f622a..beaa4117 100644 --- a/apps/web/src/locales/ru.status.json +++ b/apps/web/src/locales/ru.status.json @@ -1207,6 +1207,22 @@ "Launch #{n}", "Positions, open mints, history, and launches for {address} on xcp.fun.", "Show amounts in {currency}", - "open@@bounty" + "open@@bounty", + "Check the amount, units, and decimal places.", + "Copy diagnostics", + "Could not refresh the quote. Try again.", + "Enter a multiple of {n}.", + "Enter a value from {min} to {max}.", + "Follow language", + "Number format", + "Quote changed or could not be refreshed. Review the amounts and try again.", + "Reconnect your wallet and try again.", + "Request declined in wallet.", + "Technical details", + "The service is busy or unavailable. Try again shortly.", + "The transaction did not match the verified request. It was blocked.", + "Use digits and a dot, with up to {n} decimal places.", + "Use whole numbers only.", + "Your wallet could not complete this request. Check the details." ] } diff --git a/apps/web/src/locales/uk.json b/apps/web/src/locales/uk.json index 401f08a6..4dbdbbd1 100644 --- a/apps/web/src/locales/uk.json +++ b/apps/web/src/locales/uk.json @@ -362,6 +362,7 @@ "Chain:": "Блокчейн:", "Change in {currency} over the last {window}, since the pool opened": "Зміна у {currency} за останні {window}, відколи відкрився пул", "Change in {currency} over the last 24 hours": "Зміна у {currency} за останні 24 години", + "Check the amount, units, and decimal places.": "Перевірте суму, одиниці та кількість знаків після крапки.", "Checking availability…": "Перевірка доступності…", "Checking balance…": "Перевірка балансу…", "checking…": "перевірка…", @@ -414,12 +415,14 @@ "Copied ✓": "Скопійовано ✓", "Copy address": "Скопіювати адресу", "Copy as Markdown": "Скопіювати як Markdown", + "Copy diagnostics": "Копіювати діагностику", "Copy failed": "Не вдалося скопіювати", "Copy issuer address": "Скопіювати адресу емітента", "Copy link": "Скопіювати посилання", "Copy the link, or post it straight to X.": "Скопіюйте посилання або одразу опублікуйте в X.", "Core has no on-chain standard marker, so conformance is a predicate: exact equality against the standard's fixed raw values. This is the actual function this site runs — a launch either passes it or is not XCP-69.": "У Core немає ончейн-маркера стандарту, тож відповідність — це предикат: точна рівність із фіксованими сирими значеннями стандарту. Це та сама функція, яку виконує цей сайт: запуск або проходить її, або не є XCP-69.", "Corrections": "Виправлення", + "Could not refresh the quote. Try again.": "Не вдалося оновити котирування. Спробуйте ще раз.", "Couldn't check the {give}/{get} pool. Quotes may still use the order book.": "Не вдалося перевірити пул {give}/{get}. Котирування все ще можуть використовувати книгу ордерів.", "Couldn't confirm this signature. Updating your wallet and reconnecting usually sorts it.": "Не вдалося підтвердити цей підпис. Зазвичай допомагає оновити гаманець і підключитися знову.", "Couldn't load page {n}. Showing the last page that loaded.": "Не вдалося завантажити сторінку {n}. Показано останню завантажену.", @@ -481,7 +484,9 @@ "Earned so far": "Зароблено дотепер", "Edit token info": "Редагувати дані токена", "Editing is locked for this session": "Редагування заблоковано на цю сесію", + "Enter a multiple of {n}.": "Введіть значення, кратне {n}.", "Enter a price": "Введіть ціну", + "Enter a value from {min} to {max}.": "Введіть значення від {min} до {max}.", "Enter an amount": "Введіть суму", "Enter opens the first result · Esc closes": "Enter відкриває перший результат · Esc закриває", "escrowed": "в ескроу", @@ -533,6 +538,7 @@ "Floor": "Мінімум", "Floor price": "Мінімальна ціна", "Floor: {n}": "Мінімум: {n}", + "Follow language": "Як у вибраній мові", "Full 1M bags sold before yours": "Повних мішків по 1M, проданих до вашого", "Full specification with raw compose values and the conformance predicate: {spec}. Conformance is checked field-by-field on-chain — this site lists only launches that match exactly.": "Повна специфікація із сирими значеннями compose та предикатом відповідності: {spec}. Відповідність перевіряється ончейн поле за полем — цей сайт показує лише запуски, що збігаються точно.", "full XCP refund, automatic": "повне повернення XCP, автоматично", @@ -803,6 +809,7 @@ "Nothing resting on the book right now.": "Зараз у книзі немає жодного ордера.", "Nothing to show.": "Нема чого показати.", "now": "зараз", + "Number format": "Формат чисел", "Observed address behavior, not identity or intent. Not investment advice.": "Спостережувана поведінка адрес, а не особи чи наміри. Не є інвестиційною порадою.", "of {n} in the batch": "з {n} у пакеті", "of {n} minters, {pct}%": "з {n} мінтерів, {pct}%", @@ -911,6 +918,7 @@ "Public sale": "Публічний продаж", "public sale · 69M": "публічний продаж · 69M", "Queue": "Черга", + "Quote changed or could not be refreshed. Review the amounts and try again.": "Котирування змінилося або не оновилося. Перевірте суми та спробуйте ще раз.", "Quote moved — the numbers above are updated. Press again to swap at the new price.": "Котирування змінилося — цифри вище оновлено. Натисніть ще раз, щоб обміняти за новою ціною.", "Quote refresh countdown": "Відлік до оновлення котирування", "Quote returns and prices in": "Показувати дохідність і ціни в", @@ -928,6 +936,7 @@ "Realized PnL": "Реалізований PnL", "Recently confirmed": "Нещодавно підтверджені", "Recommended": "Рекомендовано", + "Reconnect your wallet and try again.": "Під’єднайте гаманець знову та повторіть спробу.", "redeployed": "вкладено знову", "Reference launch": "Еталонний запуск", "Refresh": "Оновити", @@ -943,6 +952,7 @@ "Repeat dumpers": "Повторні дампери", "Replace image": "Замінити зображення", "Represented": "Представлено", + "Request declined in wallet.": "Запит відхилено в гаманці.", "Resolve.": "Завершення.", "Retry": "Повторити", "Reward": "Винагорода", @@ -1041,6 +1051,7 @@ "Takes effect when it confirms. {tx}": "Набуде чинності після підтвердження. {tx}", "Target": "Мета", "Target start block": "Цільовий стартовий блок", + "Technical details": "Технічні відомості", "Telegram": "Telegram", "Ten percent above the cheapest open dispenser": "На десять відсотків вище за найдешевший відкритий автомат", "The {mult}× opening premium is structural, not a price guarantee.": "Стартова премія {mult}× — структурна, а не гарантія ціни.", @@ -1067,8 +1078,10 @@ "The per-address cap is sybil-resistant in cost only, not in principle.": "Ліміт на адресу стійкий до сибіл-атак лише в ціні, а не в принципі.", "The pool opens with 690 XCP against 31M tokens: 69/31 ≈ {mult}× the mint price. Every minter is structurally in profit at open, and the pool — not later buyers — absorbs early exits.": "Пул відкривається з 690 XCP проти 31M токенів: 69/31 ≈ {mult}× від ціни мінту. Кожен мінтер структурно в плюсі на відкритті, і саме пул — а не пізніші покупці — поглинає ранні виходи.", "The sale line sits at 69M of the 100M supply; the other 31M is reserved for the pool and only ever exists if the line is crossed — 69 + 31 = 100, locked, nowhere else for supply to be. Below the line, every escrowed satoshi refunds at the deadline: there is no partial launch to be bag-held. The {perAddress} XCP per-address cap means crossing takes at least {n} distinct addresses — more if people mint below the cap. A floor on the crowd, not a count.": "Лінія продажу стоїть на 69M із пропозиції 100M; решта 31M зарезервовано для пулу і взагалі існує лише тоді, коли лінію перетнуто — 69 + 31 = 100, заблоковано, пропозиції більше нема де бути. Нижче лінії кожен сатоші з ескроу повертається на дедлайні: немає часткового запуску, з яким можна лишитися на руках. Ліміт {perAddress} XCP на адресу означає, що для перетину потрібно щонайменше {n} різних адрес — більше, якщо люди мінтять менше за ліміт. Нижня межа натовпу, а не його підрахунок.", + "The service is busy or unavailable. Try again shortly.": "Сервіс зайнятий або недоступний. Спробуйте ще раз трохи пізніше.", "The standard removes the rug pull and the premine. It does not remove risk, and we won't pretend otherwise:": "Стандарт усуває рагпул і премайн. Ризик він не усуває, і ми не вдаватимемо, що це не так:", "The timing clauses are the two deliberate inequalities. Consensus does not require a future start — a launch confirming late simply opens instantly — so the pre-announcement guarantee lives here: {startBlock} must exceed the confirmation block. Without that clause, a creator could broadcast a nominal 1,000-block sale late, confirm just before its own deadline, and run a near-instant insider mint behind thousand-block metadata. And on the fairminter row the window check relaxes to {lte} once closed because core rewrites the deadline on early sell-out — for closed launches this site restores exact equality from the immutable {event} event (see the trap in Reading state). Everything else is exact equality on the row.": "Умови щодо часу — це дві навмисні нерівності. Консенсус не вимагає майбутнього старту — запуск, підтверджений із запізненням, просто відкривається миттєво, — тож гарантія попереднього оголошення живе тут: {startBlock} має перевищувати блок підтвердження. Без цієї умови творець міг би надіслати в мережу номінально 1 000-блоковий продаж із запізненням, підтвердити його перед самим дедлайном і провести майже миттєвий інсайдерський мінт за метаданими на тисячу блоків. А в рядку fairminter перевірка вікна після закриття пом'якшується до {lte}, бо core переписує дедлайн при достроковому розпродажі — для закритих запусків цей сайт відновлює точну рівність з незмінної події {event} (див. пастку в розділі «Читання стану»). Усе інше — точна рівність у рядку.", + "The transaction did not match the verified request. It was blocked.": "Транзакція не відповідала перевіреному запиту й була заблокована.", "The whole standard, hands-on — starting, like every launch, with the mint.": "Увесь стандарт на практиці — починаючи, як і кожен запуск, з мінту.", "The XCP-69 standard": "Стандарт XCP-69", "There is no fee-split table because there are no fees to split. Every satoshi of XCP paid by minters goes into the pool. The only costs anywhere in the system are Bitcoin transaction fees, the 0.5 XCP asset-name registration fee for named assets, and the protocol's pooldeposit gas fee debited at creation — costs paid to the network, not to us or to the creator.": "Таблиці розподілу комісій немає, бо нема чого розподіляти. Кожен сатоші XCP, сплачений мінтерами, іде в пул. Єдині витрати в усій системі — комісії за біткоїн-транзакції, реєстраційна комісія 0,5 XCP за ім'я для іменованих активів і газова комісія протоколу pooldeposit, що списується при створенні — це витрати на користь мережі, а не нам чи творцю.", @@ -1128,6 +1141,8 @@ "Upcoming": "Незабаром", "Upload failed": "Не вдалося завантажити", "Upload token image": "Завантажити зображення токена", + "Use digits and a dot, with up to {n} decimal places.": "Використовуйте цифри та крапку, не більше {n} знаків після крапки.", + "Use whole numbers only.": "Використовуйте лише цілі числа.", "Value": "Вартість", "Value and underlying amounts are what withdrawing the LP tokens now would return. XCP value is available when XCP is one side of the pair. LP profit and loss is not estimated.": "Вартість і базові суми — це те, що повернуло б виведення токенів LP зараз. Вартість у XCP доступна, коли XCP є однією зі сторін пари. Прибуток і збиток LP не оцінюються.", "Vends": "Продає", @@ -1212,6 +1227,7 @@ "Your position: {position} · {pct} of the pool": "Ваша позиція: {position} · {pct} пулу", "Your positions, history, and launches. Every graduated XCP-69 launch trades against XCP, so a position's value is a pool price away from a dollar figure.": "Ваші позиції, історія та запуски. Кожен запуск XCP-69, що випустився, торгується до XCP, тож вартість позиції відділяє від суми в доларах лише ціна пулу.", "Your profile": "Ваш профіль", + "Your wallet could not complete this request. Check the details.": "Гаманець не зміг виконати цей запит. Перевірте подробиці.", "Your wallet supplied a connection signature that didn't verify, so editing is locked for this session. Disconnect and reconnect to try again.": "Ваш гаманець надав підпис підключення, який не пройшов перевірку, тому редагування заблоковано на цю сесію. Відключіться й підключіться знову, щоб спробувати ще раз.", "Your wallet supplied a connection signature that didn't verify, so we can't confirm this address is really yours. Disconnect and reconnect to try again — nothing else on the site is affected.": "Ваш гаманець надав підпис підключення, який не пройшов перевірку, тому ми не можемо підтвердити, що ця адреса справді ваша. Відключіться й підключіться знову — на решту сайту це не впливає.", "Your wallet will ask once per route, in order — each popup is one route, nothing more.": "Гаманець запитає підтвердження один раз на маршрут, по черзі — кожне спливаюче вікно є одним маршрутом, не більше.", diff --git a/apps/web/src/locales/uk.status.json b/apps/web/src/locales/uk.status.json index e43f622a..beaa4117 100644 --- a/apps/web/src/locales/uk.status.json +++ b/apps/web/src/locales/uk.status.json @@ -1207,6 +1207,22 @@ "Launch #{n}", "Positions, open mints, history, and launches for {address} on xcp.fun.", "Show amounts in {currency}", - "open@@bounty" + "open@@bounty", + "Check the amount, units, and decimal places.", + "Copy diagnostics", + "Could not refresh the quote. Try again.", + "Enter a multiple of {n}.", + "Enter a value from {min} to {max}.", + "Follow language", + "Number format", + "Quote changed or could not be refreshed. Review the amounts and try again.", + "Reconnect your wallet and try again.", + "Request declined in wallet.", + "Technical details", + "The service is busy or unavailable. Try again shortly.", + "The transaction did not match the verified request. It was blocked.", + "Use digits and a dot, with up to {n} decimal places.", + "Use whole numbers only.", + "Your wallet could not complete this request. Check the details." ] } diff --git a/apps/web/src/locales/zh-hk.json b/apps/web/src/locales/zh-hk.json index 87878786..887b98a5 100644 --- a/apps/web/src/locales/zh-hk.json +++ b/apps/web/src/locales/zh-hk.json @@ -362,6 +362,7 @@ "Chain:": "鏈:", "Change in {currency} over the last {window}, since the pool opened": "自流動性池開放以來,最近 {window} 的 {currency} 計價漲跌", "Change in {currency} over the last 24 hours": "最近 24 小時的 {currency} 計價漲跌", + "Check the amount, units, and decimal places.": "請檢查數量、單位和小數位數。", "Checking availability…": "正在檢查是否可用…", "Checking balance…": "正在查詢餘額…", "checking…": "檢查中…", @@ -414,12 +415,14 @@ "Copied ✓": "已複製 ✓", "Copy address": "複製地址", "Copy as Markdown": "複製為 Markdown", + "Copy diagnostics": "複製診斷資料", "Copy failed": "複製失敗", "Copy issuer address": "複製發行者地址", "Copy link": "複製連結", "Copy the link, or post it straight to X.": "複製連結,或直接發到 X。", "Core has no on-chain standard marker, so conformance is a predicate: exact equality against the standard's fixed raw values. This is the actual function this site runs — a launch either passes it or is not XCP-69.": "Core 沒有鏈上的標準標記,因此符合性是一個判定式:與標準固定的原始值完全相等。這正是本站實際運行的函數 — 一次發射要麼通過它,要麼就不是 XCP-69。", "Corrections": "更正", + "Could not refresh the quote. Try again.": "無法更新報價。請重試。", "Couldn't check the {give}/{get} pool. Quotes may still use the order book.": "無法查詢 {give}/{get} 流動性池。報價仍可能來自訂單簿。", "Couldn't confirm this signature. Updating your wallet and reconnecting usually sorts it.": "無法驗證此簽名。通常更新錢包並重新連接即可解決。", "Couldn't load page {n}. Showing the last page that loaded.": "無法加載第 {n} 頁。當前顯示的是最後一次成功加載的頁面。", @@ -481,7 +484,9 @@ "Earned so far": "已獲得", "Edit token info": "編輯代幣資訊", "Editing is locked for this session": "本次會話已鎖定編輯", + "Enter a multiple of {n}.": "請輸入{n}的倍數。", "Enter a price": "輸入價格", + "Enter a value from {min} to {max}.": "請輸入{min}至{max}之間的數值。", "Enter an amount": "輸入數量", "Enter opens the first result · Esc closes": "Enter 打開第一條結果 · Esc 關閉", "escrowed": "協議託管中", @@ -533,6 +538,7 @@ "Floor": "底價", "Floor price": "底價", "Floor: {n}": "底價:{n}", + "Follow language": "跟隨語言", "Full 1M bags sold before yours": "排在你之前賣出的滿額 1M 倉位", "Full specification with raw compose values and the conformance predicate: {spec}. Conformance is checked field-by-field on-chain — this site lists only launches that match exactly.": "包含原始 compose 值和符合性判定式的完整規範:{spec}。符合性在鏈上逐欄位檢查 — 本站只列出完全匹配的發射。", "full XCP refund, automatic": "XCP 全額退款,自動執行", @@ -803,6 +809,7 @@ "Nothing resting on the book right now.": "訂單簿上當前沒有掛單。", "Nothing to show.": "暫無內容。", "now": "現在", + "Number format": "數字格式", "Observed address behavior, not identity or intent. Not investment advice.": "觀察到的是地址行為,而非身份或意圖。不構成投資建議。", "of {n} in the batch": "本批次共 {n}", "of {n} minters, {pct}%": "{n} 位鑄造者中,佔 {pct}%", @@ -911,6 +918,7 @@ "Public sale": "公開發售", "public sale · 69M": "公開發售 · 6900 萬", "Queue": "隊列", + "Quote changed or could not be refreshed. Review the amounts and try again.": "報價已更改或無法更新。請檢查數量後重試。", "Quote moved — the numbers above are updated. Press again to swap at the new price.": "報價已變動 — 上方數字已更新。再按一次即按新價格兌換。", "Quote refresh countdown": "報價刷新倒計時", "Quote returns and prices in": "收益與價格的計價單位", @@ -928,6 +936,7 @@ "Realized PnL": "已實現盈虧", "Recently confirmed": "最近已確認", "Recommended": "推薦", + "Reconnect your wallet and try again.": "請重新連接錢包後重試。", "redeployed": "再投入", "Reference launch": "參考發射", "Refresh": "重新整理", @@ -943,6 +952,7 @@ "Repeat dumpers": "重複砸盤者", "Replace image": "更換圖片", "Represented": "涉及社區", + "Request declined in wallet.": "請求已在錢包中被拒絕。", "Resolve.": "結算。", "Retry": "重試", "Reward": "獎勵", @@ -1041,6 +1051,7 @@ "Takes effect when it confirms. {tx}": "確認後生效。{tx}", "Target": "目標", "Target start block": "目標起始區塊", + "Technical details": "技術詳情", "Telegram": "Telegram", "Ten percent above the cheapest open dispenser": "比最便宜的開放售賣機高 10%", "The {mult}× opening premium is structural, not a price guarantee.": "{mult}× 的開盤溢價是結構性的,不是價格保證。", @@ -1067,8 +1078,10 @@ "The per-address cap is sybil-resistant in cost only, not in principle.": "每地址上限只在成本上抗女巫攻擊,在原理上並不能。", "The pool opens with 690 XCP against 31M tokens: 69/31 ≈ {mult}× the mint price. Every minter is structurally in profit at open, and the pool — not later buyers — absorbs early exits.": "流動性池以 690 XCP 對 3100 萬代幣開盤:69/31 ≈ 鑄造價的 {mult}×。每位鑄造者在開盤時從結構上就處於盈利,而吸收早期離場的是池子 — 不是後來的買家。", "The sale line sits at 69M of the 100M supply; the other 31M is reserved for the pool and only ever exists if the line is crossed — 69 + 31 = 100, locked, nowhere else for supply to be. Below the line, every escrowed satoshi refunds at the deadline: there is no partial launch to be bag-held. The {perAddress} XCP per-address cap means crossing takes at least {n} distinct addresses — more if people mint below the cap. A floor on the crowd, not a count.": "發售線位於 1 億供應量中的 6900 萬處;另外 3100 萬預留給流動性池,只有越過這條線才會存在 — 69 + 31 = 100,已鎖定,供應量無處可去。線以下,每一聰託管中的 XCP 都會在截止期退款:不存在讓人被套的半成品發射。每地址 {perAddress} XCP 的上限意味着越線至少需要 {n} 個不同地址 — 如果有人鑄造不滿上限,需要的地址更多。這是人羣的下限,不是人數。", + "The service is busy or unavailable. Try again shortly.": "服務繁忙或無法使用。請稍後重試。", "The standard removes the rug pull and the premine. It does not remove risk, and we won't pretend otherwise:": "這套標準消除了跑路和預挖。它並不消除風險,我們也不會假裝它能:", "The timing clauses are the two deliberate inequalities. Consensus does not require a future start — a launch confirming late simply opens instantly — so the pre-announcement guarantee lives here: {startBlock} must exceed the confirmation block. Without that clause, a creator could broadcast a nominal 1,000-block sale late, confirm just before its own deadline, and run a near-instant insider mint behind thousand-block metadata. And on the fairminter row the window check relaxes to {lte} once closed because core rewrites the deadline on early sell-out — for closed launches this site restores exact equality from the immutable {event} event (see the trap in Reading state). Everything else is exact equality on the row.": "時間條款是兩處刻意的不等式。共識並不要求未來的起始 — 一個遲到確認的發射只是立即開啓而已 — 所以預公告的保證就在這裏:{startBlock} 必須大於確認區塊。沒有這一條款,建立者可以很晚才廣播一個名義上 1,000 區塊的發售,在自己的截止期前一刻確認,然後在千區塊元數據的掩護下進行一次近乎即時的內部鑄造。另外,在公平鑄造合約記錄上,窗口期檢查在關閉後放寬為 {lte},因為 core 會在提前售罄時改寫截止期 — 對於已關閉的發射,本站會從不可變的 {event} 事件中恢復精確相等(參見「讀取狀態」中的陷阱)。其餘一切都是記錄上的精確相等。", + "The transaction did not match the verified request. It was blocked.": "交易與已驗證的請求不一致,已被攔截。", "The whole standard, hands-on — starting, like every launch, with the mint.": "親手體驗整套標準 — 和每一次發射一樣,從鑄造開始。", "The XCP-69 standard": "XCP-69 標準", "There is no fee-split table because there are no fees to split. Every satoshi of XCP paid by minters goes into the pool. The only costs anywhere in the system are Bitcoin transaction fees, the 0.5 XCP asset-name registration fee for named assets, and the protocol's pooldeposit gas fee debited at creation — costs paid to the network, not to us or to the creator.": "沒有費用分成表,因為沒有費用可分。鑄造者支付的每一聰 XCP 都進入流動性池。整個系統中唯一的成本是比特幣礦工費、命名資產的 0.5 XCP 資產名稱註冊費,以及建立時扣除的協議 pooldeposit gas 費 — 這些都付給網絡,而不是付給我們或建立者。", @@ -1128,6 +1141,8 @@ "Upcoming": "即將開始", "Upload failed": "上傳失敗", "Upload token image": "上傳代幣圖片", + "Use digits and a dot, with up to {n} decimal places.": "請使用半形數字和小數點,最多保留{n}位小數。", + "Use whole numbers only.": "請只輸入整數。", "Value": "價值", "Value and underlying amounts are what withdrawing the LP tokens now would return. XCP value is available when XCP is one side of the pair. LP profit and loss is not estimated.": "價值和底層數量指現在提取 LP 代幣能拿回的資產。當交易對一側是 XCP 時才有 XCP 價值。不估算 LP 的盈虧。", "Vends": "出售中", @@ -1212,6 +1227,7 @@ "Your position: {position} · {pct} of the pool": "你的倉位:{position} · 佔池 {pct}", "Your positions, history, and launches. Every graduated XCP-69 launch trades against XCP, so a position's value is a pool price away from a dollar figure.": "你的倉位、歷史和發射。每個畢業的 XCP-69 發射都以 XCP 計價交易,因此倉位價值只差一個池價就能換算成美元數字。", "Your profile": "我的主頁", + "Your wallet could not complete this request. Check the details.": "錢包無法完成此請求。請查看詳情。", "Your wallet supplied a connection signature that didn't verify, so editing is locked for this session. Disconnect and reconnect to try again.": "你的錢包提供的連接簽名未通過驗證,本次會話已鎖定編輯。請斷開並重新連接後再試。", "Your wallet supplied a connection signature that didn't verify, so we can't confirm this address is really yours. Disconnect and reconnect to try again — nothing else on the site is affected.": "你的錢包提供的連接簽名未能通過驗證,因此我們無法確認這個地址真的屬於你。請斷開後重新連接再試 — 網站的其他功能不受影響。", "Your wallet will ask once per route, in order — each popup is one route, nothing more.": "錢包會按順序為每條路由請求一次確認 — 每個彈窗對應一條路由,僅此而已。", diff --git a/apps/web/src/locales/zh-hk.status.json b/apps/web/src/locales/zh-hk.status.json index 72fa2c47..8849dbb1 100644 --- a/apps/web/src/locales/zh-hk.status.json +++ b/apps/web/src/locales/zh-hk.status.json @@ -1206,6 +1206,22 @@ "Launch #{n}", "Positions, open mints, history, and launches for {address} on xcp.fun.", "Show amounts in {currency}", - "open@@bounty" + "open@@bounty", + "Check the amount, units, and decimal places.", + "Copy diagnostics", + "Could not refresh the quote. Try again.", + "Enter a multiple of {n}.", + "Enter a value from {min} to {max}.", + "Follow language", + "Number format", + "Quote changed or could not be refreshed. Review the amounts and try again.", + "Reconnect your wallet and try again.", + "Request declined in wallet.", + "Technical details", + "The service is busy or unavailable. Try again shortly.", + "The transaction did not match the verified request. It was blocked.", + "Use digits and a dot, with up to {n} decimal places.", + "Use whole numbers only.", + "Your wallet could not complete this request. Check the details." ] } diff --git a/apps/web/src/locales/zh-tw.json b/apps/web/src/locales/zh-tw.json index 666d46a3..5416b856 100644 --- a/apps/web/src/locales/zh-tw.json +++ b/apps/web/src/locales/zh-tw.json @@ -362,6 +362,7 @@ "Chain:": "鏈:", "Change in {currency} over the last {window}, since the pool opened": "自流動性池開放以來,最近 {window} 的 {currency} 計價漲跌", "Change in {currency} over the last 24 hours": "最近 24 小時的 {currency} 計價漲跌", + "Check the amount, units, and decimal places.": "請檢查數量、單位與小數位數。", "Checking availability…": "正在檢查是否可用…", "Checking balance…": "正在查詢餘額…", "checking…": "檢查中…", @@ -414,12 +415,14 @@ "Copied ✓": "已複製 ✓", "Copy address": "複製地址", "Copy as Markdown": "複製為 Markdown", + "Copy diagnostics": "複製診斷資訊", "Copy failed": "複製失敗", "Copy issuer address": "複製發行者地址", "Copy link": "複製連結", "Copy the link, or post it straight to X.": "複製連結,或直接發到 X。", "Core has no on-chain standard marker, so conformance is a predicate: exact equality against the standard's fixed raw values. This is the actual function this site runs — a launch either passes it or is not XCP-69.": "Core 沒有鏈上的標準標記,因此符合性是一個判定式:與標準固定的原始值完全相等。這正是本站實際執行的函式 — 一次發射要麼通過它,要麼就不是 XCP-69。", "Corrections": "更正", + "Could not refresh the quote. Try again.": "無法更新報價。請重試。", "Couldn't check the {give}/{get} pool. Quotes may still use the order book.": "無法查詢 {give}/{get} 流動性池。報價仍可能來自訂單簿。", "Couldn't confirm this signature. Updating your wallet and reconnecting usually sorts it.": "無法驗證此簽名。通常更新錢包並重新連接即可解決。", "Couldn't load page {n}. Showing the last page that loaded.": "無法載入第 {n} 頁。當前顯示的是最後一次成功載入的頁面。", @@ -481,7 +484,9 @@ "Earned so far": "已獲得", "Edit token info": "編輯代幣資訊", "Editing is locked for this session": "本次會話已鎖定編輯", + "Enter a multiple of {n}.": "請輸入{n}的倍數。", "Enter a price": "輸入價格", + "Enter a value from {min} to {max}.": "請輸入{min}到{max}之間的值。", "Enter an amount": "輸入數量", "Enter opens the first result · Esc closes": "Enter 開啟第一條結果 · Esc 關閉", "escrowed": "協議託管中", @@ -533,6 +538,7 @@ "Floor": "底價", "Floor price": "底價", "Floor: {n}": "底價:{n}", + "Follow language": "跟隨語言", "Full 1M bags sold before yours": "排在你之前賣出的滿額 1M 倉位", "Full specification with raw compose values and the conformance predicate: {spec}. Conformance is checked field-by-field on-chain — this site lists only launches that match exactly.": "包含原始 compose 值和符合性判定式的完整規範:{spec}。符合性在鏈上逐欄位檢查 — 本站只列出完全匹配的發射。", "full XCP refund, automatic": "XCP 全額退款,自動執行", @@ -803,6 +809,7 @@ "Nothing resting on the book right now.": "訂單簿上當前沒有掛單。", "Nothing to show.": "暫無內容。", "now": "現在", + "Number format": "數字格式", "Observed address behavior, not identity or intent. Not investment advice.": "觀察到的是地址行為,而非身份或意圖。不構成投資建議。", "of {n} in the batch": "本批次共 {n}", "of {n} minters, {pct}%": "{n} 位鑄造者中,佔 {pct}%", @@ -911,6 +918,7 @@ "Public sale": "公開發售", "public sale · 69M": "公開發售 · 6900 萬", "Queue": "佇列", + "Quote changed or could not be refreshed. Review the amounts and try again.": "報價已變更或無法更新。請檢查數量後重試。", "Quote moved — the numbers above are updated. Press again to swap at the new price.": "報價已變動 — 上方數字已更新。再按一次即按新價格兌換。", "Quote refresh countdown": "報價重新整理倒計時", "Quote returns and prices in": "收益與價格的計價單位", @@ -928,6 +936,7 @@ "Realized PnL": "已實現盈虧", "Recently confirmed": "最近已確認", "Recommended": "推薦", + "Reconnect your wallet and try again.": "請重新連接錢包後重試。", "redeployed": "再投入", "Reference launch": "參考發射", "Refresh": "重新整理", @@ -943,6 +952,7 @@ "Repeat dumpers": "重複砸盤者", "Replace image": "更換圖片", "Represented": "涉及社群", + "Request declined in wallet.": "請求已在錢包中遭到拒絕。", "Resolve.": "結算。", "Retry": "重試", "Reward": "獎勵", @@ -1041,6 +1051,7 @@ "Takes effect when it confirms. {tx}": "確認後生效。{tx}", "Target": "目標", "Target start block": "目標起始區塊", + "Technical details": "技術詳細資訊", "Telegram": "Telegram", "Ten percent above the cheapest open dispenser": "比最便宜的開放販賣機高 10%", "The {mult}× opening premium is structural, not a price guarantee.": "{mult}× 的開盤溢價是結構性的,不是價格保證。", @@ -1067,8 +1078,10 @@ "The per-address cap is sybil-resistant in cost only, not in principle.": "每地址上限只在成本上抗女巫攻擊,在原理上並不能。", "The pool opens with 690 XCP against 31M tokens: 69/31 ≈ {mult}× the mint price. Every minter is structurally in profit at open, and the pool — not later buyers — absorbs early exits.": "流動性池以 690 XCP 對 3100 萬代幣開盤:69/31 ≈ 鑄造價的 {mult}×。每位鑄造者在開盤時從結構上就處於盈利,而吸收早期離場的是池子 — 不是後來的買家。", "The sale line sits at 69M of the 100M supply; the other 31M is reserved for the pool and only ever exists if the line is crossed — 69 + 31 = 100, locked, nowhere else for supply to be. Below the line, every escrowed satoshi refunds at the deadline: there is no partial launch to be bag-held. The {perAddress} XCP per-address cap means crossing takes at least {n} distinct addresses — more if people mint below the cap. A floor on the crowd, not a count.": "發售線位於 1 億供應量中的 6900 萬處;另外 3100 萬預留給流動性池,只有越過這條線才會存在 — 69 + 31 = 100,已鎖定,供應量無處可去。線以下,每一聰託管中的 XCP 都會在截止期退款:不存在讓人被套的半成品發射。每地址 {perAddress} XCP 的上限意味著越線至少需要 {n} 個不同地址 — 如果有人鑄造不滿上限,需要的地址更多。這是人群的下限,不是人數。", + "The service is busy or unavailable. Try again shortly.": "服務忙碌或無法使用。請稍後重試。", "The standard removes the rug pull and the premine. It does not remove risk, and we won't pretend otherwise:": "這套標準消除了跑路和預挖。它並不消除風險,我們也不會假裝它能:", "The timing clauses are the two deliberate inequalities. Consensus does not require a future start — a launch confirming late simply opens instantly — so the pre-announcement guarantee lives here: {startBlock} must exceed the confirmation block. Without that clause, a creator could broadcast a nominal 1,000-block sale late, confirm just before its own deadline, and run a near-instant insider mint behind thousand-block metadata. And on the fairminter row the window check relaxes to {lte} once closed because core rewrites the deadline on early sell-out — for closed launches this site restores exact equality from the immutable {event} event (see the trap in Reading state). Everything else is exact equality on the row.": "時間條款是兩處刻意的不等式。共識並不要求未來的起始 — 一個遲到確認的發射只是立即開啟而已 — 所以預公告的保證就在這裡:{startBlock} 必須大於確認區塊。沒有這一條款,建立者可以很晚才廣播一個名義上 1,000 區塊的發售,在自己的截止期前一刻確認,然後在千區塊後設資料的掩護下進行一次近乎即時的內部鑄造。另外,在公平鑄造合約記錄上,窗口期檢查在關閉後放寬為 {lte},因為 core 會在提前售罄時改寫截止期 — 對於已關閉的發射,本站會從不可變的 {event} 事件中恢復精確相等(參見「讀取狀態」中的陷阱)。其餘一切都是記錄上的精確相等。", + "The transaction did not match the verified request. It was blocked.": "交易與已驗證的請求不一致,已遭攔截。", "The whole standard, hands-on — starting, like every launch, with the mint.": "親手體驗整套標準 — 和每一次發射一樣,從鑄造開始。", "The XCP-69 standard": "XCP-69 標準", "There is no fee-split table because there are no fees to split. Every satoshi of XCP paid by minters goes into the pool. The only costs anywhere in the system are Bitcoin transaction fees, the 0.5 XCP asset-name registration fee for named assets, and the protocol's pooldeposit gas fee debited at creation — costs paid to the network, not to us or to the creator.": "沒有費用分成表,因為沒有費用可分。鑄造者支付的每一聰 XCP 都進入流動性池。整個系統中唯一的成本是比特幣礦工費、命名資產的 0.5 XCP 資產名稱註冊費,以及建立時扣除的協議 pooldeposit gas 費 — 這些都付給網路,而不是付給我們或建立者。", @@ -1128,6 +1141,8 @@ "Upcoming": "即將開始", "Upload failed": "上傳失敗", "Upload token image": "上傳代幣圖片", + "Use digits and a dot, with up to {n} decimal places.": "請使用半形數字和小數點,最多保留{n}位小數。", + "Use whole numbers only.": "請只輸入整數。", "Value": "價值", "Value and underlying amounts are what withdrawing the LP tokens now would return. XCP value is available when XCP is one side of the pair. LP profit and loss is not estimated.": "價值和底層數量指現在提取 LP 代幣能拿回的資產。當交易對一側是 XCP 時才有 XCP 價值。不估算 LP 的盈虧。", "Vends": "出售中", @@ -1212,6 +1227,7 @@ "Your position: {position} · {pct} of the pool": "你的倉位:{position} · 佔池 {pct}", "Your positions, history, and launches. Every graduated XCP-69 launch trades against XCP, so a position's value is a pool price away from a dollar figure.": "你的倉位、歷史和發射。每個畢業的 XCP-69 發射都以 XCP 計價交易,因此倉位價值只差一個池價就能換算成美元數字。", "Your profile": "我的主頁", + "Your wallet could not complete this request. Check the details.": "錢包無法完成此請求。請查看詳細資訊。", "Your wallet supplied a connection signature that didn't verify, so editing is locked for this session. Disconnect and reconnect to try again.": "你的錢包提供的連接簽名未通過驗證,本次會話已鎖定編輯。請斷開並重新連接後再試。", "Your wallet supplied a connection signature that didn't verify, so we can't confirm this address is really yours. Disconnect and reconnect to try again — nothing else on the site is affected.": "你的錢包提供的連接簽名未能通過驗證,因此我們無法確認這個地址真的屬於你。請斷開後重新連接再試 — 網站的其他功能不受影響。", "Your wallet will ask once per route, in order — each popup is one route, nothing more.": "錢包會按順序為每條路由請求一次確認 — 每個彈出視窗對應一條路由,僅此而已。", diff --git a/apps/web/src/locales/zh-tw.status.json b/apps/web/src/locales/zh-tw.status.json index 72fa2c47..8849dbb1 100644 --- a/apps/web/src/locales/zh-tw.status.json +++ b/apps/web/src/locales/zh-tw.status.json @@ -1206,6 +1206,22 @@ "Launch #{n}", "Positions, open mints, history, and launches for {address} on xcp.fun.", "Show amounts in {currency}", - "open@@bounty" + "open@@bounty", + "Check the amount, units, and decimal places.", + "Copy diagnostics", + "Could not refresh the quote. Try again.", + "Enter a multiple of {n}.", + "Enter a value from {min} to {max}.", + "Follow language", + "Number format", + "Quote changed or could not be refreshed. Review the amounts and try again.", + "Reconnect your wallet and try again.", + "Request declined in wallet.", + "Technical details", + "The service is busy or unavailable. Try again shortly.", + "The transaction did not match the verified request. It was blocked.", + "Use digits and a dot, with up to {n} decimal places.", + "Use whole numbers only.", + "Your wallet could not complete this request. Check the details." ] } diff --git a/apps/web/src/locales/zh.json b/apps/web/src/locales/zh.json index c93034ed..58e47434 100644 --- a/apps/web/src/locales/zh.json +++ b/apps/web/src/locales/zh.json @@ -362,6 +362,7 @@ "Chain:": "链:", "Change in {currency} over the last {window}, since the pool opened": "自流动性池开放以来,最近 {window} 的 {currency} 计价涨跌", "Change in {currency} over the last 24 hours": "最近 24 小时的 {currency} 计价涨跌", + "Check the amount, units, and decimal places.": "请检查数量、单位和小数位数。", "Checking availability…": "正在检查是否可用…", "Checking balance…": "正在查询余额…", "checking…": "检查中…", @@ -414,12 +415,14 @@ "Copied ✓": "已复制 ✓", "Copy address": "复制地址", "Copy as Markdown": "复制为 Markdown", + "Copy diagnostics": "复制诊断信息", "Copy failed": "复制失败", "Copy issuer address": "复制发行者地址", "Copy link": "复制链接", "Copy the link, or post it straight to X.": "复制链接,或直接发到 X。", "Core has no on-chain standard marker, so conformance is a predicate: exact equality against the standard's fixed raw values. This is the actual function this site runs — a launch either passes it or is not XCP-69.": "Core 没有链上的标准标记,因此符合性是一个判定式:与标准固定的原始值完全相等。这正是本站实际运行的函数 — 一次发射要么通过它,要么就不是 XCP-69。", "Corrections": "更正", + "Could not refresh the quote. Try again.": "无法刷新报价。请重试。", "Couldn't check the {give}/{get} pool. Quotes may still use the order book.": "无法查询 {give}/{get} 流动性池。报价仍可能来自订单簿。", "Couldn't confirm this signature. Updating your wallet and reconnecting usually sorts it.": "无法验证此签名。通常更新钱包并重新连接即可解决。", "Couldn't load page {n}. Showing the last page that loaded.": "无法加载第 {n} 页。当前显示的是最后一次成功加载的页面。", @@ -481,7 +484,9 @@ "Earned so far": "已获得", "Edit token info": "编辑代币信息", "Editing is locked for this session": "本次会话已锁定编辑", + "Enter a multiple of {n}.": "请输入{n}的倍数。", "Enter a price": "输入价格", + "Enter a value from {min} to {max}.": "请输入{min}到{max}之间的值。", "Enter an amount": "输入数量", "Enter opens the first result · Esc closes": "Enter 打开第一条结果 · Esc 关闭", "escrowed": "协议托管中", @@ -533,6 +538,7 @@ "Floor": "底价", "Floor price": "底价", "Floor: {n}": "底价:{n}", + "Follow language": "跟随语言", "Full 1M bags sold before yours": "排在你之前卖出的满额 1M 仓位", "Full specification with raw compose values and the conformance predicate: {spec}. Conformance is checked field-by-field on-chain — this site lists only launches that match exactly.": "包含原始 compose 值和符合性判定式的完整规范:{spec}。符合性在链上逐字段检查 — 本站只列出完全匹配的发射。", "full XCP refund, automatic": "XCP 全额退款,自动执行", @@ -803,6 +809,7 @@ "Nothing resting on the book right now.": "订单簿上当前没有挂单。", "Nothing to show.": "暂无内容。", "now": "现在", + "Number format": "数字格式", "Observed address behavior, not identity or intent. Not investment advice.": "观察到的是地址行为,而非身份或意图。不构成投资建议。", "of {n} in the batch": "本批次共 {n}", "of {n} minters, {pct}%": "{n} 位铸造者中,占 {pct}%", @@ -911,6 +918,7 @@ "Public sale": "公开发售", "public sale · 69M": "公开发售 · 6900 万", "Queue": "队列", + "Quote changed or could not be refreshed. Review the amounts and try again.": "报价已变更或无法刷新。请检查数量后重试。", "Quote moved — the numbers above are updated. Press again to swap at the new price.": "报价已变动 — 上方数字已更新。再按一次即按新价格兑换。", "Quote refresh countdown": "报价刷新倒计时", "Quote returns and prices in": "收益与价格的计价单位", @@ -928,6 +936,7 @@ "Realized PnL": "已实现盈亏", "Recently confirmed": "最近已确认", "Recommended": "推荐", + "Reconnect your wallet and try again.": "请重新连接钱包后重试。", "redeployed": "再投入", "Reference launch": "参考发射", "Refresh": "刷新", @@ -943,6 +952,7 @@ "Repeat dumpers": "重复砸盘者", "Replace image": "更换图片", "Represented": "涉及社区", + "Request declined in wallet.": "请求已在钱包中被拒绝。", "Resolve.": "结算。", "Retry": "重试", "Reward": "奖励", @@ -1041,6 +1051,7 @@ "Takes effect when it confirms. {tx}": "确认后生效。{tx}", "Target": "目标", "Target start block": "目标起始区块", + "Technical details": "技术详情", "Telegram": "Telegram", "Ten percent above the cheapest open dispenser": "比最便宜的开放售货机高 10%", "The {mult}× opening premium is structural, not a price guarantee.": "{mult}× 的开盘溢价是结构性的,不是价格保证。", @@ -1067,8 +1078,10 @@ "The per-address cap is sybil-resistant in cost only, not in principle.": "每地址上限只在成本上抗女巫攻击,在原理上并不能。", "The pool opens with 690 XCP against 31M tokens: 69/31 ≈ {mult}× the mint price. Every minter is structurally in profit at open, and the pool — not later buyers — absorbs early exits.": "流动性池以 690 XCP 对 3100 万代币开盘:69/31 ≈ 铸造价的 {mult}×。每位铸造者在开盘时从结构上就处于盈利,而吸收早期离场的是池子 — 不是后来的买家。", "The sale line sits at 69M of the 100M supply; the other 31M is reserved for the pool and only ever exists if the line is crossed — 69 + 31 = 100, locked, nowhere else for supply to be. Below the line, every escrowed satoshi refunds at the deadline: there is no partial launch to be bag-held. The {perAddress} XCP per-address cap means crossing takes at least {n} distinct addresses — more if people mint below the cap. A floor on the crowd, not a count.": "发售线位于 1 亿供应量中的 6900 万处;另外 3100 万预留给流动性池,只有越过这条线才会存在 — 69 + 31 = 100,已锁定,供应量无处可去。线以下,每一聪托管中的 XCP 都会在截止期退款:不存在让人被套的半成品发射。每地址 {perAddress} XCP 的上限意味着越线至少需要 {n} 个不同地址 — 如果有人铸造不满上限,需要的地址更多。这是人群的下限,不是人数。", + "The service is busy or unavailable. Try again shortly.": "服务繁忙或不可用。请稍后重试。", "The standard removes the rug pull and the premine. It does not remove risk, and we won't pretend otherwise:": "这套标准消除了跑路和预挖。它并不消除风险,我们也不会假装它能:", "The timing clauses are the two deliberate inequalities. Consensus does not require a future start — a launch confirming late simply opens instantly — so the pre-announcement guarantee lives here: {startBlock} must exceed the confirmation block. Without that clause, a creator could broadcast a nominal 1,000-block sale late, confirm just before its own deadline, and run a near-instant insider mint behind thousand-block metadata. And on the fairminter row the window check relaxes to {lte} once closed because core rewrites the deadline on early sell-out — for closed launches this site restores exact equality from the immutable {event} event (see the trap in Reading state). Everything else is exact equality on the row.": "时间条款是两处刻意的不等式。共识并不要求未来的起始 — 一个迟到确认的发射只是立即开启而已 — 所以预公告的保证就在这里:{startBlock} 必须大于确认区块。没有这一条款,创建者可以很晚才广播一个名义上 1,000 区块的发售,在自己的截止期前一刻确认,然后在千区块元数据的掩护下进行一次近乎即时的内部铸造。另外,在公平铸造合约记录上,窗口期检查在关闭后放宽为 {lte},因为 core 会在提前售罄时改写截止期 — 对于已关闭的发射,本站会从不可变的 {event} 事件中恢复精确相等(参见「读取状态」中的陷阱)。其余一切都是记录上的精确相等。", + "The transaction did not match the verified request. It was blocked.": "交易与已验证的请求不一致,已被拦截。", "The whole standard, hands-on — starting, like every launch, with the mint.": "亲手体验整套标准 — 和每一次发射一样,从铸造开始。", "The XCP-69 standard": "XCP-69 标准", "There is no fee-split table because there are no fees to split. Every satoshi of XCP paid by minters goes into the pool. The only costs anywhere in the system are Bitcoin transaction fees, the 0.5 XCP asset-name registration fee for named assets, and the protocol's pooldeposit gas fee debited at creation — costs paid to the network, not to us or to the creator.": "没有费用分成表,因为没有费用可分。铸造者支付的每一聪 XCP 都进入流动性池。整个系统中唯一的成本是比特币矿工费、命名资产的 0.5 XCP 资产名称注册费,以及创建时扣除的协议 pooldeposit gas 费 — 这些都付给网络,而不是付给我们或创建者。", @@ -1128,6 +1141,8 @@ "Upcoming": "即将开始", "Upload failed": "上传失败", "Upload token image": "上传代币图片", + "Use digits and a dot, with up to {n} decimal places.": "请使用半角数字和小数点,最多保留{n}位小数。", + "Use whole numbers only.": "请只输入整数。", "Value": "价值", "Value and underlying amounts are what withdrawing the LP tokens now would return. XCP value is available when XCP is one side of the pair. LP profit and loss is not estimated.": "价值和底层数量指现在提取 LP 代币能拿回的资产。当交易对一侧是 XCP 时才有 XCP 价值。不估算 LP 的盈亏。", "Vends": "出售中", @@ -1212,6 +1227,7 @@ "Your position: {position} · {pct} of the pool": "你的仓位:{position} · 占池 {pct}", "Your positions, history, and launches. Every graduated XCP-69 launch trades against XCP, so a position's value is a pool price away from a dollar figure.": "你的仓位、历史和发射。每个毕业的 XCP-69 发射都以 XCP 计价交易,因此仓位价值只差一个池价就能换算成美元数字。", "Your profile": "我的主页", + "Your wallet could not complete this request. Check the details.": "钱包无法完成此请求。请查看详情。", "Your wallet supplied a connection signature that didn't verify, so editing is locked for this session. Disconnect and reconnect to try again.": "你的钱包提供的连接签名未通过验证,本次会话已锁定编辑。请断开并重新连接后再试。", "Your wallet supplied a connection signature that didn't verify, so we can't confirm this address is really yours. Disconnect and reconnect to try again — nothing else on the site is affected.": "你的钱包提供的连接签名未能通过验证,因此我们无法确认这个地址真的属于你。请断开后重新连接再试 — 网站的其他功能不受影响。", "Your wallet will ask once per route, in order — each popup is one route, nothing more.": "钱包会按顺序为每条路由请求一次确认 — 每个弹窗对应一条路由,仅此而已。", diff --git a/apps/web/src/locales/zh.status.json b/apps/web/src/locales/zh.status.json index 4c75b7b4..44f3b5b5 100644 --- a/apps/web/src/locales/zh.status.json +++ b/apps/web/src/locales/zh.status.json @@ -1205,6 +1205,22 @@ "Launch #{n}", "Positions, open mints, history, and launches for {address} on xcp.fun.", "Show amounts in {currency}", - "open@@bounty" + "open@@bounty", + "Check the amount, units, and decimal places.", + "Copy diagnostics", + "Could not refresh the quote. Try again.", + "Enter a multiple of {n}.", + "Enter a value from {min} to {max}.", + "Follow language", + "Number format", + "Quote changed or could not be refreshed. Review the amounts and try again.", + "Reconnect your wallet and try again.", + "Request declined in wallet.", + "Technical details", + "The service is busy or unavailable. Try again shortly.", + "The transaction did not match the verified request. It was blocked.", + "Use digits and a dot, with up to {n} decimal places.", + "Use whole numbers only.", + "Your wallet could not complete this request. Check the details." ] } diff --git a/docs/amount-and-localization-contract.md b/docs/amount-and-localization-contract.md new file mode 100644 index 00000000..9f432273 --- /dev/null +++ b/docs/amount-and-localization-contract.md @@ -0,0 +1,45 @@ +# Amount entry and localization contract + +The interface language, display number format, and fiat currency are independent preferences. Number format follows the selected language until explicitly overridden. USD remains selectable in every language. Formatting helpers are presentation-only and must never feed editable values or compose parameters. + +## Exact inputs and Core fields + +The application pins SDK commit `79f190308d4b60cc3ec9023beada21e0ee776213` from [wallet-sdk PR 1](https://github.com/XCP/wallet-sdk/pull/1). This is an immutable review dependency, not a published SDK release. The pure `/amounts` contract and shared vectors are maintained there. + +- Preserve the whole draft, including malformed sequential edits. `-5`, `1e5`, `0,5`, grouping, and a ninth decimal cannot become another valid amount. Pasted line breaks remain visibly escaped rather than concatenating digits. +- Parse canonical ASCII digits and a period only. A field is empty, incomplete, invalid, or valid; only valid drafts yield a raw integer. Do not submit a previous valid value underneath an invalid draft. +- Core raw asset quantities are integers. Divisible assets use eight places; indivisible assets use zero. Asset identity and divisibility belong to each leg independently. XCP-69 launches in these trading forms are divisible; that does not justify assuming eight places in generic order readers. +- Fees are fractional sat/vB values, not raw asset amounts. Block counts, commission fractions, LP quantities, and text fields have separate contracts. Empty custom fee means automatic; an explicit zero is not replaced with automatic. Slippage and fee fields reject unsupported values instead of clamping them into another intent. +- Max, percentages, pair reversal, and derived limit-order legs use exact integer arithmetic. When price creates a derived quantity, rounding preserves the typed leg and the user's price constraint; it is not uniformly downward. +- Final quote refresh must succeed for the same amount, pair, account, and settings. A worse refresh cannot silently reduce a reviewed minimum. Changed deposit counterpart quantities require review again. Quote failures cannot become zero fees or balances. + +Core source was inspected at `67e10db3`, including API compose/query normalization, order matching, fairmint/fairminter, pool deposit/withdraw, asset divisibility and fee handling. Core's `int(fraction * 1e8)` can alter particular floating-point commission fractions; the SDK rejects values that cannot survive that conversion exactly rather than adjusting them silently. + +The SDK checks compose transaction envelopes and signed transaction invariants. It does not independently decode and compare every Counterparty message against the requested intent for every hosted wallet adapter. The extension's native composer has additional semantic verification. Neither capability should be described as universal payload verification. + +The direct dispenser router also uses the SDK's field serializers. It checks reviewed recipient/payment outputs, rejects changed signed transaction envelopes, and rereads vend size, price, oracle identity and stock before signing and broadcasting, including retries. That does not guarantee dispenser availability when a later block is mined, or independently reconstruct every input's parent value/network fee in this router. + +The inscription Create flow uses Core's actual `/utxos//balances` endpoint. Only a successful complete empty result authorizes a candidate as having no confirmed Counterparty balances; failed or ambiguous reads block the operation. The selected prevout is checked against its hash-verified parent bytes, amount and source script, and balances are reread after commit signing. Commit/reveal signed PSBTs and finalized raw transactions must retain their original Bitcoin envelopes. These checks cover confirmed Counterparty state, not an inventory of every Bitcoin metaprotocol. A rejected reveal stops the second broadcast; it cannot undo an already-broadcast commit. + +## Preferences and terminology + +The separate number-format picker includes German and Spanish regional number conventions, including Spain and Venezuela. The Spanish message catalog remains shared until reviewed regional vocabulary warrants overrides. Chinese script and regional catalogs remain distinct. This change does not claim that model-authored text was reviewed by native speakers. + +Current fiat estimates can follow the selected currency. Historical price charts remain explicitly USD until dated FX conversion is available; applying today's CNY rate to an old USD price would misstate historical CNY pricing. Number separators remain independently configurable on those USD charts. + +Critical errors use stable codes and localized instructions, with copyable original diagnostic details. Reviewed catalog entries keep their provenance; new model-authored entries are marked as such. Source-key coverage is a completeness check, not a translation-quality assessment. + +Verified terminology references, reviewed 7 September 2026: + +- [Counterwallet](https://github.com/CounterpartyXCP/counterwallet) documents its historical Transifex translation workflow. Its archived source is terminology/history, while current Core defines transaction semantics. +- [Zaif order-book trading](https://zaif.jp/doc_orderbook_trading) and [fees](https://zaif.jp/fee?lang=ja) inform Japanese trading vocabulary such as 指値, 成行, and 手数料. Zaif's execution mechanism is not Counterparty's order contract. +- [Uniswap's Spanish AMM explanation](https://blog.uniswap.org/es-ES/what-is-an-automated-market-maker) informs familiar liquidity and slippage vocabulary. Its Spanish regional tag alone does not establish that a different wallet noun is required in every region. +- [UniSat Wallet API](https://docs.unisat.io/dev/unisat-developer-center/unisat-wallet) and [transfer inscription guide](https://docs.unisat.io/services/products/unisat-wallet/how-to-inscribe-transfer-using-unisat-wallet) inform Bitcoin metaprotocol vocabulary. BRC-20 transferable balances and inscriptions must not be imported as Counterparty transaction semantics. + +## Validation and remaining work + +Tests exercise actual controlled input handlers, strict compose HTTP serialization, quote failures and changed intent, exact quantities above JavaScript's safe integer range, per-field settings, and language/number/currency invariance in rendered React components. Inscription fixtures cryptographically sign the production commit/reveal builders offline and prove changed envelopes stop broadcast. Actual Chromium verifies sequential invalid edits, exact large quote parameters, and independent saved number/fiat preferences. No live transaction was signed or broadcast during validation. + +Changing number or currency preferences preserves the current draft. Launchpad language changes navigate to a different localized URL and currently clear the form; preserving drafts across that navigation remains a UX follow-up. Extension language changes have a separate in-place implementation. + +Before release: publish/review the SDK dependency, review transaction-critical translations with regional speakers, and validate supported wallet adapters against the node versions being deployed. Merging, SDK release, website deployment, and extension store publication are separate actions. diff --git a/package-lock.json b/package-lock.json index 9eb3b8d0..14c212ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "devDependencies": { "@scure/bip32": "^2.3.0", "esbuild": "0.28.2", + "happy-dom": "20.14.0", "vitest": "^4.1.10" } }, @@ -38,7 +39,7 @@ "@noble/hashes": "^1.8.0", "@scure/base": "^2.2.0", "@scure/btc-signer": "^2.2.0", - "@xcp/wallet-sdk": "github:XCP/wallet-sdk#v0.4.1", + "@xcp/wallet-sdk": "github:XCP/wallet-sdk#79f190308d4b60cc3ec9023beada21e0ee776213", "babel-plugin-react-compiler": "^1.0.0", "bignumber.js": "^9.3.1", "next": "16.3.4", @@ -7045,6 +7046,23 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.67.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz", @@ -7784,7 +7802,8 @@ }, "node_modules/@xcp/wallet-sdk": { "version": "0.1.0", - "resolved": "git+ssh://git@github.com/XCP/wallet-sdk.git#13bddd2f7c6c84d52424993204a883e50a91ed5e", + "resolved": "git+ssh://git@github.com/XCP/wallet-sdk.git#79f190308d4b60cc3ec9023beada21e0ee776213", + "integrity": "sha512-rsZTm1BSwlGE0wvAjH5J7GxTpdRbu45cDFZaFoDExrWwqaDmO/qXNwybNwIbfH5bEr50yXC5SZkmb9tVBW3lhg==", "license": "MIT", "dependencies": { "@noble/curves": "^1.9.7", @@ -8349,6 +8368,19 @@ "dev": true, "license": "MIT" }, + "node_modules/buffer-image-size": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz", + "integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -9042,6 +9074,19 @@ "node": ">=8.6" } }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-stack-parser-es": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", @@ -10560,6 +10605,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/happy-dom": { + "version": "20.14.0", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.14.0.tgz", + "integrity": "sha512-4bRh1KzRvKDnFNTlLhzT1RZTpkKhQbQDl9j+7GXszWsvuspYdo29k6OHRf4PwiM6oLb8r/pMWeYiJjkfod5AvQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": ">=20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "@types/ws": "^8.18.1", + "buffer-image-size": "^0.6.4", + "entities": "^7.0.1", + "whatwg-mimetype": "^3.0.0", + "ws": "^8.21.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -15173,6 +15238,16 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", diff --git a/package.json b/package.json index 10778ee8..274f6941 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "devDependencies": { "@scure/bip32": "^2.3.0", "esbuild": "0.28.2", + "happy-dom": "20.14.0", "vitest": "^4.1.10" } } diff --git a/tests/compose-amount-safety.test.ts b/tests/compose-amount-safety.test.ts deleted file mode 100644 index 7bd264e2..00000000 --- a/tests/compose-amount-safety.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { composeAndBroadcast } from "@xcp/wallet-sdk"; -import { parseUnitsToRaw } from "../apps/web/src/lib/numeric"; -import { AmountInput, sanitizeAmount } from "../apps/web/src/components/amount-input"; - -/** - * Contract verified against local Counterparty Core 67e10db3: - * api/apiserver.py prepare_args parses quantities with int(str_arg). - * api/compose.py declares order/fairmint/pool quantities as raw integers; - * config.UNIT is 100_000_000. sat_per_vbyte is a separate float. - * These tests reach the installed wallet SDK's actual compose query builder. - * The mocked HTTP error stops before signing or broadcasting anything. - */ -afterEach(() => { vi.unstubAllGlobals(); }); - -function enter(value: string, inputType = "insertText", previous = "", decimals: 0 | 8 = 8, data = value.slice(-1)) { - const onChange = vi.fn(); - const input = AmountInput({ value: previous, onChange, decimals }); - input.props.onChange({ target: { value }, nativeEvent: { inputType, data } }); - return { value: onChange.mock.calls[0]?.[0] as string | undefined, onChange }; -} - -function typeAmount(typed: string, decimals: 0 | 8 = 8): string { - let value = ""; - for (const char of typed) { - const next = enter(value + char, "insertText", value, decimals); - expect(next.onChange, `keystroke ${JSON.stringify(char)} after ${value}`).toHaveBeenCalledOnce(); - value = next.value!; - } - return value; -} - -describe("an accepted amount retains its value", () => { - it.each([ - ["0.001", 100_000n], - ["1.234", 123_400_000n], - ["0.00000001", 1n], - ["100000000.00000001", 10_000_000_000_000_001n], - ] as const)("types %s without treating its third decimal as grouping", (typed, raw) => { - expect(typeAmount(typed)).toBe(typed); - expect(parseUnitsToRaw(typeAmount(typed))).toBe(raw); - }); - - it("normalizes a decimal keyboard comma before it reaches state", () => { - expect(typeAmount("0,001")).toBe("0.001"); - expect(parseUnitsToRaw(typeAmount("0,00000001"))).toBe(1n); - expect(typeAmount("1,234")).toBe("1.234"); - expect(enter("1,234", "insertText", "", 8, "1,234").onChange).not.toHaveBeenCalled(); - }); - - it("keeps editable decimal states", () => { - expect(sanitizeAmount("")).toBe(""); - expect(sanitizeAmount(".")).toBe("0."); - expect(sanitizeAmount("5.")).toBe("5."); - expect(sanitizeAmount(".5")).toBe(".5"); - }); - - it.each([ - "1,234", "1,234.56", "1.234,56", "1.234.567", "1,234,567", - "1 234.56", "1\u00a0234,56", "1\u202f234,56", "0,5", " 1.5 ", - "-1", "+1", "1e5", "1e-8", "NaN", "Infinity", "$12", "12 XCP", "1.2.3", "1.5", "١.٥", - ])("rejects ambiguous or unsupported paste %s", (text) => { - expect(sanitizeAmount(text)).toBeNull(); - for (const inputType of ["insertFromPaste", "insertFromDrop"]) { - expect(enter(text, inputType, "2").onChange).not.toHaveBeenCalled(); - } - }); - - it("accepts exact plain paste and rejects a ninth decimal", () => { - expect(enter("100000000.00000001", "insertFromPaste").value).toBe("100000000.00000001"); - expect(enter("0.000000001", "insertText", "0.00000000").onChange).not.toHaveBeenCalled(); - expect(sanitizeAmount("1.999999999")).toBeNull(); - }); - - it("rejects an overlong paste as a whole instead of letting the browser truncate its value", () => { - const input = AmountInput({ value: "2", onChange: vi.fn() }); - expect(input.props.maxLength).toBeUndefined(); - const text = `${"0".repeat(26)}1`; - expect(parseUnitsToRaw(text)).toBe(100_000_000n); - expect(enter(text, "insertFromPaste", "2").onChange).not.toHaveBeenCalled(); - }); - - it.each(["1\n234", "1\r234", "1\r\n234", "1,234", "1.234,56", "1e5"])( - "rejects the original clipboard/drop text %j before the browser can sanitize it", (text) => { - const input = AmountInput({ value: "2", onChange: vi.fn() }); - const preventPaste = vi.fn(); - const preventDrop = vi.fn(); - input.props.onPaste({ clipboardData: { getData: () => text }, preventDefault: preventPaste }); - input.props.onDrop({ dataTransfer: { getData: () => text }, preventDefault: preventDrop }); - expect(preventPaste).toHaveBeenCalledOnce(); - expect(preventDrop).toHaveBeenCalledOnce(); - }, - ); - - it("lets a plain decimal paste reach the normal exact-input validation", () => { - const input = AmountInput({ value: "", onChange: vi.fn() }); - const preventDefault = vi.fn(); - input.props.onPaste({ clipboardData: { getData: () => "0.00000001" }, preventDefault }); - expect(preventDefault).not.toHaveBeenCalled(); - expect(enter("0.00000001", "insertFromPaste").value).toBe("0.00000001"); - }); - - it("keeps raw satoshi fields integral", () => { - expect(typeAmount("1234", 0)).toBe("1234"); - expect(parseUnitsToRaw(typeAmount("1234", 0), 0)).toBe(1234n); - expect(sanitizeAmount("1.5", 0)).toBeNull(); - expect(enter("1,", "insertText", "1", 0).onChange).not.toHaveBeenCalled(); - }); -}); - -describe("the actual compose HTTP parameters", () => { - it.each([ - ["0.001", "100000"], - ["0.00000001", "1"], - ["100000000.00000001", "10000000000000001"], - ])("sends the exact raw digits for %s, with fractional fees separate", async (typed, expected) => { - const fetch = vi.fn(async () => new Response(JSON.stringify({ error: "test boundary reached" }), { status: 400 })); - vi.stubGlobal("fetch", fetch); - const signer = { - address: "1CounterpartyXXXXXXXXXXXXXXXUWLpVr", - signTransaction: vi.fn(), - broadcastTransaction: vi.fn(), - } as unknown as Parameters[0]; - await expect(composeAndBroadcast(signer, "order", { - give_asset: "XCP", - give_quantity: parseUnitsToRaw(typeAmount(typed))!, - get_asset: "TOKEN", - get_quantity: 1n, - expiration: 100, - fee_required: 0, - }, { feeRate: 0.1 })).rejects.toThrow("test boundary reached"); - expect(fetch).toHaveBeenCalledOnce(); - const url = new URL(String(fetch.mock.calls[0]![0])); - expect(url.pathname).toContain("/compose/order"); - expect(url.searchParams.get("give_quantity")).toBe(expected); - expect(url.searchParams.get("get_quantity")).toBe("1"); - expect(url.searchParams.get("sat_per_vbyte")).toBe("0.1"); - expect(signer.signTransaction).not.toHaveBeenCalled(); - expect(signer.broadcastTransaction).not.toHaveBeenCalled(); - }); -}); diff --git a/tests/compose-amount-safety.test.tsx b/tests/compose-amount-safety.test.tsx new file mode 100644 index 00000000..088b704f --- /dev/null +++ b/tests/compose-amount-safety.test.tsx @@ -0,0 +1,140 @@ +// @vitest-environment happy-dom +import { act, useState } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { composeAndBroadcast } from "@xcp/wallet-sdk"; +import { parseAmountDraft, type DecimalPlaces } from "@xcp/wallet-sdk/amounts"; +import vectors from "@xcp/wallet-sdk/amounts/vectors"; +import { AmountInput } from "../apps/web/src/components/amount-input"; +import { parseAmountRaw, visibleDraft } from "../apps/web/src/lib/amount-draft"; + +let root: Root | undefined; +let container: HTMLDivElement; +globalThis.IS_REACT_ACT_ENVIRONMENT = true; +afterEach(async () => { + if (root) await act(() => root!.unmount()); + root = undefined; + container?.remove(); + vi.unstubAllGlobals(); +}); + +async function mount(decimals: 0 | 8 = 8, onSubmit = vi.fn(), initial = "") { + function Form() { + const [draft, setDraft] = useState(initial); + const raw = parseAmountRaw(draft, decimals); + return
{ event.preventDefault(); if (raw !== null && raw > 0n) onSubmit(raw); }}> + + + ; + } + container = document.createElement("div"); + document.body.append(container); + root = createRoot(container); + await act(() => root!.render(
)); + return { input: container.querySelector("input")!, button: container.querySelector("button")!, onSubmit }; +} + +async function enter(input: HTMLInputElement, value: string, data: string | null = null) { + await act(() => { + Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!.call(input, value); + input.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data })); + }); +} +async function type(input: HTMLInputElement, text: string) { + for (const char of text) await enter(input, input.value + char, char); +} +async function paste(input: HTMLInputElement, text: string) { + input.setSelectionRange(0, input.value.length); + const event = new Event("paste", { bubbles: true, cancelable: true }); + Object.defineProperty(event, "clipboardData", { value: { getData: () => text } }); + await act(() => { input.dispatchEvent(event); }); +} + +describe("actual amount editing and submit boundary", () => { + it.each(["-5", "+5", "1e5", "0,5", "1,234", "1.2.3", "0.000000001"])( + "keeps sequential %s invalid instead of composing another amount", async (draft) => { + const { input, button, onSubmit } = await mount(); + await type(input, draft); + expect(input.value).toBe(draft); + expect(input.getAttribute("aria-invalid")).toBe("true"); + expect(button.disabled).toBe(true); + await act(() => { button.click(); }); + expect(onSubmit).not.toHaveBeenCalled(); + }, + ); + it("does not reuse the last valid quantity after an invalid edit", async () => { + const { input, button, onSubmit } = await mount(8, vi.fn(), "5"); + expect(button.disabled).toBe(false); + await type(input, "e2"); + expect(input.value).toBe("5e2"); + expect(button.disabled).toBe(true); + await act(() => { container.querySelector("form")!.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true })); }); + expect(onSubmit).not.toHaveBeenCalled(); + await enter(input, "5"); + await act(() => { button.click(); }); + expect(onSubmit).toHaveBeenCalledWith(500_000_000n); + }); + it("keeps an indivisible decimal draft intact and disabled", async () => { + const { input, button } = await mount(0); + await type(input, "0.5"); + expect(input.value).toBe("0.5"); + expect(button.disabled).toBe(true); + await enter(input, "100"); + expect(button.disabled).toBe(false); + expect(parseAmountRaw(input.value, 0)).toBe(100n); + }); + it.each(["1,234", "1\n234", "1\r\n234", "1\t234", "1.5", "12 XCP", "0.000000001", "1".repeat(129)])( + "keeps pasted %j invalid, including before native text cleanup", async (text) => { + const { input, button } = await mount(8, vi.fn(), "5"); + await paste(input, text); + expect(input.value).toBe(visibleDraft(text)); + expect(button.disabled).toBe(true); + expect(input.hasAttribute("maxlength")).toBe(false); + }, + ); + it("preserves dropped control text as a visible invalid draft", async () => { + const { input, button } = await mount(8, vi.fn(), "5"); + const event = new Event("drop", { bubbles: true, cancelable: true }); + Object.defineProperty(event, "dataTransfer", { value: { getData: () => "1\n234" } }); + await act(() => { input.dispatchEvent(event); }); + expect(input.value).toBe("1\\n234"); + expect(button.disabled).toBe(true); + }); + it.each(vectors.editing.filter((item) => item.decimals === 0 || item.decimals === 8))( + "replays shared editing vector $id through the rendered input", async (vector) => { + const { input, button } = await mount(vector.decimals as 0 | 8); + for (let index = 0; index < vector.drafts.length; index++) { + const draft = vector.drafts[index]!; + await enter(input, draft); + expect(input.value).toBe(draft); + const parsed = parseAmountDraft(input.value, { decimals: vector.decimals as DecimalPlaces }); + expect(parsed.status).toBe(vector.statuses[index]); + expect(button.disabled).toBe(parsed.status !== "valid" || parsed.raw === 0n); + } + }, + ); +}); + +describe("real SDK compose HTTP parameters", () => { + it.each([ + ["0.001", "100000"], ["0.00000001", "1"], ["100000000.00000001", "10000000000000001"], + ])("sends precisely the accepted %s and a separate fractional fee", async (draft, expected) => { + const fetch = vi.fn(async () => new Response(JSON.stringify({ error: "test boundary reached" }), { status: 400 })); + vi.stubGlobal("fetch", fetch); + const signer = { address: "1CounterpartyXXXXXXXXXXXXXXXUWLpVr", signTransaction: vi.fn(), broadcastTransaction: vi.fn() } as unknown as Parameters[0]; + const { input, button, onSubmit } = await mount(); + await type(input, draft); + await act(() => { button.click(); }); + const raw = onSubmit.mock.calls[0]![0] as bigint; + await expect(composeAndBroadcast(signer, "order", { + give_asset: "XCP", give_quantity: raw, get_asset: "TOKEN", get_quantity: 1n, expiration: 100, fee_required: 0, + }, { feeRate: 0.1 })).rejects.toThrow("test boundary reached"); + const url = new URL(String(fetch.mock.calls[0]![0])); + expect(url.pathname).toContain("/compose/order"); + expect(url.searchParams.get("give_quantity")).toBe(expected); + expect(url.searchParams.get("get_quantity")).toBe("1"); + expect(url.searchParams.get("sat_per_vbyte")).toBe("0.1"); + expect(signer.signTransaction).not.toHaveBeenCalled(); + expect(signer.broadcastTransaction).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/dispense-router-intent.test.tsx b/tests/dispense-router-intent.test.tsx new file mode 100644 index 00000000..e13f2c29 --- /dev/null +++ b/tests/dispense-router-intent.test.tsx @@ -0,0 +1,110 @@ +// @vitest-environment happy-dom +import { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { hex } from "@scure/base"; +import { Transaction } from "@scure/btc-signer"; +import { addressScriptPubKey, invalidatePending, readPending } from "@xcp/wallet-sdk"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { useDispenseRouter } from "../apps/web/src/app/[lang]/dispense/_lib/use-dispense-router"; + +const SOURCE = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"; +const DESTINATION = "1BoatSLRHtKNngkdXEeobR76b53LETtpyT"; +const boundary = vi.hoisted(() => ({ + sign: vi.fn(), broadcast: vi.fn(), upstream: vi.fn(), utxos: vi.fn(), + address: "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", +})); +vi.mock("@xcp/wallet-sdk", async original => ({ + ...await original(), + relayingFetch: boundary.upstream, + withAddressTransactionLock: async (_address: string, fn: () => Promise) => fn(), +})); +vi.mock("@/lib/esplora", () => ({ fetchAddressUtxos: boundary.utxos })); +vi.mock("@/lib/analytics", () => ({ trackTx: vi.fn() })); +vi.mock("@/lib/wallet/wallet-context", () => ({ useWallet: () => ({ + address: boundary.address, signTransaction: boundary.sign, broadcastTransaction: boundary.broadcast, +}) })); + +const leg = { units: 1, btcSats: 1000, dispenser: { + tx_hash: "a".repeat(64), source: DESTINATION, give_quantity: 100000000, + give_remaining: 1000000000, satoshirate: 1000, price: 1000, +} }; +let live: typeof leg.dispenser & { asset: string; status: number; oracle_address: null }; +function raw(payment = 1000n) { + const tx = new Transaction({ allowUnknownInputs: true, allowUnknownOutputs: true }); + tx.addInput({ txid: "11".repeat(32), index: 0 }); + tx.addOutput({ amount: payment, script: hex.decode(addressScriptPubKey(DESTINATION)) }); + tx.addOutput({ amount: 8000n, script: hex.decode(addressScriptPubKey(SOURCE)) }); + return hex.encode(tx.toBytes(true, false)); +} +let router: ReturnType; +function Harness() { router = useDispenseRouter(100000); return {router.phase}; } +let root: Root; +let container: HTMLDivElement; +globalThis.IS_REACT_ACT_ENVIRONMENT = true; +beforeEach(async () => { + vi.clearAllMocks(); + boundary.address = SOURCE; + localStorage.clear(); invalidatePending(); + live = { ...leg.dispenser, asset: "XCP", status: 0, oracle_address: null }; + boundary.utxos.mockResolvedValue([{ txid: "11".repeat(32), vout: 0, value: 10000, status: { confirmed: true } }]); + boundary.upstream.mockImplementation(async (url: string) => url.includes("/compose/") + ? Response.json({ result: { rawtransaction: raw() } }) + : Response.json({ result: [live] })); + boundary.sign.mockImplementation(async (unsigned: string) => unsigned); + boundary.broadcast.mockResolvedValue("broadcast-tx"); + container = document.createElement("div"); document.body.append(container); root = createRoot(container); + await act(() => root.render()); +}); +afterEach(async () => { await act(() => root.unmount()); container.remove(); }); + +describe("actual dispenser router signing and retry boundary", () => { + it("checks changed vend size before re-signing a cached transaction", async () => { + boundary.sign.mockRejectedValueOnce(new Error("User rejected")); + await act(async () => { await router.start([leg], 0.1); }); + expect(router.phase).toBe("partial"); expect(router.legs[0]?.rawHex).toBe(raw()); + live.give_quantity = 50000000; + await act(async () => { router.retry(0); }); + expect(boundary.sign).toHaveBeenCalledTimes(1); + expect(boundary.upstream.mock.calls.filter(([url]) => url.includes("/compose/"))).toHaveLength(1); + expect(boundary.broadcast).not.toHaveBeenCalled(); + expect(router.legs[0]?.error).toContain("route price changed"); + }); + + it("does not broadcast when dispenser terms move while the wallet is signing", async () => { + boundary.sign.mockImplementation(async (unsigned: string) => { live.satoshirate = 2000; return unsigned; }); + await act(async () => { await router.start([leg], 1.56); }); + expect(boundary.sign).toHaveBeenCalledTimes(1); + expect(boundary.broadcast).not.toHaveBeenCalled(); + expect(router.phase).toBe("partial"); + expect(readPending()).toHaveLength(0); + }); + + it("does not broadcast a changed payment returned by the wallet", async () => { + boundary.sign.mockResolvedValue(raw(999n)); + await act(async () => { await router.start([leg], 0.1); }); + expect(boundary.broadcast).not.toHaveBeenCalled(); + expect(router.legs[0]?.error).toContain("Transaction inputs, outputs or amounts changed"); + }); + + it("checks the active account again before broadcasting", async () => { + let resolve!: (value: string) => void; + boundary.sign.mockImplementation(() => new Promise(done => { resolve = done; })); + let pending!: Promise; + await act(async () => { pending = router.start([leg], 0.1); }); + expect(boundary.sign).toHaveBeenCalledTimes(1); + boundary.address = DESTINATION; + await act(() => root.render()); + await act(async () => { resolve(raw()); await pending; }); + expect(boundary.broadcast).not.toHaveBeenCalled(); + expect(router.legs[0]?.error).toContain("Wallet address changed"); + }); + + it("completes the unchanged reviewed payment and registers it once", async () => { + await act(async () => { await router.start([leg], 0.1); }); + expect(boundary.broadcast).toHaveBeenCalledWith(raw()); + expect(boundary.upstream.mock.calls.filter(([url]) => url.endsWith("/dispensers"))).toHaveLength(2); + expect(router.phase).toBe("done"); + expect(readPending()).toHaveLength(1); + expect(readPending()[0]).toMatchObject({ txid: "broadcast-tx", address: SOURCE, kind: "dispense" }); + }); +}); diff --git a/tests/dispense-router.test.ts b/tests/dispense-router.test.ts index fd2c25f3..ce4ceb3b 100644 --- a/tests/dispense-router.test.ts +++ b/tests/dispense-router.test.ts @@ -6,6 +6,28 @@ */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { COUNTERPARTY_API_BASE } from "@/lib/constants"; +import { hex, base64 } from "@scure/base"; +import { Transaction } from "@scure/btc-signer"; +import { addressScriptPubKey } from "@xcp/wallet-sdk"; +import { makeT } from "@/lib/i18n/t"; + +const SOURCE = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"; +const DESTINATION = "1BoatSLRHtKNngkdXEeobR76b53LETtpyT"; +const OTHER = "1CounterpartyXXXXXXXXXXXXXXXUWLpVr"; +const t = makeT({}); +const leg = { units: 1, btcSats: 1000, dispenser: { + tx_hash: "a".repeat(64), source: DESTINATION, give_quantity: 100000000, + give_remaining: 1000000000, satoshirate: 1000, price: 1000, +} }; +function transaction(payment = 1000n, destination = DESTINATION, change = SOURCE) { + const tx = new Transaction({ allowUnknownInputs: true, allowUnknownOutputs: true }); + tx.addInput({ txid: "11".repeat(32), index: 0, sequence: 0xfffffffd }); + tx.addOutput({ amount: payment, script: hex.decode(addressScriptPubKey(destination)) }); + tx.addOutput({ amount: 8000n, script: hex.decode(addressScriptPubKey(change)) }); + tx.addOutput({ amount: 0n, script: new Uint8Array([0x6a, 0x01, 0x00]) }); + return tx; +} +const raw = (tx = transaction()) => hex.encode(tx.toBytes(true, false)); function installStorage() { const store = new Map(); @@ -29,32 +51,94 @@ describe("Buy XCP compose", () => { vi.stubGlobal("fetch", (async (url: string | URL) => { calls.push(String(url)); if (calls.length === 1) throw new TypeError("Failed to fetch"); - return Response.json({ result: { rawtransaction: "deadbeef" } }); + return Response.json({ result: { rawtransaction: raw() } }); }) as unknown as typeof fetch); const { composeLeg } = await import( "@/app/[lang]/dispense/_lib/use-dispense-router" ); - const raw = await composeLeg( - "bc1qnativewitnessaddress", - { - units: 1, - btcSats: 1_000, - dispenser: { - tx_hash: "a".repeat(64), - source: "1CounterpartyXXXXXXXXXXXXXXXUWLpVr", - give_quantity: 100_000_000, - give_remaining: 100_000_000, - satoshirate: 1_000, - price: 1_000, - }, - }, + const result = await composeLeg( + SOURCE, + leg, 1, {}, + t, ); - expect(raw).toBe("deadbeef"); - expect(calls[0]).toContain(`${COUNTERPARTY_API_BASE}/addresses/bc1q`); - expect(calls[1]).toContain("/api/cp/v2/addresses/bc1q"); + expect(result).toBe(raw()); + expect(calls[0]).toContain(`${COUNTERPARTY_API_BASE}/addresses/${SOURCE}`); + expect(calls[1]).toContain(`/api/cp/v2/addresses/${SOURCE}`); + }); + + it.each([0.1, 1.56])("preserves exact satoshi quantity and fractional fee %s", async fee => { + const calls: string[] = []; + vi.stubGlobal("fetch", vi.fn(async (url: string) => { + calls.push(url); return Response.json({ result: { rawtransaction: raw() } }); + })); + const { composeLeg } = await import("@/app/[lang]/dispense/_lib/use-dispense-router"); + await composeLeg(SOURCE, leg, fee, { inputsSet: `${"11".repeat(32)}:0`, allowUnconfirmed: true }, t); + const query = new URL(calls[0]!).searchParams; + expect(query.get("quantity")).toBe("1000"); + expect(query.get("sat_per_vbyte")).toBe(String(fee)); + expect(query.get("allow_unconfirmed_inputs")).toBe("true"); + expect(query.get("inputs_set")).toBe(`${"11".repeat(32)}:0`); + }); + + it.each(["1e3", "1,000", "-1", "1.5", Number.MAX_SAFE_INTEGER + 1, Infinity])("rejects invalid direct payment %s before network", async value => { + const fetch = vi.fn(); vi.stubGlobal("fetch", fetch); + const { composeLeg } = await import("@/app/[lang]/dispense/_lib/use-dispense-router"); + await expect(composeLeg(SOURCE, { ...leg, btcSats: value as number }, 1, {}, t)).rejects.toThrow(); + expect(fetch).not.toHaveBeenCalled(); + }); + it.each([NaN, Infinity, -0.1])("rejects invalid fee %s before network", async fee => { + const fetch = vi.fn(); vi.stubGlobal("fetch", fetch); + const { composeLeg } = await import("@/app/[lang]/dispense/_lib/use-dispense-router"); + await expect(composeLeg(SOURCE, leg, fee, {}, t)).rejects.toThrow(); + expect(fetch).not.toHaveBeenCalled(); + }); + + it.each([ + ["changed payment", () => raw(transaction(1001n))], + ["changed recipient", () => raw(transaction(1000n, OTHER))], + ["redirected change", () => raw(transaction(1000n, DESTINATION, OTHER))], + ["malformed transaction", () => "deadbeef"], + ] as const)("refuses Core's %s before signing", async (_name, fixture) => { + vi.stubGlobal("fetch", vi.fn(async () => Response.json({ result: { rawtransaction: fixture() } }))); + const { composeLeg } = await import("@/app/[lang]/dispense/_lib/use-dispense-router"); + await expect(composeLeg(SOURCE, leg, 1, {}, t)).rejects.toThrow(); + }); + + it("refuses an accompanying PSBT for a different payment", async () => { + vi.stubGlobal("fetch", vi.fn(async () => Response.json({ result: { + rawtransaction: raw(), psbt: base64.encode(transaction(2000n).toPSBT()), + } }))); + const { composeLeg } = await import("@/app/[lang]/dispense/_lib/use-dispense-router"); + await expect(composeLeg(SOURCE, leg, 1, {}, t)).rejects.toMatchObject({ code: "transaction_mismatch" }); + }); + + it("allows signature bytes but refuses changed wallet outputs and sequences", async () => { + const { signDispenseLeg } = await import("@/app/[lang]/dispense/_lib/use-dispense-router"); + const signed = transaction(); signed.updateInput(0, { finalScriptSig: new Uint8Array([0x01, 0x01]) }); + await expect(signDispenseLeg(raw(), SOURCE, leg, async () => raw(signed))).resolves.toBe(raw(signed)); + await expect(signDispenseLeg(raw(), SOURCE, leg, async () => raw(transaction(1001n)))).rejects.toMatchObject({ code: "transaction_mismatch" }); + const altered = transaction(); altered.updateInput(0, { sequence: 0 }); + await expect(signDispenseLeg(raw(), SOURCE, leg, async () => raw(altered))).rejects.toMatchObject({ code: "transaction_mismatch" }); + }); + + it.each([ + { give_quantity: 50000000 }, { tx_hash: "b".repeat(64) }, + { satoshirate: 2000 }, { oracle_address: OTHER }, { give_remaining: 1 }, + ])("refuses stale dispenser terms %j", async changed => { + vi.stubGlobal("fetch", vi.fn(async () => Response.json({ result: [{ + ...leg.dispenser, asset: "XCP", status: 0, oracle_address: null, ...changed, + }] }))); + const { preflightLeg } = await import("@/app/[lang]/dispense/_lib/use-dispense-router"); + expect(await preflightLeg(leg, t)).not.toBeNull(); + }); + + it("fails closed when dispenser state cannot be verified", async () => { + vi.stubGlobal("fetch", vi.fn(async () => { throw new Error("offline"); })); + const { preflightLeg } = await import("@/app/[lang]/dispense/_lib/use-dispense-router"); + expect(await preflightLeg(leg, t)).toBe("Could not refresh the quote. Try again."); }); }); diff --git a/tests/historical-currency.test.tsx b/tests/historical-currency.test.tsx new file mode 100644 index 00000000..dc466c4f --- /dev/null +++ b/tests/historical-currency.test.tsx @@ -0,0 +1,45 @@ +// @vitest-environment happy-dom +import { act } from "react"; +import { createRoot } from "react-dom/client"; +import { expect, it, vi } from "vitest"; +import { PriceChart } from "../apps/web/src/app/[lang]/[asset]/_components/price-chart"; +import { setCurrency, useCurrency } from "../apps/web/src/lib/currency"; +import { setNumberLocale } from "../apps/web/src/lib/number-preference"; + +vi.mock("swr", () => ({ default: () => ({ data: [{ day: "2026-09-06", usd: 2 }, { day: "2026-09-07", usd: 3 }] }) })); +vi.mock("@/lib/api/launchpad-api", () => ({ fetchFxRates: async () => ({ date: "2026-09-07", rates: { CNY: 7 } }) })); +vi.mock("@/lib/analytics", () => ({ trackEvent: vi.fn() })); +globalThis.IS_REACT_ACT_ENVIRONMENT = true; + +it("keeps historical USD candles and labels separate from a current CNY preference", async () => { + const container = document.createElement("div"); document.body.append(container); + const root = createRoot(container); + const candles = [0, 1].map(offset => ({ + time: Date.parse(`2026-09-0${6 + offset}T12:00:00Z`) / 1000, + open: 0.01, high: 0.02, low: 0.01, close: 0.02, volumeXcpRaw: "100000000", trades: 1, lastBlock: 965900 + offset, + })); + function Probe() { + const currency = useCurrency(); + return <>{currency.code}; + } + try { + setCurrency("USD"); setNumberLocale("en"); + await act(() => root.render()); + const chart = () => container.querySelector("svg[role=img]")!; + const path = () => Array.from(chart().querySelectorAll("path")).map(element => element.getAttribute("d")); + const initialPath = path(); + const initialLabels = chart().textContent; + await act(() => setCurrency("CNY")); + expect(container.querySelector("output")!.textContent).toBe("CNY"); + expect(chart().getAttribute("aria-label")).toContain("against USD"); + expect(path()).toEqual(initialPath); + expect(chart().textContent).toBe(initialLabels); + expect(chart().textContent).not.toContain("CN¥"); + await act(() => setNumberLocale("fr")); + expect(chart().getAttribute("aria-label")).toContain("against USD"); + expect(path()).toEqual(initialPath); + expect(chart().textContent).toContain("$"); + } finally { + await act(() => root.unmount()); container.remove(); setCurrency("USD"); setNumberLocale("auto"); + } +}); diff --git a/tests/inscription-safety.test.ts b/tests/inscription-safety.test.ts new file mode 100644 index 00000000..a2730b00 --- /dev/null +++ b/tests/inscription-safety.test.ts @@ -0,0 +1,151 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { hex } from "@scure/base"; +import { p2tr, SigHash, Transaction, utils } from "@scure/btc-signer"; +import { taprootTweakPrivKey } from "@scure/btc-signer/utils.js"; +import { inscribeLaunch } from "../apps/web/src/lib/inscribe-launch"; +import { addressToScriptPubKey, BURN_ADDRESS } from "../apps/web/src/lib/inscriber"; +import { readPsbt, readTransaction } from "../apps/web/src/lib/transaction-verification"; + +// Offline transaction fixtures and a throwaway key; nothing reaches a node. +const key = hex.decode("01".padStart(64, "0")); +const internalKey = utils.pubSchnorr(key); +const address = p2tr(internalKey).address!; +function funding(value = 50000n, recipient = address) { + const tx = new Transaction(); + tx.addInput({ txid: "11".repeat(32), index: 0 }); + tx.addOutputAddress(recipient, value); + return { tx, utxo: { txid: tx.id, vout: 0, value: Number(value), status: { confirmed: true } } }; +} +function sign(psbt: string) { + const tx = readPsbt(psbt); + if (tx.getInput(0).tapLeafScript) { + // The create path puts the connected address's output key in the leaf. + tx.signIdx(taprootTweakPrivKey(key), 0, [SigHash.ALL]); + } else { + tx.updateInput(0, { tapInternalKey: internalKey }); + tx.signIdx(key, 0, [SigHash.ALL]); + } + return hex.encode(tx.toPSBT()); +} + +let funds: ReturnType[]; +let balanceResponse: (outpoint: string) => Response; +let parentResponse: (txid: string) => Response; +let fetchMock: ReturnType; +let signMock: ReturnType; +let broadcast: ReturnType; +function options(feeRate = 1) { + return { + asset: "SAFELAUNCH", lpAsset: "A96000000000000001", startBlock: 966100, + softCapDeadlineBlock: 967100, imageData: new Uint8Array([1, 2, 3]), + mimeType: "image/png", feeRate, description: "https://xcp.fun/SAFELAUNCH.json", + address, signPsbt: signMock, broadcast, onStep: vi.fn(), + }; +} +beforeEach(() => { + funds = [funding()]; + balanceResponse = () => Response.json({ result: [], next_cursor: null }); + parentResponse = txid => new Response(hex.encode(funds.find(f => f.tx.id === txid)!.tx.toBytes(true, false))); + fetchMock = vi.fn(async (input: string) => { + const url = new URL(input); + if (url.pathname.endsWith(`/address/${address}/utxo`)) return Response.json(funds.map(f => f.utxo)); + const balances = /\/utxos\/([^/]+)\/balances$/.exec(url.pathname); + if (balances) { + expect(url.searchParams.get("limit")).toBe("1"); + return balanceResponse(balances[1]!); + } + const parent = /\/tx\/([a-f0-9]{64})\/hex$/.exec(url.pathname); + if (parent) return parentResponse(parent[1]!); + throw new Error(`Unexpected request: ${input}`); + }); + vi.stubGlobal("fetch", fetchMock); + signMock = vi.fn(async (psbt: string) => sign(psbt)); + broadcast = vi.fn(async (raw: string) => readTransaction(raw).id); +}); +afterEach(() => vi.unstubAllGlobals()); + +describe("inscription funding and returned commit/reveal transactions", () => { + it.each([0.1, 1, 1.56])("signs unchanged commit/reveal envelopes with fractional fee rate %s", async rate => { + const result = await inscribeLaunch(options(rate)); + expect(signMock).toHaveBeenCalledTimes(2); + expect(broadcast).toHaveBeenCalledTimes(2); + expect(result.commitTxid).toBe(readTransaction(broadcast.mock.calls[0]![0]).id); + expect(result.revealTxid).toBe(readTransaction(broadcast.mock.calls[1]![0]).id); + const commit = readTransaction(broadcast.mock.calls[0]![0]); + const reveal = readTransaction(broadcast.mock.calls[1]![0]); + expect(hex.encode(commit.getInput(0).txid!)).toBe(funds[0]!.tx.id); + expect(reveal.getOutput(1).amount).toBe(546n); + expect(hex.encode(reveal.getOutput(1).script!)).toBe(hex.encode(addressToScriptPubKey(BURN_ADDRESS))); + expect(fetchMock.mock.calls.filter(([url]) => String(url).includes("/balances?"))).toHaveLength(2); + }); + + it.each([404, 429, 503])("never treats HTTP%s as proof of balance-free funding", async status => { + balanceResponse = () => new Response("unavailable", { status }); + await expect(inscribeLaunch(options())).rejects.toThrow("Could not verify funding UTXO balances"); + expect(signMock).not.toHaveBeenCalled(); expect(broadcast).not.toHaveBeenCalled(); + }); + + it.each([ + { result: null, next_cursor: null }, { result: {} }, { result: [] }, + { result: [], next_cursor: 7 }, { error: "not ready", result: [], next_cursor: null }, + ])("rejects an ambiguous no-balance response %j", async body => { + balanceResponse = () => Response.json(body); + await expect(inscribeLaunch(options())).rejects.toThrow("Could not verify funding UTXO balances"); + expect(signMock).not.toHaveBeenCalled(); expect(broadcast).not.toHaveBeenCalled(); + }); + + it("skips the larger asset-bearing UTXO and uses the verified clean one", async () => { + funds = [funding(60000n), funding(50000n)]; + balanceResponse = outpoint => Response.json({ result: outpoint.startsWith(funds[0]!.tx.id) + ? [{ asset: "TOKEN", quantity: 1, utxo: outpoint }] : [], next_cursor: null }); + await inscribeLaunch(options()); + expect(hex.encode(readPsbt(signMock.mock.calls[0]![0]).getInput(0).txid!)).toBe(funds[1]!.tx.id); + }); + + it("rejects a UTXO that gains balances during wallet approval", async () => { + signMock.mockImplementationOnce(async (psbt: string) => { + balanceResponse = () => Response.json({ result: [{ asset: "TOKEN", quantity: 1 }], next_cursor: null }); + return sign(psbt); + }); + await expect(inscribeLaunch(options())).rejects.toThrow("now carries Counterparty balances"); + expect(signMock).toHaveBeenCalledTimes(1); expect(broadcast).not.toHaveBeenCalled(); + }); + + it("stops when the post-signing balance recheck fails", async () => { + signMock.mockImplementationOnce(async (psbt: string) => { + balanceResponse = () => new Response("busy", { status: 503 }); + return sign(psbt); + }); + await expect(inscribeLaunch(options())).rejects.toThrow("Could not verify funding UTXO balances"); + expect(broadcast).not.toHaveBeenCalled(); + }); + + it.each(["hash", "value", "script"])("rejects mismatched funding parent %s before signing", async mismatch => { + if (mismatch === "hash") parentResponse = () => new Response(hex.encode(funding(50001n).tx.toBytes(true, false))); + if (mismatch === "value") funds[0]!.utxo.value++; + if (mismatch === "script") funds = [funding(50000n, BURN_ADDRESS)]; + await expect(inscribeLaunch(options())).rejects.toMatchObject({ code: "transaction_mismatch" }); + expect(signMock).not.toHaveBeenCalled(); expect(broadcast).not.toHaveBeenCalled(); + }); + + it("refuses a changed commit output returned by the wallet before any broadcast", async () => { + signMock.mockImplementationOnce(async (psbt: string) => { + const tx = readPsbt(psbt); + tx.updateOutput(0, { amount: tx.getOutput(0).amount! + 1n }); + return sign(hex.encode(tx.toPSBT())); + }); + await expect(inscribeLaunch(options())).rejects.toMatchObject({ code: "transaction_mismatch" }); + expect(broadcast).not.toHaveBeenCalled(); + }); + + it("refuses a changed reveal recipient, leaving only the already-broadcast commit", async () => { + signMock.mockImplementationOnce(async (psbt: string) => sign(psbt)); + signMock.mockImplementationOnce(async (psbt: string) => { + const tx = readPsbt(psbt); + tx.updateOutput(1, { script: addressToScriptPubKey(address) }); + return sign(hex.encode(tx.toPSBT())); + }); + await expect(inscribeLaunch(options())).rejects.toMatchObject({ code: "transaction_mismatch" }); + expect(broadcast).toHaveBeenCalledTimes(1); + }); +}); diff --git a/tests/limit-amounts.test.ts b/tests/limit-amounts.test.ts new file mode 100644 index 00000000..ca501163 --- /dev/null +++ b/tests/limit-amounts.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; +import { COUNTERPARTY_MAX_INT } from "@xcp/wallet-sdk/amounts"; +import { deriveLimitAmounts } from "../apps/web/src/lib/limit-amounts"; +import { parseWholeTokenRaw } from "../apps/web/src/lib/amount-draft"; + +describe("limit price guarantees", () => { + for (const side of ["buy", "sell"] as const) { + for (const edited of ["amount", "total"] as const) { + it(`${side}, editing ${edited}: keeps the entered leg and never weakens the price`, () => { + for (const price of [1n, 33333333n, 100000001n, 999999999n]) { + for (const value of [100000001n, 999999999n, 10000000000000001n]) { + if (edited === "total" && value * 100000000n / price > COUNTERPARTY_MAX_INT) continue; + const result = deriveLimitAmounts(side, edited, value, price)!; + expect(result[edited]).toBe(value); + const actual = result.total * 100000000n; + const limit = result.amount * price; + expect(side === "buy" ? actual <= limit : actual >= limit).toBe(true); + } + } + }); + } + } + it("rejects zero legs and overflow after derivation", () => { + expect(deriveLimitAmounts("buy", "amount", 1n, 1n)).toBeNull(); + expect(deriveLimitAmounts("sell", "amount", COUNTERPARTY_MAX_INT, 200000000n)).toBeNull(); + expect(deriveLimitAmounts("buy", "total", COUNTERPARTY_MAX_INT, 1n)).toBeNull(); + }); + it("requires whole XCP lots before scaling, without truncating fractions", () => { + expect(parseWholeTokenRaw("0.5")).toBeNull(); + expect(parseWholeTokenRaw("1e5")).toBeNull(); + expect(parseWholeTokenRaw("100")).toBe(10000000000n); + expect(parseWholeTokenRaw("92233720369")).toBeNull(); + }); +}); diff --git a/tests/presentation-preferences.test.tsx b/tests/presentation-preferences.test.tsx new file mode 100644 index 00000000..a3b3a4cc --- /dev/null +++ b/tests/presentation-preferences.test.tsx @@ -0,0 +1,82 @@ +// @vitest-environment happy-dom +import { act, useState } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeEach, expect, it, vi } from "vitest"; +import { LocaleProvider } from "../apps/web/src/lib/i18n/client"; +import type { Locale } from "../apps/web/src/lib/i18n/locales"; +import { useNumbers } from "../apps/web/src/lib/i18n/numbers"; +import { NUMBER_PREF_KEY, setNumberLocale } from "../apps/web/src/lib/number-preference"; +import { setCurrency, useCurrency, useFiat } from "../apps/web/src/lib/currency"; +import { AmountInput } from "../apps/web/src/components/amount-input"; +import { ComposeError } from "../apps/web/src/components/compose-error"; +import { parseAmountRaw } from "../apps/web/src/lib/amount-draft"; +import es from "../apps/web/src/locales/es.json"; +import ja from "../apps/web/src/locales/ja.json"; + +vi.mock("@/lib/analytics", () => ({ trackEvent: vi.fn() })); +vi.mock("@/lib/api/launchpad-api", () => ({ fetchFxRates: async () => ({ date: "2026-09-07", rates: { JPY: 150 } }) })); +let root: Root; +let container: HTMLDivElement; +globalThis.IS_REACT_ACT_ENVIRONMENT = true; +function Probe() { + const [draft, setDraft] = useState("100000000.00000001"); + const numbers = useNumbers(); const currency = useCurrency(); const fiat = useFiat(); + return <>{parseAmountRaw(draft)?.toString()} + {numbers.commasRaw(10000000000000001n)} + {currency.code}{fiat(1.5)} + + ; +} +async function render(locale: Locale) { + await act(() => root.render()); +} +beforeEach(() => { + localStorage.clear(); setNumberLocale("auto"); setCurrency("USD"); + container = document.createElement("div"); document.body.append(container); root = createRoot(container); +}); +afterEach(async () => { await act(() => root.unmount()); container.remove(); }); + +it("keeps exact intent and USD while language and display format change independently", async () => { + await render("ja"); + expect(container.querySelector("#number")!.textContent).toBe("100,000,000.00000001"); + await act(() => setNumberLocale("fr")); + expect(container.querySelector("#number")!.textContent).toContain(",00000001"); + await render("es"); + expect(container.querySelector("#number")!.textContent).toContain(",00000001"); + expect(container.querySelector("#currency")!.textContent).toBe("USD"); + for (const locale of ["de-DE", "es-VE"] as const) { + await act(() => setNumberLocale(locale)); + expect(container.querySelector("#number")!.textContent).toBe("100.000.000,00000001"); + expect(container.querySelector("input")!.value).toBe("100000000.00000001"); + expect(container.querySelector("output")!.textContent).toBe("10000000000000001"); + } + await act(() => setNumberLocale("fr")); + expect(container.querySelector("#fiat")!.textContent).toContain("1,50"); + expect(container.querySelector("input")!.value).toBe("100000000.00000001"); + expect(container.querySelector("output")!.textContent).toBe("10000000000000001"); + expect(localStorage.getItem(NUMBER_PREF_KEY)).toBe("fr"); + await act(() => setNumberLocale("es-ES")); + expect(container.querySelector("#number")!.textContent).toBe("100.000.000,00000001"); + expect(container.querySelector("input")!.value).toBe("100000000.00000001"); + expect(container.querySelector("#currency")!.textContent).toBe("USD"); + await act(() => setNumberLocale("auto")); + expect(container.querySelector("#number")!.textContent).toBe("100,000,000.00000001"); +}); + +it("responds to another tab and safely falls back from a malformed saved choice", async () => { + await render("ja"); + await act(() => { localStorage.setItem(NUMBER_PREF_KEY, "fr"); window.dispatchEvent(new StorageEvent("storage", { key: NUMBER_PREF_KEY })); }); + expect(container.querySelector("#number")!.textContent).toContain(",00000001"); + await act(() => { localStorage.setItem(NUMBER_PREF_KEY, "invalid"); window.dispatchEvent(new StorageEvent("storage", { key: NUMBER_PREF_KEY })); }); + expect(container.querySelector("#number")!.textContent).toBe("100,000,000.00000001"); +}); + +it("translates actionable errors while preserving copyable codes and original diagnostics", async () => { + await render("es"); + expect(container.textContent).toContain("Revisa la cantidad, las unidades y los decimales."); + expect(container.querySelector("pre")!.textContent).toContain('"code": "amount_precision"'); + expect(container.querySelector("pre")!.textContent).toContain("Original API diagnostic"); + await render("ja"); + expect(container.textContent).toContain("数量、単位、小数点以下の桁数を確認してください。"); + expect(container.querySelector("pre")!.textContent).toContain('"walletCode": 123'); +}); diff --git a/tests/quote-validation.test.ts b/tests/quote-validation.test.ts new file mode 100644 index 00000000..ba137077 --- /dev/null +++ b/tests/quote-validation.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { validateDepositQuote, validateWithdrawQuote, validateSwapQuote } from "../apps/web/src/lib/quote-validation"; + +const deposit = { first_deposit: false, asset_a: "XCP", asset_b: "TOKEN", quantity_a_required: "10000000000000001", quantity_b_required: "100", quantity_minted_estimate: "50" }; +const withdraw = { pool_exists: true, asset_a: "XCP", asset_b: "TOKEN", quantity: "100", quantity_a_estimate: "10000000000000001", quantity_b_estimate: "50", supply: "200" }; +describe("quote identity and exact numeric boundaries", () => { + it("matches the edited URL leg even when Core sorts assets differently", () => { + expect(validateDepositQuote(deposit, "TOKEN", "XCP", 100n)).toBe(deposit); + expect(() => validateDepositQuote(deposit, "TOKEN", "XCP", 101n)).toThrow("quote_quantity_mismatch"); + expect(() => validateDepositQuote(deposit, "OTHER", "XCP", 100n)).toThrow("quote_asset_mismatch"); + }); + it.each(["0,5", "1e5", "-1", "1.5", Number.MAX_SAFE_INTEGER + 1, "9223372036854775808"])("rejects invalid raw values %s returned by an API", value => { + expect(() => validateDepositQuote({ ...deposit, quantity_a_required: value }, "TOKEN", "XCP", 100n)).toThrow(); + expect(() => validateWithdrawQuote({ ...withdraw, quantity_a_estimate: value }, "TOKEN", "XCP", 100n)).toThrow(); + expect(() => validateSwapQuote({ estimated_output: value, price_impact: 0 })).toThrow(); + }); + it("checks LP quantity, pool existence, pair, and supply before withdrawals", () => { + expect(validateWithdrawQuote(withdraw, "TOKEN", "XCP", 100n)).toBe(withdraw); + expect(() => validateWithdrawQuote(withdraw, "TOKEN", "XCP", 101n)).toThrow(); + expect(() => validateWithdrawQuote({ ...withdraw, supply: "99" }, "TOKEN", "XCP", 100n)).toThrow(); + expect(() => validateWithdrawQuote({ ...withdraw, pool_exists: false }, "TOKEN", "XCP", 100n)).toThrow(); + expect(() => validateSwapQuote({ estimated_output: "100", price_impact: NaN })).toThrow(); + }); +}); diff --git a/tests/submitted-pending-intent.test.tsx b/tests/submitted-pending-intent.test.tsx new file mode 100644 index 00000000..da2e945b --- /dev/null +++ b/tests/submitted-pending-intent.test.tsx @@ -0,0 +1,198 @@ +// @vitest-environment happy-dom +import { act, cloneElement, type ReactElement, type ReactNode } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { invalidatePending, pendingSpentRaw, readPending } from "@xcp/wallet-sdk"; +import { SwapWidget } from "../apps/web/src/app/[lang]/swap/_components/swap-widget"; +import { TradePanel } from "../apps/web/src/app/[lang]/[asset]/_components/trade-panel"; +import { MintPanel } from "../apps/web/src/app/[lang]/[asset]/_components/mint-panel"; +import { LiquidityWidget } from "../apps/web/src/app/[lang]/swap/_components/liquidity-widget"; + +const boundary = vi.hoisted(() => ({ + status: "idle", txid: null as string | null, address: "submitted-address", + order: vi.fn(), deposit: vi.fn(), withdraw: vi.fn(), mint: vi.fn(), analytics: vi.fn(), +})); +vi.mock("@/lib/wallet/useCompose", () => ({ useCompose: () => ({ + status: boundary.status, txid: boundary.txid, + composeOrder: boundary.order, composePoolDeposit: boundary.deposit, + composeFairmint: boundary.mint, + composePoolWithdraw: boundary.withdraw, composeCancel: vi.fn(), reset: vi.fn(), +}) })); +vi.mock("@/lib/wallet/wallet-context", () => ({ useWallet: () => ({ address: boundary.address, status: "connected" }) })); +vi.mock("@xcp/wallet-sdk/react/use-spendable-balance", () => ({ useSpendableBalance: (_address: string, asset: string) => ({ + balance: asset === "LP" ? 40000000000000004n : 9000000000000000000n, pendingOutgoing: 0n, +}) })); +vi.mock("@/hooks/use-debounced", () => ({ useDebounced: (value: unknown) => value })); +vi.mock("@/hooks/use-mempool", () => ({ useMempool: () => ({ orders: [] }) })); +vi.mock("@/lib/currency", () => ({ useFiat: () => () => "$0", useFxRate: () => ({ code: "USD", rate: 1 }) })); +vi.mock("@/lib/analytics", () => ({ trackTx: boundary.analytics })); +vi.mock("@/app/[lang]/swap/_components/swap-settings", () => ({ + LIMIT_EXPIRATIONS: [100], + useSwapSettings: () => ({ slippage: 1, slippageAuto: false, expiration: 100, + limitExpiration: 100, lqSlippage: 1, customFee: 0.1, medianFeeRate: 1, + swapSettingsValid: true, limitSettingsValid: true, liquiditySettingsValid: true, + setAutoValue: () => {}, + }), +})); + +function responseFor(key: unknown) { + if (typeof key !== "string") return undefined; + if (key.includes("estimatexcpfees")) return 5n; + if (key.includes("/quote/deposit?")) return { + first_deposit: false, asset_a: "TOKEN", asset_b: "XCP", + quantity_a_required: new URL(key).searchParams.get("quantity"), + quantity_b_required: "2000000001", quantity_minted_estimate: "300000000", + }; + if (key.includes("/quote/withdraw?")) return { + pool_exists: true, asset_a: "TOKEN", asset_b: "XCP", + quantity: new URL(key).searchParams.get("quantity"), supply: "80000000000000008", + quantity_a_estimate: "1000000001", quantity_b_estimate: "2000000001", + }; + if (key.includes("/quote?")) return { + estimated_output: "1000000001", pool_output: "1000000001", book_output: "0", price_impact: 0, + }; + if (key.includes("/pools/")) return { + asset_a: "TOKEN", asset_b: "XCP", lp_asset: "LP", + reserve_a: "1000000000000000000", reserve_b: "1000000000000000000", + }; + return undefined; +} +vi.mock("swr", () => ({ default: (key: unknown) => ({ data: responseFor(key), isValidating: false, mutate: vi.fn() }) })); +vi.mock("@/lib/client", () => ({ fetchJson: async (url: string) => ({ result: responseFor(url) }) })); +vi.mock("@/components/ui/button", () => ({ CTA: ({ children, ...props }: { children: ReactNode }) => })); +vi.mock("@/components/asset-chip", () => ({ AssetChip: () => null })); +vi.mock("@/components/token-select-modal", () => ({ TokenSelectModal: () => null })); +vi.mock("@/components/order-tracker", () => ({ OrderTracker: () => null })); +vi.mock("@/components/connect-button", () => ({ ConnectButton: () => null })); +vi.mock("@/components/quote-ring", () => ({ QuoteRing: () => null })); + +let root: Root; +let container: HTMLDivElement; +let view: ReactElement; +globalThis.IS_REACT_ACT_ENVIRONMENT = true; +beforeEach(() => { + vi.clearAllMocks(); + boundary.status = "idle"; boundary.txid = null; boundary.address = "submitted-address"; + localStorage.clear(); invalidatePending(); + container = document.createElement("div"); document.body.append(container); + root = createRoot(container); +}); +afterEach(async () => { await act(() => root.unmount()); container.remove(); }); +async function render(element = view) { view = element; await act(() => root.render(element)); } +async function phase(status: string, txid: string | null = null) { + boundary.status = status; boundary.txid = txid; + // A fresh element rerenders the production component as the hook updates. + await render(cloneElement(view)); +} +async function enter(label: string, value: string) { + const input = container.querySelector(`input[aria-label="${label}"]`)!; + expect(input).not.toBeNull(); + await act(() => { + Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!.call(input, value); + input.dispatchEvent(new InputEvent("input", { bubbles: true })); + }); +} +async function click(text?: string) { + const button = text + ? [...container.querySelectorAll("button")].find(b => b.textContent === text)! + : container.querySelector("[data-testid=submit]")!; + expect(button).not.toBeNull(); expect(button.disabled).toBe(false); + await act(async () => { button.click(); }); +} +function expectReservation(asset: string, raw: bigint) { + expect(pendingSpentRaw(asset, "submitted-address")).toBe(raw); + expect(pendingSpentRaw(asset, "edited-address")).toBe(0n); +} + +describe("confirmed pending spends use the submitted form, through edits while signing", () => { + it("keeps a mint's original XCP cost, asset and account after an invalid edit", async () => { + await render(); + await enter("TOKEN to mint", "1000"); await click(); + expect(boundary.mint).toHaveBeenCalledWith({ asset: "TOKEN", quantity: 100000000000 }); + await phase("signing"); await enter("TOKEN to mint", "-1000000"); + boundary.address = "edited-address"; + await render(); + await phase("confirmed", "mint-tx"); + expectReservation("XCP", 1000000n); + expect(readPending()[0]).toMatchObject({ label: "Mint 1,000 TOKEN" }); + expect(boundary.analytics).toHaveBeenCalledWith("mint-tx", "mint", 0.02); + }); + + it("keeps the swap amount/account/action and does not reuse it for a cancellation", async () => { + await render(); + await enter("Amount of XCP to sell", "100000000.00000001"); + await click(); + expect(boundary.order).toHaveBeenCalledWith(expect.objectContaining({ give_quantity: 10000000000000001n })); + await phase("signing"); + await enter("Amount of XCP to sell", "-5"); + boundary.address = "edited-address"; + await phase("confirmed", "swap-tx"); + expectReservation("XCP", 10000000000000001n); + expect(readPending()[0]).toMatchObject({ address: "submitted-address", label: "Buy TOKEN — market order" }); + // USD is a display estimate; the reserved quantity above remains exact. + expect(boundary.analytics).toHaveBeenCalledWith("swap-tx", "buy", 200000000); + await enter("Amount of XCP to sell", "6"); + await phase("signing"); await phase("confirmed", "cancel-tx"); + expect(readPending()).toHaveLength(1); + expect(boundary.analytics).toHaveBeenCalledTimes(1); + }); + + it("keeps a limit buy's XCP debit when quantity, side, asset and account change", async () => { + await render(); + await enter("Limit price in XCP per TOKEN", "2"); + await enter("Amount of TOKEN", "3.00000001"); + await click(); + expect(boundary.order).toHaveBeenCalledWith(expect.objectContaining({ give_asset: "XCP", give_quantity: 600000002n })); + await phase("signing"); + await enter("Amount of TOKEN", "100"); + boundary.address = "edited-address"; + await render(); + await phase("confirmed", "limit-tx"); + expectReservation("XCP", 600000002n); + expect(pendingSpentRaw("OTHER")).toBe(0n); + expect(readPending()[0]).toMatchObject({ label: "Buy TOKEN — limit order" }); + expect(boundary.analytics).toHaveBeenCalledWith("limit-tx", "limit order", 12.00000004); + }); + + it("keeps both deposit legs and the submitted gas when switched to remove", async () => { + await render(); + await enter("Amount of TOKEN to deposit", "100000000.00000001"); + await click(); + expect(boundary.deposit).toHaveBeenCalledWith(expect.objectContaining({ quantity_a: 10000000000000001n, quantity_b: 2000000001n })); + await phase("signing"); + await enter("Amount of TOKEN to deposit", "1e5"); + await click("remove"); boundary.address = "edited-address"; + await phase("confirmed", "deposit-tx"); + expect(container.textContent).toContain("Deposit broadcast"); + expectReservation("TOKEN", 10000000000000001n); + expectReservation("XCP", 2000000006n); + expect(pendingSpentRaw("LP")).toBe(0n); + expect(readPending()[0]).toMatchObject({ label: "Add TOKEN/XCP liquidity" }); + expect(boundary.analytics).toHaveBeenCalledWith("deposit-tx", "liquidity added", 80.00000004); + }); + + it("keeps exact LP withdrawal units and gas when percentage and mode change", async () => { + await render(); + await click("remove"); await click(); + expect(boundary.withdraw).toHaveBeenCalledWith(expect.objectContaining({ lp_asset: "LP", quantity: 10000000000000001n })); + await phase("signing"); await click("50%"); await click("add"); + boundary.address = "edited-address"; + await phase("confirmed", "withdraw-tx"); + expect(container.textContent).toContain("Withdrawal broadcast"); + expectReservation("LP", 10000000000000001n); + expectReservation("XCP", 5n); + expect(readPending()[0]).toMatchObject({ label: "Remove TOKEN/XCP liquidity" }); + expect(boundary.analytics).toHaveBeenCalledWith("withdraw-tx", "liquidity removed", 80.00000004); + }); + + it("releases a failed submitted record so a corrected new order can register", async () => { + await render(); + await enter("Limit price in XCP per TOKEN", "2"); await enter("Amount of TOKEN", "3"); + await click(); await phase("error"); + expect(readPending()).toHaveLength(0); + await enter("Amount of TOKEN", "4"); await click(); + await phase("signing"); await phase("confirmed", "retry-tx"); + expectReservation("TOKEN", 400000000n); + expect(boundary.order).toHaveBeenCalledTimes(2); + }); +}); diff --git a/tests/swap-intent.test.tsx b/tests/swap-intent.test.tsx new file mode 100644 index 00000000..b9425b41 --- /dev/null +++ b/tests/swap-intent.test.tsx @@ -0,0 +1,99 @@ +// @vitest-environment happy-dom +import { act } from "react"; +import type { ReactNode } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { SwapWidget } from "../apps/web/src/app/[lang]/swap/_components/swap-widget"; + +const boundary = vi.hoisted(() => ({ + compose: vi.fn(), fetch: vi.fn(), keys: [] as string[], + quote: { estimated_output: "10000000000000001", pool_output: "10000000000000001", book_output: "0", price_impact: 0 }, +})); +vi.mock("@/lib/wallet/useCompose", () => ({ useCompose: () => ({ status: "idle", composeOrder: boundary.compose }) })); +vi.mock("@/lib/wallet/wallet-context", () => ({ useWallet: () => ({ address: "address", status: "connected" }) })); +vi.mock("@xcp/wallet-sdk/react/use-spendable-balance", () => ({ useSpendableBalance: () => ({ balance: 9000000000000000000n, pendingOutgoing: 0n }) })); +vi.mock("@/lib/client", () => ({ fetchJson: boundary.fetch })); +vi.mock("@/hooks/use-debounced", () => ({ useDebounced: (value: unknown) => value })); +vi.mock("@/hooks/use-mempool", () => ({ useMempool: () => ({ orders: [] }) })); +vi.mock("@/lib/currency", () => ({ useFiat: () => () => "$0", useFxRate: () => ({ code: "USD", rate: 1 }) })); +vi.mock("@/lib/analytics", () => ({ trackTx: vi.fn() })); +vi.mock("@/app/[lang]/swap/_components/swap-settings", () => ({ useSwapSettings: () => ({ slippage: 1, slippageAuto: false, customSlip: 1, expiration: 100, customFee: 0.1, swapSettingsValid: true, medianFeeRate: 1, setAutoValue: () => {} }) })); +vi.mock("swr", () => ({ default: (key: unknown) => { + if (typeof key === "string") boundary.keys.push(key); + return { data: typeof key === "string" && key.includes("/quote?") ? boundary.quote : undefined, isValidating: false, mutate: vi.fn() }; +} })); +vi.mock("@/components/ui/button", () => ({ CTA: ({ children, ...props }: { children: ReactNode }) => })); +vi.mock("@/components/asset-chip", () => ({ AssetChip: () => null })); +vi.mock("@/components/token-select-modal", () => ({ TokenSelectModal: () => null })); +vi.mock("@/components/order-tracker", () => ({ OrderTracker: () => null })); +vi.mock("@/components/connect-button", () => ({ ConnectButton: () => null })); +vi.mock("@/components/quote-ring", () => ({ QuoteRing: () => null })); + +let root: Root; +let container: HTMLDivElement; +globalThis.IS_REACT_ACT_ENVIRONMENT = true; +beforeEach(async () => { + boundary.compose.mockReset(); boundary.fetch.mockReset(); boundary.keys = []; + boundary.fetch.mockResolvedValue({ result: boundary.quote }); + container = document.createElement("div"); document.body.append(container); + root = createRoot(container); + await act(() => root.render()); +}); +afterEach(async () => { await act(() => root.unmount()); container.remove(); }); +async function enter(value: string) { + const input = container.querySelector("input")!; + await act(() => { + Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!.call(input, value); + input.dispatchEvent(new InputEvent("input", { bubbles: true })); + }); +} +const button = () => container.querySelector("[data-testid=submit]")!; + +describe("rendered swap draft to quote to compose", () => { + it("preserves a raw quantity beyond double precision through both boundaries", async () => { + await enter("100000000.00000001"); + expect(button().disabled).toBe(false); + expect(boundary.keys.some(key => key.endsWith("quantity=10000000000000001"))).toBe(true); + await act(async () => { button().click(); }); + expect(boundary.compose).toHaveBeenCalledWith(expect.objectContaining({ give_quantity: 10000000000000001n, fee_rate: 0.1 })); + }); + it("does not quote or submit an invalid edit over a previously valid quantity", async () => { + await enter("5"); boundary.keys = []; + await enter("5e2"); + expect(container.querySelector("input")!.value).toBe("5e2"); + expect(button().disabled).toBe(true); + expect(boundary.keys.some(key => key.includes("/quote?"))).toBe(false); + await act(() => { button().click(); }); + expect(boundary.compose).not.toHaveBeenCalled(); + }); + it("blocks a failed refresh instead of composing cached terms", async () => { + await enter("5"); boundary.fetch.mockRejectedValueOnce(new Error("HTTP 429")); + await act(async () => { button().click(); }); + expect(boundary.compose).not.toHaveBeenCalled(); + expect(container.textContent).toContain("Could not refresh the quote"); + }); + it("invalidates an in-flight submission when the editable intent changes", async () => { + let resolve!: (value: unknown) => void; + boundary.fetch.mockReturnValueOnce(new Promise(done => { resolve = done; })); + await enter("5"); await act(() => { button().click(); }); + await enter("6"); + await act(async () => { resolve({ result: boundary.quote }); }); + expect(boundary.compose).not.toHaveBeenCalled(); + }); + it("allows only one submission while the final quote is refreshing", async () => { + let resolve!: (value: unknown) => void; + boundary.fetch.mockReturnValueOnce(new Promise(done => { resolve = done; })); + await enter("5"); + await act(() => { button().click(); button().click(); }); + expect(boundary.fetch).toHaveBeenCalledTimes(1); + expect(button().disabled).toBe(true); + await act(async () => { resolve({ result: boundary.quote }); }); + expect(boundary.compose).toHaveBeenCalledTimes(1); + }); + it("never lowers the displayed minimum when a fresh quote is slightly worse", async () => { + await enter("5"); + boundary.fetch.mockResolvedValueOnce({ result: { ...boundary.quote, estimated_output: "9990000000000000" } }); + await act(async () => { button().click(); }); + expect(boundary.compose.mock.calls[0]![0].get_quantity).toBe(9900000000000000n); + }); +}); diff --git a/tests/trade-settings.test.tsx b/tests/trade-settings.test.tsx index 0ab3d6ac..1bdc3344 100644 --- a/tests/trade-settings.test.tsx +++ b/tests/trade-settings.test.tsx @@ -19,16 +19,18 @@ vi.mock("@/app/[lang]/swap/_lib/trade-settings-store", async (importOriginal) => }); function FeeConsumer() { - const { customFee } = useSwapSettings(); - return {customFee}; + const { customFee, swapSettingsValid } = useSwapSettings(); + return {customFee ?? "auto"}:{String(swapSettingsValid)}; } describe("trade settings fee rate", () => { it.each([ - ["0.1", "0.1"], - ["1.56", "1.56"], - ["", "0"], - ["600", "500"], + ["0.1", "0.1:true"], + ["1.56", "1.56:true"], + ["0", "0:true"], + ["", "auto:true"], + ["500", "500:true"], + ...["600", "-5", "1e5", "0,5", "NaN", "1.", "0.000000001"].map((draft) => [draft, "auto:false"]), ])("exposes the stored %s sat/vB rate as %s", (typed, expected) => { snapshot.customFeeRate = typed; const html = renderToStaticMarkup( diff --git a/vitest.config.mts b/vitest.config.mts index 34fbbe81..787b6fbd 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -4,14 +4,8 @@ import { fileURLToPath } from "node:url"; const here = (path: string) => fileURLToPath(new URL(path, import.meta.url)); /** - * Unit tests for the pure layers only — the numeric primitives, the XCP-69 - * predicate, the candle fold, and the small pure helpers the app leans on. - * - * Deliberately no jsdom and no component rendering. What is worth testing - * here is the code where being wrong is SILENT: a predicate that admits a - * non-conforming launch, or a serializer that puts the wrong digits into a - * transaction, produces something that looks entirely normal and is wrong - * permanently. A broken layout announces itself; these do not. + * Pure boundary tests run in Node. Editing tests opt into happy-dom per + * file to exercise actual React handlers and submission gates. */ export default defineConfig({ resolve: { From fd9d7d4b698203402ce1f13da8b5355be43096aa Mon Sep 17 00:00:00 2001 From: Dan Anderson Date: Mon, 7 Sep 2026 18:56:54 -0400 Subject: [PATCH 03/10] Clarify transaction timing and translated trading guidance --- .../[asset]/_components/trade-panel.tsx | 2 +- .../[lang]/dispense/_components/bridge.tsx | 4 +- .../[lang]/swap/_components/swap-settings.tsx | 8 +-- .../[lang]/swap/_components/swap-widget.tsx | 2 +- apps/web/src/locales/_source.json | 42 +++++++------- apps/web/src/locales/es.json | 52 ++++++++--------- apps/web/src/locales/es.status.json | 16 +++--- apps/web/src/locales/fr.json | 16 +++--- apps/web/src/locales/fr.status.json | 16 +++--- apps/web/src/locales/ja.json | 56 +++++++++---------- apps/web/src/locales/ja.status.json | 16 +++--- apps/web/src/locales/ko.json | 16 +++--- apps/web/src/locales/ko.status.json | 16 +++--- apps/web/src/locales/pt.json | 16 +++--- apps/web/src/locales/pt.status.json | 16 +++--- apps/web/src/locales/ru.json | 16 +++--- apps/web/src/locales/ru.status.json | 16 +++--- apps/web/src/locales/uk.json | 16 +++--- apps/web/src/locales/uk.status.json | 16 +++--- apps/web/src/locales/zh-hk.json | 16 +++--- apps/web/src/locales/zh-hk.status.json | 16 +++--- apps/web/src/locales/zh-tw.json | 16 +++--- apps/web/src/locales/zh-tw.status.json | 16 +++--- apps/web/src/locales/zh.json | 36 ++++++------ apps/web/src/locales/zh.status.json | 16 +++--- 25 files changed, 237 insertions(+), 237 deletions(-) diff --git a/apps/web/src/app/[lang]/[asset]/_components/trade-panel.tsx b/apps/web/src/app/[lang]/[asset]/_components/trade-panel.tsx index c9627514..e9792a52 100644 --- a/apps/web/src/app/[lang]/[asset]/_components/trade-panel.tsx +++ b/apps/web/src/app/[lang]/[asset]/_components/trade-panel.tsx @@ -600,7 +600,7 @@ export function TradePanel({
{fillPct !== null && (
-
{t("Fills now")}
+
{t("Estimated fill")}
diff --git a/apps/web/src/app/[lang]/dispense/_components/bridge.tsx b/apps/web/src/app/[lang]/dispense/_components/bridge.tsx index f460aaee..377c587d 100644 --- a/apps/web/src/app/[lang]/dispense/_components/bridge.tsx +++ b/apps/web/src/app/[lang]/dispense/_components/bridge.tsx @@ -531,7 +531,7 @@ function LoadCard({

) : (

- {t("Broadcast legs are final — each is its own transaction. Failed legs can be retried.")} + {t("Each route is a separate transaction. Broadcast routes cannot be canceled here. Failed routes can be retried.")}

)} @@ -713,7 +713,7 @@ function LoadCard({
{t("Arrival")}
-
{t("next block after BTC confirms")}
+
{t("when the BTC payment confirms")}
)} diff --git a/apps/web/src/app/[lang]/swap/_components/swap-settings.tsx b/apps/web/src/app/[lang]/swap/_components/swap-settings.tsx index 5e3d837e..3b17c1bc 100644 --- a/apps/web/src/app/[lang]/swap/_components/swap-settings.tsx +++ b/apps/web/src/app/[lang]/swap/_components/swap-settings.tsx @@ -242,7 +242,7 @@ export function SwapSettingsGear() {

) : s.slippage > 5 ? (

- {t("High slippage authorizes up to {pct}% price impact.", { pct: num.commas(s.slippage) })} + {t("Slippage tolerance of {pct}% permits receiving less than quoted.", { pct: num.commas(s.slippage) })}

) : s.slippage < 0.5 ? (

@@ -276,7 +276,7 @@ export function SwapSettingsGear() {

- {t("How long an unfilled remainder rests before auto-refund. {n} = fill at confirmation or refund next block.", { n: MARKET_EXPIRATION })} + {t("How many blocks an unfilled remainder stays open before expiry. {n} is the shortest setting.", { n: MARKET_EXPIRATION })}

{t("TX fee")} @@ -354,7 +354,7 @@ export function LimitSettingsGear() { {t("The Bitcoin miner fee. Default tracks the next-block median.")}

- {t("A resting order refunds in full at expiry — the price you set is enforced by the order itself.")} + {t("Any unfilled amount is returned at expiry. The order enforces your limit price.")}
); @@ -408,7 +408,7 @@ export function LiquiditySettingsGear() {

- {t("If the pool moves past this before confirmation, the whole transaction is void — nothing is debited; only the miner fee is spent.")} + {t("If these limits cannot be met at confirmation, the liquidity operation is invalid. Pool assets are not debited; the Bitcoin network fee is still paid.")}

{t("TX fee")} diff --git a/apps/web/src/app/[lang]/swap/_components/swap-widget.tsx b/apps/web/src/app/[lang]/swap/_components/swap-widget.tsx index 7c385556..3dda79ad 100644 --- a/apps/web/src/app/[lang]/swap/_components/swap-widget.tsx +++ b/apps/web/src/app/[lang]/swap/_components/swap-widget.tsx @@ -743,7 +743,7 @@ export function SwapWidget({ }`} title={ minBelowMempool - ? t("Above what the pending orders would leave. If they confirm first, this order rests for a block and refunds instead of filling — raise the slippage or use Auto.") + ? t("Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.") : undefined } > diff --git a/apps/web/src/locales/_source.json b/apps/web/src/locales/_source.json index 6a323f96..4d66a12f 100644 --- a/apps/web/src/locales/_source.json +++ b/apps/web/src/locales/_source.json @@ -717,15 +717,9 @@ "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.": [ "app/[lang]/stats/_components/stats-content.tsx" ], - "A resting order refunds in full at expiry — the price you set is enforced by the order itself.": [ - "app/[lang]/swap/_components/swap-settings.tsx" - ], "About": [ "app/[lang]/docs/page.tsx" ], - "Above what the pending orders would leave. If they confirm first, this order rests for a block and refunds instead of filling — raise the slippage or use Auto.": [ - "app/[lang]/swap/_components/swap-widget.tsx" - ], "Across {n} fully exited position": [ "app/[lang]/profile/_components/history-tab.tsx" ], @@ -867,6 +861,9 @@ "Any future block, {floor} — tighter than the presets go. Under ~2 hours pays double the network fee.": [ "app/[lang]/create/page.tsx" ], + "Any unfilled amount is returned at expiry. The order enforces your limit price.": [ + "app/[lang]/swap/_components/swap-settings.tsx" + ], "API base:": [ "app/[lang]/docs/page.tsx" ], @@ -994,9 +991,6 @@ "Broadcast as {tx}. Minting opens at block {block} ({when}) — until then the launch is announced on-chain and nobody, you included, can mint. Then it runs for 1,000 blocks (~7 days): it sells out, or everyone is refunded.": [ "app/[lang]/create/page.tsx" ], - "Broadcast legs are final — each is its own transaction. Failed legs can be retried.": [ - "app/[lang]/dispense/_components/bridge.tsx" - ], "Broadcast, waiting to confirm. Its page and editing open up once it lands in a block.": [ "app/[lang]/profile/_components/launches-tab.tsx" ], @@ -1538,6 +1532,9 @@ "Each number counts unique addresses. A sale is a pool or order-book sale captured by xcp.fun. Dump means the address sold within {blocks} blocks of graduation. A meaningful balance is more than one token and more than 1% of the address's acquired amount.": [ "app/[lang]/research/_components/live-behavior-dashboard.tsx" ], + "Each route is a separate transaction. Broadcast routes cannot be canceled here. Failed routes can be retried.": [ + "app/[lang]/dispense/_components/bridge.tsx" + ], "Earned": [ "app/[lang]/rewards/_components/earners-table.tsx" ], @@ -1578,6 +1575,9 @@ "Escrowed until the launch resolves: tokens if it sells out, full XCP refund if it doesn't.": [ "app/[lang]/[asset]/_components/mint-panel.tsx" ], + "Estimated fill": [ + "app/[lang]/[asset]/_components/trade-panel.tsx" + ], "Event": [ "app/[lang]/activity/_components/activity-view.tsx" ], @@ -1698,9 +1698,6 @@ "Filled / expires": [ "app/[lang]/[asset]/_components/activity-tabs.tsx" ], - "Fills now": [ - "app/[lang]/[asset]/_components/trade-panel.tsx" - ], "Find a launch by asset name, and sort or filter the results.": [ "app/[lang]/_components/launch-search.tsx" ], @@ -1801,9 +1798,6 @@ "Hide minted": [ "app/[lang]/_components/launch-sections.tsx" ], - "High slippage authorizes up to {pct}% price impact.": [ - "app/[lang]/swap/_components/swap-settings.tsx" - ], "Higher than this trade needs (~{pct}%).": [ "app/[lang]/swap/_components/swap-settings.tsx" ], @@ -1843,10 +1837,10 @@ "How it works — xcp.fun": [ "app/[lang]/faq/page.tsx" ], - "How long an unfilled remainder rests before auto-refund. {n} = fill at confirmation or refund next block.": [ + "How long the order rests unfilled before the remainder auto-refunds.": [ "app/[lang]/swap/_components/swap-settings.tsx" ], - "How long the order rests unfilled before the remainder auto-refunds.": [ + "How many blocks an unfilled remainder stays open before expiry. {n} is the shortest setting.": [ "app/[lang]/swap/_components/swap-settings.tsx" ], "How much your own swap moves the price. Larger swaps against the fixed reserves get a worse average price.": [ @@ -1868,7 +1862,7 @@ "If launched": [ "app/[lang]/profile/_components/minting-tab.tsx" ], - "If the pool moves past this before confirmation, the whole transaction is void — nothing is debited; only the miner fee is spent.": [ + "If these limits cannot be met at confirmation, the liquidity operation is invalid. Pool assets are not debited; the Bitcoin network fee is still paid.": [ "app/[lang]/swap/_components/swap-settings.tsx" ], "Image": [ @@ -2403,9 +2397,6 @@ "app/[lang]/activity/_components/activity-view.tsx", "app/[lang]/rewards/_components/earners-table.tsx" ], - "next block after BTC confirms": [ - "app/[lang]/dispense/_components/bridge.tsx" - ], "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.": [ "app/[lang]/rewards/_components/rewards-content.tsx" ], @@ -2788,6 +2779,9 @@ "app/[lang]/[asset]/_components/activity-tabs.tsx", "app/[lang]/profile/_components/minting-tab.tsx" ], + "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.": [ + "app/[lang]/swap/_components/swap-widget.tsx" + ], "People with xcp.fun open right now, including you. Several tabs from the same browser count once. Closing the tab removes you.": [ "components/site-presence.tsx" ], @@ -3262,6 +3256,9 @@ "Slippage": [ "app/[lang]/docs/page.tsx" ], + "Slippage tolerance of {pct}% permits receiving less than quoted.": [ + "app/[lang]/swap/_components/swap-settings.tsx" + ], "Slippage: {pct}%": [ "app/[lang]/swap/_components/swap-widget.tsx" ], @@ -3743,6 +3740,9 @@ "When do I get paid?": [ "app/[lang]/rewards/_components/rewards-content.tsx" ], + "when the BTC payment confirms": [ + "app/[lang]/dispense/_components/bridge.tsx" + ], "Where do I get the XCP to mint with?": [ "app/[lang]/faq/page.tsx" ], diff --git a/apps/web/src/locales/es.json b/apps/web/src/locales/es.json index c6e81b3a..63226c39 100644 --- a/apps/web/src/locales/es.json +++ b/apps/web/src/locales/es.json @@ -1,7 +1,7 @@ { "— minters": "— acuñadores", "· {n} pending": "· {n} pendientes", - "· {pct}% over floor": "· {pct}% sobre el piso", + "· {pct}% over floor": "· {pct}% sobre el mínimo", "· {wallets} wallets · {txs} txs": "· {wallets} billeteras · {txs} tx", "· above the mempool estimate": "· por encima de la estimación de la mempool", "· adjusts to {n}": "· se ajusta a {n}", @@ -178,7 +178,7 @@ "+{n} hidden — purchase pending in mempool": "+{n} ocultos — compra pendiente en la mempool", "+{n} more below.": "+{n} más abajo.", "+{n} more on the explorer ↗": "+{n} más en el explorador ↗", - "+{pct}% over floor · waits": "+{pct}% sobre el piso · espera", + "+{pct}% over floor · waits": "+{pct}% sobre el mínimo · en espera", "<0.01% of pool": "<0.01% del pool", "~{duration}": "~{duration}", "~{n} days": "~{n} días", @@ -195,7 +195,7 @@ "~5 weeks": "~5 semanas", "~6 hours": "~6 horas", "~7 days": "~7 días", - "−{pct}% under floor · sells first": "−{pct}% bajo el piso · se vende primero", + "−{pct}% under floor · sells first": "−{pct}% bajo el mínimo · se vende primero", "≈ {amount} · graduated": "≈ {amount} · graduados", "≈ {amount} · graduated coins": "≈ {amount} · monedas graduadas", "≈ {amount} committed": "≈ {amount} comprometidos", @@ -211,7 +211,7 @@ "💀 RIP": "💀 RIP", "0.01 XCP / 1,000 · sells out or refunds": "0.01 XCP / 1,000 · se agota o se reembolsa", "0.01 XCP per 1,000-token lot": "0.01 XCP por lote de 1,000 tokens", - "0% — rests until a counter-order takes it": "0% — queda en espera hasta que una contraorden la tome", + "0% — rests until a counter-order takes it": "0% — queda pendiente hasta cruzarse con una orden opuesta", "1 · The launch is binary": "1 · El lanzamiento es binario", "1,000,000 tokens (10 XCP)": "1,000,000 de tokens (10 XCP)", "100,000,000 — locked at close": "100,000,000 — bloqueado al cierre", @@ -230,9 +230,7 @@ "A new way to launch a coin on Counterparty — every launch identical, enforced by consensus.": "Una nueva forma de lanzar una moneda en Counterparty — cada lanzamiento idéntico, impuesto por consenso.", "A pool never runs out, it only gets worse. This is what it can absorb before its price moves 1%.": "Un pool nunca se agota, solo empeora. Esto es lo que puede absorber antes de que su precio se mueva un 1%.", "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.": "Un lanzamiento reembolsado no es dinero perdido. XCP-69 es todo o nada: el que no alcanza su soft cap devuelve cada satoshi por consenso, sin que nosotros decidamos nada y sin que nadie pueda quedárselo.", - "A resting order refunds in full at expiry — the price you set is enforced by the order itself.": "Una orden en espera se reembolsa por completo al vencer — el precio que fijas lo hace cumplir la propia orden.", "About": "Acerca de", - "Above what the pending orders would leave. If they confirm first, this order rests for a block and refunds instead of filling — raise the slippage or use Auto.": "Supera lo que dejarían las órdenes pendientes. Si se confirman antes, esta orden queda en espera un bloque y se reembolsa en vez de ejecutarse — sube el deslizamiento o usa Auto.", "Across {n} fully exited position": "En {n} posición cerrada por completo", "Across {n} fully exited positions": "En {n} posiciones cerradas por completo", "across {n} open launch": "en {n} lanzamiento abierto", @@ -277,6 +275,7 @@ "Announced before minting opens": "Anunciado antes de que abra la acuñación", "announced, not open": "anunciados, aún no abiertos", "Any future block, {floor} — tighter than the presets go. Under ~2 hours pays double the network fee.": "Cualquier bloque futuro, {floor} — más ajustado que los valores preestablecidos. Con menos de ~2 horas se paga el doble de comisión de red.", + "Any unfilled amount is returned at expiry. The order enforces your limit price.": "La cantidad sin ejecutar se devuelve al vencer. La orden respeta tu precio límite.", "API base:": "Base de la API:", "Are all Counterparty fairminters like this?": "¿Todos los fairminters de Counterparty son así?", "Arrival": "Llegada", @@ -284,7 +283,7 @@ "Asset": "Activo", "at ATH": "en su máximo histórico", "at least ~1 hour out": "al menos ~1 hora de anticipación", - "at the floor": "al piso", + "at the floor": "al precio mínimo", "at the live pool price": "al precio actual del pool", "Auto": "Auto", "Auto ({code})": "Auto ({code})", @@ -314,7 +313,6 @@ "bought without minting": "compraron sin acuñar", "Broadcast": "Transmitir", "Broadcast as {tx}. Minting opens at block {block} ({when}) — until then the launch is announced on-chain and nobody, you included, can mint. Then it runs for 1,000 blocks (~7 days): it sells out, or everyone is refunded.": "Transmitido como {tx}. La acuñación abre en el bloque {block} ({when}) — hasta entonces el lanzamiento está anunciado en la cadena y nadie, tú incluido, puede acuñar. Después corre durante 1,000 bloques (~7 días): se agota, o todos reciben su reembolso.", - "Broadcast legs are final — each is its own transaction. Failed legs can be retried.": "Los tramos transmitidos son definitivos — cada uno es su propia transacción. Los tramos fallidos se pueden reintentar.", "Broadcast, waiting to confirm. Its page and editing open up once it lands in a block.": "Transmitido, esperando confirmación. Su página y la edición se habilitan cuando entra en un bloque.", "Broadcasting commit…": "Transmitiendo el commit…", "Broadcasting reveal…": "Transmitiendo el reveal…", @@ -389,8 +387,8 @@ "Completed and confirming reward batches, linked to the transactions that sent them.": "Lotes de recompensas completados y en confirmación, enlazados a las transacciones que los enviaron.", "Compose failed ({status})": "Falló la composición ({status})", "Composing transactions": "Componer transacciones", - "composing…": "componiendo…", - "Composing…": "Componiendo…", + "composing…": "preparando transacción…", + "Composing…": "Preparando transacción…", "Confirm commit in wallet…": "Confirma el commit en tu billetera…", "confirm in wallet ({i} of {n})": "confirma en la billetera ({i} de {n})", "Confirm in wallet…": "Confirma en tu billetera…", @@ -480,6 +478,7 @@ "Dumpers": "Dumpers", "Dumping now": "Dumpeando ahora", "Each number counts unique addresses. A sale is a pool or order-book sale captured by xcp.fun. Dump means the address sold within {blocks} blocks of graduation. A meaningful balance is more than one token and more than 1% of the address's acquired amount.": "Cada número cuenta direcciones únicas. Una venta es una venta en el pool o en el libro de órdenes registrada por xcp.fun. Dump significa que la dirección vendió dentro de los {blocks} bloques posteriores a la graduación. Un saldo relevante es más de un token y más del 1% de lo que la dirección adquirió.", + "Each route is a separate transaction. Broadcast routes cannot be canceled here. Failed routes can be retried.": "Cada ruta es una transacción independiente. Las rutas ya transmitidas no se pueden cancelar aquí. Puedes reintentar las que fallaron.", "Earned": "Ganado", "Earned so far": "Ganado hasta ahora", "Edit token info": "Editar info del token", @@ -492,6 +491,7 @@ "escrowed": "en custodia", "escrowed until launch": "en custodia hasta el lanzamiento", "Escrowed until the launch resolves: tokens if it sells out, full XCP refund if it doesn't.": "En custodia hasta que el lanzamiento se resuelva: tokens si se agota, reembolso completo en XCP si no.", + "Estimated fill": "Ejecución estimada", "Event": "Evento", "Every launch that conforms to XCP-69, and what has actually been minted.": "Todos los lanzamientos que cumplen con XCP-69, y lo que de verdad se ha acuñado.", "Every LP token is burned — this liquidity can never be withdrawn.": "Todos los tokens LP están quemados — esta liquidez nunca podrá retirarse.", @@ -530,14 +530,13 @@ "filled": "ejecutada", "Filled": "Ejecutada", "Filled / expires": "Ejecutada / vence", - "Fills now": "Se ejecuta ahora", "Find a launch by asset name, and sort or filter the results.": "Busca un lanzamiento por nombre de activo y ordena o filtra los resultados.", "first at this price": "primero a este precio", "first launch": "primer lanzamiento", "First seen": "Visto por primera vez", - "Floor": "Piso", + "Floor": "Precio mínimo", "Floor price": "Precio piso", - "Floor: {n}": "Piso: {n}", + "Floor: {n}": "Precio mínimo: {n}", "Follow language": "Seguir el idioma", "Full 1M bags sold before yours": "Bolsas completas de 1M vendidas antes que la tuya", "Full specification with raw compose values and the conformance predicate: {spec}. Conformance is checked field-by-field on-chain — this site lists only launches that match exactly.": "Especificación completa con los valores brutos de compose y el predicado de conformidad: {spec}. La conformidad se verifica campo por campo en la cadena — este sitio solo lista lanzamientos que coinciden exactamente.", @@ -562,7 +561,6 @@ "held by {n} sellers": "en manos de {n} vendedores", "Hide filled": "Ocultar ejecutadas", "Hide minted": "Ocultar acuñados", - "High slippage authorizes up to {pct}% price impact.": "Un deslizamiento alto autoriza hasta {pct}% de impacto en el precio.", "Higher than this trade needs (~{pct}%).": "Más de lo que esta operación necesita (~{pct}%).", "hold one, made none": "tienen una, no crearon ninguna", "Holders": "Holders", @@ -575,15 +573,15 @@ "how it works": "cómo funciona", "How it works": "Cómo funciona", "How it works — xcp.fun": "Cómo funciona — xcp.fun", - "How long an unfilled remainder rests before auto-refund. {n} = fill at confirmation or refund next block.": "Cuánto tiempo espera el resto sin ejecutar antes del reembolso automático. {n} = se ejecuta al confirmarse o se reembolsa en el siguiente bloque.", "How long the order rests unfilled before the remainder auto-refunds.": "Cuánto tiempo espera la orden sin ejecutarse antes de que el resto se reembolse automáticamente.", + "How many blocks an unfilled remainder stays open before expiry. {n} is the shortest setting.": "Cuántos bloques permanece abierta la cantidad sin ejecutar antes de vencer. {n} es el plazo más corto.", "How much your own swap moves the price. Larger swaps against the fixed reserves get a worse average price.": "Cuánto mueve el precio tu propio swap. Los swaps más grandes contra las reservas fijas obtienen un peor precio promedio.", "How that works": "Cómo funciona", "How XCP-69 works": "Cómo funciona XCP-69", "If it fails to launch": "Si el lanzamiento falla", "If it's yours, connect that wallet to launch with it.": "Si es tuyo, conecta esa billetera para lanzar con él.", "If launched": "Si se lanza", - "If the pool moves past this before confirmation, the whole transaction is void — nothing is debited; only the miner fee is spent.": "Si el pool supera este valor antes de la confirmación, toda la transacción queda anulada — no se debita nada; solo se gasta la comisión de minería.", + "If these limits cannot be met at confirmation, the liquidity operation is invalid. Pool assets are not debited; the Bitcoin network fee is still paid.": "Si al confirmarse no se cumplen estos límites, la operación de liquidez no se ejecuta. No se debitan los activos del pool; sí se paga la comisión de red de Bitcoin.", "Image": "Imagen", "In {n} pool": "En {n} pool", "In {n} pools": "En {n} pools", @@ -669,7 +667,7 @@ "locked forever, LP burned": "bloqueada para siempre, LP quemado", "log": "log", "Lots": "Lotes", - "LP fee": "Comisión del LP", + "LP fee": "Comisión de liquidez", "LP minted (est.)": "LP acuñados (est.)", "LP positions": "Posiciones LP", "LP token {asset}": "Token LP {asset}", @@ -693,7 +691,7 @@ "Method and limits": "Método y límites", "Min LP · slippage {pct}%": "LP mín. · deslizamiento {pct}%", "Min received": "Mínimo a recibir", - "Min received · slippage {pct}%": "Mínimo recibido · deslizamiento {pct}%", + "Min received · slippage {pct}%": "Mínimo a recibir · deslizamiento {pct}%", "Minimum 1 XCP": "Mínimo 1 XCP", "Mint": "Acuñación", "Mint {amount} {asset}": "Acuñar {amount} {asset}", @@ -745,7 +743,6 @@ "New buyers": "Compradores nuevos", "Newest": "Más recientes", "Next →": "Siguiente →", - "next block after BTC confirms": "el bloque siguiente a la confirmación del BTC", "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.": "No — una acuñación deja XCP en custodia contra el lanzamiento. Si se gradúa, te quedas con los tokens; si se reembolsa, recuperas cada satoshi. Lo único que una acuñación te cuesta de verdad es la comisión de Bitcoin — que es justo la parte que cubre este programa.", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.": "No — y ese es el punto. El protocolo de fairminters permite una variación enorme: sin soft cap, sin reembolsos, comisiones para el creador de hasta el 99%, preminados, sin pool. XCP-69 fija una sola configuración, y este sitio solo lista lanzamientos que coinciden exactamente con el estándar, verificados campo por campo contra el registro de la cadena. Aquí, cada lanzamiento se comporta igual y nunca tienes que auditar los parámetros tú mismo. Un fairminter que encuentres en cualquier otro lado no trae esa garantía — lee sus campos antes de acuñar.", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.": "No — es estructural, no prometida. El pool tiene que abrir a 69/31 del precio de acuñación porque esas son las únicas cantidades que existen, pero el piso baja a medida que la gente le vende. Nada impide que un token se negocie por debajo del precio de acuñación. Lo que el pool garantiza es una oferta de compra que nunca llega a cero, no una que te vaya a gustar.", @@ -870,6 +867,7 @@ "Paste the profile URL or enter the handle.": "Pega la URL del perfil o escribe el usuario.", "Payouts": "Pagos", "pending": "pendiente", + "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.": "Las órdenes pendientes pueden dejar menos que tu mínimo a recibir. Si se confirman antes, la cantidad sin ejecutar queda pendiente hasta vencer y luego se devuelve. Revisa el deslizamiento o usa Auto.", "People with xcp.fun open right now, including you. Several tabs from the same browser count once. Closing the tab removes you.": "Personas con xcp.fun abierto ahora mismo, incluyéndote. Varias pestañas del mismo navegador cuentan una sola vez. Al cerrar la pestaña dejas de contar.", "Per address": "Por dirección", "Per mint": "Por acuñación", @@ -967,7 +965,7 @@ "route no longer has enough left": "a la ruta ya no le queda suficiente", "route price changed": "el precio de la ruta cambió", "Routes": "Rutas", - "Routing includes miner fees — a deep route can beat a cheaper, shallower one.": "El enrutamiento incluye las comisiones de minería — una ruta profunda puede ganarle a otra más barata pero con menos fondo.", + "Routing includes miner fees — a deep route can beat a cheaper, shallower one.": "La ruta incluye las comisiones de minería. Una ruta con más liquidez puede costar menos en total que otra con mejor precio y menos liquidez.", "Sale closing": "Cerrando la venta", "Save failed": "No se pudo guardar", "Saved. Changes are live; other open pages may take a minute to refresh.": "Guardado. Los cambios ya están en vivo; otras páginas abiertas pueden tardar un minuto en actualizarse.", @@ -1017,6 +1015,7 @@ "Size": "Tamaño", "Skipped: {reason}": "Omitida: {reason}", "Slippage": "Deslizamiento", + "Slippage tolerance of {pct}% permits receiving less than quoted.": "Una tolerancia al deslizamiento del {pct}% permite recibir menos de lo cotizado.", "Slippage: {pct}%": "Deslizamiento: {pct}%", "soft cap not reached": "soft cap not reached", "Sold": "Venta", @@ -1039,13 +1038,13 @@ "Supply": "Suministro", "Support & terms": "Soporte y términos", "swap": "intercambiar", - "Swap": "Swap", + "Swap": "Intercambiar", "Swap — xcp.fun": "Swap — xcp.fun", "Swap {give} for {get}": "Intercambiar {give} por {get}", "Swap {give} for {get} — market order": "Intercambiar {give} por {get} — orden de mercado", - "Swap broadcast — {tx}": "Swap transmitido — {tx}", + "Swap broadcast — {tx}": "Intercambio transmitido — {tx}", "Swap graduated XCP-69 assets through Counterparty pools and the order book, including direct token pairs when liquidity exists.": "Intercambia activos XCP-69 graduados a través de los pools de Counterparty y el libro de órdenes, incluidos pares directos entre tokens cuando hay liquidez.", - "Swap settings": "Ajustes del swap", + "Swap settings": "Ajustes del intercambio", "Switch wallet": "Cambiar de billetera", "Table view": "Vista de tabla", "Takes effect when it confirms. {tx}": "Surte efecto al confirmarse. {tx}", @@ -1141,11 +1140,11 @@ "Upcoming": "Próximo", "Upload failed": "No se pudo subir", "Upload token image": "Subir la imagen del token", - "Use digits and a dot, with up to {n} decimal places.": "Usa dígitos y un punto, con un máximo de {n} decimales.", + "Use digits and a dot, with up to {n} decimal places.": "Usa 0-9 y punto decimal; hasta {n} decimales.", "Use whole numbers only.": "Usa solo números enteros.", "Value": "Valor", "Value and underlying amounts are what withdrawing the LP tokens now would return. XCP value is available when XCP is one side of the pair. LP profit and loss is not estimated.": "El valor y los montos subyacentes son lo que devolvería retirar los tokens LP ahora. El valor en XCP está disponible cuando XCP es uno de los lados del par. No se estima la ganancia o pérdida del LP.", - "Vends": "Vende", + "Vends": "Ventas disponibles", "Venue": "Mercado", "Venue / time": "Mercado / hora", "Verdict pending": "Veredicto pendiente", @@ -1169,6 +1168,7 @@ "What xcp.fun shows for this launch, and what the hosted metadata says if your on-chain description points here. The on-chain field itself is locked forever; everything behind it is yours to curate.": "Lo que xcp.fun muestra de este lanzamiento y lo que dicen los metadatos alojados si tu descripción en cadena apunta aquí. El campo en cadena queda bloqueado para siempre; todo lo que hay detrás lo curas tú.", "When": "Cuándo", "When do I get paid?": "¿Cuándo me pagan?", + "when the BTC payment confirms": "al confirmarse el pago en BTC", "Where do I get the XCP to mint with?": "¿De dónde saco el XCP para acuñar?", "Where does the token art actually live?": "¿Dónde vive realmente el arte del token?", "Which Counterparty collections the minters come from. Creators made a card there; collectors hold one.": "De qué colecciones de Counterparty vienen los acuñadores. Los creadores hicieron una carta ahí; los coleccionistas tienen una.", @@ -1210,7 +1210,7 @@ "you": "tú", "you · {n} XCP": "tú · {n} XCP", "you have minted this launch’s max": "ya acuñaste el máximo de este lanzamiento", - "You mint": "Recibes", + "You mint": "Acuñas", "You own {name}, but {reason}.": "{name} es tuyo, pero {reason}.", "You pay": "Pagas", "You receive": "Recibes", diff --git a/apps/web/src/locales/es.status.json b/apps/web/src/locales/es.status.json index 44f3b5b5..a03c30b2 100644 --- a/apps/web/src/locales/es.status.json +++ b/apps/web/src/locales/es.status.json @@ -29,10 +29,10 @@ "A new way to launch a coin on Counterparty — every launch identical, enforced by consensus.", "A pool never runs out, it only gets worse. This is what it can absorb before its price moves 1%.", "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.", - "A resting order refunds in full at expiry — the price you set is enforced by the order itself.", + "Any unfilled amount is returned at expiry. The order enforces your limit price.", "API base:", "About", - "Above what the pending orders would leave. If they confirm first, this order rests for a block and refunds instead of filling — raise the slippage or use Auto.", + "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.", "Across {n} fully exited position", "Across {n} fully exited positions", "Across {n} open position", @@ -94,7 +94,7 @@ "Bought", "Broadcast", "Broadcast as {tx}. Minting opens at block {block} ({when}) — until then the launch is announced on-chain and nobody, you included, can mint. Then it runs for 1,000 blocks (~7 days): it sells out, or everyone is refunded.", - "Broadcast legs are final — each is its own transaction. Failed legs can be retried.", + "Each route is a separate transaction. Broadcast routes cannot be canceled here. Failed routes can be retried.", "Broadcast, waiting to confirm. Its page and editing open up once it lands in a block.", "Broadcasting commit…", "Broadcasting reveal…", @@ -256,7 +256,7 @@ "Fetching quote…", "Filled", "Filled / expires", - "Fills now", + "Estimated fill", "Find a launch by asset name, and sort or filter the results.", "First seen", "Floor", @@ -278,7 +278,7 @@ "Hard cap", "Hide filled", "Hide minted", - "High slippage authorizes up to {pct}% price impact.", + "Slippage tolerance of {pct}% permits receiving less than quoted.", "Higher than this trade needs (~{pct}%).", "Holders", "Holders:", @@ -287,14 +287,14 @@ "How XCP-69 works", "How it works", "How it works — xcp.fun", - "How long an unfilled remainder rests before auto-refund. {n} = fill at confirmation or refund next block.", + "How many blocks an unfilled remainder stays open before expiry. {n} is the shortest setting.", "How long the order rests unfilled before the remainder auto-refunds.", "How much your own swap moves the price. Larger swaps against the fixed reserves get a worse average price.", "How that works", "If it fails to launch", "If it's yours, connect that wallet to launch with it.", "If launched", - "If the pool moves past this before confirmation, the whole transaction is void — nothing is debited; only the miner fee is spent.", + "If these limits cannot be met at confirmation, the liquidity operation is invalid. Pool assets are not debited; the Bitcoin network fee is still paid.", "Image", "In XCP terms, exactly. In fiat terms, only if XCP's price held during the ~week-long window. Refunds return quantity, not value.", "In mempool", @@ -918,7 +918,7 @@ "named assets only, divisible", "need XCP?", "new address", - "next block after BTC confirms", + "when the BTC payment confirms", "no history", "none — the creator mints like everyone else", "none — you already own this name", diff --git a/apps/web/src/locales/fr.json b/apps/web/src/locales/fr.json index 71f1b550..ca40e55f 100644 --- a/apps/web/src/locales/fr.json +++ b/apps/web/src/locales/fr.json @@ -230,9 +230,7 @@ "A new way to launch a coin on Counterparty — every launch identical, enforced by consensus.": "Une nouvelle façon de lancer un coin sur Counterparty — chaque lancement identique, garanti par le consensus.", "A pool never runs out, it only gets worse. This is what it can absorb before its price moves 1%.": "Un pool ne se vide jamais, il se dégrade seulement. Voici ce qu'il peut absorber avant que son prix ne bouge de 1 %.", "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.": "Un lancement remboursé n'est pas de l'argent perdu. XCP-69 est tout ou rien : celui qui n'atteint pas son soft cap restitue chaque satoshi par consensus, sans aucune décision de notre part et sans que personne ne puisse le garder.", - "A resting order refunds in full at expiry — the price you set is enforced by the order itself.": "Un ordre en attente est intégralement remboursé à son expiration — le prix que vous fixez est garanti par l'ordre lui-même.", "About": "À propos", - "Above what the pending orders would leave. If they confirm first, this order rests for a block and refunds instead of filling — raise the slippage or use Auto.": "Au-delà de ce que laisseraient les ordres en attente. S'ils sont confirmés avant, cet ordre attend un bloc puis est remboursé au lieu d'être exécuté — augmentez le slippage ou utilisez Auto.", "Across {n} fully exited position": "Sur {n} position entièrement clôturée", "Across {n} fully exited positions": "Sur {n} positions entièrement clôturées", "across {n} open launch": "sur {n} lancement ouvert", @@ -277,6 +275,7 @@ "Announced before minting opens": "Annoncé avant l'ouverture du mint", "announced, not open": "annoncés, pas encore ouverts", "Any future block, {floor} — tighter than the presets go. Under ~2 hours pays double the network fee.": "N'importe quel bloc futur, {floor} — plus serré que les préréglages. Sous ~2 heures, les frais de réseau sont doublés.", + "Any unfilled amount is returned at expiry. The order enforces your limit price.": "Toute quantité non exécutée est restituée à expiration. L’ordre respecte votre prix limite.", "API base:": "Base de l'API :", "Are all Counterparty fairminters like this?": "Tous les fairminters Counterparty sont-ils comme ça ?", "Arrival": "Arrivée", @@ -314,7 +313,6 @@ "bought without minting": "ont acheté sans minter", "Broadcast": "Diffuser", "Broadcast as {tx}. Minting opens at block {block} ({when}) — until then the launch is announced on-chain and nobody, you included, can mint. Then it runs for 1,000 blocks (~7 days): it sells out, or everyone is refunded.": "Diffusé sous {tx}. Le mint ouvre au bloc {block} ({when}) — d'ici là, le lancement est annoncé on-chain et personne, vous compris, ne peut minter. Il court ensuite 1 000 blocs (~7 jours) : tout se vend, ou tout le monde est remboursé.", - "Broadcast legs are final — each is its own transaction. Failed legs can be retried.": "Les tronçons diffusés sont définitifs — chacun est une transaction à part entière. Les tronçons échoués peuvent être relancés.", "Broadcast, waiting to confirm. Its page and editing open up once it lands in a block.": "Diffusé, en attente de confirmation. Sa page et la modification s'ouvrent dès qu'il entre dans un bloc.", "Broadcasting commit…": "Diffusion du commit…", "Broadcasting reveal…": "Diffusion du reveal…", @@ -480,6 +478,7 @@ "Dumpers": "Dumpers", "Dumping now": "En train de dumper", "Each number counts unique addresses. A sale is a pool or order-book sale captured by xcp.fun. Dump means the address sold within {blocks} blocks of graduation. A meaningful balance is more than one token and more than 1% of the address's acquired amount.": "Chaque nombre compte des adresses uniques. Une vente est une vente dans le pool ou le carnet d'ordres enregistrée par xcp.fun. Dump signifie que l'adresse a vendu dans les {blocks} blocs suivant la graduation. Un solde significatif, c'est plus d'un token et plus de 1 % de ce que l'adresse a acquis.", + "Each route is a separate transaction. Broadcast routes cannot be canceled here. Failed routes can be retried.": "Chaque route est une transaction distincte. Les routes déjà diffusées ne peuvent pas être annulées ici. Les routes ayant échoué peuvent être réessayées.", "Earned": "Gagné", "Earned so far": "Gagné jusqu'ici", "Edit token info": "Modifier les infos du token", @@ -492,6 +491,7 @@ "escrowed": "sous séquestre", "escrowed until launch": "sous séquestre jusqu'au lancement", "Escrowed until the launch resolves: tokens if it sells out, full XCP refund if it doesn't.": "Sous séquestre jusqu'à l'issue du lancement : les tokens si tout est vendu, remboursement intégral en XCP sinon.", + "Estimated fill": "Exécution estimée", "Event": "Événement", "Every launch that conforms to XCP-69, and what has actually been minted.": "Tous les lancements conformes à XCP-69, et ce qui a réellement été minté.", "Every LP token is burned — this liquidity can never be withdrawn.": "Tous les tokens LP sont brûlés — cette liquidité ne pourra jamais être retirée.", @@ -530,7 +530,6 @@ "filled": "exécutée", "Filled": "Exécuté", "Filled / expires": "Exécuté / expire", - "Fills now": "Exécuté maintenant", "Find a launch by asset name, and sort or filter the results.": "Trouvez un lancement par nom d'actif, puis triez ou filtrez les résultats.", "first at this price": "premier à ce prix", "first launch": "premier lancement", @@ -562,7 +561,6 @@ "held by {n} sellers": "détenus par {n} vendeurs", "Hide filled": "Masquer les exécutés", "Hide minted": "Masquer les mintés", - "High slippage authorizes up to {pct}% price impact.": "Un slippage élevé autorise jusqu'à {pct} % d'impact sur le prix.", "Higher than this trade needs (~{pct}%).": "Plus que ce dont cette opération a besoin (~{pct} %).", "hold one, made none": "en détiennent une, n'en ont créé aucune", "Holders": "Détenteurs", @@ -575,15 +573,15 @@ "how it works": "comment ça marche", "How it works": "Comment ça marche", "How it works — xcp.fun": "Comment ça marche — xcp.fun", - "How long an unfilled remainder rests before auto-refund. {n} = fill at confirmation or refund next block.": "Combien de temps le reliquat non exécuté attend avant le remboursement automatique. {n} = exécution à la confirmation, sinon remboursement au bloc suivant.", "How long the order rests unfilled before the remainder auto-refunds.": "Combien de temps l'ordre attend sans exécution avant que le reliquat soit remboursé automatiquement.", + "How many blocks an unfilled remainder stays open before expiry. {n} is the shortest setting.": "Nombre de blocs pendant lesquels le reliquat non exécuté reste ouvert avant expiration. {n} est le réglage le plus court.", "How much your own swap moves the price. Larger swaps against the fixed reserves get a worse average price.": "De combien votre propre swap déplace le prix. Les swaps plus importants contre des réserves fixes obtiennent un prix moyen moins bon.", "How that works": "Comment ça marche", "How XCP-69 works": "Comment fonctionne XCP-69", "If it fails to launch": "Si le lancement échoue", "If it's yours, connect that wallet to launch with it.": "S'il est à vous, connectez ce portefeuille pour lancer avec.", "If launched": "Si lancé", - "If the pool moves past this before confirmation, the whole transaction is void — nothing is debited; only the miner fee is spent.": "Si le pool dépasse ce seuil avant la confirmation, toute la transaction est annulée — rien n'est débité ; seuls les frais de minage sont dépensés.", + "If these limits cannot be met at confirmation, the liquidity operation is invalid. Pool assets are not debited; the Bitcoin network fee is still paid.": "Si ces limites ne sont pas respectées à la confirmation, l’opération de liquidité est invalide. Les actifs du pool ne sont pas débités ; les frais du réseau Bitcoin restent dus.", "Image": "Image", "In {n} pool": "Dans {n} pool", "In {n} pools": "Dans {n} pools", @@ -745,7 +743,6 @@ "New buyers": "Nouveaux acheteurs", "Newest": "Plus récents", "Next →": "Suivant →", - "next block after BTC confirms": "le bloc suivant la confirmation des BTC", "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.": "Non — un mint place des XCP sous séquestre contre le lancement. S'il est gradué, vous gardez les tokens ; s'il est remboursé, vous récupérez chaque satoshi. La seule chose qu'un mint vous coûte réellement, ce sont les frais Bitcoin — et c'est justement la part que ce programme couvre.", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.": "Non — et c'est justement le point. Le protocole des fairminters permet d'énormes variations : pas de soft cap, pas de remboursement, des commissions créateur jusqu'à 99 %, des prémines, aucun pool. XCP-69 fixe une seule configuration, et ce site ne liste que les lancements qui correspondent exactement au standard, vérifiés champ par champ contre l'enregistrement on-chain. Ici, chaque lancement se comporte de la même manière et vous n'avez jamais à auditer les paramètres vous-même. Un fairminter croisé ailleurs n'offre aucune garantie de ce genre — lisez ses champs avant de minter.", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.": "Non — c'est structurel, pas promis. Le pool doit ouvrir à 69/31 du prix de mint parce que ce sont les seules quantités qui existent, mais le plancher s'érode à mesure que les gens lui vendent. Rien n'empêche un token de s'échanger sous le prix de mint. Ce que le pool garantit, c'est une offre d'achat qui n'atteint jamais zéro, pas une offre qui vous plaira.", @@ -870,6 +867,7 @@ "Paste the profile URL or enter the handle.": "Collez l'URL du profil ou saisissez l'identifiant.", "Payouts": "Versements", "pending": "en attente", + "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.": "Les ordres en attente peuvent réduire la quantité disponible sous votre minimum à recevoir. S’ils sont confirmés en premier, la partie non exécutée reste ouverte jusqu’à expiration, puis est restituée. Vérifiez le glissement ou choisissez Auto.", "People with xcp.fun open right now, including you. Several tabs from the same browser count once. Closing the tab removes you.": "Personnes ayant xcp.fun ouvert en ce moment, vous compris. Plusieurs onglets du même navigateur comptent une fois. Fermer l'onglet vous retire du compte.", "Per address": "Par adresse", "Per mint": "Par mint", @@ -1017,6 +1015,7 @@ "Size": "Taille", "Skipped: {reason}": "Ignorée : {reason}", "Slippage": "Slippage", + "Slippage tolerance of {pct}% permits receiving less than quoted.": "Une tolérance de glissement de {pct}% permet de recevoir moins que la quantité estimée.", "Slippage: {pct}%": "Slippage : {pct} %", "soft cap not reached": "soft cap not reached", "Sold": "Vente", @@ -1169,6 +1168,7 @@ "What xcp.fun shows for this launch, and what the hosted metadata says if your on-chain description points here. The on-chain field itself is locked forever; everything behind it is yours to curate.": "Ce que xcp.fun affiche pour ce lancement, et ce que disent les métadonnées hébergées si votre description on-chain pointe ici. Le champ on-chain lui-même est verrouillé pour toujours ; tout ce qu'il y a derrière, c'est à vous de le gérer.", "When": "Quand", "When do I get paid?": "Quand suis-je payé ?", + "when the BTC payment confirms": "à la confirmation du paiement en BTC", "Where do I get the XCP to mint with?": "Où trouver les XCP pour minter ?", "Where does the token art actually live?": "Où se trouve réellement l'illustration du token ?", "Which Counterparty collections the minters come from. Creators made a card there; collectors hold one.": "De quelles collections Counterparty viennent les minteurs. Les créateurs y ont créé une carte ; les collectionneurs en détiennent une.", diff --git a/apps/web/src/locales/fr.status.json b/apps/web/src/locales/fr.status.json index 44f3b5b5..a03c30b2 100644 --- a/apps/web/src/locales/fr.status.json +++ b/apps/web/src/locales/fr.status.json @@ -29,10 +29,10 @@ "A new way to launch a coin on Counterparty — every launch identical, enforced by consensus.", "A pool never runs out, it only gets worse. This is what it can absorb before its price moves 1%.", "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.", - "A resting order refunds in full at expiry — the price you set is enforced by the order itself.", + "Any unfilled amount is returned at expiry. The order enforces your limit price.", "API base:", "About", - "Above what the pending orders would leave. If they confirm first, this order rests for a block and refunds instead of filling — raise the slippage or use Auto.", + "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.", "Across {n} fully exited position", "Across {n} fully exited positions", "Across {n} open position", @@ -94,7 +94,7 @@ "Bought", "Broadcast", "Broadcast as {tx}. Minting opens at block {block} ({when}) — until then the launch is announced on-chain and nobody, you included, can mint. Then it runs for 1,000 blocks (~7 days): it sells out, or everyone is refunded.", - "Broadcast legs are final — each is its own transaction. Failed legs can be retried.", + "Each route is a separate transaction. Broadcast routes cannot be canceled here. Failed routes can be retried.", "Broadcast, waiting to confirm. Its page and editing open up once it lands in a block.", "Broadcasting commit…", "Broadcasting reveal…", @@ -256,7 +256,7 @@ "Fetching quote…", "Filled", "Filled / expires", - "Fills now", + "Estimated fill", "Find a launch by asset name, and sort or filter the results.", "First seen", "Floor", @@ -278,7 +278,7 @@ "Hard cap", "Hide filled", "Hide minted", - "High slippage authorizes up to {pct}% price impact.", + "Slippage tolerance of {pct}% permits receiving less than quoted.", "Higher than this trade needs (~{pct}%).", "Holders", "Holders:", @@ -287,14 +287,14 @@ "How XCP-69 works", "How it works", "How it works — xcp.fun", - "How long an unfilled remainder rests before auto-refund. {n} = fill at confirmation or refund next block.", + "How many blocks an unfilled remainder stays open before expiry. {n} is the shortest setting.", "How long the order rests unfilled before the remainder auto-refunds.", "How much your own swap moves the price. Larger swaps against the fixed reserves get a worse average price.", "How that works", "If it fails to launch", "If it's yours, connect that wallet to launch with it.", "If launched", - "If the pool moves past this before confirmation, the whole transaction is void — nothing is debited; only the miner fee is spent.", + "If these limits cannot be met at confirmation, the liquidity operation is invalid. Pool assets are not debited; the Bitcoin network fee is still paid.", "Image", "In XCP terms, exactly. In fiat terms, only if XCP's price held during the ~week-long window. Refunds return quantity, not value.", "In mempool", @@ -918,7 +918,7 @@ "named assets only, divisible", "need XCP?", "new address", - "next block after BTC confirms", + "when the BTC payment confirms", "no history", "none — the creator mints like everyone else", "none — you already own this name", diff --git a/apps/web/src/locales/ja.json b/apps/web/src/locales/ja.json index 89eb52c7..67292241 100644 --- a/apps/web/src/locales/ja.json +++ b/apps/web/src/locales/ja.json @@ -1,7 +1,7 @@ { "— minters": "— ミンター", - "· {n} pending": "· {n} 件保留中", - "· {pct}% over floor": "· フロアより {pct}% 上", + "· {n} pending": "· {n} 件承認待ち", + "· {pct}% over floor": "· 最安売値より {pct}% 高い", "· {wallets} wallets · {txs} txs": "· {wallets} ウォレット · {txs} tx", "· above the mempool estimate": "· メンプール推定より上", "· adjusts to {n}": "· {n} に調整", @@ -178,7 +178,7 @@ "+{n} hidden — purchase pending in mempool": "+{n} 件非表示 — メンプールで購入保留中", "+{n} more below.": "他 {n} 件は下に。", "+{n} more on the explorer ↗": "他 {n} 件はエクスプローラーで ↗", - "+{pct}% over floor · waits": "フロアより +{pct}% · 待機", + "+{pct}% over floor · waits": "最安売値より +{pct}% · 待機", "<0.01% of pool": "プールの 0.01% 未満", "~{duration}": "約{duration}", "~{n} days": "約 {n} 日", @@ -195,7 +195,7 @@ "~5 weeks": "約 5 週間", "~6 hours": "約 6 時間", "~7 days": "約 7 日", - "−{pct}% under floor · sells first": "フロアより −{pct}% · 先に売れる", + "−{pct}% under floor · sells first": "最安売値より −{pct}% · 先に売却", "≈ {amount} · graduated": "≈ {amount} · 卒業済み", "≈ {amount} · graduated coins": "≈ {amount} · 卒業銘柄", "≈ {amount} committed": "≈ {amount} 投入済み", @@ -211,7 +211,7 @@ "💀 RIP": "💀 RIP", "0.01 XCP / 1,000 · sells out or refunds": "1,000 枚あたり 0.01 XCP · 完売か返金か", "0.01 XCP per 1,000-token lot": "1,000 トークンのロットあたり 0.01 XCP", - "0% — rests until a counter-order takes it": "0% — 反対注文が取るまで待機", + "0% — rests until a counter-order takes it": "0% — 反対売買の注文と約定するまで待機", "1 · The launch is binary": "1 · ローンチは二択", "1,000,000 tokens (10 XCP)": "1,000,000 トークン(10 XCP)", "100,000,000 — locked at close": "100,000,000 — 終了時にロック", @@ -230,9 +230,7 @@ "A new way to launch a coin on Counterparty — every launch identical, enforced by consensus.": "Counterparty でコインをローンチする新しい方法 — すべてのローンチが同一で、コンセンサスが強制します。", "A pool never runs out, it only gets worse. This is what it can absorb before its price moves 1%.": "プールは枯渇せず、条件が悪くなるだけです。これは価格が 1% 動くまでに吸収できる量です。", "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.": "返金されたローンチはお金を失ったわけではありません。XCP-69 はオール・オア・ナッシングです: ソフトキャップに届かなかったローンチはコンセンサスにより 1 satoshi 残らず返し、私たちの判断は介在せず、誰にも保持する手段はありません。", - "A resting order refunds in full at expiry — the price you set is enforced by the order itself.": "待機注文は期限切れで全額返金されます — 指定した価格は注文そのものが保証します。", "About": "概要", - "Above what the pending orders would leave. If they confirm first, this order rests for a block and refunds instead of filling — raise the slippage or use Auto.": "保留中の注文が残す量を超えています。先に承認されると、この注文は 1 ブロック待機した後、約定せず返金されます — スリッページを上げるか自動にしてください。", "Across {n} fully exited position": "完全に手仕舞いした {n} ポジション", "Across {n} fully exited positions": "完全に手仕舞いした {n} ポジション", "across {n} open launch": "進行中の {n} ローンチ", @@ -268,7 +266,7 @@ "Amount of {asset} to sell": "売却する {asset} の数量", "Amount of XCP to deposit": "預け入れる XCP の数量", "Amount to remove": "引き出す数量", - "Amount too small — rounds to 0": "数量が小さすぎます — 0 に丸められます", + "Amount too small — rounds to 0": "数量が小さすぎるため、計算結果が 0 になります", "An XCP bounty for the first three launches to graduate, and MINTS for every mint.": "最初に卒業した 3 つのローンチへの XCP バウンティと、すべてのミントへの MINTS。", "an XCP fairminter on xcp.fun": "xcp.fun の XCP フェアミンター", "Animated WEBP shows as a still frame in the announce channel — Telegram cannot play one. A GIF moves there too.": "アニメーション WEBP は告知チャンネルでは静止画になります — Telegram は再生できません。GIF なら動きます。", @@ -277,6 +275,7 @@ "Announced before minting opens": "ミント開始前に告知", "announced, not open": "告知済み、未開始", "Any future block, {floor} — tighter than the presets go. Under ~2 hours pays double the network fee.": "未来の任意のブロック、{floor} — プリセットより細かく指定できます。約 2 時間未満ではネットワーク手数料が 2 倍になります。", + "Any unfilled amount is returned at expiry. The order enforces your limit price.": "未約定分は有効期限が切れると返還されます。注文には指定した指値が適用されます。", "API base:": "API ベース:", "Are all Counterparty fairminters like this?": "Counterparty のフェアミンターはすべてこうなのですか?", "Arrival": "到着", @@ -284,7 +283,7 @@ "Asset": "アセット", "at ATH": "ATH 到達", "at least ~1 hour out": "少なくとも約 1 時間先", - "at the floor": "フロア価格で", + "at the floor": "最安売値で", "at the live pool price": "現在のプール価格で", "Auto": "自動", "Auto ({code})": "自動 ({code})", @@ -314,7 +313,6 @@ "bought without minting": "ミントせずに購入", "Broadcast": "ブロードキャスト", "Broadcast as {tx}. Minting opens at block {block} ({when}) — until then the launch is announced on-chain and nobody, you included, can mint. Then it runs for 1,000 blocks (~7 days): it sells out, or everyone is refunded.": "{tx} としてブロードキャストしました。ミントはブロック {block}({when})に開きます — それまでローンチはオンチェーンで告知された状態で、あなたを含め誰もミントできません。その後 1,000 ブロック(約 7 日)走ります: 完売するか、全員に返金されます。", - "Broadcast legs are final — each is its own transaction. Failed legs can be retried.": "ブロードキャスト済みのレグは確定です — それぞれ独立したトランザクションです。失敗したレグは再試行できます。", "Broadcast, waiting to confirm. Its page and editing open up once it lands in a block.": "ブロードキャスト済み、承認待ち。ブロックに入るとページと編集が使えるようになります。", "Broadcasting commit…": "コミットをブロードキャスト中…", "Broadcasting reveal…": "リビールをブロードキャスト中…", @@ -389,8 +387,8 @@ "Completed and confirming reward batches, linked to the transactions that sent them.": "完了済みおよび承認中の報酬バッチ。送金したトランザクションにリンクしています。", "Compose failed ({status})": "作成に失敗しました ({status})", "Composing transactions": "トランザクションの作成", - "composing…": "作成中…", - "Composing…": "作成中…", + "composing…": "取引を作成中…", + "Composing…": "取引を作成中…", "Confirm commit in wallet…": "ウォレットでコミットを承認…", "confirm in wallet ({i} of {n})": "ウォレットで承認 ({n} 件中 {i})", "Confirm in wallet…": "ウォレットで承認してください…", @@ -444,7 +442,7 @@ "Creators": "クリエイター", "credited": "入金済み", "Currency": "通貨", - "Currently unloading:": "現在アンロード中:", + "Currently unloading:": "現在販売中:", "Custom": "カスタム", "custom block": "カスタムブロック", "Custom liquidity slippage percent": "流動性スリッページ(%)を指定", @@ -468,7 +466,7 @@ "distinct addresses": "異なるアドレス", "Distribution": "配布", "Distributions": "配布", - "Docs": "Docs", + "Docs": "ドキュメント", "Docs — xcp.fun": "ドキュメント — xcp.fun", "Does a refund make me whole?": "返金で元通りになりますか?", "Doesn't minting cost me the XCP?": "ミントすると XCP を失うのでは?", @@ -480,6 +478,7 @@ "Dumpers": "ダンパー", "Dumping now": "ダンプ中", "Each number counts unique addresses. A sale is a pool or order-book sale captured by xcp.fun. Dump means the address sold within {blocks} blocks of graduation. A meaningful balance is more than one token and more than 1% of the address's acquired amount.": "各数値はユニークアドレスを数えます。売却とは xcp.fun が捕捉したプールまたは板での売却です。ダンプとは、そのアドレスが卒業から {blocks} ブロック以内に売却したことを意味します。意味のある残高とは 1 トークン超、かつそのアドレスの取得量の 1% 超です。", + "Each route is a separate transaction. Broadcast routes cannot be canceled here. Failed routes can be retried.": "各ルートは独立したトランザクションです。ブロードキャスト済みのルートはここでは取り消せません。失敗したルートは再試行できます。", "Earned": "獲得", "Earned so far": "これまでの獲得", "Edit token info": "トークン情報を編集", @@ -492,6 +491,7 @@ "escrowed": "エスクロー中", "escrowed until launch": "ローンチまでエスクロー", "Escrowed until the launch resolves: tokens if it sells out, full XCP refund if it doesn't.": "ローンチの結果が出るまでエスクローされます。完売すればトークン、そうでなければ XCP を全額返金。", + "Estimated fill": "約定見込み", "Event": "イベント", "Every launch that conforms to XCP-69, and what has actually been minted.": "XCP-69 に準拠するすべてのローンチと、実際にミントされたもの。", "Every LP token is burned — this liquidity can never be withdrawn.": "LP トークンはすべてバーン済み — この流動性は二度と引き出せません。", @@ -530,14 +530,13 @@ "filled": "約定", "Filled": "約定済み", "Filled / expires": "約定 / 有効期限", - "Fills now": "即時約定分", "Find a launch by asset name, and sort or filter the results.": "アセット名でローンチを探し、結果を並び替え・絞り込みできます。", "first at this price": "この価格で先頭", "first launch": "最初のローンチ", "First seen": "初回確認", - "Floor": "フロア", + "Floor": "最安売値", "Floor price": "フロア価格", - "Floor: {n}": "フロア: {n}", + "Floor: {n}": "最安売値: {n}", "Follow language": "言語に合わせる", "Full 1M bags sold before yours": "自分より先に売られたフル 1M バッグ", "Full specification with raw compose values and the conformance predicate: {spec}. Conformance is checked field-by-field on-chain — this site lists only launches that match exactly.": "生の compose 値と準拠述語を含む完全な仕様: {spec}。準拠はオンチェーンでフィールドごとに検査されます — このサイトは完全に一致するローンチだけを掲載します。", @@ -562,7 +561,6 @@ "held by {n} sellers": "{n} の売り手が保有", "Hide filled": "約定済みを隠す", "Hide minted": "ミント済みを隠す", - "High slippage authorizes up to {pct}% price impact.": "高スリッページでは最大 {pct}% の価格インパクトを許容します。", "Higher than this trade needs (~{pct}%).": "この取引に必要な値(約 {pct}%)より高い設定です。", "hold one, made none": "保有あり、作成なし", "Holders": "ホルダー", @@ -575,15 +573,15 @@ "how it works": "仕組み", "How it works": "仕組み", "How it works — xcp.fun": "仕組み — xcp.fun", - "How long an unfilled remainder rests before auto-refund. {n} = fill at confirmation or refund next block.": "未約定分が自動返金されるまで待機する長さ。{n} = 承認時に約定、さもなければ次のブロックで返金。", "How long the order rests unfilled before the remainder auto-refunds.": "注文が未約定のまま待機し、残りが自動返金されるまでの長さ。", + "How many blocks an unfilled remainder stays open before expiry. {n} is the shortest setting.": "未約定分が有効期限まで待機するブロック数です。{n} が最短の設定です。", "How much your own swap moves the price. Larger swaps against the fixed reserves get a worse average price.": "自分のスワップがどれだけ価格を動かすか。固定された準備金に対して大きなスワップほど平均価格は悪くなります。", "How that works": "その仕組み", "How XCP-69 works": "XCP-69 の仕組み", "If it fails to launch": "ローンチに失敗した場合", "If it's yours, connect that wallet to launch with it.": "あなたのものなら、そのウォレットを接続してローンチしてください。", "If launched": "ローンチした場合", - "If the pool moves past this before confirmation, the whole transaction is void — nothing is debited; only the miner fee is spent.": "承認前にプールがこの値を超えて動くと、トランザクション全体が無効になります — 何も引き落とされず、トランザクション手数料のみ消費されます。", + "If these limits cannot be met at confirmation, the liquidity operation is invalid. Pool assets are not debited; the Bitcoin network fee is still paid.": "承認時にこれらの条件を満たせない場合、流動性操作は無効になり、プールの資産は引き落とされません。Bitcoin のネットワーク手数料はかかります。", "Image": "画像", "In {n} pool": "{n} プール内", "In {n} pools": "{n} プール内", @@ -745,7 +743,6 @@ "New buyers": "新規の買い手", "Newest": "新着", "Next →": "次へ →", - "next block after BTC confirms": "BTC 承認後の次のブロック", "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.": "いいえ — ミントはローンチに対して XCP をエスクローするだけです。卒業すればトークンを保有し、返金なら 1 satoshi 残らず戻ります。ミントで実際にかかるのは Bitcoin の手数料だけで、このプログラムがカバーするのはその部分です。", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.": "いいえ — そこが要点です。フェアミンターのプロトコルは非常に幅広い設定を許します: ソフトキャップなし、返金なし、最大 99% の作成者コミッション、プレマイン、プールなし。XCP-69 はそのうち一つの構成を固定し、このサイトはオンチェーンの記録とフィールドごとに照合して標準に完全一致するローンチだけを掲載します。ここではすべてのローンチが同じ振る舞いをし、パラメータを自分で監査する必要はありません。他の場所で出会うフェアミンターにはそんな保証はありません — ミントの前にフィールドを読んでください。", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.": "いいえ — 約束ではなく構造上の結果です。存在する数量が 69/31 しかないため、プールはミント価格の 69/31 でオープンせざるを得ませんが、フロアは売られるほど下がります。トークンがミント価格を下回って取引されることを止めるものはありません。プールが保証するのはゼロにならない買い気配であって、あなたが満足する買い気配ではありません。", @@ -795,7 +792,7 @@ "none — you already own this name": "なし — この名前はすでにあなたのものです", "None have finished yet — nothing to score.": "まだ終了したものはありません — 集計対象なし。", "Not enough history to chart yet.": "チャートを描くには履歴が足りません。", - "not far enough in the future": "先が近すぎます", + "not far enough in the future": "開始までの時間が短すぎます", "not launchable": "ローンチ不可", "not XCP-69": "XCP-69 ではありません", "nothing": "なし", @@ -870,6 +867,7 @@ "Paste the profile URL or enter the handle.": "プロフィールの URL を貼り付けるか、ハンドルを入力してください。", "Payouts": "支払い", "pending": "保留中", + "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.": "未承認の注文が先に約定すると、最低受取量を下回る可能性があります。未約定分は有効期限まで待機した後に返還されます。スリッページを確認するか、自動を選んでください。", "People with xcp.fun open right now, including you. Several tabs from the same browser count once. Closing the tab removes you.": "あなたを含め、今 xcp.fun を開いている人の数。同じブラウザの複数タブは 1 人として数えます。タブを閉じると外れます。", "Per address": "1 アドレスあたり", "Per mint": "ミントごと", @@ -967,7 +965,7 @@ "route no longer has enough left": "ルートの残量が不足しました", "route price changed": "ルートの価格が変わりました", "Routes": "ルート", - "Routing includes miner fees — a deep route can beat a cheaper, shallower one.": "ルーティングはトランザクション手数料込みです — 深いルートが安くて浅いルートに勝つこともあります。", + "Routing includes miner fees — a deep route can beat a cheaper, shallower one.": "経路選択には Bitcoin のネットワーク手数料も含まれます。単価が安くても、流動性が少ない経路は総費用が高くなる場合があります。", "Sale closing": "販売終了処理中", "Save failed": "保存に失敗しました", "Saved. Changes are live; other open pages may take a minute to refresh.": "保存しました。変更は反映済みです。開いている他のページは更新まで少しかかることがあります。", @@ -1017,6 +1015,7 @@ "Size": "サイズ", "Skipped: {reason}": "スキップ: {reason}", "Slippage": "スリッページ", + "Slippage tolerance of {pct}% permits receiving less than quoted.": "スリッページ許容率 {pct}% では、見積もりより少ない数量での約定を許容します。", "Slippage: {pct}%": "スリッページ: {pct}%", "soft cap not reached": "soft cap not reached", "Sold": "売却", @@ -1034,7 +1033,7 @@ "Stats are unavailable right now.": "統計は現在利用できません。", "Status": "ステータス", "Still holds": "まだ保有", - "Stop unloading & reclaim": "アンロードを停止して回収", + "Stop unloading & reclaim": "販売を停止して回収", "Stress-test the exit race": "売り抜け競争のストレステスト", "Supply": "発行量", "Support & terms": "サポートと規約", @@ -1141,11 +1140,11 @@ "Upcoming": "近日", "Upload failed": "アップロードに失敗しました", "Upload token image": "トークン画像をアップロード", - "Use digits and a dot, with up to {n} decimal places.": "半角の数字とドットを使い、小数点以下は{n}桁以内で入力してください。", + "Use digits and a dot, with up to {n} decimal places.": "半角数字と小数点のみ。小数は{n}桁まで。", "Use whole numbers only.": "整数のみを入力してください。", "Value": "価値", "Value and underlying amounts are what withdrawing the LP tokens now would return. XCP value is available when XCP is one side of the pair. LP profit and loss is not estimated.": "価値と内訳の数量は、今 LP トークンを引き出した場合に戻る量です。XCP 建ての価値はペアの一方が XCP の場合に表示されます。LP の損益は推定していません。", - "Vends": "販売中", + "Vends": "販売可能回数", "Venue": "取引所", "Venue / time": "場所 / 時刻", "Verdict pending": "判定待ち", @@ -1169,6 +1168,7 @@ "What xcp.fun shows for this launch, and what the hosted metadata says if your on-chain description points here. The on-chain field itself is locked forever; everything behind it is yours to curate.": "xcp.fun がこのローンチについて表示する内容と、オンチェーンの説明文がここを指している場合にホスト側メタデータが伝える内容です。オンチェーンの項目自体は永久にロックされますが、その先はあなたが自由に編集できます。", "When": "日時", "When do I get paid?": "いつ支払われますか?", + "when the BTC payment confirms": "BTC の支払いが承認された時", "Where do I get the XCP to mint with?": "ミントに使う XCP はどこで手に入りますか?", "Where does the token art actually live?": "トークンのアートは実際どこにあるのですか?", "Which Counterparty collections the minters come from. Creators made a card there; collectors hold one.": "ミンターがどの Counterparty コレクション出身か。クリエイターはそこでカードを作った人、コレクターは保有している人です。", @@ -1193,7 +1193,7 @@ "XCP Rewards": "XCP リワード", "XCP Rewards — xcp.fun": "XCP リワード — xcp.fun", "XCP to receive": "受け取る XCP", - "XCP to unload": "アンロードする XCP", + "XCP to unload": "売却する XCP", "XCP-69 is a fixed-parameter token launch standard built on Counterparty {pools}. There is no factory contract, no admin key, and no platform custody at any point: the protocol {is} the platform. Every action — creating a launch, minting, swapping — is a transaction you sign in your own wallet and broadcast to Bitcoin. This site is an interface over public on-chain data; if it disappeared tomorrow, every launch, refund, and pool would keep working exactly as before.": "XCP-69 は Counterparty の{pools}の上に築かれた固定パラメータのトークンローンチ標準です。ファクトリーコントラクトも管理者キーもなく、どの時点でもプラットフォームによる資産管理はありません: プロトコル{is}がプラットフォームです。ローンチの作成、ミント、スワップなど、あらゆる操作は自分のウォレットで署名して Bitcoin にブロードキャストするトランザクションです。このサイトは公開オンチェーンデータの上のインターフェースであり、明日消えても、すべてのローンチ、返金、プールはこれまで通り動き続けます。", "XCP-69 launches and mints sitting in the Bitcoin mempool, not yet confirmed.": "Bitcoin のメンプールで承認待ちの XCP-69 ローンチとミント。", "XCP-69 launches by phase, and what has actually been minted.": "フェーズ別の XCP-69 ローンチと、実際にミントされた量。", @@ -1210,7 +1210,7 @@ "you": "あなた", "you · {n} XCP": "あなた · {n} XCP", "you have minted this launch’s max": "このローンチの上限までミント済みです", - "You mint": "受け取る量", + "You mint": "ミント数量", "You own {name}, but {reason}.": "{name} はあなたのものですが、{reason}。", "You pay": "支払う量", "You receive": "受取額", diff --git a/apps/web/src/locales/ja.status.json b/apps/web/src/locales/ja.status.json index 99a3a395..0f867d9c 100644 --- a/apps/web/src/locales/ja.status.json +++ b/apps/web/src/locales/ja.status.json @@ -29,10 +29,10 @@ "A new way to launch a coin on Counterparty — every launch identical, enforced by consensus.", "A pool never runs out, it only gets worse. This is what it can absorb before its price moves 1%.", "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.", - "A resting order refunds in full at expiry — the price you set is enforced by the order itself.", + "Any unfilled amount is returned at expiry. The order enforces your limit price.", "API base:", "About", - "Above what the pending orders would leave. If they confirm first, this order rests for a block and refunds instead of filling — raise the slippage or use Auto.", + "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.", "Across {n} fully exited position", "Across {n} fully exited positions", "Across {n} open position", @@ -94,7 +94,7 @@ "Bought", "Broadcast", "Broadcast as {tx}. Minting opens at block {block} ({when}) — until then the launch is announced on-chain and nobody, you included, can mint. Then it runs for 1,000 blocks (~7 days): it sells out, or everyone is refunded.", - "Broadcast legs are final — each is its own transaction. Failed legs can be retried.", + "Each route is a separate transaction. Broadcast routes cannot be canceled here. Failed routes can be retried.", "Broadcast, waiting to confirm. Its page and editing open up once it lands in a block.", "Broadcasting commit…", "Broadcasting reveal…", @@ -256,7 +256,7 @@ "Fetching quote…", "Filled", "Filled / expires", - "Fills now", + "Estimated fill", "Find a launch by asset name, and sort or filter the results.", "First seen", "Floor", @@ -278,7 +278,7 @@ "Hard cap", "Hide filled", "Hide minted", - "High slippage authorizes up to {pct}% price impact.", + "Slippage tolerance of {pct}% permits receiving less than quoted.", "Higher than this trade needs (~{pct}%).", "Holders", "Holders:", @@ -287,14 +287,14 @@ "How XCP-69 works", "How it works", "How it works — xcp.fun", - "How long an unfilled remainder rests before auto-refund. {n} = fill at confirmation or refund next block.", + "How many blocks an unfilled remainder stays open before expiry. {n} is the shortest setting.", "How long the order rests unfilled before the remainder auto-refunds.", "How much your own swap moves the price. Larger swaps against the fixed reserves get a worse average price.", "How that works", "If it fails to launch", "If it's yours, connect that wallet to launch with it.", "If launched", - "If the pool moves past this before confirmation, the whole transaction is void — nothing is debited; only the miner fee is spent.", + "If these limits cannot be met at confirmation, the liquidity operation is invalid. Pool assets are not debited; the Bitcoin network fee is still paid.", "Image", "In XCP terms, exactly. In fiat terms, only if XCP's price held during the ~week-long window. Refunds return quantity, not value.", "In mempool", @@ -917,7 +917,7 @@ "named assets only, divisible", "need XCP?", "new address", - "next block after BTC confirms", + "when the BTC payment confirms", "no history", "none — the creator mints like everyone else", "none — you already own this name", diff --git a/apps/web/src/locales/ko.json b/apps/web/src/locales/ko.json index 8902be22..5411341f 100644 --- a/apps/web/src/locales/ko.json +++ b/apps/web/src/locales/ko.json @@ -230,9 +230,7 @@ "A new way to launch a coin on Counterparty — every launch identical, enforced by consensus.": "Counterparty에서 코인을 런칭하는 새로운 방식 — 모든 런칭이 동일하고, 합의로 강제됩니다.", "A pool never runs out, it only gets worse. This is what it can absorb before its price moves 1%.": "풀은 바닥나지 않고 조건만 나빠집니다. 가격이 1% 움직이기 전까지 풀이 받아낼 수 있는 양입니다.", "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.": "환불된 런칭은 돈을 잃은 것이 아닙니다. XCP-69는 전부 아니면 전무입니다. 소프트캡에 미달한 런칭은 합의에 따라 마지막 사토시까지 돌려주며, 우리가 결정할 여지도 누군가 가로챌 방법도 없습니다.", - "A resting order refunds in full at expiry — the price you set is enforced by the order itself.": "대기 주문은 만료 시 전액 환불됩니다 — 설정한 가격은 주문 자체가 보장합니다.", "About": "소개", - "Above what the pending orders would leave. If they confirm first, this order rests for a block and refunds instead of filling — raise the slippage or use Auto.": "대기 중인 주문이 남길 수량을 초과합니다. 그 주문이 먼저 확인되면 이 주문은 1블록 동안 대기한 뒤 체결되지 않고 환불됩니다 — 슬리피지를 높이거나 자동을 사용하세요.", "Across {n} fully exited position": "완전 청산 포지션 {n}개 합계", "Across {n} fully exited positions": "완전 청산 포지션 {n}개 합계", "across {n} open launch": "진행 중 런칭 {n}개 합계", @@ -277,6 +275,7 @@ "Announced before minting opens": "민팅 시작 전에 공지", "announced, not open": "공지됨, 미시작", "Any future block, {floor} — tighter than the presets go. Under ~2 hours pays double the network fee.": "미래의 아무 블록, {floor} — 프리셋보다 촘촘하게 지정할 수 있습니다. 약 2시간 미만이면 네트워크 수수료가 두 배입니다.", + "Any unfilled amount is returned at expiry. The order enforces your limit price.": "미체결 수량은 만료 시 반환됩니다. 주문에는 지정한 가격 한도가 적용됩니다.", "API base:": "API 기본 주소:", "Are all Counterparty fairminters like this?": "Counterparty 페어민터는 모두 이런가요?", "Arrival": "도착", @@ -314,7 +313,6 @@ "bought without minting": "민팅 없이 매수", "Broadcast": "브로드캐스트", "Broadcast as {tx}. Minting opens at block {block} ({when}) — until then the launch is announced on-chain and nobody, you included, can mint. Then it runs for 1,000 blocks (~7 days): it sells out, or everyone is refunded.": "브로드캐스트되었습니다 ({tx}). 민팅은 블록 {block} ({when})에 열립니다 — 그때까지 런칭은 온체인에 공지된 상태이며 본인을 포함해 누구도 민팅할 수 없습니다. 이후 1,000블록 (약 7일) 동안 진행됩니다: 완판되거나, 전원 환불됩니다.", - "Broadcast legs are final — each is its own transaction. Failed legs can be retried.": "브로드캐스트된 구간은 되돌릴 수 없습니다 — 각각 별도의 트랜잭션입니다. 실패한 구간은 다시 시도할 수 있습니다.", "Broadcast, waiting to confirm. Its page and editing open up once it lands in a block.": "브로드캐스트 완료, 확인 대기 중입니다. 블록에 포함되면 페이지와 편집이 열립니다.", "Broadcasting commit…": "커밋 브로드캐스트 중…", "Broadcasting reveal…": "리빌 브로드캐스트 중…", @@ -480,6 +478,7 @@ "Dumpers": "덤퍼", "Dumping now": "덤핑 중", "Each number counts unique addresses. A sale is a pool or order-book sale captured by xcp.fun. Dump means the address sold within {blocks} blocks of graduation. A meaningful balance is more than one token and more than 1% of the address's acquired amount.": "모든 숫자는 고유 주소 기준입니다. 매도는 xcp.fun이 포착한 풀 또는 호가창 매도입니다. 덤핑은 해당 주소가 졸업 후 {blocks}블록 이내에 매도한 경우입니다. 의미 있는 잔액은 토큰 1개 초과이면서 해당 주소 취득량의 1%를 초과하는 잔액입니다.", + "Each route is a separate transaction. Broadcast routes cannot be canceled here. Failed routes can be retried.": "각 경로는 별도의 트랜잭션입니다. 이미 전파된 경로는 여기서 취소할 수 없습니다. 실패한 경로는 다시 시도할 수 있습니다.", "Earned": "획득", "Earned so far": "현재까지 획득", "Edit token info": "토큰 정보 수정", @@ -492,6 +491,7 @@ "escrowed": "에스크로 중", "escrowed until launch": "런칭 확정까지 에스크로", "Escrowed until the launch resolves: tokens if it sells out, full XCP refund if it doesn't.": "런칭이 확정될 때까지 에스크로에 보관됩니다. 완판되면 토큰을, 아니면 XCP 전액을 돌려받습니다.", + "Estimated fill": "예상 체결", "Event": "이벤트", "Every launch that conforms to XCP-69, and what has actually been minted.": "XCP-69 표준을 준수하는 모든 런칭과 실제 민팅된 수량입니다.", "Every LP token is burned — this liquidity can never be withdrawn.": "LP 토큰 전량이 소각되었습니다 — 이 유동성은 절대 인출할 수 없습니다.", @@ -530,7 +530,6 @@ "filled": "체결됨", "Filled": "체결됨", "Filled / expires": "체결 / 만료", - "Fills now": "즉시 체결", "Find a launch by asset name, and sort or filter the results.": "자산 이름으로 런칭을 찾고, 결과를 정렬하거나 필터링하세요.", "first at this price": "이 가격에서 1순위", "first launch": "첫 런칭", @@ -562,7 +561,6 @@ "held by {n} sellers": "매도자 {n}명 보유", "Hide filled": "체결 숨기기", "Hide minted": "민팅한 항목 숨기기", - "High slippage authorizes up to {pct}% price impact.": "높은 슬리피지는 최대 {pct}%의 가격 영향을 허용합니다.", "Higher than this trade needs (~{pct}%).": "이 거래에 필요한 값(약 {pct}%)보다 높습니다.", "hold one, made none": "보유만 하고 제작 안 함", "Holders": "홀더", @@ -575,15 +573,15 @@ "how it works": "작동 방식", "How it works": "작동 방식", "How it works — xcp.fun": "작동 방식 — xcp.fun", - "How long an unfilled remainder rests before auto-refund. {n} = fill at confirmation or refund next block.": "미체결 잔량이 자동 환불되기 전까지 대기하는 시간입니다. {n} = 확인 시 체결, 아니면 다음 블록에 환불.", "How long the order rests unfilled before the remainder auto-refunds.": "주문이 미체결 상태로 대기하다가 잔량이 자동 환불되기까지의 시간입니다.", + "How many blocks an unfilled remainder stays open before expiry. {n} is the shortest setting.": "미체결 수량이 만료 전까지 대기하는 블록 수입니다. {n}이 가장 짧은 설정입니다.", "How much your own swap moves the price. Larger swaps against the fixed reserves get a worse average price.": "내 스왑이 가격을 얼마나 움직이는지. 고정된 준비금에 대해 스왑이 클수록 평균 가격은 나빠집니다.", "How that works": "작동 방식", "How XCP-69 works": "XCP-69 작동 방식", "If it fails to launch": "런칭에 실패하면", "If it's yours, connect that wallet to launch with it.": "본인 소유라면 그 지갑을 연결해 런칭하세요.", "If launched": "런칭 성공 시", - "If the pool moves past this before confirmation, the whole transaction is void — nothing is debited; only the miner fee is spent.": "확인 전에 풀이 이 값을 넘어 움직이면 트랜잭션 전체가 무효가 됩니다 — 아무것도 차감되지 않고 채굴 수수료만 소모됩니다.", + "If these limits cannot be met at confirmation, the liquidity operation is invalid. Pool assets are not debited; the Bitcoin network fee is still paid.": "확정 시 이 한도를 충족하지 못하면 유동성 작업은 무효가 됩니다. 풀 자산은 차감되지 않지만 비트코인 네트워크 수수료는 지불됩니다.", "Image": "이미지", "In {n} pool": "풀 {n}개에 예치", "In {n} pools": "풀 {n}개에 예치", @@ -745,7 +743,6 @@ "New buyers": "신규 매수자", "Newest": "최신순", "Next →": "다음 →", - "next block after BTC confirms": "BTC 확인 후 다음 블록", "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.": "아니요 — 민팅은 XCP를 해당 런칭에 에스크로하는 것입니다. 졸업하면 토큰을 보유하게 되고, 환불되면 마지막 사토시까지 돌려받습니다. 민팅에 실제로 드는 비용은 비트코인 수수료뿐이며, 이 프로그램이 보전하는 부분이 바로 그것입니다.", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.": "아니요 — 그것이 핵심입니다. 페어민터 프로토콜은 엄청난 변형을 허용합니다: 소프트캡 없음, 환불 없음, 최대 99%의 제작자 커미션, 프리마인, 풀 없음. XCP-69는 그중 하나의 구성을 고정하고, 이 사이트는 온체인 기록과 필드 단위로 대조해 표준에 정확히 일치하는 런칭만 표시합니다. 여기서는 모든 런칭이 동일하게 동작하며 파라미터를 직접 감사할 필요가 없습니다. 다른 곳에서 만나는 페어민터에는 그런 보장이 없습니다 — 민팅 전에 필드를 읽어 보세요.", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.": "아니요 — 약속이 아니라 구조입니다. 존재하는 수량이 69/31뿐이므로 풀은 민팅가의 69/31에서 열릴 수밖에 없지만, 바닥은 사람들이 팔수록 내려갑니다. 토큰이 민팅가 아래에서 거래되는 것을 막는 장치는 없습니다. 풀이 보장하는 것은 절대 0이 되지 않는 매수 호가이지, 마음에 드는 호가는 아닙니다.", @@ -870,6 +867,7 @@ "Paste the profile URL or enter the handle.": "프로필 URL을 붙여넣거나 핸들을 입력하세요.", "Payouts": "지급", "pending": "대기 중", + "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.": "대기 중인 주문이 먼저 확정되면 최소 수령 수량을 충족하지 못할 수 있습니다. 미체결 수량은 만료될 때까지 대기한 후 반환됩니다. 슬리피지를 확인하거나 자동을 선택하세요.", "People with xcp.fun open right now, including you. Several tabs from the same browser count once. Closing the tab removes you.": "나를 포함해 지금 xcp.fun을 열어 둔 사람 수입니다. 같은 브라우저의 여러 탭은 한 명으로 셉니다. 탭을 닫으면 빠집니다.", "Per address": "주소당 한도", "Per mint": "민팅당", @@ -1017,6 +1015,7 @@ "Size": "수량", "Skipped: {reason}": "건너뜀: {reason}", "Slippage": "슬리피지", + "Slippage tolerance of {pct}% permits receiving less than quoted.": "슬리피지 허용치가 {pct}%이면 견적보다 적은 수량을 받을 수 있습니다.", "Slippage: {pct}%": "슬리피지: {pct}%", "soft cap not reached": "soft cap not reached", "Sold": "매도", @@ -1169,6 +1168,7 @@ "What xcp.fun shows for this launch, and what the hosted metadata says if your on-chain description points here. The on-chain field itself is locked forever; everything behind it is yours to curate.": "xcp.fun이 이 런칭에 대해 보여주는 내용과, 온체인 설명이 이곳을 가리킬 때 호스팅된 메타데이터가 전하는 내용입니다. 온체인 필드 자체는 영원히 잠기지만, 그 뒤의 내용은 자유롭게 관리할 수 있습니다.", "When": "시간", "When do I get paid?": "언제 받나요?", + "when the BTC payment confirms": "BTC 결제가 확정될 때", "Where do I get the XCP to mint with?": "민팅에 쓸 XCP는 어디서 구하나요?", "Where does the token art actually live?": "토큰 아트는 실제로 어디에 있나요?", "Which Counterparty collections the minters come from. Creators made a card there; collectors hold one.": "민터들이 어떤 Counterparty 컬렉션 출신인지. 제작자는 그곳에서 카드를 만들었고, 컬렉터는 한 장을 보유합니다.", diff --git a/apps/web/src/locales/ko.status.json b/apps/web/src/locales/ko.status.json index 44f3b5b5..a03c30b2 100644 --- a/apps/web/src/locales/ko.status.json +++ b/apps/web/src/locales/ko.status.json @@ -29,10 +29,10 @@ "A new way to launch a coin on Counterparty — every launch identical, enforced by consensus.", "A pool never runs out, it only gets worse. This is what it can absorb before its price moves 1%.", "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.", - "A resting order refunds in full at expiry — the price you set is enforced by the order itself.", + "Any unfilled amount is returned at expiry. The order enforces your limit price.", "API base:", "About", - "Above what the pending orders would leave. If they confirm first, this order rests for a block and refunds instead of filling — raise the slippage or use Auto.", + "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.", "Across {n} fully exited position", "Across {n} fully exited positions", "Across {n} open position", @@ -94,7 +94,7 @@ "Bought", "Broadcast", "Broadcast as {tx}. Minting opens at block {block} ({when}) — until then the launch is announced on-chain and nobody, you included, can mint. Then it runs for 1,000 blocks (~7 days): it sells out, or everyone is refunded.", - "Broadcast legs are final — each is its own transaction. Failed legs can be retried.", + "Each route is a separate transaction. Broadcast routes cannot be canceled here. Failed routes can be retried.", "Broadcast, waiting to confirm. Its page and editing open up once it lands in a block.", "Broadcasting commit…", "Broadcasting reveal…", @@ -256,7 +256,7 @@ "Fetching quote…", "Filled", "Filled / expires", - "Fills now", + "Estimated fill", "Find a launch by asset name, and sort or filter the results.", "First seen", "Floor", @@ -278,7 +278,7 @@ "Hard cap", "Hide filled", "Hide minted", - "High slippage authorizes up to {pct}% price impact.", + "Slippage tolerance of {pct}% permits receiving less than quoted.", "Higher than this trade needs (~{pct}%).", "Holders", "Holders:", @@ -287,14 +287,14 @@ "How XCP-69 works", "How it works", "How it works — xcp.fun", - "How long an unfilled remainder rests before auto-refund. {n} = fill at confirmation or refund next block.", + "How many blocks an unfilled remainder stays open before expiry. {n} is the shortest setting.", "How long the order rests unfilled before the remainder auto-refunds.", "How much your own swap moves the price. Larger swaps against the fixed reserves get a worse average price.", "How that works", "If it fails to launch", "If it's yours, connect that wallet to launch with it.", "If launched", - "If the pool moves past this before confirmation, the whole transaction is void — nothing is debited; only the miner fee is spent.", + "If these limits cannot be met at confirmation, the liquidity operation is invalid. Pool assets are not debited; the Bitcoin network fee is still paid.", "Image", "In XCP terms, exactly. In fiat terms, only if XCP's price held during the ~week-long window. Refunds return quantity, not value.", "In mempool", @@ -918,7 +918,7 @@ "named assets only, divisible", "need XCP?", "new address", - "next block after BTC confirms", + "when the BTC payment confirms", "no history", "none — the creator mints like everyone else", "none — you already own this name", diff --git a/apps/web/src/locales/pt.json b/apps/web/src/locales/pt.json index f8aaffc0..59fa2233 100644 --- a/apps/web/src/locales/pt.json +++ b/apps/web/src/locales/pt.json @@ -230,9 +230,7 @@ "A new way to launch a coin on Counterparty — every launch identical, enforced by consensus.": "Um jeito novo de lançar uma moeda no Counterparty — todo lançamento idêntico, garantido por consenso.", "A pool never runs out, it only gets worse. This is what it can absorb before its price moves 1%.": "Um pool nunca acaba, só piora. Isto é o que ele consegue absorver antes de o preço se mover 1%.", "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.": "Um lançamento reembolsado não é dinheiro perdido. XCP-69 é tudo ou nada: o que não atinge o soft cap devolve cada satoshi por consenso, sem nenhuma decisão nossa e sem que ninguém possa ficar com ele.", - "A resting order refunds in full at expiry — the price you set is enforced by the order itself.": "Uma ordem em espera é reembolsada por completo ao expirar — o preço que você define é garantido pela própria ordem.", "About": "Sobre", - "Above what the pending orders would leave. If they confirm first, this order rests for a block and refunds instead of filling — raise the slippage or use Auto.": "Acima do que as ordens pendentes deixariam. Se elas confirmarem antes, esta ordem fica em espera por um bloco e é reembolsada em vez de executada — aumente o slippage ou use Auto.", "Across {n} fully exited position": "Em {n} posição totalmente encerrada", "Across {n} fully exited positions": "Em {n} posições totalmente encerradas", "across {n} open launch": "em {n} lançamento aberto", @@ -277,6 +275,7 @@ "Announced before minting opens": "Anunciado antes de o mint abrir", "announced, not open": "anunciados, ainda não abertos", "Any future block, {floor} — tighter than the presets go. Under ~2 hours pays double the network fee.": "Qualquer bloco futuro, {floor} — mais apertado do que as opções predefinidas. Com menos de ~2 horas, paga o dobro da taxa de rede.", + "Any unfilled amount is returned at expiry. The order enforces your limit price.": "A quantidade não executada é devolvida no vencimento. A ordem respeita seu preço limite.", "API base:": "Base da API:", "Are all Counterparty fairminters like this?": "Todos os fairminters do Counterparty são assim?", "Arrival": "Chegada", @@ -314,7 +313,6 @@ "bought without minting": "compraram sem mintar", "Broadcast": "Transmitir", "Broadcast as {tx}. Minting opens at block {block} ({when}) — until then the launch is announced on-chain and nobody, you included, can mint. Then it runs for 1,000 blocks (~7 days): it sells out, or everyone is refunded.": "Transmitido como {tx}. O mint abre no bloco {block} ({when}) — até lá o lançamento fica anunciado on-chain e ninguém, nem você, consegue mintar. Depois ele roda por 1.000 blocos (~7 dias): esgota, ou todo mundo é reembolsado.", - "Broadcast legs are final — each is its own transaction. Failed legs can be retried.": "Trechos transmitidos são definitivos — cada um é uma transação própria. Trechos que falharam podem ser repetidos.", "Broadcast, waiting to confirm. Its page and editing open up once it lands in a block.": "Transmitido, aguardando confirmação. A página e a edição são liberadas quando ele entra em um bloco.", "Broadcasting commit…": "Transmitindo o commit…", "Broadcasting reveal…": "Transmitindo o reveal…", @@ -480,6 +478,7 @@ "Dumpers": "Dumpers", "Dumping now": "Dumpando agora", "Each number counts unique addresses. A sale is a pool or order-book sale captured by xcp.fun. Dump means the address sold within {blocks} blocks of graduation. A meaningful balance is more than one token and more than 1% of the address's acquired amount.": "Cada número conta endereços únicos. Uma venda é uma venda no pool ou no livro de ofertas registrada pelo xcp.fun. Dump significa que o endereço vendeu dentro de {blocks} blocos após a graduação. Um saldo relevante é mais de um token e mais de 1% do que o endereço adquiriu.", + "Each route is a separate transaction. Broadcast routes cannot be canceled here. Failed routes can be retried.": "Cada rota é uma transação independente. Rotas já transmitidas não podem ser canceladas aqui. As que falharam podem ser tentadas novamente.", "Earned": "Ganho", "Earned so far": "Ganho até agora", "Edit token info": "Editar informações do token", @@ -492,6 +491,7 @@ "escrowed": "em custódia", "escrowed until launch": "em custódia até o lançamento", "Escrowed until the launch resolves: tokens if it sells out, full XCP refund if it doesn't.": "Em custódia até o lançamento ser resolvido: tokens se esgotar, reembolso total em XCP se não esgotar.", + "Estimated fill": "Execução estimada", "Event": "Evento", "Every launch that conforms to XCP-69, and what has actually been minted.": "Todos os lançamentos que seguem o XCP-69, e o que de fato foi mintado.", "Every LP token is burned — this liquidity can never be withdrawn.": "Todos os tokens LP estão queimados — esta liquidez nunca poderá ser retirada.", @@ -530,7 +530,6 @@ "filled": "executada", "Filled": "Executada", "Filled / expires": "Executada / vence", - "Fills now": "Executa agora", "Find a launch by asset name, and sort or filter the results.": "Encontre um lançamento pelo nome do ativo e ordene ou filtre os resultados.", "first at this price": "primeiro neste preço", "first launch": "primeiro lançamento", @@ -562,7 +561,6 @@ "held by {n} sellers": "nas mãos de {n} vendedores", "Hide filled": "Ocultar executadas", "Hide minted": "Ocultar mintados", - "High slippage authorizes up to {pct}% price impact.": "Um slippage alto autoriza até {pct}% de impacto no preço.", "Higher than this trade needs (~{pct}%).": "Mais do que esta negociação precisa (~{pct}%).", "hold one, made none": "têm uma, não criaram nenhuma", "Holders": "Holders", @@ -575,15 +573,15 @@ "how it works": "como funciona", "How it works": "Como funciona", "How it works — xcp.fun": "Como funciona — xcp.fun", - "How long an unfilled remainder rests before auto-refund. {n} = fill at confirmation or refund next block.": "Por quanto tempo o restante não executado fica em espera antes do reembolso automático. {n} = executa na confirmação ou reembolsa no bloco seguinte.", "How long the order rests unfilled before the remainder auto-refunds.": "Por quanto tempo a ordem fica em espera sem executar antes que o restante seja reembolsado automaticamente.", + "How many blocks an unfilled remainder stays open before expiry. {n} is the shortest setting.": "Quantos blocos a quantidade não executada permanece aberta antes de vencer. {n} é o prazo mais curto.", "How much your own swap moves the price. Larger swaps against the fixed reserves get a worse average price.": "Quanto o seu próprio swap move o preço. Swaps maiores contra as reservas fixas conseguem um preço médio pior.", "How that works": "Como funciona", "How XCP-69 works": "Como o XCP-69 funciona", "If it fails to launch": "Se o lançamento falhar", "If it's yours, connect that wallet to launch with it.": "Se for seu, conecte essa carteira para lançar com ele.", "If launched": "Se for lançado", - "If the pool moves past this before confirmation, the whole transaction is void — nothing is debited; only the miner fee is spent.": "Se o pool passar deste valor antes da confirmação, a transação inteira é anulada — nada é debitado; só a taxa de mineração é gasta.", + "If these limits cannot be met at confirmation, the liquidity operation is invalid. Pool assets are not debited; the Bitcoin network fee is still paid.": "Se estes limites não forem atendidos na confirmação, a operação de liquidez será inválida. Os ativos do pool não são debitados; a taxa de rede do Bitcoin ainda é paga.", "Image": "Imagem", "In {n} pool": "Em {n} pool", "In {n} pools": "Em {n} pools", @@ -745,7 +743,6 @@ "New buyers": "Novos compradores", "Newest": "Mais recentes", "Next →": "Próxima →", - "next block after BTC confirms": "o bloco seguinte à confirmação do BTC", "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.": "Não — um mint deixa XCP em custódia contra o lançamento. Se ele se graduar, você fica com os tokens; se for reembolsado, você recebe cada satoshi de volta. A única coisa que um mint realmente custa é a taxa do Bitcoin — que é justamente a parte que este programa cobre.", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.": "Não — e esse é o ponto. O protocolo de fairminters permite uma variação enorme: sem soft cap, sem reembolsos, comissões ao criador de até 99%, pré-mineração, nenhum pool. O XCP-69 fixa uma única configuração, e este site só lista lançamentos que batem exatamente com o padrão, verificados campo a campo contra o registro on-chain. Aqui, todo lançamento se comporta igual e você nunca precisa auditar os parâmetros. Um fairminter que você encontrar em qualquer outro lugar não traz essa garantia — leia os campos dele antes de mintar.", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.": "Não — é estrutural, não prometido. O pool tem que abrir a 69/31 do preço de mint porque essas são as únicas quantidades que existem, mas o piso cai conforme as pessoas vendem nele. Nada impede que um token seja negociado abaixo do mint. O que o pool garante é uma oferta de compra que nunca chega a zero, não uma oferta de que você vai gostar.", @@ -870,6 +867,7 @@ "Paste the profile URL or enter the handle.": "Cole a URL do perfil ou digite o usuário.", "Payouts": "Pagamentos", "pending": "pendente", + "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.": "Ordens pendentes podem deixar menos que seu mínimo a receber. Se forem confirmadas antes, a quantidade não executada fica aberta até vencer e depois é devolvida. Confira o slippage ou use Auto.", "People with xcp.fun open right now, including you. Several tabs from the same browser count once. Closing the tab removes you.": "Pessoas com o xcp.fun aberto agora, incluindo você. Várias abas do mesmo navegador contam uma vez só. Fechar a aba tira você da contagem.", "Per address": "Por endereço", "Per mint": "Por mint", @@ -1017,6 +1015,7 @@ "Size": "Tamanho", "Skipped: {reason}": "Pulada: {reason}", "Slippage": "Slippage", + "Slippage tolerance of {pct}% permits receiving less than quoted.": "Uma tolerância de slippage de {pct}% permite receber menos que o valor cotado.", "Slippage: {pct}%": "Slippage: {pct}%", "soft cap not reached": "soft cap not reached", "Sold": "Venda", @@ -1169,6 +1168,7 @@ "What xcp.fun shows for this launch, and what the hosted metadata says if your on-chain description points here. The on-chain field itself is locked forever; everything behind it is yours to curate.": "O que o xcp.fun mostra deste lançamento e o que os metadados hospedados dizem se a sua descrição on-chain apontar para cá. O campo on-chain fica bloqueado para sempre; tudo o que está por trás dele é você quem cura.", "When": "Quando", "When do I get paid?": "Quando eu recebo?", + "when the BTC payment confirms": "quando o pagamento em BTC for confirmado", "Where do I get the XCP to mint with?": "Onde eu consigo o XCP para mintar?", "Where does the token art actually live?": "Onde a arte do token fica de verdade?", "Which Counterparty collections the minters come from. Creators made a card there; collectors hold one.": "De quais coleções do Counterparty os minters vêm. Os criadores fizeram uma carta lá; os colecionadores têm uma.", diff --git a/apps/web/src/locales/pt.status.json b/apps/web/src/locales/pt.status.json index 44f3b5b5..a03c30b2 100644 --- a/apps/web/src/locales/pt.status.json +++ b/apps/web/src/locales/pt.status.json @@ -29,10 +29,10 @@ "A new way to launch a coin on Counterparty — every launch identical, enforced by consensus.", "A pool never runs out, it only gets worse. This is what it can absorb before its price moves 1%.", "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.", - "A resting order refunds in full at expiry — the price you set is enforced by the order itself.", + "Any unfilled amount is returned at expiry. The order enforces your limit price.", "API base:", "About", - "Above what the pending orders would leave. If they confirm first, this order rests for a block and refunds instead of filling — raise the slippage or use Auto.", + "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.", "Across {n} fully exited position", "Across {n} fully exited positions", "Across {n} open position", @@ -94,7 +94,7 @@ "Bought", "Broadcast", "Broadcast as {tx}. Minting opens at block {block} ({when}) — until then the launch is announced on-chain and nobody, you included, can mint. Then it runs for 1,000 blocks (~7 days): it sells out, or everyone is refunded.", - "Broadcast legs are final — each is its own transaction. Failed legs can be retried.", + "Each route is a separate transaction. Broadcast routes cannot be canceled here. Failed routes can be retried.", "Broadcast, waiting to confirm. Its page and editing open up once it lands in a block.", "Broadcasting commit…", "Broadcasting reveal…", @@ -256,7 +256,7 @@ "Fetching quote…", "Filled", "Filled / expires", - "Fills now", + "Estimated fill", "Find a launch by asset name, and sort or filter the results.", "First seen", "Floor", @@ -278,7 +278,7 @@ "Hard cap", "Hide filled", "Hide minted", - "High slippage authorizes up to {pct}% price impact.", + "Slippage tolerance of {pct}% permits receiving less than quoted.", "Higher than this trade needs (~{pct}%).", "Holders", "Holders:", @@ -287,14 +287,14 @@ "How XCP-69 works", "How it works", "How it works — xcp.fun", - "How long an unfilled remainder rests before auto-refund. {n} = fill at confirmation or refund next block.", + "How many blocks an unfilled remainder stays open before expiry. {n} is the shortest setting.", "How long the order rests unfilled before the remainder auto-refunds.", "How much your own swap moves the price. Larger swaps against the fixed reserves get a worse average price.", "How that works", "If it fails to launch", "If it's yours, connect that wallet to launch with it.", "If launched", - "If the pool moves past this before confirmation, the whole transaction is void — nothing is debited; only the miner fee is spent.", + "If these limits cannot be met at confirmation, the liquidity operation is invalid. Pool assets are not debited; the Bitcoin network fee is still paid.", "Image", "In XCP terms, exactly. In fiat terms, only if XCP's price held during the ~week-long window. Refunds return quantity, not value.", "In mempool", @@ -918,7 +918,7 @@ "named assets only, divisible", "need XCP?", "new address", - "next block after BTC confirms", + "when the BTC payment confirms", "no history", "none — the creator mints like everyone else", "none — you already own this name", diff --git a/apps/web/src/locales/ru.json b/apps/web/src/locales/ru.json index ec0a1a8f..67a0658d 100644 --- a/apps/web/src/locales/ru.json +++ b/apps/web/src/locales/ru.json @@ -230,9 +230,7 @@ "A new way to launch a coin on Counterparty — every launch identical, enforced by consensus.": "Новый способ запустить монету на Counterparty — каждый запуск одинаков и обеспечивается консенсусом.", "A pool never runs out, it only gets worse. This is what it can absorb before its price moves 1%.": "Пул никогда не пустеет — он лишь даёт цену всё хуже. Столько он может принять, прежде чем его цена сдвинется на 1 %.", "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.": "Запуск с возвратом — это не потерянные деньги. XCP-69 работает по принципу «всё или ничего»: запуск, не добравший софткап, возвращает каждый сатоши по консенсусу — без нашего решения и без возможности кому-либо оставить их себе.", - "A resting order refunds in full at expiry — the price you set is enforced by the order itself.": "Отложенный ордер полностью возвращается по истечении срока — вашу цену обеспечивает сам ордер.", "About": "О сайте", - "Above what the pending orders would leave. If they confirm first, this order rests for a block and refunds instead of filling — raise the slippage or use Auto.": "Больше, чем останется после ожидающих ордеров. Если они подтвердятся первыми, этот ордер подождёт блок и вернётся вместо исполнения — увеличьте проскальзывание или выберите Авто.", "Across {n} fully exited position": "Полностью закрытых позиций: {n}", "Across {n} fully exited positions": "Полностью закрытых позиций: {n}", "across {n} open launch": "открытых запусков: {n}", @@ -277,6 +275,7 @@ "Announced before minting opens": "Объявляется до открытия минта", "announced, not open": "анонсированы, ещё не открыты", "Any future block, {floor} — tighter than the presets go. Under ~2 hours pays double the network fee.": "Любой будущий блок, {floor} — точнее, чем предустановки. Меньше ~2 часов — двойная комиссия сети.", + "Any unfilled amount is returned at expiry. The order enforces your limit price.": "Неисполненный остаток возвращается по истечении срока. Ордер соблюдает заданную лимитную цену.", "API base:": "Базовый URL API:", "Are all Counterparty fairminters like this?": "Все fairminter на Counterparty такие?", "Arrival": "Поступление", @@ -314,7 +313,6 @@ "bought without minting": "купили без минта", "Broadcast": "Транслировать", "Broadcast as {tx}. Minting opens at block {block} ({when}) — until then the launch is announced on-chain and nobody, you included, can mint. Then it runs for 1,000 blocks (~7 days): it sells out, or everyone is refunded.": "Транслировано как {tx}. Минт откроется на блоке {block} ({when}) — до этого запуск объявлен в блокчейне, и никто, включая вас, минтить не может. Затем он идёт 1 000 блоков (~7 дней): либо распродаётся, либо всем возвращают средства.", - "Broadcast legs are final — each is its own transaction. Failed legs can be retried.": "Транслированные этапы окончательны — каждый из них отдельная транзакция. Неудавшиеся этапы можно повторить.", "Broadcast, waiting to confirm. Its page and editing open up once it lands in a block.": "Транслировано, ждёт подтверждения. Страница и редактирование откроются, когда транзакция попадёт в блок.", "Broadcasting commit…": "Трансляция commit…", "Broadcasting reveal…": "Трансляция reveal…", @@ -480,6 +478,7 @@ "Dumpers": "Дамперы", "Dumping now": "Дампят сейчас", "Each number counts unique addresses. A sale is a pool or order-book sale captured by xcp.fun. Dump means the address sold within {blocks} blocks of graduation. A meaningful balance is more than one token and more than 1% of the address's acquired amount.": "Каждое число — количество уникальных адресов. Продажа — это продажа в пуле или в книге ордеров, зафиксированная xcp.fun. Дамп — адрес продал в пределах {blocks} блоков после выпускного. Существенный баланс — больше одного токена и больше 1 % от объёма, который адрес приобрёл.", + "Each route is a separate transaction. Broadcast routes cannot be canceled here. Failed routes can be retried.": "Каждый маршрут — отдельная транзакция. Уже отправленные в сеть маршруты нельзя отменить здесь. Неудавшиеся можно повторить.", "Earned": "Заработано", "Earned so far": "Заработано на данный момент", "Edit token info": "Изменить информацию о токене", @@ -492,6 +491,7 @@ "escrowed": "в эскроу", "escrowed until launch": "в эскроу до завершения запуска", "Escrowed until the launch resolves: tokens if it sells out, full XCP refund if it doesn't.": "В эскроу до исхода запуска: токены, если он распродастся, полный возврат XCP — если нет.", + "Estimated fill": "Ожидаемое исполнение", "Event": "Событие", "Every launch that conforms to XCP-69, and what has actually been minted.": "Все запуски, соответствующие XCP-69, и то, что действительно было заминчено.", "Every LP token is burned — this liquidity can never be withdrawn.": "Все токены LP сожжены — эту ликвидность нельзя вывести никогда.", @@ -530,7 +530,6 @@ "filled": "исполнен", "Filled": "Исполнен", "Filled / expires": "Исполнено / истекает", - "Fills now": "Исполнится сейчас", "Find a launch by asset name, and sort or filter the results.": "Найдите запуск по названию актива, отсортируйте или отфильтруйте результаты.", "first at this price": "первый по этой цене", "first launch": "первый запуск", @@ -562,7 +561,6 @@ "held by {n} sellers": "у продавцов ({n})", "Hide filled": "Скрыть исполненные", "Hide minted": "Скрыть заминченные", - "High slippage authorizes up to {pct}% price impact.": "Высокое проскальзывание допускает влияние на цену до {pct} %.", "Higher than this trade needs (~{pct}%).": "Больше, чем нужно этой сделке (~{pct} %).", "hold one, made none": "владеют картой, не создавали", "Holders": "Холдеры", @@ -575,15 +573,15 @@ "how it works": "как это работает", "How it works": "Как это работает", "How it works — xcp.fun": "Как это работает — xcp.fun", - "How long an unfilled remainder rests before auto-refund. {n} = fill at confirmation or refund next block.": "Сколько неисполненный остаток ждёт до автоматического возврата. {n} = исполнение при подтверждении или возврат в следующем блоке.", "How long the order rests unfilled before the remainder auto-refunds.": "Сколько ордер ждёт неисполненным, прежде чем остаток вернётся автоматически.", + "How many blocks an unfilled remainder stays open before expiry. {n} is the shortest setting.": "Число блоков, в течение которых неисполненный остаток остаётся в ордере до истечения срока. {n} — минимальное значение.", "How much your own swap moves the price. Larger swaps against the fixed reserves get a worse average price.": "Насколько ваш собственный обмен сдвигает цену. Чем крупнее обмен против фиксированных резервов, тем хуже средняя цена.", "How that works": "Как это работает", "How XCP-69 works": "Как работает XCP-69", "If it fails to launch": "Если запуск провалится", "If it's yours, connect that wallet to launch with it.": "Если это ваше имя, подключите тот кошелёк, чтобы запуститься с ним.", "If launched": "Если запустится", - "If the pool moves past this before confirmation, the whole transaction is void — nothing is debited; only the miner fee is spent.": "Если пул сдвинется дальше этого до подтверждения, вся транзакция аннулируется — ничего не списывается, тратится только комиссия майнеров.", + "If these limits cannot be met at confirmation, the liquidity operation is invalid. Pool assets are not debited; the Bitcoin network fee is still paid.": "Если при подтверждении эти ограничения не соблюдены, операция с ликвидностью недействительна. Активы пула не списываются, но комиссия сети Bitcoin уплачивается.", "Image": "Изображение", "In {n} pool": "Пулов: {n}", "In {n} pools": "Пулов: {n}", @@ -745,7 +743,6 @@ "New buyers": "Новые покупатели", "Newest": "Новые", "Next →": "Далее →", - "next block after BTC confirms": "следующий блок после подтверждения BTC", "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.": "Нет — минт помещает XCP в эскроу под запуск. Если запуск выпустится, у вас остаются токены; если будет возврат, вы получите назад каждый сатоши. Единственное, что минт действительно стоит, — комиссия биткоина, и именно её покрывает эта программа.", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.": "Нет — и в этом суть. Протокол fairminter допускает огромный разброс: без софткапа, без возвратов, с комиссией создателя до 99 %, с премайном, вообще без пула. XCP-69 фиксирует одну его конфигурацию, и этот сайт перечисляет только запуски, точно соответствующие стандарту, проверенные по каждому полю против записи в блокчейне. Здесь каждый запуск ведёт себя одинаково, и вам никогда не приходится проверять параметры самим. На fairminter, встреченный где-либо ещё, такая гарантия не распространяется — читайте его поля, прежде чем минтить.", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.": "Нет — это следствие структуры, а не обещание. Пул обязан открыться по 69/31 от цены минта, потому что других количеств просто не существует, но дно опускается по мере продаж в пул. Ничто не мешает токену торговаться ниже минта. Пул гарантирует заявку на покупку, которая никогда не станет нулевой, — а не такую, которая вам понравится.", @@ -870,6 +867,7 @@ "Paste the profile URL or enter the handle.": "Вставьте URL профиля или введите имя пользователя.", "Payouts": "Выплаты", "pending": "в ожидании", + "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.": "Ожидающие ордера могут оставить меньше минимальной суммы получения. Если они подтвердятся раньше, неисполненный остаток останется в ордере до истечения срока, а затем вернётся. Проверьте проскальзывание или выберите «Авто».", "People with xcp.fun open right now, including you. Several tabs from the same browser count once. Closing the tab removes you.": "Сколько людей сейчас держат xcp.fun открытым, включая вас. Несколько вкладок одного браузера считаются за одну. Закрытие вкладки убирает вас из подсчёта.", "Per address": "На адрес", "Per mint": "За минт", @@ -1017,6 +1015,7 @@ "Size": "Объём", "Skipped: {reason}": "Пропущено: {reason}", "Slippage": "Проскальзывание", + "Slippage tolerance of {pct}% permits receiving less than quoted.": "Допуск проскальзывания {pct}% позволяет получить меньше, чем указано в котировке.", "Slippage: {pct}%": "Проскальзывание: {pct} %", "soft cap not reached": "soft cap not reached", "Sold": "Продажа", @@ -1169,6 +1168,7 @@ "What xcp.fun shows for this launch, and what the hosted metadata says if your on-chain description points here. The on-chain field itself is locked forever; everything behind it is yours to curate.": "То, что xcp.fun показывает для этого запуска, и то, что говорят размещённые метаданные, если ваше описание в блокчейне указывает сюда. Само поле в блокчейне заблокировано навсегда; всё, что за ним, вы редактируете сами.", "When": "Когда", "When do I get paid?": "Когда мне заплатят?", + "when the BTC payment confirms": "при подтверждении платежа BTC", "Where do I get the XCP to mint with?": "Где взять XCP для минта?", "Where does the token art actually live?": "Где на самом деле хранится картинка токена?", "Which Counterparty collections the minters come from. Creators made a card there; collectors hold one.": "Из каких коллекций Counterparty пришли минтеры. Создатели сделали там карту; коллекционеры ею владеют.", diff --git a/apps/web/src/locales/ru.status.json b/apps/web/src/locales/ru.status.json index beaa4117..f2421471 100644 --- a/apps/web/src/locales/ru.status.json +++ b/apps/web/src/locales/ru.status.json @@ -29,10 +29,10 @@ "A new way to launch a coin on Counterparty — every launch identical, enforced by consensus.", "A pool never runs out, it only gets worse. This is what it can absorb before its price moves 1%.", "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.", - "A resting order refunds in full at expiry — the price you set is enforced by the order itself.", + "Any unfilled amount is returned at expiry. The order enforces your limit price.", "API base:", "About", - "Above what the pending orders would leave. If they confirm first, this order rests for a block and refunds instead of filling — raise the slippage or use Auto.", + "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.", "Across {n} fully exited position", "Across {n} fully exited positions", "Across {n} open position", @@ -94,7 +94,7 @@ "Bought", "Broadcast", "Broadcast as {tx}. Minting opens at block {block} ({when}) — until then the launch is announced on-chain and nobody, you included, can mint. Then it runs for 1,000 blocks (~7 days): it sells out, or everyone is refunded.", - "Broadcast legs are final — each is its own transaction. Failed legs can be retried.", + "Each route is a separate transaction. Broadcast routes cannot be canceled here. Failed routes can be retried.", "Broadcast, waiting to confirm. Its page and editing open up once it lands in a block.", "Broadcasting commit…", "Broadcasting reveal…", @@ -256,7 +256,7 @@ "Fetching quote…", "Filled", "Filled / expires", - "Fills now", + "Estimated fill", "Find a launch by asset name, and sort or filter the results.", "First seen", "Floor", @@ -278,7 +278,7 @@ "Hard cap", "Hide filled", "Hide minted", - "High slippage authorizes up to {pct}% price impact.", + "Slippage tolerance of {pct}% permits receiving less than quoted.", "Higher than this trade needs (~{pct}%).", "Holders", "Holders:", @@ -287,14 +287,14 @@ "How XCP-69 works", "How it works", "How it works — xcp.fun", - "How long an unfilled remainder rests before auto-refund. {n} = fill at confirmation or refund next block.", + "How many blocks an unfilled remainder stays open before expiry. {n} is the shortest setting.", "How long the order rests unfilled before the remainder auto-refunds.", "How much your own swap moves the price. Larger swaps against the fixed reserves get a worse average price.", "How that works", "If it fails to launch", "If it's yours, connect that wallet to launch with it.", "If launched", - "If the pool moves past this before confirmation, the whole transaction is void — nothing is debited; only the miner fee is spent.", + "If these limits cannot be met at confirmation, the liquidity operation is invalid. Pool assets are not debited; the Bitcoin network fee is still paid.", "Image", "In XCP terms, exactly. In fiat terms, only if XCP's price held during the ~week-long window. Refunds return quantity, not value.", "In mempool", @@ -920,7 +920,7 @@ "named assets only, divisible", "need XCP?", "new address", - "next block after BTC confirms", + "when the BTC payment confirms", "no history", "none — the creator mints like everyone else", "none — you already own this name", diff --git a/apps/web/src/locales/uk.json b/apps/web/src/locales/uk.json index 4dbdbbd1..8c481e0d 100644 --- a/apps/web/src/locales/uk.json +++ b/apps/web/src/locales/uk.json @@ -230,9 +230,7 @@ "A new way to launch a coin on Counterparty — every launch identical, enforced by consensus.": "Новий спосіб запустити монету на Counterparty — кожен запуск однаковий, і це гарантує консенсус.", "A pool never runs out, it only gets worse. This is what it can absorb before its price moves 1%.": "Пул ніколи не вичерпується — ціна лише погіршується. Це те, що він може поглинути, перш ніж ціна зрушиться на 1%.", "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.": "Запуск із поверненням — це не втрачені гроші. XCP-69 працює за принципом «все або нічого»: запуск, який не досяг софткапу, повертає кожен сатоші за консенсусом — без нашого рішення і без можливості для будь-кого їх утримати.", - "A resting order refunds in full at expiry — the price you set is enforced by the order itself.": "Ордер у книзі повністю повертається після закінчення терміну — встановлену вами ціну гарантує сам ордер.", "About": "Про сайт", - "Above what the pending orders would leave. If they confirm first, this order rests for a block and refunds instead of filling — raise the slippage or use Auto.": "Більше, ніж залишать ордери в очікуванні. Якщо вони підтвердяться раніше, цей ордер зачекає один блок і повернеться замість виконання — підвищте прослизання або виберіть Авто.", "Across {n} fully exited position": "Повністю закритих позицій: {n}", "Across {n} fully exited positions": "Повністю закритих позицій: {n}", "across {n} open launch": "відкритих запусків: {n}", @@ -277,6 +275,7 @@ "Announced before minting opens": "Оголошується до відкриття мінту", "announced, not open": "анонсовані, ще не відкриті", "Any future block, {floor} — tighter than the presets go. Under ~2 hours pays double the network fee.": "Будь-який майбутній блок, {floor} — точніше, ніж дозволяють пресети. Менше ~2 годин — комісія мережі подвоюється.", + "Any unfilled amount is returned at expiry. The order enforces your limit price.": "Невиконаний залишок повертається після закінчення строку. Ордер дотримується заданої лімітної ціни.", "API base:": "База API:", "Are all Counterparty fairminters like this?": "Чи всі fairminter на Counterparty такі?", "Arrival": "Надходження", @@ -314,7 +313,6 @@ "bought without minting": "купили без мінту", "Broadcast": "Надіслати в мережу", "Broadcast as {tx}. Minting opens at block {block} ({when}) — until then the launch is announced on-chain and nobody, you included, can mint. Then it runs for 1,000 blocks (~7 days): it sells out, or everyone is refunded.": "Надіслано в мережу як {tx}. Мінт відкривається на блоці {block} ({when}) — до того запуск оголошено ончейн, і ніхто, включно з вами, не може мінтити. Далі він триває 1 000 блоків (~7 днів): або розпродається, або всі отримують повернення.", - "Broadcast legs are final — each is its own transaction. Failed legs can be retried.": "Етапи, надіслані в мережу, остаточні — кожен є окремою транзакцією. Невдалі етапи можна повторити.", "Broadcast, waiting to confirm. Its page and editing open up once it lands in a block.": "Транзакцію надіслано, очікує підтвердження. Сторінка та редагування стануть доступні, щойно вона потрапить у блок.", "Broadcasting commit…": "Надсилання commit…", "Broadcasting reveal…": "Надсилання reveal…", @@ -480,6 +478,7 @@ "Dumpers": "Дампери", "Dumping now": "Зливають зараз", "Each number counts unique addresses. A sale is a pool or order-book sale captured by xcp.fun. Dump means the address sold within {blocks} blocks of graduation. A meaningful balance is more than one token and more than 1% of the address's acquired amount.": "Кожне число рахує унікальні адреси. Продаж — це продаж у пулі або в книзі ордерів, зафіксований xcp.fun. Дамп означає, що адреса продала протягом {blocks} блоків після випуску. Суттєвий баланс — це більше одного токена і більше 1% від обсягу, який адреса придбала.", + "Each route is a separate transaction. Broadcast routes cannot be canceled here. Failed routes can be retried.": "Кожен маршрут — окрема транзакція. Уже надіслані в мережу маршрути не можна скасувати тут. Невдалі можна повторити.", "Earned": "Зароблено", "Earned so far": "Зароблено дотепер", "Edit token info": "Редагувати дані токена", @@ -492,6 +491,7 @@ "escrowed": "в ескроу", "escrowed until launch": "в ескроу до завершення запуску", "Escrowed until the launch resolves: tokens if it sells out, full XCP refund if it doesn't.": "В ескроу до завершення запуску: токени, якщо він розпродається, повне повернення XCP — якщо ні.", + "Estimated fill": "Очікуване виконання", "Event": "Подія", "Every launch that conforms to XCP-69, and what has actually been minted.": "Кожен запуск, що відповідає XCP-69, і те, що насправді змінтовано.", "Every LP token is burned — this liquidity can never be withdrawn.": "Усі LP-токени спалено — цю ліквідність неможливо вивести.", @@ -530,7 +530,6 @@ "filled": "виконано", "Filled": "Виконано", "Filled / expires": "Виконано / спливає", - "Fills now": "Виконується зараз", "Find a launch by asset name, and sort or filter the results.": "Знайдіть запуск за назвою активу, відсортуйте або відфільтруйте результати.", "first at this price": "перший за цією ціною", "first launch": "перший запуск", @@ -562,7 +561,6 @@ "held by {n} sellers": "у продавців: {n}", "Hide filled": "Приховати виконані", "Hide minted": "Сховати змінтовані", - "High slippage authorizes up to {pct}% price impact.": "Високе прослизання дозволяє вплив на ціну до {pct}%.", "Higher than this trade needs (~{pct}%).": "Більше, ніж потрібно цій угоді (~{pct}%).", "hold one, made none": "мають, але не створили", "Holders": "Холдери", @@ -575,15 +573,15 @@ "how it works": "як це працює", "How it works": "Як це працює", "How it works — xcp.fun": "Як це працює — xcp.fun", - "How long an unfilled remainder rests before auto-refund. {n} = fill at confirmation or refund next block.": "Скільки невиконаний залишок чекає до автоматичного повернення. {n} = виконання під час підтвердження або повернення в наступному блоці.", "How long the order rests unfilled before the remainder auto-refunds.": "Скільки ордер чекає невиконаним, перш ніж залишок повернеться автоматично.", + "How many blocks an unfilled remainder stays open before expiry. {n} is the shortest setting.": "Кількість блоків, протягом яких невиконаний залишок залишається в ордері до закінчення строку. {n} — мінімальне значення.", "How much your own swap moves the price. Larger swaps against the fixed reserves get a worse average price.": "Наскільки ваш власний обмін зсуває ціну. Більші обміни проти фіксованих резервів отримують гіршу середню ціну.", "How that works": "Як це працює", "How XCP-69 works": "Як працює XCP-69", "If it fails to launch": "Якщо запуск провалиться", "If it's yours, connect that wallet to launch with it.": "Якщо воно ваше, підключіть той гаманець, щоб запуститися з ним.", "If launched": "Якщо запуск відбудеться", - "If the pool moves past this before confirmation, the whole transaction is void — nothing is debited; only the miner fee is spent.": "Якщо пул зрушить за цю межу до підтвердження, вся транзакція скасовується — нічого не списується; витрачається лише комісія майнерів.", + "If these limits cannot be met at confirmation, the liquidity operation is invalid. Pool assets are not debited; the Bitcoin network fee is still paid.": "Якщо під час підтвердження ці обмеження не дотримані, операція з ліквідністю недійсна. Активи пулу не списуються, але комісія мережі Bitcoin сплачується.", "Image": "Зображення", "In {n} pool": "Пулів: {n}", "In {n} pools": "Пулів: {n}", @@ -745,7 +743,6 @@ "New buyers": "Нові покупці", "Newest": "Нові", "Next →": "Далі →", - "next block after BTC confirms": "наступний блок після підтвердження BTC", "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.": "Ні — мінт лише блокує XCP в ескроу під запуск. Якщо він випуститься, ви отримуєте токени; якщо буде повернення — ви отримуєте назад кожен сатоші. Єдине, що мінт справді вам коштує, — це комісія біткоїна, і саме цю частину покриває програма.", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.": "Ні — і в цьому суть. Протокол fairminter допускає величезну варіативність: без софткапу, без повернень, комісії творця до 99%, премайни (частка токенів, залишена засновникам), взагалі без пулу. XCP-69 фіксує одну його конфігурацію, і цей сайт показує лише запуски, що точно відповідають стандарту, перевірені поле за полем за ончейн-записом. Тут кожен запуск поводиться однаково, і вам ніколи не доводиться самостійно перевіряти параметри. Fairminter, який ви зустрінете деінде, такої гарантії не має — читайте його поля перед мінтом.", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.": "Ні — вона структурна, а не обіцяна. Пул мусить відкритися на 69/31 від ціни мінту, бо інших кількостей просто не існує, але дно опускається у міру того, як у нього продають. Ніщо не заважає токену торгуватися нижче мінту. Пул гарантує заявку на купівлю, яка ніколи не дійде до нуля, — а не заявку, яка вам сподобається.", @@ -870,6 +867,7 @@ "Paste the profile URL or enter the handle.": "Вставте URL профілю або введіть нікнейм.", "Payouts": "Виплати", "pending": "очікує", + "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.": "Ордери в очікуванні можуть залишити менше мінімальної суми отримання. Якщо вони підтвердяться раніше, невиконаний залишок залишиться в ордері до закінчення строку, а потім повернеться. Перевірте прослизання або виберіть «Авто».", "People with xcp.fun open right now, including you. Several tabs from the same browser count once. Closing the tab removes you.": "Люди, у яких зараз відкрито xcp.fun, включно з вами. Кілька вкладок одного браузера рахуються як одна. Закриття вкладки прибирає вас зі списку.", "Per address": "На адресу", "Per mint": "За мінт", @@ -1017,6 +1015,7 @@ "Size": "Кількість", "Skipped: {reason}": "Пропущено: {reason}", "Slippage": "Прослизання", + "Slippage tolerance of {pct}% permits receiving less than quoted.": "Допуск прослизання {pct}% дозволяє отримати менше, ніж зазначено в котируванні.", "Slippage: {pct}%": "Прослизання: {pct}%", "soft cap not reached": "soft cap not reached", "Sold": "Продаж", @@ -1169,6 +1168,7 @@ "What xcp.fun shows for this launch, and what the hosted metadata says if your on-chain description points here. The on-chain field itself is locked forever; everything behind it is yours to curate.": "Те, що xcp.fun показує для цього запуску, і те, що містять розміщені метадані, якщо ваш ончейн-опис вказує сюди. Саме ончейн-поле заблоковане назавжди; усе, що за ним, ви можете редагувати.", "When": "Коли", "When do I get paid?": "Коли я отримаю виплату?", + "when the BTC payment confirms": "під час підтвердження платежу BTC", "Where do I get the XCP to mint with?": "Де взяти XCP для мінту?", "Where does the token art actually live?": "Де насправді зберігається зображення токена?", "Which Counterparty collections the minters come from. Creators made a card there; collectors hold one.": "З яких колекцій Counterparty походять мінтери. Творці створили там картку; колекціонери її тримають.", diff --git a/apps/web/src/locales/uk.status.json b/apps/web/src/locales/uk.status.json index beaa4117..f2421471 100644 --- a/apps/web/src/locales/uk.status.json +++ b/apps/web/src/locales/uk.status.json @@ -29,10 +29,10 @@ "A new way to launch a coin on Counterparty — every launch identical, enforced by consensus.", "A pool never runs out, it only gets worse. This is what it can absorb before its price moves 1%.", "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.", - "A resting order refunds in full at expiry — the price you set is enforced by the order itself.", + "Any unfilled amount is returned at expiry. The order enforces your limit price.", "API base:", "About", - "Above what the pending orders would leave. If they confirm first, this order rests for a block and refunds instead of filling — raise the slippage or use Auto.", + "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.", "Across {n} fully exited position", "Across {n} fully exited positions", "Across {n} open position", @@ -94,7 +94,7 @@ "Bought", "Broadcast", "Broadcast as {tx}. Minting opens at block {block} ({when}) — until then the launch is announced on-chain and nobody, you included, can mint. Then it runs for 1,000 blocks (~7 days): it sells out, or everyone is refunded.", - "Broadcast legs are final — each is its own transaction. Failed legs can be retried.", + "Each route is a separate transaction. Broadcast routes cannot be canceled here. Failed routes can be retried.", "Broadcast, waiting to confirm. Its page and editing open up once it lands in a block.", "Broadcasting commit…", "Broadcasting reveal…", @@ -256,7 +256,7 @@ "Fetching quote…", "Filled", "Filled / expires", - "Fills now", + "Estimated fill", "Find a launch by asset name, and sort or filter the results.", "First seen", "Floor", @@ -278,7 +278,7 @@ "Hard cap", "Hide filled", "Hide minted", - "High slippage authorizes up to {pct}% price impact.", + "Slippage tolerance of {pct}% permits receiving less than quoted.", "Higher than this trade needs (~{pct}%).", "Holders", "Holders:", @@ -287,14 +287,14 @@ "How XCP-69 works", "How it works", "How it works — xcp.fun", - "How long an unfilled remainder rests before auto-refund. {n} = fill at confirmation or refund next block.", + "How many blocks an unfilled remainder stays open before expiry. {n} is the shortest setting.", "How long the order rests unfilled before the remainder auto-refunds.", "How much your own swap moves the price. Larger swaps against the fixed reserves get a worse average price.", "How that works", "If it fails to launch", "If it's yours, connect that wallet to launch with it.", "If launched", - "If the pool moves past this before confirmation, the whole transaction is void — nothing is debited; only the miner fee is spent.", + "If these limits cannot be met at confirmation, the liquidity operation is invalid. Pool assets are not debited; the Bitcoin network fee is still paid.", "Image", "In XCP terms, exactly. In fiat terms, only if XCP's price held during the ~week-long window. Refunds return quantity, not value.", "In mempool", @@ -920,7 +920,7 @@ "named assets only, divisible", "need XCP?", "new address", - "next block after BTC confirms", + "when the BTC payment confirms", "no history", "none — the creator mints like everyone else", "none — you already own this name", diff --git a/apps/web/src/locales/zh-hk.json b/apps/web/src/locales/zh-hk.json index 887b98a5..4c9869ae 100644 --- a/apps/web/src/locales/zh-hk.json +++ b/apps/web/src/locales/zh-hk.json @@ -230,9 +230,7 @@ "A new way to launch a coin on Counterparty — every launch identical, enforced by consensus.": "在 Counterparty 上發射代幣的新方式 — 每一次發射完全相同,由共識強制執行。", "A pool never runs out, it only gets worse. This is what it can absorb before its price moves 1%.": "流動性池永遠不會枯竭,只會讓價格越來越不利。這是價格變動 1% 之前它能吸收的量。", "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.": "退款的發射不等於虧錢。XCP-69 是全有或全無:未達軟頂的發射會按共識退回每一聰,無需我們決定,也沒有任何人能扣留。", - "A resting order refunds in full at expiry — the price you set is enforced by the order itself.": "掛單到期後全額退回 — 你設定的價格由訂單本身強制執行。", "About": "關於", - "Above what the pending orders would leave. If they confirm first, this order rests for a block and refunds instead of filling — raise the slippage or use Auto.": "高於待處理訂單成交後剩餘的價格。若它們先確認,這筆訂單將掛單一個區塊後退回而非成交 — 請調高滑點或使用自動。", "Across {n} fully exited position": "共 {n} 個已完全退出的倉位", "Across {n} fully exited positions": "共 {n} 個已完全退出的倉位", "across {n} open launch": "共 {n} 個進行中的發射", @@ -277,6 +275,7 @@ "Announced before minting opens": "鑄造開啓前公告", "announced, not open": "已公告,未開始", "Any future block, {floor} — tighter than the presets go. Under ~2 hours pays double the network fee.": "任意未來區塊,{floor} — 可比預設選項更近。少於約 2 小時需支付雙倍礦工費。", + "Any unfilled amount is returned at expiry. The order enforces your limit price.": "未成交的數量將在到期後退回。訂單會按你設定的限價執行。", "API base:": "API 基址:", "Are all Counterparty fairminters like this?": "所有 Counterparty 公平鑄造合約都是這樣嗎?", "Arrival": "到賬", @@ -314,7 +313,6 @@ "bought without minting": "未鑄造直接買入", "Broadcast": "廣播", "Broadcast as {tx}. Minting opens at block {block} ({when}) — until then the launch is announced on-chain and nobody, you included, can mint. Then it runs for 1,000 blocks (~7 days): it sells out, or everyone is refunded.": "已廣播為 {tx}。鑄造將在區塊 {block}({when})開啓 — 在此之前,發射已在鏈上公告,包括你在內的任何人都無法鑄造。之後持續 1,000 個區塊(約 7 天):要麼售罄,要麼所有人退款。", - "Broadcast legs are final — each is its own transaction. Failed legs can be retried.": "已廣播的分段不可撤銷 — 每段都是獨立的交易。失敗的分段可以重試。", "Broadcast, waiting to confirm. Its page and editing open up once it lands in a block.": "已廣播,等待確認。進入區塊後即可打開其頁面並編輯。", "Broadcasting commit…": "正在廣播 commit…", "Broadcasting reveal…": "正在廣播 reveal…", @@ -480,6 +478,7 @@ "Dumpers": "砸盤者", "Dumping now": "正在砸盤", "Each number counts unique addresses. A sale is a pool or order-book sale captured by xcp.fun. Dump means the address sold within {blocks} blocks of graduation. A meaningful balance is more than one token and more than 1% of the address's acquired amount.": "每個數字按獨立地址計數。賣出指 xcp.fun 記錄到的池子或訂單簿賣出。砸盤指該地址在畢業後 {blocks} 個區塊內賣出。可觀餘額指超過 1 枚代幣且超過該地址獲得量的 1%。", + "Each route is a separate transaction. Broadcast routes cannot be canceled here. Failed routes can be retried.": "每條路由都是獨立的交易。已廣播的路由無法在此取消,失敗的路由可以重試。", "Earned": "已獲得", "Earned so far": "已獲得", "Edit token info": "編輯代幣資訊", @@ -492,6 +491,7 @@ "escrowed": "協議託管中", "escrowed until launch": "由協議鎖定至發射結束", "Escrowed until the launch resolves: tokens if it sells out, full XCP refund if it doesn't.": "由協議鎖定至發射有結果為止:售罄則得到代幣,否則全額退回 XCP。", + "Estimated fill": "預計成交", "Event": "事件", "Every launch that conforms to XCP-69, and what has actually been minted.": "每一個符合 XCP-69 的發射,以及實際已鑄造的數量。", "Every LP token is burned — this liquidity can never be withdrawn.": "全部 LP 代幣已銷毀 — 這份流動性永遠無法撤出。", @@ -530,7 +530,6 @@ "filled": "已成交", "Filled": "已成交", "Filled / expires": "成交 / 到期", - "Fills now": "立即成交", "Find a launch by asset name, and sort or filter the results.": "按資產名稱查找發射,並對結果排序或篩選。", "first at this price": "此價格下排在首位", "first launch": "首次發射", @@ -562,7 +561,6 @@ "held by {n} sellers": "由 {n} 個賣家持有", "Hide filled": "隱藏已成交", "Hide minted": "隱藏已鑄造", - "High slippage authorizes up to {pct}% price impact.": "高滑點允許最高 {pct}% 的價格影響。", "Higher than this trade needs (~{pct}%).": "高於本次交易所需(約 {pct}%)。", "hold one, made none": "持有但未創作", "Holders": "持有者", @@ -575,15 +573,15 @@ "how it works": "運作機制", "How it works": "運作機制", "How it works — xcp.fun": "運作機制 — xcp.fun", - "How long an unfilled remainder rests before auto-refund. {n} = fill at confirmation or refund next block.": "未成交部分在自動退回前掛單多久。{n} = 確認時成交,否則下一個區塊退回。", "How long the order rests unfilled before the remainder auto-refunds.": "訂單在未成交狀態下掛單多久,之後剩餘部分自動退回。", + "How many blocks an unfilled remainder stays open before expiry. {n} is the shortest setting.": "未成交部分在到期前繼續掛單的區塊數。{n} 是最短設定。", "How much your own swap moves the price. Larger swaps against the fixed reserves get a worse average price.": "你自己的兌換會讓價格移動多少。面對固定的儲備,兌換量越大,平均價格越差。", "How that works": "如何運作", "How XCP-69 works": "XCP-69 如何運作", "If it fails to launch": "如果發射失敗", "If it's yours, connect that wallet to launch with it.": "如果它是你的,請連接對應錢包來發射。", "If launched": "如果發射成功", - "If the pool moves past this before confirmation, the whole transaction is void — nothing is debited; only the miner fee is spent.": "若確認前池價越過此值,整筆交易作廢 — 不會扣除任何資金,只花費礦工費。", + "If these limits cannot be met at confirmation, the liquidity operation is invalid. Pool assets are not debited; the Bitcoin network fee is still paid.": "若確認時無法滿足這些限額,流動性操作將無效。不會扣除池內資產,但仍需支付比特幣礦工費。", "Image": "圖片", "In {n} pool": "在 {n} 個池子中", "In {n} pools": "在 {n} 個池子中", @@ -745,7 +743,6 @@ "New buyers": "新買家", "Newest": "最新", "Next →": "下一頁 →", - "next block after BTC confirms": "BTC 確認後的下一個區塊", "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.": "不會 — 鑄造只是把 XCP 託管在該發射上。如果畢業,你持有代幣;如果退款,每一聰都會退回。鑄造真正花掉的只有比特幣礦工費 — 而這正是本計劃所覆蓋的部分。", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.": "不是 — 這正是重點。公平鑄造協議允許極大的變化:沒有軟頂、不退款、建立者佣金高達 99%、預挖、完全沒有流動性池。XCP-69 固定了其中的一種配置,本站只列出與標準完全匹配的發射,並逐欄位對照鏈上記錄檢查。在這裏,每一次發射的行為都完全相同,你永遠不需要自己審計參數。而你在其他任何地方遇到的公平鑄造合約都沒有這樣的保證 — 鑄造前請先讀它的欄位。", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.": "不是 — 它是結構性的,不是承諾。流動性池必須以鑄造價的 69/31 開盤,因為這是僅有的數量,但隨着人們向池中賣出,底價會不斷下降。沒有什麼能阻止代幣跌破鑄造價。池子保證的是一個永遠不會歸零的買價,而不是一個你會滿意的買價。", @@ -870,6 +867,7 @@ "Paste the profile URL or enter the handle.": "貼上主頁連結或輸入用户名。", "Payouts": "發放", "pending": "待確認", + "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.": "待確認訂單可能使可收到的數量低於你的最低接收數量。若它們先確認,未成交部分將掛單至到期後退回。請檢查滑點設定,或選擇自動。", "People with xcp.fun open right now, including you. Several tabs from the same browser count once. Closing the tab removes you.": "此刻打開 xcp.fun 的人數,包括你。同一瀏覽器的多個分頁只計一次。關閉分頁即不再計入。", "Per address": "每地址上限", "Per mint": "每筆鑄造", @@ -1017,6 +1015,7 @@ "Size": "數量", "Skipped: {reason}": "已跳過:{reason}", "Slippage": "滑點", + "Slippage tolerance of {pct}% permits receiving less than quoted.": "{pct}% 的滑點容忍度允許實際接收數量低於報價。", "Slippage: {pct}%": "滑點:{pct}%", "soft cap not reached": "soft cap not reached", "Sold": "賣出", @@ -1169,6 +1168,7 @@ "What xcp.fun shows for this launch, and what the hosted metadata says if your on-chain description points here. The on-chain field itself is locked forever; everything behind it is yours to curate.": "這是 xcp.fun 為這次發射展示的內容,以及當你的鏈上描述指向這裏時託管元數據所顯示的內容。鏈上欄位本身已永久鎖定;它背後的一切都由你來維護。", "When": "時間", "When do I get paid?": "我什麼時候能收到?", + "when the BTC payment confirms": "BTC 付款確認時", "Where do I get the XCP to mint with?": "鑄造用的 XCP 從哪裏獲得?", "Where does the token art actually live?": "代幣圖片實際存放在哪裏?", "Which Counterparty collections the minters come from. Creators made a card there; collectors hold one.": "鑄造者來自哪些 Counterparty 收藏集。創作者在那裏創作過卡牌;收藏者持有一張。", diff --git a/apps/web/src/locales/zh-hk.status.json b/apps/web/src/locales/zh-hk.status.json index 8849dbb1..d79b4c68 100644 --- a/apps/web/src/locales/zh-hk.status.json +++ b/apps/web/src/locales/zh-hk.status.json @@ -230,9 +230,9 @@ "A new way to launch a coin on Counterparty — every launch identical, enforced by consensus.", "A pool never runs out, it only gets worse. This is what it can absorb before its price moves 1%.", "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.", - "A resting order refunds in full at expiry — the price you set is enforced by the order itself.", + "Any unfilled amount is returned at expiry. The order enforces your limit price.", "About", - "Above what the pending orders would leave. If they confirm first, this order rests for a block and refunds instead of filling — raise the slippage or use Auto.", + "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.", "Across {n} fully exited position", "Across {n} fully exited positions", "across {n} open launch", @@ -313,7 +313,7 @@ "bought without minting", "Broadcast", "Broadcast as {tx}. Minting opens at block {block} ({when}) — until then the launch is announced on-chain and nobody, you included, can mint. Then it runs for 1,000 blocks (~7 days): it sells out, or everyone is refunded.", - "Broadcast legs are final — each is its own transaction. Failed legs can be retried.", + "Each route is a separate transaction. Broadcast routes cannot be canceled here. Failed routes can be retried.", "Broadcast, waiting to confirm. Its page and editing open up once it lands in a block.", "Broadcasting commit…", "Broadcasting reveal…", @@ -519,7 +519,7 @@ "filled", "Filled", "Filled / expires", - "Fills now", + "Estimated fill", "Find a launch by asset name, and sort or filter the results.", "first at this price", "first launch", @@ -549,7 +549,7 @@ "held by {n} sellers", "Hide filled", "Hide minted", - "High slippage authorizes up to {pct}% price impact.", + "Slippage tolerance of {pct}% permits receiving less than quoted.", "Higher than this trade needs (~{pct}%).", "hold one, made none", "Holders", @@ -560,7 +560,7 @@ "how it works", "How it works", "How it works — xcp.fun", - "How long an unfilled remainder rests before auto-refund. {n} = fill at confirmation or refund next block.", + "How many blocks an unfilled remainder stays open before expiry. {n} is the shortest setting.", "How long the order rests unfilled before the remainder auto-refunds.", "How much your own swap moves the price. Larger swaps against the fixed reserves get a worse average price.", "How that works", @@ -568,7 +568,7 @@ "If it fails to launch", "If it's yours, connect that wallet to launch with it.", "If launched", - "If the pool moves past this before confirmation, the whole transaction is void — nothing is debited; only the miner fee is spent.", + "If these limits cannot be met at confirmation, the liquidity operation is invalid. Pool assets are not debited; the Bitcoin network fee is still paid.", "Image", "In {n} pool", "In {n} pools", @@ -722,7 +722,7 @@ "New buyers", "Newest", "Next →", - "next block after BTC confirms", + "when the BTC payment confirms", "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.", diff --git a/apps/web/src/locales/zh-tw.json b/apps/web/src/locales/zh-tw.json index 5416b856..f82f48fc 100644 --- a/apps/web/src/locales/zh-tw.json +++ b/apps/web/src/locales/zh-tw.json @@ -230,9 +230,7 @@ "A new way to launch a coin on Counterparty — every launch identical, enforced by consensus.": "在 Counterparty 上發射代幣的新方式 — 每一次發射完全相同,由共識強制執行。", "A pool never runs out, it only gets worse. This is what it can absorb before its price moves 1%.": "流動性池永遠不會枯竭,只會讓價格越來越不利。這是價格變動 1% 之前它能吸收的量。", "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.": "退款的發射不等於虧錢。XCP-69 是全有或全無:未達軟頂的發射會按共識退回每一聰,無需我們決定,也沒有任何人能扣留。", - "A resting order refunds in full at expiry — the price you set is enforced by the order itself.": "掛單到期後全額退回 — 你設定的價格由訂單本身強制執行。", "About": "關於", - "Above what the pending orders would leave. If they confirm first, this order rests for a block and refunds instead of filling — raise the slippage or use Auto.": "高於待處理訂單成交後剩餘的價格。若它們先確認,這筆訂單將掛單一個區塊後退回而非成交 — 請調高滑點或使用自動。", "Across {n} fully exited position": "共 {n} 個已完全退出的倉位", "Across {n} fully exited positions": "共 {n} 個已完全退出的倉位", "across {n} open launch": "共 {n} 個進行中的發射", @@ -277,6 +275,7 @@ "Announced before minting opens": "鑄造開啟前公告", "announced, not open": "已公告,未開始", "Any future block, {floor} — tighter than the presets go. Under ~2 hours pays double the network fee.": "任意未來區塊,{floor} — 可比預設選項更近。少於約 2 小時需支付雙倍礦工費。", + "Any unfilled amount is returned at expiry. The order enforces your limit price.": "未成交的數量將在到期後退回。訂單會按你設定的限價執行。", "API base:": "API 基址:", "Are all Counterparty fairminters like this?": "所有 Counterparty 公平鑄造合約都是這樣嗎?", "Arrival": "到帳", @@ -314,7 +313,6 @@ "bought without minting": "未鑄造直接買入", "Broadcast": "廣播", "Broadcast as {tx}. Minting opens at block {block} ({when}) — until then the launch is announced on-chain and nobody, you included, can mint. Then it runs for 1,000 blocks (~7 days): it sells out, or everyone is refunded.": "已廣播為 {tx}。鑄造將在區塊 {block}({when})開啟 — 在此之前,發射已在鏈上公告,包括你在內的任何人都無法鑄造。之後持續 1,000 個區塊(約 7 天):要麼售罄,要麼所有人退款。", - "Broadcast legs are final — each is its own transaction. Failed legs can be retried.": "已廣播的分段不可撤銷 — 每段都是獨立的交易。失敗的分段可以重試。", "Broadcast, waiting to confirm. Its page and editing open up once it lands in a block.": "已廣播,等待確認。進入區塊後即可開啟其頁面並編輯。", "Broadcasting commit…": "正在廣播 commit…", "Broadcasting reveal…": "正在廣播 reveal…", @@ -480,6 +478,7 @@ "Dumpers": "砸盤者", "Dumping now": "正在砸盤", "Each number counts unique addresses. A sale is a pool or order-book sale captured by xcp.fun. Dump means the address sold within {blocks} blocks of graduation. A meaningful balance is more than one token and more than 1% of the address's acquired amount.": "每個數字按獨立地址計數。賣出指 xcp.fun 記錄到的池子或訂單簿賣出。砸盤指該地址在畢業後 {blocks} 個區塊內賣出。可觀餘額指超過 1 枚代幣且超過該地址獲得量的 1%。", + "Each route is a separate transaction. Broadcast routes cannot be canceled here. Failed routes can be retried.": "每條路由都是獨立的交易。已廣播的路由無法在此取消,失敗的路由可以重試。", "Earned": "已獲得", "Earned so far": "已獲得", "Edit token info": "編輯代幣資訊", @@ -492,6 +491,7 @@ "escrowed": "協議託管中", "escrowed until launch": "由協議鎖定至發射結束", "Escrowed until the launch resolves: tokens if it sells out, full XCP refund if it doesn't.": "由協議鎖定至發射有結果為止:售罄則得到代幣,否則全額退回 XCP。", + "Estimated fill": "預計成交", "Event": "事件", "Every launch that conforms to XCP-69, and what has actually been minted.": "每一個符合 XCP-69 的發射,以及實際已鑄造的數量。", "Every LP token is burned — this liquidity can never be withdrawn.": "全部 LP 代幣已銷毀 — 這份流動性永遠無法撤出。", @@ -530,7 +530,6 @@ "filled": "已成交", "Filled": "已成交", "Filled / expires": "成交 / 到期", - "Fills now": "立即成交", "Find a launch by asset name, and sort or filter the results.": "按資產名稱查詢發射,並對結果排序或篩選。", "first at this price": "此價格下排在首位", "first launch": "首次發射", @@ -562,7 +561,6 @@ "held by {n} sellers": "由 {n} 個賣家持有", "Hide filled": "隱藏已成交", "Hide minted": "隱藏已鑄造", - "High slippage authorizes up to {pct}% price impact.": "高滑點允許最高 {pct}% 的價格影響。", "Higher than this trade needs (~{pct}%).": "高於本次交易所需(約 {pct}%)。", "hold one, made none": "持有但未創作", "Holders": "持有者", @@ -575,15 +573,15 @@ "how it works": "運作機制", "How it works": "運作機制", "How it works — xcp.fun": "運作機制 — xcp.fun", - "How long an unfilled remainder rests before auto-refund. {n} = fill at confirmation or refund next block.": "未成交部分在自動退回前掛單多久。{n} = 確認時成交,否則下一個區塊退回。", "How long the order rests unfilled before the remainder auto-refunds.": "訂單在未成交狀態下掛單多久,之後剩餘部分自動退回。", + "How many blocks an unfilled remainder stays open before expiry. {n} is the shortest setting.": "未成交部分在到期前繼續掛單的區塊數。{n} 是最短設定。", "How much your own swap moves the price. Larger swaps against the fixed reserves get a worse average price.": "你自己的兌換會讓價格移動多少。面對固定的儲備,兌換量越大,平均價格越差。", "How that works": "如何運作", "How XCP-69 works": "XCP-69 如何運作", "If it fails to launch": "如果發射失敗", "If it's yours, connect that wallet to launch with it.": "如果它是你的,請連接對應錢包來發射。", "If launched": "如果發射成功", - "If the pool moves past this before confirmation, the whole transaction is void — nothing is debited; only the miner fee is spent.": "若確認前池價越過此值,整筆交易作廢 — 不會扣除任何資金,只花費礦工費。", + "If these limits cannot be met at confirmation, the liquidity operation is invalid. Pool assets are not debited; the Bitcoin network fee is still paid.": "若確認時無法滿足這些限額,流動性操作將無效。不會扣除池內資產,但仍需支付比特幣礦工費。", "Image": "圖片", "In {n} pool": "在 {n} 個池子中", "In {n} pools": "在 {n} 個池子中", @@ -745,7 +743,6 @@ "New buyers": "新買家", "Newest": "最新", "Next →": "下一頁 →", - "next block after BTC confirms": "BTC 確認後的下一個區塊", "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.": "不會 — 鑄造只是把 XCP 託管在該發射上。如果畢業,你持有代幣;如果退款,每一聰都會退回。鑄造真正花掉的只有比特幣礦工費 — 而這正是本計畫所覆蓋的部分。", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.": "不是 — 這正是重點。公平鑄造協議允許極大的變化:沒有軟頂、不退款、建立者佣金高達 99%、預挖、完全沒有流動性池。XCP-69 固定了其中的一種配置,本站只列出與標準完全匹配的發射,並逐欄位對照鏈上記錄檢查。在這裡,每一次發射的行為都完全相同,你永遠不需要自己審計參數。而你在其他任何地方遇到的公平鑄造合約都沒有這樣的保證 — 鑄造前請先讀它的欄位。", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.": "不是 — 它是結構性的,不是承諾。流動性池必須以鑄造價的 69/31 開盤,因為這是僅有的數量,但隨著人們向池中賣出,底價會不斷下降。沒有什麼能阻止代幣跌破鑄造價。池子保證的是一個永遠不會歸零的買價,而不是一個你會滿意的買價。", @@ -870,6 +867,7 @@ "Paste the profile URL or enter the handle.": "貼上主頁連結或輸入使用者名稱。", "Payouts": "發放", "pending": "待確認", + "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.": "待確認訂單可能使可收到的數量低於你的最低接收數量。若它們先確認,未成交部分將掛單至到期後退回。請檢查滑價設定,或選擇自動。", "People with xcp.fun open right now, including you. Several tabs from the same browser count once. Closing the tab removes you.": "此刻開啟 xcp.fun 的人數,包括你。同一瀏覽器的多個分頁只計一次。關閉分頁即不再計入。", "Per address": "每地址上限", "Per mint": "每筆鑄造", @@ -1017,6 +1015,7 @@ "Size": "數量", "Skipped: {reason}": "已跳過:{reason}", "Slippage": "滑點", + "Slippage tolerance of {pct}% permits receiving less than quoted.": "{pct}% 的滑價容忍度允許實際接收數量低於報價。", "Slippage: {pct}%": "滑點:{pct}%", "soft cap not reached": "soft cap not reached", "Sold": "賣出", @@ -1169,6 +1168,7 @@ "What xcp.fun shows for this launch, and what the hosted metadata says if your on-chain description points here. The on-chain field itself is locked forever; everything behind it is yours to curate.": "這是 xcp.fun 為這次發射展示的內容,以及當你的鏈上描述指向這裡時託管後設資料所顯示的內容。鏈上欄位本身已永久鎖定;它背後的一切都由你來維護。", "When": "時間", "When do I get paid?": "我什麼時候能收到?", + "when the BTC payment confirms": "BTC 付款確認時", "Where do I get the XCP to mint with?": "鑄造用的 XCP 從哪裡獲得?", "Where does the token art actually live?": "代幣圖片實際存放在哪裡?", "Which Counterparty collections the minters come from. Creators made a card there; collectors hold one.": "鑄造者來自哪些 Counterparty 收藏集。創作者在那裡創作過卡牌;收藏者持有一張。", diff --git a/apps/web/src/locales/zh-tw.status.json b/apps/web/src/locales/zh-tw.status.json index 8849dbb1..d79b4c68 100644 --- a/apps/web/src/locales/zh-tw.status.json +++ b/apps/web/src/locales/zh-tw.status.json @@ -230,9 +230,9 @@ "A new way to launch a coin on Counterparty — every launch identical, enforced by consensus.", "A pool never runs out, it only gets worse. This is what it can absorb before its price moves 1%.", "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.", - "A resting order refunds in full at expiry — the price you set is enforced by the order itself.", + "Any unfilled amount is returned at expiry. The order enforces your limit price.", "About", - "Above what the pending orders would leave. If they confirm first, this order rests for a block and refunds instead of filling — raise the slippage or use Auto.", + "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.", "Across {n} fully exited position", "Across {n} fully exited positions", "across {n} open launch", @@ -313,7 +313,7 @@ "bought without minting", "Broadcast", "Broadcast as {tx}. Minting opens at block {block} ({when}) — until then the launch is announced on-chain and nobody, you included, can mint. Then it runs for 1,000 blocks (~7 days): it sells out, or everyone is refunded.", - "Broadcast legs are final — each is its own transaction. Failed legs can be retried.", + "Each route is a separate transaction. Broadcast routes cannot be canceled here. Failed routes can be retried.", "Broadcast, waiting to confirm. Its page and editing open up once it lands in a block.", "Broadcasting commit…", "Broadcasting reveal…", @@ -519,7 +519,7 @@ "filled", "Filled", "Filled / expires", - "Fills now", + "Estimated fill", "Find a launch by asset name, and sort or filter the results.", "first at this price", "first launch", @@ -549,7 +549,7 @@ "held by {n} sellers", "Hide filled", "Hide minted", - "High slippage authorizes up to {pct}% price impact.", + "Slippage tolerance of {pct}% permits receiving less than quoted.", "Higher than this trade needs (~{pct}%).", "hold one, made none", "Holders", @@ -560,7 +560,7 @@ "how it works", "How it works", "How it works — xcp.fun", - "How long an unfilled remainder rests before auto-refund. {n} = fill at confirmation or refund next block.", + "How many blocks an unfilled remainder stays open before expiry. {n} is the shortest setting.", "How long the order rests unfilled before the remainder auto-refunds.", "How much your own swap moves the price. Larger swaps against the fixed reserves get a worse average price.", "How that works", @@ -568,7 +568,7 @@ "If it fails to launch", "If it's yours, connect that wallet to launch with it.", "If launched", - "If the pool moves past this before confirmation, the whole transaction is void — nothing is debited; only the miner fee is spent.", + "If these limits cannot be met at confirmation, the liquidity operation is invalid. Pool assets are not debited; the Bitcoin network fee is still paid.", "Image", "In {n} pool", "In {n} pools", @@ -722,7 +722,7 @@ "New buyers", "Newest", "Next →", - "next block after BTC confirms", + "when the BTC payment confirms", "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.", diff --git a/apps/web/src/locales/zh.json b/apps/web/src/locales/zh.json index 58e47434..f9c2b9e9 100644 --- a/apps/web/src/locales/zh.json +++ b/apps/web/src/locales/zh.json @@ -1,6 +1,6 @@ { "— minters": "— 铸造者", - "· {n} pending": "· {n} 笔待处理", + "· {n} pending": "· {n} 笔待确认", "· {pct}% over floor": "· 高于底价 {pct}%", "· {wallets} wallets · {txs} txs": "· {wallets} 个钱包 · {txs} 笔交易", "· above the mempool estimate": "· 高于内存池估算", @@ -46,7 +46,7 @@ "{label} {base} — or run your own counterparty-core node for trustless reads.": "{label} {base} — 或者运行你自己的 counterparty-core 节点,实现无需信任的读取。", "{label} {feature}, activated on mainnet at block 961,100 (2026-08-05). Requires core v11.2.0+.": "{label} {feature},已于主网区块 961,100(2026-08-05)激活。需要 core v11.2.0 及以上。", "{label} Counterparty on Bitcoin mainnet. XCP-69 launches are ordinary Bitcoin transactions carrying Counterparty messages.": "{label} 比特币主网上的 Counterparty。XCP-69 发射就是携带 Counterparty 消息的普通比特币交易。", - "{lead} {n} XCP left at {sats} sats/XCP. BTC lands with every sale. Closing settles ~5 blocks after it confirms and returns the rest.": "{lead} 剩余 {n} XCP,价格 {sats} sats/XCP。每卖出一笔,BTC 即到账。关闭后约 5 个区块完成结算,余下的 XCP 退回。", + "{lead} {n} XCP left at {sats} sats/XCP. BTC lands with every sale. Closing settles ~5 blocks after it confirms and returns the rest.": "{lead} 剩余 {n} XCP,价格为 {sats} sats/XCP。每次售出都会收到 BTC。关闭交易确认后约 5 个区块完成结算,并退回剩余 XCP。", "{market} price history with {n} points": "{market} 价格历史,共 {n} 个数据点", "{mint} A 1,000-block window (~7 days) from {startBlock}. Anyone can mint whole 1,000-token lots at 0.01 XCP per lot, up to 1,000,000 tokens (10 XCP) per address. Both the paid XCP and the minted tokens sit in escrow at the unspendable address — nobody holds anything until the launch resolves. The window length only ever delays failure: a sell-out settles the moment it fills, while a miss frees every minter's XCP within about a week.": "{mint} 从 {startBlock} 起的 1,000 个区块窗口期(约 7 天)。任何人都可以按每份 0.01 XCP 铸造整份 1,000 枚代币,每地址最多 1,000,000 枚(10 XCP)。支付的 XCP 和铸造的代币都由协议锁定在不可花费地址 — 在发射结束之前,没有人持有任何东西。窗口期长度只会推迟失败:售罄的那一刻就会结算,而未售罄则会在大约一周内释放每位铸造者的 XCP。", "{mints} across {launches} · {xcp} XCP committed": "{launches},共 {mints} · 已投入 {xcp} XCP", @@ -119,7 +119,7 @@ "{n} wallet": "{n} 个钱包", "{n} wallets": "{n} 个钱包", "{n} XCP ahead of you": "你前方还有 {n} XCP", - "{n} XCP incoming": "{n} XCP 到账中", + "{n} XCP incoming": "{n} XCP 待到账", "{n} XCP lands on your Counterparty balance as each payment confirms — ready to mint with.": "每笔付款确认后,{n} XCP 会进入你的 Counterparty 余额 — 可直接用于铸造。", "{n}+ addresses to sell out": "售罄需要 {n}+ 个地址", "{n}+ launches": "{n}+ 次发射", @@ -230,9 +230,7 @@ "A new way to launch a coin on Counterparty — every launch identical, enforced by consensus.": "在 Counterparty 上发射代币的新方式 — 每一次发射完全相同,由共识强制执行。", "A pool never runs out, it only gets worse. This is what it can absorb before its price moves 1%.": "流动性池永远不会枯竭,只会让价格越来越不利。这是价格变动 1% 之前它能吸收的量。", "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.": "退款的发射不等于亏钱。XCP-69 是全有或全无:未达软顶的发射会按共识退回每一聪,无需我们决定,也没有任何人能扣留。", - "A resting order refunds in full at expiry — the price you set is enforced by the order itself.": "挂单到期后全额退回 — 你设定的价格由订单本身强制执行。", "About": "关于", - "Above what the pending orders would leave. If they confirm first, this order rests for a block and refunds instead of filling — raise the slippage or use Auto.": "高于待处理订单成交后剩余的价格。若它们先确认,这笔订单将挂单一个区块后退回而非成交 — 请调高滑点或使用自动。", "Across {n} fully exited position": "共 {n} 个已完全退出的仓位", "Across {n} fully exited positions": "共 {n} 个已完全退出的仓位", "across {n} open launch": "共 {n} 个进行中的发射", @@ -268,7 +266,7 @@ "Amount of {asset} to sell": "卖出的 {asset} 数量", "Amount of XCP to deposit": "存入的 XCP 数量", "Amount to remove": "移除数量", - "Amount too small — rounds to 0": "数量太小 — 四舍五入为 0", + "Amount too small — rounds to 0": "数量太小 — 计算结果为 0", "An XCP bounty for the first three launches to graduate, and MINTS for every mint.": "最先毕业的三个发射可获 XCP 赏金,每笔铸造都可获 MINTS。", "an XCP fairminter on xcp.fun": "xcp.fun 上的 XCP 公平铸造合约", "Animated WEBP shows as a still frame in the announce channel — Telegram cannot play one. A GIF moves there too.": "动态 WEBP 在公告频道里只会显示为静态帧 — Telegram 无法播放。换成 GIF 在那里也能动。", @@ -277,6 +275,7 @@ "Announced before minting opens": "铸造开启前公告", "announced, not open": "已公告,未开始", "Any future block, {floor} — tighter than the presets go. Under ~2 hours pays double the network fee.": "任意未来区块,{floor} — 可比预设选项更近。少于约 2 小时需支付双倍矿工费。", + "Any unfilled amount is returned at expiry. The order enforces your limit price.": "未成交的数量将在到期后退回。订单会按你设定的限价执行。", "API base:": "API 基址:", "Are all Counterparty fairminters like this?": "所有 Counterparty 公平铸造合约都是这样吗?", "Arrival": "到账", @@ -314,7 +313,6 @@ "bought without minting": "未铸造直接买入", "Broadcast": "广播", "Broadcast as {tx}. Minting opens at block {block} ({when}) — until then the launch is announced on-chain and nobody, you included, can mint. Then it runs for 1,000 blocks (~7 days): it sells out, or everyone is refunded.": "已广播为 {tx}。铸造将在区块 {block}({when})开启 — 在此之前,发射已在链上公告,包括你在内的任何人都无法铸造。之后持续 1,000 个区块(约 7 天):要么售罄,要么所有人退款。", - "Broadcast legs are final — each is its own transaction. Failed legs can be retried.": "已广播的分段不可撤销 — 每段都是独立的交易。失败的分段可以重试。", "Broadcast, waiting to confirm. Its page and editing open up once it lands in a block.": "已广播,等待确认。进入区块后即可打开其页面并编辑。", "Broadcasting commit…": "正在广播 commit…", "Broadcasting reveal…": "正在广播 reveal…", @@ -389,8 +387,8 @@ "Completed and confirming reward batches, linked to the transactions that sent them.": "已完成和确认中的奖励批次,附有发放交易的链接。", "Compose failed ({status})": "构造交易失败({status})", "Composing transactions": "构造交易", - "composing…": "构造交易中…", - "Composing…": "构造交易中…", + "composing…": "正在准备交易…", + "Composing…": "正在准备交易…", "Confirm commit in wallet…": "请在钱包中确认 commit…", "confirm in wallet ({i} of {n})": "在钱包中确认(第 {i} / {n} 笔)", "Confirm in wallet…": "请在钱包中确认…", @@ -480,6 +478,7 @@ "Dumpers": "砸盘者", "Dumping now": "正在砸盘", "Each number counts unique addresses. A sale is a pool or order-book sale captured by xcp.fun. Dump means the address sold within {blocks} blocks of graduation. A meaningful balance is more than one token and more than 1% of the address's acquired amount.": "每个数字按独立地址计数。卖出指 xcp.fun 记录到的池子或订单簿卖出。砸盘指该地址在毕业后 {blocks} 个区块内卖出。可观余额指超过 1 枚代币且超过该地址获得量的 1%。", + "Each route is a separate transaction. Broadcast routes cannot be canceled here. Failed routes can be retried.": "每条路由都是独立的交易。已广播的路由无法在此取消,失败的路由可以重试。", "Earned": "已获得", "Earned so far": "已获得", "Edit token info": "编辑代币信息", @@ -492,6 +491,7 @@ "escrowed": "协议托管中", "escrowed until launch": "由协议锁定至发射结束", "Escrowed until the launch resolves: tokens if it sells out, full XCP refund if it doesn't.": "由协议锁定至发射有结果为止:售罄则得到代币,否则全额退回 XCP。", + "Estimated fill": "预计成交", "Event": "事件", "Every launch that conforms to XCP-69, and what has actually been minted.": "每一个符合 XCP-69 的发射,以及实际已铸造的数量。", "Every LP token is burned — this liquidity can never be withdrawn.": "全部 LP 代币已销毁 — 这份流动性永远无法撤出。", @@ -530,7 +530,6 @@ "filled": "已成交", "Filled": "已成交", "Filled / expires": "成交 / 到期", - "Fills now": "立即成交", "Find a launch by asset name, and sort or filter the results.": "按资产名称查找发射,并对结果排序或筛选。", "first at this price": "此价格下排在首位", "first launch": "首次发射", @@ -562,7 +561,6 @@ "held by {n} sellers": "由 {n} 个卖家持有", "Hide filled": "隐藏已成交", "Hide minted": "隐藏已铸造", - "High slippage authorizes up to {pct}% price impact.": "高滑点允许最高 {pct}% 的价格影响。", "Higher than this trade needs (~{pct}%).": "高于本次交易所需(约 {pct}%)。", "hold one, made none": "持有但未创作", "Holders": "持有者", @@ -575,15 +573,15 @@ "how it works": "运作机制", "How it works": "运作机制", "How it works — xcp.fun": "运作机制 — xcp.fun", - "How long an unfilled remainder rests before auto-refund. {n} = fill at confirmation or refund next block.": "未成交部分在自动退回前挂单多久。{n} = 确认时成交,否则下一个区块退回。", "How long the order rests unfilled before the remainder auto-refunds.": "订单在未成交状态下挂单多久,之后剩余部分自动退回。", + "How many blocks an unfilled remainder stays open before expiry. {n} is the shortest setting.": "未成交部分在到期前继续挂单的区块数。{n} 是最短设置。", "How much your own swap moves the price. Larger swaps against the fixed reserves get a worse average price.": "你自己的兑换会让价格移动多少。面对固定的储备,兑换量越大,平均价格越差。", "How that works": "如何运作", "How XCP-69 works": "XCP-69 如何运作", "If it fails to launch": "如果发射失败", "If it's yours, connect that wallet to launch with it.": "如果它是你的,请连接对应钱包来发射。", "If launched": "如果发射成功", - "If the pool moves past this before confirmation, the whole transaction is void — nothing is debited; only the miner fee is spent.": "若确认前池价越过此值,整笔交易作废 — 不会扣除任何资金,只花费矿工费。", + "If these limits cannot be met at confirmation, the liquidity operation is invalid. Pool assets are not debited; the Bitcoin network fee is still paid.": "若确认时无法满足这些限额,流动性操作将无效。不会扣除池内资产,但仍需支付比特币矿工费。", "Image": "图片", "In {n} pool": "在 {n} 个池子中", "In {n} pools": "在 {n} 个池子中", @@ -745,7 +743,6 @@ "New buyers": "新买家", "Newest": "最新", "Next →": "下一页 →", - "next block after BTC confirms": "BTC 确认后的下一个区块", "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.": "不会 — 铸造只是把 XCP 托管在该发射上。如果毕业,你持有代币;如果退款,每一聪都会退回。铸造真正花掉的只有比特币矿工费 — 而这正是本计划所覆盖的部分。", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.": "不是 — 这正是重点。公平铸造协议允许极大的变化:没有软顶、不退款、创建者佣金高达 99%、预挖、完全没有流动性池。XCP-69 固定了其中的一种配置,本站只列出与标准完全匹配的发射,并逐字段对照链上记录检查。在这里,每一次发射的行为都完全相同,你永远不需要自己审计参数。而你在其他任何地方遇到的公平铸造合约都没有这样的保证 — 铸造前请先读它的字段。", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.": "不是 — 它是结构性的,不是承诺。流动性池必须以铸造价的 69/31 开盘,因为这是仅有的数量,但随着人们向池中卖出,底价会不断下降。没有什么能阻止代币跌破铸造价。池子保证的是一个永远不会归零的买价,而不是一个你会满意的买价。", @@ -870,6 +867,7 @@ "Paste the profile URL or enter the handle.": "粘贴主页链接或输入用户名。", "Payouts": "发放", "pending": "待确认", + "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.": "待确认订单可能使可收到的数量低于你的最低接收数量。若它们先确认,未成交部分将挂单至到期后退回。请检查滑点设置,或选择自动。", "People with xcp.fun open right now, including you. Several tabs from the same browser count once. Closing the tab removes you.": "此刻打开 xcp.fun 的人数,包括你。同一浏览器的多个标签页只计一次。关闭标签页即不再计入。", "Per address": "每地址上限", "Per mint": "每笔铸造", @@ -967,7 +965,7 @@ "route no longer has enough left": "路由剩余数量不足", "route price changed": "路由价格已变动", "Routes": "路由", - "Routing includes miner fees — a deep route can beat a cheaper, shallower one.": "路由已计入矿工费 — 深度更大的路由可能胜过更便宜但更浅的路由。", + "Routing includes miner fees — a deep route can beat a cheaper, shallower one.": "选择路由时已计入矿工费。流动性更充足的路由,总成本可能低于单价更低但流动性不足的路由。", "Sale closing": "销售关闭中", "Save failed": "保存失败", "Saved. Changes are live; other open pages may take a minute to refresh.": "已保存。更改已生效;其他已打开的页面可能需要一分钟才会刷新。", @@ -1017,6 +1015,7 @@ "Size": "数量", "Skipped: {reason}": "已跳过:{reason}", "Slippage": "滑点", + "Slippage tolerance of {pct}% permits receiving less than quoted.": "{pct}% 的滑点容忍度允许实际接收数量低于报价。", "Slippage: {pct}%": "滑点:{pct}%", "soft cap not reached": "soft cap not reached", "Sold": "卖出", @@ -1141,11 +1140,11 @@ "Upcoming": "即将开始", "Upload failed": "上传失败", "Upload token image": "上传代币图片", - "Use digits and a dot, with up to {n} decimal places.": "请使用半角数字和小数点,最多保留{n}位小数。", + "Use digits and a dot, with up to {n} decimal places.": "请用 0-9 和小数点,最多 {n} 位小数。", "Use whole numbers only.": "请只输入整数。", "Value": "价值", "Value and underlying amounts are what withdrawing the LP tokens now would return. XCP value is available when XCP is one side of the pair. LP profit and loss is not estimated.": "价值和底层数量指现在提取 LP 代币能拿回的资产。当交易对一侧是 XCP 时才有 XCP 价值。不估算 LP 的盈亏。", - "Vends": "出售中", + "Vends": "可售次数", "Venue": "成交渠道", "Venue / time": "成交渠道 / 时间", "Verdict pending": "待判定", @@ -1169,6 +1168,7 @@ "What xcp.fun shows for this launch, and what the hosted metadata says if your on-chain description points here. The on-chain field itself is locked forever; everything behind it is yours to curate.": "这是 xcp.fun 为这次发射展示的内容,以及当你的链上描述指向这里时托管元数据所显示的内容。链上字段本身已永久锁定;它背后的一切都由你来维护。", "When": "时间", "When do I get paid?": "我什么时候能收到?", + "when the BTC payment confirms": "BTC 付款确认时", "Where do I get the XCP to mint with?": "铸造用的 XCP 从哪里获得?", "Where does the token art actually live?": "代币图片实际存放在哪里?", "Which Counterparty collections the minters come from. Creators made a card there; collectors hold one.": "铸造者来自哪些 Counterparty 收藏集。创作者在那里创作过卡牌;收藏者持有一张。", @@ -1210,7 +1210,7 @@ "you": "你", "you · {n} XCP": "你 · {n} XCP", "you have minted this launch’s max": "你已铸造到这次发射的上限", - "You mint": "你将获得", + "You mint": "铸造数量", "You own {name}, but {reason}.": "你拥有 {name},但{reason}。", "You pay": "你支付", "You receive": "你收到", diff --git a/apps/web/src/locales/zh.status.json b/apps/web/src/locales/zh.status.json index 44f3b5b5..a03c30b2 100644 --- a/apps/web/src/locales/zh.status.json +++ b/apps/web/src/locales/zh.status.json @@ -29,10 +29,10 @@ "A new way to launch a coin on Counterparty — every launch identical, enforced by consensus.", "A pool never runs out, it only gets worse. This is what it can absorb before its price moves 1%.", "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.", - "A resting order refunds in full at expiry — the price you set is enforced by the order itself.", + "Any unfilled amount is returned at expiry. The order enforces your limit price.", "API base:", "About", - "Above what the pending orders would leave. If they confirm first, this order rests for a block and refunds instead of filling — raise the slippage or use Auto.", + "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.", "Across {n} fully exited position", "Across {n} fully exited positions", "Across {n} open position", @@ -94,7 +94,7 @@ "Bought", "Broadcast", "Broadcast as {tx}. Minting opens at block {block} ({when}) — until then the launch is announced on-chain and nobody, you included, can mint. Then it runs for 1,000 blocks (~7 days): it sells out, or everyone is refunded.", - "Broadcast legs are final — each is its own transaction. Failed legs can be retried.", + "Each route is a separate transaction. Broadcast routes cannot be canceled here. Failed routes can be retried.", "Broadcast, waiting to confirm. Its page and editing open up once it lands in a block.", "Broadcasting commit…", "Broadcasting reveal…", @@ -256,7 +256,7 @@ "Fetching quote…", "Filled", "Filled / expires", - "Fills now", + "Estimated fill", "Find a launch by asset name, and sort or filter the results.", "First seen", "Floor", @@ -278,7 +278,7 @@ "Hard cap", "Hide filled", "Hide minted", - "High slippage authorizes up to {pct}% price impact.", + "Slippage tolerance of {pct}% permits receiving less than quoted.", "Higher than this trade needs (~{pct}%).", "Holders", "Holders:", @@ -287,14 +287,14 @@ "How XCP-69 works", "How it works", "How it works — xcp.fun", - "How long an unfilled remainder rests before auto-refund. {n} = fill at confirmation or refund next block.", + "How many blocks an unfilled remainder stays open before expiry. {n} is the shortest setting.", "How long the order rests unfilled before the remainder auto-refunds.", "How much your own swap moves the price. Larger swaps against the fixed reserves get a worse average price.", "How that works", "If it fails to launch", "If it's yours, connect that wallet to launch with it.", "If launched", - "If the pool moves past this before confirmation, the whole transaction is void — nothing is debited; only the miner fee is spent.", + "If these limits cannot be met at confirmation, the liquidity operation is invalid. Pool assets are not debited; the Bitcoin network fee is still paid.", "Image", "In XCP terms, exactly. In fiat terms, only if XCP's price held during the ~week-long window. Refunds return quantity, not value.", "In mempool", @@ -918,7 +918,7 @@ "named assets only, divisible", "need XCP?", "new address", - "next block after BTC confirms", + "when the BTC payment confirms", "no history", "none — the creator mints like everyone else", "none — you already own this name", From 1df273846179bdd003d9c267e460ec1d051563f3 Mon Sep 17 00:00:00 2001 From: Dan Anderson Date: Mon, 7 Sep 2026 19:03:33 -0400 Subject: [PATCH 04/10] fix: block invalid custom slippage drafts in Auto mode --- .../[lang]/swap/_components/swap-settings.tsx | 4 +- tests/trade-settings.test.tsx | 81 ++++++++++++++++++- 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/[lang]/swap/_components/swap-settings.tsx b/apps/web/src/app/[lang]/swap/_components/swap-settings.tsx index 3b17c1bc..0fb579fb 100644 --- a/apps/web/src/app/[lang]/swap/_components/swap-settings.tsx +++ b/apps/web/src/app/[lang]/swap/_components/swap-settings.tsx @@ -150,7 +150,7 @@ export function SwapSettingsProvider({ children }: { children: ReactNode }) { slippage, expiration, customFee, - swapSettingsValid: feeDraft.valid && expirationDraft.valid && (slippageAuto || slipDraft.valid), + swapSettingsValid: feeDraft.valid && expirationDraft.valid && slipDraft.valid, limitSettingsValid, liquiditySettingsValid: feeDraft.valid && lqSlipDraft.valid, lqCustomSlip, @@ -223,7 +223,7 @@ export function SwapSettingsGear() { max={50} onChange={(v) => { s.setCustomSlippage(v); - if (v.trim() !== "") s.setSlippageAuto(false); + if (v !== "") s.setSlippageAuto(false); }} placeholder="1.5" ariaLabel={t("Custom slippage percent")} diff --git a/tests/trade-settings.test.tsx b/tests/trade-settings.test.tsx index 1bdc3344..3000c866 100644 --- a/tests/trade-settings.test.tsx +++ b/tests/trade-settings.test.tsx @@ -1,10 +1,15 @@ +// @vitest-environment happy-dom +import { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; import { renderToStaticMarkup } from "react-dom/server"; import { SWRConfig } from "swr"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { + SwapSettingsGear, SwapSettingsProvider, useSwapSettings, } from "../apps/web/src/app/[lang]/swap/_components/swap-settings"; +import { SETTINGS_DEFAULTS, updateSettings } from "../apps/web/src/app/[lang]/swap/_lib/trade-settings-store"; const snapshot = vi.hoisted(() => ({ customFeeRate: "" })); @@ -18,6 +23,80 @@ vi.mock("@/app/[lang]/swap/_lib/trade-settings-store", async (importOriginal) => }; }); +let root: Root | undefined; +let container: HTMLDivElement | undefined; +globalThis.IS_REACT_ACT_ENVIRONMENT = true; +afterEach(async () => { + if (root) await act(() => root!.unmount()); + root = undefined; + container?.remove(); + container = undefined; +}); + +async function mountSettings(customSlippage = "") { + updateSettings({ ...SETTINGS_DEFAULTS, customSlippage }); + const compose = vi.fn(); + function SubmitConsumer() { + const settings = useSwapSettings(); + return { + event.preventDefault(); + if (settings.swapSettingsValid) compose(settings.slippage); + }}> + {String(settings.swapSettingsValid)} + + ; + } + container = document.createElement("div"); + document.body.append(container); + root = createRoot(container); + await act(() => root!.render( + new Map(), revalidateOnMount: false }}> + + , + )); + await act(() => { container!.querySelector('button[aria-label="Swap settings"]')!.click(); }); + return { + input: document.querySelector('input[aria-label="Custom slippage percent"]')!, + button: container.querySelector("form button")!, + output: container.querySelector("output")!, + compose, + }; +} + +describe("rendered custom slippage settings", () => { + it.each([" ", "\u00a0"])("keeps typed whitespace %j invalid and opts out of Auto", async (draft) => { + const { input, button, output, compose } = await mountSettings(); + expect(button.disabled).toBe(false); + await act(() => { + Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!.call(input, draft); + input.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: draft })); + }); + expect(input.value).toBe(draft); + expect(input.getAttribute("aria-invalid")).toBe("true"); + expect(output.dataset.auto).toBe("false"); + expect(button.disabled).toBe(true); + await act(() => { container!.querySelector("form")!.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true })); }); + expect(compose).not.toHaveBeenCalled(); + + const auto = [...document.querySelectorAll("button")].find((node) => node.textContent === "Auto")!; + await act(() => { auto.click(); }); + expect(input.value).toBe(""); + expect(output.dataset.auto).toBe("true"); + expect(button.disabled).toBe(false); + await act(() => { button.click(); }); + expect(compose).toHaveBeenCalledWith(1); + }); + + it("blocks an invalid saved draft even if the old Auto flag is still true", async () => { + const { input, button, compose } = await mountSettings(" "); + expect(input.value).toBe(" "); + expect(input.getAttribute("aria-invalid")).toBe("true"); + expect(button.disabled).toBe(true); + await act(() => { container!.querySelector("form")!.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true })); }); + expect(compose).not.toHaveBeenCalled(); + }); +}); + function FeeConsumer() { const { customFee, swapSettingsValid } = useSwapSettings(); return {customFee ?? "auto"}:{String(swapSettingsValid)}; From 3113116a3a7ac18d5241c75708293ba033d1de0f Mon Sep 17 00:00:00 2001 From: Dan Anderson Date: Mon, 7 Sep 2026 19:07:02 -0400 Subject: [PATCH 05/10] Clarify Japanese and Chinese transaction copy --- apps/web/src/locales/ja.json | 46 +++++++++++++------------- apps/web/src/locales/ja.status.json | 10 +++--- apps/web/src/locales/zh-hk.json | 42 +++++++++++------------ apps/web/src/locales/zh-hk.status.json | 10 +++--- apps/web/src/locales/zh-tw.json | 44 ++++++++++++------------ apps/web/src/locales/zh-tw.status.json | 10 +++--- apps/web/src/locales/zh.json | 22 ++++++------ apps/web/src/locales/zh.status.json | 10 +++--- 8 files changed, 97 insertions(+), 97 deletions(-) diff --git a/apps/web/src/locales/ja.json b/apps/web/src/locales/ja.json index f366f303..2e447fd7 100644 --- a/apps/web/src/locales/ja.json +++ b/apps/web/src/locales/ja.json @@ -88,9 +88,9 @@ "{n} of {min} minters": "ミンター {n}/{min} 人", "{n} of {total} minters": "{total} 人のミンター中 {n} 人", "{n} or later": "{n} 以降", - "{n} pending": "{n} 件保留中", - "{n} pending transaction": "{n} 件の保留中トランザクション", - "{n} pending transactions": "{n} 件の保留中トランザクション", + "{n} pending": "{n} 件承認待ち", + "{n} pending transaction": "{n} 件の承認待ちトランザクション", + "{n} pending transactions": "{n} 件の承認待ちトランザクション", "{n} refund · {xcp} XCP returned · about {when}": "{n} 件の返金 · {xcp} XCP 返還 · {when}頃", "{n} refunded · {pct}%": "{n} 件が返金 · {pct}%", "{n} refunded hidden": "返金済み {n} 件を非表示", @@ -109,8 +109,8 @@ "{n} txs": "{n} tx", "{n} TXs": "{n} TX", "{n} txs · {amounts} XCP": "{n} tx · {amounts} XCP", - "{n} unconfirmed order on this pair in the same direction. If they confirm first, this trade gets about {pct}% less than the quote. Auto slippage allows for it.": "このペアに同方向の未承認注文が {n} 件あります。先に承認されると、この取引は見積もりより約 {pct}% 少なくなります。自動スリッページはこれを見込んでいます。", - "{n} unconfirmed orders on this pair in the same direction. If they confirm first, this trade gets about {pct}% less than the quote. Auto slippage allows for it.": "このペアに同方向の未承認注文が {n} 件あります。先に承認されると、この取引は見積もりより約 {pct}% 少なくなります。自動スリッページはこれを見込んでいます。", + "{n} unconfirmed order on this pair in the same direction. If they confirm first, this trade gets about {pct}% less than the quote. Auto slippage allows for it.": "このペアに同方向の未承認注文が {n} 件あります。先に承認されると、受取数量は見積もりより約 {pct}% 少なくなります。自動スリッページはこれを見込んでいます。", + "{n} unconfirmed orders on this pair in the same direction. If they confirm first, this trade gets about {pct}% less than the quote. Auto slippage allows for it.": "このペアに同方向の未承認注文が {n} 件あります。先に承認されると、受取数量は見積もりより約 {pct}% 少なくなります。自動スリッページはこれを見込んでいます。", "{n} unconfirmed transaction in the mempool": "メンプールに未承認トランザクション {n} 件", "{n} unconfirmed transactions in the mempool": "メンプールに未承認トランザクション {n} 件", "{n} unique addresses": "{n} のユニークアドレス", @@ -154,7 +154,7 @@ "{recipe} Success and failure both end at fairminter status {closed}, so the pool row is the disambiguator:": "{recipe} 成功も失敗もフェアミンターのステータスは {closed} で終わるため、プールの行が判別材料になります:", "{recipients} recipients · {mints} mint transactions": "{recipients} 名の受取人 · {mints} 件のミントトランザクション", "{resolve} All-or-nothing at the 69M soft cap. The soft cap equals the entire public sale, so reaching it {is} selling out — there is no partial success. Sell out and the pool seeds; miss the deadline and the protocol refunds every minter automatically and destroys the escrowed supply. Resolution happens at end-of-block even on a hard-cap fill, so nobody can trade the pool in the transaction that creates it.": "{resolve} 69M のソフトキャップでオール・オア・ナッシング。ソフトキャップは公開販売の全量と等しいので、到達は{is}完売を意味します — 部分的な成功はありません。完売すればプールが作られ、締切に届かなければプロトコルが全ミンターに自動で返金し、エスクローされた供給を破棄します。ハードキャップで埋まった場合でも確定はブロック末尾で行われるため、プールを作るトランザクションの中でプールを取引することは誰にもできません。", - "{sells} pending sells · {wallets} wallets · {tokens} tokens": "{sells} 件の保留中の売り · {wallets} ウォレット · {tokens} トークン", + "{sells} pending sells · {wallets} wallets · {tokens} tokens": "{sells} 件の承認待ち売り注文 · {wallets} ウォレット · {tokens} トークン", "{shown} of {total}": "{total} 件中 {shown} 件", "{status} → minting": "{status} → ミント中", "{status} → scheduled": "{status} → 予定", @@ -174,8 +174,8 @@ "↘ Sell": "↘ 売り", "↩ Minting — {xcp} XCP{usd} raised so far, all of it returned automatically if the launch doesn't sell out.": "↩ ミント中 — これまでに {xcp} XCP{usd} を調達。ローンチが完売しなければ全額が自動的に返ります。", "+ {amount} XCP waiting to confirm": "+ {amount} XCP が承認待ち", - "+{amount} pending": "+{amount} 保留中", - "+{n} hidden — purchase pending in mempool": "+{n} 件非表示 — メンプールで購入保留中", + "+{amount} pending": "+{amount} 承認待ち", + "+{n} hidden — purchase pending in mempool": "+{n} 件非表示 — メンプールで購入の承認待ち", "+{n} more below.": "他 {n} 件は下に。", "+{n} more on the explorer ↗": "他 {n} 件はエクスプローラーで ↗", "+{pct}% over floor · waits": "最安売値より +{pct}% · 待機", @@ -251,7 +251,7 @@ "addresses": "アドレス", "adjusts to {n}": "{n} に調整", "After graduation": "卒業後", - "After mempool": "メンプール処理後", + "After mempool": "未承認注文を反映後", "All": "すべて", "All data shown on this site is public on-chain data; anything you see here you can verify yourself against a Counterparty node. The site is an interface, not a counterparty to any transaction — it never holds funds and cannot reverse, expedite, or refund anything (the protocol handles refunds on its own). Nothing here is investment advice; tokens launched through XCP-69 can and will lose value. Read the {link} before minting.": "このサイトに表示されるデータはすべて公開のオンチェーンデータで、ここで見るものはすべて Counterparty ノードで自分で検証できます。このサイトはインターフェースであり、いかなるトランザクションの相手方でもありません — 資金を保持せず、取り消し・優先処理・返金はできません(返金はプロトコルが自ら処理します)。ここにあるものは投資助言ではなく、XCP-69 でローンチされたトークンは価値を失い得るし、実際に失います。ミントの前に{link}をお読みください。", "All three bounties have been claimed.": "3 つの報奨金はすべて獲得済みです。", @@ -335,7 +335,7 @@ "Buy {n} XCP · {routes} routes": "{n} XCP を買う · {routes} ルート", "Buy BTC ↗": "BTC を買う ↗", "Buy from the cheapest dispensers here": "最安の自販機からここで購入", - "Buy pending": "買い注文 保留中", + "Buy pending": "買い注文 承認待ち", "Buy some with BTC": "BTC で購入する", "Buy XCP": "XCP を買う", "Buy XCP →": "XCP を買う →", @@ -474,7 +474,7 @@ "Double network fee at this lead — there aren't enough blocks to re-fee if the estimate comes in low, and a launch that confirms after it opens fails the standard.": "この先行時間ではネットワーク手数料が 2 倍になります — 見積もりが低かった場合に手数料を上げ直す余裕のブロック数がなく、開始後に承認されたローンチは標準を満たしません。", "down": "下落", "dropped — nothing was spent": "破棄 — 何も消費されていません", - "Dump means the first sale landed within six blocks of graduation. Repeat dump means that happened on more than one earlier launch. Balances come from Counterparty and may include tokens obtained outside the mint. Pending mempool orders can disappear without confirming or filling. Addresses are not necessarily people.": "ダンプとは、最初の売却が卒業から 6 ブロック以内に行われたことです。常習ダンプとは、それが過去の複数のローンチで起きたことです。残高は Counterparty から取得しており、ミント以外で得たトークンを含む場合があります。メンプールの保留中注文は承認も約定もせずに消えることがあります。アドレスは必ずしも人ではありません。", + "Dump means the first sale landed within six blocks of graduation. Repeat dump means that happened on more than one earlier launch. Balances come from Counterparty and may include tokens obtained outside the mint. Pending mempool orders can disappear without confirming or filling. Addresses are not necessarily people.": "ダンプとは、最初の売却が卒業から 6 ブロック以内に行われたことです。常習ダンプとは、それが過去の複数のローンチで起きたことです。残高は Counterparty から取得しており、ミント以外で得たトークンを含む場合があります。メンプールの未承認注文は承認も約定もせずに消えることがあります。アドレスは必ずしも人ではありません。", "Dumpers": "ダンパー", "Dumping now": "ダンプ中", "Each number counts unique addresses. A sale is a pool or order-book sale captured by xcp.fun. Dump means the address sold within {blocks} blocks of graduation. A meaningful balance is more than one token and more than 1% of the address's acquired amount.": "各数値はユニークアドレスを数えます。売却とは xcp.fun が捕捉したプールまたは板での売却です。ダンプとは、そのアドレスが卒業から {blocks} ブロック以内に売却したことを意味します。意味のある残高とは 1 トークン超、かつそのアドレスの取得量の 1% 超です。", @@ -489,7 +489,7 @@ "Enter an amount": "数量を入力", "Enter opens the first result · Esc closes": "Enter で先頭の結果を開く · Esc で閉じる", "escrowed": "エスクロー中", - "escrowed until launch": "ローンチまでエスクロー", + "escrowed until launch": "結果確定までエスクロー", "Escrowed until the launch resolves: tokens if it sells out, full XCP refund if it doesn't.": "ローンチの結果が出るまでエスクローされます。完売すればトークン、そうでなければ XCP を全額返金。", "Estimated fill": "約定見込み", "Event": "イベント", @@ -497,11 +497,10 @@ "Every LP token is burned — this liquidity can never be withdrawn.": "LP トークンはすべてバーン済み — この流動性は二度と引き出せません。", "Every mint on a conforming launch, counted. Ranked by mints, because that is the unit the reward is paid in.": "準拠ローンチでのすべてのミントを集計。報酬が支払われる単位であるミント数で順位付けしています。", "Every minted token dumped in one trade — the pool still quotes a bid with {left} XCP left, and sellers exit at {mult}× mint ({paid}% of what they paid). Those {left} XCP can never be withdrawn: every launch is a permanent sink for XCP itself.": "ミントされた全トークンを一度の取引で投げ売り — それでもプールは {left} XCP を残して買い気配を出し続け、売り手はミント価格の {mult}×(支払額の {paid}%)で退出します。その {left} XCP は二度と引き出せません: すべてのローンチは XCP そのものの永久の吸収先です。", - "Every route has a purchase pending in the mempool — check back in a few minutes.": "すべてのルートで購入がメンプール保留中です — 数分後にもう一度お試しください。", + "Every route has a purchase pending in the mempool — check back in a few minutes.": "すべてのルートで購入がメンプール内で承認待ちです — 数分後にもう一度お試しください。", "Every XCP-69 launch is identical: 100M supply, 69M public sale at 0.01 XCP per 1,000-token lot, 31M reserved for the liquidity pool, 10 XCP per-address cap, an on-chain pre-announcement before minting opens, and a 1,000-block window. There is no fine print to read because there is no fine print. The full parameter set lives on the {link} page.": "XCP-69 のローンチはすべて同一です: 発行量 100M、1,000 トークンのロットあたり 0.01 XCP で 69M を公開販売、31M は流動性プール用に確保、アドレスごとの上限 10 XCP、ミント開始前のオンチェーン事前告知、そして 1,000 ブロックの期間。細則がないので、読むべき細則もありません。パラメータの全セットは{link}ページにあります。", "Every XCP-69 mint, trade, burn, resting order and launch, newest first.": "XCP-69 のミント・取引・バーン・待機注文・ローンチのすべてを新しい順に。", "everyone dumps": "全員が投げ売り", - "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, what it costs (nothing), and how to integrate.": "XCP-69 のすべてを一箇所に: ローンチの仕組み、価格の仕組み、卒業の意味、費用(無料)、そして連携方法。", "Everything about XCP-69, in one place. The first half explains how launches work for anyone. The second half is for developers who want to read or build on the same on-chain data this site does.": "XCP-69 のすべてを一箇所に。前半は誰にでも分かるローンチの仕組み。後半は、このサイトと同じオンチェーンデータを読み、その上に構築したい開発者向けです。", "Everything is curl-able. Launches and mints:": "すべて curl で取れます。ローンチとミント:", "Everything this site displays comes from public APIs. You can build your own launchpad, bot, or dashboard on the same data — nothing below requires our permission.": "このサイトが表示するものはすべて公開 API から来ています。同じデータの上に独自のローンチパッド、ボット、ダッシュボードを作れます — 以下のどれにも私たちの許可は要りません。", @@ -549,7 +548,6 @@ "Graduated": "卒業", "graduated coins": "卒業銘柄", "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.": "卒業したトークンは定数積の TOKEN/XCP プールで取引されます。価格は単にプールの準備金の比率で、スワップのたびに動きます。各スワップの固定手数料 {fee} はプール自身に支払われます — LP はバーン済みなので、手数料は誰かに配られるのではなく、ロックされた流動性を厚くします。", - "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses, at which point the pool is created and its liquidity is burned. A launch that misses its target refunds every satoshi by consensus and does not count. {link}": "卒業とは完売のことです: 少なくとも {n} の異なるアドレスから {xcp} XCP を調達し、その時点でプールが作られ、流動性がバーンされます。目標に届かなかったローンチはコンセンサスにより 1 satoshi 残らず返金され、カウントされません。{link}", "Graduation": "卒業", "Graveyard": "墓場", "Graveyard — xcp.fun": "墓場 — xcp.fun", @@ -743,7 +741,6 @@ "New buyers": "新規の買い手", "Newest": "新着", "Next →": "次へ →", - "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.": "いいえ — ミントはローンチに対して XCP をエスクローするだけです。卒業すればトークンを保有し、返金なら 1 satoshi 残らず戻ります。ミントで実際にかかるのは Bitcoin の手数料だけで、このプログラムがカバーするのはその部分です。", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.": "いいえ — そこが要点です。フェアミンターのプロトコルは非常に幅広い設定を許します: ソフトキャップなし、返金なし、最大 99% の作成者コミッション、プレマイン、プールなし。XCP-69 はそのうち一つの構成を固定し、このサイトはオンチェーンの記録とフィールドごとに照合して標準に完全一致するローンチだけを掲載します。ここではすべてのローンチが同じ振る舞いをし、パラメータを自分で監査する必要はありません。他の場所で出会うフェアミンターにはそんな保証はありません — ミントの前にフィールドを読んでください。", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.": "いいえ — 約束ではなく構造上の結果です。存在する数量が 69/31 しかないため、プールはミント価格の 69/31 でオープンせざるを得ませんが、フロアは売られるほど下がります。トークンがミント価格を下回って取引されることを止めるものはありません。プールが保証するのはゼロにならない買い気配であって、あなたが満足する買い気配ではありません。", "No active {give}/{get} pool liquidity. A resting order can still fill through the order book.": "{give}/{get} プールに有効な流動性がありません。待機注文は板経由で約定できます。", @@ -816,7 +813,7 @@ "Of the launches that finished": "終了したローンチのうち", "of the typical mint fee": "一般的なミント手数料に対して", "of which {amount} realized": "うち {amount} は実現済み", - "Of XCP minted": "ミント XCP の割合", + "Of XCP minted": "投入 XCP の割合", "Off-chain by default: the chain permanently carries the URL of the asset-info JSON, and the content behind it is curatable by the asset's current owner (wallet-signature gated). Taproot creators can instead inscribe the image on-chain as the permanent description — no off-chain dependency at all. Either way, nothing economic depends on hosted content.": "デフォルトではオフチェーンです: チェーンはアセット情報 JSON の URL を永久に保持し、その先の内容はアセットの現在の所有者が(ウォレット署名で)編集できます。Taproot の作成者は代わりに画像を永久の説明文としてオンチェーンに刻めます — オフチェーンへの依存が一切なくなります。どちらにせよ、経済的な要素はホストされた内容に依存しません。", "On other launchpads, graduation is a threshold inside a live market: the token trades on a curve while everyone hopes it reaches the magic number. XCP-69 graduation is {binary} and happens before any trading exists. Sell out the 69M sale within 1,000 blocks and the launch graduates — pool seeded, LP burned, supply locked, trading live in the same block's resolution phase. Miss it and the launch never trades at all.": "他のローンチパッドでは、卒業は稼働中のマーケット内の閾値です: 皆が魔法の数字に届くことを願う間、トークンはカーブ上で取引されます。XCP-69 の卒業は{binary}で、取引が存在する前に起こります。1,000 ブロック以内に 69M の販売を完売させればローンチは卒業します — プールが作られ、LP がバーンされ、供給がロックされ、同じブロックの確定フェーズで取引が始まります。届かなければ、ローンチは一度も取引されません。", "on their Counterparty balance": "Counterparty 残高に", @@ -858,7 +855,7 @@ "Page {page} of {total}": "{total} ページ中 {page} ページ目", "Pagination": "ページ送り", "Paid": "支払額", - "paid as it sells": "売れるたびに支払い", + "paid as it sells": "売れるたびに受け取り", "Paired XCP": "ペアの XCP", "Partial": "一部", "partially filled · resting": "一部約定 · 待機中", @@ -866,7 +863,7 @@ "Partially. The 10 XCP per-address cap makes a fake crowd cost at least 69 funded addresses — it raises the price of the act, it cannot prevent it. Sybil-resistant in cost, not in principle. The mint tape, address histories, and holder spread are all public, so a manufactured crowd has to fake those too.": "部分的には。アドレスごとに 10 XCP の上限があるため、偽の群衆には少なくとも 69 の資金を持つアドレスが必要です — 行為のコストは上がりますが、防ぐことはできません。コスト面ではシビル耐性がありますが、原理的にはありません。ミント記録、アドレスの履歴、ホルダーの分布はすべて公開なので、作られた群衆はそれらも偽装しなければなりません。", "Paste the profile URL or enter the handle.": "プロフィールの URL を貼り付けるか、ハンドルを入力してください。", "Payouts": "支払い", - "pending": "保留中", + "pending": "承認待ち", "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.": "未承認の注文が先に約定すると、最低受取量を下回る可能性があります。未約定分は有効期限まで待機した後に返還されます。スリッページを確認するか、自動を選んでください。", "People with xcp.fun open right now, including you. Several tabs from the same browser count once. Closing the tab removes you.": "あなたを含め、今 xcp.fun を開いている人の数。同じブラウザの複数タブは 1 人として数えます。タブを閉じると外れます。", "Per address": "1 アドレスあたり", @@ -989,7 +986,7 @@ "Sell {asset} — market order": "{asset} を売る — 成行注文", "Sell {n} XCP": "{n} XCP を売る", "Sell out the launch above — the pool only exists on the other side of the line.": "上のローンチを完売させてください — プールは線の向こう側にしか存在しません。", - "Sell pending": "売り注文 保留中", + "Sell pending": "売り注文 承認待ち", "Sell XCP": "XCP を売る", "Seller inventory": "売り手の在庫", "Seller inventory cleared": "売り手の在庫は解消", @@ -1191,7 +1188,6 @@ "XCP in the pool": "プール内の XCP", "XCP market · cached 15 min": "XCP 市場 · 15 分キャッシュ", "XCP Price": "XCP 価格", - "XCP reserve ÷ token reserve. Moves with every swap; there is no order book and no market maker.": "XCP 準備金 ÷ トークン準備金。スワップのたびに動きます。板もマーケットメイカーもありません。", "XCP Rewards": "XCP リワード", "XCP Rewards — xcp.fun": "XCP リワード — xcp.fun", "XCP to receive": "受け取る XCP", @@ -1234,6 +1230,10 @@ "Your wallet supplied a connection signature that didn't verify, so we can't confirm this address is really yours. Disconnect and reconnect to try again — nothing else on the site is affected.": "ウォレットが提示した接続署名を検証できなかったため、このアドレスが本当にあなたのものか確認できません。切断して再接続してみてください — サイトの他の機能には影響ありません。", "Your wallet will ask once per route, in order — each popup is one route, nothing more.": "ウォレットはルートごとに順番に 1 回ずつ確認を求めます — ポップアップ 1 つが 1 ルートで、それ以上はありません。", "Your xcp.fun profile": "あなたの xcp.fun プロフィール", - "yours — reused, no fee": "あなたのもの — 再利用、手数料なし", - "zero existing supply": "既存発行量ゼロ" + "yours — reused, no fee": "所有済み — 再利用、登録料なし", + "zero existing supply": "既存発行量ゼロ", + "A mint locks your XCP until the launch resolves. If it graduates, your XCP funds the pool and you receive tokens, which can lose value. If it refunds, your XCP is returned. The Bitcoin network fee is paid in either case; rewards do not guarantee that every cost is covered.": "ミントに使う XCP はローンチの結果が確定するまでロックされます。卒業すると XCP はプールに入り、代わりにトークンを受け取ります。トークンの価値は下がることがあります。返金となった場合は XCP が戻ります。どちらの場合も Bitcoin のネットワーク手数料はかかり、リワードで全費用が補われる保証はありません。", + "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses. The pool is created and its initial LP tokens go to an unspendable address, permanently locking that liquidity. A launch that misses its target returns the contributed XCP by consensus and does not count. {link}": "卒業とは完売のことです。少なくとも {n} の異なるアドレスから {xcp} XCP を調達するとプールが作られ、初期 LP トークンは使用不可アドレスに付与されます。その流動性は永久にロックされます。目標に届かなかったローンチでは、投入した XCP がコンセンサスにより返還され、対象には数えられません。{link}", + "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, fees, and how to integrate.": "XCP-69 のすべてをここに。ローンチと価格の仕組み、卒業の意味、手数料、組み込み方法を解説します。", + "Pool price is XCP reserve ÷ token reserve. Swaps change this ratio. Limit orders can also trade on Counterparty's order book.": "プール価格は XCP 準備金 ÷ トークン準備金です。スワップでこの比率が変わります。Counterparty の板では指値注文でも取引できます。" } diff --git a/apps/web/src/locales/ja.status.json b/apps/web/src/locales/ja.status.json index 1a686b9d..9bc1a182 100644 --- a/apps/web/src/locales/ja.status.json +++ b/apps/web/src/locales/ja.status.json @@ -237,7 +237,6 @@ "Every minted token dumped in one trade — the pool still quotes a bid with {left} XCP left, and sellers exit at {mult}× mint ({paid}% of what they paid). Those {left} XCP can never be withdrawn: every launch is a permanent sink for XCP itself.", "Every route has a purchase pending in the mempool — check back in a few minutes.", "Everything about XCP-69, in one place. The first half explains how launches work for anyone. The second half is for developers who want to read or build on the same on-chain data this site does.", - "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, what it costs (nothing), and how to integrate.", "Everything is curl-able. Launches and mints:", "Everything this site displays comes from public APIs. You can build your own launchpad, bot, or dashboard on the same data — nothing below requires our permission.", "Expiration", @@ -268,7 +267,6 @@ "Get XCP — xcp.fun", "Graduated", "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.", - "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses, at which point the pool is created and its liquidity is burned. A launch that misses its target refunds every satoshi by consensus and does not count. {link}", "Graduation", "Graveyard", "Graveyard pages", @@ -454,7 +452,6 @@ "No trades yet.", "No value chart for {assets}: its recent transfer or liquidity history did not load completely, so we cannot know how many tokens this wallet held at each point. Current holdings and value are live and accurate.", "No value chart: its recent transfer or liquidity history did not load completely, so we cannot know how many tokens this wallet held at each point. Current holdings and value are live and accurate.", - "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.", "Nobody has minted yet. The first row here is available.", @@ -778,7 +775,6 @@ "XCP in pools", "XCP in the pool", "XCP market · cached 15 min", - "XCP reserve ÷ token reserve. Moves with every swap; there is no order book and no market maker.", "XCP to receive", "XCP to unload", "XCP-69 is a fixed-parameter token launch standard built on Counterparty {pools}. There is no factory contract, no admin key, and no platform custody at any point: the protocol {is} the platform. Every action — creating a launch, minting, swapping — is a transaction you sign in your own wallet and broadcast to Bitcoin. This site is an interface over public on-chain data; if it disappeared tomorrow, every launch, refund, and pool would keep working exactly as before.", @@ -1222,6 +1218,10 @@ "Use whole numbers only.", "Your wallet could not complete this request. Check the details.", "Trade history is unavailable. Please try again shortly.", - "Units unavailable" + "Units unavailable", + "A mint locks your XCP until the launch resolves. If it graduates, your XCP funds the pool and you receive tokens, which can lose value. If it refunds, your XCP is returned. The Bitcoin network fee is paid in either case; rewards do not guarantee that every cost is covered.", + "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses. The pool is created and its initial LP tokens go to an unspendable address, permanently locking that liquidity. A launch that misses its target returns the contributed XCP by consensus and does not count. {link}", + "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, fees, and how to integrate.", + "Pool price is XCP reserve ÷ token reserve. Swaps change this ratio. Limit orders can also trade on Counterparty's order book." ] } diff --git a/apps/web/src/locales/zh-hk.json b/apps/web/src/locales/zh-hk.json index 6bd5346f..9458feac 100644 --- a/apps/web/src/locales/zh-hk.json +++ b/apps/web/src/locales/zh-hk.json @@ -1,13 +1,13 @@ { "— minters": "— 鑄造者", - "· {n} pending": "· {n} 筆待處理", + "· {n} pending": "· {n} 筆待確認", "· {pct}% over floor": "· 高於底價 {pct}%", "· {wallets} wallets · {txs} txs": "· {wallets} 個錢包 · {txs} 筆交易", "· above the mempool estimate": "· 高於記憶池估算", "· adjusts to {n}": "· 調整為 {n}", "· adjusts to {n} (sells whole XCP)": "· 調整為 {n}(按整數 XCP 賣出)", "· auto": "· 自動", - "· cheapest of {n}": "· {n} 箇中最便宜", + "· cheapest of {n}": "· {n} 個中最便宜", "· exact cost {btc}": "· 實際成本 {btc}", "“Coordinated first exit” is an optimistic upper bound. “Random order” is the expected result if all 69 full bags sell and these wallets are interleaved uniformly among them. Neither line asserts how many people actually participated.": "“協調搶先退出”是一個樂觀的上限。“隨機順序”是假設 69 個滿額倉位全部賣出、且這些錢包均勻穿插其中時的期望結果。兩條線都不代表實際參與人數。", "(optional)": "(可選)", @@ -46,7 +46,7 @@ "{label} {base} — or run your own counterparty-core node for trustless reads.": "{label} {base} — 或者運行你自己的 counterparty-core 節點,實現無需信任的讀取。", "{label} {feature}, activated on mainnet at block 961,100 (2026-08-05). Requires core v11.2.0+.": "{label} {feature},已於主網區塊 961,100(2026-08-05)啟用。需要 core v11.2.0 及以上。", "{label} Counterparty on Bitcoin mainnet. XCP-69 launches are ordinary Bitcoin transactions carrying Counterparty messages.": "{label} 比特幣主網上的 Counterparty。XCP-69 發射就是攜帶 Counterparty 消息的普通比特幣交易。", - "{lead} {n} XCP left at {sats} sats/XCP. BTC lands with every sale. Closing settles ~5 blocks after it confirms and returns the rest.": "{lead} 剩餘 {n} XCP,價格 {sats} sats/XCP。每賣出一筆,BTC 即到賬。關閉後約 5 個區塊完成結算,餘下的 XCP 退回。", + "{lead} {n} XCP left at {sats} sats/XCP. BTC lands with every sale. Closing settles ~5 blocks after it confirms and returns the rest.": "{lead} 剩餘 {n} XCP,價格為 {sats} sats/XCP。每次售出都會收到 BTC。關閉交易確認後約 5 個區塊完成結算,並退回剩餘 XCP。", "{market} price history with {n} points": "{market} 價格歷史,共 {n} 個數據點", "{mint} A 1,000-block window (~7 days) from {startBlock}. Anyone can mint whole 1,000-token lots at 0.01 XCP per lot, up to 1,000,000 tokens (10 XCP) per address. Both the paid XCP and the minted tokens sit in escrow at the unspendable address — nobody holds anything until the launch resolves. The window length only ever delays failure: a sell-out settles the moment it fills, while a miss frees every minter's XCP within about a week.": "{mint} 從 {startBlock} 起的 1,000 個區塊窗口期(約 7 天)。任何人都可以按每份 0.01 XCP 鑄造整份 1,000 枚代幣,每地址最多 1,000,000 枚(10 XCP)。支付的 XCP 和鑄造的代幣都由協議鎖定在不可花費地址 — 在發射結束之前,沒有人持有任何東西。窗口期長度只會推遲失敗:售罄的那一刻就會結算,而未售罄則會在大約一週內釋放每位鑄造者的 XCP。", "{mints} across {launches} · {xcp} XCP committed": "{launches},共 {mints} · 已投入 {xcp} XCP", @@ -88,7 +88,7 @@ "{n} of {min} minters": "鑄造者 {n}/{min}", "{n} of {total} minters": "{total} 位鑄造者中的 {n} 位", "{n} or later": "{n} 或更晚", - "{n} pending": "{n} 筆待處理", + "{n} pending": "{n} 筆待確認", "{n} pending transaction": "{n} 筆待確認交易", "{n} pending transactions": "{n} 筆待確認交易", "{n} refund · {xcp} XCP returned · about {when}": "{n} 筆退款 · 退回 {xcp} XCP · 約 {when}", @@ -175,7 +175,7 @@ "↩ Minting — {xcp} XCP{usd} raised so far, all of it returned automatically if the launch doesn't sell out.": "↩ 鑄造中 — 目前已募集 {xcp} XCP{usd},如果發射未售罄,將全部自動退回。", "+ {amount} XCP waiting to confirm": "+ {amount} XCP 等待確認", "+{amount} pending": "+{amount} 待確認", - "+{n} hidden — purchase pending in mempool": "+{n} 個已隱藏 — 記憶池中有待處理的購買", + "+{n} hidden — purchase pending in mempool": "+{n} 個已隱藏 — 記憶池中有待確認的購買", "+{n} more below.": "下方還有 {n} 條。", "+{n} more on the explorer ↗": "還有 {n} 個,在區塊瀏覽器查看 ↗", "+{pct}% over floor · waits": "高於底價 +{pct}% · 排隊等待", @@ -266,7 +266,7 @@ "Amount of {asset} to sell": "賣出的 {asset} 數量", "Amount of XCP to deposit": "存入的 XCP 數量", "Amount to remove": "移除數量", - "Amount too small — rounds to 0": "數量太小 — 四捨五入為 0", + "Amount too small — rounds to 0": "數量太小 — 計算結果為 0", "An XCP bounty for the first three launches to graduate, and MINTS for every mint.": "最先畢業的三個發射可獲 XCP 賞金,每筆鑄造都可獲 MINTS。", "an XCP fairminter on xcp.fun": "xcp.fun 上的 XCP 公平鑄造合約", "Animated WEBP shows as a still frame in the announce channel — Telegram cannot play one. A GIF moves there too.": "動態 WEBP 在公告頻道里只會顯示為靜態幀 — Telegram 無法播放。換成 GIF 在那裏也能動。", @@ -387,8 +387,8 @@ "Completed and confirming reward batches, linked to the transactions that sent them.": "已完成和確認中的獎勵批次,附有發放交易的連結。", "Compose failed ({status})": "構造交易失敗({status})", "Composing transactions": "構造交易", - "composing…": "構造交易中…", - "Composing…": "構造交易中…", + "composing…": "正在準備交易…", + "Composing…": "正在準備交易…", "Confirm commit in wallet…": "請在錢包中確認 commit…", "confirm in wallet ({i} of {n})": "在錢包中確認(第 {i} / {n} 筆)", "Confirm in wallet…": "請在錢包中確認…", @@ -497,11 +497,10 @@ "Every LP token is burned — this liquidity can never be withdrawn.": "全部 LP 代幣已銷毀 — 這份流動性永遠無法撤出。", "Every mint on a conforming launch, counted. Ranked by mints, because that is the unit the reward is paid in.": "符合標準的發射上的每一筆鑄造都計入。按鑄造筆數排名,因為獎勵就是按筆發放的。", "Every minted token dumped in one trade — the pool still quotes a bid with {left} XCP left, and sellers exit at {mult}× mint ({paid}% of what they paid). Those {left} XCP can never be withdrawn: every launch is a permanent sink for XCP itself.": "所有鑄造出的代幣在一筆交易裏全部砸出 — 池子仍剩 {left} XCP 在報買價,賣出者以鑄造價的 {mult}× 離場(相當於付出的 {paid}%)。這 {left} XCP 永遠無法取出:每一次發射都是 XCP 本身的永久沉澱池。", - "Every route has a purchase pending in the mempool — check back in a few minutes.": "每條路由都有一筆購買在記憶池中待處理 — 請幾分鐘後再來。", + "Every route has a purchase pending in the mempool — check back in a few minutes.": "每條路由都有一筆購買在記憶池中等待確認 — 請幾分鐘後再來。", "Every XCP-69 launch is identical: 100M supply, 69M public sale at 0.01 XCP per 1,000-token lot, 31M reserved for the liquidity pool, 10 XCP per-address cap, an on-chain pre-announcement before minting opens, and a 1,000-block window. There is no fine print to read because there is no fine print. The full parameter set lives on the {link} page.": "每一次 XCP-69 發射都完全相同:1 億供應量,6900 萬公開發售、每份 1,000 枚代幣 0.01 XCP,3100 萬預留給流動性池,每地址上限 10 XCP,鑄造開啓前的鏈上預公告,以及 1,000 個區塊的窗口期。沒有小字條款可讀,因為根本沒有小字條款。完整參數集見{link}頁面。", "Every XCP-69 mint, trade, burn, resting order and launch, newest first.": "每一筆 XCP-69 鑄造、成交、銷毀、掛單和發射,最新在前。", "everyone dumps": "所有人砸盤", - "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, what it costs (nothing), and how to integrate.": "關於 XCP-69 的一切,盡在一處:發射如何運作、定價如何運作、畢業意味着什麼、需要花多少錢(零)、以及如何集成。", "Everything about XCP-69, in one place. The first half explains how launches work for anyone. The second half is for developers who want to read or build on the same on-chain data this site does.": "關於 XCP-69 的一切,盡在一處。前半部分向所有人解釋發射如何運作。後半部分面向開發者,幫助他們讀取本站所用的同一份鏈上數據,或在其之上構建。", "Everything is curl-able. Launches and mints:": "一切都能用 curl 取到。發射與鑄造:", "Everything this site displays comes from public APIs. You can build your own launchpad, bot, or dashboard on the same data — nothing below requires our permission.": "本站展示的一切都來自公開 API。你可以基於同一份數據構建自己的發射台、機器人或看板 — 下面的內容都不需要我們的許可。", @@ -549,7 +548,6 @@ "Graduated": "畢業", "graduated coins": "畢業代幣", "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.": "已畢業的代幣在一個恆定乘積的 TOKEN/XCP 流動性池中交易。價格就是池中兩種儲備的比率;每一次兌換都會移動它。每次兌換固定收取 {fee} 的費用,支付給池子本身 — 由於 LP 已銷毀,費用會加深鎖定的流動性,而不會付給任何人。", - "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses, at which point the pool is created and its liquidity is burned. A launch that misses its target refunds every satoshi by consensus and does not count. {link}": "畢業就是售罄:從至少 {n} 個不同地址募集 {xcp} XCP,屆時建立流動性池並銷毀其 LP 代幣(流動性永久鎖定)。未達目標的發射會按共識退回每一聰,不計入其中。{link}", "Graduation": "畢業", "Graveyard": "墓地", "Graveyard — xcp.fun": "墓地 — xcp.fun", @@ -716,7 +714,7 @@ "Minting activity": "鑄造動態", "minting is live": "鑄造已開放", "minting is live — refresh the page": "鑄造已開放 — 請刷新頁面", - "Minting is paid in XCP on your address's Counterparty balance — BTC only covers miner fees. The fastest no-account route is an on-chain dispenser: send BTC, the protocol vends XCP automatically. {link}. A max mint is 10 XCP; a launch needs 0.5 XCP for the name.": "鑄造用你地址 Counterparty 餘額中的 XCP 支付 — BTC 只用於礦工費。無需開户的最快途徑是鏈上售賣機:發送 BTC,協議自動發放 XCP。{link}。單地址最多鑄造 10 XCP;發射需要 0.5 XCP 用於註冊名稱。", + "Minting is paid in XCP on your address's Counterparty balance — BTC only covers miner fees. The fastest no-account route is an on-chain dispenser: send BTC, the protocol vends XCP automatically. {link}. A max mint is 10 XCP; a launch needs 0.5 XCP for the name.": "鑄造用你地址 Counterparty 餘額中的 XCP 支付 — BTC 只用於礦工費。無需開户的最快途徑是鏈上售賣機:發送 BTC,協議自動發放 XCP。{link}。單地址最多可投入 10 XCP 參與鑄造;發射需要 0.5 XCP 用於註冊名稱。", "Minting now": "鑄造中", "Minting opens": "鑄造開啓", "minting opens at": "鑄造開放於", @@ -743,7 +741,6 @@ "New buyers": "新買家", "Newest": "最新", "Next →": "下一頁 →", - "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.": "不會 — 鑄造只是把 XCP 託管在該發射上。如果畢業,你持有代幣;如果退款,每一聰都會退回。鑄造真正花掉的只有比特幣礦工費 — 而這正是本計劃所覆蓋的部分。", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.": "不是 — 這正是重點。公平鑄造協議允許極大的變化:沒有軟頂、不退款、建立者佣金高達 99%、預挖、完全沒有流動性池。XCP-69 固定了其中的一種配置,本站只列出與標準完全匹配的發射,並逐欄位對照鏈上記錄檢查。在這裏,每一次發射的行為都完全相同,你永遠不需要自己審計參數。而你在其他任何地方遇到的公平鑄造合約都沒有這樣的保證 — 鑄造前請先讀它的欄位。", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.": "不是 — 它是結構性的,不是承諾。流動性池必須以鑄造價的 69/31 開盤,因為這是僅有的數量,但隨着人們向池中賣出,底價會不斷下降。沒有什麼能阻止代幣跌破鑄造價。池子保證的是一個永遠不會歸零的買價,而不是一個你會滿意的買價。", "No active {give}/{get} pool liquidity. A resting order can still fill through the order book.": "{give}/{get} 流動性池當前沒有流動性。掛單仍可通過訂單簿成交。", @@ -816,7 +813,7 @@ "Of the launches that finished": "已結束的發射中", "of the typical mint fee": "相對典型鑄造礦工費", "of which {amount} realized": "其中 {amount} 已實現", - "Of XCP minted": "鑄造 XCP 佔比", + "Of XCP minted": "投入鑄造的 XCP 佔比", "Off-chain by default: the chain permanently carries the URL of the asset-info JSON, and the content behind it is curatable by the asset's current owner (wallet-signature gated). Taproot creators can instead inscribe the image on-chain as the permanent description — no off-chain dependency at all. Either way, nothing economic depends on hosted content.": "預設在鏈下:鏈上永久保存資產資訊 JSON 的 URL,其背後的內容可由資產當前所有者維護(需錢包簽名)。Taproot 建立者也可以改為把圖片銘刻上鏈作為永久描述 — 完全沒有鏈下依賴。無論哪種方式,經濟機制都不依賴託管的內容。", "On other launchpads, graduation is a threshold inside a live market: the token trades on a curve while everyone hopes it reaches the magic number. XCP-69 graduation is {binary} and happens before any trading exists. Sell out the 69M sale within 1,000 blocks and the launch graduates — pool seeded, LP burned, supply locked, trading live in the same block's resolution phase. Miss it and the launch never trades at all.": "在其他發射台上,畢業是活躍市場中的一條閾值線:代幣在曲線上交易,所有人都盼着它觸到那個神奇的數字。XCP-69 的畢業是{binary}的,而且發生在任何交易存在之前。在 1,000 個區塊內售罄 6900 萬,發射就畢業 — 流動性池注入、LP 銷毀、供應量鎖定、交易在同一區塊的結算階段開始。錯過了,這次發射就永遠不會交易。", "on their Counterparty balance": "在其 Counterparty 餘額中", @@ -864,7 +861,7 @@ "partially filled · resting": "部分成交 · 掛單中", "Partially sent": "部分已發放", "Partially. The 10 XCP per-address cap makes a fake crowd cost at least 69 funded addresses — it raises the price of the act, it cannot prevent it. Sybil-resistant in cost, not in principle. The mint tape, address histories, and holder spread are all public, so a manufactured crowd has to fake those too.": "部分可以。每地址 10 XCP 的上限讓偽造人羣至少需要 69 個有資金的地址 — 這抬高了作假的代價,但無法杜絕。在成本上抗女巫攻擊,在原理上並不能。鑄造記錄、地址歷史和持有者分佈都是公開的,所以偽造的人羣還得把這些也偽造出來。", - "Paste the profile URL or enter the handle.": "貼上主頁連結或輸入用户名。", + "Paste the profile URL or enter the handle.": "貼上主頁連結或輸入用戶名稱。", "Payouts": "發放", "pending": "待確認", "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.": "待確認訂單可能使可收到的數量低於你的最低接收數量。若它們先確認,未成交部分將掛單至到期後退回。請檢查滑點設定,或選擇自動。", @@ -965,7 +962,7 @@ "route no longer has enough left": "路由剩餘數量不足", "route price changed": "路由價格已變動", "Routes": "路由", - "Routing includes miner fees — a deep route can beat a cheaper, shallower one.": "路由已計入礦工費 — 深度更大的路由可能勝過更便宜但更淺的路由。", + "Routing includes miner fees — a deep route can beat a cheaper, shallower one.": "選擇路由時已計入礦工費。流動性更充足的路由,總成本可能低於單價更低但流動性不足的路由。", "Sale closing": "銷售關閉中", "Save failed": "保存失敗", "Saved. Changes are live; other open pages may take a minute to refresh.": "已保存。更改已生效;其他已打開的頁面可能需要一分鐘才會刷新。", @@ -1146,7 +1143,7 @@ "Use whole numbers only.": "請只輸入整數。", "Value": "價值", "Value and underlying amounts are what withdrawing the LP tokens now would return. XCP value is available when XCP is one side of the pair. LP profit and loss is not estimated.": "價值和底層數量指現在提取 LP 代幣能拿回的資產。當交易對一側是 XCP 時才有 XCP 價值。不估算 LP 的盈虧。", - "Vends": "出售中", + "Vends": "可售次數", "Venue": "成交渠道", "Venue / time": "成交渠道 / 時間", "Verdict pending": "待判定", @@ -1191,7 +1188,6 @@ "XCP in the pool": "池中 XCP", "XCP market · cached 15 min": "XCP 市場 · 快取 15 分鐘", "XCP Price": "XCP 價格", - "XCP reserve ÷ token reserve. Moves with every swap; there is no order book and no market maker.": "XCP 儲備 ÷ 代幣儲備。每次兌換都會變動;沒有訂單簿,也沒有做市商。", "XCP Rewards": "XCP 獎勵", "XCP Rewards — xcp.fun": "XCP 獎勵 — xcp.fun", "XCP to receive": "收到 XCP", @@ -1212,7 +1208,7 @@ "you": "你", "you · {n} XCP": "你 · {n} XCP", "you have minted this launch’s max": "你已鑄造到這次發射的上限", - "You mint": "你將獲得", + "You mint": "鑄造數量", "You own {name}, but {reason}.": "你擁有 {name},但{reason}。", "You pay": "你支付", "You receive": "你收到", @@ -1234,6 +1230,10 @@ "Your wallet supplied a connection signature that didn't verify, so we can't confirm this address is really yours. Disconnect and reconnect to try again — nothing else on the site is affected.": "你的錢包提供的連接簽名未能通過驗證,因此我們無法確認這個地址真的屬於你。請斷開後重新連接再試 — 網站的其他功能不受影響。", "Your wallet will ask once per route, in order — each popup is one route, nothing more.": "錢包會按順序為每條路由請求一次確認 — 每個彈窗對應一條路由,僅此而已。", "Your xcp.fun profile": "你的 xcp.fun 個人資料", - "yours — reused, no fee": "你的 — 複用,免費", - "zero existing supply": "現有供應量為零" + "yours — reused, no fee": "你的 — 重用,免註冊費", + "zero existing supply": "現有供應量為零", + "A mint locks your XCP until the launch resolves. If it graduates, your XCP funds the pool and you receive tokens, which can lose value. If it refunds, your XCP is returned. The Bitcoin network fee is paid in either case; rewards do not guarantee that every cost is covered.": "鑄造會鎖定你的 XCP,直到發射結果確定。若成功畢業,你的 XCP 會注入流動性池,你則收到代幣;代幣的價值可能下跌。若發射退款,你的 XCP 會退回。無論哪種結果,都需要支付比特幣礦工費;獎勵不保證涵蓋所有成本。", + "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses. The pool is created and its initial LP tokens go to an unspendable address, permanently locking that liquidity. A launch that misses its target returns the contributed XCP by consensus and does not count. {link}": "畢業意味着售罄:從至少 {n} 個不同地址募集 {xcp} XCP。隨後建立流動性池,初始 LP 代幣記入不可花費地址,從而永久鎖定這部分流動性。未達目標的發射會按共識退回投入的 XCP,不計入其中。{link}", + "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, fees, and how to integrate.": "關於 XCP-69 的一切,盡在一處:發射與定價如何運作、畢業意味着什麼、費用以及如何集成。", + "Pool price is XCP reserve ÷ token reserve. Swaps change this ratio. Limit orders can also trade on Counterparty's order book.": "池價等於 XCP 儲備 ÷ 代幣儲備。兌換會改變這一比率。限價單也可以在 Counterparty 的訂單簿上成交。" } diff --git a/apps/web/src/locales/zh-hk.status.json b/apps/web/src/locales/zh-hk.status.json index e04c1b17..395a285c 100644 --- a/apps/web/src/locales/zh-hk.status.json +++ b/apps/web/src/locales/zh-hk.status.json @@ -491,7 +491,6 @@ "Every XCP-69 launch is identical: 100M supply, 69M public sale at 0.01 XCP per 1,000-token lot, 31M reserved for the liquidity pool, 10 XCP per-address cap, an on-chain pre-announcement before minting opens, and a 1,000-block window. There is no fine print to read because there is no fine print. The full parameter set lives on the {link} page.", "Every XCP-69 mint, trade, burn, resting order and launch, newest first.", "everyone dumps", - "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, what it costs (nothing), and how to integrate.", "Everything about XCP-69, in one place. The first half explains how launches work for anyone. The second half is for developers who want to read or build on the same on-chain data this site does.", "Everything is curl-able. Launches and mints:", "Everything this site displays comes from public APIs. You can build your own launchpad, bot, or dashboard on the same data — nothing below requires our permission.", @@ -537,7 +536,6 @@ "Graduated", "graduated coins", "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.", - "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses, at which point the pool is created and its liquidity is burned. A launch that misses its target refunds every satoshi by consensus and does not count. {link}", "Graduation", "Graveyard", "Graveyard — xcp.fun", @@ -723,7 +721,6 @@ "Newest", "Next →", "when the BTC payment confirms", - "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.", "No active {give}/{get} pool liquidity. A resting order can still fill through the order book.", @@ -1153,7 +1150,6 @@ "XCP in the pool", "XCP market · cached 15 min", "XCP Price", - "XCP reserve ÷ token reserve. Moves with every swap; there is no order book and no market maker.", "XCP Rewards", "XCP Rewards — xcp.fun", "XCP to receive", @@ -1224,6 +1220,10 @@ "Use whole numbers only.", "Your wallet could not complete this request. Check the details.", "Trade history is unavailable. Please try again shortly.", - "Units unavailable" + "Units unavailable", + "A mint locks your XCP until the launch resolves. If it graduates, your XCP funds the pool and you receive tokens, which can lose value. If it refunds, your XCP is returned. The Bitcoin network fee is paid in either case; rewards do not guarantee that every cost is covered.", + "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses. The pool is created and its initial LP tokens go to an unspendable address, permanently locking that liquidity. A launch that misses its target returns the contributed XCP by consensus and does not count. {link}", + "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, fees, and how to integrate.", + "Pool price is XCP reserve ÷ token reserve. Swaps change this ratio. Limit orders can also trade on Counterparty's order book." ] } diff --git a/apps/web/src/locales/zh-tw.json b/apps/web/src/locales/zh-tw.json index 6d327b04..54bff765 100644 --- a/apps/web/src/locales/zh-tw.json +++ b/apps/web/src/locales/zh-tw.json @@ -1,13 +1,13 @@ { "— minters": "— 鑄造者", - "· {n} pending": "· {n} 筆待處理", + "· {n} pending": "· {n} 筆待確認", "· {pct}% over floor": "· 高於底價 {pct}%", "· {wallets} wallets · {txs} txs": "· {wallets} 個錢包 · {txs} 筆交易", "· above the mempool estimate": "· 高於記憶體池估算", "· adjusts to {n}": "· 調整為 {n}", "· adjusts to {n} (sells whole XCP)": "· 調整為 {n}(按整數 XCP 賣出)", "· auto": "· 自動", - "· cheapest of {n}": "· {n} 箇中最便宜", + "· cheapest of {n}": "· {n} 個中最便宜", "· exact cost {btc}": "· 實際成本 {btc}", "“Coordinated first exit” is an optimistic upper bound. “Random order” is the expected result if all 69 full bags sell and these wallets are interleaved uniformly among them. Neither line asserts how many people actually participated.": "“協調搶先退出”是一個樂觀的上限。“隨機順序”是假設 69 個滿額倉位全部賣出、且這些錢包均勻穿插其中時的期望結果。兩條線都不代表實際參與人數。", "(optional)": "(可選)", @@ -46,7 +46,7 @@ "{label} {base} — or run your own counterparty-core node for trustless reads.": "{label} {base} — 或者執行你自己的 counterparty-core 節點,實現無需信任的讀取。", "{label} {feature}, activated on mainnet at block 961,100 (2026-08-05). Requires core v11.2.0+.": "{label} {feature},已於主網區塊 961,100(2026-08-05)啟用。需要 core v11.2.0 及以上。", "{label} Counterparty on Bitcoin mainnet. XCP-69 launches are ordinary Bitcoin transactions carrying Counterparty messages.": "{label} 比特幣主網上的 Counterparty。XCP-69 發射就是攜帶 Counterparty 訊息的普通比特幣交易。", - "{lead} {n} XCP left at {sats} sats/XCP. BTC lands with every sale. Closing settles ~5 blocks after it confirms and returns the rest.": "{lead} 剩餘 {n} XCP,價格 {sats} sats/XCP。每賣出一筆,BTC 即到帳。關閉後約 5 個區塊完成結算,餘下的 XCP 退回。", + "{lead} {n} XCP left at {sats} sats/XCP. BTC lands with every sale. Closing settles ~5 blocks after it confirms and returns the rest.": "{lead} 剩餘 {n} XCP,價格為 {sats} sats/XCP。每次售出都會收到 BTC。關閉交易確認後約 5 個區塊完成結算,並退回剩餘 XCP。", "{market} price history with {n} points": "{market} 價格歷史,共 {n} 個數據點", "{mint} A 1,000-block window (~7 days) from {startBlock}. Anyone can mint whole 1,000-token lots at 0.01 XCP per lot, up to 1,000,000 tokens (10 XCP) per address. Both the paid XCP and the minted tokens sit in escrow at the unspendable address — nobody holds anything until the launch resolves. The window length only ever delays failure: a sell-out settles the moment it fills, while a miss frees every minter's XCP within about a week.": "{mint} 從 {startBlock} 起的 1,000 個區塊窗口期(約 7 天)。任何人都可以按每份 0.01 XCP 鑄造整份 1,000 枚代幣,每地址最多 1,000,000 枚(10 XCP)。支付的 XCP 和鑄造的代幣都由協議鎖定在不可花費地址 — 在發射結束之前,沒有人持有任何東西。窗口期長度只會推遲失敗:售罄的那一刻就會結算,而未售罄則會在大約一週內釋放每位鑄造者的 XCP。", "{mints} across {launches} · {xcp} XCP committed": "{launches},共 {mints} · 已投入 {xcp} XCP", @@ -88,7 +88,7 @@ "{n} of {min} minters": "鑄造者 {n}/{min}", "{n} of {total} minters": "{total} 位鑄造者中的 {n} 位", "{n} or later": "{n} 或更晚", - "{n} pending": "{n} 筆待處理", + "{n} pending": "{n} 筆待確認", "{n} pending transaction": "{n} 筆待確認交易", "{n} pending transactions": "{n} 筆待確認交易", "{n} refund · {xcp} XCP returned · about {when}": "{n} 筆退款 · 退回 {xcp} XCP · 約 {when}", @@ -175,7 +175,7 @@ "↩ Minting — {xcp} XCP{usd} raised so far, all of it returned automatically if the launch doesn't sell out.": "↩ 鑄造中 — 目前已募集 {xcp} XCP{usd},如果發射未售罄,將全部自動退回。", "+ {amount} XCP waiting to confirm": "+ {amount} XCP 等待確認", "+{amount} pending": "+{amount} 待確認", - "+{n} hidden — purchase pending in mempool": "+{n} 個已隱藏 — 記憶體池中有待處理的購買", + "+{n} hidden — purchase pending in mempool": "+{n} 個已隱藏 — 記憶體池中有待確認的購買", "+{n} more below.": "下方還有 {n} 條。", "+{n} more on the explorer ↗": "還有 {n} 個,在區塊瀏覽器檢視 ↗", "+{pct}% over floor · waits": "高於底價 +{pct}% · 排隊等待", @@ -266,7 +266,7 @@ "Amount of {asset} to sell": "賣出的 {asset} 數量", "Amount of XCP to deposit": "存入的 XCP 數量", "Amount to remove": "移除數量", - "Amount too small — rounds to 0": "數量太小 — 四捨五入為 0", + "Amount too small — rounds to 0": "數量太小 — 計算結果為 0", "An XCP bounty for the first three launches to graduate, and MINTS for every mint.": "最先畢業的三個發射可獲 XCP 賞金,每筆鑄造都可獲 MINTS。", "an XCP fairminter on xcp.fun": "xcp.fun 上的 XCP 公平鑄造合約", "Animated WEBP shows as a still frame in the announce channel — Telegram cannot play one. A GIF moves there too.": "動態 WEBP 在公告頻道里只會顯示為靜態幀 — Telegram 無法播放。換成 GIF 在那裡也能動。", @@ -387,8 +387,8 @@ "Completed and confirming reward batches, linked to the transactions that sent them.": "已完成和確認中的獎勵批次,附有發放交易的連結。", "Compose failed ({status})": "構造交易失敗({status})", "Composing transactions": "構造交易", - "composing…": "構造交易中…", - "Composing…": "構造交易中…", + "composing…": "正在準備交易…", + "Composing…": "正在準備交易…", "Confirm commit in wallet…": "請在錢包中確認 commit…", "confirm in wallet ({i} of {n})": "在錢包中確認(第 {i} / {n} 筆)", "Confirm in wallet…": "請在錢包中確認…", @@ -497,11 +497,10 @@ "Every LP token is burned — this liquidity can never be withdrawn.": "全部 LP 代幣已銷毀 — 這份流動性永遠無法撤出。", "Every mint on a conforming launch, counted. Ranked by mints, because that is the unit the reward is paid in.": "符合標準的發射上的每一筆鑄造都計入。按鑄造筆數排名,因為獎勵就是按筆發放的。", "Every minted token dumped in one trade — the pool still quotes a bid with {left} XCP left, and sellers exit at {mult}× mint ({paid}% of what they paid). Those {left} XCP can never be withdrawn: every launch is a permanent sink for XCP itself.": "所有鑄造出的代幣在一筆交易裡全部砸出 — 池子仍剩 {left} XCP 在報買價,賣出者以鑄造價的 {mult}× 離場(相當於付出的 {paid}%)。這 {left} XCP 永遠無法取出:每一次發射都是 XCP 本身的永久沉澱池。", - "Every route has a purchase pending in the mempool — check back in a few minutes.": "每條路由都有一筆購買在記憶體池中待處理 — 請幾分鐘後再來。", + "Every route has a purchase pending in the mempool — check back in a few minutes.": "每條路由都有一筆購買在記憶體池中等待確認 — 請幾分鐘後再來。", "Every XCP-69 launch is identical: 100M supply, 69M public sale at 0.01 XCP per 1,000-token lot, 31M reserved for the liquidity pool, 10 XCP per-address cap, an on-chain pre-announcement before minting opens, and a 1,000-block window. There is no fine print to read because there is no fine print. The full parameter set lives on the {link} page.": "每一次 XCP-69 發射都完全相同:1 億供應量,6900 萬公開發售、每份 1,000 枚代幣 0.01 XCP,3100 萬預留給流動性池,每地址上限 10 XCP,鑄造開啟前的鏈上預公告,以及 1,000 個區塊的窗口期。沒有小字條款可讀,因為根本沒有小字條款。完整參數集見{link}頁面。", "Every XCP-69 mint, trade, burn, resting order and launch, newest first.": "每一筆 XCP-69 鑄造、成交、銷毀、掛單和發射,最新在前。", "everyone dumps": "所有人砸盤", - "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, what it costs (nothing), and how to integrate.": "關於 XCP-69 的一切,盡在一處:發射如何運作、定價如何運作、畢業意味著什麼、需要花多少錢(零)、以及如何整合。", "Everything about XCP-69, in one place. The first half explains how launches work for anyone. The second half is for developers who want to read or build on the same on-chain data this site does.": "關於 XCP-69 的一切,盡在一處。前半部分向所有人解釋發射如何運作。後半部分面向開發者,幫助他們讀取本站所用的同一份鏈上資料,或在其之上構建。", "Everything is curl-able. Launches and mints:": "一切都能用 curl 取到。發射與鑄造:", "Everything this site displays comes from public APIs. You can build your own launchpad, bot, or dashboard on the same data — nothing below requires our permission.": "本站展示的一切都來自公開 API。你可以基於同一份資料構建自己的發射台、機器人或儀表板 — 下面的內容都不需要我們的許可。", @@ -549,7 +548,6 @@ "Graduated": "畢業", "graduated coins": "畢業代幣", "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.": "已畢業的代幣在一個恆定乘積的 TOKEN/XCP 流動性池中交易。價格就是池中兩種儲備的比率;每一次兌換都會移動它。每次兌換固定收取 {fee} 的費用,支付給池子本身 — 由於 LP 已銷毀,費用會加深鎖定的流動性,而不會付給任何人。", - "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses, at which point the pool is created and its liquidity is burned. A launch that misses its target refunds every satoshi by consensus and does not count. {link}": "畢業就是售罄:從至少 {n} 個不同地址募集 {xcp} XCP,屆時建立流動性池並銷毀其 LP 代幣(流動性永久鎖定)。未達目標的發射會按共識退回每一聰,不計入其中。{link}", "Graduation": "畢業", "Graveyard": "墓地", "Graveyard — xcp.fun": "墓地 — xcp.fun", @@ -716,7 +714,7 @@ "Minting activity": "鑄造動態", "minting is live": "鑄造已開放", "minting is live — refresh the page": "鑄造已開放 — 請重新整理頁面", - "Minting is paid in XCP on your address's Counterparty balance — BTC only covers miner fees. The fastest no-account route is an on-chain dispenser: send BTC, the protocol vends XCP automatically. {link}. A max mint is 10 XCP; a launch needs 0.5 XCP for the name.": "鑄造用你地址 Counterparty 餘額中的 XCP 支付 — BTC 只用於礦工費。無需開戶的最快途徑是鏈上販賣機:發送 BTC,協議自動發放 XCP。{link}。單地址最多鑄造 10 XCP;發射需要 0.5 XCP 用於註冊名稱。", + "Minting is paid in XCP on your address's Counterparty balance — BTC only covers miner fees. The fastest no-account route is an on-chain dispenser: send BTC, the protocol vends XCP automatically. {link}. A max mint is 10 XCP; a launch needs 0.5 XCP for the name.": "鑄造用你地址 Counterparty 餘額中的 XCP 支付 — BTC 只用於礦工費。無需開戶的最快途徑是鏈上販賣機:發送 BTC,協議自動發放 XCP。{link}。單地址最多可投入 10 XCP 參與鑄造;發射需要 0.5 XCP 用於註冊名稱。", "Minting now": "鑄造中", "Minting opens": "鑄造開啟", "minting opens at": "鑄造開放於", @@ -743,7 +741,6 @@ "New buyers": "新買家", "Newest": "最新", "Next →": "下一頁 →", - "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.": "不會 — 鑄造只是把 XCP 託管在該發射上。如果畢業,你持有代幣;如果退款,每一聰都會退回。鑄造真正花掉的只有比特幣礦工費 — 而這正是本計畫所覆蓋的部分。", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.": "不是 — 這正是重點。公平鑄造協議允許極大的變化:沒有軟頂、不退款、建立者佣金高達 99%、預挖、完全沒有流動性池。XCP-69 固定了其中的一種配置,本站只列出與標準完全匹配的發射,並逐欄位對照鏈上記錄檢查。在這裡,每一次發射的行為都完全相同,你永遠不需要自己審計參數。而你在其他任何地方遇到的公平鑄造合約都沒有這樣的保證 — 鑄造前請先讀它的欄位。", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.": "不是 — 它是結構性的,不是承諾。流動性池必須以鑄造價的 69/31 開盤,因為這是僅有的數量,但隨著人們向池中賣出,底價會不斷下降。沒有什麼能阻止代幣跌破鑄造價。池子保證的是一個永遠不會歸零的買價,而不是一個你會滿意的買價。", "No active {give}/{get} pool liquidity. A resting order can still fill through the order book.": "{give}/{get} 流動性池當前沒有流動性。掛單仍可透過訂單簿成交。", @@ -816,7 +813,7 @@ "Of the launches that finished": "已結束的發射中", "of the typical mint fee": "相對典型鑄造礦工費", "of which {amount} realized": "其中 {amount} 已實現", - "Of XCP minted": "鑄造 XCP 佔比", + "Of XCP minted": "投入鑄造的 XCP 佔比", "Off-chain by default: the chain permanently carries the URL of the asset-info JSON, and the content behind it is curatable by the asset's current owner (wallet-signature gated). Taproot creators can instead inscribe the image on-chain as the permanent description — no off-chain dependency at all. Either way, nothing economic depends on hosted content.": "預設在鏈下:鏈上永久儲存資產資訊 JSON 的 URL,其背後的內容可由資產當前所有者維護(需錢包簽名)。Taproot 建立者也可以改為把圖片銘刻上鏈作為永久描述 — 完全沒有鏈下依賴。無論哪種方式,經濟機制都不依賴託管的內容。", "On other launchpads, graduation is a threshold inside a live market: the token trades on a curve while everyone hopes it reaches the magic number. XCP-69 graduation is {binary} and happens before any trading exists. Sell out the 69M sale within 1,000 blocks and the launch graduates — pool seeded, LP burned, supply locked, trading live in the same block's resolution phase. Miss it and the launch never trades at all.": "在其他發射台上,畢業是活躍市場中的一條閾值線:代幣在曲線上交易,所有人都盼著它觸到那個神奇的數字。XCP-69 的畢業是{binary}的,而且發生在任何交易存在之前。在 1,000 個區塊內售罄 6900 萬,發射就畢業 — 流動性池注入、LP 銷毀、供應量鎖定、交易在同一區塊的結算階段開始。錯過了,這次發射就永遠不會交易。", "on their Counterparty balance": "在其 Counterparty 餘額中", @@ -867,7 +864,7 @@ "Paste the profile URL or enter the handle.": "貼上主頁連結或輸入使用者名稱。", "Payouts": "發放", "pending": "待確認", - "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.": "待確認訂單可能使可收到的數量低於你的最低接收數量。若它們先確認,未成交部分將掛單至到期後退回。請檢查滑價設定,或選擇自動。", + "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.": "待確認訂單可能使可收到的數量低於你的最低接收數量。若它們先確認,未成交部分將掛單至到期後退回。請檢查滑點設定,或選擇自動。", "People with xcp.fun open right now, including you. Several tabs from the same browser count once. Closing the tab removes you.": "此刻開啟 xcp.fun 的人數,包括你。同一瀏覽器的多個分頁只計一次。關閉分頁即不再計入。", "Per address": "每地址上限", "Per mint": "每筆鑄造", @@ -965,7 +962,7 @@ "route no longer has enough left": "路由剩餘數量不足", "route price changed": "路由價格已變動", "Routes": "路由", - "Routing includes miner fees — a deep route can beat a cheaper, shallower one.": "路由已計入礦工費 — 深度更大的路由可能勝過更便宜但更淺的路由。", + "Routing includes miner fees — a deep route can beat a cheaper, shallower one.": "選擇路由時已計入礦工費。流動性更充足的路由,總成本可能低於單價更低但流動性不足的路由。", "Sale closing": "銷售關閉中", "Save failed": "儲存失敗", "Saved. Changes are live; other open pages may take a minute to refresh.": "已儲存。更改已生效;其他已開啟的頁面可能需要一分鐘才會重新整理。", @@ -1015,7 +1012,7 @@ "Size": "數量", "Skipped: {reason}": "已跳過:{reason}", "Slippage": "滑點", - "Slippage tolerance of {pct}% permits receiving less than quoted.": "{pct}% 的滑價容忍度允許實際接收數量低於報價。", + "Slippage tolerance of {pct}% permits receiving less than quoted.": "{pct}% 的滑點容忍度允許實際接收數量低於報價。", "Slippage: {pct}%": "滑點:{pct}%", "soft cap not reached": "soft cap not reached", "Sold": "賣出", @@ -1146,7 +1143,7 @@ "Use whole numbers only.": "請只輸入整數。", "Value": "價值", "Value and underlying amounts are what withdrawing the LP tokens now would return. XCP value is available when XCP is one side of the pair. LP profit and loss is not estimated.": "價值和底層數量指現在提取 LP 代幣能拿回的資產。當交易對一側是 XCP 時才有 XCP 價值。不估算 LP 的盈虧。", - "Vends": "出售中", + "Vends": "可售次數", "Venue": "成交渠道", "Venue / time": "成交渠道 / 時間", "Verdict pending": "待判定", @@ -1191,7 +1188,6 @@ "XCP in the pool": "池中 XCP", "XCP market · cached 15 min": "XCP 市場 · 快取 15 分鐘", "XCP Price": "XCP 價格", - "XCP reserve ÷ token reserve. Moves with every swap; there is no order book and no market maker.": "XCP 儲備 ÷ 代幣儲備。每次兌換都會變動;沒有訂單簿,也沒有做市商。", "XCP Rewards": "XCP 獎勵", "XCP Rewards — xcp.fun": "XCP 獎勵 — xcp.fun", "XCP to receive": "收到 XCP", @@ -1212,7 +1208,7 @@ "you": "你", "you · {n} XCP": "你 · {n} XCP", "you have minted this launch’s max": "你已鑄造到這次發射的上限", - "You mint": "你將獲得", + "You mint": "鑄造數量", "You own {name}, but {reason}.": "你擁有 {name},但{reason}。", "You pay": "你支付", "You receive": "你收到", @@ -1234,6 +1230,10 @@ "Your wallet supplied a connection signature that didn't verify, so we can't confirm this address is really yours. Disconnect and reconnect to try again — nothing else on the site is affected.": "你的錢包提供的連接簽名未能通過驗證,因此我們無法確認這個地址真的屬於你。請斷開後重新連接再試 — 網站的其他功能不受影響。", "Your wallet will ask once per route, in order — each popup is one route, nothing more.": "錢包會按順序為每條路由請求一次確認 — 每個彈出視窗對應一條路由,僅此而已。", "Your xcp.fun profile": "你的 xcp.fun 個人資料", - "yours — reused, no fee": "你的 — 複用,免費", - "zero existing supply": "現有供應量為零" + "yours — reused, no fee": "你的 — 重用,免註冊費", + "zero existing supply": "現有供應量為零", + "A mint locks your XCP until the launch resolves. If it graduates, your XCP funds the pool and you receive tokens, which can lose value. If it refunds, your XCP is returned. The Bitcoin network fee is paid in either case; rewards do not guarantee that every cost is covered.": "鑄造會鎖定你的 XCP,直到發射結果確定。若成功畢業,你的 XCP 會注入流動性池,你則收到代幣;代幣的價值可能下跌。若發射退款,你的 XCP 會退回。無論哪種結果,都需要支付比特幣礦工費;獎勵不保證涵蓋所有成本。", + "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses. The pool is created and its initial LP tokens go to an unspendable address, permanently locking that liquidity. A launch that misses its target returns the contributed XCP by consensus and does not count. {link}": "畢業意味著售罄:從至少 {n} 個不同地址募集 {xcp} XCP。隨後建立流動性池,初始 LP 代幣記入不可花費地址,從而永久鎖定這部分流動性。未達目標的發射會按共識退回投入的 XCP,不計入其中。{link}", + "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, fees, and how to integrate.": "關於 XCP-69 的一切,盡在一處:發射與定價如何運作、畢業意味著什麼、費用以及如何整合。", + "Pool price is XCP reserve ÷ token reserve. Swaps change this ratio. Limit orders can also trade on Counterparty's order book.": "池價等於 XCP 儲備 ÷ 代幣儲備。兌換會改變這一比率。限價單也可以在 Counterparty 的訂單簿上成交。" } diff --git a/apps/web/src/locales/zh-tw.status.json b/apps/web/src/locales/zh-tw.status.json index e04c1b17..395a285c 100644 --- a/apps/web/src/locales/zh-tw.status.json +++ b/apps/web/src/locales/zh-tw.status.json @@ -491,7 +491,6 @@ "Every XCP-69 launch is identical: 100M supply, 69M public sale at 0.01 XCP per 1,000-token lot, 31M reserved for the liquidity pool, 10 XCP per-address cap, an on-chain pre-announcement before minting opens, and a 1,000-block window. There is no fine print to read because there is no fine print. The full parameter set lives on the {link} page.", "Every XCP-69 mint, trade, burn, resting order and launch, newest first.", "everyone dumps", - "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, what it costs (nothing), and how to integrate.", "Everything about XCP-69, in one place. The first half explains how launches work for anyone. The second half is for developers who want to read or build on the same on-chain data this site does.", "Everything is curl-able. Launches and mints:", "Everything this site displays comes from public APIs. You can build your own launchpad, bot, or dashboard on the same data — nothing below requires our permission.", @@ -537,7 +536,6 @@ "Graduated", "graduated coins", "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.", - "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses, at which point the pool is created and its liquidity is burned. A launch that misses its target refunds every satoshi by consensus and does not count. {link}", "Graduation", "Graveyard", "Graveyard — xcp.fun", @@ -723,7 +721,6 @@ "Newest", "Next →", "when the BTC payment confirms", - "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.", "No active {give}/{get} pool liquidity. A resting order can still fill through the order book.", @@ -1153,7 +1150,6 @@ "XCP in the pool", "XCP market · cached 15 min", "XCP Price", - "XCP reserve ÷ token reserve. Moves with every swap; there is no order book and no market maker.", "XCP Rewards", "XCP Rewards — xcp.fun", "XCP to receive", @@ -1224,6 +1220,10 @@ "Use whole numbers only.", "Your wallet could not complete this request. Check the details.", "Trade history is unavailable. Please try again shortly.", - "Units unavailable" + "Units unavailable", + "A mint locks your XCP until the launch resolves. If it graduates, your XCP funds the pool and you receive tokens, which can lose value. If it refunds, your XCP is returned. The Bitcoin network fee is paid in either case; rewards do not guarantee that every cost is covered.", + "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses. The pool is created and its initial LP tokens go to an unspendable address, permanently locking that liquidity. A launch that misses its target returns the contributed XCP by consensus and does not count. {link}", + "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, fees, and how to integrate.", + "Pool price is XCP reserve ÷ token reserve. Swaps change this ratio. Limit orders can also trade on Counterparty's order book." ] } diff --git a/apps/web/src/locales/zh.json b/apps/web/src/locales/zh.json index e01c5f84..3e948fbd 100644 --- a/apps/web/src/locales/zh.json +++ b/apps/web/src/locales/zh.json @@ -88,7 +88,7 @@ "{n} of {min} minters": "铸造者 {n}/{min}", "{n} of {total} minters": "{total} 位铸造者中的 {n} 位", "{n} or later": "{n} 或更晚", - "{n} pending": "{n} 笔待处理", + "{n} pending": "{n} 笔待确认", "{n} pending transaction": "{n} 笔待确认交易", "{n} pending transactions": "{n} 笔待确认交易", "{n} refund · {xcp} XCP returned · about {when}": "{n} 笔退款 · 退回 {xcp} XCP · 约 {when}", @@ -175,7 +175,7 @@ "↩ Minting — {xcp} XCP{usd} raised so far, all of it returned automatically if the launch doesn't sell out.": "↩ 铸造中 — 目前已募集 {xcp} XCP{usd},如果发射未售罄,将全部自动退回。", "+ {amount} XCP waiting to confirm": "+ {amount} XCP 等待确认", "+{amount} pending": "+{amount} 待确认", - "+{n} hidden — purchase pending in mempool": "+{n} 个已隐藏 — 内存池中有待处理的购买", + "+{n} hidden — purchase pending in mempool": "+{n} 个已隐藏 — 内存池中有待确认的购买", "+{n} more below.": "下方还有 {n} 条。", "+{n} more on the explorer ↗": "还有 {n} 个,在区块浏览器查看 ↗", "+{pct}% over floor · waits": "高于底价 +{pct}% · 排队等待", @@ -497,11 +497,10 @@ "Every LP token is burned — this liquidity can never be withdrawn.": "全部 LP 代币已销毁 — 这份流动性永远无法撤出。", "Every mint on a conforming launch, counted. Ranked by mints, because that is the unit the reward is paid in.": "符合标准的发射上的每一笔铸造都计入。按铸造笔数排名,因为奖励就是按笔发放的。", "Every minted token dumped in one trade — the pool still quotes a bid with {left} XCP left, and sellers exit at {mult}× mint ({paid}% of what they paid). Those {left} XCP can never be withdrawn: every launch is a permanent sink for XCP itself.": "所有铸造出的代币在一笔交易里全部砸出 — 池子仍剩 {left} XCP 在报买价,卖出者以铸造价的 {mult}× 离场(相当于付出的 {paid}%)。这 {left} XCP 永远无法取出:每一次发射都是 XCP 本身的永久沉淀池。", - "Every route has a purchase pending in the mempool — check back in a few minutes.": "每条路由都有一笔购买在内存池中待处理 — 请几分钟后再来。", + "Every route has a purchase pending in the mempool — check back in a few minutes.": "每条路由都有一笔购买在内存池中等待确认 — 请几分钟后再来。", "Every XCP-69 launch is identical: 100M supply, 69M public sale at 0.01 XCP per 1,000-token lot, 31M reserved for the liquidity pool, 10 XCP per-address cap, an on-chain pre-announcement before minting opens, and a 1,000-block window. There is no fine print to read because there is no fine print. The full parameter set lives on the {link} page.": "每一次 XCP-69 发射都完全相同:1 亿供应量,6900 万公开发售、每份 1,000 枚代币 0.01 XCP,3100 万预留给流动性池,每地址上限 10 XCP,铸造开启前的链上预公告,以及 1,000 个区块的窗口期。没有小字条款可读,因为根本没有小字条款。完整参数集见{link}页面。", "Every XCP-69 mint, trade, burn, resting order and launch, newest first.": "每一笔 XCP-69 铸造、成交、销毁、挂单和发射,最新在前。", "everyone dumps": "所有人砸盘", - "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, what it costs (nothing), and how to integrate.": "关于 XCP-69 的一切,尽在一处:发射如何运作、定价如何运作、毕业意味着什么、需要花多少钱(零)、以及如何集成。", "Everything about XCP-69, in one place. The first half explains how launches work for anyone. The second half is for developers who want to read or build on the same on-chain data this site does.": "关于 XCP-69 的一切,尽在一处。前半部分向所有人解释发射如何运作。后半部分面向开发者,帮助他们读取本站所用的同一份链上数据,或在其之上构建。", "Everything is curl-able. Launches and mints:": "一切都能用 curl 取到。发射与铸造:", "Everything this site displays comes from public APIs. You can build your own launchpad, bot, or dashboard on the same data — nothing below requires our permission.": "本站展示的一切都来自公开 API。你可以基于同一份数据构建自己的发射台、机器人或看板 — 下面的内容都不需要我们的许可。", @@ -549,7 +548,6 @@ "Graduated": "毕业", "graduated coins": "毕业代币", "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.": "已毕业的代币在一个恒定乘积的 TOKEN/XCP 流动性池中交易。价格就是池中两种储备的比率;每一次兑换都会移动它。每次兑换固定收取 {fee} 的费用,支付给池子本身 — 由于 LP 已销毁,费用会加深锁定的流动性,而不会付给任何人。", - "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses, at which point the pool is created and its liquidity is burned. A launch that misses its target refunds every satoshi by consensus and does not count. {link}": "毕业就是售罄:从至少 {n} 个不同地址募集 {xcp} XCP,届时创建流动性池并销毁其 LP 代币(流动性永久锁定)。未达目标的发射会按共识退回每一聪,不计入其中。{link}", "Graduation": "毕业", "Graveyard": "墓地", "Graveyard — xcp.fun": "墓地 — xcp.fun", @@ -716,7 +714,7 @@ "Minting activity": "铸造动态", "minting is live": "铸造已开放", "minting is live — refresh the page": "铸造已开放 — 请刷新页面", - "Minting is paid in XCP on your address's Counterparty balance — BTC only covers miner fees. The fastest no-account route is an on-chain dispenser: send BTC, the protocol vends XCP automatically. {link}. A max mint is 10 XCP; a launch needs 0.5 XCP for the name.": "铸造用你地址 Counterparty 余额中的 XCP 支付 — BTC 只用于矿工费。无需开户的最快途径是链上售货机:发送 BTC,协议自动发放 XCP。{link}。单地址最多铸造 10 XCP;发射需要 0.5 XCP 用于注册名称。", + "Minting is paid in XCP on your address's Counterparty balance — BTC only covers miner fees. The fastest no-account route is an on-chain dispenser: send BTC, the protocol vends XCP automatically. {link}. A max mint is 10 XCP; a launch needs 0.5 XCP for the name.": "铸造用你地址 Counterparty 余额中的 XCP 支付 — BTC 只用于矿工费。无需开户的最快途径是链上售货机:发送 BTC,协议自动发放 XCP。{link}。单地址最多可投入 10 XCP 参与铸造;发射需要 0.5 XCP 用于注册名称。", "Minting now": "铸造中", "Minting opens": "铸造开启", "minting opens at": "铸造开放于", @@ -743,7 +741,6 @@ "New buyers": "新买家", "Newest": "最新", "Next →": "下一页 →", - "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.": "不会 — 铸造只是把 XCP 托管在该发射上。如果毕业,你持有代币;如果退款,每一聪都会退回。铸造真正花掉的只有比特币矿工费 — 而这正是本计划所覆盖的部分。", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.": "不是 — 这正是重点。公平铸造协议允许极大的变化:没有软顶、不退款、创建者佣金高达 99%、预挖、完全没有流动性池。XCP-69 固定了其中的一种配置,本站只列出与标准完全匹配的发射,并逐字段对照链上记录检查。在这里,每一次发射的行为都完全相同,你永远不需要自己审计参数。而你在其他任何地方遇到的公平铸造合约都没有这样的保证 — 铸造前请先读它的字段。", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.": "不是 — 它是结构性的,不是承诺。流动性池必须以铸造价的 69/31 开盘,因为这是仅有的数量,但随着人们向池中卖出,底价会不断下降。没有什么能阻止代币跌破铸造价。池子保证的是一个永远不会归零的买价,而不是一个你会满意的买价。", "No active {give}/{get} pool liquidity. A resting order can still fill through the order book.": "{give}/{get} 流动性池当前没有流动性。挂单仍可通过订单簿成交。", @@ -816,7 +813,7 @@ "Of the launches that finished": "已结束的发射中", "of the typical mint fee": "相对典型铸造矿工费", "of which {amount} realized": "其中 {amount} 已实现", - "Of XCP minted": "铸造 XCP 占比", + "Of XCP minted": "投入铸造的 XCP 占比", "Off-chain by default: the chain permanently carries the URL of the asset-info JSON, and the content behind it is curatable by the asset's current owner (wallet-signature gated). Taproot creators can instead inscribe the image on-chain as the permanent description — no off-chain dependency at all. Either way, nothing economic depends on hosted content.": "默认在链下:链上永久保存资产信息 JSON 的 URL,其背后的内容可由资产当前所有者维护(需钱包签名)。Taproot 创建者也可以改为把图片铭刻上链作为永久描述 — 完全没有链下依赖。无论哪种方式,经济机制都不依赖托管的内容。", "On other launchpads, graduation is a threshold inside a live market: the token trades on a curve while everyone hopes it reaches the magic number. XCP-69 graduation is {binary} and happens before any trading exists. Sell out the 69M sale within 1,000 blocks and the launch graduates — pool seeded, LP burned, supply locked, trading live in the same block's resolution phase. Miss it and the launch never trades at all.": "在其他发射台上,毕业是活跃市场中的一条阈值线:代币在曲线上交易,所有人都盼着它触到那个神奇的数字。XCP-69 的毕业是{binary}的,而且发生在任何交易存在之前。在 1,000 个区块内售罄 6900 万,发射就毕业 — 流动性池注入、LP 销毁、供应量锁定、交易在同一区块的结算阶段开始。错过了,这次发射就永远不会交易。", "on their Counterparty balance": "在其 Counterparty 余额中", @@ -1191,7 +1188,6 @@ "XCP in the pool": "池中 XCP", "XCP market · cached 15 min": "XCP 市场 · 缓存 15 分钟", "XCP Price": "XCP 价格", - "XCP reserve ÷ token reserve. Moves with every swap; there is no order book and no market maker.": "XCP 储备 ÷ 代币储备。每次兑换都会变动;没有订单簿,也没有做市商。", "XCP Rewards": "XCP 奖励", "XCP Rewards — xcp.fun": "XCP 奖励 — xcp.fun", "XCP to receive": "收到 XCP", @@ -1234,6 +1230,10 @@ "Your wallet supplied a connection signature that didn't verify, so we can't confirm this address is really yours. Disconnect and reconnect to try again — nothing else on the site is affected.": "你的钱包提供的连接签名未能通过验证,因此我们无法确认这个地址真的属于你。请断开后重新连接再试 — 网站的其他功能不受影响。", "Your wallet will ask once per route, in order — each popup is one route, nothing more.": "钱包会按顺序为每条路由请求一次确认 — 每个弹窗对应一条路由,仅此而已。", "Your xcp.fun profile": "你的 xcp.fun 个人资料", - "yours — reused, no fee": "你的 — 复用,免费", - "zero existing supply": "现有供应量为零" + "yours — reused, no fee": "你的 — 复用,免注册费", + "zero existing supply": "现有供应量为零", + "A mint locks your XCP until the launch resolves. If it graduates, your XCP funds the pool and you receive tokens, which can lose value. If it refunds, your XCP is returned. The Bitcoin network fee is paid in either case; rewards do not guarantee that every cost is covered.": "铸造会锁定你的 XCP,直到发射结果确定。若成功毕业,你的 XCP 会注入流动性池,你则收到代币;代币的价值可能下跌。若发射退款,你的 XCP 会退回。无论哪种结果,都需要支付比特币矿工费;奖励不保证覆盖所有成本。", + "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses. The pool is created and its initial LP tokens go to an unspendable address, permanently locking that liquidity. A launch that misses its target returns the contributed XCP by consensus and does not count. {link}": "毕业意味着售罄:从至少 {n} 个不同地址募集 {xcp} XCP。随后创建流动性池,初始 LP 代币记入不可花费地址,从而永久锁定这部分流动性。未达目标的发射会按共识退回投入的 XCP,不计入其中。{link}", + "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, fees, and how to integrate.": "关于 XCP-69 的一切,尽在一处:发射与定价如何运作、毕业意味着什么、费用以及如何集成。", + "Pool price is XCP reserve ÷ token reserve. Swaps change this ratio. Limit orders can also trade on Counterparty's order book.": "池价等于 XCP 储备 ÷ 代币储备。兑换会改变这一比率。限价单也可以在 Counterparty 的订单簿上成交。" } diff --git a/apps/web/src/locales/zh.status.json b/apps/web/src/locales/zh.status.json index 8694a46d..8690b1b6 100644 --- a/apps/web/src/locales/zh.status.json +++ b/apps/web/src/locales/zh.status.json @@ -237,7 +237,6 @@ "Every minted token dumped in one trade — the pool still quotes a bid with {left} XCP left, and sellers exit at {mult}× mint ({paid}% of what they paid). Those {left} XCP can never be withdrawn: every launch is a permanent sink for XCP itself.", "Every route has a purchase pending in the mempool — check back in a few minutes.", "Everything about XCP-69, in one place. The first half explains how launches work for anyone. The second half is for developers who want to read or build on the same on-chain data this site does.", - "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, what it costs (nothing), and how to integrate.", "Everything is curl-able. Launches and mints:", "Everything this site displays comes from public APIs. You can build your own launchpad, bot, or dashboard on the same data — nothing below requires our permission.", "Expiration", @@ -268,7 +267,6 @@ "Get XCP — xcp.fun", "Graduated", "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.", - "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses, at which point the pool is created and its liquidity is burned. A launch that misses its target refunds every satoshi by consensus and does not count. {link}", "Graduation", "Graveyard", "Graveyard pages", @@ -455,7 +453,6 @@ "No trades yet.", "No value chart for {assets}: its recent transfer or liquidity history did not load completely, so we cannot know how many tokens this wallet held at each point. Current holdings and value are live and accurate.", "No value chart: its recent transfer or liquidity history did not load completely, so we cannot know how many tokens this wallet held at each point. Current holdings and value are live and accurate.", - "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.", "Nobody has minted yet. The first row here is available.", @@ -779,7 +776,6 @@ "XCP in pools", "XCP in the pool", "XCP market · cached 15 min", - "XCP reserve ÷ token reserve. Moves with every swap; there is no order book and no market maker.", "XCP to receive", "XCP to unload", "XCP-69 is a fixed-parameter token launch standard built on Counterparty {pools}. There is no factory contract, no admin key, and no platform custody at any point: the protocol {is} the platform. Every action — creating a launch, minting, swapping — is a transaction you sign in your own wallet and broadcast to Bitcoin. This site is an interface over public on-chain data; if it disappeared tomorrow, every launch, refund, and pool would keep working exactly as before.", @@ -1223,6 +1219,10 @@ "Use whole numbers only.", "Your wallet could not complete this request. Check the details.", "Trade history is unavailable. Please try again shortly.", - "Units unavailable" + "Units unavailable", + "A mint locks your XCP until the launch resolves. If it graduates, your XCP funds the pool and you receive tokens, which can lose value. If it refunds, your XCP is returned. The Bitcoin network fee is paid in either case; rewards do not guarantee that every cost is covered.", + "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses. The pool is created and its initial LP tokens go to an unspendable address, permanently locking that liquidity. A launch that misses its target returns the contributed XCP by consensus and does not count. {link}", + "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, fees, and how to integrate.", + "Pool price is XCP reserve ÷ token reserve. Swaps change this ratio. Limit orders can also trade on Counterparty's order book." ] } From 1acdd42afca189723bf43f8acb55ea46f61f930a Mon Sep 17 00:00:00 2001 From: Dan Anderson Date: Mon, 7 Sep 2026 19:12:24 -0400 Subject: [PATCH 06/10] Clarify Korean quantity and liquidity status copy --- apps/web/src/locales/ko.json | 32 ++++++++++++++--------------- apps/web/src/locales/ko.status.json | 10 ++++----- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/apps/web/src/locales/ko.json b/apps/web/src/locales/ko.json index 3137a03b..006284ae 100644 --- a/apps/web/src/locales/ko.json +++ b/apps/web/src/locales/ko.json @@ -1,6 +1,6 @@ { "— minters": "— 민터", - "· {n} pending": "· 대기 중 {n}건", + "· {n} pending": "· {n} 확인 대기 중", "· {pct}% over floor": "· 최저가보다 {pct}% 높음", "· {wallets} wallets · {txs} txs": "· 지갑 {wallets}개 · 트랜잭션 {txs}건", "· above the mempool estimate": "· 멤풀 추정치보다 높음", @@ -227,6 +227,7 @@ "69,000,000 (the soft cap IS the whole sale)": "69,000,000 (소프트캡이 곧 판매 전체)", "a future block — announced on-chain before minting can open": "미래의 블록 — 민팅이 열리기 전에 온체인에 공지", "A launch moves through four phases:": "런칭은 네 단계를 거칩니다:", + "A mint locks your XCP until the launch resolves. If it graduates, your XCP funds the pool and you receive tokens, which can lose value. If it refunds, your XCP is returned. The Bitcoin network fee is paid in either case; rewards do not guarantee that every cost is covered.": "민팅에 투입한 XCP는 런칭 결과가 확정될 때까지 잠깁니다. 졸업하면 XCP는 풀에 투입되고 대신 토큰을 받으며, 토큰 가치는 하락할 수 있습니다. 환불되면 XCP를 돌려받습니다. 어느 경우든 비트코인 네트워크 수수료는 지불하며, 보상이 모든 비용을 보전한다고 보장하지는 않습니다.", "A new way to launch a coin on Counterparty — every launch identical, enforced by consensus.": "Counterparty에서 코인을 런칭하는 새로운 방식 — 모든 런칭이 동일하고, 합의로 강제됩니다.", "A pool never runs out, it only gets worse. This is what it can absorb before its price moves 1%.": "풀은 바닥나지 않고 조건만 나빠집니다. 가격이 1% 움직이기 전까지 풀이 받아낼 수 있는 양입니다.", "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.": "환불된 런칭은 돈을 잃은 것이 아닙니다. XCP-69는 전부 아니면 전무입니다. 소프트캡에 미달한 런칭은 합의에 따라 마지막 사토시까지 돌려주며, 우리가 결정할 여지도 누군가 가로챌 방법도 없습니다.", @@ -266,7 +267,7 @@ "Amount of {asset} to sell": "매도할 {asset} 수량", "Amount of XCP to deposit": "예치할 XCP 수량", "Amount to remove": "제거할 수량", - "Amount too small — rounds to 0": "수량이 너무 작습니다 — 0으로 반올림됩니다", + "Amount too small — rounds to 0": "수량이 너무 작습니다 — 계산 결과가 0입니다", "An XCP bounty for the first three launches to graduate, and MINTS for every mint.": "가장 먼저 졸업하는 런칭 3개에는 XCP 바운티를, 모든 민팅에는 MINTS를 드립니다.", "an XCP fairminter on xcp.fun": "xcp.fun의 XCP 페어민터", "Animated WEBP shows as a still frame in the announce channel — Telegram cannot play one. A GIF moves there too.": "움직이는 WEBP는 공지 채널에서 정지 화면으로만 보입니다 — 텔레그램은 WEBP 애니메이션을 재생하지 못합니다. GIF는 거기서도 움직입니다.", @@ -489,8 +490,8 @@ "Enter an amount": "수량을 입력하세요", "Enter opens the first result · Esc closes": "Enter로 첫 결과 열기 · Esc로 닫기", "escrowed": "에스크로 중", - "escrowed until launch": "런칭 확정까지 에스크로", - "Escrowed until the launch resolves: tokens if it sells out, full XCP refund if it doesn't.": "런칭이 확정될 때까지 에스크로에 보관됩니다. 완판되면 토큰을, 아니면 XCP 전액을 돌려받습니다.", + "escrowed until launch": "런칭 결과 확정까지 에스크로 보관", + "Escrowed until the launch resolves: tokens if it sells out, full XCP refund if it doesn't.": "런칭 결과가 확정될 때까지 에스크로에 보관됩니다. 완판되면 토큰을, 아니면 XCP 전액을 돌려받습니다.", "Estimated fill": "예상 체결", "Event": "이벤트", "Every launch that conforms to XCP-69, and what has actually been minted.": "XCP-69 표준을 준수하는 모든 런칭과 실제 민팅된 수량입니다.", @@ -501,7 +502,7 @@ "Every XCP-69 launch is identical: 100M supply, 69M public sale at 0.01 XCP per 1,000-token lot, 31M reserved for the liquidity pool, 10 XCP per-address cap, an on-chain pre-announcement before minting opens, and a 1,000-block window. There is no fine print to read because there is no fine print. The full parameter set lives on the {link} page.": "모든 XCP-69 런칭은 동일합니다: 공급량 1억, 1,000토큰 단위당 0.01 XCP로 6,900만 공개 판매, 3,100만은 유동성 풀에 예약, 주소당 10 XCP 상한, 민팅 시작 전 온체인 사전 공지, 그리고 1,000블록의 기간. 읽어야 할 깨알 조항은 없습니다. 깨알 조항 자체가 없기 때문입니다. 전체 파라미터는 {link} 페이지에 있습니다.", "Every XCP-69 mint, trade, burn, resting order and launch, newest first.": "XCP-69의 모든 민팅, 거래, 소각, 대기 주문, 런칭을 최신순으로.", "everyone dumps": "전원 덤핑", - "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, what it costs (nothing), and how to integrate.": "XCP-69의 모든 것을 한곳에: 런칭이 작동하는 방식, 가격이 정해지는 방식, 졸업의 의미, 비용 (없음), 그리고 연동 방법.", + "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, fees, and how to integrate.": "XCP-69의 모든 것을 한곳에서: 런칭과 가격 결정 방식, 졸업의 의미, 수수료, 연동 방법을 알아보세요.", "Everything about XCP-69, in one place. The first half explains how launches work for anyone. The second half is for developers who want to read or build on the same on-chain data this site does.": "XCP-69의 모든 것을 한곳에. 전반부는 누구나 이해할 수 있는 런칭의 작동 방식입니다. 후반부는 이 사이트와 같은 온체인 데이터를 읽거나 그 위에 무언가를 만들려는 개발자를 위한 것입니다.", "Everything is curl-able. Launches and mints:": "전부 curl로 가져올 수 있습니다. 런칭과 민팅:", "Everything this site displays comes from public APIs. You can build your own launchpad, bot, or dashboard on the same data — nothing below requires our permission.": "이 사이트가 표시하는 모든 것은 공개 API에서 옵니다. 같은 데이터로 직접 런치패드, 봇, 대시보드를 만들 수 있습니다 — 아래의 어떤 것도 우리의 허가가 필요하지 않습니다.", @@ -549,7 +550,7 @@ "Graduated": "졸업", "graduated coins": "졸업 코인", "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.": "졸업한 토큰은 상수곱 TOKEN/XCP 풀에서 거래됩니다. 가격은 단순히 풀 준비금의 비율이며, 모든 스왑이 가격을 움직입니다. 각 스왑의 고정 수수료 {fee}는 풀 자체에 지급됩니다 — LP가 소각되었으므로 수수료는 누군가에게 지급되는 대신 잠긴 유동성을 두텁게 합니다.", - "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses, at which point the pool is created and its liquidity is burned. A launch that misses its target refunds every satoshi by consensus and does not count. {link}": "졸업은 완판을 뜻합니다. 서로 다른 주소 {n}개 이상에서 {xcp} XCP가 모이면 그 시점에 풀이 만들어지고 유동성이 소각됩니다. 목표에 미달한 런칭은 합의에 따라 마지막 사토시까지 환불되며 집계에 포함되지 않습니다. {link}", + "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses. The pool is created and its initial LP tokens go to an unspendable address, permanently locking that liquidity. A launch that misses its target returns the contributed XCP by consensus and does not count. {link}": "졸업은 완판을 뜻합니다. 최소 {n}개의 서로 다른 주소에서 {xcp} XCP를 모으면 풀이 생성됩니다. 초기 LP 토큰은 사용 불가 주소로 보내져 해당 유동성이 영구히 잠깁니다. 목표를 채우지 못한 런칭은 합의 규칙에 따라 납입한 XCP를 돌려주며, 졸업으로 집계되지 않습니다. {link}", "Graduation": "졸업", "Graveyard": "묘지", "Graveyard — xcp.fun": "묘지 — xcp.fun", @@ -743,7 +744,6 @@ "New buyers": "신규 매수자", "Newest": "최신순", "Next →": "다음 →", - "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.": "아니요 — 민팅은 XCP를 해당 런칭에 에스크로하는 것입니다. 졸업하면 토큰을 보유하게 되고, 환불되면 마지막 사토시까지 돌려받습니다. 민팅에 실제로 드는 비용은 비트코인 수수료뿐이며, 이 프로그램이 보전하는 부분이 바로 그것입니다.", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.": "아니요 — 그것이 핵심입니다. 페어민터 프로토콜은 엄청난 변형을 허용합니다: 소프트캡 없음, 환불 없음, 최대 99%의 제작자 커미션, 프리마인, 풀 없음. XCP-69는 그중 하나의 구성을 고정하고, 이 사이트는 온체인 기록과 필드 단위로 대조해 표준에 정확히 일치하는 런칭만 표시합니다. 여기서는 모든 런칭이 동일하게 동작하며 파라미터를 직접 감사할 필요가 없습니다. 다른 곳에서 만나는 페어민터에는 그런 보장이 없습니다 — 민팅 전에 필드를 읽어 보세요.", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.": "아니요 — 약속이 아니라 구조입니다. 존재하는 수량이 69/31뿐이므로 풀은 민팅가의 69/31에서 열릴 수밖에 없지만, 바닥은 사람들이 팔수록 내려갑니다. 토큰이 민팅가 아래에서 거래되는 것을 막는 장치는 없습니다. 풀이 보장하는 것은 절대 0이 되지 않는 매수 호가이지, 마음에 드는 호가는 아닙니다.", "No active {give}/{get} pool liquidity. A resting order can still fill through the order book.": "{give}/{get} 풀에 활성 유동성이 없습니다. 대기 주문은 호가창을 통해 체결될 수 있습니다.", @@ -757,7 +757,7 @@ "No launches refunded in this window.": "이 기간에 환불된 런칭이 없습니다.", "No launches yet.": "아직 런칭이 없습니다.", "No live launches. Start one — it sells out or everyone gets refunded.": "진행 중인 런칭이 없습니다. 직접 시작해 보세요 — 완판되거나 모두 환불됩니다.", - "No LP in this pool": "이 풀에 LP가 없습니다", + "No LP in this pool": "이 풀에 보유한 LP가 없습니다", "No matches": "일치 항목 없음", "No meaningful balance": "의미 있는 잔액 없음", "No mints from that address. Search matches a full address exactly.": "해당 주소의 민팅이 없습니다. 검색은 전체 주소와 정확히 일치해야 합니다.", @@ -816,7 +816,7 @@ "Of the launches that finished": "종료된 런칭 중", "of the typical mint fee": "일반적인 민팅 수수료 대비", "of which {amount} realized": "그중 {amount} 실현", - "Of XCP minted": "민팅 XCP 비율", + "Of XCP minted": "민팅에 납입한 XCP 비율", "Off-chain by default: the chain permanently carries the URL of the asset-info JSON, and the content behind it is curatable by the asset's current owner (wallet-signature gated). Taproot creators can instead inscribe the image on-chain as the permanent description — no off-chain dependency at all. Either way, nothing economic depends on hosted content.": "기본은 오프체인입니다: 체인은 자산 정보 JSON의 URL을 영구히 보관하고, 그 뒤의 내용은 자산의 현재 소유자가 (지갑 서명으로) 관리할 수 있습니다. Taproot 제작자는 대신 이미지를 영구 설명으로 온체인에 인스크립션할 수 있습니다 — 오프체인 의존성이 전혀 없습니다. 어느 쪽이든 경제적인 부분은 호스팅된 콘텐츠에 의존하지 않습니다.", "On other launchpads, graduation is a threshold inside a live market: the token trades on a curve while everyone hopes it reaches the magic number. XCP-69 graduation is {binary} and happens before any trading exists. Sell out the 69M sale within 1,000 blocks and the launch graduates — pool seeded, LP burned, supply locked, trading live in the same block's resolution phase. Miss it and the launch never trades at all.": "다른 런치패드에서 졸업은 살아 있는 시장 안의 문턱입니다: 모두가 마법의 숫자에 닿기를 바라는 동안 토큰은 커브 위에서 거래됩니다. XCP-69의 졸업은 {binary}이며 어떤 거래도 존재하기 전에 일어납니다. 1,000블록 안에 6,900만 판매를 완판하면 런칭은 졸업합니다 — 풀 생성, LP 소각, 공급량 잠금, 같은 블록의 확정 단계에서 거래 시작. 놓치면 런칭은 아예 거래되지 않습니다.", "on their Counterparty balance": "Counterparty 잔액에", @@ -832,7 +832,7 @@ "open": "시작", "Open": "진행 중", "Open Bitcoin price": "비트코인 가격 열기", - "open LP": "보유 LP", + "open LP": "인출 가능한 LP", "Open menu": "메뉴 열기", "open right now": "현재 진행 중", "Open XCP price": "XCP 가격 열기", @@ -858,7 +858,7 @@ "Page {page} of {total}": "{total}페이지 중 {page}페이지", "Pagination": "페이지 이동", "Paid": "지불", - "paid as it sells": "팔릴 때마다 지급", + "paid as it sells": "판매될 때마다 BTC 수령", "Paired XCP": "페어 XCP", "Partial": "부분 체결", "partially filled · resting": "일부 체결 · 대기 중", @@ -888,6 +888,7 @@ "Pool + order book": "풀 + 호가창", "Pool is empty": "풀이 비어 있습니다", "Pool price × circulating supply (issued minus burned). A convention, not a promise — the pool could not pay it out.": "풀 가격 × 유통 공급량 (발행량에서 소각분을 뺀 값). 관례일 뿐 약속이 아닙니다 — 풀이 그 금액을 지급할 수는 없습니다.", + "Pool price is XCP reserve ÷ token reserve. Swaps change this ratio. Limit orders can also trade on Counterparty's order book.": "풀 가격은 XCP 준비금 ÷ 토큰 준비금입니다. 스왑마다 이 비율이 바뀝니다. 지정가 주문은 Counterparty 호가창에서도 거래될 수 있습니다.", "Pool reserve": "풀 준비금", "Pool:": "풀:", "Pools": "풀", @@ -1142,11 +1143,11 @@ "Upcoming": "곧 시작", "Upload failed": "업로드 실패", "Upload token image": "토큰 이미지 업로드", - "Use digits and a dot, with up to {n} decimal places.": "반각 숫자와 점을 사용하고 소수점 이하는 최대 {n}자리까지 입력하세요.", + "Use digits and a dot, with up to {n} decimal places.": "숫자와 소수점(.)만 입력하세요. 소수 {n}자리까지 가능합니다.", "Use whole numbers only.": "정수만 입력하세요.", "Value": "가치", - "Value and underlying amounts are what withdrawing the LP tokens now would return. XCP value is available when XCP is one side of the pair. LP profit and loss is not estimated.": "가치와 기초 수량은 지금 LP 토큰을 인출하면 돌려받을 자산입니다. XCP 가치는 페어의 한쪽이 XCP일 때 표시됩니다. LP 손익은 추정하지 않습니다.", - "Vends": "판매 단위", + "Value and underlying amounts are what withdrawing the LP tokens now would return. XCP value is available when XCP is one side of the pair. LP profit and loss is not estimated.": "가치와 기초 자산 수량은 지금 LP 토큰으로 유동성을 회수할 때 받게 될 금액과 수량입니다. XCP 가치는 페어의 한쪽이 XCP일 때 표시됩니다. LP 손익은 추정하지 않습니다.", + "Vends": "판매 가능 횟수", "Venue": "체결 경로", "Venue / time": "체결 경로 / 시간", "Verdict pending": "판정 대기", @@ -1191,7 +1192,6 @@ "XCP in the pool": "풀 안의 XCP", "XCP market · cached 15 min": "XCP 시장 · 15분 캐시", "XCP Price": "XCP 가격", - "XCP reserve ÷ token reserve. Moves with every swap; there is no order book and no market maker.": "XCP 준비금 ÷ 토큰 준비금. 스왑마다 움직입니다. 호가창도 마켓 메이커도 없습니다.", "XCP Rewards": "XCP 보상", "XCP Rewards — xcp.fun": "XCP 보상 — xcp.fun", "XCP to receive": "받을 XCP", @@ -1212,7 +1212,7 @@ "you": "나", "you · {n} XCP": "나 · {n} XCP", "you have minted this launch’s max": "이 런칭의 최대 한도까지 민팅했습니다", - "You mint": "받는 토큰", + "You mint": "민팅 수량", "You own {name}, but {reason}.": "{name} 이름을 소유하고 있지만, {reason}.", "You pay": "지불 금액", "You receive": "받는 금액", diff --git a/apps/web/src/locales/ko.status.json b/apps/web/src/locales/ko.status.json index 8694a46d..8690b1b6 100644 --- a/apps/web/src/locales/ko.status.json +++ b/apps/web/src/locales/ko.status.json @@ -237,7 +237,6 @@ "Every minted token dumped in one trade — the pool still quotes a bid with {left} XCP left, and sellers exit at {mult}× mint ({paid}% of what they paid). Those {left} XCP can never be withdrawn: every launch is a permanent sink for XCP itself.", "Every route has a purchase pending in the mempool — check back in a few minutes.", "Everything about XCP-69, in one place. The first half explains how launches work for anyone. The second half is for developers who want to read or build on the same on-chain data this site does.", - "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, what it costs (nothing), and how to integrate.", "Everything is curl-able. Launches and mints:", "Everything this site displays comes from public APIs. You can build your own launchpad, bot, or dashboard on the same data — nothing below requires our permission.", "Expiration", @@ -268,7 +267,6 @@ "Get XCP — xcp.fun", "Graduated", "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.", - "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses, at which point the pool is created and its liquidity is burned. A launch that misses its target refunds every satoshi by consensus and does not count. {link}", "Graduation", "Graveyard", "Graveyard pages", @@ -455,7 +453,6 @@ "No trades yet.", "No value chart for {assets}: its recent transfer or liquidity history did not load completely, so we cannot know how many tokens this wallet held at each point. Current holdings and value are live and accurate.", "No value chart: its recent transfer or liquidity history did not load completely, so we cannot know how many tokens this wallet held at each point. Current holdings and value are live and accurate.", - "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.", "Nobody has minted yet. The first row here is available.", @@ -779,7 +776,6 @@ "XCP in pools", "XCP in the pool", "XCP market · cached 15 min", - "XCP reserve ÷ token reserve. Moves with every swap; there is no order book and no market maker.", "XCP to receive", "XCP to unload", "XCP-69 is a fixed-parameter token launch standard built on Counterparty {pools}. There is no factory contract, no admin key, and no platform custody at any point: the protocol {is} the platform. Every action — creating a launch, minting, swapping — is a transaction you sign in your own wallet and broadcast to Bitcoin. This site is an interface over public on-chain data; if it disappeared tomorrow, every launch, refund, and pool would keep working exactly as before.", @@ -1223,6 +1219,10 @@ "Use whole numbers only.", "Your wallet could not complete this request. Check the details.", "Trade history is unavailable. Please try again shortly.", - "Units unavailable" + "Units unavailable", + "A mint locks your XCP until the launch resolves. If it graduates, your XCP funds the pool and you receive tokens, which can lose value. If it refunds, your XCP is returned. The Bitcoin network fee is paid in either case; rewards do not guarantee that every cost is covered.", + "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses. The pool is created and its initial LP tokens go to an unspendable address, permanently locking that liquidity. A launch that misses its target returns the contributed XCP by consensus and does not count. {link}", + "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, fees, and how to integrate.", + "Pool price is XCP reserve ÷ token reserve. Swaps change this ratio. Limit orders can also trade on Counterparty's order book." ] } From a5fbcdfe3981914675e68bfea64614a120a3426c Mon Sep 17 00:00:00 2001 From: Dan Anderson Date: Mon, 7 Sep 2026 19:12:37 -0400 Subject: [PATCH 07/10] Clarify Spanish Portuguese and French transaction terminology --- apps/web/src/locales/es.json | 34 ++++++++--------- apps/web/src/locales/es.status.json | 8 ++-- apps/web/src/locales/fr.json | 58 ++++++++++++++--------------- apps/web/src/locales/fr.status.json | 8 ++-- apps/web/src/locales/pt.json | 54 +++++++++++++-------------- apps/web/src/locales/pt.status.json | 8 ++-- 6 files changed, 85 insertions(+), 85 deletions(-) diff --git a/apps/web/src/locales/es.json b/apps/web/src/locales/es.json index c07c1943..c25b7a70 100644 --- a/apps/web/src/locales/es.json +++ b/apps/web/src/locales/es.json @@ -30,7 +30,7 @@ "{cap} · {liquidity} pool": "{cap} · pool de {liquidity}", "{cap} XCP · {supply} supply": "{cap} XCP · suministro de {supply}", "{cap} XCP market cap": "Capitalización de {cap} XCP", - "{claim} By default the chain permanently carries the URL of the asset-info JSON (via {lockDescription}) while the image and info are hosted off-chain, editable only by the asset's current on-chain owner via a wallet-signed message. If that hosting ever vanished, the token's economics — supply, pool, refunds — are untouched; only the artwork would be. Creators launching from a taproot wallet can remove the dependency entirely by inscribing the image on-chain as the permanent description.": "{claim} Por defecto la cadena guarda para siempre la URL del JSON con la información del activo (mediante {lockDescription}), mientras que la imagen y la información se alojan fuera de la cadena y solo puede editarlas el dueño actual del activo en la cadena, con un mensaje firmado desde su billetera. Si ese alojamiento desapareciera, la economía del token — suministro, pool, reembolsos — quedaría intacta; solo se perdería el arte. Los creadores que lanzan desde una billetera Taproot pueden eliminar la dependencia por completo inscribiendo la imagen en la cadena como descripción permanente.", + "{claim} By default the chain permanently carries the URL of the asset-info JSON (via {lockDescription}) while the image and info are hosted off-chain, editable only by the asset's current on-chain owner via a wallet-signed message. If that hosting ever vanished, the token's economics — supply, pool, refunds — are untouched; only the artwork would be. Creators launching from a taproot wallet can remove the dependency entirely by inscribing the image on-chain as the permanent description.": "{claim} Por defecto la cadena guarda para siempre la URL del JSON con la información del activo (mediante {lockDescription}), mientras que la imagen y la información se alojan fuera de la cadena y solo puede editarlas el dueño actual del activo en la cadena, con un mensaje firmado desde su billetera. Si ese alojamiento desapareciera, la economía del token — suministro, pool, reembolsos — quedaría intacta; solo el arte se vería afectado. Los creadores que lanzan desde una billetera Taproot pueden eliminar la dependencia por completo inscribiendo la imagen en la cadena como descripción permanente.", "{claim} If XCP's price moves during the ~7-day window, a refund makes you whole in XCP terms only.": "{claim} Si el precio de XCP se mueve durante la ventana de ~7 días, el reembolso te deja como estabas solo en términos de XCP.", "{claim} The cap is per address, not per person. It raises the cost of faking a crowd of {n}; it cannot prevent one.": "{claim} El tope es por dirección, no por persona. Encarece fingir una multitud de {n}; no puede impedirla.", "{claim} The pool floor decays as people sell into it. Nothing stops a token from trading below mint price.": "{claim} El piso del pool baja a medida que la gente le vende. Nada impide que un token se negocie por debajo del precio de acuñación.", @@ -148,7 +148,7 @@ "{pct}% of pool": "{pct}% del pool", "{pct}% of the float dumped → sellers get {xcp} XCP back, exiting at {mult}× their mint price — below mint cost now.": "{pct}% del flotante vendido de golpe → los vendedores recuperan {xcp} XCP y salen a {mult}× su precio de acuñación — ya por debajo del costo de acuñar.", "{pct}% of the float dumped → sellers get {xcp} XCP back, exiting at {mult}× their mint price — the opening premium absorbing the exit.": "{pct}% del flotante vendido de golpe → los vendedores recuperan {xcp} XCP y salen a {mult}× su precio de acuñación — la prima de apertura absorbe la salida.", - "{pct}% of the LP is burned. The rest can be withdrawn by whoever holds it.": "El {pct}% del LP está quemado. El resto puede retirarlo quien lo tenga.", + "{pct}% of the LP is burned. The rest can be withdrawn by whoever holds it.": "El {pct}% de los tokens LP está quemado. Quienes tienen el resto pueden retirar la liquidez que les corresponde.", "{pct}% slippage authorizes a very unfavorable fill. The button will warn before swapping.": "Un deslizamiento de {pct}% autoriza una ejecución muy desfavorable. El botón te avisará antes de intercambiar.", "{period} {kind} from every fill on the pair — pool and order book. Volume beneath in XCP; {volume} XCP is the busiest {bucket} shown. Trend {trend} over this range.": "{kind} en intervalo {period}, a partir de cada ejecución del par — pool y libro de órdenes. Volumen abajo en XCP; el máximo por {bucket} en pantalla es {volume} XCP. Tendencia {trend} en este rango.", "{recipe} Success and failure both end at fairminter status {closed}, so the pool row is the disambiguator:": "{recipe} Tanto el éxito como el fracaso terminan con el fairminter en estado {closed}, así que la fila del pool es lo que los distingue:", @@ -207,7 +207,7 @@ "⏳ Scheduled — announced on-chain, nothing minted yet.": "⏳ Programado — anunciado en la cadena, todavía no se acuñó nada.", "◆ Pool": "◆ Pool", "✓ broadcast": "✓ transmitida", - "✓ Graduated — pool created with {xcp} XCP{usd} + 31M tokens, LP burned.": "✓ Graduado — pool creado con {xcp} XCP{usd} + 31M de tokens, LP quemado.", + "✓ Graduated — pool created with {xcp} XCP{usd} + 31M tokens, LP burned.": "✓ Graduado — pool creado con {xcp} XCP{usd} + 31M de tokens, tokens LP quemados.", "💀 RIP": "💀 RIP", "0.01 XCP / 1,000 · sells out or refunds": "0.01 XCP / 1,000 · se agota o se reembolsa", "0.01 XCP per 1,000-token lot": "0.01 XCP por lote de 1,000 tokens", @@ -430,7 +430,7 @@ "Counterparty": "Counterparty", "Counterparty market price in {currency}": "Precio de mercado de Counterparty en {currency}", "Counterparty's canonical unspendable burn address": "Dirección de quema canónica de Counterparty, imposible de gastar", - "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.": "La orden del DEX de Counterparty es la única primitiva de negociación: el emparejamiento pasa por el pool siempre que el precio del pool supere al libro de órdenes. Una {market} es una orden al precio de salida cotizado por el enrutador — se ejecuta de inmediato contra el pool y el libro al mejor precio. Una {limit} espera en el libro, y el pool la ejecuta automáticamente si su precio llega a cruzar el tuyo.", + "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.": "La orden del DEX de Counterparty es la única primitiva de negociación: el emparejamiento pasa por el pool siempre que el pool ofrezca un mejor precio que el libro de órdenes. Una {market} es una orden con la cantidad de salida cotizada por el enrutador — se ejecuta de inmediato contra el pool y el libro al mejor precio. Una {limit} espera en el libro, y el pool la ejecuta automáticamente si su precio llega a cruzar el tuyo.", "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.": "Los conteos usan direcciones únicas. Conservó, movió y vendió son resultados excluyentes para los acuñadores de cada lanzamiento graduado. Conservó significa que no vendió y mantiene un saldo relevante; movió, que no vendió y ya no tiene un saldo relevante; vendió, que hizo al menos una venta en el pool o en el libro de órdenes registrada por xcp.fun.", "Covered": "Cubierto", "Create": "Crear", @@ -501,7 +501,7 @@ "Every XCP-69 launch is identical: 100M supply, 69M public sale at 0.01 XCP per 1,000-token lot, 31M reserved for the liquidity pool, 10 XCP per-address cap, an on-chain pre-announcement before minting opens, and a 1,000-block window. There is no fine print to read because there is no fine print. The full parameter set lives on the {link} page.": "Todos los lanzamientos XCP-69 son idénticos: 100M de suministro, venta pública de 69M a 0.01 XCP por lote de 1,000 tokens, 31M reservados para el pool de liquidez, tope de 10 XCP por dirección, un anuncio previo en la cadena antes de que abra la acuñación y una ventana de 1,000 bloques. No hay letra chica que leer porque no hay letra chica. El conjunto completo de parámetros está en la página de {link}.", "Every XCP-69 mint, trade, burn, resting order and launch, newest first.": "Cada acuñación, operación, quema, orden en espera y lanzamiento de XCP-69, de más reciente a más antiguo.", "everyone dumps": "todos venden", - "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, what it costs (nothing), and how to integrate.": "Todo sobre XCP-69, en un solo lugar: cómo funcionan los lanzamientos, cómo funciona el precio, qué significa graduarse, cuánto cuesta (nada) y cómo integrarlo.", + "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, fees, and how to integrate.": "Todo sobre XCP-69, en un solo lugar: cómo funcionan los lanzamientos y los precios, qué significa graduarse, las comisiones y cómo integrarlo.", "Everything about XCP-69, in one place. The first half explains how launches work for anyone. The second half is for developers who want to read or build on the same on-chain data this site does.": "Todo sobre XCP-69, en un solo lugar. La primera mitad explica cómo funcionan los lanzamientos, para cualquiera. La segunda es para desarrolladores que quieran leer o construir sobre los mismos datos de la cadena que usa este sitio.", "Everything is curl-able. Launches and mints:": "Todo se puede consultar con curl. Lanzamientos y acuñaciones:", "Everything this site displays comes from public APIs. You can build your own launchpad, bot, or dashboard on the same data — nothing below requires our permission.": "Todo lo que muestra este sitio sale de APIs públicas. Puedes construir tu propia plataforma de lanzamiento, bot o panel con los mismos datos — nada de lo que sigue requiere nuestro permiso.", @@ -519,7 +519,7 @@ "failed {age} ago": "falló hace {age}", "Failed on": "Falló el", "fairmint pools": "pools de fairmint", - "Fairmint pools activated on 2026-08-05 and no XCP-69 launch exists yet. The first launch will be linked here as the reference token — the canonical record to validate your integration against: its fairminter row should pass the predicate above, its mint tape should show ≥{n} distinct addresses, and its pool row should show the burned LP at the unspendable address.": "Los pools de fairmint se activaron el 2026-08-05 y todavía no existe ningún lanzamiento XCP-69. El primero quedará enlazado aquí como token de referencia — el registro canónico contra el cual validar tu integración: su fila de fairminter debería pasar el predicado de arriba, su cinta de acuñaciones debería mostrar ≥{n} direcciones distintas y su fila de pool debería mostrar el LP quemado en la dirección no gastable.", + "Fairmint pools activated on 2026-08-05 and no XCP-69 launch exists yet. The first launch will be linked here as the reference token — the canonical record to validate your integration against: its fairminter row should pass the predicate above, its mint tape should show ≥{n} distinct addresses, and its pool row should show the burned LP at the unspendable address.": "Los pools de fairmint se activaron el 2026-08-05 y todavía no existe ningún lanzamiento XCP-69. El primero quedará enlazado aquí como token de referencia — el registro canónico contra el cual validar tu integración: su fila de fairminter debería pasar el predicado de arriba, su cinta de acuñaciones debería mostrar ≥{n} direcciones distintas y su fila de pool debería mostrar los tokens LP quemados en la dirección no gastable.", "Fairmint pools are live.": "Los pools de fairmint ya están activos.", "fairminter": "fairminter", "Fairminters": "Fairminters", @@ -548,8 +548,8 @@ "graduated": "graduado", "Graduated": "Graduado", "graduated coins": "monedas graduadas", - "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.": "Los tokens graduados se negocian contra un pool TOKEN/XCP de producto constante. El precio es simplemente la razón entre las reservas del pool; cada swap lo mueve. Una comisión fija de {fee} por swap se paga al propio pool — y como el LP está quemado, las comisiones engrosan la liquidez bloqueada en vez de pagarle a alguien.", - "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses, at which point the pool is created and its liquidity is burned. A launch that misses its target refunds every satoshi by consensus and does not count. {link}": "Graduarse significa agotarse: {xcp} XCP recaudados de al menos {n} direcciones distintas, momento en el que se crea el pool y su liquidez se quema. Un lanzamiento que no alcanza su meta reembolsa cada satoshi por consenso y no cuenta. {link}", + "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.": "Los tokens graduados se negocian contra un pool TOKEN/XCP de producto constante. El precio es simplemente la razón entre las reservas del pool; cada swap lo mueve. Una comisión fija de {fee} por swap se paga al propio pool — y como los tokens LP están quemados, las comisiones engrosan la liquidez bloqueada en vez de pagarle a alguien.", + "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses. The pool is created and its initial LP tokens go to an unspendable address, permanently locking that liquidity. A launch that misses its target returns the contributed XCP by consensus and does not count. {link}": "Graduarse significa agotar la venta: {xcp} XCP recaudados de al menos {n} direcciones distintas. Se crea el pool y sus tokens LP iniciales van a una dirección desde la que no se pueden gastar, bloqueando esa liquidez para siempre. Un lanzamiento que no alcanza su meta devuelve los XCP aportados por consenso y no cuenta. {link}", "Graduation": "Graduación", "Graveyard": "Cementerio", "Graveyard — xcp.fun": "Cementerio — xcp.fun", @@ -664,10 +664,10 @@ "Loading xcp.fun activity…": "Cargando actividad de xcp.fun…", "Loading…": "Cargando…", "locked forever": "bloqueado para siempre", - "locked forever, LP burned": "bloqueada para siempre, LP quemado", + "locked forever, LP burned": "bloqueada para siempre, tokens LP quemados", "log": "log", "Lots": "Lotes", - "LP fee": "Comisión de liquidez", + "LP fee": "Comisión LP", "LP minted (est.)": "LP acuñados (est.)", "LP positions": "Posiciones LP", "LP token {asset}": "Token LP {asset}", @@ -743,7 +743,7 @@ "New buyers": "Compradores nuevos", "Newest": "Más recientes", "Next →": "Siguiente →", - "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.": "No — una acuñación deja XCP en custodia contra el lanzamiento. Si se gradúa, te quedas con los tokens; si se reembolsa, recuperas cada satoshi. Lo único que una acuñación te cuesta de verdad es la comisión de Bitcoin — que es justo la parte que cubre este programa.", + "A mint locks your XCP until the launch resolves. If it graduates, your XCP funds the pool and you receive tokens, which can lose value. If it refunds, your XCP is returned. The Bitcoin network fee is paid in either case; rewards do not guarantee that every cost is covered.": "Una acuñación bloquea tus XCP hasta que se resuelva el lanzamiento. Si se gradúa, tus XCP financian el pool y recibes tokens, que pueden perder valor. Si hay reembolso, recuperas tus XCP. La comisión de red de Bitcoin se paga en ambos casos; las recompensas no garantizan cubrir todos los costos.", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.": "No — y ese es el punto. El protocolo de fairminters permite una variación enorme: sin soft cap, sin reembolsos, comisiones para el creador de hasta el 99%, preminados, sin pool. XCP-69 fija una sola configuración, y este sitio solo lista lanzamientos que coinciden exactamente con el estándar, verificados campo por campo contra el registro de la cadena. Aquí, cada lanzamiento se comporta igual y nunca tienes que auditar los parámetros tú mismo. Un fairminter que encuentres en cualquier otro lado no trae esa garantía — lee sus campos antes de acuñar.", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.": "No — es estructural, no prometida. El pool tiene que abrir a 69/31 del precio de acuñación porque esas son las únicas cantidades que existen, pero el piso baja a medida que la gente le vende. Nada impide que un token se negocie por debajo del precio de acuñación. Lo que el pool garantiza es una oferta de compra que nunca llega a cero, no una que te vaya a gustar.", "No active {give}/{get} pool liquidity. A resting order can still fill through the order book.": "No hay liquidez activa en el pool {give}/{get}. Una orden en espera todavía puede ejecutarse por el libro de órdenes.", @@ -817,8 +817,8 @@ "of the typical mint fee": "de la comisión típica de acuñación", "of which {amount} realized": "de los cuales {amount} realizados", "Of XCP minted": "De los XCP acuñados", - "Off-chain by default: the chain permanently carries the URL of the asset-info JSON, and the content behind it is curatable by the asset's current owner (wallet-signature gated). Taproot creators can instead inscribe the image on-chain as the permanent description — no off-chain dependency at all. Either way, nothing economic depends on hosted content.": "Fuera de la cadena por defecto: la cadena guarda para siempre la URL del JSON con la información del activo, y el contenido detrás de ella puede curarlo el dueño actual del activo (protegido por firma de billetera). Los creadores con Taproot pueden, en cambio, inscribir la imagen en la cadena como descripción permanente — sin ninguna dependencia externa. En cualquier caso, nada económico depende del contenido alojado.", - "On other launchpads, graduation is a threshold inside a live market: the token trades on a curve while everyone hopes it reaches the magic number. XCP-69 graduation is {binary} and happens before any trading exists. Sell out the 69M sale within 1,000 blocks and the launch graduates — pool seeded, LP burned, supply locked, trading live in the same block's resolution phase. Miss it and the launch never trades at all.": "En otras plataformas de lanzamiento, la graduación es un umbral dentro de un mercado en vivo: el token se negocia sobre una curva mientras todos esperan que llegue al número mágico. La graduación en XCP-69 es {binary} y ocurre antes de que exista cualquier negociación. Agota la venta de 69M en 1,000 bloques y el lanzamiento se gradúa — pool creado, LP quemado, suministro bloqueado, negociación activa en la fase de resolución del mismo bloque. Si no llega, el lanzamiento nunca se negocia.", + "Off-chain by default: the chain permanently carries the URL of the asset-info JSON, and the content behind it is curatable by the asset's current owner (wallet-signature gated). Taproot creators can instead inscribe the image on-chain as the permanent description — no off-chain dependency at all. Either way, nothing economic depends on hosted content.": "Fuera de la cadena por defecto: la cadena guarda para siempre la URL del JSON con la información del activo, y el contenido detrás de ella puede editarlo el dueño actual del activo (protegido por firma de billetera). Los creadores con Taproot pueden, en cambio, inscribir la imagen en la cadena como descripción permanente — sin ninguna dependencia externa. En cualquier caso, nada económico depende del contenido alojado.", + "On other launchpads, graduation is a threshold inside a live market: the token trades on a curve while everyone hopes it reaches the magic number. XCP-69 graduation is {binary} and happens before any trading exists. Sell out the 69M sale within 1,000 blocks and the launch graduates — pool seeded, LP burned, supply locked, trading live in the same block's resolution phase. Miss it and the launch never trades at all.": "En otras plataformas de lanzamiento, la graduación es un umbral dentro de un mercado en vivo: el token se negocia sobre una curva mientras todos esperan que llegue al número mágico. La graduación en XCP-69 es {binary} y ocurre antes de que exista cualquier negociación. Agota la venta de 69M en 1,000 bloques y el lanzamiento se gradúa — pool creado, tokens LP quemados, suministro bloqueado, negociación activa en la fase de resolución del mismo bloque. Si no llega, el lanzamiento nunca se negocia.", "on their Counterparty balance": "en su saldo de Counterparty", "on-chain asset name": "nombre del activo en la cadena", "on-chain since {year}": "en la cadena desde {year}", @@ -896,7 +896,7 @@ "Portfolio value over time, currently {value}": "Valor del portafolio a lo largo del tiempo, actualmente {value}", "Positions": "Posiciones", "Positions you fully exited, with the profit or loss actually taken. A dash means the focused mint-and-trade history does not explain the live zero balance, so no P&L is claimed. Basis is average-cost, so a partial sale realizes its share and leaves the rest with the tokens still held.": "Posiciones que cerraste por completo, con la ganancia o pérdida realmente obtenida. Un guion significa que el historial de acuñaciones y operaciones no explica el saldo cero actual, así que no se afirma ningún PnL. La base es el costo promedio, de modo que una venta parcial realiza su parte y deja el resto con los tokens que aún conservas.", - "Positions, open mints, history, and launches for {address} on xcp.fun.": "Posiciones, minteos abiertos, historial y lanzamientos de {address} en xcp.fun.", + "Positions, open mints, history, and launches for {address} on xcp.fun.": "Posiciones, acuñaciones abiertas, historial y lanzamientos de {address} en xcp.fun.", "Premine / commission": "Preminado / comisión", "Preparing inscription…": "Preparando la inscripción…", "Price": "Precio", @@ -1085,7 +1085,7 @@ "The XCP-69 standard": "El estándar XCP-69", "There is no fee-split table because there are no fees to split. Every satoshi of XCP paid by minters goes into the pool. The only costs anywhere in the system are Bitcoin transaction fees, the 0.5 XCP asset-name registration fee for named assets, and the protocol's pooldeposit gas fee debited at creation — costs paid to the network, not to us or to the creator.": "No hay tabla de reparto de comisiones porque no hay comisiones que repartir. Cada satoshi de XCP que pagan los acuñadores va al pool. Los únicos costos en todo el sistema son las comisiones de transacción de Bitcoin, la comisión de registro de nombre de 0.5 XCP para activos con nombre y la comisión de gas del pooldeposit del protocolo, debitada al crear — costos que se pagan a la red, no a nosotros ni al creador.", "This image is {kb} KB — inscribing caps at 385 KB. Use a smaller file or uncheck to host it instead.": "Esta imagen pesa {kb} KB — el tope para inscribir es 385 KB. Usa un archivo más pequeño o desmarca la opción para alojarla en su lugar.", - "This is the protocol's own swap math: constant product with a 50 bps fee that stays in the pool. Price is the ratio of the two reserves — one drains as the other fills, which is why the tick marks sit at center: both sides open worth {xcp} XCP each. The reserve approaches zero without ever arriving, and the LP is burned, so this liquidity can be traded against forever and withdrawn by no one — whatever XCP sits in the pool is out of circulation for good.": "Esta es la matemática de swap del propio protocolo: producto constante con una comisión de 50 bps que se queda en el pool. El precio es la razón entre las dos reservas — una se vacía mientras la otra se llena, por eso las marcas están al centro: ambos lados abren valiendo {xcp} XCP cada uno. La reserva se acerca a cero sin llegar nunca, y el LP está quemado, así que contra esta liquidez se puede negociar para siempre y nadie puede retirarla — el XCP que esté en el pool queda fuera de circulación definitivamente.", + "This is the protocol's own swap math: constant product with a 50 bps fee that stays in the pool. Price is the ratio of the two reserves — one drains as the other fills, which is why the tick marks sit at center: both sides open worth {xcp} XCP each. The reserve approaches zero without ever arriving, and the LP is burned, so this liquidity can be traded against forever and withdrawn by no one — whatever XCP sits in the pool is out of circulation for good.": "Esta es la matemática de swap del propio protocolo: producto constante con una comisión de 50 bps que se queda en el pool. El precio es la razón entre las dos reservas — una se vacía mientras la otra se llena, por eso las marcas están al centro: ambos lados abren valiendo {xcp} XCP cada uno. La reserva se acerca a cero sin llegar nunca, y los tokens LP están quemados, así que contra esta liquidez se puede negociar para siempre y nadie puede retirarla — el XCP que esté en el pool queda fuera de circulación definitivamente.", "This wallet hasn't launched anything yet.": "Esta billetera todavía no ha lanzado nada.", "This WEBP is animated. It will move on the site, but the announce channel can only post a still frame of it — Telegram has no way to play an animated WEBP. Upload the same art as a GIF and it moves there too.": "Este WEBP es animado. Se moverá en el sitio, pero el canal de anuncios solo puede publicar un cuadro fijo — Telegram no tiene forma de reproducir un WEBP animado. Sube el mismo arte como GIF y también se moverá allí.", "this window": "este periodo", @@ -1167,7 +1167,7 @@ "What is this?": "¿Qué es esto?", "What sellers did next": "Qué hicieron los vendedores después", "What sellers do next, where known dumpers are minting, and how much seller inventory remains.": "Qué hacen los vendedores después, dónde están acuñando los dumpers conocidos y cuánto inventario de vendedores queda.", - "What xcp.fun shows for this launch, and what the hosted metadata says if your on-chain description points here. The on-chain field itself is locked forever; everything behind it is yours to curate.": "Lo que xcp.fun muestra de este lanzamiento y lo que dicen los metadatos alojados si tu descripción en cadena apunta aquí. El campo en cadena queda bloqueado para siempre; todo lo que hay detrás lo curas tú.", + "What xcp.fun shows for this launch, and what the hosted metadata says if your on-chain description points here. The on-chain field itself is locked forever; everything behind it is yours to curate.": "Lo que xcp.fun muestra de este lanzamiento y lo que dicen los metadatos alojados si tu descripción en cadena apunta aquí. El campo en cadena queda bloqueado para siempre; puedes editar el contenido alojado detrás de él.", "When": "Cuándo", "When do I get paid?": "¿Cuándo me pagan?", "when the BTC payment confirms": "al confirmarse el pago en BTC", @@ -1191,7 +1191,7 @@ "XCP in the pool": "XCP en el pool", "XCP market · cached 15 min": "Mercado XCP · caché de 15 min", "XCP Price": "Precio de XCP", - "XCP reserve ÷ token reserve. Moves with every swap; there is no order book and no market maker.": "Reserva de XCP ÷ reserva de tokens. Se mueve con cada swap; no hay libro de órdenes ni creador de mercado.", + "Pool price is XCP reserve ÷ token reserve. Swaps change this ratio. Limit orders can also trade on Counterparty's order book.": "El precio del pool es la reserva de XCP ÷ la reserva de tokens. Los intercambios cambian esta proporción. Las órdenes límite también pueden ejecutarse en el libro de órdenes de Counterparty.", "XCP Rewards": "Recompensas XCP", "XCP Rewards — xcp.fun": "Recompensas XCP — xcp.fun", "XCP to receive": "XCP a recibir", diff --git a/apps/web/src/locales/es.status.json b/apps/web/src/locales/es.status.json index 8694a46d..92df3437 100644 --- a/apps/web/src/locales/es.status.json +++ b/apps/web/src/locales/es.status.json @@ -237,7 +237,7 @@ "Every minted token dumped in one trade — the pool still quotes a bid with {left} XCP left, and sellers exit at {mult}× mint ({paid}% of what they paid). Those {left} XCP can never be withdrawn: every launch is a permanent sink for XCP itself.", "Every route has a purchase pending in the mempool — check back in a few minutes.", "Everything about XCP-69, in one place. The first half explains how launches work for anyone. The second half is for developers who want to read or build on the same on-chain data this site does.", - "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, what it costs (nothing), and how to integrate.", + "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, fees, and how to integrate.", "Everything is curl-able. Launches and mints:", "Everything this site displays comes from public APIs. You can build your own launchpad, bot, or dashboard on the same data — nothing below requires our permission.", "Expiration", @@ -268,7 +268,7 @@ "Get XCP — xcp.fun", "Graduated", "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.", - "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses, at which point the pool is created and its liquidity is burned. A launch that misses its target refunds every satoshi by consensus and does not count. {link}", + "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses. The pool is created and its initial LP tokens go to an unspendable address, permanently locking that liquidity. A launch that misses its target returns the contributed XCP by consensus and does not count. {link}", "Graduation", "Graveyard", "Graveyard pages", @@ -455,7 +455,7 @@ "No trades yet.", "No value chart for {assets}: its recent transfer or liquidity history did not load completely, so we cannot know how many tokens this wallet held at each point. Current holdings and value are live and accurate.", "No value chart: its recent transfer or liquidity history did not load completely, so we cannot know how many tokens this wallet held at each point. Current holdings and value are live and accurate.", - "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.", + "A mint locks your XCP until the launch resolves. If it graduates, your XCP funds the pool and you receive tokens, which can lose value. If it refunds, your XCP is returned. The Bitcoin network fee is paid in either case; rewards do not guarantee that every cost is covered.", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.", "Nobody has minted yet. The first row here is available.", @@ -779,7 +779,7 @@ "XCP in pools", "XCP in the pool", "XCP market · cached 15 min", - "XCP reserve ÷ token reserve. Moves with every swap; there is no order book and no market maker.", + "Pool price is XCP reserve ÷ token reserve. Swaps change this ratio. Limit orders can also trade on Counterparty's order book.", "XCP to receive", "XCP to unload", "XCP-69 is a fixed-parameter token launch standard built on Counterparty {pools}. There is no factory contract, no admin key, and no platform custody at any point: the protocol {is} the platform. Every action — creating a launch, minting, swapping — is a transaction you sign in your own wallet and broadcast to Bitcoin. This site is an interface over public on-chain data; if it disappeared tomorrow, every launch, refund, and pool would keep working exactly as before.", diff --git a/apps/web/src/locales/fr.json b/apps/web/src/locales/fr.json index ebd9091b..9e4e4007 100644 --- a/apps/web/src/locales/fr.json +++ b/apps/web/src/locales/fr.json @@ -30,7 +30,7 @@ "{cap} · {liquidity} pool": "{cap} · pool de {liquidity}", "{cap} XCP · {supply} supply": "{cap} XCP · offre de {supply}", "{cap} XCP market cap": "Capitalisation de {cap} XCP", - "{claim} By default the chain permanently carries the URL of the asset-info JSON (via {lockDescription}) while the image and info are hosted off-chain, editable only by the asset's current on-chain owner via a wallet-signed message. If that hosting ever vanished, the token's economics — supply, pool, refunds — are untouched; only the artwork would be. Creators launching from a taproot wallet can remove the dependency entirely by inscribing the image on-chain as the permanent description.": "{claim} Par défaut, la chaîne conserve définitivement l'URL du JSON d'informations de l'actif (via {lockDescription}), tandis que l'image et les informations sont hébergées hors chaîne et ne peuvent être modifiées que par le propriétaire on-chain actuel de l'actif, avec un message signé depuis son portefeuille. Si cet hébergement disparaissait, l'économie du token — offre, pool, remboursements — resterait intacte ; seule l'illustration serait perdue. Les créateurs qui lancent depuis un portefeuille Taproot peuvent supprimer entièrement cette dépendance en inscrivant l'image on-chain comme description permanente.", + "{claim} By default the chain permanently carries the URL of the asset-info JSON (via {lockDescription}) while the image and info are hosted off-chain, editable only by the asset's current on-chain owner via a wallet-signed message. If that hosting ever vanished, the token's economics — supply, pool, refunds — are untouched; only the artwork would be. Creators launching from a taproot wallet can remove the dependency entirely by inscribing the image on-chain as the permanent description.": "{claim} Par défaut, la chaîne conserve définitivement l'URL du JSON d'informations de l'actif (via {lockDescription}), tandis que l'image et les informations sont hébergées hors chaîne et ne peuvent être modifiées que par le propriétaire on-chain actuel de l'actif, avec un message signé depuis son portefeuille. Si cet hébergement disparaissait, l'économie du token — offre, pool, remboursements — resterait intacte ; seule l'illustration serait affectée. Les créateurs qui lancent depuis un portefeuille Taproot peuvent supprimer entièrement cette dépendance en inscrivant l'image on-chain comme description permanente.", "{claim} If XCP's price moves during the ~7-day window, a refund makes you whole in XCP terms only.": "{claim} Si le prix du XCP bouge pendant la fenêtre d'environ 7 jours, le remboursement vous rend votre mise en XCP seulement.", "{claim} The cap is per address, not per person. It raises the cost of faking a crowd of {n}; it cannot prevent one.": "{claim} Le plafond est par adresse, pas par personne. Il renchérit la simulation d'une foule de {n} ; il ne peut pas l'empêcher.", "{claim} The pool floor decays as people sell into it. Nothing stops a token from trading below mint price.": "{claim} Le plancher du pool s'érode à mesure que les gens lui vendent. Rien n'empêche un token de s'échanger sous le prix de mint.", @@ -148,7 +148,7 @@ "{pct}% of pool": "{pct} % du pool", "{pct}% of the float dumped → sellers get {xcp} XCP back, exiting at {mult}× their mint price — below mint cost now.": "{pct} % du flottant dumpé → les vendeurs récupèrent {xcp} XCP et sortent à {mult}× leur prix de mint — désormais sous le coût du mint.", "{pct}% of the float dumped → sellers get {xcp} XCP back, exiting at {mult}× their mint price — the opening premium absorbing the exit.": "{pct} % du flottant dumpé → les vendeurs récupèrent {xcp} XCP et sortent à {mult}× leur prix de mint — la prime d'ouverture absorbe la sortie.", - "{pct}% of the LP is burned. The rest can be withdrawn by whoever holds it.": "{pct} % de la LP est brûlée. Le reste peut être retiré par qui la détient.", + "{pct}% of the LP is burned. The rest can be withdrawn by whoever holds it.": "{pct} % des tokens LP sont brûlés. Les détenteurs des tokens restants peuvent retirer la liquidité correspondante.", "{pct}% slippage authorizes a very unfavorable fill. The button will warn before swapping.": "Un slippage de {pct} % autorise une exécution très défavorable. Le bouton vous avertira avant l'échange.", "{period} {kind} from every fill on the pair — pool and order book. Volume beneath in XCP; {volume} XCP is the busiest {bucket} shown. Trend {trend} over this range.": "{kind} {period} à partir de chaque exécution sur la paire — pool et carnet d'ordres. Volume en dessous en XCP ; {volume} XCP est le {bucket} le plus actif affiché. Tendance {trend} sur cette plage.", "{recipe} Success and failure both end at fairminter status {closed}, so the pool row is the disambiguator:": "{recipe} Succès et échec se terminent tous deux avec le fairminter en statut {closed}, c'est donc la ligne du pool qui les distingue :", @@ -207,7 +207,7 @@ "⏳ Scheduled — announced on-chain, nothing minted yet.": "⏳ Programmé — annoncé on-chain, rien n'a encore été minté.", "◆ Pool": "◆ Pool", "✓ broadcast": "✓ diffusée", - "✓ Graduated — pool created with {xcp} XCP{usd} + 31M tokens, LP burned.": "✓ Gradué — pool créé avec {xcp} XCP{usd} + 31M de tokens, LP brûlée.", + "✓ Graduated — pool created with {xcp} XCP{usd} + 31M tokens, LP burned.": "✓ Gradué — pool créé avec {xcp} XCP{usd} + 31M de tokens, tokens LP brûlés.", "💀 RIP": "💀 RIP", "0.01 XCP / 1,000 · sells out or refunds": "0,01 XCP / 1 000 · tout est vendu ou tout est remboursé", "0.01 XCP per 1,000-token lot": "0,01 XCP par lot de 1 000 tokens", @@ -387,8 +387,8 @@ "Completed and confirming reward batches, linked to the transactions that sent them.": "Lots de récompenses terminés et en cours de confirmation, liés aux transactions qui les ont envoyés.", "Compose failed ({status})": "Échec de la composition ({status})", "Composing transactions": "Composer des transactions", - "composing…": "composition…", - "Composing…": "Composition…", + "composing…": "préparation de la transaction…", + "Composing…": "Préparation de la transaction…", "Confirm commit in wallet…": "Confirmez le commit dans votre portefeuille…", "confirm in wallet ({i} of {n})": "confirmez dans le portefeuille ({i} sur {n})", "Confirm in wallet…": "Confirmez dans votre portefeuille…", @@ -442,7 +442,7 @@ "Creators": "Créateurs", "credited": "crédité", "Currency": "Devise", - "Currently unloading:": "Déchargement en cours :", + "Currently unloading:": "Vente en cours :", "Custom": "Personnalisé", "custom block": "bloc personnalisé", "Custom liquidity slippage percent": "Pourcentage de slippage de liquidité personnalisé", @@ -485,7 +485,7 @@ "Editing is locked for this session": "La modification est verrouillée pour cette session", "Enter a multiple of {n}.": "Saisissez un multiple de {n}.", "Enter a price": "Saisissez un prix", - "Enter a value from {min} to {max}.": "Saisissez une valeur comprise entre {min} et {max}.", + "Enter a value from {min} to {max}.": "Saisissez une valeur de {min} à {max}.", "Enter an amount": "Saisissez un montant", "Enter opens the first result · Esc closes": "Entrée ouvre le premier résultat · Échap ferme", "escrowed": "sous séquestre", @@ -501,7 +501,7 @@ "Every XCP-69 launch is identical: 100M supply, 69M public sale at 0.01 XCP per 1,000-token lot, 31M reserved for the liquidity pool, 10 XCP per-address cap, an on-chain pre-announcement before minting opens, and a 1,000-block window. There is no fine print to read because there is no fine print. The full parameter set lives on the {link} page.": "Chaque lancement XCP-69 est identique : 100M d'offre, vente publique de 69M à 0,01 XCP par lot de 1 000 tokens, 31M réservés au pool de liquidité, plafond de 10 XCP par adresse, une annonce préalable on-chain avant l'ouverture du mint, et une fenêtre de 1 000 blocs. Il n'y a pas de petites lignes à lire parce qu'il n'y a pas de petites lignes. Le jeu complet de paramètres se trouve sur la page {link}.", "Every XCP-69 mint, trade, burn, resting order and launch, newest first.": "Chaque mint, échange, brûlage, ordre en attente et lancement XCP-69, du plus récent au plus ancien.", "everyone dumps": "tout le monde dump", - "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, what it costs (nothing), and how to integrate.": "Tout sur XCP-69, au même endroit : comment fonctionnent les lancements, comment se forme le prix, ce que signifie la graduation, ce que ça coûte (rien) et comment l'intégrer.", + "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, fees, and how to integrate.": "Tout sur XCP-69, au même endroit : comment fonctionnent les lancements et les prix, ce que signifie la graduation, les frais et comment l’intégrer.", "Everything about XCP-69, in one place. The first half explains how launches work for anyone. The second half is for developers who want to read or build on the same on-chain data this site does.": "Tout sur XCP-69, au même endroit. La première moitié explique à tout le monde comment fonctionnent les lancements. La seconde s'adresse aux développeurs qui veulent lire ou construire sur les mêmes données on-chain que ce site.", "Everything is curl-able. Launches and mints:": "Tout s'interroge avec curl. Lancements et mints :", "Everything this site displays comes from public APIs. You can build your own launchpad, bot, or dashboard on the same data — nothing below requires our permission.": "Tout ce que ce site affiche provient d'API publiques. Vous pouvez construire votre propre launchpad, bot ou tableau de bord sur les mêmes données — rien de ce qui suit ne requiert notre permission.", @@ -519,7 +519,7 @@ "failed {age} ago": "échoué il y a {age}", "Failed on": "Échoué le", "fairmint pools": "pools de fairmint", - "Fairmint pools activated on 2026-08-05 and no XCP-69 launch exists yet. The first launch will be linked here as the reference token — the canonical record to validate your integration against: its fairminter row should pass the predicate above, its mint tape should show ≥{n} distinct addresses, and its pool row should show the burned LP at the unspendable address.": "Les pools de fairmint ont été activés le 2026-08-05 et aucun lancement XCP-69 n'existe encore. Le premier lancement sera lié ici comme token de référence — l'enregistrement canonique pour valider votre intégration : sa ligne de fairminter doit passer le prédicat ci-dessus, son historique de mints doit montrer ≥{n} adresses distinctes, et sa ligne de pool doit montrer la LP brûlée à l'adresse indépensable.", + "Fairmint pools activated on 2026-08-05 and no XCP-69 launch exists yet. The first launch will be linked here as the reference token — the canonical record to validate your integration against: its fairminter row should pass the predicate above, its mint tape should show ≥{n} distinct addresses, and its pool row should show the burned LP at the unspendable address.": "Les pools de fairmint ont été activés le 2026-08-05 et aucun lancement XCP-69 n'existe encore. Le premier lancement sera lié ici comme token de référence — l'enregistrement canonique pour valider votre intégration : sa ligne de fairminter doit passer le prédicat ci-dessus, son historique de mints doit montrer ≥{n} adresses distinctes, et sa ligne de pool doit montrer les tokens LP brûlés à l'adresse indépensable.", "Fairmint pools are live.": "Les pools de fairmint sont actifs.", "fairminter": "fairminter", "Fairminters": "Fairminters", @@ -548,8 +548,8 @@ "graduated": "gradué", "Graduated": "Gradué", "graduated coins": "coins gradués", - "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.": "Les tokens gradués s'échangent contre un pool TOKEN/XCP à produit constant. Le prix est simplement le rapport des réserves du pool ; chaque swap le déplace. Des frais fixes de {fee} sur chaque swap sont versés au pool lui-même — et comme la LP est brûlée, les frais épaississent la liquidité verrouillée au lieu de payer qui que ce soit.", - "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses, at which point the pool is created and its liquidity is burned. A launch that misses its target refunds every satoshi by consensus and does not count. {link}": "Être gradué signifie être vendu en totalité : {xcp} XCP levés auprès d'au moins {n} adresses différentes, moment où le pool est créé et sa liquidité brûlée. Un lancement qui n'atteint pas son objectif rembourse chaque satoshi par consensus et ne compte pas. {link}", + "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.": "Les tokens gradués s'échangent contre un pool TOKEN/XCP à produit constant. Le prix est simplement le rapport des réserves du pool ; chaque swap le déplace. Des frais fixes de {fee} sur chaque swap sont versés au pool lui-même — et comme les tokens LP sont brûlés, les frais épaississent la liquidité verrouillée au lieu de payer qui que ce soit.", + "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses. The pool is created and its initial LP tokens go to an unspendable address, permanently locking that liquidity. A launch that misses its target returns the contributed XCP by consensus and does not count. {link}": "Être gradué signifie avoir tout vendu : {xcp} XCP levés auprès d’au moins {n} adresses distinctes. Le pool est créé et ses tokens LP initiaux vont à une adresse depuis laquelle ils ne peuvent pas être dépensés, verrouillant définitivement cette liquidité. Un lancement qui n’atteint pas son objectif restitue les XCP apportés par consensus et ne compte pas. {link}", "Graduation": "Graduation", "Graveyard": "Cimetière", "Graveyard — xcp.fun": "Cimetière — xcp.fun", @@ -646,7 +646,7 @@ "LIVE · {asset}": "LIVE · {asset}", "Load {amount} XCP via {address}": "Charger {amount} XCP via {address}", "Load more": "Charger plus", - "Load your wallet with XCP straight from Bitcoin — or unload it back. Minting costs XCP: 0.01 XCP per 1,000-token lot, 10 XCP for a max mint.": "Chargez votre portefeuille en XCP directement depuis Bitcoin — ou déchargez-le. Le mint coûte des XCP : 0,01 XCP par lot de 1 000 tokens, 10 XCP pour un mint maximal.", + "Load your wallet with XCP straight from Bitcoin — or unload it back. Minting costs XCP: 0.01 XCP per 1,000-token lot, 10 XCP for a max mint.": "Approvisionnez votre portefeuille en XCP en les achetant avec du BTC — ou revendez vos XCP contre du BTC. Le mint coûte des XCP : 0,01 XCP par lot de 1 000 tokens, 10 XCP pour un mint maximal.", "Loading activity…": "Chargement de l'activité…", "Loading historical XCP/USD": "Chargement de l'historique XCP/USD", "Loading history…": "Chargement de l'historique…", @@ -664,7 +664,7 @@ "Loading xcp.fun activity…": "Chargement de l'activité xcp.fun…", "Loading…": "Chargement…", "locked forever": "verrouillé pour toujours", - "locked forever, LP burned": "verrouillée pour toujours, LP brûlée", + "locked forever, LP burned": "verrouillée pour toujours, tokens LP brûlés", "log": "log", "Lots": "Lots", "LP fee": "Frais LP", @@ -690,8 +690,8 @@ "Message format": "Format du message", "Method and limits": "Méthode et limites", "Min LP · slippage {pct}%": "LP min. · slippage {pct} %", - "Min received": "Minimum reçu", - "Min received · slippage {pct}%": "Minimum reçu · slippage {pct} %", + "Min received": "Minimum à recevoir", + "Min received · slippage {pct}%": "Minimum à recevoir · slippage {pct} %", "Minimum 1 XCP": "Minimum 1 XCP", "Mint": "Mint", "Mint {amount} {asset}": "Minter {amount} {asset}", @@ -743,7 +743,7 @@ "New buyers": "Nouveaux acheteurs", "Newest": "Plus récents", "Next →": "Suivant →", - "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.": "Non — un mint place des XCP sous séquestre contre le lancement. S'il est gradué, vous gardez les tokens ; s'il est remboursé, vous récupérez chaque satoshi. La seule chose qu'un mint vous coûte réellement, ce sont les frais Bitcoin — et c'est justement la part que ce programme couvre.", + "A mint locks your XCP until the launch resolves. If it graduates, your XCP funds the pool and you receive tokens, which can lose value. If it refunds, your XCP is returned. The Bitcoin network fee is paid in either case; rewards do not guarantee that every cost is covered.": "Un mint bloque vos XCP jusqu’à l’issue du lancement. S’il est gradué, vos XCP financent le pool et vous recevez des tokens, qui peuvent perdre de la valeur. En cas de remboursement, vos XCP vous sont restitués. Les frais du réseau Bitcoin sont payés dans les deux cas ; les récompenses ne garantissent pas de couvrir tous les coûts.", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.": "Non — et c'est justement le point. Le protocole des fairminters permet d'énormes variations : pas de soft cap, pas de remboursement, des commissions créateur jusqu'à 99 %, des prémines, aucun pool. XCP-69 fixe une seule configuration, et ce site ne liste que les lancements qui correspondent exactement au standard, vérifiés champ par champ contre l'enregistrement on-chain. Ici, chaque lancement se comporte de la même manière et vous n'avez jamais à auditer les paramètres vous-même. Un fairminter croisé ailleurs n'offre aucune garantie de ce genre — lisez ses champs avant de minter.", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.": "Non — c'est structurel, pas promis. Le pool doit ouvrir à 69/31 du prix de mint parce que ce sont les seules quantités qui existent, mais le plancher s'érode à mesure que les gens lui vendent. Rien n'empêche un token de s'échanger sous le prix de mint. Ce que le pool garantit, c'est une offre d'achat qui n'atteint jamais zéro, pas une offre qui vous plaira.", "No active {give}/{get} pool liquidity. A resting order can still fill through the order book.": "Aucune liquidité active dans le pool {give}/{get}. Un ordre en attente peut encore être exécuté via le carnet d'ordres.", @@ -757,7 +757,7 @@ "No launches refunded in this window.": "Aucun lancement remboursé sur cette période.", "No launches yet.": "Pas encore de lancements.", "No live launches. Start one — it sells out or everyone gets refunded.": "Aucun lancement en cours. Lancez-en un — tout est vendu ou tout le monde est remboursé.", - "No LP in this pool": "Aucun LP dans ce pool", + "No LP in this pool": "Vous n’avez pas de LP dans ce pool", "No matches": "Aucun résultat", "No meaningful balance": "Pas de solde significatif", "No mints from that address. Search matches a full address exactly.": "Aucun mint depuis cette adresse. La recherche exige l'adresse complète et exacte.", @@ -818,7 +818,7 @@ "of which {amount} realized": "dont {amount} réalisés", "Of XCP minted": "Part des XCP mintés", "Off-chain by default: the chain permanently carries the URL of the asset-info JSON, and the content behind it is curatable by the asset's current owner (wallet-signature gated). Taproot creators can instead inscribe the image on-chain as the permanent description — no off-chain dependency at all. Either way, nothing economic depends on hosted content.": "Hors chaîne par défaut : la chaîne conserve définitivement l'URL du JSON d'informations de l'actif, et le contenu derrière peut être édité par le propriétaire actuel de l'actif (protégé par signature de portefeuille). Les créateurs Taproot peuvent, à la place, inscrire l'image on-chain comme description permanente — sans aucune dépendance hors chaîne. Dans les deux cas, rien d'économique ne dépend du contenu hébergé.", - "On other launchpads, graduation is a threshold inside a live market: the token trades on a curve while everyone hopes it reaches the magic number. XCP-69 graduation is {binary} and happens before any trading exists. Sell out the 69M sale within 1,000 blocks and the launch graduates — pool seeded, LP burned, supply locked, trading live in the same block's resolution phase. Miss it and the launch never trades at all.": "Sur les autres launchpads, la graduation est un seuil à l'intérieur d'un marché en direct : le token s'échange sur une courbe pendant que tout le monde espère qu'il atteigne le nombre magique. La graduation XCP-69 est {binary} et a lieu avant tout trading. Vendez les 69M en 1 000 blocs et le lancement est gradué — pool créé, LP brûlée, offre verrouillée, trading actif dans la phase de résolution du même bloc. Manquez-le et le lancement ne s'échange jamais.", + "On other launchpads, graduation is a threshold inside a live market: the token trades on a curve while everyone hopes it reaches the magic number. XCP-69 graduation is {binary} and happens before any trading exists. Sell out the 69M sale within 1,000 blocks and the launch graduates — pool seeded, LP burned, supply locked, trading live in the same block's resolution phase. Miss it and the launch never trades at all.": "Sur les autres launchpads, la graduation est un seuil à l'intérieur d'un marché en direct : le token s'échange sur une courbe pendant que tout le monde espère qu'il atteigne le nombre magique. La graduation XCP-69 est {binary} et a lieu avant tout trading. Vendez les 69M en 1 000 blocs et le lancement est gradué — pool créé, tokens LP brûlés, offre verrouillée, trading actif dans la phase de résolution du même bloc. Manquez-le et le lancement ne s'échange jamais.", "on their Counterparty balance": "sur leur solde Counterparty", "on-chain asset name": "nom d'actif on-chain", "on-chain since {year}": "on-chain depuis {year}", @@ -867,7 +867,7 @@ "Paste the profile URL or enter the handle.": "Collez l'URL du profil ou saisissez l'identifiant.", "Payouts": "Versements", "pending": "en attente", - "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.": "Les ordres en attente peuvent réduire la quantité disponible sous votre minimum à recevoir. S’ils sont confirmés en premier, la partie non exécutée reste ouverte jusqu’à expiration, puis est restituée. Vérifiez le glissement ou choisissez Auto.", + "Pending orders may leave less than your minimum received. If they confirm first, any unfilled amount stays open until expiry and is then returned. Review slippage or use Auto.": "Les ordres en attente peuvent réduire la quantité disponible sous votre minimum à recevoir. S’ils sont confirmés en premier, la partie non exécutée reste ouverte jusqu’à expiration, puis est restituée. Vérifiez le slippage ou choisissez Auto.", "People with xcp.fun open right now, including you. Several tabs from the same browser count once. Closing the tab removes you.": "Personnes ayant xcp.fun ouvert en ce moment, vous compris. Plusieurs onglets du même navigateur comptent une fois. Fermer l'onglet vous retire du compte.", "Per address": "Par adresse", "Per mint": "Par mint", @@ -965,7 +965,7 @@ "route no longer has enough left": "la route n'a plus assez de solde", "route price changed": "le prix de la route a changé", "Routes": "Routes", - "Routing includes miner fees — a deep route can beat a cheaper, shallower one.": "Le routage inclut les frais de minage — une route profonde peut battre une route moins chère mais moins fournie.", + "Routing includes miner fees — a deep route can beat a cheaper, shallower one.": "Le choix des routes tient compte des frais de minage. Une route avec plus de liquidité peut coûter moins au total qu’une autre au meilleur prix mais avec moins de liquidité.", "Sale closing": "Fermeture de la vente", "Save failed": "Échec de l'enregistrement", "Saved. Changes are live; other open pages may take a minute to refresh.": "Enregistré. Les modifications sont en ligne ; les autres pages ouvertes peuvent mettre une minute à s'actualiser.", @@ -1015,7 +1015,7 @@ "Size": "Taille", "Skipped: {reason}": "Ignorée : {reason}", "Slippage": "Slippage", - "Slippage tolerance of {pct}% permits receiving less than quoted.": "Une tolérance de glissement de {pct}% permet de recevoir moins que la quantité estimée.", + "Slippage tolerance of {pct}% permits receiving less than quoted.": "Une tolérance de slippage de {pct} % permet de recevoir moins que la quantité estimée.", "Slippage: {pct}%": "Slippage : {pct} %", "soft cap not reached": "soft cap not reached", "Sold": "Vente", @@ -1033,7 +1033,7 @@ "Stats are unavailable right now.": "Les statistiques sont indisponibles pour le moment.", "Status": "Statut", "Still holds": "Conserve encore", - "Stop unloading & reclaim": "Arrêter le déchargement et récupérer", + "Stop unloading & reclaim": "Arrêter la vente et récupérer", "Stress-test the exit race": "Testez la course à la sortie", "Supply": "Offre", "Support & terms": "Support et conditions", @@ -1085,7 +1085,7 @@ "The XCP-69 standard": "Le standard XCP-69", "There is no fee-split table because there are no fees to split. Every satoshi of XCP paid by minters goes into the pool. The only costs anywhere in the system are Bitcoin transaction fees, the 0.5 XCP asset-name registration fee for named assets, and the protocol's pooldeposit gas fee debited at creation — costs paid to the network, not to us or to the creator.": "Il n'y a pas de tableau de partage des frais parce qu'il n'y a pas de frais à partager. Chaque satoshi de XCP payé par les minteurs va dans le pool. Les seuls coûts dans tout le système sont les frais de transaction Bitcoin, les frais d'enregistrement de nom d'actif de 0,5 XCP pour les actifs nommés, et les frais de gas du pooldeposit du protocole, débités à la création — des coûts payés au réseau, pas à nous ni au créateur.", "This image is {kb} KB — inscribing caps at 385 KB. Use a smaller file or uncheck to host it instead.": "Cette image pèse {kb} Ko — l'inscription est plafonnée à 385 Ko. Utilisez un fichier plus petit ou décochez pour l'héberger à la place.", - "This is the protocol's own swap math: constant product with a 50 bps fee that stays in the pool. Price is the ratio of the two reserves — one drains as the other fills, which is why the tick marks sit at center: both sides open worth {xcp} XCP each. The reserve approaches zero without ever arriving, and the LP is burned, so this liquidity can be traded against forever and withdrawn by no one — whatever XCP sits in the pool is out of circulation for good.": "C'est la propre arithmétique de swap du protocole : produit constant avec des frais de 50 bps qui restent dans le pool. Le prix est le rapport des deux réserves — l'une se vide pendant que l'autre se remplit, c'est pourquoi les repères sont au centre : les deux côtés ouvrent à {xcp} XCP chacun. La réserve s'approche de zéro sans jamais l'atteindre, et la LP est brûlée, on peut donc trader contre cette liquidité pour toujours et personne ne peut la retirer — tout XCP qui se trouve dans le pool est définitivement hors circulation.", + "This is the protocol's own swap math: constant product with a 50 bps fee that stays in the pool. Price is the ratio of the two reserves — one drains as the other fills, which is why the tick marks sit at center: both sides open worth {xcp} XCP each. The reserve approaches zero without ever arriving, and the LP is burned, so this liquidity can be traded against forever and withdrawn by no one — whatever XCP sits in the pool is out of circulation for good.": "C'est la propre arithmétique de swap du protocole : produit constant avec des frais de 50 bps qui restent dans le pool. Le prix est le rapport des deux réserves — l'une se vide pendant que l'autre se remplit, c'est pourquoi les repères sont au centre : les deux côtés ouvrent à {xcp} XCP chacun. La réserve s'approche de zéro sans jamais l'atteindre, et les tokens LP sont brûlés, on peut donc trader contre cette liquidité pour toujours et personne ne peut la retirer — tout XCP qui se trouve dans le pool est définitivement hors circulation.", "This wallet hasn't launched anything yet.": "Ce portefeuille n'a encore rien lancé.", "This WEBP is animated. It will move on the site, but the announce channel can only post a still frame of it — Telegram has no way to play an animated WEBP. Upload the same art as a GIF and it moves there too.": "Ce WEBP est animé. Il bougera sur le site, mais le canal d'annonces ne peut en publier qu'une image fixe — Telegram n'a aucun moyen de lire un WEBP animé. Téléversez la même illustration en GIF et elle bougera là aussi.", "this window": "cette période", @@ -1107,7 +1107,7 @@ "Track record {score}/100 ({tier}) — observed on-chain reputation from the XCP.io explorer, not an endorsement.": "Historique {score}/100 ({tier}) — réputation observée on-chain d'après l'explorateur XCP.io, pas une recommandation.", "trade": "opération", "Trade from your connected wallet": "Opérez depuis votre portefeuille connecté", - "Trade history is unavailable. Please try again shortly.": "L’historique des transactions est indisponible. Réessayez dans un instant.", + "Trade history is unavailable. Please try again shortly.": "L’historique des opérations est indisponible. Réessayez dans un instant.", "Trade pressure through the pool": "Pression de trading à travers le pool", "Trade volume": "Volume d'échanges", "Trade.": "Trader.", @@ -1142,11 +1142,11 @@ "Upcoming": "À venir", "Upload failed": "Échec du téléversement", "Upload token image": "Téléverser l'image du token", - "Use digits and a dot, with up to {n} decimal places.": "Utilisez des chiffres et un point, avec au maximum {n} décimales.", - "Use whole numbers only.": "Utilisez uniquement des nombres entiers.", + "Use digits and a dot, with up to {n} decimal places.": "Utilisez 0-9 et un point ; {n} décimales max.", + "Use whole numbers only.": "Utilisez des nombres entiers.", "Value": "Valeur", "Value and underlying amounts are what withdrawing the LP tokens now would return. XCP value is available when XCP is one side of the pair. LP profit and loss is not estimated.": "La valeur et les montants sous-jacents sont ce que rendrait le retrait des tokens LP maintenant. La valeur en XCP est disponible quand XCP est l'un des côtés de la paire. Le gain ou la perte sur la LP n'est pas estimé.", - "Vends": "Vend", + "Vends": "Ventes disponibles", "Venue": "Marché", "Venue / time": "Lieu / heure", "Verdict pending": "Verdict en attente", @@ -1191,11 +1191,11 @@ "XCP in the pool": "XCP dans le pool", "XCP market · cached 15 min": "Marché XCP · cache 15 min", "XCP Price": "Prix de XCP", - "XCP reserve ÷ token reserve. Moves with every swap; there is no order book and no market maker.": "Réserve de XCP ÷ réserve de tokens. Bouge à chaque swap ; il n'y a ni carnet d'ordres ni teneur de marché.", + "Pool price is XCP reserve ÷ token reserve. Swaps change this ratio. Limit orders can also trade on Counterparty's order book.": "Le prix du pool est la réserve de XCP ÷ la réserve de tokens. Les swaps modifient ce rapport. Les ordres limite peuvent aussi être exécutés dans le carnet d’ordres de Counterparty.", "XCP Rewards": "Récompenses XCP", "XCP Rewards — xcp.fun": "Récompenses XCP — xcp.fun", "XCP to receive": "XCP à recevoir", - "XCP to unload": "XCP à décharger", + "XCP to unload": "XCP à vendre", "XCP-69 is a fixed-parameter token launch standard built on Counterparty {pools}. There is no factory contract, no admin key, and no platform custody at any point: the protocol {is} the platform. Every action — creating a launch, minting, swapping — is a transaction you sign in your own wallet and broadcast to Bitcoin. This site is an interface over public on-chain data; if it disappeared tomorrow, every launch, refund, and pool would keep working exactly as before.": "XCP-69 est un standard de lancement de tokens à paramètres fixes, construit sur les {pools} de Counterparty. Il n'y a ni contrat usine, ni clé d'administration, ni garde par la plateforme à aucun moment : le protocole {is} la plateforme. Chaque action — créer un lancement, minter, swapper — est une transaction que vous signez dans votre propre portefeuille et diffusez sur Bitcoin. Ce site est une interface sur des données publiques on-chain ; s'il disparaissait demain, chaque lancement, remboursement et pool continuerait de fonctionner exactement comme avant.", "XCP-69 launches and mints sitting in the Bitcoin mempool, not yet confirmed.": "Lancements et mints XCP-69 dans la mempool Bitcoin, pas encore confirmés.", "XCP-69 launches by phase, and what has actually been minted.": "Lancements XCP-69 par phase, et ce qui a réellement été minté.", diff --git a/apps/web/src/locales/fr.status.json b/apps/web/src/locales/fr.status.json index 8694a46d..92df3437 100644 --- a/apps/web/src/locales/fr.status.json +++ b/apps/web/src/locales/fr.status.json @@ -237,7 +237,7 @@ "Every minted token dumped in one trade — the pool still quotes a bid with {left} XCP left, and sellers exit at {mult}× mint ({paid}% of what they paid). Those {left} XCP can never be withdrawn: every launch is a permanent sink for XCP itself.", "Every route has a purchase pending in the mempool — check back in a few minutes.", "Everything about XCP-69, in one place. The first half explains how launches work for anyone. The second half is for developers who want to read or build on the same on-chain data this site does.", - "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, what it costs (nothing), and how to integrate.", + "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, fees, and how to integrate.", "Everything is curl-able. Launches and mints:", "Everything this site displays comes from public APIs. You can build your own launchpad, bot, or dashboard on the same data — nothing below requires our permission.", "Expiration", @@ -268,7 +268,7 @@ "Get XCP — xcp.fun", "Graduated", "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.", - "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses, at which point the pool is created and its liquidity is burned. A launch that misses its target refunds every satoshi by consensus and does not count. {link}", + "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses. The pool is created and its initial LP tokens go to an unspendable address, permanently locking that liquidity. A launch that misses its target returns the contributed XCP by consensus and does not count. {link}", "Graduation", "Graveyard", "Graveyard pages", @@ -455,7 +455,7 @@ "No trades yet.", "No value chart for {assets}: its recent transfer or liquidity history did not load completely, so we cannot know how many tokens this wallet held at each point. Current holdings and value are live and accurate.", "No value chart: its recent transfer or liquidity history did not load completely, so we cannot know how many tokens this wallet held at each point. Current holdings and value are live and accurate.", - "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.", + "A mint locks your XCP until the launch resolves. If it graduates, your XCP funds the pool and you receive tokens, which can lose value. If it refunds, your XCP is returned. The Bitcoin network fee is paid in either case; rewards do not guarantee that every cost is covered.", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.", "Nobody has minted yet. The first row here is available.", @@ -779,7 +779,7 @@ "XCP in pools", "XCP in the pool", "XCP market · cached 15 min", - "XCP reserve ÷ token reserve. Moves with every swap; there is no order book and no market maker.", + "Pool price is XCP reserve ÷ token reserve. Swaps change this ratio. Limit orders can also trade on Counterparty's order book.", "XCP to receive", "XCP to unload", "XCP-69 is a fixed-parameter token launch standard built on Counterparty {pools}. There is no factory contract, no admin key, and no platform custody at any point: the protocol {is} the platform. Every action — creating a launch, minting, swapping — is a transaction you sign in your own wallet and broadcast to Bitcoin. This site is an interface over public on-chain data; if it disappeared tomorrow, every launch, refund, and pool would keep working exactly as before.", diff --git a/apps/web/src/locales/pt.json b/apps/web/src/locales/pt.json index a0d757dc..a85eba8b 100644 --- a/apps/web/src/locales/pt.json +++ b/apps/web/src/locales/pt.json @@ -148,7 +148,7 @@ "{pct}% of pool": "{pct}% do pool", "{pct}% of the float dumped → sellers get {xcp} XCP back, exiting at {mult}× their mint price — below mint cost now.": "{pct}% do float vendido de uma vez → os vendedores recuperam {xcp} XCP, saindo a {mult}× o preço de mint — agora abaixo do custo de mint.", "{pct}% of the float dumped → sellers get {xcp} XCP back, exiting at {mult}× their mint price — the opening premium absorbing the exit.": "{pct}% do float vendido de uma vez → os vendedores recuperam {xcp} XCP, saindo a {mult}× o preço de mint — o prêmio de abertura absorve a saída.", - "{pct}% of the LP is burned. The rest can be withdrawn by whoever holds it.": "{pct}% do LP está queimado. O resto pode ser retirado por quem o tiver.", + "{pct}% of the LP is burned. The rest can be withdrawn by whoever holds it.": "{pct}% dos tokens LP estão queimados. Quem detém o restante pode retirar a liquidez correspondente.", "{pct}% slippage authorizes a very unfavorable fill. The button will warn before swapping.": "Um slippage de {pct}% autoriza uma execução muito desfavorável. O botão vai avisar antes de trocar.", "{period} {kind} from every fill on the pair — pool and order book. Volume beneath in XCP; {volume} XCP is the busiest {bucket} shown. Trend {trend} over this range.": "{kind} de {period} com cada execução do par — pool e livro de ofertas. Volume abaixo em XCP; {volume} XCP é o {bucket} mais movimentado exibido. Tendência {trend} neste intervalo.", "{recipe} Success and failure both end at fairminter status {closed}, so the pool row is the disambiguator:": "{recipe} Sucesso e fracasso terminam ambos com o fairminter no status {closed}, então é a linha do pool que os distingue:", @@ -207,7 +207,7 @@ "⏳ Scheduled — announced on-chain, nothing minted yet.": "⏳ Agendado — anunciado on-chain, nada mintado ainda.", "◆ Pool": "◆ Pool", "✓ broadcast": "✓ transmitida", - "✓ Graduated — pool created with {xcp} XCP{usd} + 31M tokens, LP burned.": "✓ Graduado — pool criado com {xcp} XCP{usd} + 31M de tokens, LP queimado.", + "✓ Graduated — pool created with {xcp} XCP{usd} + 31M tokens, LP burned.": "✓ Graduado — pool criado com {xcp} XCP{usd} + 31M de tokens, tokens LP queimados.", "💀 RIP": "💀 RIP", "0.01 XCP / 1,000 · sells out or refunds": "0,01 XCP / 1.000 · esgota ou reembolsa", "0.01 XCP per 1,000-token lot": "0,01 XCP por lote de 1.000 tokens", @@ -299,7 +299,7 @@ "Bid": "Compra", "binary": "binária", "Bitcoin": "Bitcoin", - "Bitcoin fee rate in sats per vbyte": "Taxa do Bitcoin em sats por vbyte", + "Bitcoin fee rate in sats per vbyte": "Taxa de rede do Bitcoin em sats por vbyte", "Bitcoin market price in {currency}": "Preço de mercado do Bitcoin em {currency}", "Bitcoin Price": "Preço do Bitcoin", "Bitcoin tx fee": "Taxa de mineração do Bitcoin", @@ -387,8 +387,8 @@ "Completed and confirming reward batches, linked to the transactions that sent them.": "Lotes de recompensas concluídos e em confirmação, vinculados às transações que os enviaram.", "Compose failed ({status})": "Falha ao compor ({status})", "Composing transactions": "Compondo transações", - "composing…": "compondo…", - "Composing…": "Montando…", + "composing…": "preparando transação…", + "Composing…": "Preparando transação…", "Confirm commit in wallet…": "Confirme o commit na carteira…", "confirm in wallet ({i} of {n})": "confirme na carteira ({i} de {n})", "Confirm in wallet…": "Confirme na carteira…", @@ -430,7 +430,7 @@ "Counterparty": "Counterparty", "Counterparty market price in {currency}": "Preço de mercado do Counterparty em {currency}", "Counterparty's canonical unspendable burn address": "Endereço de queima canônico e não gastável do Counterparty", - "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.": "A ordem da DEX do Counterparty é a única primitiva de negociação: o casamento passa pelo pool sempre que o preço do pool supera o livro de ofertas. Uma {market} é uma ordem no valor de saída cotado pelo roteador — ela executa na hora contra o pool e o livro ao melhor preço. Uma {limit} fica no livro, e o pool a executa automaticamente se o preço dele um dia cruzar o seu.", + "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.": "A ordem da DEX do Counterparty é a única primitiva de negociação: o casamento passa pelo pool sempre que ele oferece um preço melhor que o livro de ofertas. Uma {market} é uma ordem no valor de saída cotado pelo roteador — ela executa na hora contra o pool e o livro ao melhor preço. Uma {limit} fica no livro, e o pool a executa automaticamente se o preço dele um dia cruzar o seu.", "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.": "As contagens usam endereços únicos. Manteve, moveu e vendeu são resultados excludentes para os minters de cada lançamento graduado. Manteve significa que não vendeu e tem um saldo relevante; moveu, que não vendeu e não tem mais um saldo relevante; vendeu, que fez pelo menos uma venda no pool ou no livro de ofertas registrada pelo xcp.fun.", "Covered": "Coberto", "Create": "Criar", @@ -442,7 +442,7 @@ "Creators": "Criadores", "credited": "creditado", "Currency": "Moeda", - "Currently unloading:": "Descarregando agora:", + "Currently unloading:": "Venda em andamento:", "Custom": "Personalizado", "custom block": "bloco personalizado", "Custom liquidity slippage percent": "Porcentagem de slippage de liquidez personalizada", @@ -459,7 +459,7 @@ "dev": "dev", "Did not mint again": "Não mintaram de novo", "Disconnect": "Desconectar", - "Dismiss": "Dispensar", + "Dismiss": "Fechar", "Dispense": "Dispenser", "Dispense settings": "Configurações do dispenser", "Dispensers · cheapest first": "Dispensers · o mais barato primeiro", @@ -501,7 +501,7 @@ "Every XCP-69 launch is identical: 100M supply, 69M public sale at 0.01 XCP per 1,000-token lot, 31M reserved for the liquidity pool, 10 XCP per-address cap, an on-chain pre-announcement before minting opens, and a 1,000-block window. There is no fine print to read because there is no fine print. The full parameter set lives on the {link} page.": "Todo lançamento XCP-69 é idêntico: 100M de fornecimento, venda pública de 69M a 0,01 XCP por lote de 1.000 tokens, 31M reservados para o pool de liquidez, limite de 10 XCP por endereço, um anúncio prévio on-chain antes de o mint abrir e uma janela de 1.000 blocos. Não há letras miúdas para ler porque não há letras miúdas. O conjunto completo de parâmetros está na página de {link}.", "Every XCP-69 mint, trade, burn, resting order and launch, newest first.": "Cada mint, negociação, queima, ordem em espera e lançamento do XCP-69, do mais recente ao mais antigo.", "everyone dumps": "todo mundo vende", - "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, what it costs (nothing), and how to integrate.": "Tudo sobre o XCP-69, em um só lugar: como os lançamentos funcionam, como funciona o preço, o que significa graduar, quanto custa (nada) e como integrar.", + "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, fees, and how to integrate.": "Tudo sobre o XCP-69, em um só lugar: como funcionam os lançamentos e os preços, o que significa graduar, as taxas e como integrar.", "Everything about XCP-69, in one place. The first half explains how launches work for anyone. The second half is for developers who want to read or build on the same on-chain data this site does.": "Tudo sobre o XCP-69, em um só lugar. A primeira metade explica como os lançamentos funcionam, para qualquer pessoa. A segunda é para desenvolvedores que querem ler ou construir sobre os mesmos dados on-chain que este site usa.", "Everything is curl-able. Launches and mints:": "Tudo dá para consultar com curl. Lançamentos e mints:", "Everything this site displays comes from public APIs. You can build your own launchpad, bot, or dashboard on the same data — nothing below requires our permission.": "Tudo o que este site exibe vem de APIs públicas. Você pode construir sua própria launchpad, bot ou painel com os mesmos dados — nada abaixo precisa da nossa permissão.", @@ -519,7 +519,7 @@ "failed {age} ago": "falhou há {age}", "Failed on": "Falhou em", "fairmint pools": "pools de fairmint", - "Fairmint pools activated on 2026-08-05 and no XCP-69 launch exists yet. The first launch will be linked here as the reference token — the canonical record to validate your integration against: its fairminter row should pass the predicate above, its mint tape should show ≥{n} distinct addresses, and its pool row should show the burned LP at the unspendable address.": "Os pools de fairmint foram ativados em 2026-08-05 e ainda não existe nenhum lançamento XCP-69. O primeiro será linkado aqui como token de referência — o registro canônico para validar sua integração: a linha do fairminter deve passar no predicado acima, a fita de mints deve mostrar ≥{n} endereços distintos e a linha do pool deve mostrar o LP queimado no endereço não gastável.", + "Fairmint pools activated on 2026-08-05 and no XCP-69 launch exists yet. The first launch will be linked here as the reference token — the canonical record to validate your integration against: its fairminter row should pass the predicate above, its mint tape should show ≥{n} distinct addresses, and its pool row should show the burned LP at the unspendable address.": "Os pools de fairmint foram ativados em 2026-08-05 e ainda não existe nenhum lançamento XCP-69. O primeiro será linkado aqui como token de referência — o registro canônico para validar sua integração: a linha do fairminter deve passar no predicado acima, a fita de mints deve mostrar ≥{n} endereços distintos e a linha do pool deve mostrar os tokens LP queimados no endereço não gastável.", "Fairmint pools are live.": "Os pools de fairmint estão no ar.", "fairminter": "fairminter", "Fairminters": "Fairminters", @@ -548,8 +548,8 @@ "graduated": "graduado", "Graduated": "Graduado", "graduated coins": "moedas graduadas", - "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.": "Tokens graduados são negociados contra um pool TOKEN/XCP de produto constante. O preço é simplesmente a razão entre as reservas do pool; cada swap o move. Uma taxa fixa de {fee} em cada swap é paga ao próprio pool — e como o LP está queimado, as taxas engrossam a liquidez travada em vez de pagar alguém.", - "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses, at which point the pool is created and its liquidity is burned. A launch that misses its target refunds every satoshi by consensus and does not count. {link}": "Graduar significa esgotar: {xcp} XCP levantados de pelo menos {n} endereços diferentes, momento em que o pool é criado e sua liquidez é queimada. Um lançamento que não atinge a meta reembolsa cada satoshi por consenso e não conta. {link}", + "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.": "Tokens graduados são negociados contra um pool TOKEN/XCP de produto constante. O preço é simplesmente a razão entre as reservas do pool; cada swap o move. Uma taxa fixa de {fee} em cada swap é paga ao próprio pool — e como os tokens LP estão queimados, as taxas engrossam a liquidez travada em vez de pagar alguém.", + "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses. The pool is created and its initial LP tokens go to an unspendable address, permanently locking that liquidity. A launch that misses its target returns the contributed XCP by consensus and does not count. {link}": "Graduar significa esgotar a venda: {xcp} XCP arrecadados de pelo menos {n} endereços distintos. O pool é criado e seus tokens LP iniciais vão para um endereço de onde não podem ser gastos, travando essa liquidez para sempre. Um lançamento que não atinge a meta devolve os XCP contribuídos por consenso e não conta. {link}", "Graduation": "Graduação", "Graveyard": "Cemitério", "Graveyard — xcp.fun": "Cemitério — xcp.fun", @@ -646,7 +646,7 @@ "LIVE · {asset}": "LIVE · {asset}", "Load {amount} XCP via {address}": "Carregar {amount} XCP via {address}", "Load more": "Carregar mais", - "Load your wallet with XCP straight from Bitcoin — or unload it back. Minting costs XCP: 0.01 XCP per 1,000-token lot, 10 XCP for a max mint.": "Carregue sua carteira com XCP direto do Bitcoin — ou descarregue de volta. Mintar custa XCP: 0,01 XCP por lote de 1.000 tokens, 10 XCP para um mint máximo.", + "Load your wallet with XCP straight from Bitcoin — or unload it back. Minting costs XCP: 0.01 XCP per 1,000-token lot, 10 XCP for a max mint.": "Compre XCP com BTC para abastecer sua carteira — ou venda XCP por BTC. Mintar custa XCP: 0,01 XCP por lote de 1.000 tokens, 10 XCP para um mint máximo.", "Loading activity…": "Carregando atividade…", "Loading historical XCP/USD": "Carregando histórico XCP/USD", "Loading history…": "Carregando histórico…", @@ -664,10 +664,10 @@ "Loading xcp.fun activity…": "Carregando atividade do xcp.fun…", "Loading…": "Carregando…", "locked forever": "bloqueado para sempre", - "locked forever, LP burned": "travada para sempre, LP queimado", + "locked forever, LP burned": "travada para sempre, tokens LP queimados", "log": "log", "Lots": "Lotes", - "LP fee": "Taxa do LP", + "LP fee": "Taxa LP", "LP minted (est.)": "LP mintados (est.)", "LP positions": "Posições LP", "LP token {asset}": "Token LP {asset}", @@ -691,7 +691,7 @@ "Method and limits": "Método e limites", "Min LP · slippage {pct}%": "LP mín. · slippage {pct}%", "Min received": "Mínimo a receber", - "Min received · slippage {pct}%": "Mínimo recebido · slippage {pct}%", + "Min received · slippage {pct}%": "Mínimo a receber · slippage {pct}%", "Minimum 1 XCP": "Mínimo 1 XCP", "Mint": "Mint", "Mint {amount} {asset}": "Mintar {amount} {asset}", @@ -743,7 +743,7 @@ "New buyers": "Novos compradores", "Newest": "Mais recentes", "Next →": "Próxima →", - "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.": "Não — um mint deixa XCP em custódia contra o lançamento. Se ele se graduar, você fica com os tokens; se for reembolsado, você recebe cada satoshi de volta. A única coisa que um mint realmente custa é a taxa do Bitcoin — que é justamente a parte que este programa cobre.", + "A mint locks your XCP until the launch resolves. If it graduates, your XCP funds the pool and you receive tokens, which can lose value. If it refunds, your XCP is returned. The Bitcoin network fee is paid in either case; rewards do not guarantee that every cost is covered.": "Um mint bloqueia seus XCP até o lançamento ser resolvido. Se ele se graduar, seus XCP financiam o pool e você recebe tokens, que podem perder valor. Se houver reembolso, seus XCP são devolvidos. A taxa de rede do Bitcoin é paga nos dois casos; as recompensas não garantem cobrir todos os custos.", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.": "Não — e esse é o ponto. O protocolo de fairminters permite uma variação enorme: sem soft cap, sem reembolsos, comissões ao criador de até 99%, pré-mineração, nenhum pool. O XCP-69 fixa uma única configuração, e este site só lista lançamentos que batem exatamente com o padrão, verificados campo a campo contra o registro on-chain. Aqui, todo lançamento se comporta igual e você nunca precisa auditar os parâmetros. Um fairminter que você encontrar em qualquer outro lugar não traz essa garantia — leia os campos dele antes de mintar.", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.": "Não — é estrutural, não prometido. O pool tem que abrir a 69/31 do preço de mint porque essas são as únicas quantidades que existem, mas o piso cai conforme as pessoas vendem nele. Nada impede que um token seja negociado abaixo do mint. O que o pool garante é uma oferta de compra que nunca chega a zero, não uma oferta de que você vai gostar.", "No active {give}/{get} pool liquidity. A resting order can still fill through the order book.": "Não há liquidez ativa no pool {give}/{get}. Uma ordem em espera ainda pode ser executada pelo livro de ofertas.", @@ -817,8 +817,8 @@ "of the typical mint fee": "da taxa típica de um mint", "of which {amount} realized": "dos quais {amount} realizados", "Of XCP minted": "Dos XCP mintados", - "Off-chain by default: the chain permanently carries the URL of the asset-info JSON, and the content behind it is curatable by the asset's current owner (wallet-signature gated). Taproot creators can instead inscribe the image on-chain as the permanent description — no off-chain dependency at all. Either way, nothing economic depends on hosted content.": "Off-chain por padrão: a chain guarda para sempre a URL do JSON com as informações do ativo, e o conteúdo por trás dela pode ser curado pelo dono atual do ativo (protegido por assinatura da carteira). Criadores com Taproot podem, em vez disso, inscrever a imagem on-chain como descrição permanente — sem nenhuma dependência off-chain. De qualquer forma, nada econômico depende de conteúdo hospedado.", - "On other launchpads, graduation is a threshold inside a live market: the token trades on a curve while everyone hopes it reaches the magic number. XCP-69 graduation is {binary} and happens before any trading exists. Sell out the 69M sale within 1,000 blocks and the launch graduates — pool seeded, LP burned, supply locked, trading live in the same block's resolution phase. Miss it and the launch never trades at all.": "Em outras launchpads, a graduação é um limiar dentro de um mercado ao vivo: o token é negociado numa curva enquanto todo mundo torce para ele chegar ao número mágico. A graduação no XCP-69 é {binary} e acontece antes de existir qualquer negociação. Esgote a venda de 69M em 1.000 blocos e o lançamento gradua — pool criado, LP queimado, fornecimento travado, negociação ativa na fase de resolução do mesmo bloco. Não chegou lá, o lançamento nunca é negociado.", + "Off-chain by default: the chain permanently carries the URL of the asset-info JSON, and the content behind it is curatable by the asset's current owner (wallet-signature gated). Taproot creators can instead inscribe the image on-chain as the permanent description — no off-chain dependency at all. Either way, nothing economic depends on hosted content.": "Off-chain por padrão: a chain guarda para sempre a URL do JSON com as informações do ativo, e o conteúdo por trás dela pode ser editado pelo dono atual do ativo (protegido por assinatura da carteira). Criadores com Taproot podem, em vez disso, inscrever a imagem on-chain como descrição permanente — sem nenhuma dependência off-chain. De qualquer forma, nada econômico depende de conteúdo hospedado.", + "On other launchpads, graduation is a threshold inside a live market: the token trades on a curve while everyone hopes it reaches the magic number. XCP-69 graduation is {binary} and happens before any trading exists. Sell out the 69M sale within 1,000 blocks and the launch graduates — pool seeded, LP burned, supply locked, trading live in the same block's resolution phase. Miss it and the launch never trades at all.": "Em outras launchpads, a graduação é um limiar dentro de um mercado ao vivo: o token é negociado numa curva enquanto todo mundo torce para ele chegar ao número mágico. A graduação no XCP-69 é {binary} e acontece antes de existir qualquer negociação. Esgote a venda de 69M em 1.000 blocos e o lançamento gradua — pool criado, tokens LP queimados, fornecimento travado, negociação ativa na fase de resolução do mesmo bloco. Não chegou lá, o lançamento nunca é negociado.", "on their Counterparty balance": "no saldo Counterparty deles", "on-chain asset name": "nome do ativo on-chain", "on-chain since {year}": "on-chain desde {year}", @@ -965,7 +965,7 @@ "route no longer has enough left": "a rota não tem mais o suficiente", "route price changed": "o preço da rota mudou", "Routes": "Rotas", - "Routing includes miner fees — a deep route can beat a cheaper, shallower one.": "O roteamento inclui as taxas de mineração — uma rota funda pode ganhar de outra mais barata, porém mais rasa.", + "Routing includes miner fees — a deep route can beat a cheaper, shallower one.": "O roteamento inclui as taxas de mineração. Uma rota com mais liquidez pode ter um custo total menor que outra com preço melhor, mas menos liquidez.", "Sale closing": "Fechando a venda", "Save failed": "Falha ao salvar", "Saved. Changes are live; other open pages may take a minute to refresh.": "Salvo. As mudanças já estão no ar; outras páginas abertas podem levar um minuto para atualizar.", @@ -1033,7 +1033,7 @@ "Stats are unavailable right now.": "As estatísticas não estão disponíveis neste momento.", "Status": "Status", "Still holds": "Ainda mantém", - "Stop unloading & reclaim": "Parar de descarregar e recuperar", + "Stop unloading & reclaim": "Parar de vender e recuperar", "Stress-test the exit race": "Teste a corrida de saída", "Supply": "Fornecimento", "Support & terms": "Suporte e termos", @@ -1085,7 +1085,7 @@ "The XCP-69 standard": "O padrão XCP-69", "There is no fee-split table because there are no fees to split. Every satoshi of XCP paid by minters goes into the pool. The only costs anywhere in the system are Bitcoin transaction fees, the 0.5 XCP asset-name registration fee for named assets, and the protocol's pooldeposit gas fee debited at creation — costs paid to the network, not to us or to the creator.": "Não existe tabela de divisão de taxas porque não há taxas para dividir. Cada satoshi de XCP pago pelos minters vai para o pool. Os únicos custos em todo o sistema são as taxas de transação do Bitcoin, a taxa de registro de nome de 0,5 XCP para ativos nomeados e a taxa de gas do pooldeposit do protocolo, debitada na criação — custos pagos à rede, não a nós nem ao criador.", "This image is {kb} KB — inscribing caps at 385 KB. Use a smaller file or uncheck to host it instead.": "Esta imagem tem {kb} KB — o limite para inscrever é 385 KB. Use um arquivo menor ou desmarque a opção para hospedá-la em vez disso.", - "This is the protocol's own swap math: constant product with a 50 bps fee that stays in the pool. Price is the ratio of the two reserves — one drains as the other fills, which is why the tick marks sit at center: both sides open worth {xcp} XCP each. The reserve approaches zero without ever arriving, and the LP is burned, so this liquidity can be traded against forever and withdrawn by no one — whatever XCP sits in the pool is out of circulation for good.": "Esta é a matemática de swap do próprio protocolo: produto constante com uma taxa de 50 bps que fica no pool. O preço é a razão entre as duas reservas — uma esvazia enquanto a outra enche, e por isso as marcas ficam no centro: os dois lados abrem valendo {xcp} XCP cada. A reserva se aproxima de zero sem nunca chegar lá, e o LP está queimado, então dá para negociar contra essa liquidez para sempre e ninguém consegue sacá-la — o XCP que estiver no pool fica fora de circulação de vez.", + "This is the protocol's own swap math: constant product with a 50 bps fee that stays in the pool. Price is the ratio of the two reserves — one drains as the other fills, which is why the tick marks sit at center: both sides open worth {xcp} XCP each. The reserve approaches zero without ever arriving, and the LP is burned, so this liquidity can be traded against forever and withdrawn by no one — whatever XCP sits in the pool is out of circulation for good.": "Esta é a matemática de swap do próprio protocolo: produto constante com uma taxa de 50 bps que fica no pool. O preço é a razão entre as duas reservas — uma esvazia enquanto a outra enche, e por isso as marcas ficam no centro: os dois lados abrem valendo {xcp} XCP cada. A reserva se aproxima de zero sem nunca chegar lá, e os tokens LP estão queimados, então dá para negociar contra essa liquidez para sempre e ninguém consegue sacá-la — o XCP que estiver no pool fica fora de circulação de vez.", "This wallet hasn't launched anything yet.": "Esta carteira ainda não lançou nada.", "This WEBP is animated. It will move on the site, but the announce channel can only post a still frame of it — Telegram has no way to play an animated WEBP. Upload the same art as a GIF and it moves there too.": "Este WEBP é animado. Ele vai se mover no site, mas o canal de anúncios só consegue postar um quadro parado — o Telegram não tem como reproduzir um WEBP animado. Envie a mesma arte como GIF e ela se move lá também.", "this window": "este período", @@ -1142,11 +1142,11 @@ "Upcoming": "Em breve", "Upload failed": "Falha no envio", "Upload token image": "Enviar a imagem do token", - "Use digits and a dot, with up to {n} decimal places.": "Use dígitos e um ponto, com até {n} casas decimais.", + "Use digits and a dot, with up to {n} decimal places.": "Use 0-9 e ponto decimal; até {n} casas decimais.", "Use whole numbers only.": "Use apenas números inteiros.", "Value": "Valor", "Value and underlying amounts are what withdrawing the LP tokens now would return. XCP value is available when XCP is one side of the pair. LP profit and loss is not estimated.": "O valor e os montantes subjacentes são o que retirar os tokens LP agora devolveria. O valor em XCP está disponível quando XCP é um dos lados do par. O lucro ou prejuízo da LP não é estimado.", - "Vends": "Vende", + "Vends": "Vendas disponíveis", "Venue": "Mercado", "Venue / time": "Local / hora", "Verdict pending": "Veredito pendente", @@ -1167,7 +1167,7 @@ "What is this?": "O que é isso?", "What sellers did next": "O que os vendedores fizeram depois", "What sellers do next, where known dumpers are minting, and how much seller inventory remains.": "O que os vendedores fazem depois, onde os dumpers conhecidos estão mintando e quanto estoque de vendedores ainda resta.", - "What xcp.fun shows for this launch, and what the hosted metadata says if your on-chain description points here. The on-chain field itself is locked forever; everything behind it is yours to curate.": "O que o xcp.fun mostra deste lançamento e o que os metadados hospedados dizem se a sua descrição on-chain apontar para cá. O campo on-chain fica bloqueado para sempre; tudo o que está por trás dele é você quem cura.", + "What xcp.fun shows for this launch, and what the hosted metadata says if your on-chain description points here. The on-chain field itself is locked forever; everything behind it is yours to curate.": "O que o xcp.fun mostra deste lançamento e o que os metadados hospedados dizem se a sua descrição on-chain apontar para cá. O campo on-chain fica bloqueado para sempre; você pode editar o conteúdo hospedado por trás dele.", "When": "Quando", "When do I get paid?": "Quando eu recebo?", "when the BTC payment confirms": "quando o pagamento em BTC for confirmado", @@ -1191,11 +1191,11 @@ "XCP in the pool": "XCP no pool", "XCP market · cached 15 min": "Mercado XCP · cache de 15 min", "XCP Price": "Preço do XCP", - "XCP reserve ÷ token reserve. Moves with every swap; there is no order book and no market maker.": "Reserva de XCP ÷ reserva de tokens. Move a cada swap; não há livro de ofertas nem formador de mercado.", + "Pool price is XCP reserve ÷ token reserve. Swaps change this ratio. Limit orders can also trade on Counterparty's order book.": "O preço do pool é a reserva de XCP ÷ a reserva de tokens. Os swaps alteram essa proporção. Ordens limite também podem ser executadas no livro de ofertas do Counterparty.", "XCP Rewards": "Recompensas XCP", "XCP Rewards — xcp.fun": "Recompensas XCP — xcp.fun", "XCP to receive": "XCP a receber", - "XCP to unload": "XCP a descarregar", + "XCP to unload": "XCP a vender", "XCP-69 is a fixed-parameter token launch standard built on Counterparty {pools}. There is no factory contract, no admin key, and no platform custody at any point: the protocol {is} the platform. Every action — creating a launch, minting, swapping — is a transaction you sign in your own wallet and broadcast to Bitcoin. This site is an interface over public on-chain data; if it disappeared tomorrow, every launch, refund, and pool would keep working exactly as before.": "O XCP-69 é um padrão de lançamento de tokens com parâmetros fixos, construído sobre os {pools} do Counterparty. Não há contrato fábrica, nem chave de administrador, nem custódia da plataforma em momento algum: o protocolo {is} a plataforma. Toda ação — criar um lançamento, mintar, fazer swap — é uma transação que você assina na sua própria carteira e transmite para o Bitcoin. Este site é uma interface sobre dados públicos on-chain; se ele sumisse amanhã, cada lançamento, reembolso e pool continuaria funcionando exatamente como antes.", "XCP-69 launches and mints sitting in the Bitcoin mempool, not yet confirmed.": "Lançamentos e mints XCP-69 na mempool do Bitcoin, ainda sem confirmação.", "XCP-69 launches by phase, and what has actually been minted.": "Lançamentos XCP-69 por fase, e o que de fato foi mintado.", diff --git a/apps/web/src/locales/pt.status.json b/apps/web/src/locales/pt.status.json index 8694a46d..92df3437 100644 --- a/apps/web/src/locales/pt.status.json +++ b/apps/web/src/locales/pt.status.json @@ -237,7 +237,7 @@ "Every minted token dumped in one trade — the pool still quotes a bid with {left} XCP left, and sellers exit at {mult}× mint ({paid}% of what they paid). Those {left} XCP can never be withdrawn: every launch is a permanent sink for XCP itself.", "Every route has a purchase pending in the mempool — check back in a few minutes.", "Everything about XCP-69, in one place. The first half explains how launches work for anyone. The second half is for developers who want to read or build on the same on-chain data this site does.", - "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, what it costs (nothing), and how to integrate.", + "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, fees, and how to integrate.", "Everything is curl-able. Launches and mints:", "Everything this site displays comes from public APIs. You can build your own launchpad, bot, or dashboard on the same data — nothing below requires our permission.", "Expiration", @@ -268,7 +268,7 @@ "Get XCP — xcp.fun", "Graduated", "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.", - "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses, at which point the pool is created and its liquidity is burned. A launch that misses its target refunds every satoshi by consensus and does not count. {link}", + "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses. The pool is created and its initial LP tokens go to an unspendable address, permanently locking that liquidity. A launch that misses its target returns the contributed XCP by consensus and does not count. {link}", "Graduation", "Graveyard", "Graveyard pages", @@ -455,7 +455,7 @@ "No trades yet.", "No value chart for {assets}: its recent transfer or liquidity history did not load completely, so we cannot know how many tokens this wallet held at each point. Current holdings and value are live and accurate.", "No value chart: its recent transfer or liquidity history did not load completely, so we cannot know how many tokens this wallet held at each point. Current holdings and value are live and accurate.", - "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.", + "A mint locks your XCP until the launch resolves. If it graduates, your XCP funds the pool and you receive tokens, which can lose value. If it refunds, your XCP is returned. The Bitcoin network fee is paid in either case; rewards do not guarantee that every cost is covered.", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.", "Nobody has minted yet. The first row here is available.", @@ -779,7 +779,7 @@ "XCP in pools", "XCP in the pool", "XCP market · cached 15 min", - "XCP reserve ÷ token reserve. Moves with every swap; there is no order book and no market maker.", + "Pool price is XCP reserve ÷ token reserve. Swaps change this ratio. Limit orders can also trade on Counterparty's order book.", "XCP to receive", "XCP to unload", "XCP-69 is a fixed-parameter token launch standard built on Counterparty {pools}. There is no factory contract, no admin key, and no platform custody at any point: the protocol {is} the platform. Every action — creating a launch, minting, swapping — is a transaction you sign in your own wallet and broadcast to Bitcoin. This site is an interface over public on-chain data; if it disappeared tomorrow, every launch, refund, and pool would keep working exactly as before.", From 54e938f562848062a651c5222d22a4ee6efb6948 Mon Sep 17 00:00:00 2001 From: Dan Anderson Date: Mon, 7 Sep 2026 19:14:34 -0400 Subject: [PATCH 08/10] fix: clarify Russian and Ukrainian launch and transaction terms --- apps/web/src/locales/ru.json | 84 +++++++++++++------------- apps/web/src/locales/ru.status.json | 8 +-- apps/web/src/locales/uk.json | 94 ++++++++++++++--------------- apps/web/src/locales/uk.status.json | 8 +-- 4 files changed, 97 insertions(+), 97 deletions(-) diff --git a/apps/web/src/locales/ru.json b/apps/web/src/locales/ru.json index d461765b..084f1e59 100644 --- a/apps/web/src/locales/ru.json +++ b/apps/web/src/locales/ru.json @@ -12,7 +12,7 @@ "“Coordinated first exit” is an optimistic upper bound. “Random order” is the expected result if all 69 full bags sell and these wallets are interleaved uniformly among them. Neither line asserts how many people actually participated.": "«Согласованный первый выход» — оптимистичная верхняя граница. «Случайный порядок» — ожидаемый результат, если все 69 полных мешков будут проданы, а эти кошельки равномерно распределены среди них. Ни одна из линий не утверждает, сколько людей участвовало на самом деле.", "(optional)": "(необязательно)", "(you own this asset)": "(этот актив принадлежит вам)", - "{a} graduated · {b} refunded": "выпустились: {a} · возвращено: {b}", + "{a} graduated · {b} refunded": "Пулы открыты: {a} · с возвратом: {b}", "{action} anyway": "{action} всё равно", "{action} anyway — {pct}% slippage": "{action} всё равно — проскальзывание {pct} %", "{age} ago": "{age} назад", @@ -67,7 +67,7 @@ "{n} conforming launches · chain tip {height}": "запусков по стандарту: {n} · последний блок {height}", "{n} fill": "{n} исполнение", "{n} fills": "исполнений: {n}", - "{n} graduated · {pct}%": "выпустились: {n} · {pct} %", + "{n} graduated · {pct}%": "С открытым пулом: {n} · {pct}%", "{n} held": "держат: {n}", "{n} holder": "холдеров: {n}", "{n} holders": "холдеров: {n}", @@ -141,7 +141,7 @@ "{pct} of supply": "{pct} предложения", "{pct}% filled": "исполнено {pct} %", "{pct}% filled — the rest is resting on the book with {amount} {asset} escrowed.": "Исполнено {pct} % — остаток ждёт в книге ордеров, в эскроу {amount} {asset}.", - "{pct}% graduated ({a}/{b})": "{pct} % выпустились ({a}/{b})", + "{pct}% graduated ({a}/{b})": "С открытым пулом: {pct}% ({a}/{b})", "{pct}% locked": "{pct} % заблокировано", "{pct}% minted": "{pct} % заминчено", "{pct}% of ATH": "{pct} % от ATH", @@ -158,7 +158,7 @@ "{shown} of {total}": "{shown} из {total}", "{status} → minting": "{status} → идёт минт", "{status} → scheduled": "{status} → запланирован", - "{status} + {pool} returns a pool → graduated": "{status} и {pool} возвращает пул → выпустился", + "{status} + {pool} returns a pool → graduated": "{status} и {pool} возвращает запись пула → пул открыт", "{status} with no pool row → refunded (corroborate with the destruction tagged \"soft cap not reached\")": "{status} без записи пула → возвращено (подтверждается записью об уничтожении с тегом \"soft cap not reached\")", "{supply} · {pool} pool": "{supply} · пул {pool}", "{taken} of {total} XCP": "{taken} из {total} XCP", @@ -196,8 +196,8 @@ "~6 hours": "~6 часов", "~7 days": "~7 дней", "−{pct}% under floor · sells first": "−{pct} % ниже минимума · продастся первым", - "≈ {amount} · graduated": "≈ {amount} · выпустились", - "≈ {amount} · graduated coins": "≈ {amount} · выпустившиеся монеты", + "≈ {amount} · graduated": "≈ {amount} · монеты с пулом", + "≈ {amount} · graduated coins": "≈ {amount} · монеты с пулом", "≈ {amount} committed": "≈ {amount} внесено", "≈ {amount} committed to open mints": "≈ {amount} внесено в открытые минты", "≈ {amount} in pools": "≈ {amount} в пулах", @@ -207,7 +207,7 @@ "⏳ Scheduled — announced on-chain, nothing minted yet.": "⏳ Запланирован — объявлен в блокчейне, ещё ничего не заминчено.", "◆ Pool": "◆ Пул", "✓ broadcast": "✓ транслировано", - "✓ Graduated — pool created with {xcp} XCP{usd} + 31M tokens, LP burned.": "✓ Выпустился — пул создан из {xcp} XCP{usd} + 31M токенов, LP сожжены.", + "✓ Graduated — pool created with {xcp} XCP{usd} + 31M tokens, LP burned.": "✓ Пул открыт — {xcp} XCP{usd} + 31M токенов, LP-токены сожжены.", "💀 RIP": "💀 RIP", "0.01 XCP / 1,000 · sells out or refunds": "0,01 XCP / 1 000 · распродаётся или возвращается", "0.01 XCP per 1,000-token lot": "0,01 XCP за лот в 1 000 токенов", @@ -227,6 +227,7 @@ "69,000,000 (the soft cap IS the whole sale)": "69 000 000 (софткап — это и есть вся продажа)", "a future block — announced on-chain before minting can open": "будущий блок — объявляется в блокчейне до того, как минт может открыться", "A launch moves through four phases:": "Запуск проходит четыре фазы:", + "A mint locks your XCP until the launch resolves. If it graduates, your XCP funds the pool and you receive tokens, which can lose value. If it refunds, your XCP is returned. The Bitcoin network fee is paid in either case; rewards do not guarantee that every cost is covered.": "Минт блокирует ваши XCP до завершения запуска. Если пул откроется, ваши XCP пополнят его, а вы получите токены, которые могут потерять в цене. Если запуск завершится возвратом, ваши XCP вернутся. Комиссия сети Bitcoin уплачивается в любом случае; награды не гарантируют покрытия всех затрат.", "A new way to launch a coin on Counterparty — every launch identical, enforced by consensus.": "Новый способ запустить монету на Counterparty — каждый запуск одинаков и обеспечивается консенсусом.", "A pool never runs out, it only gets worse. This is what it can absorb before its price moves 1%.": "Пул никогда не пустеет — он лишь даёт цену всё хуже. Столько он может принять, прежде чем его цена сдвинется на 1 %.", "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.": "Запуск с возвратом — это не потерянные деньги. XCP-69 работает по принципу «всё или ничего»: запуск, не добравший софткап, возвращает каждый сатоши по консенсусу — без нашего решения и без возможности кому-либо оставить их себе.", @@ -250,7 +251,7 @@ "Address limit reached": "Лимит на адрес исчерпан", "addresses": "адреса", "adjusts to {n}": "будет округлено до {n}", - "After graduation": "После выпускного", + "After graduation": "После открытия пула", "After mempool": "После мемпула", "All": "Все", "All data shown on this site is public on-chain data; anything you see here you can verify yourself against a Counterparty node. The site is an interface, not a counterparty to any transaction — it never holds funds and cannot reverse, expedite, or refund anything (the protocol handles refunds on its own). Nothing here is investment advice; tokens launched through XCP-69 can and will lose value. Read the {link} before minting.": "Все данные на этом сайте — публичные данные блокчейна; всё, что вы здесь видите, можно проверить самостоятельно по ноде Counterparty. Сайт — это интерфейс, а не сторона какой-либо транзакции: он никогда не хранит средства и не может ничего отменить, ускорить или вернуть (возвраты протокол выполняет сам). Ничто здесь не является инвестиционной рекомендацией; токены, запущенные через XCP-69, могут терять и будут терять в цене. Прочитайте {link} перед минтом.", @@ -267,7 +268,7 @@ "Amount of XCP to deposit": "Сумма XCP для внесения", "Amount to remove": "Сумма к выводу", "Amount too small — rounds to 0": "Сумма слишком мала — округляется до 0", - "An XCP bounty for the first three launches to graduate, and MINTS for every mint.": "Награда в XCP за первые три выпустившихся запуска и MINTS за каждый минт.", + "An XCP bounty for the first three launches to graduate, and MINTS for every mint.": "Награда в XCP за первые три запуска с открытым пулом и MINTS за каждый минт.", "an XCP fairminter on xcp.fun": "fairminter XCP на xcp.fun", "Animated WEBP shows as a still frame in the announce channel — Telegram cannot play one. A GIF moves there too.": "Анимированный WEBP в канале анонсов показывается как статичный кадр — Telegram не умеет его проигрывать. GIF там тоже будет двигаться.", "Announce.": "Объявление.", @@ -287,7 +288,7 @@ "at the live pool price": "по текущей цене пула", "Auto": "Авто", "Auto ({code})": "Авто ({code})", - "Auto sizes slippage to the trade: what this quote needs, currently ~{pct}%.": "Авто подбирает проскальзывание под сделку: сколько нужно этой котировке, сейчас ~{pct} %.", + "Auto sizes slippage to the trade: what this quote needs, currently ~{pct}%.": "Режим «Авто» подбирает допустимое проскальзывание для этой сделки: сейчас около {pct}%.", "available": "свободно", "Available: {n}": "Доступно: {n}", "Balance": "Баланс", @@ -299,7 +300,7 @@ "Bid": "Покупка", "binary": "бинарен", "Bitcoin": "Биткоин", - "Bitcoin fee rate in sats per vbyte": "Ставка комиссии Bitcoin в сатоши за vbyte", + "Bitcoin fee rate in sats per vbyte": "Ставка комиссии Bitcoin в сатоши за виртуальный байт", "Bitcoin market price in {currency}": "Рыночная цена биткоина в {currency}", "Bitcoin Price": "Цена биткоина", "Bitcoin tx fee": "Комиссия майнеров Bitcoin", @@ -311,7 +312,7 @@ "book": "книга ордеров", "Bought": "Покупка", "bought without minting": "купили без минта", - "Broadcast": "Транслировать", + "Broadcast": "Отправлено в сеть", "Broadcast as {tx}. Minting opens at block {block} ({when}) — until then the launch is announced on-chain and nobody, you included, can mint. Then it runs for 1,000 blocks (~7 days): it sells out, or everyone is refunded.": "Транслировано как {tx}. Минт откроется на блоке {block} ({when}) — до этого запуск объявлен в блокчейне, и никто, включая вас, минтить не может. Затем он идёт 1 000 блоков (~7 дней): либо распродаётся, либо всем возвращают средства.", "Broadcast, waiting to confirm. Its page and editing open up once it lands in a block.": "Транслировано, ждёт подтверждения. Страница и редактирование откроются, когда транзакция попадёт в блок.", "Broadcasting commit…": "Трансляция commit…", @@ -431,7 +432,7 @@ "Counterparty market price in {currency}": "Рыночная цена Counterparty в {currency}", "Counterparty's canonical unspendable burn address": "Канонический адрес сжигания Counterparty, с которого невозможно потратить", "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.": "Ордер DEX Counterparty — единственный торговый примитив: сведение идёт через пул всякий раз, когда цена пула лучше книги ордеров. {market} — это ордер по выходу, рассчитанному маршрутизатором: он немедленно исполняется из пула и книги по лучшей цене. {limit} лежит в книге, и пул исполнит его автоматически, если его цена когда-нибудь пересечёт вашу.", - "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.": "Подсчёт ведётся по уникальным адресам. «Держат», «переместили» и «продали» — взаимоисключающие исходы для минтеров каждого выпустившегося запуска. «Держат» — продаж не было и текущий баланс существенный; «переместили» — продаж не было и существенного баланса нет; «продали» — хотя бы одна продажа в пуле или в книге ордеров, зафиксированная xcp.fun.", + "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.": "Подсчёт ведётся по уникальным адресам. «Держат», «переместили» и «продали» — взаимоисключающие исходы для минтеров каждого запуска с открытым пулом. «Держат» — продаж не было и текущий баланс существенный; «переместили» — продаж не было и существенного баланса нет; «продали» — хотя бы одна продажа в пуле или в книге ордеров, зафиксированная xcp.fun.", "Covered": "Покрыто", "Create": "Создать", "Created": "Создан", @@ -445,8 +446,8 @@ "Currently unloading:": "Сейчас продаётся обратно:", "Custom": "Свой", "custom block": "свой блок", - "Custom liquidity slippage percent": "Своё проскальзывание для ликвидности, %", - "Custom slippage percent": "Своё проскальзывание, %", + "Custom liquidity slippage percent": "Допустимое проскальзывание для ликвидности, %", + "Custom slippage percent": "Допустимое проскальзывание, %", "Daily": "Дневные", "Data": "Данные", "day": "день", @@ -474,10 +475,10 @@ "Double network fee at this lead — there aren't enough blocks to re-fee if the estimate comes in low, and a launch that confirms after it opens fails the standard.": "При таком запасе комиссия сети удваивается: не хватит блоков, чтобы поднять комиссию, если оценка окажется низкой, а запуск, подтверждённый после открытия, не проходит стандарт.", "down": "вниз", "dropped — nothing was spent": "отброшена — ничего не списано", - "Dump means the first sale landed within six blocks of graduation. Repeat dump means that happened on more than one earlier launch. Balances come from Counterparty and may include tokens obtained outside the mint. Pending mempool orders can disappear without confirming or filling. Addresses are not necessarily people.": "Дамп — первая продажа в пределах шести блоков после выпускного. Повторный дамп — то же самое более чем в одном предыдущем запуске. Балансы берутся из Counterparty и могут включать токены, полученные вне минта. Ордера в мемпуле могут исчезнуть, не подтвердившись и не исполнившись. Адреса — не обязательно люди.", + "Dump means the first sale landed within six blocks of graduation. Repeat dump means that happened on more than one earlier launch. Balances come from Counterparty and may include tokens obtained outside the mint. Pending mempool orders can disappear without confirming or filling. Addresses are not necessarily people.": "Дамп — первая продажа в пределах шести блоков после открытия пула. Повторный дамп — то же самое более чем в одном предыдущем запуске. Балансы берутся из Counterparty и могут включать токены, полученные вне минта. Ордера в мемпуле могут исчезнуть, не подтвердившись и не исполнившись. Адреса — не обязательно люди.", "Dumpers": "Дамперы", "Dumping now": "Дампят сейчас", - "Each number counts unique addresses. A sale is a pool or order-book sale captured by xcp.fun. Dump means the address sold within {blocks} blocks of graduation. A meaningful balance is more than one token and more than 1% of the address's acquired amount.": "Каждое число — количество уникальных адресов. Продажа — это продажа в пуле или в книге ордеров, зафиксированная xcp.fun. Дамп — адрес продал в пределах {blocks} блоков после выпускного. Существенный баланс — больше одного токена и больше 1 % от объёма, который адрес приобрёл.", + "Each number counts unique addresses. A sale is a pool or order-book sale captured by xcp.fun. Dump means the address sold within {blocks} blocks of graduation. A meaningful balance is more than one token and more than 1% of the address's acquired amount.": "Каждое число — количество уникальных адресов. Продажа — это продажа в пуле или в книге ордеров, зафиксированная xcp.fun. Дамп — адрес продал в пределах {blocks} блоков после открытия пула. Существенный баланс — больше одного токена и больше 1 % от объёма, который адрес приобрёл.", "Each route is a separate transaction. Broadcast routes cannot be canceled here. Failed routes can be retried.": "Каждый маршрут — отдельная транзакция. Уже отправленные в сеть маршруты нельзя отменить здесь. Неудавшиеся можно повторить.", "Earned": "Заработано", "Earned so far": "Заработано на данный момент", @@ -495,13 +496,13 @@ "Event": "Событие", "Every launch that conforms to XCP-69, and what has actually been minted.": "Все запуски, соответствующие XCP-69, и то, что действительно было заминчено.", "Every LP token is burned — this liquidity can never be withdrawn.": "Все токены LP сожжены — эту ликвидность нельзя вывести никогда.", - "Every mint on a conforming launch, counted. Ranked by mints, because that is the unit the reward is paid in.": "Каждый минт в запуске по стандарту, подсчитан. Сортировка по числу минтов, потому что именно в них выплачивается награда.", + "Every mint on a conforming launch, counted. Ranked by mints, because that is the unit the reward is paid in.": "Учитывается каждый минт в запуске по стандарту. Рейтинг строится по числу минтов, поскольку награда начисляется за каждую транзакцию минта.", "Every minted token dumped in one trade — the pool still quotes a bid with {left} XCP left, and sellers exit at {mult}× mint ({paid}% of what they paid). Those {left} XCP can never be withdrawn: every launch is a permanent sink for XCP itself.": "Все заминченные токены слиты одной сделкой — пул всё ещё котирует покупку, в нём осталось {left} XCP, а продавцы выходят по {mult}× от минта ({paid} % от уплаченного). Эти {left} XCP невозможно вывести никогда: каждый запуск — постоянный сток для самого XCP.", "Every route has a purchase pending in the mempool — check back in a few minutes.": "На каждом маршруте покупка ждёт в мемпуле — загляните через несколько минут.", "Every XCP-69 launch is identical: 100M supply, 69M public sale at 0.01 XCP per 1,000-token lot, 31M reserved for the liquidity pool, 10 XCP per-address cap, an on-chain pre-announcement before minting opens, and a 1,000-block window. There is no fine print to read because there is no fine print. The full parameter set lives on the {link} page.": "Каждый запуск XCP-69 одинаков: предложение 100M, публичная продажа 69M по 0,01 XCP за лот в 1 000 токенов, 31M зарезервированы для пула ликвидности, лимит 10 XCP на адрес, предварительное объявление в блокчейне до открытия минта и окно в 1 000 блоков. Мелкого шрифта читать не нужно, потому что мелкого шрифта нет. Полный набор параметров — на странице {link}.", "Every XCP-69 mint, trade, burn, resting order and launch, newest first.": "Все минты, сделки, сжигания, ожидающие ордера и запуски XCP-69 — сначала новые.", "everyone dumps": "все сливают", - "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, what it costs (nothing), and how to integrate.": "Всё об XCP-69 в одном месте: как работают запуски, как формируется цена, что значит «выпуститься», сколько это стоит (ничего) и как интегрироваться.", + "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, fees, and how to integrate.": "Всё об XCP-69 в одном месте: как работают запуски, как формируется цена, что означает открытие пула, какие есть комиссии и как интегрироваться.", "Everything about XCP-69, in one place. The first half explains how launches work for anyone. The second half is for developers who want to read or build on the same on-chain data this site does.": "Всё об XCP-69 в одном месте. Первая половина объясняет, как работают запуски, для всех. Вторая — для разработчиков, которые хотят читать те же данные из блокчейна, что и этот сайт, или строить на них.", "Everything is curl-able. Launches and mints:": "Всё доступно через curl. Запуски и минты:", "Everything this site displays comes from public APIs. You can build your own launchpad, bot, or dashboard on the same data — nothing below requires our permission.": "Всё, что показывает этот сайт, берётся из публичных API. Вы можете построить свой лаунчпад, бота или дашборд на тех же данных — ничто из перечисленного ниже не требует нашего разрешения.", @@ -545,12 +546,12 @@ "Get XCP — xcp.fun": "Получить XCP — xcp.fun", "Get XCP before it opens": "Получите XCP до открытия", "gotchas": "подводные камни", - "graduated": "выпустился", - "Graduated": "Выпустился", - "graduated coins": "выпустившиеся монеты", - "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.": "Выпустившиеся токены торгуются против пула TOKEN/XCP с постоянным произведением. Цена — это просто отношение резервов пула; каждый обмен её сдвигает. Фиксированная комиссия {fee} с каждого обмена уплачивается самому пулу — а поскольку LP сожжены, комиссии углубляют заблокированную ликвидность, а не выплачиваются кому-либо.", - "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses, at which point the pool is created and its liquidity is burned. A launch that misses its target refunds every satoshi by consensus and does not count. {link}": "Выпуститься — значит распродаться: собрать {xcp} XCP как минимум с {n} разных адресов, после чего создаётся пул, а его ликвидность сжигается. Запуск, не достигший цели, возвращает каждый сатоши по консенсусу и не засчитывается. {link}", - "Graduation": "Выпускной", + "graduated": "пул открыт", + "Graduated": "Пул открыт", + "graduated coins": "монеты с открытым пулом", + "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.": "После открытия пула токены торгуются в пуле TOKEN/XCP с постоянным произведением. Цена — отношение резервов пула; каждый обмен её меняет. Фиксированная комиссия {fee} с каждого обмена остаётся в пуле. Поскольку первоначальные LP-токены сожжены, комиссии увеличивают заблокированную ликвидность, а не выплачиваются кому-либо.", + "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses. The pool is created and its initial LP tokens go to an unspendable address, permanently locking that liquidity. A launch that misses its target returns the contributed XCP by consensus and does not count. {link}": "Пул открывается после полной распродажи токенов: собрано {xcp} XCP как минимум с {n} разных адресов. Создаётся пул, а его первоначальные LP-токены отправляются на адрес, с которого невозможно потратить средства, навсегда блокируя эту ликвидность. Если запуск не достигает цели, внесённые XCP возвращаются по правилам консенсуса, а запуск не засчитывается. {link}", + "Graduation": "Открытие пула", "Graveyard": "Кладбище", "Graveyard — xcp.fun": "Кладбище — xcp.fun", "Graveyard pages": "Страницы кладбища", @@ -743,14 +744,13 @@ "New buyers": "Новые покупатели", "Newest": "Новые", "Next →": "Далее →", - "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.": "Нет — минт помещает XCP в эскроу под запуск. Если запуск выпустится, у вас остаются токены; если будет возврат, вы получите назад каждый сатоши. Единственное, что минт действительно стоит, — комиссия биткоина, и именно её покрывает эта программа.", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.": "Нет — и в этом суть. Протокол fairminter допускает огромный разброс: без софткапа, без возвратов, с комиссией создателя до 99 %, с премайном, вообще без пула. XCP-69 фиксирует одну его конфигурацию, и этот сайт перечисляет только запуски, точно соответствующие стандарту, проверенные по каждому полю против записи в блокчейне. Здесь каждый запуск ведёт себя одинаково, и вам никогда не приходится проверять параметры самим. На fairminter, встреченный где-либо ещё, такая гарантия не распространяется — читайте его поля, прежде чем минтить.", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.": "Нет — это следствие структуры, а не обещание. Пул обязан открыться по 69/31 от цены минта, потому что других количеств просто не существует, но дно опускается по мере продаж в пул. Ничто не мешает токену торговаться ниже минта. Пул гарантирует заявку на покупку, которая никогда не станет нулевой, — а не такую, которая вам понравится.", "No active {give}/{get} pool liquidity. A resting order can still fill through the order book.": "В пуле {give}/{get} нет активной ликвидности. Отложенный ордер всё же может исполниться через книгу ордеров.", "No closed positions yet.": "Закрытых позиций пока нет.", "no history": "нет истории", "No holders found.": "Холдеры не найдены.", - "No launches have graduated yet. The first sell-out seeds the first pool — and it becomes tradeable here in the same block.": "Пока ни один запуск не выпустился. Первая распродажа создаст первый пул — и токен станет торгуемым здесь в том же блоке.", + "No launches have graduated yet. The first sell-out seeds the first pool — and it becomes tradeable here in the same block.": "Ни один запуск ещё не открыл пул. Первая полная распродажа создаст первый пул — и токен станет доступен для торговли здесь в том же блоке.", "No launches in this phase yet.": "В этой фазе пока нет запусков.", "No launches in this phase.": "В этой фазе запусков нет.", "No launches queued. Every XCP-69 launch broadcast so far has confirmed.": "Запусков в очереди нет. Все транслированные запуски XCP-69 уже подтверждены.", @@ -772,7 +772,7 @@ "No orders for {asset} are waiting in the mempool.": "Ордеров по {asset} в мемпуле нет.", "No orders queued — every order so far has confirmed.": "Ордеров в очереди нет — все ордера уже подтверждены.", "No platform, no creator fees": "Без комиссий платформы и создателя", - "No pools yet. A launch has to graduate before its liquidity exists.": "Пулов пока нет. Ликвидность появляется только после того, как запуск выпустится.", + "No pools yet. A launch has to graduate before its liquidity exists.": "Пулов пока нет. Ликвидность появится, когда запуск полностью распродаст токены и откроет пул.", "No quote for this pair": "Нет котировки для этой пары", "No sells waiting in the mempool.": "В мемпуле нет ожидающих продаж.", "No slider on this one — there is nothing to configure. Every launch is identical, checked field-by-field against the on-chain record.": "Здесь ползунка нет — настраивать нечего. Каждый запуск одинаков и проверен по каждому полю против записи в блокчейне.", @@ -784,7 +784,7 @@ "No value chart: its recent transfer or liquidity history did not load completely, so we cannot know how many tokens this wallet held at each point. Current holdings and value are live and accurate.": "Нет графика стоимости: недавняя история переводов или ликвидности загрузилась не полностью, поэтому мы не знаем, сколько токенов было в этом кошельке в каждый момент. Текущие остатки и стоимость актуальны и точны.", "No XCP committed": "XCP не внесены", "No XCP committed to open mints right now.": "Сейчас в открытые минты не внесено ни одного XCP.", - "No XCP-69 launch has graduated yet. The first three to do it earn a bounty.": "Ни один запуск XCP-69 ещё не выпустился. Первые три получат награду.", + "No XCP-69 launch has graduated yet. The first three to do it earn a bounty.": "Ни один запуск XCP-69 ещё не открыл пул. Первые три получат награду.", "No XCP-69 tokens have been sent to the Counterparty burn address yet.": "На адрес сжигания Counterparty ещё не отправлено ни одного токена XCP-69.", "Nobody has minted yet. The first row here is available.": "Никто ещё не минтил. Первая строка свободна.", "Non-conforming": "Не по стандарту", @@ -796,7 +796,7 @@ "not launchable": "запуск невозможен", "not XCP-69": "не XCP-69", "nothing": "ничего", - "Nothing has traded yet. A launch has to graduate before it has a market.": "Сделок ещё не было. Рынок появляется только после того, как запуск выпустится.", + "Nothing has traded yet. A launch has to graduate before it has a market.": "Сделок ещё не было. Торговля начинается после полной распродажи токенов и открытия пула.", "Nothing here. That's good.": "Здесь пусто. И это хорошо.", "Nothing matches “{query}”.": "По запросу «{query}» ничего не найдено.", "Nothing open matches “{query}” — {n} refunded launch hidden. Type a full name to open one.": "Среди открытых по запросу «{query}» ничего нет — скрыто запусков с возвратом: {n}. Введите полное название, чтобы открыть.", @@ -818,12 +818,12 @@ "of which {amount} realized": "из них реализовано {amount}", "Of XCP minted": "Доля XCP, внесённых в минты", "Off-chain by default: the chain permanently carries the URL of the asset-info JSON, and the content behind it is curatable by the asset's current owner (wallet-signature gated). Taproot creators can instead inscribe the image on-chain as the permanent description — no off-chain dependency at all. Either way, nothing economic depends on hosted content.": "По умолчанию вне блокчейна: блокчейн навсегда хранит URL JSON-файла с информацией об активе, а содержимое за ним может править текущий владелец актива (по подписи кошелька). Создатели с Taproot могут вместо этого записать изображение в блокчейн как постоянное описание — вообще без зависимости от внешнего хостинга. В любом случае ничто в экономике не зависит от размещённого контента.", - "On other launchpads, graduation is a threshold inside a live market: the token trades on a curve while everyone hopes it reaches the magic number. XCP-69 graduation is {binary} and happens before any trading exists. Sell out the 69M sale within 1,000 blocks and the launch graduates — pool seeded, LP burned, supply locked, trading live in the same block's resolution phase. Miss it and the launch never trades at all.": "На других лаунчпадах выпускной — это порог внутри живого рынка: токен торгуется по кривой, пока все надеются, что он дойдёт до заветной цифры. Выпускной в XCP-69 {binary} и происходит до того, как существует какая-либо торговля. Распродайте 69M за 1 000 блоков — и запуск выпустится: пул создан, LP сожжены, предложение заблокировано, торговля идёт уже в фазе разрешения того же блока. Не успели — и запуск не торгуется вообще никогда.", + "On other launchpads, graduation is a threshold inside a live market: the token trades on a curve while everyone hopes it reaches the magic number. XCP-69 graduation is {binary} and happens before any trading exists. Sell out the 69M sale within 1,000 blocks and the launch graduates — pool seeded, LP burned, supply locked, trading live in the same block's resolution phase. Miss it and the launch never trades at all.": "На других лаунчпадах открытие пула — порог внутри уже действующего рынка: токен торгуется по кривой, пока все надеются, что он достигнет нужного значения. В XCP-69 исход {binary} и определяется до начала торговли. Все 69M токенов продажи должны быть раскуплены за 1 000 блоков: тогда пул получает ликвидность, LP-токены сжигаются, объём эмиссии фиксируется, а торговля становится доступна на этапе завершения того же блока. Если цель не достигнута, торговля токеном так и не начинается.", "on their Counterparty balance": "на балансе Counterparty", "on-chain asset name": "имя актива в блокчейне", "on-chain since {year}": "в блокчейне с {year}", "Onchain events": "События в блокчейне", - "One bounty is claimed — two are still open for the next launches to graduate.": "Одна награда получена — две ещё ждут следующие запуски, которые выпустятся.", + "One bounty is claimed — two are still open for the next launches to graduate.": "Одна награда получена — две ещё ждут следующие запуски, которые откроют пулы.", "One fixed parameter set for token launches on Counterparty: the launch sells out and liquidity locks forever, or everyone is refunded. Zero creator take, enforced by consensus.": "Один фиксированный набор параметров для запуска токенов на Counterparty: запуск распродаётся и ликвидность блокируется навсегда — или всем возвращают средства. Ноль создателю, обеспечено консенсусом.", "One integration trap worth knowing: Counterparty Core 11.3+ returns {poolNormalized} and {maxNormalized} under {verbose}. Use them for display, but keep standard conformance math in raw integer satoshi units (×10⁸) so comparisons remain exact and match non-verbose event and mempool data.": "Одна ловушка интеграции, о которой стоит знать: Counterparty Core 11.3+ возвращает {poolNormalized} и {maxNormalized} при {verbose}. Используйте их для отображения, но математику соответствия стандарту ведите в сырых целых satoshi (×10⁸), чтобы сравнения оставались точными и совпадали с данными событий и мемпула без verbose.", "One satoshi under the cheapest open dispenser": "На один сатоши ниже самого дешёвого открытого автомата", @@ -877,7 +877,7 @@ "Performance (All)": "Доходность (всё время)", "Phase": "Фаза", "Place limit buy": "Выставить лимитный ордер на покупку", - "Place limit orders on graduated XCP-69 launches. Your price is enforced by the order itself — fills through the pool at confirmation or rests on the book.": "Выставляйте лимитные ордера на выпустившиеся запуски XCP-69. Вашу цену обеспечивает сам ордер — исполнение через пул при подтверждении или ожидание в книге.", + "Place limit orders on graduated XCP-69 launches. Your price is enforced by the order itself — fills through the pool at confirmation or rests on the book.": "Выставляйте лимитные ордера на токены XCP-69 с открытым пулом. Ордер соблюдает вашу лимитную цену: исполняется через пул при подтверждении или остаётся в книге ордеров.", "Place limit sell": "Выставить лимитный ордер на продажу", "PNG, JPEG, WEBP or GIF · {limit} · square (1:1) recommended": "PNG, JPEG, WEBP или GIF · {limit} · рекомендуется квадрат (1:1)", "PnL is withheld because the live balance includes activity outside indexed mints and trades.": "PnL не показан: текущий баланс включает активность вне проиндексированных минтов и сделок.", @@ -888,6 +888,7 @@ "Pool + order book": "Пул + книга ордеров", "Pool is empty": "Пул пуст", "Pool price × circulating supply (issued minus burned). A convention, not a promise — the pool could not pay it out.": "Цена пула × предложение в обращении (выпущено минус сожжено). Условность, а не обещание — пул не смог бы это выплатить.", + "Pool price is XCP reserve ÷ token reserve. Swaps change this ratio. Limit orders can also trade on Counterparty's order book.": "Цена пула — резерв XCP ÷ резерв токенов. Обмены меняют это соотношение. Лимитные ордера также могут исполняться в книге ордеров Counterparty.", "Pool reserve": "Резерв пула", "Pool:": "Пул:", "Pools": "Пулы", @@ -951,7 +952,7 @@ "Replace image": "Заменить изображение", "Represented": "Представлены", "Request declined in wallet.": "Запрос отклонён в кошельке.", - "Resolve.": "Разрешение.", + "Resolve.": "Завершение.", "Retry": "Повторить", "Reward": "Награда", "Rewards": "Награды", @@ -1043,7 +1044,7 @@ "Swap {give} for {get}": "Обменять {give} на {get}", "Swap {give} for {get} — market order": "Обменять {give} на {get} — рыночный ордер", "Swap broadcast — {tx}": "Обмен транслирован — {tx}", - "Swap graduated XCP-69 assets through Counterparty pools and the order book, including direct token pairs when liquidity exists.": "Обменивайте выпустившиеся активы XCP-69 через пулы Counterparty и книгу ордеров, включая прямые пары токенов, когда есть ликвидность.", + "Swap graduated XCP-69 assets through Counterparty pools and the order book, including direct token pairs when liquidity exists.": "Обменивайте активы XCP-69 с открытыми пулами через пулы Counterparty и книгу ордеров, включая прямые пары токенов при наличии ликвидности.", "Swap settings": "Настройки обмена", "Switch wallet": "Сменить кошелёк", "Table view": "Таблица", @@ -1068,7 +1069,7 @@ "The first fairminter ever created on Counterparty — block 866,297, before any other, and it minted out free to 1,376 addresses. The supply is 100,000,000, locked forever; no more can ever be issued. The live MINTS/XCP pool prices the reward: right now 1 MINTS trades at {price} XCP, so {n} MINTS is {worth} XCP.": "Самый первый fairminter, созданный на Counterparty — блок 866 297, раньше всех остальных, — бесплатно разошёлся по 1 376 адресам. Предложение — 100 000 000, заблокировано навсегда; выпустить больше невозможно. Цену награды задаёт живой пул MINTS/XCP: сейчас 1 MINTS торгуется по {price} XCP, значит {n} MINTS — это {worth} XCP.", "The first XCP-69 launch in history hasn't happened yet. All-or-nothing mints, at least {n} participants required, every raised XCP locked into the pool forever — enforced by consensus, not by this website.": "Первого в истории запуска XCP-69 ещё не было. Минты по принципу «всё или ничего», минимум {n} участников, каждый собранный XCP заблокирован в пуле навсегда — это обеспечивает консенсус, а не этот сайт.", "The full lifecycle is observable as Counterparty events via {endpoint}:": "Весь жизненный цикл наблюдаем как события Counterparty через {endpoint}:", - "The graduation bounty": "Награда за выпускной", + "The graduation bounty": "Награда за открытие пула", "The graveyard is unavailable right now.": "Кладбище сейчас недоступно.", "The honest questions — including the ones that don't flatter us.": "Честные вопросы — в том числе те, что нам не льстят.", "The image itself becomes the permanent on-chain description (commit + reveal, two signatures, higher fees scale with image size; the inscription is burned so it belongs to the asset forever). Max 385 KB. Taproot wallets only.": "Само изображение становится постоянным описанием в блокчейне (commit + reveal, две подписи, комиссия растёт с размером изображения; запись сжигается, так что принадлежит активу навсегда). Макс. 385 КБ. Только Taproot-кошельки.", @@ -1089,7 +1090,7 @@ "This wallet hasn't launched anything yet.": "Этот кошелёк ещё ничего не запускал.", "This WEBP is animated. It will move on the site, but the announce channel can only post a still frame of it — Telegram has no way to play an animated WEBP. Upload the same art as a GIF and it moves there too.": "Этот WEBP анимированный. На сайте он будет двигаться, но канал анонсов сможет опубликовать только стоп-кадр — Telegram не умеет проигрывать анимированный WEBP. Загрузите ту же картинку в GIF, и там она тоже будет двигаться.", "this window": "этот период", - "This XCP is escrowed by consensus. It returns automatically if a launch misses its soft cap; if the launch graduates, you receive the tokens shown below instead.": "Эти XCP находятся в эскроу по консенсусу. Они вернутся автоматически, если запуск не доберёт софткап; если запуск выпустится, вместо них вы получите токены, показанные ниже.", + "This XCP is escrowed by consensus. It returns automatically if a launch misses its soft cap; if the launch graduates, you receive the tokens shown below instead.": "Эти XCP находятся в эскроу по консенсусу. Они вернутся автоматически, если запуск не доберёт софткап; если запуск откроет пул, вместо них вы получите токены, показанные ниже.", "today": "сегодня", "token": "токен", "Token": "Токен", @@ -1103,7 +1104,7 @@ "Total": "Итого", "Total in XCP": "Итого в XCP", "Total PnL": "Общий PnL", - "Total PnL combines profit or loss already realized by partial sales with the unrealized result on tokens still held. It uses average-cost accounting over your indexed mint-and-trade history. Incoming sends and distributions enter at their market value on arrival. A dash means another balance movement could not be reconciled or priced, so exact PnL is not claimed. Positions cover graduated XCP-69 launches — the ones with a locked pool quoting them against XCP.": "Общий PnL объединяет прибыль или убыток, уже реализованные частичными продажами, с нереализованным результатом по ещё удерживаемым токенам. Используется учёт по средней стоимости на основе вашей проиндексированной истории минтов и сделок. Входящие переводы и выплаты учитываются по рыночной стоимости на момент получения. Прочерк означает, что какое-то движение баланса не удалось сверить или оценить, поэтому точный PnL не заявляется. Позиции охватывают выпустившиеся запуски XCP-69 — те, у которых есть заблокированный пул с котировкой к XCP.", + "Total PnL combines profit or loss already realized by partial sales with the unrealized result on tokens still held. It uses average-cost accounting over your indexed mint-and-trade history. Incoming sends and distributions enter at their market value on arrival. A dash means another balance movement could not be reconciled or priced, so exact PnL is not claimed. Positions cover graduated XCP-69 launches — the ones with a locked pool quoting them against XCP.": "Общий PnL объединяет прибыль или убыток, уже реализованные частичными продажами, с нереализованным результатом по ещё удерживаемым токенам. Используется учёт по средней стоимости на основе вашей проиндексированной истории минтов и сделок. Входящие переводы и выплаты учитываются по рыночной стоимости на момент получения. Прочерк означает, что какое-то движение баланса не удалось сверить или оценить, поэтому точный PnL не заявляется. Позиции охватывают запуски XCP-69 с открытыми пулами — те, у которых есть заблокированный пул с котировкой к XCP.", "Track record {score}/100 ({tier}) — observed on-chain reputation from the XCP.io explorer, not an endorsement.": "Репутация {score}/100 ({tier}) — наблюдаемая ончейн-репутация по данным обозревателя XCP.io, не рекомендация.", "trade": "сделка", "Trade from your connected wallet": "Торгуйте из подключённого кошелька", @@ -1121,7 +1122,7 @@ "Translated automatically": "Переведено автоматически", "Trustless token launches on Counterparty. All-or-nothing mints, liquidity locked by consensus, no platform custody.": "Запуски токенов на Counterparty без доверия. Минты по принципу «всё или ничего», ликвидность заблокирована консенсусом, платформа не хранит средства.", "Try again": "Попробовать снова", - "Two bounties are claimed — one is still open for the next launch to graduate.": "Две награды получены — одна ещё ждёт следующий запуск, который выпустится.", + "Two bounties are claimed — one is still open for the next launch to graduate.": "Две награды получены — одна ещё ждёт следующий запуск, который откроет пул.", "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": "Две честные оговорки. Ползунок — это граничная огибающая: чистый односторонний поток, либо все продают, либо все покупают, без чередования. Реальные рынки смешивают оба направления, и каждый круг оставляет в пуле 50 б.п., так что реальная траектория лежит строго внутри этих крайностей. И капитализация = цена × предложение в обращении — выпущенное предложение минус сожжённые токены. Это условность, а не обещание.", "TX fee": "Комиссия майнеров", "TX fees": "Комиссии майнеров", @@ -1191,7 +1192,6 @@ "XCP in the pool": "XCP в пуле", "XCP market · cached 15 min": "Рынок XCP · кэш 15 мин", "XCP Price": "Цена XCP", - "XCP reserve ÷ token reserve. Moves with every swap; there is no order book and no market maker.": "Резерв XCP ÷ резерв токенов. Меняется с каждым обменом; ни книги ордеров, ни маркетмейкера нет.", "XCP Rewards": "Награды XCP", "XCP Rewards — xcp.fun": "Награды XCP — xcp.fun", "XCP to receive": "XCP к получению", @@ -1212,7 +1212,7 @@ "you": "вы", "you · {n} XCP": "вы · {n} XCP", "you have minted this launch’s max": "вы заминтили максимум для этого запуска", - "You mint": "Вы получаете", + "You mint": "Вы минтите", "You own {name}, but {reason}.": "{name} принадлежит вам, но {reason}.", "You pay": "Вы платите", "You receive": "Вы получаете", @@ -1227,7 +1227,7 @@ "Your lifetime earnings": "Заработано вами за всё время", "Your LP balance": "Ваш баланс LP", "Your position: {position} · {pct} of the pool": "Ваша позиция: {position} · {pct} пула", - "Your positions, history, and launches. Every graduated XCP-69 launch trades against XCP, so a position's value is a pool price away from a dollar figure.": "Ваши позиции, история и запуски. Каждый выпустившийся запуск XCP-69 торгуется к XCP, поэтому от стоимости позиции до суммы в долларах — одна цена пула.", + "Your positions, history, and launches. Every graduated XCP-69 launch trades against XCP, so a position's value is a pool price away from a dollar figure.": "Ваши позиции, история и запуски. После открытия пула каждый токен XCP-69 торгуется к XCP, поэтому его цена в пуле позволяет рассчитать стоимость позиции в долларах.", "Your profile": "Ваш профиль", "Your wallet could not complete this request. Check the details.": "Кошелёк не смог выполнить этот запрос. Проверьте подробности.", "Your wallet supplied a connection signature that didn't verify, so editing is locked for this session. Disconnect and reconnect to try again.": "Кошелёк передал подпись подключения, которую не удалось проверить, поэтому редактирование заблокировано на эту сессию. Отключитесь и подключитесь снова, чтобы попробовать ещё раз.", diff --git a/apps/web/src/locales/ru.status.json b/apps/web/src/locales/ru.status.json index 2d90807e..65647856 100644 --- a/apps/web/src/locales/ru.status.json +++ b/apps/web/src/locales/ru.status.json @@ -237,7 +237,7 @@ "Every minted token dumped in one trade — the pool still quotes a bid with {left} XCP left, and sellers exit at {mult}× mint ({paid}% of what they paid). Those {left} XCP can never be withdrawn: every launch is a permanent sink for XCP itself.", "Every route has a purchase pending in the mempool — check back in a few minutes.", "Everything about XCP-69, in one place. The first half explains how launches work for anyone. The second half is for developers who want to read or build on the same on-chain data this site does.", - "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, what it costs (nothing), and how to integrate.", + "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, fees, and how to integrate.", "Everything is curl-able. Launches and mints:", "Everything this site displays comes from public APIs. You can build your own launchpad, bot, or dashboard on the same data — nothing below requires our permission.", "Expiration", @@ -268,7 +268,7 @@ "Get XCP — xcp.fun", "Graduated", "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.", - "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses, at which point the pool is created and its liquidity is burned. A launch that misses its target refunds every satoshi by consensus and does not count. {link}", + "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses. The pool is created and its initial LP tokens go to an unspendable address, permanently locking that liquidity. A launch that misses its target returns the contributed XCP by consensus and does not count. {link}", "Graduation", "Graveyard", "Graveyard pages", @@ -456,7 +456,7 @@ "No trades yet.", "No value chart for {assets}: its recent transfer or liquidity history did not load completely, so we cannot know how many tokens this wallet held at each point. Current holdings and value are live and accurate.", "No value chart: its recent transfer or liquidity history did not load completely, so we cannot know how many tokens this wallet held at each point. Current holdings and value are live and accurate.", - "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.", + "A mint locks your XCP until the launch resolves. If it graduates, your XCP funds the pool and you receive tokens, which can lose value. If it refunds, your XCP is returned. The Bitcoin network fee is paid in either case; rewards do not guarantee that every cost is covered.", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.", "Nobody has minted yet. The first row here is available.", @@ -780,7 +780,7 @@ "XCP in pools", "XCP in the pool", "XCP market · cached 15 min", - "XCP reserve ÷ token reserve. Moves with every swap; there is no order book and no market maker.", + "Pool price is XCP reserve ÷ token reserve. Swaps change this ratio. Limit orders can also trade on Counterparty's order book.", "XCP to receive", "XCP to unload", "XCP-69 is a fixed-parameter token launch standard built on Counterparty {pools}. There is no factory contract, no admin key, and no platform custody at any point: the protocol {is} the platform. Every action — creating a launch, minting, swapping — is a transaction you sign in your own wallet and broadcast to Bitcoin. This site is an interface over public on-chain data; if it disappeared tomorrow, every launch, refund, and pool would keep working exactly as before.", diff --git a/apps/web/src/locales/uk.json b/apps/web/src/locales/uk.json index 4c412d6f..cc5d34e1 100644 --- a/apps/web/src/locales/uk.json +++ b/apps/web/src/locales/uk.json @@ -12,7 +12,7 @@ "“Coordinated first exit” is an optimistic upper bound. “Random order” is the expected result if all 69 full bags sell and these wallets are interleaved uniformly among them. Neither line asserts how many people actually participated.": "«Узгоджений перший вихід» — це оптимістична верхня межа. «Випадковий порядок» — очікуваний результат, якщо всі 69 повних мішків продаються, а ці гаманці рівномірно перемішані між ними. Жодна з ліній не стверджує, скільки людей насправді брало участь.", "(optional)": "(необов'язково)", "(you own this asset)": "(цей актив ваш)", - "{a} graduated · {b} refunded": "{a} випустилися · {b} повернено", + "{a} graduated · {b} refunded": "Пули відкрито: {a} · з поверненням: {b}", "{action} anyway": "{action} все одно", "{action} anyway — {pct}% slippage": "{action} все одно — прослизання {pct}%", "{age} ago": "{age} тому", @@ -67,8 +67,8 @@ "{n} conforming launches · chain tip {height}": "Запусків за стандартом: {n} · вершина блокчейну {height}", "{n} fill": "{n} виконання", "{n} fills": "виконань: {n}", - "{n} graduated · {pct}%": "випустилося: {n} · {pct}%", - "{n} held": "{n} тримали", + "{n} graduated · {pct}%": "З відкритим пулом: {n} · {pct}%", + "{n} held": "тримають: {n}", "{n} holder": "холдерів: {n}", "{n} holders": "холдерів: {n}", "{n} in the last {days} days": "{n} за останні {days} дн.", @@ -93,7 +93,7 @@ "{n} pending transactions": "транзакцій в очікуванні: {n}", "{n} refund · {xcp} XCP returned · about {when}": "повернень: {n} · повернуто {xcp} XCP · ≈ {when}", "{n} refunded · {pct}%": "повернено: {n} · {pct}%", - "{n} refunded hidden": "приховано: {n}", + "{n} refunded hidden": "приховано запусків із поверненням: {n}", "{n} refunds · {xcp} XCP returned · about {when}": "повернень: {n} · повернуто {xcp} XCP · ≈ {when}", "{n} routes → {n} wallet signatures": "{n} маршрутів → {n} підписів гаманця", "{n} sats": "{n} sats", @@ -141,7 +141,7 @@ "{pct} of supply": "{pct} пропозиції", "{pct}% filled": "виконано {pct}%", "{pct}% filled — the rest is resting on the book with {amount} {asset} escrowed.": "Виконано {pct}% — решта чекає в книзі ордерів, в ескроу {amount} {asset}.", - "{pct}% graduated ({a}/{b})": "{pct}% випустилися ({a}/{b})", + "{pct}% graduated ({a}/{b})": "З відкритим пулом: {pct}% ({a}/{b})", "{pct}% locked": "{pct}% заблоковано", "{pct}% minted": "{pct}% змінтовано", "{pct}% of ATH": "{pct}% від ATH", @@ -158,7 +158,7 @@ "{shown} of {total}": "{shown} з {total}", "{status} → minting": "{status} → триває мінт", "{status} → scheduled": "{status} → заплановано", - "{status} + {pool} returns a pool → graduated": "{status} + {pool} повертає пул → випустився", + "{status} + {pool} returns a pool → graduated": "{status} і {pool} повертає запис пулу → пул відкрито", "{status} with no pool row → refunded (corroborate with the destruction tagged \"soft cap not reached\")": "{status} без рядка пулу → повернено (підтвердьте записом про знищення з міткою \"soft cap not reached\")", "{supply} · {pool} pool": "{supply} · пул {pool}", "{taken} of {total} XCP": "{taken} із {total} XCP", @@ -196,8 +196,8 @@ "~6 hours": "~6 годин", "~7 days": "~7 днів", "−{pct}% under floor · sells first": "−{pct}% під мінімумом · продається першим", - "≈ {amount} · graduated": "≈ {amount} · випустилися", - "≈ {amount} · graduated coins": "≈ {amount} · монети, що випустилися", + "≈ {amount} · graduated": "≈ {amount} · монети з пулом", + "≈ {amount} · graduated coins": "≈ {amount} · монети з пулом", "≈ {amount} committed": "≈ {amount} внесено", "≈ {amount} committed to open mints": "≈ {amount} внесено у відкриті мінти", "≈ {amount} in pools": "≈ {amount} у пулах", @@ -207,7 +207,7 @@ "⏳ Scheduled — announced on-chain, nothing minted yet.": "⏳ Заплановано — оголошено ончейн, ще нічого не змінтовано.", "◆ Pool": "◆ Пул", "✓ broadcast": "✓ надіслано в мережу", - "✓ Graduated — pool created with {xcp} XCP{usd} + 31M tokens, LP burned.": "✓ Випустився — пул створено з {xcp} XCP{usd} + 31M токенів, LP спалено.", + "✓ Graduated — pool created with {xcp} XCP{usd} + 31M tokens, LP burned.": "✓ Пул відкрито — {xcp} XCP{usd} + 31M токенів, LP-токени спалено.", "💀 RIP": "💀 RIP", "0.01 XCP / 1,000 · sells out or refunds": "0,01 XCP / 1 000 · або розпродаж, або повернення", "0.01 XCP per 1,000-token lot": "0,01 XCP за лот із 1 000 токенів", @@ -227,6 +227,7 @@ "69,000,000 (the soft cap IS the whole sale)": "69 000 000 (софткап І Є всім продажем)", "a future block — announced on-chain before minting can open": "майбутній блок — оголошується ончейн до того, як мінт може відкритися", "A launch moves through four phases:": "Запуск проходить чотири фази:", + "A mint locks your XCP until the launch resolves. If it graduates, your XCP funds the pool and you receive tokens, which can lose value. If it refunds, your XCP is returned. The Bitcoin network fee is paid in either case; rewards do not guarantee that every cost is covered.": "Мінт блокує ваші XCP до завершення запуску. Якщо пул відкриється, ваші XCP поповнять його, а ви отримаєте токени, які можуть втратити вартість. Якщо запуск завершиться поверненням, ваші XCP повернуться. Комісія мережі Bitcoin сплачується в будь-якому разі; винагороди не гарантують покриття всіх витрат.", "A new way to launch a coin on Counterparty — every launch identical, enforced by consensus.": "Новий спосіб запустити монету на Counterparty — кожен запуск однаковий, і це гарантує консенсус.", "A pool never runs out, it only gets worse. This is what it can absorb before its price moves 1%.": "Пул ніколи не вичерпується — ціна лише погіршується. Це те, що він може поглинути, перш ніж ціна зрушиться на 1%.", "A refunded launch is not money lost. XCP-69 is all-or-nothing: one that misses its soft cap returns every satoshi by consensus, with no decision by us and no way for anyone to keep it.": "Запуск із поверненням — це не втрачені гроші. XCP-69 працює за принципом «все або нічого»: запуск, який не досяг софткапу, повертає кожен сатоші за консенсусом — без нашого рішення і без можливості для будь-кого їх утримати.", @@ -250,7 +251,7 @@ "Address limit reached": "Ліміт на адресу вичерпано", "addresses": "адреси", "adjusts to {n}": "буде округлено до {n}", - "After graduation": "Після випуску", + "After graduation": "Після відкриття пулу", "After mempool": "Після мемпулу", "All": "Усі", "All data shown on this site is public on-chain data; anything you see here you can verify yourself against a Counterparty node. The site is an interface, not a counterparty to any transaction — it never holds funds and cannot reverse, expedite, or refund anything (the protocol handles refunds on its own). Nothing here is investment advice; tokens launched through XCP-69 can and will lose value. Read the {link} before minting.": "Усі дані на цьому сайті — публічні ончейн-дані; усе, що ви тут бачите, можна самостійно перевірити через ноду Counterparty. Сайт — це інтерфейс, а не сторона будь-якої транзакції: він ніколи не тримає кошти й не може нічого скасувати, пришвидшити чи повернути (повернення протокол здійснює сам). Ніщо тут не є інвестиційною порадою; токени, запущені через XCP-69, можуть втратити вартість — і втрачатимуть. Прочитайте {link} перед мінтом.", @@ -267,7 +268,7 @@ "Amount of XCP to deposit": "Сума XCP для внесення", "Amount to remove": "Кількість для виведення", "Amount too small — rounds to 0": "Сума замала — округлюється до 0", - "An XCP bounty for the first three launches to graduate, and MINTS for every mint.": "Винагорода в XCP для перших трьох запусків, що випустяться, і MINTS за кожен мінт.", + "An XCP bounty for the first three launches to graduate, and MINTS for every mint.": "Винагорода в XCP за перші три запуски з відкритим пулом і MINTS за кожен мінт.", "an XCP fairminter on xcp.fun": "fairminter XCP на xcp.fun", "Animated WEBP shows as a still frame in the announce channel — Telegram cannot play one. A GIF moves there too.": "Анімований WEBP у каналі анонсів показується як нерухомий кадр — Telegram не вміє його програвати. GIF там рухається.", "Announce.": "Оголошення.", @@ -287,7 +288,7 @@ "at the live pool price": "за поточною ціною пулу", "Auto": "Авто", "Auto ({code})": "Авто ({code})", - "Auto sizes slippage to the trade: what this quote needs, currently ~{pct}%.": "Авто підбирає прослизання під угоду: стільки, скільки потрібно цьому котируванню, зараз ~{pct}%.", + "Auto sizes slippage to the trade: what this quote needs, currently ~{pct}%.": "Режим «Авто» підбирає допустиме прослизання для цієї угоди: зараз близько {pct}%.", "available": "вільне", "Available: {n}": "Доступно: {n}", "Balance": "Баланс", @@ -299,7 +300,7 @@ "Bid": "Купівля", "binary": "бінарний", "Bitcoin": "Біткоїн", - "Bitcoin fee rate in sats per vbyte": "Ставка комісії Bitcoin у sats за vbyte", + "Bitcoin fee rate in sats per vbyte": "Ставка комісії Bitcoin у сатоші за віртуальний байт", "Bitcoin market price in {currency}": "Ринкова ціна біткоїна в {currency}", "Bitcoin Price": "Ціна біткоїна", "Bitcoin tx fee": "Комісія майнерів Bitcoin", @@ -311,7 +312,7 @@ "book": "книга ордерів", "Bought": "Купівля", "bought without minting": "купили без мінту", - "Broadcast": "Надіслати в мережу", + "Broadcast": "Надіслано в мережу", "Broadcast as {tx}. Minting opens at block {block} ({when}) — until then the launch is announced on-chain and nobody, you included, can mint. Then it runs for 1,000 blocks (~7 days): it sells out, or everyone is refunded.": "Надіслано в мережу як {tx}. Мінт відкривається на блоці {block} ({when}) — до того запуск оголошено ончейн, і ніхто, включно з вами, не може мінтити. Далі він триває 1 000 блоків (~7 днів): або розпродається, або всі отримують повернення.", "Broadcast, waiting to confirm. Its page and editing open up once it lands in a block.": "Транзакцію надіслано, очікує підтвердження. Сторінка та редагування стануть доступні, щойно вона потрапить у блок.", "Broadcasting commit…": "Надсилання commit…", @@ -431,7 +432,7 @@ "Counterparty market price in {currency}": "Ринкова ціна Counterparty в {currency}", "Counterparty's canonical unspendable burn address": "Канонічна адреса спалювання Counterparty — з неї неможливо витратити", "Counterparty's DEX order is the single trading primitive: matching routes through the pool whenever the pool's price beats the order book. A {market} is an order at the router's quoted output — it fills from pool and book at best price immediately. A {limit} rests on the book, and the pool fills it automatically if its price ever crosses yours.": "DEX-ордер Counterparty — єдиний торговий примітив: зведення йде через пул щоразу, коли ціна пулу краща за книгу ордерів. {market} — це ордер за котируванням маршрутизатора: він одразу виконується з пулу та книги за найкращою ціною. {limit} лишається в книзі, і пул виконує його автоматично, якщо його ціна колись перетне вашу.", - "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.": "Підрахунок ведеться за унікальними адресами. «Тримали», «перемістили» і «продали» — взаємовиключні результати для мінтерів кожного запуску, що випустився. «Тримали» — продажу не було й поточний баланс суттєвий; «перемістили» — продажу не було й суттєвого балансу немає; «продали» — принаймні один продаж у пулі або в книзі ордерів, зафіксований xcp.fun.", + "Counts use unique addresses. Held, moved, and sold are exclusive outcomes for minters of each graduated launch. Held means no sale and a meaningful current balance; moved means no sale and no meaningful current balance; sold means at least one pool or order-book sale captured by xcp.fun.": "Підрахунок ведеться за унікальними адресами. «Тримають», «перемістили» і «продали» — взаємовиключні результати для мінтерів кожного запуску з відкритим пулом. «Тримають» — продажу не було й поточний баланс суттєвий; «перемістили» — продажу не було й суттєвого балансу немає; «продали» — принаймні один продаж у пулі або в книзі ордерів, зафіксований xcp.fun.", "Covered": "Покрито", "Create": "Створити", "Created": "Створено", @@ -445,8 +446,8 @@ "Currently unloading:": "Зараз продається назад:", "Custom": "Власний", "custom block": "власний блок", - "Custom liquidity slippage percent": "Власний відсоток прослизання ліквідності", - "Custom slippage percent": "Власний відсоток прослизання", + "Custom liquidity slippage percent": "Допустиме прослизання для ліквідності, %", + "Custom slippage percent": "Допустиме прослизання, %", "Daily": "Денні", "Data": "Дані", "day": "день", @@ -474,10 +475,10 @@ "Double network fee at this lead — there aren't enough blocks to re-fee if the estimate comes in low, and a launch that confirms after it opens fails the standard.": "Подвійна комісія мережі за такого запасу часу — не вистачить блоків, щоб підняти комісію, якщо оцінка виявиться низькою, а запуск, підтверджений після відкриття, не проходить стандарт.", "down": "вниз", "dropped — nothing was spent": "відхилено — нічого не витрачено", - "Dump means the first sale landed within six blocks of graduation. Repeat dump means that happened on more than one earlier launch. Balances come from Counterparty and may include tokens obtained outside the mint. Pending mempool orders can disappear without confirming or filling. Addresses are not necessarily people.": "Дамп означає, що перший продаж відбувся протягом шести блоків після випуску. Повторний дамп — що таке траплялося більш як в одному попередньому запуску. Баланси беруться з Counterparty і можуть містити токени, отримані поза мінтом. Ордери в очікуванні в мемпулі можуть зникнути без підтвердження чи виконання. Адреси — не обов'язково окремі люди.", + "Dump means the first sale landed within six blocks of graduation. Repeat dump means that happened on more than one earlier launch. Balances come from Counterparty and may include tokens obtained outside the mint. Pending mempool orders can disappear without confirming or filling. Addresses are not necessarily people.": "Дамп означає, що перший продаж відбувся протягом шести блоків після відкриття пулу. Повторний дамп — що таке траплялося більш як в одному попередньому запуску. Баланси беруться з Counterparty і можуть містити токени, отримані поза мінтом. Ордери в очікуванні в мемпулі можуть зникнути без підтвердження чи виконання. Адреси — не обов'язково окремі люди.", "Dumpers": "Дампери", "Dumping now": "Зливають зараз", - "Each number counts unique addresses. A sale is a pool or order-book sale captured by xcp.fun. Dump means the address sold within {blocks} blocks of graduation. A meaningful balance is more than one token and more than 1% of the address's acquired amount.": "Кожне число рахує унікальні адреси. Продаж — це продаж у пулі або в книзі ордерів, зафіксований xcp.fun. Дамп означає, що адреса продала протягом {blocks} блоків після випуску. Суттєвий баланс — це більше одного токена і більше 1% від обсягу, який адреса придбала.", + "Each number counts unique addresses. A sale is a pool or order-book sale captured by xcp.fun. Dump means the address sold within {blocks} blocks of graduation. A meaningful balance is more than one token and more than 1% of the address's acquired amount.": "Кожне число рахує унікальні адреси. Продаж — це продаж у пулі або в книзі ордерів, зафіксований xcp.fun. Дамп означає, що адреса продала протягом {blocks} блоків після відкриття пулу. Суттєвий баланс — це більше одного токена і більше 1% від обсягу, який адреса придбала.", "Each route is a separate transaction. Broadcast routes cannot be canceled here. Failed routes can be retried.": "Кожен маршрут — окрема транзакція. Уже надіслані в мережу маршрути не можна скасувати тут. Невдалі можна повторити.", "Earned": "Зароблено", "Earned so far": "Зароблено дотепер", @@ -495,13 +496,13 @@ "Event": "Подія", "Every launch that conforms to XCP-69, and what has actually been minted.": "Кожен запуск, що відповідає XCP-69, і те, що насправді змінтовано.", "Every LP token is burned — this liquidity can never be withdrawn.": "Усі LP-токени спалено — цю ліквідність неможливо вивести.", - "Every mint on a conforming launch, counted. Ranked by mints, because that is the unit the reward is paid in.": "Кожен мінт у запуску за стандартом, підрахований. Рейтинг за кількістю мінтів, бо саме в цій одиниці виплачується винагорода.", + "Every mint on a conforming launch, counted. Ranked by mints, because that is the unit the reward is paid in.": "Враховується кожен мінт у запуску за стандартом. Рейтинг будується за кількістю мінтів, оскільки винагорода нараховується за кожну транзакцію мінту.", "Every minted token dumped in one trade — the pool still quotes a bid with {left} XCP left, and sellers exit at {mult}× mint ({paid}% of what they paid). Those {left} XCP can never be withdrawn: every launch is a permanent sink for XCP itself.": "Кожен змінтований токен злито однією угодою — пул усе ще котирує купівлю з {left} XCP у залишку, а продавці виходять за {mult}× від мінту ({paid}% від сплаченого). Ці {left} XCP ніколи не можна вивести: кожен запуск — постійний стік для самого XCP.", "Every route has a purchase pending in the mempool — check back in a few minutes.": "На кожному маршруті купівля очікує в мемпулі — поверніться за кілька хвилин.", "Every XCP-69 launch is identical: 100M supply, 69M public sale at 0.01 XCP per 1,000-token lot, 31M reserved for the liquidity pool, 10 XCP per-address cap, an on-chain pre-announcement before minting opens, and a 1,000-block window. There is no fine print to read because there is no fine print. The full parameter set lives on the {link} page.": "Кожен запуск XCP-69 однаковий: пропозиція 100M, публічний продаж 69M по 0,01 XCP за лот із 1 000 токенів, 31M зарезервовано для пулу ліквідності, ліміт 10 XCP на адресу, ончейн-оголошення до відкриття мінту та вікно в 1 000 блоків. Читати дрібний шрифт не потрібно, бо його немає. Повний набір параметрів — на сторінці {link}.", "Every XCP-69 mint, trade, burn, resting order and launch, newest first.": "Кожен мінт, угода, спалювання, ордер у книзі та запуск XCP-69 — спочатку найновіші.", "everyone dumps": "усі зливають", - "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, what it costs (nothing), and how to integrate.": "Усе про XCP-69 в одному місці: як працюють запуски, як формується ціна, що означає випуск, скільки це коштує (нічого) і як інтегруватися.", + "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, fees, and how to integrate.": "Усе про XCP-69 в одному місці: як працюють запуски, як формується ціна, що означає відкриття пулу, які є комісії та як інтегруватися.", "Everything about XCP-69, in one place. The first half explains how launches work for anyone. The second half is for developers who want to read or build on the same on-chain data this site does.": "Усе про XCP-69 в одному місці. Перша половина пояснює, як працюють запуски, для будь-кого. Друга — для розробників, які хочуть читати ті самі ончейн-дані, що й цей сайт, або будувати на них.", "Everything is curl-able. Launches and mints:": "Усе доступне через curl. Запуски та мінти:", "Everything this site displays comes from public APIs. You can build your own launchpad, bot, or dashboard on the same data — nothing below requires our permission.": "Усе, що показує цей сайт, надходить із публічних API. Ви можете побудувати власний лаунчпад, бота чи дашборд на тих самих даних — ніщо з наведеного нижче не потребує нашого дозволу.", @@ -545,12 +546,12 @@ "Get XCP — xcp.fun": "Отримати XCP — xcp.fun", "Get XCP before it opens": "Придбайте XCP до відкриття", "gotchas": "підводні камені", - "graduated": "випустився", - "Graduated": "Випустився", - "graduated coins": "монети, що випустилися", - "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.": "Токени, що випустилися, торгуються проти пулу TOKEN/XCP зі сталим добутком. Ціна — це просто співвідношення резервів пулу; кожен обмін її зсуває. Фіксована комісія {fee} з кожного обміну сплачується самому пулу — а оскільки LP спалено, комісії поглиблюють заблоковану ліквідність, а не виплачуються комусь.", - "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses, at which point the pool is created and its liquidity is burned. A launch that misses its target refunds every satoshi by consensus and does not count. {link}": "Випуск означає повний розпродаж: {xcp} XCP зібрано щонайменше з {n} різних адрес, після чого створюється пул і його ліквідність спалюється. Запуск, що не досяг цілі, повертає кожен сатоші за консенсусом і не враховується. {link}", - "Graduation": "Випуск", + "graduated": "пул відкрито", + "Graduated": "Пул відкрито", + "graduated coins": "монети з відкритим пулом", + "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.": "Після відкриття пулу токени торгуються в пулі TOKEN/XCP зі сталим добутком. Ціна — співвідношення резервів пулу; кожен обмін її змінює. Фіксована комісія {fee} з кожного обміну залишається в пулі. Оскільки початкові LP-токени спалено, комісії збільшують заблоковану ліквідність, а не виплачуються комусь.", + "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses. The pool is created and its initial LP tokens go to an unspendable address, permanently locking that liquidity. A launch that misses its target returns the contributed XCP by consensus and does not count. {link}": "Пул відкривається після повного розпродажу токенів: зібрано {xcp} XCP щонайменше з {n} різних адрес. Створюється пул, а його початкові LP-токени надходять на адресу, з якої неможливо витратити кошти, назавжди блокуючи цю ліквідність. Якщо запуск не досягає мети, внесені XCP повертаються за правилами консенсусу, а запуск не зараховується. {link}", + "Graduation": "Відкриття пулу", "Graveyard": "Цвинтар", "Graveyard — xcp.fun": "Цвинтар — xcp.fun", "Graveyard pages": "Сторінки цвинтаря", @@ -668,7 +669,7 @@ "log": "лог.", "Lots": "Лоти", "LP fee": "Комісія LP", - "LP minted (est.)": "LP змінтовано (оцінка)", + "LP minted (est.)": "LP до отримання (оцінка)", "LP positions": "Позиції LP", "LP token {asset}": "LP-токен {asset}", "LP value": "Вартість LP", @@ -743,14 +744,13 @@ "New buyers": "Нові покупці", "Newest": "Нові", "Next →": "Далі →", - "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.": "Ні — мінт лише блокує XCP в ескроу під запуск. Якщо він випуститься, ви отримуєте токени; якщо буде повернення — ви отримуєте назад кожен сатоші. Єдине, що мінт справді вам коштує, — це комісія біткоїна, і саме цю частину покриває програма.", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.": "Ні — і в цьому суть. Протокол fairminter допускає величезну варіативність: без софткапу, без повернень, комісії творця до 99%, премайни (частка токенів, залишена засновникам), взагалі без пулу. XCP-69 фіксує одну його конфігурацію, і цей сайт показує лише запуски, що точно відповідають стандарту, перевірені поле за полем за ончейн-записом. Тут кожен запуск поводиться однаково, і вам ніколи не доводиться самостійно перевіряти параметри. Fairminter, який ви зустрінете деінде, такої гарантії не має — читайте його поля перед мінтом.", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.": "Ні — вона структурна, а не обіцяна. Пул мусить відкритися на 69/31 від ціни мінту, бо інших кількостей просто не існує, але дно опускається у міру того, як у нього продають. Ніщо не заважає токену торгуватися нижче мінту. Пул гарантує заявку на купівлю, яка ніколи не дійде до нуля, — а не заявку, яка вам сподобається.", "No active {give}/{get} pool liquidity. A resting order can still fill through the order book.": "У пулі {give}/{get} немає активної ліквідності. Ордер у книзі все ще може виконатися через книгу ордерів.", "No closed positions yet.": "Закритих позицій поки немає.", "no history": "історії немає", "No holders found.": "Холдерів не знайдено.", - "No launches have graduated yet. The first sell-out seeds the first pool — and it becomes tradeable here in the same block.": "Жоден запуск ще не випустився. Перший повний розпродаж створить перший пул — і торгувати ним тут можна буде в тому самому блоці.", + "No launches have graduated yet. The first sell-out seeds the first pool — and it becomes tradeable here in the same block.": "Жоден запуск ще не відкрив пул. Перший повний розпродаж створить перший пул — і токен стане доступним для торгівлі тут у тому самому блоці.", "No launches in this phase yet.": "У цій фазі запусків поки немає.", "No launches in this phase.": "У цій фазі запусків немає.", "No launches queued. Every XCP-69 launch broadcast so far has confirmed.": "Запусків у черзі немає. Кожен надісланий дотепер запуск XCP-69 уже підтверджено.", @@ -772,7 +772,7 @@ "No orders for {asset} are waiting in the mempool.": "У мемпулі немає ордерів на {asset}.", "No orders queued — every order so far has confirmed.": "Ордерів у черзі немає — кожен ордер дотепер уже підтверджено.", "No platform, no creator fees": "Без комісій платформи й творця", - "No pools yet. A launch has to graduate before its liquidity exists.": "Пулів поки немає. Ліквідність з'являється лише після випуску запуску.", + "No pools yet. A launch has to graduate before its liquidity exists.": "Пулів поки немає. Ліквідність з’явиться, коли запуск повністю розпродасть токени й відкриє пул.", "No quote for this pair": "Немає котирування для цієї пари", "No sells waiting in the mempool.": "У мемпулі немає продажів в очікуванні.", "No slider on this one — there is nothing to configure. Every launch is identical, checked field-by-field against the on-chain record.": "Тут немає повзунка — налаштовувати нічого. Кожен запуск однаковий, перевірений поле за полем за ончейн-записом.", @@ -784,7 +784,7 @@ "No value chart: its recent transfer or liquidity history did not load completely, so we cannot know how many tokens this wallet held at each point. Current holdings and value are live and accurate.": "Графіка вартості немає: нещодавня історія переказів або ліквідності завантажилася не повністю, тож ми не можемо знати, скільки токенів мав цей гаманець у кожен момент. Поточні активи та їхня вартість актуальні й точні.", "No XCP committed": "XCP не внесено", "No XCP committed to open mints right now.": "Зараз у відкриті мінти не внесено жодного XCP.", - "No XCP-69 launch has graduated yet. The first three to do it earn a bounty.": "Жоден запуск XCP-69 ще не випустився. Перші три, що це зроблять, отримають винагороду.", + "No XCP-69 launch has graduated yet. The first three to do it earn a bounty.": "Жоден запуск XCP-69 ще не відкрив пул. Перші три отримають винагороду.", "No XCP-69 tokens have been sent to the Counterparty burn address yet.": "На адресу спалювання Counterparty ще не надіслано жодного токена XCP-69.", "Nobody has minted yet. The first row here is available.": "Ніхто ще не мінтив. Перший рядок тут вільний.", "Non-conforming": "Не за стандартом", @@ -796,7 +796,7 @@ "not launchable": "запуск неможливий", "not XCP-69": "не XCP-69", "nothing": "нічого", - "Nothing has traded yet. A launch has to graduate before it has a market.": "Угод ще не було. Ринок у запуску з'являється лише після випуску.", + "Nothing has traded yet. A launch has to graduate before it has a market.": "Угод ще не було. Торгівля починається після повного розпродажу токенів і відкриття пулу.", "Nothing here. That's good.": "Тут порожньо. І це добре.", "Nothing matches “{query}”.": "За запитом «{query}» нічого не знайдено.", "Nothing open matches “{query}” — {n} refunded launch hidden. Type a full name to open one.": "Серед відкритих за запитом «{query}» нічого немає — приховано повернених запусків: {n}. Введіть повну назву, щоб відкрити.", @@ -816,14 +816,14 @@ "Of the launches that finished": "Серед завершених запусків", "of the typical mint fee": "від типової комісії за мінт", "of which {amount} realized": "з них реалізовано {amount}", - "Of XCP minted": "Серед змінтованих XCP", + "Of XCP minted": "Частка XCP, внесених у мінти", "Off-chain by default: the chain permanently carries the URL of the asset-info JSON, and the content behind it is curatable by the asset's current owner (wallet-signature gated). Taproot creators can instead inscribe the image on-chain as the permanent description — no off-chain dependency at all. Either way, nothing economic depends on hosted content.": "За замовчуванням поза ланцюгом: блокчейн назавжди зберігає URL JSON-файлу з інформацією про актив, а вміст за ним може редагувати поточний власник активу (через підпис гаманцем). Творці з taproot можуть натомість записати зображення ончейн як постійний опис — без жодної залежності від хостингу. У будь-якому разі ніщо економічне не залежить від розміщеного контенту.", - "On other launchpads, graduation is a threshold inside a live market: the token trades on a curve while everyone hopes it reaches the magic number. XCP-69 graduation is {binary} and happens before any trading exists. Sell out the 69M sale within 1,000 blocks and the launch graduates — pool seeded, LP burned, supply locked, trading live in the same block's resolution phase. Miss it and the launch never trades at all.": "На інших лаунчпадах випуск — це поріг усередині живого ринку: токен торгується по кривій, поки всі сподіваються, що він дійде до магічного числа. Випуск в XCP-69 — {binary} і відбувається до того, як існує будь-яка торгівля. Розпродайте 69M за 1 000 блоків — і запуск випускається: пул засіяно, LP спалено, пропозицію заблоковано, торгівля стартує у фазі завершення того ж блоку. Не встигли — і запуск не торгується взагалі.", + "On other launchpads, graduation is a threshold inside a live market: the token trades on a curve while everyone hopes it reaches the magic number. XCP-69 graduation is {binary} and happens before any trading exists. Sell out the 69M sale within 1,000 blocks and the launch graduates — pool seeded, LP burned, supply locked, trading live in the same block's resolution phase. Miss it and the launch never trades at all.": "На інших лаунчпадах відкриття пулу — поріг усередині вже чинного ринку: токен торгується за кривою, поки всі сподіваються, що він досягне потрібного значення. У XCP-69 результат {binary} і визначається до початку торгівлі. Усі 69M токенів продажу мають бути розкуплені за 1 000 блоків: тоді пул отримує ліквідність, LP-токени спалюються, обсяг емісії фіксується, а торгівля стає доступною на етапі завершення того самого блоку. Якщо мети не досягнуто, торгівля токеном так і не починається.", "on their Counterparty balance": "на балансі Counterparty", "on-chain asset name": "ончейн-ім'я активу", "on-chain since {year}": "ончейн з {year}", "Onchain events": "Ончейн-події", - "One bounty is claimed — two are still open for the next launches to graduate.": "Одну винагороду отримано — ще дві чекають на наступні запуски, що випустяться.", + "One bounty is claimed — two are still open for the next launches to graduate.": "Одну винагороду отримано — ще дві чекають на наступні запуски, які відкриють пули.", "One fixed parameter set for token launches on Counterparty: the launch sells out and liquidity locks forever, or everyone is refunded. Zero creator take, enforced by consensus.": "Один фіксований набір параметрів для запуску токенів на Counterparty: або запуск розпродається й ліквідність блокується назавжди, або всі отримують повернення. Нуль для творця — гарантує консенсус.", "One integration trap worth knowing: Counterparty Core 11.3+ returns {poolNormalized} and {maxNormalized} under {verbose}. Use them for display, but keep standard conformance math in raw integer satoshi units (×10⁸) so comparisons remain exact and match non-verbose event and mempool data.": "Одна пастка інтеграції, про яку варто знати: Counterparty Core 11.3+ повертає {poolNormalized} та {maxNormalized} при {verbose}. Використовуйте їх для показу, але математику відповідності стандарту тримайте в сирих цілих сатоші (×10⁸), щоб порівняння лишалися точними та збігалися з даними подій і мемпулу без verbose.", "One satoshi under the cheapest open dispenser": "На один сатоші нижче за найдешевший відкритий автомат", @@ -877,7 +877,7 @@ "Performance (All)": "Дохідність (усе)", "Phase": "Фаза", "Place limit buy": "Виставити лімітну купівлю", - "Place limit orders on graduated XCP-69 launches. Your price is enforced by the order itself — fills through the pool at confirmation or rests on the book.": "Розміщуйте лімітні ордери на запуски XCP-69, що випустилися. Вашу ціну гарантує сам ордер — він виконується через пул під час підтвердження або чекає в книзі.", + "Place limit orders on graduated XCP-69 launches. Your price is enforced by the order itself — fills through the pool at confirmation or rests on the book.": "Розміщуйте лімітні ордери на токени XCP-69 з відкритим пулом. Ордер дотримується вашої лімітної ціни: виконується через пул під час підтвердження або залишається в книзі ордерів.", "Place limit sell": "Виставити лімітний продаж", "PNG, JPEG, WEBP or GIF · {limit} · square (1:1) recommended": "PNG, JPEG, WEBP або GIF · {limit} · рекомендовано квадрат (1:1)", "PnL is withheld because the live balance includes activity outside indexed mints and trades.": "PnL не показано, бо поточний баланс містить активність поза проіндексованими мінтами та угодами.", @@ -888,6 +888,7 @@ "Pool + order book": "Пул + книга ордерів", "Pool is empty": "Пул порожній", "Pool price × circulating supply (issued minus burned). A convention, not a promise — the pool could not pay it out.": "Ціна пулу × пропозиція в обігу (емітовано мінус спалено). Умовність, а не обіцянка — пул не зміг би цього виплатити.", + "Pool price is XCP reserve ÷ token reserve. Swaps change this ratio. Limit orders can also trade on Counterparty's order book.": "Ціна пулу — резерв XCP ÷ резерв токенів. Обміни змінюють це співвідношення. Лімітні ордери також можуть виконуватися в книзі ордерів Counterparty.", "Pool reserve": "Резерв пулу", "Pool:": "Пул:", "Pools": "Пули", @@ -1043,7 +1044,7 @@ "Swap {give} for {get}": "Обміняти {give} на {get}", "Swap {give} for {get} — market order": "Обміняти {give} на {get} — ринковий ордер", "Swap broadcast — {tx}": "Обмін надіслано в мережу — {tx}", - "Swap graduated XCP-69 assets through Counterparty pools and the order book, including direct token pairs when liquidity exists.": "Обмінюйте активи XCP-69, що випустилися, через пули Counterparty та книгу ордерів, зокрема прямі пари токенів, коли є ліквідність.", + "Swap graduated XCP-69 assets through Counterparty pools and the order book, including direct token pairs when liquidity exists.": "Обмінюйте активи XCP-69 з відкритими пулами через пули Counterparty та книгу ордерів, зокрема прямі пари токенів за наявності ліквідності.", "Swap settings": "Налаштування обміну", "Switch wallet": "Змінити гаманець", "Table view": "Таблиця", @@ -1068,7 +1069,7 @@ "The first fairminter ever created on Counterparty — block 866,297, before any other, and it minted out free to 1,376 addresses. The supply is 100,000,000, locked forever; no more can ever be issued. The live MINTS/XCP pool prices the reward: right now 1 MINTS trades at {price} XCP, so {n} MINTS is {worth} XCP.": "Найперший fairminter, створений у Counterparty — блок 866 297, раніше за будь-який інший, і його безкоштовно розібрали 1 376 адрес. Пропозиція — 100 000 000, заблокована назавжди; більше випустити неможливо. Ціну винагороди задає живий пул MINTS/XCP: зараз 1 MINTS торгується по {price} XCP, тож {n} MINTS — це {worth} XCP.", "The first XCP-69 launch in history hasn't happened yet. All-or-nothing mints, at least {n} participants required, every raised XCP locked into the pool forever — enforced by consensus, not by this website.": "Першого в історії запуску XCP-69 ще не було. Мінти «усе або нічого», потрібно щонайменше {n} учасників, кожен зібраний XCP назавжди блокується в пулі — це гарантує консенсус, а не цей сайт.", "The full lifecycle is observable as Counterparty events via {endpoint}:": "Повний життєвий цикл можна спостерігати як події Counterparty через {endpoint}:", - "The graduation bounty": "Винагорода за випуск", + "The graduation bounty": "Винагорода за відкриття пулу", "The graveyard is unavailable right now.": "Цвинтар зараз недоступний.", "The honest questions — including the ones that don't flatter us.": "Чесні запитання — зокрема й ті, що нам не лестять.", "The image itself becomes the permanent on-chain description (commit + reveal, two signatures, higher fees scale with image size; the inscription is burned so it belongs to the asset forever). Max 385 KB. Taproot wallets only.": "Саме зображення стає постійним ончейн-описом (commit + reveal, два підписи, комісії зростають із розміром зображення; запис спалюється, тож належить активу назавжди). Макс. 385 КБ. Лише taproot-гаманці.", @@ -1089,7 +1090,7 @@ "This wallet hasn't launched anything yet.": "Цей гаманець ще нічого не запускав.", "This WEBP is animated. It will move on the site, but the announce channel can only post a still frame of it — Telegram has no way to play an animated WEBP. Upload the same art as a GIF and it moves there too.": "Цей WEBP анімований. На сайті він рухатиметься, але канал анонсів зможе опублікувати лише статичний кадр — Telegram не вміє програвати анімований WEBP. Завантажте ту саму картинку як GIF — і там вона теж рухатиметься.", "this window": "цей період", - "This XCP is escrowed by consensus. It returns automatically if a launch misses its soft cap; if the launch graduates, you receive the tokens shown below instead.": "Ці XCP перебувають в ескроу за консенсусом. Вони повертаються автоматично, якщо запуск не досягне софткапу; якщо запуск випуститься, ви натомість отримаєте токени, показані нижче.", + "This XCP is escrowed by consensus. It returns automatically if a launch misses its soft cap; if the launch graduates, you receive the tokens shown below instead.": "Ці XCP перебувають в ескроу за консенсусом. Вони повертаються автоматично, якщо запуск не досягне софткапу; якщо запуск відкриє пул, ви натомість отримаєте токени, показані нижче.", "today": "сьогодні", "token": "токен", "Token": "Токен", @@ -1103,7 +1104,7 @@ "Total": "Усього", "Total in XCP": "Усього у XCP", "Total PnL": "Загальний PnL", - "Total PnL combines profit or loss already realized by partial sales with the unrealized result on tokens still held. It uses average-cost accounting over your indexed mint-and-trade history. Incoming sends and distributions enter at their market value on arrival. A dash means another balance movement could not be reconciled or priced, so exact PnL is not claimed. Positions cover graduated XCP-69 launches — the ones with a locked pool quoting them against XCP.": "Загальний PnL поєднує прибуток або збиток, уже реалізований частковими продажами, з нереалізованим результатом за токенами, які ви ще тримаєте. Використовується облік за середньою собівартістю на основі вашої проіндексованої історії мінтів і угод. Вхідні перекази та виплати обліковуються за ринковою вартістю на момент надходження. Тире означає, що інший рух балансу не вдалося узгодити чи оцінити, тому точний PnL не стверджується. Позиції охоплюють запуски XCP-69, що випустилися — ті, що мають заблокований пул із котируванням до XCP.", + "Total PnL combines profit or loss already realized by partial sales with the unrealized result on tokens still held. It uses average-cost accounting over your indexed mint-and-trade history. Incoming sends and distributions enter at their market value on arrival. A dash means another balance movement could not be reconciled or priced, so exact PnL is not claimed. Positions cover graduated XCP-69 launches — the ones with a locked pool quoting them against XCP.": "Загальний PnL поєднує прибуток або збиток, уже реалізований частковими продажами, з нереалізованим результатом за токенами, які ви ще тримаєте. Використовується облік за середньою собівартістю на основі вашої проіндексованої історії мінтів і угод. Вхідні перекази та виплати обліковуються за ринковою вартістю на момент надходження. Тире означає, що інший рух балансу не вдалося узгодити чи оцінити, тому точний PnL не стверджується. Позиції охоплюють запуски XCP-69 з відкритими пулами — ті, що мають заблокований пул із котируванням до XCP.", "Track record {score}/100 ({tier}) — observed on-chain reputation from the XCP.io explorer, not an endorsement.": "Репутація {score}/100 ({tier}) — спостережена ончейн-репутація за даними оглядача XCP.io, а не рекомендація.", "trade": "угода", "Trade from your connected wallet": "Торгуйте з підключеного гаманця", @@ -1121,14 +1122,14 @@ "Translated automatically": "Перекладено автоматично", "Trustless token launches on Counterparty. All-or-nothing mints, liquidity locked by consensus, no platform custody.": "Запуски токенів на Counterparty без довіри. Мінти «усе або нічого», ліквідність блокує консенсус, без зберігання коштів платформою.", "Try again": "Спробувати ще раз", - "Two bounties are claimed — one is still open for the next launch to graduate.": "Дві винагороди отримано — ще одна чекає на наступний запуск, що випуститься.", - "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": "Дві чесні примітки. Повзунок — це граничний конверт: чистий односторонній потік, лише продавці або лише покупці, без чергування. Реальні ринки змішують обидва напрямки, і кожне коло туди-назад лишає в пулі 50 б.п., тож реальна траєкторія лежить строго всередині цих крайнощів. І капіталізація = ціна × пропозиція в обігу — емітована пропозиція мінус спалені токени. Це умовність, а не обіцянка.", + "Two bounties are claimed — one is still open for the next launch to graduate.": "Дві винагороди отримано — ще одна чекає на наступний запуск, який відкриє пул.", + "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": "Дві чесні примітки. Повзунок показує граничні сценарії: чистий односторонній потік, лише продавці або лише покупці, без чергування. Реальні ринки змішують обидва напрямки, і кожне коло туди-назад лишає в пулі 50 б.п., тож реальна траєкторія лежить строго всередині цих крайнощів. І капіталізація = ціна × пропозиція в обігу — емітована пропозиція мінус спалені токени. Це умовність, а не обіцянка.", "TX fee": "Комісія майнерів", "TX fees": "Комісії майнерів", "TX fees · {n} txs": "Комісії TX · {n} tx", "Type": "Тип", "unconfirmed": "очікує підтвердження", - "Unconfirmed and provisional — mempool mints can exceed what's left, get replaced, or never confirm. Only what lands on-chain counts.": "Непідтверджені й попередні дані — мінти в мемпулі можуть перевищувати залишок, бути заміненими або так і не підтвердитися. Рахується лише те, що потрапило в мережу.", + "Unconfirmed and provisional — mempool mints can exceed what's left, get replaced, or never confirm. Only what lands on-chain counts.": "Непідтверджені й попередні дані — мінти в мемпулі можуть перевищувати залишок, бути заміненими або так і не підтвердитися. Враховується лише те, що потрапило у блокчейн.", "Unconfirmed and provisional — these orders join the book only after confirmation.": "Непідтверджені й попередні дані — ці ордери потраплять у книгу лише після підтвердження.", "Underlying amounts unavailable": "Базові суми недоступні", "Unexplained balance movement": "Непояснений рух балансу", @@ -1191,7 +1192,6 @@ "XCP in the pool": "XCP у пулі", "XCP market · cached 15 min": "Ринок XCP · кеш 15 хв", "XCP Price": "Ціна XCP", - "XCP reserve ÷ token reserve. Moves with every swap; there is no order book and no market maker.": "Резерв XCP ÷ резерв токенів. Зсувається з кожним обміном; ні книги ордерів, ні маркетмейкера.", "XCP Rewards": "Винагороди XCP", "XCP Rewards — xcp.fun": "Винагороди XCP — xcp.fun", "XCP to receive": "XCP до отримання", @@ -1212,7 +1212,7 @@ "you": "ви", "you · {n} XCP": "ви · {n} XCP", "you have minted this launch’s max": "ви змінтували максимум для цього запуску", - "You mint": "Ви отримуєте", + "You mint": "Ви мінтите", "You own {name}, but {reason}.": "Ви володієте {name}, але {reason}.", "You pay": "Ви платите", "You receive": "Ви отримуєте", @@ -1227,7 +1227,7 @@ "Your lifetime earnings": "Ваш заробіток за весь час", "Your LP balance": "Ваш баланс LP", "Your position: {position} · {pct} of the pool": "Ваша позиція: {position} · {pct} пулу", - "Your positions, history, and launches. Every graduated XCP-69 launch trades against XCP, so a position's value is a pool price away from a dollar figure.": "Ваші позиції, історія та запуски. Кожен запуск XCP-69, що випустився, торгується до XCP, тож вартість позиції відділяє від суми в доларах лише ціна пулу.", + "Your positions, history, and launches. Every graduated XCP-69 launch trades against XCP, so a position's value is a pool price away from a dollar figure.": "Ваші позиції, історія та запуски. Після відкриття пулу кожен токен XCP-69 торгується до XCP, тому його ціна в пулі дає змогу розрахувати вартість позиції в доларах.", "Your profile": "Ваш профіль", "Your wallet could not complete this request. Check the details.": "Гаманець не зміг виконати цей запит. Перевірте подробиці.", "Your wallet supplied a connection signature that didn't verify, so editing is locked for this session. Disconnect and reconnect to try again.": "Ваш гаманець надав підпис підключення, який не пройшов перевірку, тому редагування заблоковано на цю сесію. Відключіться й підключіться знову, щоб спробувати ще раз.", diff --git a/apps/web/src/locales/uk.status.json b/apps/web/src/locales/uk.status.json index 2d90807e..65647856 100644 --- a/apps/web/src/locales/uk.status.json +++ b/apps/web/src/locales/uk.status.json @@ -237,7 +237,7 @@ "Every minted token dumped in one trade — the pool still quotes a bid with {left} XCP left, and sellers exit at {mult}× mint ({paid}% of what they paid). Those {left} XCP can never be withdrawn: every launch is a permanent sink for XCP itself.", "Every route has a purchase pending in the mempool — check back in a few minutes.", "Everything about XCP-69, in one place. The first half explains how launches work for anyone. The second half is for developers who want to read or build on the same on-chain data this site does.", - "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, what it costs (nothing), and how to integrate.", + "Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, fees, and how to integrate.", "Everything is curl-able. Launches and mints:", "Everything this site displays comes from public APIs. You can build your own launchpad, bot, or dashboard on the same data — nothing below requires our permission.", "Expiration", @@ -268,7 +268,7 @@ "Get XCP — xcp.fun", "Graduated", "Graduated tokens trade against a constant-product TOKEN/XCP pool. The price is simply the ratio of the pool's reserves; every swap moves it. A fixed {fee} fee on each swap is paid to the pool itself — and since the LP is burned, fees deepen the locked liquidity rather than paying anyone out.", - "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses, at which point the pool is created and its liquidity is burned. A launch that misses its target refunds every satoshi by consensus and does not count. {link}", + "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses. The pool is created and its initial LP tokens go to an unspendable address, permanently locking that liquidity. A launch that misses its target returns the contributed XCP by consensus and does not count. {link}", "Graduation", "Graveyard", "Graveyard pages", @@ -456,7 +456,7 @@ "No trades yet.", "No value chart for {assets}: its recent transfer or liquidity history did not load completely, so we cannot know how many tokens this wallet held at each point. Current holdings and value are live and accurate.", "No value chart: its recent transfer or liquidity history did not load completely, so we cannot know how many tokens this wallet held at each point. Current holdings and value are live and accurate.", - "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.", + "A mint locks your XCP until the launch resolves. If it graduates, your XCP funds the pool and you receive tokens, which can lose value. If it refunds, your XCP is returned. The Bitcoin network fee is paid in either case; rewards do not guarantee that every cost is covered.", "No — and that's the point. The fairminter protocol allows enormous variation: no soft cap, no refunds, creator commissions up to 99%, premines, no pool at all. XCP-69 pins one configuration of it, and this site lists only launches that match the standard exactly, checked field-by-field against the on-chain record. Here, every launch behaves identically and you never audit the parameters yourself. A fairminter you encounter anywhere else carries no such guarantee — read its fields before you mint.", "No — it's structural, not promised. The pool must open at 69/31 of mint price because those are the only quantities that exist, but the floor decays as people sell into it. Nothing stops a token trading below mint. What the pool guarantees is a bid that never reaches zero, not a bid you'll like.", "Nobody has minted yet. The first row here is available.", @@ -780,7 +780,7 @@ "XCP in pools", "XCP in the pool", "XCP market · cached 15 min", - "XCP reserve ÷ token reserve. Moves with every swap; there is no order book and no market maker.", + "Pool price is XCP reserve ÷ token reserve. Swaps change this ratio. Limit orders can also trade on Counterparty's order book.", "XCP to receive", "XCP to unload", "XCP-69 is a fixed-parameter token launch standard built on Counterparty {pools}. There is no factory contract, no admin key, and no platform custody at any point: the protocol {is} the platform. Every action — creating a launch, minting, swapping — is a transaction you sign in your own wallet and broadcast to Bitcoin. This site is an interface over public on-chain data; if it disappeared tomorrow, every launch, refund, and pool would keep working exactly as before.", From c906032c3210dbb4db91fb797c131f2f7f2262e5 Mon Sep 17 00:00:00 2001 From: Dan Anderson Date: Mon, 7 Sep 2026 19:19:04 -0400 Subject: [PATCH 09/10] docs: state pool fee per swap across translations --- apps/web/src/app/[lang]/faq/_components/explainer.tsx | 2 +- apps/web/src/locales/_source.json | 2 +- apps/web/src/locales/es.json | 2 +- apps/web/src/locales/es.status.json | 2 +- apps/web/src/locales/fr.json | 2 +- apps/web/src/locales/fr.status.json | 2 +- apps/web/src/locales/ja.json | 2 +- apps/web/src/locales/ja.status.json | 2 +- apps/web/src/locales/ko.json | 2 +- apps/web/src/locales/ko.status.json | 2 +- apps/web/src/locales/notes.json | 1 + apps/web/src/locales/pt.json | 2 +- apps/web/src/locales/pt.status.json | 2 +- apps/web/src/locales/ru.json | 2 +- apps/web/src/locales/ru.status.json | 2 +- apps/web/src/locales/uk.json | 2 +- apps/web/src/locales/uk.status.json | 2 +- apps/web/src/locales/zh-hk.json | 2 +- apps/web/src/locales/zh-hk.status.json | 2 +- apps/web/src/locales/zh-tw.json | 2 +- apps/web/src/locales/zh-tw.status.json | 2 +- apps/web/src/locales/zh.json | 2 +- apps/web/src/locales/zh.status.json | 2 +- 23 files changed, 23 insertions(+), 22 deletions(-) diff --git a/apps/web/src/app/[lang]/faq/_components/explainer.tsx b/apps/web/src/app/[lang]/faq/_components/explainer.tsx index 4f71e096..e12c7f99 100644 --- a/apps/web/src/app/[lang]/faq/_components/explainer.tsx +++ b/apps/web/src/app/[lang]/faq/_components/explainer.tsx @@ -394,7 +394,7 @@ function PoolStress({ xcpUsd }: { xcpUsd: number | null }) {

{t( - "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.", + "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every swap leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.", )}

diff --git a/apps/web/src/locales/_source.json b/apps/web/src/locales/_source.json index 6fd614e9..9901ce86 100644 --- a/apps/web/src/locales/_source.json +++ b/apps/web/src/locales/_source.json @@ -3593,7 +3593,7 @@ "Two bounties are claimed — one is still open for the next launch to graduate.": [ "app/[lang]/rewards/_components/rewards-content.tsx" ], - "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": [ + "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every swap leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": [ "app/[lang]/faq/_components/explainer.tsx" ], "TX fee": [ diff --git a/apps/web/src/locales/es.json b/apps/web/src/locales/es.json index c25b7a70..f3da407c 100644 --- a/apps/web/src/locales/es.json +++ b/apps/web/src/locales/es.json @@ -1122,7 +1122,7 @@ "Trustless token launches on Counterparty. All-or-nothing mints, liquidity locked by consensus, no platform custody.": "Lanzamientos de tokens sin confianza en terceros en Counterparty. Acuñaciones todo o nada, liquidez bloqueada por consenso, sin custodia de la plataforma.", "Try again": "Intentar de nuevo", "Two bounties are claimed — one is still open for the next launch to graduate.": "Dos premios ya fueron reclamados — queda uno abierto para el próximo lanzamiento que se gradúe.", - "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": "Dos notas de honestidad. El control deslizante es una envolvente — flujo puro en un solo sentido, todos vendedores o todos compradores, nada intercalado. Los mercados reales mezclan ambas direcciones, y cada ida y vuelta deja 50 bps en el pool, así que la trayectoria real vive estrictamente dentro de estos extremos. Y capitalización = precio × suministro en circulación — suministro emitido menos tokens quemados. Es una convención, no una promesa.", + "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every swap leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": "Dos notas de honestidad. El control deslizante es una envolvente — flujo puro en un solo sentido, todos vendedores o todos compradores, nada intercalado. Los mercados reales mezclan ambas direcciones, y cada intercambio deja 50 bps en el pool, así que la trayectoria real vive estrictamente dentro de estos extremos. Y capitalización = precio × suministro en circulación — suministro emitido menos tokens quemados. Es una convención, no una promesa.", "TX fee": "Comisión de TX", "TX fees": "Comisiones de TX", "TX fees · {n} txs": "Comisiones de TX · {n} tx", diff --git a/apps/web/src/locales/es.status.json b/apps/web/src/locales/es.status.json index 92df3437..0d2b997c 100644 --- a/apps/web/src/locales/es.status.json +++ b/apps/web/src/locales/es.status.json @@ -727,7 +727,7 @@ "Trustless token launches on Counterparty. All-or-nothing mints, liquidity locked by consensus, no platform custody.", "Try again", "Two bounties are claimed — one is still open for the next launch to graduate.", - "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.", + "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every swap leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.", "Type", "Unconfirmed and provisional — mempool mints can exceed what's left, get replaced, or never confirm. Only what lands on-chain counts.", "Unconfirmed and provisional — these orders join the book only after confirmation.", diff --git a/apps/web/src/locales/fr.json b/apps/web/src/locales/fr.json index 9e4e4007..60be5f10 100644 --- a/apps/web/src/locales/fr.json +++ b/apps/web/src/locales/fr.json @@ -1122,7 +1122,7 @@ "Trustless token launches on Counterparty. All-or-nothing mints, liquidity locked by consensus, no platform custody.": "Des lancements de tokens sans tiers de confiance sur Counterparty. Des mints tout ou rien, une liquidité verrouillée par le consensus, aucune garde par la plateforme.", "Try again": "Réessayer", "Two bounties are claimed — one is still open for the next launch to graduate.": "Deux primes ont été réclamées — une reste ouverte pour le prochain lancement gradué.", - "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": "Deux notes d'honnêteté. Le curseur est une enveloppe — un flux purement à sens unique, que des vendeurs ou que des acheteurs, rien d'entrelacé. Les vrais marchés mélangent les deux sens, et chaque aller-retour laisse 50 bps dans le pool, la trajectoire réelle vit donc strictement à l'intérieur de ces extrêmes. Et capitalisation = prix × offre en circulation — offre émise moins tokens brûlés. C'est une convention, pas une promesse.", + "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every swap leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": "Deux notes d'honnêteté. Le curseur est une enveloppe — un flux purement à sens unique, que des vendeurs ou que des acheteurs, rien d'entrelacé. Les vrais marchés mélangent les deux sens, et chaque échange laisse 50 bps dans le pool, la trajectoire réelle vit donc strictement à l'intérieur de ces extrêmes. Et capitalisation = prix × offre en circulation — offre émise moins tokens brûlés. C'est une convention, pas une promesse.", "TX fee": "Frais de TX", "TX fees": "Frais de TX", "TX fees · {n} txs": "Frais de tx · {n} tx", diff --git a/apps/web/src/locales/fr.status.json b/apps/web/src/locales/fr.status.json index 92df3437..0d2b997c 100644 --- a/apps/web/src/locales/fr.status.json +++ b/apps/web/src/locales/fr.status.json @@ -727,7 +727,7 @@ "Trustless token launches on Counterparty. All-or-nothing mints, liquidity locked by consensus, no platform custody.", "Try again", "Two bounties are claimed — one is still open for the next launch to graduate.", - "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.", + "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every swap leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.", "Type", "Unconfirmed and provisional — mempool mints can exceed what's left, get replaced, or never confirm. Only what lands on-chain counts.", "Unconfirmed and provisional — these orders join the book only after confirmation.", diff --git a/apps/web/src/locales/ja.json b/apps/web/src/locales/ja.json index 2e447fd7..e06bb6dc 100644 --- a/apps/web/src/locales/ja.json +++ b/apps/web/src/locales/ja.json @@ -1119,7 +1119,7 @@ "Trustless token launches on Counterparty. All-or-nothing mints, liquidity locked by consensus, no platform custody.": "Counterparty 上のトラストレスなトークンローンチ。オール・オア・ナッシングのミント、コンセンサスでロックされる流動性、プラットフォームによる資産管理なし。", "Try again": "再試行", "Two bounties are claimed — one is still open for the next launch to graduate.": "報奨金 2 つは獲得済み — 次に卒業するローンチのために 1 つが残っています。", - "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": "正直な注記を 2 つ。このスライダーは外側の包絡線です — 純粋な一方向の流れで、全員が売るか全員が買うかのどちらかで、混ざりません。実際の市場は両方向が混ざり、往復のたびに 50 bps がプールに残るので、実際の経路は必ずこの両極の内側にあります。また、時価総額 = 価格 × 流通供給量(発行量からバーン分を引いたもの)です。慣例であって約束ではありません。", + "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every swap leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": "正直な注記を 2 つ。このスライダーは外側の包絡線です — 純粋な一方向の流れで、全員が売るか全員が買うかのどちらかで、混ざりません。実際の市場は両方向が混ざり、スワップのたびに 50 bps がプールに残るので、実際の経路は必ずこの両極の内側にあります。また、時価総額 = 価格 × 流通供給量(発行量からバーン分を引いたもの)です。慣例であって約束ではありません。", "TX fee": "TX 手数料", "TX fees": "TX 手数料", "TX fees · {n} txs": "TX 手数料 · {n} tx", diff --git a/apps/web/src/locales/ja.status.json b/apps/web/src/locales/ja.status.json index 9bc1a182..29e289c9 100644 --- a/apps/web/src/locales/ja.status.json +++ b/apps/web/src/locales/ja.status.json @@ -723,7 +723,7 @@ "Trustless token launches on Counterparty. All-or-nothing mints, liquidity locked by consensus, no platform custody.", "Try again", "Two bounties are claimed — one is still open for the next launch to graduate.", - "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.", + "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every swap leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.", "Type", "Unconfirmed and provisional — mempool mints can exceed what's left, get replaced, or never confirm. Only what lands on-chain counts.", "Unconfirmed and provisional — these orders join the book only after confirmation.", diff --git a/apps/web/src/locales/ko.json b/apps/web/src/locales/ko.json index 006284ae..af4a7362 100644 --- a/apps/web/src/locales/ko.json +++ b/apps/web/src/locales/ko.json @@ -1123,7 +1123,7 @@ "Trustless token launches on Counterparty. All-or-nothing mints, liquidity locked by consensus, no platform custody.": "Counterparty의 신뢰 불필요 토큰 런칭. 전부 아니면 전무인 민팅, 합의로 잠긴 유동성, 플랫폼 커스터디 없음.", "Try again": "다시 시도", "Two bounties are claimed — one is still open for the next launch to graduate.": "바운티 2개가 지급되었고, 다음에 졸업하는 런칭을 위해 1개가 남아 있습니다.", - "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": "솔직한 주석 두 가지. 이 슬라이더는 바깥 경계선입니다 — 순수한 한 방향 흐름으로, 전원 매도 아니면 전원 매수이며 섞이지 않습니다. 실제 시장은 양방향이 섞이고, 왕복할 때마다 50 bps가 풀에 남으므로 실제 경로는 반드시 이 양극단 안쪽에 있습니다. 그리고 시가총액 = 가격 × 유통 공급량 — 발행량에서 소각된 토큰을 뺀 값입니다. 관례일 뿐 약속이 아닙니다.", + "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every swap leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": "솔직한 주석 두 가지. 이 슬라이더는 바깥 경계선입니다 — 순수한 한 방향 흐름으로, 전원 매도 아니면 전원 매수이며 섞이지 않습니다. 실제 시장은 양방향이 섞이고, 스왑할 때마다 50 bps가 풀에 남으므로 실제 경로는 반드시 이 양극단 안쪽에 있습니다. 그리고 시가총액 = 가격 × 유통 공급량 — 발행량에서 소각된 토큰을 뺀 값입니다. 관례일 뿐 약속이 아닙니다.", "TX fee": "TX 수수료", "TX fees": "TX 수수료", "TX fees · {n} txs": "TX 수수료 · {n} tx", diff --git a/apps/web/src/locales/ko.status.json b/apps/web/src/locales/ko.status.json index 8690b1b6..de9fcc21 100644 --- a/apps/web/src/locales/ko.status.json +++ b/apps/web/src/locales/ko.status.json @@ -724,7 +724,7 @@ "Trustless token launches on Counterparty. All-or-nothing mints, liquidity locked by consensus, no platform custody.", "Try again", "Two bounties are claimed — one is still open for the next launch to graduate.", - "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.", + "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every swap leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.", "Type", "Unconfirmed and provisional — mempool mints can exceed what's left, get replaced, or never confirm. Only what lands on-chain counts.", "Unconfirmed and provisional — these orders join the book only after confirmation.", diff --git a/apps/web/src/locales/notes.json b/apps/web/src/locales/notes.json index c52be71d..671a50d5 100644 --- a/apps/web/src/locales/notes.json +++ b/apps/web/src/locales/notes.json @@ -1,5 +1,6 @@ { "_about": "What a short or ambiguous English string means on screen, for whoever translates it — the draft model and the human reviewer alike. Keyed by the English exactly as it appears in the message files. A note is worth writing whenever the English alone could be read two ways.", + "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every swap leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": "The TOKEN/XCP pool fee is 50 basis points (0.5%) per individual swap. A buy followed by a sell is two swaps, not one fee. Translate per swap, not per round trip; keep the surrounding scenario and market-cap caveats unchanged.", "Graduated": "Launch phase: the sale sold out and the token's pool opened. pump.fun's sense — the token 'graduated' to a live market — never the school sense.", "graduated": "Same as Graduated, lowercase mid-sentence.", "Refunded": "Launch phase: the sale missed its target and every minter's XCP was returned automatically.", diff --git a/apps/web/src/locales/pt.json b/apps/web/src/locales/pt.json index a85eba8b..4afc3f6b 100644 --- a/apps/web/src/locales/pt.json +++ b/apps/web/src/locales/pt.json @@ -1122,7 +1122,7 @@ "Trustless token launches on Counterparty. All-or-nothing mints, liquidity locked by consensus, no platform custody.": "Lançamentos de tokens sem confiar em terceiros no Counterparty. Mints tudo ou nada, liquidez travada por consenso, sem custódia da plataforma.", "Try again": "Tentar de novo", "Two bounties are claimed — one is still open for the next launch to graduate.": "Duas recompensas já foram resgatadas — uma continua aberta para o próximo lançamento que se graduar.", - "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": "Duas notas de honestidade. O controle deslizante é um envelope limite — fluxo puro em um único sentido, só vendedores ou só compradores, nada intercalado. Mercados reais misturam as duas direções, e cada ida e volta deixa 50 bps no pool, então o caminho real fica estritamente dentro desses extremos. E capitalização = preço × fornecimento em circulação — fornecimento emitido menos tokens queimados. É uma convenção, não uma promessa.", + "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every swap leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": "Duas notas de honestidade. O controle deslizante é um envelope limite — fluxo puro em um único sentido, só vendedores ou só compradores, nada intercalado. Mercados reais misturam as duas direções, e cada swap deixa 50 bps no pool, então o caminho real fica estritamente dentro desses extremos. E capitalização = preço × fornecimento em circulação — fornecimento emitido menos tokens queimados. É uma convenção, não uma promessa.", "TX fee": "Taxa de TX", "TX fees": "Taxas de TX", "TX fees · {n} txs": "Taxas de tx · {n} tx", diff --git a/apps/web/src/locales/pt.status.json b/apps/web/src/locales/pt.status.json index 92df3437..0d2b997c 100644 --- a/apps/web/src/locales/pt.status.json +++ b/apps/web/src/locales/pt.status.json @@ -727,7 +727,7 @@ "Trustless token launches on Counterparty. All-or-nothing mints, liquidity locked by consensus, no platform custody.", "Try again", "Two bounties are claimed — one is still open for the next launch to graduate.", - "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.", + "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every swap leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.", "Type", "Unconfirmed and provisional — mempool mints can exceed what's left, get replaced, or never confirm. Only what lands on-chain counts.", "Unconfirmed and provisional — these orders join the book only after confirmation.", diff --git a/apps/web/src/locales/ru.json b/apps/web/src/locales/ru.json index 084f1e59..c40e64e8 100644 --- a/apps/web/src/locales/ru.json +++ b/apps/web/src/locales/ru.json @@ -1123,7 +1123,7 @@ "Trustless token launches on Counterparty. All-or-nothing mints, liquidity locked by consensus, no platform custody.": "Запуски токенов на Counterparty без доверия. Минты по принципу «всё или ничего», ликвидность заблокирована консенсусом, платформа не хранит средства.", "Try again": "Попробовать снова", "Two bounties are claimed — one is still open for the next launch to graduate.": "Две награды получены — одна ещё ждёт следующий запуск, который откроет пул.", - "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": "Две честные оговорки. Ползунок — это граничная огибающая: чистый односторонний поток, либо все продают, либо все покупают, без чередования. Реальные рынки смешивают оба направления, и каждый круг оставляет в пуле 50 б.п., так что реальная траектория лежит строго внутри этих крайностей. И капитализация = цена × предложение в обращении — выпущенное предложение минус сожжённые токены. Это условность, а не обещание.", + "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every swap leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": "Две честные оговорки. Ползунок — это граничная огибающая: чистый односторонний поток, либо все продают, либо все покупают, без чередования. Реальные рынки смешивают оба направления, и каждый обмен оставляет в пуле 50 б.п., так что реальная траектория лежит строго внутри этих крайностей. И капитализация = цена × предложение в обращении — выпущенное предложение минус сожжённые токены. Это условность, а не обещание.", "TX fee": "Комиссия майнеров", "TX fees": "Комиссии майнеров", "TX fees · {n} txs": "Комиссии · транзакций: {n}", diff --git a/apps/web/src/locales/ru.status.json b/apps/web/src/locales/ru.status.json index 65647856..1e1bcb30 100644 --- a/apps/web/src/locales/ru.status.json +++ b/apps/web/src/locales/ru.status.json @@ -728,7 +728,7 @@ "Trustless token launches on Counterparty. All-or-nothing mints, liquidity locked by consensus, no platform custody.", "Try again", "Two bounties are claimed — one is still open for the next launch to graduate.", - "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.", + "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every swap leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.", "Type", "Unconfirmed and provisional — mempool mints can exceed what's left, get replaced, or never confirm. Only what lands on-chain counts.", "Unconfirmed and provisional — these orders join the book only after confirmation.", diff --git a/apps/web/src/locales/uk.json b/apps/web/src/locales/uk.json index cc5d34e1..5575f386 100644 --- a/apps/web/src/locales/uk.json +++ b/apps/web/src/locales/uk.json @@ -1123,7 +1123,7 @@ "Trustless token launches on Counterparty. All-or-nothing mints, liquidity locked by consensus, no platform custody.": "Запуски токенів на Counterparty без довіри. Мінти «усе або нічого», ліквідність блокує консенсус, без зберігання коштів платформою.", "Try again": "Спробувати ще раз", "Two bounties are claimed — one is still open for the next launch to graduate.": "Дві винагороди отримано — ще одна чекає на наступний запуск, який відкриє пул.", - "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": "Дві чесні примітки. Повзунок показує граничні сценарії: чистий односторонній потік, лише продавці або лише покупці, без чергування. Реальні ринки змішують обидва напрямки, і кожне коло туди-назад лишає в пулі 50 б.п., тож реальна траєкторія лежить строго всередині цих крайнощів. І капіталізація = ціна × пропозиція в обігу — емітована пропозиція мінус спалені токени. Це умовність, а не обіцянка.", + "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every swap leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": "Дві чесні примітки. Повзунок показує граничні сценарії: чистий односторонній потік, лише продавці або лише покупці, без чергування. Реальні ринки змішують обидва напрямки, і кожен обмін лишає в пулі 50 б.п., тож реальна траєкторія лежить строго всередині цих крайнощів. І капіталізація = ціна × пропозиція в обігу — емітована пропозиція мінус спалені токени. Це умовність, а не обіцянка.", "TX fee": "Комісія майнерів", "TX fees": "Комісії майнерів", "TX fees · {n} txs": "Комісії TX · {n} tx", diff --git a/apps/web/src/locales/uk.status.json b/apps/web/src/locales/uk.status.json index 65647856..1e1bcb30 100644 --- a/apps/web/src/locales/uk.status.json +++ b/apps/web/src/locales/uk.status.json @@ -728,7 +728,7 @@ "Trustless token launches on Counterparty. All-or-nothing mints, liquidity locked by consensus, no platform custody.", "Try again", "Two bounties are claimed — one is still open for the next launch to graduate.", - "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.", + "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every swap leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.", "Type", "Unconfirmed and provisional — mempool mints can exceed what's left, get replaced, or never confirm. Only what lands on-chain counts.", "Unconfirmed and provisional — these orders join the book only after confirmation.", diff --git a/apps/web/src/locales/zh-hk.json b/apps/web/src/locales/zh-hk.json index 9458feac..36a99d9f 100644 --- a/apps/web/src/locales/zh-hk.json +++ b/apps/web/src/locales/zh-hk.json @@ -1119,7 +1119,7 @@ "Trustless token launches on Counterparty. All-or-nothing mints, liquidity locked by consensus, no platform custody.": "Counterparty 上無需信任的代幣發射。全有或全無的鑄造,流動性由共識鎖定,沒有平台託管。", "Try again": "重試", "Two bounties are claimed — one is still open for the next launch to graduate.": "兩份賞金已被領取 — 還有一份留給下一個畢業的發射。", - "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": "兩點坦白説明。這個滑塊是一個邊界包絡 — 純粹的單向流動,要麼全是賣家,要麼全是買家,沒有交錯。真實市場是雙向混合的,而且每一次來回都會在池中留下 50 bps,所以真實路徑嚴格位於這兩個極端之內。另外,市值 = 價格 × 流通供應量 — 已發行供應量減去已銷毀代幣。這是慣例,不是承諾。", + "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every swap leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": "兩點坦白説明。這個滑塊是一個邊界包絡 — 純粹的單向流動,要麼全是賣家,要麼全是買家,沒有交錯。真實市場是雙向混合的,而且每次兌換都會在池中留下 50 bps,所以真實路徑嚴格位於這兩個極端之內。另外,市值 = 價格 × 流通供應量 — 已發行供應量減去已銷毀代幣。這是慣例,不是承諾。", "TX fee": "礦工費", "TX fees": "礦工費", "TX fees · {n} txs": "礦工費 · {n} 筆", diff --git a/apps/web/src/locales/zh-hk.status.json b/apps/web/src/locales/zh-hk.status.json index 395a285c..8d099bea 100644 --- a/apps/web/src/locales/zh-hk.status.json +++ b/apps/web/src/locales/zh-hk.status.json @@ -1085,7 +1085,7 @@ "Trustless token launches on Counterparty. All-or-nothing mints, liquidity locked by consensus, no platform custody.", "Try again", "Two bounties are claimed — one is still open for the next launch to graduate.", - "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.", + "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every swap leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.", "TX fee", "TX fees", "TX fees · {n} txs", diff --git a/apps/web/src/locales/zh-tw.json b/apps/web/src/locales/zh-tw.json index 54bff765..090c7c4c 100644 --- a/apps/web/src/locales/zh-tw.json +++ b/apps/web/src/locales/zh-tw.json @@ -1119,7 +1119,7 @@ "Trustless token launches on Counterparty. All-or-nothing mints, liquidity locked by consensus, no platform custody.": "Counterparty 上無需信任的代幣發射。全有或全無的鑄造,流動性由共識鎖定,沒有平台託管。", "Try again": "重試", "Two bounties are claimed — one is still open for the next launch to graduate.": "兩份賞金已被領取 — 還有一份留給下一個畢業的發射。", - "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": "兩點坦白說明。這個滑塊是一個邊界包絡 — 純粹的單向流動,要麼全是賣家,要麼全是買家,沒有交錯。真實市場是雙向混合的,而且每一次來回都會在池中留下 50 bps,所以真實路徑嚴格位於這兩個極端之內。另外,市值 = 價格 × 流通供應量 — 已發行供應量減去已銷毀代幣。這是慣例,不是承諾。", + "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every swap leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": "兩點坦白說明。這個滑塊是一個邊界包絡 — 純粹的單向流動,要麼全是賣家,要麼全是買家,沒有交錯。真實市場是雙向混合的,而且每次兌換都會在池中留下 50 bps,所以真實路徑嚴格位於這兩個極端之內。另外,市值 = 價格 × 流通供應量 — 已發行供應量減去已銷毀代幣。這是慣例,不是承諾。", "TX fee": "礦工費", "TX fees": "礦工費", "TX fees · {n} txs": "礦工費 · {n} 筆", diff --git a/apps/web/src/locales/zh-tw.status.json b/apps/web/src/locales/zh-tw.status.json index 395a285c..8d099bea 100644 --- a/apps/web/src/locales/zh-tw.status.json +++ b/apps/web/src/locales/zh-tw.status.json @@ -1085,7 +1085,7 @@ "Trustless token launches on Counterparty. All-or-nothing mints, liquidity locked by consensus, no platform custody.", "Try again", "Two bounties are claimed — one is still open for the next launch to graduate.", - "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.", + "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every swap leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.", "TX fee", "TX fees", "TX fees · {n} txs", diff --git a/apps/web/src/locales/zh.json b/apps/web/src/locales/zh.json index 3e948fbd..ac25148b 100644 --- a/apps/web/src/locales/zh.json +++ b/apps/web/src/locales/zh.json @@ -1119,7 +1119,7 @@ "Trustless token launches on Counterparty. All-or-nothing mints, liquidity locked by consensus, no platform custody.": "Counterparty 上无需信任的代币发射。全有或全无的铸造,流动性由共识锁定,没有平台托管。", "Try again": "重试", "Two bounties are claimed — one is still open for the next launch to graduate.": "两份赏金已被领取 — 还有一份留给下一个毕业的发射。", - "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": "两点坦白说明。这个滑块是一个边界包络 — 纯粹的单向流动,要么全是卖家,要么全是买家,没有交错。真实市场是双向混合的,而且每一次来回都会在池中留下 50 bps,所以真实路径严格位于这两个极端之内。另外,市值 = 价格 × 流通供应量 — 已发行供应量减去已销毁代币。这是惯例,不是承诺。", + "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every swap leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.": "两点坦白说明。这个滑块是一个边界包络 — 纯粹的单向流动,要么全是卖家,要么全是买家,没有交错。真实市场是双向混合的,而且每次兑换都会在池中留下 50 bps,所以真实路径严格位于这两个极端之内。另外,市值 = 价格 × 流通供应量 — 已发行供应量减去已销毁代币。这是惯例,不是承诺。", "TX fee": "矿工费", "TX fees": "矿工费", "TX fees · {n} txs": "矿工费 · {n} 笔", diff --git a/apps/web/src/locales/zh.status.json b/apps/web/src/locales/zh.status.json index 8690b1b6..de9fcc21 100644 --- a/apps/web/src/locales/zh.status.json +++ b/apps/web/src/locales/zh.status.json @@ -724,7 +724,7 @@ "Trustless token launches on Counterparty. All-or-nothing mints, liquidity locked by consensus, no platform custody.", "Try again", "Two bounties are claimed — one is still open for the next launch to graduate.", - "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every round trip leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.", + "Two honesty notes. The slider is a bounding envelope — pure one-way flow, all sellers or all buyers, nothing interleaved. Real markets mix both directions, and every swap leaves 50 bps behind in the pool, so the real path lives strictly inside these extremes. And market cap = price × circulating supply — issued supply minus burned tokens. It is a convention, not a promise.", "Type", "Unconfirmed and provisional — mempool mints can exceed what's left, get replaced, or never confirm. Only what lands on-chain counts.", "Unconfirmed and provisional — these orders join the book only after confirmation.", From e8a8479cafbf12024481da65ab750a2b0b7a844e Mon Sep 17 00:00:00 2001 From: Dan Anderson Date: Mon, 7 Sep 2026 19:20:10 -0400 Subject: [PATCH 10/10] Finish localized terminology and responsive form guidance --- apps/web/src/app/[lang]/docs/page.tsx | 6 ++-- .../rewards/_components/rewards-content.tsx | 4 +-- .../[lang]/swap/_components/swap-settings.tsx | 34 +++++++++---------- apps/web/src/components/amount-input.tsx | 2 +- apps/web/src/components/ui/popover.tsx | 2 +- apps/web/src/components/ui/tabs.tsx | 6 ++-- apps/web/src/locales/_source.json | 16 ++++----- apps/web/src/locales/es.json | 4 +-- apps/web/src/locales/fr.json | 4 +-- apps/web/src/locales/glossary.json | 16 ++++----- apps/web/src/locales/ja.json | 4 +-- apps/web/src/locales/ko.json | 4 +-- apps/web/src/locales/notes.json | 15 +++++--- apps/web/src/locales/pt.json | 4 +-- apps/web/src/locales/ru.json | 4 +-- apps/web/src/locales/uk.json | 4 +-- apps/web/src/locales/zh-hk.json | 4 +-- apps/web/src/locales/zh-tw.json | 4 +-- apps/web/src/locales/zh.json | 4 +-- docs/amount-and-localization-contract.md | 6 +++- 20 files changed, 79 insertions(+), 68 deletions(-) diff --git a/apps/web/src/app/[lang]/docs/page.tsx b/apps/web/src/app/[lang]/docs/page.tsx index c981c428..02421738 100644 --- a/apps/web/src/app/[lang]/docs/page.tsx +++ b/apps/web/src/app/[lang]/docs/page.tsx @@ -26,7 +26,7 @@ import { const PAGE_METADATA = { title: msg("Docs — xcp.fun"), description: - msg("Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, what it costs (nothing), and how to integrate."), + msg("Everything about XCP-69, in one place: how launches work, how pricing works, what graduation means, fees, and how to integrate."), }; /** The page's own metadata, plus the hreflang set for the locale it is @@ -78,7 +78,7 @@ const TOC: { section: string; items: [string, string][] }[] = [ const PRICING_TERMS: [string, string][] = [ [ msg("Price"), - msg("XCP reserve ÷ token reserve. Moves with every swap; there is no order book and no market maker."), + msg("Pool price is XCP reserve ÷ token reserve. Swaps change this ratio. Limit orders can also trade on Counterparty's order book."), ], [ msg("Market cap"), @@ -105,7 +105,7 @@ function CodeBlock({ children }: { children: string }) { /** Inline code as the docs write it. Never translated: these are field * names, statuses and endpoints the reader will type. */ function Code({ children }: { children: ReactNode }) { - return {children}; + return {children}; } export default async function DocsPage() { diff --git a/apps/web/src/app/[lang]/rewards/_components/rewards-content.tsx b/apps/web/src/app/[lang]/rewards/_components/rewards-content.tsx index f6a73e3b..73ddacc2 100644 --- a/apps/web/src/app/[lang]/rewards/_components/rewards-content.tsx +++ b/apps/web/src/app/[lang]/rewards/_components/rewards-content.tsx @@ -90,7 +90,7 @@ export function RewardsContent({ stats, earners, graduates, mintsPool, xcpUsd, b

{rich( t, - "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses, at which point the pool is created and its liquidity is burned. A launch that misses its target refunds every satoshi by consensus and does not count. {link}", + "Graduating means selling out: {xcp} XCP raised from at least {n} different addresses. The pool is created and its initial LP tokens go to an unspendable address, permanently locking that liquidity. A launch that misses its target returns the contributed XCP by consensus and does not count. {link}", { xcp: num.commas(raiseXcp), n: XCP69_MIN_PARTICIPANTS, @@ -263,7 +263,7 @@ export function RewardsContent({ stats, earners, graduates, mintsPool, xcpUsd, b {t( - "No — a mint escrows XCP against the launch. If it graduates you hold the tokens; if it refunds you get every satoshi back. The only thing a mint actually costs you is the Bitcoin fee — which is the part this programme covers.", + "A mint locks your XCP until the launch resolves. If it graduates, your XCP funds the pool and you receive tokens, which can lose value. If it refunds, your XCP is returned. The Bitcoin network fee is paid in either case; rewards do not guarantee that every cost is covered.", )}

diff --git a/apps/web/src/app/[lang]/swap/_components/swap-settings.tsx b/apps/web/src/app/[lang]/swap/_components/swap-settings.tsx index 0fb579fb..54797729 100644 --- a/apps/web/src/app/[lang]/swap/_components/swap-settings.tsx +++ b/apps/web/src/app/[lang]/swap/_components/swap-settings.tsx @@ -23,11 +23,11 @@ import { updateSettings, } from "@/app/[lang]/swap/_lib/trade-settings-store"; -/** Market orders live one block: match at confirmation or refund next block. */ +/** Shortest configured order lifetime; protocol expiry returns unfilled amounts. */ export const MARKET_EXPIRATION = 1; const SLIPPAGE_PRESETS = [0.5, 1, 2]; -/** Liquidity slippage is looser by convention — deposits drift with every - * pool trade, and a breach is benign (void tx, nothing debited). */ +/** Deposits drift with every pool trade. A breached limit invalidates the + * liquidity operation; the Bitcoin network fee is still paid. */ const LQ_SLIPPAGE_PRESETS = [0.5, 1, 2.5]; /** Resting-order lifetimes, in blocks. */ export const LIMIT_EXPIRATIONS = [ @@ -178,7 +178,7 @@ export function SwapSettingsGear() { label={t("Swap settings")} >
{t("Max slippage")}
-
+
))}
%
- {s.slippageAuto ? ( + {!parseBoundedSetting(s.customSlippage, 50).valid ? null : s.slippageAuto ? (

{t("Auto sizes slippage to the trade: what this quote needs, currently ~{pct}%.", { pct: num.commas(s.autoValue) })}

@@ -253,10 +253,10 @@ export function SwapSettingsGear() { {t("Higher than this trade needs (~{pct}%).", { pct: num.commas(s.autoValue) })}

) : null} -
+
{t("Expiration")} {t("How many blocks an unfilled remainder stays open before expiry. {n} is the shortest setting.", { n: MARKET_EXPIRATION })}

-
+
{t("TX fee")} @@ -313,7 +313,7 @@ export function LimitSettingsGear() { label={t("Limit order settings")} >
{t("Expiration")}
-
+
{LIMIT_EXPIRATIONS.map((x) => (