Ship LNURL mint pages, indexing UI, and reviews rewrite.

Add lnurl list/detail routes, OG fixtures, i18n strings, and the write/
index client flows so the site surfaces the new mint type end to end.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
michilis
2026-08-22 03:44:43 +02:00
co-authored by Cursor
parent 2a9444942b
commit c74c7fc187
66 changed files with 7603 additions and 1241 deletions
+60 -1
View File
@@ -264,6 +264,16 @@ CHECK_DB_URL=postgres://localhost/cashumints_test pnpm --filter ./api test:offli
`CHECK_DB_URL` does the same for `pnpm --filter ./api test`, which then runs the `CHECK_DB_URL` does the same for `pnpm --filter ./api test`, which then runs the
review-dedupe SQL against both backends instead of just SQLite. review-dedupe SQL against both backends instead of just SQLite.
On-demand indexing. The address rules that keep `POST /api/index` from being an SSRF
hole, the redirect hops, the slug collapse that stops one mint becoming two rows, the
invite-code decoder and the rate limiter. Nothing here touches the network: the resolver
and the fetch are both injected, because a rule that can only be exercised against the
real internet stops being exercised the first time CI runs offline.
```bash
pnpm --filter ./api test:index
```
Type checking across the workspace: Type checking across the workspace:
```bash ```bash
@@ -537,6 +547,7 @@ so a systemd `Environment=` line or a one-off `PORT=9000 pnpm dev:api` still ove
| `PROBE_TIMEOUT_MS` | `5000` | Per-mint request timeout | | `PROBE_TIMEOUT_MS` | `5000` | Per-mint request timeout |
| `FEDIMINT_OBSERVER_URL` | `https://observer.fedimint.org/api/federations` | Where federation health is read from. Empty disables the lookup, and every federation stays `announced`. See [Ecosystems](#ecosystems). | | `FEDIMINT_OBSERVER_URL` | `https://observer.fedimint.org/api/federations` | Where federation health is read from. Empty disables the lookup, and every federation stays `announced`. See [Ecosystems](#ecosystems). |
| `SCORE_PRIOR_MEAN` | `3` | Bayesian prior. See "Ranking" below before changing. | | `SCORE_PRIOR_MEAN` | `3` | Bayesian prior. See "Ranking" below before changing. |
| `INDEX_RATE_LIMIT` | `10` | `POST /api/index` submissions allowed per address per hour |
### Web (`web/`) ### Web (`web/`)
@@ -657,7 +668,7 @@ or copy refers to it, and it is planned as a later stage rather than half-built
## API ## API
Four endpoints, CORS open, no auth. Five endpoints, CORS open, no auth. Four read; the fifth writes.
| Endpoint | Notes | | Endpoint | Notes |
| -------------------- | ------------------------------------------------------------------ | | -------------------- | ------------------------------------------------------------------ |
@@ -665,9 +676,54 @@ Four endpoints, CORS open, no auth.
| `GET /api/stats` | Network counters, memoized 60s in process. | | `GET /api/stats` | Network counters, memoized 60s in process. |
| `GET /api/mints` | Everything listed, online first then score descending. `?limit=`, `?type=`. | | `GET /api/mints` | Everything listed, online first then score descending. `?limit=`, `?type=`. |
| `GET /api/mints/:host` | One listing plus its ecosystem's own fields, distribution, uptime and probe history. | | `GET /api/mints/:host` | One listing plus its ecosystem's own fields, distribution, uptime and probe history. |
| `POST /api/index` | Index a mint nobody has announced yet. Rate limited. See below. |
`/icons/*` serves the cached mint icons. `/icons/*` serves the cached mint icons.
### Indexing on demand
```bash
curl -X POST https://cashumints.space/api/index \
-H 'content-type: application/json' \
-d '{"type":"lnurl","input":"mint.600.wtf"}'
```
`type` is `cashu`, `fedimint` or `lnurl`; `input` is a URL, or an invite code for a
federation. The site itself calls this from two places: the 404 page, when somebody opens
`/mint/…`, `/fedimint/…` or `/lnurl-mint/…` for something this build has never heard of,
and the "Write a review" dialog on the three index pages.
What it does, in order, stopping at the first step that settles it:
1. **Already indexed** — `200` with the ordinary `GET /api/mints/:host` payload plus
`"existing": true`. Nothing is probed; a submission is not a reason to re-probe.
2. **Answers as what it claims** — one request, the standard `PROBE_TIMEOUT_MS`. The row
is written by the same functions the probe cycle uses, so it is indistinguishable from
one the loop created. `201`, same payload shape, plus `"indexed_from": "probe"`.
3. **Answers as something else** — `422` with `error: "wrong_type"` and `detected_type`,
which is what lets the dialog offer "this is a Cashu mint, review it there" as a
button. A federation's invite code is decoded rather than fetched: there is no
endpoint to probe.
4. **Nothing answers** — one bounded relay lookup (3s) for a NIP-87 announcement or any
review naming the address. Found: the row is written from the announcement with
`status = offline`, which is the mint that rugged last week and is exactly the one
somebody wants to review. Nothing anywhere: `404`, `error: "unverifiable"`.
Everything indexed this way is an ordinary row afterwards: the probe loop owns it from
the next cycle.
Because it fetches an address a stranger chose, it is hardened accordingly (`api/src/safe-fetch.ts`):
https only, DNS resolved and every returned address checked against the private, loopback,
link-local, CGNAT and multicast ranges *before* a socket opens, redirects followed by hand
and capped at two with every hop re-checked, a 256KB response cap, and a per-IP limit of
`INDEX_RATE_LIMIT` an hour with a clean `429`. Two simultaneous submissions of one address
share a single probe. `pnpm --filter ./api test:index` covers all of it without a network.
The limit needs to be able to tell two visitors apart, so behind the nginx block below add
`proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;` to `location /api/`. The
last entry of that header is the one used, because it is the one the proxy vouched for;
the header is ignored entirely when the peer is not loopback.
### Ranking ### Ranking
Bayesian weighted rating: Bayesian weighted rating:
@@ -792,6 +848,9 @@ server {
# blocks only if you build with PUBLIC_API_URL pointing at a separate API host. # blocks only if you build with PUBLIC_API_URL pointing at a separate API host.
location /api/ { location /api/ {
proxy_pass http://127.0.0.1:8787; proxy_pass http://127.0.0.1:8787;
# POST /api/index is rate limited per address, and without this every visitor
# arrives as 127.0.0.1 and shares one budget.
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
} }
location /icons/ { location /icons/ {
Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

+10 -4
View File
@@ -28,7 +28,7 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { Resvg } from '@resvg/resvg-js'; import { Resvg } from '@resvg/resvg-js';
import { renderSvg } from './card.mjs'; import { renderSvg } from './card.mjs';
import { cashuModel, defaultModel, fedimintModel } from './model.mjs'; import { cashuModel, defaultModel, fedimintModel, lnurlModel } from './model.mjs';
import { FIXTURES, NOW as FIXTURE_NOW } from './fixtures.mjs'; import { FIXTURES, NOW as FIXTURE_NOW } from './fixtures.mjs';
const here = path.dirname(fileURLToPath(import.meta.url)); const here = path.dirname(fileURLToPath(import.meta.url));
@@ -96,7 +96,9 @@ async function buildFixtures() {
const model = const model =
raw.type === 'fedimint' raw.type === 'fedimint'
? fedimintModel(raw, { now: FIXTURE_NOW }) ? fedimintModel(raw, { now: FIXTURE_NOW })
: cashuModel(raw, { now: FIXTURE_NOW }); : raw.type === 'lnurl'
? lnurlModel(raw, { now: FIXTURE_NOW })
: cashuModel(raw, { now: FIXTURE_NOW });
const png = await renderPng(model, null); const png = await renderPng(model, null);
const file = path.join(dir, `${raw.host}.png`); const file = path.join(dir, `${raw.host}.png`);
await writeFile(file, png); await writeFile(file, png);
@@ -114,9 +116,10 @@ async function build() {
const previous = JSON.parse(await readFile(manifestFile, 'utf8').catch(() => '{}')); const previous = JSON.parse(await readFile(manifestFile, 'utf8').catch(() => '{}'));
const previousImages = previous.images ?? {}; const previousImages = previous.images ?? {};
const [cashu, fedimint, stats] = await Promise.all([ const [cashu, fedimint, lnurl, stats] = await Promise.all([
getJson('/api/mints?type=cashu'), getJson('/api/mints?type=cashu'),
getJson('/api/mints?type=fedimint'), getJson('/api/mints?type=fedimint'),
getJson('/api/mints?type=lnurl'),
getJson('/api/stats'), getJson('/api/stats'),
]); ]);
@@ -135,6 +138,7 @@ async function build() {
const listings = [ const listings = [
...cashu.map((m) => ({ listing: m, kind: 'cashu' })), ...cashu.map((m) => ({ listing: m, kind: 'cashu' })),
...fedimint.map((m) => ({ listing: m, kind: 'fedimint' })), ...fedimint.map((m) => ({ listing: m, kind: 'fedimint' })),
...lnurl.map((m) => ({ listing: m, kind: 'lnurl' })),
]; ];
// A handful at a time: satori renders in milliseconds, the icon fetches are local, // A handful at a time: satori renders in milliseconds, the icon fetches are local,
@@ -158,7 +162,9 @@ async function build() {
const model = const model =
kind === 'fedimint' kind === 'fedimint'
? fedimintModel(detail, { iconHash: icon?.hash ?? null, now }) ? fedimintModel(detail, { iconHash: icon?.hash ?? null, now })
: cashuModel(detail, { iconHash: icon?.hash ?? null, now }); : kind === 'lnurl'
? lnurlModel(detail, { iconHash: icon?.hash ?? null, now })
: cashuModel(detail, { iconHash: icon?.hash ?? null, now });
const hash = hashModel(model); const hash = hashModel(model);
const file = `${model.slug}.${hash}.png`; const file = `${model.slug}.${hash}.png`;
+101 -15
View File
@@ -29,6 +29,8 @@ const AMBER = '#F0A93B';
const VIOLET = '#A78BFA'; const VIOLET = '#A78BFA';
const MINT = '#5FD68F'; const MINT = '#5FD68F';
const RED = '#E06450'; const RED = '#E06450';
/** The LNURL ecosystem label. The only cool blue here — see DESIGN.md. */
const LNURL = '#4EA8DE';
const GROTESK = 'Space Grotesk'; const GROTESK = 'Space Grotesk';
const INTER = 'Inter'; const INTER = 'Inter';
@@ -137,22 +139,29 @@ function statusChip(model) {
} }
} }
/** The type badge that makes a federation card unmistakable at thumbnail size. */ /**
function fedimintBadge() { * The type badge that makes a non-Cashu card unmistakable at thumbnail size.
*
* Three things separate the three ecosystems at 300px wide, and the badge is only one
* of them: the word itself, the badge's colour, and the glow behind the whole card all
* change together. Cashu gets no badge at all, which is its own signal — it is the
* default and the majority of the index.
*/
function typeBadge(label, color, borderColor) {
return h( return h(
'div', 'div',
{ {
display: 'flex', display: 'flex',
padding: '9px 14px', padding: '9px 14px',
borderRadius: '9px', borderRadius: '9px',
border: `1.5px solid rgba(167,139,250,.5)`, border: `1.5px solid ${borderColor}`,
color: VIOLET, color,
fontFamily: MONO, fontFamily: MONO,
fontWeight: 500, fontWeight: 500,
fontSize: '18px', fontSize: '18px',
letterSpacing: '3px', letterSpacing: '3px',
}, },
'FEDIMINT', label,
); );
} }
@@ -380,15 +389,86 @@ function thirdCellFedimint(model) {
); );
} }
/** The background glow: amber for a healthy card, red-tinted for trouble, violet for Fedimint. */ /**
* Third cell, LNURL: the withdraw range — never NUT limits, never a module list.
*
* This is the number that answers an LNURL reader's actual question: how much can one
* of this mint's notes be worth? It is shown in sats, converted from millisatoshi in
* the model, and it is a *range* rather than a pair of independent bounds, which is
* why the two rows read "min"/"max" the way the Cashu limits cell does.
*
* The lightning address takes the space the Fedimint cell gives its network badge: it
* is the one string on the whole card someone might copy out of a screenshot.
*/
function thirdCellLnurl(model) {
const base = {
display: 'flex',
flexDirection: 'column',
flex: 1.1,
padding: '30px 36px',
borderLeft: `1px solid ${LINE_SOFT}`,
};
if (model.warning) {
const color = model.warning.severity === 'critical' ? RED : AMBER;
return h(
'div',
{ ...base, backgroundColor: model.warning.severity === 'critical' ? 'rgba(224,100,80,.06)' : 'rgba(240,169,59,.05)' },
cellLabel('WARNING', color),
h('div', { fontFamily: GROTESK, fontWeight: 700, fontSize: '34px', lineHeight: 1.05, color }, model.warning.label),
h('div', { fontFamily: INTER, fontWeight: 500, fontSize: '19px', color: MUTED, marginTop: '14px' }, model.warning.sub),
);
}
const lines = [];
if (model.withdraw) {
if (model.withdraw.min !== null) lines.push(['min', `${compact(model.withdraw.min)} sat`]);
if (model.withdraw.max !== null) lines.push(['max', `${compact(model.withdraw.max)} sat`]);
}
return h(
'div',
base,
cellLabel('WITHDRAW'),
lines.length === 0
? h('div', { fontFamily: MONO, fontWeight: 400, fontSize: '24px', color: MUTED, marginTop: '8px' }, 'Not published')
: h(
'div',
{ display: 'flex', flexDirection: 'column', gap: '10px', marginTop: '4px' },
...lines.map(([k, v]) =>
h(
'div',
{ display: 'flex', alignItems: 'baseline', gap: '14px' },
h('div', { fontFamily: MONO, fontWeight: 400, fontSize: '20px', color: FAINT }, k),
h('div', { fontFamily: MONO, fontWeight: 500, fontSize: '27px', color: TEXT }, v),
),
),
),
model.address &&
h(
'div',
{ fontFamily: MONO, fontWeight: 400, fontSize: '18px', color: FAINT, marginTop: '16px', maxWidth: '300px' },
fitMono(model.address, 30),
),
);
}
/** The background glow: amber healthy, red for trouble, violet Fedimint, blue LNURL. */
function glow(model) { function glow(model) {
const danger = const danger =
model.status === 'offline' || (model.type === 'cashu' && model.warning && model.warning.severity === 'critical'); model.status === 'offline' ||
(model.type !== 'fedimint' && model.warning && model.warning.severity === 'critical');
const color = danger const color = danger
? 'rgba(224,100,80,.13)' ? 'rgba(224,100,80,.13)'
: model.type === 'fedimint' : model.type === 'fedimint'
? 'rgba(167,139,250,.11)' ? 'rgba(167,139,250,.11)'
: 'rgba(240,169,59,.10)'; : model.type === 'lnurl'
// Deliberately the strongest of the three. Amber and violet only have to
// separate a card from the site's own background; this one has to separate an
// LNURL card from a Fedimint card at 300px, where the badge text is a smudge
// and the tint is the first thing a reader actually resolves.
? 'rgba(78,168,222,.20)'
: 'rgba(240,169,59,.10)';
return h('div', { return h('div', {
position: 'absolute', position: 'absolute',
top: '-260px', top: '-260px',
@@ -414,10 +494,12 @@ function brandRow(tagline) {
/** One mint or federation card. `iconData` is a data: URI or null. */ /** One mint or federation card. `iconData` is a data: URI or null. */
export function mintCard(model, iconData) { export function mintCard(model, iconData) {
const isFed = model.type === 'fedimint'; const isFed = model.type === 'fedimint';
const isLnurl = model.type === 'lnurl';
const monoLine = isFed ? model.shortId : fitMono(model.domain, 46); const monoLine = isFed ? model.shortId : fitMono(model.domain, 46);
// Room beside the icon: canvas minus padding, icon, gaps and the chips. // Room beside the icon: canvas minus padding, icon, gaps and the chips. A badge is
const chipRoom = 240 + (isFed ? 180 : 0); // ~180px for FEDIMINT and ~120px for the shorter LNURL.
const chipRoom = 240 + (isFed ? 180 : isLnurl ? 120 : 0);
const { size, clamp } = nameSize(model.name, 1200 - 128 - 148 - chipRoom); const { size, clamp } = nameSize(model.name, 1200 - 128 - 148 - chipRoom);
return h( return h(
@@ -433,7 +515,7 @@ export function mintCard(model, iconData) {
fontFamily: INTER, fontFamily: INTER,
}, },
glow(model), glow(model),
brandRow(isFed ? 'Fedimint reviews' : 'Cashu mint reviews'), brandRow(isFed ? 'Fedimint reviews' : isLnurl ? 'LNURL mint reviews' : 'Cashu mint reviews'),
h( h(
'div', 'div',
{ display: 'flex', alignItems: 'center', gap: '32px', marginTop: '52px', flexGrow: 1 }, { display: 'flex', alignItems: 'center', gap: '32px', marginTop: '52px', flexGrow: 1 },
@@ -459,7 +541,8 @@ export function mintCard(model, iconData) {
model.name, model.name,
), ),
statusChip(model), statusChip(model),
isFed && fedimintBadge(), isFed && typeBadge('FEDIMINT', VIOLET, 'rgba(167,139,250,.5)'),
isLnurl && typeBadge('LNURL', LNURL, 'rgba(78,168,222,.5)'),
), ),
h('div', { fontFamily: MONO, fontWeight: 400, fontSize: '25px', color: MUTED, marginTop: '14px' }, monoLine), h('div', { fontFamily: MONO, fontWeight: 400, fontSize: '25px', color: MUTED, marginTop: '14px' }, monoLine),
// The Fedimint soft warning is a muted line, deliberately not a red cell. // The Fedimint soft warning is a muted line, deliberately not a red cell.
@@ -479,7 +562,7 @@ export function mintCard(model, iconData) {
}, },
ratingCell(model), ratingCell(model),
reviewsCell(model), reviewsCell(model),
isFed ? thirdCellFedimint(model) : thirdCellCashu(model), isFed ? thirdCellFedimint(model) : isLnurl ? thirdCellLnurl(model) : thirdCellCashu(model),
), ),
); );
} }
@@ -525,13 +608,16 @@ export function defaultCard(model) {
h( h(
'div', 'div',
{ fontFamily: INTER, fontWeight: 500, fontSize: '28px', color: MUTED, marginTop: '14px' }, { fontFamily: INTER, fontWeight: 500, fontSize: '28px', color: MUTED, marginTop: '14px' },
'Cashu mint and Fedimint reviews, signed on Nostr', 'Cashu, Fedimint and LNURL mint reviews, signed on Nostr',
), ),
h( h(
'div', 'div',
{ display: 'flex', gap: '86px', marginTop: '64px' }, // Four stats where there were three, so the gap tightens rather than the row
// running past the card's own margins.
{ display: 'flex', gap: '64px', marginTop: '64px' },
stat(model.mints, 'Cashu mints'), stat(model.mints, 'Cashu mints'),
stat(model.federations, 'federations'), stat(model.federations, 'federations'),
stat(model.lnurl, 'LNURL mints'),
stat(model.reviews, 'signed reviews'), stat(model.reviews, 'signed reviews'),
), ),
); );
+64 -1
View File
@@ -1,7 +1,7 @@
/** /**
* The edge cases the template must survive, as synthetic API payloads. * The edge cases the template must survive, as synthetic API payloads.
* *
* Run through the same cashuModel/fedimintModel derivation as real data — a fixture * Run through the same cashuModel/fedimintModel/lnurlModel derivation as real data — a fixture
* that bypassed the model would test a card no build can produce. `NOW` is pinned so * that bypassed the model would test a card no build can produce. `NOW` is pinned so
* the rendered PNGs under og-fixtures/ are byte-stable and can live in the repo as * the rendered PNGs under og-fixtures/ are byte-stable and can live in the repo as
* eyeball snapshots. * eyeball snapshots.
@@ -32,6 +32,31 @@ const base = {
}, },
}; };
/**
* The shared shape of an LNURL row's detail payload, as `GET /api/mints/:host` returns
* it: the ecosystem fields flattened across the top level, millisatoshi on the wire.
*/
const lnurlBase = {
type: 'lnurl',
status: 'online',
last_online: NOW - 300,
last_probe: NOW - 300,
first_seen: NOW - 200 * 86400,
review_count: 9,
rating_avg: 4.4,
rating_distribution: dist(6, 2, 1),
last_review_at: NOW - 20 * 86400,
features: ['mint', 'melt', 'rotate', 'split', 'merge', 'lud06', 'lud03', 'lud16', 'signed-notes'],
network: 'mainnet',
mint_pubkey: '021ab89df9cbd28cdab8c71d228ca40deba1eaebd827f24849ec4f3e919c023555',
funding_available: true,
invalid_reason: null,
min_withdrawable_msat: 5_000,
max_withdrawable_msat: 999_899_000,
lightning_address: 'mint@lnurl.example',
};
export const FIXTURES = [ export const FIXTURES = [
{ {
// A name at the 30-character mark: must step down, never clip. // A name at the 30-character mark: must step down, never clip.
@@ -109,4 +134,42 @@ export const FIXTURES = [
network: 'mainnet', network: 'mainnet',
status_source: null, status_source: null,
}, },
{
// The healthy LNURL card: blue glow, LNURL badge, a withdraw range in the third
// cell, and the lightning address under it.
...lnurlBase,
host: 'fixture-lnurl-online',
url: 'lnurl:https://lnurl.example',
base_url: 'https://lnurl.example',
name: null,
icon: null,
},
{
// Offline 20 days: red glow beats the blue one, warning cell replaces the range.
// The blue badge stays, because which ecosystem it is has not changed.
...lnurlBase,
host: 'fixture-lnurl-offline',
url: 'lnurl:https://lnurl.gone.example',
base_url: 'https://lnurl.gone.example',
name: 'Departed Notes',
icon: null,
status: 'offline',
last_online: NOW - 20 * 86400,
rating_avg: 2.1,
rating_distribution: dist(0, 1, 1, 3, 5),
review_count: 10,
},
{
// A withdraw ceiling of zero, while ONLINE: green status chip stays, and the
// critical warning cell appears anyway. The one card that must never print
// "max 0 sat" as though it were a limit.
...lnurlBase,
host: 'fixture-lnurl-no-withdrawals',
url: 'lnurl:https://lnurl.locked.example',
base_url: 'https://lnurl.locked.example',
name: 'Locked Notes Mint',
icon: null,
min_withdrawable_msat: 0,
max_withdrawable_msat: 0,
},
]; ];
+77 -4
View File
@@ -14,10 +14,15 @@
* The warning logic is imported from shared/, not restated: the image must never * The warning logic is imported from shared/, not restated: the image must never
* disagree with the page it decorates about whether a mint is frozen or gone. * disagree with the page it decorates about whether a mint is frozen or gone.
*/ */
import { getMintWarnings, parseLimits, readCapabilities } from '@cashumints/shared'; import { getMintWarnings, msatToSat, parseLimits, readCapabilities } from '@cashumints/shared';
/** Bump when the template's layout or copy changes, to regenerate every image. */ /**
export const TEMPLATE_VERSION = 1; * Bump when the template's layout or copy changes, to regenerate every image.
*
* 2: the LNURL variant. Its badge, glow and third cell are new, and the shared
* `warningCell` learned three LNURL warning kinds, so every card's hash input moved.
*/
export const TEMPLATE_VERSION = 2;
const DAY = 86_400; const DAY = 86_400;
@@ -115,6 +120,11 @@ function warningCell(warning, mint, now) {
'offline-long': 'Offline', 'offline-long': 'Offline',
'offline': 'Offline', 'offline': 'Offline',
'fedimint-offline': 'Reported offline', 'fedimint-offline': 'Reported offline',
// LNURL. `lnurl-withdrawals-disabled` gets the same two words as the Cashu
// melt-disabled cell, because it is the same statement to a reader.
'lnurl-withdrawals-disabled': 'No withdrawals',
'lnurl-no-funding': 'No mint / melt',
'lnurl-invalid': 'Not responding properly',
}[warning.kind]; }[warning.kind];
if (!label) return null; if (!label) return null;
@@ -135,11 +145,26 @@ function warningCell(warning, mint, now) {
case 'frozen': case 'frozen':
sub = 'Nothing moves in or out'; sub = 'Nothing moves in or out';
break; break;
case 'lnurl-withdrawals-disabled':
sub = 'Notes cannot be redeemed';
break;
case 'lnurl-no-funding':
// The precise half of the story, and the half that stops a reader writing the
// mint off: its notes still work, only Lightning is shut.
sub = 'Notes still rotate and split';
break;
case 'lnurl-invalid':
sub = 'Endpoint answered with junk';
break;
} }
// The mint is also offline while a capability warning outranks that fact: the cell // The mint is also offline while a capability warning outranks that fact: the cell
// still has to say so, the way the page banner folds it into its last sentence. // still has to say so, the way the page banner folds it into its last sentence.
if (['melt-only', 'melt-disabled', 'frozen'].includes(warning.kind) && mint.status === 'offline' && since !== null) { if (
['melt-only', 'melt-disabled', 'frozen', 'lnurl-withdrawals-disabled'].includes(warning.kind) &&
mint.status === 'offline' &&
since !== null
) {
sub = `Offline since ${shortDate(since)}`; sub = `Offline since ${shortDate(since)}`;
} }
@@ -227,6 +252,53 @@ export function fedimintModel(fed, { iconHash = null, now = Math.floor(Date.now(
}; };
} }
/**
* One LNURL mint's card model.
*
* The same skeleton as the other two, with the third cell answering this ecosystem's
* own question: how much can one of this mint's bearer notes be worth? That is the
* withdraw range, in sats, converted here once from the millisatoshi the wire carries.
*
* `withdraw` is null when the ceiling is zero, and deliberately: a card printing
* "max 0 sat" reads as a number rather than as a refusal, and the warning cell that
* outranks it says the true thing instead.
*/
export function lnurlModel(mint, { iconHash = null, now = Math.floor(Date.now() / 1000) } = {}) {
const baseUrl = mint.base_url ?? mint.url.replace(/^lnurl:/, '');
const domain = displayDomain(baseUrl);
const name = (mint.name ?? '').trim() || domain.split('/')[0] || domain;
const warning = getMintWarnings(mint, { now })[0] ?? null;
const offline = mint.status === 'offline';
const offlineDays =
offline && mint.last_online !== null
? Math.max(0, Math.floor((now - mint.last_online) / DAY))
: null;
const min = msatToSat(mint.min_withdrawable_msat ?? null);
const max = msatToSat(mint.max_withdrawable_msat ?? null);
return {
v: TEMPLATE_VERSION,
type: 'lnurl',
slug: mint.host,
name,
domain,
status: mint.status,
offlineDays,
rating: mint.rating_avg,
reviewCount: mint.review_count,
sentiment: sentiment(mint.rating_distribution ?? {}),
lastReviewMonth: mint.last_review_at ? monthYear(mint.last_review_at) : null,
withdraw: max !== null && max > 0 ? { min, max } : null,
/** The LUD-16 address, which is the one string a reader might actually copy. */
address: mint.lightning_address ?? null,
warning: warningCell(warning, mint, now),
iconHash,
gradient: iconHash ? null : iconGradient(domain),
initials: iconHash ? null : initials(name),
};
}
/** The default card: brand, tagline, and the network's own numbers. */ /** The default card: brand, tagline, and the network's own numbers. */
export function defaultModel(stats) { export function defaultModel(stats) {
return { return {
@@ -235,6 +307,7 @@ export function defaultModel(stats) {
slug: 'default', slug: 'default',
mints: stats.cashu_total, mints: stats.cashu_total,
federations: stats.fedimint_total, federations: stats.fedimint_total,
lnurl: stats.lnurl_total,
reviews: stats.reviews_total, reviews: stats.reviews_total,
}; };
} }
+46
View File
@@ -0,0 +1,46 @@
/**
* A contact sheet of the three ecosystems' cards at thumbnail size.
*
* Not part of the build: run it by hand (`node scripts/og/thumbs.mjs`) to check the
* one thing a full-size render cannot show you — whether a reader scrolling a timeline
* can tell a Cashu card from a Fedimint one from an LNURL one before any text is
* legible. Writes og-fixtures/_thumbs.png.
*/
import { readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { Resvg } from '@resvg/resvg-js';
const here = path.dirname(fileURLToPath(import.meta.url));
const dir = path.resolve(here, '../../og-fixtures');
/** 300px wide: roughly how a link preview renders in a feed. */
const W = 300;
const H = Math.round((630 / 1200) * W);
const GAP = 16;
const LABEL = 22;
const cards = [
['Cashu', 'fixture-no-icon.png'],
['Fedimint', 'fixture-fed-announced.png'],
['LNURL', 'fixture-lnurl-online.png'],
];
const parts = await Promise.all(
cards.map(async ([label, file], i) => {
const data = await readFile(path.join(dir, file));
const x = i * (W + GAP);
return (
`<image x="${x}" y="${LABEL}" width="${W}" height="${H}" ` +
`href="data:image/png;base64,${data.toString('base64')}"/>` +
`<text x="${x}" y="15" font-family="monospace" font-size="13" fill="#EDE8DC">${label}</text>`
);
}),
);
const svg =
`<svg xmlns="http://www.w3.org/2000/svg" width="${cards.length * (W + GAP) - GAP}" ` +
`height="${H + LABEL}"><rect width="100%" height="100%" fill="#0E0D0B"/>${parts.join('')}</svg>`;
await writeFile(path.join(dir, '_thumbs.png'), new Resvg(svg).render().asPng());
console.log(`og: og-fixtures/_thumbs.png (${cards.length} cards at ${W}px)`);
+36 -15
View File
@@ -4,7 +4,7 @@
"name": "mint-page", "name": "mint-page",
"viewportWidth": 324, "viewportWidth": 324,
"width": 324, "width": 324,
"height": 327, "height": 407,
"bones": [ "bones": [
[ [
0, 0,
@@ -16,14 +16,14 @@
[ [
20.9877, 20.9877,
0, 0,
59.2593, 54.4078,
43, 43,
8 8
], ],
[ [
20.9877, 20.9877,
43, 43,
59.2593, 54.4078,
22, 22,
8 8
], ],
@@ -102,8 +102,15 @@
0, 0,
265, 265,
100, 100,
63, 84,
8 8
],
[
0,
366,
42.284,
41,
10
] ]
] ]
}, },
@@ -111,7 +118,7 @@
"name": "mint-page", "name": "mint-page",
"viewportWidth": 712, "viewportWidth": 712,
"width": 712, "width": 712,
"height": 193, "height": 252,
"bones": [ "bones": [
[ [
0, 0,
@@ -123,19 +130,19 @@
[ [
9.5506, 9.5506,
0, 0,
26.9663, 24.7586,
43, 43,
8 8
], ],
[ [
9.5506, 9.5506,
43, 43,
26.9663, 24.7586,
22, 22,
8 8
], ],
[ [
38.764, 36.5564,
18, 18,
10.8146, 10.8146,
29, 29,
@@ -143,7 +150,7 @@
true true
], ],
[ [
40.4494, 38.2417,
29, 29,
0.9831, 0.9831,
7, 7,
@@ -211,6 +218,13 @@
100, 100,
42, 42,
8 8
],
[
0,
211,
19.2416,
41,
10
] ]
] ]
}, },
@@ -218,7 +232,7 @@
"name": "mint-page", "name": "mint-page",
"viewportWidth": 1184, "viewportWidth": 1184,
"width": 1184, "width": 1184,
"height": 172, "height": 231,
"bones": [ "bones": [
[ [
0, 0,
@@ -230,19 +244,19 @@
[ [
5.7432, 5.7432,
0, 0,
16.2162, 14.8886,
43, 43,
8 8
], ],
[ [
5.7432, 5.7432,
43, 43,
16.2162, 14.8886,
22, 22,
8 8
], ],
[ [
23.3108, 21.9832,
18, 18,
6.5034, 6.5034,
29, 29,
@@ -250,7 +264,7 @@
true true
], ],
[ [
24.3243, 22.9967,
29, 29,
0.5912, 0.5912,
7, 7,
@@ -318,9 +332,16 @@
100, 100,
21, 21,
8 8
],
[
0,
190,
11.5709,
41,
10
] ]
] ]
} }
}, },
"_hash": "f3f7f7e86a10ecca631b74af17fb0335" "_hash": "e76f6212eab4b1517002712803fae94e"
} }
+235 -102
View File
@@ -4,132 +4,181 @@
"name": "reviews-panel", "name": "reviews-panel",
"viewportWidth": 322, "viewportWidth": 322,
"width": 322, "width": 322,
"height": 727, "height": 876,
"bones": [ "bones": [
[ [
6.2112, 6.2112,
20, 18,
11.8012, 11.8012,
38, 38,
"50%" "50%"
], ],
[ [
22.9814, 22.9814,
19, 17,
32.6087, 32.6087,
22, 22,
8 8
], ],
[ [
22.9814, 22.9814,
41, 39,
70.8075, 70.8075,
16, 17,
5 5
], ],
[ [
22.9814, 22.9814,
70, 68,
20.429, 20.429,
16, 16,
8 8
], ],
[ [
46.5159, 45.8948,
68, 66,
0.9317,
20,
8
],
[
49.3109,
66,
11.8012, 11.8012,
19, 20,
8 8
], ],
[ [
22.9814, 22.9814,
98, 96,
70.8075, 70.8075,
116, 144,
8
],
[
22.9814,
246,
70.8075,
27,
8 8
], ],
[ [
6.2112, 6.2112,
255, 310,
11.8012, 11.8012,
38, 38,
"50%" "50%"
], ],
[ [
22.9814, 22.9814,
254, 309,
23.6025, 23.6025,
22, 22,
8 8
], ],
[ [
22.9814, 22.9814,
277, 331,
70.8075, 70.8075,
16, 17,
5 5
], ],
[ [
22.9814, 22.9814,
305, 360,
20.429, 20.429,
16, 16,
8 8
], ],
[ [
46.5159, 45.8948,
303, 358,
0.9317,
20,
8
],
[
49.3109,
358,
12.1118, 12.1118,
19, 20,
8 8
], ],
[ [
22.9814, 22.9814,
333, 388,
70.8075, 70.8075,
116, 120,
8
],
[
22.9814,
514,
70.8075,
27,
8 8
], ],
[ [
6.2112, 6.2112,
490, 578,
11.8012, 11.8012,
38, 38,
"50%" "50%"
], ],
[ [
22.9814, 22.9814,
489, 577,
31.0559, 31.0559,
22, 22,
8 8
], ],
[ [
22.9814, 22.9814,
512, 599,
70.8075, 70.8075,
16, 17,
5 5
], ],
[ [
22.9814, 22.9814,
540, 628,
20.429, 20.429,
16, 16,
8 8
], ],
[ [
46.5159, 45.8948,
538, 626,
0.9317,
20,
8
],
[
49.3109,
626,
12.7329, 12.7329,
19, 20,
8 8
], ],
[ [
22.9814, 22.9814,
568, 656,
70.8075, 70.8075,
139, 144,
8
],
[
22.9814,
806,
20.8075,
17,
8
],
[
22.9814,
831,
70.8075,
27,
8 8
] ]
] ]
@@ -138,132 +187,174 @@
"name": "reviews-panel", "name": "reviews-panel",
"viewportWidth": 710, "viewportWidth": 710,
"width": 710, "width": 710,
"height": 455, "height": 533,
"bones": [ "bones": [
[ [
2.8169, 2.8169,
20, 18,
5.3521, 5.3521,
38, 38,
"50%" "50%"
], ],
[ [
10.4225, 10.4225,
21, 17,
14.7887, 14.7887,
22, 22,
8 8
], ],
[ [
10.4225, 10.4225,
43, 39,
75.2421, 67.7773,
16, 17,
5 5
], ],
[ [
87.9181, 80.4533,
22, 21,
9.265, 9.265,
16, 16,
8 8
], ],
[ [
91.831, 90.5634,
42,
5.3521,
19, 19,
0.4225,
20,
8
],
[
91.831,
19,
5.3521,
20,
8 8
], ],
[ [
10.4225, 10.4225,
77, 68,
82.3944,
48,
8
],
[
10.4225,
124,
86.7606, 86.7606,
46, 27,
8 8
], ],
[ [
2.8169, 2.8169,
164, 188,
5.3521, 5.3521,
38, 38,
"50%" "50%"
], ],
[ [
10.4225, 10.4225,
165, 187,
10.7042, 10.7042,
22, 22,
8 8
], ],
[ [
10.4225, 10.4225,
187, 209,
75.2421, 67.6364,
16, 17,
5 5
], ],
[ [
87.9181, 80.3125,
166, 191,
9.265, 9.265,
16, 16,
8 8
], ],
[
90.4225,
189,
0.4225,
20,
8
],
[ [
91.6901, 91.6901,
186, 189,
5.493, 5.493,
19, 20,
8 8
], ],
[ [
10.4225, 10.4225,
221, 238,
82.3944,
48,
8
],
[
10.4225,
294,
86.7606, 86.7606,
46, 27,
8 8
], ],
[ [
2.8169, 2.8169,
308, 358,
5.3521, 5.3521,
38, 38,
"50%" "50%"
], ],
[ [
10.4225, 10.4225,
309, 357,
14.0845, 14.0845,
22, 22,
8 8
], ],
[ [
10.4225, 10.4225,
331, 379,
75.2421, 67.3548,
16, 17,
5 5
], ],
[ [
87.9181, 80.0308,
310, 361,
9.265, 9.265,
16, 16,
8 8
], ],
[
90.1408,
359,
0.4225,
20,
8
],
[ [
91.4085, 91.4085,
330, 359,
5.7746, 5.7746,
19, 20,
8 8
], ],
[ [
10.4225, 10.4225,
365, 408,
82.3944,
72,
8
],
[
10.4225,
488,
86.7606, 86.7606,
70, 27,
8 8
] ]
] ]
@@ -272,136 +363,178 @@
"name": "reviews-panel", "name": "reviews-panel",
"viewportWidth": 784, "viewportWidth": 784,
"width": 784, "width": 784,
"height": 431, "height": 533,
"bones": [ "bones": [
[ [
2.551, 2.551,
20, 18,
4.8469, 4.8469,
38, 38,
"50%" "50%"
], ],
[ [
9.4388, 9.4388,
21, 17,
13.3929, 13.3929,
22, 22,
8 8
], ],
[ [
9.4388, 9.4388,
43, 39,
77.5789, 70.8187,
16, 17,
5 5
], ],
[ [
89.0585, 82.2983,
22, 21,
8.3905, 8.3905,
16, 16,
8 8
], ],
[ [
92.602, 91.4541,
42,
4.8469,
19, 19,
0.3827,
20,
8
],
[
92.602,
19,
4.8469,
20,
8 8
], ],
[ [
9.4388, 9.4388,
77, 68,
74.6173,
48,
8
],
[
9.4388,
124,
88.0102, 88.0102,
46, 27,
8 8
], ],
[ [
2.551, 2.551,
164, 188,
4.8469, 4.8469,
38, 38,
"50%" "50%"
], ],
[ [
9.4388, 9.4388,
165, 187,
9.6939, 9.6939,
22, 22,
8 8
], ],
[ [
9.4388, 9.4388,
187, 209,
77.5789, 70.6912,
16, 17,
5 5
], ],
[ [
89.0585, 82.1708,
166, 191,
8.3905, 8.3905,
16, 16,
8 8
], ],
[
91.3265,
189,
0.3827,
20,
8
],
[ [
92.4745, 92.4745,
186, 189,
4.9745, 4.9745,
19, 20,
8 8
], ],
[ [
9.4388, 9.4388,
221, 238,
74.6173,
48,
8
],
[
9.4388,
294,
88.0102, 88.0102,
46, 27,
8 8
], ],
[ [
2.551, 2.551,
308, 358,
4.8469, 4.8469,
38, 38,
"50%" "50%"
], ],
[ [
9.4388, 9.4388,
309, 357,
12.7551, 12.7551,
22, 22,
8 8
], ],
[ [
9.4388, 9.4388,
331, 379,
77.5789, 70.4361,
16, 17,
5 5
], ],
[ [
89.0585, 81.9157,
310, 361,
8.3905, 8.3905,
16, 16,
8 8
], ],
[
91.0714,
359,
0.3827,
20,
8
],
[ [
92.2194, 92.2194,
330, 359,
5.2296, 5.2296,
19, 20,
8 8
], ],
[ [
9.4388, 9.4388,
365, 408,
74.6173,
72,
8
],
[
9.4388,
488,
88.0102, 88.0102,
46, 27,
8 8
] ]
] ]
} }
}, },
"_hash": "788093c50463a4ad673ece50c6db52da" "_hash": "d9fb0ac370be8e706ff4dfea00b0081b"
} }
+96 -38
View File
@@ -1,5 +1,23 @@
--- ---
/**
* The site footer: one component, every page, three zones.
*
* Zone one is four columns — who this is, then the two link groups people actually
* navigate to, then the language control. Zone two is a thin bar with the copyright
* and the credit, and nothing else: the moment anything else moves into that row it
* stops being a signature and starts being a fourth navigation surface.
*
* What used to be here was four flat rows, the last of which was ~20 raw language
* links wrapped over two lines. That wall is now the same switcher the header carries
* (see LanguageSwitcher.astro, `placement="footer"`), so there is one language control
* on the site in two places rather than two controls that happen to agree.
*
* Everything below is static per locale and prerenders. No hex, no npub, no counts,
* nothing fetched: the footer is the one region of every page that must be identical
* on the home page and on a mint page that failed to load.
*/
import LanguageSwitcher from './LanguageSwitcher.astro'; import LanguageSwitcher from './LanguageSwitcher.astro';
import Moai from './Moai.astro';
import { localePath, useI18n } from '../i18n'; import { localePath, useI18n } from '../i18n';
import { pageLocale } from '../i18n/paths'; import { pageLocale } from '../i18n/paths';
@@ -9,23 +27,29 @@ const GITHUB = 'https://github.com/Azzamo-net/cashumints.space';
const { locale } = pageLocale(Astro); const { locale } = pageLocale(Astro);
const t = useI18n(locale); const t = useI18n(locale);
/** /*
* Two rows: the pages people navigate to, then the ones they go looking for. The * Route slugs stay English in every language and `localePath` adds the prefix, exactly
* disclaimer sits in the second row here and again at the bottom of every mint page, * as the header does. See src/i18n/routing.ts for why that trade was made.
* which is where the decision it is about actually gets made.
*/ */
const nav = [ const explore = [
{ path: '/mints', label: t('footer.allMints') }, { href: localePath('/mints', locale), label: t('footer.allMints') },
{ path: '/fedimints', label: t('footer.allFedimints') }, { href: localePath('/fedimints', locale), label: t('footer.allFedimints') },
{ path: '/reviews', label: t('footer.allReviews') }, { href: localePath('/lnurl-mints', locale), label: t('footer.allLnurlMints') },
{ path: '/wallets', label: t('footer.wallets') }, { href: localePath('/reviews', locale), label: t('footer.allReviews') },
{ path: '/about', label: t('footer.about') }, { href: localePath('/wallets', locale), label: t('footer.wallets') },
]; ];
const legal = [ /*
{ path: '/disclaimer', label: t('footer.disclaimer') }, * GitHub sits in this column rather than beside the credit line because it is a place
{ path: '/terms', label: t('footer.terms') }, * to go, not a signature. It is the only external link in the columns, so it carries
{ path: '/privacy', label: t('footer.privacy') }, * `rel="noopener"` and nothing else needs to.
*/
const site = [
{ href: localePath('/about', locale), label: t('footer.about'), external: false },
{ href: GITHUB, label: 'GitHub', external: true },
{ href: localePath('/terms', locale), label: t('footer.terms'), external: false },
{ href: localePath('/privacy', locale), label: t('footer.privacy'), external: false },
{ href: localePath('/disclaimer', locale), label: t('footer.disclaimer'), external: false },
]; ];
/* /*
@@ -39,31 +63,65 @@ const madeBy = t('footer.madeBy', {
}); });
--- ---
<footer> <footer class="site-footer" role="contentinfo">
<div class="foot-inner"> <div class="foot-cols">
<span>{t('footer.copyright', { year: String(year) })}</span> {/*
<div class="links"> The brand column. The custodial warning lives here, under the description,
<a href={GITHUB} rel="noopener">GitHub</a> because it is the most honest sentence on the site and it was previously an
{nav.map((link) => <a href={localePath(link.path, locale)}>{link.label}</a>)} orphan floating at the bottom right of the last row. It is set in `--muted`
rather than `--faint`: quiet, but a step louder than the link columns, which is
the only typographic claim this footer makes about relative importance.
*/}
<div class="foot-brand">
<a class="foot-mark" href={localePath('/', locale)}>
<Moai size={20} />
<span>Cashumints.space</span>
</a>
<p class="foot-desc">{t('footer.tagline')}</p>
<p class="foot-warn">{t('footer.warning')}</p>
</div>
{/*
One nav landmark over both link columns, not one per column: a screen reader
listing landmarks should find "Footer", not "Footer" twice. The language control
carries its own nav, because it is a different kind of thing — it does not take
you to another page, it takes you to this page again.
*/}
<nav class="foot-nav" aria-label={t('footer.nav')}>
<div class="foot-col">
<h2 class="foot-head">{t('footer.col.explore')}</h2>
<ul>
{explore.map((link) => (
<li><a href={link.href}>{link.label}</a></li>
))}
</ul>
</div>
<div class="foot-col">
<h2 class="foot-head">{t('footer.col.site')}</h2>
<ul>
{site.map((link) => (
<li>
<a href={link.href} rel={link.external ? 'noopener' : undefined}>{link.label}</a>
</li>
))}
</ul>
</div>
</nav>
<div class="foot-col foot-lang">
<h2 class="foot-head">{t('lang.label')}</h2>
<LanguageSwitcher placement="footer" />
</div> </div>
<span class="foot-right" set:html={madeBy} />
</div> </div>
<div class="foot-inner foot-legal">
<div class="links"> {/*
{legal.map((link) => <a href={localePath(link.path, locale)}>{link.label}</a>)} The bottom bar. Two items, and the discipline is that it stays two items: the
</div> copyright and who made it. Everything else that was ever tempted into this row now
<LanguageSwitcher variant="inline" /> has a column above it.
<span class="foot-right foot-warn">{t('footer.warning')}</span> */}
<div class="foot-bar">
<span>{t('footer.copyright', { year: String(year) })}</span>
<span class="foot-credit" set:html={madeBy} />
</div> </div>
</footer> </footer>
<style>
/* The second row is quieter than the first and has no top border of its own: it
reads as a continuation of the footer, not a second footer. */
.foot-legal { padding-top: 0; border-top: none; font-size: 12.5px; }
.foot-warn { color: var(--faint); }
@media (max-width: 640px) {
.foot-legal { padding-top: 4px; }
}
</style>
+113 -101
View File
@@ -1,12 +1,17 @@
--- ---
/** /**
* The language switcher, in two shapes. * The language switcher. One control, two placements.
* *
* `menu` is the header control: a compact button showing the current language's code, * `bar` is the header's: a compact button showing the current language's code. `footer`
* opening a searchable list. `inline` is the footer's plain row of links. Both * is the Language column's: the same button, showing the language's full name, opening
* are built from the same list and the same hrefs, because they are the same control * upward so the panel is never clipped off the bottom of the document. Same list, same
* and a reader who finds one and then the other should not have to work out whether * hrefs, same markup, same script — the only differences are which label the trigger
* they do the same thing. * shows and which way the panel opens, because a reader who finds one and then the
* other should not have to work out whether they do the same thing.
*
* The footer used to carry a second, different control: every locale as a flat link,
* ~20 of them wrapped over two rows. That was not a switcher, it was a wall, and it
* grew by one row every time a language was added.
* *
* Text, not flags, in both. A flag is a country: Spanish is not Spain, Dutch is not the * Text, not flags, in both. A flag is a country: Spanish is not Spain, Dutch is not the
* Netherlands, and English belongs to nobody in particular. Each language is written in * Netherlands, and English belongs to nobody in particular. Each language is written in
@@ -15,22 +20,24 @@
* Every link points at this same page in that language, built from the unprefixed path, * Every link points at this same page in that language, built from the unprefixed path,
* so this works identically on a mint page: /mint/kashu.me and /es/mint/kashu.me are * so this works identically on a mint page: /mint/kashu.me and /es/mint/kashu.me are
* the same mint. There is no "switch to Spanish and land on the home page" case, which * the same mint. There is no "switch to Spanish and land on the home page" case, which
* is the usual failure of a switcher that only knows about the site root. * is the usual failure of a switcher that only knows about the site root. They are real
* `<a href>` elements, so a crawler follows them and a reader with no JavaScript uses
* them.
* *
* The header shape is a `<details>` rather than a scripted dropdown. It opens, closes * The control is a `<details>` rather than a scripted dropdown. It opens, closes and
* and takes the keyboard with no JavaScript at all, which matters because the whole * takes the keyboard with no JavaScript at all, which matters because the whole point
* point of this control is being able to leave a page you cannot read. The script below * of this control is being able to leave a page you cannot read. The script below only
* only adds what `<details>` does not do by itself: closing on Escape and on a click * adds what `<details>` does not do by itself: closing on Escape and on a click
* elsewhere. * elsewhere, and filtering the list as you type.
*/ */
import { LOCALES, localePath, useI18n } from '../i18n'; import { LOCALES, localePath, localeInfo, useI18n } from '../i18n';
import { pageLocale } from '../i18n/paths'; import { pageLocale } from '../i18n/paths';
interface Props { interface Props {
/** `menu` is the header control; `inline` is the footer row. */ /** `bar` is the header control; `footer` is the one in the Language column. */
variant?: 'menu' | 'inline'; placement?: 'bar' | 'footer';
} }
const { variant = 'inline' } = Astro.props; const { placement = 'bar' } = Astro.props;
const { locale, path } = pageLocale(Astro); const { locale, path } = pageLocale(Astro);
const t = useI18n(locale); const t = useI18n(locale);
@@ -43,76 +50,59 @@ const links = LOCALES.map((other) => ({
})); }));
/* /*
* Two letters in the header, because the full name of the current language is a word * The header shows two letters, because the full name of the current language is a word
* the reader already knows they are reading. The code is the locale's own, uppercased, * the reader already knows they are reading, and the header row is full. The footer
* so a fourth language needs nothing added here. * column has the width for the name and no adjacent nav to crowd, and a column headed
* "Language" reads better with "Nederlands" under it than with "NL".
*/ */
const currentCode = locale.toUpperCase(); const trigger = placement === 'footer' ? localeInfo(locale).label : locale.toUpperCase();
/*
* The search field earns its place at 20-odd languages and is noise at three. It is
* the one part of this control that needs JavaScript, so it is also the one part that
* is allowed to be absent: every link is in the panel either way.
*/
const searchable = LOCALES.length > 8;
--- ---
{ <details class={`lang-menu lang-${placement}`} data-lang-menu>
variant === 'menu' ? ( <summary aria-haspopup="true">
<details class="lang-menu" data-lang-menu> <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
<summary aria-haspopup="true"> <circle cx="12" cy="12" r="9" />
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"> <path d="M3 12h18M12 3a15 15 0 0 1 4 9 15 15 0 0 1-4 9 15 15 0 0 1-4-9 15 15 0 0 1 4-9Z" />
<circle cx="12" cy="12" r="9" /> </svg>
<path d="M3 12h18M12 3a15 15 0 0 1 4 9 15 15 0 0 1-4 9 15 15 0 0 1-4-9 15 15 0 0 1 4-9Z" /> {/* The trigger label does not say what it is on its own, so the word is in the
</svg> accessibility tree and out of the way for everyone else. */}
{/* The code alone does not say what it is, so the label is in the tree for a <span class="sr-only">{t('lang.label')}</span>
screen reader and out of the way for everyone else. */} <span class="lang-current">{trigger}</span>
<span class="sr-only">{t('lang.label')}</span> <svg class="lang-caret" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
<span class="lang-code">{currentCode}</span> <path d="m6 9 6 6 6-6" />
<svg class="lang-caret" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"> </svg>
<path d="m6 9 6 6 6-6" /> </summary>
</svg>
</summary>
<div class="lang-panel"> <div class="lang-panel">
<div class="lang-search"> {searchable && (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"> <div class="lang-search">
<circle cx="11" cy="11" r="7" /> <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="m20 20-3.5-3.5" /> <circle cx="11" cy="11" r="7" />
</svg> <path d="m20 20-3.5-3.5" />
<input </svg>
type="search" <input
placeholder={`${t('lang.label')}…`} type="search"
aria-label={t('lang.switcher')} placeholder={`${t('lang.label')}…`}
autocomplete="off" aria-label={t('lang.switcher')}
spellcheck="false" autocomplete="off"
data-lang-search spellcheck="false"
/> data-lang-search
</div> />
<nav class="lang-list" aria-label={t('lang.switcher')}>
{
links.map((other) => (
<a
href={other.href}
hreflang={other.code}
lang={other.code}
aria-current={other.current ? 'true' : undefined}
data-lang-option
data-search={`${other.label} ${other.code}`}
>
<span>{other.label}</span>
<span class="lang-option-code">{other.code.toUpperCase()}</span>
{other.current && (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
<path d="m5 13 4 4L19 7" />
</svg>
)}
</a>
))
}
</nav>
</div> </div>
</details> )}
) : ( <nav class="lang-list" aria-label={t('lang.switcher')}>
<nav class="lang-switch" aria-label={t('lang.switcher')}>
{ {
/* /*
* The current language is still a link, and it goes where it says it goes. * The current language is still a link, and it goes where it says it goes.
* `aria-current` is what tells a screen reader it is where you already are, and * `aria-current` is what tells a screen reader it is where you already are; the
* the weight is what tells everyone else. * weight and the tick are what tell everyone else.
*/ */
links.map((other) => ( links.map((other) => (
<a <a
@@ -120,18 +110,24 @@ const currentCode = locale.toUpperCase();
hreflang={other.code} hreflang={other.code}
lang={other.code} lang={other.code}
aria-current={other.current ? 'true' : undefined} aria-current={other.current ? 'true' : undefined}
data-lang-option
data-search={`${other.label} ${other.code}`}
> >
{other.label} <span>{other.label}</span>
<span class="lang-option-code">{other.code.toUpperCase()}</span>
{other.current && (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
<path d="m5 13 4 4L19 7" />
</svg>
)}
</a> </a>
)) ))
} }
</nav> </nav>
) </div>
} </details>
<style> <style>
/* ---------- the header control ---------- */
.lang-menu { position: relative; flex-shrink: 0; } .lang-menu { position: relative; flex-shrink: 0; }
.lang-menu summary { .lang-menu summary {
@@ -148,14 +144,13 @@ const currentCode = locale.toUpperCase();
.lang-menu summary:hover { color: var(--text); background: var(--card2); } .lang-menu summary:hover { color: var(--text); background: var(--card2); }
.lang-menu[open] summary { color: var(--text); background: var(--card2); } .lang-menu[open] summary { color: var(--text); background: var(--card2); }
.lang-code { font-family: var(--mono); font-size: 12.5px; letter-spacing: .04em; }
.lang-caret { color: var(--faint); transition: transform var(--dur-quick) var(--ease-out); } .lang-caret { color: var(--faint); transition: transform var(--dur-quick) var(--ease-out); }
.lang-menu[open] .lang-caret { transform: rotate(180deg); } .lang-menu[open] .lang-caret { transform: rotate(180deg); }
/* The same panel the account menu uses, so the two controls beside each other in the /* The same panel the account menu uses, so the two controls beside each other in the
header open into the same shape. */ header open into the same shape. */
.lang-panel { .lang-panel {
position: absolute; top: calc(100% + 8px); right: 0; z-index: 60; position: absolute; z-index: 60;
width: min(260px, calc(100vw - 24px)); width: min(260px, calc(100vw - 24px));
background: var(--card); border: 1px solid var(--line); border-radius: 12px; background: var(--card); border: 1px solid var(--line); border-radius: 12px;
padding: 6px; padding: 6px;
@@ -181,11 +176,11 @@ const currentCode = locale.toUpperCase();
.lang-search input::-webkit-search-cancel-button { cursor: pointer; } .lang-search input::-webkit-search-cancel-button { cursor: pointer; }
.lang-list { .lang-list {
max-height: min(360px, calc(100dvh - 150px)); max-height: min(360px, calc(100dvh - 150px));
margin-top: 6px; padding-right: 2px;
display: flex; flex-direction: column; gap: 2px; display: flex; flex-direction: column; gap: 2px;
overflow-y: auto; overscroll-behavior: contain; overflow-y: auto; overscroll-behavior: contain;
scrollbar-width: thin; scrollbar-color: var(--line) transparent; scrollbar-width: thin; scrollbar-color: var(--line) transparent;
} }
.lang-search + .lang-list { margin-top: 6px; padding-right: 2px; }
.lang-list a { .lang-list a {
display: flex; align-items: center; gap: 8px; display: flex; align-items: center; gap: 8px;
padding: 8px 10px; border-radius: 8px; padding: 8px 10px; border-radius: 8px;
@@ -201,6 +196,11 @@ const currentCode = locale.toUpperCase();
} }
.lang-list a svg { color: var(--amber); flex-shrink: 0; } .lang-list a svg { color: var(--amber); flex-shrink: 0; }
/* ---------- the header placement ---------- */
.lang-bar .lang-current { font-family: var(--mono); font-size: 12.5px; letter-spacing: .04em; }
.lang-bar .lang-panel { top: calc(100% + 8px); right: 0; }
/* /*
* Narrow: the globe alone. * Narrow: the globe alone.
* *
@@ -211,37 +211,49 @@ const currentCode = locale.toUpperCase();
* accessible name stays either way, because it was never the visible text. * accessible name stays either way, because it was never the visible text.
*/ */
@media (max-width: 760px) { @media (max-width: 760px) {
.lang-menu summary { padding: 7px 8px; gap: 0; } .lang-bar summary { padding: 7px 8px; gap: 0; }
.lang-code, .lang-bar .lang-current,
.lang-caret { display: none; } .lang-bar .lang-caret { display: none; }
} }
@media (max-width: 420px) { @media (max-width: 420px) {
.lang-menu summary { padding: 6px 6px; } .lang-bar summary { padding: 6px 6px; }
} }
/* ---------- the footer row ---------- */ /* ---------- the footer placement ---------- */
.lang-switch { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; } /*
.lang-switch a { * Aligned with the column's other text rather than inset by the trigger's padding,
font-size: 12.5px; color: var(--faint); * so the globe sits on the same vertical line as the "Language" heading above it and
transition: color var(--dur-quick); * the links in the columns beside it.
} */
.lang-switch a:hover { color: var(--text); } .lang-footer { display: inline-block; margin-inline-start: -10px; }
.lang-switch a[aria-current] { color: var(--text); font-weight: 600; } .lang-footer summary { font-size: 13.5px; }
.lang-footer .lang-current { font-weight: 500; }
/*
* Opens upward. The control is a few hundred pixels from the bottom of the document,
* so a panel hanging below it would be a scroll into empty space at best and clipped
* at worst. `inset-inline-start: 0` rather than `left`, because two of the shipped
* locales are right-to-left and the panel should hang from the trigger's leading
* edge in both directions.
*/
.lang-footer .lang-panel { bottom: calc(100% + 8px); inset-inline-start: 0; }
.lang-footer .lang-list { max-height: min(300px, calc(100dvh - 200px)); }
</style> </style>
<script> <script>
import { onReady } from '../scripts/reveal'; import { onReady } from '../scripts/reveal';
/** /**
* The two things `<details>` does not do on its own. * The three things `<details>` does not do on its own.
* *
* Everything else about this control (open, close, keyboard, focus) is the element's * Everything else about this control (open, close, keyboard, focus) is the element's
* own behaviour and works with this script blocked, which is the point of using one. * own behaviour and works with this script blocked, which is the point of using one.
* *
* Attached to the document once rather than to the element, because the view * Attached to the document once rather than to the element, because the view
* transition router replaces the header on every navigation and a per-element * transition router replaces the header and the footer on every navigation and a
* listener would be re-bound, and leak, once per page visited. * per-element listener would be re-bound, and leak, once per page visited. One
* listener set serves both placements.
*/ */
let wired = false; let wired = false;
+38 -16
View File
@@ -1,9 +1,10 @@
--- ---
import { import {
federationIdFromSlug, getMintWarnings, mintChip, baseUrlFromKey, federationIdFromSlug, getMintWarnings, mintChip,
type MintCapabilities, type MintListItem, type LnurlDetail, type MintCapabilities, type MintListItem,
} from '@cashumints/shared'; } from '@cashumints/shared';
import { iconUrl } from '../lib/api'; import { iconUrl } from '../lib/api';
import { targetPath } from '../lib/feed-resolve';
import { displayDomain, iconGradient, initials, transitionName } from '../lib/format'; import { displayDomain, iconGradient, initials, transitionName } from '../lib/format';
import { localePath, useI18n } from '../i18n'; import { localePath, useI18n } from '../i18n';
import { formatters, starString } from '../i18n/format'; import { formatters, starString } from '../i18n/format';
@@ -28,28 +29,43 @@ interface Props {
* from the detail it already has. Left off, the card simply has no chip. * from the detail it already has. Left off, the card simply has no chip.
*/ */
capabilities?: MintCapabilities | null; capabilities?: MintCapabilities | null;
/**
* The LNURL states a chip can be drawn from, for a caller that already has the
* detail. The Cashu counterpart is `capabilities` above, and this is separate rather
* than merged with it because the two ecosystems' chips are read from entirely
* different facts: NUT switches there, an advertised withdraw ceiling and a reachable
* Lightning node here. Left off, an LNURL card simply has no chip.
*/
lnurl?: Pick<LnurlDetail, 'max_withdrawable_msat' | 'funding_available'> | null;
} }
const { mint, rank, sentiment, revealDelay, capabilities } = Astro.props; const { mint, rank, sentiment, revealDelay, capabilities, lnurl } = Astro.props;
const { locale } = pageLocale(Astro); const { locale } = pageLocale(Astro);
const t = useI18n(locale); const t = useI18n(locale);
const f = formatters(t); const f = formatters(t);
/* /*
* One card, both ecosystems. * One card, all three ecosystems.
* *
* A federation has no URL, so the second line cannot be a domain: `fedimint:aeca6c…` * A federation has no URL, so the second line cannot be a domain: `fedimint:aeca6c…`
* is a database key and means nothing to a reader. It shows the federation id instead, * is a database key and means nothing to a reader. It shows the federation id instead,
* shortened, which is the thing that actually identifies it — and which is what the * shortened, which is the thing that actually identifies it — and which is what the
* search box below matches on, so pasting an id finds its federation. * search box below matches on, so pasting an id finds its federation.
* *
* An LNURL mint does have a URL, but its row key carries an `lnurl:` scheme in front of
* it, so the domain comes out of the key rather than off it directly. A reader should
* see `lnurl.21mint.me`, not `lnurl:https://lnurl.21mint.me`.
*
* Everything else on the card is identical, and deliberately: rating, review count, * Everything else on the card is identical, and deliberately: rating, review count,
* sentiment bar and status all mean the same thing for both, and a reader moving * sentiment bar and status all mean the same thing for all three, and a reader moving
* between /mints and /fedimints should not have to re-learn a card. * between the three indexes should not have to re-learn a card.
*/ */
const fedimint = mint.type === 'fedimint'; const fedimint = mint.type === 'fedimint';
const domain = fedimint ? federationIdFromSlug(mint.host) : displayDomain(mint.url); const lnurlBase = baseUrlFromKey(mint.url);
const domain = fedimint
? federationIdFromSlug(mint.host)
: displayDomain(lnurlBase ?? mint.url);
const name = fedimint const name = fedimint
? mint.name ?? t('card.unnamedFederation') ? mint.name ?? t('card.unnamedFederation')
: mint.name ?? domain.split('/')[0] ?? domain; : mint.name ?? domain.split('/')[0] ?? domain;
@@ -57,7 +73,9 @@ const icon = iconUrl(mint.icon);
const offline = mint.status === 'offline'; const offline = mint.status === 'offline';
/** Announced on Nostr and never confirmed by any check. Federations only. */ /** Announced on Nostr and never confirmed by any check. Federations only. */
const announced = mint.status === 'announced'; const announced = mint.status === 'announced';
const href = localePath(`${fedimint ? '/fedimint' : '/mint'}/${mint.host}`, locale); // One route per ecosystem, from the same table the review feed's links use, so a card
// and a feed row can never disagree about where a listing lives.
const href = localePath(targetPath(mint), locale);
/* /*
Two words at most. Offline already has its own card styling, so only the states a Two words at most. Offline already has its own card styling, so only the states a
@@ -65,14 +83,18 @@ const href = localePath(`${fedimint ? '/fedimint' : '/mint'}/${mint.host}`, loca
cannot pay back out, or one that has stopped moving sats in either direction. The cannot pay back out, or one that has stopped moving sats in either direction. The
mint page carries the explanation. mint page carries the explanation.
*/ */
const chip = capabilities const chipSource = capabilities || lnurl
? mintChip( ? {
getMintWarnings( type: mint.type,
{ status: mint.status, last_online: mint.last_online, capabilities }, status: mint.status,
warningOptions(f), last_online: mint.last_online,
), capabilities: capabilities ?? null,
chipStrings(t), ...(lnurl ?? {}),
) }
: null;
const chip = chipSource
? mintChip(getMintWarnings(chipSource, warningOptions(f)), chipStrings(t))
: null; : null;
const status = statusLabel(mint.status, t); const status = statusLabel(mint.status, t);
+54 -9
View File
@@ -11,11 +11,12 @@
* the same code that prerendered them, so a live upgrade can never leave the three * the same code that prerendered them, so a live upgrade can never leave the three
* disagreeing with each other. * disagreeing with each other.
* *
* Both a mint page and a federation page mount this, unchanged. It fetches the same * All three ecosystems' pages mount this, unchanged. It fetches the same endpoint for
* endpoint for both and updates whichever of the four regions the page actually has: * every one of them and updates whichever regions the page actually has: a federation
* a federation has no limits cell and no NUT rows, and a mint has no modules panel, so * has no limits cell and no NUT rows, an LNURL mint has features rather than NUTs and
* each lookup simply misses. `getMintWarnings` branches on the `type` in the payload, * withdraw bounds rather than mint limits, and a Cashu mint has neither of the other
* so a fresh read can never put a Cashu banner on a federation. * two panels — so each lookup simply misses. `getMintWarnings` branches on the `type` in
* the payload, so a fresh read can never put one ecosystem's banner on another's page.
*/ */
interface Props { interface Props {
host: string; host: string;
@@ -26,15 +27,18 @@ const { host } = Astro.props;
<span data-mint-live={host} hidden></span> <span data-mint-live={host} hidden></span>
<script> <script>
import { getMintWarnings, readCapabilities, type MintWarning } from '@cashumints/shared'; import {
displayFeatures, getMintWarnings, readCapabilities, type MintWarning,
} from '@cashumints/shared';
import { apiBase } from '../lib/client'; import { apiBase } from '../lib/client';
import { bannerHtml, limitsHtml, nutRowsHtml } from '../lib/mint-state'; import { bannerHtml, limitsHtml, nutRowsHtml } from '../lib/mint-state';
import { moduleRowsHtml } from '../lib/fedimint-ui'; import { moduleRowsHtml } from '../lib/fedimint-ui';
import { featureRowsHtml, withdrawLimitsHtml } from '../lib/lnurl-ui';
import { useI18n } from '../i18n/client'; import { useI18n } from '../i18n/client';
import { formatters, type Formatters } from '../i18n/format'; import { formatters, type Formatters } from '../i18n/format';
import { statusLabel, warningOptions } from '../i18n/mint'; import { statusLabel, warningOptions } from '../i18n/mint';
import { onReady, prefersReducedMotion } from '../scripts/reveal'; import { onReady, prefersReducedMotion } from '../scripts/reveal';
import type { MintDetail } from '@cashumints/shared'; import type { LnurlDetail, MintDetail } from '@cashumints/shared';
function remove(el: Element): void { function remove(el: Element): void {
el.classList.add('leaving'); el.classList.add('leaving');
@@ -85,7 +89,7 @@ const { host } = Astro.props;
} }
/** Keep the places that repeat the banner's facts in step with it. */ /** Keep the places that repeat the banner's facts in step with it. */
function renderCapabilities(mint: MintDetail, f: Formatters): void { function renderCapabilities(mint: MintDetail & Partial<LnurlDetail>, f: Formatters): void {
/* /*
* A federation's modules panel, rebuilt from a fresh announcement. Handled before * A federation's modules panel, rebuilt from a fresh announcement. Handled before
* the NUT work rather than after it because the two are mutually exclusive: the * the NUT work rather than after it because the two are mutually exclusive: the
@@ -98,6 +102,35 @@ const { host } = Astro.props;
return; return;
} }
/*
* An LNURL mint's features and withdraw bounds, from a fresh probe. Handled here
* for the same reason the modules panel is, and with the same early return: these
* elements only exist on an LNURL page, and falling through to `readCapabilities`
* on a payload with no `info` would be reading NUT switches this ecosystem has none
* of.
*
* The limits cell is rebuilt inside this branch rather than left to the shared code
* below, because the two cells hold different numbers — withdraw bounds here, NUT-04
* mint limits there — and the selector is the same.
*/
const featureRows = document.querySelector<HTMLElement>('[data-live-features]');
if (featureRows) {
featureRows.innerHTML = featureRowsHtml(
displayFeatures(mint.features, mint.observed_features),
mint.funding_available ?? null,
f,
);
const lnurlLimits = document.querySelector<HTMLElement>('[data-live-limits]');
if (lnurlLimits) {
lnurlLimits.innerHTML = withdrawLimitsHtml(
mint.min_withdrawable_msat,
mint.max_withdrawable_msat,
f,
);
}
return;
}
const caps = readCapabilities(mint.info?.nuts); const caps = readCapabilities(mint.info?.nuts);
const limits = document.querySelector<HTMLElement>('[data-live-limits]'); const limits = document.querySelector<HTMLElement>('[data-live-limits]');
@@ -155,8 +188,20 @@ const { host } = Astro.props;
*/ */
const checked = document.querySelector<HTMLElement>('[data-live-checked]'); const checked = document.querySelector<HTMLElement>('[data-live-checked]');
if (checked && mint.type !== 'fedimint') { if (checked && mint.type !== 'fedimint') {
/*
* The "up" wording differs by whether a version was discoverable, and only
* LNURL routinely has none: its version comes solely from `/openapi.json`,
* which an operator can turn off, so that cell falls back to "last confirmed"
* rather than printing "unknown" above a date. A Cashu mint has `version` set
* from `/v1/info` in every real case and takes the same branch it always did.
*/
const up =
mint.version
? t('mint.checked', { when: f.relative(mint.last_probe) })
: t('lnurl.confirmed.when', { when: f.relative(mint.last_probe) });
checked.textContent = checked.textContent =
mint.status !== 'offline' ? t('mint.checked', { when: f.relative(mint.last_probe) }) mint.status !== 'offline' ? up
: mint.last_online ? t('mint.lastSeen', { when: f.relative(mint.last_online) }) : mint.last_online ? t('mint.lastSeen', { when: f.relative(mint.last_online) })
: t('mint.neverReached'); : t('mint.neverReached');
} }
+455
View File
@@ -0,0 +1,455 @@
---
import Base from '../layouts/Base.astro';
import Moai from './Moai.astro';
import Reviews from './Reviews.astro';
import ReviewByUrl from './ReviewByUrl.astro';
import { localePath, useI18n, type Locale } from '../i18n';
/**
* The 404, as a component rather than a page, because it is reached two ways.
*
* `pages/404.astro` emits the English one at `/404`, which is also the route Astro's
* dev server and a static host look for by name; `pages/[...locale]/404.astro` emits
* the prefixed ones. Neither can import the other's template, so the page itself
* lives here and both are three lines.
*
* Also the resolver for a mint this build has never heard of, which is most of what
* the file now does.
*
* It began as a fallback for a mint discovered *between builds*: a GET against the API
* for the host in the URL, and a summary rendered from whatever came back. That covered
* the gap between discovery and the next rebuild and nothing else — a mint nobody had
* announced on Nostr was not in the index either, so the lookup missed and the reader
* got a 404 for a mint that was live and answering. `/lnurl-mint/mint.600.wtf` was the
* case that made it obvious.
*
* So the lookup became a resolver, for all three route shapes:
*
* 1. the API is asked whether it knows the host (unchanged, and the common case);
* 2. on a miss, the address is derived from the path and `POST /api/index` checks it
* live — one probe, and a Nostr lookup if nothing answers;
* 3. whatever comes back renders the page shell *and* the reviews panel, so a mint
* indexed a second ago is readable and reviewable immediately;
* 4. and every way that can fail says what happened in plain words, with the
* review-by-URL dialog offered so a mistyped address can be corrected on the spot.
*
* A federation is the one shape that cannot be resolved from its own URL: `/fedimint/`
* carries a shortened federation id, and an invite code cannot be derived from one. That
* link goes straight to step 4's not-found state, with the dialog offered for the code.
*/
interface Props {
locale: Locale;
}
const { locale } = Astro.props;
const t = useI18n(locale);
---
<Base
title={t('notfound.title')}
description={t('notfound.description')}
current="mints"
offGraph
>
<div class="page">
<div class="notfound" data-notfound>
{/* 44px, not a hero graphic: DESIGN.md says the moai is never scaled large. */}
<span class="nf-moai"><Moai size={44} /></span>
<h1>{t('notfound.heading')}</h1>
<p class="lede">{t('notfound.lede')}</p>
<form class="search nf-search" action={localePath('/mints', locale)} method="get" role="search">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<circle cx="11" cy="11" r="7" /><path d="m20 20-3.5-3.5" />
</svg>
<label class="sr-only" for="nf-search">{t('home.search.label')}</label>
<input
id="nf-search"
name="q"
type="text"
autocomplete="off"
placeholder={t('home.search.placeholder')}
data-search-input
/>
<kbd aria-hidden="true">/</kbd>
</form>
<div class="nf-actions">
<a class="btn primary" href={localePath('/mints', locale)}>{t('notfound.browse')}</a>
<a class="btn" href={localePath('/', locale)}>{t('notfound.home')}</a>
</div>
</div>
{/*
Filled in by the resolver below when the URL looks like a mint page. Nothing
here is prerendered, so this is the one place on the site that gets a page
level skeleton. `data-boneyard` marks the capture target for `pnpm bones`.
*/}
<div class="mint-fallback" data-mint-fallback hidden>
{/*
One line, in words, above the skeleton: this page is doing something unusual —
checking a stranger's mint over the network — and a screenful of bones with no
explanation reads as a page that is merely slow. Outside the capture target on
purpose, so it is not measured into the skeleton it introduces.
*/}
<p class="fb-checking" data-nf-status hidden></p>
<div data-boneyard="mint-page" data-boneyard-config='{"excludeSelectors":[".sr-only"]}'><div data-mint-fallback-body></div></div>
{/*
The real reviews panel, with no subject until one is resolved.
It is the same island every mint page runs, so a mint resolved here gets the
same list, the same filters and the same write dialog rather than a lesser
version of them — which is the difference between "this mint is reachable" and
"this mint is reviewable", and the second is the point of the whole feature.
The island sits out its first run with no subject and starts when the resolver
dispatches `cashumints:subject`.
*/}
<div class="nf-reviews" data-nf-reviews hidden>
<Reviews subject={null} reviewCount={0} distribution={{ '1': 0, '2': 0, '3': 0, '4': 0, '5': 0 }} name="" />
</div>
</div>
</div>
{/*
The same dialog the index pages carry. Here it is the way out of every dead end:
a wrong ecosystem, an address with a typo in it, or a federation link whose invite
code only the reader has. Its trigger names which ecosystem to ask about.
*/}
<ReviewByUrl type="cashu" />
</Base>
<style>
.notfound { padding: 54px 0 20px; max-width: 620px; }
.nf-moai { display: block; margin-bottom: 18px; }
.notfound h1 { font-family: var(--display); font-size: 34px; font-weight: 700; letter-spacing: -.02em; }
.nf-actions { display: flex; gap: 12px; margin-top: 22px; flex-wrap: wrap; }
/* Same search control as the home page hero, so a miss lands somewhere useful. */
.nf-search {
display: flex; align-items: center; gap: 12px; margin-top: 24px;
background: var(--card); border: 1px solid var(--line);
border-radius: 14px; padding: 14px 16px;
transition: border-color .15s, box-shadow .15s;
}
.nf-search:focus-within { border-color: var(--amber-dim); box-shadow: 0 0 0 3px rgba(240, 169, 59, .08); }
.nf-search > svg { color: var(--faint); flex-shrink: 0; }
.nf-search input {
flex: 1; background: none; border: none; outline: none; min-width: 0;
color: var(--text); font-family: var(--body); font-size: 15.5px;
}
.nf-search input::placeholder { color: var(--faint); }
.nf-search kbd {
font-family: var(--mono); font-size: 11.5px; color: var(--faint);
border: 1px solid var(--line); border-radius: 6px; padding: 2px 7px; flex-shrink: 0;
}
/* The .fb-* rules live in global.css: this page builds that markup with
innerHTML, and Astro's scoped selectors do not reach injected elements. */
.mint-fallback { padding: 20px 0 40px; }
/* The one line above the skeleton while a mint is being checked. */
.fb-checking {
font-size: 13.5px; color: var(--muted); margin-bottom: 16px;
display: flex; align-items: center; gap: 9px;
}
/* `display: flex` beats the user agent's `[hidden] { display: none }`, so an author
rule has to take it back or the line (and its pulsing dot) never goes away. */
.fb-checking[hidden] { display: none; }
.fb-checking::before {
content: ''; width: 7px; height: 7px; border-radius: 50%;
background: var(--amber); flex-shrink: 0;
animation: soft-pulse 1.4s var(--ease-in-out) infinite;
}
</style>
<script>
import { apiBase, escapeHtml, iconGradient } from '../lib/client';
import { armBuildSnapshot, clearBones, isBuildMode, showBones, swapBones } from '../lib/skeleton';
import { useI18n } from '../i18n/client';
import { formatters, starString, type Formatters } from '../i18n/format';
import { statusLabel, warningOptions } from '../i18n/mint';
import { bannerHtml } from '../lib/mint-state';
import { splitLocale } from '../i18n/routing';
import { onReady } from '../scripts/reveal';
import { indexErrorMessage, mintDisplayName, requestIndex, subjectFor } from '../lib/index-client';
import mintPageBones from '../bones/mint-page.bones.json';
import {
addressFromSlug, getMintWarnings, typeForPath,
type IndexFailure, type IndexType, type MintDetail,
} from '@cashumints/shared';
/**
* The summary this page can build from the API alone. `pnpm bones` measures the
* output of this same function, so the bones are the shape of what actually
* arrives rather than of a fuller page that never does.
*/
function fallbackHtml(mint: MintDetail, f: Formatters): string {
const domain = mint.url.replace(/^lnurl:/, '').replace(/^https?:\/\//, '');
const name = mint.name ?? domain.split('/')[0] ?? domain;
/*
* The one warning the mint has earned, drawn by the same function the mint pages
* use. It matters most on exactly the mint this resolver exists for: one indexed
* from a Nostr announcement because nothing answered at its address is, by
* definition, a mint whose page opens with "likely gone" — and a status chip alone
* does not say that.
*/
const warning = bannerHtml(getMintWarnings(mint, warningOptions(f))[0] ?? null);
return `
${warning}
<div class="fb-head">
<span class="mc-icon" style="background:${iconGradient(domain)};width:52px;height:52px;border-radius:13px;font-size:21px">${escapeHtml(
(name[0] ?? '?').toUpperCase(),
)}</span>
<span>
<span class="fb-name">${escapeHtml(name)}</span>
<span class="fb-domain">${escapeHtml(domain)}</span>
</span>
<span class="status-chip ${escapeHtml(mint.status)}"><span class="status-dot"></span>${escapeHtml(
statusLabel(mint.status, f.t),
)}</span>
</div>
<div class="fb-stats">
<span class="fb-stat"><span class="k">${escapeHtml(
f.t('notfound.fb.rating'),
)}</span><span class="v">${
mint.rating_avg === null ? f.t('time.none') : f.decimal(mint.rating_avg)
} <span class="stars" style="font-size:14px">${starString(mint.rating_avg)}</span></span></span>
<span class="fb-stat"><span class="k">${escapeHtml(
f.t('notfound.fb.reviews'),
)}</span><span class="v">${f.number(mint.review_count)}</span></span>
<span class="fb-stat"><span class="k">${escapeHtml(
f.t('notfound.fb.software'),
)}</span><span class="v" style="font-family:var(--mono);font-size:16px">${escapeHtml(
mint.version ?? f.t('mint.software.unknown'),
)}</span></span>
<span class="fb-stat"><span class="k">${escapeHtml(
f.t('notfound.fb.lastOnline'),
)}</span><span class="v" style="font-size:16px">${escapeHtml(
f.relative(mint.last_online),
)}</span></span>
</div>
<p class="fb-note">${escapeHtml(f.t('notfound.fb.note'))}</p>
<div class="fb-actions">
<button type="button" class="btn primary" data-write-open>${escapeHtml(
f.t('reviews.byUrl.open'),
)}</button>
</div>`;
}
const setup = (): void => {
const f = formatters(useI18n());
const t = f.t;
/*
* The miss could be at /mint/x or at /es/mint/x, and both resolve to the same mint.
* The prefix comes off with the same helper the build routes with, so the two can
* never disagree about what counts as a locale segment.
*/
const { path } = splitLocale(window.location.pathname);
const target = typeForPath(path);
const box = document.querySelector<HTMLElement>('[data-mint-fallback]');
const body = document.querySelector<HTMLElement>('[data-mint-fallback-body]');
const status = document.querySelector<HTMLElement>('[data-nf-status]');
const notfound = document.querySelector<HTMLElement>('[data-notfound]');
const reviewsBox = document.querySelector<HTMLElement>('[data-nf-reviews]');
const panel = document.querySelector<HTMLElement>('[data-reviews-panel]');
if (!box || !body) return;
if (isBuildMode()) {
// `pnpm bones` is measuring this page. Show a representative mint so the capture
// has a real summary to measure instead of an empty box. Imported dynamically so
// the fixture never reaches a real visitor.
void armBuildSnapshot()
.then(() => import('../lib/skeleton-fixtures'))
.then(({ FIXTURE_MINT }) => {
if (notfound) notfound.hidden = true;
box.hidden = false;
body.innerHTML = fallbackHtml(FIXTURE_MINT, f);
});
return;
}
if (!target) return; // An ordinary 404. The page above is the whole answer.
/**
* Hand the resolved mint to the reviews panel and let it run.
*
* The panel is the same island a mint page uses and it reads its subject off its own
* root, so "starting" it is writing the subject there and telling it to look again.
* Nothing about the panel is special-cased for this page.
*/
function startReviews(mint: MintDetail): void {
const subject = subjectFor(mint);
if (!panel || !subject || !reviewsBox) return;
panel.dataset['subject'] = JSON.stringify(subject);
const list = panel.querySelector<HTMLElement>('[data-review-list]');
if (list) list.dataset['reviewCount'] = String(mint.review_count);
// The write dialog's heading names the mint, and until now there was no mint to
// name. Same sentence the prerendered pages carry.
const title = document.querySelector<HTMLElement>('#write-title');
if (title) title.textContent = t('reviews.dialog.title', { name: mintDisplayName(mint) });
/*
* And the prompt in the box, which is the one line in that dialog that cannot be
* ecosystem-neutral: "how did minting and melting work out" means nothing about a
* federation. The panel picks it at build time from its subject, and this page had
* none, so it defaulted to the Cashu wording. Same key, same fallback rule.
*/
const prompt = document.querySelector<HTMLTextAreaElement>('[data-write-dialog] textarea');
if (prompt) {
const key = `reviews.dialog.bodyPlaceholder.${mint.type}`;
prompt.placeholder = t.has(key) ? t(key) : t('reviews.dialog.bodyPlaceholder.cashu');
}
reviewsBox.hidden = false;
document.dispatchEvent(new CustomEvent('cashumints:subject'));
}
/**
* The tab title for a resolved mint.
*
* One key per ecosystem rather than the Cashu one for all three: a federation's tab
* reading "Cashu mint reviews" is a small, flatly wrong sentence in the one place a
* reader keeps a page open under.
*/
function titleFor(mint: MintDetail): string {
const name = mintDisplayName(mint);
if (mint.type === 'fedimint') return t('mint.titleFedimint', { name });
if (mint.type === 'lnurl') return t('mint.titleLnurl', { name });
return t('mint.title', { name });
}
/** Render the page shell for a mint the site now has. */
function renderMint(mint: MintDetail): void {
if (status) status.hidden = true;
swapBones(body!, (slot) => {
slot.innerHTML = fallbackHtml(mint, f);
});
document.title = titleFor(mint);
startReviews(mint);
}
/**
* The honest dead end.
*
* Not the plain 404: the reader asked about a specific address and this says what
* happened to it, in the words the failure deserves, with the two ways forward that
* actually exist — correct the address, or open it as the ecosystem it turned out to
* belong to.
*/
function renderFailure(message: string, options: { detected?: IndexType; prefill?: string } = {}): void {
if (status) status.hidden = true;
clearBones(body!);
box!.hidden = true;
if (reviewsBox) reviewsBox.hidden = true;
if (!notfound) return;
notfound.hidden = false;
const lede = notfound.querySelector<HTMLElement>('.lede');
if (lede) lede.textContent = message;
const heading = notfound.querySelector<HTMLElement>('h1');
if (heading) {
// A federation is not a mint, and this heading is the one line that names it.
heading.textContent =
target!.type === 'fedimint'
? t('notfound.resolve.failedFedimint')
: t('notfound.resolve.failed');
}
const actions = notfound.querySelector<HTMLElement>('.nf-actions');
if (!actions || actions.querySelector('[data-review-url-open]')) return;
/*
* The way out, as the first action. A reader who typed one character wrong is one
* dialog away from their mint's page; a reader whose address belongs to another
* ecosystem is one button away from the right one.
*/
const kind = options.detected ?? target!.type;
const label = options.detected
? t('notfound.resolve.switch', { kind: t(`feed.ecosystem.${options.detected}`) })
: t('notfound.resolve.retry');
const button = document.createElement('button');
button.type = 'button';
button.className = 'btn primary';
button.dataset['reviewUrlOpen'] = kind;
if (options.prefill) button.dataset['reviewUrlValue'] = options.prefill;
button.textContent = label;
actions.prepend(button);
// The dialog's own island bound its triggers when the page loaded, and this button
// did not exist then.
button.addEventListener('click', () => {
document.querySelector<HTMLElement>('[data-review-url-dialog]')?.dispatchEvent(
new CustomEvent('cashumints:open', { detail: { type: kind, value: options.prefill ?? '' } }),
);
});
}
// The "page not found" copy would be wrong if this mint turns out to exist, so hold
// it back while we find out rather than flashing it and taking it away again.
if (notfound) notfound.hidden = true;
box.hidden = false;
if (status) {
status.textContent = t('notfound.resolve.checking');
status.hidden = false;
}
// The same sentence goes to `showBones` as its screen reader status line, so both
// kinds of reader are told the same thing at the same moment.
showBones(body, mintPageBones, { label: t('notfound.resolve.checking') });
void (async () => {
// 1. Does the index already know it? Much the commonest case: a mint discovered
// since the last build, whose row exists and whose page does not.
try {
const res = await fetch(`${apiBase}/api/mints/${encodeURIComponent(target.host)}`);
if (res.ok) {
renderMint((await res.json()) as MintDetail);
return;
}
} catch {
// The API is unreachable. Indexing would fail for the same reason; fall through
// to the failure state, which says so.
}
/*
* 2. Two shapes cannot be turned back into an address.
*
* A federation's page address carries a shortened id, and an invite code cannot be
* derived from one. A slug with a path in it (`mint.example.com-bitcoin`) is
* ambiguous by construction — the hyphen may be a path separator or part of a
* hostname — and guessing would mean fetching, and possibly indexing, the wrong
* host. Both say so and offer the dialog, which is where an address the reader
* actually has can go.
*/
const address = target.type === 'fedimint' ? null : addressFromSlug(target.host);
if (!address) {
renderFailure(
target.type === 'fedimint' ? t('notfound.resolve.fedimint') : t('notfound.resolve.ambiguous'),
);
return;
}
// 3. Check it live and index it if it is real.
const result = await requestIndex(target.type, address);
if (result.ok) {
renderMint(result.mint);
return;
}
const failure: IndexFailure = result.failure;
renderFailure(indexErrorMessage(result, t), {
...(failure.detected_type ? { detected: failure.detected_type } : {}),
prefill: address,
});
})();
};
onReady(setup);
</script>
+475
View File
@@ -0,0 +1,475 @@
---
/**
* Island: review a mint by pasting its address.
*
* The index pages list what this site knows. Until now, reviewing something it did not
* know was impossible from here — the only way in was a mint page, and a mint with no
* page could not be reached. This is the way in: one dialog, two steps, on /mints,
* /fedimints and /lnurl-mints alike.
*
* 1. **Identify.** One field, labelled for this page's ecosystem, with a format check
* that runs before any request. Submitting it calls `POST /api/index`, which either
* finds the mint already indexed, checks it live and indexes it, tells us it is a
* *different* ecosystem's mint (with a one-click switch), or explains why not.
* 2. **Review.** The site's ordinary write form — same stars, same identity row, same
* publisher — pointed at whatever step 1 resolved.
*
* One `<dialog>` for both, because it is one intention: the reader came here to write a
* review and the address is the first question, not a separate errand. The steps
* cross-fade in place rather than the dialog closing and reopening.
*
* The trigger lives on the page rather than in here: it belongs in the index head row,
* beside the heading, and the dialog belongs at the end of the document. Anything with
* `data-review-url-open` opens this.
*/
import WriteForm from './WriteForm.astro';
import { useI18n, type Locale } from '../i18n';
import { pageLocale } from '../i18n/paths';
import type { IndexType } from '@cashumints/shared';
interface Props {
/** Which ecosystem this page lists, and therefore what the dialog opens asking for. */
type: IndexType;
}
const { type } = Astro.props;
const { locale } = pageLocale(Astro) as { locale: Locale };
const t = useI18n(locale);
/*
* The label, the placeholder and the review prompt all differ per ecosystem, and all
* three fall back to the Cashu wording for a type whose own string has not been written
* — the same rule the reviews panel already applies to its placeholder.
*/
const wordsFor = (kind: IndexType): { label: string; placeholder: string; body: string } => {
const key = (base: string): string =>
t.has(`${base}.${kind}`) ? `${base}.${kind}` : `${base}.cashu`;
return {
label: t(key('reviews.byUrl.label')),
placeholder: t(key('reviews.byUrl.placeholder')),
body: t(key('reviews.dialog.bodyPlaceholder')),
};
};
/*
* All three ecosystems' wording travels with the dialog, not just this page's.
*
* An index page only ever asks about its own, but the 404 page carries one of these for
* whichever deep link missed — and a wrong-type answer can move a submission from one
* ecosystem to another mid-dialog, at which point "Mint URL" is the wrong label for the
* box the reader is looking at. Three short strings in an attribute is a smaller price
* than a dialog per ecosystem or a second request for a label.
*/
const words = {
cashu: wordsFor('cashu'),
fedimint: wordsFor('fedimint'),
lnurl: wordsFor('lnurl'),
};
const initial = words[type] ?? words.cashu;
---
<dialog
class="modal rbu-dialog"
data-review-url-dialog
data-rbu-type={type}
data-rbu-words={JSON.stringify(words)}
aria-labelledby="rbu-title"
>
{/*
Both steps live in the same box and only one is ever in the flow, so the dialog is
the height of whichever step is showing and the backdrop never jumps around a
half-rendered second step.
*/}
<div class="rbu-steps" data-rbu-steps>
<form class="modal-body rbu-step" data-rbu-identify novalidate>
<div class="modal-head">
<h2 id="rbu-title">{t('reviews.byUrl.title')}</h2>
<button type="button" class="copy-btn" data-rbu-close aria-label={t('common.close')}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M18 6 6 18M6 6l12 12" />
</svg>
</button>
</div>
<p class="modal-note">{t('reviews.byUrl.lede')}</p>
<label class="write-label" for="rbu-input" data-rbu-label>{initial.label}</label>
<input
id="rbu-input"
class="rbu-input"
data-rbu-input
type="text"
autocomplete="off"
autocapitalize="off"
spellcheck="false"
placeholder={initial.placeholder}
aria-describedby="rbu-error"
/>
<p class="write-error" id="rbu-error" data-rbu-error hidden role="alert"></p>
{/* Filled by the island when the API says "that is the other ecosystem's mint". */}
<p class="rbu-switch" data-rbu-switch hidden></p>
<div class="write-actions">
<button type="button" class="btn" data-rbu-close>{t('common.cancel')}</button>
<button type="submit" class="btn primary" data-rbu-check>
<span class="write-spinner" aria-hidden="true"></span>
<span data-rbu-check-label>{t('reviews.byUrl.check')}</span>
</button>
</div>
</form>
<div class="rbu-step" data-rbu-review hidden>
<WriteForm locale={locale} placeholder={initial.body} idPrefix="rbu">
<div class="modal-head" slot="head">
<h2 data-rbu-review-title>{t('reviews.byUrl.title')}</h2>
<button type="button" class="copy-btn" data-write-close aria-label={t('common.close')}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M18 6 6 18M6 6l12 12" />
</svg>
</button>
</div>
<button type="button" class="rbu-back" slot="head" data-rbu-back>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" aria-hidden="true">
<path d="m15 18-6-6 6-6" />
</svg>
{t('reviews.byUrl.back')}
</button>
{/*
The mint the reader is about to review, drawn from the payload the API just
returned. It is the confirmation that the right thing was found — a domain and
a status chip say more than a name alone — and it is why the second step does
not need to repeat the address in its heading.
*/}
<div class="rbu-preview" slot="head" data-rbu-preview></div>
</WriteForm>
</div>
</div>
</dialog>
<style>
/* The two steps share one box; only the one in the flow is measured. */
.rbu-steps { position: relative; }
/*
* `hidden` alone is not enough on the first step: it carries `.modal-body`, whose
* `display: flex` beats the user agent's `[hidden] { display: none }` and left both
* steps stacked in the dialog at once. An author rule has to take it back.
*/
.rbu-step[hidden] { display: none; }
.rbu-step { transition: opacity 140ms var(--ease-out), transform 140ms var(--ease-out); }
/* Leaving to the left, arriving from the right: the reader is moving forwards. */
.rbu-step.is-leaving { opacity: 0; transform: translateX(-10px); }
.rbu-step.is-entering { opacity: 0; transform: translateX(10px); }
/* Going back reverses it, so the motion always matches the direction of travel. */
.rbu-step.is-leaving-back { opacity: 0; transform: translateX(10px); }
.rbu-step.is-entering-back { opacity: 0; transform: translateX(-10px); }
@media (prefers-reduced-motion: reduce) {
.rbu-step { transition: none; }
.rbu-step.is-leaving, .rbu-step.is-entering,
.rbu-step.is-leaving-back, .rbu-step.is-entering-back { opacity: 1; transform: none; }
}
</style>
<script>
import { escapeHtml, iconGradient } from '../lib/client';
import { useI18n } from '../i18n/client';
import { statusLabel } from '../i18n/mint';
import { localePath, splitLocale } from '../i18n/routing';
import { onReady, prefersReducedMotion } from '../scripts/reveal';
import { closeModal, openModal } from '../lib/modal';
import { indexErrorMessage, mintDisplayName, displayAddress, requestIndex, subjectFor } from '../lib/index-client';
import { rememberFreshReview, wireWriteForm } from '../lib/write-review';
import { checkIndexInput, mintPath, type IndexSuccess, type IndexType } from '@cashumints/shared';
import type { ReviewSubject } from '../lib/review-subject';
const setup = (): void => {
const dialog = document.querySelector<HTMLDialogElement>('[data-review-url-dialog]');
if (!dialog) return;
const t = useI18n();
const identify = dialog.querySelector<HTMLFormElement>('[data-rbu-identify]')!;
const reviewStep = dialog.querySelector<HTMLElement>('[data-rbu-review]')!;
const input = dialog.querySelector<HTMLInputElement>('[data-rbu-input]')!;
const errorBox = dialog.querySelector<HTMLElement>('[data-rbu-error]')!;
const switchBox = dialog.querySelector<HTMLElement>('[data-rbu-switch]')!;
const checkButton = dialog.querySelector<HTMLButtonElement>('[data-rbu-check]')!;
const checkLabel = dialog.querySelector<HTMLElement>('[data-rbu-check-label]')!;
const preview = dialog.querySelector<HTMLElement>('[data-rbu-preview]')!;
const reviewTitle = dialog.querySelector<HTMLElement>('[data-rbu-review-title]')!;
const label = dialog.querySelector<HTMLElement>('[data-rbu-label]')!;
const bodyBox = dialog.querySelector<HTMLTextAreaElement>('textarea[name="content"]')!;
/**
* The wording for each ecosystem, carried in the markup.
*
* Read defensively: a payload that will not parse is a build bug, and a dialog that
* threw here would take the whole review flow down rather than showing a slightly
* generic label.
*/
const words = ((): Record<string, { label: string; placeholder: string; body: string }> => {
try {
return JSON.parse(dialog.dataset['rbuWords'] ?? '{}') as Record<
string,
{ label: string; placeholder: string; body: string }
>;
} catch {
return {};
}
})();
/**
* What is being asked for right now.
*
* Starts as the page's own ecosystem, and changes in two places: a trigger that
* names one (the 404 page, whose dialog serves whichever deep link missed), and the
* one-click switch after a wrong-type answer.
*/
let pageType = (dialog.dataset['rbuType'] ?? 'cashu') as IndexType;
function setType(next: IndexType): void {
pageType = next;
const wording = words[next];
if (!wording) return;
label.textContent = wording.label;
input.placeholder = wording.placeholder;
bodyBox.placeholder = wording.body;
}
/** What step 1 resolved, and what step 2 publishes against. */
let resolved: { mint: IndexSuccess; subject: ReviewSubject } | null = null;
/** True while a submission is in flight, so nothing runs over it. */
let checking = false;
/* ---------- step 2: the ordinary write form ---------- */
const writer = wireWriteForm({
dialog,
subject: () => resolved?.subject ?? null,
onBackdrop: () => input.value.trim() === '' || window.confirm(t('reviews.byUrl.confirmClose')),
onPublished: (published) => {
const mint = resolved?.mint;
if (!mint) return;
/*
* The review is on the relays; the page that shows it is somewhere else. Hand it
* forward through sessionStorage and go there: the reviews panel picks it up on
* arrival and renders it at the top, exactly as it would have if the review had
* been written on that page. Without this, an author would land on their own
* mint's page and not see what they just wrote until the relays echoed it back.
*/
rememberFreshReview(published);
const { locale } = splitLocale(window.location.pathname);
window.location.href = localePath(mintPath(mint.type, mint.host), locale);
},
});
/* ---------- moving between the steps ---------- */
function showStep(next: 'identify' | 'review'): void {
const from = next === 'review' ? identify : reviewStep;
const to = next === 'review' ? reviewStep : identify;
const back = next === 'identify';
if (prefersReducedMotion()) {
from.hidden = true;
to.hidden = false;
return;
}
from.classList.add(back ? 'is-leaving-back' : 'is-leaving');
window.setTimeout(() => {
from.hidden = true;
from.classList.remove('is-leaving', 'is-leaving-back');
to.hidden = false;
to.classList.add(back ? 'is-entering-back' : 'is-entering');
// Two frames: one for the browser to lay the pane out hidden-to-shown, one for
// the transition to have a starting value to move from.
requestAnimationFrame(() =>
requestAnimationFrame(() => to.classList.remove('is-entering', 'is-entering-back')),
);
}, 140);
}
/* ---------- the preview card ---------- */
function drawPreview(mint: IndexSuccess): void {
const name = mintDisplayName(mint);
const address = displayAddress(mint);
const note = mint.existing ? t('reviews.byUrl.existing') : t('reviews.byUrl.indexed');
preview.innerHTML =
`<span class="rbu-icon" style="background:${iconGradient(address)}">${escapeHtml(
(name[0] ?? '?').toUpperCase(),
)}</span>` +
`<span class="rbu-who">` +
`<span class="rbu-name">${escapeHtml(name)}</span>` +
// A federation submitted as a bare invite code has no name but its own id, and
// printing that id twice says nothing the first line did not.
(address === name ? '' : `<span class="rbu-domain">${escapeHtml(address)}</span>`) +
`</span>` +
`<span class="rbu-chips">` +
`<span class="status-chip ${escapeHtml(mint.status)}"><span class="status-dot"></span>${escapeHtml(
statusLabel(mint.status, t),
)}</span>` +
`<span class="rbu-note">${escapeHtml(note)}</span></span>`;
reviewTitle.textContent = t('reviews.dialog.title', { name });
}
/* ---------- step 1: identify ---------- */
function showError(message: string): void {
errorBox.textContent = message;
errorBox.hidden = message === '';
}
function setChecking(busy: boolean): void {
checking = busy;
input.disabled = busy;
checkButton.disabled = busy;
checkButton.classList.toggle('is-busy', busy);
checkLabel.textContent = busy ? t('reviews.byUrl.checking') : t('reviews.byUrl.check');
}
/** Offer the other ecosystem, with a button that re-submits under it. */
function offerSwitch(detected: IndexType): void {
switchBox.hidden = false;
switchBox.innerHTML =
`${escapeHtml(t('reviews.byUrl.detected', { kind: t(`feed.ecosystem.${detected}`) }))} ` +
`<button type="button" class="rev-retry" data-rbu-switch-to="${escapeHtml(detected)}">${escapeHtml(
t('reviews.byUrl.switchTo', { kind: t(`feed.ecosystem.${detected}`) }),
)}</button>`;
}
async function submitAddress(type: IndexType, raw: string): Promise<void> {
if (checking) return;
showError('');
switchBox.hidden = true;
// The format check runs first and costs nothing: an empty box, a sentence, or an
// invite code in a URL field are all answerable without asking the server.
const precheck = checkIndexInput(type, raw);
if (!precheck.ok) {
showError(
precheck.reason === 'empty' ? t('reviews.byUrl.error.empty')
: precheck.reason === 'bad_invite' ? t('reviews.byUrl.error.badInvite')
: t('reviews.byUrl.error.badUrl'),
);
input.focus();
return;
}
setChecking(true);
const result = await requestIndex(type, precheck.value);
setChecking(false);
if (!result.ok) {
const detected = result.failure.detected_type;
showError(indexErrorMessage(result, t));
if (detected && detected !== type) offerSwitch(detected);
input.focus();
input.select();
return;
}
const subject = subjectFor(result.mint);
if (!subject) {
// Indexed, but this build has no review kind for its ecosystem. Publishing an
// event nothing could resolve would be worse than saying so.
showError(t('reviews.byUrl.error.notAMint'));
return;
}
resolved = { mint: result.mint, subject };
drawPreview(result.mint);
writer.reset();
showStep('review');
writer.open();
}
identify.addEventListener('submit', (event) => {
event.preventDefault();
void submitAddress(pageType, input.value);
});
// The one-click switch: re-submit the same address under the ecosystem the API
// recognised, and carry straight on into the review.
switchBox.addEventListener('click', (event) => {
const button = (event.target as HTMLElement).closest<HTMLElement>('[data-rbu-switch-to]');
const detected = button?.dataset['rbuSwitchTo'] as IndexType | undefined;
if (!detected) return;
// The box is about to be asked about a different ecosystem, so it says so before
// the request goes out rather than after it comes back.
setType(detected);
void submitAddress(detected, input.value);
});
input.addEventListener('input', () => {
showError('');
switchBox.hidden = true;
});
dialog.querySelector('[data-rbu-back]')?.addEventListener('click', () => {
showStep('identify');
window.setTimeout(() => input.focus(), 160);
});
for (const button of dialog.querySelectorAll('[data-rbu-close], [data-write-close]')) {
button.addEventListener('click', () => closeModal(dialog));
}
/* ---------- opening ---------- */
/**
* Open the dialog on step 1, asking about `type`, with `prefill` in the box.
*
* Always back to the address, even if a previous visit got as far as the form: the
* reader pressed "write a review", not "carry on with the last one".
*/
function openWith(type: IndexType, prefill?: string): void {
setType(type in words ? type : ((dialog!.dataset['rbuType'] ?? 'cashu') as IndexType));
resolved = null;
identify.hidden = false;
reviewStep.hidden = true;
showError('');
switchBox.hidden = true;
setChecking(false);
if (prefill) input.value = prefill;
openModal(dialog!);
input.focus();
input.select();
}
for (const trigger of document.querySelectorAll<HTMLElement>('[data-review-url-open]')) {
trigger.addEventListener('click', () => {
/*
* A trigger may name the ecosystem it is asking about, and hand over the address
* the reader was already looking for. The index pages need neither — their
* dialog is already theirs and there is nothing to prefill — but the 404 page's
* trigger serves all three and knows exactly which address just failed.
*/
const asked = trigger.dataset['reviewUrlOpen'] as IndexType | undefined;
openWith(asked ?? ((dialog.dataset['rbuType'] ?? 'cashu') as IndexType), trigger.dataset['reviewUrlValue']);
});
}
/*
* The same thing, for a trigger that did not exist when this ran.
*
* The 404 page builds its button only once a resolution has failed, which is long
* after the loop above. Rather than have that page reach into this island's
* internals, it dispatches an event at the dialog and this answers it.
*/
dialog.addEventListener('cashumints:open', (event) => {
const detail = (event as CustomEvent<{ type?: string; value?: string }>).detail ?? {};
openWith((detail.type ?? 'cashu') as IndexType, detail.value);
});
};
onReady(setup);
</script>
+121 -493
View File
@@ -13,6 +13,7 @@
import type { RatingDistribution } from '@cashumints/shared'; import type { RatingDistribution } from '@cashumints/shared';
import type { ReviewSubject } from '../lib/review-subject'; import type { ReviewSubject } from '../lib/review-subject';
import { renderBones } from 'boneyard-js'; import { renderBones } from 'boneyard-js';
import WriteForm from './WriteForm.astro';
import reviewsBones from '../bones/reviews-panel.bones.json'; import reviewsBones from '../bones/reviews-panel.bones.json';
import { BONE_COLOR } from '../lib/skeleton'; import { BONE_COLOR } from '../lib/skeleton';
import { useI18n } from '../i18n'; import { useI18n } from '../i18n';
@@ -20,8 +21,14 @@ import { formatters } from '../i18n/format';
import { pageLocale } from '../i18n/paths'; import { pageLocale } from '../i18n/paths';
interface Props { interface Props {
/** What is being reviewed: the tags a review carries and the filters that find them. */ /**
subject: ReviewSubject; * What is being reviewed: the tags a review carries and the filters that find them.
*
* Null on the 404 page alone, which renders this panel for a mint it has not resolved
* yet: the resolver writes the subject onto the root and tells the island to start.
* Every prerendered page passes a real one.
*/
subject: ReviewSubject | null;
/** The counts the API already knows, prerendered so the panel is honest without JS. */ /** The counts the API already knows, prerendered so the panel is honest without JS. */
reviewCount: number; reviewCount: number;
distribution: RatingDistribution; distribution: RatingDistribution;
@@ -43,7 +50,7 @@ const dist = distribution;
* about anything. One key per ecosystem, falling back to the Cashu wording for a type * about anything. One key per ecosystem, falling back to the Cashu wording for a type
* whose own has not been written yet. * whose own has not been written yet.
*/ */
const placeholderKey = `reviews.dialog.bodyPlaceholder.${subject.type}`; const placeholderKey = `reviews.dialog.bodyPlaceholder.${subject?.type ?? 'cashu'}`;
const bodyPlaceholder = t.has(placeholderKey) const bodyPlaceholder = t.has(placeholderKey)
? t(placeholderKey) ? t(placeholderKey)
: t('reviews.dialog.bodyPlaceholder.cashu'); : t('reviews.dialog.bodyPlaceholder.cashu');
@@ -98,7 +105,7 @@ function escapeText(value: string): string {
aria-labelledby="reviews-title" aria-labelledby="reviews-title"
data-reveal data-reveal
data-reviews-panel data-reviews-panel
data-subject={JSON.stringify(subject)} data-subject={subject ? JSON.stringify(subject) : ''}
> >
<div class="panel-head"> <div class="panel-head">
<h2 class="panel-title" id="reviews-title">{t('reviews.title')}</h2> <h2 class="panel-title" id="reviews-title">{t('reviews.title')}</h2>
@@ -144,7 +151,15 @@ function escapeText(value: string): string {
target for `pnpm bones`, which snapshots this element's single child. target for `pnpm bones`, which snapshots this element's single child.
*/} */}
<div data-review-list data-review-count={reviewCount}> <div data-review-list data-review-count={reviewCount}>
<div data-boneyard="reviews-panel" data-boneyard-config='{"excludeSelectors":[".sr-only"]}'> {/*
`data-boneyard` only when there is something to measure.
`pnpm bones` captures two pages, and the second is a /mint/ URL with no prerendered
page — which now carries this panel too, with no subject and nothing in it. Marked
as a capture target there, it would overwrite the reviews bones taken from the real
mint page a moment earlier with the bones of an empty box.
*/}
<div data-boneyard={subject ? 'reviews-panel' : undefined} data-boneyard-config='{"excludeSelectors":[".sr-only"]}'>
<div data-review-items> <div data-review-items>
{ {
showBoneBlocks ? ( showBoneBlocks ? (
@@ -191,8 +206,8 @@ function escapeText(value: string): string {
every page load. every page load.
--> -->
<dialog class="modal write-dialog" data-write-dialog aria-labelledby="write-title"> <dialog class="modal write-dialog" data-write-dialog aria-labelledby="write-title">
<form method="dialog" class="modal-body write-form" data-write-form novalidate> <WriteForm locale={locale} placeholder={bodyPlaceholder}>
<div class="modal-head"> <div class="modal-head" slot="head">
{/* The mint's name is data, dropped into a translated sentence. */} {/* The mint's name is data, dropped into a translated sentence. */}
<h2 id="write-title">{t('reviews.dialog.title', { name })}</h2> <h2 id="write-title">{t('reviews.dialog.title', { name })}</h2>
<button type="button" class="copy-btn" data-write-close aria-label={t('common.close')}> <button type="button" class="copy-btn" data-write-close aria-label={t('common.close')}>
@@ -201,52 +216,7 @@ function escapeText(value: string): string {
</svg> </svg>
</button> </button>
</div> </div>
</WriteForm>
<p class="modal-note">{t('reviews.dialog.note')}</p>
<div class="write-id" data-write-identity></div>
<fieldset class="star-picker" data-star-picker>
<legend class="sr-only">{t('reviews.dialog.ratingLegend')}</legend>
{
[1, 2, 3, 4, 5].map((value) => (
<label class="star-option">
<input type="radio" name="rating" value={value} required />
<span aria-hidden="true">★</span>
<span class="sr-only">{t('reviews.dialog.starsSr', { n: value })}</span>
</label>
))
}
<span class="star-label" data-star-label aria-live="polite">
{t('reviews.dialog.chooseRating')}
</span>
</fieldset>
<label class="write-label" for="review-body">{t('reviews.dialog.bodyLabel')}</label>
<div class="write-box">
<textarea
id="review-body"
name="content"
rows="5"
aria-describedby="review-count"
placeholder={bodyPlaceholder}></textarea>
<span class="write-count" id="review-count" data-char-count>
{t('reviews.dialog.charCount', { n: 0, max: 2000 })}
</span>
</div>
<p class="write-status" data-write-status hidden role="status"></p>
<p class="write-error" data-write-error hidden role="alert"></p>
<div class="write-actions">
<span class="write-why" data-write-why></span>
<button type="button" class="btn" data-write-close>{t('common.cancel')}</button>
<button type="submit" class="btn primary" data-write-submit disabled>
<span class="write-spinner" aria-hidden="true"></span>
<span data-write-submit-label>{t('reviews.dialog.submit')}</span>
</button>
</div>
</form>
</dialog> </dialog>
<style> <style>
@@ -263,13 +233,11 @@ function escapeText(value: string): string {
import { useI18n } from '../i18n/client'; import { useI18n } from '../i18n/client';
import { formatters } from '../i18n/format'; import { formatters } from '../i18n/format';
import { enterStagger, onReady, prefersReducedMotion, scrollBehavior } from '../scripts/reveal'; import { enterStagger, onReady, prefersReducedMotion, scrollBehavior } from '../scripts/reveal';
import { getExtension, loadProfiles, loadReviews, publishReview, safeNpub, type LoadedReview } from '../lib/reviews-client'; import { loadProfiles, loadReviews, safeNpub, type LoadedReview } from '../lib/reviews-client';
import type { ReviewSubject } from '../lib/review-subject'; import type { ReviewSubject } from '../lib/review-subject';
import { activeSigner, onSessionChange, onSignerStatus, readSession, signerErrorMessage } from '../lib/nostr-signer'; // Stars, body, identity row, draft safety and publishing: shared with the
// The identity row is the same identity the header shows, drawn by the same helpers. // review-by-URL dialog the index pages open, so there is one signing flow on the site.
import { avatarMarkup, identityLines, methodChipHtml, openLogin, resolveProfile, toNpub } from '../lib/auth-ui'; import { takeFreshReview, wireWriteForm } from '../lib/write-review';
import { closeModal, openModal, wireModal } from '../lib/modal';
import type { Profile } from '@cashumints/shared';
// The card itself lives in one place: /reviews renders the identical markup from // The card itself lives in one place: /reviews renders the identical markup from
// the same functions. // the same functions.
import { import {
@@ -334,6 +302,12 @@ function escapeText(value: string): string {
const expected = Number(listWrap.dataset['reviewCount'] ?? '0'); const expected = Number(listWrap.dataset['reviewCount'] ?? '0');
let all: LoadedReview[] = []; let all: LoadedReview[] = [];
/**
* A review published from the review-by-URL dialog on an index page, on its way
* here. Read once, at setup, so a reload does not resurrect it; merged in below
* when the relays answer. See `takeFreshReview`.
*/
const handedForward = takeFreshReview(subject.key);
let filter: 'all' | '5' | 'critical' | 'recent' = 'all'; let filter: 'all' | '5' | 'critical' | 'recent' = 'all';
let page = 1; let page = 1;
/** /**
@@ -619,13 +593,37 @@ function escapeText(value: string): string {
// coming back for a mint the API counted reviews for means the relays did not // coming back for a mint the API counted reviews for means the relays did not
// answer, not that the reviews are gone. Saying "no reviews yet" there would // answer, not that the reviews are gone. Saying "no reviews yet" there would
// be a lie the reader cannot check. // be a lie the reader cannot check.
if (loaded.length === 0 && expected > 0) { if (loaded.length === 0 && expected > 0 && !handedForward) {
showRelayError(); showRelayError();
return; return;
} }
all = loaded; all = loaded;
/*
* The author arrived here from the review-by-URL dialog moments ago. Their own
* review is on the relays but may not be in this answer yet, so it goes to the
* top as an optimistic card — the same card, with the same "propagating" note,
* that they would have seen had they written it on this page.
*/
if (handedForward && !all.some((review) => review.id === handedForward.id)) {
optimistic.set(handedForward.id, {
accepted: handedForward.accepted,
total: handedForward.total,
});
all.unshift({
id: handedForward.id,
pubkey: handedForward.pubkey,
npub: safeNpub(handedForward.pubkey),
created_at: handedForward.created_at,
rating: handedForward.rating,
content: handedForward.content,
mint_url: subject.key,
profile: null,
});
markNewest = true;
}
/* /*
* A `#review-{id}` deep link: land on the page that holds the card, then * A `#review-{id}` deep link: land on the page that holds the card, then
* scroll to it once it is drawn. A shared link wins over the reader's own * scroll to it once it is drawn. A shared link wins over the reader's own
@@ -731,451 +729,81 @@ function escapeText(value: string): string {
/* ---------- write a review ---------- */ /* ---------- write a review ---------- */
/*
* The form itself is `lib/write-review.ts`, which the review-by-URL dialog on the
* index pages drives too. What stays here is the half that is about *this list*:
* putting the fresh review at the top, resetting the filters so it is visible, and
* pulsing the button that opened the dialog.
*/
const dialog = document.querySelector<HTMLDialogElement>('[data-write-dialog]'); const dialog = document.querySelector<HTMLDialogElement>('[data-write-dialog]');
const form = dialog?.querySelector<HTMLFormElement>('[data-write-form]');
const errorBox = dialog?.querySelector<HTMLElement>('[data-write-error]');
const statusBox = dialog?.querySelector<HTMLElement>('[data-write-status]');
const submit = dialog?.querySelector<HTMLButtonElement>('[data-write-submit]');
const submitLabel = dialog?.querySelector<HTMLElement>('[data-write-submit-label]');
const textarea = dialog?.querySelector<HTMLTextAreaElement>('textarea');
const charCount = dialog?.querySelector<HTMLElement>('[data-char-count]');
const starLabel = dialog?.querySelector<HTMLElement>('[data-star-label]');
const identityRow = dialog?.querySelector<HTMLElement>('[data-write-identity]');
const whyDisabled = dialog?.querySelector<HTMLElement>('[data-write-why]');
/** Reviews are capped at this length, and the counter turns red past it. */
const MAX_CHARS = 2000;
/** Below this a review says nothing anyone can use. */
const MIN_CHARS = 10;
// One phrase per star, in the reader's words rather than the site's. Shown as
// "2: had problems" beside the picker, and read out by the live region.
// One phrase per star, from the catalog. Read through `t` rather than held in a
// table here, so the phrases follow the page's language after a view transition.
const starPhrase = (rating: number): string => t(`reviews.dialog.stars.${rating}`);
/** True once a publish is in flight, so nothing else runs over it. */
let publishing = false;
/** Who is going to sign, resolved when the dialog opens. */
let canSign = false;
function showError(message: string): void {
if (!errorBox) return;
errorBox.textContent = message;
errorBox.hidden = message === '';
}
function showStatus(message: string): void {
if (!statusBox) return;
statusBox.textContent = message;
statusBox.hidden = message === '';
}
/* ---------- draft safety ---------- */
/**
* A written review survives an accidental close.
*
* sessionStorage, keyed by mint: it lasts as long as the tab and no longer, which
* is the life of the intention. Nothing here is secret, but nothing here belongs
* on the machine a week later either.
*/
const DRAFT_KEY = `cashumints:draft:${subject.key}`;
function saveDraft(): void {
if (!form) return;
const data = new FormData(form);
const rating = String(data.get('rating') ?? '');
const content = String(data.get('content') ?? '');
try {
if (!rating && !content.trim()) sessionStorage.removeItem(DRAFT_KEY);
else sessionStorage.setItem(DRAFT_KEY, JSON.stringify({ rating, content }));
} catch {
// Storage blocked. The draft lives in the open dialog and nowhere else.
}
}
function clearDraft(): void {
try {
sessionStorage.removeItem(DRAFT_KEY);
} catch {
// Nothing stored to begin with.
}
}
function restoreDraft(): void {
if (!form) return;
let draft: { rating?: string; content?: string } | null = null;
try {
const raw = sessionStorage.getItem(DRAFT_KEY);
draft = raw ? (JSON.parse(raw) as { rating?: string; content?: string }) : null;
} catch {
draft = null;
}
if (!draft) return;
if (draft.rating) {
const star = form.querySelector<HTMLInputElement>(`input[name="rating"][value="${draft.rating}"]`);
if (star) star.checked = true;
}
if (draft.content && textarea) textarea.value = draft.content;
syncStarLabel();
syncCount();
}
/* ---------- the form's own state ---------- */
function chosenRating(): number {
return Number(new FormData(form ?? undefined).get('rating') ?? 0);
}
function syncStarLabel(): void {
if (!starLabel) return;
const rating = chosenRating();
starLabel.textContent = rating
// The rating is a bare numeral here, so it is passed as a string: "3: good",
// never "3,0: good".
? t('reviews.dialog.starLabel', { n: String(rating), phrase: starPhrase(rating) })
: t('reviews.dialog.chooseRating');
}
function syncCount(): void {
const length = textarea?.value.length ?? 0;
if (charCount) {
charCount.textContent = t('reviews.dialog.charCount', { n: length, max: MAX_CHARS });
charCount.classList.toggle('over', length > MAX_CHARS);
}
syncSubmit();
}
/**
* Whether "Sign and publish" is available, and if not, why.
*
* The reason is always on screen: a button that is off for an unstated reason is
* a button people click twice and then leave.
*/
function syncSubmit(): void {
if (!submit) return;
const length = textarea?.value.trim().length ?? 0;
const rating = chosenRating();
const over = (textarea?.value.length ?? 0) - MAX_CHARS;
const reason = publishing
? ''
: !canSign ? t('reviews.dialog.why.login')
: !rating ? t('reviews.dialog.why.rating')
: length === 0 ? t('reviews.dialog.why.words')
: length < MIN_CHARS ? t('reviews.dialog.why.more', { n: MIN_CHARS - length })
: over > 0 ? t('reviews.dialog.why.over', { n: over })
: '';
submit.disabled = publishing || reason !== '';
if (whyDisabled) whyDisabled.textContent = publishing ? '' : reason;
}
/** The publish button, mid-flight or back to rest. */
function setBusy(busy: boolean, label = t('reviews.dialog.submit')): void {
publishing = busy;
submit?.classList.toggle('is-busy', busy);
if (submitLabel) submitLabel.textContent = label;
syncSubmit();
}
/* ---------- who is signing ---------- */
/**
* Draw the identity row, and decide whether publishing is possible at all.
*
* Three states, in the order they are likely:
* - logged in: the session's signer, named, with a way to switch
* - no session but a NIP-07 extension: quick-sign, exactly as before logins
* existed, and no session is created by using it
* - neither: the row becomes the way in, and the publish button says so
*
* The extension's pubkey is asked for here, on open, and never on page load: it
* is a question some extensions put to the reader, and a page that asks it for
* every visitor who might one day write a review is a page that nags.
*/
async function renderIdentity(): Promise<void> {
if (!identityRow) return;
const session = readSession();
if (session) {
// A session that names an extension is only as good as the extension: it can
// be uninstalled, or disabled for this site, between one visit and the next.
canSign = session.method !== 'extension' || getExtension() !== null;
syncSubmit();
if (!canSign) {
identityRow.innerHTML =
`<span class="write-id-who"><span class="write-id-lead">${escapeHtml(
t('reviews.dialog.identity.extensionGone'),
)}</span></span>` +
`<button type="button" class="btn" data-login-open>${escapeHtml(
t('login.open'),
)}</button>`;
return;
}
const npub = await toNpub(session.pubkey);
const draw = (profile: Profile | null): void => {
const { primary, secondary, named } = identityLines(profile, npub, t);
// Whichever of the two lines is the npub is the machine-flavoured one, and
// that is the one set in mono (DESIGN.md). The other is a name or a phrase.
identityRow.innerHTML =
avatarMarkup(session.pubkey, profile, npub, 34) +
`<span class="write-id-who">` +
`<span class="write-id-name${named ? '' : ' mono'}">${escapeHtml(primary)}</span>` +
`<span class="write-id-sub${named ? ' mono' : ''}">${escapeHtml(secondary)}</span></span>` +
methodChipHtml(session.method, t) +
`<button type="button" class="write-id-switch" data-login-open>${escapeHtml(
t('reviews.dialog.identity.switch'),
)}</button>`;
};
// Drawn from the npub first, then again if a name turns up: the row is the
// same height either way, so nothing under it moves when the name lands.
draw(null);
const profile = await resolveProfile(session.pubkey);
if (readSession()?.pubkey !== session.pubkey || !profile?.found) return;
draw(profile);
return;
}
if (!getExtension()) {
canSign = false;
syncSubmit();
identityRow.innerHTML =
`<span class="write-id-who"><span class="write-id-lead">${escapeHtml(
t('reviews.dialog.identity.needSigner'),
)}</span></span>` +
`<button type="button" class="btn" data-login-open>${escapeHtml(
t('reviews.dialog.identity.loginToPublish'),
)}</button>`;
return;
}
canSign = true;
syncSubmit();
identityRow.innerHTML =
`<span class="write-id-who"><span class="write-id-name">${escapeHtml(
t('reviews.dialog.identity.extension'),
)}</span>` +
`<span class="write-id-sub">${escapeHtml(
t('reviews.dialog.identity.readingKey'),
)}</span></span>` +
methodChipHtml('extension', t);
let pubkey = '';
try {
pubkey = (await getExtension()?.getPublicKey()) ?? '';
} catch {
// Declined, or the extension is locked. Publishing still works: it will ask
// again at signing time, which is the request that actually matters.
}
if (!/^[0-9a-f]{64}$/i.test(pubkey)) {
const sub = identityRow.querySelector('.write-id-sub');
if (sub) sub.textContent = t('reviews.dialog.identity.willAsk');
return;
}
const npub = await toNpub(pubkey);
const profile = await resolveProfile(pubkey);
const { primary, named } = identityLines(profile, npub, t);
identityRow.innerHTML =
avatarMarkup(pubkey, profile, npub, 34) +
`<span class="write-id-who">` +
`<span class="write-id-name${named ? '' : ' mono'}">${escapeHtml(primary)}</span>` +
`<span class="write-id-sub">${escapeHtml(
t('reviews.dialog.identity.extension'),
)}</span></span>` +
methodChipHtml('extension', t);
}
/* ---------- opening and closing ---------- */
if (dialog) { if (dialog) {
wireModal(dialog, { const writer = wireWriteForm({
// A written review is not thrown away on a stray click outside the box. dialog,
onBackdrop: () => subject: () => subject,
(textarea?.value.trim().length ?? 0) === 0 || onProfile: (pubkey, profile) => {
window.confirm(t('reviews.dialog.confirmClose')), // The relays answer the publish before the kind-0 lookup answers, near enough
onClose: () => { // every time, so the card goes up as "Anon" and the author's name cross-fades
saveDraft(); // in a moment later, exactly as it does on every other card.
showStatus(''); for (const review of all) if (review.pubkey === pubkey) review.profile = profile;
setBusy(false); patchProfiles();
},
onPublished: (published) => {
// Optimistic insert at the top. The relays will echo it back on next load.
optimistic.set(published.id, { accepted: published.accepted, total: published.total });
all.unshift({
id: published.id,
pubkey: published.pubkey,
npub: safeNpub(published.pubkey),
created_at: published.created_at,
rating: published.rating,
content: published.content,
mint_url: subject.key,
profile: null,
});
filter = 'all';
page = 1;
for (const b of root!.querySelectorAll('[data-filter]')) {
b.setAttribute('aria-pressed', String(b.getAttribute('data-filter') === 'all'));
}
markNewest = true;
render();
updateCounts();
// The dialog is about to close, so the confirmation belongs on the button that
// opened it: one amber pulse, on success only.
const trigger = document.querySelector<HTMLElement>('[data-write-open]');
if (trigger && !prefersReducedMotion()) {
trigger.classList.remove('did-it');
void trigger.offsetWidth;
trigger.classList.add('did-it');
trigger.addEventListener('animationend', () => trigger.classList.remove('did-it'), { once: true });
}
}, },
}); });
// The signer talks back while a signature is in flight: reconnecting, waiting, for (const button of dialog.querySelectorAll('[data-write-close]')) {
// approve-in-another-window. Mirrored into the dialog so the reader is never button.addEventListener('click', () => writer.close());
// watching a spinner with nothing behind it.
onSignerStatus((update) => {
if (dialog.open && publishing) showStatus(update.message);
});
// Logging in from the identity row redraws it, without closing this dialog.
onSessionChange(() => {
if (dialog.open) void renderIdentity();
});
}
textarea?.addEventListener('input', () => {
syncCount();
saveDraft();
});
form?.addEventListener('change', (event) => {
if ((event.target as HTMLInputElement).name !== 'rating') return;
syncStarLabel();
syncSubmit();
saveDraft();
});
for (const button of document.querySelectorAll('[data-write-close]')) {
button.addEventListener('click', () => closeModal(dialog!));
}
for (const trigger of document.querySelectorAll('[data-write-open]')) {
trigger.addEventListener('click', () => {
if (!dialog) return;
showError('');
showStatus('');
setBusy(false);
restoreDraft();
openModal(dialog);
void renderIdentity();
// Somewhere to land that is not the close button: the rating is the first
// thing to decide, and a checked star keeps its own place in the group.
const checked = form?.querySelector<HTMLInputElement>('input[name="rating"]:checked');
(checked ?? form?.querySelector<HTMLInputElement>('input[name="rating"]'))?.focus();
});
}
/* ---------- publishing ---------- */
form?.addEventListener('submit', async (event) => {
event.preventDefault();
if (!form || !submit || publishing) return;
const rating = chosenRating();
const content = textarea?.value ?? '';
const signer = activeSigner();
if (!signer) {
// No session and no extension. The row already says so; open the way in.
openLogin(submit);
return;
} }
showError(''); for (const trigger of document.querySelectorAll('[data-write-open]')) {
setBusy(true, t('reviews.dialog.signing')); trigger.addEventListener('click', () => writer.open());
showStatus(
signer.method === 'extension'
? t('reviews.dialog.waitingExtension')
: t('reviews.dialog.waitingSigner'),
);
try {
/*
* The author's own kind 0, fetched while the relays are still taking the
* review. Their pubkey is only knowable once the event is signed, so
* `onSigned` starts this at the earliest moment there is: by the time the
* card below is inserted the name is usually already in hand, and the author
* does not watch their own review appear as "Anon" until the next reload.
*
* `refresh` skips the cache deliberately. Anyone who has reviewed before may
* be sitting on an hour-old negative entry for their own key, and they are
* exactly the reader who would notice their name missing.
*/
let profileLookup: Promise<Profile | null> | null = null;
const result = await publishReview({
signer,
subject,
rating,
content,
onSigned: (signed) => {
setBusy(true, t('reviews.dialog.publishing'));
profileLookup = loadProfiles([signed.pubkey], { refresh: true })
.then((profiles) => profiles.get(signed.pubkey) ?? null)
.catch(() => {
// Relays unreachable for the lookup. The card keeps "Anon" over its
// npub, which is what every other unresolved card shows.
return null;
});
},
onProgress: (accepted, _settled, total) => {
showStatus(t('reviews.dialog.publishingTo', { accepted, total }));
},
});
// Optimistic insert at the top. The relays will echo it back on next load.
optimistic.set(result.event.id, { accepted: result.accepted, total: result.total });
all.unshift({
id: result.event.id,
pubkey: result.event.pubkey,
npub: safeNpub(result.event.pubkey),
created_at: result.event.created_at,
rating,
content: content.trim(),
mint_url: subject.key,
profile: null,
});
filter = 'all';
page = 1;
for (const b of root!.querySelectorAll('[data-filter]')) {
b.setAttribute('aria-pressed', String(b.getAttribute('data-filter') === 'all'));
}
markNewest = true;
render();
updateCounts();
// The dialog is about to close, so the confirmation belongs on the button
// that opened it: one amber pulse, on success only.
const trigger = document.querySelector<HTMLElement>('[data-write-open]');
if (trigger && !prefersReducedMotion()) {
trigger.classList.remove('did-it');
void trigger.offsetWidth;
trigger.classList.add('did-it');
trigger.addEventListener('animationend', () => trigger.classList.remove('did-it'), { once: true });
}
// The relays answer the publish before the lookup answers, near enough every
// time, so the card goes up as "Anon" and the author's name cross-fades in a
// moment later, exactly as it does on every other card.
void (profileLookup as Promise<Profile | null> | null)?.then((profile) => {
if (!profile?.found) return;
for (const review of all) {
if (review.pubkey === result.event.pubkey) review.profile = profile;
}
patchProfiles();
});
clearDraft();
form.reset();
syncStarLabel();
syncCount();
setBusy(false);
showStatus('');
closeModal(dialog!);
} catch (err) {
// Back to an editable dialog with everything still in it. A declined
// signature is not an error the reader made, and a relay that would not take
// it is worth another press of the same button.
setBusy(false);
showStatus('');
const message = err instanceof Error ? err.message : String(err);
showError(
message === 'no-relay'
? t('reviews.dialog.error.noRelay')
: signerErrorMessage(err, t),
);
} }
}); }
} }
}; };
onReady(setup); onReady(setup);
/*
* The 404 resolver renders this panel for a mint that had no subject when the page was
* built, and dispatches this once it has one. Setup runs again and finds it.
*
* Safe to re-run: the first pass with no subject does nothing at all — every listener
* and every fetch is inside the `if (root && parsedSubject)` guard — so nothing is
* bound twice.
*/
document.addEventListener('cashumints:subject', setup);
</script> </script>
+15 -7
View File
@@ -5,7 +5,7 @@ import { localePath, useI18n } from '../i18n';
import { pageLocale } from '../i18n/paths'; import { pageLocale } from '../i18n/paths';
interface Props { interface Props {
current?: 'mints' | 'fedimints' | 'reviews' | 'wallets' | 'about'; current?: 'mints' | 'fedimints' | 'lnurl-mints' | 'reviews' | 'wallets' | 'about';
/** Shows the compact search affordance next to the brand (mint pages, /mints). */ /** Shows the compact search affordance next to the brand (mint pages, /mints). */
mintCount?: number; mintCount?: number;
} }
@@ -21,15 +21,23 @@ const t = useI18n(locale);
/* /*
* "Cashu mints", not "Mints". * "Cashu mints", not "Mints".
* *
* Two ecosystems are listed now, so a bare "Mints" beside "Fedimints" reads as though * Three ecosystems are listed now, so a bare "Mints" beside "Fedimints" and "LNURL
* federations were something other than mints — or as though the first link covered * mints" reads as though the others were something other than mints — or as though the
* both. The extra word costs ~45px in the widest nav row and buys a label that is * first link covered all of them. The extra word costs ~45px in the widest nav row and
* unambiguous at a glance. It is the same wording on /mints' own heading, in the * buys a label that is unambiguous at a glance. It is the same wording on /mints' own
* breadcrumb and in the footer, so nothing calls the same page two things. * heading, in the breadcrumb and in the footer, so nothing calls the same page two
* things.
*
* Six items is where this row stops fitting on one line. It still does at 1280px in
* every locale; below that the nav drops to a row of its own rather than collapsing
* behind a hamburger, which is the `@media (max-width: 1180px)` block in global.css and
* the comment there explains how that number was arrived at. A fourth ecosystem would
* need a real menu instead — this row is full.
*/ */
const links = [ const links = [
{ path: '/mints', label: t('nav.mints'), key: 'mints' }, { path: '/mints', label: t('nav.mints'), key: 'mints' },
{ path: '/fedimints', label: t('nav.fedimints'), key: 'fedimints' }, { path: '/fedimints', label: t('nav.fedimints'), key: 'fedimints' },
{ path: '/lnurl-mints', label: t('nav.lnurlMints'), key: 'lnurl-mints' },
{ path: '/reviews', label: t('nav.reviews'), key: 'reviews' }, { path: '/reviews', label: t('nav.reviews'), key: 'reviews' },
{ path: '/wallets', label: t('nav.wallets'), key: 'wallets' }, { path: '/wallets', label: t('nav.wallets'), key: 'wallets' },
{ path: '/about', label: t('nav.about'), key: 'about' }, { path: '/about', label: t('nav.about'), key: 'about' },
@@ -79,7 +87,7 @@ const links = [
of it to get out. The footer keeps its own copy, where someone deliberately looking of it to get out. The footer keeps its own copy, where someone deliberately looking
for site settings expects to find one. for site settings expects to find one.
*/} */}
<LanguageSwitcher variant="menu" /> <LanguageSwitcher placement="bar" />
{/* {/*
The account control. Prerendered logged out, because a static build cannot know The account control. Prerendered logged out, because a static build cannot know
+82
View File
@@ -0,0 +1,82 @@
---
/**
* The write-a-review form: stars, body, identity row, actions.
*
* One markup, two dialogs. The reviews panel on a mint page opens it directly; the
* review-by-URL dialog on an index page opens it as its second step, once the reader's
* pasted address has been indexed. Its behaviour lives in `lib/write-review.ts`, and
* every hook below (`data-write-*`) is what that module binds to.
*
* Nothing here knows which ecosystem is being reviewed. The heading is a slot because
* the two dialogs have genuinely different heads — one names the mint, the other has a
* step back to the address field — and the placeholder is a prop for the same reason
* the panel already had one: "how did minting and melting work out" means nothing about
* a federation.
*/
import { useI18n, type Locale } from '../i18n';
interface Props {
locale: Locale;
/** The body placeholder, chosen per ecosystem by the caller. */
placeholder: string;
/**
* Prefix for the two element ids in here.
*
* A mint page carries both dialogs at once — the panel's, and the one the header
* button opens — so the ids cannot be constants. `for`/`aria-describedby` are the
* whole reason they exist; nothing styles by id.
*/
idPrefix?: string;
}
const { locale, placeholder, idPrefix = 'review' } = Astro.props;
const t = useI18n(locale);
const bodyId = `${idPrefix}-body`;
const countId = `${idPrefix}-count`;
---
<form method="dialog" class="modal-body write-form" data-write-form novalidate>
<slot name="head" />
<p class="modal-note">{t('reviews.dialog.note')}</p>
<div class="write-id" data-write-identity></div>
<fieldset class="star-picker" data-star-picker>
<legend class="sr-only">{t('reviews.dialog.ratingLegend')}</legend>
{
[1, 2, 3, 4, 5].map((value) => (
<label class="star-option">
<input type="radio" name="rating" value={value} required />
<span aria-hidden="true">★</span>
<span class="sr-only">{t('reviews.dialog.starsSr', { n: value })}</span>
</label>
))
}
<span class="star-label" data-star-label aria-live="polite">
{t('reviews.dialog.chooseRating')}
</span>
</fieldset>
<label class="write-label" for={bodyId}>{t('reviews.dialog.bodyLabel')}</label>
<div class="write-box">
<textarea id={bodyId} name="content" rows="5" aria-describedby={countId} placeholder={placeholder}
></textarea>
<span class="write-count" id={countId} data-char-count>
{t('reviews.dialog.charCount', { n: 0, max: 2000 })}
</span>
</div>
<p class="write-status" data-write-status hidden role="status"></p>
<p class="write-error" data-write-error hidden role="alert"></p>
<div class="write-actions">
<span class="write-why" data-write-why></span>
<button type="button" class="btn" data-write-close>{t('common.cancel')}</button>
<button type="submit" class="btn primary" data-write-submit disabled>
<span class="write-spinner" aria-hidden="true"></span>
<span data-write-submit-label>{t('reviews.dialog.submit')}</span>
</button>
</div>
</form>
+21
View File
@@ -61,6 +61,19 @@ translate `en.json` with it beside you.
| announced (a federation) | anunciada | aangekondigd | Its own status. NOT a synonym for unknown: see the note below. | | announced (a federation) | anunciada | aangekondigd | Its own status. NOT a synonym for unknown: see the note below. |
| confirmed (a federation) | confirmada | bevestigd | Something outside this site checked and it answered. | | confirmed (a federation) | confirmada | bevestigd | Something outside this site checked and it answered. |
| network (mainnet, signet) | la red | het netwerk | The network names themselves stay English. | | network (mainnet, signet) | la red | het netwerk | The network names themselves stay English. |
| LNURL | LNURL | LNURL | Always capitals, never inflected, never translated. |
| lnurlcash | lnurlcash | lnurlcash | Lowercase, always. The protocol's own spelling (LUD-25), like `ecash`. |
| LNURL mint | el mint LNURL | de LNURL mint | The thing, not "LNURL-acuñadora". Same noun as a Cashu mint, so it takes the same article and the same word. |
| bearer note | la nota al portador | het toonderbriefje | What an LNURL mint issues. Spanish uses the financial term; Dutch uses the everyday compound. Never "billete" / "biljet" alone, which loses the point that whoever holds it owns it. |
| to rotate (a note) | rotar | roteren | Replacing a note's secret without changing its value. Not "girar" / "draaien". |
| to split (a note) | dividir | splitsen | One note becomes two. |
| to merge (notes) | fusionar | samenvoegen | Several notes become one. |
| LUD-03, LUD-06, LUD-16, LUD-21, LUD-25 | unchanged | unchanged | Same rule as NUT-04: the number is the name. The plain-language label beside it IS translated. |
| withdraw (an LNURL mint) | la retirada | de opname | The LNURL side of melting. Deliberately the same word the Cashu warnings already use for "withdrawals disabled", so one reader meets one concept. |
| mint pubkey | la clave pública del mint | de publieke sleutel van de mint | The funding node's identity key. Its own thing, distinct from a Cashu mint's `/v1/info` pubkey. |
| lightning address | la dirección Lightning | het Lightning-adres | The `name@host` form (LUD-16). Never translate the `@`. |
| Tor / onion address | la dirección Tor | het Tor-adres | Tor keeps its capital T. "Onion" is never translated. |
| funding source | el nodo Lightning | de Lightning-node | Deliberately NOT a literal "fuente de financiación" / "financieringsbron", which reads as an investor. The copy names the thing a reader can picture: the mint's Lightning node. |
| Anon (a reviewer) | Anón | Anon | The label for a reviewer with no kind-0 profile. Site copy, so it IS translated: Spanish shortens "anónimo" to match the label's width, Dutch keeps "anon" because that is what NL Nostr users write. Decided 2026-08; used by the label and the "hide anon" filter alike. | | Anon (a reviewer) | Anón | Anon | The label for a reviewer with no kind-0 profile. Site copy, so it IS translated: Spanish shortens "anónimo" to match the label's width, Dutch keeps "anon" because that is what NL Nostr users write. Decided 2026-08; used by the label and the "hide anon" filter alike. |
## Two decisions worth knowing ## Two decisions worth knowing
@@ -75,6 +88,14 @@ not need this: `minten` and `melten` are what NL and BE Nostr users actually wri
review, "portemonnee", "ondertekenaar") reads as stiff or, worse, as a different concept. review, "portemonnee", "ondertekenaar") reads as stiff or, worse, as a different concept.
Spanish translates more, because Spanish tech writing does. Spanish translates more, because Spanish tech writing does.
**"Unavailable" is not "not supported", on an LNURL mint.** The Features panel has three
states where the Cashu NUTs panel has three and the Fedimint Modules panel has two. A
feature the operator never announced is *not announced* (`lnurl.features.notListed`); one
they did announce but that cannot work right now, because the mint's Lightning node is
unreachable, is *unavailable* (`lnurl.features.unavailable`). Those are different facts —
"they never built it" against "it is built and broken today" — and a language that uses
one word for both loses the distinction the panel exists to draw.
**"Announced" is not "unknown", in any language.** A Cashu mint that has not been probed **"Announced" is not "unknown", in any language.** A Cashu mint that has not been probed
yet is `status.unknown`: we have not looked. A federation that is `status.announced` is yet is `status.unknown`: we have not looked. A federation that is `status.announced` is
one nobody *can* look at — there is no check to run, and there may never be. Translating one nobody *can* look at — there is no check to run, and there may never be. Translating
+124 -3
View File
@@ -114,6 +114,8 @@
"card.reviewed": "اتراجعت {when}", "card.reviewed": "اتراجعت {when}",
"card.noReviews": "لم تجر أي استعراضات بعد", "card.noReviews": "لم تجر أي استعراضات بعد",
"mint.title": "مراجعات دار سكّ Cashu ‏{name}", "mint.title": "مراجعات دار سكّ Cashu ‏{name}",
"mint.titleFedimint": "{name} · مراجعات اتحاد Fedimint",
"mint.titleLnurl": "{name} · مراجعات mint من نوع LNURL",
"mint.breadcrumb": "مسار الصفحة", "mint.breadcrumb": "مسار الصفحة",
"mint.crumbMints": "النقدية الدار سكّ", "mint.crumbMints": "النقدية الدار سكّ",
"mint.copyUrl": "نسخ دار سكّ URL", "mint.copyUrl": "نسخ دار سكّ URL",
@@ -325,7 +327,7 @@
"reviews.validate.short": "أكتب ما لا يقل عن {n} شخصية ليكي يكون الاستعراض مفيداً.", "reviews.validate.short": "أكتب ما لا يقل عن {n} شخصية ليكي يكون الاستعراض مفيداً.",
"reviews.validate.long": "وتقتصر عمليات الاستعراض على {n} شخصية.", "reviews.validate.long": "وتقتصر عمليات الاستعراض على {n} شخصية.",
"feed.title": "آخر استعراضات الدار سكّ والاتحادات الفضاء", "feed.title": "آخر استعراضات الدار سكّ والاتحادات الفضاء",
"feed.description": "كل مراجعات دور السكّ والاتحادات أول ما تتنشر: {reviews} مراجعة على {mints} دار سكّ Cashu و{federations} اتحاد Fedimint، متوقعة على Nostr ومتاحة هنا من الأحدث للأقدم. صفّي حسب النظام أو التقييم أو الاسم.", "feed.description": "كل مراجعة لمنّاتة أو اتحاد أو منّاتة LNURL فور نشرها: {reviews} مراجعة تغطي {mints} منّاتة Cashu و{federations} اتحاد Fedimint و{lnurl} منّاتة LNURL، موقّعة على Nostr ومقروءة هنا من الأحدث. صفِّ حسب المنظومة أو التقييم أو الاسم.",
"feed.heading": "آخر استعراضات", "feed.heading": "آخر استعراضات",
"feed.lede": "كل استعراض ليكل اتحاد من اتحادات الدار سكّ والFedimint، أولها، يقرأ مباشرة من نوتستر. ويوقع كل واحد منهم من جانب مؤلفه ويعيش في المناورات، ومش في قاعدة بياناتنا.", "feed.lede": "كل استعراض ليكل اتحاد من اتحادات الدار سكّ والFedimint، أولها، يقرأ مباشرة من نوتستر. ويوقع كل واحد منهم من جانب مؤلفه ويعيش في المناورات، ومش في قاعدة بياناتنا.",
"feed.label": "الاستعراضات", "feed.label": "الاستعراضات",
@@ -400,7 +402,14 @@
"notfound.fb.reviews": "الاستعراضات", "notfound.fb.reviews": "الاستعراضات",
"notfound.fb.software": "البرمجيات", "notfound.fb.software": "البرمجيات",
"notfound.fb.lastOnline": "آخر الإنترنت", "notfound.fb.lastOnline": "آخر الإنترنت",
"notfound.fb.note": "ده الدار سكّ تم فهرسه بعد أن تم اخر بناء للموقع لذا ده موجز من الـ \"آي بي آي\" وتظهر الصفحة اليكاملة مع الاستعراضات في إعادة البناء التالية.", "notfound.fb.note": "فُهرس هذا الـ mint بعد آخر بناء للموقع، لذا هذا ملخّص من الواجهة الحية، ومراجعاته أدناه. تظهر الصفحة الكاملة عند البناء التالي.",
"notfound.resolve.checking": "أول زيارة: بنفحص الـ mint ده دلوقتي.",
"notfound.resolve.failed": "تعذّرت إضافة هذا الـ mint",
"notfound.resolve.failedFedimint": "تعذّرت إضافة الاتحاد ده",
"notfound.resolve.retry": "جرّب عنوان تاني",
"notfound.resolve.switch": "افتحه بوصفه mint من نوع {kind}",
"notfound.resolve.fedimint": "عنوان صفحة الاتحاد لا يحمل رمز الدعوة الخاص به، لذا لا يمكن العثور عليه من الرابط وحده. ألصق رمز الدعوة وسيُضاف.",
"notfound.resolve.ambiguous": "لا يمكن استنتاج هذا العنوان من الرابط وحده. ألصق رابط الـ mint كاملًا وسيُضاف.",
"wallets.title": "محفظات اليكاكاو الفضاء", "wallets.title": "محفظات اليكاكاو الفضاء",
"wallets.description": "محفظات (Cashu) التي تعمل مع الدار سكّ المدرجة هنا عبر الهواتف المحمولة والشبكات والبروزر وخط القيادة", "wallets.description": "محفظات (Cashu) التي تعمل مع الدار سكّ المدرجة هنا عبر الهواتف المحمولة والشبكات والبروزر وخط القيادة",
"wallets.heading": "الجدار", "wallets.heading": "الجدار",
@@ -654,5 +663,117 @@
"footer.allFedimints": "جميع الاتحادات", "footer.allFedimints": "جميع الاتحادات",
"reviews.dialog.bodyPlaceholder.cashu": "كيف كان القذف والذوبان ودعم العمل؟ كن محدداً", "reviews.dialog.bodyPlaceholder.cashu": "كيف كان القذف والذوبان ودعم العمل؟ كن محدداً",
"reviews.dialog.bodyPlaceholder.fedimint": "كيف تم الإنضمام، ودفعات Lightning ودعم العمل؟ كن محدداً", "reviews.dialog.bodyPlaceholder.fedimint": "كيف تم الإنضمام، ودفعات Lightning ودعم العمل؟ كن محدداً",
"fedimint.details.title": "تفاصيل الاتحاد" "fedimint.details.title": "تفاصيل الاتحاد",
"nav.lnurlMints": "منّاتات LNURL",
"footer.allLnurlMints": "كل منّاتات LNURL",
"lnurlMints.title": "منّاتات LNURL: منّاتات سندات lnurlcash لحاملها، مع المراجعات | Cashumints.space",
"lnurlMints.description": "كل منّاتات LNURL البالغ عددها {total} والمفهرسة من شبكة Nostr: {online} متصلة، {offline} غير متصلة. تقييمات المجتمع وحدود السحب والميزات لكل واحدة.",
"lnurlMints.heading": "منّاتات LNURL",
"lnurlMints.lede": "كل منّاتة LNURL يعرفها هذا الموقع. منّاتة LNURL تُصدر سندات lnurlcash لحاملها: سندات Lightning تحتفظ بها كسرّ، ويمكن تدويرها أو تقسيمها أو دمجها أو صهرها مجددًا إلى دفعة Lightning. تُفحص كل واحدة عبر HTTPS كل بضع دقائق.",
"lnurlMints.search.label": "ابحث في منّاتات LNURL",
"lnurlMints.search.placeholder": "ابحث بالاسم أو العنوان…",
"lnurlMints.sort.label": "ترتيب منّاتات LNURL حسب",
"lnurlMints.hideOffline": "إخفاء غير المتصلة",
"lnurlMints.showing": "عرض {total} منّاتة LNURL ({online} متصلة، {offline} غير متصلة)",
"lnurlMints.showingFiltered": "عرض {shown} من {total} منّاتة LNURL ({online} متصلة، {offline} غير متصلة)",
"lnurlMints.noResults": "لا توجد منّاتة LNURL مطابقة.",
"lnurlMints.emptyIndex": "لم تُفهرس أي منّاتة LNURL بعد.",
"lnurl.crumb": "منّاتات LNURL",
"lnurl.breadcrumb": "مسار التنقل",
"lnurl.copyAddress": "نسخ العنوان",
"lnurl.more.title": "المزيد من منّاتات LNURL",
"lnurl.more.all": "كل منّاتات LNURL ←",
"lnurl.disclaimer": "منّاتة LNURL تحتفظ بالساتوشي الخاص بك بينما تحتفظ أنت بسنداتها، فاستخدامها يعني الوثوق بمشغّلها. التقييمات آراء من المجتمع، و«متصلة» تعني يمكن الوصول إليها، لا أنها جديرة بالثقة.",
"lnurl.features.title": "الميزات",
"lnurl.features.none": "لم تُعلن هذه المنّاتة عمّا تدعمه.",
"lnurl.features.note": "الميزات هي ما أعلنه مشغّل المنّاتة على Nostr. وحيثما أمكن لهذا الموقع التحقق من إحداها عبر HTTPS فإنه يفعل، ويُنبّه عندما يكون شيء مُعلن غير متاح حاليًا.",
"lnurl.features.notListed": "غير مُعلن",
"lnurl.features.unavailable": "غير متاح",
"lnurl.feature.mint": "السكّ عبر Lightning (LUD-06)",
"lnurl.feature.melt": "الصهر إلى Lightning",
"lnurl.feature.notes": "تدوير / تقسيم / دمج السندات",
"lnurl.feature.lud16": "عنوان Lightning",
"lnurl.feature.lud21": "التحقق من الدفع (LUD-21)",
"lnurl.feature.signed-notes": "سندات موقّعة (تحقق دون اتصال)",
"lnurl.feature.onion": "عنوان Tor",
"lnurl.feature.rotate": "تدوير سند",
"lnurl.feature.split": "تقسيم سند",
"lnurl.feature.merge": "دمج السندات",
"lnurl.feature.lud06": "LUD-06 payRequest",
"lnurl.feature.lud03": "LUD-03 withdrawRequest",
"lnurl.limits.label": "حدود السحب",
"lnurl.limits.notPublished": "غير منشور",
"lnurl.limits.withdrawRow": "السحب",
"lnurl.limits.disabled": "معطّل",
"lnurl.limits.sats.one": "{n} ساتوشي",
"lnurl.limits.sats.two": "{n} ساتوشي",
"lnurl.limits.sats.few": "{n} ساتوشي",
"lnurl.limits.sats.many": "{n} ساتوشي",
"lnurl.limits.sats.other": "{n} ساتوشي",
"lnurl.details.title": "تفاصيل المنّاتة",
"lnurl.details.address": "عنوان Lightning",
"lnurl.details.mintPubkey": "المفتاح العام للمنّاتة",
"lnurl.details.onion": "عنوان Tor",
"lnurl.details.fee": "رسوم السكّ",
"lnurl.details.node": "العقدة",
"lnurl.details.capacity": "السعة المُعلنة",
"lnurl.kind.lightningAddress": "عنوان Lightning",
"lnurl.kind.mintPubkey": "المفتاح العام للمنّاتة",
"lnurl.kind.onionAddress": "عنوان Tor",
"lnurl.confirmed.label": "آخر تأكيد",
"lnurl.confirmed.when": "تم التأكيد {when}",
"lnurl.fee.base.one": "{n} ساتوشي",
"lnurl.fee.base.two": "{n} ساتوشي",
"lnurl.fee.base.few": "{n} ساتوشي",
"lnurl.fee.base.many": "{n} ساتوشي",
"lnurl.fee.base.other": "{n} ساتوشي",
"lnurl.fee.percent": "{percent}٪",
"home.topLnurl.title": "أفضل منّاتات LNURL",
"home.topLnurl.sub": "مرتّبة حسب المراجعات والتقييم",
"home.topLnurl.all": "كل منّاتات LNURL البالغة {n} ←",
"feed.ecosystem.lnurl": "LNURL",
"reviews.dialog.bodyPlaceholder.lnurl": "كيف كان السكّ والصهر وتدوير السندات؟ كن محددًا.",
"reviews.byUrl.open": "اكتب مراجعة",
"reviews.byUrl.title": "اكتب مراجعة",
"reviews.byUrl.lede": "الصق العنوان. لو الموقع لسه ما يعرفوش، هيتفحص ويتضاف دلوقتي.",
"reviews.byUrl.label.cashu": "رابط الـ mint",
"reviews.byUrl.label.fedimint": "رمز دعوة الاتحاد",
"reviews.byUrl.label.lnurl": "رابط mint من نوع LNURL",
"reviews.byUrl.placeholder.cashu": "mint.example.com",
"reviews.byUrl.placeholder.fedimint": "fed11…",
"reviews.byUrl.placeholder.lnurl": "mint.example.com",
"reviews.byUrl.check": "افحص هذا العنوان",
"reviews.byUrl.checking": "بنفحص…",
"reviews.byUrl.back": "استخدم عنوان تاني",
"reviews.byUrl.existing": "مدرج بالفعل",
"reviews.byUrl.indexed": "اتضاف دلوقتي",
"reviews.byUrl.confirmClose": "نقفل؟ العنوان اللي كتبته مش هيتحفظ.",
"reviews.byUrl.detected": "استجاب هذا العنوان بوصفه mint من نوع {kind}.",
"reviews.byUrl.switchTo": "راجعه بوصفه mint من نوع {kind}",
"reviews.byUrl.error.empty": "اكتب عنوان الأول.",
"reviews.byUrl.error.badUrl": "ده مش شكله رابط mint.",
"reviews.byUrl.error.badInvite": "هذا ليس رمز دعوة Fedimint. الرموز تبدأ بـ fed11.",
"reviews.byUrl.error.badInput": "لا يستطيع هذا الموقع فحص ذلك العنوان.",
"reviews.byUrl.error.blocked": "هذا ليس عنوانًا عامًا، لذا لا يمكن فحصه.",
"reviews.byUrl.error.notAMint": "استجاب هذا العنوان، لكن ليس بوصفه mint من هذا النوع.",
"reviews.byUrl.error.wrongType": "استجاب هذا العنوان، لكن بوصفه mint من نوع آخر.",
"reviews.byUrl.error.unverifiable": "لم يستجب شيء على ذلك العنوان، ولا أثر له على Nostr حتى الآن.",
"reviews.byUrl.error.rateLimited.one": "ده فحص كتير من نفس الاتصال. جرّب تاني بعد حوالي {n} دقيقة.",
"reviews.byUrl.error.rateLimited.other": "ده فحص كتير من نفس الاتصال. جرّب تاني بعد حوالي {n} دقيقة.",
"reviews.byUrl.error.network": "تعذّر الوصول إلى الفهرس. تحقّق من اتصالك ثم حاول مجددًا.",
"seo.lnurl.titleNoReviews": "{name}: معلومات ومراجعات منّاتة LNURL | Cashumints.space",
"seo.desc.statusWithdraw": "{status}، السحب {min}–{max} ساتوشي.",
"mint.chip.noFunding": "لا سكّ ولا صهر",
"mint.warnings.lnurlWithdrawalsDisabled.lead": "السحب معطّل.",
"mint.warnings.lnurlWithdrawalsDisabled.body.online": "تُعلن هذه المنّاتة حدًّا أقصى للسحب يساوي صفرًا، فلا يمكن استبدال أي سند تُصدره بأي شيء. لا تسكّ هنا حتى يتغير ذلك.",
"mint.warnings.lnurlWithdrawalsDisabled.body.offline": "عند آخر اتصال بها كانت هذه المنّاتة تُعلن حدًّا أقصى للسحب يساوي صفرًا، فلم يكن ممكنًا استبدال أي سند تُصدره بأي شيء. لا تسكّ هنا حتى يتغير ذلك.",
"mint.warnings.lnurlWithdrawalsDisabled.meta.online": "السحب معطّل حاليًا",
"mint.warnings.lnurlWithdrawalsDisabled.meta.offline": "كان السحب معطّلًا عند آخر ظهور",
"mint.warnings.lnurlInvalid.lead": "نقطة النهاية تستجيب لكنها غير صالحة.",
"mint.warnings.lnurlInvalid.body": "المضيف يستجيب، لكن ليس بإعلان منّاتة يستطيع هذا الموقع قراءته، لذا قد لا يعمل السحب. كل ما في الأسفل هو آخر حالة أمكن قراءتها، والمراجعات لا تزال تعمل.",
"mint.warnings.lnurlInvalid.meta": "يستجيب، لكن ليس بإعلان منّاتة صالح",
"mint.warnings.lnurlNoFunding.lead": "السكّ والصهر غير متاحين.",
"mint.warnings.lnurlNoFunding.body": "لا تزال السندات الموجودة قابلة للتدوير أو التقسيم أو الدمج، لكن لا شيء يدخل أو يخرج عبر Lightning حاليًا. المنّاتة نفسها تعمل وتستجيب؛ لكن عقدة Lightning خلفها غير قابلة للوصول منها.",
"mint.warnings.lnurlNoFunding.meta": "السكّ والصهر غير متاحين",
"mint.warnings.also.noFunding": "كانت عقدة Lightning الخاصة بها غير قابلة للوصول أيضًا، فلم يكن ممكنًا سكّ أو صهر أي شيء."
} }
+124 -3
View File
@@ -114,6 +114,8 @@
"card.reviewed": "تمت مراجعة {when}", "card.reviewed": "تمت مراجعة {when}",
"card.noReviews": "لا توجد تعليقات حتى الآن", "card.noReviews": "لا توجد تعليقات حتى الآن",
"mint.title": "{name} · جهة إصدار Cashu تقييمات", "mint.title": "{name} · جهة إصدار Cashu تقييمات",
"mint.titleFedimint": "{name} · مراجعات اتحاد Fedimint",
"mint.titleLnurl": "{name} · مراجعات mint من نوع LNURL",
"mint.breadcrumb": "مسار التنقل", "mint.breadcrumb": "مسار التنقل",
"mint.crumbMints": "جهات إصدار Cashu", "mint.crumbMints": "جهات إصدار Cashu",
"mint.copyUrl": "نسخ رابط جهة إصدار", "mint.copyUrl": "نسخ رابط جهة إصدار",
@@ -325,7 +327,7 @@
"reviews.validate.short": "اكتب ما لا يقل عن {n} حرفًا حتى تكون المراجعة مفيدة.", "reviews.validate.short": "اكتب ما لا يقل عن {n} حرفًا حتى تكون المراجعة مفيدة.",
"reviews.validate.long": "يقتصر عدد أحرف التعليقات على {n} حرفًا.", "reviews.validate.long": "يقتصر عدد أحرف التعليقات على {n} حرفًا.",
"feed.title": "أحدث التقييمات لـ جهة إصدار و«الفيدرالية» · Cashumints.space", "feed.title": "أحدث التقييمات لـ جهة إصدار و«الفيدرالية» · Cashumints.space",
"feed.description": "كل تقييم لـ جهة إصدار و«الفيدرالية» فور نشره: {reviews} تقييمًا عبر الاتحادات {mints} وجهات إصدار Cashu و{federations} وFedimint، تم التوقيع عليها في Nostr ويمكن قراءتها هنا مرتبة حسب الأحدث أولاً. قم بالتصفية حسب النظام البيئي أو التقييم أو الاسم.", "feed.description": "كل مراجعة لمنّاتة أو اتحاد أو منّاتة LNURL فور نشرها: {reviews} مراجعة تغطي {mints} منّاتة Cashu و{federations} اتحاد Fedimint و{lnurl} منّاتة LNURL، موقّعة على Nostr ومقروءة هنا من الأحدث. صفِّ حسب المنظومة أو التقييم أو الاسم.",
"feed.heading": "أحدث التقييمات", "feed.heading": "أحدث التقييمات",
"feed.lede": "كل تقييم لكل اتحاد من الاتحادات جهة إصدار Cashu وFedimint، مع عرض الأحدث أولاً، يُقرأ مباشرةً من محطات الترحيل Nostr. كل تقييم موقَّع من قبل مؤلفه ويُحفظ على محطات الترحيل، وليس في قاعدة بياناتنا.", "feed.lede": "كل تقييم لكل اتحاد من الاتحادات جهة إصدار Cashu وFedimint، مع عرض الأحدث أولاً، يُقرأ مباشرةً من محطات الترحيل Nostr. كل تقييم موقَّع من قبل مؤلفه ويُحفظ على محطات الترحيل، وليس في قاعدة بياناتنا.",
"feed.label": "التعليقات", "feed.label": "التعليقات",
@@ -400,7 +402,14 @@
"notfound.fb.reviews": "التعليقات", "notfound.fb.reviews": "التعليقات",
"notfound.fb.software": "البرامج", "notfound.fb.software": "البرامج",
"notfound.fb.lastOnline": "آخر مرة كان متصلاً بالإنترنت", "notfound.fb.lastOnline": "آخر مرة كان متصلاً بالإنترنت",
"notfound.fb.note": "تم فهرسة جهة إصدار بعد آخر عملية إعادة بناء للموقع، لذا فإن هذا ملخص مستمد من واجهة برمجة التطبيقات (API) الحية. وستظهر الصفحة الكاملة التي تحتوي على التقييمات عند إعادة البناء التالية.", "notfound.fb.note": "فُهرس هذا الـ mint بعد آخر بناء للموقع، لذا هذا ملخّص من الواجهة الحية، ومراجعاته أدناه. تظهر الصفحة الكاملة عند البناء التالي.",
"notfound.resolve.checking": "أول زيارة: يجري فحص هذا الـ mint الآن.",
"notfound.resolve.failed": "تعذّرت إضافة هذا الـ mint",
"notfound.resolve.failedFedimint": "تعذّرت إضافة هذا الاتحاد",
"notfound.resolve.retry": "جرّب عنوانًا آخر",
"notfound.resolve.switch": "افتحه بوصفه mint من نوع {kind}",
"notfound.resolve.fedimint": "عنوان صفحة الاتحاد لا يحمل رمز الدعوة الخاص به، لذا لا يمكن العثور عليه من الرابط وحده. ألصق رمز الدعوة وسيُضاف.",
"notfound.resolve.ambiguous": "لا يمكن استنتاج هذا العنوان من الرابط وحده. ألصق رابط الـ mint كاملًا وسيُضاف.",
"wallets.title": "Cashu محفظة · Cashumints.space", "wallets.title": "Cashu محفظة · Cashumints.space",
"wallets.description": "Cashu محفظة تعمل مع جهات إصدار المذكور هنا، عبر الهاتف المحمول والويب والمتصفح وخط الأوامر، مع الاستفادة من المزايا التي تتميز بها كل واحدة منها.", "wallets.description": "Cashu محفظة تعمل مع جهات إصدار المذكور هنا، عبر الهاتف المحمول والويب والمتصفح وخط الأوامر، مع الاستفادة من المزايا التي تتميز بها كل واحدة منها.",
"wallets.heading": "المحافظ", "wallets.heading": "المحافظ",
@@ -654,5 +663,117 @@
"footer.allFedimints": "جميع الاتحادات", "footer.allFedimints": "جميع الاتحادات",
"reviews.dialog.bodyPlaceholder.cashu": "كيف تم حساب الإصدار والاسترداد و\"support\"؟ كن محددًا.", "reviews.dialog.bodyPlaceholder.cashu": "كيف تم حساب الإصدار والاسترداد و\"support\"؟ كن محددًا.",
"reviews.dialog.bodyPlaceholder.fedimint": "كيف سارت الأمور فيما يتعلق بالانضمام، وLightning عملية دفع، والدعم؟ يرجى التحديد.", "reviews.dialog.bodyPlaceholder.fedimint": "كيف سارت الأمور فيما يتعلق بالانضمام، وLightning عملية دفع، والدعم؟ يرجى التحديد.",
"fedimint.details.title": "تفاصيل الاتحاد" "fedimint.details.title": "تفاصيل الاتحاد",
"nav.lnurlMints": "منّاتات LNURL",
"footer.allLnurlMints": "كل منّاتات LNURL",
"lnurlMints.title": "منّاتات LNURL: منّاتات سندات lnurlcash لحاملها، مع المراجعات | Cashumints.space",
"lnurlMints.description": "كل منّاتات LNURL البالغ عددها {total} والمفهرسة من شبكة Nostr: {online} متصلة، {offline} غير متصلة. تقييمات المجتمع وحدود السحب والميزات لكل واحدة.",
"lnurlMints.heading": "منّاتات LNURL",
"lnurlMints.lede": "كل منّاتة LNURL يعرفها هذا الموقع. منّاتة LNURL تُصدر سندات lnurlcash لحاملها: سندات Lightning تحتفظ بها كسرّ، ويمكن تدويرها أو تقسيمها أو دمجها أو صهرها مجددًا إلى دفعة Lightning. تُفحص كل واحدة عبر HTTPS كل بضع دقائق.",
"lnurlMints.search.label": "ابحث في منّاتات LNURL",
"lnurlMints.search.placeholder": "ابحث بالاسم أو العنوان…",
"lnurlMints.sort.label": "ترتيب منّاتات LNURL حسب",
"lnurlMints.hideOffline": "إخفاء غير المتصلة",
"lnurlMints.showing": "عرض {total} منّاتة LNURL ({online} متصلة، {offline} غير متصلة)",
"lnurlMints.showingFiltered": "عرض {shown} من {total} منّاتة LNURL ({online} متصلة، {offline} غير متصلة)",
"lnurlMints.noResults": "لا توجد منّاتة LNURL مطابقة.",
"lnurlMints.emptyIndex": "لم تُفهرس أي منّاتة LNURL بعد.",
"lnurl.crumb": "منّاتات LNURL",
"lnurl.breadcrumb": "مسار التنقل",
"lnurl.copyAddress": "نسخ العنوان",
"lnurl.more.title": "المزيد من منّاتات LNURL",
"lnurl.more.all": "كل منّاتات LNURL ←",
"lnurl.disclaimer": "منّاتة LNURL تحتفظ بالساتوشي الخاص بك بينما تحتفظ أنت بسنداتها، فاستخدامها يعني الوثوق بمشغّلها. التقييمات آراء من المجتمع، و«متصلة» تعني يمكن الوصول إليها، لا أنها جديرة بالثقة.",
"lnurl.features.title": "الميزات",
"lnurl.features.none": "لم تُعلن هذه المنّاتة عمّا تدعمه.",
"lnurl.features.note": "الميزات هي ما أعلنه مشغّل المنّاتة على Nostr. وحيثما أمكن لهذا الموقع التحقق من إحداها عبر HTTPS فإنه يفعل، ويُنبّه عندما يكون شيء مُعلن غير متاح حاليًا.",
"lnurl.features.notListed": "غير مُعلن",
"lnurl.features.unavailable": "غير متاح",
"lnurl.feature.mint": "السكّ عبر Lightning (LUD-06)",
"lnurl.feature.melt": "الصهر إلى Lightning",
"lnurl.feature.notes": "تدوير / تقسيم / دمج السندات",
"lnurl.feature.lud16": "عنوان Lightning",
"lnurl.feature.lud21": "التحقق من الدفع (LUD-21)",
"lnurl.feature.signed-notes": "سندات موقّعة (تحقق دون اتصال)",
"lnurl.feature.onion": "عنوان Tor",
"lnurl.feature.rotate": "تدوير سند",
"lnurl.feature.split": "تقسيم سند",
"lnurl.feature.merge": "دمج السندات",
"lnurl.feature.lud06": "LUD-06 payRequest",
"lnurl.feature.lud03": "LUD-03 withdrawRequest",
"lnurl.limits.label": "حدود السحب",
"lnurl.limits.notPublished": "غير منشور",
"lnurl.limits.withdrawRow": "السحب",
"lnurl.limits.disabled": "معطّل",
"lnurl.limits.sats.one": "{n} ساتوشي",
"lnurl.limits.sats.two": "{n} ساتوشي",
"lnurl.limits.sats.few": "{n} ساتوشي",
"lnurl.limits.sats.many": "{n} ساتوشي",
"lnurl.limits.sats.other": "{n} ساتوشي",
"lnurl.details.title": "تفاصيل المنّاتة",
"lnurl.details.address": "عنوان Lightning",
"lnurl.details.mintPubkey": "المفتاح العام للمنّاتة",
"lnurl.details.onion": "عنوان Tor",
"lnurl.details.fee": "رسوم السكّ",
"lnurl.details.node": "العقدة",
"lnurl.details.capacity": "السعة المُعلنة",
"lnurl.kind.lightningAddress": "عنوان Lightning",
"lnurl.kind.mintPubkey": "المفتاح العام للمنّاتة",
"lnurl.kind.onionAddress": "عنوان Tor",
"lnurl.confirmed.label": "آخر تأكيد",
"lnurl.confirmed.when": "تم التأكيد {when}",
"lnurl.fee.base.one": "{n} ساتوشي",
"lnurl.fee.base.two": "{n} ساتوشي",
"lnurl.fee.base.few": "{n} ساتوشي",
"lnurl.fee.base.many": "{n} ساتوشي",
"lnurl.fee.base.other": "{n} ساتوشي",
"lnurl.fee.percent": "{percent}٪",
"home.topLnurl.title": "أفضل منّاتات LNURL",
"home.topLnurl.sub": "مرتّبة حسب المراجعات والتقييم",
"home.topLnurl.all": "كل منّاتات LNURL البالغة {n} ←",
"feed.ecosystem.lnurl": "LNURL",
"reviews.dialog.bodyPlaceholder.lnurl": "كيف كان السكّ والصهر وتدوير السندات؟ كن محددًا.",
"reviews.byUrl.open": "اكتب مراجعة",
"reviews.byUrl.title": "اكتب مراجعة",
"reviews.byUrl.lede": "ألصق العنوان. إن كان هذا الموقع لا يعرفه بعد، فسيفحصه ويضيفه الآن.",
"reviews.byUrl.label.cashu": "رابط الـ mint",
"reviews.byUrl.label.fedimint": "رمز دعوة الاتحاد",
"reviews.byUrl.label.lnurl": "رابط mint من نوع LNURL",
"reviews.byUrl.placeholder.cashu": "mint.example.com",
"reviews.byUrl.placeholder.fedimint": "fed11…",
"reviews.byUrl.placeholder.lnurl": "mint.example.com",
"reviews.byUrl.check": "افحص هذا العنوان",
"reviews.byUrl.checking": "جارٍ الفحص…",
"reviews.byUrl.back": "استخدم عنوانًا آخر",
"reviews.byUrl.existing": "مدرج بالفعل",
"reviews.byUrl.indexed": "أُضيف للتو",
"reviews.byUrl.confirmClose": "إغلاق؟ العنوان الذي كتبته لن يُحفظ.",
"reviews.byUrl.detected": "استجاب هذا العنوان بوصفه mint من نوع {kind}.",
"reviews.byUrl.switchTo": "راجعه بوصفه mint من نوع {kind}",
"reviews.byUrl.error.empty": "اكتب عنوانًا أولًا.",
"reviews.byUrl.error.badUrl": "هذا لا يبدو رابط mint.",
"reviews.byUrl.error.badInvite": "هذا ليس رمز دعوة Fedimint. الرموز تبدأ بـ fed11.",
"reviews.byUrl.error.badInput": "لا يستطيع هذا الموقع فحص ذلك العنوان.",
"reviews.byUrl.error.blocked": "هذا ليس عنوانًا عامًا، لذا لا يمكن فحصه.",
"reviews.byUrl.error.notAMint": "استجاب هذا العنوان، لكن ليس بوصفه mint من هذا النوع.",
"reviews.byUrl.error.wrongType": "استجاب هذا العنوان، لكن بوصفه mint من نوع آخر.",
"reviews.byUrl.error.unverifiable": "لم يستجب شيء على ذلك العنوان، ولا أثر له على Nostr حتى الآن.",
"reviews.byUrl.error.rateLimited.one": "عدد كبير من الفحوص من اتصال واحد. حاول مجددًا بعد نحو {n} دقيقة.",
"reviews.byUrl.error.rateLimited.other": "عدد كبير من الفحوص من اتصال واحد. حاول مجددًا بعد نحو {n} دقيقة.",
"reviews.byUrl.error.network": "تعذّر الوصول إلى الفهرس. تحقّق من اتصالك ثم حاول مجددًا.",
"seo.lnurl.titleNoReviews": "{name}: معلومات ومراجعات منّاتة LNURL | Cashumints.space",
"seo.desc.statusWithdraw": "{status}، السحب {min}–{max} ساتوشي.",
"mint.chip.noFunding": "لا سكّ ولا صهر",
"mint.warnings.lnurlWithdrawalsDisabled.lead": "السحب معطّل.",
"mint.warnings.lnurlWithdrawalsDisabled.body.online": "تُعلن هذه المنّاتة حدًّا أقصى للسحب يساوي صفرًا، فلا يمكن استبدال أي سند تُصدره بأي شيء. لا تسكّ هنا حتى يتغير ذلك.",
"mint.warnings.lnurlWithdrawalsDisabled.body.offline": "عند آخر اتصال بها كانت هذه المنّاتة تُعلن حدًّا أقصى للسحب يساوي صفرًا، فلم يكن ممكنًا استبدال أي سند تُصدره بأي شيء. لا تسكّ هنا حتى يتغير ذلك.",
"mint.warnings.lnurlWithdrawalsDisabled.meta.online": "السحب معطّل حاليًا",
"mint.warnings.lnurlWithdrawalsDisabled.meta.offline": "كان السحب معطّلًا عند آخر ظهور",
"mint.warnings.lnurlInvalid.lead": "نقطة النهاية تستجيب لكنها غير صالحة.",
"mint.warnings.lnurlInvalid.body": "المضيف يستجيب، لكن ليس بإعلان منّاتة يستطيع هذا الموقع قراءته، لذا قد لا يعمل السحب. كل ما في الأسفل هو آخر حالة أمكن قراءتها، والمراجعات لا تزال تعمل.",
"mint.warnings.lnurlInvalid.meta": "يستجيب، لكن ليس بإعلان منّاتة صالح",
"mint.warnings.lnurlNoFunding.lead": "السكّ والصهر غير متاحين.",
"mint.warnings.lnurlNoFunding.body": "لا تزال السندات الموجودة قابلة للتدوير أو التقسيم أو الدمج، لكن لا شيء يدخل أو يخرج عبر Lightning حاليًا. المنّاتة نفسها تعمل وتستجيب؛ لكن عقدة Lightning خلفها غير قابلة للوصول منها.",
"mint.warnings.lnurlNoFunding.meta": "السكّ والصهر غير متاحين",
"mint.warnings.also.noFunding": "كانت عقدة Lightning الخاصة بها غير قابلة للوصول أيضًا، فلم يكن ممكنًا سكّ أو صهر أي شيء."
} }
+46 -21
View File
@@ -35,29 +35,50 @@ export const LOCALES: LocaleInfo[] = [
{ code: 'en', label: 'English', intl: 'en-GB', og: 'en_US' }, { code: 'en', label: 'English', intl: 'en-GB', og: 'en_US' },
{ code: 'es', label: 'Español', intl: 'es-ES', og: 'es_ES' }, { code: 'es', label: 'Español', intl: 'es-ES', og: 'es_ES' },
{ code: 'nl', label: 'Nederlands', intl: 'nl-NL', og: 'nl_NL' }, { code: 'nl', label: 'Nederlands', intl: 'nl-NL', og: 'nl_NL' },
{ code: 'zh', label: '简体中文', intl: 'zh-CN', og: 'zh_CN' },
{ code: 'hi', label: 'हिन्दी', intl: 'hi-IN', og: 'hi_IN' },
{ code: 'ar', label: 'العربية الفصحى', intl: 'ar-SA', og: 'ar_SA', dir: 'rtl' },
{ code: 'fr', label: 'Français', intl: 'fr-FR', og: 'fr_FR' },
{ code: 'pt', label: 'Português', intl: 'pt-BR', og: 'pt_BR' },
{ code: 'it', label: 'Italiano', intl: 'it-IT', og: 'it_IT' },
{ code: 'tr', label: 'Türkçe', intl: 'tr-TR', og: 'tr_TR' },
{ code: 'pl', label: 'Polski', intl: 'pl-PL', og: 'pl_PL' },
{ code: 'uk', label: 'Українська', intl: 'uk-UA', og: 'uk_UA' },
{ code: 'ro', label: 'Română', intl: 'ro-RO', og: 'ro_RO' },
{ code: 'el', label: 'Ελληνικά', intl: 'el-GR', og: 'el_GR' },
{ code: 'cs', label: 'Čeština', intl: 'cs-CZ', og: 'cs_CZ' },
{ code: 'sv', label: 'Svenska', intl: 'sv-SE', og: 'sv_SE' },
{ code: 'da', label: 'Dansk', intl: 'da-DK', og: 'da_DK' },
{ code: 'id', label: 'Bahasa Indonesia', intl: 'id-ID', og: 'id_ID' },
{ code: 'ur', label: 'اردو', intl: 'ur-PK', og: 'ur_PK', dir: 'rtl' },
{ code: 'ru', label: 'Русский', intl: 'ru-RU', og: 'ru_RU' },
{ code: 'de', label: 'Deutsch', intl: 'de-DE', og: 'de_DE' },
{ code: 'ja', label: '日本語', intl: 'ja-JP', og: 'ja_JP' },
{ code: 'ar-eg', label: 'العربية المصرية', intl: 'ar-EG', og: 'ar_EG', dir: 'rtl' },
{ code: 'vi', label: 'Tiếng Việt', intl: 'vi-VN', og: 'vi_VN' },
]; ];
/*
* The twenty-one locales this table used to carry, and why they are not here.
*
* Their catalogs are on disk, complete to the key, and none of them ever reached a
* reader. `index.ts` imports en, es and nl and nothing else, so every one of those
* locales rendered the English catalog under its own `<html lang>`, its own URL prefix
* and a full reciprocal hreflang set. That is not a half-translated page, which is bad;
* it is twenty-one complete duplicates of the English site, each one telling a crawler
* it is a different language. Confirmed in the build output before this was changed:
* /ja/mint/mint.mountainlake.io declared `lang="ja"` and said "All Cashu mints".
*
* Removing them costs no reader anything, because there was never anything there to
* lose. What it buys is that the hreflang set now describes three pages that really
* are three languages.
*
* Reviving one is three steps and they must all happen together, or it goes back to
* being an English page wearing a language tag:
*
* 1. add its entry here and its code to locales.mjs
* 2. import its catalog in index.ts and add it to CATALOGS <- the step that was missed
* 3. run `pnpm check:i18n`; it will name the four footer.* keys added after the
* catalogs were frozen, and they need translating before the locale ships
*
* The catalogs were also machine-translated and several are not shippable as they
* stand. The ones with damage in strings the footer itself renders, found while
* auditing this list:
*
* el the brand becomes "Καταιγίδες. Διάστημα" — "Storms. Space"
* ro the brand becomes "Casumints.space"
* ur the copyright reads "نصب شدہ:" ("Installed:") and drops NIP-87
* ja the copyright hallucinates "© 2018" and a rights-reserved clause
* ar-eg the © becomes the digit 0; "wallets" becomes "الجدار" ("the wall")
* it the © is doubled
*
* And on the two Arabic entries: `ar` (Modern Standard) is the one to bring back.
* `ar-eg` (Egyptian) is a real dialect catalog rather than a duplicate, but it is the
* more damaged of the two, and a second RTL page set competing with the first for a
* mostly-overlapping audience is not worth its SEO cost. Decided explicitly rather
* than by shipping both.
*/
export type Locale = string; export type Locale = string;
/** Just the codes, for the Astro config and for validating a URL segment. */ /** Just the codes, for the Astro config and for validating a URL segment. */
@@ -102,6 +123,10 @@ export const CLIENT_NAMESPACES = [
// redraws when a fresh announcement disagrees with the build. // redraws when a fresh announcement disagrees with the build.
'fedimints', 'fedimints',
'fedimint', 'fedimint',
// The LNURL index's count line, and the feature rows and withdraw-limits cell the
// live island redraws when a fresh probe disagrees with the build.
'lnurlMints',
'lnurl',
'card', 'card',
'reviews', 'reviews',
'feed', 'feed',
+122 -3
View File
@@ -116,6 +116,8 @@
"card.reviewed": "přezkoumáno{when}", "card.reviewed": "přezkoumáno{when}",
"card.noReviews": "dosud žádné recenze", "card.noReviews": "dosud žádné recenze",
"mint.title": "{name}· Cashu mincovna recenze", "mint.title": "{name}· Cashu mincovna recenze",
"mint.titleFedimint": "{name} · Recenze federace Fedimint",
"mint.titleLnurl": "{name} · Recenze LNURL mintu",
"mint.breadcrumb": "Drobečková navigace", "mint.breadcrumb": "Drobečková navigace",
"mint.crumbMints": "Cashu mincovny", "mint.crumbMints": "Cashu mincovny",
"mint.copyUrl": "Kopírovat URL mincovna", "mint.copyUrl": "Kopírovat URL mincovna",
@@ -351,7 +353,7 @@
"reviews.validate.short": "Napište alespoň:{n}znaky, takže přezkum je užitečný.", "reviews.validate.short": "Napište alespoň:{n}znaky, takže přezkum je užitečný.",
"reviews.validate.long": "Recenze jsou omezeny na{n}charakteristické znaky.", "reviews.validate.long": "Recenze jsou omezeny na{n}charakteristické znaky.",
"feed.title": "Nejnovější recenze mincovny a federace · Cashumints.space", "feed.title": "Nejnovější recenze mincovny a federace · Cashumints.space",
"feed.description": "Každá recenze mincovny a federace ihned po zveřejnění: {reviews} recenzí pro {mints} mincoven Cashu a {federations} federací Fedimint, podepsaných na Nostru a seřazených od nejnovější. Filtrujte podle ekosystému, hodnocení nebo názvu.", "feed.description": "Každá recenze mintu, federace a mintu LNURL hned po zveřejnění: {reviews} recenzí na {mints} mintů Cashu, {federations} federací Fedimint a {lnurl} mintů LNURL, podepsaných na Nostr a čitelných zde od nejnovějších. Filtrujte podle ekosystému, hodnocení nebo názvu.",
"feed.heading": "Poslední recenze", "feed.heading": "Poslední recenze",
"feed.lede": "Každá recenze každé Cashu mincovny a Fedimint federace, nejnovější první, číst živě z Nostr relays. Každý z nich je podepsán jeho autor a žije na relays, ne v naší databázi.", "feed.lede": "Každá recenze každé Cashu mincovny a Fedimint federace, nejnovější první, číst živě z Nostr relays. Každý z nich je podepsán jeho autor a žije na relays, ne v naší databázi.",
"feed.label": "Přezkum", "feed.label": "Přezkum",
@@ -428,7 +430,14 @@
"notfound.fb.reviews": "Přezkum", "notfound.fb.reviews": "Přezkum",
"notfound.fb.software": "Software", "notfound.fb.software": "Software",
"notfound.fb.lastOnline": "Naposledy online", "notfound.fb.lastOnline": "Naposledy online",
"notfound.fb.note": "Tato mincovna byla indexována po stránce byl poslední built. takže to je shrnutí z živé API. Celá stránka s recenzemi se objeví na další rebuiltd.", "notfound.fb.note": "Tenhle mint byl zaindexován až po posledním sestavení webu, takže tohle je souhrn z živého API a pod ním jsou recenze. Celá stránka se objeví při dalším sestavení.",
"notfound.resolve.checking": "První návštěva: právě tenhle mint kontroluji.",
"notfound.resolve.failed": "Tenhle mint se nepodařilo přidat",
"notfound.resolve.failedFedimint": "Tuhle federaci se nepodařilo přidat",
"notfound.resolve.retry": "Zkusit jinou adresu",
"notfound.resolve.switch": "Otevřít jako mint {kind}",
"notfound.resolve.fedimint": "Adresa stránky federace neobsahuje její invite code, takže tuhle nelze najít jen podle odkazu. Vlož invite code a federace se přidá.",
"notfound.resolve.ambiguous": "Tuhle adresu nejde z odkazu odvodit. Vlož celou URL mintu a mint se přidá.",
"wallets.title": "Cashu peněženky · Cashumints.space", "wallets.title": "Cashu peněženky · Cashumints.space",
"wallets.description": "Cashu peněženky, které pracují s mincovny uvedeny zde, přes mobilní, webový, prohlížeč a příkazové linky, s tím, co každý z nich je dobrý.", "wallets.description": "Cashu peněženky, které pracují s mincovny uvedeny zde, přes mobilní, webový, prohlížeč a příkazové linky, s tím, co každý z nich je dobrý.",
"wallets.heading": "Peněženky", "wallets.heading": "Peněženky",
@@ -696,5 +705,115 @@
"footer.allFedimints": "Všechny federace", "footer.allFedimints": "Všechny federace",
"reviews.dialog.bodyPlaceholder.cashu": "Jak se Mintingu, tání a podporu práce ven? Buďte specifičtí.", "reviews.dialog.bodyPlaceholder.cashu": "Jak se Mintingu, tání a podporu práce ven? Buďte specifičtí.",
"reviews.dialog.bodyPlaceholder.fedimint": "Jak se joining, Lightning platby a podpora práce ven? Buďte specifičtí.", "reviews.dialog.bodyPlaceholder.fedimint": "Jak se joining, Lightning platby a podpora práce ven? Buďte specifičtí.",
"fedimint.details.title": "Podrobnosti o federaci" "fedimint.details.title": "Podrobnosti o federaci",
"nav.lnurlMints": "Minty LNURL",
"footer.allLnurlMints": "Všechny minty LNURL",
"lnurlMints.title": "Minty LNURL: minty směnek na doručitele lnurlcash s recenzemi | Cashumints.space",
"lnurlMints.description": "Všech {total} mintů LNURL indexovaných ze sítě Nostr: {online} online, {offline} offline. Hodnocení komunity, limity výběru a možnosti každého z nich.",
"lnurlMints.heading": "Minty LNURL",
"lnurlMints.lede": "Každý mint LNURL, o kterém tento web ví. Mint LNURL vydává směnky na doručitele lnurlcash: Lightning směnky, které držíte jako tajemství a které lze otočit, rozdělit, sloučit nebo roztavit zpět na Lightning platbu. Každý se každých pár minut kontroluje přes HTTPS.",
"lnurlMints.search.label": "Hledat minty LNURL",
"lnurlMints.search.placeholder": "Hledat podle jména nebo adresy…",
"lnurlMints.sort.label": "Řadit minty LNURL podle",
"lnurlMints.hideOffline": "Skrýt offline",
"lnurlMints.showing": "Zobrazeno {total} mintů LNURL ({online} online, {offline} offline)",
"lnurlMints.showingFiltered": "Zobrazeno {shown} z {total} mintů LNURL ({online} online, {offline} offline)",
"lnurlMints.noResults": "Žádný mint LNURL neodpovídá.",
"lnurlMints.emptyIndex": "Zatím nejsou indexovány žádné minty LNURL.",
"lnurl.crumb": "Minty LNURL",
"lnurl.breadcrumb": "Drobečková navigace",
"lnurl.copyAddress": "Kopírovat adresu",
"lnurl.more.title": "Další minty LNURL",
"lnurl.more.all": "Všechny minty LNURL →",
"lnurl.disclaimer": "Mint LNURL drží vaše saty, zatímco vy držíte jeho směnky, takže používat ho znamená důvěřovat jeho provozovateli. Hodnocení jsou názory komunity a online znamená dostupný, nikoli důvěryhodný.",
"lnurl.features.title": "Funkce",
"lnurl.features.none": "Tento mint neoznámil, co podporuje.",
"lnurl.features.note": "Funkce jsou to, co provozovatel mintu oznámil na Nostr. Kde to tento web může ověřit přes HTTPS, ověří to, a upozorní, když je něco oznámeného právě teď nedostupné.",
"lnurl.features.notListed": "neoznámeno",
"lnurl.features.unavailable": "nedostupné",
"lnurl.feature.mint": "Mintování přes Lightning (LUD-06)",
"lnurl.feature.melt": "Tavení do Lightning",
"lnurl.feature.notes": "Otočení / rozdělení / sloučení směnek",
"lnurl.feature.lud16": "Lightning adresa",
"lnurl.feature.lud21": "Ověření platby (LUD-21)",
"lnurl.feature.signed-notes": "Podepsané směnky (offline ověření)",
"lnurl.feature.onion": "Tor adresa",
"lnurl.feature.rotate": "Otočit směnku",
"lnurl.feature.split": "Rozdělit směnku",
"lnurl.feature.merge": "Sloučit směnky",
"lnurl.feature.lud06": "LUD-06 payRequest",
"lnurl.feature.lud03": "LUD-03 withdrawRequest",
"lnurl.limits.label": "Limity výběru",
"lnurl.limits.notPublished": "nezveřejněno",
"lnurl.limits.withdrawRow": "Výběr",
"lnurl.limits.disabled": "vypnuto",
"lnurl.limits.sats.one": "{n} sat",
"lnurl.limits.sats.few": "{n} saty",
"lnurl.limits.sats.many": "{n} satu",
"lnurl.limits.sats.other": "{n} satů",
"lnurl.details.title": "Podrobnosti mintu",
"lnurl.details.address": "Lightning adresa",
"lnurl.details.mintPubkey": "Veřejný klíč mintu",
"lnurl.details.onion": "Tor adresa",
"lnurl.details.fee": "Poplatek za mintování",
"lnurl.details.node": "Uzel",
"lnurl.details.capacity": "Oznámená kapacita",
"lnurl.kind.lightningAddress": "Lightning adresa",
"lnurl.kind.mintPubkey": "veřejný klíč mintu",
"lnurl.kind.onionAddress": "Tor adresa",
"lnurl.confirmed.label": "Naposledy potvrzeno",
"lnurl.confirmed.when": "potvrzeno {when}",
"lnurl.fee.base.one": "{n} sat",
"lnurl.fee.base.few": "{n} saty",
"lnurl.fee.base.many": "{n} satu",
"lnurl.fee.base.other": "{n} satů",
"lnurl.fee.percent": "{percent} %",
"home.topLnurl.title": "Nejlepší minty LNURL",
"home.topLnurl.sub": "seřazeno podle recenzí a hodnocení",
"home.topLnurl.all": "Všech {n} mintů LNURL →",
"feed.ecosystem.lnurl": "LNURL",
"reviews.dialog.bodyPlaceholder.lnurl": "Jak dopadlo mintování, tavení a otáčení směnek? Buďte konkrétní.",
"reviews.byUrl.open": "Napsat recenzi",
"reviews.byUrl.title": "Napsat recenzi",
"reviews.byUrl.lede": "Vlož adresu. Pokud ji tenhle web ještě nezná, teď ji zkontroluje a přidá.",
"reviews.byUrl.label.cashu": "URL mintu",
"reviews.byUrl.label.fedimint": "Invite code federace",
"reviews.byUrl.label.lnurl": "URL LNURL mintu",
"reviews.byUrl.placeholder.cashu": "mint.example.com",
"reviews.byUrl.placeholder.fedimint": "fed11…",
"reviews.byUrl.placeholder.lnurl": "mint.example.com",
"reviews.byUrl.check": "Zkontrolovat adresu",
"reviews.byUrl.checking": "Kontroluji…",
"reviews.byUrl.back": "Použít jinou adresu",
"reviews.byUrl.existing": "Už je v seznamu",
"reviews.byUrl.indexed": "Právě přidáno",
"reviews.byUrl.confirmClose": "Zavřít? Zadaná adresa se neuchová.",
"reviews.byUrl.detected": "Ta adresa odpověděla jako mint {kind}.",
"reviews.byUrl.switchTo": "Recenzovat jako mint {kind}",
"reviews.byUrl.error.empty": "Nejdřív zadej adresu.",
"reviews.byUrl.error.badUrl": "Tohle nevypadá jako URL mintu.",
"reviews.byUrl.error.badInvite": "Tohle není Fedimint invite code. Kódy začínají na fed11.",
"reviews.byUrl.error.badInput": "Tenhle web tuhle adresu zkontrolovat neumí.",
"reviews.byUrl.error.blocked": "Tohle není veřejná adresa, takže ji nelze zkontrolovat.",
"reviews.byUrl.error.notAMint": "Ta adresa odpověděla, ale ne jako mint tohoto druhu.",
"reviews.byUrl.error.wrongType": "Ta adresa odpověděla, ale jako jiný druh mintu.",
"reviews.byUrl.error.unverifiable": "Na té adrese nic neodpovědělo a na Nostru po ní zatím není ani stopa.",
"reviews.byUrl.error.rateLimited.one": "To je hodně kontrol z jednoho připojení. Zkus to znovu asi za {n} minutu.",
"reviews.byUrl.error.rateLimited.other": "To je hodně kontrol z jednoho připojení. Zkus to znovu asi za {n} minut.",
"reviews.byUrl.error.network": "Index není dostupný. Zkontroluj připojení a zkus to znovu.",
"seo.lnurl.titleNoReviews": "{name}: informace a recenze mintu LNURL | Cashumints.space",
"seo.desc.statusWithdraw": "{status}, výběr {min}–{max} sat.",
"mint.chip.noFunding": "Bez mintování / tavení",
"mint.warnings.lnurlWithdrawalsDisabled.lead": "Výběry vypnuty.",
"mint.warnings.lnurlWithdrawalsDisabled.body.online": "Tento mint oznamuje maximální výběr nula, takže žádnou jím vydanou směnku nelze za nic vyměnit. Nemintujte zde, dokud se to nezmění.",
"mint.warnings.lnurlWithdrawalsDisabled.body.offline": "Při posledním kontaktu tento mint oznamoval maximální výběr nula, takže žádnou jím vydanou směnku nešlo za nic vyměnit. Nemintujte zde, dokud se to nezmění.",
"mint.warnings.lnurlWithdrawalsDisabled.meta.online": "Výběry jsou nyní vypnuté",
"mint.warnings.lnurlWithdrawalsDisabled.meta.offline": "Při posledním kontaktu byly výběry vypnuté",
"mint.warnings.lnurlInvalid.lead": "Endpoint odpovídá, ale neplatně.",
"mint.warnings.lnurlInvalid.body": "Host odpovídá, ale ne oznámením mintu, které tento web umí přečíst, takže výběry nemusí fungovat. Níže je poslední stav, který se podařilo načíst, a recenze stále fungují.",
"mint.warnings.lnurlInvalid.meta": "Odpovídá, ale ne platným oznámením mintu",
"mint.warnings.lnurlNoFunding.lead": "Mintování a tavení nedostupné.",
"mint.warnings.lnurlNoFunding.body": "Stávající směnky lze stále otáčet, rozdělovat nebo slučovat, ale právě teď přes Lightning nic nevchází ani nevychází. Samotný mint běží a odpovídá; Lightning uzel za ním pro něj není dostupný.",
"mint.warnings.lnurlNoFunding.meta": "Mintování a tavení nedostupné",
"mint.warnings.also.noFunding": "Jeho Lightning uzel byl také nedostupný, takže nešlo nic zmintovat ani roztavit."
} }
+118 -3
View File
@@ -114,6 +114,8 @@
"card.reviewed": "anmeldt {when}", "card.reviewed": "anmeldt {when}",
"card.noReviews": "Der er endnu ingen anmeldelser", "card.noReviews": "Der er endnu ingen anmeldelser",
"mint.title": "{name} · Cashu – anmeldelser af Mint", "mint.title": "{name} · Cashu – anmeldelser af Mint",
"mint.titleFedimint": "{name} · Anmeldelser af Fedimint-føderationen",
"mint.titleLnurl": "{name} · Anmeldelser af LNURL-minten",
"mint.breadcrumb": "Navigationsspor", "mint.breadcrumb": "Navigationsspor",
"mint.crumbMints": "Cashu-pastiller", "mint.crumbMints": "Cashu-pastiller",
"mint.copyUrl": "Kopier Mint-URL’en", "mint.copyUrl": "Kopier Mint-URL’en",
@@ -325,7 +327,7 @@
"reviews.validate.short": "Skriv mindst {n} tegn, så anmeldelsen bliver nyttig.", "reviews.validate.short": "Skriv mindst {n} tegn, så anmeldelsen bliver nyttig.",
"reviews.validate.long": "Anmeldelser må højst indeholde {n} tegn.", "reviews.validate.long": "Anmeldelser må højst indeholde {n} tegn.",
"feed.title": "Seneste anmeldelser af Mint og Federation · Cashumints.space", "feed.title": "Seneste anmeldelser af Mint og Federation · Cashumints.space",
"feed.description": "Alle anmeldelser af mints og føderationer, så snart de offentliggøres: Anmeldelser af {reviews} på tværs af mints som {mints} og Cashu samt føderationer som {federations} og Fedimint, underskrevet på Nostr og tilgængelige her – de nyeste først. Filtrer efter økosystem, bedømmelse eller navn.", "feed.description": "Hver anmeldelse af mints, føderationer og LNURL-mints, så snart den offentliggøres: {reviews} anmeldelser af {mints} Cashu-mints, {federations} Fedimint-føderationer og {lnurl} LNURL-mints, signeret på Nostr og læsbare her med nyeste først. Filtrér på økosystem, bedømmelse eller navn.",
"feed.heading": "Seneste anmeldelser", "feed.heading": "Seneste anmeldelser",
"feed.lede": "Alle anmeldelser af alle Cashu-mint- og Fedimint-federationer, med de nyeste først, læses direkte fra Nostr-relæerne. Hver enkelt er underskrevet af sin forfatter og findes på relæerne, ikke i vores database.", "feed.lede": "Alle anmeldelser af alle Cashu-mint- og Fedimint-federationer, med de nyeste først, læses direkte fra Nostr-relæerne. Hver enkelt er underskrevet af sin forfatter og findes på relæerne, ikke i vores database.",
"feed.label": "Anmeldelser", "feed.label": "Anmeldelser",
@@ -400,7 +402,14 @@
"notfound.fb.reviews": "Anmeldelser", "notfound.fb.reviews": "Anmeldelser",
"notfound.fb.software": "Software", "notfound.fb.software": "Software",
"notfound.fb.lastOnline": "Sidst online", "notfound.fb.lastOnline": "Sidst online",
"notfound.fb.note": "Denne anmeldelse blev indekseret, efter at siden sidst blev opdateret, så dette er et resumé fra live-API’en. Den fulde side med anmeldelser vises ved den næste opdatering.", "notfound.fb.note": "Denne mint blev indekseret efter sidste build af siden, så dette er et resumé fra det live API med anmeldelserne nedenfor. Hele siden kommer med ved næste build.",
"notfound.resolve.checking": "Første besøg: tjekker denne mint nu.",
"notfound.resolve.failed": "Denne mint kunne ikke tilføjes",
"notfound.resolve.failedFedimint": "Denne føderation kunne ikke tilføjes",
"notfound.resolve.retry": "Prøv en anden adresse",
"notfound.resolve.switch": "Åbn den som {kind}-mint",
"notfound.resolve.fedimint": "En føderations sideadresse indeholder ikke dens invite code, så denne kan ikke slås op ud fra linket alene. Indsæt invite code, så bliver den tilføjet.",
"notfound.resolve.ambiguous": "Denne adresse kan ikke udledes af linket alene. Indsæt mintens fulde URL, så bliver den tilføjet.",
"wallets.title": "Cashu-tegnebøger · Cashumints.space", "wallets.title": "Cashu-tegnebøger · Cashumints.space",
"wallets.description": "Cashu-tegnebøger, der fungerer sammen med de her anførte mints på mobil, web, i browseren og via kommandolinjen, alt efter hvad den enkelte er bedst til.", "wallets.description": "Cashu-tegnebøger, der fungerer sammen med de her anførte mints på mobil, web, i browseren og via kommandolinjen, alt efter hvad den enkelte er bedst til.",
"wallets.heading": "Punge", "wallets.heading": "Punge",
@@ -654,5 +663,111 @@
"footer.allFedimints": "Alle forbund", "footer.allFedimints": "Alle forbund",
"reviews.dialog.bodyPlaceholder.cashu": "Hvordan gik det med prægningen, smeltningen og understøtningen? Vær konkret.", "reviews.dialog.bodyPlaceholder.cashu": "Hvordan gik det med prægningen, smeltningen og understøtningen? Vær konkret.",
"reviews.dialog.bodyPlaceholder.fedimint": "Hvordan gik det med tilmeldingen, betalingerne via Lightning og supporten? Vær konkret.", "reviews.dialog.bodyPlaceholder.fedimint": "Hvordan gik det med tilmeldingen, betalingerne via Lightning og supporten? Vær konkret.",
"fedimint.details.title": "Oplysninger om forbundet" "fedimint.details.title": "Oplysninger om forbundet",
"nav.lnurlMints": "LNURL-mints",
"footer.allLnurlMints": "Alle LNURL-mints",
"lnurlMints.title": "LNURL-mints: mints til lnurlcash-ihændehaversedler, anmeldt | Cashumints.space",
"lnurlMints.description": "Alle {total} LNURL-mints indekseret fra Nostr-netværket: {online} online, {offline} offline. Bedømmelser fra fællesskabet, udbetalingsgrænser og funktioner for hver enkelt.",
"lnurlMints.heading": "LNURL-mints",
"lnurlMints.lede": "Hver LNURL-mint denne side kender til. En LNURL-mint udsteder lnurlcash-ihændehaversedler: Lightning-sedler, du opbevarer som en hemmelighed, og som kan roteres, deles, lægges sammen eller smeltes tilbage til en Lightning-betaling. Hver enkelt tjekkes via HTTPS hvert par minutter.",
"lnurlMints.search.label": "Søg i LNURL-mints",
"lnurlMints.search.placeholder": "Søg på navn eller adresse…",
"lnurlMints.sort.label": "Sortér LNURL-mints efter",
"lnurlMints.hideOffline": "Skjul offline",
"lnurlMints.showing": "Viser {total} LNURL-mints ({online} online, {offline} offline)",
"lnurlMints.showingFiltered": "Viser {shown} af {total} LNURL-mints ({online} online, {offline} offline)",
"lnurlMints.noResults": "Ingen LNURL-mint matcher det.",
"lnurlMints.emptyIndex": "Der er endnu ikke indekseret nogen LNURL-mints.",
"lnurl.crumb": "LNURL-mints",
"lnurl.breadcrumb": "Brødkrummesti",
"lnurl.copyAddress": "Kopiér adresse",
"lnurl.more.title": "Flere LNURL-mints",
"lnurl.more.all": "Alle LNURL-mints →",
"lnurl.disclaimer": "En LNURL-mint opbevarer dine sats, mens du holder dens sedler, så at bruge en betyder at stole på dens operatør. Bedømmelser er fællesskabets meninger, og online betyder tilgængelig, ikke troværdig.",
"lnurl.features.title": "Funktioner",
"lnurl.features.none": "Denne mint har ikke annonceret, hvad den understøtter.",
"lnurl.features.note": "Funktioner er det, mintens operatør har annonceret på Nostr. Hvor denne side kan tjekke en via HTTPS, gør den det, og siger til, når noget annonceret ikke er tilgængeligt lige nu.",
"lnurl.features.notListed": "ikke annonceret",
"lnurl.features.unavailable": "utilgængelig",
"lnurl.feature.mint": "Mint via Lightning (LUD-06)",
"lnurl.feature.melt": "Smelt til Lightning",
"lnurl.feature.notes": "Rotér / del / læg sedler sammen",
"lnurl.feature.lud16": "Lightning-adresse",
"lnurl.feature.lud21": "Betalingsverifikation (LUD-21)",
"lnurl.feature.signed-notes": "Signerede sedler (offlineverifikation)",
"lnurl.feature.onion": "Tor-adresse",
"lnurl.feature.rotate": "Rotér en seddel",
"lnurl.feature.split": "Del en seddel",
"lnurl.feature.merge": "Læg sedler sammen",
"lnurl.feature.lud06": "LUD-06 payRequest",
"lnurl.feature.lud03": "LUD-03 withdrawRequest",
"lnurl.limits.label": "Udbetalingsgrænser",
"lnurl.limits.notPublished": "ikke offentliggjort",
"lnurl.limits.withdrawRow": "Udbetaling",
"lnurl.limits.disabled": "slået fra",
"lnurl.limits.sats.one": "{n} sat",
"lnurl.limits.sats.other": "{n} sats",
"lnurl.details.title": "Oplysninger om minten",
"lnurl.details.address": "Lightning-adresse",
"lnurl.details.mintPubkey": "Mintens offentlige nøgle",
"lnurl.details.onion": "Tor-adresse",
"lnurl.details.fee": "Mint-gebyr",
"lnurl.details.node": "Node",
"lnurl.details.capacity": "Annonceret kapacitet",
"lnurl.kind.lightningAddress": "Lightning-adresse",
"lnurl.kind.mintPubkey": "mintens offentlige nøgle",
"lnurl.kind.onionAddress": "Tor-adresse",
"lnurl.confirmed.label": "Sidst bekræftet",
"lnurl.confirmed.when": "bekræftet {when}",
"lnurl.fee.base.one": "{n} sat",
"lnurl.fee.base.other": "{n} sats",
"lnurl.fee.percent": "{percent} %",
"home.topLnurl.title": "Bedste LNURL-mints",
"home.topLnurl.sub": "rangeret efter anmeldelser og bedømmelse",
"home.topLnurl.all": "Alle {n} LNURL-mints →",
"feed.ecosystem.lnurl": "LNURL",
"reviews.dialog.bodyPlaceholder.lnurl": "Hvordan gik mintning, smeltning og rotation af sedler? Vær konkret.",
"reviews.byUrl.open": "Skriv en anmeldelse",
"reviews.byUrl.title": "Skriv en anmeldelse",
"reviews.byUrl.lede": "Indsæt adressen. Kender siden den ikke endnu, bliver den tjekket og tilføjet nu.",
"reviews.byUrl.label.cashu": "Mint-URL",
"reviews.byUrl.label.fedimint": "Føderationens invite code",
"reviews.byUrl.label.lnurl": "URL til LNURL-mint",
"reviews.byUrl.placeholder.cashu": "mint.example.com",
"reviews.byUrl.placeholder.fedimint": "fed11…",
"reviews.byUrl.placeholder.lnurl": "mint.example.com",
"reviews.byUrl.check": "Tjek denne adresse",
"reviews.byUrl.checking": "Tjekker…",
"reviews.byUrl.back": "Brug en anden adresse",
"reviews.byUrl.existing": "Allerede på listen",
"reviews.byUrl.indexed": "Lige tilføjet",
"reviews.byUrl.confirmClose": "Luk? Adressen du skrev bliver ikke gemt.",
"reviews.byUrl.detected": "Den adresse svarede som en {kind}-mint.",
"reviews.byUrl.switchTo": "Anmeld den som {kind}-mint",
"reviews.byUrl.error.empty": "Skriv en adresse først.",
"reviews.byUrl.error.badUrl": "Det ligner ikke en mint-URL.",
"reviews.byUrl.error.badInvite": "Det er ikke en Fedimint invite code. Koder starter med fed11.",
"reviews.byUrl.error.badInput": "Siden kan ikke tjekke den adresse.",
"reviews.byUrl.error.blocked": "Det er ikke en offentlig adresse, så den kan ikke tjekkes.",
"reviews.byUrl.error.notAMint": "Adressen svarede, men ikke som en mint af den her slags.",
"reviews.byUrl.error.wrongType": "Adressen svarede, men som en anden slags mint.",
"reviews.byUrl.error.unverifiable": "Intet svarede på den adresse, og der er endnu ingen spor af den på Nostr.",
"reviews.byUrl.error.rateLimited.one": "Det er mange tjek fra én forbindelse. Prøv igen om cirka {n} minut.",
"reviews.byUrl.error.rateLimited.other": "Det er mange tjek fra én forbindelse. Prøv igen om cirka {n} minutter.",
"reviews.byUrl.error.network": "Indekset kunne ikke nås. Tjek din forbindelse, og prøv igen.",
"seo.lnurl.titleNoReviews": "{name}: info og anmeldelser af LNURL-minten | Cashumints.space",
"seo.desc.statusWithdraw": "{status}, udbetaling {min}–{max} sat.",
"mint.chip.noFunding": "Ingen mint / smelt",
"mint.warnings.lnurlWithdrawalsDisabled.lead": "Udbetalinger er slået fra.",
"mint.warnings.lnurlWithdrawalsDisabled.body.online": "Denne mint annoncerer en maksimal udbetaling på nul, så ingen seddel den udsteder kan indløses til noget. Lad være med at minte her, før det ændrer sig.",
"mint.warnings.lnurlWithdrawalsDisabled.body.offline": "Denne mint annoncerede en maksimal udbetaling på nul, sidst den blev nået, så ingen seddel den udstedte kunne indløses til noget. Lad være med at minte her, før det ændrer sig.",
"mint.warnings.lnurlWithdrawalsDisabled.meta.online": "Udbetalinger er slået fra lige nu",
"mint.warnings.lnurlWithdrawalsDisabled.meta.offline": "Udbetalinger var slået fra, sidst den blev set",
"mint.warnings.lnurlInvalid.lead": "Endepunktet svarer, men er ugyldigt.",
"mint.warnings.lnurlInvalid.body": "Værten svarer, men ikke med en mint-annoncering, denne side kan læse, så udbetalinger virker måske ikke. Alt nedenfor er den seneste tilstand, der kunne læses, og anmeldelser virker stadig.",
"mint.warnings.lnurlInvalid.meta": "Svarer, men ikke med en gyldig mint-annoncering",
"mint.warnings.lnurlNoFunding.lead": "Mintning og smeltning er utilgængelige.",
"mint.warnings.lnurlNoFunding.body": "Eksisterende sedler kan stadig roteres, deles eller lægges sammen, men lige nu bevæger intet sig ind eller ud via Lightning. Selve minten kører og svarer; Lightning-noden bag den kan ikke nås derfra.",
"mint.warnings.lnurlNoFunding.meta": "Mintning og smeltning utilgængelige",
"mint.warnings.also.noFunding": "Dens Lightning-node kunne heller ikke nås, så intet kunne mintes eller smeltes."
} }
+124 -9
View File
@@ -15,12 +15,12 @@
"time.justNow": "gerade eben", "time.justNow": "gerade eben",
"time.none": "-", "time.none": "-",
"time.unknownDate": "ein unbekanntes Datum", "time.unknownDate": "ein unbekanntes Datum",
"time.ago.minute": "{n}me zurück", "time.ago.minute": "vor {n} Min.",
"time.ago.hour": "{n}he zurück", "time.ago.hour": "vor {n} Std.",
"time.ago.day": "{n}de zurück", "time.ago.day": "vor {n} T.",
"time.ago.week": "{n}we zurück", "time.ago.week": "vor {n} Wo.",
"time.ago.month": "{n}moe zurück", "time.ago.month": "vor {n} Mon.",
"time.ago.year": "{n}ye zurück", "time.ago.year": "vor {n} J.",
"time.short.minute": "{n}m", "time.short.minute": "{n}m",
"time.short.hour": "{n}h", "time.short.hour": "{n}h",
"time.short.day": "{n}d", "time.short.day": "{n}d",
@@ -114,6 +114,8 @@
"card.reviewed": "Rezension zu „{when}“", "card.reviewed": "Rezension zu „{when}“",
"card.noReviews": "noch keine Bewertungen", "card.noReviews": "noch keine Bewertungen",
"mint.title": "{name} · Bewertungen zu Cashu Mint", "mint.title": "{name} · Bewertungen zu Cashu Mint",
"mint.titleFedimint": "{name} · Reviews der Fedimint-Föderation",
"mint.titleLnurl": "{name} · Reviews des LNURL Mint",
"mint.breadcrumb": "Navigationspfad", "mint.breadcrumb": "Navigationspfad",
"mint.crumbMints": "Cashu-Münzen", "mint.crumbMints": "Cashu-Münzen",
"mint.copyUrl": "URL kopieren", "mint.copyUrl": "URL kopieren",
@@ -325,7 +327,7 @@
"reviews.validate.short": "Schreibe mindestens {n} Zeichen, damit die Bewertung hilfreich ist.", "reviews.validate.short": "Schreibe mindestens {n} Zeichen, damit die Bewertung hilfreich ist.",
"reviews.validate.long": "Rezensionen sind auf {n} Zeichen begrenzt.", "reviews.validate.long": "Rezensionen sind auf {n} Zeichen begrenzt.",
"feed.title": "Aktuelle Bewertungen zu Münzprägeanstalten und Verbänden · Cashumints.space", "feed.title": "Aktuelle Bewertungen zu Münzprägeanstalten und Verbänden · Cashumints.space",
"feed.description": "Alle Bewertungen zu Mints und Föderationen, sobald sie veröffentlicht werden: {reviews}-Bewertungen zu {mints} Cashu-Mints und {federations} Fedimint-Föderationen, auf Nostr signiert und hier lesbar – die neuesten zuerst. Filtern Sie nach Ökosystem, Bewertung oder Name.", "feed.description": "Jede Bewertung von Mints, Föderationen und LNURL-Mints, sobald sie veröffentlicht wird: {reviews} Bewertungen zu {mints} Cashu-Mints, {federations} Fedimint-Föderationen und {lnurl} LNURL-Mints, auf Nostr signiert und hier neueste zuerst lesbar. Filtere nach Ökosystem, Note oder Name.",
"feed.heading": "Neueste Bewertungen", "feed.heading": "Neueste Bewertungen",
"feed.lede": "Alle Bewertungen zu allen Cashu-Münzen und der Fedimint-Föderation, sortiert nach den neuesten zuerst, werden live von den Nostr-Relays übertragen. Jede Bewertung ist vom jeweiligen Autor signiert und wird auf den Relays gespeichert, nicht in unserer Datenbank.", "feed.lede": "Alle Bewertungen zu allen Cashu-Münzen und der Fedimint-Föderation, sortiert nach den neuesten zuerst, werden live von den Nostr-Relays übertragen. Jede Bewertung ist vom jeweiligen Autor signiert und wird auf den Relays gespeichert, nicht in unserer Datenbank.",
"feed.label": "Rezensionen", "feed.label": "Rezensionen",
@@ -400,7 +402,14 @@
"notfound.fb.reviews": "Rezensionen", "notfound.fb.reviews": "Rezensionen",
"notfound.fb.software": "Software", "notfound.fb.software": "Software",
"notfound.fb.lastOnline": "Zuletzt online", "notfound.fb.lastOnline": "Zuletzt online",
"notfound.fb.note": "Diese Seite wurde erst nach der letzten Aktualisierung der Website indexiert, daher handelt es sich hierbei um eine Zusammenfassung aus der Live-API. Die vollständige Seite mit den Bewertungen wird bei der nächsten Aktualisierung angezeigt.", "notfound.fb.note": "Dieser Mint wurde nach dem letzten Build der Seite indexiert. Das hier ist eine Zusammenfassung aus der Live-API, mit den Reviews darunter. Die vollständige Seite erscheint beim nächsten Build.",
"notfound.resolve.checking": "Erster Besuch: dieser Mint wird gerade geprüft.",
"notfound.resolve.failed": "Dieser Mint konnte nicht aufgenommen werden",
"notfound.resolve.failedFedimint": "Diese Föderation konnte nicht aufgenommen werden",
"notfound.resolve.retry": "Andere Adresse versuchen",
"notfound.resolve.switch": "Als {kind} Mint öffnen",
"notfound.resolve.fedimint": "Die Seitenadresse einer Föderation enthält ihren Invite Code nicht, deshalb lässt sich diese nicht allein über den Link finden. Füg den Invite Code ein, dann wird sie aufgenommen.",
"notfound.resolve.ambiguous": "Diese Adresse lässt sich nicht allein aus dem Link ableiten. Füg die vollständige URL des Mint ein, dann wird er aufgenommen.",
"wallets.title": "Cashu-Wallets · Cashumints.space", "wallets.title": "Cashu-Wallets · Cashumints.space",
"wallets.description": "Cashu-Wallets, die mit den hier aufgeführten Mints kompatibel sind – auf Mobilgeräten, im Web, im Browser und über die Befehlszeile –, wobei jedes seine jeweiligen Stärken hat.", "wallets.description": "Cashu-Wallets, die mit den hier aufgeführten Mints kompatibel sind – auf Mobilgeräten, im Web, im Browser und über die Befehlszeile –, wobei jedes seine jeweiligen Stärken hat.",
"wallets.heading": "Geldbörsen", "wallets.heading": "Geldbörsen",
@@ -654,5 +663,111 @@
"footer.allFedimints": "Alle Verbände", "footer.allFedimints": "Alle Verbände",
"reviews.dialog.bodyPlaceholder.cashu": "Wie liefen das Prägen, das Einschmelzen und die Unterstützung ab? Gehen Sie bitte ins Detail.", "reviews.dialog.bodyPlaceholder.cashu": "Wie liefen das Prägen, das Einschmelzen und die Unterstützung ab? Gehen Sie bitte ins Detail.",
"reviews.dialog.bodyPlaceholder.fedimint": "Wie haben sich die Integration, Lightning-Zahlungen und der Support bewährt? Gehen Sie bitte konkret darauf ein.", "reviews.dialog.bodyPlaceholder.fedimint": "Wie haben sich die Integration, Lightning-Zahlungen und der Support bewährt? Gehen Sie bitte konkret darauf ein.",
"fedimint.details.title": "Angaben zum Verband" "fedimint.details.title": "Angaben zum Verband",
"nav.lnurlMints": "LNURL-Mints",
"footer.allLnurlMints": "Alle LNURL-Mints",
"lnurlMints.title": "LNURL-Mints: Mints für lnurlcash-Inhaberscheine, bewertet | Cashumints.space",
"lnurlMints.description": "Alle {total} aus dem Nostr-Netzwerk indexierten LNURL-Mints: {online} online, {offline} offline. Community-Bewertungen, Auszahlungsgrenzen und Funktionen für jeden.",
"lnurlMints.heading": "LNURL-Mints",
"lnurlMints.lede": "Jeder LNURL-Mint, den diese Seite kennt. Ein LNURL-Mint gibt lnurlcash-Inhaberscheine aus: Lightning-Scheine, die man als Geheimnis hält und die sich rotieren, teilen, zusammenführen oder zurück zu einer Lightning-Zahlung schmelzen lassen. Jeder wird alle paar Minuten über HTTPS geprüft.",
"lnurlMints.search.label": "LNURL-Mints durchsuchen",
"lnurlMints.search.placeholder": "Nach Name oder Adresse suchen…",
"lnurlMints.sort.label": "LNURL-Mints sortieren nach",
"lnurlMints.hideOffline": "Offline ausblenden",
"lnurlMints.showing": "{total} LNURL-Mints ({online} online, {offline} offline)",
"lnurlMints.showingFiltered": "{shown} von {total} LNURL-Mints ({online} online, {offline} offline)",
"lnurlMints.noResults": "Kein LNURL-Mint passt dazu.",
"lnurlMints.emptyIndex": "Es sind noch keine LNURL-Mints indexiert.",
"lnurl.crumb": "LNURL-Mints",
"lnurl.breadcrumb": "Brotkrumenpfad",
"lnurl.copyAddress": "Adresse kopieren",
"lnurl.more.title": "Weitere LNURL-Mints",
"lnurl.more.all": "Alle LNURL-Mints →",
"lnurl.disclaimer": "Ein LNURL-Mint verwahrt deine Sats, während du seine Scheine hältst – ihn zu nutzen heißt also, seinem Betreiber zu vertrauen. Bewertungen sind Meinungen der Community, und online heißt erreichbar, nicht vertrauenswürdig.",
"lnurl.features.title": "Funktionen",
"lnurl.features.none": "Dieser Mint hat nicht angekündigt, was er unterstützt.",
"lnurl.features.note": "Funktionen sind das, was der Betreiber des Mints auf Nostr angekündigt hat. Wo diese Seite eine davon über HTTPS prüfen kann, tut sie das, und sie sagt es, wenn etwas Angekündigtes gerade nicht verfügbar ist.",
"lnurl.features.notListed": "nicht angekündigt",
"lnurl.features.unavailable": "nicht verfügbar",
"lnurl.feature.mint": "Prägen über Lightning (LUD-06)",
"lnurl.feature.melt": "Schmelzen zu Lightning",
"lnurl.feature.notes": "Scheine rotieren / teilen / zusammenführen",
"lnurl.feature.lud16": "Lightning-Adresse",
"lnurl.feature.lud21": "Zahlungsprüfung (LUD-21)",
"lnurl.feature.signed-notes": "Signierte Scheine (Offline-Prüfung)",
"lnurl.feature.onion": "Tor-Adresse",
"lnurl.feature.rotate": "Einen Schein rotieren",
"lnurl.feature.split": "Einen Schein teilen",
"lnurl.feature.merge": "Scheine zusammenführen",
"lnurl.feature.lud06": "LUD-06 payRequest",
"lnurl.feature.lud03": "LUD-03 withdrawRequest",
"lnurl.limits.label": "Auszahlungsgrenzen",
"lnurl.limits.notPublished": "nicht veröffentlicht",
"lnurl.limits.withdrawRow": "Auszahlung",
"lnurl.limits.disabled": "deaktiviert",
"lnurl.limits.sats.one": "{n} Sat",
"lnurl.limits.sats.other": "{n} Sats",
"lnurl.details.title": "Details zum Mint",
"lnurl.details.address": "Lightning-Adresse",
"lnurl.details.mintPubkey": "Öffentlicher Schlüssel des Mints",
"lnurl.details.onion": "Tor-Adresse",
"lnurl.details.fee": "Prägegebühr",
"lnurl.details.node": "Node",
"lnurl.details.capacity": "Angekündigte Kapazität",
"lnurl.kind.lightningAddress": "Lightning-Adresse",
"lnurl.kind.mintPubkey": "öffentlicher Schlüssel des Mints",
"lnurl.kind.onionAddress": "Tor-Adresse",
"lnurl.confirmed.label": "Zuletzt bestätigt",
"lnurl.confirmed.when": "bestätigt {when}",
"lnurl.fee.base.one": "{n} Sat",
"lnurl.fee.base.other": "{n} Sats",
"lnurl.fee.percent": "{percent} %",
"home.topLnurl.title": "Top LNURL-Mints",
"home.topLnurl.sub": "nach Bewertungen und Note sortiert",
"home.topLnurl.all": "Alle {n} LNURL-Mints →",
"feed.ecosystem.lnurl": "LNURL",
"reviews.dialog.bodyPlaceholder.lnurl": "Wie liefen Prägen, Schmelzen und das Rotieren von Scheinen? Werde konkret.",
"reviews.byUrl.open": "Review schreiben",
"reviews.byUrl.title": "Review schreiben",
"reviews.byUrl.lede": "Adresse einfügen. Kennt diese Seite sie noch nicht, wird sie jetzt geprüft und aufgenommen.",
"reviews.byUrl.label.cashu": "Mint-URL",
"reviews.byUrl.label.fedimint": "Invite Code der Föderation",
"reviews.byUrl.label.lnurl": "URL des LNURL Mint",
"reviews.byUrl.placeholder.cashu": "mint.example.com",
"reviews.byUrl.placeholder.fedimint": "fed11…",
"reviews.byUrl.placeholder.lnurl": "mint.example.com",
"reviews.byUrl.check": "Diese Adresse prüfen",
"reviews.byUrl.checking": "Wird geprüft…",
"reviews.byUrl.back": "Andere Adresse verwenden",
"reviews.byUrl.existing": "Schon gelistet",
"reviews.byUrl.indexed": "Gerade aufgenommen",
"reviews.byUrl.confirmClose": "Schließen? Die eingegebene Adresse wird nicht behalten.",
"reviews.byUrl.detected": "Diese Adresse hat als {kind} Mint geantwortet.",
"reviews.byUrl.switchTo": "Als {kind} Mint bewerten",
"reviews.byUrl.error.empty": "Gib zuerst eine Adresse ein.",
"reviews.byUrl.error.badUrl": "Das sieht nicht nach einer Mint-URL aus.",
"reviews.byUrl.error.badInvite": "Das ist kein Fedimint Invite Code. Codes beginnen mit fed11.",
"reviews.byUrl.error.badInput": "Diese Seite kann diese Adresse nicht prüfen.",
"reviews.byUrl.error.blocked": "Das ist keine öffentliche Adresse, sie kann also nicht geprüft werden.",
"reviews.byUrl.error.notAMint": "Diese Adresse hat geantwortet, aber nicht als Mint dieser Art.",
"reviews.byUrl.error.wrongType": "Diese Adresse hat geantwortet, aber als eine andere Art von Mint.",
"reviews.byUrl.error.unverifiable": "Unter dieser Adresse hat nichts geantwortet, und auf Nostr gibt es bisher keine Spur davon.",
"reviews.byUrl.error.rateLimited.one": "Das sind viele Prüfungen von einer Verbindung. Versuch es in etwa {n} Minute noch einmal.",
"reviews.byUrl.error.rateLimited.other": "Das sind viele Prüfungen von einer Verbindung. Versuch es in etwa {n} Minuten noch einmal.",
"reviews.byUrl.error.network": "Der Index war nicht erreichbar. Prüf deine Verbindung und versuch es noch einmal.",
"seo.lnurl.titleNoReviews": "{name}: Infos und Bewertungen zum LNURL-Mint | Cashumints.space",
"seo.desc.statusWithdraw": "{status}, Auszahlung {min}–{max} Sat.",
"mint.chip.noFunding": "Kein Prägen / Schmelzen",
"mint.warnings.lnurlWithdrawalsDisabled.lead": "Auszahlungen sind deaktiviert.",
"mint.warnings.lnurlWithdrawalsDisabled.body.online": "Dieser Mint gibt eine maximale Auszahlung von null an, also lässt sich kein von ihm ausgegebener Schein gegen irgendetwas einlösen. Präge hier nichts, solange das so bleibt.",
"mint.warnings.lnurlWithdrawalsDisabled.body.offline": "Dieser Mint gab beim letzten Kontakt eine maximale Auszahlung von null an, also ließ sich kein von ihm ausgegebener Schein gegen irgendetwas einlösen. Präge hier nichts, solange das so bleibt.",
"mint.warnings.lnurlWithdrawalsDisabled.meta.online": "Auszahlungen derzeit deaktiviert",
"mint.warnings.lnurlWithdrawalsDisabled.meta.offline": "Auszahlungen waren beim letzten Kontakt deaktiviert",
"mint.warnings.lnurlInvalid.lead": "Endpunkt antwortet, ist aber ungültig.",
"mint.warnings.lnurlInvalid.body": "Der Host antwortet, aber nicht mit einer Mint-Ankündigung, die diese Seite lesen kann – Auszahlungen funktionieren also möglicherweise nicht. Alles darunter ist der letzte lesbare Stand, und Bewertungen funktionieren weiterhin.",
"mint.warnings.lnurlInvalid.meta": "Antwortet, aber nicht mit einer gültigen Mint-Ankündigung",
"mint.warnings.lnurlNoFunding.lead": "Prägen und Schmelzen nicht verfügbar.",
"mint.warnings.lnurlNoFunding.body": "Vorhandene Scheine lassen sich weiterhin rotieren, teilen oder zusammenführen, aber über Lightning bewegt sich gerade nichts hinein oder hinaus. Der Mint selbst läuft und antwortet; die Lightning-Node dahinter ist von ihm aus nicht erreichbar.",
"mint.warnings.lnurlNoFunding.meta": "Prägen und Schmelzen nicht verfügbar",
"mint.warnings.also.noFunding": "Auch seine Lightning-Node war nicht erreichbar, es konnte also weder geprägt noch geschmolzen werden."
} }
+118 -3
View File
@@ -114,6 +114,8 @@
"card.reviewed": "επανεξετάζεται{when}", "card.reviewed": "επανεξετάζεται{when}",
"card.noReviews": "δεν υπάρχουν ακόμη αξιολογήσεις", "card.noReviews": "δεν υπάρχουν ακόμη αξιολογήσεις",
"mint.title": "{name}· Κασσού νόμισμα των κριτικών", "mint.title": "{name}· Κασσού νόμισμα των κριτικών",
"mint.titleFedimint": "{name} · Κριτικές της ομοσπονδίας Fedimint",
"mint.titleLnurl": "{name} · Κριτικές του LNURL mint",
"mint.breadcrumb": "Ψωμί", "mint.breadcrumb": "Ψωμί",
"mint.crumbMints": "Κασσού νόμισματοκοπία", "mint.crumbMints": "Κασσού νόμισματοκοπία",
"mint.copyUrl": "Αντιγραφή νομισματοκοπίου URL", "mint.copyUrl": "Αντιγραφή νομισματοκοπίου URL",
@@ -325,7 +327,7 @@
"reviews.validate.short": "Γράψε τουλάχιστον{n} χαρακτήρες έτσι ώστε η κριτική είναι χρήσιμη.", "reviews.validate.short": "Γράψε τουλάχιστον{n} χαρακτήρες έτσι ώστε η κριτική είναι χρήσιμη.",
"reviews.validate.long": "Οι κριτικές περιορίζονται σε{n} χαρακτήρες.", "reviews.validate.long": "Οι κριτικές περιορίζονται σε{n} χαρακτήρες.",
"feed.title": "Τελευταίες κριτικές για το vomismatokopoo και την ομοσπονδία · Cashumints.space", "feed.title": "Τελευταίες κριτικές για το vomismatokopoo και την ομοσπονδία · Cashumints.space",
"feed.description": "Κάθε κριτική νομισματοκοπείου και ομοσπονδίας τη στιγμή που δημοσιεύεται: {reviews} κριτικές για {mints} νομισματοκοπεία Cashu και {federations} ομοσπονδίες Fedimint, υπογεγραμμένες στο Nostr και διαθέσιμες εδώ από τη νεότερη. Φιλτράρετε ανά οικοσύστημα, αξιολόγηση ή όνομα.", "feed.description": "Κάθε κριτική για mint, ομοσπονδία και mint LNURL μόλις δημοσιεύεται: {reviews} κριτικές για {mints} mints Cashu, {federations} ομοσπονδίες Fedimint και {lnurl} mints LNURL, υπογεγραμμένες στο Nostr και αναγνώσιμες εδώ με τις νεότερες πρώτα. Φιλτράρετε ανά οικοσύστημα, βαθμολογία ή όνομα.",
"feed.heading": "Τελευταίες κριτικές", "feed.heading": "Τελευταίες κριτικές",
"feed.lede": "Κάθε κριτική για κάθε Κασού νομοτόπο και Fedimint ομοσπονδία, νεότερος πρώτα, διαβάστε ζωντανά από τα ρελέ Nostr. Κάθε ένα είναι υπογεγραμμένο από τον συγγραφέα του και ζει στα ρελέ, όχι στη βάση δεδομένων μας.", "feed.lede": "Κάθε κριτική για κάθε Κασού νομοτόπο και Fedimint ομοσπονδία, νεότερος πρώτα, διαβάστε ζωντανά από τα ρελέ Nostr. Κάθε ένα είναι υπογεγραμμένο από τον συγγραφέα του και ζει στα ρελέ, όχι στη βάση δεδομένων μας.",
"feed.label": "Επανεξέταση", "feed.label": "Επανεξέταση",
@@ -400,7 +402,14 @@
"notfound.fb.reviews": "Επανεξέταση", "notfound.fb.reviews": "Επανεξέταση",
"notfound.fb.software": "Λογισμικό", "notfound.fb.software": "Λογισμικό",
"notfound.fb.lastOnline": "Τελευταία σύνδεση", "notfound.fb.lastOnline": "Τελευταία σύνδεση",
"notfound.fb.note": "Αυτό το vomismatokopo ήταν ευρετηριασμένο μετά την τελευταία κατασκευή του site, έτσι αυτό είναι μια περίληψη από το live API. Η πλήρης σελίδα με σχόλια εμφανίζεται στην επόμενη ανακατασκευή.", "notfound.fb.note": "Αυτό το mint καταχωρήθηκε μετά την τελευταία κατασκευή του ιστότοπου, οπότε αυτή είναι μια σύνοψη από το ζωντανό API, με τις κριτικές από κάτω. Η πλήρης σελίδα εμφανίζεται στην επόμενη κατασκευή.",
"notfound.resolve.checking": "Πρώτη επίσκεψη: ελέγχεται τώρα αυτό το mint.",
"notfound.resolve.failed": "Αυτό το mint δεν μπόρεσε να προστεθεί",
"notfound.resolve.failedFedimint": "Αυτή η ομοσπονδία δεν μπόρεσε να προστεθεί",
"notfound.resolve.retry": "Δοκίμασε άλλη διεύθυνση",
"notfound.resolve.switch": "Άνοιξέ το ως mint {kind}",
"notfound.resolve.fedimint": "Η διεύθυνση σελίδας μιας ομοσπονδίας δεν περιέχει τον κωδικό πρόσκλησής της, οπότε αυτή δεν βρίσκεται μόνο από τον σύνδεσμο. Επικόλλησε τον κωδικό πρόσκλησης και προστίθεται.",
"notfound.resolve.ambiguous": "Αυτή η διεύθυνση δεν προκύπτει μόνο από τον σύνδεσμο. Επικόλλησε το πλήρες URL του mint και προστίθεται.",
"wallets.title": "Πορτοφόλια Κασού · Κασουμέντος.space", "wallets.title": "Πορτοφόλια Κασού · Κασουμέντος.space",
"wallets.description": "Πορτοφόλια Cashu που λειτουργούν με τη νόμισμα που αναφέρεται εδώ, σε όλα τα κινητά, web, browser και τη γραμμή εντολών, με ό, τι ο καθένας είναι καλός σε.", "wallets.description": "Πορτοφόλια Cashu που λειτουργούν με τη νόμισμα που αναφέρεται εδώ, σε όλα τα κινητά, web, browser και τη γραμμή εντολών, με ό, τι ο καθένας είναι καλός σε.",
"wallets.heading": "Πορτοφόλια", "wallets.heading": "Πορτοφόλια",
@@ -654,5 +663,111 @@
"footer.allFedimints": "Όλες οι ομοσπονδίες", "footer.allFedimints": "Όλες οι ομοσπονδίες",
"reviews.dialog.bodyPlaceholder.cashu": "Πώς λειτούργησε η κοπή, το λιώσιμο και η υποστήριξη; Να είστε συγκεκριμένοι.", "reviews.dialog.bodyPlaceholder.cashu": "Πώς λειτούργησε η κοπή, το λιώσιμο και η υποστήριξη; Να είστε συγκεκριμένοι.",
"reviews.dialog.bodyPlaceholder.fedimint": "Πώς λειτούργησε η ένταξη, οι πληρωμές Lightning και η υποστήριξη; Να είστε συγκεκριμένοι.", "reviews.dialog.bodyPlaceholder.fedimint": "Πώς λειτούργησε η ένταξη, οι πληρωμές Lightning και η υποστήριξη; Να είστε συγκεκριμένοι.",
"fedimint.details.title": "Στοιχεία Ομοσπονδίας" "fedimint.details.title": "Στοιχεία Ομοσπονδίας",
"nav.lnurlMints": "Mints LNURL",
"footer.allLnurlMints": "Όλα τα mints LNURL",
"lnurlMints.title": "Mints LNURL: mints για κομιστικά γραμμάτια lnurlcash, με κριτικές | Cashumints.space",
"lnurlMints.description": "Και τα {total} mints LNURL που έχουν ευρετηριαστεί από το δίκτυο Nostr: {online} σε σύνδεση, {offline} εκτός σύνδεσης. Βαθμολογίες της κοινότητας, όρια ανάληψης και δυνατότητες για το καθένα.",
"lnurlMints.heading": "Mints LNURL",
"lnurlMints.lede": "Κάθε mint LNURL που γνωρίζει αυτός ο ιστότοπος. Ένα mint LNURL εκδίδει κομιστικά γραμμάτια lnurlcash: γραμμάτια Lightning που κρατάτε ως μυστικό και που μπορούν να περιστραφούν, να διαιρεθούν, να συγχωνευθούν ή να λιώσουν πίσω σε μια πληρωμή Lightning. Το καθένα ελέγχεται μέσω HTTPS κάθε λίγα λεπτά.",
"lnurlMints.search.label": "Αναζήτηση mints LNURL",
"lnurlMints.search.placeholder": "Αναζήτηση με όνομα ή διεύθυνση…",
"lnurlMints.sort.label": "Ταξινόμηση mints LNURL κατά",
"lnurlMints.hideOffline": "Απόκρυψη εκτός σύνδεσης",
"lnurlMints.showing": "Εμφανίζονται {total} mints LNURL ({online} σε σύνδεση, {offline} εκτός σύνδεσης)",
"lnurlMints.showingFiltered": "Εμφανίζονται {shown} από {total} mints LNURL ({online} σε σύνδεση, {offline} εκτός σύνδεσης)",
"lnurlMints.noResults": "Κανένα mint LNURL δεν ταιριάζει.",
"lnurlMints.emptyIndex": "Δεν έχει ευρετηριαστεί ακόμη κανένα mint LNURL.",
"lnurl.crumb": "Mints LNURL",
"lnurl.breadcrumb": "Διαδρομή πλοήγησης",
"lnurl.copyAddress": "Αντιγραφή διεύθυνσης",
"lnurl.more.title": "Περισσότερα mints LNURL",
"lnurl.more.all": "Όλα τα mints LNURL →",
"lnurl.disclaimer": "Ένα mint LNURL κρατά τα sats σας ενώ εσείς κρατάτε τα γραμμάτιά του, οπότε η χρήση του σημαίνει εμπιστοσύνη στον διαχειριστή του. Οι βαθμολογίες είναι απόψεις της κοινότητας, και «σε σύνδεση» σημαίνει προσβάσιμο, όχι αξιόπιστο.",
"lnurl.features.title": "Δυνατότητες",
"lnurl.features.none": "Αυτό το mint δεν έχει ανακοινώσει τι υποστηρίζει.",
"lnurl.features.note": "Οι δυνατότητες είναι όσα ανακοίνωσε ο διαχειριστής του mint στο Nostr. Όπου αυτός ο ιστότοπος μπορεί να ελέγξει κάποια μέσω HTTPS το κάνει, και το αναφέρει όταν κάτι ανακοινωμένο δεν είναι διαθέσιμο αυτή τη στιγμή.",
"lnurl.features.notListed": "μη ανακοινωμένο",
"lnurl.features.unavailable": "μη διαθέσιμο",
"lnurl.feature.mint": "Κοπή μέσω Lightning (LUD-06)",
"lnurl.feature.melt": "Λιώσιμο προς Lightning",
"lnurl.feature.notes": "Περιστροφή / διαίρεση / συγχώνευση γραμματίων",
"lnurl.feature.lud16": "Διεύθυνση Lightning",
"lnurl.feature.lud21": "Επαλήθευση πληρωμής (LUD-21)",
"lnurl.feature.signed-notes": "Υπογεγραμμένα γραμμάτια (επαλήθευση εκτός σύνδεσης)",
"lnurl.feature.onion": "Διεύθυνση Tor",
"lnurl.feature.rotate": "Περιστροφή γραμματίου",
"lnurl.feature.split": "Διαίρεση γραμματίου",
"lnurl.feature.merge": "Συγχώνευση γραμματίων",
"lnurl.feature.lud06": "LUD-06 payRequest",
"lnurl.feature.lud03": "LUD-03 withdrawRequest",
"lnurl.limits.label": "Όρια ανάληψης",
"lnurl.limits.notPublished": "μη δημοσιευμένο",
"lnurl.limits.withdrawRow": "Ανάληψη",
"lnurl.limits.disabled": "απενεργοποιημένο",
"lnurl.limits.sats.one": "{n} sat",
"lnurl.limits.sats.other": "{n} sats",
"lnurl.details.title": "Στοιχεία mint",
"lnurl.details.address": "Διεύθυνση Lightning",
"lnurl.details.mintPubkey": "Δημόσιο κλειδί του mint",
"lnurl.details.onion": "Διεύθυνση Tor",
"lnurl.details.fee": "Προμήθεια κοπής",
"lnurl.details.node": "Κόμβος",
"lnurl.details.capacity": "Ανακοινωμένη χωρητικότητα",
"lnurl.kind.lightningAddress": "διεύθυνση Lightning",
"lnurl.kind.mintPubkey": "δημόσιο κλειδί του mint",
"lnurl.kind.onionAddress": "διεύθυνση Tor",
"lnurl.confirmed.label": "Τελευταία επιβεβαίωση",
"lnurl.confirmed.when": "επιβεβαιώθηκε {when}",
"lnurl.fee.base.one": "{n} sat",
"lnurl.fee.base.other": "{n} sats",
"lnurl.fee.percent": "{percent}%",
"home.topLnurl.title": "Κορυφαία mints LNURL",
"home.topLnurl.sub": "κατάταξη βάσει κριτικών και βαθμολογίας",
"home.topLnurl.all": "Και τα {n} mints LNURL →",
"feed.ecosystem.lnurl": "LNURL",
"reviews.dialog.bodyPlaceholder.lnurl": "Πώς πήγαν η κοπή, το λιώσιμο και η περιστροφή γραμματίων; Γίνετε συγκεκριμένοι.",
"reviews.byUrl.open": "Γράψε μια κριτική",
"reviews.byUrl.title": "Γράψε μια κριτική",
"reviews.byUrl.lede": "Επικόλλησε τη διεύθυνση. Αν ο ιστότοπος δεν την ξέρει ακόμα, την ελέγχει και την προσθέτει τώρα.",
"reviews.byUrl.label.cashu": "URL του mint",
"reviews.byUrl.label.fedimint": "Κωδικός πρόσκλησης της ομοσπονδίας",
"reviews.byUrl.label.lnurl": "URL του LNURL mint",
"reviews.byUrl.placeholder.cashu": "mint.example.com",
"reviews.byUrl.placeholder.fedimint": "fed11…",
"reviews.byUrl.placeholder.lnurl": "mint.example.com",
"reviews.byUrl.check": "Έλεγξε αυτή τη διεύθυνση",
"reviews.byUrl.checking": "Γίνεται έλεγχος…",
"reviews.byUrl.back": "Χρήση άλλης διεύθυνσης",
"reviews.byUrl.existing": "Ήδη καταχωρημένο",
"reviews.byUrl.indexed": "Μόλις προστέθηκε",
"reviews.byUrl.confirmClose": "Να κλείσει; Η διεύθυνση που έγραψες δεν κρατιέται.",
"reviews.byUrl.detected": "Αυτή η διεύθυνση απάντησε ως mint {kind}.",
"reviews.byUrl.switchTo": "Κάνε κριτική ως mint {kind}",
"reviews.byUrl.error.empty": "Γράψε πρώτα μια διεύθυνση.",
"reviews.byUrl.error.badUrl": "Αυτό δεν μοιάζει με URL mint.",
"reviews.byUrl.error.badInvite": "Αυτός δεν είναι κωδικός πρόσκλησης Fedimint. Οι κωδικοί ξεκινούν με fed11.",
"reviews.byUrl.error.badInput": "Ο ιστότοπος δεν μπορεί να ελέγξει αυτή τη διεύθυνση.",
"reviews.byUrl.error.blocked": "Δεν είναι δημόσια διεύθυνση, οπότε δεν μπορεί να ελεγχθεί.",
"reviews.byUrl.error.notAMint": "Αυτή η διεύθυνση απάντησε, αλλά όχι ως mint αυτού του είδους.",
"reviews.byUrl.error.wrongType": "Αυτή η διεύθυνση απάντησε, αλλά ως άλλο είδος mint.",
"reviews.byUrl.error.unverifiable": "Τίποτα δεν απάντησε σε αυτή τη διεύθυνση, και δεν υπάρχει ακόμα ίχνος της στο Nostr.",
"reviews.byUrl.error.rateLimited.one": "Πολλοί έλεγχοι από μία σύνδεση. Δοκίμασε ξανά σε περίπου {n} λεπτό.",
"reviews.byUrl.error.rateLimited.other": "Πολλοί έλεγχοι από μία σύνδεση. Δοκίμασε ξανά σε περίπου {n} λεπτά.",
"reviews.byUrl.error.network": "Δεν ήταν δυνατή η σύνδεση με το ευρετήριο. Έλεγξε τη σύνδεσή σου και δοκίμασε ξανά.",
"seo.lnurl.titleNoReviews": "{name}: πληροφορίες και κριτικές για το mint LNURL | Cashumints.space",
"seo.desc.statusWithdraw": "{status}, ανάληψη {min}–{max} sat.",
"mint.chip.noFunding": "Χωρίς κοπή / λιώσιμο",
"mint.warnings.lnurlWithdrawalsDisabled.lead": "Οι αναλήψεις είναι απενεργοποιημένες.",
"mint.warnings.lnurlWithdrawalsDisabled.body.online": "Αυτό το mint ανακοινώνει μέγιστη ανάληψη μηδέν, οπότε κανένα γραμμάτιο που εκδίδει δεν μπορεί να εξαργυρωθεί με τίποτα. Μην κόβετε εδώ μέχρι να αλλάξει αυτό.",
"mint.warnings.lnurlWithdrawalsDisabled.body.offline": "Την τελευταία φορά που έγινε προσβάσιμο, αυτό το mint ανακοίνωνε μέγιστη ανάληψη μηδέν, οπότε κανένα γραμμάτιο που εξέδιδε δεν μπορούσε να εξαργυρωθεί με τίποτα. Μην κόβετε εδώ μέχρι να αλλάξει αυτό.",
"mint.warnings.lnurlWithdrawalsDisabled.meta.online": "Οι αναλήψεις είναι προς το παρόν απενεργοποιημένες",
"mint.warnings.lnurlWithdrawalsDisabled.meta.offline": "Οι αναλήψεις ήταν απενεργοποιημένες την τελευταία φορά",
"mint.warnings.lnurlInvalid.lead": "Το endpoint απαντά αλλά είναι άκυρο.",
"mint.warnings.lnurlInvalid.body": "Ο διακομιστής απαντά, αλλά όχι με ανακοίνωση mint που μπορεί να διαβάσει αυτός ο ιστότοπος, οπότε οι αναλήψεις ίσως δεν λειτουργούν. Όσα ακολουθούν είναι η τελευταία κατάσταση που διαβάστηκε, και οι κριτικές εξακολουθούν να λειτουργούν.",
"mint.warnings.lnurlInvalid.meta": "Απαντά, αλλά όχι με έγκυρη ανακοίνωση mint",
"mint.warnings.lnurlNoFunding.lead": "Η κοπή και το λιώσιμο δεν είναι διαθέσιμα.",
"mint.warnings.lnurlNoFunding.body": "Τα υπάρχοντα γραμμάτια μπορούν ακόμη να περιστραφούν, να διαιρεθούν ή να συγχωνευθούν, αλλά αυτή τη στιγμή τίποτα δεν μπαίνει ή βγαίνει μέσω Lightning. Το ίδιο το mint λειτουργεί και απαντά· ο κόμβος Lightning πίσω του δεν είναι προσβάσιμος από αυτό.",
"mint.warnings.lnurlNoFunding.meta": "Κοπή και λιώσιμο μη διαθέσιμα",
"mint.warnings.also.noFunding": "Και ο κόμβος Lightning του ήταν απροσπέλαστος, οπότε τίποτα δεν μπορούσε να κοπεί ή να λιώσει."
} }
+122 -3
View File
@@ -50,6 +50,10 @@
"footer.privacy": "Privacy", "footer.privacy": "Privacy",
"footer.madeBy": "Made with ❤️ by {azzamo} · Powered by Cashu and NIP-87", "footer.madeBy": "Made with ❤️ by {azzamo} · Powered by Cashu and NIP-87",
"footer.warning": "Mints are custodial. A mint can disappear with your money.", "footer.warning": "Mints are custodial. A mint can disappear with your money.",
"footer.tagline": "Community reviews for ecash mints, signed on Nostr.",
"footer.col.explore": "Explore",
"footer.col.site": "Site",
"footer.nav": "Footer",
"home.title": "Cashumints.space · Find a Cashu mint you can trust", "home.title": "Cashumints.space · Find a Cashu mint you can trust",
"home.description": "Find a Cashu mint you can trust. {mints} mints indexed with {reviews} community reviews signed on Nostr, plus live uptime, supported NUTs and mint software.", "home.description": "Find a Cashu mint you can trust. {mints} mints indexed with {reviews} community reviews signed on Nostr, plus live uptime, supported NUTs and mint software.",
"home.hero.title": "Find a Cashu mint", "home.hero.title": "Find a Cashu mint",
@@ -114,6 +118,8 @@
"card.reviewed": "reviewed {when}", "card.reviewed": "reviewed {when}",
"card.noReviews": "no reviews yet", "card.noReviews": "no reviews yet",
"mint.title": "{name} · Cashu mint reviews", "mint.title": "{name} · Cashu mint reviews",
"mint.titleFedimint": "{name} · Fedimint federation reviews",
"mint.titleLnurl": "{name} · LNURL mint reviews",
"mint.breadcrumb": "Breadcrumb", "mint.breadcrumb": "Breadcrumb",
"mint.crumbMints": "Cashu mints", "mint.crumbMints": "Cashu mints",
"mint.copyUrl": "Copy mint URL", "mint.copyUrl": "Copy mint URL",
@@ -325,7 +331,7 @@
"reviews.validate.short": "Write at least {n} characters so the review is useful.", "reviews.validate.short": "Write at least {n} characters so the review is useful.",
"reviews.validate.long": "Reviews are limited to {n} characters.", "reviews.validate.long": "Reviews are limited to {n} characters.",
"feed.title": "Latest mint and federation reviews · Cashumints.space", "feed.title": "Latest mint and federation reviews · Cashumints.space",
"feed.description": "Every mint and federation review as it is published: {reviews} reviews across {mints} Cashu mints and {federations} Fedimint federations, signed on Nostr and readable here newest first. Filter by ecosystem, by rating, or by name.", "feed.description": "Every mint, federation and LNURL mint review as it is published: {reviews} reviews across {mints} Cashu mints, {federations} Fedimint federations and {lnurl} LNURL mints, signed on Nostr and readable here newest first. Filter by ecosystem, by rating, or by name.",
"feed.heading": "Latest reviews", "feed.heading": "Latest reviews",
"feed.lede": "Every review of every Cashu mint and Fedimint federation, newest first, read live from the Nostr relays. Each one is signed by its author and lives on the relays, not in our database.", "feed.lede": "Every review of every Cashu mint and Fedimint federation, newest first, read live from the Nostr relays. Each one is signed by its author and lives on the relays, not in our database.",
"feed.label": "Reviews", "feed.label": "Reviews",
@@ -400,7 +406,14 @@
"notfound.fb.reviews": "Reviews", "notfound.fb.reviews": "Reviews",
"notfound.fb.software": "Software", "notfound.fb.software": "Software",
"notfound.fb.lastOnline": "Last online", "notfound.fb.lastOnline": "Last online",
"notfound.fb.note": "This mint was indexed after the site was last built, so this is a summary from the live API. The full page with reviews appears at the next rebuild.", "notfound.fb.note": "This mint was indexed after the site was last built, so this is a summary from the live API, with its reviews below. The full page appears at the next rebuild.",
"notfound.resolve.checking": "First visit: checking this mint now.",
"notfound.resolve.failed": "This mint could not be added",
"notfound.resolve.failedFedimint": "This federation could not be added",
"notfound.resolve.retry": "Try another address",
"notfound.resolve.switch": "Open it on {kind}",
"notfound.resolve.fedimint": "A federation page address does not carry its invite code, so this one cannot be looked up from the link alone. Paste the invite code and it is added.",
"notfound.resolve.ambiguous": "This address cannot be worked out from the link alone. Paste the mint’s full URL and it is added.",
"wallets.title": "Cashu wallets · Cashumints.space", "wallets.title": "Cashu wallets · Cashumints.space",
"wallets.description": "Cashu wallets that work with the mints listed here, across mobile, web, browser and the command line, with what each one is good at.", "wallets.description": "Cashu wallets that work with the mints listed here, across mobile, web, browser and the command line, with what each one is good at.",
"wallets.heading": "Wallets", "wallets.heading": "Wallets",
@@ -654,5 +667,111 @@
"footer.allFedimints": "All federations", "footer.allFedimints": "All federations",
"reviews.dialog.bodyPlaceholder.cashu": "How did minting, melting and support work out? Be specific.", "reviews.dialog.bodyPlaceholder.cashu": "How did minting, melting and support work out? Be specific.",
"reviews.dialog.bodyPlaceholder.fedimint": "How did joining, Lightning payments and support work out? Be specific.", "reviews.dialog.bodyPlaceholder.fedimint": "How did joining, Lightning payments and support work out? Be specific.",
"fedimint.details.title": "Federation details" "fedimint.details.title": "Federation details",
"nav.lnurlMints": "LNURL mints",
"footer.allLnurlMints": "All LNURL mints",
"lnurlMints.title": "LNURL mints: lnurlcash bearer note mints, reviewed | Cashumints.space",
"lnurlMints.description": "All {total} LNURL mints indexed from the Nostr network: {online} online, {offline} offline. Community ratings, withdraw limits and capabilities for each.",
"lnurlMints.heading": "LNURL mints",
"lnurlMints.lede": "Every LNURL mint this site knows about. An LNURL mint issues lnurlcash bearer notes: Lightning notes you hold as a secret, which can be rotated, split, merged or melted back to a Lightning payment. Each one is checked over HTTPS every few minutes.",
"lnurlMints.search.label": "Search LNURL mints",
"lnurlMints.search.placeholder": "Search by name or address…",
"lnurlMints.sort.label": "Sort LNURL mints by",
"lnurlMints.hideOffline": "Hide offline",
"lnurlMints.showing": "Showing {total} LNURL mints ({online} online, {offline} offline)",
"lnurlMints.showingFiltered": "Showing {shown} of {total} LNURL mints ({online} online, {offline} offline)",
"lnurlMints.noResults": "No LNURL mint matches that.",
"lnurlMints.emptyIndex": "No LNURL mints are indexed yet.",
"lnurl.crumb": "LNURL mints",
"lnurl.breadcrumb": "Breadcrumb",
"lnurl.copyAddress": "Copy address",
"lnurl.more.title": "More LNURL mints",
"lnurl.more.all": "All LNURL mints →",
"lnurl.disclaimer": "An LNURL mint holds your sats while you hold its notes, so using one means trusting its operator. Ratings are community opinions, and online means reachable, not trustworthy.",
"lnurl.features.title": "Features",
"lnurl.features.none": "This mint has not announced what it supports.",
"lnurl.features.note": "Features are what the mint's operator announced on Nostr. Where this site can check one over HTTPS it does, and says so when something announced is unavailable right now.",
"lnurl.features.notListed": "not announced",
"lnurl.features.unavailable": "unavailable",
"lnurl.feature.mint": "Mint via Lightning (LUD-06)",
"lnurl.feature.melt": "Melt to Lightning",
"lnurl.feature.notes": "Rotate / split / merge notes",
"lnurl.feature.lud16": "Lightning address",
"lnurl.feature.lud21": "Payment verification (LUD-21)",
"lnurl.feature.signed-notes": "Signed notes (offline verification)",
"lnurl.feature.onion": "Tor address",
"lnurl.feature.rotate": "Rotate a note",
"lnurl.feature.split": "Split a note",
"lnurl.feature.merge": "Merge notes",
"lnurl.feature.lud06": "LUD-06 payRequest",
"lnurl.feature.lud03": "LUD-03 withdrawRequest",
"lnurl.limits.label": "Withdraw limits",
"lnurl.limits.notPublished": "not published",
"lnurl.limits.withdrawRow": "Withdraw",
"lnurl.limits.disabled": "disabled",
"lnurl.limits.sats.one": "{n} sat",
"lnurl.limits.sats.other": "{n} sats",
"lnurl.details.title": "Details",
"lnurl.details.address": "Lightning address",
"lnurl.details.mintPubkey": "Mint pubkey",
"lnurl.details.onion": "Tor address",
"lnurl.details.fee": "Mint fee",
"lnurl.details.node": "Node",
"lnurl.details.capacity": "Announced capacity",
"lnurl.kind.lightningAddress": "lightning address",
"lnurl.kind.mintPubkey": "mint pubkey",
"lnurl.kind.onionAddress": "Tor address",
"lnurl.confirmed.label": "Last confirmed",
"lnurl.confirmed.when": "confirmed {when}",
"lnurl.fee.base.one": "{n} sat",
"lnurl.fee.base.other": "{n} sats",
"lnurl.fee.percent": "{percent}%",
"home.topLnurl.title": "Top LNURL mints",
"home.topLnurl.sub": "ranked by reviews and rating",
"home.topLnurl.all": "All {n} LNURL mints →",
"feed.ecosystem.lnurl": "LNURL",
"reviews.dialog.bodyPlaceholder.lnurl": "How did minting, melting and rotating notes work out? Be specific.",
"reviews.byUrl.open": "Write a review",
"reviews.byUrl.title": "Write a review",
"reviews.byUrl.lede": "Paste the address. If this site does not know it yet, it is checked and added now.",
"reviews.byUrl.label.cashu": "Mint URL",
"reviews.byUrl.label.fedimint": "Federation invite code",
"reviews.byUrl.label.lnurl": "LNURL mint URL",
"reviews.byUrl.placeholder.cashu": "mint.example.com",
"reviews.byUrl.placeholder.fedimint": "fed11…",
"reviews.byUrl.placeholder.lnurl": "mint.example.com",
"reviews.byUrl.check": "Check this address",
"reviews.byUrl.checking": "Checking…",
"reviews.byUrl.back": "Use a different address",
"reviews.byUrl.existing": "Already listed",
"reviews.byUrl.indexed": "Just added",
"reviews.byUrl.confirmClose": "Close this? The address you typed is not kept.",
"reviews.byUrl.detected": "That address answered as a mint on {kind}.",
"reviews.byUrl.switchTo": "Review it on {kind}",
"reviews.byUrl.error.empty": "Enter an address first.",
"reviews.byUrl.error.badUrl": "That does not look like a mint URL.",
"reviews.byUrl.error.badInvite": "That is not a Fedimint invite code. Codes start with fed11.",
"reviews.byUrl.error.badInput": "This site cannot check that address.",
"reviews.byUrl.error.blocked": "That is not a public address, so it cannot be checked.",
"reviews.byUrl.error.notAMint": "That address answered, but not as a mint of this kind.",
"reviews.byUrl.error.wrongType": "That address answered, but as a different kind of mint.",
"reviews.byUrl.error.unverifiable": "Nothing responded at that address, and no trace of it exists on Nostr yet.",
"reviews.byUrl.error.rateLimited.one": "That is a lot of checks from one connection. Try again in about {n} minute.",
"reviews.byUrl.error.rateLimited.other": "That is a lot of checks from one connection. Try again in about {n} minutes.",
"reviews.byUrl.error.network": "The index could not be reached. Check your connection, then try again.",
"seo.lnurl.titleNoReviews": "{name}: LNURL mint info and reviews | Cashumints.space",
"seo.desc.statusWithdraw": "{status}, withdraw {min}–{max} sat.",
"mint.chip.noFunding": "No mint / melt",
"mint.warnings.lnurlWithdrawalsDisabled.lead": "Withdrawals disabled.",
"mint.warnings.lnurlWithdrawalsDisabled.body.online": "This mint advertises a maximum withdrawal of zero, so no note it issues can be redeemed for anything. Do not mint here until that changes.",
"mint.warnings.lnurlWithdrawalsDisabled.body.offline": "This mint advertised a maximum withdrawal of zero when it was last reached, so no note it issued could be redeemed for anything. Do not mint here until that changes.",
"mint.warnings.lnurlWithdrawalsDisabled.meta.online": "Withdrawals currently disabled",
"mint.warnings.lnurlWithdrawalsDisabled.meta.offline": "Withdrawals were disabled when last seen",
"mint.warnings.lnurlInvalid.lead": "Endpoint responding but invalid.",
"mint.warnings.lnurlInvalid.body": "The host answers, but not with a mint advertisement this site can read, so withdrawals may not work. Anything below is the last state that did parse, and reviews still work.",
"mint.warnings.lnurlInvalid.meta": "Responding, but not with a valid mint advertisement",
"mint.warnings.lnurlNoFunding.lead": "Minting and melting unavailable.",
"mint.warnings.lnurlNoFunding.body": "Existing notes can still be rotated, split, or merged, but nothing moves in or out via Lightning right now. The mint itself is up and answering; the Lightning node behind it is not reachable from it.",
"mint.warnings.lnurlNoFunding.meta": "Minting and melting unavailable",
"mint.warnings.also.noFunding": "Its Lightning node was also unreachable, so nothing could be minted or melted."
} }
+122 -3
View File
@@ -54,6 +54,10 @@
"footer.privacy": "Privacidad", "footer.privacy": "Privacidad",
"footer.madeBy": "Hecho con ❤️ por {azzamo} · Funciona con Cashu y NIP-87", "footer.madeBy": "Hecho con ❤️ por {azzamo} · Funciona con Cashu y NIP-87",
"footer.warning": "Los mints son custodiales. Un mint puede desaparecer con tu dinero.", "footer.warning": "Los mints son custodiales. Un mint puede desaparecer con tu dinero.",
"footer.tagline": "Reseñas de la comunidad para mints de ecash, firmadas en Nostr.",
"footer.col.explore": "Explorar",
"footer.col.site": "Sitio",
"footer.nav": "Pie de página",
"home.title": "Cashumints.space · Encuentra un mint Cashu en el que confiar", "home.title": "Cashumints.space · Encuentra un mint Cashu en el que confiar",
"home.description": "Encuentra un mint Cashu en el que confiar. {mints} mints indexados con {reviews} reseñas de la comunidad firmadas en Nostr, además de disponibilidad en directo, NUTs compatibles y software del mint.", "home.description": "Encuentra un mint Cashu en el que confiar. {mints} mints indexados con {reviews} reseñas de la comunidad firmadas en Nostr, además de disponibilidad en directo, NUTs compatibles y software del mint.",
"home.hero.title": "Encuentra un mint Cashu", "home.hero.title": "Encuentra un mint Cashu",
@@ -118,6 +122,8 @@
"card.reviewed": "reseñado {when}", "card.reviewed": "reseñado {when}",
"card.noReviews": "sin reseñas aún", "card.noReviews": "sin reseñas aún",
"mint.title": "{name} · Reseñas del mint Cashu", "mint.title": "{name} · Reseñas del mint Cashu",
"mint.titleFedimint": "{name} · Reseñas de la federación Fedimint",
"mint.titleLnurl": "{name} · Reseñas del mint LNURL",
"mint.breadcrumb": "Ruta de navegación", "mint.breadcrumb": "Ruta de navegación",
"mint.crumbMints": "Mints Cashu", "mint.crumbMints": "Mints Cashu",
"mint.copyUrl": "Copiar la URL del mint", "mint.copyUrl": "Copiar la URL del mint",
@@ -329,7 +335,7 @@
"reviews.validate.short": "Escribe al menos {n} caracteres para que la reseña sirva de algo.", "reviews.validate.short": "Escribe al menos {n} caracteres para que la reseña sirva de algo.",
"reviews.validate.long": "Las reseñas están limitadas a {n} caracteres.", "reviews.validate.long": "Las reseñas están limitadas a {n} caracteres.",
"feed.title": "Últimas reseñas de mints y federaciones · Cashumints.space", "feed.title": "Últimas reseñas de mints y federaciones · Cashumints.space",
"feed.description": "Cada reseña de un mint o una federación según se publica: {reviews} reseñas de {mints} mints Cashu y {federations} federaciones Fedimint, firmadas en Nostr y legibles aquí de la más nueva a la más antigua. Filtra por ecosistema, por valoración o por nombre.", "feed.description": "Cada reseña de mint, federación y mint LNURL según se publica: {reviews} reseñas sobre {mints} mints Cashu, {federations} federaciones Fedimint y {lnurl} mints LNURL, firmadas en Nostr y legibles aquí de más nueva a más antigua. Filtra por ecosistema, por valoración o por nombre.",
"feed.heading": "Últimas reseñas", "feed.heading": "Últimas reseñas",
"feed.lede": "Todas las reseñas de todos los mints Cashu y federaciones Fedimint, de la más nueva a la más antigua, leídas en directo de los relays de Nostr. Cada una está firmada por quien la escribió y vive en los relays, no en nuestra base de datos.", "feed.lede": "Todas las reseñas de todos los mints Cashu y federaciones Fedimint, de la más nueva a la más antigua, leídas en directo de los relays de Nostr. Cada una está firmada por quien la escribió y vive en los relays, no en nuestra base de datos.",
"feed.label": "Reseñas", "feed.label": "Reseñas",
@@ -404,7 +410,14 @@
"notfound.fb.reviews": "Reseñas", "notfound.fb.reviews": "Reseñas",
"notfound.fb.software": "Software", "notfound.fb.software": "Software",
"notfound.fb.lastOnline": "Última vez en línea", "notfound.fb.lastOnline": "Última vez en línea",
"notfound.fb.note": "Este mint se indexó después de la última compilación del sitio, así que esto es un resumen sacado de la API en directo. La página completa con reseñas aparecerá en la próxima compilación.", "notfound.fb.note": "Este mint se indexó después de la última compilación del sitio, así que este es un resumen de la API en vivo, con sus reseñas debajo. La página completa aparece en la siguiente compilación.",
"notfound.resolve.checking": "Primera visita: comprobando este mint ahora.",
"notfound.resolve.failed": "No se pudo añadir este mint",
"notfound.resolve.failedFedimint": "No se pudo añadir esta federación",
"notfound.resolve.retry": "Probar otra dirección",
"notfound.resolve.switch": "Abrirlo como mint {kind}",
"notfound.resolve.fedimint": "La dirección de la página de una federación no lleva su código de invitación, así que esta no se puede buscar solo con el enlace. Pega el código de invitación y se añade.",
"notfound.resolve.ambiguous": "Esta dirección no se puede deducir solo con el enlace. Pega la URL completa del mint y se añade.",
"wallets.title": "Monederos Cashu · Cashumints.space", "wallets.title": "Monederos Cashu · Cashumints.space",
"wallets.description": "Monederos Cashu que funcionan con los mints listados aquí, para móvil, web, navegador y línea de comandos, con lo que hace bien cada uno.", "wallets.description": "Monederos Cashu que funcionan con los mints listados aquí, para móvil, web, navegador y línea de comandos, con lo que hace bien cada uno.",
"wallets.heading": "Monederos", "wallets.heading": "Monederos",
@@ -658,5 +671,111 @@
"footer.allFedimints": "Todas las federaciones", "footer.allFedimints": "Todas las federaciones",
"reviews.dialog.bodyPlaceholder.cashu": "¿Qué tal fueron la acuñación, el fundido y el soporte? Sé concreto.", "reviews.dialog.bodyPlaceholder.cashu": "¿Qué tal fueron la acuñación, el fundido y el soporte? Sé concreto.",
"reviews.dialog.bodyPlaceholder.fedimint": "¿Qué tal fueron unirte, los pagos Lightning y el soporte? Sé concreto.", "reviews.dialog.bodyPlaceholder.fedimint": "¿Qué tal fueron unirte, los pagos Lightning y el soporte? Sé concreto.",
"fedimint.details.title": "Datos de la federación" "fedimint.details.title": "Datos de la federación",
"nav.lnurlMints": "Mints LNURL",
"footer.allLnurlMints": "Todos los mints LNURL",
"lnurlMints.title": "Mints LNURL: mints de notas al portador lnurlcash, con reseñas | Cashumints.space",
"lnurlMints.description": "Los {total} mints LNURL indexados desde la red Nostr: {online} en línea, {offline} fuera de línea. Valoraciones de la comunidad, límites de retirada y capacidades de cada uno.",
"lnurlMints.heading": "Mints LNURL",
"lnurlMints.lede": "Todos los mints LNURL que este sitio conoce. Un mint LNURL emite notas al portador lnurlcash: notas Lightning que guardas como un secreto y que se pueden rotar, dividir, fusionar o fundir de vuelta a un pago Lightning. Cada uno se comprueba por HTTPS cada pocos minutos.",
"lnurlMints.search.label": "Buscar mints LNURL",
"lnurlMints.search.placeholder": "Buscar por nombre o dirección…",
"lnurlMints.sort.label": "Ordenar mints LNURL por",
"lnurlMints.hideOffline": "Ocultar fuera de línea",
"lnurlMints.showing": "Mostrando {total} mints LNURL ({online} en línea, {offline} fuera de línea)",
"lnurlMints.showingFiltered": "Mostrando {shown} de {total} mints LNURL ({online} en línea, {offline} fuera de línea)",
"lnurlMints.noResults": "Ningún mint LNURL coincide con eso.",
"lnurlMints.emptyIndex": "Todavía no hay mints LNURL indexados.",
"lnurl.crumb": "Mints LNURL",
"lnurl.breadcrumb": "Ruta de navegación",
"lnurl.copyAddress": "Copiar dirección",
"lnurl.more.title": "Más mints LNURL",
"lnurl.more.all": "Todos los mints LNURL →",
"lnurl.disclaimer": "Un mint LNURL guarda tus sats mientras tú guardas sus notas, así que usar uno significa confiar en su operador. Las valoraciones son opiniones de la comunidad, y en línea significa accesible, no confiable.",
"lnurl.features.title": "Funciones",
"lnurl.features.none": "Este mint no ha anunciado qué admite.",
"lnurl.features.note": "Las funciones son lo que el operador del mint anunció en Nostr. Cuando este sitio puede comprobar alguna por HTTPS lo hace, y avisa cuando algo anunciado no está disponible ahora mismo.",
"lnurl.features.notListed": "no anunciado",
"lnurl.features.unavailable": "no disponible",
"lnurl.feature.mint": "Acuñar por Lightning (LUD-06)",
"lnurl.feature.melt": "Fundir a Lightning",
"lnurl.feature.notes": "Rotar / dividir / fusionar notas",
"lnurl.feature.lud16": "Dirección Lightning",
"lnurl.feature.lud21": "Verificación de pago (LUD-21)",
"lnurl.feature.signed-notes": "Notas firmadas (verificación sin conexión)",
"lnurl.feature.onion": "Dirección Tor",
"lnurl.feature.rotate": "Rotar una nota",
"lnurl.feature.split": "Dividir una nota",
"lnurl.feature.merge": "Fusionar notas",
"lnurl.feature.lud06": "payRequest LUD-06",
"lnurl.feature.lud03": "withdrawRequest LUD-03",
"lnurl.limits.label": "Límites de retirada",
"lnurl.limits.notPublished": "no publicado",
"lnurl.limits.withdrawRow": "Retirada",
"lnurl.limits.disabled": "desactivado",
"lnurl.limits.sats.one": "{n} sat",
"lnurl.limits.sats.other": "{n} sats",
"lnurl.details.title": "Datos del mint",
"lnurl.details.address": "Dirección Lightning",
"lnurl.details.mintPubkey": "Clave pública del mint",
"lnurl.details.onion": "Dirección Tor",
"lnurl.details.fee": "Comisión de acuñación",
"lnurl.details.node": "Nodo",
"lnurl.details.capacity": "Capacidad anunciada",
"lnurl.kind.lightningAddress": "dirección Lightning",
"lnurl.kind.mintPubkey": "clave pública del mint",
"lnurl.kind.onionAddress": "dirección Tor",
"lnurl.confirmed.label": "Última confirmación",
"lnurl.confirmed.when": "confirmado {when}",
"lnurl.fee.base.one": "{n} sat",
"lnurl.fee.base.other": "{n} sats",
"lnurl.fee.percent": "{percent}%",
"home.topLnurl.title": "Mejores mints LNURL",
"home.topLnurl.sub": "ordenados por reseñas y valoración",
"home.topLnurl.all": "Los {n} mints LNURL →",
"feed.ecosystem.lnurl": "LNURL",
"reviews.dialog.bodyPlaceholder.lnurl": "¿Qué tal fueron acuñar, fundir y rotar notas? Sé concreto.",
"reviews.byUrl.open": "Escribir una reseña",
"reviews.byUrl.title": "Escribir una reseña",
"reviews.byUrl.lede": "Pega la dirección. Si este sitio aún no la conoce, la comprueba y la añade ahora.",
"reviews.byUrl.label.cashu": "URL del mint",
"reviews.byUrl.label.fedimint": "Código de invitación de la federación",
"reviews.byUrl.label.lnurl": "URL del mint LNURL",
"reviews.byUrl.placeholder.cashu": "mint.example.com",
"reviews.byUrl.placeholder.fedimint": "fed11…",
"reviews.byUrl.placeholder.lnurl": "mint.example.com",
"reviews.byUrl.check": "Comprobar esta dirección",
"reviews.byUrl.checking": "Comprobando…",
"reviews.byUrl.back": "Usar otra dirección",
"reviews.byUrl.existing": "Ya está en la lista",
"reviews.byUrl.indexed": "Recién añadido",
"reviews.byUrl.confirmClose": "¿Cerrar? La dirección que escribiste no se guarda.",
"reviews.byUrl.detected": "Esa dirección respondió como un mint {kind}.",
"reviews.byUrl.switchTo": "Reseñarlo como mint {kind}",
"reviews.byUrl.error.empty": "Escribe una dirección primero.",
"reviews.byUrl.error.badUrl": "Eso no parece la URL de un mint.",
"reviews.byUrl.error.badInvite": "Eso no es un código de invitación de Fedimint. Los códigos empiezan por fed11.",
"reviews.byUrl.error.badInput": "Este sitio no puede comprobar esa dirección.",
"reviews.byUrl.error.blocked": "Esa no es una dirección pública, así que no se puede comprobar.",
"reviews.byUrl.error.notAMint": "Esa dirección respondió, pero no como un mint de este tipo.",
"reviews.byUrl.error.wrongType": "Esa dirección respondió, pero como un mint de otro tipo.",
"reviews.byUrl.error.unverifiable": "Nada respondió en esa dirección, y todavía no hay ni rastro de ella en Nostr.",
"reviews.byUrl.error.rateLimited.one": "Son muchas comprobaciones desde una misma conexión. Inténtalo otra vez en unos {n} minuto.",
"reviews.byUrl.error.rateLimited.other": "Son muchas comprobaciones desde una misma conexión. Inténtalo otra vez en unos {n} minutos.",
"reviews.byUrl.error.network": "No se pudo contactar con el índice. Comprueba tu conexión y vuelve a intentarlo.",
"seo.lnurl.titleNoReviews": "{name}: información y reseñas del mint LNURL | Cashumints.space",
"seo.desc.statusWithdraw": "{status}, retirada {min}–{max} sat.",
"mint.chip.noFunding": "Sin acuñar / fundir",
"mint.warnings.lnurlWithdrawalsDisabled.lead": "Retiradas desactivadas.",
"mint.warnings.lnurlWithdrawalsDisabled.body.online": "Este mint anuncia una retirada máxima de cero, así que ninguna nota que emita puede canjearse por nada. No acuñes aquí hasta que eso cambie.",
"mint.warnings.lnurlWithdrawalsDisabled.body.offline": "Este mint anunciaba una retirada máxima de cero la última vez que se pudo contactar, así que ninguna nota que emitiera podía canjearse por nada. No acuñes aquí hasta que eso cambie.",
"mint.warnings.lnurlWithdrawalsDisabled.meta.online": "Retiradas desactivadas actualmente",
"mint.warnings.lnurlWithdrawalsDisabled.meta.offline": "Las retiradas estaban desactivadas la última vez que se vio",
"mint.warnings.lnurlInvalid.lead": "El endpoint responde pero no es válido.",
"mint.warnings.lnurlInvalid.body": "El servidor responde, pero no con un anuncio de mint que este sitio pueda leer, así que las retiradas podrían no funcionar. Lo de abajo es el último estado que sí se pudo leer, y las reseñas siguen funcionando.",
"mint.warnings.lnurlInvalid.meta": "Responde, pero no con un anuncio de mint válido",
"mint.warnings.lnurlNoFunding.lead": "Acuñación y fundido no disponibles.",
"mint.warnings.lnurlNoFunding.body": "Las notas existentes todavía se pueden rotar, dividir o fusionar, pero ahora mismo nada entra ni sale por Lightning. El mint está activo y responde; el nodo Lightning que hay detrás no es accesible desde él.",
"mint.warnings.lnurlNoFunding.meta": "Acuñación y fundido no disponibles",
"mint.warnings.also.noFunding": "Su nodo Lightning también estaba inaccesible, así que no se podía acuñar ni fundir nada."
} }
+118 -3
View File
@@ -114,6 +114,8 @@
"card.reviewed": "a évalué {when}", "card.reviewed": "a évalué {when}",
"card.noReviews": "pas encore d'avis", "card.noReviews": "pas encore d'avis",
"mint.title": "{name} · Cashu mint avis", "mint.title": "{name} · Cashu mint avis",
"mint.titleFedimint": "{name} · Avis sur la fédération Fedimint",
"mint.titleLnurl": "{name} · Avis sur le mint LNURL",
"mint.breadcrumb": "Fil d'Ariane", "mint.breadcrumb": "Fil d'Ariane",
"mint.crumbMints": "Cashu mints", "mint.crumbMints": "Cashu mints",
"mint.copyUrl": "Copier l'URL mint", "mint.copyUrl": "Copier l'URL mint",
@@ -325,7 +327,7 @@
"reviews.validate.short": "Écrivez au moins les caractères {n} pour que l'examen soit utile.", "reviews.validate.short": "Écrivez au moins les caractères {n} pour que l'examen soit utile.",
"reviews.validate.long": "Les avis sont limités aux personnages {n}.", "reviews.validate.long": "Les avis sont limités aux personnages {n}.",
"feed.title": "Derniers avis sur le mint et la fédération · Cashumints.space", "feed.title": "Derniers avis sur le mint et la fédération · Cashumints.space",
"feed.description": "Chaque revue de mint et de fédération telle qu'elle est publiée : critiques de {reviews} dans les fédérations {mints} Cashu mints et {federations} Fedimint, signées sur Nostr et lisibles ici les plus récentes en premier. Filtrez par écosystème, par note ou par nom.", "feed.description": "Chaque avis sur un mint, une fédération et un mint LNURL dès sa publication : {reviews} avis portant sur {mints} mints Cashu, {federations} fédérations Fedimint et {lnurl} mints LNURL, signés sur Nostr et lisibles ici du plus récent au plus ancien. Filtrez par écosystème, par note ou par nom.",
"feed.heading": "Derniers avis", "feed.heading": "Derniers avis",
"feed.lede": "Chaque revue de chaque fédération Cashu mint et Fedimint, la plus récente en premier, lue en direct depuis les relais Nostr. Chacun est signé par son auteur et vit sur les relais, pas dans notre base de données.", "feed.lede": "Chaque revue de chaque fédération Cashu mint et Fedimint, la plus récente en premier, lue en direct depuis les relais Nostr. Chacun est signé par son auteur et vit sur les relais, pas dans notre base de données.",
"feed.label": "Avis", "feed.label": "Avis",
@@ -400,7 +402,14 @@
"notfound.fb.reviews": "Avis", "notfound.fb.reviews": "Avis",
"notfound.fb.software": "Logiciel", "notfound.fb.software": "Logiciel",
"notfound.fb.lastOnline": "Dernière connexion", "notfound.fb.lastOnline": "Dernière connexion",
"notfound.fb.note": "Ce mint a été indexé après la dernière création du site, il s'agit donc d'un résumé de l'API en direct. La page complète avec les critiques apparaîtra lors de la prochaine reconstruction.", "notfound.fb.note": "Ce mint a été indexé après la dernière construction du site : voici un résumé issu de l’API en direct, avec ses avis en dessous. La page complète apparaît à la prochaine construction.",
"notfound.resolve.checking": "Première visite : ce mint est en cours de vérification.",
"notfound.resolve.failed": "Ce mint n’a pas pu être ajouté",
"notfound.resolve.failedFedimint": "Cette fédération n’a pas pu être ajoutée",
"notfound.resolve.retry": "Essayer une autre adresse",
"notfound.resolve.switch": "L’ouvrir comme mint {kind}",
"notfound.resolve.fedimint": "L’adresse de la page d’une fédération ne contient pas son code d’invitation, donc celle-ci ne peut pas être retrouvée à partir du lien seul. Colle le code d’invitation et elle est ajoutée.",
"notfound.resolve.ambiguous": "Cette adresse ne peut pas être déduite du lien seul. Colle l’URL complète du mint et il est ajouté.",
"wallets.title": "Portefeuilles Cashu · Cashumints.space", "wallets.title": "Portefeuilles Cashu · Cashumints.space",
"wallets.description": "Portefeuilles Cashu qui fonctionnent avec le mints répertorié ici, sur mobile, Web, navigateur et ligne de commande, avec ce pour quoi chacun est bon.", "wallets.description": "Portefeuilles Cashu qui fonctionnent avec le mints répertorié ici, sur mobile, Web, navigateur et ligne de commande, avec ce pour quoi chacun est bon.",
"wallets.heading": "Portefeuilles", "wallets.heading": "Portefeuilles",
@@ -654,5 +663,111 @@
"footer.allFedimints": "Toutes les fédérations", "footer.allFedimints": "Toutes les fédérations",
"reviews.dialog.bodyPlaceholder.cashu": "Comment se sont déroulés les minting, melting et le support ? Soyez précis.", "reviews.dialog.bodyPlaceholder.cashu": "Comment se sont déroulés les minting, melting et le support ? Soyez précis.",
"reviews.dialog.bodyPlaceholder.fedimint": "Comment s'est déroulée l'adhésion, les paiements Lightning et l'assistance ? Soyez précis.", "reviews.dialog.bodyPlaceholder.fedimint": "Comment s'est déroulée l'adhésion, les paiements Lightning et l'assistance ? Soyez précis.",
"fedimint.details.title": "Détails de la fédération" "fedimint.details.title": "Détails de la fédération",
"nav.lnurlMints": "Mints LNURL",
"footer.allLnurlMints": "Tous les mints LNURL",
"lnurlMints.title": "Mints LNURL : mints de billets au porteur lnurlcash, avec avis | Cashumints.space",
"lnurlMints.description": "Les {total} mints LNURL indexés depuis le réseau Nostr : {online} en ligne, {offline} hors ligne. Notes de la communauté, limites de retrait et fonctionnalités de chacun.",
"lnurlMints.heading": "Mints LNURL",
"lnurlMints.lede": "Tous les mints LNURL que ce site connaît. Un mint LNURL émet des billets au porteur lnurlcash : des billets Lightning que vous conservez comme un secret et qui peuvent être tournés, divisés, fusionnés ou refondus en un paiement Lightning. Chacun est vérifié en HTTPS toutes les quelques minutes.",
"lnurlMints.search.label": "Rechercher des mints LNURL",
"lnurlMints.search.placeholder": "Rechercher par nom ou adresse…",
"lnurlMints.sort.label": "Trier les mints LNURL par",
"lnurlMints.hideOffline": "Masquer hors ligne",
"lnurlMints.showing": "{total} mints LNURL affichés ({online} en ligne, {offline} hors ligne)",
"lnurlMints.showingFiltered": "{shown} sur {total} mints LNURL affichés ({online} en ligne, {offline} hors ligne)",
"lnurlMints.noResults": "Aucun mint LNURL ne correspond.",
"lnurlMints.emptyIndex": "Aucun mint LNURL n'est encore indexé.",
"lnurl.crumb": "Mints LNURL",
"lnurl.breadcrumb": "Fil d'Ariane",
"lnurl.copyAddress": "Copier l'adresse",
"lnurl.more.title": "Plus de mints LNURL",
"lnurl.more.all": "Tous les mints LNURL →",
"lnurl.disclaimer": "Un mint LNURL détient vos sats pendant que vous détenez ses billets : l'utiliser revient donc à faire confiance à son opérateur. Les notes sont des avis de la communauté, et en ligne signifie joignable, pas digne de confiance.",
"lnurl.features.title": "Fonctionnalités",
"lnurl.features.none": "Ce mint n'a pas annoncé ce qu'il prend en charge.",
"lnurl.features.note": "Les fonctionnalités sont ce que l'opérateur du mint a annoncé sur Nostr. Quand ce site peut en vérifier une en HTTPS, il le fait, et il le signale lorsqu'une fonctionnalité annoncée est indisponible en ce moment.",
"lnurl.features.notListed": "non annoncé",
"lnurl.features.unavailable": "indisponible",
"lnurl.feature.mint": "Minting via Lightning (LUD-06)",
"lnurl.feature.melt": "Melting vers Lightning",
"lnurl.feature.notes": "Tourner / diviser / fusionner des billets",
"lnurl.feature.lud16": "Adresse Lightning",
"lnurl.feature.lud21": "Vérification de paiement (LUD-21)",
"lnurl.feature.signed-notes": "Billets signés (vérification hors ligne)",
"lnurl.feature.onion": "Adresse Tor",
"lnurl.feature.rotate": "Tourner un billet",
"lnurl.feature.split": "Diviser un billet",
"lnurl.feature.merge": "Fusionner des billets",
"lnurl.feature.lud06": "payRequest LUD-06",
"lnurl.feature.lud03": "withdrawRequest LUD-03",
"lnurl.limits.label": "Limites de retrait",
"lnurl.limits.notPublished": "non publié",
"lnurl.limits.withdrawRow": "Retrait",
"lnurl.limits.disabled": "désactivé",
"lnurl.limits.sats.one": "{n} sat",
"lnurl.limits.sats.other": "{n} sats",
"lnurl.details.title": "Détails du mint",
"lnurl.details.address": "Adresse Lightning",
"lnurl.details.mintPubkey": "Clé publique du mint",
"lnurl.details.onion": "Adresse Tor",
"lnurl.details.fee": "Frais de minting",
"lnurl.details.node": "Nœud",
"lnurl.details.capacity": "Capacité annoncée",
"lnurl.kind.lightningAddress": "adresse Lightning",
"lnurl.kind.mintPubkey": "clé publique du mint",
"lnurl.kind.onionAddress": "adresse Tor",
"lnurl.confirmed.label": "Dernière confirmation",
"lnurl.confirmed.when": "confirmé {when}",
"lnurl.fee.base.one": "{n} sat",
"lnurl.fee.base.other": "{n} sats",
"lnurl.fee.percent": "{percent} %",
"home.topLnurl.title": "Meilleurs mints LNURL",
"home.topLnurl.sub": "classés par avis et note",
"home.topLnurl.all": "Les {n} mints LNURL →",
"feed.ecosystem.lnurl": "LNURL",
"reviews.dialog.bodyPlaceholder.lnurl": "Comment se sont passés le minting, le melting et la rotation des billets ? Soyez précis.",
"reviews.byUrl.open": "Écrire un avis",
"reviews.byUrl.title": "Écrire un avis",
"reviews.byUrl.lede": "Colle l’adresse. Si ce site ne la connaît pas encore, elle est vérifiée et ajoutée maintenant.",
"reviews.byUrl.label.cashu": "URL du mint",
"reviews.byUrl.label.fedimint": "Code d’invitation de la fédération",
"reviews.byUrl.label.lnurl": "URL du mint LNURL",
"reviews.byUrl.placeholder.cashu": "mint.example.com",
"reviews.byUrl.placeholder.fedimint": "fed11…",
"reviews.byUrl.placeholder.lnurl": "mint.example.com",
"reviews.byUrl.check": "Vérifier cette adresse",
"reviews.byUrl.checking": "Vérification…",
"reviews.byUrl.back": "Utiliser une autre adresse",
"reviews.byUrl.existing": "Déjà répertorié",
"reviews.byUrl.indexed": "Ajouté à l’instant",
"reviews.byUrl.confirmClose": "Fermer ? L’adresse saisie n’est pas conservée.",
"reviews.byUrl.detected": "Cette adresse a répondu comme un mint {kind}.",
"reviews.byUrl.switchTo": "L’évaluer comme mint {kind}",
"reviews.byUrl.error.empty": "Saisis d’abord une adresse.",
"reviews.byUrl.error.badUrl": "Cela ne ressemble pas à une URL de mint.",
"reviews.byUrl.error.badInvite": "Ce n’est pas un code d’invitation Fedimint. Les codes commencent par fed11.",
"reviews.byUrl.error.badInput": "Ce site ne peut pas vérifier cette adresse.",
"reviews.byUrl.error.blocked": "Ce n’est pas une adresse publique, elle ne peut donc pas être vérifiée.",
"reviews.byUrl.error.notAMint": "Cette adresse a répondu, mais pas comme un mint de ce type.",
"reviews.byUrl.error.wrongType": "Cette adresse a répondu, mais comme un autre type de mint.",
"reviews.byUrl.error.unverifiable": "Rien n’a répondu à cette adresse, et il n’en existe encore aucune trace sur Nostr.",
"reviews.byUrl.error.rateLimited.one": "Cela fait beaucoup de vérifications depuis une seule connexion. Réessaie dans environ {n} minute.",
"reviews.byUrl.error.rateLimited.other": "Cela fait beaucoup de vérifications depuis une seule connexion. Réessaie dans environ {n} minutes.",
"reviews.byUrl.error.network": "L’index est injoignable. Vérifie ta connexion, puis réessaie.",
"seo.lnurl.titleNoReviews": "{name} : infos et avis sur le mint LNURL | Cashumints.space",
"seo.desc.statusWithdraw": "{status}, retrait {min}–{max} sat.",
"mint.chip.noFunding": "Ni minting ni melting",
"mint.warnings.lnurlWithdrawalsDisabled.lead": "Retraits désactivés.",
"mint.warnings.lnurlWithdrawalsDisabled.body.online": "Ce mint annonce un retrait maximum de zéro : aucun billet qu'il émet ne peut donc être échangé contre quoi que ce soit. N'y faites pas de minting tant que cela n'a pas changé.",
"mint.warnings.lnurlWithdrawalsDisabled.body.offline": "Ce mint annonçait un retrait maximum de zéro lors du dernier contact : aucun billet qu'il émettait ne pouvait donc être échangé contre quoi que ce soit. N'y faites pas de minting tant que cela n'a pas changé.",
"mint.warnings.lnurlWithdrawalsDisabled.meta.online": "Retraits actuellement désactivés",
"mint.warnings.lnurlWithdrawalsDisabled.meta.offline": "Les retraits étaient désactivés lors du dernier contact",
"mint.warnings.lnurlInvalid.lead": "Le point de terminaison répond mais est invalide.",
"mint.warnings.lnurlInvalid.body": "L'hôte répond, mais pas avec une annonce de mint que ce site sait lire : les retraits risquent donc de ne pas fonctionner. Tout ce qui suit correspond au dernier état lisible, et les avis fonctionnent toujours.",
"mint.warnings.lnurlInvalid.meta": "Répond, mais pas avec une annonce de mint valide",
"mint.warnings.lnurlNoFunding.lead": "Minting et melting indisponibles.",
"mint.warnings.lnurlNoFunding.body": "Les billets existants peuvent toujours être tournés, divisés ou fusionnés, mais rien n'entre ni ne sort par Lightning pour le moment. Le mint lui-même est actif et répond ; le nœud Lightning derrière lui n'est pas joignable depuis celui-ci.",
"mint.warnings.lnurlNoFunding.meta": "Minting et melting indisponibles",
"mint.warnings.also.noFunding": "Son nœud Lightning était également injoignable, il était donc impossible de faire du minting ou du melting."
} }
+118 -3
View File
@@ -114,6 +114,8 @@
"card.reviewed": "समीक्षा की गई {when}", "card.reviewed": "समीक्षा की गई {when}",
"card.noReviews": "अभी तक कोई समीक्षा नहीं", "card.noReviews": "अभी तक कोई समीक्षा नहीं",
"mint.title": "{name} · Cashu मिंट समीक्षाएँ", "mint.title": "{name} · Cashu मिंट समीक्षाएँ",
"mint.titleFedimint": "{name} · Fedimint फ़ेडरेशन की समीक्षाएँ",
"mint.titleLnurl": "{name} · LNURL mint की समीक्षाएँ",
"mint.breadcrumb": "ब्रेडक्रम्ब", "mint.breadcrumb": "ब्रेडक्रम्ब",
"mint.crumbMints": "Cashu मिंट्स", "mint.crumbMints": "Cashu मिंट्स",
"mint.copyUrl": "URL मिंट की प्रतिलिपि बनाएँ", "mint.copyUrl": "URL मिंट की प्रतिलिपि बनाएँ",
@@ -325,7 +327,7 @@
"reviews.validate.short": "समीक्षा उपयोगी हो, इसके लिए कम से कम {n} अक्षर लिखें।", "reviews.validate.short": "समीक्षा उपयोगी हो, इसके लिए कम से कम {n} अक्षर लिखें।",
"reviews.validate.long": "समीक्षाएँ {n} वर्णों तक सीमित हैं।", "reviews.validate.long": "समीक्षाएँ {n} वर्णों तक सीमित हैं।",
"feed.title": "नवीनतम मिंट और फेडरेशन समीक्षाएँ · Cashumints.space", "feed.title": "नवीनतम मिंट और फेडरेशन समीक्षाएँ · Cashumints.space",
"feed.description": "प्रत्येक मिंट और संघ समीक्षा जैसे ही प्रकाशित होती है: {reviews} समीक्षाएँ {mints} Cashu मिंट्स और {federations} Fedimint संघों में, Nostr पर हस्ताक्षरित और यहाँ नवीनतम से पहले पढ़ने योग्य। इकोसिस्टम, रेटिंग, या नाम के अनुसार फ़िल्टर करें।", "feed.description": "हर मिंट, फ़ेडरेशन और LNURL मिंट समीक्षा प्रकाशित होते ही: {mints} Cashu मिंट, {federations} Fedimint फ़ेडरेशन और {lnurl} LNURL मिंट पर {reviews} समीक्षाएँ, Nostr पर हस्ताक्षरित और यहाँ नवीनतम पहले पढ़ने योग्य. पारिस्थितिकी, रेटिंग या नाम से छाँटें.",
"feed.heading": "नवीनतम समीक्षाएँ", "feed.heading": "नवीनतम समीक्षाएँ",
"feed.lede": "हर Cashu, मिंट और Fedimint संघ की हर समीक्षा, नवीनतम से शुरू होकर, Nostr रिले से सीधे पढ़ी जाती है। प्रत्येक समीक्षा अपने लेखक द्वारा हस्ताक्षरित होती है और हमारे डेटाबेस में नहीं, बल्कि रिले पर रहती है।", "feed.lede": "हर Cashu, मिंट और Fedimint संघ की हर समीक्षा, नवीनतम से शुरू होकर, Nostr रिले से सीधे पढ़ी जाती है। प्रत्येक समीक्षा अपने लेखक द्वारा हस्ताक्षरित होती है और हमारे डेटाबेस में नहीं, बल्कि रिले पर रहती है।",
"feed.label": "समीक्षाएँ", "feed.label": "समीक्षाएँ",
@@ -400,7 +402,14 @@
"notfound.fb.reviews": "समीक्षाएँ", "notfound.fb.reviews": "समीक्षाएँ",
"notfound.fb.software": "सॉफ़्टवेयर", "notfound.fb.software": "सॉफ़्टवेयर",
"notfound.fb.lastOnline": "अंतिम ऑनलाइन", "notfound.fb.lastOnline": "अंतिम ऑनलाइन",
"notfound.fb.note": "यह मिंट साइट की पिछली पुनर्निर्माण के बाद अनुक्रमित किया गया था, इसलिए यह लाइव एपीआई से लिया गया सारांश है। समीक्षाओं सहित पूरा पृष्ठ अगली पुनर्निर्माण में दिखाई देगा।", "notfound.fb.note": "यह mint साइट के पिछले बिल्ड के बाद इंडेक्स हुआ था, इसलिए यह लाइव API से बना सारांश है और नीचे इसकी समीक्षाएँ हैं। पूरा पेज अगले बिल्ड पर दिखेगा।",
"notfound.resolve.checking": "पहली बार: इस mint की जाँच अभी हो रही है।",
"notfound.resolve.failed": "यह mint जोड़ा नहीं जा सका",
"notfound.resolve.failedFedimint": "यह फ़ेडरेशन जोड़ा नहीं जा सका",
"notfound.resolve.retry": "दूसरा पता आज़माएँ",
"notfound.resolve.switch": "इसे {kind} mint के रूप में खोलें",
"notfound.resolve.fedimint": "फ़ेडरेशन के पेज के पते में उसका invite code नहीं होता, इसलिए सिर्फ़ लिंक से इसे खोजा नहीं जा सकता। invite code चिपकाएँ, यह जुड़ जाएगा।",
"notfound.resolve.ambiguous": "सिर्फ़ लिंक से यह पता निकाला नहीं जा सकता। mint का पूरा URL चिपकाएँ, यह जुड़ जाएगा।",
"wallets.title": "Cashu वॉलेट्स · Cashumints.space", "wallets.title": "Cashu वॉलेट्स · Cashumints.space",
"wallets.description": "Cashu वॉलेट जो यहाँ सूचीबद्ध मिंट्स के साथ मोबाइल, वेब, ब्राउज़र और कमांड लाइन पर काम करते हैं, और प्रत्येक किस काम में अच्छा है।", "wallets.description": "Cashu वॉलेट जो यहाँ सूचीबद्ध मिंट्स के साथ मोबाइल, वेब, ब्राउज़र और कमांड लाइन पर काम करते हैं, और प्रत्येक किस काम में अच्छा है।",
"wallets.heading": "बटुए", "wallets.heading": "बटुए",
@@ -654,5 +663,111 @@
"footer.allFedimints": "सभी महासंघ", "footer.allFedimints": "सभी महासंघ",
"reviews.dialog.bodyPlaceholder.cashu": "मिंटिंग, मेल्टिंग और सपोर्ट का हिसाब कैसे निकला? विशिष्ट रूप से बताएं।", "reviews.dialog.bodyPlaceholder.cashu": "मिंटिंग, मेल्टिंग और सपोर्ट का हिसाब कैसे निकला? विशिष्ट रूप से बताएं।",
"reviews.dialog.bodyPlaceholder.fedimint": "जॉइनिंग, Lightning भुगतान और सपोर्ट का काम कैसे हुआ? विशिष्ट रूप से बताएं।", "reviews.dialog.bodyPlaceholder.fedimint": "जॉइनिंग, Lightning भुगतान और सपोर्ट का काम कैसे हुआ? विशिष्ट रूप से बताएं।",
"fedimint.details.title": "संघ का विवरण" "fedimint.details.title": "संघ का विवरण",
"nav.lnurlMints": "LNURL मिंट",
"footer.allLnurlMints": "सभी LNURL मिंट",
"lnurlMints.title": "LNURL मिंट: lnurlcash वाहक नोट मिंट, समीक्षा सहित | Cashumints.space",
"lnurlMints.description": "Nostr नेटवर्क से अनुक्रमित सभी {total} LNURL मिंट: {online} ऑनलाइन, {offline} ऑफ़लाइन. हर एक के लिए समुदाय की रेटिंग, निकासी सीमाएँ और सुविधाएँ.",
"lnurlMints.heading": "LNURL मिंट",
"lnurlMints.lede": "इस साइट को ज्ञात हर LNURL मिंट. एक LNURL मिंट lnurlcash वाहक नोट जारी करता है: लाइटनिंग नोट जिन्हें आप एक रहस्य की तरह रखते हैं, और जिन्हें घुमाया, बाँटा, जोड़ा या वापस लाइटनिंग भुगतान में पिघलाया जा सकता है. हर एक की HTTPS पर कुछ मिनटों के अंतराल पर जाँच होती है.",
"lnurlMints.search.label": "LNURL मिंट खोजें",
"lnurlMints.search.placeholder": "नाम या पते से खोजें…",
"lnurlMints.sort.label": "LNURL मिंट क्रमबद्ध करें",
"lnurlMints.hideOffline": "ऑफ़लाइन छिपाएँ",
"lnurlMints.showing": "{total} LNURL मिंट दिखाए जा रहे हैं ({online} ऑनलाइन, {offline} ऑफ़लाइन)",
"lnurlMints.showingFiltered": "{total} में से {shown} LNURL मिंट दिखाए जा रहे हैं ({online} ऑनलाइन, {offline} ऑफ़लाइन)",
"lnurlMints.noResults": "कोई LNURL मिंट मेल नहीं खाता.",
"lnurlMints.emptyIndex": "अभी तक कोई LNURL मिंट अनुक्रमित नहीं है.",
"lnurl.crumb": "LNURL मिंट",
"lnurl.breadcrumb": "ब्रेडक्रम्ब",
"lnurl.copyAddress": "पता कॉपी करें",
"lnurl.more.title": "और LNURL मिंट",
"lnurl.more.all": "सभी LNURL मिंट →",
"lnurl.disclaimer": "एक LNURL मिंट आपके sats रखता है जबकि आप उसके नोट रखते हैं, इसलिए उसका उपयोग करने का मतलब है उसके संचालक पर भरोसा करना. रेटिंग समुदाय की राय हैं, और ऑनलाइन का मतलब पहुँच योग्य है, भरोसेमंद नहीं.",
"lnurl.features.title": "सुविधाएँ",
"lnurl.features.none": "इस मिंट ने घोषित नहीं किया कि वह क्या समर्थन करता है.",
"lnurl.features.note": "सुविधाएँ वही हैं जो मिंट के संचालक ने Nostr पर घोषित कीं. जहाँ यह साइट HTTPS पर किसी की जाँच कर सकती है, वहाँ करती है, और बताती है जब कोई घोषित चीज़ अभी उपलब्ध नहीं है.",
"lnurl.features.notListed": "घोषित नहीं",
"lnurl.features.unavailable": "अनुपलब्ध",
"lnurl.feature.mint": "लाइटनिंग से मिंट (LUD-06)",
"lnurl.feature.melt": "लाइटनिंग में पिघलाना",
"lnurl.feature.notes": "नोट घुमाना / बाँटना / जोड़ना",
"lnurl.feature.lud16": "लाइटनिंग पता",
"lnurl.feature.lud21": "भुगतान सत्यापन (LUD-21)",
"lnurl.feature.signed-notes": "हस्ताक्षरित नोट (ऑफ़लाइन सत्यापन)",
"lnurl.feature.onion": "Tor पता",
"lnurl.feature.rotate": "नोट घुमाएँ",
"lnurl.feature.split": "नोट बाँटें",
"lnurl.feature.merge": "नोट जोड़ें",
"lnurl.feature.lud06": "LUD-06 payRequest",
"lnurl.feature.lud03": "LUD-03 withdrawRequest",
"lnurl.limits.label": "निकासी सीमाएँ",
"lnurl.limits.notPublished": "प्रकाशित नहीं",
"lnurl.limits.withdrawRow": "निकासी",
"lnurl.limits.disabled": "बंद",
"lnurl.limits.sats.one": "{n} sat",
"lnurl.limits.sats.other": "{n} sats",
"lnurl.details.title": "मिंट विवरण",
"lnurl.details.address": "लाइटनिंग पता",
"lnurl.details.mintPubkey": "मिंट की सार्वजनिक कुंजी",
"lnurl.details.onion": "Tor पता",
"lnurl.details.fee": "मिंट शुल्क",
"lnurl.details.node": "नोड",
"lnurl.details.capacity": "घोषित क्षमता",
"lnurl.kind.lightningAddress": "लाइटनिंग पता",
"lnurl.kind.mintPubkey": "मिंट की सार्वजनिक कुंजी",
"lnurl.kind.onionAddress": "Tor पता",
"lnurl.confirmed.label": "अंतिम पुष्टि",
"lnurl.confirmed.when": "{when} पुष्ट",
"lnurl.fee.base.one": "{n} sat",
"lnurl.fee.base.other": "{n} sats",
"lnurl.fee.percent": "{percent}%",
"home.topLnurl.title": "शीर्ष LNURL मिंट",
"home.topLnurl.sub": "समीक्षाओं और रेटिंग के अनुसार",
"home.topLnurl.all": "सभी {n} LNURL मिंट →",
"feed.ecosystem.lnurl": "LNURL",
"reviews.dialog.bodyPlaceholder.lnurl": "मिंट करना, पिघलाना और नोट घुमाना कैसा रहा? विशिष्ट रहें.",
"reviews.byUrl.open": "समीक्षा लिखें",
"reviews.byUrl.title": "समीक्षा लिखें",
"reviews.byUrl.lede": "पता चिपकाएँ। अगर यह साइट उसे अभी तक नहीं जानती, तो उसे अभी जाँचकर जोड़ लिया जाएगा।",
"reviews.byUrl.label.cashu": "mint का URL",
"reviews.byUrl.label.fedimint": "फ़ेडरेशन का invite code",
"reviews.byUrl.label.lnurl": "LNURL mint का URL",
"reviews.byUrl.placeholder.cashu": "mint.example.com",
"reviews.byUrl.placeholder.fedimint": "fed11…",
"reviews.byUrl.placeholder.lnurl": "mint.example.com",
"reviews.byUrl.check": "यह पता जाँचें",
"reviews.byUrl.checking": "जाँच रहे हैं…",
"reviews.byUrl.back": "दूसरा पता इस्तेमाल करें",
"reviews.byUrl.existing": "पहले से सूची में",
"reviews.byUrl.indexed": "अभी जोड़ा गया",
"reviews.byUrl.confirmClose": "बंद करें? आपने जो पता लिखा है वह सहेजा नहीं जाएगा।",
"reviews.byUrl.detected": "उस पते ने {kind} mint की तरह जवाब दिया।",
"reviews.byUrl.switchTo": "इसे {kind} mint के रूप में समीक्षा करें",
"reviews.byUrl.error.empty": "पहले कोई पता लिखें।",
"reviews.byUrl.error.badUrl": "यह mint के URL जैसा नहीं लगता।",
"reviews.byUrl.error.badInvite": "यह Fedimint का invite code नहीं है। कोड fed11 से शुरू होते हैं।",
"reviews.byUrl.error.badInput": "यह साइट उस पते की जाँच नहीं कर सकती।",
"reviews.byUrl.error.blocked": "यह सार्वजनिक पता नहीं है, इसलिए इसकी जाँच नहीं हो सकती।",
"reviews.byUrl.error.notAMint": "उस पते ने जवाब तो दिया, पर इस तरह के mint के रूप में नहीं।",
"reviews.byUrl.error.wrongType": "उस पते ने जवाब दिया, पर किसी और तरह के mint के रूप में।",
"reviews.byUrl.error.unverifiable": "उस पते पर कुछ भी जवाब नहीं आया, और Nostr पर भी अभी उसका कोई निशान नहीं है।",
"reviews.byUrl.error.rateLimited.one": "एक ही कनेक्शन से बहुत सारी जाँचें हो चुकी हैं। लगभग {n} मिनट बाद फिर कोशिश करें।",
"reviews.byUrl.error.rateLimited.other": "एक ही कनेक्शन से बहुत सारी जाँचें हो चुकी हैं। लगभग {n} मिनट बाद फिर कोशिश करें।",
"reviews.byUrl.error.network": "इंडेक्स तक नहीं पहुँच सके। अपना कनेक्शन जाँचकर फिर कोशिश करें।",
"seo.lnurl.titleNoReviews": "{name}: LNURL मिंट जानकारी और समीक्षाएँ | Cashumints.space",
"seo.desc.statusWithdraw": "{status}, निकासी {min}–{max} sat.",
"mint.chip.noFunding": "मिंट / मेल्ट बंद",
"mint.warnings.lnurlWithdrawalsDisabled.lead": "निकासी बंद है.",
"mint.warnings.lnurlWithdrawalsDisabled.body.online": "यह मिंट अधिकतम निकासी शून्य घोषित करता है, इसलिए इसके जारी किए किसी भी नोट को किसी चीज़ के बदले भुनाया नहीं जा सकता. जब तक यह न बदले, यहाँ मिंट न करें.",
"mint.warnings.lnurlWithdrawalsDisabled.body.offline": "अंतिम बार संपर्क होने पर इस मिंट ने अधिकतम निकासी शून्य घोषित की थी, इसलिए इसके जारी किए किसी भी नोट को किसी चीज़ के बदले भुनाया नहीं जा सकता था. जब तक यह न बदले, यहाँ मिंट न करें.",
"mint.warnings.lnurlWithdrawalsDisabled.meta.online": "निकासी फ़िलहाल बंद है",
"mint.warnings.lnurlWithdrawalsDisabled.meta.offline": "अंतिम बार देखे जाने पर निकासी बंद थी",
"mint.warnings.lnurlInvalid.lead": "एंडपॉइंट जवाब दे रहा है पर अमान्य है.",
"mint.warnings.lnurlInvalid.body": "होस्ट जवाब देता है, पर ऐसी मिंट घोषणा के साथ नहीं जिसे यह साइट पढ़ सके, इसलिए निकासी शायद काम न करे. नीचे जो है वह अंतिम पढ़ी जा सकी स्थिति है, और समीक्षाएँ अब भी काम करती हैं.",
"mint.warnings.lnurlInvalid.meta": "जवाब देता है, पर वैध मिंट घोषणा के साथ नहीं",
"mint.warnings.lnurlNoFunding.lead": "मिंट और मेल्ट अनुपलब्ध.",
"mint.warnings.lnurlNoFunding.body": "मौजूदा नोट अब भी घुमाए, बाँटे या जोड़े जा सकते हैं, पर अभी लाइटनिंग से कुछ भी अंदर या बाहर नहीं जा रहा. मिंट खुद चालू है और जवाब दे रहा है; उसके पीछे का लाइटनिंग नोड उससे पहुँच में नहीं है.",
"mint.warnings.lnurlNoFunding.meta": "मिंट और मेल्ट अनुपलब्ध",
"mint.warnings.also.noFunding": "इसका लाइटनिंग नोड भी पहुँच से बाहर था, इसलिए कुछ भी मिंट या मेल्ट नहीं किया जा सकता था."
} }
+118 -3
View File
@@ -114,6 +114,8 @@
"card.reviewed": "{when}", "card.reviewed": "{when}",
"card.noReviews": "belum ada ulasan", "card.noReviews": "belum ada ulasan",
"mint.title": "{name}", "mint.title": "{name}",
"mint.titleFedimint": "{name} · Ulasan federasi Fedimint",
"mint.titleLnurl": "{name} · Ulasan mint LNURL",
"mint.breadcrumb": "Breadcrumb", "mint.breadcrumb": "Breadcrumb",
"mint.crumbMints": "Mints Cashu", "mint.crumbMints": "Mints Cashu",
"mint.copyUrl": "Salin URL mint", "mint.copyUrl": "Salin URL mint",
@@ -325,7 +327,7 @@
"reviews.validate.short": "Tulis setidaknya karakter {n} sehingga ulasan berguna.", "reviews.validate.short": "Tulis setidaknya karakter {n} sehingga ulasan berguna.",
"reviews.validate.long": "Ulasan terbatas pada karakter {n}.", "reviews.validate.long": "Ulasan terbatas pada karakter {n}.",
"feed.title": "Ulasan mint dan federasi terbaru", "feed.title": "Ulasan mint dan federasi terbaru",
"feed.description": "Setiap ulasan mint dan federasi saat diterbitkan: {reviews} ulasan di {mints} mint Cashu dan {federations} federasi Fedimint, ditandatangani di Nostr dan ditampilkan dari yang terbaru. Saring berdasarkan ekosistem, peringkat, atau nama.", "feed.description": "Setiap ulasan mint, federasi, dan mint LNURL begitu dipublikasikan: {reviews} ulasan atas {mints} mint Cashu, {federations} federasi Fedimint, dan {lnurl} mint LNURL, ditandatangani di Nostr dan dapat dibaca di sini mulai dari yang terbaru. Saring berdasarkan ekosistem, penilaian, atau nama.",
"feed.heading": "Ulasan Terbaru", "feed.heading": "Ulasan Terbaru",
"feed.lede": "Setiap ulasan dari setiap Cushu mint dan Fedimint federation, terbaru pertama, membaca langsung dari relays Nostr. Masing-masing ditandatangani oleh penulis dan tinggal di relay, tidak dalam database kami.", "feed.lede": "Setiap ulasan dari setiap Cushu mint dan Fedimint federation, terbaru pertama, membaca langsung dari relays Nostr. Masing-masing ditandatangani oleh penulis dan tinggal di relay, tidak dalam database kami.",
"feed.label": "Ulasan", "feed.label": "Ulasan",
@@ -400,7 +402,14 @@
"notfound.fb.reviews": "Ulasan", "notfound.fb.reviews": "Ulasan",
"notfound.fb.software": "Perangkat Lunak", "notfound.fb.software": "Perangkat Lunak",
"notfound.fb.lastOnline": "Online terakhir", "notfound.fb.lastOnline": "Online terakhir",
"notfound.fb.note": "Mint ini diindeks setelah situs terakhir dibangun, jadi ini adalah ringkasan dari API hidup. Halaman penuh dengan ulasan muncul di pembangunan kembali berikutnya.", "notfound.fb.note": "Mint ini diindeks setelah situs terakhir dibangun, jadi ini ringkasan dari API langsung, dengan ulasannya di bawah. Halaman lengkapnya muncul pada pembangunan berikutnya.",
"notfound.resolve.checking": "Kunjungan pertama: mint ini sedang diperiksa.",
"notfound.resolve.failed": "Mint ini tidak bisa ditambahkan",
"notfound.resolve.failedFedimint": "Federasi ini tidak bisa ditambahkan",
"notfound.resolve.retry": "Coba alamat lain",
"notfound.resolve.switch": "Buka sebagai mint {kind}",
"notfound.resolve.fedimint": "Alamat halaman sebuah federasi tidak memuat kode undangannya, jadi yang ini tidak bisa dicari hanya dari tautannya. Tempel kode undangannya dan federasi itu ditambahkan.",
"notfound.resolve.ambiguous": "Alamat ini tidak bisa disimpulkan hanya dari tautannya. Tempel URL lengkap mint itu dan mint itu ditambahkan.",
"wallets.title": "Dompet Cashu", "wallets.title": "Dompet Cashu",
"wallets.description": "Dompet Cashu yang bekerja dengan permen yang ada di sini, di ponsel, web, browser dan baris perintah, dengan keahlian masing-masing.", "wallets.description": "Dompet Cashu yang bekerja dengan permen yang ada di sini, di ponsel, web, browser dan baris perintah, dengan keahlian masing-masing.",
"wallets.heading": "Dompet", "wallets.heading": "Dompet",
@@ -654,5 +663,111 @@
"footer.allFedimints": "Semua federasi", "footer.allFedimints": "Semua federasi",
"reviews.dialog.bodyPlaceholder.cashu": "Bagaimana perpaduan, pencairan dan dukungan bekerja? Lebih spesifik.", "reviews.dialog.bodyPlaceholder.cashu": "Bagaimana perpaduan, pencairan dan dukungan bekerja? Lebih spesifik.",
"reviews.dialog.bodyPlaceholder.fedimint": "Bagaimana bergabung, pembayaran Petir dan dukungan berhasil? Lebih spesifik.", "reviews.dialog.bodyPlaceholder.fedimint": "Bagaimana bergabung, pembayaran Petir dan dukungan berhasil? Lebih spesifik.",
"fedimint.details.title": "Rincian Federasi" "fedimint.details.title": "Rincian Federasi",
"nav.lnurlMints": "Mint LNURL",
"footer.allLnurlMints": "Semua mint LNURL",
"lnurlMints.title": "Mint LNURL: mint nota atas unjuk lnurlcash, dengan ulasan | Cashumints.space",
"lnurlMints.description": "Seluruh {total} mint LNURL yang diindeks dari jaringan Nostr: {online} daring, {offline} luring. Penilaian komunitas, batas penarikan, dan fitur untuk masing-masing.",
"lnurlMints.heading": "Mint LNURL",
"lnurlMints.lede": "Setiap mint LNURL yang diketahui situs ini. Mint LNURL menerbitkan nota atas unjuk lnurlcash: nota Lightning yang Anda simpan sebagai rahasia, dan dapat diputar, dipecah, digabung, atau dilebur kembali menjadi pembayaran Lightning. Masing-masing diperiksa lewat HTTPS setiap beberapa menit.",
"lnurlMints.search.label": "Cari mint LNURL",
"lnurlMints.search.placeholder": "Cari berdasarkan nama atau alamat…",
"lnurlMints.sort.label": "Urutkan mint LNURL berdasarkan",
"lnurlMints.hideOffline": "Sembunyikan yang luring",
"lnurlMints.showing": "Menampilkan {total} mint LNURL ({online} daring, {offline} luring)",
"lnurlMints.showingFiltered": "Menampilkan {shown} dari {total} mint LNURL ({online} daring, {offline} luring)",
"lnurlMints.noResults": "Tidak ada mint LNURL yang cocok.",
"lnurlMints.emptyIndex": "Belum ada mint LNURL yang diindeks.",
"lnurl.crumb": "Mint LNURL",
"lnurl.breadcrumb": "Remah roti",
"lnurl.copyAddress": "Salin alamat",
"lnurl.more.title": "Mint LNURL lainnya",
"lnurl.more.all": "Semua mint LNURL →",
"lnurl.disclaimer": "Mint LNURL menyimpan sats Anda sementara Anda memegang notanya, jadi memakainya berarti memercayai operatornya. Penilaian adalah pendapat komunitas, dan daring berarti dapat dijangkau, bukan tepercaya.",
"lnurl.features.title": "Fitur",
"lnurl.features.none": "Mint ini belum mengumumkan apa yang didukungnya.",
"lnurl.features.note": "Fitur adalah apa yang diumumkan operator mint di Nostr. Bila situs ini dapat memeriksanya lewat HTTPS, ia melakukannya, dan memberi tahu bila sesuatu yang diumumkan sedang tidak tersedia.",
"lnurl.features.notListed": "tidak diumumkan",
"lnurl.features.unavailable": "tidak tersedia",
"lnurl.feature.mint": "Mint lewat Lightning (LUD-06)",
"lnurl.feature.melt": "Lebur ke Lightning",
"lnurl.feature.notes": "Putar / pecah / gabung nota",
"lnurl.feature.lud16": "Alamat Lightning",
"lnurl.feature.lud21": "Verifikasi pembayaran (LUD-21)",
"lnurl.feature.signed-notes": "Nota bertanda tangan (verifikasi luring)",
"lnurl.feature.onion": "Alamat Tor",
"lnurl.feature.rotate": "Putar sebuah nota",
"lnurl.feature.split": "Pecah sebuah nota",
"lnurl.feature.merge": "Gabungkan nota",
"lnurl.feature.lud06": "LUD-06 payRequest",
"lnurl.feature.lud03": "LUD-03 withdrawRequest",
"lnurl.limits.label": "Batas penarikan",
"lnurl.limits.notPublished": "tidak dipublikasikan",
"lnurl.limits.withdrawRow": "Penarikan",
"lnurl.limits.disabled": "dinonaktifkan",
"lnurl.limits.sats.one": "{n} sat",
"lnurl.limits.sats.other": "{n} sat",
"lnurl.details.title": "Detail mint",
"lnurl.details.address": "Alamat Lightning",
"lnurl.details.mintPubkey": "Kunci publik mint",
"lnurl.details.onion": "Alamat Tor",
"lnurl.details.fee": "Biaya mint",
"lnurl.details.node": "Node",
"lnurl.details.capacity": "Kapasitas yang diumumkan",
"lnurl.kind.lightningAddress": "alamat Lightning",
"lnurl.kind.mintPubkey": "kunci publik mint",
"lnurl.kind.onionAddress": "alamat Tor",
"lnurl.confirmed.label": "Terakhir dikonfirmasi",
"lnurl.confirmed.when": "dikonfirmasi {when}",
"lnurl.fee.base.one": "{n} sat",
"lnurl.fee.base.other": "{n} sat",
"lnurl.fee.percent": "{percent}%",
"home.topLnurl.title": "Mint LNURL teratas",
"home.topLnurl.sub": "diperingkat berdasarkan ulasan dan penilaian",
"home.topLnurl.all": "Semua {n} mint LNURL →",
"feed.ecosystem.lnurl": "LNURL",
"reviews.dialog.bodyPlaceholder.lnurl": "Bagaimana pengalaman mint, lebur, dan memutar nota? Jelaskan secara spesifik.",
"reviews.byUrl.open": "Tulis ulasan",
"reviews.byUrl.title": "Tulis ulasan",
"reviews.byUrl.lede": "Tempel alamatnya. Kalau situs ini belum mengenalnya, alamat itu diperiksa dan ditambahkan sekarang.",
"reviews.byUrl.label.cashu": "URL mint",
"reviews.byUrl.label.fedimint": "Kode undangan federasi",
"reviews.byUrl.label.lnurl": "URL mint LNURL",
"reviews.byUrl.placeholder.cashu": "mint.example.com",
"reviews.byUrl.placeholder.fedimint": "fed11…",
"reviews.byUrl.placeholder.lnurl": "mint.example.com",
"reviews.byUrl.check": "Periksa alamat ini",
"reviews.byUrl.checking": "Memeriksa…",
"reviews.byUrl.back": "Pakai alamat lain",
"reviews.byUrl.existing": "Sudah terdaftar",
"reviews.byUrl.indexed": "Baru ditambahkan",
"reviews.byUrl.confirmClose": "Tutup? Alamat yang kamu ketik tidak disimpan.",
"reviews.byUrl.detected": "Alamat itu menjawab sebagai mint {kind}.",
"reviews.byUrl.switchTo": "Ulas sebagai mint {kind}",
"reviews.byUrl.error.empty": "Isi alamatnya dulu.",
"reviews.byUrl.error.badUrl": "Itu tidak terlihat seperti URL mint.",
"reviews.byUrl.error.badInvite": "Itu bukan kode undangan Fedimint. Kode dimulai dengan fed11.",
"reviews.byUrl.error.badInput": "Situs ini tidak bisa memeriksa alamat itu.",
"reviews.byUrl.error.blocked": "Itu bukan alamat publik, jadi tidak bisa diperiksa.",
"reviews.byUrl.error.notAMint": "Alamat itu menjawab, tetapi bukan sebagai mint jenis ini.",
"reviews.byUrl.error.wrongType": "Alamat itu menjawab, tetapi sebagai mint jenis lain.",
"reviews.byUrl.error.unverifiable": "Tidak ada yang menjawab di alamat itu, dan jejaknya belum ada di Nostr.",
"reviews.byUrl.error.rateLimited.one": "Terlalu banyak pemeriksaan dari satu koneksi. Coba lagi sekitar {n} menit lagi.",
"reviews.byUrl.error.rateLimited.other": "Terlalu banyak pemeriksaan dari satu koneksi. Coba lagi sekitar {n} menit lagi.",
"reviews.byUrl.error.network": "Indeks tidak dapat dihubungi. Periksa koneksimu, lalu coba lagi.",
"seo.lnurl.titleNoReviews": "{name}: info dan ulasan mint LNURL | Cashumints.space",
"seo.desc.statusWithdraw": "{status}, penarikan {min}–{max} sat.",
"mint.chip.noFunding": "Tanpa mint / lebur",
"mint.warnings.lnurlWithdrawalsDisabled.lead": "Penarikan dinonaktifkan.",
"mint.warnings.lnurlWithdrawalsDisabled.body.online": "Mint ini mengumumkan penarikan maksimum nol, sehingga tidak ada nota yang diterbitkannya dapat ditukar dengan apa pun. Jangan melakukan mint di sini sampai hal itu berubah.",
"mint.warnings.lnurlWithdrawalsDisabled.body.offline": "Saat terakhir dapat dihubungi, mint ini mengumumkan penarikan maksimum nol, sehingga tidak ada nota yang diterbitkannya dapat ditukar dengan apa pun. Jangan melakukan mint di sini sampai hal itu berubah.",
"mint.warnings.lnurlWithdrawalsDisabled.meta.online": "Penarikan sedang dinonaktifkan",
"mint.warnings.lnurlWithdrawalsDisabled.meta.offline": "Penarikan dinonaktifkan saat terakhir terlihat",
"mint.warnings.lnurlInvalid.lead": "Endpoint merespons tetapi tidak valid.",
"mint.warnings.lnurlInvalid.body": "Host merespons, tetapi bukan dengan pengumuman mint yang dapat dibaca situs ini, jadi penarikan mungkin tidak berfungsi. Semua di bawah ini adalah keadaan terakhir yang berhasil dibaca, dan ulasan tetap berfungsi.",
"mint.warnings.lnurlInvalid.meta": "Merespons, tetapi bukan dengan pengumuman mint yang valid",
"mint.warnings.lnurlNoFunding.lead": "Mint dan lebur tidak tersedia.",
"mint.warnings.lnurlNoFunding.body": "Nota yang ada masih dapat diputar, dipecah, atau digabung, tetapi saat ini tidak ada yang masuk atau keluar lewat Lightning. Mint-nya sendiri hidup dan merespons; node Lightning di belakangnya tidak dapat dijangkau dari sana.",
"mint.warnings.lnurlNoFunding.meta": "Mint dan lebur tidak tersedia",
"mint.warnings.also.noFunding": "Node Lightning-nya juga tidak dapat dijangkau, sehingga tidak ada yang bisa di-mint atau dilebur."
} }
+118 -3
View File
@@ -114,6 +114,8 @@
"card.reviewed": "recensione {when}", "card.reviewed": "recensione {when}",
"card.noReviews": "nessuna recensione", "card.noReviews": "nessuna recensione",
"mint.title": "{name} · Cashu mint recensioni", "mint.title": "{name} · Cashu mint recensioni",
"mint.titleFedimint": "{name} · Recensioni della federazione Fedimint",
"mint.titleLnurl": "{name} · Recensioni del mint LNURL",
"mint.breadcrumb": "Percorso di navigazione", "mint.breadcrumb": "Percorso di navigazione",
"mint.crumbMints": "Cashu mints", "mint.crumbMints": "Cashu mints",
"mint.copyUrl": "Copia URL mint", "mint.copyUrl": "Copia URL mint",
@@ -325,7 +327,7 @@
"reviews.validate.short": "Scrivi almeno {n} caratteri in modo che la recensione sia utile.", "reviews.validate.short": "Scrivi almeno {n} caratteri in modo che la recensione sia utile.",
"reviews.validate.long": "Le recensioni sono limitate ai caratteri {n}.", "reviews.validate.long": "Le recensioni sono limitate ai caratteri {n}.",
"feed.title": "Ultimi mint e recensioni federazioni · Cashumints.space", "feed.title": "Ultimi mint e recensioni federazioni · Cashumints.space",
"feed.description": "Ogni recensione mint e federazione come è pubblicato: {reviews} recensioni attraverso {mints} Cashu mints e {federations} Fedimint federazioni, firmate Nostr e leggibili qui prima. Filtra per ecosistema, per valutazione, o per nome.", "feed.description": "Ogni recensione di mint, federazione e mint LNURL appena viene pubblicata: {reviews} recensioni su {mints} mint Cashu, {federations} federazioni Fedimint e {lnurl} mint LNURL, firmate su Nostr e leggibili qui dalla più recente. Filtra per ecosistema, valutazione o nome.",
"feed.heading": "Ultime recensioni", "feed.heading": "Ultime recensioni",
"feed.lede": "Ogni recensione di ogni Cashu mint e Fedimint federazione, prima nuova, leggere dal vivo dai relè Nostr. Ognuno è firmato dal suo autore e vive sui relè, non nel nostro database.", "feed.lede": "Ogni recensione di ogni Cashu mint e Fedimint federazione, prima nuova, leggere dal vivo dai relè Nostr. Ognuno è firmato dal suo autore e vive sui relè, non nel nostro database.",
"feed.label": "Recensioni", "feed.label": "Recensioni",
@@ -400,7 +402,14 @@
"notfound.fb.reviews": "Recensioni", "notfound.fb.reviews": "Recensioni",
"notfound.fb.software": "Software software", "notfound.fb.software": "Software software",
"notfound.fb.lastOnline": "Ultimo online", "notfound.fb.lastOnline": "Ultimo online",
"notfound.fb.note": "Questo mint è stato indicizzato dopo l'ultima costruzione del sito, quindi questo è un riassunto dell'API dal vivo. La pagina completa con le recensioni appare alla prossima ricostruzione.", "notfound.fb.note": "Questo mint è stato indicizzato dopo l’ultima build del sito, quindi questo è un riepilogo dall’API dal vivo, con le recensioni sotto. La pagina completa compare alla prossima build.",
"notfound.resolve.checking": "Prima visita: sto controllando questo mint adesso.",
"notfound.resolve.failed": "Non è stato possibile aggiungere questo mint",
"notfound.resolve.failedFedimint": "Non è stato possibile aggiungere questa federazione",
"notfound.resolve.retry": "Prova un altro indirizzo",
"notfound.resolve.switch": "Aprilo come mint {kind}",
"notfound.resolve.fedimint": "L’indirizzo della pagina di una federazione non contiene il suo codice di invito, quindi non si può risalire a questa dal solo link. Incolla il codice di invito e verrà aggiunta.",
"notfound.resolve.ambiguous": "Questo indirizzo non si può ricavare dal solo link. Incolla l’URL completo del mint e verrà aggiunto.",
"wallets.title": "Cashu portafogli · Cashumints.space", "wallets.title": "Cashu portafogli · Cashumints.space",
"wallets.description": "Cashu portafogli che funzionano con il mints elencati qui, attraverso mobile, web, browser e la riga di comando, con quello che ognuno è buono a.", "wallets.description": "Cashu portafogli che funzionano con il mints elencati qui, attraverso mobile, web, browser e la riga di comando, con quello che ognuno è buono a.",
"wallets.heading": "Portafogli", "wallets.heading": "Portafogli",
@@ -654,5 +663,111 @@
"footer.allFedimints": "Tutte le federazioni", "footer.allFedimints": "Tutte le federazioni",
"reviews.dialog.bodyPlaceholder.cashu": "Come ha funzionato minting, melting e supporto? Sii specifico.", "reviews.dialog.bodyPlaceholder.cashu": "Come ha funzionato minting, melting e supporto? Sii specifico.",
"reviews.dialog.bodyPlaceholder.fedimint": "Come si è unito, Lightning pagamenti e supporto funziona? Sii specifico.", "reviews.dialog.bodyPlaceholder.fedimint": "Come si è unito, Lightning pagamenti e supporto funziona? Sii specifico.",
"fedimint.details.title": "Dettagli della Federazione" "fedimint.details.title": "Dettagli della Federazione",
"nav.lnurlMints": "Mint LNURL",
"footer.allLnurlMints": "Tutti i mint LNURL",
"lnurlMints.title": "Mint LNURL: mint di banconote al portatore lnurlcash, con recensioni | Cashumints.space",
"lnurlMints.description": "Tutti i {total} mint LNURL indicizzati dalla rete Nostr: {online} online, {offline} offline. Valutazioni della comunità, limiti di prelievo e funzionalità di ciascuno.",
"lnurlMints.heading": "Mint LNURL",
"lnurlMints.lede": "Ogni mint LNURL che questo sito conosce. Un mint LNURL emette banconote al portatore lnurlcash: banconote Lightning che conservi come un segreto e che si possono ruotare, dividere, unire o rifondere in un pagamento Lightning. Ognuno viene controllato via HTTPS ogni pochi minuti.",
"lnurlMints.search.label": "Cerca mint LNURL",
"lnurlMints.search.placeholder": "Cerca per nome o indirizzo…",
"lnurlMints.sort.label": "Ordina i mint LNURL per",
"lnurlMints.hideOffline": "Nascondi offline",
"lnurlMints.showing": "Mostrati {total} mint LNURL ({online} online, {offline} offline)",
"lnurlMints.showingFiltered": "Mostrati {shown} di {total} mint LNURL ({online} online, {offline} offline)",
"lnurlMints.noResults": "Nessun mint LNURL corrisponde.",
"lnurlMints.emptyIndex": "Non ci sono ancora mint LNURL indicizzati.",
"lnurl.crumb": "Mint LNURL",
"lnurl.breadcrumb": "Percorso di navigazione",
"lnurl.copyAddress": "Copia indirizzo",
"lnurl.more.title": "Altri mint LNURL",
"lnurl.more.all": "Tutti i mint LNURL →",
"lnurl.disclaimer": "Un mint LNURL custodisce i tuoi sat mentre tu custodisci le sue banconote, quindi usarne uno significa fidarsi del suo operatore. Le valutazioni sono opinioni della comunità, e online significa raggiungibile, non affidabile.",
"lnurl.features.title": "Funzionalità",
"lnurl.features.none": "Questo mint non ha annunciato cosa supporta.",
"lnurl.features.note": "Le funzionalità sono ciò che l'operatore del mint ha annunciato su Nostr. Dove questo sito può verificarne una via HTTPS lo fa, e segnala quando qualcosa di annunciato non è disponibile in questo momento.",
"lnurl.features.notListed": "non annunciato",
"lnurl.features.unavailable": "non disponibile",
"lnurl.feature.mint": "Minting via Lightning (LUD-06)",
"lnurl.feature.melt": "Melting verso Lightning",
"lnurl.feature.notes": "Ruota / dividi / unisci banconote",
"lnurl.feature.lud16": "Indirizzo Lightning",
"lnurl.feature.lud21": "Verifica del pagamento (LUD-21)",
"lnurl.feature.signed-notes": "Banconote firmate (verifica offline)",
"lnurl.feature.onion": "Indirizzo Tor",
"lnurl.feature.rotate": "Ruota una banconota",
"lnurl.feature.split": "Dividi una banconota",
"lnurl.feature.merge": "Unisci banconote",
"lnurl.feature.lud06": "payRequest LUD-06",
"lnurl.feature.lud03": "withdrawRequest LUD-03",
"lnurl.limits.label": "Limiti di prelievo",
"lnurl.limits.notPublished": "non pubblicato",
"lnurl.limits.withdrawRow": "Prelievo",
"lnurl.limits.disabled": "disattivato",
"lnurl.limits.sats.one": "{n} sat",
"lnurl.limits.sats.other": "{n} sat",
"lnurl.details.title": "Dettagli del mint",
"lnurl.details.address": "Indirizzo Lightning",
"lnurl.details.mintPubkey": "Chiave pubblica del mint",
"lnurl.details.onion": "Indirizzo Tor",
"lnurl.details.fee": "Commissione di minting",
"lnurl.details.node": "Nodo",
"lnurl.details.capacity": "Capacità annunciata",
"lnurl.kind.lightningAddress": "indirizzo Lightning",
"lnurl.kind.mintPubkey": "chiave pubblica del mint",
"lnurl.kind.onionAddress": "indirizzo Tor",
"lnurl.confirmed.label": "Ultima conferma",
"lnurl.confirmed.when": "confermato {when}",
"lnurl.fee.base.one": "{n} sat",
"lnurl.fee.base.other": "{n} sat",
"lnurl.fee.percent": "{percent}%",
"home.topLnurl.title": "Migliori mint LNURL",
"home.topLnurl.sub": "ordinati per recensioni e valutazione",
"home.topLnurl.all": "Tutti i {n} mint LNURL →",
"feed.ecosystem.lnurl": "LNURL",
"reviews.dialog.bodyPlaceholder.lnurl": "Come sono andati minting, melting e la rotazione delle banconote? Sii concreto.",
"reviews.byUrl.open": "Scrivi una recensione",
"reviews.byUrl.title": "Scrivi una recensione",
"reviews.byUrl.lede": "Incolla l’indirizzo. Se questo sito non lo conosce ancora, viene controllato e aggiunto ora.",
"reviews.byUrl.label.cashu": "URL del mint",
"reviews.byUrl.label.fedimint": "Codice di invito della federazione",
"reviews.byUrl.label.lnurl": "URL del mint LNURL",
"reviews.byUrl.placeholder.cashu": "mint.example.com",
"reviews.byUrl.placeholder.fedimint": "fed11…",
"reviews.byUrl.placeholder.lnurl": "mint.example.com",
"reviews.byUrl.check": "Controlla questo indirizzo",
"reviews.byUrl.checking": "Controllo in corso…",
"reviews.byUrl.back": "Usa un altro indirizzo",
"reviews.byUrl.existing": "Già elencato",
"reviews.byUrl.indexed": "Appena aggiunto",
"reviews.byUrl.confirmClose": "Chiudere? L’indirizzo digitato non viene conservato.",
"reviews.byUrl.detected": "Quell’indirizzo ha risposto come un mint {kind}.",
"reviews.byUrl.switchTo": "Recensiscilo come mint {kind}",
"reviews.byUrl.error.empty": "Inserisci prima un indirizzo.",
"reviews.byUrl.error.badUrl": "Non sembra l’URL di un mint.",
"reviews.byUrl.error.badInvite": "Non è un codice di invito Fedimint. I codici iniziano con fed11.",
"reviews.byUrl.error.badInput": "Questo sito non può controllare quell’indirizzo.",
"reviews.byUrl.error.blocked": "Non è un indirizzo pubblico, quindi non può essere controllato.",
"reviews.byUrl.error.notAMint": "Quell’indirizzo ha risposto, ma non come un mint di questo tipo.",
"reviews.byUrl.error.wrongType": "Quell’indirizzo ha risposto, ma come un mint di un altro tipo.",
"reviews.byUrl.error.unverifiable": "A quell’indirizzo non ha risposto nulla, e su Nostr non c’è ancora traccia.",
"reviews.byUrl.error.rateLimited.one": "Sono molti controlli da una sola connessione. Riprova tra circa {n} minuto.",
"reviews.byUrl.error.rateLimited.other": "Sono molti controlli da una sola connessione. Riprova tra circa {n} minuti.",
"reviews.byUrl.error.network": "Non è stato possibile raggiungere l’indice. Controlla la connessione e riprova.",
"seo.lnurl.titleNoReviews": "{name}: informazioni e recensioni del mint LNURL | Cashumints.space",
"seo.desc.statusWithdraw": "{status}, prelievo {min}–{max} sat.",
"mint.chip.noFunding": "Né mint né melt",
"mint.warnings.lnurlWithdrawalsDisabled.lead": "Prelievi disattivati.",
"mint.warnings.lnurlWithdrawalsDisabled.body.online": "Questo mint dichiara un prelievo massimo pari a zero, quindi nessuna banconota che emette può essere riscossa per qualcosa. Non fare minting qui finché non cambia.",
"mint.warnings.lnurlWithdrawalsDisabled.body.offline": "Questo mint dichiarava un prelievo massimo pari a zero l'ultima volta che è stato raggiunto, quindi nessuna banconota che emetteva poteva essere riscossa per qualcosa. Non fare minting qui finché non cambia.",
"mint.warnings.lnurlWithdrawalsDisabled.meta.online": "Prelievi attualmente disattivati",
"mint.warnings.lnurlWithdrawalsDisabled.meta.offline": "I prelievi erano disattivati all'ultimo contatto",
"mint.warnings.lnurlInvalid.lead": "L'endpoint risponde ma non è valido.",
"mint.warnings.lnurlInvalid.body": "L'host risponde, ma non con un annuncio di mint che questo sito sappia leggere, quindi i prelievi potrebbero non funzionare. Quello che segue è l'ultimo stato leggibile, e le recensioni funzionano ancora.",
"mint.warnings.lnurlInvalid.meta": "Risponde, ma non con un annuncio di mint valido",
"mint.warnings.lnurlNoFunding.lead": "Minting e melting non disponibili.",
"mint.warnings.lnurlNoFunding.body": "Le banconote esistenti si possono ancora ruotare, dividere o unire, ma al momento nulla entra o esce via Lightning. Il mint è attivo e risponde; il nodo Lightning che sta dietro non è raggiungibile da lì.",
"mint.warnings.lnurlNoFunding.meta": "Minting e melting non disponibili",
"mint.warnings.also.noFunding": "Anche il suo nodo Lightning era irraggiungibile, quindi non si poteva fare né minting né melting."
} }
+118 -3
View File
@@ -114,6 +114,8 @@
"card.reviewed": "レビュー {when}", "card.reviewed": "レビュー {when}",
"card.noReviews": "レビューはまだありません", "card.noReviews": "レビューはまだありません",
"mint.title": "{name} · Cashuミントレビュー", "mint.title": "{name} · Cashuミントレビュー",
"mint.titleFedimint": "{name} · Fedimint フェデレーションのレビュー",
"mint.titleLnurl": "{name} · LNURL mint のレビュー",
"mint.breadcrumb": "パン粉", "mint.breadcrumb": "パン粉",
"mint.crumbMints": "キャッシュミント", "mint.crumbMints": "キャッシュミント",
"mint.copyUrl": "ミントURLをコピーする", "mint.copyUrl": "ミントURLをコピーする",
@@ -325,7 +327,7 @@
"reviews.validate.short": "少なくとも{n}文字を書くと、レビューが便利です。", "reviews.validate.short": "少なくとも{n}文字を書くと、レビューが便利です。",
"reviews.validate.long": "レビューは{n}文字に制限されています。", "reviews.validate.long": "レビューは{n}文字に制限されています。",
"feed.title": "最新のミントとフェデレーションレビュー・カウンティン。 スペース", "feed.title": "最新のミントとフェデレーションレビュー・カウンティン。 スペース",
"feed.description": "すべてのミントとフェデレーションのレビューが公開されています: {reviews} レビューを渡る {mints} Cashu mints と {federations} Fedimint のフェデレーション, ノストに署名し、ここ最新で読みやすく. 生態系、評価、または名前でフィルタリングします。", "feed.description": "ミント、フェデレーション、LNURL ミントのレビューを公開され次第すべて掲載:{mints} 件の Cashu ミント、{federations} 件の Fedimint フェデレーション、{lnurl} 件の LNURL ミントにわたる {reviews} 件のレビューを、Nostr 上で署名された新しい順に読めます。エコシステム、評価、名前で絞り込めます。",
"feed.heading": "最新のレビュー", "feed.heading": "最新のレビュー",
"feed.lede": "すべての Cashu mint と Fedimint のフェデレーションのすべてのレビューは、Nostr リレーからライブを最初に読みます。 それぞれがその著者によって署名され、私たちのデータベースではなく、リレーに住んでいます。", "feed.lede": "すべての Cashu mint と Fedimint のフェデレーションのすべてのレビューは、Nostr リレーからライブを最初に読みます。 それぞれがその著者によって署名され、私たちのデータベースではなく、リレーに住んでいます。",
"feed.label": "レビュー", "feed.label": "レビュー",
@@ -400,7 +402,14 @@
"notfound.fb.reviews": "レビュー", "notfound.fb.reviews": "レビュー",
"notfound.fb.software": "ソフトウェア", "notfound.fb.software": "ソフトウェア",
"notfound.fb.lastOnline": "最新オンライン", "notfound.fb.lastOnline": "最新オンライン",
"notfound.fb.note": "サイトのビルド後、この mint がインデックス化されました。ライブ API のサマリーです。 次の再ビルドでレビュー付きのフルページが表示されます。", "notfound.fb.note": "この mint はサイトの最終ビルド後にインデックスされたため、これはライブ API からの要約で、下にレビューがあります。完全なページは次回のビルドで表示されます。",
"notfound.resolve.checking": "初めての訪問です。この mint を今確認しています。",
"notfound.resolve.failed": "この mint は追加できませんでした",
"notfound.resolve.failedFedimint": "このフェデレーションは追加できませんでした",
"notfound.resolve.retry": "別のアドレスを試す",
"notfound.resolve.switch": "{kind} の mint として開く",
"notfound.resolve.fedimint": "フェデレーションのページアドレスには invite code が含まれないため、リンクだけでは見つけられません。invite code を貼り付ければ追加されます。",
"notfound.resolve.ambiguous": "このアドレスはリンクだけからは特定できません。mint の完全な URL を貼り付ければ追加されます。",
"wallets.title": "Cashuウォレット・Cashumints スペース", "wallets.title": "Cashuウォレット・Cashumints スペース",
"wallets.description": "モバイル、ウェブ、ブラウザ、コマンドラインで、ここにリストされているミントで動作するキャッシュウォレットは、それぞれが得意です。", "wallets.description": "モバイル、ウェブ、ブラウザ、コマンドラインで、ここにリストされているミントで動作するキャッシュウォレットは、それぞれが得意です。",
"wallets.heading": "ウォレット", "wallets.heading": "ウォレット",
@@ -654,5 +663,111 @@
"footer.allFedimints": "すべてのフェデレーション", "footer.allFedimints": "すべてのフェデレーション",
"reviews.dialog.bodyPlaceholder.cashu": "鉱山、溶融、サポートはどのように機能しましたか? 具体的に。", "reviews.dialog.bodyPlaceholder.cashu": "鉱山、溶融、サポートはどのように機能しましたか? 具体的に。",
"reviews.dialog.bodyPlaceholder.fedimint": "参加方法、支払いのLightning、サポートはどのように機能しますか? 具体的に。", "reviews.dialog.bodyPlaceholder.fedimint": "参加方法、支払いのLightning、サポートはどのように機能しますか? 具体的に。",
"fedimint.details.title": "フェデレーションの詳細" "fedimint.details.title": "フェデレーションの詳細",
"nav.lnurlMints": "LNURL ミント",
"footer.allLnurlMints": "すべての LNURL ミント",
"lnurlMints.title": "LNURL ミント:lnurlcash 無記名ノートのミント評価 | Cashumints.space",
"lnurlMints.description": "Nostr ネットワークから索引した {total} 件の LNURL ミント:オンライン {online} 件、オフライン {offline} 件。各ミントのコミュニティ評価、出金上限、機能を掲載。",
"lnurlMints.heading": "LNURL ミント",
"lnurlMints.lede": "このサイトが把握しているすべての LNURL ミント。LNURL ミントは lnurlcash 無記名ノートを発行します。シークレットとして保持する Lightning のノートで、ローテーション・分割・統合、あるいは Lightning 支払いへメルトして戻すことができます。各ミントは数分ごとに HTTPS で確認しています。",
"lnurlMints.search.label": "LNURL ミントを検索",
"lnurlMints.search.placeholder": "名前またはアドレスで検索…",
"lnurlMints.sort.label": "LNURL ミントの並び順",
"lnurlMints.hideOffline": "オフラインを隠す",
"lnurlMints.showing": "{total} 件の LNURL ミントを表示中(オンライン {online} 件、オフライン {offline} 件)",
"lnurlMints.showingFiltered": "{total} 件中 {shown} 件の LNURL ミントを表示中(オンライン {online} 件、オフライン {offline} 件)",
"lnurlMints.noResults": "該当する LNURL ミントはありません。",
"lnurlMints.emptyIndex": "まだ LNURL ミントは索引されていません。",
"lnurl.crumb": "LNURL ミント",
"lnurl.breadcrumb": "パンくずリスト",
"lnurl.copyAddress": "アドレスをコピー",
"lnurl.more.title": "他の LNURL ミント",
"lnurl.more.all": "すべての LNURL ミント →",
"lnurl.disclaimer": "LNURL ミントはあなたの sats を預かり、あなたはそのノートを持ちます。つまり利用は運営者を信頼するということです。評価はコミュニティの意見であり、オンラインは到達可能という意味で、信頼できるという意味ではありません。",
"lnurl.features.title": "機能",
"lnurl.features.none": "このミントは対応機能を告知していません。",
"lnurl.features.note": "機能はミントの運営者が Nostr で告知した内容です。HTTPS で確認できるものは確認し、告知された機能が現在利用できない場合はその旨を表示します。",
"lnurl.features.notListed": "未告知",
"lnurl.features.unavailable": "利用不可",
"lnurl.feature.mint": "Lightning でミント(LUD-06)",
"lnurl.feature.melt": "Lightning へメルト",
"lnurl.feature.notes": "ノートのローテーション / 分割 / 統合",
"lnurl.feature.lud16": "Lightning アドレス",
"lnurl.feature.lud21": "支払い検証(LUD-21)",
"lnurl.feature.signed-notes": "署名付きノート(オフライン検証)",
"lnurl.feature.onion": "Tor アドレス",
"lnurl.feature.rotate": "ノートをローテーション",
"lnurl.feature.split": "ノートを分割",
"lnurl.feature.merge": "ノートを統合",
"lnurl.feature.lud06": "LUD-06 payRequest",
"lnurl.feature.lud03": "LUD-03 withdrawRequest",
"lnurl.limits.label": "出金上限",
"lnurl.limits.notPublished": "未公開",
"lnurl.limits.withdrawRow": "出金",
"lnurl.limits.disabled": "無効",
"lnurl.limits.sats.one": "{n} sat",
"lnurl.limits.sats.other": "{n} sats",
"lnurl.details.title": "ミントの詳細",
"lnurl.details.address": "Lightning アドレス",
"lnurl.details.mintPubkey": "ミントの公開鍵",
"lnurl.details.onion": "Tor アドレス",
"lnurl.details.fee": "ミント手数料",
"lnurl.details.node": "ノード",
"lnurl.details.capacity": "告知済みキャパシティ",
"lnurl.kind.lightningAddress": "Lightning アドレス",
"lnurl.kind.mintPubkey": "ミントの公開鍵",
"lnurl.kind.onionAddress": "Tor アドレス",
"lnurl.confirmed.label": "最終確認",
"lnurl.confirmed.when": "{when}に確認",
"lnurl.fee.base.one": "{n} sat",
"lnurl.fee.base.other": "{n} sats",
"lnurl.fee.percent": "{percent}%",
"home.topLnurl.title": "人気の LNURL ミント",
"home.topLnurl.sub": "レビュー数と評価で順位付け",
"home.topLnurl.all": "全 {n} 件の LNURL ミント →",
"feed.ecosystem.lnurl": "LNURL",
"reviews.dialog.bodyPlaceholder.lnurl": "ミント、メルト、ノートのローテーションはどうでしたか?具体的にどうぞ。",
"reviews.byUrl.open": "レビューを書く",
"reviews.byUrl.title": "レビューを書く",
"reviews.byUrl.lede": "アドレスを貼り付けてください。このサイトがまだ知らないアドレスなら、今すぐ確認して追加します。",
"reviews.byUrl.label.cashu": "mint の URL",
"reviews.byUrl.label.fedimint": "フェデレーションの invite code",
"reviews.byUrl.label.lnurl": "LNURL mint の URL",
"reviews.byUrl.placeholder.cashu": "mint.example.com",
"reviews.byUrl.placeholder.fedimint": "fed11…",
"reviews.byUrl.placeholder.lnurl": "mint.example.com",
"reviews.byUrl.check": "このアドレスを確認",
"reviews.byUrl.checking": "確認中…",
"reviews.byUrl.back": "別のアドレスを使う",
"reviews.byUrl.existing": "すでに掲載済み",
"reviews.byUrl.indexed": "追加しました",
"reviews.byUrl.confirmClose": "閉じますか?入力したアドレスは保存されません。",
"reviews.byUrl.detected": "このアドレスは {kind} の mint として応答しました。",
"reviews.byUrl.switchTo": "{kind} の mint としてレビューする",
"reviews.byUrl.error.empty": "まずアドレスを入力してください。",
"reviews.byUrl.error.badUrl": "mint の URL には見えません。",
"reviews.byUrl.error.badInvite": "これは Fedimint の invite code ではありません。コードは fed11 で始まります。",
"reviews.byUrl.error.badInput": "このサイトではそのアドレスを確認できません。",
"reviews.byUrl.error.blocked": "公開アドレスではないため、確認できません。",
"reviews.byUrl.error.notAMint": "そのアドレスは応答しましたが、この種類の mint ではありませんでした。",
"reviews.byUrl.error.wrongType": "そのアドレスは応答しましたが、別の種類の mint でした。",
"reviews.byUrl.error.unverifiable": "そのアドレスからは何も応答がなく、Nostr にもまだ痕跡がありません。",
"reviews.byUrl.error.rateLimited.one": "同じ接続からの確認が多すぎます。約 {n} 分後にもう一度お試しください。",
"reviews.byUrl.error.rateLimited.other": "同じ接続からの確認が多すぎます。約 {n} 分後にもう一度お試しください。",
"reviews.byUrl.error.network": "インデックスに接続できませんでした。接続を確認してからもう一度お試しください。",
"seo.lnurl.titleNoReviews": "{name}:LNURL ミントの情報とレビュー | Cashumints.space",
"seo.desc.statusWithdraw": "{status}、出金 {min}〜{max} sat。",
"mint.chip.noFunding": "ミント / メルト不可",
"mint.warnings.lnurlWithdrawalsDisabled.lead": "出金は無効です。",
"mint.warnings.lnurlWithdrawalsDisabled.body.online": "このミントは出金上限をゼロと告知しているため、発行されるノートは何とも交換できません。これが変わるまでここでミントしないでください。",
"mint.warnings.lnurlWithdrawalsDisabled.body.offline": "最後に到達できたとき、このミントは出金上限をゼロと告知していたため、発行されたノートは何とも交換できませんでした。これが変わるまでここでミントしないでください。",
"mint.warnings.lnurlWithdrawalsDisabled.meta.online": "現在、出金は無効",
"mint.warnings.lnurlWithdrawalsDisabled.meta.offline": "最後に確認した時点で出金は無効でした",
"mint.warnings.lnurlInvalid.lead": "エンドポイントは応答しますが無効です。",
"mint.warnings.lnurlInvalid.body": "ホストは応答しますが、このサイトが読めるミントの告知ではないため、出金が機能しない可能性があります。以下は最後に解析できた状態で、レビューは引き続き利用できます。",
"mint.warnings.lnurlInvalid.meta": "応答はあるが、有効なミント告知ではない",
"mint.warnings.lnurlNoFunding.lead": "ミントとメルトは利用できません。",
"mint.warnings.lnurlNoFunding.body": "既存のノートはローテーション・分割・統合が引き続き可能ですが、現在 Lightning 経由で資金は出入りしません。ミント自体は稼働して応答していますが、その背後の Lightning ノードに到達できていません。",
"mint.warnings.lnurlNoFunding.meta": "ミントとメルトは利用不可",
"mint.warnings.also.noFunding": "Lightning ノードにも到達できず、ミントもメルトもできませんでした。"
} }
+1 -26
View File
@@ -11,29 +11,4 @@
*/ */
export const DEFAULT_LOCALE = 'en'; export const DEFAULT_LOCALE = 'en';
export const LOCALE_CODES = [ export const LOCALE_CODES = ['en', 'es', 'nl'];
'en',
'es',
'nl',
'zh',
'hi',
'ar',
'fr',
'pt',
'it',
'tr',
'pl',
'uk',
'ro',
'el',
'cs',
'sv',
'da',
'id',
'ur',
'ru',
'de',
'ja',
'ar-eg',
'vi',
];
+122 -3
View File
@@ -50,6 +50,10 @@
"footer.privacy": "Privacy", "footer.privacy": "Privacy",
"footer.madeBy": "Gemaakt met ❤️ door {azzamo} · Draait op Cashu en NIP-87", "footer.madeBy": "Gemaakt met ❤️ door {azzamo} · Draait op Cashu en NIP-87",
"footer.warning": "Mints zijn custodiaal. Een mint kan er met jouw geld vandoor gaan.", "footer.warning": "Mints zijn custodiaal. Een mint kan er met jouw geld vandoor gaan.",
"footer.tagline": "Reviews uit de community voor ecash mints, ondertekend op Nostr.",
"footer.col.explore": "Verkennen",
"footer.col.site": "Site",
"footer.nav": "Voettekst",
"home.title": "Cashumints.space · Vind een Cashu mint die je kunt vertrouwen", "home.title": "Cashumints.space · Vind een Cashu mint die je kunt vertrouwen",
"home.description": "Vind een Cashu mint die je kunt vertrouwen. {mints} mints geïndexeerd met {reviews} reviews uit de community, ondertekend op Nostr, plus live uptime, ondersteunde NUTs en mint-software.", "home.description": "Vind een Cashu mint die je kunt vertrouwen. {mints} mints geïndexeerd met {reviews} reviews uit de community, ondertekend op Nostr, plus live uptime, ondersteunde NUTs en mint-software.",
"home.hero.title": "Vind een Cashu mint", "home.hero.title": "Vind een Cashu mint",
@@ -114,6 +118,8 @@
"card.reviewed": "laatste review {when}", "card.reviewed": "laatste review {when}",
"card.noReviews": "nog geen reviews", "card.noReviews": "nog geen reviews",
"mint.title": "{name} · Reviews van deze Cashu mint", "mint.title": "{name} · Reviews van deze Cashu mint",
"mint.titleFedimint": "{name} · Reviews van de Fedimint-federatie",
"mint.titleLnurl": "{name} · Reviews van de LNURL mint",
"mint.breadcrumb": "Kruimelpad", "mint.breadcrumb": "Kruimelpad",
"mint.crumbMints": "Cashu mints", "mint.crumbMints": "Cashu mints",
"mint.copyUrl": "Mint-URL kopiëren", "mint.copyUrl": "Mint-URL kopiëren",
@@ -325,7 +331,7 @@
"reviews.validate.short": "Schrijf minstens {n} tekens, anders heeft niemand iets aan de review.", "reviews.validate.short": "Schrijf minstens {n} tekens, anders heeft niemand iets aan de review.",
"reviews.validate.long": "Reviews mogen maximaal {n} tekens lang zijn.", "reviews.validate.long": "Reviews mogen maximaal {n} tekens lang zijn.",
"feed.title": "Laatste reviews van mints en federaties · Cashumints.space", "feed.title": "Laatste reviews van mints en federaties · Cashumints.space",
"feed.description": "Elke review van een mint of federatie zodra hij verschijnt: {reviews} reviews over {mints} Cashu mints en {federations} Fedimint-federaties, ondertekend op Nostr en hier te lezen met de nieuwste bovenaan. Filter op ecosysteem, op beoordeling of op naam.", "feed.description": "Elke beoordeling van een mint, federatie en LNURL mint zodra die verschijnt: {reviews} beoordelingen over {mints} Cashu mints, {federations} Fedimint-federaties en {lnurl} LNURL mints, ondertekend op Nostr en hier te lezen met de nieuwste bovenaan. Filter op ecosysteem, op cijfer of op naam.",
"feed.heading": "Laatste reviews", "feed.heading": "Laatste reviews",
"feed.lede": "Elke review van elke Cashu mint en elke Fedimint-federatie, nieuwste bovenaan, live gelezen van de Nostr-relays. Elke review is ondertekend door wie hem schreef en staat op de relays, niet in onze database.", "feed.lede": "Elke review van elke Cashu mint en elke Fedimint-federatie, nieuwste bovenaan, live gelezen van de Nostr-relays. Elke review is ondertekend door wie hem schreef en staat op de relays, niet in onze database.",
"feed.label": "Reviews", "feed.label": "Reviews",
@@ -400,7 +406,14 @@
"notfound.fb.reviews": "Reviews", "notfound.fb.reviews": "Reviews",
"notfound.fb.software": "Software", "notfound.fb.software": "Software",
"notfound.fb.lastOnline": "Laatst online", "notfound.fb.lastOnline": "Laatst online",
"notfound.fb.note": "Deze mint is geïndexeerd nadat de site voor het laatst gebouwd is, dus dit is een samenvatting uit de live API. De volledige pagina met reviews verschijnt bij de volgende build.", "notfound.fb.note": "Deze mint is geïndexeerd nadat de site voor het laatst gebouwd werd, dus dit is een samenvatting uit de live API, met de reviews eronder. De volledige pagina verschijnt bij de volgende build.",
"notfound.resolve.checking": "Eerste bezoek: deze mint wordt nu gecontroleerd.",
"notfound.resolve.failed": "Deze mint kon niet worden toegevoegd",
"notfound.resolve.failedFedimint": "Deze federatie kon niet worden toegevoegd",
"notfound.resolve.retry": "Een ander adres proberen",
"notfound.resolve.switch": "Openen als {kind} mint",
"notfound.resolve.fedimint": "Het paginadres van een federatie bevat de invite code niet, dus deze is niet op te zoeken met alleen de link. Plak de invite code en hij wordt toegevoegd.",
"notfound.resolve.ambiguous": "Dit adres is niet af te leiden uit alleen de link. Plak de volledige URL van de mint en hij wordt toegevoegd.",
"wallets.title": "Cashu wallets · Cashumints.space", "wallets.title": "Cashu wallets · Cashumints.space",
"wallets.description": "Cashu wallets die werken met de mints die hier staan, voor mobiel, web, browser en de opdrachtregel, met waar elke wallet goed in is.", "wallets.description": "Cashu wallets die werken met de mints die hier staan, voor mobiel, web, browser en de opdrachtregel, met waar elke wallet goed in is.",
"wallets.heading": "Wallets", "wallets.heading": "Wallets",
@@ -654,5 +667,111 @@
"footer.allFedimints": "Alle federaties", "footer.allFedimints": "Alle federaties",
"reviews.dialog.bodyPlaceholder.cashu": "Hoe gingen minten, melten en de support? Wees concreet.", "reviews.dialog.bodyPlaceholder.cashu": "Hoe gingen minten, melten en de support? Wees concreet.",
"reviews.dialog.bodyPlaceholder.fedimint": "Hoe gingen meedoen, Lightning-betalingen en de support? Wees concreet.", "reviews.dialog.bodyPlaceholder.fedimint": "Hoe gingen meedoen, Lightning-betalingen en de support? Wees concreet.",
"fedimint.details.title": "Gegevens van de federatie" "fedimint.details.title": "Gegevens van de federatie",
"nav.lnurlMints": "LNURL mints",
"footer.allLnurlMints": "Alle LNURL mints",
"lnurlMints.title": "LNURL mints: mints voor lnurlcash toonderbriefjes, beoordeeld | Cashumints.space",
"lnurlMints.description": "Alle {total} LNURL mints die vanaf het Nostr-netwerk zijn geïndexeerd: {online} online, {offline} offline. Beoordelingen van de gemeenschap, opnamelimieten en mogelijkheden per mint.",
"lnurlMints.heading": "LNURL mints",
"lnurlMints.lede": "Elke LNURL mint die deze site kent. Een LNURL mint geeft lnurlcash toonderbriefjes uit: Lightning-briefjes die je als geheim bewaart en die je kunt roteren, splitsen, samenvoegen of terug melten naar een Lightning-betaling. Elke mint wordt om de paar minuten via HTTPS gecontroleerd.",
"lnurlMints.search.label": "Zoek LNURL mints",
"lnurlMints.search.placeholder": "Zoek op naam of adres…",
"lnurlMints.sort.label": "Sorteer LNURL mints op",
"lnurlMints.hideOffline": "Verberg offline",
"lnurlMints.showing": "{total} LNURL mints getoond ({online} online, {offline} offline)",
"lnurlMints.showingFiltered": "{shown} van {total} LNURL mints getoond ({online} online, {offline} offline)",
"lnurlMints.noResults": "Geen LNURL mint komt daarmee overeen.",
"lnurlMints.emptyIndex": "Er zijn nog geen LNURL mints geïndexeerd.",
"lnurl.crumb": "LNURL mints",
"lnurl.breadcrumb": "Kruimelpad",
"lnurl.copyAddress": "Adres kopiëren",
"lnurl.more.title": "Meer LNURL mints",
"lnurl.more.all": "Alle LNURL mints →",
"lnurl.disclaimer": "Een LNURL mint houdt jouw sats vast terwijl jij zijn briefjes vasthoudt, dus er een gebruiken betekent de beheerder vertrouwen. Beoordelingen zijn meningen van de gemeenschap, en online betekent bereikbaar, niet betrouwbaar.",
"lnurl.features.title": "Functies",
"lnurl.features.none": "Deze mint heeft niet aangekondigd wat hij ondersteunt.",
"lnurl.features.note": "Functies zijn wat de beheerder van de mint op Nostr heeft aangekondigd. Waar deze site er een via HTTPS kan controleren doet hij dat, en hij zegt het als iets aangekondigds nu niet beschikbaar is.",
"lnurl.features.notListed": "niet aangekondigd",
"lnurl.features.unavailable": "niet beschikbaar",
"lnurl.feature.mint": "Minten via Lightning (LUD-06)",
"lnurl.feature.melt": "Melten naar Lightning",
"lnurl.feature.notes": "Briefjes roteren / splitsen / samenvoegen",
"lnurl.feature.lud16": "Lightning-adres",
"lnurl.feature.lud21": "Betalingsverificatie (LUD-21)",
"lnurl.feature.signed-notes": "Ondertekende briefjes (offline verificatie)",
"lnurl.feature.onion": "Tor-adres",
"lnurl.feature.rotate": "Een briefje roteren",
"lnurl.feature.split": "Een briefje splitsen",
"lnurl.feature.merge": "Briefjes samenvoegen",
"lnurl.feature.lud06": "LUD-06 payRequest",
"lnurl.feature.lud03": "LUD-03 withdrawRequest",
"lnurl.limits.label": "Opnamelimieten",
"lnurl.limits.notPublished": "niet gepubliceerd",
"lnurl.limits.withdrawRow": "Opnemen",
"lnurl.limits.disabled": "uitgeschakeld",
"lnurl.limits.sats.one": "{n} sat",
"lnurl.limits.sats.other": "{n} sats",
"lnurl.details.title": "Gegevens van de mint",
"lnurl.details.address": "Lightning-adres",
"lnurl.details.mintPubkey": "Publieke sleutel van de mint",
"lnurl.details.onion": "Tor-adres",
"lnurl.details.fee": "Mintkosten",
"lnurl.details.node": "Node",
"lnurl.details.capacity": "Aangekondigde capaciteit",
"lnurl.kind.lightningAddress": "Lightning-adres",
"lnurl.kind.mintPubkey": "publieke sleutel van de mint",
"lnurl.kind.onionAddress": "Tor-adres",
"lnurl.confirmed.label": "Laatst bevestigd",
"lnurl.confirmed.when": "bevestigd {when}",
"lnurl.fee.base.one": "{n} sat",
"lnurl.fee.base.other": "{n} sats",
"lnurl.fee.percent": "{percent}%",
"home.topLnurl.title": "Top LNURL mints",
"home.topLnurl.sub": "gerangschikt op beoordelingen en cijfer",
"home.topLnurl.all": "Alle {n} LNURL mints →",
"feed.ecosystem.lnurl": "LNURL",
"reviews.dialog.bodyPlaceholder.lnurl": "Hoe gingen minten, melten en het roteren van briefjes? Wees concreet.",
"reviews.byUrl.open": "Review schrijven",
"reviews.byUrl.title": "Review schrijven",
"reviews.byUrl.lede": "Plak het adres. Kent deze site het nog niet, dan wordt het nu gecontroleerd en toegevoegd.",
"reviews.byUrl.label.cashu": "Mint-URL",
"reviews.byUrl.label.fedimint": "Invite code van de federatie",
"reviews.byUrl.label.lnurl": "URL van de LNURL mint",
"reviews.byUrl.placeholder.cashu": "mint.example.com",
"reviews.byUrl.placeholder.fedimint": "fed11…",
"reviews.byUrl.placeholder.lnurl": "mint.example.com",
"reviews.byUrl.check": "Dit adres controleren",
"reviews.byUrl.checking": "Controleren…",
"reviews.byUrl.back": "Ander adres gebruiken",
"reviews.byUrl.existing": "Staat er al bij",
"reviews.byUrl.indexed": "Net toegevoegd",
"reviews.byUrl.confirmClose": "Sluiten? Het adres dat je typte wordt niet bewaard.",
"reviews.byUrl.detected": "Dat adres antwoordde als een {kind} mint.",
"reviews.byUrl.switchTo": "Als {kind} mint reviewen",
"reviews.byUrl.error.empty": "Vul eerst een adres in.",
"reviews.byUrl.error.badUrl": "Dat lijkt geen mint-URL.",
"reviews.byUrl.error.badInvite": "Dat is geen Fedimint invite code. Codes beginnen met fed11.",
"reviews.byUrl.error.badInput": "Deze site kan dat adres niet controleren.",
"reviews.byUrl.error.blocked": "Dat is geen openbaar adres, dus het kan niet gecontroleerd worden.",
"reviews.byUrl.error.notAMint": "Dat adres antwoordde, maar niet als een mint van dit soort.",
"reviews.byUrl.error.wrongType": "Dat adres antwoordde, maar als een ander soort mint.",
"reviews.byUrl.error.unverifiable": "Op dat adres reageerde niets, en op Nostr is er nog geen spoor van te vinden.",
"reviews.byUrl.error.rateLimited.one": "Dat zijn veel controles vanaf één verbinding. Probeer het over ongeveer {n} minuut opnieuw.",
"reviews.byUrl.error.rateLimited.other": "Dat zijn veel controles vanaf één verbinding. Probeer het over ongeveer {n} minuten opnieuw.",
"reviews.byUrl.error.network": "De index was niet bereikbaar. Controleer je verbinding en probeer het opnieuw.",
"seo.lnurl.titleNoReviews": "{name}: info en beoordelingen van de LNURL mint | Cashumints.space",
"seo.desc.statusWithdraw": "{status}, opname {min}–{max} sat.",
"mint.chip.noFunding": "Geen mint / melt",
"mint.warnings.lnurlWithdrawalsDisabled.lead": "Opnemen staat uit.",
"mint.warnings.lnurlWithdrawalsDisabled.body.online": "Deze mint adverteert een maximale opname van nul, dus geen enkel briefje dat hij uitgeeft kan voor iets worden ingewisseld. Mint hier niet totdat dat verandert.",
"mint.warnings.lnurlWithdrawalsDisabled.body.offline": "Deze mint adverteerde een maximale opname van nul toen hij voor het laatst bereikt werd, dus geen enkel briefje dat hij uitgaf kon voor iets worden ingewisseld. Mint hier niet totdat dat verandert.",
"mint.warnings.lnurlWithdrawalsDisabled.meta.online": "Opnemen staat momenteel uit",
"mint.warnings.lnurlWithdrawalsDisabled.meta.offline": "Opnemen stond uit toen hij voor het laatst gezien werd",
"mint.warnings.lnurlInvalid.lead": "Endpoint antwoordt maar is ongeldig.",
"mint.warnings.lnurlInvalid.body": "De host antwoordt, maar niet met een mintaankondiging die deze site kan lezen, dus opnemen werkt mogelijk niet. Alles hieronder is de laatste staat die wel te lezen was, en beoordelingen werken nog steeds.",
"mint.warnings.lnurlInvalid.meta": "Antwoordt, maar niet met een geldige mintaankondiging",
"mint.warnings.lnurlNoFunding.lead": "Minten en melten niet beschikbaar.",
"mint.warnings.lnurlNoFunding.body": "Bestaande briefjes kunnen nog steeds geroteerd, gesplitst of samengevoegd worden, maar er gaat nu niets in of uit via Lightning. De mint zelf is bereikbaar en antwoordt; de Lightning-node erachter is vanaf daar niet bereikbaar.",
"mint.warnings.lnurlNoFunding.meta": "Minten en melten niet beschikbaar",
"mint.warnings.also.noFunding": "De Lightning-node was ook onbereikbaar, dus er kon niets gemint of gemelt worden."
} }
+122 -3
View File
@@ -116,6 +116,8 @@
"card.reviewed": "Recenzja {when}", "card.reviewed": "Recenzja {when}",
"card.noReviews": "Brak jeszcze recenzji", "card.noReviews": "Brak jeszcze recenzji",
"mint.title": "{name} · Recenzje Cashu Mint", "mint.title": "{name} · Recenzje Cashu Mint",
"mint.titleFedimint": "{name} · Recenzje federacji Fedimint",
"mint.titleLnurl": "{name} · Recenzje mintu LNURL",
"mint.breadcrumb": "Okruszek chleba", "mint.breadcrumb": "Okruszek chleba",
"mint.crumbMints": "Mennice cashu", "mint.crumbMints": "Mennice cashu",
"mint.copyUrl": "Skopiuj URL Mint", "mint.copyUrl": "Skopiuj URL Mint",
@@ -351,7 +353,7 @@
"reviews.validate.short": "Napisz przynajmniej {n} postaci, żeby recenzja była przydatna.", "reviews.validate.short": "Napisz przynajmniej {n} postaci, żeby recenzja była przydatna.",
"reviews.validate.long": "Recenzje ograniczają się do {n} postaci.", "reviews.validate.long": "Recenzje ograniczają się do {n} postaci.",
"feed.title": "Najnowsze recenzje mennic i federacji · Cashumints.space", "feed.title": "Najnowsze recenzje mennic i federacji · Cashumints.space",
"feed.description": "Każda recenzja mennicy i federacji w miarę publikacji: {reviews} recenzje {mints} w mennicach Cashu i {federations} federacjach Fedimint, podpisane na Nostr i dostępne tutaj najnowsze. Filtruj według ekosystemu, oceny lub nazwy.", "feed.description": "Każda opinia o mincie, federacji i mincie LNURL zaraz po opublikowaniu: {reviews} opinii o {mints} mintach Cashu, {federations} federacjach Fedimint i {lnurl} mintach LNURL, podpisanych w Nostr i czytelnych tutaj od najnowszych. Filtruj po ekosystemie, ocenie lub nazwie.",
"feed.heading": "Najnowsze recenzje", "feed.heading": "Najnowsze recenzje",
"feed.lede": "Każda recenzja każdej mennicy Cashu i federacji Fedimint, najnowsza, czytana na żywo z przekaźników Nostr. Każdy z nich jest podpisany przez autora i znajduje się na przekaźnikach, a nie w naszej bazie danych.", "feed.lede": "Każda recenzja każdej mennicy Cashu i federacji Fedimint, najnowsza, czytana na żywo z przekaźników Nostr. Każdy z nich jest podpisany przez autora i znajduje się na przekaźnikach, a nie w naszej bazie danych.",
"feed.label": "Recenzje", "feed.label": "Recenzje",
@@ -428,7 +430,14 @@
"notfound.fb.reviews": "Recenzje", "notfound.fb.reviews": "Recenzje",
"notfound.fb.software": "Oprogramowanie", "notfound.fb.software": "Oprogramowanie",
"notfound.fb.lastOnline": "Ostatni czas online", "notfound.fb.lastOnline": "Ostatni czas online",
"notfound.fb.note": "Ta mennica została zindeksowana po ostatnim uruchomieniu strony, więc to jest podsumowanie z aktywnego API. Pełna strona z recenzjami pojawia się przy kolejnej odbudowie.", "notfound.fb.note": "Ten mint został zaindeksowany po ostatnim zbudowaniu strony, więc to podsumowanie z działającego API, a poniżej są recenzje. Pełna strona pojawi się przy następnym budowaniu.",
"notfound.resolve.checking": "Pierwsza wizyta: sprawdzam ten mint teraz.",
"notfound.resolve.failed": "Nie udało się dodać tego mintu",
"notfound.resolve.failedFedimint": "Nie udało się dodać tej federacji",
"notfound.resolve.retry": "Spróbuj innego adresu",
"notfound.resolve.switch": "Otwórz jako mint {kind}",
"notfound.resolve.fedimint": "Adres strony federacji nie zawiera jej kodu zaproszenia, więc nie da się jej odnaleźć na podstawie samego linku. Wklej kod zaproszenia, a zostanie dodana.",
"notfound.resolve.ambiguous": "Tego adresu nie da się odtworzyć z samego linku. Wklej pełny URL mintu, a zostanie dodany.",
"wallets.title": "Portfele Cashu · Cashumints.space", "wallets.title": "Portfele Cashu · Cashumints.space",
"wallets.description": "Portfele cashu, które współpracują z wymienionymi tutaj monetami – na urządzeniach mobilnych, w internecie, przeglądarce i wierszu poleceń – z tym, do czego każdy z nich jest dobry.", "wallets.description": "Portfele cashu, które współpracują z wymienionymi tutaj monetami – na urządzeniach mobilnych, w internecie, przeglądarce i wierszu poleceń – z tym, do czego każdy z nich jest dobry.",
"wallets.heading": "Portfele", "wallets.heading": "Portfele",
@@ -696,5 +705,115 @@
"footer.allFedimints": "Wszystkie federacje", "footer.allFedimints": "Wszystkie federacje",
"reviews.dialog.bodyPlaceholder.cashu": "Jak wyglądało bicie, topienie i wspieranie? Bądź konkretny.", "reviews.dialog.bodyPlaceholder.cashu": "Jak wyglądało bicie, topienie i wspieranie? Bądź konkretny.",
"reviews.dialog.bodyPlaceholder.fedimint": "Jak wyglądało dołączenie, płatności i wsparcie Lightning? Bądź konkretny.", "reviews.dialog.bodyPlaceholder.fedimint": "Jak wyglądało dołączenie, płatności i wsparcie Lightning? Bądź konkretny.",
"fedimint.details.title": "Szczegóły federacji" "fedimint.details.title": "Szczegóły federacji",
"nav.lnurlMints": "Minty LNURL",
"footer.allLnurlMints": "Wszystkie minty LNURL",
"lnurlMints.title": "Minty LNURL: minty not na okaziciela lnurlcash z opiniami | Cashumints.space",
"lnurlMints.description": "Wszystkie {total} mintów LNURL zindeksowanych z sieci Nostr: {online} online, {offline} offline. Oceny społeczności, limity wypłat i możliwości każdego z nich.",
"lnurlMints.heading": "Minty LNURL",
"lnurlMints.lede": "Każdy mint LNURL, o którym wie ta strona. Mint LNURL emituje noty na okaziciela lnurlcash: noty Lightning, które trzymasz jako sekret i które można obrócić, podzielić, połączyć albo przetopić z powrotem na płatność Lightning. Każdy jest sprawdzany przez HTTPS co kilka minut.",
"lnurlMints.search.label": "Szukaj mintów LNURL",
"lnurlMints.search.placeholder": "Szukaj po nazwie lub adresie…",
"lnurlMints.sort.label": "Sortuj minty LNURL według",
"lnurlMints.hideOffline": "Ukryj offline",
"lnurlMints.showing": "Pokazano {total} mintów LNURL ({online} online, {offline} offline)",
"lnurlMints.showingFiltered": "Pokazano {shown} z {total} mintów LNURL ({online} online, {offline} offline)",
"lnurlMints.noResults": "Żaden mint LNURL nie pasuje.",
"lnurlMints.emptyIndex": "Nie zindeksowano jeszcze żadnych mintów LNURL.",
"lnurl.crumb": "Minty LNURL",
"lnurl.breadcrumb": "Okruszki nawigacyjne",
"lnurl.copyAddress": "Kopiuj adres",
"lnurl.more.title": "Więcej mintów LNURL",
"lnurl.more.all": "Wszystkie minty LNURL →",
"lnurl.disclaimer": "Mint LNURL przechowuje twoje saty, a ty trzymasz jego noty, więc korzystanie z niego oznacza zaufanie operatorowi. Oceny to opinie społeczności, a online znaczy osiągalny, nie godny zaufania.",
"lnurl.features.title": "Funkcje",
"lnurl.features.none": "Ten mint nie ogłosił, co obsługuje.",
"lnurl.features.note": "Funkcje to to, co operator minta ogłosił w Nostr. Tam, gdzie ta strona może sprawdzić coś przez HTTPS, robi to i informuje, gdy coś ogłoszonego jest w tej chwili niedostępne.",
"lnurl.features.notListed": "nieogłoszone",
"lnurl.features.unavailable": "niedostępne",
"lnurl.feature.mint": "Mintowanie przez Lightning (LUD-06)",
"lnurl.feature.melt": "Przetapianie na Lightning",
"lnurl.feature.notes": "Obracanie / dzielenie / łączenie not",
"lnurl.feature.lud16": "Adres Lightning",
"lnurl.feature.lud21": "Weryfikacja płatności (LUD-21)",
"lnurl.feature.signed-notes": "Podpisane noty (weryfikacja offline)",
"lnurl.feature.onion": "Adres Tor",
"lnurl.feature.rotate": "Obróć notę",
"lnurl.feature.split": "Podziel notę",
"lnurl.feature.merge": "Połącz noty",
"lnurl.feature.lud06": "LUD-06 payRequest",
"lnurl.feature.lud03": "LUD-03 withdrawRequest",
"lnurl.limits.label": "Limity wypłat",
"lnurl.limits.notPublished": "nieopublikowane",
"lnurl.limits.withdrawRow": "Wypłata",
"lnurl.limits.disabled": "wyłączone",
"lnurl.limits.sats.one": "{n} sat",
"lnurl.limits.sats.few": "{n} saty",
"lnurl.limits.sats.many": "{n} satów",
"lnurl.limits.sats.other": "{n} sata",
"lnurl.details.title": "Szczegóły minta",
"lnurl.details.address": "Adres Lightning",
"lnurl.details.mintPubkey": "Klucz publiczny minta",
"lnurl.details.onion": "Adres Tor",
"lnurl.details.fee": "Opłata za mintowanie",
"lnurl.details.node": "Węzeł",
"lnurl.details.capacity": "Ogłoszona pojemność",
"lnurl.kind.lightningAddress": "adres Lightning",
"lnurl.kind.mintPubkey": "klucz publiczny minta",
"lnurl.kind.onionAddress": "adres Tor",
"lnurl.confirmed.label": "Ostatnie potwierdzenie",
"lnurl.confirmed.when": "potwierdzony {when}",
"lnurl.fee.base.one": "{n} sat",
"lnurl.fee.base.few": "{n} saty",
"lnurl.fee.base.many": "{n} satów",
"lnurl.fee.base.other": "{n} sata",
"lnurl.fee.percent": "{percent}%",
"home.topLnurl.title": "Najlepsze minty LNURL",
"home.topLnurl.sub": "według opinii i oceny",
"home.topLnurl.all": "Wszystkie {n} mintów LNURL →",
"feed.ecosystem.lnurl": "LNURL",
"reviews.dialog.bodyPlaceholder.lnurl": "Jak poszło mintowanie, przetapianie i obracanie not? Napisz konkretnie.",
"reviews.byUrl.open": "Napisz recenzję",
"reviews.byUrl.title": "Napisz recenzję",
"reviews.byUrl.lede": "Wklej adres. Jeśli ta strona jeszcze go nie zna, sprawdzi go i doda teraz.",
"reviews.byUrl.label.cashu": "URL mintu",
"reviews.byUrl.label.fedimint": "Kod zaproszenia federacji",
"reviews.byUrl.label.lnurl": "URL mintu LNURL",
"reviews.byUrl.placeholder.cashu": "mint.example.com",
"reviews.byUrl.placeholder.fedimint": "fed11…",
"reviews.byUrl.placeholder.lnurl": "mint.example.com",
"reviews.byUrl.check": "Sprawdź ten adres",
"reviews.byUrl.checking": "Sprawdzanie…",
"reviews.byUrl.back": "Użyj innego adresu",
"reviews.byUrl.existing": "Już na liście",
"reviews.byUrl.indexed": "Właśnie dodany",
"reviews.byUrl.confirmClose": "Zamknąć? Wpisany adres nie zostanie zachowany.",
"reviews.byUrl.detected": "Ten adres odpowiedział jako mint {kind}.",
"reviews.byUrl.switchTo": "Oceń go jako mint {kind}",
"reviews.byUrl.error.empty": "Najpierw wpisz adres.",
"reviews.byUrl.error.badUrl": "To nie wygląda na URL mintu.",
"reviews.byUrl.error.badInvite": "To nie jest kod zaproszenia Fedimint. Kody zaczynają się od fed11.",
"reviews.byUrl.error.badInput": "Ta strona nie może sprawdzić tego adresu.",
"reviews.byUrl.error.blocked": "To nie jest adres publiczny, więc nie da się go sprawdzić.",
"reviews.byUrl.error.notAMint": "Ten adres odpowiedział, ale nie jako mint tego rodzaju.",
"reviews.byUrl.error.wrongType": "Ten adres odpowiedział, ale jako mint innego rodzaju.",
"reviews.byUrl.error.unverifiable": "Pod tym adresem nic nie odpowiedziało, a na Nostr nie ma po nim jeszcze śladu.",
"reviews.byUrl.error.rateLimited.one": "To dużo sprawdzeń z jednego połączenia. Spróbuj ponownie za około {n} minutę.",
"reviews.byUrl.error.rateLimited.other": "To dużo sprawdzeń z jednego połączenia. Spróbuj ponownie za około {n} minut.",
"reviews.byUrl.error.network": "Nie udało się połączyć z indeksem. Sprawdź połączenie i spróbuj ponownie.",
"seo.lnurl.titleNoReviews": "{name}: informacje i opinie o mincie LNURL | Cashumints.space",
"seo.desc.statusWithdraw": "{status}, wypłata {min}–{max} sat.",
"mint.chip.noFunding": "Brak mintowania / przetapiania",
"mint.warnings.lnurlWithdrawalsDisabled.lead": "Wypłaty wyłączone.",
"mint.warnings.lnurlWithdrawalsDisabled.body.online": "Ten mint ogłasza maksymalną wypłatę równą zeru, więc żadnej wyemitowanej przez niego noty nie da się na nic wymienić. Nie mintuj tutaj, dopóki to się nie zmieni.",
"mint.warnings.lnurlWithdrawalsDisabled.body.offline": "Przy ostatnim kontakcie ten mint ogłaszał maksymalną wypłatę równą zeru, więc żadnej wyemitowanej przez niego noty nie dało się na nic wymienić. Nie mintuj tutaj, dopóki to się nie zmieni.",
"mint.warnings.lnurlWithdrawalsDisabled.meta.online": "Wypłaty obecnie wyłączone",
"mint.warnings.lnurlWithdrawalsDisabled.meta.offline": "Przy ostatnim kontakcie wypłaty były wyłączone",
"mint.warnings.lnurlInvalid.lead": "Endpoint odpowiada, ale nieprawidłowo.",
"mint.warnings.lnurlInvalid.body": "Host odpowiada, ale nie ogłoszeniem minta, które ta strona potrafi odczytać, więc wypłaty mogą nie działać. Poniżej jest ostatni stan, który udało się odczytać, a opinie nadal działają.",
"mint.warnings.lnurlInvalid.meta": "Odpowiada, ale nie prawidłowym ogłoszeniem minta",
"mint.warnings.lnurlNoFunding.lead": "Mintowanie i przetapianie niedostępne.",
"mint.warnings.lnurlNoFunding.body": "Istniejące noty nadal można obracać, dzielić lub łączyć, ale w tej chwili nic nie wchodzi ani nie wychodzi przez Lightning. Sam mint działa i odpowiada; węzeł Lightning za nim jest z niego nieosiągalny.",
"mint.warnings.lnurlNoFunding.meta": "Mintowanie i przetapianie niedostępne",
"mint.warnings.also.noFunding": "Jego węzeł Lightning też był nieosiągalny, więc nie dało się nic zmintować ani przetopić."
} }
+118 -3
View File
@@ -114,6 +114,8 @@
"card.reviewed": "revisado {when}", "card.reviewed": "revisado {when}",
"card.noReviews": "ainda não há comentários", "card.noReviews": "ainda não há comentários",
"mint.title": "{name} · Cashu mint comentários", "mint.title": "{name} · Cashu mint comentários",
"mint.titleFedimint": "{name} · Avaliações da federação Fedimint",
"mint.titleLnurl": "{name} · Avaliações do mint LNURL",
"mint.breadcrumb": "Pão ralado", "mint.breadcrumb": "Pão ralado",
"mint.crumbMints": "Cashu mints", "mint.crumbMints": "Cashu mints",
"mint.copyUrl": "Copiar URL mint", "mint.copyUrl": "Copiar URL mint",
@@ -325,7 +327,7 @@
"reviews.validate.short": "Escreva pelo menos caracteres {n} para que a revisão seja útil.", "reviews.validate.short": "Escreva pelo menos caracteres {n} para que a revisão seja útil.",
"reviews.validate.long": "As resenhas são limitadas a caracteres {n}.", "reviews.validate.long": "As resenhas são limitadas a caracteres {n}.",
"feed.title": "Últimas análises de mint e da federação · Cashumints.space", "feed.title": "Últimas análises de mint e da federação · Cashumints.space",
"feed.description": "Cada análise de mint e de federação conforme publicada: análises de {reviews} nas federações {mints} Cashu mints e {federations} Fedimint, assinadas em Nostr e legíveis aqui, as mais recentes primeiro. Filtre por ecossistema, por classificação ou por nome.", "feed.description": "Cada avaliação de mint, federação e mint LNURL assim que é publicada: {reviews} avaliações sobre {mints} mints Cashu, {federations} federações Fedimint e {lnurl} mints LNURL, assinadas no Nostr e legíveis aqui da mais recente para a mais antiga. Filtre por ecossistema, por nota ou por nome.",
"feed.heading": "Últimas avaliações", "feed.heading": "Últimas avaliações",
"feed.lede": "Cada revisão de cada federação Cashu mint e Fedimint, as mais recentes primeiro, lida ao vivo nos relés Nostr. Cada um é assinado por seu autor e reside nos relés, não em nosso banco de dados.", "feed.lede": "Cada revisão de cada federação Cashu mint e Fedimint, as mais recentes primeiro, lida ao vivo nos relés Nostr. Cada um é assinado por seu autor e reside nos relés, não em nosso banco de dados.",
"feed.label": "Avaliações", "feed.label": "Avaliações",
@@ -400,7 +402,14 @@
"notfound.fb.reviews": "Avaliações", "notfound.fb.reviews": "Avaliações",
"notfound.fb.software": "Programas", "notfound.fb.software": "Programas",
"notfound.fb.lastOnline": "Última vez on-line", "notfound.fb.lastOnline": "Última vez on-line",
"notfound.fb.note": "Este mint foi indexado após a última construção do site, portanto, este é um resumo da API ativa. A página completa com comentários aparece na próxima reconstrução.", "notfound.fb.note": "Este mint foi indexado depois da última construção do site, por isso este é um resumo da API em direto, com as avaliações abaixo. A página completa aparece na próxima construção.",
"notfound.resolve.checking": "Primeira visita: a verificar este mint agora.",
"notfound.resolve.failed": "Não foi possível adicionar este mint",
"notfound.resolve.failedFedimint": "Não foi possível adicionar esta federação",
"notfound.resolve.retry": "Tentar outro endereço",
"notfound.resolve.switch": "Abri-lo como mint {kind}",
"notfound.resolve.fedimint": "O endereço da página de uma federação não contém o seu código de convite, por isso esta não pode ser encontrada só com o link. Cola o código de convite e ela é adicionada.",
"notfound.resolve.ambiguous": "Este endereço não pode ser deduzido só a partir do link. Cola o URL completo do mint e ele é adicionado.",
"wallets.title": "Carteiras Cashu · Cashumints.space", "wallets.title": "Carteiras Cashu · Cashumints.space",
"wallets.description": "Carteiras Cashu que funcionam com o mints listado aqui, em dispositivos móveis, web, navegador e linha de comando, com o que cada uma é boa.", "wallets.description": "Carteiras Cashu que funcionam com o mints listado aqui, em dispositivos móveis, web, navegador e linha de comando, com o que cada uma é boa.",
"wallets.heading": "Carteiras", "wallets.heading": "Carteiras",
@@ -654,5 +663,111 @@
"footer.allFedimints": "Todas as federações", "footer.allFedimints": "Todas as federações",
"reviews.dialog.bodyPlaceholder.cashu": "Como funcionou minting, melting e suporte? Seja específico.", "reviews.dialog.bodyPlaceholder.cashu": "Como funcionou minting, melting e suporte? Seja específico.",
"reviews.dialog.bodyPlaceholder.fedimint": "Como funcionou a adesão, os pagamentos e o suporte do Lightning? Seja específico.", "reviews.dialog.bodyPlaceholder.fedimint": "Como funcionou a adesão, os pagamentos e o suporte do Lightning? Seja específico.",
"fedimint.details.title": "Detalhes da federação" "fedimint.details.title": "Detalhes da federação",
"nav.lnurlMints": "Mints LNURL",
"footer.allLnurlMints": "Todos os mints LNURL",
"lnurlMints.title": "Mints LNURL: mints de notas ao portador lnurlcash, avaliados | Cashumints.space",
"lnurlMints.description": "Todos os {total} mints LNURL indexados da rede Nostr: {online} on-line, {offline} off-line. Avaliações da comunidade, limites de saque e recursos de cada um.",
"lnurlMints.heading": "Mints LNURL",
"lnurlMints.lede": "Todos os mints LNURL que este site conhece. Um mint LNURL emite notas ao portador lnurlcash: notas Lightning que você guarda como um segredo e que podem ser rotacionadas, divididas, mescladas ou derretidas de volta em um pagamento Lightning. Cada um é verificado por HTTPS a cada poucos minutos.",
"lnurlMints.search.label": "Pesquisar mints LNURL",
"lnurlMints.search.placeholder": "Pesquisar por nome ou endereço…",
"lnurlMints.sort.label": "Ordenar mints LNURL por",
"lnurlMints.hideOffline": "Ocultar off-line",
"lnurlMints.showing": "Mostrando {total} mints LNURL ({online} on-line, {offline} off-line)",
"lnurlMints.showingFiltered": "Mostrando {shown} de {total} mints LNURL ({online} on-line, {offline} off-line)",
"lnurlMints.noResults": "Nenhum mint LNURL corresponde a isso.",
"lnurlMints.emptyIndex": "Ainda não há mints LNURL indexados.",
"lnurl.crumb": "Mints LNURL",
"lnurl.breadcrumb": "Trilha de navegação",
"lnurl.copyAddress": "Copiar endereço",
"lnurl.more.title": "Mais mints LNURL",
"lnurl.more.all": "Todos os mints LNURL →",
"lnurl.disclaimer": "Um mint LNURL guarda seus sats enquanto você guarda as notas dele, então usá-lo significa confiar em seu operador. As avaliações são opiniões da comunidade, e on-line significa acessível, não confiável.",
"lnurl.features.title": "Recursos",
"lnurl.features.none": "Este mint não anunciou o que oferece.",
"lnurl.features.note": "Os recursos são o que o operador do mint anunciou no Nostr. Quando este site consegue verificar algum por HTTPS, ele o faz, e avisa quando algo anunciado não está disponível no momento.",
"lnurl.features.notListed": "não anunciado",
"lnurl.features.unavailable": "indisponível",
"lnurl.feature.mint": "Minting via Lightning (LUD-06)",
"lnurl.feature.melt": "Melting para Lightning",
"lnurl.feature.notes": "Rotacionar / dividir / mesclar notas",
"lnurl.feature.lud16": "Endereço Lightning",
"lnurl.feature.lud21": "Verificação de pagamento (LUD-21)",
"lnurl.feature.signed-notes": "Notas assinadas (verificação off-line)",
"lnurl.feature.onion": "Endereço Tor",
"lnurl.feature.rotate": "Rotacionar uma nota",
"lnurl.feature.split": "Dividir uma nota",
"lnurl.feature.merge": "Mesclar notas",
"lnurl.feature.lud06": "payRequest LUD-06",
"lnurl.feature.lud03": "withdrawRequest LUD-03",
"lnurl.limits.label": "Limites de saque",
"lnurl.limits.notPublished": "não publicado",
"lnurl.limits.withdrawRow": "Saque",
"lnurl.limits.disabled": "desativado",
"lnurl.limits.sats.one": "{n} sat",
"lnurl.limits.sats.other": "{n} sats",
"lnurl.details.title": "Detalhes do mint",
"lnurl.details.address": "Endereço Lightning",
"lnurl.details.mintPubkey": "Chave pública do mint",
"lnurl.details.onion": "Endereço Tor",
"lnurl.details.fee": "Taxa de minting",
"lnurl.details.node": "Nó",
"lnurl.details.capacity": "Capacidade anunciada",
"lnurl.kind.lightningAddress": "endereço Lightning",
"lnurl.kind.mintPubkey": "chave pública do mint",
"lnurl.kind.onionAddress": "endereço Tor",
"lnurl.confirmed.label": "Última confirmação",
"lnurl.confirmed.when": "confirmado {when}",
"lnurl.fee.base.one": "{n} sat",
"lnurl.fee.base.other": "{n} sats",
"lnurl.fee.percent": "{percent}%",
"home.topLnurl.title": "Melhores mints LNURL",
"home.topLnurl.sub": "classificados por avaliações e nota",
"home.topLnurl.all": "Todos os {n} mints LNURL →",
"feed.ecosystem.lnurl": "LNURL",
"reviews.dialog.bodyPlaceholder.lnurl": "Como foram o minting, o melting e a rotação de notas? Seja específico.",
"reviews.byUrl.open": "Escrever uma avaliação",
"reviews.byUrl.title": "Escrever uma avaliação",
"reviews.byUrl.lede": "Cola o endereço. Se este site ainda não o conhece, ele é verificado e adicionado agora.",
"reviews.byUrl.label.cashu": "URL do mint",
"reviews.byUrl.label.fedimint": "Código de convite da federação",
"reviews.byUrl.label.lnurl": "URL do mint LNURL",
"reviews.byUrl.placeholder.cashu": "mint.example.com",
"reviews.byUrl.placeholder.fedimint": "fed11…",
"reviews.byUrl.placeholder.lnurl": "mint.example.com",
"reviews.byUrl.check": "Verificar este endereço",
"reviews.byUrl.checking": "A verificar…",
"reviews.byUrl.back": "Usar outro endereço",
"reviews.byUrl.existing": "Já está listado",
"reviews.byUrl.indexed": "Acabado de adicionar",
"reviews.byUrl.confirmClose": "Fechar? O endereço que escreveste não é guardado.",
"reviews.byUrl.detected": "Esse endereço respondeu como um mint {kind}.",
"reviews.byUrl.switchTo": "Avaliá-lo como mint {kind}",
"reviews.byUrl.error.empty": "Escreve primeiro um endereço.",
"reviews.byUrl.error.badUrl": "Isso não parece o URL de um mint.",
"reviews.byUrl.error.badInvite": "Isso não é um código de convite Fedimint. Os códigos começam por fed11.",
"reviews.byUrl.error.badInput": "Este site não consegue verificar esse endereço.",
"reviews.byUrl.error.blocked": "Esse não é um endereço público, por isso não pode ser verificado.",
"reviews.byUrl.error.notAMint": "Esse endereço respondeu, mas não como um mint deste tipo.",
"reviews.byUrl.error.wrongType": "Esse endereço respondeu, mas como um mint de outro tipo.",
"reviews.byUrl.error.unverifiable": "Nada respondeu nesse endereço, e ainda não há qualquer rasto dele no Nostr.",
"reviews.byUrl.error.rateLimited.one": "São muitas verificações a partir de uma só ligação. Tenta de novo daqui a cerca de {n} minuto.",
"reviews.byUrl.error.rateLimited.other": "São muitas verificações a partir de uma só ligação. Tenta de novo daqui a cerca de {n} minutos.",
"reviews.byUrl.error.network": "Não foi possível contactar o índice. Verifica a tua ligação e tenta de novo.",
"seo.lnurl.titleNoReviews": "{name}: informações e avaliações do mint LNURL | Cashumints.space",
"seo.desc.statusWithdraw": "{status}, saque de {min}–{max} sat.",
"mint.chip.noFunding": "Sem mint / melt",
"mint.warnings.lnurlWithdrawalsDisabled.lead": "Saques desativados.",
"mint.warnings.lnurlWithdrawalsDisabled.body.online": "Este mint anuncia um saque máximo de zero, então nenhuma nota que ele emite pode ser resgatada por nada. Não faça minting aqui até que isso mude.",
"mint.warnings.lnurlWithdrawalsDisabled.body.offline": "Este mint anunciava um saque máximo de zero no último contato, então nenhuma nota que ele emitia podia ser resgatada por nada. Não faça minting aqui até que isso mude.",
"mint.warnings.lnurlWithdrawalsDisabled.meta.online": "Saques desativados no momento",
"mint.warnings.lnurlWithdrawalsDisabled.meta.offline": "Os saques estavam desativados no último contato",
"mint.warnings.lnurlInvalid.lead": "O endpoint responde, mas é inválido.",
"mint.warnings.lnurlInvalid.body": "O host responde, mas não com um anúncio de mint que este site consiga ler, então os saques podem não funcionar. O que está abaixo é o último estado legível, e as avaliações continuam funcionando.",
"mint.warnings.lnurlInvalid.meta": "Responde, mas não com um anúncio de mint válido",
"mint.warnings.lnurlNoFunding.lead": "Minting e melting indisponíveis.",
"mint.warnings.lnurlNoFunding.body": "As notas existentes ainda podem ser rotacionadas, divididas ou mescladas, mas nada entra nem sai via Lightning no momento. O mint em si está no ar e respondendo; o nó Lightning por trás dele não está acessível a partir dali.",
"mint.warnings.lnurlNoFunding.meta": "Minting e melting indisponíveis",
"mint.warnings.also.noFunding": "O nó Lightning também estava inacessível, então nada podia ser mintado ou derretido."
} }
+120 -3
View File
@@ -115,6 +115,8 @@
"card.reviewed": "revăzut{when}", "card.reviewed": "revăzut{when}",
"card.noReviews": "nu există comentarii încă", "card.noReviews": "nu există comentarii încă",
"mint.title": "{name}· Cashu monetărie recenzii", "mint.title": "{name}· Cashu monetărie recenzii",
"mint.titleFedimint": "{name} · Recenzii ale federației Fedimint",
"mint.titleLnurl": "{name} · Recenzii ale mintului LNURL",
"mint.breadcrumb": "Cremă de pâine", "mint.breadcrumb": "Cremă de pâine",
"mint.crumbMints": "Caju muntetării", "mint.crumbMints": "Caju muntetării",
"mint.copyUrl": "Copiază URL- ul monetărie", "mint.copyUrl": "Copiază URL- ul monetărie",
@@ -338,7 +340,7 @@
"reviews.validate.short": "Scrie cel puţin{n} caractere, astfel încât revizuirea este utilă.", "reviews.validate.short": "Scrie cel puţin{n} caractere, astfel încât revizuirea este utilă.",
"reviews.validate.long": "Revizuirile sunt limitate la{n}Personaje.", "reviews.validate.long": "Revizuirile sunt limitate la{n}Personaje.",
"feed.title": "Ultimele reviste monetărie și federație · Casumints.space", "feed.title": "Ultimele reviste monetărie și federație · Casumints.space",
"feed.description": "Fiecare recenzie a unei monetării sau federații, pe măsură ce este publicată: {reviews} recenzii pentru {mints} monetării Cashu și {federations} federații Fedimint, semnate pe Nostr și afișate aici de la cea mai nouă. Filtrează după ecosistem, evaluare sau nume.", "feed.description": "Fiecare recenzie de mint, federație și mint LNURL pe măsură ce este publicată: {reviews} recenzii pentru {mints} mint-uri Cashu, {federations} federații Fedimint și {lnurl} mint-uri LNURL, semnate pe Nostr și de citit aici de la cele mai noi. Filtrează după ecosistem, evaluare sau nume.",
"feed.heading": "Ultimele recenzii", "feed.heading": "Ultimele recenzii",
"feed.lede": "Fiecare recenzie a fiecărei federații Cashu monetărie și Fedimint, cea mai nouă primă, citește în direct de la releele Nostr. Fiecare este semnată de autorul său și trăiește pe relee, nu în baza noastră de date.", "feed.lede": "Fiecare recenzie a fiecărei federații Cashu monetărie și Fedimint, cea mai nouă primă, citește în direct de la releele Nostr. Fiecare este semnată de autorul său și trăiește pe relee, nu în baza noastră de date.",
"feed.label": "Revizuiri", "feed.label": "Revizuiri",
@@ -414,7 +416,14 @@
"notfound.fb.reviews": "Revizuiri", "notfound.fb.reviews": "Revizuiri",
"notfound.fb.software": "Programe", "notfound.fb.software": "Programe",
"notfound.fb.lastOnline": "Ultima dată online", "notfound.fb.lastOnline": "Ultima dată online",
"notfound.fb.note": "Această monetărie a fost indexată după ce site-ul a fost construit ultima dată, astfel încât acesta este un rezumat al API live. Pagina completă cu recenzii apare la următoarea reconstrucție.", "notfound.fb.note": "Acest mint a fost indexat după ultima construire a site-ului, deci acesta e un rezumat din API-ul live, cu recenziile dedesubt. Pagina completă apare la următoarea construire.",
"notfound.resolve.checking": "Prima vizită: verific acest mint acum.",
"notfound.resolve.failed": "Acest mint nu a putut fi adăugat",
"notfound.resolve.failedFedimint": "Această federație nu a putut fi adăugată",
"notfound.resolve.retry": "Încearcă altă adresă",
"notfound.resolve.switch": "Deschide-l ca mint {kind}",
"notfound.resolve.fedimint": "Adresa paginii unei federații nu conține codul ei de invitație, așa că aceasta nu poate fi găsită doar din link. Lipește codul de invitație și va fi adăugată.",
"notfound.resolve.ambiguous": "Această adresă nu poate fi dedusă doar din link. Lipește URL-ul complet al mintului și va fi adăugat.",
"wallets.title": "Portofele caju · Casumints.space", "wallets.title": "Portofele caju · Casumints.space",
"wallets.description": "Portofelele Cashu care lucrează cu monetării enumerate aici, prin telefon mobil, web, browser și linia de comandă, cu ceea ce fiecare este bun la.", "wallets.description": "Portofelele Cashu care lucrează cu monetării enumerate aici, prin telefon mobil, web, browser și linia de comandă, cu ceea ce fiecare este bun la.",
"wallets.heading": "Portofele", "wallets.heading": "Portofele",
@@ -675,5 +684,113 @@
"footer.allFedimints": "Toate federațiile", "footer.allFedimints": "Toate federațiile",
"reviews.dialog.bodyPlaceholder.cashu": "Cum a funcţionat menţinerea, topirea şi susţinerea?", "reviews.dialog.bodyPlaceholder.cashu": "Cum a funcţionat menţinerea, topirea şi susţinerea?",
"reviews.dialog.bodyPlaceholder.fedimint": "Cum s-a realizat aderarea, plăţile pentru fulger şi sprijinul acordat?", "reviews.dialog.bodyPlaceholder.fedimint": "Cum s-a realizat aderarea, plăţile pentru fulger şi sprijinul acordat?",
"fedimint.details.title": "Detaliile Federaţiei" "fedimint.details.title": "Detaliile Federaţiei",
"nav.lnurlMints": "Mint-uri LNURL",
"footer.allLnurlMints": "Toate mint-urile LNURL",
"lnurlMints.title": "Mint-uri LNURL: mint-uri de bilete la purtător lnurlcash, cu recenzii | Cashumints.space",
"lnurlMints.description": "Toate cele {total} mint-uri LNURL indexate din rețeaua Nostr: {online} online, {offline} offline. Evaluări ale comunității, limite de retragere și funcții pentru fiecare.",
"lnurlMints.heading": "Mint-uri LNURL",
"lnurlMints.lede": "Fiecare mint LNURL pe care îl cunoaște acest site. Un mint LNURL emite bilete la purtător lnurlcash: bilete Lightning pe care le păstrezi ca pe un secret și care pot fi rotite, împărțite, unite sau topite înapoi într-o plată Lightning. Fiecare este verificat prin HTTPS la câteva minute.",
"lnurlMints.search.label": "Caută mint-uri LNURL",
"lnurlMints.search.placeholder": "Caută după nume sau adresă…",
"lnurlMints.sort.label": "Sortează mint-urile LNURL după",
"lnurlMints.hideOffline": "Ascunde offline",
"lnurlMints.showing": "Se afișează {total} mint-uri LNURL ({online} online, {offline} offline)",
"lnurlMints.showingFiltered": "Se afișează {shown} din {total} mint-uri LNURL ({online} online, {offline} offline)",
"lnurlMints.noResults": "Niciun mint LNURL nu corespunde.",
"lnurlMints.emptyIndex": "Niciun mint LNURL nu este indexat încă.",
"lnurl.crumb": "Mint-uri LNURL",
"lnurl.breadcrumb": "Firimituri de navigare",
"lnurl.copyAddress": "Copiază adresa",
"lnurl.more.title": "Mai multe mint-uri LNURL",
"lnurl.more.all": "Toate mint-urile LNURL →",
"lnurl.disclaimer": "Un mint LNURL îți păstrează sat-ii cât timp tu îi păstrezi biletele, deci folosirea lui înseamnă să ai încredere în operatorul său. Evaluările sunt păreri ale comunității, iar online înseamnă accesibil, nu demn de încredere.",
"lnurl.features.title": "Funcții",
"lnurl.features.none": "Acest mint nu a anunțat ce acceptă.",
"lnurl.features.note": "Funcțiile sunt ceea ce operatorul mint-ului a anunțat pe Nostr. Acolo unde acest site poate verifica una prin HTTPS, o face, și semnalează când ceva anunțat nu este disponibil chiar acum.",
"lnurl.features.notListed": "neanunțat",
"lnurl.features.unavailable": "indisponibil",
"lnurl.feature.mint": "Mint prin Lightning (LUD-06)",
"lnurl.feature.melt": "Topire către Lightning",
"lnurl.feature.notes": "Rotire / împărțire / unire bilete",
"lnurl.feature.lud16": "Adresă Lightning",
"lnurl.feature.lud21": "Verificarea plății (LUD-21)",
"lnurl.feature.signed-notes": "Bilete semnate (verificare offline)",
"lnurl.feature.onion": "Adresă Tor",
"lnurl.feature.rotate": "Rotește un bilet",
"lnurl.feature.split": "Împarte un bilet",
"lnurl.feature.merge": "Unește bilete",
"lnurl.feature.lud06": "LUD-06 payRequest",
"lnurl.feature.lud03": "LUD-03 withdrawRequest",
"lnurl.limits.label": "Limite de retragere",
"lnurl.limits.notPublished": "nepublicat",
"lnurl.limits.withdrawRow": "Retragere",
"lnurl.limits.disabled": "dezactivat",
"lnurl.limits.sats.one": "{n} sat",
"lnurl.limits.sats.few": "{n} sați",
"lnurl.limits.sats.other": "{n} de sați",
"lnurl.details.title": "Detalii mint",
"lnurl.details.address": "Adresă Lightning",
"lnurl.details.mintPubkey": "Cheia publică a mint-ului",
"lnurl.details.onion": "Adresă Tor",
"lnurl.details.fee": "Comision de mint",
"lnurl.details.node": "Nod",
"lnurl.details.capacity": "Capacitate anunțată",
"lnurl.kind.lightningAddress": "adresă Lightning",
"lnurl.kind.mintPubkey": "cheia publică a mint-ului",
"lnurl.kind.onionAddress": "adresă Tor",
"lnurl.confirmed.label": "Ultima confirmare",
"lnurl.confirmed.when": "confirmat {when}",
"lnurl.fee.base.one": "{n} sat",
"lnurl.fee.base.few": "{n} sați",
"lnurl.fee.base.other": "{n} de sați",
"lnurl.fee.percent": "{percent}%",
"home.topLnurl.title": "Top mint-uri LNURL",
"home.topLnurl.sub": "clasate după recenzii și evaluare",
"home.topLnurl.all": "Toate cele {n} mint-uri LNURL →",
"feed.ecosystem.lnurl": "LNURL",
"reviews.dialog.bodyPlaceholder.lnurl": "Cum au mers mint-ul, topirea și rotirea biletelor? Fii concret.",
"reviews.byUrl.open": "Scrie o recenzie",
"reviews.byUrl.title": "Scrie o recenzie",
"reviews.byUrl.lede": "Lipește adresa. Dacă site-ul nu o cunoaște încă, o verifică și o adaugă acum.",
"reviews.byUrl.label.cashu": "URL-ul mintului",
"reviews.byUrl.label.fedimint": "Codul de invitație al federației",
"reviews.byUrl.label.lnurl": "URL-ul mintului LNURL",
"reviews.byUrl.placeholder.cashu": "mint.example.com",
"reviews.byUrl.placeholder.fedimint": "fed11…",
"reviews.byUrl.placeholder.lnurl": "mint.example.com",
"reviews.byUrl.check": "Verifică această adresă",
"reviews.byUrl.checking": "Se verifică…",
"reviews.byUrl.back": "Folosește altă adresă",
"reviews.byUrl.existing": "Deja în listă",
"reviews.byUrl.indexed": "Tocmai adăugat",
"reviews.byUrl.confirmClose": "Închizi? Adresa scrisă nu se păstrează.",
"reviews.byUrl.detected": "Acea adresă a răspuns ca un mint {kind}.",
"reviews.byUrl.switchTo": "Recenzează-l ca mint {kind}",
"reviews.byUrl.error.empty": "Scrie mai întâi o adresă.",
"reviews.byUrl.error.badUrl": "Asta nu pare un URL de mint.",
"reviews.byUrl.error.badInvite": "Acesta nu e un cod de invitație Fedimint. Codurile încep cu fed11.",
"reviews.byUrl.error.badInput": "Site-ul nu poate verifica acea adresă.",
"reviews.byUrl.error.blocked": "Nu e o adresă publică, deci nu poate fi verificată.",
"reviews.byUrl.error.notAMint": "Acea adresă a răspuns, dar nu ca un mint de acest fel.",
"reviews.byUrl.error.wrongType": "Acea adresă a răspuns, dar ca un alt fel de mint.",
"reviews.byUrl.error.unverifiable": "Nimic nu a răspuns la acea adresă și nu există încă nicio urmă a ei pe Nostr.",
"reviews.byUrl.error.rateLimited.one": "Sunt multe verificări de pe o singură conexiune. Încearcă din nou în aproximativ {n} minut.",
"reviews.byUrl.error.rateLimited.other": "Sunt multe verificări de pe o singură conexiune. Încearcă din nou în aproximativ {n} de minute.",
"reviews.byUrl.error.network": "Indexul nu a putut fi contactat. Verifică-ți conexiunea și încearcă din nou.",
"seo.lnurl.titleNoReviews": "{name}: informații și recenzii despre mint-ul LNURL | Cashumints.space",
"seo.desc.statusWithdraw": "{status}, retragere {min}–{max} sat.",
"mint.chip.noFunding": "Fără mint / topire",
"mint.warnings.lnurlWithdrawalsDisabled.lead": "Retrageri dezactivate.",
"mint.warnings.lnurlWithdrawalsDisabled.body.online": "Acest mint anunță o retragere maximă de zero, deci niciun bilet emis de el nu poate fi schimbat pe nimic. Nu face mint aici până nu se schimbă asta.",
"mint.warnings.lnurlWithdrawalsDisabled.body.offline": "La ultimul contact, acest mint anunța o retragere maximă de zero, deci niciun bilet emis de el nu putea fi schimbat pe nimic. Nu face mint aici până nu se schimbă asta.",
"mint.warnings.lnurlWithdrawalsDisabled.meta.online": "Retrageri dezactivate în prezent",
"mint.warnings.lnurlWithdrawalsDisabled.meta.offline": "Retragerile erau dezactivate la ultimul contact",
"mint.warnings.lnurlInvalid.lead": "Endpoint-ul răspunde, dar este invalid.",
"mint.warnings.lnurlInvalid.body": "Gazda răspunde, dar nu cu un anunț de mint pe care acest site îl poate citi, așa că retragerile s-ar putea să nu funcționeze. Mai jos este ultima stare care a putut fi citită, iar recenziile funcționează în continuare.",
"mint.warnings.lnurlInvalid.meta": "Răspunde, dar nu cu un anunț de mint valid",
"mint.warnings.lnurlNoFunding.lead": "Mint și topire indisponibile.",
"mint.warnings.lnurlNoFunding.body": "Biletele existente pot fi în continuare rotite, împărțite sau unite, dar chiar acum nimic nu intră și nu iese prin Lightning. Mint-ul în sine funcționează și răspunde; nodul Lightning din spatele lui nu îi este accesibil.",
"mint.warnings.lnurlNoFunding.meta": "Mint și topire indisponibile",
"mint.warnings.also.noFunding": "Și nodul său Lightning era inaccesibil, deci nu se putea face mint sau topire."
} }
+128 -9
View File
@@ -15,12 +15,12 @@
"time.justNow": "только что", "time.justNow": "только что",
"time.none": "-", "time.none": "-",
"time.unknownDate": "неизвестная дата", "time.unknownDate": "неизвестная дата",
"time.ago.minute": "{n}m назад", "time.ago.minute": "{n} мин. назад",
"time.ago.hour": "{n}h назад", "time.ago.hour": "{n} ч. назад",
"time.ago.day": "{n}d назад", "time.ago.day": "{n} дн. назад",
"time.ago.week": "{n}w назад", "time.ago.week": "{n} нед. назад",
"time.ago.month": "{n}mo назад", "time.ago.month": "{n} мес. назад",
"time.ago.year": "{n}y назад", "time.ago.year": "{n} г. назад",
"time.short.minute": "{n}m", "time.short.minute": "{n}m",
"time.short.hour": "{n}h", "time.short.hour": "{n}h",
"time.short.day": "{n}d", "time.short.day": "{n}d",
@@ -116,6 +116,8 @@
"card.reviewed": "отзыв оставлен {when}", "card.reviewed": "отзыв оставлен {when}",
"card.noReviews": "пока нет отзывов", "card.noReviews": "пока нет отзывов",
"mint.title": "{name} · Отзывы о минте Cashu", "mint.title": "{name} · Отзывы о минте Cashu",
"mint.titleFedimint": "{name} · Отзывы о федерации Fedimint",
"mint.titleLnurl": "{name} · Отзывы о LNURL-минте",
"mint.breadcrumb": "Навигационная цепочка", "mint.breadcrumb": "Навигационная цепочка",
"mint.crumbMints": "Минты Cashu", "mint.crumbMints": "Минты Cashu",
"mint.copyUrl": "Скопировать URL минта", "mint.copyUrl": "Скопировать URL минта",
@@ -351,7 +353,7 @@
"reviews.validate.short": "Напишите не менее {n} символов, чтобы отзыв был полезен.", "reviews.validate.short": "Напишите не менее {n} символов, чтобы отзыв был полезен.",
"reviews.validate.long": "Длина отзыва ограничена {n} символами.", "reviews.validate.long": "Длина отзыва ограничена {n} символами.",
"feed.title": "Последние отзывы о минтах и федерациях · Cashumints.space", "feed.title": "Последние отзывы о минтах и федерациях · Cashumints.space",
"feed.description": "Все отзывы о минтах и федерациях по мере публикации: {reviews} отзывов о {mints} минтах Cashu и {federations} федерациях Fedimint. Они подписаны в Nostr и показаны от новых к старым. Фильтруйте по экосистеме, рейтингу или названию.", "feed.description": "Каждый отзыв о минте, федерации и LNURL-минте сразу после публикации: {reviews} отзывов о {mints} минтах Cashu, {federations} федерациях Fedimint и {lnurl} LNURL-минтах, подписанных в Nostr и доступных здесь начиная с самых новых. Фильтруйте по экосистеме, оценке или названию.",
"feed.heading": "Последние отзывы", "feed.heading": "Последние отзывы",
"feed.lede": "Все отзывы о минтах Cashu и федерациях Fedimint, от новых к старым, напрямую с Nostr relays. Каждый отзыв подписан автором и хранится на relays, а не в нашей базе данных.", "feed.lede": "Все отзывы о минтах Cashu и федерациях Fedimint, от новых к старым, напрямую с Nostr relays. Каждый отзыв подписан автором и хранится на relays, а не в нашей базе данных.",
"feed.label": "Отзывы", "feed.label": "Отзывы",
@@ -428,7 +430,14 @@
"notfound.fb.reviews": "Отзывы", "notfound.fb.reviews": "Отзывы",
"notfound.fb.software": "Программное обеспечение", "notfound.fb.software": "Программное обеспечение",
"notfound.fb.lastOnline": "Последний раз онлайн", "notfound.fb.lastOnline": "Последний раз онлайн",
"notfound.fb.note": "Эта запись была проиндексирована после последнего обновления сайта, поэтому это сводка данных из активного API. Полная страница с отзывами появится после следующего обновления сайта.", "notfound.fb.note": "Этот минт был проиндексирован после последней сборки сайта, поэтому здесь сводка из живого API, а ниже его отзывы. Полная страница появится при следующей сборке.",
"notfound.resolve.checking": "Первое посещение: проверяем этот минт.",
"notfound.resolve.failed": "Не удалось добавить этот минт",
"notfound.resolve.failedFedimint": "Не удалось добавить эту федерацию",
"notfound.resolve.retry": "Попробовать другой адрес",
"notfound.resolve.switch": "Открыть как минт {kind}",
"notfound.resolve.fedimint": "Адрес страницы федерации не содержит её код приглашения, поэтому найти её по одной ссылке нельзя. Вставь код приглашения, и она будет добавлена.",
"notfound.resolve.ambiguous": "Этот адрес нельзя восстановить по одной ссылке. Вставь полный URL минта, и он будет добавлен.",
"wallets.title": "Кошельки Cashu · Cashumints.space", "wallets.title": "Кошельки Cashu · Cashumints.space",
"wallets.description": "Кошельки Cashu для перечисленных здесь минтов: мобильные, веб-кошельки, расширения и CLI, с описанием сильных сторон каждого.", "wallets.description": "Кошельки Cashu для перечисленных здесь минтов: мобильные, веб-кошельки, расширения и CLI, с описанием сильных сторон каждого.",
"wallets.heading": "Кошельки", "wallets.heading": "Кошельки",
@@ -696,5 +705,115 @@
"footer.allFedimints": "Все федерации", "footer.allFedimints": "Все федерации",
"reviews.dialog.bodyPlaceholder.cashu": "Как работали mint, melt и поддержка? Опишите конкретно.", "reviews.dialog.bodyPlaceholder.cashu": "Как работали mint, melt и поддержка? Опишите конкретно.",
"reviews.dialog.bodyPlaceholder.fedimint": "Как прошло подключение к системе, работали ли платежи Lightning и поддержка? Расскажите подробнее.", "reviews.dialog.bodyPlaceholder.fedimint": "Как прошло подключение к системе, работали ли платежи Lightning и поддержка? Расскажите подробнее.",
"fedimint.details.title": "Сведения о федерации" "fedimint.details.title": "Сведения о федерации",
"nav.lnurlMints": "LNURL-минты",
"footer.allLnurlMints": "Все LNURL-минты",
"lnurlMints.title": "LNURL-минты: минты предъявительских нот lnurlcash с отзывами | Cashumints.space",
"lnurlMints.description": "Все {total} LNURL-минтов, проиндексированных из сети Nostr: {online} онлайн, {offline} офлайн. Оценки сообщества, лимиты вывода и возможности каждого.",
"lnurlMints.heading": "LNURL-минты",
"lnurlMints.lede": "Все LNURL-минты, которые знает этот сайт. LNURL-минт выпускает предъявительские ноты lnurlcash: ноты Lightning, которые вы храните как секрет и которые можно провернуть, разделить, объединить или расплавить обратно в платёж Lightning. Каждый проверяется по HTTPS раз в несколько минут.",
"lnurlMints.search.label": "Поиск LNURL-минтов",
"lnurlMints.search.placeholder": "Поиск по имени или адресу…",
"lnurlMints.sort.label": "Сортировать LNURL-минты по",
"lnurlMints.hideOffline": "Скрыть офлайн",
"lnurlMints.showing": "Показано {total} LNURL-минтов ({online} онлайн, {offline} офлайн)",
"lnurlMints.showingFiltered": "Показано {shown} из {total} LNURL-минтов ({online} онлайн, {offline} офлайн)",
"lnurlMints.noResults": "Ни один LNURL-минт не подходит.",
"lnurlMints.emptyIndex": "LNURL-минты ещё не проиндексированы.",
"lnurl.crumb": "LNURL-минты",
"lnurl.breadcrumb": "Хлебные крошки",
"lnurl.copyAddress": "Скопировать адрес",
"lnurl.more.title": "Другие LNURL-минты",
"lnurl.more.all": "Все LNURL-минты →",
"lnurl.disclaimer": "LNURL-минт хранит ваши саты, пока вы держите его ноты, так что пользоваться им — значит доверять его оператору. Оценки — это мнения сообщества, а «онлайн» означает доступен, а не надёжен.",
"lnurl.features.title": "Возможности",
"lnurl.features.none": "Этот минт не объявлял, что он поддерживает.",
"lnurl.features.note": "Возможности — это то, что оператор минта объявил в Nostr. Там, где сайт может проверить их по HTTPS, он это делает и сообщает, если объявленное сейчас недоступно.",
"lnurl.features.notListed": "не объявлено",
"lnurl.features.unavailable": "недоступно",
"lnurl.feature.mint": "Чеканка через Lightning (LUD-06)",
"lnurl.feature.melt": "Плавка в Lightning",
"lnurl.feature.notes": "Проворот / разделение / объединение нот",
"lnurl.feature.lud16": "Lightning-адрес",
"lnurl.feature.lud21": "Проверка платежа (LUD-21)",
"lnurl.feature.signed-notes": "Подписанные ноты (офлайн-проверка)",
"lnurl.feature.onion": "Tor-адрес",
"lnurl.feature.rotate": "Провернуть ноту",
"lnurl.feature.split": "Разделить ноту",
"lnurl.feature.merge": "Объединить ноты",
"lnurl.feature.lud06": "LUD-06 payRequest",
"lnurl.feature.lud03": "LUD-03 withdrawRequest",
"lnurl.limits.label": "Лимиты вывода",
"lnurl.limits.notPublished": "не опубликовано",
"lnurl.limits.withdrawRow": "Вывод",
"lnurl.limits.disabled": "отключено",
"lnurl.limits.sats.one": "{n} сат",
"lnurl.limits.sats.few": "{n} сата",
"lnurl.limits.sats.many": "{n} сатов",
"lnurl.limits.sats.other": "{n} сатов",
"lnurl.details.title": "Данные минта",
"lnurl.details.address": "Lightning-адрес",
"lnurl.details.mintPubkey": "Публичный ключ минта",
"lnurl.details.onion": "Tor-адрес",
"lnurl.details.fee": "Комиссия за чеканку",
"lnurl.details.node": "Узел",
"lnurl.details.capacity": "Объявленная ёмкость",
"lnurl.kind.lightningAddress": "Lightning-адрес",
"lnurl.kind.mintPubkey": "публичный ключ минта",
"lnurl.kind.onionAddress": "Tor-адрес",
"lnurl.confirmed.label": "Последнее подтверждение",
"lnurl.confirmed.when": "подтверждён {when}",
"lnurl.fee.base.one": "{n} сат",
"lnurl.fee.base.few": "{n} сата",
"lnurl.fee.base.many": "{n} сатов",
"lnurl.fee.base.other": "{n} сатов",
"lnurl.fee.percent": "{percent}%",
"home.topLnurl.title": "Лучшие LNURL-минты",
"home.topLnurl.sub": "по отзывам и оценке",
"home.topLnurl.all": "Все {n} LNURL-минтов →",
"feed.ecosystem.lnurl": "LNURL",
"reviews.dialog.bodyPlaceholder.lnurl": "Как прошли чеканка, плавка и проворот нот? Пишите конкретно.",
"reviews.byUrl.open": "Написать отзыв",
"reviews.byUrl.title": "Написать отзыв",
"reviews.byUrl.lede": "Вставь адрес. Если сайт его ещё не знает, он проверит его и добавит прямо сейчас.",
"reviews.byUrl.label.cashu": "URL минта",
"reviews.byUrl.label.fedimint": "Код приглашения федерации",
"reviews.byUrl.label.lnurl": "URL LNURL-минта",
"reviews.byUrl.placeholder.cashu": "mint.example.com",
"reviews.byUrl.placeholder.fedimint": "fed11…",
"reviews.byUrl.placeholder.lnurl": "mint.example.com",
"reviews.byUrl.check": "Проверить этот адрес",
"reviews.byUrl.checking": "Проверяем…",
"reviews.byUrl.back": "Другой адрес",
"reviews.byUrl.existing": "Уже в списке",
"reviews.byUrl.indexed": "Только что добавлен",
"reviews.byUrl.confirmClose": "Закрыть? Введённый адрес не сохранится.",
"reviews.byUrl.detected": "Этот адрес ответил как минт {kind}.",
"reviews.byUrl.switchTo": "Оценить как минт {kind}",
"reviews.byUrl.error.empty": "Сначала введи адрес.",
"reviews.byUrl.error.badUrl": "Это не похоже на URL минта.",
"reviews.byUrl.error.badInvite": "Это не код приглашения Fedimint. Коды начинаются с fed11.",
"reviews.byUrl.error.badInput": "Сайт не может проверить этот адрес.",
"reviews.byUrl.error.blocked": "Это не публичный адрес, поэтому проверить его нельзя.",
"reviews.byUrl.error.notAMint": "Адрес ответил, но не как минт этого типа.",
"reviews.byUrl.error.wrongType": "Адрес ответил, но как минт другого типа.",
"reviews.byUrl.error.unverifiable": "По этому адресу ничего не ответило, и в Nostr о нём пока нет ни следа.",
"reviews.byUrl.error.rateLimited.one": "Слишком много проверок с одного соединения. Попробуй снова примерно через {n} минуту.",
"reviews.byUrl.error.rateLimited.other": "Слишком много проверок с одного соединения. Попробуй снова примерно через {n} минут.",
"reviews.byUrl.error.network": "Не удалось связаться с индексом. Проверь соединение и попробуй снова.",
"seo.lnurl.titleNoReviews": "{name}: информация и отзывы о LNURL-минте | Cashumints.space",
"seo.desc.statusWithdraw": "{status}, вывод {min}–{max} сат.",
"mint.chip.noFunding": "Нет чеканки / плавки",
"mint.warnings.lnurlWithdrawalsDisabled.lead": "Вывод отключён.",
"mint.warnings.lnurlWithdrawalsDisabled.body.online": "Этот минт объявляет максимальный вывод в ноль, поэтому ни одну выпущенную им ноту нельзя ни на что обменять. Не чеканьте здесь, пока это не изменится.",
"mint.warnings.lnurlWithdrawalsDisabled.body.offline": "При последнем контакте этот минт объявлял максимальный вывод в ноль, поэтому ни одну выпущенную им ноту нельзя было ни на что обменять. Не чеканьте здесь, пока это не изменится.",
"mint.warnings.lnurlWithdrawalsDisabled.meta.online": "Вывод сейчас отключён",
"mint.warnings.lnurlWithdrawalsDisabled.meta.offline": "При последнем контакте вывод был отключён",
"mint.warnings.lnurlInvalid.lead": "Эндпоинт отвечает, но некорректно.",
"mint.warnings.lnurlInvalid.body": "Хост отвечает, но не объявлением минта, которое сайт может прочитать, поэтому вывод может не работать. Ниже — последнее состояние, которое удалось разобрать, а отзывы по-прежнему работают.",
"mint.warnings.lnurlInvalid.meta": "Отвечает, но не корректным объявлением минта",
"mint.warnings.lnurlNoFunding.lead": "Чеканка и плавка недоступны.",
"mint.warnings.lnurlNoFunding.body": "Существующие ноты по-прежнему можно провернуть, разделить или объединить, но сейчас через Lightning ничего не входит и не выходит. Сам минт работает и отвечает; узел Lightning за ним для него недоступен.",
"mint.warnings.lnurlNoFunding.meta": "Чеканка и плавка недоступны",
"mint.warnings.also.noFunding": "Его узел Lightning тоже был недоступен, поэтому ничего нельзя было отчеканить или расплавить."
} }
+118 -3
View File
@@ -114,6 +114,8 @@
"card.reviewed": "recenserat {when}", "card.reviewed": "recenserat {when}",
"card.noReviews": "inga recensioner ännu", "card.noReviews": "inga recensioner ännu",
"mint.title": "{name} · Cashu – recensioner i nyskick", "mint.title": "{name} · Cashu – recensioner i nyskick",
"mint.titleFedimint": "{name} · Recensioner av Fedimint-federationen",
"mint.titleLnurl": "{name} · Recensioner av LNURL-minten",
"mint.breadcrumb": "Navigationsspår", "mint.breadcrumb": "Navigationsspår",
"mint.crumbMints": "Cashu-pastiller", "mint.crumbMints": "Cashu-pastiller",
"mint.copyUrl": "Kopiera Mint-URL:en", "mint.copyUrl": "Kopiera Mint-URL:en",
@@ -325,7 +327,7 @@
"reviews.validate.short": "Skriv minst {n} tecken så att recensionen blir till nytta.", "reviews.validate.short": "Skriv minst {n} tecken så att recensionen blir till nytta.",
"reviews.validate.long": "Recensioner får högst {n} tecken.", "reviews.validate.long": "Recensioner får högst {n} tecken.",
"feed.title": "Senaste recensioner av mynt och federationer · Cashumints.space", "feed.title": "Senaste recensioner av mynt och federationer · Cashumints.space",
"feed.description": "Alla recensioner av mynt och federationer i takt med att de publiceras: Recensioner av {reviews} från mynten {mints} och Cashu samt federationerna {federations} och Fedimint, signerade på Nostr och tillgängliga här, med de senaste först. Filtrera efter ekosystem, betyg eller namn.", "feed.description": "Varje recension av mints, federationer och LNURL-mints så fort den publiceras: {reviews} recensioner om {mints} Cashu-mints, {federations} Fedimint-federationer och {lnurl} LNURL-mints, signerade på Nostr och läsbara här med nyast först. Filtrera på ekosystem, betyg eller namn.",
"feed.heading": "Senaste recensionerna", "feed.heading": "Senaste recensionerna",
"feed.lede": "Alla recensioner av alla Cashu-mint- och Fedimint-federationer, med de senaste först, läses direkt från Nostr-reläerna. Varje recension är signerad av sin författare och finns lagrad på reläerna, inte i vår databas.", "feed.lede": "Alla recensioner av alla Cashu-mint- och Fedimint-federationer, med de senaste först, läses direkt från Nostr-reläerna. Varje recension är signerad av sin författare och finns lagrad på reläerna, inte i vår databas.",
"feed.label": "Recensioner", "feed.label": "Recensioner",
@@ -400,7 +402,14 @@
"notfound.fb.reviews": "Recensioner", "notfound.fb.reviews": "Recensioner",
"notfound.fb.software": "Programvara", "notfound.fb.software": "Programvara",
"notfound.fb.lastOnline": "Senast online", "notfound.fb.lastOnline": "Senast online",
"notfound.fb.note": "Denna recension indexerades efter det att webbplatsen senast uppdaterades, så detta är en sammanfattning från live-API:et. Hela sidan med recensioner visas vid nästa uppdatering.", "notfound.fb.note": "Den här minten indexerades efter att sajten senast byggdes, så det här är en sammanfattning från det live-API:t, med recensionerna nedanför. Hela sidan dyker upp vid nästa bygge.",
"notfound.resolve.checking": "Första besöket: kontrollerar minten nu.",
"notfound.resolve.failed": "Den här minten kunde inte läggas till",
"notfound.resolve.failedFedimint": "Den här federationen kunde inte läggas till",
"notfound.resolve.retry": "Prova en annan adress",
"notfound.resolve.switch": "Öppna den som {kind}-mint",
"notfound.resolve.fedimint": "En federations sidadress innehåller inte dess invite code, så den här går inte att slå upp enbart via länken. Klistra in invite code så läggs den till.",
"notfound.resolve.ambiguous": "Adressen går inte att härleda enbart ur länken. Klistra in mintens fullständiga URL så läggs den till.",
"wallets.title": "Cashu-plånböcker · Cashumints.space", "wallets.title": "Cashu-plånböcker · Cashumints.space",
"wallets.description": "Cashu-plånböcker som fungerar med de mynt som anges här, både på mobil, webben, i webbläsaren och via kommandoraden, och som utnyttjar respektive plattforms styrkor.", "wallets.description": "Cashu-plånböcker som fungerar med de mynt som anges här, både på mobil, webben, i webbläsaren och via kommandoraden, och som utnyttjar respektive plattforms styrkor.",
"wallets.heading": "Plånböcker", "wallets.heading": "Plånböcker",
@@ -654,5 +663,111 @@
"footer.allFedimints": "Alla förbund", "footer.allFedimints": "Alla förbund",
"reviews.dialog.bodyPlaceholder.cashu": "Hur gick det med präglingen, smältningen och stödet? Var så detaljerad som möjligt.", "reviews.dialog.bodyPlaceholder.cashu": "Hur gick det med präglingen, smältningen och stödet? Var så detaljerad som möjligt.",
"reviews.dialog.bodyPlaceholder.fedimint": "Hur gick det med registreringen, betalningarna med Lightning och supporten? Var så detaljerad som möjligt.", "reviews.dialog.bodyPlaceholder.fedimint": "Hur gick det med registreringen, betalningarna med Lightning och supporten? Var så detaljerad som möjligt.",
"fedimint.details.title": "Uppgifter om förbundet" "fedimint.details.title": "Uppgifter om förbundet",
"nav.lnurlMints": "LNURL-mints",
"footer.allLnurlMints": "Alla LNURL-mints",
"lnurlMints.title": "LNURL-mints: mints för lnurlcash-innehavarsedlar, recenserade | Cashumints.space",
"lnurlMints.description": "Alla {total} LNURL-mints som indexerats från Nostr-nätverket: {online} online, {offline} offline. Betyg från gemenskapen, uttagsgränser och funktioner för var och en.",
"lnurlMints.heading": "LNURL-mints",
"lnurlMints.lede": "Varje LNURL-mint den här sajten känner till. En LNURL-mint ger ut lnurlcash-innehavarsedlar: Lightning-sedlar som du håller som en hemlighet och som kan roteras, delas, slås ihop eller smältas tillbaka till en Lightning-betalning. Var och en kontrolleras via HTTPS med några minuters mellanrum.",
"lnurlMints.search.label": "Sök LNURL-mints",
"lnurlMints.search.placeholder": "Sök på namn eller adress…",
"lnurlMints.sort.label": "Sortera LNURL-mints efter",
"lnurlMints.hideOffline": "Dölj offline",
"lnurlMints.showing": "Visar {total} LNURL-mints ({online} online, {offline} offline)",
"lnurlMints.showingFiltered": "Visar {shown} av {total} LNURL-mints ({online} online, {offline} offline)",
"lnurlMints.noResults": "Ingen LNURL-mint matchar det.",
"lnurlMints.emptyIndex": "Inga LNURL-mints är indexerade ännu.",
"lnurl.crumb": "LNURL-mints",
"lnurl.breadcrumb": "Brödsmulor",
"lnurl.copyAddress": "Kopiera adress",
"lnurl.more.title": "Fler LNURL-mints",
"lnurl.more.all": "Alla LNURL-mints →",
"lnurl.disclaimer": "En LNURL-mint förvarar dina sats medan du håller dess sedlar, så att använda en betyder att lita på dess operatör. Betyg är åsikter från gemenskapen, och online betyder nåbar, inte pålitlig.",
"lnurl.features.title": "Funktioner",
"lnurl.features.none": "Den här minten har inte annonserat vad den stöder.",
"lnurl.features.note": "Funktioner är vad mintens operatör annonserat på Nostr. Där sajten kan kontrollera en via HTTPS gör den det, och säger till när något annonserat inte är tillgängligt just nu.",
"lnurl.features.notListed": "inte annonserat",
"lnurl.features.unavailable": "otillgängligt",
"lnurl.feature.mint": "Minta via Lightning (LUD-06)",
"lnurl.feature.melt": "Smälta till Lightning",
"lnurl.feature.notes": "Rotera / dela / slå ihop sedlar",
"lnurl.feature.lud16": "Lightning-adress",
"lnurl.feature.lud21": "Betalningsverifiering (LUD-21)",
"lnurl.feature.signed-notes": "Signerade sedlar (offlineverifiering)",
"lnurl.feature.onion": "Tor-adress",
"lnurl.feature.rotate": "Rotera en sedel",
"lnurl.feature.split": "Dela en sedel",
"lnurl.feature.merge": "Slå ihop sedlar",
"lnurl.feature.lud06": "LUD-06 payRequest",
"lnurl.feature.lud03": "LUD-03 withdrawRequest",
"lnurl.limits.label": "Uttagsgränser",
"lnurl.limits.notPublished": "inte publicerat",
"lnurl.limits.withdrawRow": "Uttag",
"lnurl.limits.disabled": "avstängt",
"lnurl.limits.sats.one": "{n} sat",
"lnurl.limits.sats.other": "{n} sats",
"lnurl.details.title": "Uppgifter om minten",
"lnurl.details.address": "Lightning-adress",
"lnurl.details.mintPubkey": "Mintens publika nyckel",
"lnurl.details.onion": "Tor-adress",
"lnurl.details.fee": "Mintavgift",
"lnurl.details.node": "Nod",
"lnurl.details.capacity": "Annonserad kapacitet",
"lnurl.kind.lightningAddress": "Lightning-adress",
"lnurl.kind.mintPubkey": "mintens publika nyckel",
"lnurl.kind.onionAddress": "Tor-adress",
"lnurl.confirmed.label": "Senast bekräftad",
"lnurl.confirmed.when": "bekräftad {when}",
"lnurl.fee.base.one": "{n} sat",
"lnurl.fee.base.other": "{n} sats",
"lnurl.fee.percent": "{percent} %",
"home.topLnurl.title": "Toppade LNURL-mints",
"home.topLnurl.sub": "rankade efter recensioner och betyg",
"home.topLnurl.all": "Alla {n} LNURL-mints →",
"feed.ecosystem.lnurl": "LNURL",
"reviews.dialog.bodyPlaceholder.lnurl": "Hur gick mintning, smältning och rotering av sedlar? Var konkret.",
"reviews.byUrl.open": "Skriv en recension",
"reviews.byUrl.title": "Skriv en recension",
"reviews.byUrl.lede": "Klistra in adressen. Om sajten inte känner till den än kontrolleras och läggs den till nu.",
"reviews.byUrl.label.cashu": "Mint-URL",
"reviews.byUrl.label.fedimint": "Federationens invite code",
"reviews.byUrl.label.lnurl": "URL till LNURL-mint",
"reviews.byUrl.placeholder.cashu": "mint.example.com",
"reviews.byUrl.placeholder.fedimint": "fed11…",
"reviews.byUrl.placeholder.lnurl": "mint.example.com",
"reviews.byUrl.check": "Kontrollera adressen",
"reviews.byUrl.checking": "Kontrollerar…",
"reviews.byUrl.back": "Använd en annan adress",
"reviews.byUrl.existing": "Finns redan med",
"reviews.byUrl.indexed": "Nyss tillagd",
"reviews.byUrl.confirmClose": "Stänga? Adressen du skrev sparas inte.",
"reviews.byUrl.detected": "Adressen svarade som en {kind}-mint.",
"reviews.byUrl.switchTo": "Recensera den som {kind}-mint",
"reviews.byUrl.error.empty": "Skriv en adress först.",
"reviews.byUrl.error.badUrl": "Det ser inte ut som en mint-URL.",
"reviews.byUrl.error.badInvite": "Det är ingen Fedimint invite code. Koder börjar med fed11.",
"reviews.byUrl.error.badInput": "Sajten kan inte kontrollera den adressen.",
"reviews.byUrl.error.blocked": "Det är ingen offentlig adress, så den går inte att kontrollera.",
"reviews.byUrl.error.notAMint": "Adressen svarade, men inte som en mint av det här slaget.",
"reviews.byUrl.error.wrongType": "Adressen svarade, men som ett annat slags mint.",
"reviews.byUrl.error.unverifiable": "Inget svarade på den adressen, och det finns ännu inga spår av den på Nostr.",
"reviews.byUrl.error.rateLimited.one": "Det är många kontroller från en och samma anslutning. Försök igen om ungefär {n} minut.",
"reviews.byUrl.error.rateLimited.other": "Det är många kontroller från en och samma anslutning. Försök igen om ungefär {n} minuter.",
"reviews.byUrl.error.network": "Indexet gick inte att nå. Kontrollera din uppkoppling och försök igen.",
"seo.lnurl.titleNoReviews": "{name}: info och recensioner om LNURL-minten | Cashumints.space",
"seo.desc.statusWithdraw": "{status}, uttag {min}–{max} sat.",
"mint.chip.noFunding": "Ingen mint / smält",
"mint.warnings.lnurlWithdrawalsDisabled.lead": "Uttag är avstängda.",
"mint.warnings.lnurlWithdrawalsDisabled.body.online": "Den här minten annonserar ett maximalt uttag på noll, så ingen sedel den ger ut kan lösas in mot någonting. Minta inte här förrän det ändras.",
"mint.warnings.lnurlWithdrawalsDisabled.body.offline": "Den här minten annonserade ett maximalt uttag på noll när den senast nåddes, så ingen sedel den gav ut kunde lösas in mot någonting. Minta inte här förrän det ändras.",
"mint.warnings.lnurlWithdrawalsDisabled.meta.online": "Uttag är avstängda just nu",
"mint.warnings.lnurlWithdrawalsDisabled.meta.offline": "Uttag var avstängda när den senast sågs",
"mint.warnings.lnurlInvalid.lead": "Endpunkten svarar men är ogiltig.",
"mint.warnings.lnurlInvalid.body": "Värden svarar, men inte med en mint-annonsering som den här sajten kan läsa, så uttag kanske inte fungerar. Allt nedan är det senaste tillstånd som gick att läsa, och recensioner fungerar fortfarande.",
"mint.warnings.lnurlInvalid.meta": "Svarar, men inte med en giltig mint-annonsering",
"mint.warnings.lnurlNoFunding.lead": "Mintning och smältning är otillgängliga.",
"mint.warnings.lnurlNoFunding.body": "Befintliga sedlar kan fortfarande roteras, delas eller slås ihop, men just nu rör sig ingenting in eller ut via Lightning. Själva minten är uppe och svarar; Lightning-noden bakom den är inte nåbar därifrån.",
"mint.warnings.lnurlNoFunding.meta": "Mintning och smältning otillgängliga",
"mint.warnings.also.noFunding": "Dess Lightning-nod var också onåbar, så ingenting kunde mintas eller smältas."
} }
+118 -3
View File
@@ -114,6 +114,8 @@
"card.reviewed": "İnceleme {when}", "card.reviewed": "İnceleme {when}",
"card.noReviews": "Henüz inceleme yok", "card.noReviews": "Henüz inceleme yok",
"mint.title": "{name} · Cashu mint incelemeleri", "mint.title": "{name} · Cashu mint incelemeleri",
"mint.titleFedimint": "{name} · Fedimint federasyonu değerlendirmeleri",
"mint.titleLnurl": "{name} · LNURL mint değerlendirmeleri",
"mint.breadcrumb": "Ekmek kırıntısı", "mint.breadcrumb": "Ekmek kırıntısı",
"mint.crumbMints": "Cashu darphaneleri", "mint.crumbMints": "Cashu darphaneleri",
"mint.copyUrl": "Kopyala URL", "mint.copyUrl": "Kopyala URL",
@@ -325,7 +327,7 @@
"reviews.validate.short": "En az {n} karakter yaz ki inceleme faydalı olsun.", "reviews.validate.short": "En az {n} karakter yaz ki inceleme faydalı olsun.",
"reviews.validate.long": "İncelemeler {n} karakterle sınırlıdır.", "reviews.validate.long": "İncelemeler {n} karakterle sınırlıdır.",
"feed.title": "En son darphaneler ve federasyon incelemeleri · Cashumints.space", "feed.title": "En son darphaneler ve federasyon incelemeleri · Cashumints.space",
"feed.description": "Her mint ve federasyon incelemesi yayımlandığı halde: {mints} Cashu darphaneleri ve {federations} Fedimint federasyonlarında {reviews} incelemeler, Nostr'da imzalanmış ve burada en yeni en yeni olarak okunabilir. Ekosisteme, dereceye veya isme göre filtreleyin.", "feed.description": "Her mint, federasyon ve LNURL mint değerlendirmesi yayımlandığı anda: {mints} Cashu mint'i, {federations} Fedimint federasyonu ve {lnurl} LNURL mint'i hakkında {reviews} değerlendirme, Nostr üzerinde imzalanmış ve burada en yeniden başlayarak okunabilir. Ekosisteme, puana veya isme göre filtreleyin.",
"feed.heading": "Son incelemeler", "feed.heading": "Son incelemeler",
"feed.lede": "Her Cashu darphanesi ve Fedimint federasyonunun her incelemesi, en yenisi ilk olarak, Nostr rölelerinden canlı olarak okundu. Her biri yazarı tarafından imzalanmıştır ve rölelerde yaşıyor, veritabanımızda değil.", "feed.lede": "Her Cashu darphanesi ve Fedimint federasyonunun her incelemesi, en yenisi ilk olarak, Nostr rölelerinden canlı olarak okundu. Her biri yazarı tarafından imzalanmıştır ve rölelerde yaşıyor, veritabanımızda değil.",
"feed.label": "İncelemeler", "feed.label": "İncelemeler",
@@ -400,7 +402,14 @@
"notfound.fb.reviews": "İncelemeler", "notfound.fb.reviews": "İncelemeler",
"notfound.fb.software": "Yazılım", "notfound.fb.software": "Yazılım",
"notfound.fb.lastOnline": "Son çevrimiçi", "notfound.fb.lastOnline": "Son çevrimiçi",
"notfound.fb.note": "Bu mint, site son inşa edildikten sonra indekslendi, bu yüzden canlı API'den bir özet. Yorumların olduğu tam sayfa bir sonraki yeniden yapımda görünür.", "notfound.fb.note": "Bu mint, site en son derlendikten sonra dizine eklendi; bu yüzden burada canlı API’den bir özet ve altında değerlendirmeleri var. Sayfanın tamamı bir sonraki derlemede görünür.",
"notfound.resolve.checking": "İlk ziyaret: bu mint şimdi kontrol ediliyor.",
"notfound.resolve.failed": "Bu mint eklenemedi",
"notfound.resolve.failedFedimint": "Bu federasyon eklenemedi",
"notfound.resolve.retry": "Başka bir adres dene",
"notfound.resolve.switch": "{kind} mint olarak aç",
"notfound.resolve.fedimint": "Bir federasyonun sayfa adresi davet kodunu taşımaz, bu yüzden bu federasyon yalnızca bağlantıyla bulunamaz. Davet kodunu yapıştır, federasyon eklenir.",
"notfound.resolve.ambiguous": "Bu adres yalnızca bağlantıdan çıkarılamıyor. Mintin tam URL’sini yapıştır, mint eklenir.",
"wallets.title": "Cashu cüzdanları · Cashumints.space", "wallets.title": "Cashu cüzdanları · Cashumints.space",
"wallets.description": "Burada listelenen mintlerle mobil, web, tarayıcı ve komut satırı üzerinden çalışan Cashu cüzdanları, her birinin neye iyi olduğunu gösteriyor.", "wallets.description": "Burada listelenen mintlerle mobil, web, tarayıcı ve komut satırı üzerinden çalışan Cashu cüzdanları, her birinin neye iyi olduğunu gösteriyor.",
"wallets.heading": "Cüzdanlar", "wallets.heading": "Cüzdanlar",
@@ -654,5 +663,111 @@
"footer.allFedimints": "Tüm federasyonlar", "footer.allFedimints": "Tüm federasyonlar",
"reviews.dialog.bodyPlaceholder.cashu": "Basıcılık, eritme ve destek nasıl sonuçlandı? Spesifik olun.", "reviews.dialog.bodyPlaceholder.cashu": "Basıcılık, eritme ve destek nasıl sonuçlandı? Spesifik olun.",
"reviews.dialog.bodyPlaceholder.fedimint": "Katılım, Lightning ödemeleri ve destek nasıl sonuçlandı? Spesifik olun.", "reviews.dialog.bodyPlaceholder.fedimint": "Katılım, Lightning ödemeleri ve destek nasıl sonuçlandı? Spesifik olun.",
"fedimint.details.title": "Federasyon detayları" "fedimint.details.title": "Federasyon detayları",
"nav.lnurlMints": "LNURL mint'leri",
"footer.allLnurlMints": "Tüm LNURL mint'leri",
"lnurlMints.title": "LNURL mint'leri: lnurlcash hamiline senet mint'leri, değerlendirmelerle | Cashumints.space",
"lnurlMints.description": "Nostr ağından dizinlenen {total} LNURL mint'inin tamamı: {online} çevrimiçi, {offline} çevrimdışı. Her biri için topluluk puanları, çekim limitleri ve özellikler.",
"lnurlMints.heading": "LNURL mint'leri",
"lnurlMints.lede": "Bu sitenin bildiği her LNURL mint'i. Bir LNURL mint'i lnurlcash hamiline senetleri çıkarır: bir sır olarak sakladığınız, döndürülebilen, bölünebilen, birleştirilebilen veya bir Lightning ödemesine geri eritilebilen Lightning senetleri. Her biri birkaç dakikada bir HTTPS üzerinden kontrol edilir.",
"lnurlMints.search.label": "LNURL mint'lerinde ara",
"lnurlMints.search.placeholder": "İsme veya adrese göre ara…",
"lnurlMints.sort.label": "LNURL mint'lerini şuna göre sırala",
"lnurlMints.hideOffline": "Çevrimdışıları gizle",
"lnurlMints.showing": "{total} LNURL mint'i gösteriliyor ({online} çevrimiçi, {offline} çevrimdışı)",
"lnurlMints.showingFiltered": "{total} LNURL mint'inden {shown} tanesi gösteriliyor ({online} çevrimiçi, {offline} çevrimdışı)",
"lnurlMints.noResults": "Eşleşen LNURL mint'i yok.",
"lnurlMints.emptyIndex": "Henüz dizinlenmiş LNURL mint'i yok.",
"lnurl.crumb": "LNURL mint'leri",
"lnurl.breadcrumb": "Gezinti yolu",
"lnurl.copyAddress": "Adresi kopyala",
"lnurl.more.title": "Daha fazla LNURL mint'i",
"lnurl.more.all": "Tüm LNURL mint'leri →",
"lnurl.disclaimer": "Bir LNURL mint'i siz onun senetlerini tutarken sizin sat'larınızı tutar; yani birini kullanmak işletmecisine güvenmek demektir. Puanlar topluluk görüşüdür ve çevrimiçi, erişilebilir demektir, güvenilir değil.",
"lnurl.features.title": "Özellikler",
"lnurl.features.none": "Bu mint neyi desteklediğini duyurmadı.",
"lnurl.features.note": "Özellikler, mint işletmecisinin Nostr'da duyurduklarıdır. Bu site birini HTTPS üzerinden doğrulayabildiğinde doğrular ve duyurulan bir şey şu anda kullanılamıyorsa bunu belirtir.",
"lnurl.features.notListed": "duyurulmadı",
"lnurl.features.unavailable": "kullanılamıyor",
"lnurl.feature.mint": "Lightning ile mint (LUD-06)",
"lnurl.feature.melt": "Lightning'e erit",
"lnurl.feature.notes": "Senetleri döndür / böl / birleştir",
"lnurl.feature.lud16": "Lightning adresi",
"lnurl.feature.lud21": "Ödeme doğrulaması (LUD-21)",
"lnurl.feature.signed-notes": "İmzalı senetler (çevrimdışı doğrulama)",
"lnurl.feature.onion": "Tor adresi",
"lnurl.feature.rotate": "Bir seneti döndür",
"lnurl.feature.split": "Bir seneti böl",
"lnurl.feature.merge": "Senetleri birleştir",
"lnurl.feature.lud06": "LUD-06 payRequest",
"lnurl.feature.lud03": "LUD-03 withdrawRequest",
"lnurl.limits.label": "Çekim limitleri",
"lnurl.limits.notPublished": "yayımlanmadı",
"lnurl.limits.withdrawRow": "Çekim",
"lnurl.limits.disabled": "devre dışı",
"lnurl.limits.sats.one": "{n} sat",
"lnurl.limits.sats.other": "{n} sat",
"lnurl.details.title": "Mint ayrıntıları",
"lnurl.details.address": "Lightning adresi",
"lnurl.details.mintPubkey": "Mint açık anahtarı",
"lnurl.details.onion": "Tor adresi",
"lnurl.details.fee": "Mint ücreti",
"lnurl.details.node": "Düğüm",
"lnurl.details.capacity": "Duyurulan kapasite",
"lnurl.kind.lightningAddress": "Lightning adresi",
"lnurl.kind.mintPubkey": "mint açık anahtarı",
"lnurl.kind.onionAddress": "Tor adresi",
"lnurl.confirmed.label": "Son doğrulama",
"lnurl.confirmed.when": "{when} doğrulandı",
"lnurl.fee.base.one": "{n} sat",
"lnurl.fee.base.other": "{n} sat",
"lnurl.fee.percent": "%{percent}",
"home.topLnurl.title": "En iyi LNURL mint'leri",
"home.topLnurl.sub": "değerlendirme ve puana göre sıralandı",
"home.topLnurl.all": "{n} LNURL mint'inin tümü →",
"feed.ecosystem.lnurl": "LNURL",
"reviews.dialog.bodyPlaceholder.lnurl": "Mint etme, eritme ve senet döndürme nasıl gitti? Somut olun.",
"reviews.byUrl.open": "Bir değerlendirme yaz",
"reviews.byUrl.title": "Bir değerlendirme yaz",
"reviews.byUrl.lede": "Adresi yapıştır. Bu site adresi henüz bilmiyorsa şimdi kontrol eder ve ekler.",
"reviews.byUrl.label.cashu": "Mint URL’si",
"reviews.byUrl.label.fedimint": "Federasyonun davet kodu",
"reviews.byUrl.label.lnurl": "LNURL mint URL’si",
"reviews.byUrl.placeholder.cashu": "mint.example.com",
"reviews.byUrl.placeholder.fedimint": "fed11…",
"reviews.byUrl.placeholder.lnurl": "mint.example.com",
"reviews.byUrl.check": "Bu adresi kontrol et",
"reviews.byUrl.checking": "Kontrol ediliyor…",
"reviews.byUrl.back": "Başka bir adres kullan",
"reviews.byUrl.existing": "Zaten listede",
"reviews.byUrl.indexed": "Yeni eklendi",
"reviews.byUrl.confirmClose": "Kapatılsın mı? Yazdığın adres saklanmaz.",
"reviews.byUrl.detected": "Bu adres bir {kind} mint olarak yanıt verdi.",
"reviews.byUrl.switchTo": "{kind} mint olarak değerlendir",
"reviews.byUrl.error.empty": "Önce bir adres gir.",
"reviews.byUrl.error.badUrl": "Bu bir mint URL’sine benzemiyor.",
"reviews.byUrl.error.badInvite": "Bu bir Fedimint davet kodu değil. Kodlar fed11 ile başlar.",
"reviews.byUrl.error.badInput": "Bu site o adresi kontrol edemiyor.",
"reviews.byUrl.error.blocked": "Bu genel bir adres değil, bu yüzden kontrol edilemiyor.",
"reviews.byUrl.error.notAMint": "Bu adres yanıt verdi, ama bu türden bir mint olarak değil.",
"reviews.byUrl.error.wrongType": "Bu adres yanıt verdi, ama başka türden bir mint olarak.",
"reviews.byUrl.error.unverifiable": "Bu adreste hiçbir şey yanıt vermedi ve Nostr’da da henüz izine rastlanmadı.",
"reviews.byUrl.error.rateLimited.one": "Tek bağlantıdan çok fazla kontrol yapıldı. Yaklaşık {n} dakika sonra tekrar dene.",
"reviews.byUrl.error.rateLimited.other": "Tek bağlantıdan çok fazla kontrol yapıldı. Yaklaşık {n} dakika sonra tekrar dene.",
"reviews.byUrl.error.network": "Dizine ulaşılamadı. Bağlantını kontrol edip tekrar dene.",
"seo.lnurl.titleNoReviews": "{name}: LNURL mint bilgileri ve değerlendirmeleri | Cashumints.space",
"seo.desc.statusWithdraw": "{status}, çekim {min}–{max} sat.",
"mint.chip.noFunding": "Mint / erit yok",
"mint.warnings.lnurlWithdrawalsDisabled.lead": "Çekimler devre dışı.",
"mint.warnings.lnurlWithdrawalsDisabled.body.online": "Bu mint azami çekimi sıfır olarak duyuruyor, yani çıkardığı hiçbir senet hiçbir şeye çevrilemez. Bu değişene kadar burada mint etmeyin.",
"mint.warnings.lnurlWithdrawalsDisabled.body.offline": "Bu mint'e en son ulaşıldığında azami çekimi sıfır olarak duyuruyordu, yani çıkardığı hiçbir senet hiçbir şeye çevrilemiyordu. Bu değişene kadar burada mint etmeyin.",
"mint.warnings.lnurlWithdrawalsDisabled.meta.online": "Çekimler şu anda devre dışı",
"mint.warnings.lnurlWithdrawalsDisabled.meta.offline": "En son görüldüğünde çekimler devre dışıydı",
"mint.warnings.lnurlInvalid.lead": "Uç nokta yanıt veriyor ama geçersiz.",
"mint.warnings.lnurlInvalid.body": "Sunucu yanıt veriyor, ancak bu sitenin okuyabileceği bir mint duyurusuyla değil; bu yüzden çekimler çalışmayabilir. Aşağıdakiler okunabilen son durumdur ve değerlendirmeler hâlâ çalışıyor.",
"mint.warnings.lnurlInvalid.meta": "Yanıt veriyor, ama geçerli bir mint duyurusuyla değil",
"mint.warnings.lnurlNoFunding.lead": "Mint etme ve eritme kullanılamıyor.",
"mint.warnings.lnurlNoFunding.body": "Mevcut senetler hâlâ döndürülebilir, bölünebilir veya birleştirilebilir, ancak şu anda Lightning üzerinden hiçbir şey girip çıkmıyor. Mint'in kendisi ayakta ve yanıt veriyor; arkasındaki Lightning düğümüne oradan erişilemiyor.",
"mint.warnings.lnurlNoFunding.meta": "Mint etme ve eritme kullanılamıyor",
"mint.warnings.also.noFunding": "Lightning düğümüne de erişilemiyordu, bu yüzden hiçbir şey mint edilemez veya eritilemezdi."
} }
+122 -3
View File
@@ -116,6 +116,8 @@
"card.reviewed": "відгук {when}", "card.reviewed": "відгук {when}",
"card.noReviews": "Поки що немає відгуків", "card.noReviews": "Поки що немає відгуків",
"mint.title": "{name} · Відгуки про мінт Cashu", "mint.title": "{name} · Відгуки про мінт Cashu",
"mint.titleFedimint": "{name} · Відгуки про федерацію Fedimint",
"mint.titleLnurl": "{name} · Відгуки про LNURL-мінт",
"mint.breadcrumb": "Навігаційний ланцюжок", "mint.breadcrumb": "Навігаційний ланцюжок",
"mint.crumbMints": "Мінти Cashu", "mint.crumbMints": "Мінти Cashu",
"mint.copyUrl": "Скопіювати URL мінту", "mint.copyUrl": "Скопіювати URL мінту",
@@ -351,7 +353,7 @@
"reviews.validate.short": "Напишіть щонайменше {n} символів, щоб відгук був корисним.", "reviews.validate.short": "Напишіть щонайменше {n} символів, щоб відгук був корисним.",
"reviews.validate.long": "Відгук може містити не більше {n} символів.", "reviews.validate.long": "Відгук може містити не більше {n} символів.",
"feed.title": "Останні відгуки про мінти та федерації · Cashumints.space", "feed.title": "Останні відгуки про мінти та федерації · Cashumints.space",
"feed.description": "Усі відгуки про мінти й федерації одразу після публікації: {reviews} відгуків про {mints} мінтів Cashu та {federations} федерацій Fedimint, підписаних у Nostr. Нові відгуки показано першими. Фільтруйте за екосистемою, оцінкою або назвою.", "feed.description": "Кожен відгук про мінт, федерацію та LNURL-мінт одразу після публікації: {reviews} відгуків про {mints} мінтів Cashu, {federations} федерацій Fedimint і {lnurl} LNURL-мінтів, підписаних у Nostr і доступних тут від найновіших. Фільтруйте за екосистемою, оцінкою або назвою.",
"feed.heading": "Останні огляди", "feed.heading": "Останні огляди",
"feed.lede": "Усі відгуки про кожен мінт Cashu і кожну федерацію Fedimint, від найновіших, читаються безпосередньо з Nostr relays. Кожен відгук підписаний автором і зберігається на relays, а не в нашій базі даних.", "feed.lede": "Усі відгуки про кожен мінт Cashu і кожну федерацію Fedimint, від найновіших, читаються безпосередньо з Nostr relays. Кожен відгук підписаний автором і зберігається на relays, а не в нашій базі даних.",
"feed.label": "Відгуки", "feed.label": "Відгуки",
@@ -428,7 +430,14 @@
"notfound.fb.reviews": "Відгуки", "notfound.fb.reviews": "Відгуки",
"notfound.fb.software": "Програмне забезпечення", "notfound.fb.software": "Програмне забезпечення",
"notfound.fb.lastOnline": "Востаннє онлайн", "notfound.fb.lastOnline": "Востаннє онлайн",
"notfound.fb.note": "Цей мінт проіндексовано після останньої збірки сайту, тому тут показано зведення з актуального API. Повна сторінка з відгуками з’явиться після наступної збірки.", "notfound.fb.note": "Цей мінт проіндексовано після останньої збірки сайту, тож тут зведення з живого API, а нижче його відгуки. Повна сторінка з’явиться під час наступної збірки.",
"notfound.resolve.checking": "Перший візит: перевіряємо цей мінт.",
"notfound.resolve.failed": "Не вдалося додати цей мінт",
"notfound.resolve.failedFedimint": "Не вдалося додати цю федерацію",
"notfound.resolve.retry": "Спробувати іншу адресу",
"notfound.resolve.switch": "Відкрити як мінт {kind}",
"notfound.resolve.fedimint": "Адреса сторінки федерації не містить її коду запрошення, тож знайти її лише за посиланням не вийде. Встав код запрошення, і її буде додано.",
"notfound.resolve.ambiguous": "Цю адресу не відновити лише за посиланням. Встав повний URL мінта, і його буде додано.",
"wallets.title": "Гаманці Cashu · Cashumints.space", "wallets.title": "Гаманці Cashu · Cashumints.space",
"wallets.description": "Гаманці Cashu, що працюють із переліченими тут мінтами на мобільних пристроях, у вебі, браузері та командному рядку, із зазначенням переваг кожного.", "wallets.description": "Гаманці Cashu, що працюють із переліченими тут мінтами на мобільних пристроях, у вебі, браузері та командному рядку, із зазначенням переваг кожного.",
"wallets.heading": "Гаманці", "wallets.heading": "Гаманці",
@@ -696,5 +705,115 @@
"footer.allFedimints": "Усі федерації", "footer.allFedimints": "Усі федерації",
"reviews.dialog.bodyPlaceholder.cashu": "Як працювали внесення через mint, виведення через melt і підтримка? Пишіть конкретно.", "reviews.dialog.bodyPlaceholder.cashu": "Як працювали внесення через mint, виведення через melt і підтримка? Пишіть конкретно.",
"reviews.dialog.bodyPlaceholder.fedimint": "Як працювали приєднання, платежі Lightning і підтримка? Пишіть конкретно.", "reviews.dialog.bodyPlaceholder.fedimint": "Як працювали приєднання, платежі Lightning і підтримка? Пишіть конкретно.",
"fedimint.details.title": "Деталі федерації" "fedimint.details.title": "Деталі федерації",
"nav.lnurlMints": "LNURL-мінти",
"footer.allLnurlMints": "Усі LNURL-мінти",
"lnurlMints.title": "LNURL-мінти: мінти пред'явницьких нот lnurlcash з відгуками | Cashumints.space",
"lnurlMints.description": "Усі {total} LNURL-мінтів, проіндексованих із мережі Nostr: {online} онлайн, {offline} офлайн. Оцінки спільноти, ліміти виведення та можливості кожного.",
"lnurlMints.heading": "LNURL-мінти",
"lnurlMints.lede": "Кожен LNURL-мінт, про який знає цей сайт. LNURL-мінт випускає пред'явницькі ноти lnurlcash: ноти Lightning, які ви тримаєте як секрет і які можна прокрутити, розділити, об'єднати або розплавити назад у платіж Lightning. Кожен перевіряється по HTTPS кожні кілька хвилин.",
"lnurlMints.search.label": "Пошук LNURL-мінтів",
"lnurlMints.search.placeholder": "Пошук за назвою або адресою…",
"lnurlMints.sort.label": "Сортувати LNURL-мінти за",
"lnurlMints.hideOffline": "Сховати офлайн",
"lnurlMints.showing": "Показано {total} LNURL-мінтів ({online} онлайн, {offline} офлайн)",
"lnurlMints.showingFiltered": "Показано {shown} із {total} LNURL-мінтів ({online} онлайн, {offline} офлайн)",
"lnurlMints.noResults": "Жоден LNURL-мінт не збігається.",
"lnurlMints.emptyIndex": "LNURL-мінти ще не проіндексовані.",
"lnurl.crumb": "LNURL-мінти",
"lnurl.breadcrumb": "Хлібні крихти",
"lnurl.copyAddress": "Скопіювати адресу",
"lnurl.more.title": "Інші LNURL-мінти",
"lnurl.more.all": "Усі LNURL-мінти →",
"lnurl.disclaimer": "LNURL-мінт зберігає ваші сати, поки ви тримаєте його ноти, тож користуватися ним означає довіряти його оператору. Оцінки — це думки спільноти, а «онлайн» означає доступний, а не надійний.",
"lnurl.features.title": "Можливості",
"lnurl.features.none": "Цей мінт не оголошував, що він підтримує.",
"lnurl.features.note": "Можливості — це те, що оператор мінта оголосив у Nostr. Там, де сайт може перевірити їх по HTTPS, він це робить і повідомляє, якщо оголошене зараз недоступне.",
"lnurl.features.notListed": "не оголошено",
"lnurl.features.unavailable": "недоступно",
"lnurl.feature.mint": "Карбування через Lightning (LUD-06)",
"lnurl.feature.melt": "Плавлення в Lightning",
"lnurl.feature.notes": "Прокрутка / поділ / об'єднання нот",
"lnurl.feature.lud16": "Lightning-адреса",
"lnurl.feature.lud21": "Перевірка платежу (LUD-21)",
"lnurl.feature.signed-notes": "Підписані ноти (офлайн-перевірка)",
"lnurl.feature.onion": "Tor-адреса",
"lnurl.feature.rotate": "Прокрутити ноту",
"lnurl.feature.split": "Розділити ноту",
"lnurl.feature.merge": "Об'єднати ноти",
"lnurl.feature.lud06": "LUD-06 payRequest",
"lnurl.feature.lud03": "LUD-03 withdrawRequest",
"lnurl.limits.label": "Ліміти виведення",
"lnurl.limits.notPublished": "не опубліковано",
"lnurl.limits.withdrawRow": "Виведення",
"lnurl.limits.disabled": "вимкнено",
"lnurl.limits.sats.one": "{n} сат",
"lnurl.limits.sats.few": "{n} сати",
"lnurl.limits.sats.many": "{n} сатів",
"lnurl.limits.sats.other": "{n} сатів",
"lnurl.details.title": "Дані мінта",
"lnurl.details.address": "Lightning-адреса",
"lnurl.details.mintPubkey": "Публічний ключ мінта",
"lnurl.details.onion": "Tor-адреса",
"lnurl.details.fee": "Комісія за карбування",
"lnurl.details.node": "Вузол",
"lnurl.details.capacity": "Оголошена ємність",
"lnurl.kind.lightningAddress": "Lightning-адреса",
"lnurl.kind.mintPubkey": "публічний ключ мінта",
"lnurl.kind.onionAddress": "Tor-адреса",
"lnurl.confirmed.label": "Останнє підтвердження",
"lnurl.confirmed.when": "підтверджено {when}",
"lnurl.fee.base.one": "{n} сат",
"lnurl.fee.base.few": "{n} сати",
"lnurl.fee.base.many": "{n} сатів",
"lnurl.fee.base.other": "{n} сатів",
"lnurl.fee.percent": "{percent}%",
"home.topLnurl.title": "Найкращі LNURL-мінти",
"home.topLnurl.sub": "за відгуками та оцінкою",
"home.topLnurl.all": "Усі {n} LNURL-мінтів →",
"feed.ecosystem.lnurl": "LNURL",
"reviews.dialog.bodyPlaceholder.lnurl": "Як пройшли карбування, плавлення та прокрутка нот? Пишіть конкретно.",
"reviews.byUrl.open": "Написати відгук",
"reviews.byUrl.title": "Написати відгук",
"reviews.byUrl.lede": "Встав адресу. Якщо сайт її ще не знає, він перевірить її й додасть зараз.",
"reviews.byUrl.label.cashu": "URL мінта",
"reviews.byUrl.label.fedimint": "Код запрошення федерації",
"reviews.byUrl.label.lnurl": "URL LNURL-мінта",
"reviews.byUrl.placeholder.cashu": "mint.example.com",
"reviews.byUrl.placeholder.fedimint": "fed11…",
"reviews.byUrl.placeholder.lnurl": "mint.example.com",
"reviews.byUrl.check": "Перевірити цю адресу",
"reviews.byUrl.checking": "Перевіряємо…",
"reviews.byUrl.back": "Інша адреса",
"reviews.byUrl.existing": "Уже в списку",
"reviews.byUrl.indexed": "Щойно додано",
"reviews.byUrl.confirmClose": "Закрити? Введена адреса не збережеться.",
"reviews.byUrl.detected": "Ця адреса відповіла як мінт {kind}.",
"reviews.byUrl.switchTo": "Оцінити як мінт {kind}",
"reviews.byUrl.error.empty": "Спочатку введи адресу.",
"reviews.byUrl.error.badUrl": "Це не схоже на URL мінта.",
"reviews.byUrl.error.badInvite": "Це не код запрошення Fedimint. Коди починаються з fed11.",
"reviews.byUrl.error.badInput": "Сайт не може перевірити цю адресу.",
"reviews.byUrl.error.blocked": "Це не публічна адреса, тож перевірити її не можна.",
"reviews.byUrl.error.notAMint": "Адреса відповіла, але не як мінт цього типу.",
"reviews.byUrl.error.wrongType": "Адреса відповіла, але як мінт іншого типу.",
"reviews.byUrl.error.unverifiable": "За цією адресою ніщо не відповіло, і в Nostr про неї поки що немає й сліду.",
"reviews.byUrl.error.rateLimited.one": "Забагато перевірок з одного з’єднання. Спробуй ще раз приблизно через {n} хвилину.",
"reviews.byUrl.error.rateLimited.other": "Забагато перевірок з одного з’єднання. Спробуй ще раз приблизно через {n} хвилин.",
"reviews.byUrl.error.network": "Не вдалося зв’язатися з індексом. Перевір з’єднання і спробуй ще раз.",
"seo.lnurl.titleNoReviews": "{name}: інформація та відгуки про LNURL-мінт | Cashumints.space",
"seo.desc.statusWithdraw": "{status}, виведення {min}–{max} сат.",
"mint.chip.noFunding": "Без карбування / плавлення",
"mint.warnings.lnurlWithdrawalsDisabled.lead": "Виведення вимкнено.",
"mint.warnings.lnurlWithdrawalsDisabled.body.online": "Цей мінт оголошує максимальне виведення в нуль, тож жодну випущену ним ноту не можна ні на що обміняти. Не карбуйте тут, доки це не зміниться.",
"mint.warnings.lnurlWithdrawalsDisabled.body.offline": "Під час останнього контакту цей мінт оголошував максимальне виведення в нуль, тож жодну випущену ним ноту не можна було ні на що обміняти. Не карбуйте тут, доки це не зміниться.",
"mint.warnings.lnurlWithdrawalsDisabled.meta.online": "Виведення зараз вимкнено",
"mint.warnings.lnurlWithdrawalsDisabled.meta.offline": "Під час останнього контакту виведення було вимкнено",
"mint.warnings.lnurlInvalid.lead": "Ендпоінт відповідає, але некоректно.",
"mint.warnings.lnurlInvalid.body": "Хост відповідає, але не оголошенням мінта, яке сайт може прочитати, тож виведення може не працювати. Нижче — останній стан, який вдалося розібрати, а відгуки й далі працюють.",
"mint.warnings.lnurlInvalid.meta": "Відповідає, але не коректним оголошенням мінта",
"mint.warnings.lnurlNoFunding.lead": "Карбування та плавлення недоступні.",
"mint.warnings.lnurlNoFunding.body": "Наявні ноти й далі можна прокрутити, розділити або об'єднати, але зараз через Lightning нічого не входить і не виходить. Сам мінт працює й відповідає; вузол Lightning за ним для нього недоступний.",
"mint.warnings.lnurlNoFunding.meta": "Карбування та плавлення недоступні",
"mint.warnings.also.noFunding": "Його вузол Lightning теж був недоступний, тож нічого не можна було відкарбувати чи розплавити."
} }
+145 -30
View File
@@ -1,13 +1,13 @@
{ {
"site.name": "Cashumints. space", "site.name": "Cashumints.space",
"common.close": "بندکرو", "common.close": "بند کریں",
"common.cancel": "منسوخ", "common.cancel": "منسوخ کریں",
"common.done": "بند", "common.done": "ہو گیا",
"common.back": "ما وٴ س", "common.back": "واپس",
"common.retry": "دوبارہ کوشش", "common.retry": "دوبارہ کوشش",
"common.connect": "نظر انداز کریں", "common.connect": "منسلک کریں",
"common.copied": "کُتب‌خانے", "common.copied": "کاپی ہو گیا",
"common.copyFull": "مکمل کاپی کریں{kind}", "common.copyFull": "مکمل {kind} کاپی کریں",
"common.previousPage": "پہلے صفحہ", "common.previousPage": "پہلے صفحہ",
"common.nextPage": "اگلا صفحہ", "common.nextPage": "اگلا صفحہ",
"common.reviewPages": "کے صفحات دیکھیں", "common.reviewPages": "کے صفحات دیکھیں",
@@ -15,27 +15,27 @@
"time.justNow": "ابھی", "time.justNow": "ابھی",
"time.none": "-", "time.none": "-",
"time.unknownDate": "ایک نامعلوم تاریخ", "time.unknownDate": "ایک نامعلوم تاریخ",
"time.ago.minute": "{n}میٹر پہلے", "time.ago.minute": "{n} منٹ پہلے",
"time.ago.hour": "{n}ہی قبل", "time.ago.hour": "{n} گھنٹے پہلے",
"time.ago.day": "{n}ڈی۔", "time.ago.day": "{n} دن پہلے",
"time.ago.week": "{n}ویسے ہی", "time.ago.week": "{n} ہفتے پہلے",
"time.ago.month": "{n}م.", "time.ago.month": "{n} ماہ پہلے",
"time.ago.year": "{n}Y پہلے", "time.ago.year": "{n} سال پہلے",
"time.short.minute": "{n}م", "time.short.minute": "{n} منٹ",
"time.short.hour": "{n}ح", "time.short.hour": "{n} گھنٹے",
"time.short.day": "{n}ڈ", "time.short.day": "{n} دن",
"time.short.week": "{n}وے", "time.short.week": "{n} ہفتے",
"time.short.month": "{n}ما وٴ س", "time.short.month": "{n} ماہ",
"time.short.year": "{n}Y", "time.short.year": "{n} سال",
"status.online": "آن لائن", "status.online": "آن لائن",
"status.offline": "غیر متصل", "status.offline": "غیر متصل",
"status.degraded": "غیر متصل", "status.degraded": "غیر متصل",
"status.unknown": "ابھی تک چیک نہیں کیا گیا", "status.unknown": "ابھی تک چیک نہیں کیا گیا",
"nav.skip": "مواد کی تلاش", "nav.skip": "مواد پر جائیں",
"nav.main": "مین", "nav.main": "مین",
"nav.mints": "Cashu منتر", "nav.mints": "Cashu منتر",
"nav.reviews": "اعادہ", "nav.reviews": "جائزے",
"nav.wallets": "دیواری", "nav.wallets": "والیٹس",
"nav.about": "کے بارے میں", "nav.about": "کے بارے میں",
"nav.searchMints": "تلاش{n}منٹس", "nav.searchMints": "تلاش{n}منٹس",
"lang.label": "زبان", "lang.label": "زبان",
@@ -43,11 +43,11 @@
"footer.copyright": "نصب شدہ:{year}Cashumints. space", "footer.copyright": "نصب شدہ:{year}Cashumints. space",
"footer.allMints": "تمام کیوشو منتر", "footer.allMints": "تمام کیوشو منتر",
"footer.allReviews": "تمام جائزے", "footer.allReviews": "تمام جائزے",
"footer.wallets": "دیواری", "footer.wallets": "والیٹس",
"footer.about": "کے بارے میں", "footer.about": "کے بارے میں",
"footer.disclaimer": "غیر متصل", "footer.disclaimer": "دستبرداری",
"footer.terms": "مدت", "footer.terms": "شرائط",
"footer.privacy": "پُرتشدد", "footer.privacy": "رازداری",
"footer.madeBy": " ⁇ کے ساتھ بنایا گیا ہے۔{azzamo}· Cashu اور این آئی پی-87 کی طرف سے طاقت ور", "footer.madeBy": " ⁇ کے ساتھ بنایا گیا ہے۔{azzamo}· Cashu اور این آئی پی-87 کی طرف سے طاقت ور",
"footer.warning": "آپ کے پیسوں سے منٹ ختم ہو سکتے ہیں ۔", "footer.warning": "آپ کے پیسوں سے منٹ ختم ہو سکتے ہیں ۔",
"home.title": "Cashumints. space · Cashu منٹ پر آپ اعتماد کر سکتے ہیں۔", "home.title": "Cashumints. space · Cashu منٹ پر آپ اعتماد کر سکتے ہیں۔",
@@ -114,6 +114,8 @@
"card.reviewed": "طےشدہ{when}", "card.reviewed": "طےشدہ{when}",
"card.noReviews": "ابھی تک کوئی تجزیے نہیں", "card.noReviews": "ابھی تک کوئی تجزیے نہیں",
"mint.title": "{name}· Cashu منٹ کے جائزے", "mint.title": "{name}· Cashu منٹ کے جائزے",
"mint.titleFedimint": "{name} · Fedimint فیڈریشن کے ریویو",
"mint.titleLnurl": "{name} · LNURL mint کے ریویو",
"mint.breadcrumb": "بِل‌کومب", "mint.breadcrumb": "بِل‌کومب",
"mint.crumbMints": "Cashu منتر", "mint.crumbMints": "Cashu منتر",
"mint.copyUrl": "منٹ کاپی کریں", "mint.copyUrl": "منٹ کاپی کریں",
@@ -325,7 +327,7 @@
"reviews.validate.short": "کم سے کم لکھیں{n}اس طرح کے حروف مفید ہیں۔", "reviews.validate.short": "کم سے کم لکھیں{n}اس طرح کے حروف مفید ہیں۔",
"reviews.validate.long": "تجزیے محدود ہیں۔{n}حروف", "reviews.validate.long": "تجزیے محدود ہیں۔{n}حروف",
"feed.title": "Last Mint and Federation تجزیے · Cashumints. space -", "feed.title": "Last Mint and Federation تجزیے · Cashumints. space -",
"feed.description": "ہر منٹ اور فیڈریشن جائزہ جتنا شائع ہوتا ہے:{reviews}پورے جائزے{mints}Cashu منٹس اور{federations}نوسٹر پر دستخط کیے گئے، اور یہاں سب سے پہلے نئے سرے سے پڑھے۔", "feed.description": "ہر منٹ، فیڈریشن اور LNURL منٹ کا جائزہ شائع ہوتے ہی: {mints} Cashu منٹس، {federations} Fedimint فیڈریشنز اور {lnurl} LNURL منٹس پر {reviews} جائزے، Nostr پر دستخط شدہ اور یہاں نئے سے پرانے پڑھنے کے لیے۔ ماحولیاتی نظام، درجہ بندی یا نام سے چھانٹیں۔",
"feed.heading": "پچھلے جائزے", "feed.heading": "پچھلے جائزے",
"feed.lede": "ہر Cashu منٹ اور فیدمینٹ فیڈریشن کا ہر جائزہ، نیا سب سے پہلے، نوسٹر ریلویز سے پڑھے۔ ہر شخص اس کے مصنف کی طرف سے دستخط کیا جاتا ہے اور ہماری ڈیٹا بیس میں نہیں۔", "feed.lede": "ہر Cashu منٹ اور فیدمینٹ فیڈریشن کا ہر جائزہ، نیا سب سے پہلے، نوسٹر ریلویز سے پڑھے۔ ہر شخص اس کے مصنف کی طرف سے دستخط کیا جاتا ہے اور ہماری ڈیٹا بیس میں نہیں۔",
"feed.label": "اعادہ", "feed.label": "اعادہ",
@@ -400,7 +402,14 @@
"notfound.fb.reviews": "اعادہ", "notfound.fb.reviews": "اعادہ",
"notfound.fb.software": "سافٹ وئیر", "notfound.fb.software": "سافٹ وئیر",
"notfound.fb.lastOnline": "آخری آن لائن", "notfound.fb.lastOnline": "آخری آن لائن",
"notfound.fb.note": "اس منٹ کو سائٹ کی آخری تعمیر کے بعد انڈیکس کیا گیا تھا، چنانچہ یہ صفحہ زندہ اے پی آئی سے مکمل صفحہ ہے جس میں تجزیے اگلے ازسرنو ملتے ہیں۔", "notfound.fb.note": "یہ mint سائٹ کے آخری بلڈ کے بعد انڈیکس ہوا، اس لیے یہ لائیو API سے ایک خلاصہ ہے اور نیچے اس کے ریویو ہیں۔ مکمل صفحہ اگلے بلڈ پر آئے گا۔",
"notfound.resolve.checking": "پہلی بار: اس mint کو ابھی جانچا جا رہا ہے۔",
"notfound.resolve.failed": "یہ mint شامل نہیں کیا جا سکا",
"notfound.resolve.failedFedimint": "یہ فیڈریشن شامل نہیں کی جا سکی",
"notfound.resolve.retry": "کوئی اور پتہ آزمائیں",
"notfound.resolve.switch": "اسے {kind} mint کے طور پر کھولیں",
"notfound.resolve.fedimint": "فیڈریشن کے صفحے کے پتے میں اس کا invite code نہیں ہوتا، اس لیے صرف لنک سے اسے تلاش نہیں کیا جا سکتا۔ invite code چسپاں کریں، یہ شامل ہو جائے گی۔",
"notfound.resolve.ambiguous": "صرف لنک سے یہ پتہ اخذ نہیں کیا جا سکتا۔ mint کا مکمل URL چسپاں کریں، یہ شامل ہو جائے گا۔",
"wallets.title": "Cashu Citutes · Cashumints. space", "wallets.title": "Cashu Citutes · Cashumints. space",
"wallets.description": "کیش چندر گپت جو یہاں کے منٹوں کے ساتھ کام کرتے ہیں، موبائل، ویب، براؤزر اور کمانڈ لائن کے ساتھ،", "wallets.description": "کیش چندر گپت جو یہاں کے منٹوں کے ساتھ کام کرتے ہیں، موبائل، ویب، براؤزر اور کمانڈ لائن کے ساتھ،",
"wallets.heading": "دیواری", "wallets.heading": "دیواری",
@@ -654,5 +663,111 @@
"footer.allFedimints": "تمام رعایتیں", "footer.allFedimints": "تمام رعایتیں",
"reviews.dialog.bodyPlaceholder.cashu": "اِس سلسلے میں ایک مثال پر غور کریں ۔", "reviews.dialog.bodyPlaceholder.cashu": "اِس سلسلے میں ایک مثال پر غور کریں ۔",
"reviews.dialog.bodyPlaceholder.fedimint": "اِس سلسلے میں ایک مثال پر غور کریں ۔", "reviews.dialog.bodyPlaceholder.fedimint": "اِس سلسلے میں ایک مثال پر غور کریں ۔",
"fedimint.details.title": "فیڈریشن تفصیلات" "fedimint.details.title": "فیڈریشن تفصیلات",
"nav.lnurlMints": "LNURL منٹس",
"footer.allLnurlMints": "تمام LNURL منٹس",
"lnurlMints.title": "LNURL منٹس: lnurlcash حاملِ نوٹ منٹس، جائزوں کے ساتھ | Cashumints.space",
"lnurlMints.description": "Nostr نیٹ ورک سے فہرست بند کیے گئے تمام {total} LNURL منٹس: {online} آن لائن، {offline} آف لائن۔ ہر ایک کے لیے کمیونٹی کی درجہ بندی، رقم نکالنے کی حدود اور خصوصیات۔",
"lnurlMints.heading": "LNURL منٹس",
"lnurlMints.lede": "ہر وہ LNURL منٹ جو یہ سائٹ جانتی ہے۔ ایک LNURL منٹ lnurlcash حاملِ نوٹ جاری کرتا ہے: لائٹننگ نوٹ جنہیں آپ ایک راز کی طرح رکھتے ہیں اور جنہیں گھمایا، تقسیم کیا، ملایا یا واپس لائٹننگ ادائیگی میں پگھلایا جا سکتا ہے۔ ہر ایک کی HTTPS پر چند منٹ بعد جانچ ہوتی ہے۔",
"lnurlMints.search.label": "LNURL منٹس تلاش کریں",
"lnurlMints.search.placeholder": "نام یا پتے سے تلاش کریں…",
"lnurlMints.sort.label": "LNURL منٹس ترتیب دیں بلحاظ",
"lnurlMints.hideOffline": "آف لائن چھپائیں",
"lnurlMints.showing": "{total} LNURL منٹس دکھائے جا رہے ہیں ({online} آن لائن، {offline} آف لائن)",
"lnurlMints.showingFiltered": "{total} میں سے {shown} LNURL منٹس دکھائے جا رہے ہیں ({online} آن لائن، {offline} آف لائن)",
"lnurlMints.noResults": "کوئی LNURL منٹ مماثل نہیں۔",
"lnurlMints.emptyIndex": "ابھی تک کوئی LNURL منٹ فہرست بند نہیں ہوا۔",
"lnurl.crumb": "LNURL منٹس",
"lnurl.breadcrumb": "بریڈ کرمب",
"lnurl.copyAddress": "پتہ کاپی کریں",
"lnurl.more.title": "مزید LNURL منٹس",
"lnurl.more.all": "تمام LNURL منٹس ←",
"lnurl.disclaimer": "ایک LNURL منٹ آپ کے sats رکھتا ہے جبکہ آپ اس کے نوٹ رکھتے ہیں، تو اسے استعمال کرنے کا مطلب ہے اس کے چلانے والے پر بھروسہ کرنا۔ درجہ بندیاں کمیونٹی کی رائے ہیں، اور آن لائن کا مطلب قابلِ رسائی ہے، قابلِ بھروسہ نہیں۔",
"lnurl.features.title": "خصوصیات",
"lnurl.features.none": "اس منٹ نے اعلان نہیں کیا کہ وہ کیا سہولت دیتا ہے۔",
"lnurl.features.note": "خصوصیات وہ ہیں جو منٹ کے چلانے والے نے Nostr پر اعلان کیں۔ جہاں یہ سائٹ HTTPS پر کسی کی تصدیق کر سکتی ہے وہاں کرتی ہے، اور بتاتی ہے جب کوئی اعلان کردہ چیز ابھی دستیاب نہ ہو۔",
"lnurl.features.notListed": "اعلان نہیں کیا گیا",
"lnurl.features.unavailable": "دستیاب نہیں",
"lnurl.feature.mint": "لائٹننگ کے ذریعے منٹ (LUD-06)",
"lnurl.feature.melt": "لائٹننگ میں پگھلانا",
"lnurl.feature.notes": "نوٹ گھمانا / تقسیم / ملانا",
"lnurl.feature.lud16": "لائٹننگ پتہ",
"lnurl.feature.lud21": "ادائیگی کی تصدیق (LUD-21)",
"lnurl.feature.signed-notes": "دستخط شدہ نوٹ (آف لائن تصدیق)",
"lnurl.feature.onion": "Tor پتہ",
"lnurl.feature.rotate": "ایک نوٹ گھمائیں",
"lnurl.feature.split": "ایک نوٹ تقسیم کریں",
"lnurl.feature.merge": "نوٹ ملائیں",
"lnurl.feature.lud06": "LUD-06 payRequest",
"lnurl.feature.lud03": "LUD-03 withdrawRequest",
"lnurl.limits.label": "نکالنے کی حدود",
"lnurl.limits.notPublished": "شائع نہیں",
"lnurl.limits.withdrawRow": "نکالنا",
"lnurl.limits.disabled": "بند",
"lnurl.limits.sats.one": "{n} sat",
"lnurl.limits.sats.other": "{n} sats",
"lnurl.details.title": "منٹ کی تفصیلات",
"lnurl.details.address": "لائٹننگ پتہ",
"lnurl.details.mintPubkey": "منٹ کی عوامی کلید",
"lnurl.details.onion": "Tor پتہ",
"lnurl.details.fee": "منٹ فیس",
"lnurl.details.node": "نوڈ",
"lnurl.details.capacity": "اعلان کردہ گنجائش",
"lnurl.kind.lightningAddress": "لائٹننگ پتہ",
"lnurl.kind.mintPubkey": "منٹ کی عوامی کلید",
"lnurl.kind.onionAddress": "Tor پتہ",
"lnurl.confirmed.label": "آخری تصدیق",
"lnurl.confirmed.when": "{when} تصدیق ہوئی",
"lnurl.fee.base.one": "{n} sat",
"lnurl.fee.base.other": "{n} sats",
"lnurl.fee.percent": "{percent}%",
"home.topLnurl.title": "بہترین LNURL منٹس",
"home.topLnurl.sub": "جائزوں اور درجہ بندی کے مطابق",
"home.topLnurl.all": "تمام {n} LNURL منٹس ←",
"feed.ecosystem.lnurl": "LNURL",
"reviews.dialog.bodyPlaceholder.lnurl": "منٹ کرنا، پگھلانا اور نوٹ گھمانا کیسا رہا؟ واضح لکھیں۔",
"reviews.byUrl.open": "ریویو لکھیں",
"reviews.byUrl.title": "ریویو لکھیں",
"reviews.byUrl.lede": "پتہ چسپاں کریں۔ اگر یہ سائٹ اسے ابھی نہیں جانتی، تو اسے ابھی جانچ کر شامل کر لیا جائے گا۔",
"reviews.byUrl.label.cashu": "mint کا URL",
"reviews.byUrl.label.fedimint": "فیڈریشن کا invite code",
"reviews.byUrl.label.lnurl": "LNURL mint کا URL",
"reviews.byUrl.placeholder.cashu": "mint.example.com",
"reviews.byUrl.placeholder.fedimint": "fed11…",
"reviews.byUrl.placeholder.lnurl": "mint.example.com",
"reviews.byUrl.check": "یہ پتہ جانچیں",
"reviews.byUrl.checking": "جانچ جاری ہے…",
"reviews.byUrl.back": "کوئی اور پتہ استعمال کریں",
"reviews.byUrl.existing": "پہلے سے فہرست میں",
"reviews.byUrl.indexed": "ابھی شامل ہوا",
"reviews.byUrl.confirmClose": "بند کریں؟ آپ کا لکھا ہوا پتہ محفوظ نہیں رہے گا۔",
"reviews.byUrl.detected": "اس پتے نے {kind} mint کے طور پر جواب دیا۔",
"reviews.byUrl.switchTo": "اسے {kind} mint کے طور پر ریویو کریں",
"reviews.byUrl.error.empty": "پہلے کوئی پتہ لکھیں۔",
"reviews.byUrl.error.badUrl": "یہ mint کے URL جیسا نہیں لگتا۔",
"reviews.byUrl.error.badInvite": "یہ Fedimint کا invite code نہیں ہے۔ کوڈ fed11 سے شروع ہوتے ہیں۔",
"reviews.byUrl.error.badInput": "یہ سائٹ اس پتے کو نہیں جانچ سکتی۔",
"reviews.byUrl.error.blocked": "یہ عوامی پتہ نہیں ہے، اس لیے اسے جانچا نہیں جا سکتا۔",
"reviews.byUrl.error.notAMint": "اس پتے نے جواب تو دیا، مگر اس قسم کے mint کے طور پر نہیں۔",
"reviews.byUrl.error.wrongType": "اس پتے نے جواب دیا، مگر کسی اور قسم کے mint کے طور پر۔",
"reviews.byUrl.error.unverifiable": "اس پتے پر کچھ بھی جواب نہیں آیا، اور Nostr پر بھی ابھی اس کا کوئی نشان نہیں۔",
"reviews.byUrl.error.rateLimited.one": "ایک ہی کنکشن سے بہت زیادہ جانچیں ہو چکی ہیں۔ تقریباً {n} منٹ بعد دوبارہ کوشش کریں۔",
"reviews.byUrl.error.rateLimited.other": "ایک ہی کنکشن سے بہت زیادہ جانچیں ہو چکی ہیں۔ تقریباً {n} منٹ بعد دوبارہ کوشش کریں۔",
"reviews.byUrl.error.network": "انڈیکس تک رسائی نہیں ہو سکی۔ اپنا کنکشن دیکھ کر دوبارہ کوشش کریں۔",
"seo.lnurl.titleNoReviews": "{name}: LNURL منٹ کی معلومات اور جائزے | Cashumints.space",
"seo.desc.statusWithdraw": "{status}، نکالنا {min}–{max} sat۔",
"mint.chip.noFunding": "منٹ / میلٹ بند",
"mint.warnings.lnurlWithdrawalsDisabled.lead": "نکالنا بند ہے۔",
"mint.warnings.lnurlWithdrawalsDisabled.body.online": "یہ منٹ زیادہ سے زیادہ نکالنے کی حد صفر بتاتا ہے، اس لیے اس کا جاری کردہ کوئی نوٹ کسی چیز کے بدلے نہیں بھنایا جا سکتا۔ جب تک یہ نہ بدلے، یہاں منٹ نہ کریں۔",
"mint.warnings.lnurlWithdrawalsDisabled.body.offline": "آخری رابطے کے وقت یہ منٹ زیادہ سے زیادہ نکالنے کی حد صفر بتا رہا تھا، اس لیے اس کا جاری کردہ کوئی نوٹ کسی چیز کے بدلے نہیں بھنایا جا سکتا تھا۔ جب تک یہ نہ بدلے، یہاں منٹ نہ کریں۔",
"mint.warnings.lnurlWithdrawalsDisabled.meta.online": "نکالنا اس وقت بند ہے",
"mint.warnings.lnurlWithdrawalsDisabled.meta.offline": "آخری بار دیکھے جانے پر نکالنا بند تھا",
"mint.warnings.lnurlInvalid.lead": "اینڈ پوائنٹ جواب دے رہا ہے مگر غلط ہے۔",
"mint.warnings.lnurlInvalid.body": "میزبان جواب دیتا ہے، مگر ایسے منٹ اعلان کے ساتھ نہیں جسے یہ سائٹ پڑھ سکے، اس لیے نکالنا شاید کام نہ کرے۔ نیچے جو ہے وہ آخری قابلِ مطالعہ حالت ہے، اور جائزے اب بھی کام کرتے ہیں۔",
"mint.warnings.lnurlInvalid.meta": "جواب دیتا ہے، مگر درست منٹ اعلان کے ساتھ نہیں",
"mint.warnings.lnurlNoFunding.lead": "منٹ اور میلٹ دستیاب نہیں۔",
"mint.warnings.lnurlNoFunding.body": "موجودہ نوٹ اب بھی گھمائے، تقسیم یا ملائے جا سکتے ہیں، مگر ابھی لائٹننگ کے ذریعے کچھ اندر یا باہر نہیں جا رہا۔ منٹ خود چل رہا ہے اور جواب دے رہا ہے؛ اس کے پیچھے کا لائٹننگ نوڈ اس سے قابلِ رسائی نہیں۔",
"mint.warnings.lnurlNoFunding.meta": "منٹ اور میلٹ دستیاب نہیں",
"mint.warnings.also.noFunding": "اس کا لائٹننگ نوڈ بھی ناقابلِ رسائی تھا، اس لیے کچھ بھی منٹ یا میلٹ نہیں کیا جا سکتا تھا۔"
} }
+118 -3
View File
@@ -114,6 +114,8 @@
"card.reviewed": "Xem lại {when}", "card.reviewed": "Xem lại {when}",
"card.noReviews": "chưa xem lại", "card.noReviews": "chưa xem lại",
"mint.title": "{name} · Cashu bình luận mint", "mint.title": "{name} · Cashu bình luận mint",
"mint.titleFedimint": "{name} · Đánh giá liên minh Fedimint",
"mint.titleLnurl": "{name} · Đánh giá mint LNURL",
"mint.breadcrumb": "Mảnh bánh mì", "mint.breadcrumb": "Mảnh bánh mì",
"mint.crumbMints": "mint", "mint.crumbMints": "mint",
"mint.copyUrl": "Chép URL mint", "mint.copyUrl": "Chép URL mint",
@@ -325,7 +327,7 @@
"reviews.validate.short": "Viết ít nhất {n} chữ cái nên bài phê bình rất hữu ích.", "reviews.validate.short": "Viết ít nhất {n} chữ cái nên bài phê bình rất hữu ích.",
"reviews.validate.long": "Các bài ôn lại chỉ có {n} chữ cái.", "reviews.validate.long": "Các bài ôn lại chỉ có {n} chữ cái.",
"feed.title": "Canh mint và thức ăn nội thất mới nhất. không gian", "feed.title": "Canh mint và thức ăn nội thất mới nhất. không gian",
"feed.description": "Mỗi bản đánh giá về mint và dinh dưỡng như được xuất bản: {reviews} bài phê bình trên {mints} Kẹo mint và {federations} Fediminations, ký tên trên Nostr và có thể đọc ở đây trước. Lọc theo hệ sinh thái, đánh giá, hay tên.", "feed.description": "Mọi đánh giá về mint, liên minh và mint LNURL ngay khi được đăng: {reviews} đánh giá trên {mints} mint Cashu, {federations} liên minh Fedimint và {lnurl} mint LNURL, được ký trên Nostr và đọc ở đây theo thứ tự mới nhất trước. Lọc theo hệ sinh thái, điểm số hoặc tên.",
"feed.heading": "phê bình mới nhất", "feed.heading": "phê bình mới nhất",
"feed.lede": "Tất cả các bản đánh giá của tất cả các công ty truyền thông Cashu và Fedimint, đều mới nhất, đọc trực tiếp từ các thông tin liên lạc của Nostr. Mỗi cái được ký bởi tác giả của nó và sống trên các rơ-le, không phải trong cơ sở dữ liệu.", "feed.lede": "Tất cả các bản đánh giá của tất cả các công ty truyền thông Cashu và Fedimint, đều mới nhất, đọc trực tiếp từ các thông tin liên lạc của Nostr. Mỗi cái được ký bởi tác giả của nó và sống trên các rơ-le, không phải trong cơ sở dữ liệu.",
"feed.label": "Xem lại", "feed.label": "Xem lại",
@@ -400,7 +402,14 @@
"notfound.fb.reviews": "Xem lại", "notfound.fb.reviews": "Xem lại",
"notfound.fb.software": "Phần mềm", "notfound.fb.software": "Phần mềm",
"notfound.fb.lastOnline": "trực tuyến cuối cùng", "notfound.fb.lastOnline": "trực tuyến cuối cùng",
"notfound.fb.note": "Thanh mint này được lập danh mục sau khi trang web được xây dựng lần cuối, nên đây là bản tóm tắt trực tiếp từ ADI. Trang đầy đủ có đánh giá xuất hiện ở lần xây dựng tiếp theo.", "notfound.fb.note": "Mint này được lập chỉ mục sau lần dựng trang gần nhất, nên đây là bản tóm tắt từ API trực tiếp, kèm các đánh giá bên dưới. Trang đầy đủ sẽ xuất hiện ở lần dựng tiếp theo.",
"notfound.resolve.checking": "Lần đầu ghé thăm: đang kiểm tra mint này.",
"notfound.resolve.failed": "Không thể thêm mint này",
"notfound.resolve.failedFedimint": "Không thể thêm liên minh này",
"notfound.resolve.retry": "Thử địa chỉ khác",
"notfound.resolve.switch": "Mở nó như mint {kind}",
"notfound.resolve.fedimint": "Địa chỉ trang của một liên minh không chứa mã mời, nên không thể tra cứu liên minh này chỉ từ đường dẫn. Hãy dán mã mời vào và nó sẽ được thêm.",
"notfound.resolve.ambiguous": "Không thể suy ra địa chỉ này chỉ từ đường dẫn. Hãy dán URL đầy đủ của mint và nó sẽ được thêm.",
"wallets.title": "Tiền trong ví. không gian", "wallets.title": "Tiền trong ví. không gian",
"wallets.description": "Ví tiền làm việc với mint được liệt kê ở đây, qua điện thoại di động, web, trình duyệt và dòng lệnh, với những gì mỗi cái đều tốt.", "wallets.description": "Ví tiền làm việc với mint được liệt kê ở đây, qua điện thoại di động, web, trình duyệt và dòng lệnh, với những gì mỗi cái đều tốt.",
"wallets.heading": "Comment", "wallets.heading": "Comment",
@@ -654,5 +663,111 @@
"footer.allFedimints": "Mọi liên kết", "footer.allFedimints": "Mọi liên kết",
"reviews.dialog.bodyPlaceholder.cashu": "Việc đúc, nấu chảy và hỗ trợ diễn ra thế nào? Cụ thể hơn.", "reviews.dialog.bodyPlaceholder.cashu": "Việc đúc, nấu chảy và hỗ trợ diễn ra thế nào? Cụ thể hơn.",
"reviews.dialog.bodyPlaceholder.fedimint": "Làm thế nào để tham gia, thanh toán Lightning nhoáng và hỗ trợ? Cụ thể hơn.", "reviews.dialog.bodyPlaceholder.fedimint": "Làm thế nào để tham gia, thanh toán Lightning nhoáng và hỗ trợ? Cụ thể hơn.",
"fedimint.details.title": "Chi tiết Liên bang" "fedimint.details.title": "Chi tiết Liên bang",
"nav.lnurlMints": "Mint LNURL",
"footer.allLnurlMints": "Tất cả mint LNURL",
"lnurlMints.title": "Mint LNURL: mint phát hành giấy nhận nợ vô danh lnurlcash, có đánh giá | Cashumints.space",
"lnurlMints.description": "Toàn bộ {total} mint LNURL được lập chỉ mục từ mạng Nostr: {online} trực tuyến, {offline} ngoại tuyến. Điểm đánh giá của cộng đồng, hạn mức rút và tính năng của từng mint.",
"lnurlMints.heading": "Mint LNURL",
"lnurlMints.lede": "Mọi mint LNURL mà trang này biết. Một mint LNURL phát hành giấy nhận nợ vô danh lnurlcash: những tờ Lightning bạn giữ như một bí mật, có thể xoay vòng, tách, gộp hoặc nung chảy trở lại thành một khoản thanh toán Lightning. Mỗi mint được kiểm tra qua HTTPS vài phút một lần.",
"lnurlMints.search.label": "Tìm mint LNURL",
"lnurlMints.search.placeholder": "Tìm theo tên hoặc địa chỉ…",
"lnurlMints.sort.label": "Sắp xếp mint LNURL theo",
"lnurlMints.hideOffline": "Ẩn mint ngoại tuyến",
"lnurlMints.showing": "Đang hiển thị {total} mint LNURL ({online} trực tuyến, {offline} ngoại tuyến)",
"lnurlMints.showingFiltered": "Đang hiển thị {shown} trong {total} mint LNURL ({online} trực tuyến, {offline} ngoại tuyến)",
"lnurlMints.noResults": "Không có mint LNURL nào khớp.",
"lnurlMints.emptyIndex": "Chưa có mint LNURL nào được lập chỉ mục.",
"lnurl.crumb": "Mint LNURL",
"lnurl.breadcrumb": "Đường dẫn",
"lnurl.copyAddress": "Sao chép địa chỉ",
"lnurl.more.title": "Thêm mint LNURL",
"lnurl.more.all": "Tất cả mint LNURL →",
"lnurl.disclaimer": "Mint LNURL giữ sats của bạn trong khi bạn giữ giấy nhận nợ của nó, nên dùng một mint nghĩa là tin tưởng người vận hành. Điểm đánh giá là ý kiến của cộng đồng, và trực tuyến nghĩa là truy cập được, không phải đáng tin cậy.",
"lnurl.features.title": "Tính năng",
"lnurl.features.none": "Mint này chưa công bố nó hỗ trợ những gì.",
"lnurl.features.note": "Tính năng là những gì người vận hành mint đã công bố trên Nostr. Chỗ nào trang này kiểm tra được qua HTTPS thì sẽ kiểm tra, và sẽ nói rõ khi một tính năng đã công bố hiện không dùng được.",
"lnurl.features.notListed": "chưa công bố",
"lnurl.features.unavailable": "không dùng được",
"lnurl.feature.mint": "Mint qua Lightning (LUD-06)",
"lnurl.feature.melt": "Nung chảy sang Lightning",
"lnurl.feature.notes": "Xoay vòng / tách / gộp giấy nhận nợ",
"lnurl.feature.lud16": "Địa chỉ Lightning",
"lnurl.feature.lud21": "Xác minh thanh toán (LUD-21)",
"lnurl.feature.signed-notes": "Giấy nhận nợ có chữ ký (xác minh ngoại tuyến)",
"lnurl.feature.onion": "Địa chỉ Tor",
"lnurl.feature.rotate": "Xoay vòng một tờ",
"lnurl.feature.split": "Tách một tờ",
"lnurl.feature.merge": "Gộp các tờ",
"lnurl.feature.lud06": "LUD-06 payRequest",
"lnurl.feature.lud03": "LUD-03 withdrawRequest",
"lnurl.limits.label": "Hạn mức rút",
"lnurl.limits.notPublished": "chưa công bố",
"lnurl.limits.withdrawRow": "Rút",
"lnurl.limits.disabled": "đã tắt",
"lnurl.limits.sats.one": "{n} sat",
"lnurl.limits.sats.other": "{n} sat",
"lnurl.details.title": "Chi tiết mint",
"lnurl.details.address": "Địa chỉ Lightning",
"lnurl.details.mintPubkey": "Khóa công khai của mint",
"lnurl.details.onion": "Địa chỉ Tor",
"lnurl.details.fee": "Phí mint",
"lnurl.details.node": "Nút",
"lnurl.details.capacity": "Dung lượng đã công bố",
"lnurl.kind.lightningAddress": "địa chỉ Lightning",
"lnurl.kind.mintPubkey": "khóa công khai của mint",
"lnurl.kind.onionAddress": "địa chỉ Tor",
"lnurl.confirmed.label": "Xác nhận lần cuối",
"lnurl.confirmed.when": "xác nhận {when}",
"lnurl.fee.base.one": "{n} sat",
"lnurl.fee.base.other": "{n} sat",
"lnurl.fee.percent": "{percent}%",
"home.topLnurl.title": "Mint LNURL hàng đầu",
"home.topLnurl.sub": "xếp hạng theo đánh giá và điểm số",
"home.topLnurl.all": "Tất cả {n} mint LNURL →",
"feed.ecosystem.lnurl": "LNURL",
"reviews.dialog.bodyPlaceholder.lnurl": "Việc mint, nung chảy và xoay vòng giấy nhận nợ diễn ra thế nào? Hãy nói cụ thể.",
"reviews.byUrl.open": "Viết đánh giá",
"reviews.byUrl.title": "Viết đánh giá",
"reviews.byUrl.lede": "Dán địa chỉ vào đây. Nếu trang này chưa biết địa chỉ đó, nó sẽ được kiểm tra và thêm vào ngay.",
"reviews.byUrl.label.cashu": "URL của mint",
"reviews.byUrl.label.fedimint": "Mã mời của liên minh",
"reviews.byUrl.label.lnurl": "URL của mint LNURL",
"reviews.byUrl.placeholder.cashu": "mint.example.com",
"reviews.byUrl.placeholder.fedimint": "fed11…",
"reviews.byUrl.placeholder.lnurl": "mint.example.com",
"reviews.byUrl.check": "Kiểm tra địa chỉ này",
"reviews.byUrl.checking": "Đang kiểm tra…",
"reviews.byUrl.back": "Dùng địa chỉ khác",
"reviews.byUrl.existing": "Đã có trong danh sách",
"reviews.byUrl.indexed": "Vừa được thêm",
"reviews.byUrl.confirmClose": "Đóng lại? Địa chỉ bạn vừa gõ sẽ không được giữ.",
"reviews.byUrl.detected": "Địa chỉ đó trả lời như một mint {kind}.",
"reviews.byUrl.switchTo": "Đánh giá nó như mint {kind}",
"reviews.byUrl.error.empty": "Hãy nhập địa chỉ trước.",
"reviews.byUrl.error.badUrl": "Cái đó trông không giống URL của một mint.",
"reviews.byUrl.error.badInvite": "Đó không phải mã mời Fedimint. Mã bắt đầu bằng fed11.",
"reviews.byUrl.error.badInput": "Trang này không thể kiểm tra địa chỉ đó.",
"reviews.byUrl.error.blocked": "Đó không phải địa chỉ công khai, nên không thể kiểm tra.",
"reviews.byUrl.error.notAMint": "Địa chỉ đó có trả lời, nhưng không phải với tư cách một mint loại này.",
"reviews.byUrl.error.wrongType": "Địa chỉ đó có trả lời, nhưng là một loại mint khác.",
"reviews.byUrl.error.unverifiable": "Không có gì trả lời ở địa chỉ đó, và trên Nostr cũng chưa có dấu vết nào.",
"reviews.byUrl.error.rateLimited.one": "Có quá nhiều lượt kiểm tra từ một kết nối. Thử lại sau khoảng {n} phút.",
"reviews.byUrl.error.rateLimited.other": "Có quá nhiều lượt kiểm tra từ một kết nối. Thử lại sau khoảng {n} phút.",
"reviews.byUrl.error.network": "Không liên hệ được với chỉ mục. Kiểm tra kết nối rồi thử lại.",
"seo.lnurl.titleNoReviews": "{name}: thông tin và đánh giá mint LNURL | Cashumints.space",
"seo.desc.statusWithdraw": "{status}, rút {min}–{max} sat.",
"mint.chip.noFunding": "Không mint / nung chảy",
"mint.warnings.lnurlWithdrawalsDisabled.lead": "Đã tắt rút tiền.",
"mint.warnings.lnurlWithdrawalsDisabled.body.online": "Mint này công bố mức rút tối đa bằng không, nên không tờ nào nó phát hành có thể đổi lấy bất cứ thứ gì. Đừng mint ở đây cho tới khi điều đó thay đổi.",
"mint.warnings.lnurlWithdrawalsDisabled.body.offline": "Lần cuối liên hệ được, mint này công bố mức rút tối đa bằng không, nên không tờ nào nó phát hành có thể đổi lấy bất cứ thứ gì. Đừng mint ở đây cho tới khi điều đó thay đổi.",
"mint.warnings.lnurlWithdrawalsDisabled.meta.online": "Hiện đã tắt rút tiền",
"mint.warnings.lnurlWithdrawalsDisabled.meta.offline": "Rút tiền đã tắt ở lần nhìn thấy cuối cùng",
"mint.warnings.lnurlInvalid.lead": "Endpoint có phản hồi nhưng không hợp lệ.",
"mint.warnings.lnurlInvalid.body": "Máy chủ có phản hồi, nhưng không phải bằng một công bố mint mà trang này đọc được, nên việc rút có thể không hoạt động. Mọi thứ bên dưới là trạng thái cuối cùng đọc được, và phần đánh giá vẫn hoạt động.",
"mint.warnings.lnurlInvalid.meta": "Có phản hồi, nhưng không phải công bố mint hợp lệ",
"mint.warnings.lnurlNoFunding.lead": "Không thể mint và nung chảy.",
"mint.warnings.lnurlNoFunding.body": "Các tờ hiện có vẫn xoay vòng, tách hoặc gộp được, nhưng hiện tại không có gì ra vào qua Lightning. Bản thân mint vẫn chạy và phản hồi; nút Lightning phía sau nó thì không truy cập được từ đó.",
"mint.warnings.lnurlNoFunding.meta": "Không thể mint và nung chảy",
"mint.warnings.also.noFunding": "Nút Lightning của nó cũng không truy cập được, nên không thể mint hay nung chảy gì."
} }
+118 -3
View File
@@ -114,6 +114,8 @@
"card.reviewed": "已审核 {when}", "card.reviewed": "已审核 {when}",
"card.noReviews": "尚无评论", "card.noReviews": "尚无评论",
"mint.title": "{name} · Cashu 铸币服务 评测", "mint.title": "{name} · Cashu 铸币服务 评测",
"mint.titleFedimint": "{name} · Fedimint 联邦评价",
"mint.titleLnurl": "{name} · LNURL mint 评价",
"mint.breadcrumb": "面包屑导航", "mint.breadcrumb": "面包屑导航",
"mint.crumbMints": "Cashu 铸币服务", "mint.crumbMints": "Cashu 铸币服务",
"mint.copyUrl": "复制 铸币服务 的网址", "mint.copyUrl": "复制 铸币服务 的网址",
@@ -325,7 +327,7 @@
"reviews.validate.short": "请至少输入 {n} 个字符,以便该评论对他人有所帮助。", "reviews.validate.short": "请至少输入 {n} 个字符,以便该评论对他人有所帮助。",
"reviews.validate.long": "评论长度限制为 {n} 个字符。", "reviews.validate.long": "评论长度限制为 {n} 个字符。",
"feed.title": "最新的 铸币服务 和联盟评测 · Cashumints.space", "feed.title": "最新的 铸币服务 和联盟评测 · Cashumints.space",
"feed.description": "每篇关于 铸币服务 及联盟的评测一经发布即收录: 涵盖 {mints}、Cashu、铸币服务、{federations} 及 Fedimint 联盟的 {reviews} 评测,已在 Nostr 上签署,可在此按最新顺序阅读。可按生态系统、评分或名称进行筛选。", "feed.description": "每一条铸造厂、联邦和 LNURL 铸造厂的评价一经发布即在此呈现:涵盖 {mints} 个 Cashu 铸造厂、{federations} 个 Fedimint 联邦和 {lnurl} 个 LNURL 铸造厂的 {reviews} 条评价,在 Nostr 上签名,按最新在前排列。可按生态、评分或名称筛选。",
"feed.heading": "最新评论", "feed.heading": "最新评论",
"feed.lede": "所有关于 Cashu、铸币服务 和 Fedimint 联盟的评论(按最新顺序排列),均实时读取自 Nostr 中继服务器。每条评论均由其作者签名,并存储在中继服务器上,而非我们的数据库中。", "feed.lede": "所有关于 Cashu、铸币服务 和 Fedimint 联盟的评论(按最新顺序排列),均实时读取自 Nostr 中继服务器。每条评论均由其作者签名,并存储在中继服务器上,而非我们的数据库中。",
"feed.label": "评论", "feed.label": "评论",
@@ -400,7 +402,14 @@
"notfound.fb.reviews": "评论", "notfound.fb.reviews": "评论",
"notfound.fb.software": "软件", "notfound.fb.software": "软件",
"notfound.fb.lastOnline": "最后一次上线", "notfound.fb.lastOnline": "最后一次上线",
"notfound.fb.note": "该 铸币服务 是在网站上次重建后被收录的,因此这是来自实时 API 的摘要。包含评论的完整页面将在下次重建时显示。", "notfound.fb.note": "这个 mint 是在本站最近一次构建之后收录的,所以这里是来自实时 API 的摘要,下面是它的评价。完整页面会在下次构建时出现。",
"notfound.resolve.checking": "第一次访问:正在检查这个 mint。",
"notfound.resolve.failed": "无法收录这个 mint",
"notfound.resolve.failedFedimint": "无法收录这个联邦",
"notfound.resolve.retry": "试另一个地址",
"notfound.resolve.switch": "作为 {kind} mint 打开",
"notfound.resolve.fedimint": "联邦的页面地址不包含 invite code,所以仅凭链接找不到它。粘贴 invite code,它就会被收录。",
"notfound.resolve.ambiguous": "仅凭链接推不出这个地址。粘贴 mint 的完整 URL,它就会被收录。",
"wallets.title": "Cashu 钱包 · Cashumints.space", "wallets.title": "Cashu 钱包 · Cashumints.space",
"wallets.description": "Cashu 钱包可与此处列出的 铸币服务 配合使用,支持移动端、网页端、浏览器和命令行等多种平台,各平台均发挥其独特优势。", "wallets.description": "Cashu 钱包可与此处列出的 铸币服务 配合使用,支持移动端、网页端、浏览器和命令行等多种平台,各平台均发挥其独特优势。",
"wallets.heading": "钱包", "wallets.heading": "钱包",
@@ -654,5 +663,111 @@
"footer.allFedimints": "所有联合会", "footer.allFedimints": "所有联合会",
"reviews.dialog.bodyPlaceholder.cashu": "铸造电子现金、赎回电子现金 以及相关支持服务的使用情况如何?请具体说明。", "reviews.dialog.bodyPlaceholder.cashu": "铸造电子现金、赎回电子现金 以及相关支持服务的使用情况如何?请具体说明。",
"reviews.dialog.bodyPlaceholder.fedimint": "加入Lightning后,支付和支持方面的情况如何?请具体说明。", "reviews.dialog.bodyPlaceholder.fedimint": "加入Lightning后,支付和支持方面的情况如何?请具体说明。",
"fedimint.details.title": "联合会详情" "fedimint.details.title": "联合会详情",
"nav.lnurlMints": "LNURL 铸造厂",
"footer.allLnurlMints": "所有 LNURL 铸造厂",
"lnurlMints.title": "LNURL 铸造厂:lnurlcash 不记名票据铸造厂评价 | Cashumints.space",
"lnurlMints.description": "从 Nostr 网络索引到的全部 {total} 个 LNURL 铸造厂:{online} 个在线,{offline} 个离线。每个都附有社区评分、提现额度和功能。",
"lnurlMints.heading": "LNURL 铸造厂",
"lnurlMints.lede": "本站已知的每一个 LNURL 铸造厂。LNURL 铸造厂发行 lnurlcash 不记名票据:以密钥形式持有的闪电票据,可以轮换、拆分、合并,或熔回成一笔闪电付款。每个铸造厂每隔几分钟通过 HTTPS 检查一次。",
"lnurlMints.search.label": "搜索 LNURL 铸造厂",
"lnurlMints.search.placeholder": "按名称或地址搜索…",
"lnurlMints.sort.label": "LNURL 铸造厂排序方式",
"lnurlMints.hideOffline": "隐藏离线",
"lnurlMints.showing": "显示 {total} 个 LNURL 铸造厂({online} 个在线,{offline} 个离线)",
"lnurlMints.showingFiltered": "显示 {total} 个中的 {shown} 个 LNURL 铸造厂({online} 个在线,{offline} 个离线)",
"lnurlMints.noResults": "没有匹配的 LNURL 铸造厂。",
"lnurlMints.emptyIndex": "尚未索引任何 LNURL 铸造厂。",
"lnurl.crumb": "LNURL 铸造厂",
"lnurl.breadcrumb": "面包屑导航",
"lnurl.copyAddress": "复制地址",
"lnurl.more.title": "更多 LNURL 铸造厂",
"lnurl.more.all": "所有 LNURL 铸造厂 →",
"lnurl.disclaimer": "LNURL 铸造厂保管你的聪,而你持有它的票据,所以使用它意味着信任其运营者。评分只是社区意见,在线表示可访问,不代表可信。",
"lnurl.features.title": "功能",
"lnurl.features.none": "该铸造厂没有公告它支持哪些功能。",
"lnurl.features.note": "功能是铸造厂运营者在 Nostr 上公告的内容。本站能通过 HTTPS 核实的都会核实,并在已公告的功能当前不可用时说明。",
"lnurl.features.notListed": "未公告",
"lnurl.features.unavailable": "不可用",
"lnurl.feature.mint": "通过闪电铸造(LUD-06)",
"lnurl.feature.melt": "熔回闪电",
"lnurl.feature.notes": "轮换 / 拆分 / 合并票据",
"lnurl.feature.lud16": "闪电地址",
"lnurl.feature.lud21": "付款验证(LUD-21)",
"lnurl.feature.signed-notes": "已签名票据(离线验证)",
"lnurl.feature.onion": "Tor 地址",
"lnurl.feature.rotate": "轮换票据",
"lnurl.feature.split": "拆分票据",
"lnurl.feature.merge": "合并票据",
"lnurl.feature.lud06": "LUD-06 payRequest",
"lnurl.feature.lud03": "LUD-03 withdrawRequest",
"lnurl.limits.label": "提现额度",
"lnurl.limits.notPublished": "未公布",
"lnurl.limits.withdrawRow": "提现",
"lnurl.limits.disabled": "已停用",
"lnurl.limits.sats.one": "{n} 聪",
"lnurl.limits.sats.other": "{n} 聪",
"lnurl.details.title": "铸造厂详情",
"lnurl.details.address": "闪电地址",
"lnurl.details.mintPubkey": "铸造厂公钥",
"lnurl.details.onion": "Tor 地址",
"lnurl.details.fee": "铸造手续费",
"lnurl.details.node": "节点",
"lnurl.details.capacity": "已公告容量",
"lnurl.kind.lightningAddress": "闪电地址",
"lnurl.kind.mintPubkey": "铸造厂公钥",
"lnurl.kind.onionAddress": "Tor 地址",
"lnurl.confirmed.label": "最后确认",
"lnurl.confirmed.when": "{when}确认",
"lnurl.fee.base.one": "{n} 聪",
"lnurl.fee.base.other": "{n} 聪",
"lnurl.fee.percent": "{percent}%",
"home.topLnurl.title": "热门 LNURL 铸造厂",
"home.topLnurl.sub": "按评价数和评分排名",
"home.topLnurl.all": "全部 {n} 个 LNURL 铸造厂 →",
"feed.ecosystem.lnurl": "LNURL",
"reviews.dialog.bodyPlaceholder.lnurl": "铸造、熔回和轮换票据的体验如何?请具体说明。",
"reviews.byUrl.open": "写一条评价",
"reviews.byUrl.title": "写一条评价",
"reviews.byUrl.lede": "粘贴地址。如果本站还不认识它,现在就会检查并收录。",
"reviews.byUrl.label.cashu": "mint 的 URL",
"reviews.byUrl.label.fedimint": "联邦的 invite code",
"reviews.byUrl.label.lnurl": "LNURL mint 的 URL",
"reviews.byUrl.placeholder.cashu": "mint.example.com",
"reviews.byUrl.placeholder.fedimint": "fed11…",
"reviews.byUrl.placeholder.lnurl": "mint.example.com",
"reviews.byUrl.check": "检查这个地址",
"reviews.byUrl.checking": "检查中…",
"reviews.byUrl.back": "换一个地址",
"reviews.byUrl.existing": "已经收录",
"reviews.byUrl.indexed": "刚刚收录",
"reviews.byUrl.confirmClose": "要关闭吗?你输入的地址不会保留。",
"reviews.byUrl.detected": "该地址以 {kind} mint 的身份作出了回应。",
"reviews.byUrl.switchTo": "作为 {kind} mint 来评价",
"reviews.byUrl.error.empty": "请先输入地址。",
"reviews.byUrl.error.badUrl": "这看起来不像 mint 的 URL。",
"reviews.byUrl.error.badInvite": "这不是 Fedimint 的 invite code。代码以 fed11 开头。",
"reviews.byUrl.error.badInput": "本站无法检查该地址。",
"reviews.byUrl.error.blocked": "这不是公开地址,因此无法检查。",
"reviews.byUrl.error.notAMint": "该地址有回应,但不是这一类的 mint。",
"reviews.byUrl.error.wrongType": "该地址有回应,但属于另一类 mint。",
"reviews.byUrl.error.unverifiable": "该地址没有任何回应,Nostr 上也还没有它的踪迹。",
"reviews.byUrl.error.rateLimited.one": "同一个连接检查得太频繁了。请在大约 {n} 分钟后再试。",
"reviews.byUrl.error.rateLimited.other": "同一个连接检查得太频繁了。请在大约 {n} 分钟后再试。",
"reviews.byUrl.error.network": "无法连接到索引。请检查网络后再试。",
"seo.lnurl.titleNoReviews": "{name}:LNURL 铸造厂信息与评价 | Cashumints.space",
"seo.desc.statusWithdraw": "{status},提现 {min}–{max} 聪。",
"mint.chip.noFunding": "无法铸造/熔回",
"mint.warnings.lnurlWithdrawalsDisabled.lead": "提现已停用。",
"mint.warnings.lnurlWithdrawalsDisabled.body.online": "该铸造厂公告的最大提现额为零,因此它发行的任何票据都无法兑换成任何东西。在此改变之前,不要在这里铸造。",
"mint.warnings.lnurlWithdrawalsDisabled.body.offline": "最后一次联系上该铸造厂时,它公告的最大提现额为零,因此它发行的任何票据都无法兑换成任何东西。在此改变之前,不要在这里铸造。",
"mint.warnings.lnurlWithdrawalsDisabled.meta.online": "目前已停用提现",
"mint.warnings.lnurlWithdrawalsDisabled.meta.offline": "最后一次见到时提现已停用",
"mint.warnings.lnurlInvalid.lead": "端点有响应但无效。",
"mint.warnings.lnurlInvalid.body": "该主机有响应,但返回的不是本站能读取的铸造厂公告,因此提现可能无法使用。下方是最后一次成功解析到的状态,评价功能仍然可用。",
"mint.warnings.lnurlInvalid.meta": "有响应,但不是有效的铸造厂公告",
"mint.warnings.lnurlNoFunding.lead": "无法铸造和熔回。",
"mint.warnings.lnurlNoFunding.body": "已有票据仍可轮换、拆分或合并,但目前没有任何资金能通过闪电进出。铸造厂本身正常运行并有响应;它背后的闪电节点从它那里无法访问。",
"mint.warnings.lnurlNoFunding.meta": "无法铸造和熔回",
"mint.warnings.also.noFunding": "它的闪电节点当时同样无法访问,因此既不能铸造也不能熔回。"
} }
+1 -1
View File
@@ -23,7 +23,7 @@ import '../styles/global.css';
interface Props { interface Props {
title: string; title: string;
description: string; description: string;
current?: 'mints' | 'fedimints' | 'reviews' | 'wallets' | 'about'; current?: 'mints' | 'fedimints' | 'lnurl-mints' | 'reviews' | 'wallets' | 'about';
mintCount?: number; mintCount?: number;
ogType?: string; ogType?: string;
/** /**
+32 -4
View File
@@ -1,4 +1,7 @@
import type { FedimintDetail, Health, MintDetail, MintListItem, Stats } from '@cashumints/shared'; import { ECOSYSTEMS } from '@cashumints/shared';
import type {
FedimintDetail, Health, LnurlDetail, MintDetail, MintListItem, Stats,
} from '@cashumints/shared';
/** /**
* Build-time API base. Islands use PUBLIC_API_BASE (see api-client.ts) so the browser * Build-time API base. Islands use PUBLIC_API_BASE (see api-client.ts) so the browser
@@ -18,9 +21,9 @@ async function get<T>(path: string): Promise<T> {
* *
* `fetchMints` asks for `type=cashu` rather than for everything, which is what keeps * `fetchMints` asks for `type=cashu` rather than for everything, which is what keeps
* /mints, the mint pages, the home page grid and the sitemap's mint entries meaning * /mints, the mint pages, the home page grid and the sitemap's mint entries meaning
* exactly what they meant before federations were indexed. Federations are their own * exactly what they meant before federations were indexed. Each ecosystem is its own
* call, their own pages and their own count; nothing has to filter a mixed list and * call, its own pages and its own count; nothing has to filter a mixed list and nothing
* nothing silently grew. * silently grew when a second or a third was added.
*/ */
export const fetchMints = (): Promise<MintListItem[]> => get<MintListItem[]>('/api/mints?type=cashu'); export const fetchMints = (): Promise<MintListItem[]> => get<MintListItem[]>('/api/mints?type=cashu');
export const fetchFedimints = (): Promise<MintListItem[]> => export const fetchFedimints = (): Promise<MintListItem[]> =>
@@ -30,8 +33,33 @@ export const fetchMint = (host: string): Promise<MintDetail> =>
/** The same endpoint, typed for a federation's flattened fields. */ /** The same endpoint, typed for a federation's flattened fields. */
export const fetchFedimint = (host: string): Promise<FedimintDetail> => export const fetchFedimint = (host: string): Promise<FedimintDetail> =>
get<FedimintDetail>(`/api/mints/${encodeURIComponent(host)}`); get<FedimintDetail>(`/api/mints/${encodeURIComponent(host)}`);
export const fetchLnurlMints = (): Promise<MintListItem[]> =>
get<MintListItem[]>('/api/mints?type=lnurl');
/** The same endpoint again, typed for an LNURL mint's flattened fields. */
export const fetchLnurlMint = (host: string): Promise<LnurlDetail> =>
get<LnurlDetail>(`/api/mints/${encodeURIComponent(host)}`);
export const fetchStats = (): Promise<Stats> => get<Stats>('/api/stats'); export const fetchStats = (): Promise<Stats> => get<Stats>('/api/stats');
/**
* Every ecosystem's listing, in `ECOSYSTEMS` order, fetched together.
*
* For the pages that are about *all* of them rather than one — /reviews and the home
* page's review strip — where naming each ecosystem's fetch by hand is exactly the kind
* of list that gets one entry short. The filter pills on /reviews are already built from
* `ECOSYSTEMS`, so before this existed a new ecosystem got a pill that could never match
* anything: the pill came from that list and the data did not. This closes that gap, and
* a fourth ecosystem now needs no edit on either side.
*
* One request per ecosystem rather than a single unfiltered `/api/mints`, deliberately:
* the per-type responses are what every other page already asks for, so they are warm in
* the API's own path, and a caller gets them back already grouped.
*/
export async function fetchAllListings(): Promise<MintListItem[][]> {
return Promise.all(
ECOSYSTEMS.map((type) => get<MintListItem[]>(`/api/mints?type=${encodeURIComponent(type)}`)),
);
}
/** Health is allowed to be down at build time: the ticker just renders without it. */ /** Health is allowed to be down at build time: the ticker just renders without it. */
export async function fetchHealth(): Promise<Health | null> { export async function fetchHealth(): Promise<Health | null> {
try { try {
+59 -9
View File
@@ -16,8 +16,9 @@
* there is nothing to translate in a lookup anyway. * there is nothing to translate in a lookup anyway.
*/ */
import { import {
federationIdFromKey, federationIdFromSlug, mintUrlsFromEvent, normalizeMintUrl, baseUrlFromKey, federationIdFromKey, federationIdFromSlug, hostIdentifier, mintUrlsFromEvent,
reviewEcosystem, reviewTargetId, type MintListItem, type NostrEventLike, normalizeMintUrl, reviewEcosystem, reviewTargetId,
type MintListItem, type NostrEventLike,
} from '@cashumints/shared'; } from '@cashumints/shared';
import { displayDomain } from './format'; import { displayDomain } from './format';
@@ -30,13 +31,14 @@ export interface FeedTarget {
name: string; name: string;
/** The label a card prints: a domain, or a shortened federation id. */ /** The label a card prints: a domain, or a shortened federation id. */
domain: string; domain: string;
/** `cashu` or `fedimint`. Picks both the resolver and the link's route. */ /** `cashu`, `fedimint` or `lnurl`. Picks both the resolver and the link's route. */
type: string; type: string;
/** /**
* The `d` identifier reviews of this thing carry, when that is how it is found. * The `d` identifier reviews of this thing carry, when that is how it is found.
* *
* A federation id. null for a mint, which is resolved through its `u` tag instead — * A federation id, or an LNURL mint's normalized host. null for a Cashu mint, which is
* that is how every legacy review points at one, and changing it would strand them. * resolved through its `u` tag instead — that is how every legacy review points at
* one, and changing it would strand them.
*/ */
ref: string | null; ref: string | null;
} }
@@ -74,6 +76,29 @@ export function resolveFeedTarget(event: NostrEventLike, index: FeedIndex): Feed
return d ? index.byRef.get(d.toLowerCase()) ?? null : null; return d ? index.byRef.get(d.toLowerCase()) ?? null : null;
} }
/*
* LNURL resolves by `d` and then by `u`, because both are genuinely used.
*
* The `d` a review carries is either the mint's funding-node pubkey or its host, and
* the feed index only knows the host — the list payload carries no pubkey. So a
* pubkey-`d` review falls through to `u`, which an LNURL review can actually be
* resolved by: unlike a federation's opaque invite code, it is a real URL that the
* same normalizer the Cashu branch uses can turn into a row key.
*/
if (ecosystem === 'lnurl') {
const d = reviewTargetId(event);
const byRef = d ? index.byRef.get(d.trim().toLowerCase()) : undefined;
if (byRef) return byRef;
for (const raw of mintUrlsFromEvent(event)) {
const normalized = normalizeMintUrl(raw);
if (!normalized) continue;
const found = index.byUrl.get(`lnurl:${normalized.url}`);
if (found) return found;
}
return null;
}
if (ecosystem !== 'cashu') return null; if (ecosystem !== 'cashu') return null;
const raw = mintUrlsFromEvent(event)[0]; const raw = mintUrlsFromEvent(event)[0];
@@ -83,9 +108,21 @@ export function resolveFeedTarget(event: NostrEventLike, index: FeedIndex): Feed
return index.byUrl.get(normalized.url) ?? null; return index.byUrl.get(normalized.url) ?? null;
} }
/** The page a card links to. One route per ecosystem, decided in one place. */ /**
* The page a card links to. One route per ecosystem, decided in one place.
*
* The default is `/mint/`, not the newest ecosystem: a row of some type this build has
* never heard of is far more likely to be an older one, and every row written before
* `mints.type` existed means Cashu.
*/
const ROUTES: Record<string, string> = {
fedimint: '/fedimint',
lnurl: '/lnurl-mint',
cashu: '/mint',
};
export function targetPath(target: Pick<FeedTarget, 'type' | 'host'>): string { export function targetPath(target: Pick<FeedTarget, 'type' | 'host'>): string {
return `/${target.type === 'fedimint' ? 'fedimint' : 'mint'}/${target.host}`; return `${ROUTES[target.type] ?? '/mint'}/${target.host}`;
} }
/** /**
@@ -103,14 +140,27 @@ export function targetPath(target: Pick<FeedTarget, 'type' | 'host'>): string {
export function feedTargets(...lists: Array<readonly MintListItem[]>): FeedTarget[] { export function feedTargets(...lists: Array<readonly MintListItem[]>): FeedTarget[] {
return lists.flat().map((item) => { return lists.flat().map((item) => {
const federationId = federationIdFromKey(item.url); const federationId = federationIdFromKey(item.url);
const domain = federationId ? federationIdFromSlug(item.host) : displayDomain(item.url); const lnurlBase = baseUrlFromKey(item.url);
/*
* An LNURL row's key is `lnurl:https://host`, so the label has to come out of the
* key rather than off `item.url` directly — `displayDomain` on the raw key would
* print the scheme prefix at a reader. Its `ref` is the host identifier, which is
* the `d` a review carries whenever the mint has no funding-node pubkey; reviews
* that carry the pubkey instead resolve through `u`, which is why this does not
* need one.
*/
const domain = federationId
? federationIdFromSlug(item.host)
: displayDomain(lnurlBase ?? item.url);
return { return {
url: item.url, url: item.url,
host: item.host, host: item.host,
name: item.name ?? domain.split('/')[0] ?? domain, name: item.name ?? domain.split('/')[0] ?? domain,
domain, domain,
type: item.type, type: item.type,
ref: federationId, ref: federationId ?? (lnurlBase ? hostIdentifier(lnurlBase) : null),
}; };
}); });
} }
+172
View File
@@ -0,0 +1,172 @@
/**
* The browser half of on-demand indexing.
*
* One request — `POST /api/index` — with the two callers that make it: the 404
* resolver, which asks on the reader's behalf when they open a deep link this build has
* never heard of, and the review-by-URL dialog, where they ask deliberately.
*
* Both need the same three things and neither should reimplement them: the request, a
* plain sentence for every way it can fail, and the `ReviewSubject` the review dialog
* publishes against. The failure vocabulary itself is `@cashumints/shared`, so the API
* and this file cannot drift about what `wrong_type` means.
*/
import {
fedimintSubject,
lnurlSubject,
mintSubject,
type ReviewSubject,
} from './review-subject';
import { apiBase } from './client';
import type { Translator } from '../i18n/translate';
import {
isIndexFailure,
type IndexFailure,
type IndexSuccess,
type IndexType,
type FedimintDetail,
type LnurlDetail,
type MintDetail,
} from '@cashumints/shared';
/**
* What came back.
*
* The HTTP status is kept beside the body rather than collapsed into a boolean, because
* 200 and 201 mean visibly different things to a reader — "already listed" against "we
* just checked it for you" — and the dialog says so.
*/
export type IndexResult =
| { ok: true; status: number; mint: IndexSuccess }
| { ok: false; status: number; failure: IndexFailure };
/** A network failure, shaped like every other failure so callers have one path. */
function offline(): IndexResult {
return {
ok: false,
status: 0,
failure: { error: 'invalid_response', message: 'the API could not be reached' },
};
}
/**
* Ask the API to index an address, and wait for the answer.
*
* There is deliberately no client-side timeout. The endpoint bounds itself — one probe
* at the standard five seconds, then at most three more for the relay lookup — and a
* shorter timer here would abandon requests that are about to succeed while leaving the
* server doing the work anyway. The caller shows a "checking" state and means it.
*/
export async function requestIndex(type: IndexType, input: string): Promise<IndexResult> {
let res: Response;
try {
res = await fetch(`${apiBase}/api/index`, {
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json' },
body: JSON.stringify({ type, input }),
});
} catch {
return offline();
}
let body: unknown;
try {
body = (await res.json()) as unknown;
} catch {
return offline();
}
if (!body || typeof body !== 'object') return offline();
if (res.ok && !isIndexFailure(body as IndexSuccess | IndexFailure)) {
return { ok: true, status: res.status, mint: body as IndexSuccess };
}
return { ok: false, status: res.status, failure: body as IndexFailure };
}
/**
* The sentence a reader gets for a failure.
*
* Plain words, and specific: "nothing responded at this address and no trace of it
* exists on Nostr yet" is a different situation from "that address answered, but not as
* an LNURL mint", and a reader can act on the difference — the first means checking the
* spelling, the second means picking a different page. A generic "could not be added"
* would hide both.
*
* `wrong_type` is handled by the caller rather than here, because the useful response to
* it is a button, not a sentence.
*/
export function indexErrorMessage(result: { status: number; failure: IndexFailure }, t: Translator): string {
const { failure, status } = result;
switch (failure.error) {
case 'bad_input':
return t('reviews.byUrl.error.badInput');
case 'bad_type':
return t('reviews.byUrl.error.badInput');
case 'blocked_host':
return t('reviews.byUrl.error.blocked');
case 'invalid_invite':
return t('reviews.byUrl.error.badInvite');
case 'invalid_response':
return status === 0
? t('reviews.byUrl.error.network')
: t('reviews.byUrl.error.notAMint');
case 'unverifiable':
return t('reviews.byUrl.error.unverifiable');
case 'rate_limited':
return t('reviews.byUrl.error.rateLimited', { n: retryMinutes(failure) });
case 'wrong_type':
return t('reviews.byUrl.error.wrongType');
default:
return t('reviews.byUrl.error.network');
}
}
/** A `retry_after` in whole minutes, at least one, for the "try again in…" sentence. */
function retryMinutes(failure: IndexFailure): number {
return Math.max(1, Math.ceil((failure.retry_after ?? 3600) / 60));
}
/**
* The review subject for an indexed mint, whichever ecosystem it belongs to.
*
* The same three builders the three page templates use, chosen by the payload's own
* `type` rather than by what the caller asked for — a submission can come back as a
* different ecosystem than it went out as (that is what `detected_type` is for), and
* the row that exists is the one the review has to point at.
*/
export function subjectFor(mint: MintDetail): ReviewSubject | null {
if (mint.type === 'fedimint') return fedimintSubject(mint as FedimintDetail);
if (mint.type === 'lnurl') return lnurlSubject(mint as LnurlDetail);
if (mint.type === 'cashu') return mintSubject(mint);
// A type this build has no review kind for. Nothing here can point a review at it.
return null;
}
/**
* The name to show for a mint, falling back the way every card does.
*
* A federation usually has one from its announcement; one submitted as a bare invite
* code has nothing but its id, and its shortened id is then both its name and its
* address. Callers that render both lines drop the second when they match, rather than
* printing the same string twice.
*/
export function mintDisplayName(mint: MintDetail): string {
const domain = displayAddress(mint);
return mint.name ?? domain.split('/')[0] ?? domain;
}
/**
* The address to print under that name.
*
* A federation has no URL — its key is `fedimint:<id>` — so it shows its id instead,
* shortened the way the federation pages shorten it. Everything else shows the address
* a reader would type.
*/
export function displayAddress(mint: MintDetail): string {
if (mint.type === 'fedimint') {
const id = mint.federation_id ?? mint.host;
return id.length > 24 ? `${id.slice(0, 16)}…${id.slice(-6)}` : id;
}
return mint.url.replace(/^lnurl:/, '').replace(/^https?:\/\//, '').replace(/\/+$/, '');
}
+188
View File
@@ -0,0 +1,188 @@
/**
* The LNURL mint page's Features panel and withdraw-limits cell, as HTML strings.
*
* The Cashu counterparts are `nutRowsHtml` and `limitsHtml` in mint-state.ts, and the
* Fedimint one is `moduleRowsHtml` in fedimint-ui.ts. This is deliberately their twin:
* same three-column row, same check, same grey "not published", same classes, so a
* reader who has learned to scan one panel can scan the others without relearning them
* and there is no second stylesheet.
*
* Built as strings rather than as markup in the page for the same reason those files
* give: two things render this panel — the page at build time from the last probe, and
* the live island from a fresh read of the API — and two implementations would drift.
*
* Where it differs from both, and why:
*
* - **Three states, not two.** The Fedimint panel has no disabled state because a
* `modules` tag says nothing about whether a module is switched on. Here there
* genuinely is a third state: a mint can publish `mint` and `melt`, and still be
* unable to do either right now because the Lightning node behind it is unreachable.
* That is checkable over HTTP, so the row says "unavailable" rather than showing a
* tick beside something that would fail if tried.
* - **`features` is never rewritten.** The announcement is the operator's claim about
* what they built; the probe can only mark a claimed capability unavailable, never
* add or remove one. That asymmetry is normative in `docs/KIND-LNURL-MINT.md`, and
* `featureStates` in shared/ is where it is enforced.
*/
import {
FEATURE_NAMES_EN, HIGHLIGHT_FEATURES, featureStates, msatToSat,
type FeatureState, type HighlightFeature,
} from '@cashumints/shared';
import type { Formatters } from '../i18n/format';
const CHECK_ICON =
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" ' +
'stroke-width="2.5" aria-hidden="true"><path d="m5 13 4 4L19 7"/></svg>';
const PAUSE_ICON =
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" ' +
'stroke-width="2.5" aria-hidden="true"><path d="M10 5v14M16 5v14"/></svg>';
function escape(value: string): string {
return value
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
/**
* A feature's plain-language name in this locale.
*
* Same rule as `localNutName` and `localModuleName`: the catalog first, `shared`'s
* English table second, and the published token last. A mint advertising something this
* build has never heard of renders as `quantum-notes` rather than as a guess, which is
* still a true label.
*/
export function localFeatureName(feature: string, f: Formatters): string {
const key = `lnurl.feature.${feature}`;
if (f.t.has(key)) return f.t(key);
return FEATURE_NAMES_EN[feature] ?? feature;
}
/**
* The short mono label in the first column.
*
* The LUD numbers are spec names and are the same in every language, so they are shown
* literally. The note operations have no such name, and the column shows the vocabulary
* token itself — which is exactly what a publisher would put in a `features` tag, and so
* is the string worth being able to recognise.
*/
const ROW_TOKENS: Record<HighlightFeature, string> = {
mint: 'mint',
melt: 'melt',
notes: 'notes',
lud16: 'LUD-16',
lud21: 'LUD-21',
'signed-notes': 'signed',
onion: 'onion',
};
/**
* The seven named rows.
*
* `fundingAvailable` is null when nothing has probed the mint yet, which reads as "no
* reason to doubt the announcement" rather than as a failure — a mint discovered
* minutes ago should not have its capabilities greyed out for not having been checked.
*/
export function featureRowsHtml(
features: readonly string[],
fundingAvailable: boolean | null,
f: Formatters,
): string {
const states = featureStates(features, fundingAvailable);
return HIGHLIGHT_FEATURES.map((key: HighlightFeature) => {
const state: FeatureState = states[key];
const badge =
state === 'ok'
? `<span class="nut-state ok">${CHECK_ICON}</span>`
: state === 'unavailable'
? `<span class="nut-state disabled">${PAUSE_ICON}${escape(f.t('lnurl.features.unavailable'))}</span>`
: `<span class="nut-state none">${escape(f.t('lnurl.features.notListed'))}</span>`;
const classes =
state === 'ok' ? 'nut-row' : state === 'unavailable' ? 'nut-row disabled' : 'nut-row off';
return (
`<div class="${classes}" data-feature-row="${escape(key)}">` +
`<span class="nut-num">${escape(ROW_TOKENS[key])}</span>` +
`<span class="nut-name">${escape(localFeatureName(key, f))}</span>` +
`${badge}</div>`
);
}).join('');
}
/**
* The inside of the verdict strip's withdraw-limits cell.
*
* The Cashu cell shows NUT-04's mint limits and splits in two when a direction is
* switched off. This one shows the withdraw bounds, which are the numbers that actually
* answer a reader's question here: how much can one of this mint's notes be worth?
*
* A ceiling of zero is not rendered as "0 sat". It is the disabling value — no note this
* mint issues can be redeemed for anything — and it gets the struck-through treatment
* the Cashu cell gives a disabled direction, with the banner above doing the explaining.
*/
export function withdrawLimitsHtml(
minMsat: number | null | undefined,
maxMsat: number | null | undefined,
f: Formatters,
): string {
const min = msatToSat(minMsat ?? null);
const max = msatToSat(maxMsat ?? null);
if (min === null && max === null) {
return `<div class="v-mono"><span>${escape(f.t('lnurl.limits.notPublished'))}</span></div>`;
}
if (max === 0) {
return (
'<div class="v-mono lim-split">' +
'<div class="lim-row off"><span class="lim-k">' +
`${escape(f.t('lnurl.limits.withdrawRow'))}</span>` +
'<span class="lim-body">' +
`<b class="lim-v">${escape(f.t('lnurl.limits.sats', { n: 0 }))}</b>` +
`<span class="lim-off">${escape(f.t('lnurl.limits.disabled'))}</span>` +
'</span></div></div>'
);
}
const lines = [
min !== null
? `<span>${escape(f.t('mint.limits.min'))}</span> <b>${escape(f.t('lnurl.limits.sats', { n: min }))}</b>`
: '',
max !== null
? `<span>${escape(f.t('mint.limits.max'))}</span> <b>${escape(f.t('lnurl.limits.sats', { n: max }))}</b>`
: '',
].filter(Boolean);
return `<div class="v-mono">${lines.join('<br />')}</div>`;
}
/**
* The mint fee, as one line, or null when there is nothing to say.
*
* A mint that publishes no `Mint fees:` entry is fee-free by spec, and a row reading
* "Fee: none" on every one of them would be furniture. Both parts are shown when both
* are set, because a flat fee and a percentage answer different questions.
*/
export function feeText(
baseMsat: number | null | undefined,
ppm: number | null | undefined,
f: Formatters,
): string | null {
const base = baseMsat ?? 0;
const percentPpm = ppm ?? 0;
if (base === 0 && percentPpm === 0) return null;
const parts: string[] = [];
if (base > 0) parts.push(f.t('lnurl.fee.base', { n: msatToSat(base) ?? 0 }));
if (percentPpm > 0) {
// ppm to percent, at most three decimals: 100 ppm is 0.01%, and a mint charging
// 25_000 ppm is charging 2.5%. Trailing zeros are trimmed so the common cases read
// as "0.01%" rather than "0.010%".
const percent = (percentPpm / 10_000).toFixed(3).replace(/\.?0+$/, '');
parts.push(f.t('lnurl.fee.percent', { percent }));
}
return parts.join(' + ');
}
+50 -4
View File
@@ -16,14 +16,15 @@
* has to stay plain data — no functions, no class instances. * has to stay plain data — no functions, no class instances.
*/ */
import { import {
KIND_FEDIMINT_ANNOUNCEMENT, KIND_MINT_ANNOUNCEMENT, PRIMARY_RELAY, mintUrlSpellings, KIND_FEDIMINT_ANNOUNCEMENT, KIND_LNURL_ANNOUNCEMENT, KIND_MINT_ANNOUNCEMENT, PRIMARY_RELAY,
type FedimintDetail, type MintDetail, lnurlIdentifiers, mintUrlSpellings,
type FedimintDetail, type LnurlDetail, type MintDetail,
} from '@cashumints/shared'; } from '@cashumints/shared';
export interface ReviewSubject { export interface ReviewSubject {
/** `cashu` or `fedimint`. Only used for labelling; no logic branches on it. */ /** `cashu`, `fedimint` or `lnurl`. Only used for labelling; no logic branches on it. */
type: string; type: string;
/** The announcement kind, written to the review's `k` tag: 38172 or 38173. */ /** The announcement kind, written to the review's `k` tag: 38172, 38173 or 38174. */
kind: number; kind: number;
/** /**
* The `d` identifier of the announcement being reviewed, and the value the `#d` relay * The `d` identifier of the announcement being reviewed, and the value the `#d` relay
@@ -67,6 +68,14 @@ export interface ReviewSubject {
* nothing else, so a federation asks by `d` only and this is 0 for one. * nothing else, so a federation asks by `d` only and this is 0 for one.
*/ */
filterByU: boolean; filterByU: boolean;
/**
* Every `d` a review of this thing could carry, for the relay filter.
*
* Only LNURL sets this, and only because its identifier can legitimately change once
* — from a host to a pubkey — leaving reviews under both. Absent everywhere else,
* where `d` is the one identifier there has ever been.
*/
alternateIds?: string[];
} }
/** A Cashu mint, reviewed exactly as it has always been. */ /** A Cashu mint, reviewed exactly as it has always been. */
@@ -99,6 +108,39 @@ export function fedimintSubject(
}; };
} }
/**
* An LNURL mint, identified per `docs/KIND-LNURL-MINT.md`.
*
* `d` is the funding node's pubkey when the mint has one and its normalized host
* otherwise, and `alternateIds` carries the other spelling so the reviews panel asks
* the relays for both. That is not defensive coding: a mint announced by host before it
* had a funding source, and by pubkey afterwards, genuinely has reviews under each, and
* a panel asking only for the current identifier would show half of them.
*
* `filterByU` is true, unlike the Fedimint side. An LNURL mint's address is a short URL
* rather than a 150-character invite code, so no relay rejects the filter for being too
* large, and `u` is the tag a client that has never heard of 38174 is most likely to get
* right.
*/
export function lnurlSubject(
mint: Pick<LnurlDetail, 'url' | 'base_url' | 'lnurl_id' | 'mint_pubkey' | 'announcer_pubkey'>,
): ReviewSubject {
const baseUrl = mint.base_url ?? mint.url.replace(/^lnurl:/, '');
const ids = lnurlIdentifiers(baseUrl, mint.mint_pubkey);
return {
type: 'lnurl',
kind: KIND_LNURL_ANNOUNCEMENT,
d: mint.lnurl_id ?? ids[0] ?? null,
u: mintUrlSpellings(baseUrl),
marker: 'lnurl',
announcer: mint.announcer_pubkey ?? null,
key: mint.url,
filterByU: true,
alternateIds: ids,
};
}
/** /**
* The tags a published review carries, per NIP-87. * The tags a published review carries, per NIP-87.
* *
@@ -115,6 +157,10 @@ export function fedimintSubject(
* the ones written after. NOTES.md has the history. * the ones written after. NOTES.md has the history.
* - Fedimint writes `38173:<announcer pubkey>:<federation id>`, which is the correct * - Fedimint writes `38173:<announcer pubkey>:<federation id>`, which is the correct
* form, because there is no legacy to preserve and the announcer is known. * form, because there is no legacy to preserve and the announcer is known.
* - LNURL writes `38174:<announcer pubkey>:<d>`, the correct form for the same reason
* — the kind is new, so there is nothing to stay compatible with. When nothing has
* announced the mint yet there is no announcer, and the pointer falls back to the
* identifier in the author position exactly as Cashu does.
* *
* A subject with neither `d` nor any `u` value cannot be pointed at, and the caller * A subject with neither `d` nor any `u` value cannot be pointed at, and the caller
* refuses to publish rather than sign an event nothing can resolve. * refuses to publish rather than sign an event nothing can resolve.
+17 -8
View File
@@ -104,13 +104,21 @@ export function lastPublish(): number {
* *
* Two filters, OR'd, matching the old site's read path: * Two filters, OR'd, matching the old site's read path:
* - `#d` = the announcement's identifier, with `#k` = its kind (proper NIP-87). For a * - `#d` = the announcement's identifier, with `#k` = its kind (proper NIP-87). For a
* mint that is the pubkey from /v1/info; for a federation, the federation id. * mint that is the pubkey from /v1/info; for a federation, the federation id; for an
* LNURL mint, its funding node's pubkey or its host.
* - `#u` = the address, in every spelling the old site published * - `#u` = the address, in every spelling the old site published
* *
* The URL filter is what finds legacy mint reviews, and dropping it would lose most of * The URL filter is what finds legacy mint reviews, and dropping it would lose most of
* them. A federation sets `filterByU: false` and is asked by `#d` alone — its `u` value * them. A federation sets `filterByU: false` and is asked by `#d` alone — its `u` value
* is a 150 character invite code, and relays reject a filter item that long outright, * is a 150 character invite code, and relays reject a filter item that long outright,
* so sending it costs round trips and returns nothing. See `review-subject.ts`. * so sending it costs round trips and returns nothing. See `review-subject.ts`.
*
* An LNURL mint sends more than one `#d` value, and it is the only subject that does:
* its identifier legitimately changes once in its life, from its host to its funding
* node's pubkey, and reviews written either side of that carry different `d` tags for
* the same mint. Both are asked for in one filter, and both are accepted by the
* re-check below, so a mint that gained a funding source does not appear to lose half
* its reviews.
*/ */
/** Longest body a card will render. Anything real fits; a stunt event is cut. */ /** Longest body a card will render. Anything real fits; a stunt event is cut. */
const MAX_RENDERED_CHARS = 4000; const MAX_RENDERED_CHARS = 4000;
@@ -121,10 +129,13 @@ export async function loadReviews(subject: ReviewSubject): Promise<LoadedReview[
const filters: Record<string, unknown>[] = []; const filters: Record<string, unknown>[] = [];
if (subject.d) { const identifiers = [...new Set([...(subject.alternateIds ?? []), subject.d]
.filter((id): id is string => Boolean(id)))];
if (identifiers.length > 0) {
filters.push({ filters.push({
kinds: [KIND_REVIEW], kinds: [KIND_REVIEW],
'#d': [subject.d], '#d': identifiers,
'#k': [String(subject.kind)], '#k': [String(subject.kind)],
limit: 500, limit: 500,
}); });
@@ -151,13 +162,11 @@ export async function loadReviews(subject: ReviewSubject): Promise<LoadedReview[
* event is re-checked against the subject's own tags, mirroring the two filters. * event is re-checked against the subject's own tags, mirroring the two filters.
*/ */
const uSpellings = new Set(subject.u); const uSpellings = new Set(subject.u);
const knownIds = new Set(identifiers);
const aboutSubject = (event: NostrEvent): boolean => { const aboutSubject = (event: NostrEvent): boolean => {
if (event.kind !== KIND_REVIEW) return false; if (event.kind !== KIND_REVIEW) return false;
if ( const d = tagValue(event.tags, 'd');
subject.d && if (d && knownIds.has(d) && tagValue(event.tags, 'k') === String(subject.kind)) {
tagValue(event.tags, 'd') === subject.d &&
tagValue(event.tags, 'k') === String(subject.kind)
) {
return true; return true;
} }
return subject.filterByU && tagValues(event.tags, 'u').some((u) => uSpellings.has(u)); return subject.filterByU && tagValues(event.tags, 'u').some((u) => uSpellings.has(u));
+555
View File
@@ -0,0 +1,555 @@
/**
* The write-a-review form, as one behaviour two dialogs share.
*
* It used to live inside the reviews panel's island, which was right while a review
* could only be written from a mint's own page: the panel had the subject, the dialog
* was part of the panel, and there was nothing to share it with.
*
* Review-by-URL changed that. The three index pages now open a dialog whose *first*
* step works out what is being reviewed — a reader pastes an address, the site indexes
* it — and whose second step is this form, identically. Two copies of a signing flow is
* the last thing this codebase should have, so the form moved here and both callers
* drive it through `wireWriteForm`.
*
* What is deliberately *not* here: anything about the list of reviews. Rendering,
* filtering, counts and optimistic insertion belong to whoever is showing reviews, and
* the index pages are not. This module signs and publishes, and reports what happened.
*
* The one behavioural difference between the two callers is where the subject comes
* from — a prerendered data attribute on a mint page, a resolved API response in the
* dialog — so it is a function rather than a value, read at submit time.
*/
import { useI18n } from '../i18n/client';
import { escapeHtml } from './client';
import { closeModal, openModal, wireModal } from './modal';
import { activeSigner, onSessionChange, onSignerStatus, readSession, signerErrorMessage } from './nostr-signer';
import { avatarMarkup, identityLines, methodChipHtml, openLogin, resolveProfile, toNpub } from './auth-ui';
import { getExtension, loadProfiles, publishReview } from './reviews-client';
import type { ReviewSubject } from './review-subject';
import type { Profile } from '@cashumints/shared';
/** Reviews are capped at this length, and the counter turns red past it. */
export const MAX_CHARS = 2000;
/** Below this a review says nothing anyone can use. */
export const MIN_CHARS = 10;
/** What a completed publish hands back to whoever opened the dialog. */
export interface PublishedReview {
id: string;
pubkey: string;
created_at: number;
rating: number;
content: string;
/** How many relays took it, of how many tried. Shown on the card. */
accepted: number;
total: number;
/** The subject it was published against, which the caller may have just resolved. */
subject: ReviewSubject;
}
export interface WriteFormOptions {
/** The `<dialog>` the form lives in. This module wires its modal behaviour. */
dialog: HTMLDialogElement;
/** What is being reviewed, read at open and at submit. Null means "not yet known". */
subject: () => ReviewSubject | null;
/** Called once the relays have taken the event. */
onPublished?: (review: PublishedReview) => void;
/** The author's own kind 0, once it lands. Only used to un-Anon their fresh card. */
onProfile?: (pubkey: string, profile: Profile) => void;
/**
* Asked before a backdrop click closes the dialog, in addition to this module's own
* "is there a half-written review in the box?" check. The review-by-URL dialog uses
* it to protect its first step as well.
*/
onBackdrop?: () => boolean;
}
export interface WriteFormApi {
/** Restore any draft, draw the identity row, and focus the rating. */
open(): void;
close(): void;
/** Clear the form and its stored draft. After publishing, or a subject change. */
reset(): void;
/** Re-read who is signing. Called when the subject changes under an open dialog. */
refreshIdentity(): void;
/** Show an error line inside the dialog, in the same place a publish failure uses. */
showError(message: string): void;
}
/**
* Wire one dialog's write form. Returns the handful of things a caller drives it with.
*
* Every element is looked up under the dialog, so two of these can coexist on one page
* without reaching into each other — which is exactly what happens on a mint page that
* also carries the review-by-URL dialog.
*/
export function wireWriteForm(options: WriteFormOptions): WriteFormApi {
const { dialog } = options;
const t = useI18n();
const form = dialog.querySelector<HTMLFormElement>('[data-write-form]');
const errorBox = dialog.querySelector<HTMLElement>('[data-write-error]');
const statusBox = dialog.querySelector<HTMLElement>('[data-write-status]');
const submit = dialog.querySelector<HTMLButtonElement>('[data-write-submit]');
const submitLabel = dialog.querySelector<HTMLElement>('[data-write-submit-label]');
const textarea = dialog.querySelector<HTMLTextAreaElement>('textarea[name="content"]');
const charCount = dialog.querySelector<HTMLElement>('[data-char-count]');
const starLabel = dialog.querySelector<HTMLElement>('[data-star-label]');
const identityRow = dialog.querySelector<HTMLElement>('[data-write-identity]');
const whyDisabled = dialog.querySelector<HTMLElement>('[data-write-why]');
// One phrase per star, from the catalog, read through `t` rather than held in a table
// here so the phrases follow the page's language after a view transition.
const starPhrase = (rating: number): string => t(`reviews.dialog.stars.${rating}`);
/** True once a publish is in flight, so nothing else runs over it. */
let publishing = false;
/** Who is going to sign, resolved when the dialog opens. */
let canSign = false;
function showError(message: string): void {
if (!errorBox) return;
errorBox.textContent = message;
errorBox.hidden = message === '';
}
function showStatus(message: string): void {
if (!statusBox) return;
statusBox.textContent = message;
statusBox.hidden = message === '';
}
/* ---------- draft safety ---------- */
/**
* A written review survives an accidental close.
*
* sessionStorage, keyed by subject: it lasts as long as the tab and no longer, which
* is the life of the intention. Nothing here is secret, but nothing here belongs on
* the machine a week later either.
*/
const draftKey = (): string | null => {
const key = options.subject()?.key;
return key ? `cashumints:draft:${key}` : null;
};
function saveDraft(): void {
const key = draftKey();
if (!form || !key) return;
const data = new FormData(form);
const rating = String(data.get('rating') ?? '');
const content = String(data.get('content') ?? '');
try {
if (!rating && !content.trim()) sessionStorage.removeItem(key);
else sessionStorage.setItem(key, JSON.stringify({ rating, content }));
} catch {
// Storage blocked. The draft lives in the open dialog and nowhere else.
}
}
function clearDraft(): void {
const key = draftKey();
if (!key) return;
try {
sessionStorage.removeItem(key);
} catch {
// Nothing stored to begin with.
}
}
function restoreDraft(): void {
const key = draftKey();
if (!form || !key) return;
let draft: { rating?: string; content?: string } | null = null;
try {
const raw = sessionStorage.getItem(key);
draft = raw ? (JSON.parse(raw) as { rating?: string; content?: string }) : null;
} catch {
draft = null;
}
if (!draft) return;
if (draft.rating) {
const star = form.querySelector<HTMLInputElement>(`input[name="rating"][value="${draft.rating}"]`);
if (star) star.checked = true;
}
if (draft.content && textarea) textarea.value = draft.content;
syncStarLabel();
syncCount();
}
/* ---------- the form's own state ---------- */
function chosenRating(): number {
return Number(new FormData(form ?? undefined).get('rating') ?? 0);
}
function syncStarLabel(): void {
if (!starLabel) return;
const rating = chosenRating();
starLabel.textContent = rating
// The rating is a bare numeral here, so it is passed as a string: "3: good",
// never "3,0: good".
? t('reviews.dialog.starLabel', { n: String(rating), phrase: starPhrase(rating) })
: t('reviews.dialog.chooseRating');
}
function syncCount(): void {
const length = textarea?.value.length ?? 0;
if (charCount) {
charCount.textContent = t('reviews.dialog.charCount', { n: length, max: MAX_CHARS });
charCount.classList.toggle('over', length > MAX_CHARS);
}
syncSubmit();
}
/**
* Whether "Sign and publish" is available, and if not, why.
*
* The reason is always on screen: a button that is off for an unstated reason is a
* button people click twice and then leave.
*/
function syncSubmit(): void {
if (!submit) return;
const length = textarea?.value.trim().length ?? 0;
const rating = chosenRating();
const over = (textarea?.value.length ?? 0) - MAX_CHARS;
const reason = publishing
? ''
: !canSign ? t('reviews.dialog.why.login')
: !rating ? t('reviews.dialog.why.rating')
: length === 0 ? t('reviews.dialog.why.words')
: length < MIN_CHARS ? t('reviews.dialog.why.more', { n: MIN_CHARS - length })
: over > 0 ? t('reviews.dialog.why.over', { n: over })
: '';
submit.disabled = publishing || reason !== '';
if (whyDisabled) whyDisabled.textContent = publishing ? '' : reason;
}
/** The publish button, mid-flight or back to rest. */
function setBusy(busy: boolean, label = t('reviews.dialog.submit')): void {
publishing = busy;
submit?.classList.toggle('is-busy', busy);
if (submitLabel) submitLabel.textContent = label;
syncSubmit();
}
/* ---------- who is signing ---------- */
/**
* Draw the identity row, and decide whether publishing is possible at all.
*
* Three states, in the order they are likely:
* - logged in: the session's signer, named, with a way to switch
* - no session but a NIP-07 extension: quick-sign, exactly as before logins
* existed, and no session is created by using it
* - neither: the row becomes the way in, and the publish button says so
*
* The extension's pubkey is asked for here, on open, and never on page load: it is a
* question some extensions put to the reader, and a page that asks it for every
* visitor who might one day write a review is a page that nags.
*/
async function renderIdentity(): Promise<void> {
if (!identityRow) return;
const session = readSession();
if (session) {
// A session that names an extension is only as good as the extension: it can be
// uninstalled, or disabled for this site, between one visit and the next.
canSign = session.method !== 'extension' || getExtension() !== null;
syncSubmit();
if (!canSign) {
identityRow.innerHTML =
`<span class="write-id-who"><span class="write-id-lead">${escapeHtml(
t('reviews.dialog.identity.extensionGone'),
)}</span></span>` +
`<button type="button" class="btn" data-login-open>${escapeHtml(t('login.open'))}</button>`;
return;
}
const npub = await toNpub(session.pubkey);
const draw = (profile: Profile | null): void => {
const { primary, secondary, named } = identityLines(profile, npub, t);
// Whichever of the two lines is the npub is the machine-flavoured one, and that
// is the one set in mono (DESIGN.md). The other is a name or a phrase.
identityRow.innerHTML =
avatarMarkup(session.pubkey, profile, npub, 34) +
`<span class="write-id-who">` +
`<span class="write-id-name${named ? '' : ' mono'}">${escapeHtml(primary)}</span>` +
`<span class="write-id-sub${named ? ' mono' : ''}">${escapeHtml(secondary)}</span></span>` +
methodChipHtml(session.method, t) +
`<button type="button" class="write-id-switch" data-login-open>${escapeHtml(
t('reviews.dialog.identity.switch'),
)}</button>`;
};
// Drawn from the npub first, then again if a name turns up: the row is the same
// height either way, so nothing under it moves when the name lands.
draw(null);
const profile = await resolveProfile(session.pubkey);
if (readSession()?.pubkey !== session.pubkey || !profile?.found) return;
draw(profile);
return;
}
if (!getExtension()) {
canSign = false;
syncSubmit();
identityRow.innerHTML =
`<span class="write-id-who"><span class="write-id-lead">${escapeHtml(
t('reviews.dialog.identity.needSigner'),
)}</span></span>` +
`<button type="button" class="btn" data-login-open>${escapeHtml(
t('reviews.dialog.identity.loginToPublish'),
)}</button>`;
return;
}
canSign = true;
syncSubmit();
identityRow.innerHTML =
`<span class="write-id-who"><span class="write-id-name">${escapeHtml(
t('reviews.dialog.identity.extension'),
)}</span>` +
`<span class="write-id-sub">${escapeHtml(t('reviews.dialog.identity.readingKey'))}</span></span>` +
methodChipHtml('extension', t);
let pubkey = '';
try {
pubkey = (await getExtension()?.getPublicKey()) ?? '';
} catch {
// Declined, or the extension is locked. Publishing still works: it will ask again
// at signing time, which is the request that actually matters.
}
if (!/^[0-9a-f]{64}$/i.test(pubkey)) {
const sub = identityRow.querySelector('.write-id-sub');
if (sub) sub.textContent = t('reviews.dialog.identity.willAsk');
return;
}
const npub = await toNpub(pubkey);
const profile = await resolveProfile(pubkey);
const { primary, named } = identityLines(profile, npub, t);
identityRow.innerHTML =
avatarMarkup(pubkey, profile, npub, 34) +
`<span class="write-id-who">` +
`<span class="write-id-name${named ? '' : ' mono'}">${escapeHtml(primary)}</span>` +
`<span class="write-id-sub">${escapeHtml(t('reviews.dialog.identity.extension'))}</span></span>` +
methodChipHtml('extension', t);
}
/* ---------- opening and closing ---------- */
wireModal(dialog, {
// A written review is not thrown away on a stray click outside the box.
onBackdrop: () => {
if ((textarea?.value.trim().length ?? 0) > 0 && !window.confirm(t('reviews.dialog.confirmClose'))) {
return false;
}
return options.onBackdrop ? options.onBackdrop() : true;
},
onClose: () => {
saveDraft();
showStatus('');
setBusy(false);
},
});
// The signer talks back while a signature is in flight: reconnecting, waiting,
// approve-in-another-window. Mirrored into the dialog so the reader is never watching
// a spinner with nothing behind it.
onSignerStatus((update) => {
if (dialog.open && publishing) showStatus(update.message);
});
// Logging in from the identity row redraws it, without closing this dialog.
onSessionChange(() => {
if (dialog.open) void renderIdentity();
});
textarea?.addEventListener('input', () => {
syncCount();
saveDraft();
});
form?.addEventListener('change', (event) => {
if ((event.target as HTMLInputElement).name !== 'rating') return;
syncStarLabel();
syncSubmit();
saveDraft();
});
/* ---------- publishing ---------- */
form?.addEventListener('submit', async (event) => {
event.preventDefault();
if (!form || !submit || publishing) return;
const subject = options.subject();
if (!subject) return;
const rating = chosenRating();
const content = textarea?.value ?? '';
const signer = activeSigner();
if (!signer) {
// No session and no extension. The row already says so; open the way in.
openLogin(submit);
return;
}
showError('');
setBusy(true, t('reviews.dialog.signing'));
showStatus(
signer.method === 'extension'
? t('reviews.dialog.waitingExtension')
: t('reviews.dialog.waitingSigner'),
);
try {
/*
* The author's own kind 0, fetched while the relays are still taking the review.
* Their pubkey is only knowable once the event is signed, so `onSigned` starts
* this at the earliest moment there is: by the time the card is inserted the name
* is usually already in hand, and the author does not watch their own review
* appear as "Anon" until the next reload.
*
* `refresh` skips the cache deliberately. Anyone who has reviewed before may be
* sitting on an hour-old negative entry for their own key, and they are exactly
* the reader who would notice their name missing.
*/
let profileLookup: Promise<Profile | null> | null = null;
const result = await publishReview({
signer,
subject,
rating,
content,
onSigned: (signed) => {
setBusy(true, t('reviews.dialog.publishing'));
profileLookup = loadProfiles([signed.pubkey], { refresh: true })
.then((profiles) => profiles.get(signed.pubkey) ?? null)
.catch(() => {
// Relays unreachable for the lookup. The card keeps "Anon" over its npub,
// which is what every other unresolved card shows.
return null;
});
},
onProgress: (accepted, _settled, total) => {
showStatus(t('reviews.dialog.publishingTo', { accepted, total }));
},
});
void (profileLookup as Promise<Profile | null> | null)?.then((profile) => {
if (profile?.found) options.onProfile?.(result.event.pubkey, profile);
});
clearDraft();
options.onPublished?.({
id: result.event.id,
pubkey: result.event.pubkey,
created_at: result.event.created_at,
rating,
content: content.trim(),
accepted: result.accepted,
total: result.total,
subject,
});
form.reset();
syncStarLabel();
syncCount();
setBusy(false);
showStatus('');
closeModal(dialog);
} catch (err) {
// Back to an editable dialog with everything still in it. A declined signature is
// not an error the reader made, and a relay that would not take it is worth
// another press of the same button.
setBusy(false);
showStatus('');
const message = err instanceof Error ? err.message : String(err);
showError(message === 'no-relay' ? t('reviews.dialog.error.noRelay') : signerErrorMessage(err, t));
}
});
return {
open(): void {
showError('');
showStatus('');
setBusy(false);
restoreDraft();
openModal(dialog);
void renderIdentity();
// Somewhere to land that is not the close button: the rating is the first thing
// to decide, and a checked star keeps its own place in the group.
const checked = form?.querySelector<HTMLInputElement>('input[name="rating"]:checked');
(checked ?? form?.querySelector<HTMLInputElement>('input[name="rating"]'))?.focus();
},
close(): void {
closeModal(dialog);
},
reset(): void {
form?.reset();
syncStarLabel();
syncCount();
showError('');
showStatus('');
},
refreshIdentity(): void {
void renderIdentity();
},
showError,
};
}
/* ---------- handing a fresh review to the page that shows it ---------- */
/**
* Where a review published from the review-by-URL dialog waits for its own page.
*
* The dialog lives on an index page and the review belongs on a mint's page, so
* publishing is immediately followed by a navigation. Relays take a moment to echo an
* event back, and the author landing on a page that does not yet show what they wrote
* reads as a failure — so the review travels with them, in sessionStorage, and the
* reviews panel puts it at the top on arrival exactly as it would have if the review
* had been written there.
*
* sessionStorage rather than a query parameter: the review body would not fit in a URL,
* and a review is not something to put in a link somebody might share. It is read once
* and removed, and it expires anyway — a stale entry from an hour ago must never be
* inserted into a list that has since loaded the real thing.
*/
const FRESH_PREFIX = 'cashumints:fresh:';
/** How long a handed-forward review stays valid. Long enough for one navigation. */
const FRESH_TTL_MS = 2 * 60 * 1000;
export function rememberFreshReview(published: PublishedReview): void {
try {
sessionStorage.setItem(
FRESH_PREFIX + published.subject.key,
JSON.stringify({ at: Date.now(), review: published }),
);
} catch {
// Storage blocked. The review is on the relays either way; it simply shows up on
// the next load instead of this one.
}
}
/** The review waiting for this subject, if any. Removed as it is read. */
export function takeFreshReview(key: string): PublishedReview | null {
const storageKey = FRESH_PREFIX + key;
try {
const raw = sessionStorage.getItem(storageKey);
if (!raw) return null;
sessionStorage.removeItem(storageKey);
const parsed = JSON.parse(raw) as { at?: number; review?: PublishedReview };
if (!parsed.review || typeof parsed.at !== 'number') return null;
return Date.now() - parsed.at < FRESH_TTL_MS ? parsed.review : null;
} catch {
return null;
}
}
+22
View File
@@ -0,0 +1,22 @@
---
import NotFound from '../components/NotFound.astro';
import { splitLocale } from '../i18n';
/**
* The English 404, at `/404`, and the only one a server finds without being told.
*
* It sits outside `[...locale]/` on purpose. Astro recognises a custom 404 by route
* name — the route has to be exactly `/404` — and `[...locale]/404` is not that, so
* with only the prefixed file the dev server fell back to Astro's own default page
* for every miss, including `/mint/{host}` misses that `pnpm bones` measures. A
* static host has the same blind spot: nginx's `error_page` names `/404.html`.
*
* The locale comes off the URL rather than being fixed to English because the dev
* server renders this route with the pathname that missed. Prerendered, that pathname
* is `/404` and this is English; in dev, a miss under `/es/` renders in Spanish, which
* is what nginx's per-prefix `try_files` does in production.
*/
const { locale } = splitLocale(Astro.url.pathname);
---
<NotFound locale={locale} />
+16 -208
View File
@@ -1,221 +1,29 @@
--- ---
import Base from '../../layouts/Base.astro'; import NotFound from '../../components/NotFound.astro';
import Moai from '../../components/Moai.astro'; import { DEFAULT_LOCALE, LOCALES, type Locale } from '../../i18n';
import { localePath, useI18n, type Locale } from '../../i18n';
import { localePaths } from '../../i18n/paths';
/** /**
* Also the fallback for a mint discovered between builds. * The prefixed 404s: `/es/404`, `/nl/404`, one per language.
* *
* A new mint's page does not exist until the next rebuild, so on a /mint/{host} miss * English is deliberately absent here — `pages/404.astro` emits it at `/404`, where
* this asks the API for that host and renders a page shell from it. Not as complete * Astro and nginx both expect to find it, and two routes emitting the same path is a
* as the prerendered page, but the mint is reachable and reviewable immediately * collision. Everything the page does lives in the component both files render.
* instead of invisible for a day (FRONTEND.md).
* *
* Emitted per locale: `/404`, `/es/404`, `/nl/404`. A static host serves the one at the * A static host serves the root 404 for every miss unless it is told otherwise, so the
* root for every miss unless it is told otherwise, so the README's nginx block adds an * README's nginx block adds a `try_files` fallback per prefix. That is what keeps a
* `error_page` per prefix. That is what keeps a miss under /es/ from answering in * miss under /es/ answering in Spanish.
* English.
*/ */
export const getStaticPaths = localePaths; export function getStaticPaths() {
return LOCALES.filter(({ code }) => code !== DEFAULT_LOCALE).map(({ code }) => ({
params: { locale: code },
props: { locale: code as Locale },
}));
}
interface Props { interface Props {
locale: Locale; locale: Locale;
} }
const { locale } = Astro.props; const { locale } = Astro.props;
const t = useI18n(locale);
--- ---
<Base <NotFound locale={locale} />
title={t('notfound.title')}
description={t('notfound.description')}
current="mints"
offGraph
>
<div class="page">
<div class="notfound" data-notfound>
{/* 44px, not a hero graphic: DESIGN.md says the moai is never scaled large. */}
<span class="nf-moai"><Moai size={44} /></span>
<h1>{t('notfound.heading')}</h1>
<p class="lede">{t('notfound.lede')}</p>
<form class="search nf-search" action={localePath('/mints', locale)} method="get" role="search">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<circle cx="11" cy="11" r="7" /><path d="m20 20-3.5-3.5" />
</svg>
<label class="sr-only" for="nf-search">{t('home.search.label')}</label>
<input
id="nf-search"
name="q"
type="text"
autocomplete="off"
placeholder={t('home.search.placeholder')}
data-search-input
/>
<kbd aria-hidden="true">/</kbd>
</form>
<div class="nf-actions">
<a class="btn primary" href={localePath('/mints', locale)}>{t('notfound.browse')}</a>
<a class="btn" href={localePath('/', locale)}>{t('notfound.home')}</a>
</div>
</div>
{/*
Filled in by the fallback below when the URL looks like a mint page. Nothing
here is prerendered, so this is the one place on the site that gets a page
level skeleton. `data-boneyard` marks the capture target for `pnpm bones`.
*/}
<div class="mint-fallback" data-mint-fallback hidden>
<div data-boneyard="mint-page" data-boneyard-config='{"excludeSelectors":[".sr-only"]}'><div data-mint-fallback-body></div></div>
</div>
</div>
</Base>
<style>
.notfound { padding: 54px 0 20px; max-width: 620px; }
.nf-moai { display: block; margin-bottom: 18px; }
.notfound h1 { font-family: var(--display); font-size: 34px; font-weight: 700; letter-spacing: -.02em; }
.nf-actions { display: flex; gap: 12px; margin-top: 22px; flex-wrap: wrap; }
/* Same search control as the home page hero, so a miss lands somewhere useful. */
.nf-search {
display: flex; align-items: center; gap: 12px; margin-top: 24px;
background: var(--card); border: 1px solid var(--line);
border-radius: 14px; padding: 14px 16px;
transition: border-color .15s, box-shadow .15s;
}
.nf-search:focus-within { border-color: var(--amber-dim); box-shadow: 0 0 0 3px rgba(240, 169, 59, .08); }
.nf-search > svg { color: var(--faint); flex-shrink: 0; }
.nf-search input {
flex: 1; background: none; border: none; outline: none; min-width: 0;
color: var(--text); font-family: var(--body); font-size: 15.5px;
}
.nf-search input::placeholder { color: var(--faint); }
.nf-search kbd {
font-family: var(--mono); font-size: 11.5px; color: var(--faint);
border: 1px solid var(--line); border-radius: 6px; padding: 2px 7px; flex-shrink: 0;
}
/* The .fb-* rules live in global.css: this page builds that markup with
innerHTML, and Astro's scoped selectors do not reach injected elements. */
.mint-fallback { padding: 20px 0 40px; }
</style>
<script>
import { apiBase, escapeHtml, iconGradient } from '../../lib/client';
import { armBuildSnapshot, clearBones, isBuildMode, showBones, swapBones } from '../../lib/skeleton';
import { useI18n } from '../../i18n/client';
import { formatters, starString, type Formatters } from '../../i18n/format';
import { statusLabel } from '../../i18n/mint';
import { splitLocale } from '../../i18n/routing';
import { onReady } from '../../scripts/reveal';
import mintPageBones from '../../bones/mint-page.bones.json';
import type { MintDetail } from '@cashumints/shared';
/**
* The summary this page can build from the API alone. `pnpm bones` measures the
* output of this same function, so the bones are the shape of what actually
* arrives rather than of a fuller page that never does.
*/
function fallbackHtml(mint: MintDetail, f: Formatters): string {
const domain = mint.url.replace(/^https?:\/\//, '');
const name = mint.name ?? domain.split('/')[0] ?? domain;
return `
<div class="fb-head">
<span class="mc-icon" style="background:${iconGradient(domain)};width:52px;height:52px;border-radius:13px;font-size:21px">${escapeHtml(
(name[0] ?? '?').toUpperCase(),
)}</span>
<span>
<span class="fb-name">${escapeHtml(name)}</span>
<span class="fb-domain">${escapeHtml(mint.url)}</span>
</span>
<span class="status-chip ${escapeHtml(mint.status)}"><span class="status-dot"></span>${escapeHtml(
statusLabel(mint.status, f.t),
)}</span>
</div>
<div class="fb-stats">
<span class="fb-stat"><span class="k">${escapeHtml(
f.t('notfound.fb.rating'),
)}</span><span class="v">${
mint.rating_avg === null ? f.t('time.none') : f.decimal(mint.rating_avg)
} <span class="stars" style="font-size:14px">${starString(mint.rating_avg)}</span></span></span>
<span class="fb-stat"><span class="k">${escapeHtml(
f.t('notfound.fb.reviews'),
)}</span><span class="v">${f.number(mint.review_count)}</span></span>
<span class="fb-stat"><span class="k">${escapeHtml(
f.t('notfound.fb.software'),
)}</span><span class="v" style="font-family:var(--mono);font-size:16px">${escapeHtml(
mint.version ?? f.t('mint.software.unknown'),
)}</span></span>
<span class="fb-stat"><span class="k">${escapeHtml(
f.t('notfound.fb.lastOnline'),
)}</span><span class="v" style="font-size:16px">${escapeHtml(
f.relative(mint.last_online),
)}</span></span>
</div>
<p class="fb-note">${escapeHtml(f.t('notfound.fb.note'))}</p>`;
}
const setup = (): void => {
const f = formatters(useI18n());
/*
* The miss could be at /mint/x or at /es/mint/x, and both resolve to the same mint.
* The prefix comes off with the same helper the build routes with, so the two can
* never disagree about what counts as a locale segment.
*/
const { path } = splitLocale(window.location.pathname);
const match = /^\/mint\/([^/]+)\/?$/.exec(path);
const box = document.querySelector<HTMLElement>('[data-mint-fallback]');
const body = document.querySelector<HTMLElement>('[data-mint-fallback-body]');
const notfound = document.querySelector<HTMLElement>('[data-notfound]');
if (body && box && isBuildMode()) {
// `pnpm bones` is measuring this page. Show a representative mint so the
// capture has a real summary to measure instead of an empty box.
// Imported dynamically so the fixture never reaches a real visitor.
void armBuildSnapshot()
.then(() => import('../../lib/skeleton-fixtures'))
.then(({ FIXTURE_MINT }) => {
if (notfound) notfound.hidden = true;
box.hidden = false;
body.innerHTML = fallbackHtml(FIXTURE_MINT, f);
});
} else if (match?.[1] && box && body) {
const host = decodeURIComponent(match[1]);
// The "page not found" copy would be wrong if the API knows this mint, so hold
// it back while we ask instead of flashing it and taking it away again.
if (notfound) notfound.hidden = true;
box.hidden = false;
showBones(body, mintPageBones, { label: f.t('notfound.loading', { host }) });
void (async () => {
try {
const res = await fetch(`${apiBase}/api/mints/${encodeURIComponent(host)}`);
if (!res.ok) throw new Error(`api responded ${res.status}`);
const mint = (await res.json()) as MintDetail;
swapBones(body, (target) => {
target.innerHTML = fallbackHtml(mint, f);
});
const domain = mint.url.replace(/^https?:\/\//, '');
const name = mint.name ?? domain.split('/')[0] ?? domain;
document.title = f.t('mint.title', { name });
} catch {
// No such mint, or the API is unreachable. The plain 404 is the honest
// answer, so put it back rather than leaving bones on screen.
clearBones(body);
box.hidden = true;
if (notfound) notfound.hidden = false;
}
})();
}
};
onReady(setup);
</script>
+21 -3
View File
@@ -14,6 +14,7 @@
*/ */
import Base from '../../layouts/Base.astro'; import Base from '../../layouts/Base.astro';
import MintCard from '../../components/MintCard.astro'; import MintCard from '../../components/MintCard.astro';
import ReviewByUrl from '../../components/ReviewByUrl.astro';
import { fetchFedimints } from '../../lib/api'; import { fetchFedimints } from '../../lib/api';
import { localePath, useI18n, type Locale } from '../../i18n'; import { localePath, useI18n, type Locale } from '../../i18n';
import { itemListNode } from '../../lib/schema'; import { itemListNode } from '../../lib/schema';
@@ -66,8 +67,18 @@ const schema = [
<Base title={t('fedimints.title')} description={description} current="fedimints" schema={schema}> <Base title={t('fedimints.title')} description={description} current="fedimints" schema={schema}>
<div class="page"> <div class="page">
<div class="index-head"> <div class="index-head">
<h1>{t('fedimints.heading')}</h1> <div class="index-head-text">
<p class="lede">{t('fedimints.lede')}</p> <h1>{t('fedimints.heading')}</h1>
<p class="lede">{t('fedimints.lede')}</p>
</div>
{/*
The way in for something this site has never seen. Every other route to writing
a review starts from a page that already exists, which is exactly the case a
reader with a brand new mint does not have.
*/}
<button class="btn primary index-head-action" type="button" data-review-url-open="fedimint">
{t('reviews.byUrl.open')}
</button>
</div> </div>
<div class="controls" data-mint-controls> <div class="controls" data-mint-controls>
@@ -142,10 +153,17 @@ const schema = [
) )
} }
</div> </div>
{/* Two steps in one dialog: identify the mint, then the ordinary review form. */}
<ReviewByUrl type="fedimint" />
</Base> </Base>
<style> <style>
.index-head { margin-bottom: 24px; } /* Heading and lede on the left, the one primary action on the right, wrapping to
its own line before the lede has to give up any width. */
.index-head { margin-bottom: 24px; display: flex; align-items: flex-start; gap: 16px; flex-wrap: wrap; }
.index-head-text { flex: 1 1 320px; }
.index-head-action { flex-shrink: 0; }
.index-head h1 { font-family: var(--display); font-size: 32px; font-weight: 700; letter-spacing: -.02em; line-height: 1.1; } .index-head h1 { font-family: var(--display); font-size: 32px; font-weight: 700; letter-spacing: -.02em; line-height: 1.1; }
.index-head .lede { max-width: 620px; font-size: 15.5px; margin-top: 10px; } .index-head .lede { max-width: 620px; font-size: 15.5px; margin-top: 10px; }
+59 -6
View File
@@ -3,7 +3,10 @@ import Base from '../../layouts/Base.astro';
import ColorBends from '../../components/ColorBends.astro'; import ColorBends from '../../components/ColorBends.astro';
import MintCard from '../../components/MintCard.astro'; import MintCard from '../../components/MintCard.astro';
import PulseTicker from '../../components/PulseTicker.astro'; import PulseTicker from '../../components/PulseTicker.astro';
import { fetchFedimint, fetchFedimints, fetchHealth, fetchMint, fetchMints, fetchStats } from '../../lib/api'; import {
fetchFedimint, fetchFedimints, fetchHealth, fetchLnurlMint, fetchLnurlMints, fetchMint,
fetchMints, fetchStats,
} from '../../lib/api';
import { fetchLatestReviews } from '../../lib/nostr-build'; import { fetchLatestReviews } from '../../lib/nostr-build';
import { feedTargets, targetPath } from '../../lib/feed-resolve'; import { feedTargets, targetPath } from '../../lib/feed-resolve';
import { displayDomain, iconGradient, sentiment, shortNpub } from '../../lib/format'; import { displayDomain, iconGradient, sentiment, shortNpub } from '../../lib/format';
@@ -23,23 +26,27 @@ const { locale } = Astro.props;
const t = useI18n(locale); const t = useI18n(locale);
const f = formatters(t); const f = formatters(t);
const [mints, federations, stats, health] = await Promise.all([ const [mints, federations, lnurlMints, stats, health] = await Promise.all([
fetchMints(), fetchMints(),
fetchFedimints(), fetchFedimints(),
fetchLnurlMints(),
fetchStats(), fetchStats(),
fetchHealth(), fetchHealth(),
]); ]);
const top = mints.slice(0, 6); const top = mints.slice(0, 6);
const topFederations = federations.slice(0, 6); const topFederations = federations.slice(0, 6);
const topLnurl = lnurlMints.slice(0, 6);
/* /*
* The reviews strip resolves against both ecosystems, because its heading is "latest * The reviews strip resolves against every ecosystem, because its heading is "latest
* reviews" and not "latest mint reviews". Federations rarely reach it in practice — the * reviews" and not "latest mint reviews". Federations rarely reach it in practice — the
* junk filter wants a written body and nearly every 38173 review is a bare `[5/5]` — * junk filter wants a written body and nearly every 38173 review is a bare `[5/5]` —
* but when one does, it is genuinely among the latest and it links to its own page. * but when one does, it is genuinely among the latest and it links to its own page. The
* same is true of an LNURL mint, which today has the further handicap that 38174 is a
* proposed kind with barely any events on the network at all.
*/ */
const reviewTargets = feedTargets(mints, federations); const reviewTargets = feedTargets(mints, federations, lnurlMints);
/* /*
* The mint details and the relay read have nothing to say to each other — the reviews * The mint details and the relay read have nothing to say to each other — the reviews
@@ -50,10 +57,11 @@ const reviewTargets = feedTargets(mints, federations);
* few enough that they are all real, recent, written reviews. Anything the relays * few enough that they are all real, recent, written reviews. Anything the relays
* cannot fill is simply fewer cards, and the carousel's controls disappear with them. * cannot fill is simply fewer cards, and the carousel's controls disappear with them.
*/ */
const [details, fediDetails, latest] = await Promise.all([ const [details, fediDetails, lnurlDetails, latest] = await Promise.all([
// Distributions give the card sentiment bars real data instead of a rating proxy. // Distributions give the card sentiment bars real data instead of a rating proxy.
Promise.all(top.map((m) => fetchMint(m.host).catch(() => null))), Promise.all(top.map((m) => fetchMint(m.host).catch(() => null))),
Promise.all(topFederations.map((federation) => fetchFedimint(federation.host).catch(() => null))), Promise.all(topFederations.map((federation) => fetchFedimint(federation.host).catch(() => null))),
Promise.all(topLnurl.map((mint) => fetchLnurlMint(mint.host).catch(() => null))),
fetchLatestReviews(reviewTargets, 10), fetchLatestReviews(reviewTargets, 10),
]); ]);
@@ -63,6 +71,20 @@ const capabilities = details.map((d) => (d ? readCapabilities(d.info?.nuts) : nu
// No capability read for federations: they publish no NUT switches to draw a chip from. // No capability read for federations: they publish no NUT switches to draw a chip from.
const fediSentiments = fediDetails.map((d) => (d ? sentiment(d.rating_distribution) : undefined)); const fediSentiments = fediDetails.map((d) => (d ? sentiment(d.rating_distribution) : undefined));
const lnurlSentiments = lnurlDetails.map((d) => (d ? sentiment(d.rating_distribution) : undefined));
/*
* An LNURL card's chip comes from two probed facts rather than from NUT switches: an
* advertised withdraw ceiling of zero, and a Lightning node the mint cannot reach.
*/
const lnurlChips = lnurlDetails.map((d) =>
d
? {
max_withdrawable_msat: d.max_withdrawable_msat ?? null,
funding_available: d.funding_available ?? null,
}
: null,
);
const description = t('home.description', { const description = t('home.description', {
mints: stats.mints_total, mints: stats.mints_total,
reviews: stats.reviews_total, reviews: stats.reviews_total,
@@ -191,6 +213,37 @@ const signedBody = t('home.why.signed.body', {
) )
} }
{/*
The LNURL grid: the third ecosystem strip, in the same order the nav lists them.
Same ranked cards as /lnurl-mints, same real sentiment bars, and the same chip rule
— so a reader moving down the page meets one card three times rather than three
cards once each.
*/}
{
topLnurl.length > 0 && (
<section class="section" aria-labelledby="top-lnurl">
<div class="sec-head" data-reveal>
<h2 class="sec-title" id="top-lnurl">{t('home.topLnurl.title')}</h2>
<span class="sec-sub">{t('home.topLnurl.sub')}</span>
<a class="sec-link" href={localePath('/lnurl-mints', locale)}>
{t('home.topLnurl.all', { n: stats.lnurl_total })}
</a>
</div>
<div class="mint-grid">
{topLnurl.map((mint, i) => (
<MintCard
mint={mint}
rank={i + 1}
sentiment={lnurlSentiments[i]}
lnurl={lnurlChips[i]}
revealDelay={i * 50}
/>
))}
</div>
</section>
)
}
<section class="section" aria-labelledby="latest-reviews-title" id="latest-reviews"> <section class="section" aria-labelledby="latest-reviews-title" id="latest-reviews">
<div class="sec-head" data-reveal> <div class="sec-head" data-reveal>
<h2 class="sec-title" id="latest-reviews-title">{t('home.latest.title')}</h2> <h2 class="sec-title" id="latest-reviews-title">{t('home.latest.title')}</h2>
@@ -0,0 +1,695 @@
---
/**
* One LNURL mint. The mint page's layout, with the panels an lnurlcash mint can
* honestly fill.
*
* What is the same: the identity header, the warning banner slot, the four-cell verdict
* strip, the reviews panel (identical component, identical cards, identical write
* dialog), the related ring and the disclaimer.
*
* What is different, and why:
*
* - The NUTs panel becomes a Features panel. Same rows, but three states rather than
* two: a mint can publish `mint` and `melt` and still be unable to perform either
* right now, because the Lightning node behind it is unreachable. That is checkable
* over HTTP — see NOTES-LNURL.md — so it is shown rather than papered over with a
* tick.
* - The limits cell shows *withdraw* bounds, not NUT-04 mint limits. They answer the
* question a reader of this page actually has: how much can one of this mint's
* bearer notes be worth?
* - The sidebar gains the three things an LNURL mint publishes that no other ecosystem
* does: a lightning address, a mint pubkey, and a Tor address.
*
* Everything the reviews panel needs comes from `lnurlSubject`, so nothing in the panel,
* the card, the publisher or the dialog is aware this page exists.
*/
import Base from '../../../layouts/Base.astro';
import Reviews from '../../../components/Reviews.astro';
import MintLive from '../../../components/MintLive.astro';
import Sparkline from '../../../components/Sparkline.astro';
import CopyableId from '../../../components/CopyableId.astro';
import MintCard from '../../../components/MintCard.astro';
import {
displayFeatures, getMintWarnings, isTestNetwork, msatToSat, otherFeatures,
type LnurlDetail, type MintListItem,
} from '@cashumints/shared';
import { fetchLnurlMint, fetchLnurlMints, iconUrl } from '../../../lib/api';
import { ogImageFor } from '../../../lib/og';
import { lnurlSubject } from '../../../lib/review-subject';
import { breadcrumbNode, mintServiceNode } from '../../../lib/schema';
import { clampWords, cleanMintDescription, isIndexableMint, nameCollisions, seoName } from '../../../lib/seo';
import { PRODUCT_JSONLD } from '../../../lib/site';
import { bannerHtml } from '../../../lib/mint-state';
import { feeText, featureRowsHtml, localFeatureName, withdrawLimitsHtml } from '../../../lib/lnurl-ui';
import { localNetworkName } from '../../../lib/fedimint-ui';
import { displayDomain, iconGradient, initials, transitionName, truncateMiddle } from '../../../lib/format';
import { localePath, useI18n } from '../../../i18n';
import { formatters, starString } from '../../../i18n/format';
import { statusLabel, warningOptions } from '../../../i18n/mint';
import { localePathsFor } from '../../../i18n/paths';
/** Every LNURL mint, in every locale. One API call per mint, shared across the locales. */
export async function getStaticPaths() {
const mints = await fetchLnurlMints();
const details = await Promise.all(
mints.map(async (mint, index) => {
try {
return { detail: await fetchLnurlMint(mint.host), rank: index + 1 };
} catch {
return null;
}
}),
);
const collisions = nameCollisions(mints);
/*
* The same related ring the other two ecosystems use: position i links to i+1, i+2
* and i+3, wrapping. Deterministic, never itself, and drawn only from mints whose own
* pages are indexable — a page nobody should index needs no crawl paths out of it.
*/
const ring = mints.filter(isIndexableMint);
const ringIndex = new Map(ring.map((mint, index) => [mint.host, index]));
const related = (host: string): MintListItem[] => {
const at = ringIndex.get(host);
if (at === undefined || ring.length < 2) return [];
return [1, 2, 3]
.filter((step) => step < ring.length)
.map((step) => ring[(at + step) % ring.length]!);
};
return localePathsFor(
details
.filter((d): d is { detail: LnurlDetail; rank: number } => d !== null)
.map(({ detail, rank }) => ({
params: { host: detail.host },
props: {
mint: detail,
rank,
related: related(detail.host),
seoName: seoName(detail.name, detail.host, collisions),
},
})),
);
}
const { mint, rank, related, seoName: titleName, locale } = Astro.props;
const t = useI18n(locale);
const f = formatters(t);
/*
* The row key carries an `lnurl:` scheme in front of the URL, so everything a reader
* sees comes from `base_url` instead. A page printing `lnurl:https://…` at somebody
* would be showing them a database key.
*/
const baseUrl = mint.base_url ?? mint.url.replace(/^lnurl:/, '');
const domain = displayDomain(baseUrl);
const name = mint.name ?? domain.split('/')[0] ?? domain;
const icon = iconUrl(mint.icon);
const status = statusLabel(mint.status, t);
const offline = mint.status === 'offline';
/*
* What the operator announced, plus what the probe actually saw.
*
* A union, because the two are different kinds of evidence and both are worth showing.
* The announcement is richer — only an operator can tell you their notes rotate — but
* `kind:38174` is a proposed kind, so today most LNURL mints have no announcement at
* all and the probe is the only list there is. The panel's own note explains where the
* rows came from; `LnurlFields.features` still holds the announcement verbatim.
*/
const features = displayFeatures(mint.features, mint.observed_features);
const extraFeatures = otherFeatures(features);
const network = mint.network ?? null;
const fee = feeText(mint.fee_base_msat, mint.fee_ppm, f);
/*
* The software string, from `GET /openapi.json` — the only place an LNURL mint's version
* is discoverable at all. No header carries it, and the one-pager's title is operator
* configurable, so it identifies nothing. See NOTES-LNURL.md §6.
*/
const software = mint.version;
const softwareName = software?.split('/')[0] ?? null;
/* verdict: rating. Identical to the other two pages', because a rating means the same. */
const dist = mint.rating_distribution;
const distMax = Math.max(1, ...Object.values(dist));
const ratedCount = Object.values(dist).reduce((a, b) => a + b, 0);
const unrated = mint.review_count - ratedCount;
/*
* The banner.
*
* `getMintWarnings` takes the LNURL branch on `type`, so this page can receive the three
* offline tiers — identical thresholds and identical words to a Cashu mint's — plus the
* two states only this ecosystem has: a zero withdraw ceiling, and a Lightning node the
* mint cannot reach. Nothing here is derived from the `features` tag, which is a claim
* rather than an observation.
*/
const warning = getMintWarnings(mint, warningOptions(f))[0] ?? null;
/* SEO. Same templates as a mint page, with the Cashu limits clause swapped for withdraw. */
const hasRating = mint.review_count > 0 && mint.rating_avg !== null;
const offlineDays =
offline && mint.last_online !== null
? Math.max(0, Math.floor(Date.now() / 1000 / 86400 - mint.last_online / 86400))
: null;
const title =
offlineDays !== null
? t('seo.mint.titleOffline', { name: titleName, n: offlineDays })
: hasRating
? t('seo.mint.title', {
name: titleName,
rating: f.decimal(mint.rating_avg!),
n: mint.review_count,
})
: t('seo.lnurl.titleNoReviews', { name: titleName });
const lead =
cleanMintDescription(mint.description) ?? t('seo.desc.fallbackDomain', { domain });
const ratingPart = hasRating
? t('seo.desc.rated', { rating: f.decimal(mint.rating_avg!), n: mint.review_count })
: t('seo.desc.noReviews');
/*
* The third sentence. A warning's own meta line replaces the plain status, because the
* state is the one thing a search result must not soften.
*
* Otherwise it is the withdraw range, which is this ecosystem's answer to the Cashu
* description's mint limits: the same shape of sentence about the number that actually
* matters here.
*/
const minSat = msatToSat(mint.min_withdrawable_msat ?? null);
const maxSat = msatToSat(mint.max_withdrawable_msat ?? null);
const statusPart = warning
? `${warning.meta}.`
: minSat !== null && maxSat !== null
? t('seo.desc.statusWithdraw', { status, min: f.number(minSat), max: f.number(maxSat) })
: t('seo.desc.status', { status });
const description = clampWords(`${lead} ${ratingPart} ${statusPart}`, 158);
/* The card: one English render shared by all locales, hashed filename per state. */
const ogImage = ogImageFor(mint.host);
const imageAlt = hasRating
? t('seo.mint.ogAlt', { name: titleName, rating: f.decimal(mint.rating_avg!), n: mint.review_count })
: t('seo.mint.ogAltNoReviews', { name: titleName });
const indexable = isIndexableMint(mint);
const crumbs = [
{ name: t('nav.home'), url: new URL(localePath('/', locale), Astro.site).href },
{ name: t('lnurl.crumb'), url: new URL(localePath('/lnurl-mints', locale), Astro.site).href },
{ name: domain },
];
const canonical = new URL(localePath(`/lnurl-mint/${mint.host}`, locale), Astro.site).href;
const schema = [
breadcrumbNode(canonical, crumbs),
mintServiceNode({
canonical,
name: titleName,
description,
product: PRODUCT_JSONLD,
mintUrl: baseUrl,
icon: icon ? new URL(icon, Astro.site).href : null,
rating:
ratedCount > 0 && mint.rating_avg !== null
? {
value: Number(mint.rating_avg.toFixed(1)),
ratingCount: ratedCount,
reviewCount: mint.review_count,
}
: null,
}),
];
---
<Base
title={title}
description={description}
current="lnurl-mints"
noindex={!indexable}
schema={schema}
image={ogImage ?? undefined}
imageAlt={imageAlt}
>
<div class="page">
<nav class="crumbs" aria-label={t('lnurl.breadcrumb')}>
<a href={localePath('/', locale)}>{t('nav.home')}</a>
<span class="sep" aria-hidden="true">/</span>
<a href={localePath('/lnurl-mints', locale)}>{t('lnurl.crumb')}</a>
<span class="sep" aria-hidden="true">/</span>
<span class="here mono">{domain}</span>
</nav>
<div class="mint-head">
{
icon ? (
<img
class="mint-icon"
src={icon}
alt=""
width="72"
height="72"
loading="eager"
decoding="async"
fetchpriority="high"
style={`view-transition-name:${transitionName('icon', mint.host)}`}
/>
) : (
<span
class="mint-icon"
style={`background:${iconGradient(domain)};view-transition-name:${transitionName('icon', mint.host)}`}
aria-hidden="true"
>
{initials(name)}
</span>
)
}
<div class="mint-id">
<div class="mint-name-row">
<h1 style={`view-transition-name:${transitionName('name', mint.host)}`}>{name}</h1>
<span class:list={['status-chip', mint.status]} data-live-status>
<span class="status-dot"></span><span data-live-status-label>{status}</span>
</span>
</div>
<div class="mint-domain">
<span class="mono">{baseUrl}</span>
<button class="copy-btn" data-copy={baseUrl} aria-label={t('mint.copyUrl')} type="button">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<rect x="9" y="9" width="12" height="12" rx="2" /><path d="M5 15V5a2 2 0 0 1 2-2h10" />
</svg>
</button>
</div>
<div class="mint-tags">
<span class="tag">{t('mint.rankByScore', { rank: String(rank) })}</span>
{
/*
Not mainnet is called out rather than stated flatly, exactly as it is on a
federation page: a signet mint's notes are worth nothing, and someone
scanning for the real one needs that to be loud.
*/
network && (
<span class:list={['tag', { testnet: isTestNetwork(network) }]}>
{localNetworkName(network, f)}
</span>
)
}
{softwareName && <span class="tag mono">{softwareName}</span>}
</div>
</div>
<div class="head-actions">
{
mint.lightning_address && (
<button class="btn" data-copy={mint.lightning_address} type="button">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M13 2 4 14h7l-1 8 9-12h-7l1-8Z" />
</svg>
{t('lnurl.copyAddress')}
</button>
)
}
<button class="btn" data-copy={baseUrl} type="button">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<rect x="9" y="9" width="12" height="12" rx="2" /><path d="M5 15V5a2 2 0 0 1 2-2h10" />
</svg>
{t('mint.copyUrl')}
</button>
<button class="btn primary" data-write-open type="button">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M12 20h9" /><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z" />
</svg>
{t('mint.writeReview')}
</button>
</div>
</div>
<Fragment set:html={bannerHtml(warning)} />
<section class="verdict" aria-labelledby="lnurl-summary">
<h2 class="sr-only" id="lnurl-summary">{t('mint.summary')}</h2>
<div class="v-cell rating-cell" data-reveal data-reveal-delay="0">
<div class="v-label">{t('mint.rating.label')}</div>
<div class="rating-body">
<div class="rating-figure">
{
mint.rating_avg === null ? (
<>
<div class="v-big">{t('time.none')}</div>
<div class="v-sub">{t('mint.rating.none')}</div>
</>
) : (
<>
<div class="v-big" data-count-up={mint.rating_avg.toFixed(1)} data-count-decimals="1">
{f.decimal(mint.rating_avg)}
</div>
<div class="stars" aria-hidden="true">{starString(mint.rating_avg)}</div>
<div class="v-sub">
{t('mint.rating.reviews', { n: mint.review_count })}
{unrated > 0 && <><br />{t('mint.rating.unrated', { n: unrated })}</>}
</div>
</>
)
}
</div>
<div class="dist" aria-hidden="true">
{
([5, 4, 3, 2, 1] as const).map((n) => {
const count = dist[String(n) as '1' | '2' | '3' | '4' | '5'];
return (
<div class="dist-row">
<span class="n">{n}</span>
<span class="dist-bar">
<span
class:list={['dist-fill', { low: n === 1 }]}
style={`width:${Math.round((count / distMax) * 100)}%`}
/>
</span>
<span class="c">{f.number(count)}</span>
</div>
);
})
}
</div>
</div>
</div>
<div class="v-cell" data-reveal data-reveal-delay="60">
<div class="v-label">{t('mint.activity.label')}</div>
<div class="v-big">{f.shortDuration(mint.last_review_at)}</div>
<div class="v-sub">
{mint.last_review_at ? t('mint.activity.since') : t('mint.activity.noReviews')}
{mint.reviews_90d > 0 && (
<><br />{t('mint.activity.recent', { n: mint.reviews_90d })}</>
)}
</div>
<Sparkline probes={mint.probes_recent} uptime={mint.uptime_30d} />
</div>
<div class="v-cell" data-reveal data-reveal-delay="120">
<div class="v-label">{t('lnurl.limits.label')}</div>
{/*
Withdraw bounds, not mint limits: this is what one of this mint's notes can be
worth. A ceiling of zero is struck out rather than printed as "0 sat", because
it is the disabling value and the banner above is already explaining it.
*/}
<div data-live-limits set:html={withdrawLimitsHtml(mint.min_withdrawable_msat, mint.max_withdrawable_msat, f)} />
</div>
<div class="v-cell" data-reveal data-reveal-delay="180">
<div class="v-label">{software ? t('mint.software.label') : t('lnurl.confirmed.label')}</div>
<div class="v-mono">
{/*
A version when one was discoverable, and the last confirmation when not.
An LNURL mint's version comes only from `/openapi.json`, which an operator
can turn off, so "unknown" here is common enough that a cell reading it twice
would be furniture — the fallback says something true instead.
*/}
{software && <><b>{software.replace('/', ' ')}</b><br /></>}
<span data-live-checked>
{
offline
? mint.last_online
? t('mint.lastSeen', { when: f.relative(mint.last_online) })
: t('mint.neverReached')
: software
? t('mint.checked', { when: f.relative(mint.last_probe) })
: t('lnurl.confirmed.when', { when: f.relative(mint.last_probe) })
}
</span>
</div>
</div>
</section>
<div class="cols">
<Reviews
subject={lnurlSubject(mint)}
reviewCount={mint.review_count}
distribution={mint.rating_distribution}
name={name}
/>
<aside class="side">
<div class="panel" data-reveal>
<h2 class="side-title">{t('lnurl.features.title')}</h2>
{
features.length === 0 ? (
<p class="side-empty">{t('lnurl.features.none')}</p>
) : (
<>
<div class="nut-hi" data-live-features set:html={featureRowsHtml(features, mint.funding_available ?? null, f)} />
{extraFeatures.length > 0 && (
<div class="nut-rest">
{extraFeatures.map((feature) => (
<span class="nut-chip" title={localFeatureName(feature, f)}>{feature}</span>
))}
</div>
)}
<p class="nut-more">{t('lnurl.features.note')}</p>
</>
)
}
</div>
<div class="panel" data-reveal>
<h2 class="side-title">{t('lnurl.details.title')}</h2>
<div class="kv">
{
mint.lightning_address && (
<div class="kv-row">
<span class="kv-k">{t('lnurl.details.address')}</span>
<CopyableId
class="kv-v"
value={mint.lightning_address}
label={mint.lightning_address}
kind={t('lnurl.kind.lightningAddress')}
/>
</div>
)
}
{
mint.mint_pubkey && (
<div class="kv-row">
<span class="kv-k">{t('lnurl.details.mintPubkey')}</span>
<CopyableId
class="kv-v"
value={mint.mint_pubkey}
label={truncateMiddle(mint.mint_pubkey, 10, 6)}
kind={t('lnurl.kind.mintPubkey')}
/>
</div>
)
}
{
mint.onion_url && (
<div class="kv-row">
<span class="kv-k">{t('lnurl.details.onion')}</span>
<CopyableId
class="kv-v"
value={mint.onion_url}
label={truncateMiddle(mint.onion_url, 14, 12)}
kind={t('lnurl.kind.onionAddress')}
/>
</div>
)
}
{
fee && (
<div class="kv-row">
<span class="kv-k">{t('lnurl.details.fee')}</span>
<span class="kv-v">{fee}</span>
</div>
)
}
{
network && (
<div class="kv-row">
<span class="kv-k">{t('fedimint.details.network')}</span>
<span class="kv-v">{localNetworkName(network, f)}</span>
</div>
)
}
{
/*
The funding node, as the mint chooses to describe it. `nodeCapacity` is
explicitly the *publicly announced* channel capacity — lnurl-mint reads
it from the public graph, never from a private `listfunds` view — so
printing it discloses nothing the node's own gossip does not.
*/
mint.node_alias && (
<div class="kv-row">
<span class="kv-k">{t('lnurl.details.node')}</span>
<span class="kv-v">{mint.node_alias}</span>
</div>
)
}
{
mint.node_capacity_msat !== null && mint.node_capacity_msat !== undefined && (
<div class="kv-row">
<span class="kv-k">{t('lnurl.details.capacity')}</span>
<span class="kv-v">
{t('lnurl.limits.sats', { n: msatToSat(mint.node_capacity_msat) ?? 0 })}
</span>
</div>
)
}
{
mint.announcer_pubkey && (
<div class="kv-row">
<span class="kv-k">{t('fedimint.details.announcedBy')}</span>
<CopyableId
class="kv-v"
value={mint.announcer_pubkey}
label={truncateMiddle(mint.announcer_pubkey, 10, 6)}
kind={t('mint.kind.mintPubkey')}
/>
</div>
)
}
<div class="kv-row">
<span class="kv-k">{t('mint.details.firstSeen')}</span>
<span class="kv-v">{f.monthYear(mint.first_seen)}</span>
</div>
</div>
</div>
</aside>
</div>
{
related.length > 0 && (
<section class="more-mints" aria-labelledby="more-lnurl">
<div class="sec-head" data-reveal>
<h2 class="sec-title" id="more-lnurl">{t('lnurl.more.title')}</h2>
<a class="sec-link" href={localePath('/lnurl-mints', locale)}>{t('lnurl.more.all')}</a>
</div>
<div class="mint-grid">
{related.map((item) => <MintCard mint={item} />)}
</div>
</section>
)
}
<p class="mint-disclaimer">
{t('lnurl.disclaimer')}
<a href={localePath('/disclaimer', locale)}>{t('mint.disclaimerLink')}</a>.
</p>
</div>
<MintLive host={mint.host} />
</Base>
<style>
.more-mints { margin-top: 52px; }
.mint-disclaimer {
margin-top: 30px; padding-top: 18px; border-top: 1px solid var(--line-soft);
font-size: 13px; color: var(--faint); max-width: 720px;
}
.mint-disclaimer a { color: var(--amber); border-bottom: 1px dotted rgba(240, 169, 59, .4); }
.mint-head { display: flex; align-items: flex-start; gap: 20px; flex-wrap: wrap; margin-bottom: 22px; }
.mint-icon {
width: 72px; height: 72px; border-radius: 18px; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
font-family: var(--display); font-weight: 700; font-size: 32px; color: #0E0D0B;
border: 1px solid rgba(255, 255, 255, .14); object-fit: cover; overflow: hidden;
}
.mint-id { min-width: 240px; flex: 1; }
.mint-name-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
h1 { font-family: var(--display); font-size: 32px; font-weight: 700; letter-spacing: -.02em; line-height: 1.1; overflow-wrap: anywhere; }
.mint-domain {
display: flex; align-items: center; gap: 8px; margin-top: 8px;
font-size: 14px; color: var(--muted);
}
.mint-domain .mono { font-family: var(--mono); font-size: 13.5px; overflow-wrap: anywhere; }
.copy-btn {
background: none; border: none; padding: 3px; cursor: pointer;
color: var(--faint); display: inline-flex; border-radius: 5px;
transition: color .15s;
}
.copy-btn:hover { color: var(--amber); }
.mint-tags { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.tag { font-size: 12px; font-weight: 500; color: var(--muted); border: 1px solid var(--line); border-radius: 7px; padding: 3px 9px; }
.tag.mono { font-family: var(--mono); font-size: 11.5px; }
/* Not mainnet: the notes are play money and the badge has to read that way. */
.tag.testnet { color: var(--amber); border-color: var(--amber-dim); background: rgba(240, 169, 59, .07); font-weight: 600; }
.head-actions { margin-left: auto; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.verdict {
display: grid; grid-template-columns: 1.35fr 1fr 1fr 1fr;
border: 1px solid var(--line); border-radius: var(--radius);
background: var(--card); overflow: hidden; margin-bottom: 28px;
}
.v-cell { padding: 20px; border-right: 1px solid var(--line-soft); position: relative; }
.v-cell:last-child { border-right: none; }
.v-label {
font-size: 11px; font-weight: 600; letter-spacing: .09em; text-transform: uppercase;
color: var(--faint); margin-bottom: 10px; display: flex; align-items: center; gap: 6px;
height: 16px;
}
.v-big { font-family: var(--display); font-size: 34px; font-weight: 700; letter-spacing: -.02em; line-height: 1; }
.v-sub { font-size: 13px; color: var(--muted); margin-top: 6px; }
.rating-body { display: flex; gap: 24px; align-items: flex-start; flex-wrap: wrap; }
.rating-figure { flex: 0 0 auto; }
.rating-body .dist { flex: 1 1 150px; }
.rating-cell .stars { font-size: 15px; margin-top: 7px; }
.cols { display: grid; grid-template-columns: 1fr 372px; gap: 26px; align-items: start; }
.side { display: flex; flex-direction: column; gap: 20px; }
.side .panel { padding: 20px; }
.side-empty { font-size: 13.5px; color: var(--muted); }
.nut-rest { display: flex; flex-wrap: wrap; gap: 6px; }
.nut-chip { font-family: var(--mono); font-size: 10.5px; line-height: 1.5; color: var(--muted); border: 1px solid var(--line); border-radius: 5px; padding: 2px 6px; }
.nut-more { font-size: 12px; color: var(--faint); margin-top: 10px; line-height: 1.55; }
.kv-row :global(.cid.kv-v) { flex: 1; font-size: 12.5px; color: var(--text); }
.kv-row :global(.cid.kv-v:hover) { color: var(--amber); }
@media (max-width: 1020px) {
.cols { grid-template-columns: 1fr; }
.verdict { grid-template-columns: 1fr 1fr; }
.v-cell:nth-child(2) { border-right: none; }
.v-cell:nth-child(1), .v-cell:nth-child(2) { border-bottom: 1px solid var(--line-soft); }
}
@media (max-width: 640px) {
.verdict { grid-template-columns: 1fr; }
.v-cell { border-right: none; border-bottom: 1px solid var(--line-soft); }
.v-cell:last-child { border-bottom: none; }
.head-actions { margin-left: 0; width: 100%; }
.head-actions .btn { flex: 1 1 44%; justify-content: center; }
.page { padding: 20px var(--page-pad) 60px; }
h1 { font-size: 26px; }
}
</style>
<script>
import { wireCopyableIds, wireCopyButtons } from '../../../lib/client';
import { countUp, onReady, whenRevealed } from '../../../scripts/reveal';
onReady(() => {
wireCopyButtons();
wireCopyableIds();
const big = document.querySelector<HTMLElement>('[data-count-up]');
const cell = big?.closest<HTMLElement>('[data-reveal]');
if (big && cell) {
const target = Number(big.dataset['countUp']);
const decimals = Number(big.dataset['countDecimals'] ?? '0');
whenRevealed(cell, () => countUp(big, target, { decimals }));
}
});
</script>
+442
View File
@@ -0,0 +1,442 @@
---
import Base from '../../layouts/Base.astro';
import MintCard from '../../components/MintCard.astro';
import ReviewByUrl from '../../components/ReviewByUrl.astro';
import type { LnurlDetail } from '@cashumints/shared';
import { fetchLnurlMint, fetchLnurlMints } from '../../lib/api';
import { localePath, useI18n, type Locale } from '../../i18n';
import { itemListNode } from '../../lib/schema';
import { isIndexableMint } from '../../lib/seo';
import { localePaths } from '../../i18n/paths';
/**
* The LNURL mint index. /mints' twin, and built from the same parts for the same reason
* /fedimints is: same container, same card, same search, same result line, same
* leave-and-return animation.
*
* Unlike /fedimints it keeps the full sort set including "recently seen online". That
* page drops it because a third of its rows have no `last_online` at all — nothing
* checks those federations, so ordering on the column is a shuffle rather than a sort.
* An LNURL mint is genuinely probed over HTTPS, exactly as a Cashu mint is, so every row
* here has a real answer and the sort means what it says.
*/
/** One page per locale: `/lnurl-mints`, `/es/lnurl-mints`, `/nl/lnurl-mints`. */
export const getStaticPaths = localePaths;
interface Props {
locale: Locale;
}
const { locale } = Astro.props;
// `t` formats the numbers inside its own strings, so no separate formatter is needed.
const t = useI18n(locale);
const mints = await fetchLnurlMints();
/*
The list payload carries no LNURL fields, so the "no withdrawals" and "no mint / melt"
chips need each mint's detail. One fetch per mint, at build time, against the API on
the same machine — exactly what /mints does for its NUT switches. A mint whose detail
cannot be read simply gets no chip.
*/
const details = await Promise.all(
mints.map((mint) => fetchLnurlMint(mint.host).catch(() => null)),
);
const chipSources = details.map((detail: LnurlDetail | null) =>
detail
? {
max_withdrawable_msat: detail.max_withdrawable_msat ?? null,
funding_available: detail.funding_available ?? null,
}
: null,
);
const online = mints.filter((m) => m.status === 'online').length;
const offline = mints.filter((m) => m.status === 'offline').length;
const description = t('lnurlMints.description', { total: mints.length, online, offline });
/*
* The index, as a list a crawler can read in order.
*
* Only the mints whose own pages are indexable: listing a page here while telling a
* crawler not to index it is a contradiction. The order is the server-rendered one,
* which is the score order; the sort control rewrites the DOM but not this markup,
* so `ItemListOrderDescending` describes what shipped.
*/
const canonical = new URL(localePath('/lnurl-mints', locale), Astro.site).href;
const schema = [
itemListNode({
canonical,
fragment: 'lnurl-mints',
name: t('lnurlMints.heading'),
items: mints.filter(isIndexableMint).map((mint) => ({
name: mint.name ?? mint.host,
url: new URL(localePath(`/lnurl-mint/${mint.host}`, locale), Astro.site).href,
})),
}),
];
---
<Base
title={t('lnurlMints.title')}
description={description}
current="lnurl-mints"
schema={schema}
>
{/*
The same .page container the home page and the mint pages use, so the heading,
the controls, the count line and the grid all share one left edge and one width.
*/}
<div class="page">
<div class="index-head">
<div class="index-head-text">
<h1>{t('lnurlMints.heading')}</h1>
<p class="lede">{t('lnurlMints.lede')}</p>
</div>
{/*
The way in for something this site has never seen. Every other route to writing
a review starts from a page that already exists, which is exactly the case a
reader with a brand new mint does not have.
*/}
<button class="btn primary index-head-action" type="button" data-review-url-open="lnurl">
{t('reviews.byUrl.open')}
</button>
</div>
<div class="controls" data-mint-controls>
<form class="search" role="search" onsubmit="return false">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<circle cx="11" cy="11" r="7" /><path d="m20 20-3.5-3.5" />
</svg>
<label class="sr-only" for="mint-search">{t('lnurlMints.search.label')}</label>
<input
id="mint-search"
type="text"
autocomplete="off"
placeholder={t('lnurlMints.search.placeholder')}
data-search-input
/>
<kbd aria-hidden="true">/</kbd>
</form>
<div class="control-group">
<label class="sr-only" for="mint-sort">{t('lnurlMints.sort.label')}</label>
<div class="select-wrap">
<select id="mint-sort" data-sort>
<option value="score">{t('mints.sort.score')}</option>
<option value="rating">{t('mints.sort.rating')}</option>
<option value="reviews">{t('mints.sort.reviews')}</option>
<option value="recent">{t('mints.sort.recent')}</option>
<option value="seen">{t('mints.sort.seen')}</option>
</select>
<svg class="chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" aria-hidden="true">
<path d="m6 9 6 6 6-6" />
</svg>
</div>
{/*
A pressed pill rather than a native checkbox: it is the same control the
review filters use, and it is a real button, so it keeps the focus ring.
*/}
<button class="toggle-chip" type="button" data-hide-offline aria-pressed="false">
<span class="toggle-dot" aria-hidden="true"></span>
{t('lnurlMints.hideOffline')}
</button>
</div>
</div>
<p class="result-count" data-result-count aria-live="polite">
{t('lnurlMints.showing', { total: mints.length, online, offline })}
</p>
<div class="mint-grid" data-mint-grid>
{mints.map((mint, i) => <MintCard mint={mint} rank={i + 1} lnurl={chipSources[i]} />)}
</div>
<p class="no-results" data-no-results hidden>
{t('lnurlMints.noResults')}
<button class="link-btn" type="button" data-clear-search>{t('mints.clearSearch')}</button>
</p>
{mints.length === 0 && <p class="no-results">{t('lnurlMints.emptyIndex')}</p>}
</div>
{/* Two steps in one dialog: identify the mint, then the ordinary review form. */}
<ReviewByUrl type="lnurl" />
</Base>
<style>
/* Heading and lede on the left, the one primary action on the right, wrapping to
its own line before the lede has to give up any width. */
.index-head { margin-bottom: 24px; display: flex; align-items: flex-start; gap: 16px; flex-wrap: wrap; }
.index-head-text { flex: 1 1 320px; }
.index-head-action { flex-shrink: 0; }
.index-head h1 { font-family: var(--display); font-size: 32px; font-weight: 700; letter-spacing: -.02em; line-height: 1.1; }
.index-head .lede { max-width: 620px; font-size: 15.5px; margin-top: 10px; }
.controls { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin-bottom: 14px; }
.search {
display: flex; align-items: center; gap: 12px; flex: 1 1 320px; min-width: 240px;
background: var(--card); border: 1px solid var(--line);
border-radius: 12px; padding: 10px 16px;
transition: border-color .15s, box-shadow .15s;
}
.search:focus-within { border-color: var(--amber-dim); box-shadow: 0 0 0 3px rgba(240, 169, 59, .08); }
.search svg { color: var(--faint); flex-shrink: 0; }
.search input {
flex: 1; background: none; border: none; outline: none; min-width: 0;
color: var(--text); font-family: var(--body); font-size: 15px;
}
.search input::placeholder { color: var(--faint); }
.search kbd {
font-family: var(--mono); font-size: 11.5px; color: var(--faint);
border: 1px solid var(--line); border-radius: 6px; padding: 2px 7px; flex-shrink: 0;
}
/* Search grows; sort and toggle sit together at the right and wrap as a pair. */
.control-group { display: flex; gap: 10px; align-items: center; margin-left: auto; flex-wrap: wrap; }
.select-wrap { position: relative; display: flex; align-items: center; }
.select-wrap select {
appearance: none; -webkit-appearance: none;
background: var(--card); border: 1px solid var(--line); border-radius: 9px;
color: var(--text); font-family: var(--body); font-size: 14px;
padding: 10px 34px 10px 14px; cursor: pointer;
transition: border-color .15s;
}
.select-wrap select:hover { border-color: #38342A; }
.select-wrap .chevron { position: absolute; right: 12px; color: var(--faint); pointer-events: none; }
.select-wrap option { background: var(--card); color: var(--text); }
.toggle-chip {
display: inline-flex; align-items: center; gap: 8px;
font-family: var(--body); font-size: 13.5px; font-weight: 500;
color: var(--muted); cursor: pointer;
border: 1px solid var(--line); background: none; border-radius: 999px;
padding: 9px 15px;
transition: color .15s, border-color .15s, background .15s;
}
.toggle-chip:hover { color: var(--text); border-color: #38342A; }
.toggle-dot {
width: 7px; height: 7px; border-radius: 50%; background: var(--faint); flex-shrink: 0;
transition: background .15s;
}
.toggle-chip[aria-pressed='true'] {
color: #141109; background: var(--amber); border-color: var(--amber); font-weight: 600;
}
.toggle-chip[aria-pressed='true'] .toggle-dot { background: #141109; }
.result-count { font-size: 13px; color: var(--faint); margin-bottom: 16px; font-family: var(--mono); }
/*
* A card leaving the results fades and settles back a touch, 120ms, then it is
* hidden and the grid reflows around it. Positions are not animated: the browser's
* own reflow is instant and honest, and 58 cards sliding to new places would be a
* light show rather than a search result.
*/
.mint-grid :global(.mint-card.leaving) {
transform: scale(.98);
transition: opacity 120ms var(--ease-out), transform 120ms var(--ease-out);
}
.no-results { color: var(--muted); font-size: 14.5px; padding: 34px 0; }
.link-btn {
background: none; border: none; padding: 0; cursor: pointer;
font-family: var(--body); font-size: inherit; color: var(--amber);
border-bottom: 1px solid var(--amber-dim);
}
.link-btn:hover { border-bottom-color: var(--amber); }
@media (max-width: 640px) {
/* Sort and toggle take their own row, still sharing the grid's left edge. */
.control-group { margin-left: 0; width: 100%; }
.select-wrap, .select-wrap select { flex: 1; }
}
</style>
<script>
import { wireCopyableIds } from '../../lib/client';
import { useI18n } from '../../i18n/client';
import { enterStagger, onLeave, prefersReducedMotion, onReady, swapText } from '../../scripts/reveal';
/** How long a card takes to fade out of the results before it is hidden. */
const LEAVE_MS = 120;
const setup = (): void => {
wireCopyableIds();
// Re-read on every arrival, so the count line under the filters follows the page.
// `t` formats its own numbers, so the grouping separator follows the locale too.
const t = useI18n();
const grid = document.querySelector<HTMLElement>('[data-mint-grid]');
const searchInput = document.querySelector<HTMLInputElement>('[data-search-input]');
const sortSelect = document.querySelector<HTMLSelectElement>('[data-sort]');
const hideOffline = document.querySelector<HTMLButtonElement>('[data-hide-offline]');
const countEl = document.querySelector<HTMLElement>('[data-result-count]');
const noResults = document.querySelector<HTMLElement>('[data-no-results]');
const clearSearch = document.querySelector<HTMLButtonElement>('[data-clear-search]');
if (grid && searchInput && sortSelect && hideOffline) {
const cards = [...grid.querySelectorAll<HTMLElement>('[data-mint-card]')];
const num = (card: HTMLElement, key: string) => Number(card.dataset[key] ?? 0);
const offlineRank = (card: HTMLElement) => (card.dataset['status'] === 'offline' ? 1 : 0);
const SORTS: Record<string, (a: HTMLElement, b: HTMLElement) => number> = {
// Default keeps the API's rule: online first, then score. Every other sort
// ranks offline mints on their merits, so a dead mint with 40 good reviews
// still shows up under "Most reviewed".
score: (a, b) => offlineRank(a) - offlineRank(b) || num(b, 'score') - num(a, 'score'),
rating: (a, b) => num(b, 'rating') - num(a, 'rating') || num(b, 'reviews') - num(a, 'reviews'),
reviews: (a, b) => num(b, 'reviews') - num(a, 'reviews'),
recent: (a, b) => num(b, 'lastReview') - num(a, 'lastReview'),
seen: (a, b) => num(b, 'lastOnline') - num(a, 'lastOnline'),
};
/**
* One pending fade per card, at most.
*
* Typing into the search box calls `apply` on every keystroke, so a card can be
* told to leave and then to come back before its fade has finished. Cancelling
* the timer is what keeps those from stacking: the card either goes out once, or
* never starts.
*/
const leaving = new Map<HTMLElement, number>();
function show(card: HTMLElement, visible: boolean): void {
const pending = leaving.get(card);
if (pending !== undefined) {
window.clearTimeout(pending);
leaving.delete(card);
}
if (visible) {
const wasHidden = card.hidden;
card.classList.remove('leaving');
card.hidden = false;
// Back in the results after being filtered out: a plain fade, no rise. It
// was here before, it is not new.
if (wasHidden && !prefersReducedMotion()) {
card.classList.add('appearing');
card.addEventListener('animationend', () => card.classList.remove('appearing'), { once: true });
}
return;
}
if (card.hidden) return;
if (prefersReducedMotion()) {
card.hidden = true;
return;
}
card.classList.add('leaving');
leaving.set(
card,
window.setTimeout(() => {
leaving.delete(card);
card.hidden = true;
card.classList.remove('leaving');
}, LEAVE_MS),
);
}
function apply(): void {
const query = searchInput!.value.trim().toLowerCase();
// A pasted mint URL should find the mint, so match on the bare domain too.
const needle = query.replace(/^https?:\/\//, '').replace(/\/+$/, '');
const hide = hideOffline!.getAttribute('aria-pressed') === 'true';
let visible = 0;
let onlineCount = 0;
let offlineCount = 0;
for (const card of cards) {
const matches =
needle === '' ||
(card.dataset['name'] ?? '').includes(needle) ||
(card.dataset['domain'] ?? '').includes(needle);
const isOffline = card.dataset['status'] === 'offline';
const keep = matches && !(hide && isOffline);
show(card, keep);
if (keep) {
visible++;
if (isOffline) offlineCount++;
else if (card.dataset['status'] === 'online') onlineCount++;
}
}
const sorted = [...cards].sort(SORTS[sortSelect!.value] ?? SORTS['score']!);
for (const card of sorted) grid!.append(card);
// A rank chip is a claim about position in the default order. It means nothing
// in a rating sort and nothing in a filtered list, so it is hidden in both.
const ranked = sortSelect!.value === 'score' && needle === '' && !hide;
let rank = 0;
for (const card of sorted) {
const chip = card.querySelector<HTMLElement>('.mc-rank');
if (!chip) continue;
if (!ranked) {
chip.hidden = true;
continue;
}
rank++;
chip.hidden = false;
chip.textContent = `#${rank}`;
chip.classList.toggle('gold', rank === 1);
}
if (countEl) {
// The count is the answer to what the reader just typed, so it cross-fades
// rather than flicking from one number to another.
swapText(
countEl,
visible === cards.length
? t('lnurlMints.showing', { total: visible, online: onlineCount, offline: offlineCount })
: t('lnurlMints.showingFiltered', {
shown: visible,
total: cards.length,
online: onlineCount,
offline: offlineCount,
}),
);
}
if (noResults) noResults.hidden = visible > 0;
}
searchInput.addEventListener('input', apply);
sortSelect.addEventListener('change', () => {
apply();
// A new order is a new list: it arrives the way the grid arrived, once, and
// the stagger is capped so the last card is never more than 400ms behind.
enterStagger(cards.filter((card) => !card.hidden));
});
hideOffline.addEventListener('click', () => {
const on = hideOffline.getAttribute('aria-pressed') === 'true';
hideOffline.setAttribute('aria-pressed', String(!on));
apply();
});
clearSearch?.addEventListener('click', () => {
searchInput.value = '';
searchInput.focus();
apply();
});
// Deep links from the home page quick filters: /mints?q=…&sort=…
const params = new URLSearchParams(window.location.search);
const q = params.get('q');
const sort = params.get('sort');
if (q) searchInput.value = q;
if (sort && sort in SORTS) sortSelect.value = sort;
if (q || sort) apply();
onLeave(() => {
for (const timer of leaving.values()) window.clearTimeout(timer);
leaving.clear();
});
}
};
onReady(setup);
</script>
+21 -3
View File
@@ -1,6 +1,7 @@
--- ---
import Base from '../../layouts/Base.astro'; import Base from '../../layouts/Base.astro';
import MintCard from '../../components/MintCard.astro'; import MintCard from '../../components/MintCard.astro';
import ReviewByUrl from '../../components/ReviewByUrl.astro';
import { readCapabilities } from '@cashumints/shared'; import { readCapabilities } from '@cashumints/shared';
import { fetchMint, fetchMints } from '../../lib/api'; import { fetchMint, fetchMints } from '../../lib/api';
import { localePath, useI18n, type Locale } from '../../i18n'; import { localePath, useI18n, type Locale } from '../../i18n';
@@ -66,8 +67,18 @@ const schema = [
*/} */}
<div class="page"> <div class="page">
<div class="index-head"> <div class="index-head">
<h1>{t('mints.heading')}</h1> <div class="index-head-text">
<p class="lede">{t('mints.lede')}</p> <h1>{t('mints.heading')}</h1>
<p class="lede">{t('mints.lede')}</p>
</div>
{/*
The way in for something this site has never seen. Every other route to writing
a review starts from a page that already exists, which is exactly the case a
reader with a brand new mint does not have.
*/}
<button class="btn primary index-head-action" type="button" data-review-url-open="cashu">
{t('reviews.byUrl.open')}
</button>
</div> </div>
<div class="controls" data-mint-controls> <div class="controls" data-mint-controls>
@@ -125,10 +136,17 @@ const schema = [
<button class="link-btn" type="button" data-clear-search>{t('mints.clearSearch')}</button> <button class="link-btn" type="button" data-clear-search>{t('mints.clearSearch')}</button>
</p> </p>
</div> </div>
{/* Two steps in one dialog: identify the mint, then the ordinary review form. */}
<ReviewByUrl type="cashu" />
</Base> </Base>
<style> <style>
.index-head { margin-bottom: 24px; } /* Heading and lede on the left, the one primary action on the right, wrapping to
its own line before the lede has to give up any width. */
.index-head { margin-bottom: 24px; display: flex; align-items: flex-start; gap: 16px; flex-wrap: wrap; }
.index-head-text { flex: 1 1 320px; }
.index-head-action { flex-shrink: 0; }
.index-head h1 { font-family: var(--display); font-size: 32px; font-weight: 700; letter-spacing: -.02em; line-height: 1.1; } .index-head h1 { font-family: var(--display); font-size: 32px; font-weight: 700; letter-spacing: -.02em; line-height: 1.1; }
.index-head .lede { max-width: 620px; font-size: 15.5px; margin-top: 10px; } .index-head .lede { max-width: 620px; font-size: 15.5px; margin-top: 10px; }
+12 -9
View File
@@ -9,7 +9,7 @@
* review looks and behaves identically wherever you meet it. * review looks and behaves identically wherever you meet it.
*/ */
import Base from '../../layouts/Base.astro'; import Base from '../../layouts/Base.astro';
import { fetchFedimints, fetchMints, fetchStats } from '../../lib/api'; import { fetchAllListings, fetchStats } from '../../lib/api';
import { fetchReviewFeed } from '../../lib/nostr-build'; import { fetchReviewFeed } from '../../lib/nostr-build';
import { feedTargets, targetPath } from '../../lib/feed-resolve'; import { feedTargets, targetPath } from '../../lib/feed-resolve';
import { reviewHtml } from '../../lib/review-cards'; import { reviewHtml } from '../../lib/review-cards';
@@ -33,20 +33,22 @@ const f = formatters(t);
/** How many reviews the build renders. Enough to fill the page above and below the fold. */ /** How many reviews the build renders. Enough to fill the page above and below the fold. */
const PRERENDER_COUNT = 30; const PRERENDER_COUNT = 30;
const [mints, federations, stats] = await Promise.all([ const [listings, stats] = await Promise.all([fetchAllListings(), fetchStats()]);
fetchMints(),
fetchFedimints(),
fetchStats(),
]);
/** /**
* Everything the feed can resolve a review to, both ecosystems in one list. * Everything the feed can resolve a review to, every ecosystem in one list.
* *
* Handed to the island as JSON so the browser can resolve a review back to a listing * Handed to the island as JSON so the browser can resolve a review back to a listing
* without asking the API again. Name and domain are what the card's link shows; `ref` * without asking the API again. Name and domain are what the card's link shows; `ref`
* is the federation id a 38173 review points at, read straight out of the row key. * is the identifier a review's `d` tag points at — a federation id, or an LNURL mint's
* host — read straight out of the row key.
*
* Fetched through `fetchAllListings`, which is driven by `ECOSYSTEMS`, so this list and
* the filter pills below can never disagree about which ecosystems exist. They did once:
* the pills were built from that list while the data was two fetches written out by
* hand, which meant a new ecosystem got a pill that matched nothing.
*/ */
const feedMints = feedTargets(mints, federations); const feedMints = feedTargets(...listings);
const feed = await fetchReviewFeed(feedMints, PRERENDER_COUNT); const feed = await fetchReviewFeed(feedMints, PRERENDER_COUNT);
const mintByHost = new Map(feedMints.map((mint) => [mint.host, mint])); const mintByHost = new Map(feedMints.map((mint) => [mint.host, mint]));
@@ -82,6 +84,7 @@ const description = t('feed.description', {
reviews: stats.reviews_total, reviews: stats.reviews_total,
mints: stats.cashu_total, mints: stats.cashu_total,
federations: stats.fedimint_total, federations: stats.fedimint_total,
lnurl: stats.lnurl_total,
}); });
/** /**
+27 -2
View File
@@ -14,7 +14,7 @@
* /404 is deliberately absent, in every locale. * /404 is deliberately absent, in every locale.
*/ */
import type { APIRoute } from 'astro'; import type { APIRoute } from 'astro';
import { fetchFedimints, fetchMints } from '../lib/api'; import { fetchFedimints, fetchLnurlMints, fetchMints } from '../lib/api';
import { alternates } from '../i18n/routing'; import { alternates } from '../i18n/routing';
import { isIndexableMint } from '../lib/seo'; import { isIndexableMint } from '../lib/seo';
import { DEFAULT_LOCALE, LOCALE_CODES } from '../i18n/config'; import { DEFAULT_LOCALE, LOCALE_CODES } from '../i18n/config';
@@ -42,6 +42,12 @@ const STATIC_PAGES: Array<Omit<Entry, 'lastmod'>> = [
* changes most and matters most keeps the higher number. * changes most and matters most keeps the higher number.
*/ */
{ path: '/fedimints', changefreq: 'daily', priority: 0.8 }, { path: '/fedimints', changefreq: 'daily', priority: 0.8 },
/*
* Level with /fedimints, not below it. The two are peers: both are a second-tier
* ecosystem index next to Cashu's, and neither has a claim on being read first. The
* ordering in this list is display order and says nothing to a crawler.
*/
{ path: '/lnurl-mints', changefreq: 'daily', priority: 0.8 },
{ path: '/reviews', changefreq: 'daily', priority: 0.7 }, { path: '/reviews', changefreq: 'daily', priority: 0.7 },
{ path: '/wallets', changefreq: 'monthly', priority: 0.5 }, { path: '/wallets', changefreq: 'monthly', priority: 0.5 },
{ path: '/about', changefreq: 'monthly', priority: 0.5 }, { path: '/about', changefreq: 'monthly', priority: 0.5 },
@@ -62,6 +68,9 @@ const MINT_PRIORITY = 0.8;
/** One notch below a mint page, for the same reason /fedimints sits below /mints. */ /** One notch below a mint page, for the same reason /fedimints sits below /mints. */
const FEDIMINT_PRIORITY = 0.7; const FEDIMINT_PRIORITY = 0.7;
/** Level with a federation page: the same second-tier ecosystem, the same weight. */
const LNURL_PRIORITY = 0.7;
/** /**
* Non-default locales sit slightly below their English counterpart, which is the same * Non-default locales sit slightly below their English counterpart, which is the same
* thing `x-default` says in the alternate set: when a crawler has to pick one, pick * thing `x-default` says in the alternate set: when a crawler has to pick one, pick
@@ -83,7 +92,11 @@ export const GET: APIRoute = async ({ site }) => {
const origin = site ?? new URL('https://cashumints.space'); const origin = site ?? new URL('https://cashumints.space');
const builtAt = Math.floor(Date.now() / 1000); const builtAt = Math.floor(Date.now() / 1000);
const [mints, federations] = await Promise.all([fetchMints(), fetchFedimints()]); const [mints, federations, lnurlMints] = await Promise.all([
fetchMints(),
fetchFedimints(),
fetchLnurlMints(),
]);
const entries: Entry[] = [ const entries: Entry[] = [
...STATIC_PAGES.map((page) => ({ ...page, lastmod: builtAt })), ...STATIC_PAGES.map((page) => ({ ...page, lastmod: builtAt })),
@@ -121,6 +134,18 @@ export const GET: APIRoute = async ({ site }) => {
changefreq: 'weekly' as const, changefreq: 'weekly' as const,
priority: FEDIMINT_PRIORITY, priority: FEDIMINT_PRIORITY,
})), })),
/*
* LNURL mints, on the same rule again. Their `lastmod` behaves like a Cashu mint's
* rather than a federation's: they are genuinely probed, so `last_online` is set on
* every one that has ever answered, and the build-time fallback is only reached by a
* mint that never has — which `isIndexableMint` has already filtered out.
*/
...lnurlMints.filter(isIndexableMint).map((mint) => ({
path: `/lnurl-mint/${mint.host}`,
lastmod: Math.max(mint.last_review_at ?? 0, mint.last_online ?? 0) || builtAt,
changefreq: 'weekly' as const,
priority: LNURL_PRIORITY,
})),
]; ];
/** /**
+155 -20
View File
@@ -194,6 +194,20 @@
--violet: #A78BFA; --violet: #A78BFA;
--mint: #5FD68F; --mint: #5FD68F;
--red: #E06450; --red: #E06450;
/*
* The LNURL ecosystem accent, and the only cool blue on this site.
*
* The palette is otherwise warm, which is exactly what makes this readable as "a
* different ecosystem" at thumbnail size on a social card. Every other candidate was
* already taken by a meaning the colour discipline reserves: amber is the brand,
* mint is online/success, red is offline/critical, and violet is Nostr-protocol
* (which the Fedimint badge borrows). A blue is the only hue left that a reader
* cannot mistake for a status.
*
* Used for the LNURL card's badge and glow, and nowhere else. It is an ecosystem
* label, not a state, and must never decorate one.
*/
--lnurl: #4EA8DE;
--radius: 14px; --radius: 14px;
--body: 'Inter', 'Inter Fallback', system-ui, -apple-system, 'Segoe UI', sans-serif; --body: 'Inter', 'Inter Fallback', system-ui, -apple-system, 'Segoe UI', sans-serif;
@@ -523,18 +537,85 @@ main { flex: 1; }
.kv-row .copy-btn { margin-left: auto; } .kv-row .copy-btn { margin-left: auto; }
/* ---------- footer ---------- */ /* ---------- footer ---------- */
footer { border-top: 1px solid var(--line-soft); margin-top: 70px; }
.foot-inner { /*
max-width: var(--page-max); margin: 0 auto; padding: 28px; * Three zones: the columns, then a thin bar. `main { flex: 1 }` and the column layout
display: flex; align-items: center; gap: 20px; flex-wrap: wrap; * on `body` are what put this at the bottom of the viewport on a short page (the LNURL
font-size: 13px; color: var(--faint); * index with one mint) rather than halfway up it, so this needs no min-height of its
* own. `margin-top: 70px` is the gap after a long page; on a short one the flex growth
* of `main` swallows it.
*/
.site-footer { border-top: 1px solid var(--line-soft); margin-top: 70px; }
.foot-cols {
max-width: var(--page-max); margin: 0 auto;
padding: 44px var(--page-pad) 36px;
display: grid;
/*
* Four visible columns from three grid tracks: the middle one is the nav landmark,
* which holds Explore and Site. One landmark over both, rather than a track each,
* is what keeps the landmark list honest — see Footer.astro.
*/
grid-template-columns: minmax(0, 2.1fr) minmax(0, 2fr) minmax(0, 1.2fr);
gap: 40px 44px;
}
.foot-nav { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 40px 32px; }
/* The brand column. Narrower than its track so the sentences break at a readable
measure instead of running the full width of a 1240px grid cell. */
.foot-brand { max-width: 34ch; }
.foot-mark {
display: inline-flex; align-items: center; gap: 10px;
font-family: var(--display); font-weight: 700; font-size: 15.5px; letter-spacing: -.01em;
}
.foot-desc { color: var(--muted); font-size: 13.5px; line-height: 1.55; margin-top: 12px; }
/*
* The custodial warning. `--muted`, not `--faint`: it is the one sentence in this
* footer that a reader is worse off for having missed, and it used to be an orphan in
* the bottom right corner of the last row.
*/
.foot-warn { color: var(--muted); font-size: 13px; line-height: 1.5; margin-top: 10px; }
/* The small-caps label, the same one `.side-title` and `.fb-stat .k` use. An h2 in the
markup, so the margin is stated in full and the UA's own h2 margins cannot show. */
.foot-head {
font-size: 11px; font-weight: 600; letter-spacing: .09em; text-transform: uppercase;
color: var(--faint); margin: 0 0 14px;
}
.foot-col ul { list-style: none; display: flex; flex-direction: column; gap: 8px; }
.foot-col a {
font-size: 13.5px; color: var(--muted);
transition: color var(--dur-quick);
}
.foot-col a:hover { color: var(--text); }
.foot-bar {
max-width: var(--page-max); margin: 0 auto;
padding: 16px var(--page-pad);
border-top: 1px solid var(--line-soft);
display: flex; align-items: center; justify-content: space-between;
gap: 8px 24px; flex-wrap: wrap;
font-size: 12.5px; color: var(--faint);
} }
.foot-inner .links { display: flex; gap: 18px; flex-wrap: wrap; }
.foot-inner a:hover { color: var(--muted); }
.foot-right { margin-left: auto; }
/* Dotted underline like every other in-sentence link: colour alone is not enough /* Dotted underline like every other in-sentence link: colour alone is not enough
to mark a link inside running text (WCAG 1.4.1). */ to mark a link inside running text (WCAG 1.4.1). */
.foot-right a { color: var(--amber); border-bottom: 1px dotted rgba(240, 169, 59, .4); } .foot-credit a { color: var(--amber); border-bottom: 1px dotted rgba(240, 169, 59, .4); }
/*
* The stack.
*
* At 900px the three tracks become one, which puts the columns in source order —
* Brand, then the nav's two columns side by side, then Language. Explore and Site stay
* paired down to 460px because two short link lists next to each other read as one
* block; below that they are two lists that would each be one word wide.
*/
@media (max-width: 900px) {
.foot-cols { grid-template-columns: minmax(0, 1fr); gap: 34px; }
.foot-brand { max-width: 46ch; }
}
@media (max-width: 460px) {
.foot-nav { grid-template-columns: minmax(0, 1fr); gap: 30px; }
}
/* ---------- prose (about, wallets) ---------- */ /* ---------- prose (about, wallets) ---------- */
.prose { max-width: 720px; } .prose { max-width: 720px; }
@@ -559,24 +640,29 @@ footer { border-top: 1px solid var(--line-soft); margin-top: 70px; }
/* /*
* Below this the header stops being one row. * Below this the header stops being one row.
* *
* Five nav items, the brand, the language control and the account control do not fit * Six nav items, the brand, the language control and the account control do not fit
* across anything narrower than a laptop, and a header that does not fit is a page that * across anything narrower than a laptop, and a header that does not fit is a page that
* scrolls sideways: the body inherits the overflow and every section under it drifts. So * scrolls sideways: the body inherits the overflow and every section under it drifts. So
* the nav takes a row of its own and the two controls stay up top with the brand, which * the nav takes a row of its own and the two controls stay up top with the brand, which
* is also the order they are wanted in: what page am I on, then who am I and what * is also the order they are wanted in: what page am I on, then who am I and what
* language is this. * language is this.
* *
* 1040px, not the 620px this used to be. The old figure was already two pixels short of * The number is set by the widest locale, never by English. Picking the English figure
* fitting four English items at 768, and five need 400px against the 303px the first row * is how this shipped broken once already: every locale renders the same stylesheet, so
* has left there — no amount of tightening the padding closes a gap that size. * the breakpoint has to clear the widest of them, with room for the next language to be
* longer still.
* *
* The number is set by Spanish, which is the longest of the three ("Mints Cashu / * History, because the direction of travel matters here: 620px fitted four items,
* Fedimints / Reseñas / Monederos / Acerca de") and needs 1008px to fit on one row * 1040px fitted five (set by Spanish at 1008px against English's 906), and adding the
* against English's 906. Picking the English figure is how this shipped broken once * LNURL index made six. Measured across all shipped locales at the time it was added,
* already: every locale renders the same stylesheet, so the breakpoint has to clear the * the widest first row needs 1123px, so this sits at 1180 — the same kind of headroom
* widest of them, with room for the next language to be longer still. * the 1040 figure carried over its own 1008.
*
* At 1280px, the widest desktop this site targets, all six fit on one row with room to
* spare in every locale. A seventh would not, and would need a real menu rather than
* another bump: this row is full.
*/ */
@media (max-width: 1040px) { @media (max-width: 1180px) {
.topbar-inner { flex-wrap: wrap; row-gap: 10px; } .topbar-inner { flex-wrap: wrap; row-gap: 10px; }
/* Pushes the pair to the right of the first row, where they were before it wrapped. */ /* Pushes the pair to the right of the first row, where they were before it wrapped. */
.topbar-inner > .lang-menu { margin-left: auto; } .topbar-inner > .lang-menu { margin-left: auto; }
@@ -601,7 +687,10 @@ footer { border-top: 1px solid var(--line-soft); margin-top: 70px; }
@media (max-width: 640px) { @media (max-width: 640px) {
:root { --page-pad: 18px; } :root { --page-pad: 18px; }
.mint-grid { grid-template-columns: minmax(0, 1fr); } .mint-grid { grid-template-columns: minmax(0, 1fr); }
.foot-right { margin-left: 0; } /* Two centred lines rather than one row pushed to both edges: at this width the
copyright and the credit would otherwise sit in opposite corners with a gulf
between them. */
.foot-bar { justify-content: center; text-align: center; gap: 6px; }
} }
@media (max-width: 420px) { @media (max-width: 420px) {
.brand { gap: 0; } .brand { gap: 0; }
@@ -884,6 +973,52 @@ footer { border-top: 1px solid var(--line-soft); margin-top: 70px; }
.fb-stats { display: flex; gap: 26px; flex-wrap: wrap; margin-top: 8px; } .fb-stats { display: flex; gap: 26px; flex-wrap: wrap; margin-top: 8px; }
.fb-stat .k { font-size: 11px; letter-spacing: .09em; text-transform: uppercase; color: var(--faint); display: block; } .fb-stat .k { font-size: 11px; letter-spacing: .09em; text-transform: uppercase; color: var(--faint); display: block; }
.fb-stat .v { font-family: var(--display); font-size: 24px; font-weight: 700; } .fb-stat .v { font-family: var(--display); font-size: 24px; font-weight: 700; }
/* The one action a resolved-on-arrival mint offers: review it, right now. */
.fb-actions { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 18px; }
/* The reviews panel the resolver starts once it has a subject to give it. */
.nf-reviews { margin-top: 26px; }
/* ---------- review by URL (the two-step dialog) ---------- */
/*
* Step one asks for an address. It is the same input the search boxes use, so it reads
* as one control family across the site rather than as a form that turned up in a modal.
*/
.rbu-input {
background: var(--bg); border: 1px solid var(--line); border-radius: 10px;
color: var(--text); font-family: var(--mono); font-size: 14px;
padding: 12px 12px; width: 100%; display: block;
}
.rbu-input:focus { outline: 2px solid var(--amber); outline-offset: 1px; }
.rbu-input:disabled { color: var(--muted); background: var(--card2); }
.rbu-input::placeholder { color: var(--faint); font-family: var(--mono); }
/* "That address answered as a Cashu mint" plus the button that acts on it. */
.rbu-switch { font-size: 13px; color: var(--muted); line-height: 1.6; }
/* Step two's way back, above the mint it is about to review. */
.rbu-back {
align-self: flex-start; display: inline-flex; align-items: center; gap: 5px;
border: none; background: none; cursor: pointer; padding: 2px 0;
font-family: var(--body); font-size: 12.5px; color: var(--muted);
}
.rbu-back:hover { color: var(--amber); }
/* The mint that was found: enough to recognise it by, and no more. */
.rbu-preview {
display: flex; align-items: center; gap: 11px; flex-wrap: wrap;
background: var(--card2); border: 1px solid var(--line-soft);
border-radius: 11px; padding: 10px 12px;
}
.rbu-icon {
width: 36px; height: 36px; border-radius: 9px; flex-shrink: 0;
display: inline-flex; align-items: center; justify-content: center;
font-family: var(--display); font-size: 16px; font-weight: 700; color: #141109;
}
.rbu-who { display: flex; flex-direction: column; gap: 1px; min-width: 120px; flex: 1; }
.rbu-name { font-size: 14px; font-weight: 600; overflow-wrap: anywhere; }
.rbu-domain { font-family: var(--mono); font-size: 11.5px; color: var(--faint); overflow-wrap: anywhere; }
.rbu-chips { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.rbu-note { font-size: 11.5px; color: var(--faint); }
/* ---------- skeletons ---------- */ /* ---------- skeletons ---------- */
/* /*