28 Commits
Author SHA1 Message Date
Michilis 4c1156f766 Merge pull request 'Stop a listing page from rewriting the next one's grid after navigation.' (#11) from dev into main
Reviewed-on: #11
2026-09-10 18:29:13 +00:00
michilisandClaude Fable 5.1 ae7664fbe0 Stop a listing page from rewriting the next one's grid after navigation.
Switching between /mints, /fedimints and /lnurl-mints showed the right list
for a moment and then flashed back to the previous ecosystem's mints. The
client router keeps every page's script alive, and onReady re-runs each
setup on every arrival, so a visited page's setup also ran on the next page.
All three grids were marked with the same bare data-mint-grid, so the stale
setup found the new grid, fetched its own type and overwrote it.

Mark and query each grid by ecosystem (data-mint-grid="cashu" etc.), the way
the home page already scopes data-home-grid, so a stale setup finds nothing
and bails. Skip the render in hydrateMintGrid when the grid has already been
detached by the router. Document the re-run-everywhere contract on onReady,
and add a static wiring test so a bare marker cannot come back.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 22:36:26 +02:00
Michilis d40f8b3d0f Merge pull request 'Show a brief publishing note that fades out after a few seconds.' (#10) from dev into main
Reviewed-on: #10
2026-09-01 03:48:29 +00:00
michilisandCursor 70b35f4ccc Show a brief publishing note that fades out after a few seconds.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 22:42:00 +02:00
Michilis e79d54ba34 Merge pull request 'Order the latest-reviews strip by created_at alone.' (#9) from dev into main
Reviewed-on: #9
2026-08-25 15:45:39 +00:00
michilisandClaude Opus 5 1eade490c8 Order the latest-reviews strip by created_at alone.
The home page carousel floated reviews whose author had a kind 0 ahead of
everything else, capped at 90 days old, so a named review from two months ago
sat between two reviews from this week. Under a heading that says "Latest
reviews", with each card's foot printing the very timestamp being overruled,
that reads as broken rather than as curation.

Strict recency now, newest first, as the last thing that happens to the list,
with the event id breaking ties so two builds of the same events agree. Names
and avatars are still resolved and still shown; they no longer decide the
order.

The candidate scan stops at the limit rather than gathering twelve times it:
the scan already runs newest first, so nothing further down can outrank what
it has, and the build resolves ten profiles per locale instead of a hundred
and twenty.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 17:23:22 +02:00
Michilis 81901a862e Merge pull request 'Dev' (#8) from dev into main
Reviewed-on: #8
2026-08-25 14:40:01 +00:00
michilisandClaude Opus 5 860a4de009 Check in the dynamic-mint-data analysis it was already sitting on.
It was untracked in the working tree. web/src/lib/mint-cards.ts cites it for the
reasoning behind hydrating rather than moving the list to SSR, and a comment
pointing at a file nobody else has is worse than no comment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 16:34:54 +02:00
michilisandClaude Opus 5 24fe2003b6 Drop the nightly rebuild timer.
The timer was load-bearing while the mint list was a build-time snapshot: a
rebuild was the only way a new mint, a new review count or a changed status ever
reached /mints. The list hydrates now, so all three arrive within a second of
load, in every language, and rebuilding 2,000 pages at 03:30 to refresh numbers
that refresh themselves is twenty minutes of CPU for nothing.

cashumints-web.service stays exactly as it is — it is the deploy-time publish
step, and now the only thing that starts it is a deploy. A build still produces
what only a build can: the prerendered HTML a crawler reads, a social card per
mint, the sitemap and hreflang set, and a /mint/{host} page for every mint known
at build time.

The one thing that gets staler is that last item. A mint indexed since the last
deploy has no prerendered page: /mint/newhost is a 404, whose resolver looks the
address up against the live API and renders it — readable, reviewable, noindex
until a deploy gives it a real page. That was already true between nightly
builds; this only lengthens the window.

README documents the one-time host commands to remove the installed timer, and
gains a "Live lists" section describing what replaced it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 16:29:09 +02:00
michilisandClaude Opus 5 14548179a0 Hydrate the mint lists from the live API after paint.
/mints was a snapshot of whatever the API held when `astro build` ran, and stayed
that until the next build: a mint indexed at noon was reviewable at once — the 404
resolver saw to that — and simply had no card until 03:30. Every card's rating,
review count and status were as stale as the page.

The three index pages and the home page's three top-six strips now refetch
`GET /api/mints?type=…` once, after paint, and rebuild their grids. The
prerendered cards stay: they are the first paint, what a crawler indexes, and the
whole page without JavaScript. Hydration only ever replaces them with something
newer, and never with nothing — neither a failed fetch nor a well-formed empty
array touches a grid that has cards in it.

To make that affordable, the list payload grew the facts a chip is drawn from:
`nuts`, `capabilities`, and the two probed LNURL fields. /mints and /lnurl-mints
were fetching `GET /api/mints/:host` once per mint at build time to read two
booleans off each; that N+1 is gone from both, which takes the build from
fifty-six requests to one and is what makes the same read possible in a browser.
Additive: `MintDetail` already had all four.

web/src/lib/mint-cards.ts is MintCard.astro's parallel renderer, the same
relationship review-cards.ts has with the reviews panel. Same classes, same
data-* attributes — the sort, the search, the rank chips and the shared-element
view transitions all read the DOM — and the same i18n, through the page's own
inlined catalog rather than a build-time one.

Base.astro gained `clientNamespaces`, so the home page can inline the `home.`
catalog its strips need to rewrite "All 60 mints →" without putting 2KB of
marketing copy on 1,300 mint pages. check-i18n reads the prop off the page, so
the two cannot disagree.

Verified in Chromium against the built site: 60 prerendered cards become 61
including a mint inserted after the build; sort, search and hide-offline operate
on the new cards; /es/mints renders "En línea", "54 reseñas", "4,9" and "Solo
fundir"; JavaScript disabled still shows all 60; an aborted or empty API leaves
the grid alone; and a navigation away and back re-hydrates. 2016 pages build,
link and hreflang checks pass, 30 web tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 16:27:33 +02:00
michilisandClaude Opus 5 060c7f1a59 Refuse to publish a site built from a hollow index.
Health answering 200 and the index being complete are different claims. A year of
~31-event backfills left a perfectly healthy API serving a real, correct, complete
list of eight mints. A build against that succeeds — it prerenders eight cards —
and rsync --delete-after then replaces fifty-five with eight.

cashumints-web.service gains a second ExecStartPre after the health wait: count
/api/mints, and exit non-zero below MIN_MINTS_FOR_BUILD (default 20, overridable
with `systemctl edit`). A refusal aborts the unit before `pnpm build`, and
publishing is ExecStartPost, so the previously published site is untouched; the
OnFailure alert added in the last commit says why.

Counted by the "host": key rather than by counting braces, because the list
payload is about to carry a nested object per mint. A curl that fails at all
counts as zero, which is below every floor — so an API that fell over between the
health check and this line refuses the build instead of sailing through it.

Verified against three live APIs: 73 mints passes, a doctored 8-mint database
fails with the reason, and a dead port fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 16:13:16 +02:00
michilisandClaude Opus 5 0ebc8ada54 Make a crash loop reach somebody instead of scrolling past.
`Restart=on-failure` with no start limit is an infinite loop by definition: the
unit never reaches `failed`, `systemctl status` stays active (auto-restart), and
the only evidence is a journal moving at four lines a second. That is how 464
restarts over fifteen hours went unnoticed.

All three units now stop after five failures in 120s and run
OnFailure=cashumints-alert@%n.service. The window is 120s and not 60s because
RestartSec=5s plus a process that takes a few seconds to die can spread five
failures past a sixty second window, reset the counter, and loop forever anyway.

cashumints-alert@.service is a oneshot that takes the failed unit's name as its
instance. Configuration is /etc/cashumints/alert.env: NTFY_URL gets a plain-text
body, WEBHOOK_URL gets JSON carrying `content` so one payload fits Discord and
Slack-compatible endpoints. With neither set — or the file absent — it still
writes to the journal at ERROR via a `<3>` syslog prefix, so `journalctl -p err -t
cashumints-alert` is a complete history on a host nobody configured.

It cannot become a second thing to debug: each curl is bounded at 10s, each
failure falls back to a journal line, and the shell ends in `true`, so the alerter
always exits 0. Verified with systemd-analyze verify and by running the ExecStart
body against a local sink — the JSON parses, and every branch exits 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 16:10:01 +02:00
michilisandClaude Opus 5 9ffa53094d Make a starved discovery cycle say so, in the log and on /api/health.
For about a year the production RELAYS list did not include the relay carrying
the kind 38000/38172 archive. Every backfill read about thirty events, wrote them
faithfully, reported ok=true, and the nightly build republished an index of eight
mints. Nothing measured the difference between "the cycle completed" and "the
cycle read anything", so nothing went red.

Three signals now do:

  - Per-relay attribution. queryRelays() replaces pool.querySync(), which merges
    every relay into one deduplicated array and throws away who sent what. It
    keeps one subscription per relay over the pool's existing sockets and shares
    a single alreadyHaveEvent across them, so an event five relays carry is still
    verified once; receivedEvent fires before that check, which is what makes the
    per-relay count mean "what this relay contributed". The deadline moved out of
    each Subscription's own EOSE timer so `eose` means a frame arrived rather than
    something timed out.

  - A WARN naming any relay that will not connect, on every cycle, and any relay
    that connected and sent nothing, on backfills only. An incremental cycle is
    supposed to come back empty.

  - BACKFILL_MIN_EVENTS, default 200. Under it, ERROR discovery starvation
    suspected and a flag health reports as discovery_starved, forcing 503. Sticky
    across incremental cycles so an hourly cycle finding four events cannot clear
    what a backfill diagnosed; stored in the database so a restart cannot either.

A fresh database is starved until its first backfill lands. That is intended: it
holds the build's health gate rather than publishing a site made from nothing.

Verified against the live relay set — 1528 events, five relays connected, EOSE on
all five, health 200 — and against an unreachable list, which produces the two
WARN lines, the ERROR, and 503.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 16:07:01 +02:00
michilisandClaude Opus 5 65307ba278 Compile the API instead of running its TypeScript in production.
The unit's ExecStart named src/index.ts, so every start depended on the host
having Node 22.18 or newer for native type stripping. A deploy onto a host with
Node 20 met ERR_UNKNOWN_FILE_EXTENSION, exited in under a second, and was
restarted 464 times over fifteen hours with nothing anywhere going red.

api/tsconfig.json now emits to api/dist. The source keeps its explicit .ts import
specifiers, which is what makes `node --watch src/index.ts` work in development;
rewriteRelativeImportExtensions turns them into .js on the way out, so what runs
in production is ordinary ESM that any Node from 20.18 up will start.

`pnpm build` builds shared, then api, then web. `pnpm dev` is unchanged.

deploy/ is tracked rather than ignored: the unit files are the thing an operator
copies to /etc/systemd/system, and the alert unit added next has to live
somewhere a deploy can find it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 15:58:53 +02:00
Michilis 987d7b2d84 Merge pull request 'Clarify the empty-WEB_ROOT hint to point at cashumints-web.' (#7) from dev into main
Reviewed-on: #7
2026-08-25 04:46:47 +00:00
michilisandCursor 06ba3d35e7 Clarify the empty-WEB_ROOT hint to point at cashumints-web.
A bare pnpm build only fills dist; production needs the publish unit.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 06:46:23 +02:00
Michilis 4e22db5261 Merge pull request 'Serve the prerendered site from Node instead of nginx root.' (#6) from dev into main
Reviewed-on: #6
2026-08-25 04:28:30 +00:00
michilisandCursor 79a115be38 Serve the prerendered site from Node instead of nginx root.
Avoids www-data traversing the cashumints tree and keeps rebuilds from blanking a live root.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 06:27:27 +02:00
Michilis 8373853971 Merge pull request 'Dev' (#5) from dev into main
Reviewed-on: #5
2026-08-24 20:36:41 +00:00
michilisandCursor 301679d340 Wire locale catalogs, repair mint terminology, and confirm login with a toast.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-24 22:35:43 +02:00
michilisandCursor b95aab2bcd Improve write-review flow and require Node 22.18 for native TS stripping.
Gate login, reveal the form after a rating, and wire inline Rate this mint; drop --experimental-strip-types.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-24 21:52:11 +02:00
Michilis c1f4565bfe Merge pull request 'Dev' (#4) from dev into main
Reviewed-on: #4
2026-08-22 04:00:46 +00:00
michilisandCursor c74c7fc187 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>
2026-08-22 03:44:43 +02:00
michilisandCursor 2a9444942b Index, probe, and announce LNURL mints in the API.
Wire discovery and probing for LNURL mints, add rate-limited POST /api/index
for user submissions, and optionally announce confirmed state to relays.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 03:44:35 +02:00
michilisandCursor c97b44018d Add shared LNURL types, indexing helpers, and warnings.
Introduce lnurl as a first-class mint type with probe/announcement fields
and shared helpers the API and web can both rely on.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 03:44:27 +02:00
michilisandCursor 36c01861f5 Document LNURL mint kind and live probe notes.
Capture the NIP kind contract and wire-level NOTES so indexing and probing
can follow observed LNURL mint behavior rather than outdated assumptions.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 03:44:20 +02:00
Michilis f138066ee6 Merge pull request 'Add configurable Plausible analytics' (#3) from dev into main
Reviewed-on: #3
2026-08-21 04:11:35 +00:00
michilisandCursor be322cb0d8 Add configurable Plausible analytics
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-21 06:11:04 +02:00
121 changed files with 19803 additions and 2549 deletions
+24
View File
@@ -38,6 +38,11 @@ PUBLIC_API_URL=
# Canonical origin for canonical links, OpenGraph tags and the sitemap. # Canonical origin for canonical links, OpenGraph tags and the sitemap.
SITE_URL=https://cashumints.space SITE_URL=https://cashumints.space
# Plausible-compatible analytics script and the domain reported with each page view.
# Leave either value empty to disable analytics.
PLAUSIBLE_URL=https://analytics.azzamo.net/js/script.js
PLAUSIBLE_DOMAIN=cashumints.space
# Whether a rated mint's JSON-LD also carries the review-snippet-eligible Product # Whether a rated mint's JSON-LD also carries the review-snippet-eligible Product
# type next to Service (web/src/lib/schema.ts explains the trade). On by default; # type next to Service (web/src/lib/schema.ts explains the trade). On by default;
# set to 0 to ship the plain Service node on the next build, no code change needed. # set to 0 to ship the plain Service node on the next build, no code change needed.
@@ -91,6 +96,19 @@ SEO_PRODUCT_JSONLD=1
# so reviews the old site published to snort/primal were invisible to it. # so reviews the old site published to snort/primal were invisible to it.
RELAYS=wss://relay.cashumints.space,wss://nos.lol,wss://relay.azzamo.net,wss://relay.snort.social,wss://relay.primal.net RELAYS=wss://relay.cashumints.space,wss://nos.lol,wss://relay.azzamo.net,wss://relay.snort.social,wss://relay.primal.net
# How many events a backfill has to read before it counts as having read anything.
#
# A backfill asks every relay above for the whole history of four kinds; on a working
# relay list that is thousands of events. Under this floor, discovery logs
# `ERROR discovery starvation suspected` and /api/health answers 503 with
# `discovery_starved: true` until the next backfill clears it.
#
# This exists because a RELAYS list missing the relay that carries the announcement
# archive returned about thirty events per backfill for a year, reported ok=true every
# time, and left the index at eight mints with every health signal green. Lower it only
# for a private or test relay that genuinely holds less; 1 disables the check.
#BACKFILL_MIN_EVENTS=200
# Profile relays for the BUILD (kind 0, prerendered reviewer names on the home # Profile relays for the BUILD (kind 0, prerendered reviewer names on the home
# page). A wider pool than RELAYS on purpose: relay.cashumints.space holds no kind # page). A wider pool than RELAYS on purpose: relay.cashumints.space holds no kind
# 0 at all and snort/primal hold almost none, so the two aggregators below are what # 0 at all and snort/primal hold almost none, so the two aggregators below are what
@@ -151,6 +169,12 @@ FEDIMINT_OBSERVER_URL=https://observer.fedimint.org/api/federations
# which is the literal BACKEND.md behaviour and ranks noticeably worse. # which is the literal BACKEND.md behaviour and ranks noticeably worse.
SCORE_PRIOR_MEAN=3 SCORE_PRIOR_MEAN=3
# How many mints one address may submit to `POST /api/index` in an hour. Ten is several
# times what any honest use of the review dialog needs; raise it while exercising the
# whole flow end to end, which trips ten in about a minute. Behind a proxy, the limiter
# can only tell visitors apart if `X-Forwarded-For` is forwarded (see README, "API").
# INDEX_RATE_LIMIT=10
# ─── Tooling ───────────────────────────────────────────────────────────────── # ─── Tooling ─────────────────────────────────────────────────────────────────
# Dev server Boneyard captures against (`pnpm bones`). Defaults to WEB_PORT. # Dev server Boneyard captures against (`pnpm bones`). Defaults to WEB_PORT.
+350
View File
@@ -0,0 +1,350 @@
# NOTES-LNURL.md
What the LNURL mint endpoints **actually** return, recorded from the live reference
instance `https://lnurl.21mint.me` on 2026-08-21 and from a locally run
[dni/lnurl-mint](https://github.com/dni/lnurl-mint) (commit `a70cae1`) used to
reproduce the states the live instance does not currently exhibit.
Same rule as NOTES.md: where the software's README and the wire disagree, the wire
wins, and the disagreement is written down rather than quietly resolved.
---
## 1. The probe endpoint: `GET /.well-known/lnurlw/{username}`
**This is the LNURL equivalent of Cashu's `/v1/info`, and it is not the endpoint the
brief expected.** Findings, in the order they mattered:
### `/p` does not exist
The brief (and older readings of the project) describe `GET /p` as the LUD-06
payRequest carrying the `withdrawLink` mint advertisement. It is a **404** on the live
instance:
```
$ curl -i https://lnurl.21mint.me/p
HTTP/2 404
content-type: application/json
{"detail":"Not Found"}
```
The repository's own README says so explicitly — the LUD-16 well-known alias is
"this mint's **only** payRequest entry point (no separate bare `/p`)" — and
`router.py` registers no such route. `/p/cb` (the callback) exists; bare `/p` never
did. Note the body shape: `{"detail":"Not Found"}` with a real 404 status is FastAPI's
*unmatched route* handler. Every **registered** route answers errors as LNURL does
instead (see §5), so this shape is a reliable "this software does not have that
endpoint" signal.
### Both well-known aliases answer, and only one carries the limits
| Endpoint | Carries |
| --- | --- |
| `GET /.well-known/lnurlp/{username}` | LUD-06 payRequest: `minSendable`/`maxSendable`, `metadata`, `withdrawLink` |
| `GET /.well-known/lnurlw/{username}` | LUD-03-shaped withdrawRequest: `minWithdrawable`/`maxWithdrawable`, `defaultDescription`, `mintPubkey`, `payLink`, node identity |
`{username}` is the configured `USERNAME` (default `mint`) **or** the LUD-16 reserved
bare-domain `_`. Both resolve to the identical mint identity; `_` is used as the probe
path because it needs no prior knowledge of the operator's chosen username.
**The chosen primary probe endpoint is `/.well-known/lnurlw/_`**, because it is the one
carrying the withdraw limits, the description, and the mint's node identity — every
field the site renders. `/.well-known/lnurlp/_` is fetched as a **fallback only**, when
the withdraw side does not answer.
### Live response, `/.well-known/lnurlw/_`
```json
{
"tag": "withdrawRequest",
"callback": "https://lnurl.21mint.me/w",
"minWithdrawable": 5000,
"maxWithdrawable": 999899000,
"defaultDescription": "lnurlcash bearer note on lnurl.21mint.me",
"mintPubkey": "021ab89df9cbd28cdab8c71d228ca40deba1eaebd827f24849ec4f3e919c023555",
"payLink": "https://lnurl.21mint.me/.well-known/lnurlp/mint",
"nodeAlias": "Azzamo",
"nodeUri": "021ab89df9cbd28cdab8c71d228ca40deba1eaebd827f24849ec4f3e919c023555@145.239.92.138:9736",
"nodeColor": "#68f442",
"nodeCapacity": 30027500000,
"nodeNumChannels": 8,
"nodeNumPeers": 18
}
```
Headers: `server: nginx/1.22.1`, `content-type: application/json`. **No version header
of any kind**, on this or any other endpoint.
Notes on the fields:
- **All amounts are millisatoshi.** `minWithdrawable: 5000` is 5 sat;
`maxWithdrawable: 999899000` is 999,899 sat. `nodeCapacity` is msat too
(30,027,500 sat here). Divide by 1000 for display.
- `maxWithdrawable` is **fee-adjusted**: it is `MAX_SENDABLE_MSAT` minus the mint fee
at that amount (`router.max_mintable_msat`), not the raw setting. Likewise
`minWithdrawable` is `MIN_MINT_MSAT`. So these two numbers are the bounds a freshly
minted note's *value* can fall into — which is exactly what a reader wants — and not
the bounds of what they must *pay*. The payRequest's `minSendable`/`maxSendable` are
the pay-side numbers, and they differ (11,000 vs 10,000 msat locally, because of the
1000 msat base fee).
- `defaultDescription` is always `"lnurlcash bearer note on {host}"` — generated, never
operator-written. Useful as a description fallback and nothing more; it says what the
software is, not what this mint is.
- **`k1` is absent.** The model declares it `str | None = None`; its absence from the
wire is the first proof that `response_model_exclude_none = True` is in force
(`error_handler.py:26`). That matters for everything in §3.
### Live response, `/.well-known/lnurlp/_`
```json
{
"tag": "payRequest",
"callback": "https://lnurl.21mint.me/p/cb",
"minSendable": 6000,
"maxSendable": 1000000000,
"metadata": "[[\"text/plain\", \"Mint an lnurlcash bearer note on lnurl.21mint.me\"], [\"text/identifier\", \"_@lnurl.21mint.me\"], [\"text/plain\", \"Mint fees: 1000,100\"]]",
"withdrawLink": "https://lnurl.21mint.me/w"
}
```
- `metadata` is a **JSON-encoded string containing a JSON array** of `[mime, value]`
pairs — parse twice.
- `text/identifier` **echoes the username actually queried**. Querying `_` returns
`_@lnurl.21mint.me`; querying `mint` returns `mint@lnurl.21mint.me`. So the probe
cannot read the operator's real username off the `_` response. It reads it off
`payLink` on the withdraw side instead (`…/lnurlp/mint` → username `mint`), which is
built from `settings.username` unconditionally.
- The `Mint fees: <base_msat>,<ppm>` entry is present only when a fee is configured;
its absence means fee-free, per spec.
---
## 2. Every other endpoint, and what it is worth to a probe
| Endpoint | Live result | Verdict |
| --- | --- | --- |
| `GET /` | 200, 13,804 bytes of HTML, `<title>21 lnurl-mint</title>` | Fetched **only** for the onion address (§4) |
| `GET /openapi.json` | 200, `info.version` = `"0.1.0"`, `info.title` = `"lnurl-mint"` | **The only version source there is** (§6) |
| `GET /docs` | 200, Swagger UI | Ignored |
| `GET /w` (no `k1`) | 200 `{"status":"ERROR","reason":"Request validation error: Field required \`k1\`."}` | Ignored — never a mint advertisement |
| `GET /w?k1=deadbeef` | 200 `{"status":"ERROR","reason":"Unknown note."}` | Ignored — per-note, not per-mint |
| `GET /verify/{hash}` | 200 `{"status":"ERROR","reason":"Not found"}` | **Useless as a capability probe** (§5) |
| `GET /p` | 404 `{"detail":"Not Found"}` | Does not exist |
The site probes **`/.well-known/lnurlw/_`, then `/.well-known/lnurlp/_` on failure, and
`GET /openapi.json` + `GET /` opportunistically**. Nothing else is fetched, and nothing
that mutates state is ever called — `/p/cb` would make the mint issue a real invoice on
every probe cycle, which is not a thing a directory gets to do to a stranger's node.
---
## 3. The degraded state: no funding source (**empirically reproduced**)
The README: *"Without one, minting and melting are unavailable (rotate/split/merge of
existing notes still work)."* The question was whether that is visible over HTTP. It is.
`_mint_address_response` (`router.py:480`) only populates the node fields
`if funding_source.backend:`, and wraps the lookup in `try/except` that logs and leaves
them `None` on failure. With `response_model_exclude_none = True`, `None` fields are
**omitted from the wire entirely**.
Run locally with no `FUNDINGSOURCE_*` set at all:
```
$ curl http://127.0.0.1:8137/.well-known/lnurlw/_
{"tag":"withdrawRequest","callback":"https://lnurl.test/w","minWithdrawable":10000,
"maxWithdrawable":999999000,"defaultDescription":"lnurlcash bearer note on lnurl.test",
"payLink":"https://lnurl.test/.well-known/lnurlp/mint"}
```
`mintPubkey`, `nodeAlias`, `nodeUri`, `nodeColor`, `nodeCapacity`, `nodeNumChannels`
and `nodeNumPeers` are **all gone**. The response is otherwise a completely valid
withdrawRequest with real limits — the mint is up, serving, and answering. Its startup
log says the rest out loud:
```
WARNING:root:No funding source configured (FUNDINGSOURCE_BACKEND unset) -
minting, melting, and offline verification are all unavailable.
```
`/.well-known/lnurlp/_` **still answers normally** in this state (200, full payRequest),
so the pay side is *not* a degraded-state signal — only the withdraw side is.
### The detection rule, and the honest thing to say about it
> **`mintPubkey` absent from a valid mint-address response ⇒ no funding source is
> reachable ⇒ minting and melting are unavailable right now.**
One bit, and it necessarily conflates two causes:
1. no funding source was ever configured, and
2. one was configured but the node is unreachable at this moment (`except Exception`
above catches it and returns the same reduced response).
They are **not distinguishable over HTTP**, and the site does not try. It does not need
to: the user-facing consequence is identical in both cases — nothing moves in or out
over Lightning until it comes back — and that is exactly what the warning says. Calling
it "no funding source" specifically would be a guess; calling it "minting and melting
unavailable" is the observation.
The same bit also carries two other meanings, which is worth stating plainly because
three site features read it:
- it is the `signed-notes` capability (`signing.mint_pubkey` needs the same funding
source, and the README ties them: *"without a funding source, both fields are simply
omitted"*), and
- it is the canonical `d` identifier for the Nostr announcement (see
`docs/KIND-LNURL-MINT.md`).
That last one is why the `d` rule has to be sticky: a mint indexed while its node was
up must not silently change identity because its node had a bad minute. See the doc.
---
## 4. Tor / onion: HTML only, and only over clearnet
`ONION_URL` is **never** in any JSON response. It appears in exactly one place: the
one-pager's "Also via Tor" block (`frontend._tor_section`), and only when the request
did *not* arrive over the onion host itself (otherwise `public_base_url` already made
the onion the primary and the block would be a duplicate). A clearnet probe is
therefore the case where it does show.
Reproduced locally with `ONION_URL` set:
```html
<h2>Also via Tor</h2>
<div class="qr">…</div>
<button class="copy" data-copy="LNURL1…" title="Copy LNURL">LNURL1…</button>
<button class="copy" data-copy="mint@abcdefgh….onion" title="Copy lightning address">⚡ mint@…onion</button>
```
Cheaply parseable: scan the `GET /` body for a `*.onion` host. The live instance
advertises none, so `onion` is absent there.
**Caveat worth recording:** the fixture address in lnurl-mint's own tests
(`abcdefghijklmnop1234567890abcdefghijklmnop1234567890abcdefgh.onion`) contains
`0`, `1`, `8` and `9`, which are **not in base32's alphabet** — it is not a valid v3
address. A strict `[a-z2-7]{56}` matcher rejects it. The site's matcher is deliberately
looser (`[a-z0-9]{16,60}\.onion`) so that a real-but-unusual address is still shown;
nothing is fetched over Tor, so a wrong match costs a displayed string and nothing more.
---
## 5. LUD-21 verify is **not** probe-detectable (and why the site does not pretend)
`VERIFY_ENABLED=false` makes `/verify/{payment_hash}` raise `HTTPException(404, "Not
found")`. An *enabled* endpoint asked about a payment hash it has never seen raises
`HTTPException(404, "Not found")` too. Both then pass through
`LnurlErrorResponseHandler`, which converts any `HTTPException` into
`200 {"status": "ERROR", "reason": <detail>}`.
Measured, same binary, both settings:
```
VERIFY_ENABLED=true → 200 {"status":"ERROR","reason":"Not found"}
VERIFY_ENABLED=false → 200 {"status":"ERROR","reason":"Not found"}
```
**Byte-identical.** There is no status code, no header, and no reason string that
separates them.
The only place a `verify` URL is genuinely advertised is the response of `/p/cb` — the
pay callback — and calling that **creates a real Lightning invoice on the operator's
node**. Doing that every ten minutes, forever, to every mint in the index, is not
acceptable behaviour for a directory.
**Resolution:** `lud21` is an **announcement-only** capability. The probe never sets it
and never clears it. If an operator declares it in their `features` tag, the site shows
it; otherwise the row is simply absent. This is written into
`docs/KIND-LNURL-MINT.md` as a normative property of the vocabulary, not left as an
implementation quirk.
---
## 6. Version: `/openapi.json`, and only there
No `Server`, `X-Powered-By` or any other header identifies the software — nginx fronts
it and reports only itself. The one-pager's `<title>` is operator-configurable
(`settings.title`; the live instance sets `"21 lnurl-mint"`), so it identifies nothing
reliably.
FastAPI's generated `GET /openapi.json` carries both:
```json
{"info": {"title": "lnurl-mint",
"description": "Minimal lnurlcash (LUD-25, Lightning bearer assets) mint - LUD-03/LUD-06 only.",
"version": "0.1.0"}}
```
- `info.title` = `"lnurl-mint"` — a genuine software fingerprint, hardcoded in
`server.py`, not operator-settable.
- `info.version` = `__version__`, resolved from installed package metadata
(`lnurl_mint/__init__.py`), falling back to `LNURL_MINT_VERSION` or
`"0.0.0+unknown"`.
Two observed values: the live instance reports `0.1.0`; a source checkout with no
installed metadata reports `0.0.0+unknown`. The site stores `lnurl-mint/<version>`,
and treats a `0.0.0+unknown` as **no version** rather than displaying it — it is the
library's "I don't know" sentinel, not a release.
The live instance still serves `/docs` from a jsdelivr CDN, whereas HEAD (`a70cae1`,
*"report real version in openapi, vendor swagger ui for /docs"*) vendors it. So the
deployed build predates that commit, and its `0.1.0` is the pre-commit hardcoded value.
Recorded because it means **a version string from this endpoint is a weak signal**: it
was not always wired to the real package version.
---
## 7. Error convention, and what "online" must therefore mean
Every **registered** route returns HTTP **200** for its errors, with an LNURL body:
```json
{"status": "ERROR", "reason": "…"}
```
Only an **unregistered path** gives a real 404 (`{"detail":"Not Found"}`).
Consequence, and it is the single most important parsing rule here: **HTTP 200 does not
mean the mint is up.** A probe that stopped at the status code would call every
misconfigured host, every `Unknown user.`, and every wrong-path hit "online".
So the site's online test is on the parsed body, never the status:
- `tag == "withdrawRequest"` **and** numeric `minWithdrawable`/`maxWithdrawable` → online
(or degraded, per §3);
- `tag == "payRequest"` with numeric `minSendable`/`maxSendable` (fallback endpoint) → online;
- anything else that parses as JSON — including `{"status":"ERROR"}` — → **invalid**, a
distinct third outcome that is neither online nor offline. It renders the
"Endpoint responding but invalid" banner, because a host that answers with something
that is not a mint advertisement is a different problem from a host that does not
answer at all.
---
## 8. Summary of what the endpoints expose beyond the withdraw params
Everything below is real, observed, and stored in `ecosystem_json`:
| Field | Source | Used for |
| --- | --- | --- |
| `mintPubkey` | lnurlw | `d` identifier, `signed-notes` feature, degraded detection, sidebar row |
| `payLink` | lnurlw | Deriving the LUD-16 lightning address (`{username}@{host}`) |
| `nodeAlias` | lnurlw | Identity header fallback name |
| `nodeUri` | lnurlw | `pubkey@host:port` — the sidebar's node connect string |
| `nodeColor` | lnurlw | Not rendered (a node's own colour is not this site's palette) |
| `nodeCapacity` | lnurlw | msat, publicly announced channel capacity — sidebar |
| `nodeNumChannels` / `nodeNumPeers` | lnurlw | Sidebar |
| `defaultDescription` | lnurlw | Description fallback |
| `minSendable` / `maxSendable` | lnurlp | Pay-side limits, distinct from withdraw limits |
| `metadata` → `text/plain` | lnurlp | Description fallback, ranked above `defaultDescription` |
| `metadata` → `text/identifier` | lnurlp | Confirms the lightning address |
| `metadata` → `Mint fees:` | lnurlp | Mint fee disclosure (base msat, ppm) |
| `info.version` / `info.title` | openapi | Software cell |
| `*.onion` | `GET /` HTML | `onion` feature, sidebar row |
`nodeCapacity` is explicitly the **publicly announced** capacity only — lnurl-mint
sources it from the public graph (`lnd GetNodeInfo` / `cln listchannels`), never from a
private `listfunds`/`ListChannels` view. Displaying it discloses nothing the node's own
gossip does not already.
+32
View File
@@ -130,6 +130,38 @@ are matched by bare domain (too loose, a review of `mint.minibits.cash/Bitcoin`
`mint.minibits.cash/Anything`). The new backend fixes both directions: one normalizer, path is part `mint.minibits.cash/Anything`). The new backend fixes both directions: one normalizer, path is part
of identity, applied to mints and reviews alike (see `api/src/normalize.ts`). of identity, applied to mints and reviews alike (see `api/src/normalize.ts`).
### The slug is deterministic, not reversible
Found while building on-demand indexing (`POST /api/index`), which is the first thing that
has to turn a *page address* back into a *mint address* rather than the other way round.
`mint.example.com/Bitcoin` slugs to `mint.example.com-bitcoin`, and so would a mint whose
hostname really is `mint.example.com-bitcoin`. Looking a row up is unaffected — both
spellings are stored, and the slug column is what the lookup uses — but *indexing* from a
slug means opening a socket to whatever it decodes to, and guessing wrong there means
probing, and possibly listing, somebody else's server.
So `addressFromSlug` (shared/src/indexing.ts) only derives an address from the one
unambiguous shape: a plain hostname, optionally with the `-3338` port suffix. Anything
else returns null and the 404 page says it cannot work the address out from the link
alone, offering the dialog where a reader can paste the full URL including its path.
The `lnurl-` collision prefix is deliberately *not* stripped there either: a slug only
takes that prefix at insert time, so a link carrying one describes a row that already
exists and never reaches that code, while a hostname that merely begins `lnurl-` is a
real possibility.
Two URL shapes that fought the normalizer while testing this, both now covered by
`api/src/check-index.ts`:
- **The port suffix round trip.** `https://smilemoji.cash:3338` slugs to
`smilemoji.cash-3338`, which reads as a hostname with a trailing number until you know
the rule. The derivation has to put the colon back, and only for a 2 to 5 digit tail.
- **Case, scheme and trailing slash together.** `MINT.600.WTF`, `http://mint.600.wtf`,
`mint.600.wtf/` and `https://mint.600.wtf:443/` are one mint, and the endpoint has to
answer "already indexed" for all four rather than creating a second row. They collapse
in `normalizeMintUrl`, before the row is looked up and before the in-flight map is
keyed, which is what makes the dedup and the lookup agree.
## Old scoring ## Old scoring
`src/hooks/usePopularMints.ts`: `src/hooks/usePopularMints.ts`:
+854 -148
View File
File diff suppressed because it is too large Load Diff
+10 -7
View File
@@ -4,14 +4,17 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "node --env-file-if-exists=../.env --experimental-strip-types --watch src/index.ts", "build": "tsc -p tsconfig.json",
"start": "node --env-file-if-exists=../.env --experimental-strip-types src/index.ts", "dev": "node --env-file-if-exists=../.env --watch src/index.ts",
"seed": "node --env-file-if-exists=../.env --experimental-strip-types src/seed.ts", "start": "node --env-file-if-exists=../.env dist/index.js",
"migrate": "node --env-file-if-exists=../.env --experimental-strip-types src/migrate.ts", "seed": "node --env-file-if-exists=../.env src/seed.ts",
"migrate": "node --env-file-if-exists=../.env src/migrate.ts",
"typecheck": "tsc -p tsconfig.json --noEmit", "typecheck": "tsc -p tsconfig.json --noEmit",
"test": "node --env-file-if-exists=../.env --experimental-strip-types src/check.ts", "test": "node --env-file-if-exists=../.env src/check.ts",
"test:warnings": "node --env-file-if-exists=../.env --experimental-strip-types src/check-warnings.ts", "test:warnings": "node --env-file-if-exists=../.env src/check-warnings.ts",
"test:offline": "node --env-file-if-exists=../.env --experimental-strip-types src/check-offline.ts" "test:offline": "node --env-file-if-exists=../.env src/check-offline.ts",
"test:lnurl": "node --env-file-if-exists=../.env src/check-lnurl.ts",
"test:index": "node src/check-index.ts"
}, },
"dependencies": { "dependencies": {
"@cashumints/shared": "workspace:*", "@cashumints/shared": "workspace:*",
+253
View File
@@ -0,0 +1,253 @@
/**
* Publishing `kind:38174` announcements for LNURL mints this indexer has confirmed.
*
* **This writes to public relays.** Nostr has no delete that anyone is obliged to
* honour, so an event published here is on the network permanently, signed by this
* site's key and attributed to it. That is the whole reason for the gating below, and
* the reason it is two switches rather than one.
*
* Why publish at all: 38174 is a proposed kind (see `docs/KIND-LNURL-MINT.md`) and a
* proposed kind with no events is a document rather than a protocol. An indexer that
* has already probed a mint and confirmed what it serves is the one party in a position
* to put honest announcements on the network before any operator has heard of the kind.
*
* ## What is announced, and what deliberately is not
*
* Only what the probe actually **observed**. The vocabulary has eleven values; this
* publishes at most seven of them, and the four it never publishes are the point:
*
* - `rotate`, `split`, `merge` — these are only provable by calling `/w/cb`, which
* mutates or destroys a note. Not probeable, not published, even though the one
* implementation in existence supports all three unconditionally. Inferring them
* from a version string would be publishing a guess under this site's signature.
* - `lud21` — genuinely undetectable: verify-disabled and unknown-payment-hash return
* byte-identical responses. See NOTES-LNURL.md §5.
*
* An operator's own announcement can and should claim more; theirs is a statement about
* what they built, this is a statement about what was seen. When both exist the newer
* `created_at` wins, which is normal addressable-event behaviour and means an operator
* takes over their own listing simply by publishing one.
*/
import { finalizeEvent, getPublicKey, type EventTemplate } from 'nostr-tools/pure';
import { SimplePool } from 'nostr-tools/pool';
import * as nip19 from 'nostr-tools/nip19';
import {
KIND_LNURL_ANNOUNCEMENT, lnurlIdentifier, observedFeatures, type LnurlFields,
} from '@cashumints/shared';
import { announceConfig } from './config.ts';
import { getDb, getState, setState } from './db.ts';
import { log } from './log.ts';
import { parseEcosystem, type MintRow } from './mints.ts';
/**
* Republish interval.
*
* An addressable event is replaced, not duplicated, so republishing is cheap — but it
* is still traffic to somebody else's relay and a new `created_at` on every cycle would
* make this site's announcement permanently outrank an operator's own. Content changes
* are published immediately; an unchanged announcement is refreshed once a week, which
* keeps it from ageing out of relays that prune.
*/
const REPUBLISH_AFTER_S = 7 * 24 * 60 * 60;
/** State-table key holding what was last published for one mint, and when. */
const stateKey = (url: string): string => `announce:${url}`;
/**
* The secret key to sign with, as 32 bytes.
*
* Accepts an `nsec1…` or bare hex. Returns null — with a loud log line rather than a
* throw — for anything else: a misconfigured key must stop announcements and must not
* stop the indexer, which has a directory to serve either way.
*/
export function parseAnnounceKey(raw: string): Uint8Array | null {
const value = raw.trim();
if (!value) return null;
if (value.startsWith('nsec1')) {
try {
const decoded = nip19.decode(value);
if (decoded.type === 'nsec') return decoded.data;
} catch {
return null;
}
return null;
}
if (!/^[0-9a-f]{64}$/i.test(value)) return null;
return Uint8Array.from(Buffer.from(value, 'hex'));
}
/**
* The `features` this site is willing to sign for, from one mint's stored probe.
*
* Thin, and deliberately: the rule about what a probe may claim lives in
* `observedFeatures` in shared/, next to the vocabulary it draws from, so the list the
* page renders and the list this signs cannot drift apart. See that function for why
* `rotate`, `split`, `merge` and `lud21` are never among them.
*/
export function announcedFeatures(fields: LnurlFields): string[] {
return observedFeatures({
fundingAvailable: fields.funding_available,
maxWithdrawableMsat: fields.max_withdrawable_msat,
maxSendableMsat: fields.max_sendable_msat,
lightningAddress: fields.lightning_address,
mintPubkey: fields.mint_pubkey,
onionUrl: fields.onion_url,
});
}
/** The event this site would publish for one confirmed LNURL mint. */
export function announcementTemplate(
row: MintRow,
fields: LnurlFields,
createdAt: number,
): EventTemplate {
const tags: string[][] = [
['d', lnurlIdentifier(fields.base_url, fields.mint_pubkey)],
['u', fields.base_url],
];
const features = announcedFeatures(fields);
if (features.length > 0) tags.push(['features', features.join(',')]);
// Only when the mint said so. Absent reads as mainnet per the kind document, and
// asserting mainnet on a mint that never claimed a network would be this site
// inventing the one fact that decides whether the money is real.
if (fields.network) tags.push(['n', fields.network]);
/*
* `content` is left empty unless the mint published a name of its own.
*
* Empty content means "use the publisher's kind 0", per NIP-87 and the kind document
* — and the publisher here is this site, whose kind 0 is this site. That is the
* honest default for an announcement this site wrote: it is not the mint speaking.
* A name the mint itself served (its node alias) is worth passing on, and nothing else
* from the row is, because everything else came from a previous announcement.
*/
const content = row.name ? JSON.stringify({ name: row.name }) : '';
return { kind: KIND_LNURL_ANNOUNCEMENT, created_at: createdAt, tags, content };
}
/** Stable fingerprint of an announcement's meaning, for the change detector. */
function fingerprint(template: EventTemplate): string {
return JSON.stringify([template.tags, template.content]);
}
/**
* Which rows are eligible.
*
* Confirmed by probing, and only that: `status = 'online'` with a base URL and a
* withdraw ceiling means an advertisement genuinely parsed at some point. A mint that
* has never answered, is offline, or is only "responding but invalid" is never
* announced — this site does not put its signature on the existence of something it has
* not seen.
*
* A `degraded-funding` mint **is** announced: it is up and serving, and the reduced
* `features` list already tells the whole story.
*/
function eligible(row: MintRow, fields: LnurlFields | null): fields is LnurlFields {
if (row.type !== 'lnurl' || row.status !== 'online') return false;
if (!fields?.base_url) return false;
if (fields.invalid_reason) return false;
return fields.max_withdrawable_msat !== null;
}
export interface AnnounceResult {
eligible: number;
published: number;
skipped: number;
failed: number;
}
/**
* Publish an announcement for every confirmed LNURL mint that needs one.
*
* Returns counts rather than throwing: a relay refusing an event is not a reason for a
* probe cycle to fail. Does nothing at all, and says why once, when announcing is off —
* which is the default and is how every deployment that has not opted in behaves.
*/
export async function announceLnurlMints(
now = Math.floor(Date.now() / 1000),
): Promise<AnnounceResult> {
const empty: AnnounceResult = { eligible: 0, published: 0, skipped: 0, failed: 0 };
const settings = announceConfig();
if (!settings) return empty;
const secret = parseAnnounceKey(settings.secretKey);
if (!secret) {
log.error('announce disabled: ANNOUNCE_KEY is not an nsec or 64 hex characters');
return empty;
}
const db = await getDb();
const rows = await db.all<MintRow>(`SELECT * FROM mints WHERE type = 'lnurl'`);
const pool = new SimplePool();
const result: AnnounceResult = { ...empty };
try {
for (const row of rows) {
const fields = parseEcosystem<LnurlFields>(row);
if (!eligible(row, fields)) continue;
result.eligible++;
const template = announcementTemplate(row, fields, now);
const mark = fingerprint(template);
const previous = await getState(stateKey(row.url));
if (previous) {
try {
const { mark: lastMark, at } = JSON.parse(previous) as { mark: string; at: number };
if (lastMark === mark && now - at < REPUBLISH_AFTER_S) {
result.skipped++;
continue;
}
} catch {
// Unreadable marker: republish, which is the safe direction.
}
}
const event = finalizeEvent(template, secret);
/*
* `Promise.allSettled`, not `Promise.all`: one relay refusing the event (rate
* limits, a paid-relay policy, a write it does not accept) must not stop the
* others from taking it. One acceptance is a successful publish.
*/
const outcomes = await Promise.allSettled(pool.publish(settings.relays, event));
const accepted = outcomes.filter((o) => o.status === 'fulfilled').length;
if (accepted === 0) {
result.failed++;
log.warn('announcement rejected by every relay', {
url: row.url,
d: template.tags[0]?.[1],
relays: settings.relays.length,
});
continue;
}
await setState(stateKey(row.url), JSON.stringify({ mark, at: now }));
result.published++;
log.info('announced lnurl mint', {
url: row.url,
d: template.tags[0]?.[1],
features: template.tags.find((t) => t[0] === 'features')?.[1] ?? '',
relays: `${accepted}/${settings.relays.length}`,
event: event.id,
});
}
} finally {
try {
pool.close(settings.relays);
} catch {
// A relay that is already gone throws on close. Nothing to do about it.
}
}
if (result.eligible > 0) {
log.info('announce cycle', { ...result, pubkey: getPublicKey(secret) });
}
return result;
}
+474
View File
@@ -0,0 +1,474 @@
/**
* pnpm --filter ./api test:index
*
* The checks for on-demand indexing (`POST /api/index`). Same shape as `check.ts`: no
* framework, throws on the first failure, prints a count.
*
* Three things are being defended here, and they are in descending order of how bad it
* would be to get them wrong:
*
* 1. **SSRF.** This is the one endpoint that fetches an address a stranger chose, so
* every refusal it makes is asserted against a resolver and a fetch that this file
* controls. Nothing here touches the network: a rule that can only be exercised by
* pointing the test at a real host is a rule that stops being exercised the first
* time CI runs offline.
* 2. **Slug collapse.** Two spellings of one mint must never become two rows with half
* its reviews on each. That is the bug the normalizer was written for, and this
* endpoint is a new way to reintroduce it — a reader can now type the spelling
* discovery never saw.
* 3. **Invite decoding**, which is what makes a Fedimint submission possible at all.
*/
import assert from 'node:assert/strict';
import {
checkIndexInput,
federationIdFromInviteCode,
isNut06Info,
isPrivateIpAddress,
lnurlKey,
normalizeMintUrl,
typeForPath,
} from '@cashumints/shared';
import { checkDestination, safeFetchText, type FetchDeps } from './safe-fetch.ts';
import { RATE_LIMIT, resetRateLimits, takeToken } from './rate-limit.ts';
/** A real code off the relay pool, the same one `check.ts` parses an announcement from. */
const REAL_INVITE =
'fed11qvqzggnhwden5te0v9cxjtn9vd3jue3wvfkxjmnyva6kzunyd9skutnwv46z7qqqzc28wumn8ghj7' +
'end9e3hgunz9e5k7tmhwvhszqfq4m9xejq0l3fsh5k4fvyks8mwmwdyzhpk9e909l3atczpxuqxlgss2f35eg';
let checks = 0;
function check(name: string, fn: () => void): void {
try {
fn();
checks++;
} catch (err) {
console.error(`FAIL: ${name}`);
throw err;
}
}
async function checkAsync(name: string, fn: () => Promise<void>): Promise<void> {
try {
await fn();
checks++;
} catch (err) {
console.error(`FAIL: ${name}`);
throw err;
}
}
/* ---------- address rules ---------- */
check('every private, loopback and link-local range is refused', () => {
for (const address of [
'127.0.0.1', '127.1.2.3', '10.0.0.1', '10.255.255.255',
'172.16.0.1', '172.20.10.5', '172.31.255.255',
'192.168.0.1', '192.168.1.5',
'169.254.169.254', // the cloud metadata endpoint, the reason this exists
'0.0.0.0', '100.64.0.1', // this-network and carrier-grade NAT
'224.0.0.1', '255.255.255.255',
'::1', '::', 'fe80::1', 'fc00::1', 'fd12:3456::1', '::ffff:127.0.0.1', '::ffff:10.0.0.1',
]) {
assert.equal(isPrivateIpAddress(address), true, `${address} must be refused`);
}
});
check('ordinary public addresses are not', () => {
for (const address of ['1.1.1.1', '8.8.8.8', '157.245.26.63', '172.15.0.1', '172.32.0.1', '2606:4700::1111']) {
assert.equal(isPrivateIpAddress(address), false, `${address} must be allowed`);
}
});
await checkAsync('a URL whose hostname is a private literal never reaches DNS', async () => {
// If any of these consulted the resolver, this one would throw rather than answer.
const explode: FetchDeps = {
resolve: () => {
throw new Error('a literal address must not be resolved');
},
};
for (const url of [
'https://127.0.0.1/v1/info',
'https://10.0.0.1/v1/info',
'https://172.16.4.4/v1/info',
'https://192.168.1.5/v1/info',
'https://169.254.169.254/latest/meta-data/',
'https://[::1]/v1/info',
'https://localhost/v1/info',
'https://mint.local/v1/info',
'https://abcdefghij234567.onion/v1/info',
]) {
const verdict = await checkDestination(new URL(url), explode);
assert.equal(verdict?.kind, 'blocked', `${url} must be refused`);
}
});
await checkAsync('a public-looking name that resolves privately is refused', async () => {
const deps: FetchDeps = { resolve: async () => ['10.0.0.5'] };
const verdict = await checkDestination(new URL('https://internal.example.com'), deps);
assert.equal(verdict?.kind, 'blocked');
// One private answer among several is enough: the socket would pick one of them.
const mixed: FetchDeps = { resolve: async () => ['93.184.216.34', '127.0.0.1'] };
assert.equal((await checkDestination(new URL('https://mixed.example.com'), mixed))?.kind, 'blocked');
const public_: FetchDeps = { resolve: async () => ['93.184.216.34'] };
assert.equal(await checkDestination(new URL('https://mint.example.com'), public_), null);
});
await checkAsync('a name that does not resolve is unresolved, not blocked', async () => {
// The distinction the rugged-mint case turns on: a mint whose operator let the domain
// lapse must reach the Nostr lookup, not be rejected as an inadmissible address.
const gone: FetchDeps = { resolve: async () => null };
const verdict = await checkDestination(new URL('https://gone.example.com'), gone);
assert.equal(verdict?.kind, 'unresolved');
const outcome = await safeFetchText(
'https://gone.example.com/v1/info',
{ accept: 'application/json' },
{ ...gone, fetchImpl: (() => { throw new Error('must not connect'); }) as unknown as typeof fetch },
);
assert.equal(outcome.state, 'unreachable');
});
await checkAsync('http is refused outright: this endpoint is https only', async () => {
assert.equal((await checkDestination(new URL('http://mint.example.com')))?.kind, 'blocked');
assert.equal((await checkDestination(new URL('ftp://mint.example.com')))?.kind, 'blocked');
});
/* ---------- redirects ---------- */
/** A fetch that answers from a table, and records every URL it was asked for. */
function scriptedFetch(routes: Record<string, Response>): { fetch: typeof fetch; seen: string[] } {
const seen: string[] = [];
const impl = (async (input: unknown): Promise<Response> => {
const url = String(input);
seen.push(url);
const res = routes[url];
if (!res) throw new Error(`unexpected fetch of ${url}`);
return res;
}) as typeof fetch;
return { fetch: impl, seen };
}
await checkAsync('a redirect to a private address is refused before it is fetched', async () => {
const { fetch: impl, seen } = scriptedFetch({
'https://mint.example.com/v1/info': new Response(null, {
status: 302,
headers: { location: 'https://169.254.169.254/latest/meta-data/' },
}),
});
const outcome = await safeFetchText(
'https://mint.example.com/v1/info',
{ accept: 'application/json' },
{ fetchImpl: impl, resolve: async () => ['93.184.216.34'] },
);
assert.equal(outcome.state, 'blocked');
assert.match(outcome.state === 'blocked' ? outcome.reason : '', /redirected/);
// The crux: the metadata endpoint was never connected to, only reasoned about.
assert.deepEqual(seen, ['https://mint.example.com/v1/info']);
});
await checkAsync('a redirect to a name that resolves privately is refused too', async () => {
const { fetch: impl, seen } = scriptedFetch({
'https://mint.example.com/v1/info': new Response(null, {
status: 301,
headers: { location: 'https://internal.example.com/v1/info' },
}),
});
const outcome = await safeFetchText(
'https://mint.example.com/v1/info',
{ accept: 'application/json' },
{
fetchImpl: impl,
resolve: async (host) => (host === 'mint.example.com' ? ['93.184.216.34'] : ['10.1.2.3']),
},
);
assert.equal(outcome.state, 'blocked');
assert.equal(seen.length, 1, 'the private hop must never be fetched');
});
await checkAsync('two redirects are followed, a third is not', async () => {
const ok = { 'content-type': 'application/json' };
const routes: Record<string, Response> = {
'https://a.example.com/v1/info': new Response(null, {
status: 302,
headers: { location: 'https://b.example.com/v1/info' },
}),
'https://b.example.com/v1/info': new Response(null, {
status: 302,
headers: { location: 'https://c.example.com/v1/info' },
}),
'https://c.example.com/v1/info': new Response('{"name":"ok"}', { headers: ok }),
};
const deps = { resolve: async () => ['93.184.216.34'] };
const two = await safeFetchText(
'https://a.example.com/v1/info',
{ accept: 'application/json' },
{ ...deps, fetchImpl: scriptedFetch(routes).fetch },
);
assert.equal(two.state, 'ok', 'two hops are within the cap');
const deeper = {
...routes,
'https://c.example.com/v1/info': new Response(null, {
status: 302,
headers: { location: 'https://d.example.com/v1/info' },
}),
};
const three = scriptedFetch(deeper);
const over = await safeFetchText(
'https://a.example.com/v1/info',
{ accept: 'application/json' },
{ ...deps, fetchImpl: three.fetch },
);
assert.equal(over.state, 'unreachable');
assert.equal(three.seen.length, 3, 'the fourth address is never fetched');
});
await checkAsync('a body over the cap and a body of the wrong type are both refused', async () => {
const deps = { resolve: async () => ['93.184.216.34'] };
const big = scriptedFetch({
'https://mint.example.com/v1/info': new Response('x'.repeat(2048), {
headers: { 'content-type': 'application/json' },
}),
});
const capped = await safeFetchText(
'https://mint.example.com/v1/info',
{ accept: 'application/json', maxBytes: 512 },
{ ...deps, fetchImpl: big.fetch },
);
assert.equal(capped.state, 'unreachable');
const image = scriptedFetch({
'https://mint.example.com/v1/info': new Response('\x89PNG', {
headers: { 'content-type': 'image/png' },
}),
});
const wrongType = await safeFetchText(
'https://mint.example.com/v1/info',
{ accept: 'application/json' },
{ ...deps, fetchImpl: image.fetch },
);
assert.equal(wrongType.state, 'unreachable');
assert.match(wrongType.state === 'unreachable' ? wrongType.reason : '', /content type/);
});
/* ---------- slug collapse ---------- */
check('every spelling of one mint collapses to one row key', () => {
const spellings = [
'https://mint.600.wtf',
'mint.600.wtf',
'mint.600.wtf/',
'https://mint.600.wtf/',
'https://MINT.600.WTF',
'http://mint.600.wtf',
'https://mint.600.wtf:443/',
' https://mint.600.wtf/ ',
].map((raw) => normalizeMintUrl(raw));
const urls = new Set(spellings.map((s) => s?.url));
const hosts = new Set(spellings.map((s) => s?.host));
assert.equal(urls.size, 1, `one canonical URL, got ${[...urls].join(', ')}`);
assert.equal(hosts.size, 1, `one routing slug, got ${[...hosts].join(', ')}`);
assert.equal([...urls][0], 'https://mint.600.wtf');
assert.equal([...hosts][0], 'mint.600.wtf');
// And the LNURL row key derived from it is one value too, since that is what the
// endpoint actually looks the row up by.
assert.equal(new Set(spellings.map((s) => lnurlKey(s!.url))).size, 1);
});
check('a path is still part of a mint identity, and still collapses per path', () => {
const withPath = ['https://mint.example.com/Bitcoin', 'mint.example.com/Bitcoin/'].map((raw) =>
normalizeMintUrl(raw),
);
assert.equal(new Set(withPath.map((s) => s?.url)).size, 1);
assert.notEqual(withPath[0]?.url, normalizeMintUrl('https://mint.example.com')?.url);
});
/* ---------- what the browser checks before it asks ---------- */
check('the client-side pre-check accepts what the normalizer accepts', () => {
assert.deepEqual(checkIndexInput('lnurl', 'mint.600.wtf'), {
ok: true,
value: 'https://mint.600.wtf',
});
assert.deepEqual(checkIndexInput('cashu', ' https://21mint.me/ '), {
ok: true,
value: 'https://21mint.me',
});
assert.deepEqual(checkIndexInput('cashu', ''), { ok: false, reason: 'empty' });
assert.deepEqual(checkIndexInput('cashu', 'not a url at all'), { ok: false, reason: 'bad_url' });
// A private address fails the pre-check for the same reason the server refuses it.
assert.deepEqual(checkIndexInput('cashu', 'http://127.0.0.1:3338'), {
ok: false,
reason: 'bad_url',
});
});
check('an invite code pasted into a URL field is named as an invite code', () => {
const code = REAL_INVITE;
assert.deepEqual(checkIndexInput('cashu', code), { ok: false, reason: 'bad_invite' });
assert.deepEqual(checkIndexInput('lnurl', code), { ok: false, reason: 'bad_invite' });
assert.deepEqual(checkIndexInput('fedimint', code), { ok: true, value: code });
assert.deepEqual(checkIndexInput('fedimint', 'https://mint.example.com'), {
ok: false,
reason: 'bad_invite',
});
});
/* ---------- invite codes ---------- */
check('a real invite code yields the federation id its announcement carries', () => {
// The `d` tag of the announcement this code came in: decoding must agree with it, or
// a federation submitted by code would get a second row beside the announced one.
assert.equal(
federationIdFromInviteCode(REAL_INVITE),
'aeca6cc80ffc530bd2d54b09681f6edb9a415c362e4af2fe3d5e04137006fa21',
);
assert.equal(federationIdFromInviteCode(REAL_INVITE.toUpperCase()), federationIdFromInviteCode(REAL_INVITE));
});
check('anything that is not an invite code decodes to nothing', () => {
for (const junk of [
'',
'fed1',
'fed11',
'https://mint.example.com',
`${REAL_INVITE}x`, // checksum fails
REAL_INVITE.slice(0, -1), // truncated
REAL_INVITE.replace('fed11q', 'fed11p'), // one flipped character
'lnbc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq', // valid-ish bech32, wrong hrp
]) {
assert.equal(federationIdFromInviteCode(junk), null, `${junk.slice(0, 24)} must not decode`);
}
});
/* ---------- what counts as a mint ---------- */
check('a NUT-06 document is told apart from an arbitrary JSON endpoint', () => {
assert.ok(isNut06Info({ nuts: { '4': { methods: [] } } }));
assert.ok(isNut06Info({ pubkey: '03c21ef6'.padEnd(66, 'a'), name: 'x' }));
assert.ok(isNut06Info({ name: 'Some mint', version: 'cdk-mintd/0.17.5' }));
// The shapes a host that is not a mint actually answers with.
assert.ok(!isNut06Info({ status: 'ok' }));
assert.ok(!isNut06Info({ detail: 'Not Found' }));
assert.ok(!isNut06Info({ tag: 'withdrawRequest', minWithdrawable: 1 }));
assert.ok(!isNut06Info([]));
assert.ok(!isNut06Info('hello'));
assert.ok(!isNut06Info(null));
assert.ok(!isNut06Info({ nuts: {} }), 'an empty nuts object proves nothing');
});
/* ---------- rate limiting ---------- */
check('the eleventh submission in an hour is refused, and refusals do not extend it', () => {
resetRateLimits();
const now = 1_800_000_000_000;
for (let i = 0; i < RATE_LIMIT; i++) {
assert.equal(takeToken('1.2.3.4', now + i).ok, true, `submission ${i + 1} must be allowed`);
}
const refused = takeToken('1.2.3.4', now + RATE_LIMIT);
assert.equal(refused.ok, false);
assert.ok(refused.retryAfter > 0 && refused.retryAfter <= 3600);
// Pressing the button again must not push the window out.
const again = takeToken('1.2.3.4', now + RATE_LIMIT + 1000);
assert.ok(again.retryAfter <= refused.retryAfter, 'a refusal must not extend the wait');
// A different address has its own budget.
assert.equal(takeToken('5.6.7.8', now + RATE_LIMIT).ok, true);
// An hour later the window has slid past the first submission.
assert.equal(takeToken('1.2.3.4', now + 3_600_001).ok, true);
resetRateLimits();
});
/* ---------- the deep-link routes ---------- */
check('every deep link shape maps to the ecosystem that owns it', () => {
assert.deepEqual(typeForPath('/mint/mint.600.wtf'), { type: 'cashu', host: 'mint.600.wtf' });
assert.deepEqual(typeForPath('/lnurl-mint/mint.600.wtf'), { type: 'lnurl', host: 'mint.600.wtf' });
assert.deepEqual(typeForPath('/fedimint/fed-aeca6cc80ffc530b'), {
type: 'fedimint',
host: 'fed-aeca6cc80ffc530b',
});
// A trailing slash is the same page; anything else is not a mint page at all.
assert.deepEqual(typeForPath('/mint/x.example.com/'), { type: 'cashu', host: 'x.example.com' });
assert.equal(typeForPath('/mints'), null);
assert.equal(typeForPath('/'), null);
assert.equal(typeForPath('/mint/'), null);
});
/* ---------- one submission, one row ---------- */
/*
* The dedup and the write path, against a throwaway in-memory database and with no
* network involved at all: a Fedimint submission is decoded rather than fetched, so it
* exercises `indexSubmission` end to end — the in-flight map, the insert, and the
* payload read back — without touching a relay or a mint.
*
* The import is deferred until after `DATABASE_URL` is set, because the config resolves
* its target on first use and this must not open the real database.
*/
process.env['DATABASE_URL'] = ':memory:';
const { indexSubmission, inFlightCount } = await import('./index-mint.ts');
const { closeDb } = await import('./db.ts');
await checkAsync('two simultaneous submissions of one address share one probe', async () => {
assert.equal(inFlightCount(), 0);
const first = indexSubmission('fedimint', REAL_INVITE);
assert.equal(inFlightCount(), 1, 'the first submission claims the key immediately');
// Deliberately a different spelling of the same code: the key is the decoded
// federation id, so the two collapse before anything is written.
const second = indexSubmission('fedimint', REAL_INVITE.toUpperCase());
assert.equal(inFlightCount(), 1, 'the second submission joins the first, it does not start');
const [a, b] = await Promise.all([first, second]);
assert.equal(a, b, 'both callers get the same answer object');
assert.equal(a.status, 201);
assert.equal(inFlightCount(), 0, 'the entry is released when the work finishes');
const created = a.body as { host?: string; status?: string; invite_codes?: string[] };
assert.equal(created.host, 'fed-aeca6cc80ffc530b');
assert.equal(created.status, 'announced', 'nothing checks a federation, so nothing claims it is up');
assert.deepEqual(created.invite_codes, [REAL_INVITE.toLowerCase()]);
// And once it is a row, submitting it again is a lookup rather than a write.
const again = await indexSubmission('fedimint', REAL_INVITE);
assert.equal(again.status, 200);
assert.equal((again.body as { existing?: boolean }).existing, true);
});
await checkAsync('a submission of the wrong shape never reaches the work at all', async () => {
const junk = await indexSubmission('fedimint', 'fed11not-a-real-code');
assert.equal(junk.status, 422);
assert.equal((junk.body as { error?: string }).error, 'invalid_invite');
assert.equal(inFlightCount(), 0);
const wrongType = await indexSubmission('cashu-ish', 'https://mint.example.com');
assert.equal((wrongType.body as { error?: string }).error, 'bad_type');
// A private address is refused by the normalizer, before DNS and before the map.
const private_ = await indexSubmission('cashu', 'https://192.168.1.5');
assert.equal((private_.body as { error?: string }).error, 'blocked_host');
assert.equal(inFlightCount(), 0);
});
await closeDb();
console.log(`ok, ${checks} index checks passed`);
+935
View File
@@ -0,0 +1,935 @@
/**
* pnpm --filter ./api test:lnurl
*
* The LNURL ecosystem, checked against the two things it has to agree with: the
* responses recorded in `NOTES-LNURL.md`, and the rules written down in
* `docs/KIND-LNURL-MINT.md`. Those two documents are the specification; this file is
* what stops the code and the documents drifting apart in silence.
*
* Four groups:
*
* 1. **Parsers**, against verbatim captures of the live reference instance and of a
* locally run mint with no funding source. Not hand-written approximations — the
* bytes that were actually on the wire.
* 2. **The `d` rules**, including the sticky-identity case that the kind document
* spends a section on and that a naive implementation gets wrong.
* 3. **The features vocabulary**, including the two negative cases that matter:
* nothing invents `lud21`, and nothing invents `rotate`/`split`/`merge`.
* 4. **A full round trip** — announcement and review published to a real relay by this
* site's own publisher, read back by the indexer's own parsers, resolved to a row.
* The relay is `test-relay.ts`, in-process and on an ephemeral port, so this runs
* in CI with nothing installed and never touches a public relay.
*/
import assert from 'node:assert/strict';
import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools/pure';
import { SimplePool } from 'nostr-tools/pool';
import * as nip19 from 'nostr-tools/nip19';
import {
ANNOUNCEMENT_KINDS,
FEATURE_VOCABULARY,
KIND_LNURL_ANNOUNCEMENT,
KIND_REVIEW,
addressFromPayLink,
baseUrlFromKey,
ecosystemForKind,
featureStates,
getMintWarnings,
hasFeature,
hostIdentifier,
isMintPubkey,
lnurlIdentifier,
lnurlIdentifiers,
lnurlKey,
mintChip,
msatToSat,
normalizeLnurlNetwork,
normalizeNetwork,
onionFromHtml,
otherFeatures,
parseAdvertisement,
parseFeatures,
parseLnurlAnnouncement,
parsePayInfo,
parseRating,
parseSoftware,
reviewEcosystem,
type LnurlFields,
type NostrEventLike,
} from '@cashumints/shared';
import { announcedFeatures, announcementTemplate, parseAnnounceKey } from './announce.ts';
import { announceConfig } from './config.ts';
import type { MintRow } from './mints.ts';
import { startTestRelay } from './test-relay.ts';
let checks = 0;
function check(name: string, fn: () => void): void {
try {
fn();
checks++;
} catch (err) {
console.error(`FAIL: ${name}`);
throw err;
}
}
async function checkAsync(name: string, fn: () => Promise<void>): Promise<void> {
try {
await fn();
checks++;
} catch (err) {
console.error(`FAIL: ${name}`);
throw err;
}
}
/* ------------------------------------------------------------------ *
* Captures. Verbatim, from NOTES-LNURL.md.
* ------------------------------------------------------------------ */
/** `GET https://lnurl.21mint.me/.well-known/lnurlw/_`, 2026-08-21. */
const LIVE_WITHDRAW = {
tag: 'withdrawRequest',
callback: 'https://lnurl.21mint.me/w',
minWithdrawable: 5000,
maxWithdrawable: 999899000,
defaultDescription: 'lnurlcash bearer note on lnurl.21mint.me',
mintPubkey: '021ab89df9cbd28cdab8c71d228ca40deba1eaebd827f24849ec4f3e919c023555',
payLink: 'https://lnurl.21mint.me/.well-known/lnurlp/mint',
nodeAlias: 'Azzamo',
nodeUri: '021ab89df9cbd28cdab8c71d228ca40deba1eaebd827f24849ec4f3e919c023555@145.239.92.138:9736',
nodeColor: '#68f442',
nodeCapacity: 30027500000,
nodeNumChannels: 8,
nodeNumPeers: 18,
};
/** `GET https://lnurl.21mint.me/.well-known/lnurlp/_`, same run. */
const LIVE_PAY = {
tag: 'payRequest',
callback: 'https://lnurl.21mint.me/p/cb',
minSendable: 6000,
maxSendable: 1000000000,
metadata:
'[["text/plain", "Mint an lnurlcash bearer note on lnurl.21mint.me"], ' +
'["text/identifier", "_@lnurl.21mint.me"], ["text/plain", "Mint fees: 1000,100"]]',
withdrawLink: 'https://lnurl.21mint.me/w',
};
/**
* The same endpoint on a locally run mint with no `FUNDINGSOURCE_*` configured at all.
*
* Every node field is *absent* rather than null, because the software runs with
* `response_model_exclude_none`. This object is the degraded state, and it is the reason
* the probe can report one.
*/
const NO_FUNDING_WITHDRAW = {
tag: 'withdrawRequest',
callback: 'https://lnurl.test/w',
minWithdrawable: 10000,
maxWithdrawable: 999999000,
defaultDescription: 'lnurlcash bearer note on lnurl.test',
payLink: 'https://lnurl.test/.well-known/lnurlp/mint',
};
/* ------------------------------------------------------------------ *
* 1. Parsers
* ------------------------------------------------------------------ */
check('the live mint advertisement parses into every field the site renders', () => {
const ad = parseAdvertisement(LIVE_WITHDRAW);
assert.ok(ad, 'the live advertisement must parse');
assert.equal(ad.minWithdrawableMsat, 5000);
assert.equal(ad.maxWithdrawableMsat, 999899000);
assert.equal(ad.defaultDescription, 'lnurlcash bearer note on lnurl.21mint.me');
assert.equal(ad.mintPubkey, '021ab89df9cbd28cdab8c71d228ca40deba1eaebd827f24849ec4f3e919c023555');
assert.equal(ad.nodeAlias, 'Azzamo');
assert.equal(ad.nodeCapacityMsat, 30027500000);
assert.equal(ad.nodeChannels, 8);
assert.equal(ad.nodePeers, 18);
assert.equal(ad.fundingAvailable, true);
});
check('millisatoshi limits become the sat figures the verdict strip shows', () => {
assert.equal(msatToSat(5000), 5);
assert.equal(msatToSat(999899000), 999899);
// Floored, not rounded: both numbers are bounds, and rounding a ceiling up would
// advertise a note larger than the mint will ever issue.
assert.equal(msatToSat(1999), 1);
assert.equal(msatToSat(999), 0);
assert.equal(msatToSat(null), null);
assert.equal(msatToSat(-1), null);
});
check('a missing mintPubkey is the degraded state, not a parse failure', () => {
const ad = parseAdvertisement(NO_FUNDING_WITHDRAW);
assert.ok(ad, 'a mint with no funding source still serves a valid advertisement');
assert.equal(ad.fundingAvailable, false);
assert.equal(ad.mintPubkey, null);
assert.equal(ad.nodeAlias, null);
assert.equal(ad.nodeUri, null);
// The limits are real and must still render. This is the whole point of the state.
assert.equal(msatToSat(ad.minWithdrawableMsat), 10);
assert.equal(msatToSat(ad.maxWithdrawableMsat), 999999);
});
check('an LNURL error body is not a mint advertisement, despite arriving as HTTP 200', () => {
// These endpoints answer their errors with 200. A probe keying on the status code
// would call every one of these "online".
assert.equal(parseAdvertisement({ status: 'ERROR', reason: 'Unknown user.' }), null);
assert.equal(parseAdvertisement({ status: 'ERROR', reason: 'Unknown note.' }), null);
assert.equal(parseAdvertisement({ detail: 'Not Found' }), null);
assert.equal(parseAdvertisement(LIVE_PAY), null, 'a payRequest is not a withdrawRequest');
assert.equal(parseAdvertisement(null), null);
assert.equal(parseAdvertisement('withdrawRequest'), null);
assert.equal(parseAdvertisement([]), null);
});
check('an advertisement with inverted or missing bounds is rejected', () => {
assert.equal(parseAdvertisement({ ...LIVE_WITHDRAW, minWithdrawable: 900, maxWithdrawable: 100 }), null);
assert.equal(parseAdvertisement({ ...LIVE_WITHDRAW, maxWithdrawable: undefined }), null);
assert.equal(parseAdvertisement({ ...LIVE_WITHDRAW, maxWithdrawable: 'lots' }), null);
});
check('a zero withdraw ceiling parses, because the warning needs to see it', () => {
// Rejecting this would turn "withdrawals disabled" into "offline", which is a
// different and much less useful thing to tell a reader.
const ad = parseAdvertisement({ ...LIVE_WITHDRAW, minWithdrawable: 0, maxWithdrawable: 0 });
assert.ok(ad);
assert.equal(ad.maxWithdrawableMsat, 0);
});
check('the payRequest metadata is parsed twice and yields fee, description and address', () => {
const pay = parsePayInfo(LIVE_PAY);
assert.ok(pay);
assert.equal(pay.minSendableMsat, 6000);
assert.equal(pay.maxSendableMsat, 1000000000);
assert.equal(pay.description, 'Mint an lnurlcash bearer note on lnurl.21mint.me');
assert.equal(pay.feeBaseMsat, 1000);
assert.equal(pay.feePpm, 100);
assert.equal(pay.withdrawLink, 'https://lnurl.21mint.me/w');
});
check('the "Mint fees:" entry never becomes the description', () => {
// It shares `text/plain` with the description, so a naive first-match reader shows
// "Mint fees: 1000,100" as what the mint is.
const pay = parsePayInfo({
...LIVE_PAY,
metadata: '[["text/plain", "Mint fees: 2000,50"], ["text/plain", "A real description"]]',
});
assert.equal(pay?.description, 'A real description');
assert.equal(pay?.feeBaseMsat, 2000);
assert.equal(pay?.feePpm, 50);
});
check('a fee-free mint reports no fee rather than zero', () => {
const pay = parsePayInfo({ ...LIVE_PAY, metadata: '[["text/plain", "Just a mint"]]' });
assert.equal(pay?.feeBaseMsat, null);
assert.equal(pay?.feePpm, null);
});
check('unparseable metadata costs the description, not the whole response', () => {
const pay = parsePayInfo({ ...LIVE_PAY, metadata: 'not json at all' });
assert.ok(pay, 'the limits above the metadata are still good');
assert.equal(pay.minSendableMsat, 6000);
assert.equal(pay.description, null);
});
check('the lightning address comes from payLink, never from the echoed identifier', () => {
// `text/identifier` echoes whichever username was queried, so probing `_` gets back
// `_@host` — LUD-16's bare-domain form, and not a name to show a reader.
assert.equal(parsePayInfo(LIVE_PAY)?.identifier, '_@lnurl.21mint.me');
assert.equal(addressFromPayLink(LIVE_WITHDRAW.payLink), 'mint@lnurl.21mint.me');
assert.equal(addressFromPayLink('https://x.example/.well-known/lnurlp/_'), null);
assert.equal(addressFromPayLink('https://x.example/somewhere/else'), null);
assert.equal(addressFromPayLink(null), null);
assert.equal(addressFromPayLink('not a url'), null);
});
check('an onion address is found in the one-pager and nowhere else', () => {
const html =
'<h2>Also via Tor</h2><button class="copy" ' +
'data-copy="mint@abcdefghijklmnopqrstuvwxyz234567abcdefghijklmnopqrstuvwx.onion" ' +
'title="Copy lightning address">&#9889;</button>';
assert.equal(
onionFromHtml(html),
'abcdefghijklmnopqrstuvwxyz234567abcdefghijklmnopqrstuvwx.onion',
);
assert.equal(onionFromHtml('<h1>a mint with no tor section</h1>'), null);
});
check('the version comes from openapi, and the unknown sentinel is not a version', () => {
assert.equal(
parseSoftware({ info: { title: 'lnurl-mint', version: '0.1.0' } }),
'lnurl-mint/0.1.0',
);
// What a source checkout with no installed package metadata reports. It is the
// library saying "I do not know", not a release, and must not reach a reader.
assert.equal(parseSoftware({ info: { title: 'lnurl-mint', version: '0.0.0+unknown' } }), null);
assert.equal(parseSoftware({ info: { version: '1.2.3' } }), null);
assert.equal(parseSoftware({}), null);
assert.equal(parseSoftware(null), null);
});
/* ------------------------------------------------------------------ *
* 2. Identity: the `d` rules
* ------------------------------------------------------------------ */
check('a mint pubkey is 66 hex characters beginning 02 or 03', () => {
assert.equal(isMintPubkey(LIVE_WITHDRAW.mintPubkey), true);
assert.equal(isMintPubkey('03' + 'a'.repeat(64)), true);
// A Cashu mint pubkey or a federation id is 64 characters, and must never be
// mistaken for one of these.
assert.equal(isMintPubkey('a'.repeat(64)), false);
assert.equal(isMintPubkey('04' + 'a'.repeat(64)), false);
assert.equal(isMintPubkey('021ab8'), false);
assert.equal(isMintPubkey(null), false);
});
check('the two `d` forms can never be confused for one another', () => {
const host = hostIdentifier('https://lnurl.21mint.me');
assert.equal(host, 'lnurl.21mint.me');
assert.equal(isMintPubkey(host), false, 'a host is never pubkey-shaped');
assert.ok(host.includes('.'), 'a host always contains a dot; a pubkey never does');
});
check('the `d` fallback drops the scheme and the trailing slash but keeps the path', () => {
assert.equal(hostIdentifier('https://mint.example.com/lnurl/'), 'mint.example.com/lnurl');
assert.equal(hostIdentifier('https://Mint.Example.com'), 'mint.example.com');
});
check('the identifier prefers the pubkey and falls back to the host', () => {
assert.equal(
lnurlIdentifier('https://lnurl.21mint.me', LIVE_WITHDRAW.mintPubkey),
LIVE_WITHDRAW.mintPubkey,
);
assert.equal(lnurlIdentifier('https://lnurl.21mint.me', null), 'lnurl.21mint.me');
// Junk in the pubkey position falls back rather than being published as a `d`.
assert.equal(lnurlIdentifier('https://lnurl.21mint.me', 'nonsense'), 'lnurl.21mint.me');
});
check('a mint that gained a pubkey is still reviewable under its old host identifier', () => {
// The case the kind document spends a section on. Reviews written before the mint had
// a funding source carry the host `d`; asking only for the current identifier would
// silently strand every one of them.
const both = lnurlIdentifiers('https://lnurl.21mint.me', LIVE_WITHDRAW.mintPubkey);
assert.deepEqual(both, [LIVE_WITHDRAW.mintPubkey, 'lnurl.21mint.me']);
const hostOnly = lnurlIdentifiers('https://lnurl.21mint.me', null);
assert.deepEqual(hostOnly, ['lnurl.21mint.me']);
});
check('the row key round-trips the base URL', () => {
const key = lnurlKey('https://lnurl.21mint.me');
assert.equal(key, 'lnurl:https://lnurl.21mint.me');
assert.equal(baseUrlFromKey(key), 'https://lnurl.21mint.me');
// Not an LNURL key, and must not be mistaken for one.
assert.equal(baseUrlFromKey('https://mint.example.com'), null);
assert.equal(baseUrlFromKey('fedimint:' + 'a'.repeat(64)), null);
});
/* ------------------------------------------------------------------ *
* 3. The features vocabulary
* ------------------------------------------------------------------ */
check('the features tag splits like a modules tag, and tolerates what publishers write', () => {
assert.deepEqual(parseFeatures('mint,melt,rotate'), ['mint', 'melt', 'rotate']);
assert.deepEqual(parseFeatures('mint, melt, rotate'), ['mint', 'melt', 'rotate']);
assert.deepEqual(parseFeatures('MINT,Melt'), ['mint', 'melt']);
assert.deepEqual(parseFeatures('mint,mint,melt'), ['mint', 'melt']);
assert.deepEqual(parseFeatures(''), []);
assert.deepEqual(parseFeatures(null), []);
// Junk tokens are dropped, not carried into a chip.
assert.deepEqual(parseFeatures('mint,<script>,melt'), ['mint', 'melt']);
});
check('an unrecognised feature is kept and shown, never dropped', () => {
// The kind document requires consumers to ignore tokens they do not know rather than
// reject the event, which is what lets the vocabulary grow without a new kind.
const features = parseFeatures('mint,melt,quantum-notes');
assert.deepEqual(features, ['mint', 'melt', 'quantum-notes']);
assert.deepEqual(otherFeatures(features), ['quantum-notes']);
});
check('the vocabulary is exactly what the kind document lists', () => {
assert.deepEqual([...FEATURE_VOCABULARY], [
'mint', 'melt', 'rotate', 'split', 'merge',
'lud06', 'lud03', 'lud16', 'lud21',
'signed-notes', 'onion',
]);
});
check('the notes row is satisfied by any one of rotate, split or merge', () => {
assert.equal(hasFeature(['rotate'], 'notes'), true);
assert.equal(hasFeature(['split'], 'notes'), true);
assert.equal(hasFeature(['merge'], 'notes'), true);
assert.equal(hasFeature(['mint', 'melt'], 'notes'), false);
});
check('a funding outage marks mint, melt and signed-notes unavailable, and nothing else', () => {
const features = ['mint', 'melt', 'rotate', 'split', 'merge', 'lud16', 'lud21', 'signed-notes'];
const down = featureStates(features, false);
assert.equal(down.mint, 'unavailable');
assert.equal(down.melt, 'unavailable');
assert.equal(down['signed-notes'], 'unavailable');
// Exactly what still works with no Lightning node, and the reason this state is
// rendered rather than collapsed into "offline".
assert.equal(down.notes, 'ok');
assert.equal(down.lud16, 'ok');
assert.equal(down.lud21, 'ok');
assert.equal(down.onion, 'none');
const up = featureStates(features, true);
assert.equal(up.mint, 'ok');
assert.equal(up['signed-notes'], 'ok');
// Nothing probed yet is not a reason to doubt an operator's claim.
const unknown = featureStates(features, null);
assert.equal(unknown.mint, 'ok');
});
check('a feature that was never claimed stays absent even when funding is down', () => {
const states = featureStates(['rotate'], false);
assert.equal(states.mint, 'none', 'absent, not "unavailable" — it was never claimed');
assert.equal(states.notes, 'ok');
});
check('the network tag agrees with the Fedimint side, bitcoin included', () => {
for (const value of ['bitcoin', 'mainnet', 'MAIN', 'signet', 'regtest', 'testnet4', '', null]) {
assert.equal(
normalizeLnurlNetwork(value),
normalizeNetwork(value),
`the two normalizers disagree about ${JSON.stringify(value)}`,
);
}
});
/* ------------------------------------------------------------------ *
* 4. The announcement
* ------------------------------------------------------------------ */
const ANNOUNCER = 'f'.repeat(64);
function announcement(tags: string[][], content = ''): NostrEventLike {
return {
id: 'e'.repeat(64),
pubkey: ANNOUNCER,
kind: KIND_LNURL_ANNOUNCEMENT,
created_at: 1_780_000_000,
content,
tags,
};
}
check('kind 38174 is wired into the one table every ecosystem is read from', () => {
assert.equal(ANNOUNCEMENT_KINDS.lnurl, 38174);
assert.equal(ecosystemForKind(38174), 'lnurl');
assert.equal(ecosystemForKind('38174'), 'lnurl');
// The neighbours are untouched.
assert.equal(ecosystemForKind(38172), 'cashu');
assert.equal(ecosystemForKind(38173), 'fedimint');
});
check('a full announcement parses into every field a row needs', () => {
const parsed = parseLnurlAnnouncement(
announcement(
[
['d', LIVE_WITHDRAW.mintPubkey],
['u', 'https://lnurl.21mint.me'],
['features', 'mint,melt,rotate,split,merge,lud06,lud03,lud16,lud21,signed-notes'],
['n', 'mainnet'],
],
JSON.stringify({ name: '21 Mint', about: 'Bearer notes.' }),
),
);
assert.ok(parsed);
assert.equal(parsed.identifier, LIVE_WITHDRAW.mintPubkey);
assert.equal(parsed.mintPubkey, LIVE_WITHDRAW.mintPubkey);
assert.equal(parsed.baseUrl, 'https://lnurl.21mint.me');
assert.equal(parsed.slug, 'lnurl.21mint.me');
assert.equal(parsed.network, 'mainnet');
assert.equal(parsed.name, '21 Mint');
assert.equal(parsed.about, 'Bearer notes.');
assert.equal(parsed.announcerPubkey, ANNOUNCER);
assert.equal(parsed.features.length, 10);
});
check('an announcement by host parses, and carries no pubkey', () => {
const parsed = parseLnurlAnnouncement(
announcement([['d', 'lnurl.example.com'], ['u', 'https://lnurl.example.com']]),
);
assert.ok(parsed);
assert.equal(parsed.identifier, 'lnurl.example.com');
assert.equal(parsed.mintPubkey, null, 'a host `d` is not a pubkey and must not read as one');
});
check('an announcement with no usable `u` is rejected', () => {
// `u` is REQUIRED for this kind, unlike 38172: a host-form `d` has no scheme and is
// not fetchable, so without `u` there is no address at all.
assert.equal(parseLnurlAnnouncement(announcement([['d', 'lnurl.example.com']])), null);
assert.equal(
parseLnurlAnnouncement(announcement([['d', 'lnurl.example.com'], ['u', 'not a url']])),
null,
);
// An onion cannot be the canonical `u`; the normalizer refuses it for every ecosystem.
assert.equal(
parseLnurlAnnouncement(
announcement([['d', 'lnurl.example.com'], ['u', 'http://abcdefghijklmnop.onion']]),
),
null,
);
});
check('an announcement with an unusable `d` is rejected', () => {
assert.equal(parseLnurlAnnouncement(announcement([['u', 'https://lnurl.example.com']])), null);
assert.equal(
parseLnurlAnnouncement(
announcement([['d', 'not an identifier'], ['u', 'https://lnurl.example.com']]),
),
null,
);
});
check('a `d` naming one mint and a `u` naming another is accepted, deliberately', () => {
// Not a validation failure: a mint's `d` is its funding node's pubkey, which has no
// relationship to its hostname at all. Cross-checking them would reject exactly the
// events the identity rule exists to allow.
const parsed = parseLnurlAnnouncement(
announcement([['d', LIVE_WITHDRAW.mintPubkey], ['u', 'https://somewhere.else.example']]),
);
assert.ok(parsed);
assert.equal(parsed.baseUrl, 'https://somewhere.else.example');
});
check('hostile announcement content degrades to no metadata', () => {
for (const content of ['not json', '[]', 'null', '{"name": 12345}', '""']) {
const parsed = parseLnurlAnnouncement(
announcement([['d', 'lnurl.example.com'], ['u', 'https://lnurl.example.com']], content),
);
assert.ok(parsed, `content ${content} must not reject the announcement`);
assert.equal(parsed.name, null);
}
// A `javascript:` picture never reaches an img src.
const parsed = parseLnurlAnnouncement(
announcement(
[['d', 'lnurl.example.com'], ['u', 'https://lnurl.example.com']],
JSON.stringify({ picture: 'javascript:alert(1)' }),
),
);
assert.equal(parsed?.picture, null);
});
/* ------------------------------------------------------------------ *
* 5. Reviews
* ------------------------------------------------------------------ */
function review(tags: string[][], content = '[5/5] Good mint.'): NostrEventLike {
return {
id: 'd'.repeat(64),
pubkey: 'c'.repeat(64),
kind: KIND_REVIEW,
created_at: 1_780_000_100,
content,
tags,
};
}
check('a k=38174 review is filed under lnurl and nothing else', () => {
assert.equal(reviewEcosystem(review([['k', '38174']])), 'lnurl');
assert.equal(reviewEcosystem(review([['k', '38172']])), 'cashu');
assert.equal(reviewEcosystem(review([['k', '38173']])), 'fedimint');
// A review with no `k` is Cashu, because every one of those predates everything else.
assert.equal(reviewEcosystem(review([])), 'cashu');
// A kind this build has no ecosystem for belongs to nobody.
assert.equal(reviewEcosystem(review([['k', '39999']])), null);
});
check('the rating convention is identical across all three ecosystems', () => {
// A reader comparing a Cashu mint to an LNURL one must be comparing the same scale,
// so this uses the site's existing parser with no LNURL-specific path at all.
assert.equal(parseRating(review([['k', '38174']], '[4/5] Fast melts')), 4);
assert.equal(parseRating(review([['k', '38174'], ['rating', '2']])), 2);
assert.equal(parseRating(review([['k', '38174'], ['rating', '0.8']])), 4);
assert.equal(parseRating(review([['k', '38174']], 'no rating here')), null);
});
/* ------------------------------------------------------------------ *
* 6. Warnings
* ------------------------------------------------------------------ */
const onlineMint = {
type: 'lnurl',
status: 'online' as const,
last_online: 1_786_999_910,
first_seen: 1_769_720_000,
};
const NOW = 1_787_000_000;
check('a healthy LNURL mint gets no banner', () => {
const warnings = getMintWarnings(
{ ...onlineMint, max_withdrawable_msat: 999899000, funding_available: true },
{ now: NOW },
);
assert.deepEqual(warnings, []);
});
check('a zero withdraw ceiling is critical and says withdrawals are disabled', () => {
const warnings = getMintWarnings(
{ ...onlineMint, max_withdrawable_msat: 0, funding_available: true },
{ now: NOW },
);
assert.equal(warnings[0]?.kind, 'lnurl-withdrawals-disabled');
assert.equal(warnings[0]?.severity, 'critical');
assert.match(warnings[0]!.lead, /Withdrawals disabled/);
assert.equal(mintChip(warnings)?.label, 'No withdrawals');
});
check('never having probed is not "withdrawals disabled"', () => {
// `=== 0`, not falsy. null means nothing has looked yet.
const warnings = getMintWarnings(
{ ...onlineMint, max_withdrawable_msat: null, funding_available: null },
{ now: NOW },
);
assert.deepEqual(warnings, []);
});
check('an unreachable funding source is a warning that says what still works', () => {
const warnings = getMintWarnings(
{ ...onlineMint, max_withdrawable_msat: 999899000, funding_available: false },
{ now: NOW },
);
assert.equal(warnings[0]?.kind, 'lnurl-no-funding');
assert.equal(warnings[0]?.severity, 'warning');
assert.match(warnings[0]!.body, /rotated, split, or merged/);
assert.match(warnings[0]!.body, /nothing moves in or out/);
assert.equal(mintChip(warnings)?.label, 'No mint / melt');
});
check('a host responding with junk says so, rather than saying offline', () => {
const warnings = getMintWarnings(
{
...onlineMint,
status: 'degraded',
max_withdrawable_msat: 999899000,
invalid_reason: 'mint replied: Unknown user.',
},
{ now: NOW },
);
assert.equal(warnings[0]?.kind, 'lnurl-invalid');
assert.match(warnings[0]!.lead, /Endpoint responding but invalid/);
assert.match(warnings[0]!.body, /withdrawals may not work/);
});
check('the offline tiers are exactly the Cashu ones', () => {
const day = 86400;
const tiers: Array<[number, string]> = [
[2 * day, 'offline'],
[10 * day, 'offline-long'],
[40 * day, 'gone'],
];
for (const [age, kind] of tiers) {
const warnings = getMintWarnings(
{ type: 'lnurl', status: 'offline', last_online: NOW - age, first_seen: NOW - 200 * day },
{ now: NOW },
);
assert.equal(warnings[0]?.kind, kind, `${age / day} days offline should be ${kind}`);
// The same input with `type: cashu` reaches the same tier, which is what makes
// "identical to Cashu" a checked claim rather than a comment.
const cashu = getMintWarnings(
{ type: 'cashu', status: 'offline', last_online: NOW - age, first_seen: NOW - 200 * day },
{ now: NOW },
);
assert.equal(cashu[0]?.kind, kind);
assert.equal(cashu[0]?.lead, warnings[0]?.lead, 'and says the same words');
}
});
check('a mint that never answered once lands in the top tier', () => {
const warnings = getMintWarnings(
{ type: 'lnurl', status: 'offline', last_online: null, first_seen: NOW - 40 * 86400 },
{ now: NOW },
);
assert.equal(warnings[0]?.kind, 'gone');
});
check('withdrawals disabled outranks an offline banner, and still says it is offline', () => {
const warnings = getMintWarnings(
{
type: 'lnurl',
status: 'offline',
last_online: NOW - 3 * 86400,
first_seen: NOW - 200 * 86400,
max_withdrawable_msat: 0,
},
{ now: NOW },
);
assert.equal(warnings[0]?.kind, 'lnurl-withdrawals-disabled');
assert.match(warnings[0]!.body, /also been offline/);
});
check('an offline banner rescues the funding fact into its last sentence', () => {
const warnings = getMintWarnings(
{
type: 'lnurl',
status: 'offline',
last_online: NOW - 40 * 86400,
first_seen: NOW - 200 * 86400,
funding_available: false,
},
{ now: NOW },
);
assert.equal(warnings[0]?.kind, 'gone');
assert.match(warnings[0]!.body, /Lightning node was also unreachable/);
});
check('no LNURL warning is ever derived from the features tag', () => {
// `features` is the operator's claim about what they built. A banner derived from a
// claim rather than an observation is the invention the Fedimint branch refuses to
// make, and this branch refuses it too.
const claimed = getMintWarnings(
{ ...onlineMint, features: [], max_withdrawable_msat: 5000, funding_available: true } as never,
{ now: NOW },
);
assert.deepEqual(claimed, []);
});
/* ------------------------------------------------------------------ *
* 7. The publisher
* ------------------------------------------------------------------ */
const probedRow: MintRow = {
url: 'lnurl:https://lnurl.21mint.me',
host: 'lnurl.21mint.me',
type: 'lnurl',
name: '21 Mint',
description: null,
icon_url: null,
icon_file: null,
pubkey: null,
info_json: null,
ecosystem_json: null,
nuts_json: null,
version: 'lnurl-mint/0.1.0',
status: 'online',
consecutive_fails: 0,
last_online: 1_786_999_910,
last_probe: 1_786_999_910,
first_seen: 1_769_720_000,
updated_at: 1_786_999_910,
};
const probedFields: LnurlFields = {
lnurl_id: LIVE_WITHDRAW.mintPubkey,
base_url: 'https://lnurl.21mint.me',
features: [],
network: 'mainnet',
announced_at: null,
announcer_pubkey: null,
mint_pubkey: LIVE_WITHDRAW.mintPubkey,
funding_available: true,
probe_endpoint: '/.well-known/lnurlw/_',
invalid_reason: null,
min_withdrawable_msat: 5000,
max_withdrawable_msat: 999899000,
min_sendable_msat: 6000,
max_sendable_msat: 1000000000,
fee_base_msat: 1000,
fee_ppm: 100,
lightning_address: 'mint@lnurl.21mint.me',
onion_url: null,
node_alias: 'Azzamo',
node_uri: LIVE_WITHDRAW.nodeUri,
node_capacity_msat: 30027500000,
node_channels: 8,
node_peers: 18,
observed_features: ['mint', 'melt', 'lud06', 'lud03', 'lud16', 'signed-notes'],
};
check('announcing is off unless all three switches are set', () => {
assert.equal(announceConfig({}), null);
assert.equal(announceConfig({ ANNOUNCE_LNURL: 'true' }), null, 'no key, no publish');
assert.equal(
announceConfig({ ANNOUNCE_LNURL: 'true', ANNOUNCE_KEY: 'nsec1x' }),
null,
'no relays, no publish — there is deliberately no default',
);
assert.equal(
announceConfig({ ANNOUNCE_LNURL: 'false', ANNOUNCE_KEY: 'k', ANNOUNCE_RELAYS: 'wss://r' }),
null,
);
const on = announceConfig({
ANNOUNCE_LNURL: 'true',
ANNOUNCE_KEY: 'nsec1x',
ANNOUNCE_RELAYS: 'wss://one, wss://two, http://not-a-relay',
});
assert.deepEqual(on, { secretKey: 'nsec1x', relays: ['wss://one', 'wss://two'] });
});
check('the signing key accepts an nsec or hex, and refuses anything else', () => {
const secret = generateSecretKey();
const hex = Buffer.from(secret).toString('hex');
assert.deepEqual(parseAnnounceKey(hex), secret);
assert.deepEqual(parseAnnounceKey(nip19.nsecEncode(secret)), secret);
assert.equal(parseAnnounceKey('npub1abc'), null);
assert.equal(parseAnnounceKey('nsec1notvalid'), null);
assert.equal(parseAnnounceKey('deadbeef'), null);
assert.equal(parseAnnounceKey(''), null);
});
check('this site announces only what it observed', () => {
const features = announcedFeatures(probedFields);
assert.deepEqual(features, ['mint', 'melt', 'lud06', 'lud03', 'lud16', 'signed-notes']);
// The four it must never publish, and the reasons, from the kind document:
assert.ok(!features.includes('lud21'), 'verify is undetectable over HTTP');
for (const operation of ['rotate', 'split', 'merge']) {
assert.ok(
!features.includes(operation),
`${operation} is only provable by calling /w/cb, which mutates a stranger's note`,
);
}
});
check('a mint with no reachable node is announced without the funded capabilities', () => {
const features = announcedFeatures({ ...probedFields, funding_available: false });
assert.deepEqual(features, ['lud06', 'lud03', 'lud16']);
assert.ok(!features.includes('signed-notes'), 'note signing needs the same node');
});
check('a zero withdraw ceiling is never announced as melt', () => {
const features = announcedFeatures({ ...probedFields, max_withdrawable_msat: 0 });
assert.ok(!features.includes('melt'));
assert.ok(features.includes('mint'), 'the pay side is unaffected');
});
check('the announcement template is shaped exactly as the kind document says', () => {
const template = announcementTemplate(probedRow, probedFields, 1_787_000_000);
assert.equal(template.kind, 38174);
assert.deepEqual(template.tags[0], ['d', LIVE_WITHDRAW.mintPubkey]);
assert.deepEqual(template.tags[1], ['u', 'https://lnurl.21mint.me']);
assert.deepEqual(template.tags[2], ['features', 'mint,melt,lud06,lud03,lud16,signed-notes']);
assert.deepEqual(template.tags[3], ['n', 'mainnet']);
assert.equal(template.content, JSON.stringify({ name: '21 Mint' }));
});
check('a mint that never named a network is not assigned one', () => {
// Absent reads as mainnet, so asserting it would be this site inventing the one fact
// that decides whether the money is real.
const template = announcementTemplate(probedRow, { ...probedFields, network: null }, 1);
assert.equal(template.tags.some((t) => t[0] === 'n'), false);
});
check('an announcement this site wrote defers to its own kind 0 when there is no name', () => {
const template = announcementTemplate({ ...probedRow, name: null }, probedFields, 1);
assert.equal(template.content, '');
});
/* ------------------------------------------------------------------ *
* 8. The round trip, against a real relay
* ------------------------------------------------------------------ */
await checkAsync('a 38174 announcement and a k=38174 review round-trip a relay', async () => {
const relay = await startTestRelay();
const pool = new SimplePool();
try {
const siteKey = generateSecretKey();
const sitePubkey = getPublicKey(siteKey);
const reviewerKey = generateSecretKey();
/* --- publish the announcement, using the publisher's own template builder --- */
const created = 1_787_000_000;
const announcementEvent = finalizeEvent(
announcementTemplate(probedRow, probedFields, created),
siteKey,
);
await Promise.allSettled(pool.publish([relay.url], announcementEvent));
/* --- publish a review of it, shaped as the kind document specifies --- */
const reviewEvent = finalizeEvent(
{
kind: KIND_REVIEW,
created_at: created + 60,
content: '[5/5] Rotations are instant and melts have never failed me.',
tags: [
['k', String(KIND_LNURL_ANNOUNCEMENT)],
['u', probedFields.base_url, 'lnurl'],
['d', probedFields.lnurl_id],
['a', `${KIND_LNURL_ANNOUNCEMENT}:${sitePubkey}:${probedFields.lnurl_id}`, relay.url],
],
},
reviewerKey,
);
await Promise.allSettled(pool.publish([relay.url], reviewEvent));
/* --- read both back the way the indexer does --- */
const announcements = await pool.querySync(
[relay.url],
{ kinds: [KIND_LNURL_ANNOUNCEMENT] },
{ maxWait: 4000 },
);
assert.equal(announcements.length, 1, 'the relay should hold exactly one announcement');
const parsed = parseLnurlAnnouncement(announcements[0]!);
assert.ok(parsed, 'the indexer must be able to parse what the site published');
assert.equal(parsed.identifier, LIVE_WITHDRAW.mintPubkey);
assert.equal(parsed.baseUrl, 'https://lnurl.21mint.me');
assert.equal(parsed.mintPubkey, LIVE_WITHDRAW.mintPubkey);
assert.equal(parsed.network, 'mainnet');
assert.equal(parsed.announcerPubkey, sitePubkey);
assert.deepEqual(parsed.features, ['mint', 'melt', 'lud06', 'lud03', 'lud16', 'signed-notes']);
assert.equal(parsed.name, '21 Mint');
/* --- the review, asked for exactly as the targeted pass asks --- */
const reviews = await pool.querySync(
[relay.url],
{
kinds: [KIND_REVIEW],
'#k': [String(KIND_LNURL_ANNOUNCEMENT)],
'#d': lnurlIdentifiers(parsed.baseUrl, parsed.mintPubkey),
},
{ maxWait: 4000 },
);
assert.equal(reviews.length, 1, 'the #d/#k filter the indexer uses must find it');
assert.equal(reviewEcosystem(reviews[0]!), 'lnurl');
assert.equal(parseRating(reviews[0]!), 5);
/* --- and by `u`, which is how a client that does not know this kind writes one --- */
const byUrl = await pool.querySync(
[relay.url],
{ kinds: [KIND_REVIEW], '#u': [probedFields.base_url] },
{ maxWait: 4000 },
);
assert.equal(byUrl.length, 1, 'the #u filter must find it too');
/* --- replacement: a second announcement for the same `d` replaces the first --- */
const updated = finalizeEvent(
announcementTemplate(
probedRow,
{ ...probedFields, funding_available: false },
created + 3600,
),
siteKey,
);
await Promise.allSettled(pool.publish([relay.url], updated));
const after = relay.byKind(KIND_LNURL_ANNOUNCEMENT);
assert.equal(after.length, 1, '38174 is addressable: the relay holds one, not two');
assert.equal(
after[0]?.tags.find((t) => t[0] === 'features')?.[1],
'lud06,lud03,lud16',
'and it is the newer one, with the funded capabilities dropped',
);
} finally {
pool.close([relay.url]);
await relay.close();
}
});
console.log(`ok, ${checks} LNURL checks passed`);
+49
View File
@@ -112,6 +112,20 @@ export const config = {
iconDir: process.env['ICON_DIR'] ?? path.join(apiRoot, 'data', 'icons'), iconDir: process.env['ICON_DIR'] ?? path.join(apiRoot, 'data', 'icons'),
relays: (process.env['RELAYS']?.split(',').map((r) => r.trim()).filter(Boolean) ?? relays: (process.env['RELAYS']?.split(',').map((r) => r.trim()).filter(Boolean) ??
[...DEFAULT_RELAYS]) as string[], [...DEFAULT_RELAYS]) as string[],
/**
* The floor a backfill cycle has to clear before it counts as a real read.
*
* For about a year this deployment's RELAYS list did not include the relay carrying
* the kind 38000/38172 archive. Every backfill returned about thirty events, wrote
* them, reported ok=true, and the index sat at eight mints while every health signal
* stayed green. A backfill asks five relays for the whole history of four kinds; on a
* working relay set it comes back with thousands. Anything under this is not a quiet
* network, it is a misconfigured one, and it says so in the log and on /api/health.
*
* Raise it on a deployment that genuinely has more history, lower it for a local
* test relay. It is deliberately not zero-able: set it to 1 if you mean "off".
*/
backfillMinEvents: int('BACKFILL_MIN_EVENTS', 200),
probeIntervalMin: int('PROBE_INTERVAL_MIN', 10), probeIntervalMin: int('PROBE_INTERVAL_MIN', 10),
discoveryIntervalMin: int('DISCOVERY_INTERVAL_MIN', 60), discoveryIntervalMin: int('DISCOVERY_INTERVAL_MIN', 60),
probeConcurrency: int('PROBE_CONCURRENCY', 8), probeConcurrency: int('PROBE_CONCURRENCY', 8),
@@ -119,4 +133,39 @@ export const config = {
userAgent: 'cashumints.space-indexer/1.0', userAgent: 'cashumints.space-indexer/1.0',
} as const; } as const;
/**
* Publishing `kind:38174` announcements, which is **off unless two switches are set**.
*
* One switch would be enough to make it work and is not enough to make it safe. This
* writes signed events to public relays, permanently and under this site's name, so
* turning it on has to be a thing somebody did on purpose rather than a thing that
* happened because a key was left in an env file from a test run:
*
* ANNOUNCE_LNURL=true the intent
* ANNOUNCE_KEY=nsec1… the identity it will be signed with
* ANNOUNCE_RELAYS=wss://… where, explicitly — there is deliberately no default
*
* The missing default on ANNOUNCE_RELAYS is the important one. Falling back to the
* site's read pool would mean the difference between a CI run against a local test relay
* and a permanent write to five public ones was a single unset variable.
*
* Returns null — never throws — when any of the three is missing, because a
* misconfigured publisher must not stop an indexer that still has a directory to serve.
* See the README section "Publishing LNURL mint announcements".
*/
export function announceConfig(
env: NodeJS.ProcessEnv = process.env,
): { secretKey: string; relays: string[] } | null {
if (env['ANNOUNCE_LNURL']?.trim().toLowerCase() !== 'true') return null;
const secretKey = env['ANNOUNCE_KEY']?.trim() ?? '';
const relays = (env['ANNOUNCE_RELAYS'] ?? '')
.split(',')
.map((relay) => relay.trim())
.filter((relay) => relay.startsWith('ws://') || relay.startsWith('wss://'));
if (!secretKey || relays.length === 0) return null;
return { secretKey, relays };
}
export const startedAt = Math.floor(Date.now() / 1000); export const startedAt = Math.floor(Date.now() / 1000);
+454 -26
View File
@@ -3,25 +3,29 @@ import type { Event as NostrEvent, Filter } from 'nostr-tools';
import { import {
ANNOUNCEMENT_KINDS, ANNOUNCEMENT_KINDS,
KIND_FEDIMINT_ANNOUNCEMENT, KIND_FEDIMINT_ANNOUNCEMENT,
KIND_LNURL_ANNOUNCEMENT,
KIND_MINT_ANNOUNCEMENT, KIND_MINT_ANNOUNCEMENT,
KIND_REVIEW, KIND_REVIEW,
isCashuMintReview,
mintPubkeyRef, mintPubkeyRef,
mintUrlSpellings, mintUrlSpellings,
mintUrlsFromEvent, mintUrlsFromEvent,
normalizeMintUrl, normalizeMintUrl,
parseFedimintAnnouncement, parseFedimintAnnouncement,
parseLnurlAnnouncement,
parseRating, parseRating,
reviewEcosystem, reviewEcosystem,
reviewTargetId, reviewTargetId,
reviewTargetKind, reviewTargetKind,
type FedimintAnnouncement, type FedimintAnnouncement,
type LnurlAnnouncement,
type LnurlFields,
type RelayHealth,
} from '@cashumints/shared'; } from '@cashumints/shared';
import { config } from './config.ts'; import { config } from './config.ts';
import { getDb, setState, getStateNumber } from './db.ts'; import { getDb, setState, getState, getStateNumber } from './db.ts';
import type { Sql } from './db-driver.ts'; import type { Sql } from './db-driver.ts';
import { log } from './log.ts'; import { log } from './log.ts';
import { insertMintIfNew, upsertFedimint } from './mints.ts'; import { insertMintIfNew, upsertFedimint, upsertLnurl } from './mints.ts';
const QUERY_LIMIT = 500; const QUERY_LIMIT = 500;
const MAX_WAIT_MS = 12_000; const MAX_WAIT_MS = 12_000;
@@ -36,11 +40,130 @@ export interface DiscoveryResult {
newMints: string[]; newMints: string[];
newReviews: number; newReviews: number;
ok: boolean; ok: boolean;
/** What each configured relay actually did, in `config.relays` order. */
relays: RelayHealth[];
/** A backfill that came in under `config.backfillMinEvents`. */
starved: boolean;
} }
/**
* What the last cycle did, kept so /api/health can answer for it.
*
* Written to the `state` table rather than held in memory, because the question it
* answers — "is discovery actually reading anything?" — has to survive the restart that
* would otherwise reset it to "no cycle yet, nothing to report". A process that crash
* loops would clear an in-memory flag on every attempt.
*/
export interface DiscoveryReport {
at: number;
mode: 'backfill' | 'incremental';
events: number;
ok: boolean;
relays: RelayHealth[];
starved: boolean;
}
/** `state` key holding the JSON of the above. */
const REPORT_KEY = 'last_discovery_report';
/**
* The last cycle's report, or null before any cycle has run.
*
* A row that will not parse reads as null — the same as no cycle — because the caller
* is a health endpoint and "I cannot tell you" must not be dressed up as "fine".
*/
export async function lastDiscoveryReport(): Promise<DiscoveryReport | null> {
const raw = await getState(REPORT_KEY);
if (!raw) return null;
try {
const parsed = JSON.parse(raw) as DiscoveryReport;
return Array.isArray(parsed.relays) ? parsed : null;
} catch {
return null;
}
}
/**
* Per-relay bookkeeping for one cycle.
*
* Every relay in `config.relays` gets a row up front, including the ones that are never
* reached, because a relay that produced no row at all is exactly the one worth naming:
* the year-long starvation was a relay list that connected cleanly and simply did not
* hold the archive, and the only field that would have shown it is a zero here.
*
* `events` counts what a relay sent *before* cross-relay deduplication, so five relays
* carrying the same 400 events report 400 each rather than 400 once and 0 four times.
* Attribution is the whole point; the deduplicated total is reported separately.
*/
class RelayTally {
private readonly rows = new Map<string, { events: number; subs: number; eoses: number; connected: boolean }>();
constructor(urls: readonly string[]) {
for (const url of urls) {
this.rows.set(url, { events: 0, subs: 0, eoses: 0, connected: false });
}
}
private row(url: string) {
let found = this.rows.get(url);
if (!found) {
found = { events: 0, subs: 0, eoses: 0, connected: false };
this.rows.set(url, found);
}
return found;
}
connected(url: string): void {
this.row(url).connected = true;
}
subscribed(url: string): void {
this.row(url).subs++;
}
event(url: string): void {
this.row(url).events++;
}
eose(url: string): void {
this.row(url).eoses++;
}
/** One row per configured relay, in configuration order. */
list(): RelayHealth[] {
return [...this.rows.entries()].map(([url, row]) => ({
url,
connected: row.connected,
events: row.events,
// A cycle asks a relay many questions. It only counts as having reached the end
// of the stream if it reached the end of every one of them.
eose: row.subs > 0 && row.eoses === row.subs,
}));
}
}
/**
* Long enough that the relay's own EOSE timer never wins.
*
* `Subscription` fires `oneose` both when an EOSE frame arrives and when its internal
* timer expires, so the two are indistinguishable from the callback. Pushing that timer
* out of reach and running the deadline here instead is what makes `eose` in the report
* mean "the relay said it was done" rather than "something gave up".
*/
const NEVER_EOSE_MS = 24 * 60 * 60 * 1000;
let pool: SimplePool | null = null; let pool: SimplePool | null = null;
function getPool(): SimplePool { /**
* The process's one relay pool.
*
* Exported because `relay-lookup.ts` asks the same relays a different question — "has
* anyone ever mentioned this address?", for a mint a reader just submitted that does
* not answer — and opening a second pool for it would mean a second set of sockets to
* the same five relays, with its own reconnect behaviour and its own lifetime to get
* wrong at shutdown.
*/
export function getPool(): SimplePool {
pool ??= new SimplePool(); pool ??= new SimplePool();
return pool; return pool;
} }
@@ -54,12 +177,100 @@ export function closePool(): void {
pool = null; pool = null;
} }
/**
* Ask every configured relay one filter, and record what each of them did.
*
* This replaces `pool.querySync(config.relays, …)`, which answers the same question and
* throws the attribution away: it merges five relays into one deduplicated array, so a
* relay list where four relays are empty and one carries everything is indistinguishable
* from five healthy ones. That indistinguishability is the bug this whole file is being
* changed for — a year of ~31-event backfills, `ok=true` every time.
*
* What it keeps from `querySync`, deliberately:
*
* - One subscription per relay over the pool's existing sockets, so this is the same
* number of connections as before.
* - A single `alreadyHaveEvent` shared across all five. `AbstractRelay._onmessage`
* consults it *before* `JSON.parse` and signature verification, so an event five
* relays all carry is still verified once. Per-relay `querySync` calls would have
* verified it five times, which at 500 events a page is real CPU.
* - `receivedEvent`, which fires on the way past that check, so the per-relay count is
* what the relay sent rather than what was new because of it.
*
* What it changes: the deadline is run here rather than by each `Subscription`'s own
* EOSE timer, so `oneose` firing means an EOSE frame actually arrived. See NEVER_EOSE_MS.
*
* Never throws. A relay that will not connect is a fact to record, not a reason to
* abandon the four that did.
*/
async function queryRelays(filter: Filter, tally: RelayTally | null): Promise<NostrEvent[]> {
const events: NostrEvent[] = [];
const known = new Set<string>();
const alreadyHaveEvent = (id: string): boolean => {
if (known.has(id)) return true;
known.add(id);
return false;
};
await Promise.all(
config.relays.map(async (url) => {
let relay;
try {
// The same connection budget subscribeMap would have used for this maxWait.
relay = await getPool().ensureRelay(url, {
connectionTimeout: Math.max(MAX_WAIT_MS * 0.8, MAX_WAIT_MS - 1000),
});
} catch {
// Left as connected=false in the tally, which is the whole report this needs.
return;
}
tally?.connected(url);
await new Promise<void>((resolve) => {
let settled = false;
let deadline: ReturnType<typeof setTimeout> | undefined;
const finish = (): void => {
if (settled) return;
settled = true;
if (deadline !== undefined) clearTimeout(deadline);
resolve();
};
try {
const sub = relay.subscribe([filter], {
onevent: (event) => events.push(event),
alreadyHaveEvent,
receivedEvent: () => tally?.event(url),
oneose: () => {
tally?.eose(url);
sub.close('closed automatically on eose');
},
onclose: finish,
eoseTimeout: NEVER_EOSE_MS,
});
tally?.subscribed(url);
deadline = setTimeout(() => sub.close('closed on maxWait'), MAX_WAIT_MS);
} catch {
// The socket went away between ensureRelay and the REQ.
finish();
}
});
}),
);
return events;
}
/** /**
* Query one kind, paging backwards with `until` until a page yields nothing new. * Query one kind, paging backwards with `until` until a page yields nothing new.
* Relays cap `limit` independently, so paging is the only way a fresh database * Relays cap `limit` independently, so paging is the only way a fresh database
* converges to the complete history. * converges to the complete history.
*/ */
async function fetchKind(kind: number, since: number | null): Promise<NostrEvent[]> { async function fetchKind(
kind: number,
since: number | null,
tally: RelayTally | null,
): Promise<NostrEvent[]> {
const seen = new Map<string, NostrEvent>(); const seen = new Map<string, NostrEvent>();
let until: number | undefined; let until: number | undefined;
@@ -70,7 +281,7 @@ async function fetchKind(kind: number, since: number | null): Promise<NostrEvent
let batch: NostrEvent[]; let batch: NostrEvent[];
try { try {
batch = await getPool().querySync(config.relays, filter, { maxWait: MAX_WAIT_MS }); batch = await queryRelays(filter, tally);
} catch (err) { } catch (err) {
log.warn('relay query failed', { log.warn('relay query failed', {
kind, kind,
@@ -111,6 +322,7 @@ async function fetchKind(kind: number, since: number | null): Promise<NostrEvent
async function fetchReviewsForMint( async function fetchReviewsForMint(
target: ReviewTarget, target: ReviewTarget,
since: number | null, since: number | null,
tally: RelayTally | null,
): Promise<NostrEvent[]> { ): Promise<NostrEvent[]> {
const filters: Filter[] = []; const filters: Filter[] = [];
const base: Filter = { kinds: [KIND_REVIEW], limit: QUERY_LIMIT }; const base: Filter = { kinds: [KIND_REVIEW], limit: QUERY_LIMIT };
@@ -137,6 +349,27 @@ async function fetchReviewsForMint(
'#d': [target.federationId], '#d': [target.federationId],
'#k': [String(KIND_FEDIMINT_ANNOUNCEMENT)], '#k': [String(KIND_FEDIMINT_ANNOUNCEMENT)],
}); });
} else if (target.type === 'lnurl') {
/*
* Asked for by every identifier it has ever had, not just its current one.
*
* An LNURL mint's `d` is its funding node's pubkey when it has one and its host
* otherwise, so a mint that gained a funding source has reviews filed under both
* spellings — the older ones under the host, the newer under the pubkey. One `#d`
* filter carrying both is one round trip and finds all of them; asking for only the
* current identifier would silently strand the earlier half. See the kind document.
*
* `#u` is asked as well, and unlike a federation's invite codes an LNURL mint's URLs
* are short enough that no relay rejects the filter.
*/
if (target.identifiers.length > 0) {
filters.push({
...base,
'#d': target.identifiers,
'#k': [String(KIND_LNURL_ANNOUNCEMENT)],
});
}
filters.push({ ...base, '#u': mintUrlSpellings(target.baseUrl ?? target.url) });
} else { } else {
if (target.pubkey) { if (target.pubkey) {
filters.push({ ...base, '#d': [target.pubkey], '#k': [String(KIND_MINT_ANNOUNCEMENT)] }); filters.push({ ...base, '#d': [target.pubkey], '#k': [String(KIND_MINT_ANNOUNCEMENT)] });
@@ -145,11 +378,7 @@ async function fetchReviewsForMint(
} }
const batches = await Promise.all( const batches = await Promise.all(
filters.map((filter) => filters.map((filter) => queryRelays(filter, tally).catch(() => [] as NostrEvent[])),
getPool()
.querySync(config.relays, filter, { maxWait: MAX_WAIT_MS })
.catch(() => [] as NostrEvent[]),
),
); );
return batches.flat(); return batches.flat();
@@ -164,10 +393,14 @@ async function fetchReviewsForMint(
*/ */
interface ReviewTarget { interface ReviewTarget {
type: string; type: string;
/** Row key: the mint URL, or `fedimint:<id>`. */ /** Row key: the mint URL, `fedimint:<id>`, or `lnurl:<base url>`. */
url: string; url: string;
pubkey: string | null; pubkey: string | null;
federationId: string | null; federationId: string | null;
/** LNURL: every `d` this mint could be reviewed under. Empty for the others. */
identifiers: string[];
/** LNURL: the fetchable https URL inside the row key. */
baseUrl: string | null;
} }
/** /**
@@ -201,6 +434,41 @@ async function ingestFedimints(
} }
} }
/**
* Every LNURL mint the batch announces, inserted or refreshed.
*
* Deduped **by normalized base URL**, not by the `d` tag, which is the one place this
* genuinely differs from `ingestFedimints`. A federation has exactly one identity for
* life; an LNURL mint's identifier is its funding node's pubkey when it has one and its
* host otherwise, so the same mint legitimately announces under two different `d` values
* across its life. Keying on `d` would split it into two pages with half its reviews on
* each. The `u` tag is present in every form of the event, so it is the key.
*
* Newest announcement per URL wins inside the batch, for the same reason the Fedimint
* side does it: the same mint is announced by more than one npub and more than once, and
* writing each of those would be one UPDATE per event to reach the same final state.
*/
async function ingestLnurl(
events: Iterable<NostrEvent>,
now: number,
newRows: Set<string>,
): Promise<void> {
const newest = new Map<string, LnurlAnnouncement>();
for (const event of events) {
const announcement = parseLnurlAnnouncement(event);
if (!announcement) continue;
const existing = newest.get(announcement.baseUrl);
if (existing && existing.announcedAt >= announcement.announcedAt) continue;
newest.set(announcement.baseUrl, announcement);
}
for (const announcement of newest.values()) {
const created = await upsertLnurl(announcement, now);
if (created) newRows.add(created);
}
}
/** /**
* Every mint URL the batch points at, normalized and inserted if new. * Every mint URL the batch points at, normalized and inserted if new.
* *
@@ -247,6 +515,18 @@ interface MintIndex {
byFederation: Map<string, string>; byFederation: Map<string, string>;
/** Invite code to row key, for a review that carries `u` but no usable `d`. */ /** Invite code to row key, for a review that carries `u` but no usable `d`. */
byInvite: Map<string, string>; byInvite: Map<string, string>;
/**
* Every LNURL identifier to its row key: the mint pubkey *and* the normalized host,
* both pointing at the same row.
*
* Kept apart from `byPubkey`, which is the Cashu index over `mints.pubkey`. An LNURL
* mint's key is its funding node's identity, not a Cashu mint pubkey, and mixing the
* two namespaces would let a review of one resolve to the other. Nothing writes
* `mints.pubkey` for an LNURL row, for exactly that reason.
*/
byLnurlId: Map<string, string>;
/** Normalized base URL to row key, for a review that carries `u` but no usable `d`. */
byLnurlUrl: Map<string, string>;
} }
async function loadMintIndex(): Promise<MintIndex> { async function loadMintIndex(): Promise<MintIndex> {
@@ -263,12 +543,30 @@ async function loadMintIndex(): Promise<MintIndex> {
const byPubkey = new Map<string, string>(); const byPubkey = new Map<string, string>();
const byFederation = new Map<string, string>(); const byFederation = new Map<string, string>();
const byInvite = new Map<string, string>(); const byInvite = new Map<string, string>();
const byLnurlId = new Map<string, string>();
const byLnurlUrl = new Map<string, string>();
for (const row of rows) { for (const row of rows) {
byHost.set(row.host, row.url); byHost.set(row.host, row.url);
// First writer wins, matching the LIMIT 1 the per-event query used. // First writer wins, matching the LIMIT 1 the per-event query used.
if (row.pubkey && !byPubkey.has(row.pubkey)) byPubkey.set(row.pubkey, row.url); if (row.pubkey && !byPubkey.has(row.pubkey)) byPubkey.set(row.pubkey, row.url);
if (row.type === 'lnurl' && row.ecosystem_json) {
try {
const fields = JSON.parse(row.ecosystem_json) as Partial<LnurlFields>;
// Both identifiers, always: a mint announced by host and later by pubkey has
// reviews under each, and both belong to this one row.
for (const id of [fields.lnurl_id, fields.mint_pubkey]) {
if (id) byLnurlId.set(id.toLowerCase(), row.url);
}
if (fields.base_url) byLnurlUrl.set(fields.base_url, row.url);
} catch {
// Unparseable column: this row simply cannot be matched by identifier. It still
// has a page and still resolves by its key, which is honest.
}
continue;
}
if (row.type !== 'fedimint' || !row.ecosystem_json) continue; if (row.type !== 'fedimint' || !row.ecosystem_json) continue;
try { try {
const fields = JSON.parse(row.ecosystem_json) as { const fields = JSON.parse(row.ecosystem_json) as {
@@ -283,7 +581,7 @@ async function loadMintIndex(): Promise<MintIndex> {
} }
} }
return { byHost, byPubkey, byFederation, byInvite }; return { byHost, byPubkey, byFederation, byInvite, byLnurlId, byLnurlUrl };
} }
/** /**
@@ -303,6 +601,7 @@ function resolveReviewTarget(event: NostrEvent, index: MintIndex): string | null
*/ */
const ecosystem = reviewEcosystem(event); const ecosystem = reviewEcosystem(event);
if (ecosystem === 'fedimint') return resolveFedimintReview(event, index); if (ecosystem === 'fedimint') return resolveFedimintReview(event, index);
if (ecosystem === 'lnurl') return resolveLnurlReview(event, index);
// A `k` naming a kind this build has no ecosystem for: not ours to file. // A `k` naming a kind this build has no ecosystem for: not ours to file.
if (ecosystem === null) return null; if (ecosystem === null) return null;
@@ -344,6 +643,39 @@ function resolveFedimintReview(event: NostrEvent, index: MintIndex): string | nu
return null; return null;
} }
/**
* Which LNURL mint a review is about.
*
* `d` first, matched against both identifiers a mint can have — its funding node's
* pubkey and its normalized host — because a mint that gained a funding source has
* reviews written under each and both name the same page.
*
* The `u` fallback is not the afterthought it is on the Fedimint side. An LNURL mint's
* `u` tag is a real, normalizable https URL, so a review that carries only an address
* resolves exactly as a Cashu review does, through the same normalizer. That matters
* for reviews written by clients that do not know this kind: `u` is the tag they are
* most likely to get right.
*
* A review of a mint this site has never seen resolves to nothing and is dropped, as it
* is for the other two ecosystems. There is no page to put it on.
*/
function resolveLnurlReview(event: NostrEvent, index: MintIndex): string | null {
const d = reviewTargetId(event);
if (d) {
const found = index.byLnurlId.get(d.trim().toLowerCase());
if (found) return found;
}
for (const raw of mintUrlsFromEvent(event)) {
const normalized = normalizeMintUrl(raw);
if (!normalized) continue;
const found = index.byLnurlUrl.get(normalized.url);
if (found) return found;
}
return null;
}
/** Postgres caps a statement at 65535 bounds parameters; this keeps every batch clear of it. */ /** Postgres caps a statement at 65535 bounds parameters; this keeps every batch clear of it. */
const INSERT_BATCH = 500; const INSERT_BATCH = 500;
@@ -402,8 +734,19 @@ async function ingestReviews(events: Iterable<NostrEvent>, index: MintIndex): Pr
*/ */
const target = resolveReviewTarget(e, index); const target = resolveReviewTarget(e, index);
if (!target) continue; if (!target) continue;
const fedimint = reviewTargetKind(e) === KIND_FEDIMINT_ANNOUNCEMENT; /*
if (!fedimint && !isCashuMintReview(e) && mintUrlsFromEvent(e).length === 0) continue; * A review whose `k` names an ecosystem this build knows has already proved itself
* by resolving: `resolveReviewTarget` runs one resolver per ecosystem and returns a
* row only for an event whose `k` matches that resolver, so nothing here has to
* re-test it. Written against `ANNOUNCEMENT_KINDS` rather than against a list of
* kind numbers, so a fourth ecosystem needs no edit here either.
*
* The `u` clause is what keeps the legacy events: a great many kind 38000 on the
* network carry no `k` at all, every one of them is about a Cashu mint, and they are
* accepted on having a resolvable mint URL exactly as they always were.
*/
const kindKnown = reviewTargetKind(e) !== null && reviewEcosystem(e) !== null;
if (!kindKnown && mintUrlsFromEvent(e).length === 0) continue;
const k = reviewTargetKind(e); const k = reviewTargetKind(e);
@@ -449,6 +792,7 @@ export async function runDiscovery(backfill: boolean): Promise<DiscoveryResult>
const since = lastRun === null ? null : Math.max(0, lastRun - 3600); const since = lastRun === null ? null : Math.max(0, lastRun - 3600);
const newMints = new Set<string>(); const newMints = new Set<string>();
const tally = new RelayTally(config.relays);
let newReviews = 0; let newReviews = 0;
let events = 0; let events = 0;
let ok = true; let ok = true;
@@ -463,23 +807,25 @@ export async function runDiscovery(backfill: boolean): Promise<DiscoveryResult>
const announcementsByType = new Map<string, NostrEvent[]>(); const announcementsByType = new Map<string, NostrEvent[]>();
await Promise.all( await Promise.all(
Object.entries(ANNOUNCEMENT_KINDS).map(async ([type, kind]) => { Object.entries(ANNOUNCEMENT_KINDS).map(async ([type, kind]) => {
announcementsByType.set(type, await fetchKind(kind, since)); announcementsByType.set(type, await fetchKind(kind, since, tally));
}), }),
); );
const announcements = announcementsByType.get('cashu') ?? []; const announcements = announcementsByType.get('cashu') ?? [];
const federations = announcementsByType.get('fedimint') ?? []; const federations = announcementsByType.get('fedimint') ?? [];
events += announcements.length + federations.length; const lnurlMints = announcementsByType.get('lnurl') ?? [];
events += announcements.length + federations.length + lnurlMints.length;
await ingestMintUrls(announcements, now, newMints); await ingestMintUrls(announcements, now, newMints);
await ingestFedimints(federations, now, newMints); await ingestFedimints(federations, now, newMints);
await ingestLnurl(lnurlMints, now, newMints);
// Announcements alone miss mints that only ever appear in a review's `u` tag, // Announcements alone miss mints that only ever appear in a review's `u` tag,
// so reviews feed discovery too. // so reviews feed discovery too.
const reviews = await fetchKind(KIND_REVIEW, since); const reviews = await fetchKind(KIND_REVIEW, since, tally);
// The recent window catches anything a relay dropped from the unbounded query. // The recent window catches anything a relay dropped from the unbounded query.
const recent = const recent =
since === null ? await fetchKind(KIND_REVIEW, now - RECENT_WINDOW_S) : []; since === null ? await fetchKind(KIND_REVIEW, now - RECENT_WINDOW_S, tally) : [];
const byId = new Map<string, NostrEvent>(); const byId = new Map<string, NostrEvent>();
for (const e of [...reviews, ...recent]) byId.set(e.id, e); for (const e of [...reviews, ...recent]) byId.set(e.id, e);
@@ -505,15 +851,32 @@ export async function runDiscovery(backfill: boolean): Promise<DiscoveryResult>
const targets: ReviewTarget[] = rows.map((row) => { const targets: ReviewTarget[] = rows.map((row) => {
let federationId: string | null = null; let federationId: string | null = null;
if (row.type === 'fedimint' && row.ecosystem_json) { let identifiers: string[] = [];
let baseUrl: string | null = null;
if (row.ecosystem_json) {
try { try {
federationId = if (row.type === 'fedimint') {
(JSON.parse(row.ecosystem_json) as { federation_id?: string }).federation_id ?? null; federationId =
(JSON.parse(row.ecosystem_json) as { federation_id?: string }).federation_id ?? null;
} else if (row.type === 'lnurl') {
const fields = JSON.parse(row.ecosystem_json) as Partial<LnurlFields>;
baseUrl = fields.base_url ?? null;
// Deduped, because a mint with no funding source has `lnurl_id` and
// `mint_pubkey` describing the same thing and a relay filter listing one
// value twice is one wasted slot.
identifiers = [...new Set(
[fields.lnurl_id, fields.mint_pubkey]
.filter((id): id is string => Boolean(id))
.map((id) => id.toLowerCase()),
)];
}
} catch { } catch {
// Nothing to ask by. The global sweep above already had its chance. // Nothing to ask by. The global sweep above already had its chance.
} }
} }
return { type: row.type, url: row.url, pubkey: row.pubkey, federationId };
return { type: row.type, url: row.url, pubkey: row.pubkey, federationId, identifiers, baseUrl };
}); });
const CONCURRENCY = 6; const CONCURRENCY = 6;
@@ -523,7 +886,7 @@ export async function runDiscovery(backfill: boolean): Promise<DiscoveryResult>
while (cursor < targets.length) { while (cursor < targets.length) {
const target = targets[cursor++]; const target = targets[cursor++];
if (!target) continue; if (!target) continue;
const found = await fetchReviewsForMint(target, since); const found = await fetchReviewsForMint(target, since, tally);
if (found.length > 0) { if (found.length > 0) {
events += found.length; events += found.length;
newReviews += await ingestReviews(found, index); newReviews += await ingestReviews(found, index);
@@ -541,14 +904,79 @@ export async function runDiscovery(backfill: boolean): Promise<DiscoveryResult>
log.error('discovery failed', { reason: err instanceof Error ? err.message : String(err) }); log.error('discovery failed', { reason: err instanceof Error ? err.message : String(err) });
} }
const mode = backfill ? 'backfill' : 'incremental';
const relays = tally.list();
/*
* Name the relay, every time, one line each.
*
* A relay that would not connect is worth saying on any cycle: the address is wrong,
* or it is down, and neither gets better by itself. A relay that connected and sent
* nothing is only news on a backfill — an incremental cycle asking for the last hour
* of four kinds legitimately comes back empty, and warning about that hourly would
* train everyone to skip the line that eventually matters.
*/
for (const relay of relays) {
if (!relay.connected) {
log.warn('discovery relay unreachable', { relay: relay.url, mode });
continue;
}
if (backfill && relay.events === 0) {
log.warn('discovery relay returned no events', { relay: relay.url, mode });
} else if (!relay.eose) {
log.warn('discovery relay never reached EOSE', {
relay: relay.url,
mode,
events: relay.events,
});
}
}
/*
* The floor, and the flag the health endpoint reads.
*
* Only a backfill is measured against it. A backfill asks for the entire history of
* every announcement kind and every review, so on a working relay set it is thousands
* of events; an incremental cycle asks for one interval and is supposed to be small.
*
* The flag is sticky across incremental cycles: an hourly cycle that finds four
* events must not clear a starvation a backfill diagnosed, so a non-backfill carries
* forward whatever the last backfill concluded.
*/
let starved: boolean;
if (backfill) {
starved = events < config.backfillMinEvents;
if (starved) {
log.error('discovery starvation suspected', {
events,
floor: config.backfillMinEvents,
relays: relays.length,
silent: relays.filter((r) => r.events === 0).length,
unreachable: relays.filter((r) => !r.connected).length,
hint: 'check RELAYS: a relay list missing the announcement archive looks exactly like this',
});
}
} else {
// No backfill has ever run in this deployment: nothing has confirmed the relay set
// reads anything, and saying "fine" would be the whole original bug.
starved = (await lastDiscoveryReport())?.starved ?? true;
}
const report: DiscoveryReport = { at: now, mode, events, ok, relays, starved };
// A report that cannot be written is not worth failing a cycle over; the cycle's own
// work is already committed, and health degrades on the stale timestamp instead.
await setState(REPORT_KEY, JSON.stringify(report)).catch(() => undefined);
log.info('discovery cycle', { log.info('discovery cycle', {
mode: backfill ? 'backfill' : 'incremental', mode,
events, events,
new_mints: newMints.size, new_mints: newMints.size,
new_reviews: newReviews, new_reviews: newReviews,
ok, ok,
starved,
relays: relays.map((r) => `${r.url}=${r.connected ? r.events : 'down'}`).join(' '),
ms: Date.now() - started, ms: Date.now() - started,
}); });
return { events, newMints: [...newMints], newReviews, ok }; return { events, newMints: [...newMints], newReviews, ok, relays, starved };
} }
+536
View File
@@ -0,0 +1,536 @@
/**
* On-demand indexing: `POST /api/index`.
*
* The fifth endpoint, and the first one that writes. Everything else this API serves is
* a read of rows the probe and discovery loops put there on their own schedule; this is
* a reader saying "I have an address you do not know, look at it now" and getting a page
* back in the same request.
*
* It exists because of a specific failure the site had: opening `/lnurl-mint/mint.600.wtf`
* — a live, healthy mint — returned the 404 page, because the only way into the index
* was a Nostr announcement that nobody had published. A directory whose answer to "here
* is a mint you have not got" is a 404 is a directory that can only ever list what other
* people already listed.
*
* The order of the checks below is the whole design, and each step is only reached
* because the one above it did not settle the question:
*
* 1. **Already indexed?** Then there is nothing to do and nothing to fetch. Answering
* from the row is not an optimisation, it is the correct answer: a mint's status is
* the probe loop's business, and a submission is not a reason to re-probe it.
* 2. **Does it answer, as what it claims to be?** One probe, the standard timeout,
* through the guarded fetcher in `safe-fetch.ts`.
* 3. **Does it answer as something else?** A Cashu mint pasted into the LNURL box is
* the single most likely mistake, and "that address answered, but as a Cashu mint"
* with a button to go there is worth far more than "invalid".
* 4. **Did it ever exist?** Silence over HTTPS is not proof of absence. One bounded
* relay lookup separates a mint that rugged from an address nobody ever used —
* see `relay-lookup.ts`, which is where that argument is made at length.
*
* A row written here is an ordinary row the moment it exists. The probe loop owns it
* from the next cycle, discovery will fill in its reviews, and nothing downstream can
* tell how it arrived.
*/
import {
fedimintKey,
federationIdFromInviteCode,
isIndexType,
isNut06Info,
lnurlKey,
normalizeMintUrl,
parseAdvertisement,
parsePayInfo,
PAY_INFO_PATH,
WITHDRAW_INFO_PATH,
type IndexFailure,
type IndexSource,
type IndexSuccess,
type IndexType,
type MintInfo,
} from '@cashumints/shared';
import { getDb } from './db.ts';
import { log } from './log.ts';
import { probeLnurl } from './lnurl-probe.ts';
import {
insertFedimintFromInvite,
insertLnurlIfNew,
insertMintIfNew,
mintByHost,
mintByUrl,
upsertLnurl,
type MintRow,
} from './mints.ts';
import { applyLnurlResult, recordCashuOnline } from './probe.ts';
import { getMintDetail, resetStatsCache } from './queries.ts';
import { traceOnRelays } from './relay-lookup.ts';
import { checkDestination, guardedTextFetcher, MAX_PROBE_BYTES, safeFetchText } from './safe-fetch.ts';
import { cacheIcon } from './icons.ts';
export interface IndexOutcome {
status: 200 | 201 | 404 | 422 | 429;
body: IndexSuccess | IndexFailure;
}
function fail(
status: 404 | 422 | 429,
error: IndexFailure['error'],
message: string,
extra: Partial<IndexFailure> = {},
): IndexOutcome {
return { status, body: { error, message, ...extra } };
}
/**
* The response body for a row, in the shape `GET /api/mints/:host` returns.
*
* Read back through `getMintDetail` rather than assembled here, so the payload a
* freshly indexed mint arrives in is byte for byte the payload it will have on every
* request after this one. The 404 resolver renders the same page shell from both, and
* a second shape to keep in step would be a second shape to get wrong.
*/
async function payload(host: string, existing: boolean, source?: IndexSource): Promise<IndexOutcome> {
const detail = await getMintDetail(host);
if (!detail) {
// The row was written moments ago; a miss here means the write did not land.
return fail(422, 'invalid_response', 'The mint was indexed but could not be read back');
}
// The new row changes the counts three pages print in a sentence, and those are
// memoised for a minute. A reader who has just indexed a mint should see it counted.
if (!existing) resetStatsCache();
return {
status: existing ? 200 : 201,
body: { ...detail, existing, ...(source ? { indexed_from: source } : {}) },
};
}
/* ---------- what kind of bad input is this? ---------- */
/**
* Why `normalizeMintUrl` said no.
*
* It answers null for everything, which is right for a discovery loop reading tags and
* wrong for a person who just typed something: "that is not a URL" and "we will not
* fetch a private address" are different mistakes with different fixes.
*/
function rejectionFor(input: string): IndexFailure['error'] {
const raw = input.trim();
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `https://${raw}`;
try {
const url = new URL(withScheme);
if (url.protocol !== 'https:' && url.protocol !== 'http:') return 'bad_input';
return url.hostname ? 'blocked_host' : 'bad_input';
} catch {
return 'bad_input';
}
}
/* ---------- cashu ---------- */
/** What one guarded `/v1/info` request concluded. */
type InfoProbe =
| { state: 'mint'; info: MintInfo; latencyMs: number }
| { state: 'answered'; detail: string }
| { state: 'blocked'; reason: string }
| { state: 'silent'; reason: string };
async function probeCashuInfo(url: string): Promise<InfoProbe> {
const started = Date.now();
const outcome = await safeFetchText(`${url}/v1/info`, {
accept: 'application/json',
maxBytes: MAX_PROBE_BYTES,
});
if (outcome.state === 'blocked') return { state: 'blocked', reason: outcome.reason };
if (outcome.state === 'unreachable') return { state: 'silent', reason: outcome.reason };
if (outcome.status < 200 || outcome.status >= 300) {
return { state: 'answered', detail: `HTTP ${outcome.status} from /v1/info` };
}
let body: unknown;
try {
body = JSON.parse(outcome.body) as unknown;
} catch {
return { state: 'answered', detail: 'the response was not JSON' };
}
if (!isNut06Info(body)) {
return { state: 'answered', detail: 'the response was not a NUT-06 mint info document' };
}
return { state: 'mint', info: body, latencyMs: Date.now() - started };
}
/* ---------- lnurl ---------- */
/**
* Is this host an LNURL mint, asked cheaply, for wrong-type detection only.
*
* The full `probeLnurl` fetches four endpoints and is what runs when LNURL is what was
* claimed. This is the other direction — a Cashu submission that did not answer as one
* — and only has to answer yes or no, so it reads the two advertisements and stops.
*/
async function looksLikeLnurl(url: string): Promise<boolean> {
const get = guardedTextFetcher();
const [withdraw, pay] = await Promise.all([
get(`${url}${WITHDRAW_INFO_PATH}`, 'application/json', MAX_PROBE_BYTES),
get(`${url}${PAY_INFO_PATH}`, 'application/json', MAX_PROBE_BYTES),
]);
const parsed = (raw: { body: string } | null, parse: (value: unknown) => unknown): boolean => {
if (!raw) return false;
try {
return parse(JSON.parse(raw.body) as unknown) !== null;
} catch {
return false;
}
};
return parsed(withdraw, parseAdvertisement) || parsed(pay, parsePayInfo);
}
/** The same question the other way round, for an LNURL submission that did not parse. */
async function looksLikeCashu(url: string): Promise<boolean> {
const probe = await probeCashuInfo(url);
return probe.state === 'mint';
}
/* ---------- writing a row that nothing answered for ---------- */
/**
* Record a mint that Nostr remembers and HTTPS does not: the rugged-mint case.
*
* `consecutive_fails` is set straight to the offline threshold rather than to the one
* failure that actually happened, and that is deliberate. The ladder in `statusForFails`
* only ever counts upwards, so a row stored as `offline` with one failure behind it
* would be *promoted* to `degraded` by its next failed probe — a mint appearing to
* recover by staying dark. Three failures is what "offline" means everywhere else in
* this database, so that is what an offline row carries.
*/
async function markOfflineFromAnnouncement(
row: MintRow,
meta: { name: string | null; about: string | null; picture: string | null },
now: number,
): Promise<void> {
const db = await getDb();
const iconFile = meta.picture ? await cacheIcon(row, meta.picture) : row.icon_file;
await db.run(
`UPDATE mints SET
name = COALESCE(name, ?),
description = COALESCE(description, ?),
icon_url = COALESCE(icon_url, ?),
icon_file = ?,
status = 'offline',
consecutive_fails = 3,
last_probe = ?,
updated_at = ?
WHERE url = ?`,
meta.name,
meta.about,
meta.picture,
iconFile,
now,
now,
row.url,
);
// One real failed probe, because one real probe really did fail. The uptime figure
// and the sparkline should show that this site looked and got nothing.
await db.run(
'INSERT INTO probes (mint_url, ts, ok, latency_ms) VALUES (?, ?, 0, NULL)',
row.url,
now,
);
}
/**
* The last resort for a URL nothing answered at: ask the relays, and index what they
* remember. Returns the outcome to send back, whichever way it went.
*/
async function fromRelays(
type: 'cashu' | 'lnurl',
url: string,
now: number,
): Promise<IndexOutcome> {
const trace = await traceOnRelays(type, url);
if (!trace.found) {
return fail(
404,
'unverifiable',
'Nothing answered at that address, and no announcement or review of it exists on Nostr',
);
}
/*
* An LNURL announcement is written through the same path discovery uses, so the row
* carries its features, network and announcer exactly as it would have — but only when
* the announcement is about *this* address. A `#d` filter can also match an
* announcement whose `u` is a different URL under the same host identifier, and that
* one belongs to a different row.
*/
const useAnnouncement = type === 'lnurl' && trace.lnurl?.baseUrl === url;
const created = useAnnouncement
? await upsertLnurl(trace.lnurl!, now)
: type === 'lnurl'
? await insertLnurlIfNew(url, now)
: await insertMintIfNew(url, now);
const key = created ?? (type === 'lnurl' ? lnurlKey(url) : url);
const row = await mintByUrl(key);
if (!row) return fail(422, 'invalid_response', 'The mint could not be indexed');
/*
* Only a row this call brought into being is marked offline.
*
* The `existing` check upstream means there is almost always one, but an upsert can
* land on a row that was already there — and a row the probe loop has an opinion about
* is not one an unreachable submission gets to overwrite. Its status is the loop's to
* decide; this returns what is on file instead.
*/
if (created === null && row.status !== 'unknown') return payload(row.host, true);
await markOfflineFromAnnouncement(
row,
{ name: trace.name, about: trace.about, picture: trace.picture },
now,
);
log.info('indexed from nostr', {
url: key,
type,
reviews: trace.reviews,
announced: trace.announcedAt !== null,
});
return payload(row.host, false, 'announcement');
}
/* ---------- the three type paths ---------- */
async function indexFedimint(
code: string,
federationId: string,
now: number,
): Promise<IndexOutcome> {
const key = fedimintKey(federationId);
await insertFedimintFromInvite(code, federationId, now);
const row = await mintByUrl(key);
if (!row) return fail(422, 'invalid_invite', 'The federation could not be indexed');
log.info('indexed from invite code', { url: key });
return payload(row.host, false, 'invite');
}
async function indexCashu(url: string, host: string, now: number): Promise<IndexOutcome> {
const probe = await probeCashuInfo(url);
if (probe.state === 'blocked') {
return fail(422, 'blocked_host', `That address cannot be checked: ${probe.reason}`);
}
if (probe.state === 'answered') {
if (await looksLikeLnurl(url)) {
return fail(422, 'wrong_type', 'That address answered as an LNURL mint, not a Cashu mint', {
detected_type: 'lnurl',
});
}
return fail(422, 'invalid_response', `That address answered, but ${probe.detail}`);
}
if (probe.state === 'silent') return fromRelays('cashu', url, now);
// A live mint. Insert, then write exactly what a successful probe writes.
await insertMintIfNew(url, now);
const row = await mintByUrl(url);
if (!row) {
/*
* The URL is fine and the mint answered, so the only way here is a slug already
* held by a different URL — `host/a-b` and `host/a/b` both slug to `host-a-b`. That
* is a real limitation of the routing scheme rather than anything the reader did,
* and it is worth saying so plainly instead of claiming their mint is invalid.
*/
const holder = await mintByHost(host);
return fail(
422,
'invalid_response',
holder
? `That mint's page address is already taken by ${holder.url}`
: 'The mint could not be indexed',
);
}
await recordCashuOnline(row, probe.info, probe.latencyMs, now);
log.info('indexed on demand', { url, type: 'cashu', status: 'online' });
return payload(row.host, false, 'probe');
}
async function indexLnurl(url: string, now: number): Promise<IndexOutcome> {
// The base URL's own address is checked once here, so a blocked host is reported as
// blocked rather than as four endpoints that happened not to answer.
let parsed: URL;
try {
parsed = new URL(url);
} catch {
return fail(422, 'bad_input', 'That is not a URL');
}
const verdict = await checkDestination(parsed);
if (verdict?.kind === 'blocked') {
return fail(422, 'blocked_host', `That address cannot be checked: ${verdict.reason}`);
}
// A host whose name no longer resolves is the rugged case, not a bad submission.
if (verdict) return fromRelays('lnurl', url, now);
let result: Awaited<ReturnType<typeof probeLnurl>>;
try {
result = await probeLnurl(url, guardedTextFetcher());
} catch {
// Neither endpoint answered at all. It may still be a mint that used to be one.
return fromRelays('lnurl', url, now);
}
if (result.outcome === 'invalid') {
if (await looksLikeCashu(url)) {
return fail(422, 'wrong_type', 'That address answered as a Cashu mint, not an LNURL mint', {
detected_type: 'cashu',
});
}
return fail(
422,
'invalid_response',
`That address answered, but not as an LNURL mint: ${result.invalidReason ?? 'unrecognised response'}`,
);
}
const key = lnurlKey(url);
await insertLnurlIfNew(url, now);
const row = await mintByUrl(key);
if (!row) return fail(422, 'invalid_response', 'The mint could not be indexed');
await applyLnurlResult(row, result, now);
log.info('indexed on demand', { url: key, type: 'lnurl', status: 'online' });
return payload(row.host, false, 'probe');
}
/* ---------- the entry point ---------- */
/**
* In-flight submissions, keyed by what they would create.
*
* Two readers pasting the same address at the same moment — which is exactly what
* happens when a link is shared — must produce one probe and one row, not a race
* between two inserts and two visits to a stranger's mint. The second caller awaits the
* first one's promise and gets its answer.
*
* Keyed on the *normalized* identifier rather than the raw input, so `mint.600.wtf` and
* `https://mint.600.wtf/` collapse into one entry for the same reason they collapse
* into one row.
*/
const inFlight = new Map<string, Promise<IndexOutcome>>();
/** How many submissions are being worked on right now. For the checks. */
export function inFlightCount(): number {
return inFlight.size;
}
/** How many probes may be in flight at once, across every submitter. */
const MAX_IN_FLIGHT = 8;
export async function indexSubmission(rawType: string, rawInput: unknown): Promise<IndexOutcome> {
if (!isIndexType(rawType)) {
return fail(422, 'bad_type', 'type must be one of cashu, fedimint or lnurl');
}
const type: IndexType = rawType;
if (typeof rawInput !== 'string' || rawInput.trim() === '') {
return fail(422, 'bad_input', 'input is required');
}
// Long enough for any real invite code (they run to ~400 characters), short enough
// that nothing absurd reaches a parser.
const input = rawInput.trim().slice(0, 2048);
let key: string;
let url = '';
let host = '';
if (type === 'fedimint') {
const federationId = federationIdFromInviteCode(input);
if (!federationId) return fail(422, 'invalid_invite', 'That is not a Fedimint invite code');
key = fedimintKey(federationId);
} else {
const normalized = normalizeMintUrl(input);
if (!normalized) {
const reason = rejectionFor(input);
return fail(
422,
reason,
reason === 'blocked_host'
? 'That address is not a public one this site will check'
: 'That is not a mint URL',
);
}
url = normalized.url;
host = normalized.host;
key = type === 'lnurl' ? lnurlKey(url) : url;
}
/*
* The dedup check comes before every await that follows, and that ordering is the
* whole guarantee. An earlier version looked the row up first and registered the
* in-flight entry afterwards, which left a window: two requests that arrived in the
* same tick both got past the lookup before either had registered, and both probed.
* Nothing may be awaited between computing the key and claiming it.
*/
const pending = inFlight.get(key);
if (pending) return pending;
if (inFlight.size >= MAX_IN_FLIGHT) {
/*
* A global ceiling on outbound probes, under the same reason code as the per-address
* limit because it is the same answer to the reader: not now, try shortly. The
* per-address budget already stops one browser tab from doing this; this stops many
* of them at once from turning this process into a load generator pointed at
* whatever host they picked.
*/
return fail(429, 'rate_limited', 'Too many checks in flight right now', { retry_after: 30 });
}
const now = Math.floor(Date.now() / 1000);
const work = (async (): Promise<IndexOutcome> => {
/*
* Is this identifier already a row? The same lookup for all three types, by key
* first and then by routing slug, because the slug is what the insert path dedupes
* on — `https://mint.example.com/Bitcoin` and its lowercase spelling are one mint
* under two URLs, and answering "not indexed" for the second would create the
* duplicate the normalizer exists to prevent. Nothing is probed on this path.
*/
const existing =
(await mintByUrl(key)) ?? (host ? await rowByHostOfType(host, type) : undefined);
if (existing) return payload(existing.host, true);
if (type === 'fedimint') {
return indexFedimint(input.toLowerCase(), key.slice('fedimint:'.length), now);
}
if (type === 'lnurl') return indexLnurl(url, now);
return indexCashu(url, host, now);
})().finally(() => inFlight.delete(key));
inFlight.set(key, work);
return work;
}
/**
* A row holding this routing slug, but only when it belongs to the ecosystem asked
* about.
*
* An LNURL mint and a Cashu mint can share a hostname, and the LNURL one then takes the
* `lnurl-` prefixed slug (see `insertLnurlIfNew`). Without the type test, submitting
* that LNURL mint's URL would find the *Cashu* row on the bare slug and answer "already
* indexed" with the wrong mint's page.
*/
async function rowByHostOfType(host: string, type: IndexType): Promise<MintRow | undefined> {
const row = await mintByHost(host);
return row && row.type === type ? row : undefined;
}
+15
View File
@@ -1,4 +1,5 @@
import { serve } from '@hono/node-server'; import { serve } from '@hono/node-server';
import { announceLnurlMints } from './announce.ts';
import { config } from './config.ts'; import { config } from './config.ts';
import { closeDb, getDb, pruneProbes } from './db.ts'; import { closeDb, getDb, pruneProbes } from './db.ts';
import { closePool, runDiscovery } from './discovery.ts'; import { closePool, runDiscovery } from './discovery.ts';
@@ -31,6 +32,20 @@ async function probeCycle(): Promise<void> {
probeRunning = true; probeRunning = true;
try { try {
await track(probeAll()); await track(probeAll());
/*
* Announcing runs after probing, in the same cycle, because it publishes only what
* the probe just confirmed — running it on its own timer would mean signing for a
* state that could be an interval old. Off by default; see `announceConfig`.
*
* Its own failures are caught here rather than allowed to mark the probe cycle
* failed: whether relays accepted an event says nothing about whether this site
* successfully checked its mints.
*/
await track(announceLnurlMints()).catch((err: unknown) => {
log.error('announce cycle failed', {
reason: err instanceof Error ? err.message : String(err),
});
});
} catch (err) { } catch (err) {
log.error('probe cycle failed', { reason: err instanceof Error ? err.message : String(err) }); log.error('probe cycle failed', { reason: err instanceof Error ? err.message : String(err) });
} finally { } finally {
+274
View File
@@ -0,0 +1,274 @@
/**
* Checking an LNURL mint.
*
* Unlike a federation, which has no public endpoint and is checked by reading somebody
* else's index, an LNURL mint answers over HTTPS and this site checks it itself — the
* same relationship it has with a Cashu mint. What differs is that "answered" and
* "working" are two questions here rather than one, and the endpoint layout is not what
* the software's own README describes. `NOTES-LNURL.md` records what is actually on the
* wire; the three rules that shape this file are:
*
* 1. **There is no bare `/p`.** The mint advertisement — withdraw limits, description,
* mint pubkey, node identity — lives at `/.well-known/lnurlw/_`. The payRequest at
* `/.well-known/lnurlp/_` carries the pay-side limits and the fee, and is the
* fallback when the withdraw side does not answer.
* 2. **HTTP 200 proves nothing.** Every registered route returns 200 with an LNURL
* `{"status":"ERROR"}` body for its errors. Only the parsed body decides.
* 3. **Absence is the signal.** `None` fields are dropped from responses entirely, so
* a missing `mintPubkey` is how "the funding source is not reachable" reaches the
* wire. That is the degraded-but-online state.
*
* Nothing here calls anything that mutates. `/p/cb` would make the mint issue a real
* invoice on its operator's node, and `/w/cb` burns notes; neither is something a
* directory gets to do to a stranger on a ten minute timer. The cost of that restraint
* is that LUD-21 verify cannot be detected at all — see the kind document, which makes
* that normative rather than incidental.
*/
import {
PAY_INFO_PATH,
WITHDRAW_INFO_PATH,
addressFromPayLink,
onionFromHtml,
parseAdvertisement,
parsePayInfo,
parseSoftware,
type LnurlAdvertisement,
type LnurlPayInfo,
} from '@cashumints/shared';
import { config } from './config.ts';
import { readBodyBounded } from './http.ts';
/**
* Body caps, per endpoint.
*
* The JSON documents are a few hundred bytes; 64KB is room for a mint with an unusually
* chatty metadata blob and nothing more. The one-pager is real HTML with an inline QR
* SVG — 13.8KB on the live instance — so it gets its own, larger cap. Both bound an
* untrusted server, in bytes, on top of the abort timer that bounds it in seconds.
*/
const MAX_JSON_BYTES = 64 * 1024;
const MAX_HTML_BYTES = 512 * 1024;
/** What one probe of one LNURL mint concluded. */
export interface LnurlProbeResult {
/**
* `online` — a mint advertisement parsed, and the mint's Lightning node answered.
* `degraded-funding` — an advertisement parsed, but the node behind it did not, so
* minting and melting are unavailable while rotate/split/merge still work. Still
* an `ok` probe: the mint is up, and this is what warnings are for.
* `invalid` — the host answered with something that is not a mint advertisement.
* Neither up nor down, and counted as a failed probe with a reason attached.
*/
outcome: 'online' | 'degraded-funding' | 'invalid';
latencyMs: number;
/** Which path answered. Stored so a page can say what was actually checked. */
endpoint: string;
advertisement: LnurlAdvertisement | null;
pay: LnurlPayInfo | null;
lightningAddress: string | null;
onionUrl: string | null;
software: string | null;
/** Short, human-readable, and only set when `outcome` is `invalid`. */
invalidReason: string | null;
}
/**
* How this module reaches a mint.
*
* A parameter rather than a hard-wired `fetch`, because there are two callers with
* genuinely different threat models. The probe loop checks addresses that reached the
* database through discovery or the seed list, and uses the plain fetcher below.
* `POST /api/index` checks an address a stranger typed thirty seconds ago, and passes
* the guarded one from `safe-fetch.ts`, which resolves DNS and refuses private ranges
* before a socket opens. The parsing, the endpoint layout and every rule about what
* counts as a mint are identical either way, which is the point of the seam.
*/
export type TextFetcher = (
url: string,
accept: string,
maxBytes: number,
) => Promise<{ body: string; status: number } | null>;
/** A fetch that is bounded in time and in bytes, and never throws for a caller. */
const fetchBounded: TextFetcher = async (
url: string,
accept: string,
maxBytes: number,
): Promise<{ body: string; status: number } | null> => {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), config.probeTimeoutMs);
try {
const res = await fetch(url, {
signal: controller.signal,
headers: { Accept: accept, 'User-Agent': config.userAgent },
redirect: 'follow',
});
const body = await readBodyBounded(res, maxBytes);
if (!body) return null;
return { body: body.toString('utf8'), status: res.status };
} catch {
// Timed out, DNS failure, TLS failure, connection reset. All the same to a caller:
// nothing was learned.
return null;
} finally {
clearTimeout(timer);
}
};
/**
* What one JSON endpoint did.
*
* Three outcomes, not two, and the middle one is why this is not simply
* `Promise<unknown | null>`: a host that answered with something that will not parse is
* *reachable*, and the site owes its readers a different sentence for that than for a
* host that is not there at all. Collapsing them would file every misconfigured proxy
* and every parked domain under "offline".
*/
type JsonProbe =
| { state: 'unreachable' }
| { state: 'unparseable'; status: number }
| { state: 'parsed'; value: unknown; status: number };
async function fetchJson(url: string, get: TextFetcher): Promise<JsonProbe> {
const res = await get(url, 'application/json', MAX_JSON_BYTES);
if (!res) return { state: 'unreachable' };
try {
return { state: 'parsed', value: JSON.parse(res.body) as unknown, status: res.status };
} catch {
return { state: 'unparseable', status: res.status };
}
}
/** The parsed body, or null for anything that did not parse. */
function jsonValue(probe: JsonProbe): unknown {
return probe.state === 'parsed' ? probe.value : null;
}
/**
* The reason string for a response that arrived but was not a mint advertisement.
*
* Kept short and specific, because it is what the "responding but invalid" banner shows
* underneath and because the three cases are genuinely different problems: a host that
* is not this software at all, a host that is but is refusing, and a host serving
* something that is not JSON.
*/
function invalidReasonFor(probe: JsonProbe): string {
if (probe.state === 'unreachable') return 'no response';
if (probe.state === 'unparseable') {
// Almost always an HTML error page from a proxy, or a parked domain. The status
// code is the only part of it worth repeating back.
return `HTTP ${probe.status}, and the body was not JSON`;
}
const body = probe.value;
if (body === null || body === undefined) return 'empty JSON response';
if (typeof body !== 'object' || Array.isArray(body)) return 'response was not a JSON object';
const record = body as Record<string, unknown>;
// The LNURL error convention: 200 with a status/reason pair. Common and informative.
if (record['status'] === 'ERROR') {
const reason = typeof record['reason'] === 'string' ? record['reason'].slice(0, 120) : '';
return reason ? `mint replied: ${reason}` : 'mint replied with an LNURL error';
}
if (typeof record['tag'] === 'string') return `unexpected tag "${String(record['tag']).slice(0, 40)}"`;
if (record['detail'] !== undefined) return 'endpoint not found on this host';
return 'response was not a mint advertisement';
}
/**
* Probe one LNURL mint.
*
* The withdraw side first, because it is the only endpoint carrying everything the site
* renders. The payRequest is fetched too, but for different reasons in the two cases:
* alongside a good advertisement it adds the fee, the pay-side limits and a
* human-written description; when the advertisement failed it is the fallback that can
* still prove the host is a live LNURL mint whose withdraw alias is simply configured
* under a username this probe cannot guess.
*
* The two opportunistic reads — the one-pager for a Tor address, `/openapi.json` for a
* version — never affect the outcome. A mint is not less online because its operator
* turned off the docs endpoint.
*/
export async function probeLnurl(
baseUrl: string,
get: TextFetcher = fetchBounded,
): Promise<LnurlProbeResult> {
const started = Date.now();
const [withdrawProbe, payProbe] = await Promise.all([
fetchJson(`${baseUrl}${WITHDRAW_INFO_PATH}`, get),
fetchJson(`${baseUrl}${PAY_INFO_PATH}`, get),
]);
const advertisement = parseAdvertisement(jsonValue(withdrawProbe));
const pay = parsePayInfo(jsonValue(payProbe));
const latencyMs = Date.now() - started;
const base = {
latencyMs,
advertisement,
pay,
lightningAddress: addressFromPayLink(advertisement?.payLink) ?? null,
};
if (!advertisement && !pay) {
/*
* Nothing parsed on either side. Distinguish "the host said something" from "the
* host said nothing at all": a timeout is an ordinary failed probe and feeds the
* consecutive-fails machinery as a plain offline, while a reply that is not a mint
* advertisement is the invalid state and gets to say why.
*/
const answered =
withdrawProbe.state !== 'unreachable' || payProbe.state !== 'unreachable';
if (!answered) throw new Error('no response from either LNURL endpoint');
/*
* Report on whichever endpoint actually said something, preferring the withdraw
* side. A host whose withdraw alias times out while its payRequest returns an HTML
* error page should say what the payRequest did, not "no response".
*/
const reported = withdrawProbe.state === 'unreachable' ? payProbe : withdrawProbe;
return {
...base,
outcome: 'invalid',
endpoint: withdrawProbe.state === 'unreachable' ? PAY_INFO_PATH : WITHDRAW_INFO_PATH,
onionUrl: null,
software: null,
invalidReason: invalidReasonFor(reported),
};
}
// Only worth two extra requests once the host has proved it is a mint.
const [html, openapi] = await Promise.all([
get(`${baseUrl}/`, 'text/html', MAX_HTML_BYTES),
fetchJson(`${baseUrl}/openapi.json`, get),
]);
const extras = {
onionUrl: html ? onionFromHtml(html.body) : null,
software: parseSoftware(jsonValue(openapi)),
invalidReason: null,
};
if (!advertisement) {
/*
* The payRequest answered and the withdraw alias did not.
*
* Still online — the host is demonstrably a live LNURL mint — but the site has no
* withdraw limits, no mint pubkey and no node identity for it, and `funding_available`
* stays unknown rather than being guessed at from the pay side. `/.well-known/lnurlp/_`
* is recorded as the endpoint so the page says what was actually checked.
*/
return { ...base, ...extras, outcome: 'online', endpoint: PAY_INFO_PATH };
}
return {
...base,
...extras,
outcome: advertisement.fundingAvailable ? 'online' : 'degraded-funding',
endpoint: WITHDRAW_INFO_PATH,
};
}
+292 -7
View File
@@ -1,6 +1,6 @@
import { import {
fedimintKey, fedimintSlug, normalizeMintUrl, LNURL_SLUG_PREFIX, fedimintKey, fedimintSlug, lnurlIdentifier, lnurlKey, normalizeMintUrl,
type FedimintAnnouncement, type FedimintFields, type FedimintAnnouncement, type FedimintFields, type LnurlAnnouncement, type LnurlFields,
} from '@cashumints/shared'; } from '@cashumints/shared';
import { getDb } from './db.ts'; import { getDb } from './db.ts';
import { log } from './log.ts'; import { log } from './log.ts';
@@ -18,7 +18,7 @@ const reportedSkips = new Set<string>();
export interface MintRow { export interface MintRow {
url: string; url: string;
host: string; host: string;
/** 'cashu' or 'fedimint'. Rows written before the column existed default to 'cashu'. */ /** 'cashu', 'fedimint' or 'lnurl'. Rows written before the column existed default to 'cashu'. */
type: string; type: string;
name: string | null; name: string | null;
description: string | null; description: string | null;
@@ -26,7 +26,10 @@ export interface MintRow {
icon_file: string | null; icon_file: string | null;
pubkey: string | null; pubkey: string | null;
info_json: string | null; info_json: string | null;
/** Type-specific data. `FedimintFields` for a federation, null for a Cashu mint. */ /**
* Type-specific data: `FedimintFields` for a federation, `LnurlFields` for an LNURL
* mint, null for a Cashu one. Read it back through `parseEcosystem`.
*/
ecosystem_json: string | null; ecosystem_json: string | null;
nuts_json: string | null; nuts_json: string | null;
version: string | null; version: string | null;
@@ -106,11 +109,23 @@ export async function insertMintIfNew(
/* ---------- fedimint ---------- */ /* ---------- fedimint ---------- */
/** Read a federation row's type-specific columns back out. */ /**
export function parseEcosystem(row: Pick<MintRow, 'ecosystem_json'>): FedimintFields | null { * Read a row's type-specific column back out.
*
* Generic, defaulting to `FedimintFields` so every existing call site reads exactly as
* it did. The caller already knows the row's `type` — it is what decided to call this
* at all — so the type argument is a statement of what was stored, not a guess.
*
* A column that will not parse degrades to null rather than throwing. Such a row still
* has a page and still resolves by its key; it simply has no ecosystem-specific fields
* on it, which is honest and is better than a probe cycle dying on one bad row.
*/
export function parseEcosystem<T = FedimintFields>(
row: Pick<MintRow, 'ecosystem_json'>,
): T | null {
if (!row.ecosystem_json) return null; if (!row.ecosystem_json) return null;
try { try {
return JSON.parse(row.ecosystem_json) as FedimintFields; return JSON.parse(row.ecosystem_json) as T;
} catch { } catch {
return null; return null;
} }
@@ -196,12 +211,282 @@ export async function upsertFedimint(
return null; return null;
} }
/**
* Insert a federation from an invite code alone, with no announcement behind it.
*
* For `POST /api/index`: a reader pastes the one thing they have, and there is nothing
* else to go on. The id comes out of the code itself (`federationIdFromInviteCode`), so
* the row is keyed exactly as an announced one would be and the two can never become
* two rows for one federation.
*
* `announced` rather than `unknown`, and the distinction is the same one the status
* carries everywhere else: `unknown` means nothing has checked yet, `announced` means
* there is nothing this site *can* check. A federation has no public endpoint, so an
* invite code is exactly as much as anyone will ever be able to confirm from here until
* the observer lookup covers it.
*
* Returns the row key when a row was created, null when one already existed.
*/
export async function insertFedimintFromInvite(
code: string,
federationId: string,
now = Math.floor(Date.now() / 1000),
): Promise<string | null> {
const db = await getDb();
const url = fedimintKey(federationId);
const fields: FedimintFields = {
federation_id: federationId,
invite_codes: [code.trim().toLowerCase()],
// Everything an announcement would have carried. Discovery fills these in when one
// turns up, and until then the page says only what the code itself proves.
modules: [],
network: null,
announced_at: null,
announcer_pubkey: null,
status_source: null,
};
const result = await db.run(
`INSERT INTO mints (url, host, type, ecosystem_json, status, first_seen, updated_at)
VALUES (?, ?, 'fedimint', ?, 'announced', ?, ?)
ON CONFLICT DO NOTHING`,
url,
fedimintSlug(federationId),
JSON.stringify(fields),
now,
now,
);
return result.changes > 0 ? url : null;
}
/** Every federation row, for the probe cycle and for review resolution. */ /** Every federation row, for the probe cycle and for review resolution. */
export async function fedimintRows(): Promise<MintRow[]> { export async function fedimintRows(): Promise<MintRow[]> {
const db = await getDb(); const db = await getDb();
return db.all<MintRow>(`SELECT * FROM mints WHERE type = 'fedimint'`); return db.all<MintRow>(`SELECT * FROM mints WHERE type = 'fedimint'`);
} }
/* ---------- lnurl ---------- */
/**
* The routing slug an LNURL mint gets, given what the table already holds.
*
* `mints.host` carries a UNIQUE index across every ecosystem, so a Cashu mint and an
* LNURL mint on one hostname would compete for a single slug and — with the existing
* insert path — the loser would silently not be tracked at all. That is the one outcome
* worth writing code to avoid: a mint that exists, is reviewable, and has no page.
*
* So the clean slug is used whenever it is free, which is very nearly always, and a
* colliding row takes `lnurl-` in front instead. Decided once at insert and then stored,
* never recomputed: a mint that took the prefixed slug keeps it even if the row it
* collided with later disappears, because its URL is already indexed and linked and a
* silently moving page is worse than a slightly long one.
*/
async function lnurlSlug(preferred: string, url: string): Promise<string | null> {
const db = await getDb();
const takenBy = async (slug: string): Promise<string | null> => {
const row = await db.get<{ url: string }>('SELECT url FROM mints WHERE host = ?', slug);
return row && row.url !== url ? row.url : null;
};
const clash = await takenBy(preferred);
if (!clash) return preferred;
const prefixed = LNURL_SLUG_PREFIX + preferred;
const secondClash = await takenBy(prefixed);
if (!secondClash) {
log.info('lnurl slug taken, using prefixed form', { url, slug: prefixed, existing: clash });
return prefixed;
}
log.warn('lnurl slug collision, mint not tracked', { url, slug: preferred, existing: clash });
return null;
}
/** The `ecosystem_json` an LNURL row starts life with, before anything has probed it. */
function blankLnurlFields(baseUrl: string, identifier: string): LnurlFields {
return {
lnurl_id: identifier,
base_url: baseUrl,
features: [],
network: null,
announced_at: null,
announcer_pubkey: null,
mint_pubkey: null,
funding_available: null,
probe_endpoint: null,
invalid_reason: null,
min_withdrawable_msat: null,
max_withdrawable_msat: null,
min_sendable_msat: null,
max_sendable_msat: null,
fee_base_msat: null,
fee_ppm: null,
lightning_address: null,
onion_url: null,
node_alias: null,
node_uri: null,
node_capacity_msat: null,
node_channels: null,
node_peers: null,
observed_features: [],
};
}
/**
* Insert an LNURL mint by URL alone, with no announcement behind it.
*
* For the seed list and for operators added by hand. `status` starts `unknown` and the
* very next probe cycle decides it, exactly as a seeded Cashu mint works — nothing here
* claims the mint is up, and `insertMintIfNew`'s rule that an existing row is never
* touched applies just as strictly.
*
* Returns the row key when a row was created, null otherwise.
*/
export async function insertLnurlIfNew(
rawUrl: string,
now = Math.floor(Date.now() / 1000),
): Promise<string | null> {
const normalized = normalizeMintUrl(rawUrl);
if (!normalized) {
if (!reportedSkips.has(rawUrl)) {
reportedSkips.add(rawUrl);
log.warn('skipped lnurl url', { url: rawUrl.slice(0, 80) });
}
return null;
}
const db = await getDb();
const url = lnurlKey(normalized.url);
const existing = await db.get<{ url: string }>('SELECT url FROM mints WHERE url = ?', url);
if (existing) return null;
const host = await lnurlSlug(normalized.host, url);
if (host === null) return null;
const result = await db.run(
`INSERT INTO mints (url, host, type, ecosystem_json, status, first_seen, updated_at)
VALUES (?, ?, 'lnurl', ?, 'unknown', ?, ?)
ON CONFLICT DO NOTHING`,
url,
host,
JSON.stringify(blankLnurlFields(normalized.url, lnurlIdentifier(normalized.url, null))),
now,
now,
);
return result.changes > 0 ? url : null;
}
/**
* Insert or refresh an LNURL mint from a kind 38174 announcement.
*
* **Deduped on the normalized base URL, not on the `d` tag**, and that is the whole
* reason this function is not a copy of `upsertFedimint`. An LNURL mint's identifier is
* its funding node's pubkey when it has one and its host otherwise, so the same mint
* legitimately appears under two different `d` values across its life — announced by
* host before a funding source was configured, by pubkey afterwards. Keying rows on `d`
* would split one mint into two pages with half its reviews on each. The `u` tag is the
* one value present in every form of the event, so it is the key.
*
* Unlike `insertMintIfNew` this does update an existing row, because an announcement is
* the only source of `features`, the network and the operator's own name for the mint.
* Older announcements are ignored (`announced_at` goes forwards only) so a replayed
* event from last year cannot overwrite this week's capability list.
*
* Two things are deliberately never written here:
*
* - **The status columns.** Whether a mint is up is a probe's answer. An announcement
* is not evidence of anything being up.
* - **Anything a probe learned.** `mint_pubkey`, the limits, the address, the node
* fields and `funding_available` are all carried over from the existing row
* untouched. In particular a `mint_pubkey` already observed is *never* cleared by an
* announcement that lacks one: identity is sticky, per the kind document.
*
* Returns the row's key when a row was created, null when one was merely updated.
*/
export async function upsertLnurl(
announcement: LnurlAnnouncement,
now = Math.floor(Date.now() / 1000),
): Promise<string | null> {
const db = await getDb();
const url = lnurlKey(announcement.baseUrl);
const existing = await db.get<MintRow>('SELECT * FROM mints WHERE url = ?', url);
const previous = existing ? parseEcosystem<LnurlFields>(existing) : null;
if (previous && (previous.announced_at ?? 0) > announcement.announcedAt) return null;
/*
* The stored pubkey wins over the announcement's.
*
* A probe reads `mintPubkey` off the mint itself; an announcement is a stranger's
* claim about it. Where both exist the observed one is the better evidence, and where
* only the announcement has one it is still worth keeping, so this prefers the probe
* and falls back rather than overwriting in either direction.
*/
const mintPubkey = previous?.mint_pubkey ?? announcement.mintPubkey;
const fields: LnurlFields = {
...(previous ?? blankLnurlFields(announcement.baseUrl, announcement.identifier)),
lnurl_id: lnurlIdentifier(announcement.baseUrl, mintPubkey),
base_url: announcement.baseUrl,
features: announcement.features,
network: announcement.network,
announced_at: announcement.announcedAt,
announcer_pubkey: announcement.announcerPubkey,
mint_pubkey: mintPubkey,
};
if (!existing) {
const host = await lnurlSlug(announcement.slug, url);
if (host === null) return null;
await db.run(
`INSERT INTO mints (url, host, type, name, description, icon_url, ecosystem_json,
status, first_seen, updated_at)
VALUES (?, ?, 'lnurl', ?, ?, ?, ?, 'unknown', ?, ?)
ON CONFLICT DO NOTHING`,
url,
host,
announcement.name,
announcement.about,
announcement.picture,
JSON.stringify(fields),
now,
now,
);
return url;
}
await db.run(
`UPDATE mints SET
name = COALESCE(?, name),
description = COALESCE(?, description),
icon_url = COALESCE(?, icon_url),
ecosystem_json = ?,
updated_at = ?
WHERE url = ?`,
announcement.name,
announcement.about,
announcement.picture,
JSON.stringify(fields),
now,
url,
);
return null;
}
/** Every LNURL row, for the probe cycle and for review resolution. */
export async function lnurlRows(): Promise<MintRow[]> {
const db = await getDb();
return db.all<MintRow>(`SELECT * FROM mints WHERE type = 'lnurl'`);
}
/** Canonical mint URL for a normalized slug, or null if no such mint is tracked. */ /** Canonical mint URL for a normalized slug, or null if no such mint is tracked. */
export async function mintUrlByHost(host: string): Promise<string | null> { export async function mintUrlByHost(host: string): Promise<string | null> {
const db = await getDb(); const db = await getDb();
+373 -48
View File
@@ -1,10 +1,14 @@
import { parseNuts, type FedimintFields, type MintInfo } from '@cashumints/shared'; import {
lnurlIdentifier, observedFeatures, parseNuts,
type FedimintFields, type LnurlFields, type MintInfo,
} from '@cashumints/shared';
import { config } from './config.ts'; import { config } from './config.ts';
import { getDb, setState } from './db.ts'; import { getDb, setState } from './db.ts';
import { fetchObserverIndex, OBSERVER_SOURCE, type ObserverIndex } from './fedimint-observer.ts'; import { fetchObserverIndex, OBSERVER_SOURCE, type ObserverIndex } from './fedimint-observer.ts';
import { readBodyBounded } from './http.ts'; import { readBodyBounded } from './http.ts';
import { cacheIcon } from './icons.ts'; import { cacheIcon } from './icons.ts';
import { log } from './log.ts'; import { log } from './log.ts';
import { probeLnurl } from './lnurl-probe.ts';
import { allMintRows, mintByUrl, parseEcosystem, type MintRow } from './mints.ts'; import { allMintRows, mintByUrl, parseEcosystem, type MintRow } from './mints.ts';
export interface ProbeResult { export interface ProbeResult {
@@ -28,6 +32,64 @@ export function statusForFails(fails: number): 'online' | 'degraded' | 'offline'
*/ */
const MAX_INFO_BYTES = 256 * 1024; const MAX_INFO_BYTES = 256 * 1024;
/**
* Write everything a successful Cashu probe learned, and record the probe.
*
* Split out of `probeMint` because the on-demand indexer has already made this exact
* request: a reader pastes a mint URL, `POST /api/index` fetches `/v1/info` once
* through the guarded fetcher to decide whether the address is a mint at all, and then
* needs the row to end up in precisely the state a probe cycle would have left it in.
* Re-probing to achieve that would hit a stranger's mint twice for one submission and
* would leave two ways for "what a good probe writes" to drift apart.
*/
export async function recordCashuOnline(
row: MintRow,
info: MintInfo,
latencyMs: number,
now = Math.floor(Date.now() / 1000),
): Promise<void> {
const db = await getDb();
const nuts = parseNuts(info.nuts);
const iconFile = await cacheIcon(row, info.icon_url ?? null);
await db.run(
`UPDATE mints SET
name = COALESCE(?, name),
description = COALESCE(?, description),
icon_url = ?,
icon_file = ?,
pubkey = COALESCE(?, pubkey),
info_json = ?,
nuts_json = ?,
version = COALESCE(?, version),
status = 'online',
consecutive_fails = 0,
last_online = ?,
last_probe = ?,
updated_at = ?
WHERE url = ?`,
info.name ?? null,
info.description ?? null,
info.icon_url ?? null,
iconFile,
info.pubkey ?? null,
JSON.stringify(info),
JSON.stringify(nuts),
info.version ?? null,
now,
now,
now,
row.url,
);
await db.run(
'INSERT INTO probes (mint_url, ts, ok, latency_ms) VALUES (?, ?, 1, ?)',
row.url,
now,
latencyMs,
);
}
async function fetchInfo(url: string): Promise<{ info: MintInfo; latencyMs: number }> { async function fetchInfo(url: string): Promise<{ info: MintInfo; latencyMs: number }> {
const started = Date.now(); const started = Date.now();
const controller = new AbortController(); const controller = new AbortController();
@@ -65,46 +127,7 @@ export async function probeMint(row: MintRow): Promise<ProbeResult> {
try { try {
const { info, latencyMs } = await fetchInfo(row.url); const { info, latencyMs } = await fetchInfo(row.url);
const nuts = parseNuts(info.nuts); await recordCashuOnline(row, info, latencyMs, now);
const iconFile = await cacheIcon(row, info.icon_url ?? null);
await db.run(
`UPDATE mints SET
name = COALESCE(?, name),
description = COALESCE(?, description),
icon_url = ?,
icon_file = ?,
pubkey = COALESCE(?, pubkey),
info_json = ?,
nuts_json = ?,
version = COALESCE(?, version),
status = 'online',
consecutive_fails = 0,
last_online = ?,
last_probe = ?,
updated_at = ?
WHERE url = ?`,
info.name ?? null,
info.description ?? null,
info.icon_url ?? null,
iconFile,
info.pubkey ?? null,
JSON.stringify(info),
JSON.stringify(nuts),
info.version ?? null,
now,
now,
now,
row.url,
);
await db.run(
'INSERT INTO probes (mint_url, ts, ok, latency_ms) VALUES (?, ?, 1, ?)',
row.url,
now,
latencyMs,
);
const changed = row.status !== 'online'; const changed = row.status !== 'online';
if (changed) log.info('mint state change', { url: row.url, from: row.status, to: 'online' }); if (changed) log.info('mint state change', { url: row.url, from: row.status, to: 'online' });
@@ -251,6 +274,297 @@ async function checkFederations(rows: MintRow[], now: number): Promise<ProbeResu
return results; return results;
} }
/* ---------- lnurl ---------- */
/**
* Probe one LNURL mint and write the result.
*
* Same shape as `probeMint` and the same guarantee: on failure the cached metadata
* columns are left untouched, which is what lets an offline LNURL mint still render its
* full page with a banner over it and a working review form. The rug-review guarantee
* covers all three ecosystems, and this is the half of it that lives in the indexer.
*
* Three outcomes rather than two, and the middle one is the point:
*
* - **online** — an advertisement parsed and the mint's Lightning node answered.
* - **degraded-funding** — an advertisement parsed and the node did not. The mint is
* up: it is serving, its limits are real, and rotate/split/merge still work. It is
* recorded as a *successful* probe with `funding_available: false`, which the
* warnings turn into "minting and melting unavailable". Storing it as `degraded`
* status instead would be wrong twice over — that value already means "one or two
* checks failed" here, and none did — and this is exactly the shape the Cashu side
* already uses for a mint that answers perfectly while refusing to move sats.
* - **invalid** — the host answered with something that is not a mint advertisement.
* Counted as a failed probe, because nothing about the mint was confirmed, but with
* `invalid_reason` recorded so the page can say "responding but invalid" instead of
* the flatly wrong "offline".
*/
async function probeLnurlMint(row: MintRow): Promise<ProbeResult> {
const now = Math.floor(Date.now() / 1000);
const previous = parseEcosystem<LnurlFields>(row);
const baseUrl = previous?.base_url ?? row.url.replace(/^lnurl:/, '');
try {
return await applyLnurlResult(row, await probeLnurl(baseUrl), now);
} catch (err) {
return await recordLnurlFailure(row, previous, err, now);
}
}
/**
* Write what one LNURL probe concluded, whichever of the three things it concluded.
*
* Split out of `probeLnurlMint` for the same reason `recordCashuOnline` was: the
* on-demand indexer runs `probeLnurl` itself, through the guarded fetcher, and the row
* it creates has to land in exactly the state a probe cycle would have left it in. One
* writer, two callers.
*/
export async function applyLnurlResult(
row: MintRow,
result: Awaited<ReturnType<typeof probeLnurl>>,
now = Math.floor(Date.now() / 1000),
): Promise<ProbeResult> {
const db = await getDb();
const previous = parseEcosystem<LnurlFields>(row);
const baseUrl = previous?.base_url ?? row.url.replace(/^lnurl:/, '');
if (result.outcome === 'invalid') {
const fails = row.consecutive_fails + 1;
const status = statusForFails(fails);
const fields: LnurlFields = {
...(previous ?? emptyLnurlFields(baseUrl)),
invalid_reason: result.invalidReason,
probe_endpoint: result.endpoint,
};
await db.run(
`UPDATE mints SET consecutive_fails = ?, status = ?, ecosystem_json = ?,
last_probe = ?, updated_at = ?
WHERE url = ?`,
fails,
status,
JSON.stringify(fields),
now,
now,
row.url,
);
await db.run(
'INSERT INTO probes (mint_url, ts, ok, latency_ms) VALUES (?, ?, 0, NULL)',
row.url,
now,
);
const changed = row.status !== status;
if (changed) {
log.info('mint state change', {
url: row.url, from: row.status, to: status, fails, reason: result.invalidReason,
});
}
return { url: row.url, ok: false, latencyMs: null, status, changed };
}
const ad = result.advertisement;
/*
* `mint_pubkey` is sticky: `?? previous?.mint_pubkey` and never the other way round.
* A node that was unreachable for this one request drops the field from the
* response, and letting that clear a pubkey the site has already seen would change
* the mint's Nostr identity — its `d` tag — every time its node hiccuped. See
* "When a mint gains a pubkey after being announced by host" in the kind document.
*/
const mintPubkey = ad?.mintPubkey ?? previous?.mint_pubkey ?? null;
const fields: LnurlFields = {
...(previous ?? emptyLnurlFields(baseUrl)),
lnurl_id: lnurlIdentifier(baseUrl, mintPubkey),
base_url: baseUrl,
mint_pubkey: mintPubkey,
/*
* Left as whatever it was when the payRequest fallback answered and the withdraw
* side did not: that endpoint carries no node section at all, so its silence is
* not evidence either way. Only a parsed advertisement gets to set this.
*/
funding_available: ad ? ad.fundingAvailable : previous?.funding_available ?? null,
probe_endpoint: result.endpoint,
invalid_reason: null,
min_withdrawable_msat: ad?.minWithdrawableMsat ?? previous?.min_withdrawable_msat ?? null,
max_withdrawable_msat: ad?.maxWithdrawableMsat ?? previous?.max_withdrawable_msat ?? null,
min_sendable_msat: result.pay?.minSendableMsat ?? previous?.min_sendable_msat ?? null,
max_sendable_msat: result.pay?.maxSendableMsat ?? previous?.max_sendable_msat ?? null,
fee_base_msat: result.pay?.feeBaseMsat ?? null,
fee_ppm: result.pay?.feePpm ?? null,
lightning_address: result.lightningAddress ?? previous?.lightning_address ?? null,
onion_url: result.onionUrl ?? null,
node_alias: ad?.nodeAlias ?? null,
node_uri: ad?.nodeUri ?? null,
node_capacity_msat: ad?.nodeCapacityMsat ?? null,
node_channels: ad?.nodeChannels ?? null,
node_peers: ad?.nodePeers ?? null,
/*
* What this probe actually saw, kept beside — never merged into — whatever the
* operator announced. It is the only capability list a mint nobody has announced
* has, which today is every LNURL mint on the network, and it is what the page
* renders when `features` is empty. `displayFeatures` does the joining.
*/
observed_features: observedFeatures({
fundingAvailable: ad ? ad.fundingAvailable : null,
maxWithdrawableMsat: ad?.maxWithdrawableMsat ?? null,
maxSendableMsat: result.pay?.maxSendableMsat ?? null,
lightningAddress: result.lightningAddress,
mintPubkey,
onionUrl: result.onionUrl,
}),
};
const iconFile = await cacheIcon(row, row.icon_url);
/*
* `COALESCE(description, ?)`, the opposite way round from the Cashu probe.
*
* There, `/v1/info` is the mint's own word about itself and rightly overwrites a
* cached value. Here the candidate is a string the mint *software* generates —
* "Mint an lnurlcash bearer note on {host}" is a template, not a sentence anyone
* wrote — so it fills a gap and never displaces an operator's own announcement
* metadata.
*
* `name` is deliberately not written at all. The only candidate the endpoints offer
* is `nodeAlias`, and that is the *Lightning node's* name, not the mint's: the
* software's own one-pager prints it under a "Node" heading, separate from the
* mint's title. Calling a mint after its node would be a small invention, and the
* existing fallback — the hostname — is both true and what a reader typed to get
* here. The alias is kept in `ecosystem_json` and shown in the sidebar, where it is
* labelled as what it is.
*/
await db.run(
`UPDATE mints SET
description = COALESCE(description, ?),
icon_file = ?,
ecosystem_json = ?,
version = COALESCE(?, version),
status = 'online',
consecutive_fails = 0,
last_online = ?,
last_probe = ?,
updated_at = ?
WHERE url = ?`,
result.pay?.description ?? ad?.defaultDescription ?? null,
iconFile,
JSON.stringify(fields),
result.software,
now,
now,
now,
row.url,
);
await db.run(
'INSERT INTO probes (mint_url, ts, ok, latency_ms) VALUES (?, ?, 1, ?)',
row.url,
now,
result.latencyMs,
);
const changed = row.status !== 'online' || previous?.funding_available !== fields.funding_available;
if (changed) {
log.info('mint state change', {
url: row.url,
from: row.status,
to: 'online',
funding: fields.funding_available === false ? 'unavailable' : 'ok',
});
}
return { url: row.url, ok: true, latencyMs: result.latencyMs, status: 'online', changed };
}
/**
* Write a failed LNURL probe: the host said nothing at all.
*
* Its own function only because two callers reach it — the probe cycle and the
* on-demand indexer, which treats a host that never answered as a candidate for the
* Nostr lookup rather than as a failure to report immediately.
*/
async function recordLnurlFailure(
row: MintRow,
previous: LnurlFields | null,
err: unknown,
now: number,
): Promise<ProbeResult> {
const db = await getDb();
const fails = row.consecutive_fails + 1;
const status = statusForFails(fails);
/*
* A host that said nothing at all is offline, not invalid. Clearing `invalid_reason`
* here matters: a mint that spent a day serving junk and then went dark should stop
* showing "responding but invalid" the moment it stops responding.
*/
if (previous?.invalid_reason) {
await db.run(
'UPDATE mints SET ecosystem_json = ? WHERE url = ?',
JSON.stringify({ ...previous, invalid_reason: null } satisfies LnurlFields),
row.url,
);
}
await db.run(
`UPDATE mints SET consecutive_fails = ?, status = ?, last_probe = ?, updated_at = ?
WHERE url = ?`,
fails,
status,
now,
now,
row.url,
);
await db.run(
'INSERT INTO probes (mint_url, ts, ok, latency_ms) VALUES (?, ?, 0, NULL)',
row.url,
now,
);
const changed = row.status !== status;
if (changed) {
log.info('mint state change', {
url: row.url,
from: row.status,
to: status,
fails,
reason: err instanceof Error ? err.message : String(err),
});
}
return { url: row.url, ok: false, latencyMs: null, status, changed };
}
/** The `ecosystem_json` shape a row falls back to when its own will not parse. */
function emptyLnurlFields(baseUrl: string): LnurlFields {
return {
lnurl_id: lnurlIdentifier(baseUrl, null),
base_url: baseUrl,
features: [],
network: null,
announced_at: null,
announcer_pubkey: null,
mint_pubkey: null,
funding_available: null,
probe_endpoint: null,
invalid_reason: null,
min_withdrawable_msat: null,
max_withdrawable_msat: null,
min_sendable_msat: null,
max_sendable_msat: null,
fee_base_msat: null,
fee_ppm: null,
lightning_address: null,
onion_url: null,
node_alias: null,
node_uri: null,
node_capacity_msat: null,
node_channels: null,
node_peers: null,
observed_features: [],
};
}
/** Run `tasks` with at most `limit` in flight. */ /** Run `tasks` with at most `limit` in flight. */
async function pooled<T>(items: T[], limit: number, fn: (item: T) => Promise<unknown>): Promise<void> { async function pooled<T>(items: T[], limit: number, fn: (item: T) => Promise<unknown>): Promise<void> {
let cursor = 0; let cursor = 0;
@@ -270,16 +584,26 @@ export async function probeAll(rows?: MintRow[]): Promise<ProbeResult[]> {
const started = Date.now(); const started = Date.now();
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
// Two ecosystems, two entirely different checks: an HTTP fetch per mint, and one /*
// lookup covering every federation. Split here rather than inside the worker so the * Three ecosystems, three checks.
// federations are not each waiting behind a slot in the mint pool. *
const mints = targets.filter((row) => row.type !== 'fedimint'); * Two of them are an HTTP fetch per row and one is a single lookup covering every
* federation, so the federations are split out rather than each waiting behind a slot
* in the fetch pool. Cashu and LNURL mints share that pool — they are the same kind of
* work, a handful of small HTTPS requests each — and are told apart inside the worker
* by `type` rather than by two pools that would each idle waiting for the other.
*
* The default arm is Cashu, not LNURL: a row of some type this build has never heard
* of is far more likely to be an older ecosystem than a newer one, and `/v1/info` is
* what every row written before the column existed means.
*/
const federations = targets.filter((row) => row.type === 'fedimint'); const federations = targets.filter((row) => row.type === 'fedimint');
const fetched = targets.filter((row) => row.type !== 'fedimint');
const results: ProbeResult[] = []; const results: ProbeResult[] = [];
const [, federationResults] = await Promise.all([ const [, federationResults] = await Promise.all([
pooled(mints, config.probeConcurrency, async (row) => { pooled(fetched, config.probeConcurrency, async (row) => {
results.push(await probeMint(row)); results.push(row.type === 'lnurl' ? await probeLnurlMint(row) : await probeMint(row));
}), }),
checkFederations(federations, now), checkFederations(federations, now),
]); ]);
@@ -288,7 +612,8 @@ export async function probeAll(rows?: MintRow[]): Promise<ProbeResult[]> {
const online = results.filter((r) => r.ok).length; const online = results.filter((r) => r.ok).length;
const changed = results.filter((r) => r.changed).length; const changed = results.filter((r) => r.changed).length;
log.info('probe cycle', { log.info('probe cycle', {
mints: mints.length, mints: fetched.filter((row) => row.type !== 'lnurl').length,
lnurl: fetched.filter((row) => row.type === 'lnurl').length,
federations: federations.length, federations: federations.length,
online, online,
down: results.length - online, down: results.length - online,
+123 -15
View File
@@ -3,6 +3,7 @@ import {
compareMints, compareMints,
NEUTRAL_PRIOR_MEAN, NEUTRAL_PRIOR_MEAN,
parseNuts, parseNuts,
readCapabilities,
type Health, type Health,
type MintDetail, type MintDetail,
type MintInfo, type MintInfo,
@@ -11,10 +12,12 @@ import {
type MintType, type MintType,
type ProbeSample, type ProbeSample,
type RatingDistribution, type RatingDistribution,
type LnurlFields,
type Stats, type Stats,
} from '@cashumints/shared'; } from '@cashumints/shared';
import { config, startedAt } from './config.ts'; import { config, startedAt } from './config.ts';
import { getDb, getStateNumber, getState } from './db.ts'; import { getDb, getStateNumber, getState } from './db.ts';
import { lastDiscoveryReport } from './discovery.ts';
import { mintByHost, parseEcosystem, type MintRow } from './mints.ts'; import { mintByHost, parseEcosystem, type MintRow } from './mints.ts';
/** /**
@@ -104,6 +107,39 @@ function round1(n: number | null): number | null {
return n === null ? null : Math.round(n * 10) / 10; return n === null ? null : Math.round(n * 10) / 10;
} }
/**
* The NUT numbers a row publishes.
*
* `nuts_json` is what the prober wrote and wins; `info.nuts` is the raw NUT-06 object it
* was derived from, kept as a fallback for rows written before that column existed. One
* function so a list card and a detail page can never read a different answer off the
* same row.
*/
function rowNuts(row: MintRow, info: MintInfo | null): string[] {
if (row.nuts_json) {
try {
const parsed = JSON.parse(row.nuts_json) as string[];
if (parsed.length > 0) return parsed;
} catch {
// Fall through to the info object below.
}
}
return info ? parseNuts(info.nuts) : [];
}
/**
* One list item.
*
* The chip fields at the bottom are why this now parses `info_json`. The alternative was
* what /mints and /lnurl-mints used to do: fetch `GET /api/mints/:host` once per mint to
* read two booleans off each one. That is an acceptable price for a build machine
* rendering fifty-five cards once a night and an unacceptable one for every browser that
* opens the page, which is what the list has to survive now that it hydrates.
*
* Facts, not sentences. `capabilities` is two booleans and `mintChip` turns them into
* "Melt only" in the reader's language, wherever the card is being drawn. Rendering the
* label here would ship one language to twenty-four locales.
*/
function toListItem(row: MintRow, agg: AggRow | undefined, mean: number, now: number): MintListItem { function toListItem(row: MintRow, agg: AggRow | undefined, mean: number, now: number): MintListItem {
const base = { const base = {
review_count: agg?.review_count ?? 0, review_count: agg?.review_count ?? 0,
@@ -112,6 +148,15 @@ function toListItem(row: MintRow, agg: AggRow | undefined, mean: number, now: nu
last_review_at: agg?.last_review_at ?? null, last_review_at: agg?.last_review_at ?? null,
}; };
const info = parseInfo(row.info_json);
// A federation and an LNURL mint have no `info_json` and so get null, which is the
// honest value: not "both NUTs are enabled", but "there is nothing here to read".
const capabilities = info ? readCapabilities(info.nuts) : null;
// Only the two facts the LNURL chip is drawn from, not the whole ecosystem blob: this
// payload is fetched by every visitor on three pages.
const lnurl = row.type === 'lnurl' ? parseEcosystem<LnurlFields>(row) : null;
return { return {
url: row.url, url: row.url,
host: row.host, host: row.host,
@@ -125,6 +170,14 @@ function toListItem(row: MintRow, agg: AggRow | undefined, mean: number, now: nu
score: bayesianScore(base, mean, now), score: bayesianScore(base, mean, now),
last_review_at: base.last_review_at, last_review_at: base.last_review_at,
version: row.version, version: row.version,
nuts: rowNuts(row, info),
capabilities,
...(lnurl
? {
max_withdrawable_msat: lnurl.max_withdrawable_msat ?? null,
funding_available: lnurl.funding_available ?? null,
}
: {}),
}; };
} }
@@ -231,16 +284,9 @@ export async function getMintDetail(host: string): Promise<MintDetail | null> {
const item = toListItem(row, agg.get(row.url), mean, now); const item = toListItem(row, agg.get(row.url), mean, now);
const info = parseInfo(row.info_json); const info = parseInfo(row.info_json);
// `item.nuts` is the same read, through `rowNuts`. It used to be computed a second
let nuts: string[] = []; // time here with a subtly different fallback rule; one function now answers for both.
if (row.nuts_json) { const nuts = item.nuts;
try {
nuts = JSON.parse(row.nuts_json) as string[];
} catch {
nuts = [];
}
}
if (nuts.length === 0 && info) nuts = parseNuts(info.nuts);
/* /*
* Type-specific columns are spread across the payload rather than nested under a key. * Type-specific columns are spread across the payload rather than nested under a key.
@@ -289,7 +335,8 @@ export async function getStats(): Promise<Stats> {
* COUNT(*) FILTER, not SUM(status = 'online'): Postgres has no implicit cast from * COUNT(*) FILTER, not SUM(status = 'online'): Postgres has no implicit cast from
* boolean to integer, so the SQLite spelling is a type error there. * boolean to integer, so the SQLite spelling is a type error there.
*/ */
const [counts, federations, reviews, fedimintReviews] = await Promise.all([ const [counts, federations, lnurl, lnurlFunding, reviews, fedimintReviews, lnurlReviews] =
await Promise.all([
db.get<{ total: number; online: number; offline: number; degraded: number }>( db.get<{ total: number; online: number; offline: number; degraded: number }>(
`SELECT `SELECT
COUNT(*) AS total, COUNT(*) AS total,
@@ -306,6 +353,24 @@ export async function getStats(): Promise<Stats> {
COUNT(*) FILTER (WHERE status = 'announced') AS announced COUNT(*) FILTER (WHERE status = 'announced') AS announced
FROM mints WHERE type = 'fedimint'`, FROM mints WHERE type = 'fedimint'`,
), ),
db.get<{ total: number; online: number; offline: number }>(
`SELECT
COUNT(*) AS total,
COUNT(*) FILTER (WHERE status = 'online') AS online,
COUNT(*) FILTER (WHERE status = 'offline') AS offline
FROM mints WHERE type = 'lnurl'`,
),
/*
* The degraded-funding count is read in JavaScript rather than in SQL, because the
* flag lives inside `ecosystem_json` and neither a `LIKE '%"funding_available":false%'`
* (which depends on how the two drivers happen to serialise) nor a JSON operator
* (which SQLite and Postgres spell differently) is portable. There are a handful of
* these rows, the whole result is memoised for a minute, and a correct answer on
* both backends is worth one small scan.
*/
db.all<{ ecosystem_json: string | null }>(
`SELECT ecosystem_json FROM mints WHERE type = 'lnurl' AND status = 'online'`,
),
db.get<{ n: number; last: number | null }>( db.get<{ n: number; last: number | null }>(
`WITH latest AS (${LATEST_REVIEWS}) SELECT COUNT(*) AS n, MAX(created_at) AS last FROM latest`, `WITH latest AS (${LATEST_REVIEWS}) SELECT COUNT(*) AS n, MAX(created_at) AS last FROM latest`,
), ),
@@ -314,7 +379,18 @@ export async function getStats(): Promise<Stats> {
SELECT COUNT(*) AS n FROM latest SELECT COUNT(*) AS n FROM latest
WHERE mint_url IN (SELECT url FROM mints WHERE type = 'fedimint')`, WHERE mint_url IN (SELECT url FROM mints WHERE type = 'fedimint')`,
), ),
]); db.get<{ n: number }>(
`WITH latest AS (${LATEST_REVIEWS})
SELECT COUNT(*) AS n FROM latest
WHERE mint_url IN (SELECT url FROM mints WHERE type = 'lnurl')`,
),
]);
let lnurlDegradedFunding = 0;
for (const row of lnurlFunding) {
const fields = parseEcosystem<LnurlFields>(row);
if (fields?.funding_available === false) lnurlDegradedFunding++;
}
const cashuTotal = counts?.total ?? 0; const cashuTotal = counts?.total ?? 0;
@@ -332,6 +408,11 @@ export async function getStats(): Promise<Stats> {
fedimint_offline: federations?.offline ?? 0, fedimint_offline: federations?.offline ?? 0,
fedimint_announced: federations?.announced ?? 0, fedimint_announced: federations?.announced ?? 0,
fedimint_reviews: fedimintReviews?.n ?? 0, fedimint_reviews: fedimintReviews?.n ?? 0,
lnurl_total: lnurl?.total ?? 0,
lnurl_online: lnurl?.online ?? 0,
lnurl_offline: lnurl?.offline ?? 0,
lnurl_degraded_funding: lnurlDegradedFunding,
lnurl_reviews: lnurlReviews?.n ?? 0,
}; };
statsCache = { at: now, value }; statsCache = { at: now, value };
@@ -343,28 +424,55 @@ export function resetStatsCache(): void {
statsCache = null; statsCache = null;
} }
/** Health bypasses the stats cache: it is the endpoint you page on. */ /**
* Health bypasses the stats cache: it is the endpoint you page on.
*
* Three things can degrade it, and they are three different failures:
*
* probeStale nothing has checked a mint in three intervals
* !discoveryOk the last discovery cycle threw
* report.starved the last backfill read less than BACKFILL_MIN_EVENTS
*
* The third is the one added after the postmortem, and it is the only one that would
* have caught a year of the index sitting at eight mints: the cycles were completing,
* `ok` was true, the probes were fresh, and the relay list simply did not contain the
* relay holding the archive. "Ran without throwing" is not the same claim as "read
* anything", and only the second one is worth a green light.
*
* A deployment with an empty database reports degraded until its first backfill lands,
* because until then nothing has confirmed the relay set reads anything at all. That is
* intended: it holds `cashumints-web.service` at its health gate rather than letting it
* publish a site built from nothing.
*/
export async function getHealth(): Promise<Health> { export async function getHealth(): Promise<Health> {
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
const db = await getDb(); const db = await getDb();
const [lastProbe, lastDiscovery, discoveryOkRaw, tracked] = await Promise.all([ const [lastProbe, lastDiscovery, discoveryOkRaw, tracked, report] = await Promise.all([
getStateNumber('last_probe_at'), getStateNumber('last_probe_at'),
getStateNumber('last_discovery_at'), getStateNumber('last_discovery_at'),
getState('last_discovery_ok'), getState('last_discovery_ok'),
db.get<{ n: number }>('SELECT COUNT(*) AS n FROM mints'), db.get<{ n: number }>('SELECT COUNT(*) AS n FROM mints'),
lastDiscoveryReport(),
]); ]);
const discoveryOk = discoveryOkRaw !== '0'; const discoveryOk = discoveryOkRaw !== '0';
const staleAfter = config.probeIntervalMin * 60 * 3; const staleAfter = config.probeIntervalMin * 60 * 3;
const probeStale = lastProbe === null || now - lastProbe > staleAfter; const probeStale = lastProbe === null || now - lastProbe > staleAfter;
// No report at all is starvation by default: see the note above.
const starved = report?.starved ?? true;
return { return {
status: probeStale || !discoveryOk ? 'degraded' : 'ok', status: probeStale || !discoveryOk || starved ? 'degraded' : 'ok',
uptime_s: now - startedAt, uptime_s: now - startedAt,
last_probe_at: lastProbe, last_probe_at: lastProbe,
last_discovery_at: lastDiscovery, last_discovery_at: lastDiscovery,
mints_tracked: tracked?.n ?? 0, mints_tracked: tracked?.n ?? 0,
updated_at: now, updated_at: now,
discovery_relays: report?.relays ?? [],
last_discovery_events: report?.events ?? null,
last_discovery_mode: report?.mode ?? null,
discovery_starved: starved,
backfill_min_events: config.backfillMinEvents,
}; };
} }
+92
View File
@@ -0,0 +1,92 @@
/**
* A per-address budget for the one endpoint that does work on a stranger's behalf.
*
* Every other route reads rows this process already has. `POST /api/index` resolves
* DNS, opens a socket to an address somebody chose, and may query five relays, so it is
* the one place where a loop in a browser tab costs this server real outbound work —
* and costs whoever is at the other end an unexpected visitor.
*
* In-process and in-memory, deliberately. The alternatives were considered and both are
* worse here: a table would put a write on the path of every submission for a counter
* that may be forgotten at any time, and doing it in nginx would put the limit in a
* file the application cannot see, cannot test, and does not ship — this repository's
* README documents an nginx block that a deployment is free to edit, so a limit that
* lives only there is a limit that silently varies per deployment. The cost of keeping
* it here is that a restart forgives everyone and a second process would double the
* allowance; at ten an hour, neither matters. BACKEND.md records the choice.
*/
/**
* Submissions one address may make per window.
*
* Ten by default, which is BACKEND.md's number and is several times what any honest use
* of the dialog needs. `INDEX_RATE_LIMIT` raises or lowers it, which exists for two
* real cases rather than as reflexive configurability: exercising the whole flow
* end to end trips a limit of ten in about a minute, and a deployment behind a proxy
* that does not forward the client address has every visitor sharing one bucket until
* it does. Both are better served by a number than by a code change.
*/
export const RATE_LIMIT = (() => {
const raw = Number.parseInt(process.env['INDEX_RATE_LIMIT'] ?? '', 10);
return Number.isFinite(raw) && raw > 0 ? raw : 10;
})();
/** The window those submissions are counted over. */
export const RATE_WINDOW_MS = 60 * 60 * 1000;
/** Addresses tracked at once. Past this the oldest bucket is dropped, not the newest. */
const MAX_TRACKED = 5000;
const hits = new Map<string, number[]>();
export interface RateVerdict {
ok: boolean;
/** Submissions left in this window after this one. */
remaining: number;
/** Seconds until the oldest counted submission falls out of the window. */
retryAfter: number;
}
/**
* Count one submission from `key`, and say whether it is allowed.
*
* A sliding window over timestamps rather than a fixed bucket, so ten submissions at
* 10:59 do not give a fresh ten at 11:00. A refused submission is *not* counted: being
* over the limit should not extend the wait every time the reader presses the button
* again, which is what turns a rate limit into a lockout.
*/
export function takeToken(key: string, now = Date.now()): RateVerdict {
const cutoff = now - RATE_WINDOW_MS;
const recent = (hits.get(key) ?? []).filter((at) => at > cutoff);
if (recent.length >= RATE_LIMIT) {
hits.set(key, recent);
const oldest = recent[0] ?? now;
return {
ok: false,
remaining: 0,
retryAfter: Math.max(1, Math.ceil((oldest + RATE_WINDOW_MS - now) / 1000)),
};
}
recent.push(now);
hits.set(key, recent);
if (hits.size > MAX_TRACKED) sweep(cutoff);
return { ok: true, remaining: RATE_LIMIT - recent.length, retryAfter: 0 };
}
/** Drop every bucket with nothing left in the window. Called only when the map grows. */
function sweep(cutoff: number): void {
for (const [key, times] of hits) {
const live = times.filter((at) => at > cutoff);
if (live.length === 0) hits.delete(key);
else hits.set(key, live);
}
}
/** Forget every counter. For tests, which must not inherit each other's budgets. */
export function resetRateLimits(): void {
hits.clear();
}
+173
View File
@@ -0,0 +1,173 @@
/**
* "Has anyone ever heard of this mint?"
*
* The last question `POST /api/index` asks before giving up, and the one that makes the
* rugged-mint case work. A reader pastes an address; nothing answers at it. That is two
* different situations wearing the same silence:
*
* - a typo, a dead domain, an address nobody has ever used — nothing to index; or
* - a mint that ran for two years, took people's money, and went dark last week.
*
* The second is exactly the mint somebody most wants to write a review of, and it is
* the one this site exists to keep a page for. HTTP cannot tell them apart, but Nostr
* can: a mint that was ever real has an announcement, or reviews, or both, and a typo
* has neither. So before answering "we cannot verify that", the endpoint spends one
* bounded query asking the relay pool.
*
* Bounded is the operative word. This runs inside a request a person is waiting on, so
* it gets one round of filters and `RELAY_LOOKUP_MS` to answer them; whatever has
* arrived by then is the answer. The discovery loop is where exhaustive paging lives —
* it runs on a timer, with nobody watching — and the row this creates is an ordinary
* row that the next discovery cycle will fill in properly.
*/
import type { Event as NostrEvent, Filter } from 'nostr-tools';
import {
KIND_LNURL_ANNOUNCEMENT,
KIND_MINT_ANNOUNCEMENT,
KIND_REVIEW,
hostIdentifier,
mintUrlSpellings,
parseAnnouncementMetadata,
parseLnurlAnnouncement,
reviewEcosystem,
type LnurlAnnouncement,
} from '@cashumints/shared';
import { config } from './config.ts';
import { getPool } from './discovery.ts';
import { log } from './log.ts';
/**
* How long the relays get, in total, for the whole lookup.
*
* Three seconds, which is the number BACKEND.md now records for this endpoint. It is
* chosen against the two facts that matter: the pool's own `querySync` resolves as soon
* as every relay has sent EOSE, which on this pool is usually well under a second, and
* the request this sits inside has already spent up to five seconds failing to reach
* the mint. Eight seconds of a reader watching a spinner is the outer bound of what
* this feature may cost, and three is what buys nearly all of the recall — the events
* being asked for are single, indexed, tag-filtered lookups, not a sweep.
*/
export const RELAY_LOOKUP_MS = 3000;
/** Bounded so a hostile or confused relay cannot answer with a hundred thousand rows. */
const LOOKUP_LIMIT = 60;
/** What the relays remember about an address that does not answer over HTTPS. */
export interface RelayTrace {
/** True when anything at all was found: an announcement, or a review, or both. */
found: boolean;
/** How many kind 38000 events name this address. Nothing is stored from them here. */
reviews: number;
/** Metadata from the newest announcement, when there was one. */
name: string | null;
picture: string | null;
about: string | null;
announcerPubkey: string | null;
announcedAt: number | null;
/** The parsed 38174, for an LNURL mint: the only source of its features and network. */
lnurl: LnurlAnnouncement | null;
}
const EMPTY: RelayTrace = {
found: false,
reviews: 0,
name: null,
picture: null,
about: null,
announcerPubkey: null,
announcedAt: null,
lnurl: null,
};
/**
* Run several filters against the pool at once, with one deadline over all of them.
*
* `querySync`'s own `maxWait` bounds each call, but a relay that accepts a connection
* and then never sends EOSE can outlive it; the race is what guarantees the caller gets
* an answer inside the budget whatever the sockets do.
*/
async function query(filters: Filter[]): Promise<NostrEvent[]> {
const started = Date.now();
const events = new Map<string, NostrEvent>();
const work = Promise.all(
filters.map((filter) =>
getPool()
.querySync(config.relays, filter, { maxWait: RELAY_LOOKUP_MS })
.then((batch) => {
for (const event of batch) events.set(event.id, event);
})
.catch(() => undefined),
),
);
await Promise.race([work, new Promise((resolve) => setTimeout(resolve, RELAY_LOOKUP_MS))]);
log.info('relay trace', { filters: filters.length, events: events.size, ms: Date.now() - started });
return [...events.values()];
}
/**
* Everything the relays know about one unreachable address.
*
* The filters are the same ones the discovery loop uses to resolve a review, asked in
* the other direction: there, "which mint is this review about?"; here, "are there any
* reviews, and any announcement, for this address?". Reusing `mintUrlSpellings` is what
* makes the two agree — a mint announced with a trailing slash is found by an address
* typed without one.
*/
export async function traceOnRelays(type: string, url: string): Promise<RelayTrace> {
const spellings = mintUrlSpellings(url);
const announcementKind = type === 'lnurl' ? KIND_LNURL_ANNOUNCEMENT : KIND_MINT_ANNOUNCEMENT;
const filters: Filter[] = [
{ kinds: [announcementKind], '#u': spellings, limit: LOOKUP_LIMIT },
{ kinds: [KIND_REVIEW], '#u': spellings, limit: LOOKUP_LIMIT },
];
/*
* An LNURL mint with no funding source is announced — and reviewed — under its bare
* host in `d` rather than under a pubkey, and a client that only writes `d` leaves no
* `u` to match on. One extra filter covers those; a Cashu mint's `d` is a pubkey
* nobody can derive from a URL, so it has no equivalent.
*/
if (type === 'lnurl') {
const identifier = hostIdentifier(url);
filters.push({ kinds: [announcementKind, KIND_REVIEW], '#d': [identifier], limit: LOOKUP_LIMIT });
}
const events = await query(filters);
if (events.length === 0) return EMPTY;
let reviews = 0;
let newest: NostrEvent | null = null;
for (const event of events) {
if (event.kind === KIND_REVIEW) {
// A review found by `#u` could be about any ecosystem; only count the ones whose
// `k` agrees, so a federation review carrying a stray URL is not evidence here.
if (reviewEcosystem(event) === type) reviews++;
continue;
}
if (event.kind !== announcementKind) continue;
if (!newest || event.created_at > newest.created_at) newest = event;
}
if (!newest) {
return reviews > 0 ? { ...EMPTY, found: true, reviews } : EMPTY;
}
const lnurl = type === 'lnurl' ? parseLnurlAnnouncement(newest) : null;
const meta = parseAnnouncementMetadata(newest.content);
return {
found: true,
reviews,
name: lnurl?.name ?? meta.name,
picture: lnurl?.picture ?? meta.picture,
about: lnurl?.about ?? meta.about,
announcerPubkey: newest.pubkey,
announcedAt: newest.created_at,
lnurl,
};
}
+288
View File
@@ -0,0 +1,288 @@
/**
* Fetching a URL a stranger typed.
*
* Every other fetch in this codebase goes to an address that reached the database
* through discovery or the seed list. `POST /api/index` is the first one that does not:
* a reader pastes something, and this server opens a socket to it. That inverts who the
* untrusted party is — it is no longer just the *response* that cannot be trusted, it
* is the *destination* — so this module exists to answer one question before any packet
* leaves: is that address somewhere this server has any business connecting to?
*
* Four rules, and each is here because leaving it out is a known exploit:
*
* 1. **https only.** Not http-upgraded-to-https, not any other scheme. `file:` reads
* the disk, `gopher:` used to be a way to make a server speak arbitrary protocols,
* and plaintext http to a stranger's address is a downgrade nobody asked for.
* 2. **Resolve first, judge the addresses, then connect.** A hostname check alone
* stops `http://127.0.0.1` and nothing else: `internal.example.com` is a perfectly
* ordinary public name that resolves to `10.0.0.5`, and only DNS can say so. Every
* address the name resolves to is checked, not the first: a name with one public
* and one private A record must be refused, not raced.
* 3. **Every redirect hop is a new destination.** A public URL that 302s to
* `http://169.254.169.254/latest/meta-data/` is the cloud-metadata attack in its
* classic form. Redirects are followed manually, capped at two, and each hop goes
* through the same check as the first.
* 4. **Bounded in bytes, in time, and in content type.** A stranger's server can be
* slow forever and large forever; `readBodyBounded` and an abort timer cap both,
* and a response that is not the media type asked for is not read at all.
*
* What this cannot close is the DNS rebinding window: the name is resolved here, and
* the socket resolves it again a moment later, and a hostile resolver can answer
* differently the second time. Closing it means dialing the vetted IP with the hostname
* pinned for TLS, and Node exposes no supported way to do that through `fetch` (undici's
* dispatchers are not a public API here). The exposure is a single GET whose body is
* parsed as JSON and discarded unless it is a valid mint advertisement, so the practical
* gain from a rebind is one unauthenticated GET — noted here rather than left implicit.
*/
import { lookup } from 'node:dns/promises';
import { isBlockedHostname, isPrivateIpAddress } from '@cashumints/shared';
import { config } from './config.ts';
import { readBodyBounded } from './http.ts';
/** BACKEND.md's cap for this endpoint: no probe response may exceed it. */
export const MAX_PROBE_BYTES = 256 * 1024;
/** How many redirects a probe follows before giving up. */
export const MAX_REDIRECTS = 2;
/**
* What one guarded fetch did.
*
* `blocked` and `unreachable` are kept apart because the endpoint answers them
* differently: a blocked address is a refusal this site made and can explain, while an
* unreachable one is a mint that may still be real and falls through to the Nostr
* lookup. Collapsing them would file "you may not point us at 10.0.0.1" under "we
* checked Nostr and found nothing", which is not what happened.
*/
export type FetchOutcome =
| { state: 'ok'; status: number; body: string; contentType: string | null; url: string }
| { state: 'blocked'; reason: string }
| { state: 'unreachable'; reason: string };
/**
* The two pieces of the outside world this module touches.
*
* Injectable so the SSRF rules can be tested without a network: `check-index.ts` hands
* in a resolver that answers `10.0.0.1` for an ordinary-looking name, and a fetch that
* returns a redirect to a private address, and asserts that neither is ever connected
* to. Those are the two attacks this file exists to stop, and a rule that is only
* exercised against the real internet is a rule that is not exercised.
*/
export interface FetchDeps {
resolve?: (hostname: string) => Promise<string[] | null>;
fetchImpl?: typeof fetch;
}
/** Every address a hostname resolves to, or null when it does not resolve at all. */
async function resolveAll(hostname: string): Promise<string[] | null> {
try {
const records = await lookup(hostname, { all: true, verbatim: true });
const addresses = records.map((record) => record.address).filter(Boolean);
return addresses.length > 0 ? addresses : null;
} catch {
return null;
}
}
/**
* Why an address was not connected to.
*
* Two kinds, and keeping them apart is load-bearing rather than tidy. `blocked` is a
* refusal this site made — the address is one it will not fetch, whoever asked — and
* the endpoint reports it as such. `unresolved` is the network saying nothing, and a
* name that no longer resolves is the *normal* state of a mint whose operator walked
* away: that submission has to fall through to the Nostr lookup, not be rejected as if
* the reader had typed something inadmissible.
*/
export interface DestinationVerdict {
kind: 'blocked' | 'unresolved';
reason: string;
}
/**
* May this server connect to this URL?
*
* Returns null when it may. Exported because the on-demand indexer runs it once up
* front — before it decides whether to probe at all — and because the redirect loop
* below runs it again on every hop.
*/
export async function checkDestination(
target: URL,
deps: FetchDeps = {},
): Promise<DestinationVerdict | null> {
const blocked = (reason: string): DestinationVerdict => ({ kind: 'blocked', reason });
if (target.protocol !== 'https:') return blocked('only https addresses are checked');
const hostname = target.hostname.toLowerCase().replace(/^\[|\]$/g, '');
if (!hostname) return blocked('no hostname');
// localhost, .onion, .local, a bare label, an IP literal in a private range: all
// refusable without asking a resolver anything.
if (isBlockedHostname(target.hostname)) return blocked('not a public address');
// An IP literal has already been judged by the line above; a name has to be resolved.
if (/^[\d.]+$/.test(hostname) || hostname.includes(':')) return null;
const addresses = await (deps.resolve ?? resolveAll)(hostname);
if (addresses === null) return { kind: 'unresolved', reason: 'the address does not resolve' };
// Every answer, not the first: a name with one public and one private record must be
// refused outright rather than depending on which one the socket happens to pick.
const private_ = addresses.find((address) => isPrivateIpAddress(address));
return private_ === undefined ? null : blocked(`resolves to a private address (${private_})`);
}
/**
* A response's media type, without its parameters. `application/json; charset=utf-8`
* and `application/json` are the same answer.
*/
function mediaType(res: Response): string | null {
const header = res.headers.get('content-type');
return header ? (header.split(';')[0] ?? '').trim().toLowerCase() : null;
}
/**
* Is this media type plausible for what was asked for?
*
* Deliberately lenient in one direction only. A mint serving JSON as `text/plain` is a
* misconfiguration this site should still read — several real ones do — so anything
* text-shaped or unlabelled passes. What it refuses is a response that is *positively*
* something else: an image, a video, an archive, an executable. Those are never a mint
* advertisement, and reading 256KB of one to find out costs bandwidth for nothing.
*/
function plausibleType(type: string | null, accept: string): boolean {
if (type === null) return true; // Unlabelled. The parser is the real check.
if (type.startsWith('text/') || type.includes('json') || type.includes('xml')) return true;
if (accept.includes('html') && type.includes('html')) return true;
return false;
}
export interface SafeFetchOptions {
accept: string;
maxBytes?: number;
timeoutMs?: number;
}
/**
* GET a stranger's URL, following at most two redirects and checking every hop.
*
* `redirect: 'manual'` rather than `follow`, which is the crux: with `follow`, undici
* resolves and connects to the redirect target itself and this code never sees the
* address. Doing the hops by hand is what makes rule 3 above enforceable at all.
*/
export async function safeFetchText(
rawUrl: string,
options: SafeFetchOptions,
deps: FetchDeps = {},
): Promise<FetchOutcome> {
const maxBytes = options.maxBytes ?? MAX_PROBE_BYTES;
const timeoutMs = options.timeoutMs ?? config.probeTimeoutMs;
let target: URL;
try {
target = new URL(rawUrl);
} catch {
return { state: 'blocked', reason: 'not a URL' };
}
/*
* One timer for the whole chain, not one per hop.
*
* Otherwise two redirects turn the endpoint's "standard 5s timeout" into fifteen
* seconds of a reader watching a spinner, and a hostile server can extend that for
* as long as the redirect cap allows.
*/
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
const verdict = await checkDestination(target, deps);
if (verdict) {
// A name that does not resolve is not a refusal, it is a mint that is not there,
// and the caller has a whole extra step for that case.
if (verdict.kind === 'unresolved') {
return { state: 'unreachable', reason: verdict.reason };
}
return {
state: 'blocked',
reason:
hop === 0
? verdict.reason
: `redirected to an address we will not fetch: ${verdict.reason}`,
};
}
let res: Response;
try {
res = await (deps.fetchImpl ?? fetch)(target, {
signal: controller.signal,
redirect: 'manual',
headers: { Accept: options.accept, 'User-Agent': config.userAgent },
});
} catch (err) {
return {
state: 'unreachable',
reason: err instanceof Error ? err.message : 'connection failed',
};
}
if (res.status >= 300 && res.status < 400) {
const location = res.headers.get('location');
// Drain rather than leak the socket: a redirect body is never read.
await res.body?.cancel().catch(() => undefined);
if (!location) return { state: 'unreachable', reason: `HTTP ${res.status} with no target` };
if (hop === MAX_REDIRECTS) return { state: 'unreachable', reason: 'too many redirects' };
try {
target = new URL(location, target);
} catch {
return { state: 'unreachable', reason: 'redirect target is not a URL' };
}
continue;
}
const type = mediaType(res);
if (!plausibleType(type, options.accept)) {
await res.body?.cancel().catch(() => undefined);
return { state: 'unreachable', reason: `unexpected content type ${type ?? 'none'}` };
}
const body = await readBodyBounded(res, maxBytes);
if (!body) {
return { state: 'unreachable', reason: `body larger than ${maxBytes} bytes or unreadable` };
}
return {
state: 'ok',
status: res.status,
body: body.toString('utf8'),
contentType: type,
url: target.href,
};
}
// Unreachable in practice: the loop returns or continues, and the last iteration
// refuses to continue. Kept so the function has one type on every path.
return { state: 'unreachable', reason: 'too many redirects' };
} finally {
clearTimeout(timer);
}
}
/**
* The `TextFetcher` shape `probeLnurl` takes, backed by the guarded fetch above.
*
* The probe loop keeps its own plain fetcher: those rows are addresses this site chose
* to track and have already been through `normalizeMintUrl`. This one is for the path
* where the address arrived seconds ago from a stranger.
*/
export function guardedTextFetcher(deps: FetchDeps = {}) {
return async (
url: string,
accept: string,
maxBytes: number,
): Promise<{ body: string; status: number } | null> => {
const outcome = await safeFetchText(url, { accept, maxBytes }, deps);
return outcome.state === 'ok' ? { body: outcome.body, status: outcome.status } : null;
};
}
+32 -2
View File
@@ -10,7 +10,7 @@
import { closeDb, getDb } from './db.ts'; import { closeDb, getDb } from './db.ts';
import { closePool, runDiscovery } from './discovery.ts'; import { closePool, runDiscovery } from './discovery.ts';
import { log } from './log.ts'; import { log } from './log.ts';
import { insertMintIfNew } from './mints.ts'; import { insertLnurlIfNew, insertMintIfNew } from './mints.ts';
import { probeAll } from './probe.ts'; import { probeAll } from './probe.ts';
import { listMints } from './queries.ts'; import { listMints } from './queries.ts';
@@ -54,6 +54,23 @@ const SEED_MINTS = [
'https://kashu.me', 'https://kashu.me',
]; ];
/**
* LNURL mints, seeded by URL alone.
*
* Short for the obvious reason: `kind:38174` is a proposed kind (see
* `docs/KIND-LNURL-MINT.md`) and there are no announcements on the network yet, so
* unlike Cashu — where this list was read off real 38172 events — there is nothing to
* read it off. This is the reference instance the ecosystem was built against, and
* discovery takes over the moment operators start announcing.
*
* Seeded with no status: `insertLnurlIfNew` writes `unknown` and the first probe cycle
* decides, exactly as it does for a Cashu mint. Being on this list is not a claim that
* a mint is up, and is not an endorsement of it.
*/
const SEED_LNURL = [
'https://lnurl.21mint.me',
];
function pad(s: string, width: number): string { function pad(s: string, width: number): string {
return s.length > width ? `${s.slice(0, width - 1)}…` : s.padEnd(width); return s.length > width ? `${s.slice(0, width - 1)}…` : s.padEnd(width);
} }
@@ -64,7 +81,15 @@ async function main(): Promise<void> {
let added = 0; let added = 0;
for (const url of SEED_MINTS) if (await insertMintIfNew(url)) added++; for (const url of SEED_MINTS) if (await insertMintIfNew(url)) added++;
log.info('seed list ingested', { listed: SEED_MINTS.length, added });
let addedLnurl = 0;
for (const url of SEED_LNURL) if (await insertLnurlIfNew(url)) addedLnurl++;
log.info('seed list ingested', {
listed: SEED_MINTS.length + SEED_LNURL.length,
added: added + addedLnurl,
lnurl: addedLnurl,
});
await probeAll(); await probeAll();
const discovery = await runDiscovery(true); const discovery = await runDiscovery(true);
@@ -109,6 +134,7 @@ async function main(): Promise<void> {
const mints = listed.filter((m) => m.type === 'cashu'); const mints = listed.filter((m) => m.type === 'cashu');
const federations = listed.filter((m) => m.type === 'fedimint'); const federations = listed.filter((m) => m.type === 'fedimint');
const lnurlMints = listed.filter((m) => m.type === 'lnurl');
const count = (rows: typeof listed, status: string) => const count = (rows: typeof listed, status: string) =>
rows.filter((m) => m.status === status).length; rows.filter((m) => m.status === status).length;
const reviews = listed.reduce((sum, m) => sum + m.review_count, 0); const reviews = listed.reduce((sum, m) => sum + m.review_count, 0);
@@ -128,6 +154,10 @@ async function main(): Promise<void> {
`${count(federations, 'offline')} reported down, ` + `${count(federations, 'offline')} reported down, ` +
`${count(federations, 'announced')} announced only)`, `${count(federations, 'announced')} announced only)`,
); );
console.log(
`${lnurlMints.length} LNURL mints (${count(lnurlMints, 'online')} online, ` +
`${count(lnurlMints, 'offline')} offline)`,
);
console.log(`${reviews} reviews indexed`); console.log(`${reviews} reviews indexed`);
closePool(); closePool();
+75
View File
@@ -1,9 +1,41 @@
import { Hono } from 'hono'; import { Hono } from 'hono';
import type { Context } from 'hono';
import { cors } from 'hono/cors'; import { cors } from 'hono/cors';
import { serveStatic } from '@hono/node-server/serve-static'; import { serveStatic } from '@hono/node-server/serve-static';
import path from 'node:path'; import path from 'node:path';
import { config } from './config.ts'; import { config } from './config.ts';
import { indexSubmission } from './index-mint.ts';
import { getHealth, getMintDetail, getStats, listMints } from './queries.ts'; import { getHealth, getMintDetail, getStats, listMints } from './queries.ts';
import { RATE_LIMIT, takeToken } from './rate-limit.ts';
/** The largest `POST /api/index` body read. A JSON object with two short strings. */
const MAX_BODY_BYTES = 8 * 1024;
/**
* Who is submitting, for the rate limiter.
*
* The socket's peer address, unless that peer is the loopback interface — in which case
* this process is behind the reverse proxy the README documents, every request has the
* same peer, and `X-Forwarded-For` is the only thing that tells two visitors apart.
*
* The *last* entry of that header, not the first. nginx's `$proxy_add_x_forwarded_for`
* appends the peer it actually saw to whatever the client sent, so the first entry is a
* value a client can write for itself — a free way around the limit — and the last is
* the one the proxy vouched for. When the peer is not loopback the header is ignored
* entirely, because then there is no proxy to have vouched for anything.
*/
function clientKey(c: Context): string {
const socket = (c.env as { incoming?: { socket?: { remoteAddress?: string } } } | undefined)
?.incoming?.socket;
const peer = socket?.remoteAddress ?? '';
const loopback = peer === '' || peer === '::1' || peer === '127.0.0.1' || peer.startsWith('::ffff:127.');
if (!loopback) return peer;
const forwarded = c.req.header('x-forwarded-for') ?? '';
const hops = forwarded.split(',').map((hop) => hop.trim()).filter(Boolean);
return hops[hops.length - 1] ?? peer ?? 'unknown';
}
export function createApp(): Hono { export function createApp(): Hono {
const app = new Hono(); const app = new Hono();
@@ -41,6 +73,49 @@ export function createApp(): Hono {
return c.json(detail); return c.json(detail);
}); });
/*
* The one endpoint that writes: index a mint nobody has announced yet.
*
* It is a POST because it creates a row, and it is rate limited because it is the
* only route that makes this server fetch an address somebody else chose. Everything
* it actually does lives in `index-mint.ts`; what is here is the shape of the request
* and the two things that can only be decided at the edge — who is asking, and how
* much body to read from them.
*/
app.post('/api/index', async (c) => {
const verdict = takeToken(clientKey(c));
if (!verdict.ok) {
c.header('Retry-After', String(verdict.retryAfter));
return c.json(
{
error: 'rate_limited',
message: `At most ${RATE_LIMIT} submissions an hour from one address`,
retry_after: verdict.retryAfter,
},
429,
);
}
// An invite code runs to a few hundred characters; nothing legitimate is near this.
const declared = Number(c.req.header('content-length') ?? '0');
if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
return c.json({ error: 'bad_input', message: 'Request body too large' }, 422);
}
let body: { type?: unknown; input?: unknown };
try {
body = (await c.req.json()) as { type?: unknown; input?: unknown };
} catch {
return c.json({ error: 'bad_input', message: 'Body must be JSON' }, 422);
}
const outcome = await indexSubmission(String(body?.type ?? ''), body?.input);
if (outcome.status === 429 && 'retry_after' in outcome.body) {
c.header('Retry-After', String(outcome.body.retry_after ?? 30));
}
return c.json(outcome.body, outcome.status);
});
// Cached mint icons, so an offline mint keeps its icon. // Cached mint icons, so an offline mint keeps its icon.
app.use( app.use(
'/icons/*', '/icons/*',
+311
View File
@@ -0,0 +1,311 @@
/**
* A Nostr relay, in one file, for tests.
*
* Enough of NIP-01 to accept an EVENT, answer a REQ and close a subscription, plus the
* addressable-replacement rule, which is not optional here: the whole point of the
* round-trip test is a `kind:38174`, and an addressable kind that a relay stores twice
* would let a broken publisher pass.
*
* Written rather than depended on, deliberately. The alternatives were a real relay
* binary (which CI would have to install and keep running) or a `ws` dependency added to
* the lockfile for test-only code. Node 22 ships a WebSocket *client* — which is what
* nostr-tools uses — but no server, so the handshake and framing below are the actual
* cost of not adding either, and RFC 6455 is small when the only frames that matter are
* short unfragmented text ones.
*
* **Not for production.** No authentication, no persistence, no NIP-42, no rate limits,
* no fragmentation support beyond a single continuation, and everything lives in a Map
* until the process exits. `api/src/check-lnurl.ts` is the only caller.
*/
import { createHash } from 'node:crypto';
import { createServer, type IncomingMessage, type Server } from 'node:http';
import type { Duplex } from 'node:stream';
/** RFC 6455's fixed handshake GUID. */
const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
interface StoredEvent {
id: string;
pubkey: string;
kind: number;
created_at: number;
content: string;
tags: string[][];
sig?: string;
}
type Filter = Record<string, unknown>;
/* ---------- framing ---------- */
/** Encode one unfragmented text frame, server to client, never masked. */
function encodeText(text: string): Buffer {
const payload = Buffer.from(text, 'utf8');
const length = payload.length;
let header: Buffer;
if (length < 126) {
header = Buffer.from([0x81, length]);
} else if (length < 65536) {
header = Buffer.alloc(4);
header[0] = 0x81;
header[1] = 126;
header.writeUInt16BE(length, 2);
} else {
header = Buffer.alloc(10);
header[0] = 0x81;
header[1] = 127;
header.writeBigUInt64BE(BigInt(length), 2);
}
return Buffer.concat([header, payload]);
}
interface DecodedFrame {
opcode: number;
payload: Buffer;
/** Total bytes consumed, so the caller can advance its buffer. */
size: number;
}
/**
* Decode one frame from the front of `buffer`, or null when it is not all there yet.
*
* Client frames are always masked, per the spec, so the mask is applied unconditionally
* when the bit is set and ignored when it is not — a browser or Node client never omits
* it, and a test relay has no reason to reject one that did.
*/
function decodeFrame(buffer: Buffer): DecodedFrame | null {
if (buffer.length < 2) return null;
const first = buffer[0]!;
const second = buffer[1]!;
const opcode = first & 0x0f;
const masked = (second & 0x80) !== 0;
let length = second & 0x7f;
let offset = 2;
if (length === 126) {
if (buffer.length < offset + 2) return null;
length = buffer.readUInt16BE(offset);
offset += 2;
} else if (length === 127) {
if (buffer.length < offset + 8) return null;
const big = buffer.readBigUInt64BE(offset);
// A test relay has no business buffering a 4GB frame.
if (big > 8n * 1024n * 1024n) throw new Error('frame too large');
length = Number(big);
offset += 8;
}
let mask: Buffer | null = null;
if (masked) {
if (buffer.length < offset + 4) return null;
mask = buffer.subarray(offset, offset + 4);
offset += 4;
}
if (buffer.length < offset + length) return null;
const payload = Buffer.from(buffer.subarray(offset, offset + length));
if (mask) {
for (let i = 0; i < payload.length; i++) payload[i] = payload[i]! ^ mask[i % 4]!;
}
return { opcode, payload, size: offset + length };
}
/* ---------- filters ---------- */
function matchesFilter(event: StoredEvent, filter: Filter): boolean {
const ids = filter['ids'] as string[] | undefined;
if (ids && !ids.includes(event.id)) return false;
const authors = filter['authors'] as string[] | undefined;
if (authors && !authors.includes(event.pubkey)) return false;
const kinds = filter['kinds'] as number[] | undefined;
if (kinds && !kinds.includes(event.kind)) return false;
const since = filter['since'] as number | undefined;
if (typeof since === 'number' && event.created_at < since) return false;
const until = filter['until'] as number | undefined;
if (typeof until === 'number' && event.created_at > until) return false;
// `#e`, `#p`, `#d`, `#k`, `#u`: match any value of that single-letter tag.
for (const [key, wanted] of Object.entries(filter)) {
if (!key.startsWith('#') || key.length !== 2) continue;
const name = key.slice(1);
const values = wanted as string[];
const present = event.tags.filter((t) => t[0] === name).map((t) => t[1]);
if (!present.some((value) => value !== undefined && values.includes(value))) return false;
}
return true;
}
/**
* The storage key for an event.
*
* Addressable kinds (30000–39999) are keyed by `kind:pubkey:d`, so a second announcement
* from the same publisher for the same mint replaces the first instead of accumulating —
* which is exactly the behaviour `kind:38174` depends on and therefore exactly what a
* test of it must reproduce. Everything else is keyed by its own id.
*/
function storageKey(event: StoredEvent): string {
if (event.kind >= 30000 && event.kind < 40000) {
const d = event.tags.find((t) => t[0] === 'd')?.[1] ?? '';
return `${event.kind}:${event.pubkey}:${d}`;
}
if (event.kind === 0 || event.kind === 3 || (event.kind >= 10000 && event.kind < 20000)) {
return `${event.kind}:${event.pubkey}`;
}
return event.id;
}
/* ---------- the relay ---------- */
export interface TestRelay {
/** `ws://127.0.0.1:<port>`, ready to hand to a pool. */
url: string;
/** Every event currently stored, newest first. */
events(): StoredEvent[];
/** Events of one kind, newest first. */
byKind(kind: number): StoredEvent[];
close(): Promise<void>;
}
/**
* Start a relay on an ephemeral port.
*
* Port 0 rather than a fixed one so two tests, or two CI jobs on one machine, cannot
* collide — the caller reads the real port back off `url`.
*/
export async function startTestRelay(): Promise<TestRelay> {
const stored = new Map<string, StoredEvent>();
const sockets = new Set<Duplex>();
const server: Server = createServer((_req, res) => {
// Not a websocket upgrade. NIP-11 would go here on a real relay.
res.writeHead(426, { 'Content-Type': 'text/plain' });
res.end('websocket only');
});
server.on('upgrade', (req: IncomingMessage, socket: Duplex) => {
const key = req.headers['sec-websocket-key'];
if (typeof key !== 'string') {
socket.destroy();
return;
}
const accept = createHash('sha1').update(key + WS_GUID).digest('base64');
socket.write(
'HTTP/1.1 101 Switching Protocols\r\n' +
'Upgrade: websocket\r\n' +
'Connection: Upgrade\r\n' +
`Sec-WebSocket-Accept: ${accept}\r\n\r\n`,
);
sockets.add(socket);
socket.on('close', () => sockets.delete(socket));
socket.on('error', () => sockets.delete(socket));
const send = (message: unknown): void => {
if (!socket.destroyed) socket.write(encodeText(JSON.stringify(message)));
};
let buffer = Buffer.alloc(0);
socket.on('data', (chunk: Buffer) => {
buffer = Buffer.concat([buffer, chunk]);
for (;;) {
let frame: DecodedFrame | null;
try {
frame = decodeFrame(buffer);
} catch {
socket.destroy();
return;
}
if (!frame) break;
buffer = buffer.subarray(frame.size);
if (frame.opcode === 0x8) {
socket.end();
return;
}
// Ping: answer with a pong carrying the same payload, per the spec.
if (frame.opcode === 0x9) {
const pong = encodeText('');
pong[0] = 0x8a;
socket.write(pong);
continue;
}
if (frame.opcode !== 0x1) continue;
let message: unknown;
try {
message = JSON.parse(frame.payload.toString('utf8'));
} catch {
continue;
}
if (!Array.isArray(message)) continue;
const [verb, ...rest] = message as [string, ...unknown[]];
if (verb === 'EVENT') {
const event = rest[0] as StoredEvent | undefined;
if (!event?.id) continue;
const existing = stored.get(storageKey(event));
// Older replacement for an addressable kind: keep what is there, and still
// answer OK, which is what a real relay does.
if (!existing || existing.created_at <= event.created_at) {
stored.set(storageKey(event), event);
}
send(['OK', event.id, true, '']);
continue;
}
if (verb === 'REQ') {
const subId = rest[0] as string;
const filters = rest.slice(1) as Filter[];
const all = [...stored.values()].sort((a, b) => b.created_at - a.created_at);
for (const filter of filters) {
const limit = typeof filter['limit'] === 'number' ? (filter['limit'] as number) : Infinity;
let sent = 0;
for (const event of all) {
if (sent >= limit) break;
if (!matchesFilter(event, filter)) continue;
send(['EVENT', subId, event]);
sent++;
}
}
send(['EOSE', subId]);
continue;
}
if (verb === 'CLOSE') {
send(['CLOSED', rest[0] as string, '']);
}
}
});
});
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
const address = server.address();
if (!address || typeof address === 'string') throw new Error('relay did not bind a port');
return {
url: `ws://127.0.0.1:${address.port}`,
events: () => [...stored.values()].sort((a, b) => b.created_at - a.created_at),
byKind: (kind) =>
[...stored.values()].filter((e) => e.kind === kind).sort((a, b) => b.created_at - a.created_at),
close: async () => {
for (const socket of sockets) socket.destroy();
sockets.clear();
await new Promise<void>((resolve) => server.close(() => resolve()));
},
};
}
+16 -1
View File
@@ -7,7 +7,22 @@
"strict": true, "strict": true,
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
"noImplicitOverride": true, "noImplicitOverride": true,
"noEmit": true, /*
* This project is compiled now, rather than run straight off `src/*.ts`.
*
* The reason is a fifteen-hour crash loop: the unit's ExecStart named `src/index.ts`,
* the host's `/usr/bin/node` was 20, and native type stripping is 22.18 and newer, so
* every start died on ERR_UNKNOWN_FILE_EXTENSION and systemd restarted it 464 times
* without anything going red. Emitting plain `.js` removes the host's Node version
* from the set of things that can break a deploy.
*
* `rewriteRelativeImportExtensions` is what lets the source keep its explicit `.ts`
* specifiers — which is what makes `node --watch src/index.ts` work in development —
* while the emitted files import `./config.js` and run anywhere.
*/
"outDir": "dist",
"rootDir": "src",
"sourceMap": true,
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"rewriteRelativeImportExtensions": true, "rewriteRelativeImportExtensions": true,
"skipLibCheck": true, "skipLibCheck": true,
+23
View File
@@ -0,0 +1,23 @@
# /etc/cashumints/alert.env
#
# Read by cashumints-alert@.service, which systemd starts when any of the three units
# fails. Everything here is optional: with the file absent or both values empty, an
# alert is still written to the journal at ERROR priority and is readable with
#
# journalctl -p err -t cashumints-alert
#
# Set one or both to have failures leave the machine.
#
# Install it root-owned and not world-readable — a webhook URL is a capability:
# sudo install -d -m 0755 /etc/cashumints
# sudo install -m 0640 -o root -g root deploy/alert.env.example /etc/cashumints/alert.env
# sudo systemctl daemon-reload
# An ntfy topic URL. Free and public at ntfy.sh; pick a topic name nobody will guess,
# because anyone who knows it can read and post to it.
#NTFY_URL=https://ntfy.sh/cashumints-alerts-CHANGE-ME
# Anything that accepts a JSON POST. The body carries `unit`, `host`, `at`, `text` and
# `content` — the last of which is what Discord and most Slack-compatible endpoints read,
# so one payload fits all three.
#WEBHOOK_URL=https://discord.com/api/webhooks/…
+101
View File
@@ -0,0 +1,101 @@
# /etc/systemd/system/cashumints-alert@.service
#
# The unit that makes a failure audible.
#
# The other three units each carry `OnFailure=cashumints-alert@%n.service`, so systemd
# starts one of these with the failed unit's name as the instance — `%i` below is
# literally `cashumints.service`, `cashumints-web.service` or `cashumints-site.service`.
#
# Why it exists: the API once crash looped 464 times over fifteen hours and nothing said
# so. `Restart=on-failure` with no start limit is an infinite loop that never reaches a
# `failed` state, so the journal filled with identical lines nobody was reading and
# every signal stayed green. The other half of the fix is StartLimitBurst= in each unit,
# which turns the loop into a failure; this is what carries that failure off the machine.
#
# Install:
# sudo install -m 0644 deploy/cashumints-alert@.service /etc/systemd/system/
# sudo install -d -m 0755 /etc/cashumints
# sudo install -m 0640 -o root -g root deploy/alert.env.example /etc/cashumints/alert.env
# sudo systemctl daemon-reload
#
# No [Install] section and never enabled: OnFailure= starts it, and a unit that also
# started at boot would page on every reboot.
[Unit]
Description=Notify that %i failed
# No OnFailure= here. An alerter that alerts about its own failure is a loop, and this
# one is written so its worst case is a journal line rather than a retry.
[Service]
Type=oneshot
# The one file an operator edits, and the only reason this unit is configurable at all.
# Absent is a supported state — the leading `-` says so — and then the ExecStart below
# still writes to the journal at ERROR, which is what `systemctl status` and
# `journalctl -p err` read. See alert.env.example.
EnvironmentFile=-/etc/cashumints/alert.env
# So `journalctl -t cashumints-alert` finds every alert, whichever unit triggered it.
SyslogIdentifier=cashumints-alert
# Everything is inside one shell so the "nothing configured" branch is reachable without
# a second unit. The pieces, in order:
#
# - `printf '<3>…'` on stdout. systemd reads that syslog prefix off a journal stream
# and files the line at priority 3, ERROR, so `journalctl -p err` is a complete
# history of failures on a host with no webhook configured at all. `<4>` is warning.
# A prefix rather than systemd-cat, so the unit needs nothing from the filesystem it
# has just sandboxed itself away from.
# - NTFY_URL is a topic URL (https://ntfy.sh/your-topic). It gets a plain-text body
# naming the failed unit, plus the header names ntfy understands.
# - WEBHOOK_URL gets a JSON POST instead, for Discord, Slack or anything that speaks
# `{"content": …}` — every key is sent, so one payload fits all of them.
# - `--max-time 10` and a `||` fallback on each: an alert that hangs would hold the
# failed unit's job open, and an alert that fails must not itself become a second
# failed unit for somebody to notice. The shell ends in `true` for the same reason.
#
# `%i` is the failed unit's name, passed as an argument rather than interpolated into
# the shell text: systemd expands specifiers before /bin/sh ever sees the line, and a
# unit name is not a thing to trust to quoting.
ExecStart=/bin/sh -c '\
UNIT="$1"; \
HOST="$(hostname)"; \
WHEN="$(date -Is)"; \
TEXT="$UNIT failed on $HOST at $WHEN"; \
printf "<3>%s\\n" "$TEXT"; \
if [ -n "$NTFY_URL" ]; then \
/usr/bin/curl -fsS --max-time 10 \
-H "Title: cashumints: $UNIT failed" \
-H "Priority: high" \
-H "Tags: rotating_light" \
-d "$TEXT" "$NTFY_URL" >/dev/null \
|| printf "<3>%s\\n" "alert: POST to NTFY_URL failed"; \
fi; \
if [ -n "$WEBHOOK_URL" ]; then \
/usr/bin/curl -fsS --max-time 10 \
-H "Content-Type: application/json" \
-d "{\\"unit\\":\\"$UNIT\\",\\"host\\":\\"$HOST\\",\\"at\\":\\"$WHEN\\",\\"text\\":\\"$TEXT\\",\\"content\\":\\"$TEXT\\"}" \
"$WEBHOOK_URL" >/dev/null \
|| printf "<3>%s\\n" "alert: POST to WEBHOOK_URL failed"; \
fi; \
if [ -z "$NTFY_URL" ] && [ -z "$WEBHOOK_URL" ]; then \
printf "<4>%s\\n" "alert: no NTFY_URL or WEBHOOK_URL in /etc/cashumints/alert.env, journal only"; \
fi; \
true' _ %i
# It sends one HTTP request and writes one line. It needs no identity of its own, and
# DynamicUser gives it a throwaway one rather than sharing `nobody` with everything else
# on the host that also could not be bothered to make a user.
DynamicUser=yes
NoNewPrivileges=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictSUIDSGID=true
LockPersonality=true
# An alert that cannot reach the network in ten seconds is not worth a stuck job.
TimeoutStartSec=30
+67
View File
@@ -0,0 +1,67 @@
# /etc/systemd/system/cashumints-site.service
#
# Serves the built site on loopback. nginx proxies to it and never opens a file itself,
# which is the point: when nginx held a `root` inside /home/cashumints, every directory
# down to dist had to be traversable by www-data, and the one that was not took the
# whole site down as a blanket 404 with nothing in the error log naming the cause.
#
# This is a long-running daemon, unlike cashumints-web.service next to it — that one is
# the oneshot that produces what this one serves.
[Unit]
Description=cashumints.space static site server
Wants=network-online.target
After=network-online.target
# Give up after five failures in two minutes instead of restarting forever. A process
# that cannot start will not start on the 4000th attempt either, and `failed` in
# `systemctl status` is a far louder signal than a journal scrolling past. The window
# matches cashumints.service; see the note there for why it is 120s and not 60s. These
# two are [Unit] keys; systemd ignores them under [Service] with only a warning.
StartLimitIntervalSec=120
StartLimitBurst=5
# Carry a failure off the machine. `%n` is this unit's own name, so the alert says
# which one died. cashumints-alert@.service writes to the journal at ERROR always and
# curls NTFY_URL or WEBHOOK_URL from /etc/cashumints/alert.env when either is set.
OnFailure=cashumints-alert@%n.service
# Not Requires=cashumints.service: the pages are prerendered, so the site keeps serving
# a correct-as-of-last-build copy while the API is down. Only the islands go quiet.
[Service]
Type=simple
User=cashumints
Group=cashumints
WorkingDirectory=/home/cashumints/CashuMints.space/web
# The tree comes from cashumints-web.service, which rsyncs it here after a build.
# Serving web/dist directly would mean a rebuild empties the site for the length of it.
StateDirectory=cashumints
Environment=NODE_ENV=production
Environment=SITE_PORT=8789
Environment=SITE_HOST=127.0.0.1
Environment=WEB_ROOT=/var/lib/cashumints/web
ExecStart=/usr/bin/node server.mjs
Restart=on-failure
RestartSec=5s
KillSignal=SIGTERM
# In-flight responses finish; idle keep-alive connections are closed at once.
TimeoutStopSec=15s
UMask=0027
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
# Read-only rather than absent: server.mjs itself lives under /home/cashumints.
ProtectHome=read-only
ReadWritePaths=/var/lib/cashumints
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictSUIDSGID=true
LockPersonality=true
[Install]
WantedBy=multi-user.target
+141
View File
@@ -0,0 +1,141 @@
# /etc/systemd/system/cashumints-web.service
#
# The frontend is static: `output: 'static'` in astro.config.mjs, and the whole site is
# produced ahead of time. There is no frontend build to keep alive, so this unit is a
# build rather than a daemon — one shot of `pnpm build`, which compiles shared/, renders
# a social card per mint and prerenders every page from the live API. The daemon that
# hands the result out is cashumints-site.service.
#
# Run it after a deploy, and only after a deploy:
# sudo systemctl start cashumints-web
#
# There used to be a cashumints-web.timer firing this at 03:30 every night, because the
# mint list was a snapshot of whatever the API held when the build ran and a nightly
# rebuild was the only way it ever changed. The list hydrates from the API after paint
# now, so a new mint, a new review count and a changed status all reach the page within
# a second of load, and rebuilding 2,000 pages at 03:30 to refresh numbers that refresh
# themselves is 20 minutes of CPU for nothing.
#
# What a build still produces, and therefore what a deploy is still for: the prerendered
# HTML a crawler reads, the social card per mint, the sitemap, and a `/mint/{host}` page
# for every mint known at build time. A mint indexed since the last deploy has no page of
# its own until the next one; the 404 fallback resolves it against the live API, so it is
# readable and reviewable in the meantime. That was already true between nightly builds.
#
# There is deliberately no [Install] section — this belongs to a deploy, not to a boot.
[Unit]
Description=Rebuild the cashumints.space static site
# Every page's data comes from the API over loopback, so the API has to be up.
# Requires= rather than Wants=: a dead API should abort the build, not replace a good
# site with an empty one.
Requires=cashumints.service
After=cashumints.service network-online.target
Wants=network-online.target
# Carry a failure off the machine. `%n` is this unit's own name, so the alert says
# which one died. cashumints-alert@.service writes to the journal at ERROR always and
# curls NTFY_URL or WEBHOOK_URL from /etc/cashumints/alert.env when either is set.
OnFailure=cashumints-alert@%n.service
[Service]
Type=oneshot
User=cashumints
Group=cashumints
WorkingDirectory=/home/cashumints/CashuMints.space
# Where the published copy lands. Shared with the API and the site server, and created
# by systemd with this unit's ownership if it is not there yet.
StateDirectory=cashumints
Environment=NODE_ENV=production
# Where the build reaches the API. Must match PORT= in cashumints.service.
Environment=API_URL=http://127.0.0.1:8788
Environment=SITE_URL=https://cashumints.space
# Browser-facing origin. Empty means same origin: islands fetch /api/... and nginx
# forwards it. Set this only if the API ever moves to its own hostname. Declared here
# even though it is empty, because systemd's environment wins over .env — so what a
# production build emits cannot drift with an edit to that file.
Environment=PUBLIC_API_URL=
# After= orders the start; it does not wait for the port to accept connections. At boot
# the API is still opening its database and probing, so block until it reports healthy
# rather than letting the first fetch die on ECONNREFUSED. /api/health answers 503 until
# it is genuinely ready, and curl -f treats that as a failure, so the loop keeps waiting.
ExecStartPre=/usr/bin/timeout 90 /bin/sh -c 'until curl -sf -o /dev/null http://127.0.0.1:8788/api/health; do sleep 1; done'
# Then: does the API actually have an index to build a site out of?
#
# Health answering 200 says the process is up and its last backfill read something. It
# does not say how many mints are in the table, and those are different questions — the
# year of ~31-event backfills had a healthy API serving a real, complete, correct list of
# eight mints. A build against that succeeds, prerenders eight cards, and rsync happily
# replaces fifty-five with eight.
#
# So count the list before spending twenty minutes building from it. Below the floor
# this exits non-zero, systemd abandons the unit at ExecStartPre, and — because publishing
# is ExecStartPost, after the build — the previously published site is never touched. The
# site stays exactly as it was and the OnFailure alert says why.
#
# Counted by the `"host":` key, one per item, rather than by counting `{`: the list
# payload carries a nested object per mint (its NUT capability switches), so brace
# counting would report roughly double. No jq: it is not installed on this host and a
# build gate should not add a dependency to run.
#
# `Q` is a double-quote character, built with printf rather than written literally,
# because this whole command is already inside systemd's single quotes and a quote of
# either kind in the grep pattern would end the argument early.
#
# A curl that fails for any reason leaves `n` empty, `$${n:-0}` reads that as zero, and
# zero is below every floor — so an API that fell over between the health check above and
# this line refuses the build rather than sailing through it.
Environment=MIN_MINTS_FOR_BUILD=20
ExecStartPre=/bin/sh -c 'Q=$$(printf "\\042"); \
n=$$(curl -sf --max-time 30 http://127.0.0.1:8788/api/mints | grep -o "$${Q}host$${Q}:" | wc -l); \
if [ "$${n:-0}" -lt "$$MIN_MINTS_FOR_BUILD" ]; then \
printf "<3>%s\\n" "refusing to build: /api/mints returned $${n:-0} mints, floor is $$MIN_MINTS_FOR_BUILD. Previous site left untouched."; \
exit 1; \
fi; \
printf "%s\\n" "build gate: $$n mints, floor $$MIN_MINTS_FOR_BUILD"'
# Check `which pnpm` on the host: a corepack or pnpm-home install sits outside /usr/bin,
# and systemd's PATH does not include it.
ExecStart=/usr/bin/pnpm build
# Publish, as a separate step from building.
#
# `astro build` empties dist before it writes, so the site server cannot read dist
# directly — a rebuild would be a minute of 404s. It serves this copy instead, and the
# copy is only touched once a build has succeeded: a failed build leaves the previous
# site up rather than replacing it with a half-written one, which is the same reason
# Requires=cashumints.service is above and the same reason the mint-count gate is an
# ExecStartPre rather than a check after the fact.
#
# --delay-updates stages the changed files and renames them in at the end, so the window
# where the tree is a mix of two builds is a rename rather than a whole transfer, and
# --delete-after keeps removals from landing before their replacements. Unchanged files
# — every hashed asset and card, which is nearly all of it — are not touched at all.
ExecStartPost=/usr/bin/rsync -a --delete-after --delay-updates web/dist/ /var/lib/cashumints/web/
# ~200 prerendered pages plus a card per mint. Minutes, not seconds, on a small VPS, and
# TimeoutStartSec is what bounds a Type=oneshot.
TimeoutStartSec=1800
# A build should not starve the API it is reading from.
Nice=10
# The site server runs as cashumints and reads its own files, so this no longer has to
# be world-readable — it was 0022 for nginx, back when nginx opened the files as
# www-data. Kept at 0022 anyway: rsync preserves these modes into the published copy,
# and a readable static site is easier to inspect than one that needs sudo.
UMask=0022
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
# ProtectHome is deliberately absent, unlike in cashumints.service: this unit writes
# inside /home/cashumints — web/dist, web/public/og, web/src/generated and the pnpm
# store are all under it.
ProtectSystem=full
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
+70
View File
@@ -0,0 +1,70 @@
# /etc/systemd/system/cashumints.service
[Unit]
Description=cashumints.space indexer and API
Wants=network-online.target
After=network-online.target
# Stop after five failures in two minutes rather than restarting forever.
#
# The window is 120s and not 60s because RestartSec=5s below means five attempts take
# a little over twenty seconds of restarts plus however long each attempt lives before
# it dies. A process that fails *slowly* — a database that times out, a port that takes
# four seconds to refuse — can spread five failures past a sixty second window and reset
# the counter forever, which is the loop this is supposed to stop. 120s covers that.
#
# The failure this exists for: ExecStart named a .ts file, /usr/bin/node was 20, and
# every start died in under a second. 464 restarts over fifteen hours, and because
# Restart=on-failure without a start limit never reaches a `failed` state, nothing
# anywhere went red. Both keys belong to [Unit] — under [Service] systemd only warns and
# ignores them.
StartLimitIntervalSec=120
StartLimitBurst=5
# Carry a failure off the machine. `%n` is this unit's own name, so the alert says
# which one died. cashumints-alert@.service writes to the journal at ERROR always and
# curls NTFY_URL or WEBHOOK_URL from /etc/cashumints/alert.env when either is set.
OnFailure=cashumints-alert@%n.service
[Service]
Type=simple
User=cashumints
Group=cashumints
WorkingDirectory=/home/cashumints/CashuMints.space/api
# StateDirectory creates /var/lib/cashumints with the service user's ownership.
StateDirectory=cashumints
Environment=NODE_ENV=production
Environment=PORT=8788
Environment=DB_PATH=/var/lib/cashumints/cashumints.db
Environment=ICON_DIR=/var/lib/cashumints/icons
# Compiled JavaScript, run by the distribution's own node.
#
# This line used to read `src/index.ts`, which made every start depend on the host
# having Node 22.18 or newer for native type stripping. A host with Node 20 answered
# that with ERR_UNKNOWN_FILE_EXTENSION in under a second, 464 times over fifteen hours,
# and nothing anywhere went red. `pnpm build` now emits api/dist, so what runs here is
# ordinary ESM and any Node from 20.18 up will start it.
#
# Deliberately /usr/bin/node and nothing else: an nvm or fnm path is invisible to this
# unit's ProtectHome and breaks silently at the next version bump.
ExecStart=/usr/bin/node --env-file-if-exists=../.env dist/index.js
Restart=on-failure
RestartSec=5s
KillSignal=SIGTERM
TimeoutStopSec=30s
UMask=0027
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/var/lib/cashumints
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
[Install]
WantedBy=multi-user.target
+160
View File
@@ -0,0 +1,160 @@
# /etc/nginx/sites-available/cashumints.space
#
# nginx terminates TLS and proxies. It opens no file belonging to this project — not the
# built site, not an icon — and that is deliberate.
#
# It used to point a `root` at web/dist. Because nginx runs as www-data and everything
# this project owns runs as cashumints, that arrangement required every directory from /
# down to dist to be traversable by a user with no other business in the tree. A home
# directory at its default 0700 anywhere in that chain broke the entire site, and it
# broke it invisibly: `try_files` treats a permission error as a plain miss, so the
# symptom was a blanket 404, or an internal-redirect loop that ended in a 500 with the
# real cause named nowhere.
#
# Two upstreams now, both on loopback, both owned by the same user that built what they
# serve:
#
# 127.0.0.1:8789 cashumints-site.service the prerendered site
# 127.0.0.1:8788 cashumints.service /api/* and /icons/*
#
# Routing that used to live here lives with the thing that owns it. The locale 404 rule
# in particular was a hand-maintained alternation of 23 codes in a file that is not in
# the repository; adding a language meant remembering to edit it, and forgetting was
# silent. web/server.mjs resolves those from the built tree.
proxy_cache_path /var/cache/nginx/cashumints
levels=1:2
keys_zone=cashumints:10m
max_size=256m
inactive=10m
use_temp_path=off;
# Keep a few connections open to each upstream rather than reconnecting per request.
# Both processes hold idle sockets longer than nginx does, so nginx is always the side
# that closes and there is no window where it reuses a socket the upstream just dropped
# — that race is what produces sporadic 502s under load.
upstream cashumints_site {
server 127.0.0.1:8789;
keepalive 16;
}
upstream cashumints_api {
server 127.0.0.1:8788;
keepalive 8;
}
server {
listen 80;
listen [::]:80;
server_name cashumints.space;
location /.well-known/acme-challenge/ {
root /var/www/html;
}
location / {
return 301 https://cashumints.space$request_uri;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name cashumints.space;
# nginx 1.25 and later want `http2 on;` on its own line and warn about the form above.
# Left as is because it is the form that works on both, and Debian 12 ships 1.22.
ssl_certificate /etc/letsencrypt/live/cashumints.space/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cashumints.space/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Set here rather than upstream: this is the only part of the stack that knows a
# request arrived over TLS. Add `preload` only once you are content never to serve
# this name over plain HTTP again.
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# The upstreams send no Content-Encoding, so compression is nginx's to do. gzip_proxied
# any is required — without it nginx refuses to compress a proxied response at all.
gzip on;
gzip_proxied any;
gzip_vary on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_types text/plain text/css text/javascript application/javascript application/json
application/manifest+json application/xml image/svg+xml;
# Nothing here accepts an upload. The API's largest body is an 8 KB JSON submission,
# and rejecting the oversized ones at the edge keeps them off the Node process.
client_max_body_size 16k;
# Both upstreams are a process on this machine. A slow response is a bug, not a
# network condition, and failing fast beats holding a worker for a minute.
proxy_connect_timeout 2s;
proxy_read_timeout 30s;
proxy_send_timeout 30s;
# HTTP/1.1 with an empty Connection header is what makes the keepalive pools above
# work; the default 1.0 opens a new socket per request.
proxy_http_version 1.1;
proxy_set_header Connection "";
# Every proxied location includes Debian's /etc/nginx/proxy_params, which sets Host,
# X-Real-IP, X-Forwarded-For and X-Forwarded-Proto. That include is load-bearing, not
# decorative: the API's rate limiter reads the last hop of X-Forwarded-For to tell two
# visitors apart, and without it every request arrives from the loopback peer and
# shares one bucket. On a distro that ships no proxy_params, set those four by hand.
# Do not also set X-Forwarded-For alongside the include — declaring it twice is what
# produces nginx's "could not build optimal proxy_headers_hash" warning.
# The prerendered site. Cache-Control comes from server.mjs — a year and immutable for
# anything with a content hash in its name, revalidate-every-time for markup — so
# there is nothing to restate here.
location / {
proxy_pass http://cashumints_site;
include proxy_params;
}
# Health must always reflect the live process.
location = /api/health {
proxy_pass http://cashumints_api;
proxy_cache off;
# add_header replaces rather than merges: declaring one here drops every add_header
# inherited from the server block, so HSTS has to be restated alongside it.
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Cache-Control "no-store" always;
include proxy_params;
}
# Briefly cache the read-heavy endpoints.
location ~ ^/api/(mints|stats)(?:/|$|\?) {
proxy_pass http://cashumints_api;
proxy_cache cashumints;
proxy_cache_valid 200 30s;
proxy_cache_valid 404 10s;
proxy_cache_use_stale updating error timeout http_500 http_502 http_503;
proxy_cache_background_update on;
proxy_cache_lock on;
# Restated for the same reason as in /api/health above.
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Cache-Status $upstream_cache_status always;
include proxy_params;
}
location /api/ {
proxy_pass http://cashumints_api;
include proxy_params;
}
location /icons/ {
proxy_pass http://cashumints_api;
proxy_cache cashumints;
proxy_cache_valid 200 1d;
include proxy_params;
}
# No error_page and no try_files. A miss is the site server's 404 page, in the right
# language and with a 404 status; an nginx error page here would replace it with a
# blank one and hide which upstream failed.
}
+384
View File
@@ -0,0 +1,384 @@
# NIP-87 extension: `kind:38174`, LNURL mint announcements
`draft` `optional`
> **Status.** This is a proposed extension to [NIP-87][nip87], written to be submitted
> as a PR to [nostr-protocol/nips][nips]. It is not part of NIP-87 today. It is
> implemented and published by [cashumints.space](https://cashumints.space), which is
> also where the reference indexer for it lives. Nothing here changes 38172, 38173 or
> 38000; a client that implements only NIP-87 as written keeps working unchanged, and
> gets partial support for these events for free (see [Reviews](#reviews)).
[nip87]: https://github.com/nostr-protocol/nips/blob/master/87.md
[nips]: https://github.com/nostr-protocol/nips
---
## Rationale
NIP-87 gives ecash mints a way to be discovered, and gives users a way to recommend
them: an operator publishes an addressable announcement, a user publishes a
`kind:38000` naming the announcement's kind in a `k` tag, and a client queries by that
`k` to find recommendations for one ecosystem without seeing the others.
It covers exactly two ecosystems, one kind each: `38172` for Cashu, `38173` for
Fedimint. A third has since shipped — **lnurlcash** ([LUD-25][lud25]), Lightning bearer
notes served over plain [LUD-03][lud03] `withdrawRequest` and [LUD-06][lud06]
`payRequest`, implemented by [dni/lnurl-mint][lnurlmint] — and it has no kind. The
result today is that an lnurlcash mint either goes unannounced or is announced as
something it is not.
The pattern generalises cleanly, and this document does nothing but apply it:
- an LNURL mint has a **stable identifier** (its funding node's public key) → `d`
- it has an **address you connect to** (its https base URL) → `u`
- it runs on a **network** → `n`
- it has a **capability list** → `features`, the analogue of `nuts` and `modules`
The one thing genuinely new here is the capability vocabulary, because Cashu's NUT
numbers and Fedimint's module names have no LNURL equivalent to borrow. It is defined
in full below, and every value in it is grounded in a specific code path in
`lnurl-mint`, because a vocabulary that outruns any implementation is a wish list.
[lud25]: https://github.com/lnurl/luds/blob/luds/25.md
[lud03]: https://github.com/lnurl/luds/blob/luds/03.md
[lud06]: https://github.com/lnurl/luds/blob/luds/06.md
[lud16]: https://github.com/lnurl/luds/blob/luds/16.md
[lud21]: https://github.com/lnurl/luds/blob/luds/21.md
[lnurlmint]: https://github.com/dni/lnurl-mint
---
## Choosing the kind number
`38174` — the next free slot in the NIP-87 family.
Checked before claiming it, on 2026-08-21:
| Source | Result |
| --- | --- |
| [`nips/README.md`][nipsreadme] kind index | `38172` and `38173` listed; `38174` **absent**, and nothing else in `38100–38199` is assigned |
| GitHub search over `nostr-protocol/nips`, issues **and** PRs, for `38174` | **0 results** (open or closed) |
`38174` is in the *addressable* range (`30000 ≤ kind < 40000`), which is required: an
announcement must be replaceable per `(pubkey, kind, d)` so an operator can update
their capability list in place, exactly as 38172 and 38173 are.
**If `38174` is claimed before this is merged**, take the next free kind in the same
range, prefer keeping the family contiguous, and record the collision and the
replacement here — implementations read the kind from one constant
(`KIND_LNURL_ANNOUNCEMENT`) precisely so that this is a one-line change.
[nipsreadme]: https://github.com/nostr-protocol/nips/blob/master/README.md
---
## The announcement event
LNURL mints SHOULD publish `kind:38174` to announce their capabilities and how to reach
them.
```json
{
"kind": 38174,
"pubkey": "<publisher-pubkey>",
"content": "<optional-kind:0-style-metadata>",
"tags": [
["d", "021ab89df9cbd28cdab8c71d228ca40deba1eaebd827f24849ec4f3e919c023555"],
["u", "https://lnurl.21mint.me"],
["features", "mint,melt,rotate,split,merge,lud06,lud03,lud16,lud21,signed-notes"],
["n", "mainnet"]
]
}
```
### `d` — the identifier
The `d` tag MUST be one of the following, in this order of preference:
1. **The mint's `mintPubkey`**, lowercase hex, when the mint advertises one. This is
the funding node's own identity key — a 33-byte compressed secp256k1 public key, 66
hex characters, beginning `02` or `03` — exposed on the mint-address endpoint
whenever a funding source is configured, and the same key the mint signs notes with
under LUD-25 offline verification.
2. **The normalized host**, when it does not: the lowercase hostname, plus `:port` if
non-default, plus the base path with no trailing slash, and **no scheme**. For
`https://mint.example.com/lnurl/` that is `mint.example.com/lnurl`.
The two forms are unambiguous by construction: form 1 is always exactly 66 hex
characters, form 2 always contains a `.` and never matches `^[0-9a-f]{66}$`.
#### When a mint gains a pubkey after being announced by host
This is not hypothetical. `mintPubkey` is present only while a funding source is
configured **and reachable**; a mint run without one — or announced during an outage —
legitimately has no pubkey to publish, and gains one later.
The rules, which exist so that identity never silently forks:
- **Prefer the pubkey once it exists.** A publisher that has been announcing by host
and learns a `mintPubkey` SHOULD begin publishing under the pubkey `d`.
- **The identifier is sticky.** Once a mint has been announced under a pubkey `d`, a
publisher MUST NOT revert to the host form because the node happened to be
unreachable at publish time. Absence of `mintPubkey` in one response is a statement
about the node's availability in that instant, not about the mint's identity.
- **The old announcement SHOULD be left in place**, not deleted. It is addressable, so
it stays queryable, and reviews already pointing at the host `d` keep resolving.
Publishers MAY additionally re-publish the host-`d` event with the same `u` tag so
both identifiers lead to the same live address.
- **Consumers MUST accept either.** An indexer resolving a review's `d` SHOULD try, in
order: exact match on a known `mintPubkey`; exact match on a known normalized host;
then fall back to the `u` tag. It MUST NOT treat the two identifiers for one mint as
two mints — deduplication is by **normalized base URL** (`u`), which is the one value
present in every form of the event.
### `u` — the address
The https base URL of the mint, with no trailing slash: `https://lnurl.21mint.me`.
`u` is REQUIRED in this kind, unlike in 38172 where it is a SHOULD. An LNURL mint's
`d` may be a bare host string with no scheme, which is not fetchable, so without `u`
there would be events carrying no usable address at all. Multiple `u` tags MAY appear;
the first is canonical. `.onion` addresses MUST NOT be the canonical `u` — they belong
in `features` as `onion` and are discovered from the mint itself.
Everything a client needs hangs off this base:
| Path | Role |
| --- | --- |
| `{u}/.well-known/lnurlw/_` | mint advertisement: withdraw limits, `mintPubkey`, node identity |
| `{u}/.well-known/lnurlp/_` | LUD-06 payRequest (also `{u}/.well-known/lnurlp/{username}`) |
| `{u}/w?k1=…` | LUD-03 withdrawRequest for one note |
| `{u}/w/cb` | melt / rotate / split / merge |
| `{u}/p/cb` | LUD-06 callback, mints a note |
| `{u}/verify/{payment_hash}` | LUD-21, when `lud21` is advertised |
`_` is [LUD-16][lud16]'s reserved bare-domain username and reaches the same identity as
the operator's configured username, so a consumer can probe without knowing it.
### `n` — the network
Same values and same meaning as NIP-87: `mainnet`, `testnet`, `signet` or `regtest`.
Absent reads as `mainnet`. Consumers SHOULD accept `bitcoin` as a synonym for
`mainnet`, because every Fedimint announcement in the wild writes it and publishers
copy each other.
### `features` — the capability list
One tag, one comma-separated list of lowercase tokens, the direct analogue of Cashu's
`nuts` and Fedimint's `modules`:
```json
["features", "mint,melt,rotate,split,merge,lud06,lud03,lud16,lud21,signed-notes"]
```
Consumers MUST tolerate whitespace after commas, MUST lowercase before comparing, and
MUST ignore tokens they do not recognise rather than rejecting the event — that is what
lets this vocabulary grow without a new kind.
#### The vocabulary
Every value below names something `lnurl-mint` actually implements. The "Grounded in"
column is the specific thing that makes the claim checkable.
**Note operations** — the four branches of the LUD-25 callback `GET /w/cb`, plus
minting. These are what the mint *does*.
| Value | Meaning | Grounded in |
| --- | --- | --- |
| `mint` | A new bearer note can be created by paying a Lightning invoice; the payment preimage becomes the note. | `router.get_pay_callback` (`/p/cb`). **Requires a funding source** — it issues the invoice. Also refused outright while `SUNSET_MINT` is on. |
| `melt` | A note can be redeemed back to a BOLT-11 payment. | `/w/cb` with a `pr` parameter. **Requires a funding source** — it pays the invoice. |
| `rotate` | A note's secret can be replaced by one the holder generates, without changing its value. | `/w/cb` with neither `pr` nor `amount`; the holder supplies `h = sha256(new secret)`. No funding source needed. |
| `split` | One note becomes two, of a chosen amount and the remainder. | `/w/cb` with `amount`; holder supplies `h` and `h2`. No funding source needed. Refused while `SUNSET_MINT` is on, since it grows outstanding liability. |
| `merge` | Several notes become one worth their sum. | `/w/cb` with multiple `k1`. No funding source needed. |
The `mint`/`melt` versus `rotate`/`split`/`merge` division is not cosmetic: it is
exactly the line a missing funding source falls along. Without one the service still
runs and the last three still work, while nothing moves in or out over Lightning. That
is a real, distinct state, and consumers should be able to render it — see
[Availability is not capability](#availability-is-not-capability).
**LNURL sub-specifications** — the wire formats spoken, so a wallet knows what to expect.
| Value | Meaning | Grounded in |
| --- | --- | --- |
| `lud06` | Serves a LUD-06 `payRequest` and its callback. | `router.get_lnaddress` returns `tag: "payRequest"`; `/p/cb` returns `pr`. |
| `lud03` | Serves a LUD-03 `withdrawRequest` per note, informational, never burning. | `router.get_withdraw` (`GET /w?k1=`). Advertised as `callback` on the mint-address response. |
| `lud16` | Payable at a lightning address, `{username}@{host}` and the bare-domain `_@{host}`. | `/.well-known/lnurlp/{username}`, with `text/identifier` in the payRequest metadata. |
| `lud21` | Serves `GET /verify/{payment_hash}` so a wallet with no node can poll settlement. | `router.verify_invoice`, gated on `VERIFY_ENABLED`. **Announcement-only** — see below. |
**Optional extras.**
| Value | Meaning | Grounded in |
| --- | --- | --- |
| `signed-notes` | The mint advertises a `mintPubkey` and returns recoverable signatures over each new note, so a holder can verify issuer and amount offline. | `signing.mint_pubkey` / `signing.sign_note`; `sig`/`sig2` on the `/w/cb` response. Requires a funding source (it signs via the node's `signmessage`). |
| `onion` | The mint also answers on a Tor hidden service. | `ONION_URL`; advertised in the one-pager's "Also via Tor" block, and used as the callback base when a request arrives over it. |
**Not in the vocabulary, deliberately:** anything about fees (they are already disclosed
in the payRequest `metadata` as `Mint fees: <base_msat>,<ppm>`, which is authoritative
and live), anything about the mint's balance or liability, and anything a consumer would
have to take on trust with no way to check.
#### Availability is not capability
`features` says what the mint **implements**. It does not say what is **working right
now**. A mint whose funding node is unreachable still implements `mint` and `melt`; it
just cannot perform them this minute.
Consumers that probe SHOULD present these as two different things. The concrete, checkable
signal, for `lnurl-mint`: if a mint-address response omits `mintPubkey` while still
returning valid `minWithdrawable`/`maxWithdrawable`, the funding source is not reachable,
and `mint`, `melt` and `signed-notes` are unavailable until it is — while `rotate`,
`split` and `merge` continue to work normally.
Consumers MUST NOT rewrite the announcement's `features` from a probe. The announcement
is the operator's statement of what they built; the probe is an observation about a
moment.
#### What a prober may and may not conclude
This matters enough to be normative, because two of these are counter-intuitive and one
is a trap.
| Value | Probe-detectable? | How |
| --- | --- | --- |
| `lud06`, `lud16` | **Yes** | `/.well-known/lnurlp/_` returns `tag: "payRequest"`; `payLink` on the withdraw side names the address. |
| `lud03` | **Yes** | The mint-address response's `callback` points at `/w`. |
| `signed-notes` | **Yes** | `mintPubkey` present on the mint-address response. |
| `onion` | **Yes** | A `*.onion` host in the one-pager at `GET /`. Never in any JSON. |
| `mint`, `melt` | **Partly** | Implementation cannot be probed without minting; *availability* is the `mintPubkey` bit above. |
| `rotate`, `split`, `merge` | **No** | Only `/w/cb` proves them, and calling it mutates or destroys a stranger's note. |
| `lud21` | **No — and this is the trap** | See below. |
**`lud21` cannot be probed.** `VERIFY_ENABLED=false` makes `/verify/{hash}` raise a 404
with detail `"Not found"`. An *enabled* endpoint asked about an unknown payment hash
raises a 404 with detail `"Not found"` too. Both are then converted by the LNURL error
handler into an identical `200 {"status":"ERROR","reason":"Not found"}`. The two states
are byte-identical on the wire — verified against a live instance and a local build of
both configurations. The only genuine advertisement of a `verify` URL is in the response
to `/p/cb`, and calling that **creates a real invoice on the operator's node**, which no
directory should be doing on a timer.
So `lud21` is announcement-only: a prober MUST NOT set it, and MUST NOT clear it either.
### `content`
Optional, and exactly NIP-87's rule: a stringified kind-0-style metadata object
(`name`, `picture`, `about`, …). **If `content` is empty, consumers should fall back to
the `kind:0` of the event's `pubkey`** for the mint's display information.
```json
{"name": "21 Mint", "picture": "https://lnurl.21mint.me/icon.png", "about": "lnurlcash bearer notes, mainnet."}
```
Consumers MUST treat `content` as untrusted text written by anyone: unparseable JSON,
wrong types and hostile strings degrade to "no metadata", never to an error and never to
unescaped output.
---
## Reviews
There is **no new review kind**. A review of an LNURL mint is a plain NIP-87
`kind:38000` whose `k` tag names `38174`:
```json
{
"kind": 38000,
"pubkey": "<reviewer-pubkey>",
"content": "[5/5] Rotations are instant and melts have never failed me.",
"tags": [
["k", "38174"],
["d", "021ab89df9cbd28cdab8c71d228ca40deba1eaebd827f24849ec4f3e919c023555"],
["u", "https://lnurl.21mint.me", "lnurl"],
["a", "38174:<announcer-pubkey>:021ab89df9cbd28cdab8c71d228ca40deba1eaebd827f24849ec4f3e919c023555", "wss://relay.cashumints.space"]
]
}
```
Reusing `38000` is the whole point of doing it this way. An existing NIP-87 client that
knows nothing about `38174` still sees a well-formed recommendation event from an author
it follows, still reads the rating and the text, and still knows it is *not* about Cashu
or Fedimint because the `k` tag says a kind it does not recognise. It half-understands
these reviews for free, and degrades to ignoring them rather than to misfiling them.
- **`k`** — REQUIRED, `"38174"`. This is what separates an LNURL review from a Cashu one,
and it is load-bearing: a Cashu mint's pubkey and an LNURL mint's `mintPubkey` are both
66-hex-character `d` values, so a resolver keying on `d` alone could file one as the
other.
- **`d`** — the announcement's identifier, per the `d` rules above. As NIP-87 says, this
can be computed from the mint's own pubkey even when no announcement event exists.
- **`u`** — OPTIONAL, the mint's https base URL. A third element MAY carry the free-form
marker `lnurl`, matching how NIP-87's own example marks `cashu` and `fedimint`.
- **`a`** — OPTIONAL, `38174:<announcer-pubkey>:<d>` plus a relay hint, pointing at the
announcement event itself.
### Rating
Ratings follow this site's existing convention (NOTES.md), unchanged across all three
ecosystems, because a reader comparing a Cashu mint to an LNURL one must be comparing
the same scale:
1. a `["rating", "<n>"]` tag with `n` in `1..5`, else
2. a `["rating", "<f>"]` tag with `f` in `0..1`, scaled to `1..5` (NIP-87 suggests this
form), else
3. an `[N/5]` prefix on `content`, which is how the great majority of real reviews on
the network encode it.
Unparseable means **no rating**, and the review is excluded from averages. It is never
defaulted to 5.
---
## Query patterns
Recommendations for LNURL mints from people a user follows:
```json
["REQ", "<id>", {"kinds": [38000], "authors": ["<user>", "<contacts…>"], "#k": ["38174"]}]
```
Every review of one specific mint:
```json
["REQ", "<id>", {"kinds": [38000], "#k": ["38174"], "#d": ["021ab8…3555"]}]
```
Announcements, directly:
```json
["REQ", "<id>", {"kinds": [38174]}]
["REQ", "<id>", {"kinds": [38174], "#d": ["021ab8…3555"]}]
```
All three ecosystems in one subscription, which is what an aggregator actually opens:
```json
["REQ", "<id>", {"kinds": [38172, 38173, 38174]}]
["REQ", "<id>", {"kinds": [38000], "#k": ["38172", "38173", "38174"]}]
```
As NIP-87 already warns for `38172`/`38173`: querying announcements directly, with no
web of trust between the reader and the publisher, will surface whatever anyone chose to
publish. Clients doing that SHOULD apply spam prevention or restrict to relays they
trust. Nothing about an announcement is evidence that the mint behind it is honest, or
even that it exists.
---
## Reference implementation
- **Constant and parse rules** — `shared/src/nostr.ts` (`KIND_LNURL_ANNOUNCEMENT`,
`ANNOUNCEMENT_KINDS`), `shared/src/lnurl.ts` (vocabulary, `d` rules, announcement
parser).
- **Discovery and review resolution** — `api/src/discovery.ts`.
- **Probing** — `api/src/lnurl-probe.ts`. What it found on a live instance, and every
ambiguity it had to resolve, is recorded in `NOTES-LNURL.md`.
- **Publishing 38174** — `api/src/announce.ts`. Off by default; see the README section
"Publishing LNURL mint announcements" before turning it on, because it writes to
public relays.
The implementation and this document are meant to be checked against each other:
`api/src/check-lnurl.ts` asserts the `d` rules, the vocabulary, the tag shapes and the
round trip described here, so a change to one that is not a change to the other fails
the test suite.
+361
View File
@@ -0,0 +1,361 @@
# Dynamic mint data: analysis
**Status:** analysis only. No code or config was changed.
**Goal:** new and updated mints, stats, and reviews should appear on the list (and related list surfaces) immediately, without waiting for the nightly static rebuild.
**Current architecture (important correction):** nginx does **not** serve `web/dist` as files. Production is:
```
browser → nginx :443 → cashumints-site (web/server.mjs :8789) → published copy of dist
→ cashumints_api (Hono :8788) → /api/*, /icons/*
```
`cashumints-web.timer` rebuilds at **03:30** daily, then `rsync`s `web/dist/` to `WEB_ROOT` (`/var/lib/cashumints/web`). Markup is a snapshot of whatever the API returned at that build.
---
## 1. Data flow audit
There are **no Astro content collections**. Every dynamic page either fetches the Hono API at **build time** (`web/src/lib/api.ts`, `API_URL`, default `http://127.0.0.1:8787` / prod `8788`) or hydrates in the browser from `/api/…` and/or Nostr.
Astro “islands” here are **not** React/Vue/Svelte with `client:load`. There is **zero** `client:*` usage. Runtime behaviour is vanilla `<script>` modules in `.astro` files, bundled by Vite, re-run on view transitions via `onReady()`.
### 1.1 Every route under `web/src/pages`
| Route | `getStaticPaths` | Build-time data | Client-side at runtime |
|---|---|---|---|
| `[...locale]/index.astro` (`/`, `/es`, …) | `localePaths` (one HTML page per locale) | `fetchMints`, `fetchFedimints`, `fetchLnurlMints`, `fetchStats`, `fetchHealth`; N+1 `fetchMint` / `fetchFedimint` / `fetchLnurlMint` for the top 6 of each; `fetchLatestReviews()` from relays via `nostr-build.ts` | **PulseTicker** refreshes `/api/stats` + `/api/health`. Search form is GET to `/mints`. Review carousel is **not** re-queried. ColorBends is WebGL only. |
| `[...locale]/mints.astro` | `localePaths` | `fetchMints()` then **one `fetchMint(host)` per mint** for NUT chips | Search / sort / hide-offline only rearrange **already-rendered** cards. **No live refetch.** |
| `[...locale]/fedimints.astro` | `localePaths` | `fetchFedimints()` (no N+1; no NUT chips) | Same client filter/sort as `/mints`. **No live refetch.** |
| `[...locale]/lnurl-mints.astro` | `localePaths` | `fetchLnurlMints()` + N+1 `fetchLnurlMint` for chips | Same. **No live refetch.** |
| `[...locale]/mint/[host].astro` | `localePathsFor` over **every cashu mint known at build** | `fetchMints()` + `fetchMint(host)` per mint, shared across locales | **MintLive** → `GET /api/mints/:host`. **Reviews** → Nostr via `nostr-tools` `SimplePool`. |
| `[...locale]/fedimint/[id].astro` | same pattern, `fetchFedimints` | federation detail | Same two islands. |
| `[...locale]/lnurl-mint/[host].astro` | same pattern, `fetchLnurlMints` | LNURL detail | Same two islands. |
| `[...locale]/reviews.astro` | `localePaths` | `fetchAllListings()`, `fetchStats()`, `fetchReviewFeed(…, 30)` from relays | **Re-queries relays** (`feed-client.ts` `loadFeed`), paginates, filters. This is the working “live list” pattern. |
| `[...locale]/wallets.astro` | `localePaths` | Hardcoded wallet array in the page | None |
| `[...locale]/about.astro` | `localePaths` | `fetchStats()` for counts in copy | None |
| `[...locale]/privacy.astro`, `terms.astro`, `disclaimer.astro` | `localePaths` | i18n catalogs only | None |
| `[...locale]/404.astro` + `pages/404.astro` | prefixed locales / English `/404` | None (static 404 shell) | **NotFound resolver:** `GET /api/mints/:host`, on miss `POST /api/index`, then reviews panel |
| `sitemap.xml.ts` | n/a (endpoint at build) | `fetchMints` / `fetchFedimints` / `fetchLnurlMints`; `isIndexableMint` filter | n/a |
| `robots.txt.ts` | n/a | `SITE_URL` only; `Disallow: /api/`, `/icons/` | n/a |
24 locales (`en` plus 23 prefixes). List pages are ~24 HTML files each. Mint detail pages are `mints × locales` (README ballpark: ~55 cashu mints × 24 ≈ 1,300+ pages, plus federations and LNURL).
### 1.2 Why `/mints` shows only a subset, with stale stats and reviews
**Not pagination.** `fetchMints()` calls `GET /api/mints?type=cashu` with **no `limit`**. `listMints()` in the API returns every matching row, sorted by score. The optional `?limit=` query is unused by the site.
**Not an `isIndexableMint` filter on the grid.** Unreachable, unreviewed mints still render as cards. That predicate only drops them from JSON-LD `ItemList` and the sitemap.
**The list is a frozen HTML snapshot.** `mints.astro` does:
```ts
const mints = await fetchMints();
const capabilities = await Promise.all(
mints.map((mint) => fetchMint(mint.host).then(…).catch(() => null)),
);
```
then maps every item to `<MintCard>`. After publish, those cards do not change until the next `cashumints-web.timer` run.
What that freezes:
| Card field | Source at build | Live counterpart |
|---|---|---|
| Presence of the mint | API `mints` table at 03:30 | Discovery + `POST /api/index` write new rows immediately |
| `review_count`, `rating_avg`, `score`, `last_review_at` | SQL aggregates of **ingested** Nostr reviews | Relays (and the API, once discovery has ingested) |
| `status`, `last_online` | last probe stored in the DB | `GET /api/mints/:host` (what MintLive already uses) |
| Melt-only / frozen chip | N+1 detail fetch of cached `/v1/info` | same detail payload |
**Why a mint can exist “on the site” but not on the list:** the 404 resolver (`NotFound.astro`) looks up `/mint/{host}` against the **live** API and, if missing, calls `POST /api/index`. That mint becomes reviewable at once. It does **not** get a card on `/mints` until rebuild. Same for Review-by-URL. The README states this as intended:
> Mint pages are prerendered, so new mints and new review counts appear at the next build. … an unbuilt mint still resolves through the client-side fallback on the 404 page.
**Why reviews look wrong on the list and right on the detail page:** `/mints` never talks to Nostr. It prints `MintListItem.review_count` / `rating_avg` from the API snapshot. The detail page’s `Reviews.astro` calls `loadReviews(subject)` against relays and replaces the panel. A review written today is on the detail page as soon as relays answer; the list card still shows yesterday’s count.
**Home page extra subset:** `mints.slice(0, 6)` (and the same for federations / LNURL). “Latest reviews” is a **build-time** relay read (`fetchLatestReviews(…, 10)`), unlike `/reviews`, which re-queries on load.
### 1.3 How the mint detail page fetches Nostr (the pattern that works)
**Component:** `web/src/components/Reviews.astro`
Root: `[data-reviews-panel]` with `data-subject={JSON.stringify(subject)}`.
Script imports `loadReviews` / `loadProfiles` from `web/src/lib/reviews-client.ts`.
**Library:** [`nostr-tools`](https://github.com/nbd-wtf/nostr-tools) **v2.10.4** — `SimplePool` from `nostr-tools/pool`, plus `nip19`. **Not NDK.**
**Relays** (`DEFAULT_RELAYS` in `shared/src/nostr.ts`, overridable with `PUBLIC_REVIEW_RELAYS`):
- `wss://relay.cashumints.space`
- `wss://nos.lol`
- `wss://relay.azzamo.net`
- `wss://relay.snort.social`
- `wss://relay.primal.net`
Profiles add `wss://purplepag.es` and `wss://relay.nostr.net`.
**Query:** kind `38000` (addressable reviews), two OR’d filters, `limit: 500`, `maxWait: 8000` ms:
1. `#d` = mint pubkey (plus LNURL alternate ids) and `#k` = announcement kind
2. `#u` = URL spellings (legacy Cashu reviews; federations skip this)
Newest event per author wins. In-tab cache 5 minutes. Empty result is treated as a relay error if the API had counted reviews.
**Live status (API, not Nostr):** `MintLive.astro` is a hidden `[data-mint-live={host}]` marker. Its script `fetch(`${apiBase}/api/mints/${host}`)` and patches the status chip, banner, limits, and NUT/module/feature rows. `apiBase` is `import.meta.env.PUBLIC_API_URL`, empty in production → same-origin `/api/…`.
**Unbuilt mint URLs:** `server.mjs` serves the locale 404. `NotFound.astro` derives the address from the path, `GET /api/mints/:host`, then `POST /api/index` on miss, then dispatches `cashumints:subject` so the same Reviews island starts.
---
## 2. API coverage
All routes are in `api/src/server.ts`. CORS is enabled on `/api/*` and `/icons/*` (`hono/cors`, default allow-all). Production uses **same origin** (`PUBLIC_API_URL=` empty; nginx proxies `/api/` and `/icons/`), so browsers never hit a cross-origin API unless that env is set.
### 2.1 Existing endpoints
#### `GET /api/mints`
- Query: `?type=cashu|fedimint|lnurl` (unknown type → empty array); optional `?limit=N`
- No type filter → **entire index**, each item tagged `type`
- **No default limit.** Full list.
- Response: `MintListItem[]`
```ts
{
url: string;
host: string;
name: string | null;
icon: string | null; // "/icons/…" or null
type: 'cashu' | 'fedimint' | 'lnurl' | string;
status: 'online' | 'degraded' | 'offline' | 'unknown' | 'announced';
last_online: number | null; // unix
review_count: number; // ingested, one-per-author
rating_avg: number | null; // 1 decimal
score: number; // Bayesian, from those aggregates
last_review_at: number | null;
version: string | null;
}
```
**This is enough to rebuild the `/mints` grid** (name, icon, rating, count, status, sort keys). It is **not** enough for melt-only / frozen chips (`nuts` / capabilities) or for true sentiment bars (`rating_distribution`). Those currently require N+1 `GET /api/mints/:host`.
#### `GET /api/mints/:host`
- 404 `{ error, message }` if unknown
- Response: `MintDetail` = list item plus:
```ts
{
description, pubkey, info /* NUT-06 */, nuts: string[],
first_seen, last_probe, updated_at,
rating_distribution: { '1'..'5': number },
reviews_90d, uptime_30d, probes_recent: { ts, ok, latency_ms }[],
// fedimint extras when type=fedimint: federation_id, invite_codes, modules, …
// lnurl extras when type=lnurl: features, funding_available, withdraw bounds, …
}
```
Used by MintLive and the 404 resolver. **No review bodies.**
#### `GET /api/stats`
In-process memo **60s**. Cashu `mints_*` counts are cashu-only.
```ts
{
mints_total, mints_online, mints_offline, mints_degraded,
reviews_total, last_review_at, updated_at,
cashu_total, // same as mints_total
fedimint_total, fedimint_online, fedimint_offline, fedimint_announced, fedimint_reviews,
lnurl_total, lnurl_online, lnurl_offline, lnurl_degraded_funding, lnurl_reviews
}
```
PulseTicker already consumes this live.
#### `GET /api/health`
`Cache-Control: no-store`. `{ status, uptime_s, last_probe_at, last_discovery_at, mints_tracked, updated_at }`. 503 when degraded.
#### `POST /api/index`
The only write. Body `{ type, input }`. Rate limit **10/hour/IP** (`INDEX_RATE_LIMIT`). Success 200/201: `MintDetail & { existing, indexed_from? }`. Failures: `bad_input`, `blocked_host`, `wrong_type`, `unverifiable`, `rate_limited`, etc.
#### `GET /icons/:file`
Static files, `Cache-Control: public, max-age=86400`.
### 2.2 Gaps
| Need | Exists? | Notes |
|---|---|---|
| Full mint / federation / LNURL lists | **Yes** | `/api/mints?type=…` |
| Per-mint stats (status, uptime, distribution, probes) | **Yes** | `/api/mints/:host` |
| Review **bodies** / recent review text | **No HTTP endpoint** | Live only via Nostr (`reviews-client.ts`, `feed-client.ts`). API stores ratings for aggregates, not content for the UI. |
| List payload with NUT chips / LNURL chips | **No** | List item has no `nuts`, no `rating_distribution`. Today: N+1 at **build**. Client-side N+1 per visitor would be wasteful. |
| Global review feed JSON | **No** | `/reviews` talks to relays, not the API. |
| Snapshot JSON in `dist` | **None** | No checked-in mint dump. Stale data **is** the prerendered HTML. |
Discovery itself is not the list cap (`QUERY_LIMIT` 500 × `MAX_PAGES` 20). The visible cap is **whatever was in the DB when `astro build` ran**.
---
## 3. Option analysis
### 3a. Client-side hydration (keep static build)
Keep `output: 'static'`, `server.mjs`, nginx, and the timer. On `/mints` (and twins), fetch the live API after load and rebuild the grid — the same idea as Reviews / PulseTicker / MintLive.
**What already exists to copy**
- PulseTicker: prerender numbers, then `fetch(`${apiBase}/api/stats`)`.
- `/reviews`: prerender ~30 cards, then `loadFeed()` replaces them.
- MintLive: patch in place from `/api/mints/:host`.
- `/mints` script already sorts/filters on `data-*` (`data-score`, `data-rating`, `data-reviews`, `data-last-review`, `data-status`, …). New cards only need those attributes.
**There are no `client:load` islands to add.** Work is extra `<script>` in the list pages (or a shared module) plus an HTML builder for a card, because `MintCard.astro` cannot run in the browser.
Concrete changes:
| File | Change |
|---|---|
| `web/src/pages/[...locale]/mints.astro` | After paint, `fetch('/api/mints?type=cashu')`, rebuild `[data-mint-grid]`, update `[data-result-count]`. Keep prerendered cards as noscript / first-paint fallback. |
| `web/src/pages/[...locale]/fedimints.astro` | Same, `?type=fedimint`. |
| `web/src/pages/[...locale]/lnurl-mints.astro` | Same, `?type=lnurl`. |
| `web/src/lib/mint-cards.ts` (**new**) | Browser HTML for one card, mirroring `MintCard.astro` (same pattern as `review-cards.ts`). |
| `web/src/lib/api.ts` | Optional: a tiny browser `fetch` helper using `apiBase` from `client.ts` (build-time `API_URL` must **not** ship to the browser). |
| `web/src/pages/[...locale]/index.astro` | Optional but needed for “immediately”: refresh top-6 grids from the same list endpoint; optionally reuse `/reviews`’s `loadFeed` for the carousel (today that strip is build-only). |
| `web/src/components/MintCard.astro` | Untouched if the JS builder is a parallel renderer; or extract shared markup helpers. |
**Chips:** either (i) leave prerendered chips stale, (ii) add `nuts` / chip flags to `MintListItem` + `toListItem()` in `api/src/queries.ts` (small, one-time API change), or (iii) N+1 detail fetches from every browser (do not do this).
**New mint click path:** hydrated list → `/mint/{newhost}` → 404 HTML → NotFound resolver → live page. Already designed. No SSR required for that URL to work.
**Dependencies:** none new (`nostr-tools` already in `web`). No adapter. No nginx / systemd change for the site server.
**Config:** none if `PUBLIC_API_URL` stays empty.
### 3b. Hybrid SSR (`@astrojs/node`, `prerender = false`)
Astro 5 dropped `output: 'hybrid'`. Pattern: install an adapter, keep `output: 'static'`, set `export const prerender = false` on routes that must run per request.
Concrete changes:
| File / unit | Change |
|---|---|
| `web/package.json` | Add `@astrojs/node`. `start` would become something like `node dist/server/entry.mjs` instead of `node server.mjs`. |
| `web/astro.config.mjs` | `import node from '@astrojs/node'`; `adapter: node({ mode: 'standalone' })`. `output` can stay `'static'`. |
| `web/src/pages/[...locale]/mints.astro` (and fedimints / lnurl-mints; maybe `index.astro`) | `export const prerender = false`. Drop `getStaticPaths` **or** keep it only if those routes stay static. SSR pages fetch `fetchMints()` **per request**. |
| `web/server.mjs` | **Cannot stay as the only server.** Node adapter emits `dist/server/entry.mjs` plus `dist/client/` for assets. Locale 404s, ETag/cache policy, path jail, and “refuse empty root” all live in `server.mjs` today and would need reimplementation or a wrapper. |
| `cashumints-site.service` | `ExecStart` → adapter entry; `WEB_ROOT` layout changes (`client/` vs flat dist). |
| `cashumints-web.service` | `rsync` of `web/dist/` is wrong for `client/` + `server/`. Publish + restart model changes. A failed SSR process takes down **HTML**, not only islands. |
| nginx | Still reverse-proxy; upstream may stay `:8789` if the adapter listens there. `proxy_read_timeout` 30s is fine for API-backed SSR. |
SSR **does not** by itself refresh review **bodies** on the list (there are none). It **does** make list HTML match the API on every request, including for crawlers, and can emit real 200s for new `/mint/{host}` if that route is also `prerender = false` (today new hosts are **404** until rebuild).
Per-request cost: `/mints` already does N+1 detail fetches for chips × 24 locales if you SSR all locales naively. Needs a cache or list-payload chips.
### 3c. Caching layers
**In-repo**
| Layer | TTL | Affects |
|---|---|---|
| `getStats()` memo (`api/src/queries.ts`) | 60s | `/api/stats` |
| Review / feed `SimplePool` caches in the tab | 5 min | detail reviews, `/reviews` |
| Icons | 1 day (`max-age=86400`) | `/icons/*` |
| `GET /api/mints`, `/api/stats`, `/api/mints/:host` | **no** `Cache-Control` from Hono (except health `no-store`) | nginx fills the gap |
**nginx** (`README.md` sample; not in this repo)
```nginx
proxy_cache_path … keys_zone=cashumints:10m max_size=256m inactive=10m;
location ~ ^/api/(mints|stats)(?:/|$|\?) {
proxy_cache cashumints;
proxy_cache_valid 200 30s;
proxy_cache_valid 404 10s;
proxy_cache_use_stale updating error timeout http_500 http_502 http_503;
proxy_cache_background_update on;
proxy_cache_lock on;
}
location /icons/ { proxy_cache …; proxy_cache_valid 200 1d; }
location = /api/health { proxy_cache off; }
```
**HTML** from `server.mjs`: `Cache-Control: public, max-age=0, must-revalidate` + ETag. Browsers revalidate; they do not keep a 24h stale `/mints`.
**Implications**
- Client-side list fetch: **30s** nginx micro-cache is desirable (thundering herd). Not the 24h bug.
- After `POST /api/index`, a new mint can be missing from `GET /api/mints` for up to 30s (404 cache 10s on unknown host). Acceptable; optional `Cache-Control: s-maxage=30` on the API to make it explicit.
- SSR `/mints` would still sit behind that 30s API cache unless the SSR process talks to the API on loopback **bypassing nginx** (`API_URL=http://127.0.0.1:8788`), which the **build** already does. An SSR Node process on the same machine should keep using loopback, not `https://cashumints.space/api`.
- No purge/invalidation API exists. Not needed for 30s TTL.
**Rate limits:** only `POST /api/index`. `GET /api/mints` is unbounded. A list page per visitor is one small JSON payload (~tens of mints), not a risk.
---
## 4. Recommendation
**Do 3a (client-side hydration of the list from `GET /api/mints`).** Least infrastructure change that fixes: full live mint list, live card stats, live review **counts** on the list.
Do **not** move `/mints` to SSR for this goal. That replaces `server.mjs`, the publish path, and systemd, to solve a problem the API + existing island style already solve. Use SSR later only if you need **200 + HTML** for mint URLs that do not exist at build time (SEO for brand-new hosts). Those URLs already **work** for humans via the 404 resolver.
### Why 3a is enough
1. The API already returns the full cashu list with scores and review aggregates.
2. Same-origin `/api` is already proxied; PulseTicker proves browser → API works in production.
3. New mints already resolve on click (404 → `POST /api/index`). Hydrating the list is the missing half.
4. Review **text** on `/mints` was never a list feature; card counts come from the API. Once the list refetches, counts match what MintLive/stats use (discovery-ingested). Bodies stay on the detail page via Nostr, as now.
5. `/reviews` already has live bodies.
### Effort
**Small–medium, ~1–2 days** for `/mints` + fedimints + lnurl-mints + a shared card renderer. Another half day if the home top grids and “latest reviews” strip should match.
Optional extra (half day): add chip fields to `GET /api/mints` so the client does not N+1.
### Files that would be touched (3a)
**Required for the list pages**
- `web/src/pages/[...locale]/mints.astro`
- `web/src/pages/[...locale]/fedimints.astro`
- `web/src/pages/[...locale]/lnurl-mints.astro`
- `web/src/lib/mint-cards.ts` (new; browser card HTML + `fetch` against `apiBase`)
**Likely**
- `web/src/pages/[...locale]/index.astro` (top-6 + “all N mints” count; otherwise home still lies)
- `web/src/components/MintCard.astro` only if extracting shared helpers rather than duplicating markup
**Optional API (chips)**
- `shared/src/types.ts` (`MintListItem`)
- `api/src/queries.ts` (`toListItem`)
**Not required:** `astro.config.mjs`, `web/package.json` (unless you add a test), `web/server.mjs`, nginx, systemd units, `@astrojs/node`.
### Risks
| Risk | Severity | Mitigation |
|---|---|---|
| **SEO:** crawlers see the prerendered subset | Medium if you **replace** HTML with an empty grid. Low if you **keep** build-time cards and enhance. Google does not run the same JS as users. | Keep prerendered cards. Hydration only adds/updates. `ItemList` JSON-LD stays a snapshot; acceptable. |
| **New mint URLs return HTTP 404** until rebuild | Low for UX (resolver fills in). Medium for SEO of a mint indexed today. | Out of scope for list freshness. SSR on `mint/[host].astro` is the fix if needed. |
| **Nostr relay latency** | Does not apply to the **list** if you use the API. Applies to `/reviews` and detail panels already (8–9s timeout, bones, retry). | Do not query relays on `/mints`. |
| **API rate limits** | None on GET. | — |
| **CORS** | None while `PUBLIC_API_URL` is empty (same origin). Hono already sends CORS if the API is split later. | Do not bake `API_URL` (`127.0.0.1`) into browser code. |
| **nginx 30s cache** | Cards can lag indexing by 30s | Fine. Loopback SSR would skip it; client fetch will not. |
| **Card markup drift** | JS builder vs `MintCard.astro` | One HTML helper, or accept a short duplication like `review-cards.ts` vs the Astro card. |
| **View transitions** | Cards use `data-vt-*` names applied on click | Preserve those attributes in the builder. |
| **i18n** | Card strings must use `useI18n()` in the island, not build-time `t` | Same as Reviews / PulseTicker. |
| **Home “latest reviews”** | Still nightly unless you reuse `loadFeed` | Separate follow-up; `/reviews` is already live. |
### Suggested sequence (when implementing)
1. Hydrate `/mints` from `GET /api/mints?type=cashu`; keep prerendered fallback.
2. Repeat for `/fedimints` and `/lnurl-mints`.
3. Hydrate the home top grids (and the “all N” link) from the same endpoints.
4. Only if chips go stale in a way users notice: extend the list payload.
5. Revisit SSR only for mint-detail **status codes** / OG / sitemap freshness — not for the list.
+3 -2
View File
@@ -4,7 +4,7 @@
"version": "2.0.0", "version": "2.0.0",
"type": "module", "type": "module",
"engines": { "engines": {
"node": ">=20" "node": ">=20.18"
}, },
"scripts": { "scripts": {
"dev": "pnpm --parallel --filter ./api --filter ./web dev", "dev": "pnpm --parallel --filter ./api --filter ./web dev",
@@ -12,7 +12,8 @@
"dev:web": "pnpm --filter ./web dev", "dev:web": "pnpm --filter ./web dev",
"seed": "pnpm --filter ./api seed", "seed": "pnpm --filter ./api seed",
"bones": "pnpm --filter ./web bones", "bones": "pnpm --filter ./web bones",
"build": "pnpm --filter ./shared build && pnpm --filter ./web build", "build": "pnpm --filter ./shared build && pnpm --filter ./api build && pnpm --filter ./web build",
"start": "pnpm --filter ./web start",
"check:links": "pnpm --filter ./web check:links", "check:links": "pnpm --filter ./web check:links",
"typecheck": "pnpm -r typecheck", "typecheck": "pnpm -r typecheck",
"check:i18n": "pnpm --filter ./web check:i18n", "check:i18n": "pnpm --filter ./web check:i18n",
+198
View File
@@ -300,3 +300,201 @@ export function shortInviteCode(code: string, head = 14, tail = 8): string {
if (code.length <= head + tail + 1) return code; if (code.length <= head + tail + 1) return code;
return `${code.slice(0, head)}…${code.slice(-tail)}`; return `${code.slice(0, head)}…${code.slice(-tail)}`;
} }
/* ---------- decoding an invite code ---------- */
/**
* Decoding one, which until now nothing in this codebase did.
*
* `isInviteCode` above is a shape check, and a shape check is all the *display* side
* has ever needed: a code arrives inside an announcement that already carries the
* federation id in its `d` tag, and the code itself is handed to a wallet verbatim.
*
* On-demand indexing changes that. A reader pasting an invite code into the review
* dialog hands over the only thing they have, and there is no announcement beside it
* to read an id off — so the id has to come out of the code, or the federation cannot
* be keyed, deduped against what is already indexed, or given a page.
*
* Two layers, both small and both self-contained (a bech32 dependency for one function
* would be the tail wagging the dog):
*
* 1. **bech32m**, per BIP-350: the same alphabet and checksum as bech32 with a
* different constant. Fedimint uses bech32m, so a code that verifies under the
* *bech32* constant is rejected rather than accepted — it would mean the code was
* produced by something else.
* 2. **fedimint's consensus encoding** of `Vec<InviteCodePart>`: a BigSize count, then
* per part a BigSize tag, a BigSize length and that many bytes. Tag 1 is the
* federation id, 32 bytes. Everything else — guardian API URLs (tag 0), an API
* secret (tag 2), anything a later fedimint adds — is skipped by its length
* without being understood, which is what the tag/length framing is for.
*
* Written against real codes off the relay pool, not against a reading of the Rust, and
* `check-index.ts` asserts it still decodes them.
*/
/** bech32's alphabet, and its two checksum constants. `1` is deliberately not in it. */
const BECH32_CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
const BECH32M_CONST = 0x2bc830a3;
const GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
function bech32Polymod(values: readonly number[]): number {
let chk = 1;
for (const value of values) {
const top = chk >> 25;
chk = ((chk & 0x1ffffff) << 5) ^ value;
for (let i = 0; i < 5; i++) if ((top >> i) & 1) chk ^= GENERATOR[i]!;
}
return chk >>> 0;
}
function hrpExpand(hrp: string): number[] {
const out: number[] = [];
for (const char of hrp) out.push(char.charCodeAt(0) >> 5);
out.push(0);
for (const char of hrp) out.push(char.charCodeAt(0) & 31);
return out;
}
/**
* The payload bytes of a bech32m string, or null if it is not a valid one.
*
* Length capped well above any real invite code: the checksum is only meaningful over
* a string somebody could plausibly have produced, and an unbounded input here would
* be an unbounded loop below.
*/
function decodeBech32m(input: string): { hrp: string; bytes: Uint8Array } | null {
if (input.length < 8 || input.length > 4000) return null;
// Mixed case is invalid in bech32; one case throughout is not.
if (input !== input.toLowerCase() && input !== input.toUpperCase()) return null;
const value = input.toLowerCase();
const split = value.lastIndexOf('1');
if (split < 1 || split + 7 > value.length) return null;
const hrp = value.slice(0, split);
for (const char of hrp) {
const code = char.charCodeAt(0);
if (code < 33 || code > 126) return null;
}
const data: number[] = [];
for (const char of value.slice(split + 1)) {
const index = BECH32_CHARSET.indexOf(char);
if (index === -1) return null;
data.push(index);
}
if (bech32Polymod([...hrpExpand(hrp), ...data]) !== BECH32M_CONST) return null;
// Five-bit groups to eight, dropping the checksum and the final partial group.
const payload = data.slice(0, -6);
const bytes: number[] = [];
let acc = 0;
let bits = 0;
for (const group of payload) {
acc = (acc << 5) | group;
bits += 5;
while (bits >= 8) {
bits -= 8;
bytes.push((acc >> bits) & 0xff);
}
}
// Leftover bits must be zero padding, and there must be fewer than five of them.
if (bits >= 5 || ((acc << (8 - bits)) & 0xff) !== 0) return null;
return { hrp, bytes: Uint8Array.from(bytes) };
}
/** A cursor over the decoded bytes, reading fedimint's BigSize integers and blobs. */
class ByteReader {
private at = 0;
constructor(private readonly bytes: Uint8Array) {}
get done(): boolean {
return this.at >= this.bytes.length;
}
/**
* Lightning's BigSize, which is what fedimint encodes a `u64` as: one byte under
* 0xfd, otherwise a marker and 2, 4 or 8 big-endian bytes.
*
* Returns null rather than throwing when the buffer runs out, so a truncated code is
* a rejected code and not an exception a caller has to catch.
*/
bigSize(): number | null {
const first = this.byte();
if (first === null) return null;
if (first < 0xfd) return first;
const width = first === 0xfd ? 2 : first === 0xfe ? 4 : 8;
let value = 0;
for (let i = 0; i < width; i++) {
const next = this.byte();
if (next === null) return null;
// Above 2^53 nothing here is a real length or tag anyway, and the arithmetic
// stops being exact, so an absurd value is refused rather than rounded.
value = value * 256 + next;
if (value > Number.MAX_SAFE_INTEGER) return null;
}
return value;
}
bytesOf(length: number): Uint8Array | null {
if (length < 0 || this.at + length > this.bytes.length) return null;
const slice = this.bytes.subarray(this.at, this.at + length);
this.at += length;
return slice;
}
private byte(): number | null {
return this.at < this.bytes.length ? this.bytes[this.at++]! : null;
}
}
/** The tag fedimint gives the federation id inside an invite code. */
const INVITE_PART_FEDERATION_ID = 1;
/** A federation id is a 32 byte hash. A part of any other length is not one. */
const FEDERATION_ID_BYTES = 32;
/** Real codes carry two or three parts. This only has to stop a hostile count. */
const MAX_INVITE_PARTS = 64;
function toHex(bytes: Uint8Array): string {
let out = '';
for (const byte of bytes) out += byte.toString(16).padStart(2, '0');
return out;
}
/**
* The federation id inside an invite code, or null if the code is not one.
*
* Null covers every way a pasted string can fail — wrong prefix, a typo the checksum
* catches, valid bech32m that is not an invite code, an invite code with no federation
* id part — because a caller has exactly one thing to say about all of them ("that is
* not an invite code") and telling them apart would be telling a stranger which of
* their guesses was closest.
*/
export function federationIdFromInviteCode(code: string): string | null {
const raw = code?.trim();
if (!raw || !/^fed1[a-z0-9]+$/i.test(raw)) return null;
const decoded = decodeBech32m(raw);
// `fed1` is the human-readable part; the `1` after it is bech32's separator.
if (!decoded || decoded.hrp !== 'fed1') return null;
const reader = new ByteReader(decoded.bytes);
const parts = reader.bigSize();
if (parts === null || parts === 0 || parts > MAX_INVITE_PARTS) return null;
for (let i = 0; i < parts; i++) {
const tag = reader.bigSize();
const length = tag === null ? null : reader.bigSize();
const value = length === null ? null : reader.bytesOf(length);
if (value === null) return null;
if (tag === INVITE_PART_FEDERATION_ID && value.length === FEDERATION_ID_BYTES) {
return toHex(value);
}
}
return null;
}
+2
View File
@@ -5,3 +5,5 @@ export * from './score.js';
export * from './nuts.js'; export * from './nuts.js';
export * from './warnings.js'; export * from './warnings.js';
export * from './fedimint.js'; export * from './fedimint.js';
export * from './lnurl.js';
export * from './indexing.js';
+208
View File
@@ -0,0 +1,208 @@
/**
* On-demand indexing: the contract `POST /api/index` speaks.
*
* Both ends of that request are in this repository — the API answers it, the 404
* resolver and the review-by-URL dialog send it — so the shapes live here rather than
* being written out twice and drifting. The reason codes in particular are the whole
* point of this file: the API decides *what happened*, the browser decides *what to
* say about it*, and a typo in a string literal must not be the thing that silently
* turns a precise sentence into a generic one.
*
* The endpoint itself is documented in BACKEND.md. What is here is only what both
* sides need to agree on: which types are indexable, what a valid submission looks
* like before any network call, and what comes back.
*/
import { federationIdFromInviteCode, isInviteCode } from './fedimint.js';
import { normalizeMintUrl } from './normalize.js';
import type { MintDetail, MintInfo } from './types.js';
/** The three ecosystems a reader can hand this site an identifier for. */
export const INDEXABLE_TYPES = ['cashu', 'fedimint', 'lnurl'] as const;
export type IndexType = (typeof INDEXABLE_TYPES)[number];
export function isIndexType(value: unknown): value is IndexType {
return typeof value === 'string' && (INDEXABLE_TYPES as readonly string[]).includes(value);
}
/**
* Why a submission did not become a row.
*
* Every one of these is a different sentence to a reader, which is why they are not
* collapsed into a generic failure:
*
* `bad_type` the `type` field was not one of the three
* `bad_input` the input is not a URL (or an invite code) at all
* `blocked_host` it resolves somewhere this server will not fetch from
* `wrong_type` the host answered, as a *different* ecosystem's mint
* `invalid_response` the host answered, as nothing this site recognises
* `invalid_invite` the invite code does not decode
* `unverifiable` nothing answered, and Nostr has never heard of it either
* `rate_limited` too many submissions from one address this hour
*/
export type IndexFailureReason =
| 'bad_type'
| 'bad_input'
| 'blocked_host'
| 'wrong_type'
| 'invalid_response'
| 'invalid_invite'
| 'unverifiable'
| 'rate_limited';
/** How a row that did not already exist came to be written. */
export type IndexSource = 'probe' | 'announcement' | 'invite';
export interface IndexFailure {
error: IndexFailureReason;
/** English, for a log or a `curl`. The browser renders its own translated copy. */
message: string;
/**
* The ecosystem this address *does* look like, when the answer said so.
*
* Only ever set beside `wrong_type`, and it is what lets the dialog offer "this looks
* like a Cashu mint, review it there instead" with a button rather than making the
* reader work out which page they wanted.
*/
detected_type?: IndexType;
/** Seconds until the next submission is accepted. Only beside `rate_limited`. */
retry_after?: number;
}
/** 200 or 201: the same payload `GET /api/mints/:host` returns, plus how it got there. */
export type IndexSuccess = MintDetail & {
/** True when the identifier was already indexed and nothing was probed. */
existing: boolean;
/** Absent on an `existing` hit: nothing was written, so nothing wrote it. */
indexed_from?: IndexSource;
};
export type IndexResponse = IndexSuccess | IndexFailure;
export function isIndexFailure(body: IndexResponse): body is IndexFailure {
return typeof (body as IndexFailure).error === 'string';
}
/**
* The client-side pre-check, run before any network call.
*
* Deliberately shallow: it answers "could this possibly be an address of this kind?"
* and nothing more. Whether the mint exists, answers, or is what it claims is the
* server's question, and asking the browser to guess would only produce a second
* opinion to disagree with. What it does catch is the common typo — an empty box, a
* sentence, a `fed1…` pasted into the Cashu field — before a request goes out.
*
* Returns the value to submit, which is the input with the scheme the normalizer would
* add, so the dialog can show the reader what it is about to check.
*/
export function checkIndexInput(
type: IndexType,
input: string,
): { ok: true; value: string } | { ok: false; reason: 'empty' | 'bad_url' | 'bad_invite' } {
const raw = input.trim();
if (!raw) return { ok: false, reason: 'empty' };
if (type === 'fedimint') {
return isInviteCode(raw) && federationIdFromInviteCode(raw) !== null
? { ok: true, value: raw.toLowerCase() }
: { ok: false, reason: 'bad_invite' };
}
// A pasted invite code in a URL field is a wrong-field mistake, not a malformed URL,
// and saying "that is an invite code" is more use than "that is not a URL".
if (isInviteCode(raw)) return { ok: false, reason: 'bad_invite' };
const normalized = normalizeMintUrl(raw);
return normalized ? { ok: true, value: normalized.url } : { ok: false, reason: 'bad_url' };
}
/**
* Is this body a NUT-06 mint info document?
*
* The probe needs a test that a Cashu mint passes and an arbitrary JSON endpoint fails,
* because `/v1/info` on a host that is not a mint is very often a 200 with *something*
* on it — an API index, a health check, a framework's error object. NUT-06 makes every
* field optional, so the test is "does it carry any of the things only a mint has":
* a `nuts` object, or a mint pubkey, or the name/version pair a mint's info always has.
*
* Kept here rather than in the prober because the wrong-type detection on the LNURL
* path runs the same test against the same document, and two spellings of "is this a
* Cashu mint" is exactly how a submission ends up filed under both ecosystems.
*/
export function isNut06Info(body: unknown): body is MintInfo {
if (!body || typeof body !== 'object' || Array.isArray(body)) return false;
const info = body as Record<string, unknown>;
const nuts = info['nuts'];
if (nuts && typeof nuts === 'object' && !Array.isArray(nuts) && Object.keys(nuts).length > 0) {
return true;
}
// A mint pubkey is 33 compressed bytes, exactly as an LNURL mint's is.
if (typeof info['pubkey'] === 'string' && /^0[23][0-9a-f]{64}$/i.test(info['pubkey'])) {
return true;
}
return typeof info['name'] === 'string' && typeof info['version'] === 'string';
}
/**
* The route a page for this listing lives at, given its type and routing slug.
*
* One table, because three pages, the 404 resolver, the review dialog and the sitemap
* all have to agree on it, and the failure mode of disagreeing is a link to a page that
* does not exist. Locale prefixing is the caller's job (`localePath`).
*/
export const MINT_ROUTES: Record<IndexType, string> = {
cashu: '/mint',
fedimint: '/fedimint',
lnurl: '/lnurl-mint',
};
/** `/mint/mint.example.com`, unprefixed. */
export function mintPath(type: string, host: string): string {
const base = MINT_ROUTES[type as IndexType] ?? MINT_ROUTES.cashu;
return `${base}/${host}`;
}
/**
* Which ecosystem a page path belongs to, or null when it is not a listing page.
*
* The 404 resolver's first question: the reader asked for *something*, and whether
* this build has any business indexing it on their behalf is decided entirely by the
* shape of the path they used.
*/
export function typeForPath(path: string): { type: IndexType; host: string } | null {
const match = /^\/(mint|fedimint|lnurl-mint)\/([^/]+)\/?$/.exec(path);
if (!match?.[1] || !match[2]) return null;
const type: IndexType =
match[1] === 'fedimint' ? 'fedimint' : match[1] === 'lnurl-mint' ? 'lnurl' : 'cashu';
return { type, host: decodeURIComponent(match[2]) };
}
/**
* The address a `/mint/{slug}` or `/lnurl-mint/{slug}` deep link implies, or null when
* the slug cannot be turned back into one.
*
* Routing slugs are deterministic but not reversible: `mint.example.com/Bitcoin` becomes
* `mint.example.com-bitcoin`, and so would a mint at `mint.example.com-bitcoin` if one
* existed. Looking a row up by slug is unaffected — that is what the column is for — but
* *indexing* from a slug means fetching an address, and guessing which of two readings a
* hyphen had would mean probing the wrong host and possibly indexing it.
*
* So only the unambiguous shape is derived: a plain hostname, optionally with the port
* suffix the slug spells `-3338`. Everything else returns null, and the caller says it
* cannot look this one up from the link alone and offers the dialog, where the reader
* can paste the address including its path.
*
* The `lnurl-` collision prefix is deliberately *not* stripped. A slug only takes that
* prefix at insert time, so a link carrying one describes a row that exists and never
* reaches this function; a slug that merely starts with those characters is far more
* likely to be a mint whose hostname begins `lnurl-`, and turning it into a different
* host would mean probing — and possibly indexing — somebody else's server.
*/
export function addressFromSlug(slug: string): string | null {
const value = slug.trim().toLowerCase();
const match = /^([a-z0-9-]+(?:\.[a-z0-9-]+)*\.[a-z]{2,})(?:-(\d{2,5}))?$/.exec(value);
if (!match?.[1]) return null;
return `https://${match[1]}${match[2] ? `:${match[2]}` : ''}`;
}
+714
View File
@@ -0,0 +1,714 @@
/**
* LNURL mints: what a `kind:38174` announcement contains, what the mint's own endpoints
* return, and the handful of derived values the API, the pages and the islands all have
* to agree on.
*
* The kind is this site's own proposed NIP-87 extension, specified in
* `docs/KIND-LNURL-MINT.md`. That document and this file are meant to be read together:
* every rule below is stated there normatively, and `api/src/check-lnurl.ts` asserts the
* two have not drifted apart.
*
* The endpoint parsing was written against the live reference instance and against a
* locally run build of the mint software, not against a reading of the README — the same
* rule NOTES.md sets for the Cashu side. `NOTES-LNURL.md` records what was actually on
* the wire. Four places where the wire and the prose disagreed, all resolved for the wire:
*
* - There is no bare `/p`. The payRequest lives only at `/.well-known/lnurlp/{username}`,
* and `/p` is a hard 404. The mint advertisement — limits, description, pubkey, node
* identity — is on the *withdraw* side, `/.well-known/lnurlw/{username}`.
* - Every registered route answers its errors with **HTTP 200** and an LNURL
* `{"status":"ERROR"}` body. A status code proves nothing; only the parsed body does.
* - `None` fields are dropped from responses entirely, so "no funding source" shows up
* as `mintPubkey` and the node fields being *absent*, not null.
* - LUD-21 verify cannot be distinguished from an unknown payment hash. It is
* announcement-only; nothing here infers it.
*
* Every amount on the wire is millisatoshi. Nothing in this file rounds; `msatToSat`
* is where that decision is made once.
*/
import {
sanitizeDisplayText, sanitizePictureUrl, tagValue, tagValues, type NostrEventLike,
} from './nostr.js';
import { displayDomain, normalizeMintUrl } from './normalize.js';
/** The scheme on the synthetic `mints.url` an LNURL row is keyed by. */
export const LNURL_KEY_SCHEME = 'lnurl:';
/**
* Routing-slug prefix, used **only** on collision.
*
* `mints.host` is globally unique across every ecosystem, so an LNURL mint and a Cashu
* mint on the same hostname would fight over one slug and the loser would silently not
* be tracked. Almost always they do not collide, and the LNURL mint keeps the clean
* `lnurl.21mint.me` slug; when one does, the row takes `lnurl-` in front rather than
* vanishing. See `insertLnurlMint` for why that is decided at insert time and then never
* revisited.
*/
export const LNURL_SLUG_PREFIX = 'lnurl-';
/**
* The primary key an LNURL row uses.
*
* Namespaced rather than storing the bare URL, for the same reason `fedimint:` is: the
* column is the table's primary key across all three ecosystems, and one host serving
* both a Cashu mint and an LNURL mint must produce two rows, not a collision. The base
* URL is recoverable in full, so nothing is lost by the prefix.
*/
export function lnurlKey(baseUrl: string): string {
return LNURL_KEY_SCHEME + baseUrl;
}
/** The base URL inside an `lnurl:` key, or null if that is not what this is. */
export function baseUrlFromKey(key: string): string | null {
if (!key.startsWith(LNURL_KEY_SCHEME)) return null;
const url = key.slice(LNURL_KEY_SCHEME.length);
return url.startsWith('https://') ? url : null;
}
/* ---------- identity ---------- */
/**
* A mint pubkey: the funding node's identity key, 33 bytes compressed, 66 hex characters
* beginning `02` or `03`.
*
* Strict about the length and the prefix on purpose. This value becomes the `d` tag, and
* the whole reason the two `d` forms are unambiguous is that one of them is exactly this
* shape and the other never is.
*/
export function isMintPubkey(value: unknown): value is string {
return typeof value === 'string' && /^0[23][0-9a-f]{64}$/i.test(value);
}
/**
* The `d` fallback: a mint's normalized host, with no scheme and no trailing slash.
*
* `https://mint.example.com/lnurl/` becomes `mint.example.com/lnurl`. Always contains a
* dot and never matches `isMintPubkey`, which is what keeps the two forms apart.
*/
export function hostIdentifier(baseUrl: string): string {
return displayDomain(baseUrl).toLowerCase();
}
/**
* The canonical `d` for a mint, given whatever is known about it.
*
* Pubkey when there is one, host otherwise — and the pubkey is *sticky*: a caller
* passing a previously known pubkey keeps it even when the current probe found none,
* because a node being unreachable for one request is not a change of identity. See
* "When a mint gains a pubkey after being announced by host" in the kind document.
*/
export function lnurlIdentifier(baseUrl: string, mintPubkey: string | null | undefined): string {
return isMintPubkey(mintPubkey) ? mintPubkey.toLowerCase() : hostIdentifier(baseUrl);
}
/**
* Both identifiers a review of this mint could carry, for a `#d` relay filter and for
* resolution.
*
* A mint announced by host before it had a funding source, and by pubkey afterwards, has
* reviews pointing at both. Asking for only the current one strands the older half.
*/
export function lnurlIdentifiers(
baseUrl: string,
mintPubkey: string | null | undefined,
): string[] {
const host = hostIdentifier(baseUrl);
return isMintPubkey(mintPubkey) ? [mintPubkey.toLowerCase(), host] : [host];
}
/* ---------- features ---------- */
/**
* Note operations: the four branches of LUD-25's `/w/cb`, plus minting.
*
* Split into two groups because that is where a missing funding source cuts. `mint` and
* `melt` both need the node — one issues an invoice, the other pays one — while
* `rotate`, `split` and `merge` only rewrite this mint's own book and keep working with
* no node at all. That distinction is the whole reason the degraded state is worth
* rendering rather than collapsing into "offline".
*/
export const FUNDED_FEATURES = ['mint', 'melt'] as const;
export const NOTE_FEATURES = ['rotate', 'split', 'merge'] as const;
/** The LNURL sub-specifications a mint can speak. */
export const SPEC_FEATURES = ['lud06', 'lud03', 'lud16', 'lud21'] as const;
/** Optional extras, both of which are genuinely visible from outside. */
export const EXTRA_FEATURES = ['signed-notes', 'onion'] as const;
/**
* The whole vocabulary, and the only values this build has an opinion about.
*
* Anything else in a `features` tag is kept and shown as its own chip rather than
* dropped: the kind document requires consumers to ignore tokens they do not recognise,
* and rendering an unknown capability under its published name says exactly as much as
* it should.
*/
export const FEATURE_VOCABULARY: readonly string[] = [
...FUNDED_FEATURES, ...NOTE_FEATURES, ...SPEC_FEATURES, ...EXTRA_FEATURES,
];
/**
* The named rows on the mint page's Features panel, in display order.
*
* `notes` is a row and not a feature: the three note-rewriting operations are one thing
* to a reader ("can I reshape what I hold?") and always ship together, so one row
* satisfied by any of them beats three rows that are always identical. Every other row
* is one vocabulary value.
*/
export const HIGHLIGHT_FEATURES = [
'mint', 'melt', 'notes', 'lud16', 'lud21', 'signed-notes', 'onion',
] as const;
export type HighlightFeature = (typeof HIGHLIGHT_FEATURES)[number];
/** Which vocabulary values satisfy each named row. */
export const FEATURE_ALIASES: Record<string, readonly string[]> = {
mint: ['mint'],
melt: ['melt'],
notes: ['rotate', 'split', 'merge'],
lud16: ['lud16'],
lud21: ['lud21'],
'signed-notes': ['signed-notes'],
onion: ['onion'],
};
/**
* Rows whose row is only as good as the funding source behind it.
*
* A mint that implements minting, melting and note signing still cannot do any of them
* while its node is unreachable, and the panel says so rather than showing a tick that
* is not true today. `rotate`/`split`/`merge` are deliberately absent from this set:
* they are exactly what still works.
*/
export const FUNDING_DEPENDENT: readonly HighlightFeature[] = ['mint', 'melt', 'signed-notes'];
/**
* Plain-language names, and the English source of truth for them.
*
* The catalogs carry a translation per key under `lnurl.feature.`; anything neither
* knows renders as its own published token, which is still a true label.
*/
export const FEATURE_NAMES_EN: Record<string, string> = {
mint: 'Mint via Lightning (LUD-06)',
melt: 'Melt to Lightning',
notes: 'Rotate / split / merge notes',
lud16: 'Lightning address',
lud21: 'Payment verification (LUD-21)',
'signed-notes': 'Signed notes (offline verification)',
onion: 'Tor address',
rotate: 'Rotate a note',
split: 'Split a note',
merge: 'Merge notes',
lud06: 'LUD-06 payRequest',
lud03: 'LUD-03 withdrawRequest',
};
/**
* Split a `features` tag: `"mint,melt,rotate,lud06"`.
*
* Comma separated, but whitespace is tolerated because a publisher writing
* `"mint, melt"` meant the same thing — the same latitude `parseModules` gives a
* `modules` tag. Lowercased, deduped, order preserved, and bounded so a hostile tag
* cannot become a thousand chips on a page.
*/
export function parseFeatures(value: string | null | undefined): string[] {
if (!value) return [];
const out: string[] = [];
const seen = new Set<string>();
for (const part of value.split(/[,\s]+/)) {
const feature = part.trim().toLowerCase();
if (!feature || feature.length > 32 || seen.has(feature)) continue;
if (!/^[a-z0-9_-]+$/.test(feature)) continue;
seen.add(feature);
out.push(feature);
if (out.length >= 40) break;
}
return out;
}
/** Which named row a vocabulary value belongs to, or null for the chip list. */
export function highlightFeatureFor(feature: string): HighlightFeature | null {
const name = feature.toLowerCase();
for (const key of HIGHLIGHT_FEATURES) {
if (FEATURE_ALIASES[key]?.includes(name)) return key;
}
return null;
}
/** True when this mint claims anything satisfying one of the named rows. */
export function hasFeature(features: readonly string[], key: HighlightFeature): boolean {
const aliases = FEATURE_ALIASES[key] ?? [];
return features.some((feature) => aliases.includes(feature.toLowerCase()));
}
/** Features with no named row of their own, for the chip list under the seven. */
export function otherFeatures(features: readonly string[]): string[] {
return features.filter((feature) => highlightFeatureFor(feature) === null);
}
/**
* What one row of the Features panel says.
*
* Three states rather than two, which is the one place this panel is richer than the
* Fedimint Modules panel it is modelled on: a capability can be published, and still be
* unavailable this minute because the node behind it is unreachable. Collapsing that
* into "supported" would show a tick beside something that would fail if tried.
*/
export type FeatureState = 'ok' | 'unavailable' | 'none';
/**
* The state of every named row, given what was announced and what the probe saw.
*
* The announcement decides whether a capability exists at all; the probe can only take
* one away, and only the three that depend on a funding source. That asymmetry is
* normative in the kind document: a prober never rewrites an operator's `features`.
*
* `fundingAvailable` is null when nothing has probed yet, which reads as "no reason to
* doubt it" rather than as a failure.
*/
export function featureStates(
features: readonly string[],
fundingAvailable: boolean | null,
): Record<HighlightFeature, FeatureState> {
const out = {} as Record<HighlightFeature, FeatureState>;
for (const key of HIGHLIGHT_FEATURES) {
if (!hasFeature(features, key)) {
out[key] = 'none';
continue;
}
out[key] =
fundingAvailable === false && FUNDING_DEPENDENT.includes(key) ? 'unavailable' : 'ok';
}
return out;
}
/**
* The features a probe can honestly claim, from what it observed.
*
* Every entry has an observation behind it, and the four that are missing are the
* point. `rotate`, `split` and `merge` are only provable by calling `/w/cb`, which
* mutates or destroys a stranger's note; `lud21` is genuinely undetectable, because a
* disabled verify endpoint and an unknown payment hash return byte-identical responses
* (NOTES-LNURL.md §5). None of the four is inferred from a version string.
*
* Two callers, one definition, deliberately: the page renders this beside an operator's
* announced list, and the publisher signs it into a `kind:38174`. Those must not be
* able to disagree about what this site claims to have seen.
*
* The order is the vocabulary's own, so two runs over one mint produce identical
* output and the publisher's change detector does not fire on a reordering.
*/
export function observedFeatures(probe: {
fundingAvailable: boolean | null;
maxWithdrawableMsat: number | null;
maxSendableMsat: number | null;
lightningAddress: string | null;
mintPubkey: string | null;
onionUrl: string | null;
}): string[] {
const features: string[] = [];
/*
* `mint` and `melt` each need two things: the mint advertising the relevant side,
* and a funding source that can actually perform it. A mint whose node is unreachable
* implements both and can do neither, and this site only ever saw it in the state
* where it could not — so it does not say otherwise.
*/
const funded = probe.fundingAvailable === true;
if (funded && probe.maxSendableMsat !== null) features.push('mint');
if (funded && (probe.maxWithdrawableMsat ?? 0) > 0) features.push('melt');
if (probe.maxSendableMsat !== null) features.push('lud06');
// The advertisement's `callback` is `/w`, the LUD-03 withdrawRequest, and parsing the
// advertisement at all is what proves it was served.
if (probe.maxWithdrawableMsat !== null) features.push('lud03');
if (probe.lightningAddress) features.push('lud16');
if (probe.mintPubkey && funded) features.push('signed-notes');
if (probe.onionUrl) features.push('onion');
return features;
}
/**
* What the Features panel renders: what the operator announced, plus what was observed.
*
* A union, and it has to be one. The announcement is the operator's claim about what
* they built and is the richer list — only they can tell you that notes rotate. The
* probe is this site's own observation and is the *only* list for a mint nobody has
* announced yet, which today is every LNURL mint on the network.
*
* Announced values come first so an operator's own ordering survives. Note that this is
* a display concern and nothing else: `LnurlFields.features` still holds the
* announcement verbatim, and the publisher still signs only `observedFeatures`. The
* kind document's rule is that a prober never rewrites an operator's list, and nothing
* here does — it renders two lists side by side.
*/
export function displayFeatures(
announced: readonly string[] | null | undefined,
observed: readonly string[] | null | undefined,
): string[] {
return [...new Set([...(announced ?? []), ...(observed ?? [])])];
}
/* ---------- amounts ---------- */
/**
* Millisatoshi to satoshi, floored.
*
* Floored rather than rounded because both of the numbers this converts are *bounds*: a
* `maxWithdrawable` rounded up advertises a note larger than the mint will ever issue,
* and a `minWithdrawable` rounded down advertises one it will refuse. Flooring keeps the
* displayed range inside the real one at both ends, which is the safe direction to be
* wrong in. Sub-sat amounts floor to 0, which is true and is what the "withdrawals
* disabled" warning keys on.
*/
export function msatToSat(msat: number | null | undefined): number | null {
if (typeof msat !== 'number' || !Number.isFinite(msat) || msat < 0) return null;
return Math.floor(msat / 1000);
}
/* ---------- the mint's own endpoints ---------- */
/** The path the mint advertisement lives on. `_` is LUD-16's reserved bare-domain name. */
export const WITHDRAW_INFO_PATH = '/.well-known/lnurlw/_';
/** The LUD-06 payRequest, used as a fallback when the withdraw side does not answer. */
export const PAY_INFO_PATH = '/.well-known/lnurlp/_';
/**
* What a mint advertisement yields once parsed.
*
* Everything is optional except the two limits and the tag, because that is genuinely
* what varies: a mint with no funding source omits its whole node section, and the
* response is still valid and still worth rendering.
*/
export interface LnurlAdvertisement {
minWithdrawableMsat: number;
maxWithdrawableMsat: number;
defaultDescription: string | null;
mintPubkey: string | null;
payLink: string | null;
nodeAlias: string | null;
nodeUri: string | null;
nodeCapacityMsat: number | null;
nodeChannels: number | null;
nodePeers: number | null;
/**
* Whether the mint's funding source answered.
*
* Derived, not read: `mintPubkey` is populated only when a funding source is both
* configured and reachable, and is dropped from the response otherwise. One bit, and
* it is the only HTTP-visible signal there is — see NOTES-LNURL.md for why it is not
* possible to separate "never configured" from "unreachable right now", and why the
* site does not try.
*/
fundingAvailable: boolean;
}
function num(value: unknown): number | null {
return typeof value === 'number' && Number.isFinite(value) ? value : null;
}
function str(value: unknown, max = 400): string | null {
if (typeof value !== 'string') return null;
const text = value.trim();
if (!text || text.length > max) return null;
return text;
}
/**
* Parse `/.well-known/lnurlw/_`.
*
* Returns null for anything that is not a withdrawRequest carrying both limits — which
* includes the mint's own `{"status":"ERROR"}` bodies, since those arrive with HTTP 200
* and would otherwise read as a successful probe. The caller turns null into the
* "responding but invalid" state, which is neither online nor offline.
*/
export function parseAdvertisement(body: unknown): LnurlAdvertisement | null {
if (!body || typeof body !== 'object' || Array.isArray(body)) return null;
const raw = body as Record<string, unknown>;
if (raw['tag'] !== 'withdrawRequest') return null;
const min = num(raw['minWithdrawable']);
const max = num(raw['maxWithdrawable']);
// Both bounds are required, and inverted bounds are not a mint advertisement.
if (min === null || max === null || min < 0 || max < 0 || min > max) return null;
const mintPubkey = isMintPubkey(raw['mintPubkey']) ? String(raw['mintPubkey']).toLowerCase() : null;
return {
minWithdrawableMsat: min,
maxWithdrawableMsat: max,
defaultDescription: str(raw['defaultDescription']),
mintPubkey,
payLink: str(raw['payLink']),
nodeAlias: sanitizeDisplayText(raw['nodeAlias'], 64) ?? null,
nodeUri: str(raw['nodeUri'], 200),
nodeCapacityMsat: num(raw['nodeCapacity']),
nodeChannels: num(raw['nodeNumChannels']),
nodePeers: num(raw['nodeNumPeers']),
fundingAvailable: mintPubkey !== null,
};
}
/** What the LUD-06 payRequest yields. Fetched for the fee, the address and the limits. */
export interface LnurlPayInfo {
minSendableMsat: number;
maxSendableMsat: number;
/** The `text/plain` entry: the closest thing to an operator-written description. */
description: string | null;
/** The `text/identifier` entry, a LUD-16 lightning address. */
identifier: string | null;
/** `Mint fees: <base>,<ppm>`, absent when the mint is fee-free. */
feeBaseMsat: number | null;
feePpm: number | null;
/** The `withdrawLink` extension: lnurlcash's pointer back to the withdraw side. */
withdrawLink: string | null;
}
/**
* Parse `/.well-known/lnurlp/_`.
*
* `metadata` is a JSON *string* holding a JSON array of `[mime, value]` pairs, so it is
* parsed twice. A metadata blob that will not parse costs the description and the
* address and nothing else: the limits above it are still good.
*/
export function parsePayInfo(body: unknown): LnurlPayInfo | null {
if (!body || typeof body !== 'object' || Array.isArray(body)) return null;
const raw = body as Record<string, unknown>;
if (raw['tag'] !== 'payRequest') return null;
const min = num(raw['minSendable']);
const max = num(raw['maxSendable']);
if (min === null || max === null || min < 0 || max < 0 || min > max) return null;
let description: string | null = null;
let identifier: string | null = null;
let feeBaseMsat: number | null = null;
let feePpm: number | null = null;
try {
const entries: unknown = JSON.parse(typeof raw['metadata'] === 'string' ? raw['metadata'] : '[]');
if (Array.isArray(entries)) {
for (const entry of entries) {
if (!Array.isArray(entry) || typeof entry[0] !== 'string' || typeof entry[1] !== 'string') {
continue;
}
const [mime, value] = entry as [string, string];
// `Mint fees: <base_msat>,<ppm>` shares the `text/plain` mime with the
// description, so it is recognised by its prefix and taken out of the running
// for one — otherwise a fee-charging mint's description would be its fee line.
const fees = /^Mint fees:\s*(\d+)\s*,\s*(\d+)\s*$/i.exec(value);
if (mime === 'text/plain' && fees) {
feeBaseMsat = Number.parseInt(fees[1]!, 10);
feePpm = Number.parseInt(fees[2]!, 10);
continue;
}
if (mime === 'text/plain' && description === null) {
description = sanitizeDisplayText(value, 400) ?? null;
}
if (mime === 'text/identifier' && identifier === null) {
identifier = isLightningAddress(value) ? value.trim().toLowerCase() : null;
}
}
}
} catch {
// Unparseable metadata. The limits are still real, so this is not a failed probe.
}
return {
minSendableMsat: min,
maxSendableMsat: max,
description,
identifier,
feeBaseMsat,
feePpm,
withdrawLink: str(raw['withdrawLink']),
};
}
/** `name@domain`, and only that. Same shape rule the review cards apply to a NIP-05. */
export function isLightningAddress(value: unknown): value is string {
if (typeof value !== 'string') return false;
const text = value.trim();
return text.length <= 128 && /^[a-z0-9._+-]+@[a-z0-9-]+(\.[a-z0-9-]+)+$/i.test(text);
}
/**
* The mint's lightning address, derived from `payLink`.
*
* Not read off the payRequest's own `text/identifier`, which echoes back whatever
* username was queried — probing `_` gets `_@host`, which is LUD-16's bare-domain form
* and not something to render at a reader. `payLink` is built from the operator's
* configured username unconditionally, so it is the one place the real name appears.
*/
export function addressFromPayLink(payLink: string | null | undefined): string | null {
if (!payLink) return null;
let url: URL;
try {
url = new URL(payLink);
} catch {
return null;
}
const username = /\/\.well-known\/lnurlp\/([^/?#]+)$/.exec(url.pathname)?.[1];
if (!username || username === '_') return null;
const address = `${decodeURIComponent(username)}@${url.hostname.toLowerCase()}`;
return isLightningAddress(address) ? address : null;
}
/**
* A `*.onion` host in the one-pager, or null.
*
* Deliberately looser than base32's alphabet. A v3 address is 56 characters of `a-z2-7`,
* but the mint software's own test fixture is not valid base32, and a real address that
* does not fit the expected shape is still the operator's address. Nothing is ever
* fetched over Tor by this site, so the cost of a wrong match is one displayed string.
*/
export function onionFromHtml(html: string): string | null {
const match = /\b([a-z0-9]{16,60}\.onion)\b/i.exec(html);
return match?.[1]?.toLowerCase() ?? null;
}
/**
* The software version, from `GET /openapi.json`.
*
* `0.0.0+unknown` is the package's own "I could not find my metadata" sentinel, not a
* release, so it is treated as no version at all rather than printed at a reader. The
* title is checked too: an arbitrary FastAPI app on the same host would otherwise
* contribute its version to a mint's page.
*/
export function parseSoftware(body: unknown): string | null {
if (!body || typeof body !== 'object') return null;
const info = (body as Record<string, unknown>)['info'];
if (!info || typeof info !== 'object') return null;
const record = info as Record<string, unknown>;
const title = str(record['title'], 64);
const version = str(record['version'], 64);
if (!title || !version) return null;
if (!/^[\w.+-]+$/.test(version) || version.startsWith('0.0.0+unknown')) return null;
if (!/^[\w.@/ -]+$/.test(title)) return null;
return `${title}/${version}`;
}
/* ---------- the announcement ---------- */
export interface LnurlAnnouncement {
/** The `d` tag: a mint pubkey, or a normalized host. */
identifier: string;
/** The `d` tag when it was a pubkey, else null. Feeds the sticky identity rule. */
mintPubkey: string | null;
/** The canonical `u` tag, normalized. This is what rows are deduped by. */
baseUrl: string;
/** The routing slug the normalizer derived from that URL. */
slug: string;
/** The `features` tag, split. */
features: string[];
/** The `n` tag, normalized. */
network: string | null;
/** From `content`, which is kind-0-shaped metadata. */
name: string | null;
picture: string | null;
about: string | null;
announcerPubkey: string;
announcedAt: number;
}
/**
* Read a kind 38174 event, or return null if it is not one this site can use.
*
* `u` is required here where NIP-87 makes it a SHOULD for 38172, and the kind document
* says why: an LNURL mint's `d` may be a bare host with no scheme, which is not
* something to fetch, so an announcement with no usable `u` carries no address at all
* and there is nothing to probe, key a row by, or link to.
*
* The `d` tag is *not* required to match the `u` tag's host. A mint that has a pubkey
* announces under it, and checking the two against each other would reject exactly the
* events the identity rule exists to allow.
*/
export function parseLnurlAnnouncement(event: NostrEventLike): LnurlAnnouncement | null {
const d = tagValue(event.tags, 'd')?.trim().toLowerCase();
if (!d || d.length > 200) return null;
// A `d` that is neither a pubkey nor host-shaped is not an identifier this build can
// resolve a review against, so the announcement is not usable even if `u` is fine.
if (!isMintPubkey(d) && !/^[a-z0-9.-]+\.[a-z]{2,}(:\d+)?(\/[\w./~-]*)?$/.test(d)) return null;
let normalized: ReturnType<typeof normalizeMintUrl> = null;
for (const raw of tagValues(event.tags, 'u')) {
normalized = normalizeMintUrl(raw);
if (normalized) break;
}
if (!normalized) return null;
const meta = parseLnurlMetadata(event.content);
return {
identifier: d,
mintPubkey: isMintPubkey(d) ? d : null,
baseUrl: normalized.url,
slug: normalized.host,
features: parseFeatures(tagValue(event.tags, 'features')),
network: normalizeLnurlNetwork(tagValue(event.tags, 'n')),
name: meta.name,
picture: meta.picture,
about: meta.about,
announcerPubkey: event.pubkey,
announcedAt: event.created_at,
};
}
/**
* The `content` of a 38174, which the kind document defines as kind-0-style metadata.
*
* Same defensive posture as the Fedimint parser: this is arbitrary text written by
* anyone with a relay connection, so unparseable JSON, wrong types and junk fields all
* degrade to "no metadata" rather than throwing.
*/
export function parseLnurlMetadata(content: string): {
name: string | null;
picture: string | null;
about: string | null;
} {
const empty = { name: null, picture: null, about: null };
if (!content.trim()) return empty;
let meta: Record<string, unknown>;
try {
const parsed: unknown = JSON.parse(content);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return empty;
meta = parsed as Record<string, unknown>;
} catch {
return empty;
}
const first = (...keys: string[]): unknown => {
for (const key of keys) if (meta[key] !== undefined && meta[key] !== null) return meta[key];
return undefined;
};
return {
name: sanitizeDisplayText(first('name', 'display_name', 'mint_name'), 64) ?? null,
picture: sanitizePictureUrl(first('picture', 'icon_url', 'image')) ?? null,
about: sanitizeDisplayText(first('about', 'description'), 400) ?? null,
};
}
/**
* Normalize an `n` tag.
*
* The same mapping the Fedimint side applies, and for the same reason: NIP-87 names the
* value `mainnet`, publishers copy each other, and `bitcoin` is what they write. Kept
* separate from `normalizeNetwork` only so the two ecosystems' rules can diverge later
* without one quietly changing the other; today they agree, and `check-lnurl.ts` asserts
* that they still do.
*/
export function normalizeLnurlNetwork(value: string | null | undefined): string | null {
const raw = value?.trim().toLowerCase();
if (!raw) return null;
if (raw === 'bitcoin' || raw === 'main' || raw === 'mainnet') return 'mainnet';
if (!/^[a-z0-9]{1,20}$/.test(raw)) return null;
return raw;
}
+64
View File
@@ -28,6 +28,9 @@ function isDisallowedHost(hostname: string): boolean {
if (hostname.endsWith('.local')) return true; if (hostname.endsWith('.local')) return true;
if (PRIVATE_IPV4.test(hostname)) return true; if (PRIVATE_IPV4.test(hostname)) return true;
if (isPrivateIpv6(hostname)) return true; if (isPrivateIpv6(hostname)) return true;
// Anything that parses as an IP literal is judged by the resolved-address rule too,
// so the two checks cannot disagree about, say, 100.64.0.1 or 224.0.0.1.
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname) && isPrivateIpAddress(hostname)) return true;
// A bare label with no dot cannot be a public host. // A bare label with no dot cannot be a public host.
if (!hostname.includes('.') && !hostname.includes(':')) return true; if (!hostname.includes('.') && !hostname.includes(':')) return true;
return false; return false;
@@ -50,6 +53,67 @@ function isPrivateIpv6(hostname: string): boolean {
return false; return false;
} }
/**
* Is this literal IP address one the indexer must never connect to?
*
* Written against a *resolved* address rather than a hostname, which is the difference
* between this and `isDisallowedHost` above: `mint.example.com` looks like an ordinary
* public name and can resolve to `127.0.0.1`, and only the answer DNS gave can tell you
* so. `POST /api/index` fetches URLs a stranger typed, so it resolves first and checks
* every address here before a socket is opened.
*
* Broader than the hostname rule on purpose. Beyond loopback, link-local and the three
* RFC-1918 ranges it also refuses carrier-grade NAT (100.64/10), `0.0.0.0/8`, the
* benchmarking and documentation ranges, multicast and the broadcast address: none of
* them is a public mint, and each of them is somewhere on a network this server can see
* and a stranger should not be able to point it at.
*/
export function isPrivateIpAddress(value: string): boolean {
const ip = value.trim().toLowerCase().replace(/^\[|\]$/g, '');
if (!ip) return true;
const v4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(ip);
if (v4) {
const [a, b] = [Number(v4[1]), Number(v4[2])];
if (a === undefined || b === undefined || a > 255 || b > 255) return true;
if (a === 0 || a === 10 || a === 127) return true; // this-network, RFC1918, loopback
if (a === 169 && b === 254) return true; // link-local
if (a === 172 && b >= 16 && b <= 31) return true; // RFC1918
if (a === 192 && b === 168) return true; // RFC1918
if (a === 192 && b === 0) return true; // IETF protocol assignments / 192.0.2.0 docs
if (a === 198 && (b === 18 || b === 19)) return true; // benchmarking
if (a === 198 && b === 51) return true; // documentation
if (a === 203 && b === 0) return true; // documentation
if (a === 100 && b >= 64 && b <= 127) return true; // carrier-grade NAT
if (a >= 224) return true; // multicast, reserved, broadcast
return false;
}
if (!ip.includes(':')) return true; // Not an address this function understands.
// An IPv4-mapped or IPv4-compatible address is judged on its IPv4 half.
const mapped = /:((?:\d{1,3}\.){3}\d{1,3})$/.exec(ip);
if (mapped?.[1]) return isPrivateIpAddress(mapped[1]);
if (ip === '::' || ip === '::1') return true;
if (/^f[cd]/.test(ip)) return true; // unique local, fc00::/7
if (/^fe[89ab]/.test(ip)) return true; // link-local, fe80::/10
if (/^ff/.test(ip)) return true; // multicast
return false;
}
/**
* Is this hostname one the indexer must never fetch, before DNS is consulted at all?
*
* The cheap half of the check: `localhost`, `.onion`, `.local`, a bare label with no
* dot, and an IP literal that is already disqualified by `isPrivateIpAddress`. Exported
* so the on-demand indexer can refuse the obvious cases without paying for a lookup,
* and so a redirect hop can be judged by the same rule its origin was.
*/
export function isBlockedHostname(hostname: string): boolean {
return isDisallowedHost(hostname.toLowerCase());
}
/** /**
* May the indexer fetch this URL? http(s) only, and never a private or local host. * May the indexer fetch this URL? http(s) only, and never a private or local host.
* *
+15
View File
@@ -13,6 +13,15 @@ export const KIND_REVIEW = 38000;
export const KIND_MINT_ANNOUNCEMENT = 38172; export const KIND_MINT_ANNOUNCEMENT = 38172;
/** NIP-87 Fedimint federation announcement. */ /** NIP-87 Fedimint federation announcement. */
export const KIND_FEDIMINT_ANNOUNCEMENT = 38173; export const KIND_FEDIMINT_ANNOUNCEMENT = 38173;
/**
* LNURL mint announcement.
*
* Not in NIP-87: this site's own proposed extension to it, specified in
* `docs/KIND-LNURL-MINT.md` and intended for a PR to nostr-protocol/nips. 38174 is the
* next free slot in the family — checked against the kind index in the nips README and
* against every issue and PR in that repository before it was claimed.
*/
export const KIND_LNURL_ANNOUNCEMENT = 38174;
/** Profile metadata. */ /** Profile metadata. */
export const KIND_PROFILE = 0; export const KIND_PROFILE = 0;
@@ -30,6 +39,7 @@ export const KIND_PROFILE = 0;
export const ANNOUNCEMENT_KINDS = { export const ANNOUNCEMENT_KINDS = {
cashu: KIND_MINT_ANNOUNCEMENT, cashu: KIND_MINT_ANNOUNCEMENT,
fedimint: KIND_FEDIMINT_ANNOUNCEMENT, fedimint: KIND_FEDIMINT_ANNOUNCEMENT,
lnurl: KIND_LNURL_ANNOUNCEMENT,
} as const; } as const;
/** /**
@@ -224,6 +234,11 @@ export function isFedimintReview(event: NostrEventLike): boolean {
return tagValue(event.tags, 'k') === String(KIND_FEDIMINT_ANNOUNCEMENT); return tagValue(event.tags, 'k') === String(KIND_FEDIMINT_ANNOUNCEMENT);
} }
/** True when the event's `k` tag marks it as being about an LNURL mint. */
export function isLnurlReview(event: NostrEventLike): boolean {
return tagValue(event.tags, 'k') === String(KIND_LNURL_ANNOUNCEMENT);
}
/** /**
* The kind a review says it is about, from its `k` tag, as a number. * The kind a review says it is about, from its `k` tag, as a number.
* *
+191 -5
View File
@@ -1,5 +1,7 @@
/** Shapes returned by the API. The web app builds against these. */ /** Shapes returned by the API. The web app builds against these. */
import type { MintCapabilities } from './warnings.js';
/** /**
* Statuses a listed thing can be in. * Statuses a listed thing can be in.
* *
@@ -18,7 +20,7 @@ export type MintStatus = 'online' | 'degraded' | 'offline' | 'unknown' | 'announ
* has to be able to hold a value it does not have a page for. Compare against * has to be able to hold a value it does not have a page for. Compare against
* `ANNOUNCEMENT_KINDS` rather than switching exhaustively on this. * `ANNOUNCEMENT_KINDS` rather than switching exhaustively on this.
*/ */
export type MintType = 'cashu' | 'fedimint' | (string & {}); export type MintType = 'cashu' | 'fedimint' | 'lnurl' | (string & {});
/** One item of `GET /api/mints`. */ /** One item of `GET /api/mints`. */
export interface MintListItem { export interface MintListItem {
@@ -26,7 +28,7 @@ export interface MintListItem {
host: string; host: string;
name: string | null; name: string | null;
icon: string | null; icon: string | null;
/** 'cashu' or 'fedimint'. Rows written before the column existed read as 'cashu'. */ /** 'cashu', 'fedimint' or 'lnurl'. Rows written before the column existed read as 'cashu'. */
type: MintType; type: MintType;
status: MintStatus; status: MintStatus;
last_online: number | null; last_online: number | null;
@@ -35,6 +37,40 @@ export interface MintListItem {
score: number; score: number;
last_review_at: number | null; last_review_at: number | null;
version: string | null; version: string | null;
/* ---- card chips ----
*
* The three fields below exist so a card can be drawn from the list payload alone.
* Before them, /mints fetched `GET /api/mints/:host` once per mint at build time just
* to read two booleans off each one, which is fine for fifty-five mints on one build
* machine and is not fine for every visitor's browser once the list hydrates. They are
* facts, never rendered strings: the label a chip prints is decided by `mintChip` in
* the reader's own language, on whichever side is drawing the card.
*
* A federation has no counterpart and needs none — it publishes no capability list, so
* `mintChip` returns null for one and always will. See the Fedimint branch of
* `getMintWarnings`.
*/
/**
* NUT numbers this mint publishes, as strings: `["4", "5", "17"]`. Cashu only; empty
* for the other ecosystems and for a mint whose `/v1/info` has never been read.
*/
nuts: string[];
/**
* NUT-04 and NUT-05 switches, the Cashu chip's only input. null means nothing is
* cached for this mint, which is a different fact from "both are on" — see
* `readCapabilities`.
*/
capabilities: MintCapabilities | null;
/**
* LNURL: the advertised withdraw ceiling, millisatoshi. Optional rather than
* `| null`, so this stays exactly what `Partial<LnurlFields>` declares on `MintDetail`
* and the two do not have to be kept identical by hand.
*/
max_withdrawable_msat?: number | null;
/** LNURL: whether the last probe reached the mint's funding node. */
funding_available?: boolean | null;
} }
export interface ProbeSample { export interface ProbeSample {
@@ -48,10 +84,14 @@ export type RatingDistribution = Record<'1' | '2' | '3' | '4' | '5', number>;
/** /**
* `GET /api/mints/:host`. * `GET /api/mints/:host`.
* *
* The Fedimint keys are optional and absent on a Cashu mint, which is what keeps the * The Fedimint and LNURL keys are optional and absent on a Cashu mint, which is what
* Cashu payload unchanged. Read them through `FedimintDetail` after checking `type`. * keeps the Cashu payload unchanged. Read them through `FedimintDetail` or
* `LnurlDetail` after checking `type`.
*/ */
export interface MintDetail extends MintListItem, Partial<FedimintFields> { export interface MintDetail
extends MintListItem,
Partial<FedimintFields>,
Partial<LnurlFields> {
description: string | null; description: string | null;
pubkey: string | null; pubkey: string | null;
info: MintInfo | null; info: MintInfo | null;
@@ -100,6 +140,97 @@ export interface FedimintFields {
/** `GET /api/mints/:host` for a Fedimint federation: the detail plus its own fields. */ /** `GET /api/mints/:host` for a Fedimint federation: the detail plus its own fields. */
export type FedimintDetail = MintDetail & FedimintFields; export type FedimintDetail = MintDetail & FedimintFields;
/**
* What `ecosystem_json` holds for an LNURL row.
*
* Two sources, never mixed: `features` is what the operator *announced* on Nostr, and
* everything under "probed" is what the mint's own endpoints said when they were last
* reached. The kind document makes that separation normative — a prober never rewrites
* an operator's capability list — and keeping the two in different fields is what makes
* it impossible to do by accident.
*
* Every millisatoshi field is stored exactly as the wire gave it. Conversion to sats
* happens once, at render, through `msatToSat`.
*/
export interface LnurlFields {
/** The `d` tag: the mint pubkey when it has one, else the normalized host. */
lnurl_id: string;
/** The https base URL. `host` is the routing slug derived from it. */
base_url: string;
/** The `features` tag, split. The operator's claim, never edited by a probe. */
features: string[];
/**
* What the last probe actually observed this mint serving.
*
* Kept apart from `features` above rather than merged into it, because the two are
* different kinds of statement — a claim and an observation — and the kind document
* makes it normative that a prober never rewrites the first. The page renders their
* union through `displayFeatures`; the publisher signs only this one.
*/
observed_features: string[];
/** The `n` tag, normalized — `bitcoin` and `mainnet` both arrive as `mainnet`. */
network: string | null;
/** `created_at` of the newest announcement seen. null for a seeded row. */
announced_at: number | null;
/** Who published that announcement. The `a` tag of a review points back at them. */
announcer_pubkey: string | null;
/* ---- probed: from the mint's own endpoints ---- */
/**
* The funding node's identity key, from the mint advertisement.
*
* Sticky once learned: a probe that finds none does not clear it, because a node
* being unreachable for one request is not a change of identity. Its *absence from
* the latest probe* is recorded separately, in `funding_available`.
*/
mint_pubkey: string | null;
/**
* Whether the last probe found a reachable funding source.
*
* null before anything has probed. false is the degraded-but-online state: the mint
* answers, its limits are real, and `rotate`/`split`/`merge` still work, but nothing
* moves in or out over Lightning. See NOTES-LNURL.md for why this one bit cannot
* distinguish "never configured" from "unreachable right now", and why that is fine.
*/
funding_available: boolean | null;
/** Which endpoint answered: the withdraw advertisement, or the payRequest fallback. */
probe_endpoint: string | null;
/**
* Set when the host answered but with something that is not a mint advertisement.
*
* A distinct outcome from both online and offline, and it has to be: these endpoints
* return HTTP 200 for their errors, so "responding" and "working" are different
* questions. Carries the short reason, for the banner.
*/
invalid_reason: string | null;
/** Withdraw bounds, millisatoshi: what a note's value can actually be. */
min_withdrawable_msat: number | null;
max_withdrawable_msat: number | null;
/** Pay bounds, millisatoshi: what a minter can actually send. Not the same numbers. */
min_sendable_msat: number | null;
max_sendable_msat: number | null;
/** `Mint fees: <base>,<ppm>` from the payRequest metadata. Absent means fee-free. */
fee_base_msat: number | null;
fee_ppm: number | null;
/** The LUD-16 address, derived from `payLink` rather than the echoed identifier. */
lightning_address: string | null;
/** The Tor address from the one-pager, when one is advertised. */
onion_url: string | null;
/** The funding node, as the mint chooses to describe it. All optional, all msat. */
node_alias: string | null;
node_uri: string | null;
node_capacity_msat: number | null;
node_channels: number | null;
node_peers: number | null;
}
/** `GET /api/mints/:host` for an LNURL mint: the detail plus its own fields. */
export type LnurlDetail = MintDetail & LnurlFields;
/** /**
* `GET /api/stats`. * `GET /api/stats`.
* *
@@ -127,6 +258,41 @@ export interface Stats {
fedimint_announced: number; fedimint_announced: number;
/** Reviews of federations (`k` = 38173), included in `reviews_total`. */ /** Reviews of federations (`k` = 38173), included in `reviews_total`. */
fedimint_reviews: number; fedimint_reviews: number;
lnurl_total: number;
/** LNURL mints a probe reached. Online includes the degraded-funding ones. */
lnurl_online: number;
lnurl_offline: number;
/**
* Online mints whose funding source was unreachable at the last probe: up and
* serving, but nothing moves in or out over Lightning. A subset of `lnurl_online`,
* never added to it.
*/
lnurl_degraded_funding: number;
/** Reviews of LNURL mints (`k` = 38174), included in `reviews_total`. */
lnurl_reviews: number;
}
/**
* How one configured relay answered during the last discovery cycle.
*
* The three facts are deliberately separate, because the failure this exists to catch
* had all three looking different from each other: the relays in `RELAYS` connected
* fine, reached EOSE fine, and simply did not carry the archive, so `events` was the
* only field that would have said anything. A relay that is down and a relay that is
* up and empty are different problems with different fixes.
*/
export interface RelayHealth {
url: string;
/** A socket was opened to it. false means the address is wrong or the relay is down. */
connected: boolean;
/**
* Events it sent, counted before cross-relay deduplication — so this is what *this*
* relay contributed, not what was new because of it. Zero on a connected relay is
* the interesting number.
*/
events: number;
/** Every query it was asked ended in a real EOSE rather than in a timeout. */
eose: boolean;
} }
/** `GET /api/health`. */ /** `GET /api/health`. */
@@ -137,6 +303,26 @@ export interface Health {
last_discovery_at: number | null; last_discovery_at: number | null;
mints_tracked: number; mints_tracked: number;
updated_at: number; updated_at: number;
/**
* Per-relay outcome of the last discovery cycle. Empty until one has run — including
* on a fresh database, which is why a brand new deployment reports degraded until its
* first backfill finishes.
*/
discovery_relays: RelayHealth[];
/** Unique events the last discovery cycle received. null before the first one. */
last_discovery_events: number | null;
/** Which kind of cycle those numbers describe. */
last_discovery_mode: 'backfill' | 'incremental' | null;
/**
* The last backfill came back under `backfill_min_events`, or none has run yet.
*
* This is the flag that would have caught a year of ~31-event backfills against a
* relay list missing the archive. It forces `status` to degraded, and /api/health to
* 503, which is what the build gate and the site's own health checks read.
*/
discovery_starved: boolean;
/** `BACKFILL_MIN_EVENTS`, echoed so a reader of this payload can see the threshold. */
backfill_min_events: number;
} }
/** /**
+240 -9
View File
@@ -88,7 +88,14 @@ export type MintWarningKind =
| 'offline' // offline under 7 days | 'offline' // offline under 7 days
// Fedimint // Fedimint
| 'fedimint-offline' // a real check reported the guardians down | 'fedimint-offline' // a real check reported the guardians down
| 'never-confirmed'; // announced on Nostr, and nothing has ever confirmed it | 'never-confirmed' // announced on Nostr, and nothing has ever confirmed it
// LNURL. The offline tiers above are shared verbatim rather than twinned: `gone`,
// `offline-long` and `offline` are about reachability, which means exactly the same
// thing for an LNURL mint as for a Cashu one, and their copy already reads correctly
// for both. Only the states with no Cashu equivalent are new.
| 'lnurl-withdrawals-disabled' // maxWithdrawable is zero: nothing can be redeemed
| 'lnurl-invalid' // the host answers, but not with a mint advertisement
| 'lnurl-no-funding'; // up and serving, but its Lightning node is unreachable
export interface MintWarning { export interface MintWarning {
kind: MintWarningKind; kind: MintWarningKind;
@@ -139,6 +146,19 @@ export interface MintWarningInput {
capabilities?: Pick<MintCapabilities, 'mintDisabled' | 'meltDisabled'> | null; capabilities?: Pick<MintCapabilities, 'mintDisabled' | 'meltDisabled'> | null;
/** Only used to date "never answered a single check". */ /** Only used to date "never answered a single check". */
first_seen?: number; first_seen?: number;
/* ---- LNURL ---- */
/**
* The advertised withdraw ceiling, millisatoshi. Zero is the disabling value, and it
* is checked as `=== 0` rather than as falsy: `null` means nothing has probed, which
* is not the same claim at all.
*/
max_withdrawable_msat?: number | null;
/** false when the last probe found the mint's Lightning node unreachable. */
funding_available?: boolean | null;
/** Set when the host answered with something that is not a mint advertisement. */
invalid_reason?: string | null;
} }
export interface MintWarningOptions { export interface MintWarningOptions {
@@ -218,9 +238,28 @@ export const WARNING_COPY_EN: Record<string, string> = {
'This federation was announced on Nostr on {date}, {days} ago, nothing has confirmed since then that it is running, and nobody has reviewed it recently. Everything below came from the announcement.', 'This federation was announced on Nostr on {date}, {days} ago, nothing has confirmed since then that it is running, and nobody has reviewed it recently. Everything below came from the announcement.',
'neverConfirmed.meta': 'Announced {date}, never confirmed', 'neverConfirmed.meta': 'Announced {date}, never confirmed',
'lnurlWithdrawalsDisabled.lead': 'Withdrawals disabled.',
'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.',
'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.',
'lnurlWithdrawalsDisabled.meta.online': 'Withdrawals currently disabled',
'lnurlWithdrawalsDisabled.meta.offline': 'Withdrawals were disabled when last seen',
'lnurlInvalid.lead': 'Endpoint responding but invalid.',
'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.',
'lnurlInvalid.meta': 'Responding, but not with a valid mint advertisement',
'lnurlNoFunding.lead': 'Minting and melting unavailable.',
'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.',
'lnurlNoFunding.meta': 'Minting and melting unavailable',
'also.meltDisabled': 'Before going offline it had also disabled withdrawals.', 'also.meltDisabled': 'Before going offline it had also disabled withdrawals.',
'also.mintDisabled': 'Before going offline it had also disabled new minting.', 'also.mintDisabled': 'Before going offline it had also disabled new minting.',
'also.offline': 'It has also been offline since {date} ({days}).', 'also.offline': 'It has also been offline since {date} ({days}).',
'also.noFunding': 'Its Lightning node was also unreachable, so nothing could be minted or melted.',
}; };
/** /**
@@ -247,12 +286,24 @@ const englishStrings: WarningStrings = (key, vars) => {
/** Severity order, highest first. Index 0 of the returned list is the one to show. */ /** Severity order, highest first. Index 0 of the returned list is the one to show. */
const ORDER: MintWarningKind[] = [ const ORDER: MintWarningKind[] = [
'gone', 'melt-disabled', 'frozen', 'offline-long', 'melt-only', 'offline', 'gone',
'melt-disabled', 'frozen', 'lnurl-withdrawals-disabled',
'offline-long',
'melt-only', 'lnurl-invalid', 'lnurl-no-funding',
'offline',
// Appended rather than interleaved. The Fedimint kinds never share a list with the // Appended rather than interleaved. The Fedimint kinds never share a list with the
// Cashu ones (the two branches are exclusive), so their position relative to those // Cashu ones (the two branches are exclusive), so their position relative to those
// is arbitrary — and appending leaves every existing rank exactly where it was. // is arbitrary — and appending leaves every existing rank exactly where it was.
'fedimint-offline', 'never-confirmed', 'fedimint-offline', 'never-confirmed',
]; ];
/*
* The LNURL kinds *are* interleaved, unlike the Fedimint ones, and they have to be:
* that branch reuses `gone`, `offline-long` and `offline`, so its warnings genuinely
* share a list with those ranks and appending would put a critical
* "withdrawals disabled" below a mild "offline since yesterday". The relative order of
* everything that existed before is unchanged, which is what `check-warnings.ts`
* asserts.
*/
/** /**
* How stale an unconfirmed announcement has to be before it is worth saying so. * How stale an unconfirmed announcement has to be before it is worth saying so.
@@ -284,6 +335,7 @@ export function getMintWarnings(
options: MintWarningOptions = {}, options: MintWarningOptions = {},
): MintWarning[] { ): MintWarning[] {
if (mint.type === 'fedimint') return fedimintWarnings(mint, options); if (mint.type === 'fedimint') return fedimintWarnings(mint, options);
if (mint.type === 'lnurl') return lnurlWarnings(mint, options);
return cashuWarnings(mint, options); return cashuWarnings(mint, options);
} }
@@ -373,13 +425,7 @@ function cashuWarnings(
const caps = mint.capabilities ?? readCapabilities(mint.info?.nuts); const caps = mint.capabilities ?? readCapabilities(mint.info?.nuts);
const { mintDisabled, meltDisabled } = caps; const { mintDisabled, meltDisabled } = caps;
const offline = mint.status === 'offline'; const { offline, since, days, tier } = offlineTier(mint, now);
const since = mint.last_online;
const days = offline && since !== null ? Math.max(0, Math.floor((now - since) / 86400)) : null;
// Offline with no last_online means it has never once answered, which is at least
// as bad as a month of silence, so it lands in the top tier rather than the bottom.
const tier = !offline ? 0 : days === null ? 3 : days >= 30 ? 3 : days >= 7 ? 2 : 1;
/* /*
* Cached flags describe the last configuration seen, not the current one, and the * Cached flags describe the last configuration seen, not the current one, and the
@@ -468,6 +514,183 @@ function cashuWarnings(
return out; return out;
} }
/**
* How long a listing has been unreachable, in the four bands the banners key on.
*
* Shared by the Cashu and LNURL branches, which apply exactly the same thresholds —
* being unreachable means the same thing whether the endpoint that stopped answering
* was `/v1/info` or a mint advertisement, and two copies of "is 7 days long?" is two
* places for it to become 8 in one of them.
*
* Tier 3 covers both a month of silence and never having answered at all: offline with
* no `last_online` means it has never once answered, which is at least as bad as a
* month of it, so it lands in the top tier rather than the bottom.
*/
function offlineTier(
mint: Pick<MintWarningInput, 'status' | 'last_online'>,
now: number,
): { offline: boolean; since: number | null; days: number | null; tier: 0 | 1 | 2 | 3 } {
const offline = mint.status === 'offline';
const since = mint.last_online;
const days = offline && since !== null ? Math.max(0, Math.floor((now - since) / 86400)) : null;
const tier = !offline ? 0 : days === null ? 3 : days >= 30 ? 3 : days >= 7 ? 2 : 1;
return { offline, since, days, tier };
}
/**
* What can be said about an LNURL mint.
*
* Between the two extremes of the other ecosystems. A federation publishes no switches
* at all, so its page can only talk about reachability; a Cashu mint publishes NUT-04
* and NUT-05 flags, so its page can be specific about which direction is broken. An
* LNURL mint sits in between: two things about it are genuinely checkable over HTTP,
* and both get a banner.
*
* - `maxWithdrawable` of zero. A mint advertising that no note can be redeemed for
* anything is the closest analogue this ecosystem has to "withdrawals disabled",
* and it is read from the advertisement rather than inferred, so it ranks with the
* Cashu capability banners.
* - A funding source the mint cannot reach. Distinctive to lnurlcash and worth its
* own sentence, because it is *partial*: the mint is up, its notes still rotate,
* split and merge, and only the two operations that need a Lightning node are
* unavailable. Calling that "offline" would be wrong in both directions.
*
* The third, `lnurl-invalid`, is about this site's own reading rather than the mint's
* configuration: these endpoints answer their errors with HTTP 200, so a host that
* responds with something unparseable is a state that has to be named rather than
* silently counted as either up or down.
*
* Note what is deliberately missing: nothing here reads `features`. That tag is the
* operator's claim about what they built, and a banner derived from a claim rather than
* from an observation would be the same invention the Fedimint branch refuses to make.
*/
function lnurlWarnings(
mint: MintWarningInput,
options: MintWarningOptions = {},
): MintWarning[] {
const now = options.now ?? Math.floor(Date.now() / 1000);
const date = options.formatDate ?? defaultDate;
const month = options.formatMonth ?? defaultMonth;
const s = options.strings ?? englishStrings;
const { offline, since, days, tier } = offlineTier(mint, now);
const tense = offline ? 'offline' : 'online';
const dayCount = s('dayCount', { n: days ?? 0 });
// `=== 0`, never falsy: null means nothing has probed this mint yet, and "we have not
// looked" must not render as "withdrawals are disabled".
const withdrawalsDisabled = mint.max_withdrawable_msat === 0;
const invalid = Boolean(mint.invalid_reason);
const noFunding = mint.funding_available === false;
const out: MintWarning[] = [];
// Pushed in ORDER, so the first one added is the one that wins.
if (tier === 3) {
out.push({
kind: 'gone',
severity: 'critical',
lead: s('gone.lead'),
body:
since !== null
? s('gone.body.since', { date: date(since), days: dayCount })
: mint.first_seen
? s('gone.body.neverDated', { month: month(mint.first_seen) })
: s('gone.body.never'),
meta: since !== null ? s('gone.meta.since', { date: date(since) }) : s('gone.meta.never'),
});
}
if (withdrawalsDisabled) {
out.push({
kind: 'lnurl-withdrawals-disabled',
severity: 'critical',
lead: s('lnurlWithdrawalsDisabled.lead'),
body: s(`lnurlWithdrawalsDisabled.body.${tense}`),
meta: s(`lnurlWithdrawalsDisabled.meta.${tense}`),
});
}
if (tier === 2 && since !== null) {
out.push({
kind: 'offline-long',
severity: 'critical',
lead: s('offlineLong.lead', { days: dayCount, date: date(since) }),
body: s('offlineLong.body'),
meta: s('offlineLong.meta', { date: date(since) }),
});
}
if (invalid) {
out.push({
kind: 'lnurl-invalid',
severity: 'warning',
lead: s('lnurlInvalid.lead'),
body: s('lnurlInvalid.body'),
meta: s('lnurlInvalid.meta'),
});
}
if (noFunding) {
out.push({
kind: 'lnurl-no-funding',
severity: 'warning',
lead: s('lnurlNoFunding.lead'),
body: s('lnurlNoFunding.body'),
meta: s('lnurlNoFunding.meta'),
});
}
if (tier === 1 && since !== null) {
out.push({
kind: 'offline',
severity: 'warning',
lead: s('offline.lead', { date: date(since) }),
body: s('offline.body'),
meta: s('offline.meta', { date: date(since) }),
});
}
const primary = out[0];
if (primary) {
const extra = lnurlCollapsed(primary.kind, { noFunding, offline, since, dayCount, date, s });
if (extra) primary.body += ` ${extra}`;
}
return out;
}
/**
* The one sentence a losing LNURL condition earns inside the winner's text.
*
* Same contract as `collapsed`, and separate from it because the conditions being
* folded in are different ones: there is no NUT-04 or NUT-05 here, and the fact worth
* rescuing from an offline banner is that the mint's Lightning node was down too.
*/
function lnurlCollapsed(
kind: MintWarningKind,
ctx: {
noFunding: boolean;
offline: boolean;
since: number | null;
dayCount: string;
date: (unix: number) => string;
s: WarningStrings;
},
): string | null {
if (kind === 'gone' || kind === 'offline-long' || kind === 'offline') {
return ctx.noFunding ? ctx.s('also.noFunding') : null;
}
// A capability banner outranked an offline one: say the mint is also unreachable,
// otherwise the page reads as if it were up and merely misconfigured.
if (kind === 'lnurl-withdrawals-disabled' && ctx.offline && ctx.since !== null) {
return ctx.s('also.offline', { date: ctx.date(ctx.since), days: ctx.dayCount });
}
return null;
}
/** /**
* The one sentence a losing condition earns inside the winner's text. Banners never * The one sentence a losing condition earns inside the winner's text. Banners never
* stack, but a mint that is both gone and had stopped paying out is a worse story * stack, but a mint that is both gone and had stopped paying out is a worse story
@@ -533,6 +756,8 @@ export const CHIP_COPY_EN: Record<string, string> = {
frozen: 'Frozen', frozen: 'Frozen',
noWithdrawals: 'No withdrawals', noWithdrawals: 'No withdrawals',
meltOnly: 'Melt only', meltOnly: 'Melt only',
/** LNURL: up and serving, but nothing moves in or out over Lightning. */
noFunding: 'No mint / melt',
}; };
export function mintChip(warnings: MintWarning[], strings?: WarningStrings): MintChip | null { export function mintChip(warnings: MintWarning[], strings?: WarningStrings): MintChip | null {
@@ -541,7 +766,13 @@ export function mintChip(warnings: MintWarning[], strings?: WarningStrings): Min
if (kinds.has('frozen')) return { label: s('frozen'), severity: 'critical' }; if (kinds.has('frozen')) return { label: s('frozen'), severity: 'critical' };
// "Melt only" is the other direction, so it cannot double as the label here. // "Melt only" is the other direction, so it cannot double as the label here.
if (kinds.has('melt-disabled')) return { label: s('noWithdrawals'), severity: 'critical' }; if (kinds.has('melt-disabled')) return { label: s('noWithdrawals'), severity: 'critical' };
// An LNURL mint advertising a zero withdraw ceiling is the same statement to a
// reader as a Cashu mint with melting off, so it earns the same two words.
if (kinds.has('lnurl-withdrawals-disabled')) {
return { label: s('noWithdrawals'), severity: 'critical' };
}
if (kinds.has('melt-only')) return { label: s('meltOnly'), severity: 'warning' }; if (kinds.has('melt-only')) return { label: s('meltOnly'), severity: 'warning' };
if (kinds.has('lnurl-no-funding')) return { label: s('noFunding'), severity: 'warning' };
return null; return null;
} }
+8
View File
@@ -28,6 +28,12 @@ const apiUrl = process.env.API_URL ?? 'http://127.0.0.1:8787';
// https://api.cashumints.space. // https://api.cashumints.space.
const publicApiUrl = process.env.PUBLIC_API_URL ?? ''; const publicApiUrl = process.env.PUBLIC_API_URL ?? '';
// Plausible-compatible analytics endpoint and site identifier. Empty values disable the
// script, which is useful for local builds that should not report page views.
const plausibleUrl =
process.env.PLAUSIBLE_URL ?? 'https://analytics.azzamo.net/js/script.js';
const plausibleDomain = process.env.PLAUSIBLE_DOMAIN ?? 'cashumints.space';
// Dev server and preview port. Set WEB_PORT in .env; a --port flag still overrides it. // Dev server and preview port. Set WEB_PORT in .env; a --port flag still overrides it.
const port = Number.parseInt(process.env.WEB_PORT ?? '', 10) || 4321; const port = Number.parseInt(process.env.WEB_PORT ?? '', 10) || 4321;
@@ -73,6 +79,8 @@ export default defineConfig({
vite: { vite: {
define: { define: {
'import.meta.env.PUBLIC_API_URL': JSON.stringify(publicApiUrl), 'import.meta.env.PUBLIC_API_URL': JSON.stringify(publicApiUrl),
'import.meta.env.PLAUSIBLE_URL': JSON.stringify(plausibleUrl),
'import.meta.env.PLAUSIBLE_DOMAIN': JSON.stringify(plausibleDomain),
}, },
server: { proxy }, server: { proxy },
}, },
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

+2 -1
View File
@@ -8,12 +8,13 @@
"og": "node scripts/og/build-og.mjs", "og": "node scripts/og/build-og.mjs",
"og:fixtures": "node scripts/og/build-og.mjs --fixtures", "og:fixtures": "node scripts/og/build-og.mjs --fixtures",
"build": "node scripts/og/build-og.mjs && astro build", "build": "node scripts/og/build-og.mjs && astro build",
"start": "node server.mjs",
"preview": "astro preview", "preview": "astro preview",
"typecheck": "astro check", "typecheck": "astro check",
"bones": "node --env-file-if-exists=../.env scripts/bones.mjs", "bones": "node --env-file-if-exists=../.env scripts/bones.mjs",
"check:links": "node scripts/check-links.mjs dist", "check:links": "node scripts/check-links.mjs dist",
"check:i18n": "node scripts/check-i18n.mjs", "check:i18n": "node scripts/check-i18n.mjs",
"test": "node --experimental-strip-types --test test/*.test.mjs", "test": "node --test test/*.test.mjs",
"check:hreflang": "node scripts/check-hreflang.mjs dist" "check:hreflang": "node scripts/check-hreflang.mjs dist"
}, },
"dependencies": { "dependencies": {
+17 -1
View File
@@ -242,6 +242,8 @@ const DYNAMIC_PREFIXES = [
'fedimint.network.', // localNetworkName(network) 'fedimint.network.', // localNetworkName(network)
'feed.ecosystem.', // the /reviews filter, one option per ECOSYSTEMS entry 'feed.ecosystem.', // the /reviews filter, one option per ECOSYSTEMS entry
'reviews.dialog.bodyPlaceholder.', // one prompt per ecosystem, keyed by subject.type 'reviews.dialog.bodyPlaceholder.', // one prompt per ecosystem, keyed by subject.type
'reviews.dialog.lede.', // the dialog's invitation, one per ecosystem
'reviews.rate.', // the reviews panel's inline "Rate this mint" row
]; ];
const isDynamic = (key) => DYNAMIC_PREFIXES.some((prefix) => key.startsWith(prefix)); const isDynamic = (key) => DYNAMIC_PREFIXES.some((prefix) => key.startsWith(prefix));
@@ -266,6 +268,20 @@ for (const file of files) {
// A .ts file under lib/ or scripts/ is island code wholesale. // A .ts file under lib/ or scripts/ is island code wholesale.
const shipsToBrowser = /\/(lib|scripts)\//.test(relative) && relative.endsWith('.ts'); const shipsToBrowser = /\/(lib|scripts)\//.test(relative) && relative.endsWith('.ts');
/*
* A page can inline one extra namespace for its own islands, through Base.astro's
* `clientNamespaces` prop. The home page does: its grids hydrate from the API and
* rewrite their own "All 56 mints" links, whose strings live under `home.` — a
* namespace not worth inlining on 1,300 mint pages that never read it.
*
* Read out of the page rather than listed here, so the prop and this check cannot
* disagree. A namespace a page does not actually pass is still a leak.
*/
const extraNamespaces = new Set(
[...(/clientNamespaces=\{\[([^\]]*)\]\}/.exec(source)?.[1] ?? '').matchAll(/'([\w-]+)'/g)]
.map((m) => m[1]),
);
for (const match of source.matchAll(T_CALL)) { for (const match of source.matchAll(T_CALL)) {
const key = match[2]; const key = match[2];
used.add(key); used.add(key);
@@ -278,7 +294,7 @@ for (const file of files) {
for (const match of clientSource.matchAll(T_CALL)) { for (const match of clientSource.matchAll(T_CALL)) {
const key = match[2]; const key = match[2];
const namespace = key.split('.')[0]; const namespace = key.split('.')[0];
if (!clientNamespaces.has(namespace)) { if (!clientNamespaces.has(namespace) && !extraNamespaces.has(namespace)) {
clientLeaks.push({ key, file: relative, namespace }); clientLeaks.push({ key, file: relative, namespace });
} }
} }
+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)`);
+386
View File
@@ -0,0 +1,386 @@
/**
* The production web server.
*
* The site is `output: 'static'`: `pnpm build` prerenders every page and this process
* only hands the result out over HTTP. nginx sits in front of it and proxies, rather
* than pointing a `root` at the built tree, so that nothing outside this file decides
* what is readable. That is not a stylistic preference — it removes two whole classes
* of failure the file-serving arrangement kept producing:
*
* traversal permissions nginx runs as www-data and the build runs as cashumints,
* so every directory from / down to dist had to be traversable
* by a user with no other business in it. One 0700 home
* directory anywhere in the chain took the site down, and
* `try_files` reports a permission error as a plain miss, so
* the symptom was a blanket 404 or an internal-redirect loop
* ending in 500 — never the actual cause.
*
* duplicated routing the locale 404 rule lived in nginx as a hand-maintained
* alternation of 23 codes. Adding a language meant editing a
* file that is not in this repository, and forgetting to was
* silent. Locale 404s are resolved by looking in the built
* tree now, so the list cannot drift.
*
* Reading files is all it does. There is no template, no database handle and no route
* table: `/api/*` and `/icons/*` belong to the API on its own port and nginx forwards
* them there directly.
*
* Env:
* SITE_PORT 8789 port to listen on
* SITE_HOST 127.0.0.1 interface to bind; loopback because nginx terminates TLS
* WEB_ROOT ./dist the tree to serve
*/
import fs from 'node:fs';
import fsp from 'node:fs/promises';
import http from 'node:http';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
function int(raw, fallback) {
const n = Number.parseInt(raw ?? '', 10);
return Number.isFinite(n) && n > 0 ? n : fallback;
}
/**
* The tree to serve, absolute.
*
* Defaults to the build output next to this file, which is what `pnpm start` and the
* tests use. In production it points at a copy outside the checkout: `pnpm build`
* empties dist before it writes, so serving dist directly means a rebuild takes the
* whole site down for the length of the build. See cashumints-web.service.
*/
export const ROOT = path.resolve(process.env.WEB_ROOT ?? path.join(here, 'dist'));
const PORT = int(process.env.SITE_PORT, 8789);
const HOST = process.env.SITE_HOST ?? '127.0.0.1';
/** One line per event, key=value after the message. Matches the API's log format. */
function log(level, msg, fields = {}) {
const parts = [new Date().toISOString(), level.toUpperCase(), msg];
for (const [k, v] of Object.entries(fields)) {
if (v !== undefined && v !== null) parts.push(`${k}=${v}`);
}
const line = parts.join(' ');
if (level === 'error') console.error(line);
else console.log(line);
}
const TYPES = new Map(Object.entries({
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.mjs': 'text/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.map': 'application/json; charset=utf-8',
'.webmanifest': 'application/manifest+json; charset=utf-8',
'.xml': 'application/xml; charset=utf-8',
'.txt': 'text/plain; charset=utf-8',
'.svg': 'image/svg+xml',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.webp': 'image/webp',
'.avif': 'image/avif',
'.gif': 'image/gif',
'.ico': 'image/x-icon',
'.woff2': 'font/woff2',
'.woff': 'font/woff',
'.ttf': 'font/ttf',
'.wasm': 'application/wasm',
}));
const IMMUTABLE = 'public, max-age=31536000, immutable';
const WEEK = 'public, max-age=604800';
const HOUR = 'public, max-age=3600';
/** Zero lifetime but cacheable: the client keeps the body and revalidates into a 304. */
const REVALIDATE = 'public, max-age=0, must-revalidate';
/**
* How long a response may be reused.
*
* Everything Vite and the card builder emit carries a content hash in its filename, so
* those are immutable for a year — a changed file is a changed URL. Markup is the
* opposite: the URLs are permanent and the bytes change on every rebuild, so it
* revalidates every time and the ETag below turns that into a 304 in the usual case.
*
* `/og/default.png` is the one unhashed card, served for pages that have no mint of
* their own, so it gets an hour rather than a year.
*/
export function cacheControl(urlPath, ext) {
if (ext === '.html') return REVALIDATE;
if (urlPath === '/og/default.png') return HOUR;
if (urlPath.startsWith('/_astro/') || urlPath.startsWith('/og/')) return IMMUTABLE;
if (urlPath === '/robots.txt' || urlPath === '/sitemap.xml') return HOUR;
return WEEK;
}
/**
* Turn a request path into a path inside ROOT, or null if it escapes or is malformed.
*
* Returns the *relative* path so the caller can join it against ROOT; every segment is
* checked rather than trusting `path.join` to have swallowed the `..`. Dotfiles are
* refused outright: a static build emits none, so a request for one is either a probe
* or a mistake, and neither should be answered with bytes.
*/
export function safePath(urlPath) {
if (urlPath.includes('\0')) return null;
const normalized = path.posix.normalize(urlPath);
if (!normalized.startsWith('/')) return null;
const segments = normalized.split('/').filter(Boolean);
for (const segment of segments) {
if (segment === '..' || segment.startsWith('.')) return null;
}
return segments;
}
async function statFile(file) {
try {
const stats = await fsp.stat(file);
return stats.isFile() ? stats : null;
} catch {
return null;
}
}
/**
* The candidates for one request path, in order.
*
* Astro emits directory-style routes — /mints is dist/mints/index.html — and
* `trailingSlash: 'ignore'` means /mints and /mints/ are both the page. Trying the
* literal path first keeps assets a single stat; the `.html` candidate covers the flat
* files at the root, /404.html among them.
*/
export function candidates(segments) {
const rel = segments.join('/');
if (rel === '') return ['index.html'];
return [rel, `${rel}/index.html`, `${rel}.html`];
}
/**
* The 404 body for a path, and it is not always the English one.
*
* A miss under /es keeps the visitor on the Spanish 404 rather than bouncing them into
* English, which is the same reason `redirectToDefaultLocale` is false in the Astro
* config. Which prefixes count is decided by what the build actually emitted: if
* <prefix>/404/index.html exists, the prefix is a locale. Nothing to keep in sync.
*/
async function notFoundBody(segments) {
const first = segments[0];
if (first) {
const localized = path.join(ROOT, first, '404', 'index.html');
const stats = await statFile(localized);
if (stats) return { file: localized, stats };
}
const fallback = path.join(ROOT, '404.html');
const stats = await statFile(fallback);
return stats ? { file: fallback, stats } : null;
}
/** nginx's ETag format: hex mtime and hex size, strong. Cheap, and changes on rebuild. */
function etagFor(stats) {
return `"${Math.floor(stats.mtimeMs / 1000).toString(16)}-${stats.size.toString(16)}"`;
}
/** RFC 9110: a list of entity tags, or `*`. Weak comparison is right for GET. */
function etagMatches(header, etag) {
if (!header) return false;
if (header.trim() === '*') return true;
const bare = etag.replace(/^W\//, '');
return header
.split(',')
.map((candidate) => candidate.trim().replace(/^W\//, ''))
.includes(bare);
}
function notModified(req, etag, lastModified) {
if (etagMatches(req.headers['if-none-match'], etag)) return true;
// Only consulted when the client sent no ETag, per RFC 9110 §13.1.3.
if (req.headers['if-none-match']) return false;
const since = Date.parse(req.headers['if-modified-since'] ?? '');
return Number.isFinite(since) && Math.floor(lastModified / 1000) * 1000 <= since;
}
/**
* Headers every response carries.
*
* No CSP here on purpose. The site loads an analytics script from another origin, the
* review islands open websockets to whatever relays are configured and the status
* islands fetch whatever mint URLs the index holds, so a policy tight enough to be
* worth having has to be derived from those lists rather than guessed at — and a wrong
* one fails as a silently broken island. HSTS belongs to nginx, which is what actually
* terminates TLS.
*/
function baseHeaders() {
return {
'X-Content-Type-Options': 'nosniff',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'X-Frame-Options': 'DENY',
};
}
function send(res, status, headers, body) {
res.writeHead(status, { ...baseHeaders(), ...headers });
res.end(body);
}
/** Stream a file, or just its headers for HEAD. */
function sendFile(req, res, status, file, stats, urlPath) {
const ext = path.extname(file).toLowerCase();
const etag = etagFor(stats);
const headers = {
'Content-Type': TYPES.get(ext) ?? 'application/octet-stream',
'Content-Length': stats.size,
'Last-Modified': new Date(stats.mtimeMs).toUTCString(),
ETag: etag,
'Cache-Control': cacheControl(urlPath, ext),
...baseHeaders(),
};
// A 304 must not carry a body or a Content-Length describing one.
if (status === 200 && notModified(req, etag, stats.mtimeMs)) {
delete headers['Content-Length'];
delete headers['Content-Type'];
res.writeHead(304, headers);
res.end();
return;
}
res.writeHead(status, headers);
if (req.method === 'HEAD') {
res.end();
return;
}
const stream = fs.createReadStream(file);
stream.on('error', (err) => {
log('error', 'read failed', { file, err: err.message });
res.destroy();
});
// Kill the read when the client hangs up mid-transfer rather than draining the file.
res.on('close', () => stream.destroy());
stream.pipe(res);
}
async function handle(req, res) {
if (req.method !== 'GET' && req.method !== 'HEAD') {
send(res, 405, { Allow: 'GET, HEAD', 'Content-Type': 'text/plain; charset=utf-8' }, 'Method Not Allowed\n');
return;
}
let urlPath;
try {
urlPath = decodeURIComponent(new URL(req.url, 'http://localhost').pathname);
} catch {
send(res, 400, { 'Content-Type': 'text/plain; charset=utf-8' }, 'Bad Request\n');
return;
}
const segments = safePath(urlPath);
if (!segments) {
send(res, 400, { 'Content-Type': 'text/plain; charset=utf-8' }, 'Bad Request\n');
return;
}
for (const candidate of candidates(segments)) {
const file = path.join(ROOT, candidate);
// Belt and braces: safePath already refused `..`, this refuses anything that still
// resolved outside the tree, a symlink in the build output included.
if (file !== ROOT && !file.startsWith(ROOT + path.sep)) break;
const stats = await statFile(file);
if (stats) {
sendFile(req, res, 200, file, stats, urlPath);
return;
}
}
const miss = await notFoundBody(segments);
if (!miss) {
send(res, 404, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': REVALIDATE }, 'Not Found\n');
return;
}
// Served as a 404, not a 200 with a 404-shaped body: a soft 404 gets every typo'd
// URL indexed as a real page.
sendFile(req, res, 404, miss.file, miss.stats, urlPath);
}
export function createServer() {
const server = http.createServer((req, res) => {
handle(req, res).catch((err) => {
log('error', 'request failed', { path: req.url, err: err.message });
if (!res.headersSent) {
send(res, 500, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-store' }, 'Internal Server Error\n');
} else {
res.destroy();
}
});
});
/*
* Longer than nginx's upstream keepalive, and headersTimeout longer still.
*
* If this end closes an idle connection at the same moment nginx reuses it, nginx has
* nothing to retry and reports 502. Outlasting the proxy makes the proxy always the
* one to close, which is the race-free direction.
*/
server.keepAliveTimeout = 65_000;
server.headersTimeout = 66_000;
// A malformed request line should not take the process with it.
server.on('clientError', (err, socket) => {
if (err.code === 'ECONNRESET' || !socket.writable) return;
socket.end('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
});
return server;
}
/**
* Refuse to start on an empty or unreadable root.
*
* The failure this prevents is the one that is hardest to see: a process that starts
* cleanly, answers every request with a 404 and looks healthy to anything watching the
* port. Exiting non-zero puts the reason in `systemctl status` instead.
*/
async function checkRoot() {
const index = path.join(ROOT, 'index.html');
if (await statFile(index)) return;
log('error', 'web root has no index.html', {
root: ROOT,
hint: 'a manual `pnpm build` only writes web/dist; `systemctl start cashumints-web` builds and publishes to WEB_ROOT',
});
process.exit(1);
}
/** Only when run directly, so the tests can import the pieces above. */
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
await checkRoot();
const server = createServer();
server.listen(PORT, HOST, () => {
log('info', 'site listening', { host: HOST, port: PORT, root: ROOT });
});
let shuttingDown = false;
for (const signal of ['SIGTERM', 'SIGINT']) {
process.on(signal, () => {
if (shuttingDown) return;
shuttingDown = true;
log('info', 'shutting down', { signal });
server.close(() => process.exit(0));
// Idle keep-alive connections would otherwise hold the close open for a minute.
server.closeIdleConnections();
setTimeout(() => {
server.closeAllConnections();
process.exit(0);
}, 10_000).unref();
});
}
}
+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>
+492
View File
@@ -0,0 +1,492 @@
---
/**
* 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 invitation and the two review prompts all differ per ecosystem, and
* all of them 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; lede: 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')),
lede: t(key('reviews.dialog.lede')),
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} lede={initial.lede} 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 ledeBox = dialog.querySelector<HTMLElement>('[data-write-lede]')!;
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; lede: string; body: string }
> => {
try {
return JSON.parse(dialog.dataset['rbuWords'] ?? '{}') as Record<
string,
{ label: string; placeholder: string; lede: 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;
ledeBox.textContent = wording.lede;
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 ---------- */
/**
* Cross-fade to the other step, and hand back the moment it is in the flow.
*
* `onShown` is where anything that focuses belongs: a hidden element cannot take
* focus, so setting up the incoming step before this callback runs would put the
* cursor on the document body and leave the reader pressing Tab to find the form.
* It runs before the pane is painted, so the step is never seen half-arranged.
*/
function showStep(next: 'identify' | 'review', onShown?: () => void): 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;
onShown?.();
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');
onShown?.();
// 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>
+213 -510
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;
@@ -37,16 +44,19 @@ const f = formatters(t);
const dist = distribution; const dist = distribution;
/* /*
* The placeholder asks about the thing being reviewed, which is the one sentence in * Three strings ask about the thing being reviewed, and none of them can be
* this panel that cannot be ecosystem-neutral without going vague: "how did minting and * ecosystem-neutral without going vague: "how did minting and melting work out" means
* melting work out" means nothing about a federation, and "how did it go" means nothing * nothing about a federation, "rate this mint" is the wrong noun for one, and "how did
* about anything. One key per ecosystem, falling back to the Cashu wording for a type * it go" means nothing about anything. One key per ecosystem each, falling back to the
* whose own has not been written yet. * Cashu wording for a type whose own has not been written yet.
*/ */
const placeholderKey = `reviews.dialog.bodyPlaceholder.${subject.type}`; const forEcosystem = (base: string): string => {
const bodyPlaceholder = t.has(placeholderKey) const key = `${base}.${subject?.type ?? 'cashu'}`;
? t(placeholderKey) return t.has(key) ? t(key) : t(`${base}.cashu`);
: t('reviews.dialog.bodyPlaceholder.cashu'); };
const bodyPlaceholder = forEcosystem('reviews.dialog.bodyPlaceholder');
const dialogLede = forEcosystem('reviews.dialog.lede');
const rateLabel = forEcosystem('reviews.rate');
const critical = dist['1'] + dist['2']; const critical = dist['1'] + dist['2'];
const fiveStar = dist['5']; const fiveStar = dist['5'];
@@ -98,7 +108,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>
@@ -128,6 +138,26 @@ function escapeText(value: string): string {
</div> </div>
</div> </div>
{/*
The way in, where the reviews are.
The header button at the top of the page is a long way from here, and by the time
somebody has read what other people wrote they are at the other end of the panel.
This is the same flow, opened the same way — one `[data-write-open]` among several
— with one addition: pressing a star opens the dialog with that star already
chosen, so a rating takes exactly one press.
One button rather than five, because five buttons cannot live inside one and a row
that is only reachable star by star is a row a keyboard cannot use. The stars are
spans; which one was pressed is read off the click.
*/}
<button class="rate-row" type="button" data-write-open>
<span class="rate-row-label">{rateLabel}</span>
<span class="rate-row-stars" aria-hidden="true">
{[1, 2, 3, 4, 5].map((value) => <span class="rate-star" data-rate={value}>★</span>)}
</span>
</button>
{/* One line of consequence when the toggle above is on: how much is showing. */} {/* One line of consequence when the toggle above is on: how much is showing. */}
<p class="anon-note" data-anon-note hidden></p> <p class="anon-note" data-anon-note hidden></p>
@@ -144,7 +174,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 +229,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} lede={dialogLede} 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,58 +239,50 @@ 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>
/* The write dialog's own styles live in global.css beside the other modals: the /* The write dialog's own styles live in global.css beside the other modals: the
identity row and the status line are injected by the island, and Astro's scoped identity row and the status line are injected by the island, and Astro's scoped
selectors do not reach markup added through innerHTML. */ selectors do not reach markup added through innerHTML. The row below is in the
template, so it is styled here, where it is read. */
/*
* The inline way in. A row rather than a button-shaped button: it spans the panel
* the way the head above it does, so it reads as part of the reviews rather than as
* a second copy of the header's action.
*/
.rate-row {
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
width: 100%; text-align: left; cursor: pointer;
padding: 12px 20px; border: none; border-bottom: 1px solid var(--line-soft);
background: none; color: var(--text); font-family: var(--body);
transition: background .15s;
}
.rate-row:hover { background: var(--card2); }
.rate-row:focus-visible { outline: 2px solid var(--amber); outline-offset: -2px; }
.rate-row-label { font-size: 13.5px; font-weight: 500; }
.rate-row-stars { display: flex; margin-left: auto; }
.rate-star {
font-size: 21px; line-height: 1; color: var(--line);
width: 30px; height: 30px;
display: inline-flex; align-items: center; justify-content: center;
transition: color .12s;
}
/* Fill the star under the pointer and every star before it, exactly as the picker in
the dialog does, so the row promises what the dialog then shows. */
.rate-row-stars:hover .rate-star:hover,
.rate-row-stars:hover .rate-star:has(~ .rate-star:hover) { color: var(--amber); }
/* Keyboard and touch never hover, and five grey stars read as switched off: the row
warms them when it has focus or under a thumb, without pretending a rating has
been chosen. */
.rate-row:focus-visible .rate-star,
.rate-row:active .rate-star { color: var(--amber-dim); }
@media (max-width: 480px) {
.rate-row { padding: 12px 16px; }
}
.load-row { padding: 16px 20px; display: flex; justify-content: center; border-top: 1px solid var(--line-soft); } .load-row { padding: 16px 20px; display: flex; justify-content: center; border-top: 1px solid var(--line-soft); }
.pager { display: flex; gap: 6px; align-items: center; font-size: 13.5px; flex-wrap: wrap; justify-content: center; } .pager { display: flex; gap: 6px; align-items: center; font-size: 13.5px; flex-wrap: wrap; justify-content: center; }
@@ -263,13 +293,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 +362,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;
/** /**
@@ -360,14 +394,28 @@ function escapeText(value: string): string {
let pendingPaint = 0; let pendingPaint = 0;
/** Set for one paint after publishing, to mark the new card as it goes in. */ /** Set for one paint after publishing, to mark the new card as it goes in. */
let markNewest = false; let markNewest = false;
/** /** Reviews published in this session, shown at the top until relays echo them back. */
* Reviews published in this session, shown at the top until relays echo them back.
*
* Keyed by event id, holding how many relays took it: a review that only two of
* four relays accepted says so on its card, because that is a thing the author
* may want to act on.
*/
const optimistic = new Map<string, { accepted: number; total: number }>(); const optimistic = new Map<string, { accepted: number; total: number }>();
/** Optimistic cards whose brief publishing note is still visible. */
const publishingNotes = new Set<string>();
const PUBLISHING_NOTE_MS = 4_000;
const PUBLISHING_NOTE_FADE_MS = 220;
function dismissPublishingNote(id: string): void {
publishingNotes.delete(id);
const note = document.querySelector<HTMLElement>(
`[id="review-${id.toLowerCase()}"] .propagating`,
);
if (!note) return;
note.classList.add('is-out');
window.setTimeout(() => note.remove(), PUBLISHING_NOTE_FADE_MS);
}
function markOptimistic(id: string, accepted: number, total: number): void {
optimistic.set(id, { accepted, total });
publishingNotes.add(id);
window.setTimeout(() => dismissPublishingNote(id), PUBLISHING_NOTE_MS);
}
/* ---------- rendering ---------- */ /* ---------- rendering ---------- */
@@ -442,8 +490,7 @@ function escapeText(value: string): string {
target.innerHTML = slice target.innerHTML = slice
.map((review) => .map((review) =>
reviewHtml(review, f, { reviewHtml(review, f, {
propagating: optimistic.has(review.id), propagating: publishingNotes.has(review.id),
publishedTo: optimistic.get(review.id) ?? null,
permalink, permalink,
}), }),
) )
@@ -619,13 +666,34 @@ 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)) {
markOptimistic(handedForward.id, handedForward.accepted, 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 +799,86 @@ 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.
markOptimistic(published.id, published.accepted, 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', (event) => {
showStatus( // A star in the inline row carries the rating it stands for; every other
signer.method === 'extension' // trigger on the page is just "open it".
? t('reviews.dialog.waitingExtension') const star = (event.target as HTMLElement).closest<HTMLElement>('[data-rate]');
: t('reviews.dialog.waitingSigner'), writer.open(star ? { rating: Number(star.dataset['rate']) } : {});
);
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
+122
View File
@@ -0,0 +1,122 @@
---
/**
* The write-a-review form: a login gate, 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.
*
* Three states, only one of which is ever on screen:
*
* 1. **Gate.** No session and no NIP-07 extension: there is nothing to sign with, so
* the dialog asks for a login before it asks for anything else. Writing a review
* into a form that cannot publish it is the one outcome worth designing away.
* 2. **Stars.** The rating is the whole first step. It is the one thing every review
* must carry, it takes one tap, and it is what the rest of the form is *for*.
* 3. **The rest.** Body, identity and publish, revealed once a rating is chosen.
*
* 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 lede and placeholder are props for the same
* reason: "how did minting and melting work out" means nothing about a federation.
*/
import { useI18n, type Locale } from '../i18n';
interface Props {
locale: Locale;
/** The one line under the heading, chosen per ecosystem by the caller. */
lede: string;
/** 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, lede, 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" data-write-lede>{lede}</p>
{/*
The gate. Shown in place of everything below it when there is no signer at all, and
never on a page load: whether a reader has one is a browser fact the island checks
as the dialog opens. Its button is an ordinary `data-login-open`, so the same popup
the header opens is the one that opens here.
*/}
<div class="write-gate" data-write-gate hidden>
<p class="write-gate-title">{t('reviews.dialog.gate.title')}</p>
<p class="write-gate-lead">{t('reviews.dialog.gate.lead')}</p>
<div class="write-actions">
<button type="button" class="btn" data-write-close>{t('common.cancel')}</button>
<button type="button" class="btn primary" data-login-open>{t('login.open')}</button>
</div>
</div>
<div class="write-steps" data-write-steps>
<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>
))
}
{/*
The only "Choose a rating" on screen. It used to be said here and again beside
the publish button, which is one prompt too many for one decision.
*/}
<span class="star-label" data-star-label aria-live="polite">
{t('reviews.dialog.chooseRating')}
</span>
</fieldset>
<div class="write-rest" data-write-rest hidden>
<div class="write-id" data-write-identity></div>
<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>
{/*
What pressing that button actually does, beside the button rather than in the
heading: it is a consequence, and a consequence belongs next to the thing that
causes it, not above a form nobody has filled in yet.
*/}
<p class="write-permanent">{t('reviews.dialog.permanent')}</p>
</div>
</div>
</form>
+32 -2
View File
@@ -4,8 +4,17 @@ The terms this site keeps consistent across every language. Decide a term once h
then use it everywhere: a reader who meets "reseña" on the home page and "opinión" on a then use it everywhere: a reader who meets "reseña" on the home page and "opinión" on a
mint page has to work out whether they are the same thing. mint page has to work out whether they are the same thing.
Adding a fourth language starts here, not in the catalog. Fill in the column, then Adding a language starts here, not in the catalog. Fill in the column, then translate
translate `en.json` with it beside you. `en.json` with it beside you.
The table has columns for the three hand-written languages. The other twenty-one catalogs
were machine-translated in bulk, and only six of them (de, da, sv, id, vi, tr) have since
had the first row of this table applied throughout: in those, a mint is a `mint`. The
remaining fifteen keep the machine's word for it below the chrome, which is usually the
local term for a coin factory. A native pass on one of them means adding its column here
first, then working through `xx.json` against it. The house rules below hold in every
language, translated column or not, and the first row of the table is the one the machine
got wrong most often.
## House rules, in every language ## House rules, in every language
@@ -61,6 +70,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 +97,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
+164 -28
View File
@@ -34,25 +34,29 @@
"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": "اللغة",
"lang.switcher": "اختار لغة", "lang.switcher": "اختار لغة",
"footer.copyright": "0{year} Cashumints.space", "footer.copyright": "© {year} Cashumints.space",
"footer.allMints": "كل دور سكّ Cashu", "footer.allMints": "كل دور سكّ Cashu",
"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 وNIP-87", "footer.madeBy": "اتعمل بـ ❤️ بواسطة {azzamo} · بدعم Cashu وNIP-87",
"footer.warning": "النزلاء متحفظون الدار سكّ ممكن يختفي مع ماليك.", "footer.warning": "دور السكّ بتحفظ فلوسك عندها. أي دار سكّ ممكن تختفي بفلوسك.",
"footer.tagline": "تقييمات المجتمع لدور سكّ ecash، موقّعة على Nostr.",
"footer.col.explore": "استكشف",
"footer.col.site": "الموقع",
"footer.nav": "تذييل الصفحة",
"home.title": "Cashumints.space · لاقي دار سكّ Cashu تقدر تثق فيها", "home.title": "Cashumints.space · لاقي دار سكّ Cashu تقدر تثق فيها",
"home.description": "لاقي دار سكّ Cashu تقدر تثق فيها. عندنا {mints} دار سكّ مفهرسة و{reviews} مراجعة من المجتمع ومتوقعة على Nostr، مع حالة التشغيل المباشرة وNUTs المدعومة وبرنامج دار السكّ.", "home.description": "لاقي دار سكّ Cashu تقدر تثق فيها. عندنا {mints} دار سكّ مفهرسة و{reviews} مراجعة من المجتمع ومتوقعة على Nostr، مع حالة التشغيل المباشرة وNUTs المدعومة وبرنامج دار السكّ.",
"home.hero.title": "اعثر على الدار سكّ", "home.hero.title": "لاقي دار سكّ Cashu",
"home.hero.titleEm": "تقدر الثقة", "home.hero.titleEm": "تقدر الثقة",
"home.hero.lede": "كل دار سكّ على شبكة Nostr مع استعراضات حقيقية للمجتمع لا بوّابات، بس وقّعَ الاستعراضاتَ إنت يُمْكِنُ أَنْ تَتأكّدَ نفسك.", "home.hero.lede": "كل دار سكّ على شبكة Nostr مع استعراضات حقيقية للمجتمع لا بوّابات، بس وقّعَ الاستعراضاتَ إنت يُمْكِنُ أَنْ تَتأكّدَ نفسك.",
"home.search.label": "فتش الدار سكّ بالاسم أو النطاق أو URL", "home.search.label": "فتش الدار سكّ بالاسم أو النطاق أو URL",
@@ -89,7 +93,7 @@
"pulse.indexer": "الرقم القياسي", "pulse.indexer": "الرقم القياسي",
"pulse.connected": "متصل", "pulse.connected": "متصل",
"pulse.stale": "قديم", "pulse.stale": "قديم",
"mints.title": "جميع الدار سكّ اليكاكاوي الفضاء", "mints.title": "كل دور سكّ Cashu · Cashumints.space",
"mints.description": "كل دور سكّ Cashu وعددها {total} والمفهرسة من Nostr: ‏{online} أونلاين و{offline} أوفلاين، مع تقييمات المجتمع ووقت التشغيل وبرنامج دار السكّ. دوّر ورتّب القائمة كاملة.", "mints.description": "كل دور سكّ Cashu وعددها {total} والمفهرسة من Nostr: ‏{online} أونلاين و{offline} أوفلاين، مع تقييمات المجتمع ووقت التشغيل وبرنامج دار السكّ. دوّر ورتّب القائمة كاملة.",
"mints.heading": "كل دور سكّ Cashu", "mints.heading": "كل دور سكّ Cashu",
"mints.lede": "كل دار سكّ اكتشفت على شبكة Nostr بما في ذليك التي اختفت الدار سكّ الخارجي يبقى في القائمة حتى تقدر العثور عليها واستعراضها", "mints.lede": "كل دار سكّ اكتشفت على شبكة Nostr بما في ذليك التي اختفت الدار سكّ الخارجي يبقى في القائمة حتى تقدر العثور عليها واستعراضها",
@@ -114,6 +118,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",
@@ -261,6 +267,9 @@
"reviews.empty": "لا استعراضات بعد سيكون الأول ليكتابة واحدة.", "reviews.empty": "لا استعراضات بعد سيكون الأول ليكتابة واحدة.",
"reviews.emptyWritten": "لا استعراضات مكتوبة هنا بعد. سيكون الأول ليكتابة واحدة.", "reviews.emptyWritten": "لا استعراضات مكتوبة هنا بعد. سيكون الأول ليكتابة واحدة.",
"reviews.emptyFilter": "لا استعراضات تطابق ده المرشح", "reviews.emptyFilter": "لا استعراضات تطابق ده المرشح",
"reviews.rate.cashu": "قيّم جهة الإصدار دي",
"reviews.rate.fedimint": "قيّم الاتحاد ده",
"reviews.rate.lnurl": "قيّم جهة الإصدار دي",
"reviews.noscript": "(الاستعراضات موقعة على (Nostr وتحمّل في مركبك لذا دي القائمة تحتاج (جافاسكريت فالتقديرات والعدادات الواردة أعلاه تأتي من الموقع نفسه.", "reviews.noscript": "(الاستعراضات موقعة على (Nostr وتحمّل في مركبك لذا دي القائمة تحتاج (جافاسكريت فالتقديرات والعدادات الواردة أعلاه تأتي من الموقع نفسه.",
"reviews.relayError": "مينفعش أن تصل إلى النفقة. وقد تكون الاستعراضات غير كاملة.", "reviews.relayError": "مينفعش أن تصل إلى النفقة. وقد تكون الاستعراضات غير كاملة.",
"reviews.srRating": "{rating} من 5", "reviews.srRating": "{rating} من 5",
@@ -273,7 +282,7 @@
"reviews.anonNote": "دي أول مراجعة من الـnpub ده، ومفيش نشاط تاني ليه", "reviews.anonNote": "دي أول مراجعة من الـnpub ده، ومفيش نشاط تاني ليه",
"reviews.published": "نُشرت", "reviews.published": "نُشرت",
"reviews.publishedPartial": "نُشرت إلى {accepted} من {total} إعادة شحن.", "reviews.publishedPartial": "نُشرت إلى {accepted} من {total} إعادة شحن.",
"reviews.propagating": "البروغات، قد يستغرق الأمر لحظة للظهور في مكان آخر.", "reviews.propagating": "بننشر على موزّعات Nostr",
"reviews.summary.line.one": "{count} تصنيف دون تعليق: {breakdown}", "reviews.summary.line.one": "{count} تصنيف دون تعليق: {breakdown}",
"reviews.summary.line.other": "{count} تصنيف دون تعليق: {breakdown}", "reviews.summary.line.other": "{count} تصنيف دون تعليق: {breakdown}",
"reviews.summary.lineDay.one": "{count} تصنيف دون تعليق على {when}: {breakdown}", "reviews.summary.lineDay.one": "{count} تصنيف دون تعليق على {when}: {breakdown}",
@@ -287,7 +296,12 @@
"reviews.breakdown.1": "{n} نجم واحد", "reviews.breakdown.1": "{n} نجم واحد",
"reviews.breakdown.none": "{n} بدون تصنيف", "reviews.breakdown.none": "{n} بدون تصنيف",
"reviews.dialog.title": "راجع {name}", "reviews.dialog.title": "راجع {name}",
"reviews.dialog.note": "وقعت بمفتاح (Nostr) الخاص بك ونُشرت إلى المناورات العامة ومينفعش تحريرها أو حذفها.", "reviews.dialog.lede.cashu": "احكيلنا تجربتك مع جهة الإصدار دي.",
"reviews.dialog.lede.fedimint": "احكيلنا تجربتك مع الاتحاد ده.",
"reviews.dialog.lede.lnurl": "احكيلنا تجربتك مع جهة الإصدار دي.",
"reviews.dialog.permanent": "بيتنشر بشكل دائم على موزّعات Nostr العامة.",
"reviews.dialog.gate.title": "سجّل الدخول عشان تكتب تقييم",
"reviews.dialog.gate.lead": "التقييمات بتتوقّع بمفتاح Nostr بتاعك. المفتاح بيفضل في المُوقِّع، والموقع ده عمره ما بيشوفه.",
"reviews.dialog.ratingLegend": "تقديرك", "reviews.dialog.ratingLegend": "تقديرك",
"reviews.dialog.starsSr.one": "{n}", "reviews.dialog.starsSr.one": "{n}",
"reviews.dialog.starsSr.other": "{n} نجوم", "reviews.dialog.starsSr.other": "{n} نجوم",
@@ -308,7 +322,6 @@
"reviews.dialog.waitingSigner": "في انتظار توقيعك", "reviews.dialog.waitingSigner": "في انتظار توقيعك",
"reviews.dialog.confirmClose": "إغلاق ده الاستعراض؟ مشروعك محتفظ به حتى تغادر الموقع", "reviews.dialog.confirmClose": "إغلاق ده الاستعراض؟ مشروعك محتفظ به حتى تغادر الموقع",
"reviews.dialog.why.login": "تسجيل في النشر", "reviews.dialog.why.login": "تسجيل في النشر",
"reviews.dialog.why.rating": "اختيار تقدير",
"reviews.dialog.why.words": "أكتب بضع كلمات", "reviews.dialog.why.words": "أكتب بضع كلمات",
"reviews.dialog.why.more.one": "{n} شخصية أخرى", "reviews.dialog.why.more.one": "{n} شخصية أخرى",
"reviews.dialog.why.more.other": "{n} المزيد من الشخصيات", "reviews.dialog.why.more.other": "{n} المزيد من الشخصيات",
@@ -324,8 +337,8 @@
"reviews.validate.rating": "اختار تقدير من 1 إلى 5 نجوم", "reviews.validate.rating": "اختار تقدير من 1 إلى 5 نجوم",
"reviews.validate.short": "أكتب ما لا يقل عن {n} شخصية ليكي يكون الاستعراض مفيداً.", "reviews.validate.short": "أكتب ما لا يقل عن {n} شخصية ليكي يكون الاستعراض مفيداً.",
"reviews.validate.long": "وتقتصر عمليات الاستعراض على {n} شخصية.", "reviews.validate.long": "وتقتصر عمليات الاستعراض على {n} شخصية.",
"feed.title": "آخر استعراضات الدار سكّ والاتحادات الفضاء", "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": "كل استعراض ليكل اتحاد من اتحادات الدار سكّ والFedimint، أولها، يقرأ مباشرة من نوتستر. ويوقع كل واحد منهم من جانب مؤلفه ويعيش في المناورات، ومش في قاعدة بياناتنا.", "feed.lede": "كل استعراض ليكل اتحاد من اتحادات الدار سكّ والFedimint، أولها، يقرأ مباشرة من نوتستر. ويوقع كل واحد منهم من جانب مؤلفه ويعيش في المناورات، ومش في قاعدة بياناتنا.",
"feed.label": "الاستعراضات", "feed.label": "الاستعراضات",
@@ -342,7 +355,7 @@
"feed.relayError": "لم أستطع الوصول إلى التوابيت عرض الاستعراضات من المبنى الأخير.", "feed.relayError": "لم أستطع الوصول إلى التوابيت عرض الاستعراضات من المبنى الأخير.",
"feed.relayEmpty": "لم تعد أي استعراضات من المؤخرات عرض الاستعراضات من المبنى الأخير.", "feed.relayEmpty": "لم تعد أي استعراضات من المؤخرات عرض الاستعراضات من المبنى الأخير.",
"feed.reading": "اقرأ التوابيت", "feed.reading": "اقرأ التوابيت",
"login.open": "اسحب", "login.open": "تسجيل الدخول",
"login.title": "اركب مع (Nostr)", "login.title": "اركب مع (Nostr)",
"login.note": "مفتاحك يبقى في موقّعك الموقع ده لا يستطيع رؤيته", "login.note": "مفتاحك يبقى في موقّعك الموقع ده لا يستطيع رؤيته",
"login.extension.name": "التمديد", "login.extension.name": "التمديد",
@@ -369,6 +382,10 @@
"login.status.waitingSigner": "في انتظار توقيعك", "login.status.waitingSigner": "في انتظار توقيعك",
"login.status.connecting": "الاتصال بعلامتك", "login.status.connecting": "الاتصال بعلامتك",
"login.status.connected": "متصل", "login.status.connected": "متصل",
"login.toast.loggedIn": "تم تسجيل الدخول",
"login.toast.fetchingProfile": "بنجيب بروفايلك على Nostr من المرحّلات…",
"login.toast.profile": "البروفايل: {name}",
"login.toast.noProfile": "مفيش بروفايل اتلاقى على المرحّلات",
"login.method.extension": "التمديد", "login.method.extension": "التمديد",
"login.method.remote": "عن بُعد", "login.method.remote": "عن بُعد",
"login.method.primal": "Primal", "login.method.primal": "Primal",
@@ -389,8 +406,8 @@
"signer.error.timeout": "موقّعك لم يجيب في الوقت المناسب افتحه، تأكد أنه لسه متصلاً ثم حاول تاني", "signer.error.timeout": "موقّعك لم يجيب في الوقت المناسب افتحه، تأكد أنه لسه متصلاً ثم حاول تاني",
"signer.error.cancelled": "تم إلغاء الإتصال", "signer.error.cancelled": "تم إلغاء الإتصال",
"signer.error.declined": "رفض طلب التوقيع", "signer.error.declined": "رفض طلب التوقيع",
"notfound.title": "لم يعثر على كشلومين. الفضاء", "notfound.title": "الصفحة مش موجودة · Cashumints.space",
"notfound.description": "الصفحة دي مفيش على اليكشومة. الفضاء", "notfound.description": "الصفحة دي مش موجودة على Cashumints.space.",
"notfound.heading": "الصفحة دي غير موجودة", "notfound.heading": "الصفحة دي غير موجودة",
"notfound.lede": "قد يكون قد تحرك، أو قد لا يتم فهرسة الدار سكّ بعد. ابحث عن دار سكّ بالاسم أو المجال، أو عجينة الدار سكّ.", "notfound.lede": "قد يكون قد تحرك، أو قد لا يتم فهرسة الدار سكّ بعد. ابحث عن دار سكّ بالاسم أو المجال، أو عجينة الدار سكّ.",
"notfound.browse": "فجر كل الدار سكّ", "notfound.browse": "فجر كل الدار سكّ",
@@ -400,8 +417,15 @@
"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 بعد آخر بناء للموقع، لذا هذا ملخّص من الواجهة الحية، ومراجعاته أدناه. تظهر الصفحة الكاملة عند البناء التالي.",
"wallets.title": "محفظات اليكاكاو الفضاء", "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.description": "محفظات (Cashu) التي تعمل مع الدار سكّ المدرجة هنا عبر الهواتف المحمولة والشبكات والبروزر وخط القيادة", "wallets.description": "محفظات (Cashu) التي تعمل مع الدار سكّ المدرجة هنا عبر الهواتف المحمولة والشبكات والبروزر وخط القيادة",
"wallets.heading": "الجدار", "wallets.heading": "الجدار",
"wallets.lede": "تحتاج إلى محفظة لحجز أي من دي الدار سكّ معظم المحافظ تتحدث مع العديد من الدار سكّ في آن واحد، لذليك إنت لا ترتبط مع أول واحد تحاول.", "wallets.lede": "تحتاج إلى محفظة لحجز أي من دي الدار سكّ معظم المحافظ تتحدث مع العديد من الدار سكّ في آن واحد، لذليك إنت لا ترتبط مع أول واحد تحاول.",
@@ -452,8 +476,8 @@
"about.numbers.p1": "عندنا {mints} دار سكّ مفهرسة، {online} منها بيرد، و{reviews} مراجعة محسوبة من المرحّلات اللي فوق.", "about.numbers.p1": "عندنا {mints} دار سكّ مفهرسة، {online} منها بيرد، و{reviews} مراجعة محسوبة من المرحّلات اللي فوق.",
"about.source.title": "المصدر والائتمان", "about.source.title": "المصدر والائتمان",
"about.source.p1": "الموقع مفتوح المصدر على {github}، وبناه وبيشغله {azzamo}. لو فيه حاجة غلط في صفحة دار سكّ، صلّحها عند المصدر: بيانات دار السكّ جاية من {code} بتاعها، والمراجعات جاية من Nostr.", "about.source.p1": "الموقع مفتوح المصدر على {github}، وبناه وبيشغله {azzamo}. لو فيه حاجة غلط في صفحة دار سكّ، صلّحها عند المصدر: بيانات دار السكّ جاية من {code} بتاعها، والمراجعات جاية من Nostr.",
"terms.title": "(أ) مصطلحات الاستخدام؛ الفضاء", "terms.title": "شروط الاستخدام · Cashumints.space",
"terms.description": "(ب) شروط استخدام اليكشومين. الفضاء: ما هو الموقع، وما لا يفعله، ومن هو المسؤول عن ما. لا حسابات، لا أوامر، لا إنتساب إلى أي نعن", "terms.description": "شروط استخدام Cashumints.space: الموقع ده إيه، وبيعمل إيه ومش بيعمل إيه، ومين المسؤول عن إيه. مفيش حسابات، مفيش ضمانات، ومفيش أي ارتباط بأي دار سكّ.",
"terms.heading": "مدة الاستخدام", "terms.heading": "مدة الاستخدام",
"terms.lede": "{date} شروط دنيئة للموقع الذي يفهرس البيانات العامة بس آخر تحديث", "terms.lede": "{date} شروط دنيئة للموقع الذي يفهرس البيانات العامة بس آخر تحديث",
"terms.what.title": "ما الموقع ده", "terms.what.title": "ما الموقع ده",
@@ -479,8 +503,8 @@
"terms.changes.p1": "وممكن تتغير دي المصطلحات. التاريخ في القمة يقول لما فعلوا الموقع هو مصدر مفتوح، لذليك كل تغيير في الصفحة دي هو في تاريخ الالتزام.", "terms.changes.p1": "وممكن تتغير دي المصطلحات. التاريخ في القمة يقول لما فعلوا الموقع هو مصدر مفتوح، لذليك كل تغيير في الصفحة دي هو في تاريخ الالتزام.",
"terms.contact.title": "الاتصال", "terms.contact.title": "الاتصال",
"terms.contact.p1": "ومن الأفضل تقديم القضايا وطلبات التصفية في {github}. ويدير الموقع {azzamo}.", "terms.contact.p1": "ومن الأفضل تقديم القضايا وطلبات التصفية في {github}. ويدير الموقع {azzamo}.",
"privacy.title": "الخصوصية؟ الفضاء", "privacy.title": "الخصوصية · Cashumints.space",
"privacy.description": "الخصوصية على (كلومينتس)، الفضاء: لا حسابات، لا كوكيز تتبع، لا محللين. ويُستخدم التخزين المحلي في التقاط الصور ولا يُرسل أي شيء في أي مكان. الاستعراضات موقّعة في جهازك الخاص", "privacy.description": "الخصوصية على Cashumints.space: مفيش حسابات، مفيش كوكيز تتبّع، مفيش تحليلات. الـ localStorage بيتستخدم لتخزين البروفايلات مؤقتًا ومفيش أي حاجة بتتبعت لأي مكان. التقييمات بتتوقّع في المتصفح بتاعك.",
"privacy.heading": "الخصوصية", "privacy.heading": "الخصوصية",
"privacy.lede": "{date} نجمع القليل كما يمكن للموقع آخر تحديث", "privacy.lede": "{date} نجمع القليل كما يمكن للموقع آخر تحديث",
"privacy.noAccounts.title": "مفيش حسابات", "privacy.noAccounts.title": "مفيش حسابات",
@@ -507,7 +531,7 @@
"privacy.publishing.p1": "كتابة المراجعة بتحصل في متصفحك، مش على خادمنا. الحدث بيمضيه امتداد NIP-07 عندك (Alby أو nos2x أو شبيه) وبيتذاع من متصفحك للمرحّلات مباشرة. مفتاحك الخاص عمره ما بيسيب الامتداد والموقع مش بيشوفه. إحنا مابنبنيش إدارة مفاتيح خاصة ومابنطلبش مفتاح أو عبارة استرداد.", "privacy.publishing.p1": "كتابة المراجعة بتحصل في متصفحك، مش على خادمنا. الحدث بيمضيه امتداد NIP-07 عندك (Alby أو nos2x أو شبيه) وبيتذاع من متصفحك للمرحّلات مباشرة. مفتاحك الخاص عمره ما بيسيب الامتداد والموقع مش بيشوفه. إحنا مابنبنيش إدارة مفاتيح خاصة ومابنطلبش مفتاح أو عبارة استرداد.",
"privacy.permanent.title": "أي شيء تنشره لـ(Nostr) عام ودائم", "privacy.permanent.title": "أي شيء تنشره لـ(Nostr) عام ودائم",
"privacy.permanent.p1": "الجزء ده يستاهل تقراه مرتين. المراجعة حدث متوقّع على مرحّلات عامة. أول ما تتنشر، خلاص بقت عامة:", "privacy.permanent.p1": "الجزء ده يستاهل تقراه مرتين. المراجعة حدث متوقّع على مرحّلات عامة. أول ما تتنشر، خلاص بقت عامة:",
"privacy.permanent.li1": "أي شخص يستطيع قراءتها دلوقتي وبعد ذليك، باستخدام أيّ زبون Nostr. إنه مش \"عاماً عن \"اليكالومينتس\" إنه عام على الشبكة", "privacy.permanent.li1": "أي حد يقدر يقراها، دلوقتي وبعدين، بأي عميل Nostr. مش ”عامة على Cashumints.space“، دي عامة على الشبكة.",
"privacy.permanent.li2": "إنه مرتبط بشكل دائم بمفتاحك العام (Nostr) إلى جانب كل ما نشره المفتاح", "privacy.permanent.li2": "إنه مرتبط بشكل دائم بمفتاحك العام (Nostr) إلى جانب كل ما نشره المفتاح",
"privacy.permanent.li3": "حذفها طلب ومش أمر تقدر أن تنشري حدثاً للحذف وبعض البطاقات ستفي به ليكن النفقة حرّة في تجاهله وأي شخص قد نسخ الحدث معالجة استعراض منشور بصفة دائمة.", "privacy.permanent.li3": "حذفها طلب ومش أمر تقدر أن تنشري حدثاً للحذف وبعض البطاقات ستفي به ليكن النفقة حرّة في تجاهله وأي شخص قد نسخ الحدث معالجة استعراض منشور بصفة دائمة.",
"privacy.permanent.li4": "مينفعشنا حذفها ليك يمكننا أن نتوقف عن عرضه هنا، ذليك لا يغير شيئاً على البطولات.", "privacy.permanent.li4": "مينفعشنا حذفها ليك يمكننا أن نتوقف عن عرضه هنا، ذليك لا يغير شيئاً على البطولات.",
@@ -563,9 +587,9 @@
"seo.desc.statusLimits": "{status}، الحدود {limits}.", "seo.desc.statusLimits": "{status}، الحدود {limits}.",
"seo.desc.fedModules": "{status}. الوحدات: {modules}.", "seo.desc.fedModules": "{status}. الوحدات: {modules}.",
"seo.desc.fedModulesNetwork": "{status}. الوحدات: {modules} على {network}.", "seo.desc.fedModulesNetwork": "{status}. الوحدات: {modules} على {network}.",
"seo.mint.ogAlt.one": "{name}: {rating} نجوم من {n}", "seo.mint.ogAlt.one": "{name}: {rating} نجمة من {n} تقييم على Cashumints.space",
"seo.mint.ogAlt.other": "{name}: {rating} نجوم من {n} استعراضات بشأن اليكمائن.", "seo.mint.ogAlt.other": "{name}: {rating} نجمة من {n} تقييمات على Cashumints.space",
"seo.mint.ogAltNoReviews": "{name} بشأن اليكمائن", "seo.mint.ogAltNoReviews": "{name} على Cashumints.space",
"nav.home": "البيت", "nav.home": "البيت",
"mint.more.title": "المزيد من الدار سكّ", "mint.more.title": "المزيد من الدار سكّ",
"mint.more.all": "انظر كل الدار سكّ", "mint.more.all": "انظر كل الدار سكّ",
@@ -573,7 +597,7 @@
"status.announced": "بس", "status.announced": "بس",
"card.unnamedFederation": "الاتحاد غير المسمى", "card.unnamedFederation": "الاتحاد غير المسمى",
"card.notChecked": "لم يتم فحصها", "card.notChecked": "لم يتم فحصها",
"fedimints.title": "جميع الاتحادات الفيديوية الفضاء", "fedimints.title": "كل اتحادات Fedimint · Cashumints.space",
"fedimints.description": "كل اتحادات Fedimint وعددها {total} والمعلنة على Nostr: ‏{online} اتأكد إنها شغالة بفحص خارجي، و{announced} اتعلن عنها من غير تأكيد. تقييمات المجتمع والوحدات وأكواد الدعوة لكل اتحاد.", "fedimints.description": "كل اتحادات Fedimint وعددها {total} والمعلنة على Nostr: ‏{online} اتأكد إنها شغالة بفحص خارجي، و{announced} اتعلن عنها من غير تأكيد. تقييمات المجتمع والوحدات وأكواد الدعوة لكل اتحاد.",
"fedimints.heading": "جميع الاتحادات", "fedimints.heading": "جميع الاتحادات",
"fedimints.lede": "كل اتحاد Fedimint أعلن على شبكة Nostr ومفيش في الاتحاد نقطة نهاية خاصة بالمركز العام، لذا تأكد أن بعضها يجري من خلال شيك خارجي، والبقية مدرجة بالضبط كما أعلن عنها.", "fedimints.lede": "كل اتحاد Fedimint أعلن على شبكة Nostr ومفيش في الاتحاد نقطة نهاية خاصة بالمركز العام، لذا تأكد أن بعضها يجري من خلال شيك خارجي، والبقية مدرجة بالضبط كما أعلن عنها.",
@@ -654,5 +678,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 الخاصة بها غير قابلة للوصول أيضًا، فلم يكن ممكنًا سكّ أو صهر أي شيء."
} }
+146 -10
View File
@@ -34,14 +34,14 @@
"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": "اللغة",
"lang.switcher": "اختر لغة", "lang.switcher": "اختر لغة",
"footer.copyright": "© {year} Cashumints.space", "footer.copyright": "© {year} Cashumints.space",
"footer.allMints": "الكل جهات إصدار Cashu", "footer.allMints": "كل جهات إصدار Cashu",
"footer.allReviews": "جميع التقييمات", "footer.allReviews": "جميع التقييمات",
"footer.wallets": "المحافظ", "footer.wallets": "المحافظ",
"footer.about": "نبذة عن", "footer.about": "نبذة عن",
@@ -50,6 +50,10 @@
"footer.privacy": "الخصوصية", "footer.privacy": "الخصوصية",
"footer.madeBy": "صُمم بحب ❤️ بواسطة {azzamo} · مدعوم بواسطة Cashu وNIP-87", "footer.madeBy": "صُمم بحب ❤️ بواسطة {azzamo} · مدعوم بواسطة Cashu وNIP-87",
"footer.warning": "جهات الإصدار ائتمانية. يمكن لجهة إصدار أن تختفي بأموالك.", "footer.warning": "جهات الإصدار ائتمانية. يمكن لجهة إصدار أن تختفي بأموالك.",
"footer.tagline": "تقييمات المجتمع لجهات إصدار ecash، موقَّعة على Nostr.",
"footer.col.explore": "استكشاف",
"footer.col.site": "الموقع",
"footer.nav": "تذييل الصفحة",
"home.title": "Cashumints.space · ابحث عن جهة إصدار Cashu يمكنك الوثوق بها", "home.title": "Cashumints.space · ابحث عن جهة إصدار Cashu يمكنك الوثوق بها",
"home.description": "ابحث عن جهة إصدار Cashu يمكنك الوثوق بها. تمت فهرسة {mints} جهة إصدار مع {reviews} مراجعة مجتمعية موقعة على Nostr، إلى جانب حالة التشغيل المباشرة وNUTs المدعومة وبرمجيات جهة الإصدار.", "home.description": "ابحث عن جهة إصدار Cashu يمكنك الوثوق بها. تمت فهرسة {mints} جهة إصدار مع {reviews} مراجعة مجتمعية موقعة على Nostr، إلى جانب حالة التشغيل المباشرة وNUTs المدعومة وبرمجيات جهة الإصدار.",
"home.hero.title": "ابحث عن جهة إصدار Cashu", "home.hero.title": "ابحث عن جهة إصدار Cashu",
@@ -91,7 +95,7 @@
"pulse.stale": "قديم", "pulse.stale": "قديم",
"mints.title": "الكل جهات إصدار Cashu · Cashumints.space", "mints.title": "الكل جهات إصدار Cashu · Cashumints.space",
"mints.description": "جميع {total} وجهات إصدار Cashu المُفهرسة من شبكة Nostr، {online} متصل بالإنترنت و{offline} غير متصل بالإنترنت، مع تقييمات المجتمع ووقت التشغيل وجهة إصدار البرامج. ابحث في القائمة الكاملة وقم بفرزها.", "mints.description": "جميع {total} وجهات إصدار Cashu المُفهرسة من شبكة Nostr، {online} متصل بالإنترنت و{offline} غير متصل بالإنترنت، مع تقييمات المجتمع ووقت التشغيل وجهة إصدار البرامج. ابحث في القائمة الكاملة وقم بفرزها.",
"mints.heading": "الكل جهات إصدار Cashu", "mints.heading": "كل جهات إصدار Cashu",
"mints.lede": "جميع العناوين جهة إصدار التي تم اكتشافها على شبكة Nostr، بما في ذلك تلك التي لم تعد موجودة. أما العناوين جهات إصدار غير المتصلة بالإنترنت، فستظل مدرجة في القائمة حتى تتمكن من العثور عليها ومراجعتها.", "mints.lede": "جميع العناوين جهة إصدار التي تم اكتشافها على شبكة Nostr، بما في ذلك تلك التي لم تعد موجودة. أما العناوين جهات إصدار غير المتصلة بالإنترنت، فستظل مدرجة في القائمة حتى تتمكن من العثور عليها ومراجعتها.",
"mints.search.label": "ابحث عن جهات إصدار حسب الاسم أو النطاق", "mints.search.label": "ابحث عن جهات إصدار حسب الاسم أو النطاق",
"mints.search.placeholder": "البحث حسب الاسم أو النطاق…", "mints.search.placeholder": "البحث حسب الاسم أو النطاق…",
@@ -114,6 +118,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": "نسخ رابط جهة إصدار",
@@ -261,6 +267,9 @@
"reviews.empty": "لا توجد تعليقات حتى الآن. كن أول من يكتب تعليقًا.", "reviews.empty": "لا توجد تعليقات حتى الآن. كن أول من يكتب تعليقًا.",
"reviews.emptyWritten": "لا توجد تعليقات مكتوبة هنا حتى الآن. كن أول من يكتب تعليقًا.", "reviews.emptyWritten": "لا توجد تعليقات مكتوبة هنا حتى الآن. كن أول من يكتب تعليقًا.",
"reviews.emptyFilter": "لا توجد تقييمات تتطابق مع هذا الفلتر.", "reviews.emptyFilter": "لا توجد تقييمات تتطابق مع هذا الفلتر.",
"reviews.rate.cashu": "قيّم جهة الإصدار هذه",
"reviews.rate.fedimint": "قيّم هذا الاتحاد",
"reviews.rate.lnurl": "قيّم جهة الإصدار هذه",
"reviews.noscript": "يتم تسجيل التعليقات على Nostr ويتم تحميلها في متصفحك، لذا تحتاج هذه القائمة إلى JavaScript. أما التقييمات وأعداد التعليقات المذكورة أعلاه فهي مستمدة من الموقع نفسه.", "reviews.noscript": "يتم تسجيل التعليقات على Nostr ويتم تحميلها في متصفحك، لذا تحتاج هذه القائمة إلى JavaScript. أما التقييمات وأعداد التعليقات المذكورة أعلاه فهي مستمدة من الموقع نفسه.",
"reviews.relayError": "تعذر الوصول إلى جهاز الترحيل. قد تكون المراجعات غير مكتملة.", "reviews.relayError": "تعذر الوصول إلى جهاز الترحيل. قد تكون المراجعات غير مكتملة.",
"reviews.srRating": "{rating} من أصل 5", "reviews.srRating": "{rating} من أصل 5",
@@ -273,7 +282,7 @@
"reviews.anonNote": "أول تقييم من هذا npub، لم يتم العثور على أي نشاط آخر", "reviews.anonNote": "أول تقييم من هذا npub، لم يتم العثور على أي نشاط آخر",
"reviews.published": "تم النشر.", "reviews.published": "تم النشر.",
"reviews.publishedPartial": "تم النشر إلى {accepted} من أصل {total} مرحل.", "reviews.publishedPartial": "تم النشر إلى {accepted} من أصل {total} مرحل.",
"reviews.propagating": "أثناء النشر، قد يستغرق الأمر لحظة حتى يظهر في مكان آخر.", "reviews.propagating": "جارٍ النشر إلى مرحلات Nostr",
"reviews.summary.line.one": "{count} تقييم بدون تعليق: {breakdown}", "reviews.summary.line.one": "{count} تقييم بدون تعليق: {breakdown}",
"reviews.summary.line.other": "{count} تقييمًا بدون تعليق: {breakdown}", "reviews.summary.line.other": "{count} تقييمًا بدون تعليق: {breakdown}",
"reviews.summary.lineDay.one": "تقييم {count} بدون تعليق على {when}: {breakdown}", "reviews.summary.lineDay.one": "تقييم {count} بدون تعليق على {when}: {breakdown}",
@@ -287,7 +296,12 @@
"reviews.breakdown.1": "{n} نجمة واحدة", "reviews.breakdown.1": "{n} نجمة واحدة",
"reviews.breakdown.none": "{n} بدون تقييم", "reviews.breakdown.none": "{n} بدون تقييم",
"reviews.dialog.title": "التقييم {name}", "reviews.dialog.title": "التقييم {name}",
"reviews.dialog.note": "تم التوقيع عليه باستخدام مفتاحك Nostr ونشره على الموزعات العامة. ولا يمكن تعديله أو حذفه.", "reviews.dialog.lede.cashu": "شارك تجربتك مع جهة الإصدار هذه.",
"reviews.dialog.lede.fedimint": "شارك تجربتك مع هذا الاتحاد.",
"reviews.dialog.lede.lnurl": "شارك تجربتك مع جهة الإصدار هذه.",
"reviews.dialog.permanent": "يُنشر بشكل دائم على موزّعات Nostr العامة.",
"reviews.dialog.gate.title": "سجّل الدخول لكتابة تقييم",
"reviews.dialog.gate.lead": "تُوقَّع التقييمات بمفتاح Nostr الخاص بك. يبقى المفتاح في المُوقِّع، ولا يطّلع عليه هذا الموقع أبدًا.",
"reviews.dialog.ratingLegend": "تقييمك", "reviews.dialog.ratingLegend": "تقييمك",
"reviews.dialog.starsSr.one": "{n} نجمة", "reviews.dialog.starsSr.one": "{n} نجمة",
"reviews.dialog.starsSr.other": "{n} نجمة", "reviews.dialog.starsSr.other": "{n} نجمة",
@@ -308,7 +322,6 @@
"reviews.dialog.waitingSigner": "في انتظار الموقّع", "reviews.dialog.waitingSigner": "في انتظار الموقّع",
"reviews.dialog.confirmClose": "هل تريد إغلاق هذه المراجعة؟ سيتم الاحتفاظ بمسودتك حتى تغادر الموقع.", "reviews.dialog.confirmClose": "هل تريد إغلاق هذه المراجعة؟ سيتم الاحتفاظ بمسودتك حتى تغادر الموقع.",
"reviews.dialog.why.login": "تسجيل الدخول للنشر", "reviews.dialog.why.login": "تسجيل الدخول للنشر",
"reviews.dialog.why.rating": "اختر تقييمًا",
"reviews.dialog.why.words": "اكتب بضع كلمات", "reviews.dialog.why.words": "اكتب بضع كلمات",
"reviews.dialog.why.more.one": "{n} حرف إضافي", "reviews.dialog.why.more.one": "{n} حرف إضافي",
"reviews.dialog.why.more.other": "{n} حرف إضافي", "reviews.dialog.why.more.other": "{n} حرف إضافي",
@@ -325,7 +338,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": "التعليقات",
@@ -369,6 +382,10 @@
"login.status.waitingSigner": "في انتظار الموقّع", "login.status.waitingSigner": "في انتظار الموقّع",
"login.status.connecting": "الاتصال بالموقّع", "login.status.connecting": "الاتصال بالموقّع",
"login.status.connected": "متصل", "login.status.connected": "متصل",
"login.toast.loggedIn": "تم تسجيل الدخول",
"login.toast.fetchingProfile": "جارٍ جلب ملفك الشخصي على Nostr من خوادم الترحيل…",
"login.toast.profile": "الملف الشخصي: {name}",
"login.toast.noProfile": "لم يُعثر على ملف شخصي على خوادم الترحيل",
"login.method.extension": "التمديد", "login.method.extension": "التمديد",
"login.method.remote": "عن بُعد", "login.method.remote": "عن بُعد",
"login.method.primal": "Primal", "login.method.primal": "Primal",
@@ -400,7 +417,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 +678,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 الخاصة بها غير قابلة للوصول أيضًا، فلم يكن ممكنًا سكّ أو صهر أي شيء."
} }
+38
View File
@@ -58,6 +58,40 @@ export const LOCALES: LocaleInfo[] = [
{ code: 'vi', label: 'Tiếng Việt', intl: 'vi-VN', og: 'vi_VN' }, { code: 'vi', label: 'Tiếng Việt', intl: 'vi-VN', og: 'vi_VN' },
]; ];
/*
* All twenty-four, and what "shipping" one actually required.
*
* These locales spent a while listed here while `index.ts` imported only en, es and nl.
* That is the failure worth remembering, because it does not look like one: every other
* locale rendered the *English* catalog under its own `<html lang>`, its own URL prefix
* and a full reciprocal hreflang set. Not a half-translated page — twenty-one complete
* duplicates of the English site, each telling a crawler it was a different language.
* They were removed from this table rather than wired up, which fixed the SEO claim and
* left twenty-one finished catalogs on disk reaching nobody.
*
* They are back, and this time the catalog is imported and in `CATALOGS` alongside the
* entry here. Those two edits are one change; either alone is the bug above. The build
* cannot tell you when you make it — `catalogFor()` falls back to English by design —
* so the guard is `test/i18n-wiring.test.mjs`, which fails when a locale in this table
* has no catalog of its own behind it.
*
* Adding a language is therefore four things: the entry here, the code in `locales.mjs`,
* the import and `CATALOGS` line in `index.ts`, and `xx.json` itself.
*
* On translation quality: these catalogs were machine-translated in bulk and then
* repaired by hand across the strings that appear on every page — the brand name, the
* footer, the nav, the language switcher, the custody warning. The brand in particular
* was being translated as a phrase ("Καταιγίδες. Διάστημα", "ログイン スペース"), which is
* why `site.name` is now asserted identical to English in every catalog by the same
* test. Below the chrome, the body copy is still machine output: understandable, not
* idiomatic, and worth a native pass per language before anyone calls it finished.
*
* `ar` (Modern Standard) and `ar-eg` (Egyptian) are both here and both real catalogs
* rather than duplicates. Two RTL page sets do compete for a mostly-overlapping
* audience; that is a content decision to revisit with traffic in hand, not a reason to
* leave a finished catalog unreachable.
*/
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 +136,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',
+154 -20
View File
@@ -33,26 +33,30 @@
"status.unknown": "Ještě není zkontrolováno", "status.unknown": "Ještě není zkontrolováno",
"nav.skip": "Přeskočit na obsah", "nav.skip": "Přeskočit na obsah",
"nav.main": "Hlavní", "nav.main": "Hlavní",
"nav.mints": "Cashu mincovny", "nav.mints": "Cashu minty",
"nav.reviews": "Přezkum", "nav.reviews": "Recenze",
"nav.wallets": "Peněženky", "nav.wallets": "Peněženky",
"nav.about": "O", "nav.about": "O",
"nav.searchMints": "Hledat{n}mincovny", "nav.searchMints": "Hledat mezi {n} minty",
"lang.label": "Jazyk", "lang.label": "Jazyk",
"lang.switcher": "Vyberte si jazyk", "lang.switcher": "Vyberte si jazyk",
"footer.copyright": "©{year}Cashumints.space", "footer.copyright": "© {year} Cashumints.space",
"footer.allMints": "All Cashu mincovny", "footer.allMints": "Všechny Cashu minty",
"footer.allReviews": "Všechny recenze", "footer.allReviews": "Všechny recenze",
"footer.wallets": "Peněženky", "footer.wallets": "Peněženky",
"footer.about": "O", "footer.about": "O",
"footer.disclaimer": "Zveřejnění", "footer.disclaimer": "Právní upozornění",
"footer.terms": "Podmínky", "footer.terms": "Podmínky",
"footer.privacy": "Ochrana soukromí", "footer.privacy": "Ochrana soukromí",
"footer.madeBy": "Vyrobeno z pryže{azzamo}· Pohon Cashu a NIP-87", "footer.madeBy": "Vytvořeno s ❤️ od {azzamo} · Poháněno Cashu a NIP-87",
"footer.warning": "Mincovny jsou opatrovnictví. Mincovna může zmizet s vašimi penězi.", "footer.warning": "Minty jsou custodiální. Mint může zmizet s vašimi penězi.",
"home.title": "Cashumints.space · Najít Cashu mincovna můžete věřit", "footer.tagline": "Recenze komunity na ecash minty, podepsané na Nostru.",
"home.description": "Najděte mincovnu Cashu, které můžete důvěřovat. Indexujeme {mints} mincoven s {reviews} komunitními recenzemi podepsanými na Nostru, živou dostupností, podporovanými NUTy a softwarem mincovny.", "footer.col.explore": "Prozkoumat",
"home.hero.title": "Najít Cashu mincovna", "footer.col.site": "Web",
"footer.nav": "Zápatí",
"home.title": "Cashumints.space · Najdi Cashu mint, kterému můžeš věřit",
"home.description": "Najdi Cashu mint, kterému můžeš věřit. {mints} mintů v indexu, {reviews} komunitních recenzí podepsaných na Nostru, k tomu živá dostupnost, podporované NUTy a software mintu.",
"home.hero.title": "Najdi Cashu mint",
"home.hero.titleEm": "Můžeš věřit.", "home.hero.titleEm": "Můžeš věřit.",
"home.hero.lede": "Každá mincovna na síti Nostr, s opravdovými komunitními recenzemi. Žádné brány, jen podepsal recenze si můžete ověřit sami.", "home.hero.lede": "Každá mincovna na síti Nostr, s opravdovými komunitními recenzemi. Žádné brány, jen podepsal recenze si můžete ověřit sami.",
"home.search.label": "Hledat mincovny podle jména, domain, nebo URL", "home.search.label": "Hledat mincovny podle jména, domain, nebo URL",
@@ -91,7 +95,7 @@
"pulse.stale": "oschlý", "pulse.stale": "oschlý",
"mints.title": "Všechny Cashu mincovny · Cashumints.space", "mints.title": "Všechny Cashu mincovny · Cashumints.space",
"mints.description": "Všechny{total}Cashu mincovny indexovány z sítě Nostr,{online}online a{offline}Offline, s komunitou ratingstime, čas a mincovna software. hledání a třídit celý seznam.", "mints.description": "Všechny{total}Cashu mincovny indexovány z sítě Nostr,{online}online a{offline}Offline, s komunitou ratingstime, čas a mincovna software. hledání a třídit celý seznam.",
"mints.heading": "All Cashu mincovny", "mints.heading": "Všechny Cashu minty",
"mints.lede": "Každá mincovna objevil na síti Nostr, včetně těch, které jsou gone. offline mincovny zůstat uveden, takže můžete stále najít a zkontrolovat je.", "mints.lede": "Každá mincovna objevil na síti Nostr, včetně těch, které jsou gone. offline mincovny zůstat uveden, takže můžete stále najít a zkontrolovat je.",
"mints.search.label": "Hledat mincovny podle názvu nebo domény", "mints.search.label": "Hledat mincovny podle názvu nebo domény",
"mints.search.placeholder": "Hledání podle jména nebo původu", "mints.search.placeholder": "Hledání podle jména nebo původu",
@@ -116,6 +120,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",
@@ -277,6 +283,9 @@
"reviews.empty": "Ještě žádné recenze. Buďte první, kdo napsat jeden.", "reviews.empty": "Ještě žádné recenze. Buďte první, kdo napsat jeden.",
"reviews.emptyWritten": "Už žádné písemné recenze. Buďte první, kdo napsat jeden.", "reviews.emptyWritten": "Už žádné písemné recenze. Buďte první, kdo napsat jeden.",
"reviews.emptyFilter": "Žádné recenze se neshodují s tímto filmem.", "reviews.emptyFilter": "Žádné recenze se neshodují s tímto filmem.",
"reviews.rate.cashu": "Ohodnoťte tento mint",
"reviews.rate.fedimint": "Ohodnoťte tuto federaci",
"reviews.rate.lnurl": "Ohodnoťte tento mint",
"reviews.noscript": "Recenze jsou podepsány na Nostr a načíst do browseru, takže tento seznam potřebuje JavaScript. Hodnocení a počty výše pocházejí z webu sám.", "reviews.noscript": "Recenze jsou podepsány na Nostr a načíst do browseru, takže tento seznam potřebuje JavaScript. Hodnocení a počty výše pocházejí z webu sám.",
"reviews.relayError": "Nelze dosáhnout relé. Recenze mohou být nedokončené.", "reviews.relayError": "Nelze dosáhnout relé. Recenze mohou být nedokončené.",
"reviews.srRating": "{rating}z 5", "reviews.srRating": "{rating}z 5",
@@ -289,7 +298,7 @@
"reviews.anonNote": "První přezkum z tohoto npub, žádná jiná činnost nenalezena", "reviews.anonNote": "První přezkum z tohoto npub, žádná jiná činnost nenalezena",
"reviews.published": "Publikováno.", "reviews.published": "Publikováno.",
"reviews.publishedPartial": "Zveřejněno{accepted}z{total}relé.", "reviews.publishedPartial": "Zveřejněno{accepted}z{total}relé.",
"reviews.propagating": "Propagatování může chvíli trvat, než se objeví jinde.", "reviews.propagating": "Publikování na Nostr relayích",
"reviews.summary.line.one": "{count}hodnocení bez komentáře:{breakdown}", "reviews.summary.line.one": "{count}hodnocení bez komentáře:{breakdown}",
"reviews.summary.line.other": "{count}ratingy bez komentáře:{breakdown}", "reviews.summary.line.other": "{count}ratingy bez komentáře:{breakdown}",
"reviews.summary.line.few": "{count}ratingy bez komentáře:{breakdown}", "reviews.summary.line.few": "{count}ratingy bez komentáře:{breakdown}",
@@ -307,7 +316,12 @@
"reviews.breakdown.1": "{n}jedna hvězda", "reviews.breakdown.1": "{n}jedna hvězda",
"reviews.breakdown.none": "{n}bez ratingu", "reviews.breakdown.none": "{n}bez ratingu",
"reviews.dialog.title": "Přezkum{name}", "reviews.dialog.title": "Přezkum{name}",
"reviews.dialog.note": "Podepsáno vaším klíčem Nostr a zveřejněno pro veřejné relé. Nelze jej editovat nebo smazat.", "reviews.dialog.lede.cashu": "Podělte se o svou zkušenost s tímto mintem.",
"reviews.dialog.lede.fedimint": "Podělte se o svou zkušenost s touto federací.",
"reviews.dialog.lede.lnurl": "Podělte se o svou zkušenost s tímto mintem.",
"reviews.dialog.permanent": "Zveřejněno natrvalo na veřejných Nostr relayích.",
"reviews.dialog.gate.title": "Přihlaste se, abyste mohli napsat recenzi",
"reviews.dialog.gate.lead": "Recenze se podepisují vaším klíčem Nostr. Zůstává ve vašem signeru, tato stránka ho nikdy nevidí.",
"reviews.dialog.ratingLegend": "Vaše hodnocení", "reviews.dialog.ratingLegend": "Vaše hodnocení",
"reviews.dialog.starsSr.one": "{n}hvězda", "reviews.dialog.starsSr.one": "{n}hvězda",
"reviews.dialog.starsSr.other": "{n}hvězdy", "reviews.dialog.starsSr.other": "{n}hvězdy",
@@ -330,7 +344,6 @@
"reviews.dialog.waitingSigner": "Čekám na váš podpis.", "reviews.dialog.waitingSigner": "Čekám na váš podpis.",
"reviews.dialog.confirmClose": "Váš návrh zůstane, dokud neopustíte místo.", "reviews.dialog.confirmClose": "Váš návrh zůstane, dokud neopustíte místo.",
"reviews.dialog.why.login": "Přihlásit se k publikování", "reviews.dialog.why.login": "Přihlásit se k publikování",
"reviews.dialog.why.rating": "Vyberte si rating",
"reviews.dialog.why.words": "Napiš pár slov.", "reviews.dialog.why.words": "Napiš pár slov.",
"reviews.dialog.why.more.one": "{n}více charakteru", "reviews.dialog.why.more.one": "{n}více charakteru",
"reviews.dialog.why.more.other": "{n}více znaků", "reviews.dialog.why.more.other": "{n}více znaků",
@@ -351,7 +364,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",
@@ -397,6 +410,10 @@
"login.status.waitingSigner": "Čekám na váš podpis.", "login.status.waitingSigner": "Čekám na váš podpis.",
"login.status.connecting": "Připojení k vašemu signeru", "login.status.connecting": "Připojení k vašemu signeru",
"login.status.connected": "Připojeno", "login.status.connected": "Připojeno",
"login.toast.loggedIn": "Přihlášeno",
"login.toast.fetchingProfile": "Načítání vašeho profilu Nostr z relé…",
"login.toast.profile": "Profil: {name}",
"login.toast.noProfile": "Na relé nebyl nalezen žádný profil",
"login.method.extension": "Rozšíření", "login.method.extension": "Rozšíření",
"login.method.remote": "Vzdálené", "login.method.remote": "Vzdálené",
"login.method.primal": "Primal", "login.method.primal": "Primal",
@@ -428,7 +445,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",
@@ -584,8 +608,8 @@
"seo.mint.titleOffline.other": "{name}: offline pro{n}Dny od Cashumints.space", "seo.mint.titleOffline.other": "{name}: offline pro{n}Dny od Cashumints.space",
"seo.mint.titleOffline.few": "{name}: offline pro{n}Dny od Cashumints.space", "seo.mint.titleOffline.few": "{name}: offline pro{n}Dny od Cashumints.space",
"seo.mint.titleOffline.many": "{name}: offline pro{n}Dny od Cashumints.space", "seo.mint.titleOffline.many": "{name}: offline pro{n}Dny od Cashumints.space",
"seo.mint.titleNoReviews": "{name}: Cashu mincovna info a recenze", "seo.mint.titleNoReviews": "{name}: informace a recenze Cashu mintu | Cashumints.space",
"seo.fedimint.titleNoReviews": "{name}: Fedigt federation info a recenze", "seo.fedimint.titleNoReviews": "{name}: informace a recenze federace Fedimint | Cashumints.space",
"seo.desc.fallback": "Hodnocení Společenství pro{name}({domain}Izzie.", "seo.desc.fallback": "Hodnocení Společenství pro{name}({domain}Izzie.",
"seo.desc.fallbackDomain": "Recenze Společenství pro Cashu mincovna at{domain}.", "seo.desc.fallbackDomain": "Recenze Společenství pro Cashu mincovna at{domain}.",
"seo.desc.rated.one": "Jmenovité{rating}5 od{n}Podepsal Nostr recenzi.", "seo.desc.rated.one": "Jmenovité{rating}5 od{n}Podepsal Nostr recenzi.",
@@ -696,5 +720,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."
} }
+251 -121
View File
@@ -33,15 +33,15 @@
"status.unknown": "Ikke kontrolleret endnu", "status.unknown": "Ikke kontrolleret endnu",
"nav.skip": "Gå direkte til indholdet", "nav.skip": "Gå direkte til indholdet",
"nav.main": "Hovedside", "nav.main": "Hovedside",
"nav.mints": "Cashu-pastiller", "nav.mints": "Cashu-mints",
"nav.reviews": "Anmeldelser", "nav.reviews": "Anmeldelser",
"nav.wallets": "Punge", "nav.wallets": "Punge",
"nav.about": "Om", "nav.about": "Om",
"nav.searchMints": "Søg efter {n}-pastiller", "nav.searchMints": "Søg blandt {n} mints",
"lang.label": "Sprog", "lang.label": "Sprog",
"lang.switcher": "Vælg et sprog", "lang.switcher": "Vælg et sprog",
"footer.copyright": "© {year} Cashumints.space", "footer.copyright": "© {year} Cashumints.space",
"footer.allMints": "Alle Cashu-mønter", "footer.allMints": "Alle Cashu-mints",
"footer.allReviews": "Alle anmeldelser", "footer.allReviews": "Alle anmeldelser",
"footer.wallets": "Punge", "footer.wallets": "Punge",
"footer.about": "Om", "footer.about": "Om",
@@ -49,21 +49,25 @@
"footer.terms": "Vilkår", "footer.terms": "Vilkår",
"footer.privacy": "Privatliv", "footer.privacy": "Privatliv",
"footer.madeBy": "Lavet med ❤️ af {azzamo} · Drevet af Cashu og NIP-87", "footer.madeBy": "Lavet med ❤️ af {azzamo} · Drevet af Cashu og NIP-87",
"footer.warning": "Møntværksteder fungerer som depot. Et møntværksted kan forsvinde med dine penge.", "footer.warning": "Mints er depotbaserede. Et mint kan forsvinde med dine penge.",
"home.title": "Cashumints.space · Find en Cashu i perfekt stand, som du kan stole på", "footer.tagline": "Anmeldelser fra fællesskabet af ecash-mints, signeret på Nostr.",
"home.description": "Find en Cashu-mining-platform, du kan stole på. {mints}-mining-platforme, der er indekseret med {reviews}-brugeranmeldelser, godkendt på Nostr, samt oplysninger om realtidsdriftstid, understøttet NUTs og mining-software.", "footer.col.explore": "Udforsk",
"home.hero.title": "Find en Cashu i perfekt stand", "footer.col.site": "Websted",
"footer.nav": "Sidefod",
"home.title": "Cashumints.space · Find et Cashu-mint du kan stole på",
"home.description": "Find et Cashu-mint du kan stole på. {mints} mints i indekset med {reviews} anmeldelser fra fællesskabet, signeret på Nostr, plus live oppetid, understøttede NUTs og mint-software.",
"home.hero.title": "Find et Cashu-mint",
"home.hero.titleEm": "du kan stole på", "home.hero.titleEm": "du kan stole på",
"home.hero.lede": "Alle butikker på Nostr-netværket, med ægte anmeldelser fra brugerne. Ingen mellemmænd – kun underskrevne anmeldelser, som du selv kan kontrollere.", "home.hero.lede": "Hvert mint på Nostr-netværket, med rigtige anmeldelser fra fællesskabet. Ingen gatekeepere, bare signerede anmeldelser du selv kan verificere.",
"home.search.label": "Søg efter hjemmesider efter navn, domæne eller URL", "home.search.label": "Søg efter mints på navn, domæne eller URL",
"home.search.placeholder": "Søg efter navn, domæne, eller indsæt en Mint-URL…", "home.search.placeholder": "Søg efter navn, domæne, eller indsæt en Mint-URL…",
"home.filters.mostReviewed": "Mest anmeldte", "home.filters.mostReviewed": "Mest anmeldte",
"home.filters.highestRated": "Bedst bedømte", "home.filters.highestRated": "Bedst bedømte",
"home.filters.recentlyActive": "Senest aktive", "home.filters.recentlyActive": "Senest aktive",
"home.filters.websockets": "Understøtter WebSockets", "home.filters.websockets": "Understøtter WebSockets",
"home.top.title": "De bedste Cashu-pastiller", "home.top.title": "De bedste Cashu-mints",
"home.top.sub": "rangeret efter anmeldelser og bedømmelser", "home.top.sub": "rangeret efter anmeldelser og bedømmelser",
"home.top.all": "Alle {n}-mynter →", "home.top.all": "Alle {n} mints →",
"home.latest.title": "Seneste anmeldelser", "home.latest.title": "Seneste anmeldelser",
"home.latest.sub": "live fra Nostr-netværket", "home.latest.sub": "live fra Nostr-netværket",
"home.latest.prev": "Tidligere anmeldelser", "home.latest.prev": "Tidligere anmeldelser",
@@ -77,33 +81,33 @@
"home.why.verify.title": "Tjek det selv", "home.why.verify.title": "Tjek det selv",
"home.why.verify.body": "Anmeldelserne findes på de offentlige Nostr-relæer, ikke i vores database. Ret et hvilket som helst cli-relæ mod de samme relæer, så vil du se de samme data.", "home.why.verify.body": "Anmeldelserne findes på de offentlige Nostr-relæer, ikke i vores database. Ret et hvilket som helst cli-relæ mod de samme relæer, så vil du se de samme data.",
"home.why.checked.title": "Kontrolleres løbende", "home.why.checked.title": "Kontrolleres løbende",
"home.why.checked.body": "Vi undersøger hver enkelt server med hensyn til oppetid, softwareversion, begrænsninger og understøttede NUTs, så de specifikationer, du ser, er opdaterede og ikke selvrapporterede.", "home.why.checked.body": "Vi prober hvert mint for oppetid, softwareversion, grænser og understøttede NUTs, så de specifikationer du ser er aktuelle, ikke selvrapporterede.",
"home.cta.title": "Overvejer du at købe en mynte? Læs dette først.", "home.cta.title": "Overvejer du et mint? Læs op først.",
"home.cta.body": "Her finder du din ecash. Læs, hvad andre har skrevet, inden du sætter sats i, og skriv en anmeldelse, når du selv har brugt en.", "home.cta.body": "Et mint holder din ecash. Læs, hvad andre har fundet, før du sætter sats ind, og skriv en anmeldelse, når du selv har brugt et.",
"home.cta.explore": "Se alle pastiller", "home.cta.explore": "Se alle mints",
"home.cta.learn": "Læs mere om Cashu", "home.cta.learn": "Læs mere om Cashu",
"pulse.mintsIndexed": "indekserede mynter", "pulse.mintsIndexed": "mints indekseret",
"pulse.reviewsOnNostr": "anmeldelser af Nostr", "pulse.reviewsOnNostr": "anmeldelser af Nostr",
"pulse.lastReview": "sidste gennemgang", "pulse.lastReview": "sidste gennemgang",
"pulse.mintsOnline": "mynter online", "pulse.mintsOnline": "mints online",
"pulse.indexer": "indekseringsprogram", "pulse.indexer": "indekseringsprogram",
"pulse.connected": "forbundet", "pulse.connected": "forbundet",
"pulse.stale": "gammel", "pulse.stale": "gammel",
"mints.title": "Alle Cashu-mønter · Cashumints.space", "mints.title": "Alle Cashu-mints · Cashumints.space",
"mints.description": "Alle {total}- og Cashu-miner, der er indekseret fra Nostr-netværket, {online} online og {offline} offline, med brugeranmeldelser, oppetid og miningssoftware. Søg i og sorter den fulde liste.", "mints.description": "Alle {total} Cashu-mints indekseret fra Nostr-netværket, {online} online og {offline} offline, med bedømmelser fra fællesskabet, oppetid og mint-software. Søg og sortér hele listen.",
"mints.heading": "Alle Cashu-mønter", "mints.heading": "Alle Cashu-mints",
"mints.lede": "Alle mints, der er fundet på Nostr-netværket, herunder dem, der ikke længere findes. Offline-mints forbliver på listen, så du stadig kan finde og gennemse dem.", "mints.lede": "Alle mints, der er fundet på Nostr-netværket, herunder dem, der ikke længere findes. Offline-mints forbliver på listen, så du stadig kan finde og gennemse dem.",
"mints.search.label": "Søg efter mønter efter navn eller domæne", "mints.search.label": "Søg efter mints på navn eller domæne",
"mints.search.placeholder": "Søg efter navn eller domæne…", "mints.search.placeholder": "Søg efter navn eller domæne…",
"mints.sort.label": "Sorter pastiller", "mints.sort.label": "Sortér mints",
"mints.sort.score": "Bedst samlet set", "mints.sort.score": "Bedst samlet set",
"mints.sort.rating": "Bedst bedømte", "mints.sort.rating": "Bedst bedømte",
"mints.sort.reviews": "Mest anmeldte", "mints.sort.reviews": "Mest anmeldte",
"mints.sort.recent": "Nyligt anmeldt", "mints.sort.recent": "Nyligt anmeldt",
"mints.sort.seen": "Senest set online", "mints.sort.seen": "Senest set online",
"mints.hideOffline": "Skjul offline", "mints.hideOffline": "Skjul offline",
"mints.showing": "Viser {total}-mønter ({online} online, {offline} offline)", "mints.showing": "Viser {total} mints ({online} online, {offline} offline)",
"mints.showingFiltered": "Viser {shown} af {total}-mønter ({online} online, {offline} offline)", "mints.showingFiltered": "Viser {shown} af {total} mints ({online} online, {offline} offline)",
"mints.noResults": "Der findes ingen matchende mints. Prøv med et domæne, eller indsæt en mint-URL.", "mints.noResults": "Der findes ingen matchende mints. Prøv med et domæne, eller indsæt en mint-URL.",
"mints.clearSearch": "Ryd søgningen", "mints.clearSearch": "Ryd søgningen",
"card.noRatings": "Der er endnu ingen bedømmelser", "card.noRatings": "Der er endnu ingen bedømmelser",
@@ -113,71 +117,73 @@
"card.neverSeen": "aldrig set på nettet", "card.neverSeen": "aldrig set på nettet",
"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} · anmeldelser af Cashu-mint",
"mint.titleFedimint": "{name} · anmeldelser af Fedimint-føderationen",
"mint.titleLnurl": "{name} · anmeldelser af LNURL-mint",
"mint.breadcrumb": "Navigationsspor", "mint.breadcrumb": "Navigationsspor",
"mint.crumbMints": "Cashu-pastiller", "mint.crumbMints": "Cashu-mints",
"mint.copyUrl": "Kopier Mint-URL’en", "mint.copyUrl": "Kopiér mint-URL",
"mint.qrButton": "Mint QR", "mint.qrButton": "Mint QR",
"mint.writeReview": "Skriv en anmeldelse", "mint.writeReview": "Skriv en anmeldelse",
"mint.rankByScore": "#{rank} efter score", "mint.rankByScore": "#{rank} efter score",
"mint.tagLightning": "Lightning", "mint.tagLightning": "Lightning",
"mint.summary": "Oversigt over Mint", "mint.summary": "Mint-oversigt",
"mint.rating.label": "Fællesskabsvurdering", "mint.rating.label": "Fællesskabsvurdering",
"mint.rating.none": "Der er endnu ingen bedømmelser", "mint.rating.none": "Ingen bedømmelser endnu",
"mint.rating.reviews.one": "Anmeldelse af {n}", "mint.rating.reviews.one": "{n} anmeldelse",
"mint.rating.reviews.other": "Anmeldelser af {n}", "mint.rating.reviews.other": "{n} anmeldelser",
"mint.rating.unrated.one": "{n} har ikke givet nogen bedømmelse", "mint.rating.unrated.one": "{n} har ikke givet nogen bedømmelse",
"mint.rating.unrated.other": "{n} har ikke givet nogen bedømmelse", "mint.rating.unrated.other": "{n} har ikke givet nogen bedømmelse",
"mint.activity.label": "Aktivitet", "mint.activity.label": "Aktivitet",
"mint.activity.since": "siden sidste gennemgang", "mint.activity.since": "siden sidste anmeldelse",
"mint.activity.noReviews": "Der er endnu ingen anmeldelser", "mint.activity.noReviews": "ingen anmeldelser endnu",
"mint.activity.recent.one": "Anmeldelser af {n} inden for de seneste 90 dage", "mint.activity.recent.one": "{n} anmeldelse de seneste 90 dage",
"mint.activity.recent.other": "Anmeldelser af {n} i de seneste 90 dage", "mint.activity.recent.other": "{n} anmeldelser de seneste 90 dage",
"mint.limits.label": "Grænser", "mint.limits.label": "Grænser",
"mint.limits.notPublished": "ikke offentliggjort", "mint.limits.notPublished": "ikke offentliggjort",
"mint.limits.min": "min", "mint.limits.min": "min",
"mint.limits.max": "max", "mint.limits.max": "max",
"mint.limits.mintRow": "mynte", "mint.limits.mintRow": "mint",
"mint.limits.meltRow": "smelte", "mint.limits.meltRow": "melt",
"mint.limits.disabled": "deaktiveret", "mint.limits.disabled": "deaktiveret",
"mint.limits.range": "{min} til {max} {unit}", "mint.limits.range": "{min} til {max} {unit}",
"mint.limits.upTo": "op til {max} {unit}", "mint.limits.upTo": "op til {max} {unit}",
"mint.limits.from": "fra {min} {unit}", "mint.limits.from": "fra {min} {unit}",
"mint.software.label": "Mint-software", "mint.software.label": "Mint-software",
"mint.software.unknown": "ukendt", "mint.software.unknown": "ukendt",
"mint.checked": "markeret {when}", "mint.checked": "tjekket {when}",
"mint.lastSeen": "sidst set {when}", "mint.lastSeen": "sidst set {when}",
"mint.neverReached": "aldrig nået", "mint.neverReached": "aldrig nået",
"mint.uptime.checks": "{ok} fra den seneste kontrol af {total} er tilgængelig", "mint.uptime.checks": "{ok} af de seneste {total} tjek kunne nås",
"mint.uptime.percent": "{percent}% kan kontaktes i løbet af 30 dage", "mint.uptime.percent": "{percent}% kunne nås over 30 dage",
"mint.uptime.description": "{ok} af de seneste {total}-kontroller blev gennemført med succes", "mint.uptime.description": "{ok} af de seneste {total} tjek lykkedes",
"mint.operator.title": "Fra operatøren", "mint.operator.title": "Fra operatøren",
"mint.operator.motd": "Dagens budskab", "mint.operator.motd": "Dagens besked",
"mint.operator.beta": "Denne mint beskriver sig selv som eksperimentel eller i beta-fase. Læs udbyderens egne ord nedenfor, inden du opbevarer noget, du ikke har råd til at miste.", "mint.operator.beta": "Denne mint beskriver sig selv som eksperimentel eller i beta-fase. Læs udbyderens egne ord nedenfor, inden du opbevarer noget, du ikke har råd til at miste.",
"mint.nuts.title": "Understøttet NUTs", "mint.nuts.title": "Understøttede NUTs",
"mint.nuts.noneOffline": "Der er ingen NUT-liste gemt i cachen for denne udgave; den gik offline, før der blev registreret en.", "mint.nuts.noneOffline": "Ingen NUT-liste er cachet for dette mint, det gik offline, før en blev registreret.",
"mint.nuts.noneYet": "Der er endnu ikke gemt nogen NUT-liste i cachen for denne udgave.", "mint.nuts.noneYet": "Ingen NUT-liste er cachet for dette mint endnu.",
"mint.nuts.supported": "{n} af {total}, kendt som NUTs, understøttes", "mint.nuts.supported": "{n} af {total} kendte NUTs understøttes",
"mint.nuts.whatAre": "Hvad er NUTs?", "mint.nuts.whatAre": "Hvad er NUTs?",
"mint.nutRow.disabled": "deaktiveret", "mint.nutRow.disabled": "deaktiveret",
"mint.nutRow.notSupported": "understøttes ikke", "mint.nutRow.notSupported": "understøttes ikke",
"mint.details.title": "Oplysninger om mynten", "mint.details.title": "Mint-oplysninger",
"mint.details.pubkey": "Offentlig nøgle", "mint.details.pubkey": "Pubkey",
"mint.details.nostr": "Nostr", "mint.details.nostr": "Nostr",
"mint.details.email": "E-mail", "mint.details.email": "E-mail",
"mint.details.serverTime": "Servertid", "mint.details.serverTime": "Servertid",
"mint.details.firstSeen": "Første gang set", "mint.details.firstSeen": "Første gang set",
"mint.details.terms": "Vilkår", "mint.details.terms": "Vilkår",
"mint.kind.identifier": "identifikator", "mint.kind.identifier": "identifikator",
"mint.kind.mintPubkey": "generer en offentlig nøgle", "mint.kind.mintPubkey": "mint-pubkey",
"mint.kind.nostrAddress": "Nostr-adresse", "mint.kind.nostrAddress": "Nostr-adresse",
"mint.kind.emailAddress": "e-mail-adresse", "mint.kind.emailAddress": "e-mail-adresse",
"mint.kind.npub": "npub", "mint.kind.npub": "npub",
"mint.disclaimer": "Denne udbyder fungerer som depotforvalter: Den opbevarer dine penge og kan til enhver tid ophøre med at gøre det. Bedømmelser er udtryk for brugernes meninger, og »online« betyder blot, at tjenesten er tilgængelig, ikke at den er pålidelig.", "mint.disclaimer": "Dette mint er depotbaseret: det holder dine penge, og det kan holde op når som helst. Bedømmelser er meninger fra fællesskabet, og online betyder tilgængelig, ikke troværdig.",
"mint.disclaimerLink": "Læs den fulde ansvarsfraskrivelse", "mint.disclaimerLink": "Læs den fulde ansvarsfraskrivelse",
"mint.qr.title": "Føj {name} til din tegnebog", "mint.qr.title": "Føj {name} til din tegnebog",
"mint.qr.note": "Scan denne kode med en Cashu-tegnebog for at tilføje mønten, eller kopier URL’en og indsæt den.", "mint.qr.note": "Scan denne med en Cashu-wallet for at tilføje mintet, eller kopiér URL'en og indsæt den.",
"mint.qr.warn": "Når du tilføjer en betalingsmetode, betyder det, at du betror den dine penge.", "mint.qr.warn": "At tilføje et mint betyder at betro det dine penge.",
"mint.qr.whatMeans": "Hvad det betyder", "mint.qr.whatMeans": "Hvad det betyder",
"mint.qr.wallets": "Tegnebøger, der understøtter Cashu", "mint.qr.wallets": "Tegnebøger, der understøtter Cashu",
"mint.chip.frozen": "Frossen", "mint.chip.frozen": "Frossen",
@@ -186,9 +192,9 @@
"mint.warnings.dayCount.one": "{n}-dag", "mint.warnings.dayCount.one": "{n}-dag",
"mint.warnings.dayCount.other": "{n} dage", "mint.warnings.dayCount.other": "{n} dage",
"mint.warnings.gone.lead": "Sandsynligvis væk.", "mint.warnings.gone.lead": "Sandsynligvis væk.",
"mint.warnings.gone.body.since": "Denne wallet har været utilgængelig siden {date} ({days}). Betragt de midler, der er deponeret der, som tabte, og overfør ikke nye midler. Nedenstående oplysninger viser den seneste kendte status.", "mint.warnings.gone.body.since": "Dette mint har været utilgængeligt siden {date} ({days}). Betragt midler, der ligger der, som udsatte, og send ikke nye midler. Detaljerne nedenfor viser dets sidst kendte tilstand.",
"mint.warnings.gone.body.neverDated": "Denne møntudsteder har ikke reageret på en eneste kontrol, siden vi begyndte at overvåge den i {month}. Betragt midler, der er deponeret der, som værende i fare, og overfør ikke nye midler. Alt nedenfor stammer fra meddelelsen fra Nostr, ikke fra møntudstederen.", "mint.warnings.gone.body.neverDated": "Dette mint har ikke svaret på et eneste tjek, siden vi begyndte at holde øje med det i {month}. Betragt midler, der ligger der, som udsatte, og send ikke nye midler. Alt nedenfor stammer fra Nostr-annonceringen, ikke fra mintet.",
"mint.warnings.gone.body.never": "Dette møntværk har ikke besvaret en eneste forespørgsel, siden vi begyndte at følge det. Betragt midler, der er deponeret der, som udsatte, og indbetal ikke nye midler. Alt nedenfor stammer fra meddelelsen Nostr, ikke fra møntværket.", "mint.warnings.gone.body.never": "Dette mint har ikke svaret på et eneste tjek, siden vi begyndte at holde øje med det. Betragt midler, der ligger der, som udsatte, og send ikke nye midler. Alt nedenfor stammer fra Nostr-annonceringen, ikke fra mintet.",
"mint.warnings.gone.meta.since": "Offline siden {date}", "mint.warnings.gone.meta.since": "Offline siden {date}",
"mint.warnings.gone.meta.never": "Har aldrig været tilgængelig, siden den blev opdaget", "mint.warnings.gone.meta.never": "Har aldrig været tilgængelig, siden den blev opdaget",
"mint.warnings.meltDisabled.lead": "Udbetalinger er deaktiveret.", "mint.warnings.meltDisabled.lead": "Udbetalinger er deaktiveret.",
@@ -196,32 +202,32 @@
"mint.warnings.meltDisabled.body.offline": "Denne mint havde smeltningen af Lightning (NUT-05) deaktiveret: Du kan indsætte sats, men du kan ikke hæve dem via Lightning. Indsæt ikke penge, før smeltningen er genoptaget.", "mint.warnings.meltDisabled.body.offline": "Denne mint havde smeltningen af Lightning (NUT-05) deaktiveret: Du kan indsætte sats, men du kan ikke hæve dem via Lightning. Indsæt ikke penge, før smeltningen er genoptaget.",
"mint.warnings.meltDisabled.meta.online": "Udbetalinger er i øjeblikket deaktiveret", "mint.warnings.meltDisabled.meta.online": "Udbetalinger er i øjeblikket deaktiveret",
"mint.warnings.meltDisabled.meta.offline": "Udbetalinger var deaktiveret, da de sidst blev set", "mint.warnings.meltDisabled.meta.offline": "Udbetalinger var deaktiveret, da de sidst blev set",
"mint.warnings.frozen.lead": "Frossen mynte.", "mint.warnings.frozen.lead": "Mint frosset.",
"mint.warnings.frozen.body.online": "Både udstedelse og indløsning er deaktiveret. Ingen sats kan overføres ind eller ud via Lightning.", "mint.warnings.frozen.body.online": "Både minting og melting er slået fra. Ingen sats kan flytte ind eller ud via Lightning.",
"mint.warnings.frozen.body.offline": "Både udstedelse og indløsning er deaktiveret. Ingen sats kan overføres ind eller ud via Lightning.", "mint.warnings.frozen.body.offline": "Både minting og melting var slået fra. Ingen sats kan flytte ind eller ud via Lightning.",
"mint.warnings.frozen.meta.online": "Der er i øjeblikket indført en frysning; der foregår ingen ind- eller udførsel", "mint.warnings.frozen.meta.online": "Der er i øjeblikket indført en frysning; der foregår ingen ind- eller udførsel",
"mint.warnings.frozen.meta.offline": "Frosset, da den sidst blev set", "mint.warnings.frozen.meta.offline": "Frosset, da den sidst blev set",
"mint.warnings.offlineLong.lead": "Ikke tilgængelig for {days}, fra og med {date}.", "mint.warnings.offlineLong.lead": "Ikke tilgængelig for {days}, fra og med {date}.",
"mint.warnings.offlineLong.body": "Mint-versionen er muligvis ikke længere tilgængelig. Alt nedenfor viser den seneste kendte version, og anmeldelserne fungerer stadig.", "mint.warnings.offlineLong.body": "Mint-versionen er muligvis ikke længere tilgængelig. Alt nedenfor viser den seneste kendte version, og anmeldelserne fungerer stadig.",
"mint.warnings.offlineLong.meta": "Offline siden {date}", "mint.warnings.offlineLong.meta": "Offline siden {date}",
"mint.warnings.meltOnly.lead": "Må kun smeltes.", "mint.warnings.meltOnly.lead": "Må kun smeltes.",
"mint.warnings.meltOnly.body.online": "Ny udstedelse (NUT-04) er deaktiveret: Du kan ikke indbetale, men eksisterende ecash kan stadig hæves. Dette betyder ofte, at en udstedelse er ved at blive afviklet, så hvis du ejer ecash, bør du overveje at sælge dem.", "mint.warnings.meltOnly.body.online": "Ny minting (NUT-04) er slået fra: du kan ikke indbetale, men eksisterende ecash kan stadig hæves. Det betyder ofte, at et mint er ved at lukke ned, så hvis du holder dets ecash, så overvej at melte ud.",
"mint.warnings.meltOnly.body.offline": "Udstedelse af nye tokens (NUT-04) er blevet deaktiveret: Du kan ikke indbetale, men eksisterende ecash kan stadig hæves. Dette betyder ofte, at en udstedelse er ved at blive afviklet, så hvis du ejer ecash, bør du overveje at sælge dem.", "mint.warnings.meltOnly.body.offline": "Ny minting (NUT-04) var slået fra: du kan ikke indbetale, men eksisterende ecash kan stadig hæves. Det betyder ofte, at et mint er ved at lukke ned, så hvis du holder dets ecash, så overvej at melte ud.",
"mint.warnings.meltOnly.meta.online": "Kun smeltning i øjeblikket", "mint.warnings.meltOnly.meta.online": "Kun smeltning i øjeblikket",
"mint.warnings.meltOnly.meta.offline": "Smelt kun, når den sidst blev set", "mint.warnings.meltOnly.meta.offline": "Smelt kun, når den sidst blev set",
"mint.warnings.offline.lead": "Har været offline siden {date}.", "mint.warnings.offline.lead": "Har været offline siden {date}.",
"mint.warnings.offline.body": "Detaljerne blev bekræftet dengang. Du kan stadig skrive en anmeldelse.", "mint.warnings.offline.body": "Detaljerne blev bekræftet dengang. Du kan stadig skrive en anmeldelse.",
"mint.warnings.offline.meta": "Offline siden {date}", "mint.warnings.offline.meta": "Offline siden {date}",
"mint.warnings.also.meltDisabled": "Inden den gik offline, havde den også deaktiveret udbetalinger.", "mint.warnings.also.meltDisabled": "Inden den gik offline, havde den også deaktiveret udbetalinger.",
"mint.warnings.also.mintDisabled": "Inden den gik offline, havde den også deaktiveret udstedelsen af nye mønter.", "mint.warnings.also.mintDisabled": "Inden det gik offline, havde det også slået nyt minting fra.",
"mint.warnings.also.offline": "Den har desuden været offline siden {date} ({days}).", "mint.warnings.also.offline": "Den har desuden været offline siden {date} ({days}).",
"nut.0": "Notation og modeller", "nut.0": "Notation og modeller",
"nut.1": "Udveksling af offentlige nøgler i Mint", "nut.1": "Udveksling af offentlige nøgler i Mint",
"nut.2": "Nøglesæt og gebyrer", "nut.2": "Nøglesæt og gebyrer",
"nut.3": "Bytning af tokens", "nut.3": "Bytning af tokens",
"nut.4": "Prægning via Lightning", "nut.4": "Minting via Lightning",
"nut.5": "Smeltning via Lightning", "nut.5": "Smeltning via Lightning",
"nut.6": "Oplysninger om mynte", "nut.6": "Mint-information",
"nut.7": "Kontrol af tokenets tilstand", "nut.7": "Kontrol af tokenets tilstand",
"nut.8": "For meget betalte Lightning-gebyrer", "nut.8": "For meget betalte Lightning-gebyrer",
"nut.9": "Gendan signaturer", "nut.9": "Gendan signaturer",
@@ -235,7 +241,7 @@
"nut.17": "WebSocket-abonnementer", "nut.17": "WebSocket-abonnementer",
"nut.18": "Betalingsanmodninger", "nut.18": "Betalingsanmodninger",
"nut.19": "Cachelagrede svar", "nut.19": "Cachelagrede svar",
"nut.20": "Signerede citater i perfekt stand", "nut.20": "Signerede mint-quotes",
"nut.21": "Ryd godkendelse", "nut.21": "Ryd godkendelse",
"nut.22": "Blind autentificering", "nut.22": "Blind autentificering",
"nut.23": "BOLT11 betalingsmetode", "nut.23": "BOLT11 betalingsmetode",
@@ -261,6 +267,9 @@
"reviews.empty": "Der er endnu ingen anmeldelser. Vær den første til at skrive en.", "reviews.empty": "Der er endnu ingen anmeldelser. Vær den første til at skrive en.",
"reviews.emptyWritten": "Der er endnu ingen skriftlige anmeldelser her. Vær den første til at skrive en.", "reviews.emptyWritten": "Der er endnu ingen skriftlige anmeldelser her. Vær den første til at skrive en.",
"reviews.emptyFilter": "Der er ingen anmeldelser, der matcher dette filter.", "reviews.emptyFilter": "Der er ingen anmeldelser, der matcher dette filter.",
"reviews.rate.cashu": "Bedøm denne mint",
"reviews.rate.fedimint": "Bedøm denne føderation",
"reviews.rate.lnurl": "Bedøm denne mint",
"reviews.noscript": "Anmeldelserne er underskrevet med Nostr og indlæses i din browser, så denne liste kræver JavaScript. Bedømmelserne og antallet ovenfor stammer fra selve hjemmesiden.", "reviews.noscript": "Anmeldelserne er underskrevet med Nostr og indlæses i din browser, så denne liste kræver JavaScript. Bedømmelserne og antallet ovenfor stammer fra selve hjemmesiden.",
"reviews.relayError": "Det var ikke muligt at oprette forbindelse til relæet. Anmeldelserne kan være ufuldstændige.", "reviews.relayError": "Det var ikke muligt at oprette forbindelse til relæet. Anmeldelserne kan være ufuldstændige.",
"reviews.srRating": "{rating} ud af 5", "reviews.srRating": "{rating} ud af 5",
@@ -273,7 +282,7 @@
"reviews.anonNote": "Første anmeldelse af denne npub; der er ikke fundet andre aktiviteter", "reviews.anonNote": "Første anmeldelse af denne npub; der er ikke fundet andre aktiviteter",
"reviews.published": "Udgivet.", "reviews.published": "Udgivet.",
"reviews.publishedPartial": "Udgivet til {accepted} blandt {total}-relæerne.", "reviews.publishedPartial": "Udgivet til {accepted} blandt {total}-relæerne.",
"reviews.propagating": "Når indholdet opdateres, kan det tage et øjeblik, før det vises andre steder.", "reviews.propagating": "Udgiver til Nostr-relays",
"reviews.summary.line.one": "{count}-vurdering uden kommentar: {breakdown}", "reviews.summary.line.one": "{count}-vurdering uden kommentar: {breakdown}",
"reviews.summary.line.other": "{count}-vurderinger uden kommentar: {breakdown}", "reviews.summary.line.other": "{count}-vurderinger uden kommentar: {breakdown}",
"reviews.summary.lineDay.one": "{count}-vurdering uden kommentar til {when}: {breakdown}", "reviews.summary.lineDay.one": "{count}-vurdering uden kommentar til {when}: {breakdown}",
@@ -287,7 +296,12 @@
"reviews.breakdown.1": "{n} én stjerne", "reviews.breakdown.1": "{n} én stjerne",
"reviews.breakdown.none": "{n} uden bedømmelse", "reviews.breakdown.none": "{n} uden bedømmelse",
"reviews.dialog.title": "Anmeldelse af {name}", "reviews.dialog.title": "Anmeldelse af {name}",
"reviews.dialog.note": "Underskrevet med din Nostr-nøgle og offentliggjort på offentlige relæer. Den kan hverken redigeres eller slettes.", "reviews.dialog.lede.cashu": "Del din oplevelse med denne mint.",
"reviews.dialog.lede.fedimint": "Del din oplevelse med denne føderation.",
"reviews.dialog.lede.lnurl": "Del din oplevelse med denne mint.",
"reviews.dialog.permanent": "Offentliggøres permanent på offentlige Nostr-relays.",
"reviews.dialog.gate.title": "Log ind for at skrive en anmeldelse",
"reviews.dialog.gate.lead": "Anmeldelser signeres med din Nostr-nøgle. Den bliver i din signer, dette websted ser den aldrig.",
"reviews.dialog.ratingLegend": "Din bedømmelse", "reviews.dialog.ratingLegend": "Din bedømmelse",
"reviews.dialog.starsSr.one": "{n}-stjerne", "reviews.dialog.starsSr.one": "{n}-stjerne",
"reviews.dialog.starsSr.other": "{n}-stjerner", "reviews.dialog.starsSr.other": "{n}-stjerner",
@@ -308,7 +322,6 @@
"reviews.dialog.waitingSigner": "Vi venter på din underskriver", "reviews.dialog.waitingSigner": "Vi venter på din underskriver",
"reviews.dialog.confirmClose": "Vil du lukke denne anmeldelse? Dit udkast gemmes, indtil du forlader siden.", "reviews.dialog.confirmClose": "Vil du lukke denne anmeldelse? Dit udkast gemmes, indtil du forlader siden.",
"reviews.dialog.why.login": "Log ind for at offentliggøre", "reviews.dialog.why.login": "Log ind for at offentliggøre",
"reviews.dialog.why.rating": "Vælg en bedømmelse",
"reviews.dialog.why.words": "Skriv et par ord", "reviews.dialog.why.words": "Skriv et par ord",
"reviews.dialog.why.more.one": "{n} flere tegn", "reviews.dialog.why.more.one": "{n} flere tegn",
"reviews.dialog.why.more.other": "{n} flere tegn", "reviews.dialog.why.more.other": "{n} flere tegn",
@@ -325,7 +338,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",
@@ -333,12 +346,12 @@
"feed.showingBuild.other": "Her vises de seneste anmeldelser af {n} fra den seneste version", "feed.showingBuild.other": "Her vises de seneste anmeldelser af {n} fra den seneste version",
"feed.noscript": "Anmeldelserne er gemt under Nostr og indlæses i din browser, så filtrering og den fulde liste kræver JavaScript. Anmeldelserne under {n} nedenfor blev hentet fra relæerne, da denne side blev oprettet.", "feed.noscript": "Anmeldelserne er gemt under Nostr og indlæses i din browser, så filtrering og den fulde liste kræver JavaScript. Anmeldelserne under {n} nedenfor blev hentet fra relæerne, da denne side blev oprettet.",
"feed.emptyBuild": "Der kunne ikke hentes nogen anmeldelser fra relæerne, da denne side blev oprettet. Listen opdateres fra relæerne i din browser.", "feed.emptyBuild": "Der kunne ikke hentes nogen anmeldelser fra relæerne, da denne side blev oprettet. Listen opdateres fra relæerne i din browser.",
"feed.foot": "Anmeldelser er meninger fra brugerne og ikke verificerede påstande. Læs {link}, før du betror en møntudsteder dine penge.", "feed.foot": "Anmeldelser er meninger fra fællesskabet, ikke verificerede påstande. Læs {link}, før du betror et mint dine penge.",
"feed.footLink": "ansvarsfraskrivelse", "feed.footLink": "ansvarsfraskrivelse",
"feed.showing": "Viser anmeldelser af {shown} og {total}", "feed.showing": "Viser anmeldelser af {shown} og {total}",
"feed.showingMatching": "Viser {shown} af {matching} relevante anmeldelser (i alt {total})", "feed.showingMatching": "Viser {shown} af {matching} relevante anmeldelser (i alt {total})",
"feed.emptyRelays": "Der kom ingen anmeldelser tilbage fra relæerne. Åbn en vilkårlig Mint-side for at læse og skrive anmeldelser der.", "feed.emptyRelays": "Der kom ingen anmeldelser tilbage fra relæerne. Åbn en vilkårlig Mint-side for at læse og skrive anmeldelser der.",
"feed.emptyFilter": "Der er ingen anmeldelser, der matcher dette filter. Ryd søgefeltet, eller vælg »Alle«.", "feed.emptyFilter": "Ingen anmeldelser matcher dette filter. Ryd mint-feltet, eller prøv Alle.",
"feed.relayError": "Det var ikke muligt at oprette forbindelse til relæerne. Her vises anmeldelserne fra den seneste build.", "feed.relayError": "Det var ikke muligt at oprette forbindelse til relæerne. Her vises anmeldelserne fra den seneste build.",
"feed.relayEmpty": "Der kom ingen anmeldelser tilbage fra relæerne. Her vises anmeldelserne fra den seneste build.", "feed.relayEmpty": "Der kom ingen anmeldelser tilbage fra relæerne. Her vises anmeldelserne fra den seneste build.",
"feed.reading": "Aflæsning af relæerne…", "feed.reading": "Aflæsning af relæerne…",
@@ -369,6 +382,10 @@
"login.status.waitingSigner": "Vi venter på din underskriver", "login.status.waitingSigner": "Vi venter på din underskriver",
"login.status.connecting": "Opret forbindelse til din underskriver", "login.status.connecting": "Opret forbindelse til din underskriver",
"login.status.connected": "Forbundet", "login.status.connected": "Forbundet",
"login.toast.loggedIn": "Logget ind",
"login.toast.fetchingProfile": "Henter din Nostr-profil fra relæerne…",
"login.toast.profile": "Profil: {name}",
"login.toast.noProfile": "Ingen profil fundet på relæerne",
"login.method.extension": "Udvidelse", "login.method.extension": "Udvidelse",
"login.method.remote": "Fjernbetjening", "login.method.remote": "Fjernbetjening",
"login.method.primal": "Primal", "login.method.primal": "Primal",
@@ -392,19 +409,26 @@
"notfound.title": "Siden blev ikke fundet · Cashumints.space", "notfound.title": "Siden blev ikke fundet · Cashumints.space",
"notfound.description": "Den side findes ikke på Cashumints.space.", "notfound.description": "Den side findes ikke på Cashumints.space.",
"notfound.heading": "Denne side findes ikke", "notfound.heading": "Denne side findes ikke",
"notfound.lede": "Den er muligvis flyttet, eller mønteriet er måske endnu ikke indekseret. Søg efter et mønteri ved navn eller domæne, eller indsæt en URL til mønteriet.", "notfound.lede": "Den er måske flyttet, eller mintet er måske ikke indekseret endnu. Søg efter et mint på navn eller domæne, eller indsæt en mint-URL.",
"notfound.browse": "Se alle pastiller", "notfound.browse": "Se alle mints",
"notfound.home": "Gå hjem", "notfound.home": "Gå hjem",
"notfound.loading": "Indlæser {host}", "notfound.loading": "Indlæser {host}",
"notfound.fb.rating": "Bedømmelse", "notfound.fb.rating": "Bedømmelse",
"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",
"wallets.lede": "Du skal have en tegnebog til at opbevare ecash fra en af disse udstedere. De fleste tegnebøger kommunikerer med flere udstedere på én gang, så du er ikke bundet til den første, du prøver.", "wallets.lede": "Du skal bruge en wallet for at holde ecash fra et af disse mints. De fleste wallets taler med flere mints ad gangen, så du er ikke bundet til det første, du prøver.",
"wallets.note": "Denne liste er ikke udtømmende og udgør ikke en anbefaling. Cashu-projektet fører et mere omfattende register på {link}. En wallet opbevarer nøgler, mens en mint opbevarer pengene: Læs anmeldelserne af mints, før du vælger, hvor din sats skal opbevares.", "wallets.note": "Denne liste er ikke udtømmende og udgør ikke en anbefaling. Cashu-projektet fører et mere omfattende register på {link}. En wallet opbevarer nøgler, mens en mint opbevarer pengene: Læs anmeldelserne af mints, før du vælger, hvor din sats skal opbevares.",
"wallets.platform.android": "Android", "wallets.platform.android": "Android",
"wallets.platform.ios": "iOS", "wallets.platform.ios": "iOS",
@@ -413,11 +437,11 @@
"wallets.platform.cli": "CLI", "wallets.platform.cli": "CLI",
"wallets.platform.python": "Python", "wallets.platform.python": "Python",
"wallets.item.minibits": "Mobil tegnebog ecash med egen mint, understøttelse af Lightning-adresser og Nostr-kontakter.", "wallets.item.minibits": "Mobil tegnebog ecash med egen mint, understøttelse af Lightning-adresser og Nostr-kontakter.",
"wallets.item.nutstash": "En browser-wallet, der kan håndtere flere udstedere på én gang og importere og eksportere rå tokens.", "wallets.item.nutstash": "Browser-wallet, der håndterer flere mints ad gangen og importerer og eksporterer rå tokens.",
"wallets.item.enuts": "Mobil tegnebog med fokus på saldi fra flere møntudstedere og overførsel af ecash til Nostr.", "wallets.item.enuts": "Mobil wallet med fokus på saldi hos flere mints og på at sende ecash over Nostr.",
"wallets.item.cashume": "Reference-webwallet fra Cashu-projektet. Kører udelukkende i browseren og kan installeres som en PWA.", "wallets.item.cashume": "Reference-webwallet fra Cashu-projektet. Kører udelukkende i browseren og kan installeres som en PWA.",
"wallets.item.boardwalk": "Web-tegnebog baseret på Lightning-adresser og afsendelse af ecash via et link.", "wallets.item.boardwalk": "Web-tegnebog baseret på Lightning-adresser og afsendelse af ecash via et link.",
"wallets.item.coinos": "Lightning-tegnebog og salgssted, der også driver sin egen Cashu-møntudstedelse.", "wallets.item.coinos": "Lightning-wallet og salgssted, der også kører sit eget Cashu-mint.",
"wallets.item.oxchat": "Nostr-messenger med en indbygget ecash-tegnebog til at sende sats i chats.", "wallets.item.oxchat": "Nostr-messenger med en indbygget ecash-tegnebog til at sende sats i chats.",
"wallets.item.nutshell": "Referenceimplementeringen. Kører som en wallet på kommandolinjen og som mint-software.", "wallets.item.nutshell": "Referenceimplementeringen. Kører som en wallet på kommandolinjen og som mint-software.",
"about.title": "Om Cashumints.space", "about.title": "Om Cashumints.space",
@@ -425,21 +449,21 @@
"about.heading": "Om", "about.heading": "Om",
"about.lede": "Cashumints.space viser alle Cashu-mints, vi kan finde på Nostr-netværket, og viser, hvad folk siger om dem. Den er udviklet af Azzamo.", "about.lede": "Cashumints.space viser alle Cashu-mints, vi kan finde på Nostr-netværket, og viser, hvad folk siger om dem. Den er udviklet af Azzamo.",
"about.cashu.title": "Hvad er Cashu?", "about.cashu.title": "Hvad er Cashu?",
"about.cashu.p1": "Cashu er en ecash-protokol til Bitcoin. En udsteder udsteder anonymiserede tokens, der er bakket op af de sats, den besidder, og disse tokens overføres mellem brugere, uden at udstederen får kendskab til, hvem der har sendt hvad til hvem. Privatlivsbeskyttelsen er reel, og det er netop pointen: tokens er ikke konti, og udstederen kan ikke knytte det token, den har signeret, til det token, der kommer tilbage.", "about.cashu.p1": "Cashu er en ecash-protokol til Bitcoin. Et mint udsteder blindede tokens dækket af de sats, det holder, og de tokens bevæger sig mellem folk, uden at mintet får at vide, hvem der sendte hvad til hvem. Privatlivet er ægte, og det er hele pointen: tokens er ikke konti, og mintet kan ikke koble den token, det signerede, til den token, der kommer tilbage.",
"about.cashu.p2": "Du kan læse protokollen og dens specifikationer på {link}. Selve specifikationerne hedder NUTs, og det er her, tallene NUT på hver enkelt mint-side stammer fra.", "about.cashu.p2": "Du kan læse protokollen og dens specifikationer på {link}. Selve specifikationerne hedder NUTs, og det er her, tallene NUT på hver enkelt mint-side stammer fra.",
"about.mints.title": "Hvad en mint er, og hvad der er fordele og ulemper ved den", "about.mints.title": "Hvad en mint er, og hvad der er fordele og ulemper ved den",
"about.mints.p1": "En udstedelse er en form for deponering. Når du udsteder ecash, overdrager du sats til den, der driver den pågældende server, og du stoler på, at vedkommende vil indfri tokens, når du indløser dem igen. Der er ingen beskyttelse mod svindel, ingen deponeringsordning og ingen mulighed for at gøre krav gældende. En udsteder kan forsvinde med saldoen, og det er der eksempler på.", "about.mints.p1": "Et mint er depotbaseret. Når du minter ecash, giver du sats til den, der driver den server, og du stoler på, at de indfrier dine tokens, når du melter dem ud igen. Der er intet svindelbevis, ingen escrow og ingen klagevej. En mint-operatør kan forsvinde med saldoen, og mints har gjort det.",
"about.mints.p2": "Det er netop derfor, der findes anmeldelser her. Denne side siger aldrig, at en mint er sikker. Status på en mint-side angiver {code}, ikke »pålidelig«, og det betyder blot, at serveren svarede, da vi sidst spurgte. Betragt ecash-saldoer som kontanter i en frakkelomme: fine at bruge, men ikke egnet til opsparing.", "about.mints.p2": "Det er netop derfor, der findes anmeldelser her. Denne side siger aldrig, at en mint er sikker. Status på en mint-side angiver {code}, ikke »pålidelig«, og det betyder blot, at serveren svarede, da vi sidst spurgte. Betragt ecash-saldoer som kontanter i en frakkelomme: fine at bruge, men ikke egnet til opsparing.",
"about.nip87.title": "Hvad er NIP-87?", "about.nip87.title": "Hvad er NIP-87?",
"about.nip87.p1": "NIP-87 er den fastlagte betegnelse for Nostr, der anvendes til at annoncere og anbefale ecash-mønter. Her er der to typer begivenheder, der er relevante:", "about.nip87.p1": "NIP-87 er Nostr-konventionen for at annoncere og anbefale ecash-mints. To event-typer betyder noget her:",
"about.nip87.li1": "{code} annoncerer en mint. URL’en til mintet findes i {tag}-tagget. På denne måde opdager webstedet mints, det aldrig har set før.", "about.nip87.li1": "{code} annoncerer en mint. URL’en til mintet findes i {tag}-tagget. På denne måde opdager webstedet mints, det aldrig har set før.",
"about.nip87.li2": "{code} er en anmeldelse. Den indeholder vurderingen og anmeldelsesteksten og henviser til den møntudsteder, den handler om, ved hjælp af et {utag}-tag (møntudstederens URL) og et {dtag}-tag (møntudstederens egen offentlige nøgle).", "about.nip87.li2": "{code} er en anmeldelse. Den bærer bedømmelsen og anmeldelsesteksten og peger på det mint, den handler om, med et {utag}-tag (mint-URL'en) og et {dtag}-tag (mintets egen offentlige nøgle).",
"about.nip87.p2": "Hver anmeldelse er underskrevet med forfatterens Nostr-nøgle. Vi betegner aldrig en anmeldelse som »verificeret«, fordi ingen har verificeret den: Den er underskrevet, hvilket er en anden og mere nyttig påstand. Det betyder, at du kan kontrollere, hvem der har skrevet den, og at den ikke er blevet ændret. Det betyder ikke, at forfatteren er ærlig eller nogensinde har brugt møntmaskinen.", "about.nip87.p2": "Hver anmeldelse er signeret med forfatterens Nostr-nøgle. Vi kalder aldrig en anmeldelse »verificeret«, for ingen har verificeret den: den er signeret, hvilket er en anden og mere nyttig påstand. Det betyder, at du kan se, hvem der skrev den, og at den ikke er ændret. Det betyder ikke, at forfatteren er ærlig eller nogensinde har brugt mintet.",
"about.how.title": "Sådan fungerer denne hjemmeside", "about.how.title": "Sådan fungerer denne hjemmeside",
"about.how.p1": "Der er to sløjfer og én regel. Reglen er, at møntfabrikken ejer sine metadata, og at Nostr ejer anmeldelserne, så dette websted gemmer ingen af delene som en autoritet.", "about.how.p1": "Der er to sløjfer og én regel. Reglen er, at mintet ejer sine metadata, og Nostr ejer anmeldelserne, så dette websted gemmer ingen af delene som autoritet.",
"about.how.li1": "{strong} forespørger alle kendte mints om {code} hver tiende minut og registrerer svarene: navn, beskrivelse, version, understøttede NUTs, begrænsninger, kontaktoplysninger. Den registrerer også, om mienten overhovedet svarede, og det er herfra, at oppetiden stammer.", "about.how.li1": "{strong} forespørger alle kendte mints om {code} hver tiende minut og registrerer svarene: navn, beskrivelse, version, understøttede NUTs, begrænsninger, kontaktoplysninger. Den registrerer også, om mienten overhovedet svarede, og det er herfra, at oppetiden stammer.",
"about.how.li1Label": "Sonderløkken", "about.how.li1Label": "Sonderløkken",
"about.how.li2": "{strong} opretter forbindelse til Nostr-relæerne hver time, læser meddelelser og anmeldelser om mønter og tilføjer alle mønter, den ikke har set før.", "about.how.li2": "{strong} forbinder til Nostr-relæerne hver time, læser mint-annonceringer og anmeldelser og tilføjer ethvert mint, den ikke har set før.",
"about.how.li2Label": "Opdagelsessløjfen", "about.how.li2Label": "Opdagelsessløjfen",
"about.how.p2": "Det vigtige er, hvad der sker, når en mint holder op med at svare. Dens cachelagrede metadata overskrives aldrig af en mislykket forespørgsel, så en nedlagt mint bevarer sin fulde side: dens specifikationer, historie og anmeldelser, med et banner, der angiver, hvornår den gik ned. Den tidligere version af dette websted viste i stedet en fejlside, hvilket betød, at netop de mints, som brugerne havde mest brug for at advare hinanden om, var dem, der ikke kunne anmeldes. Det var hovedårsagen til omlægningen.", "about.how.p2": "Det vigtige er, hvad der sker, når en mint holder op med at svare. Dens cachelagrede metadata overskrives aldrig af en mislykket forespørgsel, så en nedlagt mint bevarer sin fulde side: dens specifikationer, historie og anmeldelser, med et banner, der angiver, hvornår den gik ned. Den tidligere version af dette websted viste i stedet en fejlside, hvilket betød, at netop de mints, som brugerne havde mest brug for at advare hinanden om, var dem, der ikke kunne anmeldes. Det var hovedårsagen til omlægningen.",
"about.how.p3": "Anmeldelsestekster gemmes aldrig hos os. Når du åbner en mint-side, kommunikerer din browser direkte med relæerne og henter selv anmeldelserne, og når du skriver en anmeldelse, underskrives den med din egen Nostr-udvidelse og sendes direkte til disse relæer. Vi holder styr på antallet, så siderne kan rangordnes og sorteres – ikke mere end det. Intet her kan redigeres eller slettes af os, heller ikke anmeldelser, som vi helst ville undvære.", "about.how.p3": "Anmeldelsestekster gemmes aldrig hos os. Når du åbner en mint-side, kommunikerer din browser direkte med relæerne og henter selv anmeldelserne, og når du skriver en anmeldelse, underskrives den med din egen Nostr-udvidelse og sendes direkte til disse relæer. Vi holder styr på antallet, så siderne kan rangordnes og sorteres – ikke mere end det. Intet her kan redigeres eller slettes af os, heller ikke anmeldelser, som vi helst ville undvære.",
@@ -451,23 +475,23 @@
"about.numbers.title": "Lige nu", "about.numbers.title": "Lige nu",
"about.numbers.p1": "{mints}-mønstre indekseret, {online} af disse svarer, {reviews}-gennemgange talt fra relæerne ovenfor.", "about.numbers.p1": "{mints}-mønstre indekseret, {online} af disse svarer, {reviews}-gennemgange talt fra relæerne ovenfor.",
"about.source.title": "Kilde og kreditering", "about.source.title": "Kilde og kreditering",
"about.source.p1": "Webstedet er open source og findes på {github}. Det er oprettet og drives af {azzamo}. Hvis der er fejl på en møntside, skal du rette dem i kildekoden: Metadataene for mønten stammer fra møntens egen {code}, og anmeldelserne stammer fra Nostr.", "about.source.p1": "Webstedet er open source på {github}. Det er bygget og drevet af {azzamo}. Hvis noget er galt på en mint-side, så ret det ved kilden: mint-metadata kommer fra mintets eget {code}, og anmeldelser kommer fra Nostr.",
"terms.title": "Brugsbetingelser · Cashumints.space", "terms.title": "Brugsbetingelser · Cashumints.space",
"terms.description": "Brugsbetingelser for Cashumints.space: hvad hjemmesiden er, hvad den ikke gør, og hvem der er ansvarlig for hvad. Ingen konti, ingen garantier, ingen tilknytning til nogen møntfabrik.", "terms.description": "Brugsbetingelser for Cashumints.space: hvad webstedet er, hvad det ikke gør, og hvem der er ansvarlig for hvad. Ingen konti, ingen garantier, ingen tilknytning til noget mint.",
"terms.heading": "Brugsbetingelser", "terms.heading": "Brugsbetingelser",
"terms.lede": "Enkel beskrivelse af et websted, der udelukkende indekserer offentlige data. Senest opdateret {date}.", "terms.lede": "Enkel beskrivelse af et websted, der udelukkende indekserer offentlige data. Senest opdateret {date}.",
"terms.what.title": "Hvad denne hjemmeside er", "terms.what.title": "Hvad denne hjemmeside er",
"terms.what.p1": "Cashumints.space er et indeks over offentligt tilgængelige data om Cashu-mønter. Der er to typer data, som begge stammer fra andre kilder:", "terms.what.p1": "Cashumints.space er et indeks over offentligt tilgængelige data om Cashu-mints. To slags data, begge fra et andet sted:",
"terms.what.li1": "Metadata om møntudstederne, hentet fra hver møntudsteders eget {code}-endpoint: navn, beskrivelse, softwareversion, understøttede NUTs, kontaktoplysninger og om møntudstederen svarede, da vi sidst henvendte os.", "terms.what.li1": "Mint-metadata, læst fra hvert mints eget {code}-endpoint: navn, beskrivelse, softwareversion, understøttede NUTs, kontaktoplysninger og om mintet svarede, sidst vi spurgte.",
"terms.what.li2": "Anmeldelser, hentet fra de offentlige Nostr-relæer. Dette er NIP-87-begivenheder, som forfatterne har offentliggjort på relæer, som alle kan søge i.", "terms.what.li2": "Anmeldelser, hentet fra de offentlige Nostr-relæer. Dette er NIP-87-begivenheder, som forfatterne har offentliggjort på relæer, som alle kan søge i.",
"terms.what.p2": "Vi indsamler disse data, normaliserer dem, rangordner dem og viser dem. Vi skaber dem ikke.", "terms.what.p2": "Vi indsamler disse data, normaliserer dem, rangordner dem og viser dem. Vi skaber dem ikke.",
"terms.noAccounts.title": "Ingen garantier, ingen ansvar", "terms.noAccounts.title": "Ingen garantier, ingen ansvar",
"terms.noAccounts.p1": "Der er ikke noget, man skal tilmelde sig. Hjemmesiden stilles til rådighed »som den er«, uden nogen form for garanti: hverken for, at oplysningerne er korrekte, at de er opdaterede, eller at hjemmesiden vil være tilgængelig. Oplysninger om mønter kan være minutes eller timer gamle, en mønt kan have ændret sig siden vores sidste kontrol, og et relæ kan undlade at svare, i hvilket tilfælde du ser færre oplysninger, end der faktisk findes.", "terms.noAccounts.p1": "Der er intet at tilmelde sig. Webstedet stilles til rådighed, som det er, uden nogen form for garanti: ikke for at data er korrekte, ikke for at de er aktuelle, ikke for at webstedet er tilgængeligt. Mint-data kan være minutter eller timer gamle, et mint kan ændre sig, efter vi sidst tjekkede, og et relæ kan undlade at svare, og så ser du mindre, end der findes.",
"terms.noAccounts.p2": "Brug det på eget ansvar. Vi påtager os intet ansvar for tab, der måtte opstå som følge af brugen af dette websted eller af, at du stoler på oplysninger, du læser her. Se {link} for de specifikke risici ved brug af en Cashu-mønt.", "terms.noAccounts.p2": "Brug det på eget ansvar. Vi er ikke ansvarlige for tab, der følger af at bruge dette websted eller af at stole på noget, du læser her. Se {link} for de specifikke risici ved at bruge et Cashu-mint.",
"terms.noAccounts.link": "ansvarsfraskrivelse", "terms.noAccounts.link": "ansvarsfraskrivelse",
"terms.noCustody.title": "Vi opbevarer ikke dine penge", "terms.noCustody.title": "Vi opbevarer ikke dine penge",
"terms.noCustody.p1": "Dette websted har aldrig midler i sin varetægt. Det har ingen tegnebog, det har ikke adgang til ecash, og det kan ikke flytte, spærre eller gendanne noget som helst. Hvis du har et problem med en mint, er mintoperatøren den eneste, der kan hjælpe dig, og vi kan ikke kontakte dem på dine vegne.", "terms.noCustody.p1": "Dette websted har aldrig midler i sin varetægt. Det har ingen tegnebog, det har ikke adgang til ecash, og det kan ikke flytte, spærre eller gendanne noget som helst. Hvis du har et problem med en mint, er mintoperatøren den eneste, der kan hjælpe dig, og vi kan ikke kontakte dem på dine vegne.",
"terms.noCustody.p2": "Vi er ikke tilknyttet nogen af de her anførte møntfabrikker, og ingen møntfabrik betaler for at blive opført, for sin placering eller for sin bedømmelse. Møntfabrikkerne udvælges på baggrund af offentlige Nostr-meddelelser og rangordnes ved hjælp af en algoritme baseret på offentlige anmeldelsesdata.", "terms.noCustody.p2": "Vi er ikke tilknyttet noget mint på listen her, og intet mint betaler for sin opførelse, sin placering eller sin bedømmelse. Mints findes via offentlige Nostr-annonceringer og rangordnes af en algoritme over offentlige anmeldelsesdata.",
"terms.reviews.title": "Anmeldelser er andres ord", "terms.reviews.title": "Anmeldelser er andres ord",
"terms.reviews.p1": "Anmeldelser er brugergenereret indhold, der offentliggøres på de offentlige Nostr-relæer af deres forfattere. Vi viser dem, men vi har ikke kontrol over dem. Vi har ikke skrevet dem, vi kan ikke redigere dem, og vi kan ikke slette dem fra de relæer, hvor de er gemt: Det er signerede begivenheder på et netværk, som vi ikke ejer.", "terms.reviews.p1": "Anmeldelser er brugergenereret indhold, der offentliggøres på de offentlige Nostr-relæer af deres forfattere. Vi viser dem, men vi har ikke kontrol over dem. Vi har ikke skrevet dem, vi kan ikke redigere dem, og vi kan ikke slette dem fra de relæer, hvor de er gemt: Det er signerede begivenheder på et netværk, som vi ikke ejer.",
"terms.reviews.p2": "En anmeldelse er en persons oplevelse, ikke et verificeret faktum. Hvis du offentliggør en anmeldelse, er du ansvarlig for indholdet. Offentliggør ikke noget ulovligt, og vær opmærksom på, at det, du offentliggør, er offentligt og forbliver der for altid (se {link}).", "terms.reviews.p2": "En anmeldelse er en persons oplevelse, ikke et verificeret faktum. Hvis du offentliggør en anmeldelse, er du ansvarlig for indholdet. Offentliggør ikke noget ulovligt, og vær opmærksom på, at det, du offentliggør, er offentligt og forbliver der for altid (se {link}).",
@@ -511,33 +535,33 @@
"privacy.permanent.li2": "Den er permanent knyttet til din offentlige nøgle Nostr sammen med alt andet, som denne nøgle nogensinde har offentliggjort.", "privacy.permanent.li2": "Den er permanent knyttet til din offentlige nøgle Nostr sammen med alt andet, som denne nøgle nogensinde har offentliggjort.",
"privacy.permanent.li3": "At slette den er en anmodning, ikke en ordre. Du kan offentliggøre en sletningsbegivenhed, og nogle relæer vil efterkomme den, men et relæ kan frit vælge at ignorere den, og alle, der allerede har kopieret begivenheden, har den stadig. Betragt en offentliggjort anmeldelse som permanent.", "privacy.permanent.li3": "At slette den er en anmodning, ikke en ordre. Du kan offentliggøre en sletningsbegivenhed, og nogle relæer vil efterkomme den, men et relæ kan frit vælge at ignorere den, og alle, der allerede har kopieret begivenheden, har den stadig. Betragt en offentliggjort anmeldelse som permanent.",
"privacy.permanent.li4": "Vi kan ikke slette det for dig. Vi kan dog sørge for, at det ikke længere vises her; det ændrer dog ikke noget på relæerne.", "privacy.permanent.li4": "Vi kan ikke slette det for dig. Vi kan dog sørge for, at det ikke længere vises her; det ændrer dog ikke noget på relæerne.",
"privacy.permanent.p2": "Hvis du vil anmelde en mynte uden at det knyttes til din primære identitet, skal du bruge en separat nøgle med koden Nostr. Bemærk, at en nøgle uden anden aktivitet fremhæves som sådan ved negative anmeldelser, da en helt ny nøgle, der skriver en anmeldelse med én stjerne, er et mønster, som læserne fortjener at se.", "privacy.permanent.p2": "Hvis du vil anmelde et mint, uden at det knyttes til din primære identitet, så brug en separat Nostr-nøgle. Bemærk, at en nøgle uden anden aktivitet vises som sådan ved hårde anmeldelser, fordi en helt ny nøgle, der skriver en anmeldelse på én stjerne, er et mønster, læsere fortjener at se.",
"privacy.contact.title": "Spørgsmål", "privacy.contact.title": "Spørgsmål",
"privacy.contact.p1": "Webstedet er open source: Hvis du vil tjekke noget af det ovenstående, kan du læse koden på {github}. Spørgsmål og rettelser kan indsendes som en issue der.", "privacy.contact.p1": "Webstedet er open source: Hvis du vil tjekke noget af det ovenstående, kan du læse koden på {github}. Spørgsmål og rettelser kan indsendes som en issue der.",
"disclaimer.title": "Ansvarsfraskrivelse · Cashumints.space", "disclaimer.title": "Ansvarsfraskrivelse · Cashumints.space",
"disclaimer.description": "Hvad du bør vide, før du bruger en Cashu-mønt: Mønter er under opbevaring; en mønt kan forsvinde sammen med dine penge; anmeldelser er udtryk for personlige meninger, og »online« betyder blot, at de er tilgængelige, ikke at de er pålidelige.", "disclaimer.description": "Hvad du bør vide, før du bruger et Cashu-mint: mints er depotbaserede, et mint kan forsvinde med dine penge, anmeldelser er meninger, og online betyder tilgængelig, ikke troværdig.",
"disclaimer.heading": "Ansvarsfraskrivelse", "disclaimer.heading": "Ansvarsfraskrivelse",
"disclaimer.lede": "Læs dette, før du investerer penge i en møntfabrik. Senest opdateret {date}.", "disclaimer.lede": "Læs dette, før du lægger penge i et mint. Senest opdateret {date}.",
"disclaimer.callout": "En Cashu-mønt opbevarer dine penge for dig. Hvis den forsvinder, er din ecash væk, og der er ingen, du kan klage til. Opbevar kun det, du har råd til at miste.", "disclaimer.callout": "Et Cashu-mint opbevarer dine penge for dig. Hvis det forsvinder, er din ecash væk, og der er ingen at klage til. Behold kun det, du har råd til at miste.",
"disclaimer.custodial.title": "Mints er forvaringsmyndigheder", "disclaimer.custodial.title": "Mints er forvaringsmyndigheder",
"disclaimer.custodial.p1": "Når du udsteder ecash, afleverer du bitcoin til udstederen og modtager tokens til gengæld. Disse tokens udgør en fordring på den pågældende udsteder. De er ikke bitcoin, som du besidder, og det er udstederen, der afgør, om de skal indfries.", "disclaimer.custodial.p1": "Når du minter ecash, giver du bitcoin til mint-operatøren og får tokens tilbage. De tokens er et krav på den operatør. De er ikke bitcoin, du holder, og operatøren er den, der bestemmer, om de indfries.",
"disclaimer.custodial.p2": "Det betyder, at alle mints er forbundet med de samme risici: operatøren kan forsvinde, blive tvunget til at indstille driften, miste midlerne på grund af en fejl eller et hackerangreb, eller ganske enkelt slukke for serveren. Der er ingen indskudsforsikring, ingen tilbageførsel af betalinger, ingen supportlinje og ingen retsmidler, man kan regne med. Dette gælder også for den bedst vurderede mint på dette websted.", "disclaimer.custodial.p2": "Det betyder, at alle mints er forbundet med de samme risici: operatøren kan forsvinde, blive tvunget til at indstille driften, miste midlerne på grund af en fejl eller et hackerangreb, eller ganske enkelt slukke for serveren. Der er ingen indskudsforsikring, ingen tilbageførsel af betalinger, ingen supportlinje og ingen retsmidler, man kan regne med. Dette gælder også for den bedst vurderede mint på dette websted.",
"disclaimer.custodial.p3": "Cashu er udviklet til små beløb, der flyttes hurtigt. Brug den på denne måde: Hold de penge, du skal til at bruge, ikke dine opsparinger, og fordel dem på flere udbydere i stedet for at placere det hele hos én.", "disclaimer.custodial.p3": "Cashu er lavet til små beløb, der bevæger sig hurtigt. Brug det sådan: hold det, du er ved at bruge, ikke din opsparing, og spred det over flere mints i stedet for at parkere alt i ét.",
"disclaimer.reviews.title": "Anmeldelser er meninger, ikke verificerede påstande", "disclaimer.reviews.title": "Anmeldelser er meninger, ikke verificerede påstande",
"disclaimer.reviews.p1": "Hver eneste anmeldelse her er skrevet af en bruger med brugernavnet Nostr og underskrevet med vedkommendes nøgle. At den er underskrevet betyder, at vi kan se, hvilken nøgle der har skrevet den. Det betyder ikke, at anmeldelsen er sand, at forfatteren har brugt møntudstedelsen, eller at forfatteren ikke er operatøren, en konkurrent eller én person med ti nøgler.", "disclaimer.reviews.p1": "Hver anmeldelse her er skrevet af nogen på Nostr og signeret med deres nøgle. Signeret betyder, at vi kan fortælle dig, hvilken nøgle der skrev den. Det betyder ikke, at anmeldelsen er sand, at forfatteren har brugt mintet, eller at forfatteren ikke er operatøren, en konkurrent eller én person med ti nøgler.",
"disclaimer.reviews.p2": "Vi verificerer ikke anmeldere, og vi redigerer eller fjerner ikke anmeldelser, som vi er uenige i. Læs et par stykker, se på, hvad de rent faktisk siger, og vægt en butik med tre anmeldelser anderledes end en med tre hundrede.", "disclaimer.reviews.p2": "Vi verificerer ikke anmeldere, og vi redigerer eller fjerner ikke anmeldelser, vi er uenige i. Læs et par stykker, se hvad de faktisk siger, og vægt et mint med tre anmeldelser anderledes end et med tre hundrede.",
"disclaimer.status.title": "»Online« betyder, at man kan få fat i det, ikke at det er pålideligt", "disclaimer.status.title": "»Online« betyder, at man kan få fat i det, ikke at det er pålideligt",
"disclaimer.status.p1": "Vi tjekker alle mints regelmæssigt og viser, om de har svaret. Det er alt, hvad en grøn status betyder: serveren har svaret. Det siger intet om, hvorvidt operatøren er solvent, ærlig eller stadig har til hensigt at indfri dine tokens i morgen.", "disclaimer.status.p1": "Vi tjekker alle mints regelmæssigt og viser, om de har svaret. Det er alt, hvad en grøn status betyder: serveren har svaret. Det siger intet om, hvorvidt operatøren er solvent, ærlig eller stadig har til hensigt at indfri dine tokens i morgen.",
"disclaimer.status.p2": "En møntudsteder kan være online helt indtil det øjeblik, hvor den ikke længere er det. Oplysninger om oppetid, understøttede NUTs og softwareversion giver dig et indblik i, hvordan en møntudsteder drives, men ingen af disse oplysninger udgør en garanti for dine penge.", "disclaimer.status.p2": "Et mint kan være online lige indtil det øjeblik, det ikke er. Oppetidshistorik, understøttede NUTs og softwareversion siger noget om, hvordan et mint drives, men ingen af dem er et løfte om dine penge.",
"disclaimer.ranking.title": "Sådan fungerer ranglisten, og hvad den ikke betyder", "disclaimer.ranking.title": "Sådan fungerer ranglisten, og hvad den ikke betyder",
"disclaimer.ranking.p1": "Mints sorteres ved hjælp af en algoritme baseret på data fra offentlige anmeldelser: bedømmelse, antal anmeldelser og seneste aktivitet, hvor online-mints som standard prioriteres frem for offline-mints. De nærmere detaljer fremgår af open source-koden.", "disclaimer.ranking.p1": "Mints sorteres ved hjælp af en algoritme baseret på data fra offentlige anmeldelser: bedømmelse, antal anmeldelser og seneste aktivitet, hvor online-mints som standard prioriteres frem for offline-mints. De nærmere detaljer fremgår af open source-koden.",
"disclaimer.ranking.p2": "Ingen betaler for placeringen. Der er ingen sponsorerede pladser, ingen fremhævede opslag og ingen aftaler med nogen møntfabrik på dette websted. En høj placering betyder blot, at algoritmen har vurderet de offentligt tilgængelige data positivt – og intet andet. Det er hverken en anbefaling eller en sikkerhedsvurdering.", "disclaimer.ranking.p2": "Ingen betaler for placering. Der er ingen sponsoreret plads, ingen fremhævet opførelse og ingen aftale med noget mint på dette websted. En høj placering betyder, at algoritmen kunne lide de offentlige data, og intet mere. Det er ikke en anbefaling, og det er ikke en sikkerhedsvurdering.",
"disclaimer.notAdvice.title": "Dette er ikke finansiel rådgivning", "disclaimer.notAdvice.title": "Dette er ikke finansiel rådgivning",
"disclaimer.notAdvice.p1": "Intet på dette websted udgør finansiel, investeringsmæssig, juridisk eller skattemæssig rådgivning. Vi kender ikke din situation, og vi giver dig ikke råd. Det er din beslutning, om du vil bruge en møntudsteder, og hvilken du vælger.", "disclaimer.notAdvice.p1": "Intet på dette websted er finansiel, investerings-, juridisk eller skattemæssig rådgivning. Vi kender ikke din situation, og vi rådgiver dig ikke. At bruge et mint, og at vælge hvilket, er din beslutning.",
"disclaimer.dyor.title": "Undersøg selv", "disclaimer.dyor.title": "Undersøg selv",
"disclaimer.dyor.li1": "Tjek udbyderen. En tjeneste, der drives af en person, der har et omdømme at miste, er noget helt andet end en anonym URL.", "disclaimer.dyor.li1": "Slå operatøren op. Et mint drevet af nogen med et offentligt omdømme at miste er noget andet end en anonym URL.",
"disclaimer.dyor.li2": "Læs selve mintens vilkår og dens MOTD. En mint, der kalder sig selv en test- eller betaversion, mener det alvorligt.", "disclaimer.dyor.li2": "Læs selve mintens vilkår og dens MOTD. En mint, der kalder sig selv en test- eller betaversion, mener det alvorligt.",
"disclaimer.dyor.li3": "Start i det små. Præg lidt, brug det, smelt det om igen, og se, hvad der sker, før du satser mere på det.", "disclaimer.dyor.li3": "Start i det små. Mint lidt, brug det, melt det ud igen, og se hvad der sker, før du betror det mere.",
"disclaimer.dyor.li4": "Vær opmærksom på grænserne. En lav maksimumsgrænse er ofte tegn på en forsigtig operatør, mens en høj grænse ikke nødvendigvis er tegn på en sikker operatør.", "disclaimer.dyor.li4": "Vær opmærksom på grænserne. En lav maksimumsgrænse er ofte tegn på en forsigtig operatør, mens en høj grænse ikke nødvendigvis er tegn på en sikker operatør.",
"disclaimer.dyor.li5": "Sørg for at tage backup af dine tokens, og husk, at et token er et ihændehaverbevis: den, der er i besiddelse af det, kan bruge det.", "disclaimer.dyor.li5": "Sørg for at tage backup af dine tokens, og husk, at et token er et ihændehaverbevis: den, der er i besiddelse af det, kan bruge det.",
"disclaimer.site.title": "Om selve hjemmesiden", "disclaimer.site.title": "Om selve hjemmesiden",
@@ -546,18 +570,18 @@
"disclaimer.updated": "20. august 2026", "disclaimer.updated": "20. august 2026",
"login.getPrimal": "Få Primal", "login.getPrimal": "Få Primal",
"reviews.dialog.identity.extensionGone": "Din browserudvidelse reagerer ikke. Log ind igen, eller brug en ekstern underskriver.", "reviews.dialog.identity.extensionGone": "Din browserudvidelse reagerer ikke. Log ind igen, eller brug en ekstern underskriver.",
"seo.orgDescription": "En oversigt over Cashu-minter med realtidsstatus fra hver enkelt mint og brugeranmeldelser, der er underskrevet på Nostr.", "seo.orgDescription": "En oversigt over Cashu-mints, med live status fra hvert mint og anmeldelser fra fællesskabet signeret på Nostr.",
"seo.ogImageAlt": "Cashumints.space: Find en Cashu i perfekt stand, som du kan stole på.", "seo.ogImageAlt": "Cashumints.space: find et Cashu-mint du kan stole på.",
"seo.mint.title.one": "Anmeldelser af {name}: Vurdering af {rating} fra anmeldelsen af {n} | Cashumints.space", "seo.mint.title.one": "{name} anmeldelser: {rating} fra {n} anmeldelse | Cashumints.space",
"seo.mint.title.other": "Anmeldelser af {name}: Vurdering af {rating} baseret på anmeldelser af {n} | Cashumints.space", "seo.mint.title.other": "{name} anmeldelser: {rating} fra {n} anmeldelser | Cashumints.space",
"seo.mint.titleOffline.one": "{name}: offline for {n}-dagen | Cashumints.space", "seo.mint.titleOffline.one": "{name}: offline i {n} dag | Cashumints.space",
"seo.mint.titleOffline.other": "{name}: har været offline i {n} dage | Cashumints.space", "seo.mint.titleOffline.other": "{name}: offline i {n} dage | Cashumints.space",
"seo.mint.titleNoReviews": "{name}: Oplysninger og anmeldelser om Cashu | Cashumints.space", "seo.mint.titleNoReviews": "{name}: info og anmeldelser om Cashu-mint | Cashumints.space",
"seo.fedimint.titleNoReviews": "{name}: Oplysninger og anmeldelser om Fedimint-føderationen | Cashumints.space", "seo.fedimint.titleNoReviews": "{name}: Oplysninger og anmeldelser om Fedimint-føderationen | Cashumints.space",
"seo.desc.fallback": "Brugeranmeldelser af {name} ({domain}).", "seo.desc.fallback": "Anmeldelser fra fællesskabet af {name} ({domain}).",
"seo.desc.fallbackDomain": "Brugeranmeldelser af Cashu i mintfarve på {domain}.", "seo.desc.fallbackDomain": "Anmeldelser fra fællesskabet af Cashu-mintet på {domain}.",
"seo.desc.rated.one": "Anmeldelse af {rating}/5 fra {n}, underskrevet af Nostr.", "seo.desc.rated.one": "{rating}/5 fra {n} signeret Nostr-anmeldelse.",
"seo.desc.rated.other": "Anmeldelser af {rating}/5 fra {n}, underskrevet af Nostr.", "seo.desc.rated.other": "{rating}/5 fra {n} signerede Nostr-anmeldelser.",
"seo.desc.noReviews": "Der er endnu ingen anmeldelser.", "seo.desc.noReviews": "Der er endnu ingen anmeldelser.",
"seo.desc.status": "{status}", "seo.desc.status": "{status}",
"seo.desc.statusLimits": "{status}, grænser {limits}.", "seo.desc.statusLimits": "{status}, grænser {limits}.",
@@ -567,8 +591,8 @@
"seo.mint.ogAlt.other": "{name}: {rating}-stjerner fra anmeldelser af {n} på Cashumints.space", "seo.mint.ogAlt.other": "{name}: {rating}-stjerner fra anmeldelser af {n} på Cashumints.space",
"seo.mint.ogAltNoReviews": "{name} på Cashumints.space", "seo.mint.ogAltNoReviews": "{name} på Cashumints.space",
"nav.home": "Hjem", "nav.home": "Hjem",
"mint.more.title": "Flere pastiller", "mint.more.title": "Flere mints",
"mint.more.all": "Se alle pastiller", "mint.more.all": "Se alle mints",
"nav.fedimints": "Fedimints", "nav.fedimints": "Fedimints",
"status.announced": "Kun annonceret", "status.announced": "Kun annonceret",
"card.unnamedFederation": "Ubenævnt forbund", "card.unnamedFederation": "Ubenævnt forbund",
@@ -620,7 +644,7 @@
"fedimint.more.all": "Se alle forbund", "fedimint.more.all": "Se alle forbund",
"fedimint.disclaimer": "Når man melder sig ind i en sammenslutning, betyder det, at man overlader sine penge til dens forvaltere. Bedømmelserne er udtryk for fællesskabets holdninger, og at en kandidat er bekræftet som opstillet, betyder blot, at vedkommende kan kontaktes – ikke at vedkommende er pålidelig.", "fedimint.disclaimer": "Når man melder sig ind i en sammenslutning, betyder det, at man overlader sine penge til dens forvaltere. Bedømmelserne er udtryk for fællesskabets holdninger, og at en kandidat er bekræftet som opstillet, betyder blot, at vedkommende kan kontaktes – ikke at vedkommende er pålidelig.",
"fedimint.module.lightning": "Lightning", "fedimint.module.lightning": "Lightning",
"fedimint.module.mint": "Ecash i perfekt stand", "fedimint.module.mint": "Ecash-mint",
"fedimint.module.wallet": "On-chain-tegnebog", "fedimint.module.wallet": "On-chain-tegnebog",
"fedimint.module.meta": "Metadata", "fedimint.module.meta": "Metadata",
"fedimint.module.stability_pool": "Stabilitetspulje", "fedimint.module.stability_pool": "Stabilitetspulje",
@@ -636,7 +660,7 @@
"feed.ecosystem.cashu": "Cashu", "feed.ecosystem.cashu": "Cashu",
"feed.ecosystem.fedimint": "Fedimint", "feed.ecosystem.fedimint": "Fedimint",
"feed.filterBySubject": "Filtrer efter »mint« eller »federation«", "feed.filterBySubject": "Filtrer efter »mint« eller »federation«",
"feed.anySubject": "Enhver mønt eller møntforening", "feed.anySubject": "Ethvert mint eller enhver føderation",
"feed.clearSubject": "Ryd navnefilteret", "feed.clearSubject": "Ryd navnefilteret",
"home.topFedimints.title": "De bedste Fedimint-modeller", "home.topFedimints.title": "De bedste Fedimint-modeller",
"home.topFedimints.sub": "rangeret efter anmeldelser og bedømmelser", "home.topFedimints.sub": "rangeret efter anmeldelser og bedømmelser",
@@ -652,7 +676,113 @@
"mint.warnings.neverConfirmed.body.quiet": "Denne sammenslutning blev annonceret på Nostr for {date} og {days} siden; der er siden da ikke kommet nogen bekræftelse på, at den er i drift, og ingen har anmeldt den for nylig. Alt nedenfor stammer fra annonceringen.", "mint.warnings.neverConfirmed.body.quiet": "Denne sammenslutning blev annonceret på Nostr for {date} og {days} siden; der er siden da ikke kommet nogen bekræftelse på, at den er i drift, og ingen har anmeldt den for nylig. Alt nedenfor stammer fra annonceringen.",
"mint.warnings.neverConfirmed.meta": "{date} blev annonceret, men aldrig bekræftet", "mint.warnings.neverConfirmed.meta": "{date} blev annonceret, men aldrig bekræftet",
"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 minting, melting og support? 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."
} }
+260 -130
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",
@@ -33,26 +33,30 @@
"status.unknown": "Noch nicht geprüft", "status.unknown": "Noch nicht geprüft",
"nav.skip": "Zum Inhalt springen", "nav.skip": "Zum Inhalt springen",
"nav.main": "Hauptseite", "nav.main": "Hauptseite",
"nav.mints": "Cashu-Münzen", "nav.mints": "Cashu-Mints",
"nav.reviews": "Rezensionen", "nav.reviews": "Rezensionen",
"nav.wallets": "Geldbörsen", "nav.wallets": "Geldbörsen",
"nav.about": "Über", "nav.about": "Über",
"nav.searchMints": "Suche nach „{n}“-Münzen", "nav.searchMints": "Unter {n} Mints suchen",
"lang.label": "Sprache", "lang.label": "Sprache",
"lang.switcher": "Wählen Sie eine Sprache aus", "lang.switcher": "Wählen Sie eine Sprache aus",
"footer.copyright": "© {year} Cashumints.space", "footer.copyright": "© {year} Cashumints.space",
"footer.allMints": "Alle Cashu-Münzen", "footer.allMints": "Alle Cashu-Mints",
"footer.allReviews": "Alle Bewertungen", "footer.allReviews": "Alle Bewertungen",
"footer.wallets": "Geldbörsen", "footer.wallets": "Geldbörsen",
"footer.about": "Über", "footer.about": "Über",
"footer.disclaimer": "Haftungsausschluss", "footer.disclaimer": "Haftungsausschluss",
"footer.terms": "Begriffe", "footer.terms": "Nutzungsbedingungen",
"footer.privacy": "Datenschutz", "footer.privacy": "Datenschutz",
"footer.madeBy": "Mit ❤️ erstellt von {azzamo} · Unterstützt von Cashu und NIP-87", "footer.madeBy": "Mit ❤️ erstellt von {azzamo} · Unterstützt von Cashu und NIP-87",
"footer.warning": "Münzprägeanstalten fungieren als Verwahrer. Eine Münzprägeanstalt kann mit Ihrem Geld verschwinden.", "footer.warning": "Mints sind treuhänderisch. Ein Mint kann mit deinem Geld verschwinden.",
"home.title": "Cashumints.space · Finden Sie eine Cashu-Mint, der Sie vertrauen können", "footer.tagline": "Community-Bewertungen für ecash-Mints, signiert auf Nostr.",
"home.description": "Finden Sie eine Cashu-Mint, der Sie vertrauen können. {mints} listet Mints auf, die mit Bewertungen der {reviews}-Community auf Nostr verzeichnet sind, sowie Informationen zur aktuellen Verfügbarkeit, zu den unterstützten NUTs und zur Mint-Software.", "footer.col.explore": "Entdecken",
"home.hero.title": "Finde eine Cashu-Münzprägeanstalt", "footer.col.site": "Seite",
"footer.nav": "Fußzeile",
"home.title": "Cashumints.space · Finde ein Cashu-Mint, dem du vertrauen kannst",
"home.description": "Finde ein Cashu-Mint, dem du vertrauen kannst. {mints} Mints im Index, {reviews} auf Nostr signierte Community-Bewertungen, dazu Live-Verfügbarkeit, unterstützte NUTs und Mint-Software.",
"home.hero.title": "Finde ein Cashu-Mint",
"home.hero.titleEm": "auf die man sich verlassen kann", "home.hero.titleEm": "auf die man sich verlassen kann",
"home.hero.lede": "Jede Mint im Nostr-Netzwerk, mit echten Bewertungen aus der Community. Keine Gatekeeper, nur signierte Bewertungen, die Sie selbst überprüfen können.", "home.hero.lede": "Jede Mint im Nostr-Netzwerk, mit echten Bewertungen aus der Community. Keine Gatekeeper, nur signierte Bewertungen, die Sie selbst überprüfen können.",
"home.search.label": "Mints nach Name, Domain oder URL suchen", "home.search.label": "Mints nach Name, Domain oder URL suchen",
@@ -61,9 +65,9 @@
"home.filters.highestRated": "Am besten bewertet", "home.filters.highestRated": "Am besten bewertet",
"home.filters.recentlyActive": "Zuletzt aktiv", "home.filters.recentlyActive": "Zuletzt aktiv",
"home.filters.websockets": "Unterstützt WebSockets", "home.filters.websockets": "Unterstützt WebSockets",
"home.top.title": "Die besten Cashu-Münzen", "home.top.title": "Die besten Cashu-Mints",
"home.top.sub": "nach Bewertungen und Sternebewertung sortiert", "home.top.sub": "nach Bewertungen und Sternebewertung sortiert",
"home.top.all": "Alle „{n}“-Minzbonbons →", "home.top.all": "Alle {n} Mints →",
"home.latest.title": "Neueste Bewertungen", "home.latest.title": "Neueste Bewertungen",
"home.latest.sub": "Live aus dem Nostr-Netzwerk", "home.latest.sub": "Live aus dem Nostr-Netzwerk",
"home.latest.prev": "Frühere Rezensionen", "home.latest.prev": "Frühere Rezensionen",
@@ -78,106 +82,108 @@
"home.why.verify.body": "Die Bewertungen werden auf öffentlichen Nostr-Relays gespeichert, nicht in unserer Datenbank. Wenn Sie einen beliebigen Client auf dieselben Relays verweisen, werden Ihnen dieselben Daten angezeigt.", "home.why.verify.body": "Die Bewertungen werden auf öffentlichen Nostr-Relays gespeichert, nicht in unserer Datenbank. Wenn Sie einen beliebigen Client auf dieselben Relays verweisen, werden Ihnen dieselben Daten angezeigt.",
"home.why.checked.title": "Wird kontinuierlich überprüft", "home.why.checked.title": "Wird kontinuierlich überprüft",
"home.why.checked.body": "Wir überprüfen jede Mint-Version auf Verfügbarkeit, Softwareversion, Einschränkungen und unterstützte NUTs, sodass die angezeigten Spezifikationen aktuell sind und nicht auf Selbstangaben beruhen.", "home.why.checked.body": "Wir überprüfen jede Mint-Version auf Verfügbarkeit, Softwareversion, Einschränkungen und unterstützte NUTs, sodass die angezeigten Spezifikationen aktuell sind und nicht auf Selbstangaben beruhen.",
"home.cta.title": "Sie denken darüber nach, eine Münze zu kaufen? Lesen Sie erst einmal hier.", "home.cta.title": "Du denkst über ein Mint nach? Lies erst nach.",
"home.cta.body": "In einer Mint wird Ihr E-Geld aufbewahrt. Lesen Sie die Erfahrungen anderer Nutzer, bevor Sie Sats einzahlen, und hinterlassen Sie eine Bewertung, wenn Sie selbst eine Mint genutzt haben.", "home.cta.body": "Ein Mint verwahrt dein ecash. Lies, was andere herausgefunden haben, bevor du Sats einzahlst, und schreib eine Bewertung, wenn du selbst eines genutzt hast.",
"home.cta.explore": "Entdecke alle Minzsorten", "home.cta.explore": "Alle Mints entdecken",
"home.cta.learn": "Erfahren Sie mehr über Cashu", "home.cta.learn": "Erfahren Sie mehr über Cashu",
"pulse.mintsIndexed": "Münzen, nach Index geordnet", "pulse.mintsIndexed": "Mints im Index",
"pulse.reviewsOnNostr": "Bewertungen zu Nostr", "pulse.reviewsOnNostr": "Bewertungen zu Nostr",
"pulse.lastReview": "letzte Überprüfung", "pulse.lastReview": "letzte Überprüfung",
"pulse.mintsOnline": "Minzbonbons online", "pulse.mintsOnline": "Mints online",
"pulse.indexer": "Indexierer", "pulse.indexer": "Indexierer",
"pulse.connected": "verbunden", "pulse.connected": "verbunden",
"pulse.stale": "abgestanden", "pulse.stale": "abgestanden",
"mints.title": "Alle Cashu-Mints · Cashumints.space", "mints.title": "Alle Cashu-Mints · Cashumints.space",
"mints.description": "Alle „{total}“-Cashu-Mints, die aus dem Nostr-Netzwerk indexiert wurden: {online} (online) und {offline} (offline), mit Community-Bewertungen, Verfügbarkeitsdaten und Mint-Software. Durchsuchen und sortieren Sie die vollständige Liste.", "mints.description": "Alle {total} Cashu-Mints aus dem Nostr-Netzwerk indexiert, {online} online und {offline} offline, mit Community-Bewertungen, Verfügbarkeit und Mint-Software. Durchsuche und sortiere die ganze Liste.",
"mints.heading": "Alle Cashu-Münzen", "mints.heading": "Alle Cashu-Mints",
"mints.lede": "Alle im Nostr-Netzwerk entdeckten Mints, einschließlich derjenigen, die nicht mehr existieren. Offline-Mints bleiben weiterhin aufgelistet, sodass Sie sie nach wie vor finden und einsehen können.", "mints.lede": "Jedes im Nostr-Netzwerk gefundene Mint, auch die, die es nicht mehr gibt. Offline-Mints bleiben gelistet, damit du sie weiterhin finden und bewerten kannst.",
"mints.search.label": "Such nach Mints nach Namen oder Domain", "mints.search.label": "Such nach Mints nach Namen oder Domain",
"mints.search.placeholder": "Nach Name oder Domain suchen…", "mints.search.placeholder": "Nach Name oder Domain suchen…",
"mints.sort.label": "Minzbonbons sortieren", "mints.sort.label": "Mints sortieren",
"mints.sort.score": "Gesamtbester", "mints.sort.score": "Gesamtbester",
"mints.sort.rating": "Am besten bewertet", "mints.sort.rating": "Am besten bewertet",
"mints.sort.reviews": "Am häufigsten bewertet", "mints.sort.reviews": "Am häufigsten bewertet",
"mints.sort.recent": "Kürzlich rezensiert", "mints.sort.recent": "Kürzlich rezensiert",
"mints.sort.seen": "Kürzlich online angesehen", "mints.sort.seen": "Kürzlich online angesehen",
"mints.hideOffline": "Als offline ausblenden", "mints.hideOffline": "Als offline ausblenden",
"mints.showing": "{total}-Münzen anzeigen ({online} online, {offline} offline)", "mints.showing": "{total} Mints angezeigt ({online} online, {offline} offline)",
"mints.showingFiltered": "Anzeige von „{shown}“ von „{total}“ Mints ({online} online, {offline} offline)", "mints.showingFiltered": "Anzeige von „{shown}“ von „{total}“ Mints ({online} online, {offline} offline)",
"mints.noResults": "Es wurden keine Übereinstimmungen gefunden. Versuchen Sie es mit einer Domain oder fügen Sie eine Mint-URL ein.", "mints.noResults": "Es wurden keine Übereinstimmungen gefunden. Versuchen Sie es mit einer Domain oder fügen Sie eine Mint-URL ein.",
"mints.clearSearch": "Suche löschen", "mints.clearSearch": "Suche löschen",
"card.noRatings": "Noch keine Bewertungen", "card.noRatings": "Noch keine Bewertungen",
"card.reviews.one": "Rezension zu „{n}“", "card.reviews.one": "{n} Bewertung",
"card.reviews.other": "Bewertungen zu „{n}“", "card.reviews.other": "{n} Bewertungen",
"card.lastSeen": "Zuletzt gesehen unter {when}", "card.lastSeen": "Zuletzt gesehen unter {when}",
"card.neverSeen": "noch nie online gesehen", "card.neverSeen": "noch nie online gesehen",
"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 zum Cashu-Mint",
"mint.titleFedimint": "{name} · Bewertungen zur Fedimint-Föderation",
"mint.titleLnurl": "{name} · Bewertungen zum LNURL-Mint",
"mint.breadcrumb": "Navigationspfad", "mint.breadcrumb": "Navigationspfad",
"mint.crumbMints": "Cashu-Münzen", "mint.crumbMints": "Cashu-Mints",
"mint.copyUrl": "URL kopieren", "mint.copyUrl": "Mint-URL kopieren",
"mint.qrButton": "Mint QR", "mint.qrButton": "Mint QR",
"mint.writeReview": "Eine Bewertung schreiben", "mint.writeReview": "Eine Bewertung schreiben",
"mint.rankByScore": "# {rank} nach Punktestand", "mint.rankByScore": "#{rank} nach Punktestand",
"mint.tagLightning": "Blitz", "mint.tagLightning": "Lightning",
"mint.summary": "Zusammenfassung zu Mint", "mint.summary": "Mint-Zusammenfassung",
"mint.rating.label": "Gemeinschaftstarif", "mint.rating.label": "Community-Bewertung",
"mint.rating.none": "Noch keine Bewertungen", "mint.rating.none": "Noch keine Bewertungen",
"mint.rating.reviews.one": "Rezension zu „{n}“", "mint.rating.reviews.one": "{n} Bewertung",
"mint.rating.reviews.other": "Bewertungen zu „{n}“", "mint.rating.reviews.other": "{n} Bewertungen",
"mint.rating.unrated.one": "{n}t keine Bewertung abgegeben.", "mint.rating.unrated.one": "{n} hat keine Bewertung abgegeben",
"mint.rating.unrated.other": "{n}t keine Bewertung abgegeben.", "mint.rating.unrated.other": "{n} haben keine Bewertung abgegeben",
"mint.activity.label": "Aktivität", "mint.activity.label": "Aktivität",
"mint.activity.since": "seit der letzten Überprüfung", "mint.activity.since": "seit der letzten Bewertung",
"mint.activity.noReviews": "noch keine Bewertungen", "mint.activity.noReviews": "noch keine Bewertungen",
"mint.activity.recent.one": "{n}-Bewertung in den letzten 90 Tagen", "mint.activity.recent.one": "{n} Bewertung in den letzten 90 Tagen",
"mint.activity.recent.other": "Bewertungen zu „{n}“ in den letzten 90 Tagen", "mint.activity.recent.other": "{n} Bewertungen in den letzten 90 Tagen",
"mint.limits.label": "Grenzen", "mint.limits.label": "Limits",
"mint.limits.notPublished": "nicht veröffentlicht", "mint.limits.notPublished": "nicht veröffentlicht",
"mint.limits.min": "min", "mint.limits.min": "min",
"mint.limits.max": "max", "mint.limits.max": "max",
"mint.limits.mintRow": "Minze", "mint.limits.mintRow": "minten",
"mint.limits.meltRow": "schmelzen", "mint.limits.meltRow": "schmelzen",
"mint.limits.disabled": "deaktiviert", "mint.limits.disabled": "deaktiviert",
"mint.limits.range": "{min} bis {max} {unit}", "mint.limits.range": "{min} bis {max} {unit}",
"mint.limits.upTo": "bis zu {max} {unit}", "mint.limits.upTo": "bis zu {max} {unit}",
"mint.limits.from": "aus {min} {unit}", "mint.limits.from": "ab {min} {unit}",
"mint.software.label": "Mint-Software", "mint.software.label": "Mint-Software",
"mint.software.unknown": "unbekannt", "mint.software.unknown": "unbekannt",
"mint.checked": "überprüft unter {when}", "mint.checked": "geprüft {when}",
"mint.lastSeen": "Zuletzt gesehen unter {when}", "mint.lastSeen": "zuletzt gesehen {when}",
"mint.neverReached": "nie erreicht", "mint.neverReached": "nie erreicht",
"mint.uptime.checks": "{ok} der letzten {total}-Prüfung ist erreichbar", "mint.uptime.checks": "{ok} der letzten {total} Prüfungen erreichbar",
"mint.uptime.percent": "{percent}%, erreichbar über 30 Tage", "mint.uptime.percent": "{percent}% erreichbar über 30 Tage",
"mint.uptime.description": "{ok}e der letzten „{total}“-Prüfungen waren erfolgreich", "mint.uptime.description": "{ok} der letzten {total} Prüfungen erfolgreich",
"mint.operator.title": "Vom Betreiber", "mint.operator.title": "Vom Betreiber",
"mint.operator.motd": "Botschaft des Tages", "mint.operator.motd": "Nachricht des Tages",
"mint.operator.beta": "Diese Mint bezeichnet sich selbst als experimentell oder als Beta-Version. Lesen Sie die folgenden Aussagen des Betreibers, bevor Sie etwas dort hinterlegen, das Sie nicht verlieren dürfen.", "mint.operator.beta": "Diese Mint bezeichnet sich selbst als experimentell oder als Beta-Version. Lesen Sie die folgenden Aussagen des Betreibers, bevor Sie etwas dort hinterlegen, das Sie nicht verlieren dürfen.",
"mint.nuts.title": "Unterstützte NUTs", "mint.nuts.title": "Unterstützte NUTs",
"mint.nuts.noneOffline": "Für diese Mint wurde keine NUT-Liste zwischengespeichert; sie ging offline, bevor eine Liste erfasst werden konnte.", "mint.nuts.noneOffline": "Für diese Mint wurde keine NUT-Liste zwischengespeichert; sie ging offline, bevor eine Liste erfasst werden konnte.",
"mint.nuts.noneYet": "Für diese Prägeanstalt wurde noch keine NUT-Liste zwischengespeichert.", "mint.nuts.noneYet": "Für dieses Mint wurde noch keine NUT-Liste zwischengespeichert.",
"mint.nuts.supported": "{n}e unter {total} bekannten unterstützten NUTs", "mint.nuts.supported": "{n} von {total} bekannten NUTs unterstützt",
"mint.nuts.whatAre": "Was sind NUTs?", "mint.nuts.whatAre": "Was sind NUTs?",
"mint.nutRow.disabled": "deaktiviert", "mint.nutRow.disabled": "deaktiviert",
"mint.nutRow.notSupported": "wird nicht unterstützt", "mint.nutRow.notSupported": "wird nicht unterstützt",
"mint.details.title": "Details zur Münzprägeanstalt", "mint.details.title": "Mint-Details",
"mint.details.pubkey": "Öffentlicher Schlüssel", "mint.details.pubkey": "Pubkey",
"mint.details.nostr": "Nostr", "mint.details.nostr": "Nostr",
"mint.details.email": "E-Mail", "mint.details.email": "E-Mail",
"mint.details.serverTime": "Serverzeit", "mint.details.serverTime": "Serverzeit",
"mint.details.firstSeen": "Erstmals gesehen", "mint.details.firstSeen": "Erstmals gesehen",
"mint.details.terms": "Begriffe", "mint.details.terms": "Nutzungsbedingungen",
"mint.kind.identifier": "Bezeichner", "mint.kind.identifier": "Bezeichner",
"mint.kind.mintPubkey": "Mint-Schlüssel", "mint.kind.mintPubkey": "Mint-Pubkey",
"mint.kind.nostrAddress": "Unsere Adresse", "mint.kind.nostrAddress": "Nostr-Adresse",
"mint.kind.emailAddress": "E-Mail-Adresse", "mint.kind.emailAddress": "E-Mail-Adresse",
"mint.kind.npub": "npub", "mint.kind.npub": "npub",
"mint.disclaimer": "Diese Mint fungiert als Verwahrstelle: Sie verwahrt Ihr Geld und kann dies jederzeit einstellen. Bewertungen sind Meinungen der Community, und „online erreichbar“ bedeutet lediglich, dass die Plattform online verfügbar ist – nicht, dass sie vertrauenswürdig ist.", "mint.disclaimer": "Diese Mint fungiert als Verwahrstelle: Sie verwahrt Ihr Geld und kann dies jederzeit einstellen. Bewertungen sind Meinungen der Community, und „online erreichbar“ bedeutet lediglich, dass die Plattform online verfügbar ist – nicht, dass sie vertrauenswürdig ist.",
"mint.disclaimerLink": "Lesen Sie den vollständigen Haftungsausschluss", "mint.disclaimerLink": "Lesen Sie den vollständigen Haftungsausschluss",
"mint.qr.title": "Füge „{name}“ zu deinem Wallet hinzu", "mint.qr.title": "Füge „{name}“ zu deinem Wallet hinzu",
"mint.qr.note": "Scanne diesen Code mit einer Cashu-Wallet, um die Münze hinzuzufügen, oder kopiere die URL und füge sie ein.", "mint.qr.note": "Scanne das mit einer Cashu-Wallet, um das Mint hinzuzufügen, oder kopiere die URL und füge sie ein.",
"mint.qr.warn": "Eine Münzprägeanstalt hinzuzufügen bedeutet, ihr Ihr Geld anzuvertrauen.", "mint.qr.warn": "Ein Mint hinzuzufügen heißt, ihm dein Geld anzuvertrauen.",
"mint.qr.whatMeans": "Was das bedeutet", "mint.qr.whatMeans": "Was das bedeutet",
"mint.qr.wallets": "Wallets, die Cashu unterstützen", "mint.qr.wallets": "Wallets, die Cashu unterstützen",
"mint.chip.frozen": "Gefroren", "mint.chip.frozen": "Gefroren",
@@ -187,8 +193,8 @@
"mint.warnings.dayCount.other": "{n}-Tage", "mint.warnings.dayCount.other": "{n}-Tage",
"mint.warnings.gone.lead": "Wahrscheinlich weg.", "mint.warnings.gone.lead": "Wahrscheinlich weg.",
"mint.warnings.gone.body.since": "Diese Mint ist seit dem {date} ({days}) nicht mehr erreichbar. Betrachten Sie die dort hinterlegten Gelder als verloren und überweisen Sie keine neuen Gelder mehr dorthin. Die folgenden Angaben zeigen den letzten bekannten Status.", "mint.warnings.gone.body.since": "Diese Mint ist seit dem {date} ({days}) nicht mehr erreichbar. Betrachten Sie die dort hinterlegten Gelder als verloren und überweisen Sie keine neuen Gelder mehr dorthin. Die folgenden Angaben zeigen den letzten bekannten Status.",
"mint.warnings.gone.body.neverDated": "Diese Münzprägeanstalt hat seit Beginn unserer Beobachtung unter {month} keinen einzigen Scheck eingelöst. Betrachten Sie dort hinterlegte Gelder als verloren und überweisen Sie keine neuen Gelder. Alle folgenden Informationen stammen aus der Nostr-Mitteilung, nicht von der Münzprägeanstalt.", "mint.warnings.gone.body.neverDated": "Dieses Mint hat seit Beginn unserer Beobachtung im {month} auf keine einzige Prüfung geantwortet. Betrachte dort verwahrtes Geld als gefährdet und zahle nichts Neues ein. Alles Folgende stammt aus der Nostr-Ankündigung, nicht vom Mint.",
"mint.warnings.gone.body.never": "Diese Münzprägeanstalt hat seit Beginn unserer Beobachtung keinen einzigen Scheck eingelöst. Betrachten Sie dort hinterlegte Gelder als gefährdet und überweisen Sie keine neuen Beträge. Die folgenden Informationen stammen aus der Ankündigung von Nostr, nicht von der Münzprägeanstalt.", "mint.warnings.gone.body.never": "Dieses Mint hat seit Beginn unserer Beobachtung auf keine einzige Prüfung geantwortet. Betrachte dort verwahrtes Geld als gefährdet und zahle nichts Neues ein. Alles Folgende stammt aus der Nostr-Ankündigung, nicht vom Mint.",
"mint.warnings.gone.meta.since": "Offline seit {date}", "mint.warnings.gone.meta.since": "Offline seit {date}",
"mint.warnings.gone.meta.never": "Seit seiner Entdeckung war es nie erreichbar", "mint.warnings.gone.meta.never": "Seit seiner Entdeckung war es nie erreichbar",
"mint.warnings.meltDisabled.lead": "Auszahlungen sind deaktiviert.", "mint.warnings.meltDisabled.lead": "Auszahlungen sind deaktiviert.",
@@ -196,32 +202,32 @@
"mint.warnings.meltDisabled.body.offline": "Bei dieser Mint war die Funktion „Lightning Melting“ (NUT-05) deaktiviert: Sie können Sats einzahlen, aber nicht über Lightning abheben. Bitte nehmen Sie keine Einzahlungen vor, bis die Funktion wieder verfügbar ist.", "mint.warnings.meltDisabled.body.offline": "Bei dieser Mint war die Funktion „Lightning Melting“ (NUT-05) deaktiviert: Sie können Sats einzahlen, aber nicht über Lightning abheben. Bitte nehmen Sie keine Einzahlungen vor, bis die Funktion wieder verfügbar ist.",
"mint.warnings.meltDisabled.meta.online": "Auszahlungen sind derzeit deaktiviert", "mint.warnings.meltDisabled.meta.online": "Auszahlungen sind derzeit deaktiviert",
"mint.warnings.meltDisabled.meta.offline": "Auszahlungen waren zuletzt deaktiviert.", "mint.warnings.meltDisabled.meta.offline": "Auszahlungen waren zuletzt deaktiviert.",
"mint.warnings.frozen.lead": "Gefrorene Minze.", "mint.warnings.frozen.lead": "Mint eingefroren.",
"mint.warnings.frozen.body.online": "Sowohl das Prägen als auch das Einschmelzen sind deaktiviert. Über Lightning können keine Sats ein- oder ausgezahlt werden.", "mint.warnings.frozen.body.online": "Minten und Melten sind beide deaktiviert. Über Lightning können keine Sats rein oder raus.",
"mint.warnings.frozen.body.offline": "Sowohl das Prägen als auch das Einschmelzen wurden deaktiviert. Über Lightning können keine Sats ein- oder ausgezahlt werden.", "mint.warnings.frozen.body.offline": "Minten und Melten waren beide deaktiviert. Über Lightning können keine Sats rein oder raus.",
"mint.warnings.frozen.meta.online": "Derzeit ist alles eingefroren, es kommt nichts herein und nichts geht hinaus", "mint.warnings.frozen.meta.online": "Derzeit ist alles eingefroren, es kommt nichts herein und nichts geht hinaus",
"mint.warnings.frozen.meta.offline": "Zuletzt gesehen: eingefroren", "mint.warnings.frozen.meta.offline": "Zuletzt gesehen: eingefroren",
"mint.warnings.offlineLong.lead": "Für „{days}“ offline, da {date}.", "mint.warnings.offlineLong.lead": "Für „{days}“ offline, da {date}.",
"mint.warnings.offlineLong.body": "Die Seite ist möglicherweise nicht mehr verfügbar. Alle folgenden Angaben entsprechen dem letzten bekannten Stand, und die Bewertungen funktionieren weiterhin.", "mint.warnings.offlineLong.body": "Das Mint ist womöglich weg. Alles Folgende ist sein letzter bekannter Stand, und Bewertungen funktionieren weiterhin.",
"mint.warnings.offlineLong.meta": "Offline seit {date}", "mint.warnings.offlineLong.meta": "Offline seit {date}",
"mint.warnings.meltOnly.lead": "Nur schmelzen.", "mint.warnings.meltOnly.lead": "Nur schmelzen.",
"mint.warnings.meltOnly.body.online": "Die Ausgabe neuer Coins (NUT-04) ist deaktiviert: Sie können keine Einzahlungen vornehmen, aber vorhandenes Ecash kann weiterhin abgehoben werden. Dies bedeutet oft, dass eine Coin-Ausgabe ausläuft. Wenn Sie also Ecash dieser Coin besitzen, sollten Sie in Erwägung ziehen, es sich auszahlen zu lassen.", "mint.warnings.meltOnly.body.online": "Neues Minten (NUT-04) ist deaktiviert: Einzahlen geht nicht, vorhandenes ecash lässt sich aber weiterhin auszahlen. Das heißt oft, dass ein Mint den Betrieb einstellt. Wenn du sein ecash hältst, denk übers Melten nach.",
"mint.warnings.meltOnly.body.offline": "Die Ausgabe neuer Coins (NUT-04) wurde deaktiviert: Einzahlungen sind nicht mehr möglich, aber bereits vorhandenes E-Cash kann weiterhin abgehoben werden. Dies deutet oft darauf hin, dass eine Coin-Ausgabe ausläuft. Wenn Sie also E-Cash dieser Coin besitzen, sollten Sie in Erwägung ziehen, es sich auszahlen zu lassen.", "mint.warnings.meltOnly.body.offline": "Neues Minten (NUT-04) war deaktiviert: Einzahlen geht nicht, vorhandenes ecash lässt sich aber weiterhin auszahlen. Das heißt oft, dass ein Mint den Betrieb einstellt. Wenn du sein ecash hältst, denk übers Melten nach.",
"mint.warnings.meltOnly.meta.online": "Derzeit nur schmelzen", "mint.warnings.meltOnly.meta.online": "Derzeit nur schmelzen",
"mint.warnings.meltOnly.meta.offline": "Erst schmelzen, wenn zuletzt gesehen", "mint.warnings.meltOnly.meta.offline": "Erst schmelzen, wenn zuletzt gesehen",
"mint.warnings.offline.lead": "Seit dem {date} offline.", "mint.warnings.offline.lead": "Seit dem {date} offline.",
"mint.warnings.offline.body": "Die Details wurden damals zuletzt bestätigt. Sie können weiterhin eine Bewertung abgeben.", "mint.warnings.offline.body": "Die Details wurden damals zuletzt bestätigt. Sie können weiterhin eine Bewertung abgeben.",
"mint.warnings.offline.meta": "Offline seit {date}", "mint.warnings.offline.meta": "Offline seit {date}",
"mint.warnings.also.meltDisabled": "Bevor es offline ging, hatte es zudem Auszahlungen deaktiviert.", "mint.warnings.also.meltDisabled": "Bevor es offline ging, hatte es zudem Auszahlungen deaktiviert.",
"mint.warnings.also.mintDisabled": "Bevor das System offline ging, hatte es zudem die Ausgabe neuer Coins deaktiviert.", "mint.warnings.also.mintDisabled": "Vor dem Offlinegehen hatte es auch neues Minten deaktiviert.",
"mint.warnings.also.offline": "Außerdem ist die Website „{date}“ ({days}) seit einiger Zeit nicht mehr erreichbar.", "mint.warnings.also.offline": "Außerdem ist die Website „{date}“ ({days}) seit einiger Zeit nicht mehr erreichbar.",
"nut.0": "Notation und Modelle", "nut.0": "Notation und Modelle",
"nut.1": "Austausch öffentlicher Schlüssel bei Mint", "nut.1": "Austausch öffentlicher Schlüssel bei Mint",
"nut.2": "Schlüsselsätze und Gebühren", "nut.2": "Schlüsselsätze und Gebühren",
"nut.3": "Token tauschen", "nut.3": "Token tauschen",
"nut.4": "Prägung über Lightning", "nut.4": "Minten über Lightning",
"nut.5": "Schmelzen durch Blitzschlag", "nut.5": "Melten über Lightning",
"nut.6": "Informationen zur Münzprägeanstalt", "nut.6": "Mint-Informationen",
"nut.7": "Überprüfung des Token-Status", "nut.7": "Überprüfung des Token-Status",
"nut.8": "Zu viel gezahlte Lightning-Gebühren", "nut.8": "Zu viel gezahlte Lightning-Gebühren",
"nut.9": "Signaturen wiederherstellen", "nut.9": "Signaturen wiederherstellen",
@@ -235,7 +241,7 @@
"nut.17": "WebSocket-Abonnements", "nut.17": "WebSocket-Abonnements",
"nut.18": "Zahlungsaufforderungen", "nut.18": "Zahlungsaufforderungen",
"nut.19": "Zwischengespeicherte Antworten", "nut.19": "Zwischengespeicherte Antworten",
"nut.20": "Signierte Zitate in neuwertigem Zustand", "nut.20": "Signierte Mint-Quotes",
"nut.21": "Authentifizierung löschen", "nut.21": "Authentifizierung löschen",
"nut.22": "Blinde Authentifizierung", "nut.22": "Blinde Authentifizierung",
"nut.23": "Zahlungsmethode BOLT11", "nut.23": "Zahlungsmethode BOLT11",
@@ -261,6 +267,9 @@
"reviews.empty": "Es gibt noch keine Bewertungen. Schreiben Sie die erste Bewertung.", "reviews.empty": "Es gibt noch keine Bewertungen. Schreiben Sie die erste Bewertung.",
"reviews.emptyWritten": "Hier gibt es noch keine schriftlichen Bewertungen. Schreiben Sie die erste Bewertung.", "reviews.emptyWritten": "Hier gibt es noch keine schriftlichen Bewertungen. Schreiben Sie die erste Bewertung.",
"reviews.emptyFilter": "Es gibt keine Bewertungen, die diesem Filter entsprechen.", "reviews.emptyFilter": "Es gibt keine Bewertungen, die diesem Filter entsprechen.",
"reviews.rate.cashu": "Diese Mint bewerten",
"reviews.rate.fedimint": "Diese Föderation bewerten",
"reviews.rate.lnurl": "Diese Mint bewerten",
"reviews.noscript": "Die Bewertungen werden auf Nostr signiert und in Ihrem Browser geladen, daher benötigt diese Liste JavaScript. Die Bewertungen und Zahlen oben stammen von der Website selbst.", "reviews.noscript": "Die Bewertungen werden auf Nostr signiert und in Ihrem Browser geladen, daher benötigt diese Liste JavaScript. Die Bewertungen und Zahlen oben stammen von der Website selbst.",
"reviews.relayError": "Das Relais konnte nicht erreicht werden. Die Bewertungen sind möglicherweise unvollständig.", "reviews.relayError": "Das Relais konnte nicht erreicht werden. Die Bewertungen sind möglicherweise unvollständig.",
"reviews.srRating": "{rating} von 5", "reviews.srRating": "{rating} von 5",
@@ -273,7 +282,7 @@
"reviews.anonNote": "Erste Rezension von diesem Verlag, keine weiteren Aktivitäten gefunden", "reviews.anonNote": "Erste Rezension von diesem Verlag, keine weiteren Aktivitäten gefunden",
"reviews.published": "Veröffentlicht.", "reviews.published": "Veröffentlicht.",
"reviews.publishedPartial": "Veröffentlicht auf {accepted} der {total}-Relais.", "reviews.publishedPartial": "Veröffentlicht auf {accepted} der {total}-Relais.",
"reviews.propagating": "Die Übertragung kann einen Moment dauern, bis sie an anderer Stelle angezeigt wird.", "reviews.propagating": "Veröffentlichen auf Nostr-Relays",
"reviews.summary.line.one": "{count}-Bewertung ohne Kommentar: {breakdown}", "reviews.summary.line.one": "{count}-Bewertung ohne Kommentar: {breakdown}",
"reviews.summary.line.other": "{count}-Bewertungen ohne Kommentar: {breakdown}", "reviews.summary.line.other": "{count}-Bewertungen ohne Kommentar: {breakdown}",
"reviews.summary.lineDay.one": "{count}-Bewertung ohne Kommentar auf {when}: {breakdown}", "reviews.summary.lineDay.one": "{count}-Bewertung ohne Kommentar auf {when}: {breakdown}",
@@ -287,7 +296,12 @@
"reviews.breakdown.1": "{n}, ein Stern", "reviews.breakdown.1": "{n}, ein Stern",
"reviews.breakdown.none": "{n}, ohne Bewertung", "reviews.breakdown.none": "{n}, ohne Bewertung",
"reviews.dialog.title": "Rezension: „{name}“", "reviews.dialog.title": "Rezension: „{name}“",
"reviews.dialog.note": "Mit Ihrem Nostr-Schlüssel signiert und auf öffentlichen Relays veröffentlicht. Es kann weder bearbeitet noch gelöscht werden.", "reviews.dialog.lede.cashu": "Teilen Sie Ihre Erfahrung mit dieser Mint.",
"reviews.dialog.lede.fedimint": "Teilen Sie Ihre Erfahrung mit dieser Föderation.",
"reviews.dialog.lede.lnurl": "Teilen Sie Ihre Erfahrung mit dieser Mint.",
"reviews.dialog.permanent": "Wird dauerhaft auf öffentlichen Nostr-Relays veröffentlicht.",
"reviews.dialog.gate.title": "Melden Sie sich an, um eine Bewertung zu schreiben",
"reviews.dialog.gate.lead": "Bewertungen werden mit Ihrem Nostr-Schlüssel signiert. Er bleibt in Ihrem Signierer, diese Website sieht ihn nie.",
"reviews.dialog.ratingLegend": "Ihre Bewertung", "reviews.dialog.ratingLegend": "Ihre Bewertung",
"reviews.dialog.starsSr.one": "{n}-Star", "reviews.dialog.starsSr.one": "{n}-Star",
"reviews.dialog.starsSr.other": "{n}-Stars", "reviews.dialog.starsSr.other": "{n}-Stars",
@@ -308,7 +322,6 @@
"reviews.dialog.waitingSigner": "Warten auf Ihren Unterzeichner", "reviews.dialog.waitingSigner": "Warten auf Ihren Unterzeichner",
"reviews.dialog.confirmClose": "Diese Bewertung schließen? Ihr Entwurf wird gespeichert, bis Sie die Website verlassen.", "reviews.dialog.confirmClose": "Diese Bewertung schließen? Ihr Entwurf wird gespeichert, bis Sie die Website verlassen.",
"reviews.dialog.why.login": "Anmelden, um zu veröffentlichen", "reviews.dialog.why.login": "Anmelden, um zu veröffentlichen",
"reviews.dialog.why.rating": "Wählen Sie eine Bewertung aus",
"reviews.dialog.why.words": "Schreib ein paar Worte", "reviews.dialog.why.words": "Schreib ein paar Worte",
"reviews.dialog.why.more.one": "{n}t ein noch charakterstärkerer Charakter", "reviews.dialog.why.more.one": "{n}t ein noch charakterstärkerer Charakter",
"reviews.dialog.why.more.other": "{n} weitere Zeichen", "reviews.dialog.why.more.other": "{n} weitere Zeichen",
@@ -324,21 +337,21 @@
"reviews.validate.rating": "Wählen Sie eine Bewertung von 1 bis 5 Sternen aus.", "reviews.validate.rating": "Wählen Sie eine Bewertung von 1 bis 5 Sternen aus.",
"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 Mints und Föderationen · 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": "Jede Bewertung zu jedem Cashu-Mint und jeder Fedimint-Föderation, neueste zuerst, live von den Nostr-Relays gelesen. Jede ist von ihrem Autor signiert und liegt auf den Relays, nicht in unserer Datenbank.",
"feed.label": "Rezensionen", "feed.label": "Rezensionen",
"feed.showingBuild.one": "Anzeige der aktuellsten Überprüfung aus dem letzten Build", "feed.showingBuild.one": "Anzeige der aktuellsten Überprüfung aus dem letzten Build",
"feed.showingBuild.other": "Anzeige der neuesten Bewertungen für „{n}“ aus dem letzten Build", "feed.showingBuild.other": "Anzeige der neuesten Bewertungen für „{n}“ aus dem letzten Build",
"feed.noscript": "Die Bewertungen sind auf Nostr signiert und werden in Ihrem Browser geladen, daher sind für die Filterfunktion und die vollständige Liste JavaScript erforderlich. Die unten aufgeführten Bewertungen zu „{n}“ wurden beim Erstellen dieser Seite aus den Relays ausgelesen.", "feed.noscript": "Die Bewertungen sind auf Nostr signiert und werden in Ihrem Browser geladen, daher sind für die Filterfunktion und die vollständige Liste JavaScript erforderlich. Die unten aufgeführten Bewertungen zu „{n}“ wurden beim Erstellen dieser Seite aus den Relays ausgelesen.",
"feed.emptyBuild": "Zum Zeitpunkt der Erstellung dieser Seite konnten keine Rückmeldungen von den Relais ausgelesen werden. Die Liste wird in Ihrem Browser automatisch mit den Daten der Relais aktualisiert.", "feed.emptyBuild": "Zum Zeitpunkt der Erstellung dieser Seite konnten keine Rückmeldungen von den Relais ausgelesen werden. Die Liste wird in Ihrem Browser automatisch mit den Daten der Relais aktualisiert.",
"feed.foot": "Bewertungen sind Meinungen der Community und keine verifizierten Angaben. Lesen Sie die „{link}“, bevor Sie einer Münzprägeanstalt Ihr Geld anvertrauen.", "feed.foot": "Bewertungen sind Meinungen der Community, keine geprüften Angaben. Lies den {link}, bevor du einem Mint dein Geld anvertraust.",
"feed.footLink": "Haftungsausschluss", "feed.footLink": "Haftungsausschluss",
"feed.showing": "{shown} von {total} Bewertungen werden angezeigt", "feed.showing": "{shown} von {total} Bewertungen werden angezeigt",
"feed.showingMatching": "{shown} von {matching} passenden Bewertungen werden angezeigt (insgesamt {total})", "feed.showingMatching": "{shown} von {matching} passenden Bewertungen werden angezeigt (insgesamt {total})",
"feed.emptyRelays": "Von den Relays wurden keine Bewertungen zurückgemeldet. Öffne eine beliebige Mint-Seite, um dort Bewertungen zu lesen und zu schreiben.", "feed.emptyRelays": "Von den Relays wurden keine Bewertungen zurückgemeldet. Öffne eine beliebige Mint-Seite, um dort Bewertungen zu lesen und zu schreiben.",
"feed.emptyFilter": "Es wurden keine Bewertungen gefunden, die diesem Filter entsprechen. Leeren Sie das Suchfeld oder wählen Sie „Alle“ aus.", "feed.emptyFilter": "Keine Bewertungen passen zu diesem Filter. Leere das Mint-Feld oder probier Alle.",
"feed.relayError": "Die Relais konnten nicht erreicht werden. Es werden die Bewertungen aus dem letzten Build angezeigt.", "feed.relayError": "Die Relais konnten nicht erreicht werden. Es werden die Bewertungen aus dem letzten Build angezeigt.",
"feed.relayEmpty": "Von den Relais wurden keine Rückmeldungen erhalten. Es werden die Rückmeldungen aus dem letzten Build angezeigt.", "feed.relayEmpty": "Von den Relais wurden keine Rückmeldungen erhalten. Es werden die Rückmeldungen aus dem letzten Build angezeigt.",
"feed.reading": "Die Relais ablesen…", "feed.reading": "Die Relais ablesen…",
@@ -369,6 +382,10 @@
"login.status.waitingSigner": "Warten auf Ihren Unterzeichner", "login.status.waitingSigner": "Warten auf Ihren Unterzeichner",
"login.status.connecting": "Verbindung zum Unterzeichner herstellen", "login.status.connecting": "Verbindung zum Unterzeichner herstellen",
"login.status.connected": "Verbunden", "login.status.connected": "Verbunden",
"login.toast.loggedIn": "Angemeldet",
"login.toast.fetchingProfile": "Dein Nostr-Profil wird von den Relays geladen…",
"login.toast.profile": "Profil: {name}",
"login.toast.noProfile": "Kein Profil auf den Relays gefunden",
"login.method.extension": "Erweiterung", "login.method.extension": "Erweiterung",
"login.method.remote": "Fernbedienung", "login.method.remote": "Fernbedienung",
"login.method.primal": "Ursprünglich", "login.method.primal": "Ursprünglich",
@@ -393,18 +410,25 @@
"notfound.description": "Diese Seite existiert auf Cashumints.space nicht.", "notfound.description": "Diese Seite existiert auf Cashumints.space nicht.",
"notfound.heading": "Diese Seite existiert nicht", "notfound.heading": "Diese Seite existiert nicht",
"notfound.lede": "Möglicherweise wurde sie verschoben, oder die Mint ist noch nicht indexiert. Suchen Sie nach einer Mint anhand des Namens oder der Domain, oder fügen Sie die URL der Mint ein.", "notfound.lede": "Möglicherweise wurde sie verschoben, oder die Mint ist noch nicht indexiert. Suchen Sie nach einer Mint anhand des Namens oder der Domain, oder fügen Sie die URL der Mint ein.",
"notfound.browse": "Alle Minzsorten anzeigen", "notfound.browse": "Alle Mints ansehen",
"notfound.home": "Geh nach Hause", "notfound.home": "Geh nach Hause",
"notfound.loading": "{host} wird geladen", "notfound.loading": "{host} wird geladen",
"notfound.fb.rating": "Bewertung", "notfound.fb.rating": "Bewertung",
"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",
"wallets.lede": "Sie benötigen eine Wallet, um E-Geld von einer dieser Prägeanstalten aufzubewahren. Die meisten Wallets kommunizieren gleichzeitig mit mehreren Prägeanstalten, sodass Sie nicht an die erste gebunden sind, die Sie ausprobieren.", "wallets.lede": "Du brauchst eine Wallet, um ecash von einem dieser Mints zu halten. Die meisten Wallets sprechen mit mehreren Mints gleichzeitig, du bist also nicht an das erste gebunden, das du ausprobierst.",
"wallets.note": "Diese Liste erhebt keinen Anspruch auf Vollständigkeit und stellt keine Empfehlung dar. Das Cashu-Projekt führt unter {link} ein umfassenderes Verzeichnis. Eine Wallet verwahrt die Schlüssel, eine Mint verwahrt das Geld: Lies dir die Bewertungen der Mints durch, bevor du dich entscheidest, wo deine Sats aufbewahrt werden sollen.", "wallets.note": "Diese Liste erhebt keinen Anspruch auf Vollständigkeit und stellt keine Empfehlung dar. Das Cashu-Projekt führt unter {link} ein umfassenderes Verzeichnis. Eine Wallet verwahrt die Schlüssel, eine Mint verwahrt das Geld: Lies dir die Bewertungen der Mints durch, bevor du dich entscheidest, wo deine Sats aufbewahrt werden sollen.",
"wallets.platform.android": "Android", "wallets.platform.android": "Android",
"wallets.platform.ios": "iOS", "wallets.platform.ios": "iOS",
@@ -412,11 +436,11 @@
"wallets.platform.extension": "Browser-Erweiterung", "wallets.platform.extension": "Browser-Erweiterung",
"wallets.platform.cli": "CLI", "wallets.platform.cli": "CLI",
"wallets.platform.python": "Python", "wallets.platform.python": "Python",
"wallets.item.minibits": "Mobile E-Cash-Wallet mit eigener Coin-Generierung, Unterstützung für Lightning-Adressen und Nostr-Kontakten.", "wallets.item.minibits": "Mobile ecash-Wallet mit eigenem Mint, Unterstützung für Lightning-Adressen und Nostr-Kontakten.",
"wallets.item.nutstash": "Browser-Wallet, das mehrere Mints gleichzeitig verwaltet und Raw-Token importiert und exportiert.", "wallets.item.nutstash": "Browser-Wallet, das mehrere Mints gleichzeitig verwaltet und Raw-Token importiert und exportiert.",
"wallets.item.enuts": "Mobile Geldbörse mit Schwerpunkt auf Guthaben in verschiedenen Währungen und dem Versenden von E-Geld über Nostr.", "wallets.item.enuts": "Mobile Wallet mit Fokus auf Guthaben bei mehreren Mints und dem Versenden von ecash über Nostr.",
"wallets.item.cashume": "Referenz-Web-Wallet aus dem Cashu-Projekt. Läuft vollständig im Browser und kann als PWA installiert werden.", "wallets.item.cashume": "Referenz-Web-Wallet aus dem Cashu-Projekt. Läuft vollständig im Browser und kann als PWA installiert werden.",
"wallets.item.boardwalk": "Ein Web-Wallet, das auf Lightning-Adressen basiert und das Versenden von E-Cash per Link ermöglicht.", "wallets.item.boardwalk": "Web-Wallet rund um Lightning-Adressen und das Versenden von ecash per Link.",
"wallets.item.coinos": "Lightning-Wallet und Kassensystem, das zudem über eine eigene Cashu-Mint verfügt.", "wallets.item.coinos": "Lightning-Wallet und Kassensystem, das zudem über eine eigene Cashu-Mint verfügt.",
"wallets.item.oxchat": "Nostr Messenger mit integrierter eCash-Geldbörse zum Versenden von Sats in Chats.", "wallets.item.oxchat": "Nostr Messenger mit integrierter eCash-Geldbörse zum Versenden von Sats in Chats.",
"wallets.item.nutshell": "Die Referenzimplementierung. Läuft als Wallet über die Befehlszeile und als Mint-Software.", "wallets.item.nutshell": "Die Referenzimplementierung. Läuft als Wallet über die Befehlszeile und als Mint-Software.",
@@ -425,23 +449,23 @@
"about.heading": "Über", "about.heading": "Über",
"about.lede": "Cashumints.space listet alle Cashu-Mints auf, die wir im Nostr-Netzwerk finden können, und zeigt Ihnen, was andere darüber sagen. Die Seite wurde von Azzamo erstellt.", "about.lede": "Cashumints.space listet alle Cashu-Mints auf, die wir im Nostr-Netzwerk finden können, und zeigt Ihnen, was andere darüber sagen. Die Seite wurde von Azzamo erstellt.",
"about.cashu.title": "Was ist Cashu?", "about.cashu.title": "Was ist Cashu?",
"about.cashu.p1": "Cashu ist ein E-Cash-Protokoll für Bitcoin. Eine Prägeanstalt gibt „blinded“ Token aus, die durch die von ihr gehaltenen Sats gedeckt sind, und diese Token werden zwischen den Nutzern ausgetauscht, ohne dass die Prägeanstalt erfährt, wer was an wen gesendet hat. Der Datenschutz ist gewährleistet, und genau darum geht es: Token sind keine Konten, und die Prägeanstalt kann den von ihr signierten Token nicht mit dem zurückkommenden Token in Verbindung bringen.", "about.cashu.p1": "Cashu ist ein ecash-Protokoll für Bitcoin. Ein Mint gibt blinde Token aus, die durch die von ihm gehaltenen Sats gedeckt sind, und diese Token wandern zwischen Menschen, ohne dass das Mint erfährt, wer wem was geschickt hat. Die Privatsphäre ist echt und sie ist der Punkt: Token sind keine Konten, und das Mint kann den Token, den es signiert hat, nicht mit dem Token verknüpfen, der zurückkommt.",
"about.cashu.p2": "Das Protokoll und die dazugehörigen Spezifikationen können Sie unter {link} einsehen. Die Spezifikationen selbst werden als „NUTs“ bezeichnet – daher stammen auch die NUT-Nummern auf jeder Mint-Seite.", "about.cashu.p2": "Das Protokoll und die dazugehörigen Spezifikationen können Sie unter {link} einsehen. Die Spezifikationen selbst werden als „NUTs“ bezeichnet – daher stammen auch die NUT-Nummern auf jeder Mint-Seite.",
"about.mints.title": "Was eine „Mint“ ist und welche Vor- und Nachteile sie mit sich bringt", "about.mints.title": "Was eine „Mint“ ist und welche Vor- und Nachteile sie mit sich bringt",
"about.mints.p1": "Eine Mint-Plattform fungiert als Verwahrer. Wenn Sie E-Cash prägen, übergeben Sie Sats an den Betreiber des jeweiligen Servers und vertrauen darauf, dass dieser die Token einlöst, wenn Sie sie wieder einlösen. Es gibt keinen Betrugsschutz, keine Treuhandverwaltung und keine Regressmöglichkeiten. Ein Mint-Betreiber kann mit dem Guthaben verschwinden – und das ist bereits vorgekommen.", "about.mints.p1": "Ein Mint ist treuhänderisch. Wenn du ecash mintest, gibst du Sats an denjenigen, der diesen Server betreibt, und vertraust darauf, dass er die Token einlöst, wenn du sie wieder meltest. Es gibt keinen Betrugsbeweis, keine Treuhand und keinen Rechtsweg. Ein Mint-Betreiber kann mit dem Guthaben verschwinden, und Mints haben das getan.",
"about.mints.p2": "Genau aus diesem Grund gibt es hier Bewertungen. Diese Website versichert Ihnen niemals, dass eine Währung sicher ist. Der Status auf einer Währungsseite lautet „{code}“ (der Server hat geantwortet) und nicht „vertrauenswürdig“; dies bedeutet lediglich, dass der Server bei unserer letzten Anfrage geantwortet hat. Behandeln Sie Ihr eCash-Guthaben wie Bargeld in der Jackentasche: zum Ausgeben geeignet, zum Sparen ungeeignet.", "about.mints.p2": "Genau deshalb gibt es hier Bewertungen. Diese Website sagt dir nie, dass ein Mint sicher ist. Der Status auf einer Mint-Seite sagt {code}, nicht „vertrauenswürdig“, und er heißt nur, dass der Server geantwortet hat, als wir zuletzt gefragt haben. Behandle ecash-Guthaben wie Bargeld in der Manteltasche: gut zum Ausgeben, falsch zum Sparen.",
"about.nip87.title": "Was ist NIP-87?", "about.nip87.title": "Was ist NIP-87?",
"about.nip87.p1": "NIP-87 ist die Nostr-Konvention zur Bekanntgabe und Empfehlung von E-Cash-Prägeanstalten. Dabei spielen zwei Arten von Ereignissen eine Rolle:", "about.nip87.p1": "NIP-87 ist die Nostr-Konvention, um ecash-Mints anzukündigen und zu empfehlen. Zwei Event-Arten sind hier wichtig:",
"about.nip87.li1": "{code} kündigt eine neue „Mint“ an. Die URL der „Mint“ befindet sich im Tag „{tag}“. Auf diese Weise erkennt die Website neue „Mints“, die ihr bisher noch nicht bekannt waren.", "about.nip87.li1": "{code} kündigt eine neue „Mint“ an. Die URL der „Mint“ befindet sich im Tag „{tag}“. Auf diese Weise erkennt die Website neue „Mints“, die ihr bisher noch nicht bekannt waren.",
"about.nip87.li2": "„{code}“ ist eine Bewertung. Sie enthält die Bewertung und den Bewertungstext und verweist mit einem „{utag}“-Tag (der Mint-URL) und einem „{dtag}“-Tag (dem öffentlichen Schlüssel der Mint) auf die betreffende Mint.", "about.nip87.li2": "„{code}“ ist eine Bewertung. Sie enthält die Bewertung und den Bewertungstext und verweist mit einem „{utag}“-Tag (der Mint-URL) und einem „{dtag}“-Tag (dem öffentlichen Schlüssel der Mint) auf die betreffende Mint.",
"about.nip87.p2": "Jede Bewertung ist mit dem Nostr-Schlüssel ihres Autors signiert. Wir bezeichnen eine Bewertung niemals als „verifiziert“, da niemand sie verifiziert hat: Sie ist signiert, was eine andere und aussagekräftigere Aussage ist. Das bedeutet, dass Sie überprüfen können, wer sie verfasst hat, und dass sie nicht verändert wurde. Es bedeutet jedoch nicht, dass der Autor ehrlich ist oder die Münzprägeanstalt jemals genutzt hat.", "about.nip87.p2": "Jede Bewertung ist mit dem Nostr-Schlüssel ihres Autors signiert. Wir nennen eine Bewertung nie „verifiziert“, denn niemand hat sie verifiziert: sie ist signiert, was eine andere und nützlichere Aussage ist. Das heißt, du kannst prüfen, wer sie geschrieben hat und dass sie nicht verändert wurde. Es heißt nicht, dass der Autor ehrlich ist oder das Mint je genutzt hat.",
"about.how.title": "So funktioniert diese Website", "about.how.title": "So funktioniert diese Website",
"about.how.p1": "Es gibt zwei Schleifen und eine Regel. Die Regel lautet, dass die Münzanstalt Eigentümerin ihrer Metadaten ist und Nostr Eigentümer der Bewertungen ist; daher speichert diese Website keines von beiden als maßgebliche Quelle.", "about.how.p1": "Es gibt zwei Schleifen und eine Regel. Die Regel lautet: dem Mint gehören seine Metadaten und Nostr gehören die Bewertungen, also speichert diese Website beides nicht als maßgebliche Quelle.",
"about.how.li1": "{strong} fragt alle bekannten Mint-Server alle zehn Minuten nach „{code}“ ab und protokolliert die Antwort: Name, Beschreibung, Version, unterstützte NUTs, Einschränkungen, Kontaktdaten. Außerdem wird erfasst, ob der Mint-Server überhaupt geantwortet hat – daher stammt auch der Begriff „Verfügbarkeit“.", "about.how.li1": "{strong} fragt alle bekannten Mint-Server alle zehn Minuten nach „{code}“ ab und protokolliert die Antwort: Name, Beschreibung, Version, unterstützte NUTs, Einschränkungen, Kontaktdaten. Außerdem wird erfasst, ob der Mint-Server überhaupt geantwortet hat – daher stammt auch der Begriff „Verfügbarkeit“.",
"about.how.li1Label": "Die Messschleife", "about.how.li1Label": "Die Messschleife",
"about.how.li2": "{strong} stellt stündlich eine Verbindung zu den Nostr-Relays her, liest Ankündigungen und Bewertungen zu den Mints und fügt alle Mints hinzu, die es zuvor noch nicht gesehen hat.", "about.how.li2": "{strong} stellt stündlich eine Verbindung zu den Nostr-Relays her, liest Ankündigungen und Bewertungen zu den Mints und fügt alle Mints hinzu, die es zuvor noch nicht gesehen hat.",
"about.how.li2Label": "Der Entdeckungskreislauf", "about.how.li2Label": "Der Entdeckungskreislauf",
"about.how.p2": "Entscheidend ist, was passiert, wenn eine Münzprägeanstalt nicht mehr antwortet. Ihre im Cache gespeicherten Metadaten werden durch eine fehlgeschlagene Abfrage niemals überschrieben, sodass eine inaktive Münzprägeanstalt ihre vollständige Seite behält: ihre Spezifikationen, ihre Geschichte und ihre Bewertungen, ergänzt durch einen Hinweis darauf, wann sie den Betrieb eingestellt hat. Die vorherige Version dieser Website zeigte stattdessen eine Fehlerseite an, was bedeutete, dass gerade jene Münzprägeanstalten, vor denen sich die Nutzer am dringendsten warnen mussten, nicht bewertet werden konnten. Das war der Hauptgrund für die Neugestaltung.", "about.how.p2": "Entscheidend ist, was passiert, wenn ein Mint nicht mehr antwortet. Seine zwischengespeicherten Metadaten werden von einer fehlgeschlagenen Abfrage nie überschrieben, also behält ein totes Mint seine vollständige Seite: seine Daten, seine Historie und seine Bewertungen, mit einem Banner, das sagt, wann es verstummt ist. Die vorherige Version dieser Website zeigte stattdessen eine Fehlerseite, was hieß, dass genau die Mints, vor denen sich Leute am dringendsten warnen mussten, nicht bewertet werden konnten. Das war der Hauptgrund für den Neubau.",
"about.how.p3": "Bewertungstexte werden bei uns niemals gespeichert. Wenn Sie eine Mint-Seite öffnen, kommuniziert Ihr Browser direkt mit den Relays und liest die Bewertungen selbst aus; wenn Sie eine Bewertung verfassen, wird diese von Ihrer eigenen Nostr-Erweiterung signiert und direkt an diese Relays gesendet. Wir führen lediglich eine Zählung, damit die Seiten bewertet und sortiert werden können – mehr nicht. Nichts davon kann von uns bearbeitet oder gelöscht werden, auch keine Bewertungen, die wir lieber nicht hätten.", "about.how.p3": "Bewertungstexte werden bei uns niemals gespeichert. Wenn Sie eine Mint-Seite öffnen, kommuniziert Ihr Browser direkt mit den Relays und liest die Bewertungen selbst aus; wenn Sie eine Bewertung verfassen, wird diese von Ihrer eigenen Nostr-Erweiterung signiert und direkt an diese Relays gesendet. Wir führen lediglich eine Zählung, damit die Seiten bewertet und sortiert werden können – mehr nicht. Nichts davon kann von uns bearbeitet oder gelöscht werden, auch keine Bewertungen, die wir lieber nicht hätten.",
"about.relays.title": "Relais, die wir gelesen haben", "about.relays.title": "Relais, die wir gelesen haben",
"about.relays.p1": "Dies sind die Relays, die sowohl der Indexer als auch Ihr Browser verwenden:", "about.relays.p1": "Dies sind die Relays, die sowohl der Indexer als auch Ihr Browser verwenden:",
@@ -453,7 +477,7 @@
"about.source.title": "Quelle und Bildnachweis", "about.source.title": "Quelle und Bildnachweis",
"about.source.p1": "Die Website ist als Open-Source-Projekt unter {github} verfügbar. Sie wird von {azzamo} erstellt und betrieben. Sollte auf einer Mint-Seite ein Fehler vorliegen, korrigieren Sie diesen bitte direkt an der Quelle: Die Metadaten der Mints stammen von der jeweiligen Mint-eigenen Seite unter {code}, und die Bewertungen stammen von Nostr.", "about.source.p1": "Die Website ist als Open-Source-Projekt unter {github} verfügbar. Sie wird von {azzamo} erstellt und betrieben. Sollte auf einer Mint-Seite ein Fehler vorliegen, korrigieren Sie diesen bitte direkt an der Quelle: Die Metadaten der Mints stammen von der jeweiligen Mint-eigenen Seite unter {code}, und die Bewertungen stammen von Nostr.",
"terms.title": "Nutzungsbedingungen · Cashumints.space", "terms.title": "Nutzungsbedingungen · Cashumints.space",
"terms.description": "Nutzungsbedingungen für Cashumints.space: Was die Website ist, was sie nicht leistet und wer wofür verantwortlich ist. Keine Konten, keine Gewährleistungen, keine Verbindung zu einer Münzprägeanstalt.", "terms.description": "Nutzungsbedingungen für Cashumints.space: was die Website ist, was sie nicht tut und wer wofür verantwortlich ist. Keine Konten, keine Gewährleistung, keine Verbindung zu irgendeinem Mint.",
"terms.heading": "Nutzungsbedingungen", "terms.heading": "Nutzungsbedingungen",
"terms.lede": "Einfach ausgedrückt: Eine Website, die ausschließlich öffentliche Daten indexiert. Zuletzt aktualisiert am {date}.", "terms.lede": "Einfach ausgedrückt: Eine Website, die ausschließlich öffentliche Daten indexiert. Zuletzt aktualisiert am {date}.",
"terms.what.title": "Was diese Website ist", "terms.what.title": "Was diese Website ist",
@@ -466,8 +490,8 @@
"terms.noAccounts.p2": "Die Nutzung erfolgt auf eigene Gefahr. Wir übernehmen keine Haftung für Verluste, die sich aus der Nutzung dieser Website oder dem Vertrauen auf hier veröffentlichte Informationen ergeben. Die spezifischen Risiken bei der Nutzung einer Cashu-Mint finden Sie unter {link}.", "terms.noAccounts.p2": "Die Nutzung erfolgt auf eigene Gefahr. Wir übernehmen keine Haftung für Verluste, die sich aus der Nutzung dieser Website oder dem Vertrauen auf hier veröffentlichte Informationen ergeben. Die spezifischen Risiken bei der Nutzung einer Cashu-Mint finden Sie unter {link}.",
"terms.noAccounts.link": "Haftungsausschluss", "terms.noAccounts.link": "Haftungsausschluss",
"terms.noCustody.title": "Wir verwahren Ihr Geld nicht", "terms.noCustody.title": "Wir verwahren Ihr Geld nicht",
"terms.noCustody.p1": "Diese Website verwahrt zu keinem Zeitpunkt Geldbeträge. Sie verfügt über keine Wallet, hat keinen Zugriff auf E-Geld und kann keine Gelder bewegen, sperren oder wiederherstellen. Sollten Sie ein Problem mit einer Mint haben, ist der Betreiber der Mint der einzige Ansprechpartner, der Ihnen helfen kann, und wir können in Ihrem Namen keinen Kontakt zu ihm aufnehmen.", "terms.noCustody.p1": "Diese Website nimmt nie Gelder in Verwahrung. Sie hat keine Wallet, sie fasst kein ecash an, und sie kann nichts bewegen, einfrieren oder wiederherstellen. Wenn du ein Problem mit einem Mint hast, ist der Mint-Betreiber der Einzige, der dir helfen kann, und wir können ihn nicht für dich kontaktieren.",
"terms.noCustody.p2": "Wir stehen in keiner Verbindung zu den hier aufgeführten Münzprägeanstalten, und keine Münzprägeanstalt bezahlt für ihre Aufnahme in die Liste, ihre Platzierung oder ihre Bewertung. Die Münzprägeanstalten werden anhand öffentlicher Nostr-Meldungen ermittelt und anhand öffentlicher Bewertungsdaten durch einen Algorithmus in eine Rangliste eingeordnet.", "terms.noCustody.p2": "Wir stehen mit keinem hier gelisteten Mint in Verbindung, und kein Mint bezahlt für seinen Eintrag, seinen Rang oder seine Bewertung. Mints werden aus öffentlichen Nostr-Ankündigungen gefunden und von einem Algorithmus über öffentliche Bewertungsdaten eingeordnet.",
"terms.reviews.title": "Rezensionen sind die Worte anderer Menschen", "terms.reviews.title": "Rezensionen sind die Worte anderer Menschen",
"terms.reviews.p1": "Bewertungen sind nutzergenerierte Inhalte, die von ihren Autoren auf öffentlichen Nostr-Relays veröffentlicht werden. Wir zeigen sie an, haben jedoch keinen Einfluss darauf. Wir haben sie nicht verfasst, können sie nicht bearbeiten und auch nicht von den Relays löschen, auf denen sie gespeichert sind: Es handelt sich um signierte Ereignisse in einem Netzwerk, das uns nicht gehört.", "terms.reviews.p1": "Bewertungen sind nutzergenerierte Inhalte, die von ihren Autoren auf öffentlichen Nostr-Relays veröffentlicht werden. Wir zeigen sie an, haben jedoch keinen Einfluss darauf. Wir haben sie nicht verfasst, können sie nicht bearbeiten und auch nicht von den Relays löschen, auf denen sie gespeichert sind: Es handelt sich um signierte Ereignisse in einem Netzwerk, das uns nicht gehört.",
"terms.reviews.p2": "Eine Bewertung gibt die Erfahrungen einer einzelnen Person wieder und stellt keine nachgewiesene Tatsache dar. Wenn Sie eine Bewertung veröffentlichen, sind Sie für deren Inhalt verantwortlich. Veröffentlichen Sie keine rechtswidrigen Inhalte und beachten Sie, dass Ihre Veröffentlichung öffentlich und dauerhaft ist (siehe „{link}“).", "terms.reviews.p2": "Eine Bewertung gibt die Erfahrungen einer einzelnen Person wieder und stellt keine nachgewiesene Tatsache dar. Wenn Sie eine Bewertung veröffentlichen, sind Sie für deren Inhalt verantwortlich. Veröffentlichen Sie keine rechtswidrigen Inhalte und beachten Sie, dass Ihre Veröffentlichung öffentlich und dauerhaft ist (siehe „{link}“).",
@@ -511,33 +535,33 @@
"privacy.permanent.li2": "Es ist dauerhaft mit Ihrem öffentlichen Nostr-Schlüssel verknüpft, ebenso wie alles andere, was dieser Schlüssel jemals veröffentlicht hat.", "privacy.permanent.li2": "Es ist dauerhaft mit Ihrem öffentlichen Nostr-Schlüssel verknüpft, ebenso wie alles andere, was dieser Schlüssel jemals veröffentlicht hat.",
"privacy.permanent.li3": "Das Löschen ist eine Anfrage, kein Befehl. Sie können ein Löschereignis veröffentlichen, und einige Relays werden diesem nachkommen, aber es steht einem Relay frei, es zu ignorieren, und jeder, der das Ereignis bereits kopiert hat, verfügt weiterhin darüber. Behandeln Sie eine veröffentlichte Bewertung als dauerhaft.", "privacy.permanent.li3": "Das Löschen ist eine Anfrage, kein Befehl. Sie können ein Löschereignis veröffentlichen, und einige Relays werden diesem nachkommen, aber es steht einem Relay frei, es zu ignorieren, und jeder, der das Ereignis bereits kopiert hat, verfügt weiterhin darüber. Behandeln Sie eine veröffentlichte Bewertung als dauerhaft.",
"privacy.permanent.li4": "Wir können es nicht für Sie löschen. Wir können jedoch verhindern, dass es hier angezeigt wird; das ändert jedoch nichts an den Relais.", "privacy.permanent.li4": "Wir können es nicht für Sie löschen. Wir können jedoch verhindern, dass es hier angezeigt wird; das ändert jedoch nichts an den Relais.",
"privacy.permanent.p2": "Wenn Sie eine Münze bewerten möchten, ohne dass dies mit Ihrer Hauptidentität in Verbindung gebracht wird, verwenden Sie einen separaten Nostr-Schlüssel. Beachten Sie, dass ein Schlüssel, der sonst keine Aktivitäten aufweist, bei negativen Bewertungen als solcher gekennzeichnet wird, da ein brandneuer Schlüssel, der eine Ein-Stern-Bewertung abgibt, ein Muster ist, das die Leser zu Recht erkennen sollten.", "privacy.permanent.p2": "Wenn du ein Mint bewerten willst, ohne dass das an deine Hauptidentität gebunden ist, nimm einen separaten Nostr-Schlüssel. Beachte, dass ein Schlüssel ohne weitere Aktivität bei harten Bewertungen als solcher gekennzeichnet wird, denn ein brandneuer Schlüssel, der eine Ein-Stern-Bewertung schreibt, ist ein Muster, das Leser sehen dürfen.",
"privacy.contact.title": "Fragen", "privacy.contact.title": "Fragen",
"privacy.contact.p1": "Die Website ist Open Source: Wenn Sie einen der oben genannten Punkte überprüfen möchten, können Sie den Quellcode unter {github} einsehen. Fragen und Korrekturen können dort als „Issue“ eingereicht werden.", "privacy.contact.p1": "Die Website ist Open Source: Wenn Sie einen der oben genannten Punkte überprüfen möchten, können Sie den Quellcode unter {github} einsehen. Fragen und Korrekturen können dort als „Issue“ eingereicht werden.",
"disclaimer.title": "Haftungsausschluss · Cashumints.space", "disclaimer.title": "Haftungsausschluss · Cashumints.space",
"disclaimer.description": "Was Sie vor der Nutzung einer Cashu-Mint wissen sollten: Mints sind Verwahrdienste; eine Mint kann mit Ihrem Geld verschwinden; Bewertungen sind Meinungen, und „online“ bedeutet lediglich „erreichbar“, nicht „vertrauenswürdig“.", "disclaimer.description": "Was Sie vor der Nutzung einer Cashu-Mint wissen sollten: Mints sind Verwahrdienste; eine Mint kann mit Ihrem Geld verschwinden; Bewertungen sind Meinungen, und „online“ bedeutet lediglich „erreichbar“, nicht „vertrauenswürdig“.",
"disclaimer.heading": "Haftungsausschluss", "disclaimer.heading": "Haftungsausschluss",
"disclaimer.lede": "Lesen Sie dies, bevor Sie Geld in eine Münzprägeanstalt investieren. Zuletzt aktualisiert am {date}.", "disclaimer.lede": "Lies das, bevor du Geld in ein Mint gibst. Zuletzt aktualisiert am {date}.",
"disclaimer.callout": "Eine Cashu-Münze verwahrt Ihr Geld für Sie. Wenn sie verschwindet, ist Ihr E-Geld weg, und es gibt niemanden, an den Sie sich wenden können. Bewahren Sie nur so viel auf, wie Sie sich leisten können, zu verlieren.", "disclaimer.callout": "Ein Cashu-Mint verwahrt dein Geld für dich. Wenn es verschwindet, ist dein ecash weg, und es gibt niemanden, an den du dich wenden kannst. Halte dort nur, was du dir leisten kannst zu verlieren.",
"disclaimer.custodial.title": "Münzprägeanstalten sind Verwahrstellen", "disclaimer.custodial.title": "Mints sind treuhänderisch",
"disclaimer.custodial.p1": "Wenn Sie Ecash prägen, übergeben Sie Bitcoin an den Betreiber der Prägeanstalt und erhalten dafür Token zurück. Diese Token stellen einen Anspruch gegenüber diesem Betreiber dar. Es handelt sich dabei nicht um Bitcoin, das Sie besitzen, und der Betreiber entscheidet, ob er diese Token einlöst.", "disclaimer.custodial.p1": "Wenn du ecash mintest, gibst du Bitcoin an den Mint-Betreiber und bekommst Token zurück. Diese Token sind ein Anspruch gegen diesen Betreiber. Sie sind kein Bitcoin, das du hältst, und der Betreiber entscheidet, ob er sie einlöst.",
"disclaimer.custodial.p2": "Das bedeutet, dass jede Mint dieselben Risiken birgt: Der Betreiber kann verschwinden, zur Einstellung des Betriebs gezwungen werden, die Gelder durch einen Fehler oder einen Hackerangriff verlieren oder den Server einfach abschalten. Es gibt keine Einlagensicherung, keine Rückbuchungen, keinen Kundendienst und keine Rechtsmittel, auf die man sich verlassen könnte. Das gilt sogar für die bestbewertete Mint auf dieser Website.", "disclaimer.custodial.p2": "Das bedeutet, dass jede Mint dieselben Risiken birgt: Der Betreiber kann verschwinden, zur Einstellung des Betriebs gezwungen werden, die Gelder durch einen Fehler oder einen Hackerangriff verlieren oder den Server einfach abschalten. Es gibt keine Einlagensicherung, keine Rückbuchungen, keinen Kundendienst und keine Rechtsmittel, auf die man sich verlassen könnte. Das gilt sogar für die bestbewertete Mint auf dieser Website.",
"disclaimer.custodial.p3": "Cashu ist für schnelle Transaktionen mit kleinen Beträgen konzipiert. Nutzen Sie es entsprechend: Bewahren Sie darin nur das Geld auf, das Sie bald ausgeben möchten, nicht Ihre Ersparnisse, und verteilen Sie es auf verschiedene Mints, anstatt alles in einem zu parken.", "disclaimer.custodial.p3": "Cashu ist für schnelle Transaktionen mit kleinen Beträgen konzipiert. Nutzen Sie es entsprechend: Bewahren Sie darin nur das Geld auf, das Sie bald ausgeben möchten, nicht Ihre Ersparnisse, und verteilen Sie es auf verschiedene Mints, anstatt alles in einem zu parken.",
"disclaimer.reviews.title": "Bewertungen sind Meinungen, keine überprüften Behauptungen", "disclaimer.reviews.title": "Bewertungen sind Meinungen, keine überprüften Behauptungen",
"disclaimer.reviews.p1": "Jede Bewertung hier wurde von einem Nutzer auf Nostr verfasst und mit dessen Schlüssel signiert. „Signiert“ bedeutet, dass wir Ihnen mitteilen können, welcher Schlüssel die Bewertung verfasst hat. Das bedeutet jedoch nicht, dass die Bewertung der Wahrheit entspricht, dass der Autor die Münzprägeanstalt genutzt hat oder dass der Autor nicht der Betreiber, ein Konkurrent oder eine Person mit zehn Schlüsseln ist.", "disclaimer.reviews.p1": "Jede Bewertung hier wurde von jemandem auf Nostr geschrieben und mit dessen Schlüssel signiert. Signiert heißt, wir können dir sagen, welcher Schlüssel sie geschrieben hat. Es heißt nicht, dass die Bewertung wahr ist, dass der Autor das Mint genutzt hat, oder dass der Autor nicht der Betreiber, ein Konkurrent oder eine Person mit zehn Schlüsseln ist.",
"disclaimer.reviews.p2": "Wir überprüfen die Verfasser von Bewertungen nicht und bearbeiten oder löschen keine Bewertungen, mit denen wir nicht einverstanden sind. Lesen Sie sich einige davon durch, schauen Sie sich an, was darin tatsächlich steht, und bewerten Sie ein Produkt mit drei Bewertungen anders als eines mit dreihundert.", "disclaimer.reviews.p2": "Wir verifizieren keine Verfasser, und wir bearbeiten oder löschen keine Bewertungen, mit denen wir nicht einverstanden sind. Lies ein paar, schau, was sie tatsächlich sagen, und gewichte ein Mint mit drei Bewertungen anders als eines mit dreihundert.",
"disclaimer.status.title": "„Online“ bedeutet „erreichbar“, nicht „vertrauenswürdig“", "disclaimer.status.title": "„Online“ bedeutet „erreichbar“, nicht „vertrauenswürdig“",
"disclaimer.status.p1": "Wir überprüfen jede Mint regelmäßig und zeigen an, ob sie geantwortet hat. Das ist alles, was ein grüner Status bedeutet: Der Server hat geantwortet. Er sagt nichts darüber aus, ob der Betreiber zahlungsfähig oder ehrlich ist oder ob er auch morgen noch beabsichtigt, Ihre Token einzulösen.", "disclaimer.status.p1": "Wir überprüfen jede Mint regelmäßig und zeigen an, ob sie geantwortet hat. Das ist alles, was ein grüner Status bedeutet: Der Server hat geantwortet. Er sagt nichts darüber aus, ob der Betreiber zahlungsfähig oder ehrlich ist oder ob er auch morgen noch beabsichtigt, Ihre Token einzulösen.",
"disclaimer.status.p2": "Eine Mint kann so lange online sein, bis sie es nicht mehr ist. Die Verfügbarkeitsstatistik, die unterstützten NUTs und die Softwareversion geben zwar Aufschluss darüber, wie eine Mint betrieben wird, aber keines dieser Elemente ist eine Garantie für Ihr Geld.", "disclaimer.status.p2": "Eine Mint kann so lange online sein, bis sie es nicht mehr ist. Die Verfügbarkeitsstatistik, die unterstützten NUTs und die Softwareversion geben zwar Aufschluss darüber, wie eine Mint betrieben wird, aber keines dieser Elemente ist eine Garantie für Ihr Geld.",
"disclaimer.ranking.title": "Wie das Ranking funktioniert und was es nicht bedeutet", "disclaimer.ranking.title": "Wie das Ranking funktioniert und was es nicht bedeutet",
"disclaimer.ranking.p1": "Die Mints werden anhand eines Algorithmus auf der Grundlage öffentlicher Bewertungsdaten sortiert: Bewertung, Anzahl der Bewertungen und aktuelle Aktivität, wobei Online-Mints standardmäßig vor Offline-Mints stehen. Die Einzelheiten sind im Open-Source-Code zu finden.", "disclaimer.ranking.p1": "Die Mints werden anhand eines Algorithmus auf der Grundlage öffentlicher Bewertungsdaten sortiert: Bewertung, Anzahl der Bewertungen und aktuelle Aktivität, wobei Online-Mints standardmäßig vor Offline-Mints stehen. Die Einzelheiten sind im Open-Source-Code zu finden.",
"disclaimer.ranking.p2": "Niemand bezahlt für die Platzierung. Auf dieser Website gibt es keine gesponserten Plätze, keine beworbenen Einträge und keine Vereinbarungen mit einer Münzprägeanstalt. Eine hohe Platzierung bedeutet lediglich, dass der Algorithmus die öffentlich zugänglichen Daten positiv bewertet hat – mehr nicht. Es handelt sich weder um eine Empfehlung noch um eine Sicherheitsbewertung.", "disclaimer.ranking.p2": "Niemand bezahlt für die Platzierung. Es gibt keinen gesponserten Platz, keinen beworbenen Eintrag und keine Absprache mit irgendeinem Mint auf dieser Website. Ein hoher Rang heißt, dass dem Algorithmus die öffentlichen Daten gefallen haben, mehr nicht. Es ist keine Empfehlung und keine Sicherheitsbewertung.",
"disclaimer.notAdvice.title": "Dies ist keine Finanzberatung.", "disclaimer.notAdvice.title": "Dies ist keine Finanzberatung.",
"disclaimer.notAdvice.p1": "Nichts auf dieser Website stellt eine Finanz-, Anlage-, Rechts- oder Steuerberatung dar. Wir kennen Ihre Situation nicht und beraten Sie nicht. Die Entscheidung, eine Münzprägeanstalt zu nutzen und welche Sie wählen, liegt bei Ihnen.", "disclaimer.notAdvice.p1": "Nichts auf dieser Website ist Finanz-, Anlage-, Rechts- oder Steuerberatung. Wir kennen deine Situation nicht und wir beraten dich nicht. Ein Mint zu nutzen, und welches, ist deine Entscheidung.",
"disclaimer.dyor.title": "Recherchiere selbst", "disclaimer.dyor.title": "Recherchiere selbst",
"disclaimer.dyor.li1": "Informieren Sie sich über den Betreiber. Eine Mint, die von jemandem betrieben wird, der seinen guten Ruf zu verlieren hat, ist etwas ganz anderes als eine anonyme URL.", "disclaimer.dyor.li1": "Informieren Sie sich über den Betreiber. Eine Mint, die von jemandem betrieben wird, der seinen guten Ruf zu verlieren hat, ist etwas ganz anderes als eine anonyme URL.",
"disclaimer.dyor.li2": "Lies dir die Nutzungsbedingungen der Mint selbst und deren „MOTD“ durch. Eine Mint, die sich selbst als „Test“ oder „Beta“ bezeichnet, meint das auch so.", "disclaimer.dyor.li2": "Lies dir die Nutzungsbedingungen der Mint selbst und deren „MOTD“ durch. Eine Mint, die sich selbst als „Test“ oder „Beta“ bezeichnet, meint das auch so.",
"disclaimer.dyor.li3": "Fang klein an. Präge ein bisschen Geld, gib es aus, schmelze es wieder ein und schau, was passiert, bevor du ihm mehr anvertraust.", "disclaimer.dyor.li3": "Fang klein an. Minte ein wenig, gib es aus, melte es wieder heraus, und schau, was passiert, bevor du ihm mehr anvertraust.",
"disclaimer.dyor.li4": "Achten Sie auf die Grenzwerte. Ein niedriger Höchstwert ist oft ein Zeichen für einen umsichtigen Betreiber, während ein hoher Wert nicht unbedingt auf Sicherheit hindeutet.", "disclaimer.dyor.li4": "Achten Sie auf die Grenzwerte. Ein niedriger Höchstwert ist oft ein Zeichen für einen umsichtigen Betreiber, während ein hoher Wert nicht unbedingt auf Sicherheit hindeutet.",
"disclaimer.dyor.li5": "Erstellen Sie regelmäßig Sicherungskopien Ihrer Token und denken Sie daran, dass ein Token ein Inhaberwert ist: Wer ihn besitzt, kann ihn ausgeben.", "disclaimer.dyor.li5": "Erstellen Sie regelmäßig Sicherungskopien Ihrer Token und denken Sie daran, dass ein Token ein Inhaberwert ist: Wer ihn besitzt, kann ihn ausgeben.",
"disclaimer.site.title": "Über die Website selbst", "disclaimer.site.title": "Über die Website selbst",
@@ -546,29 +570,29 @@
"disclaimer.updated": "20. August 2026", "disclaimer.updated": "20. August 2026",
"login.getPrimal": "Get Primal", "login.getPrimal": "Get Primal",
"reviews.dialog.identity.extensionGone": "Ihre Browser-Erweiterung reagiert nicht. Melden Sie sich bitte erneut an oder verwenden Sie einen Remote-Signierer.", "reviews.dialog.identity.extensionGone": "Ihre Browser-Erweiterung reagiert nicht. Melden Sie sich bitte erneut an oder verwenden Sie einen Remote-Signierer.",
"seo.orgDescription": "Ein Verzeichnis der Cashu-Mints mit Echtzeit-Statusangaben zu jedem Mint und Community-Bewertungen, die auf Nostr unterzeichnet wurden.", "seo.orgDescription": "Ein Verzeichnis von Cashu-Mints, mit Live-Status jedes Mints und Community-Bewertungen, signiert auf Nostr.",
"seo.ogImageAlt": "Cashumints.space: Finden Sie eine Cashu-Münzprägeanstalt, der Sie vertrauen können.", "seo.ogImageAlt": "Cashumints.space: finde ein Cashu-Mint, dem du vertrauen kannst.",
"seo.mint.title.one": "Bewertungen zu „{name}“: Bewertung „{rating}“ aus der Rezension zu „{n}“ | Cashumints.space", "seo.mint.title.one": "{name} Bewertungen: {rating} aus {n} Bewertung | Cashumints.space",
"seo.mint.title.other": "Bewertungen zu „{name}“: Bewertung „{rating}“ aus den Bewertungen zu „{n}“ | Cashumints.space", "seo.mint.title.other": "{name} Bewertungen: {rating} aus {n} Bewertungen | Cashumints.space",
"seo.mint.titleOffline.one": "{name}: Am „{n}“-Tag offline | Cashumints.space", "seo.mint.titleOffline.one": "{name}: seit {n} Tag offline | Cashumints.space",
"seo.mint.titleOffline.other": "{name}t seit {n} Tagen offline | Cashumints.space", "seo.mint.titleOffline.other": "{name}: seit {n} Tagen offline | Cashumints.space",
"seo.mint.titleNoReviews": "{name}: Informationen und Bewertungen zu Cashu Mint | Cashumints.space", "seo.mint.titleNoReviews": "{name}: Infos und Bewertungen zum Cashu-Mint | Cashumints.space",
"seo.fedimint.titleNoReviews": "{name}: Informationen und Bewertungen zur Fedimint-Föderation | Cashumints.space", "seo.fedimint.titleNoReviews": "{name}: Informationen und Bewertungen zur Fedimint-Föderation | Cashumints.space",
"seo.desc.fallback": "Nutzerbewertungen für „{name}“ ({domain}).", "seo.desc.fallback": "Community-Bewertungen für {name} ({domain}).",
"seo.desc.fallbackDomain": "Nutzerbewertungen zur Cashu-Minze unter {domain}.", "seo.desc.fallbackDomain": "Community-Bewertungen für das Cashu-Mint unter {domain}.",
"seo.desc.rated.one": "Bewertung: „{rating}/5“ – Rezension von {n}, unterzeichnet mit „Nostr“.", "seo.desc.rated.one": "{rating}/5 aus {n} signierter Nostr-Bewertung.",
"seo.desc.rated.other": "Bewertung: „{rating}/5“ aus den von „{n}“ unterzeichneten Nostr-Rezensionen.", "seo.desc.rated.other": "{rating}/5 aus {n} signierten Nostr-Bewertungen.",
"seo.desc.noReviews": "Noch keine Bewertungen.", "seo.desc.noReviews": "Noch keine Bewertungen.",
"seo.desc.status": "{status}.", "seo.desc.status": "{status}.",
"seo.desc.statusLimits": "{status}, Grenzen {limits}.", "seo.desc.statusLimits": "{status}, Limits {limits}.",
"seo.desc.fedModules": "{status}. Module: {modules}.", "seo.desc.fedModules": "{status}. Module: {modules}.",
"seo.desc.fedModulesNetwork": "{status}-Module: {modules} auf {network}.", "seo.desc.fedModulesNetwork": "{status}. Module: {modules} auf {network}.",
"seo.mint.ogAlt.one": "{name}: „{rating}“ – 5 Sterne aus der Rezension zu „{n}“ auf Cashumints.space", "seo.mint.ogAlt.one": "{name}: {rating} Sterne aus {n} Bewertung auf Cashumints.space",
"seo.mint.ogAlt.other": "{name}: „{rating}“ – Bewertungen von „{n}“ auf Cashumints.space", "seo.mint.ogAlt.other": "{name}: {rating} Sterne aus {n} Bewertungen auf Cashumints.space",
"seo.mint.ogAltNoReviews": "{name} auf Cashumints.space", "seo.mint.ogAltNoReviews": "{name} auf Cashumints.space",
"nav.home": "Startseite", "nav.home": "Startseite",
"mint.more.title": "Noch mehr Minzbonbons", "mint.more.title": "Mehr Mints",
"mint.more.all": "Alle Minzsorten anzeigen", "mint.more.all": "Alle Mints ansehen",
"nav.fedimints": "Fedimints", "nav.fedimints": "Fedimints",
"status.announced": "Nur angekündigt", "status.announced": "Nur angekündigt",
"card.unnamedFederation": "Unbenannter Verband", "card.unnamedFederation": "Unbenannter Verband",
@@ -619,8 +643,8 @@
"fedimint.more.title": "Weitere Verbände", "fedimint.more.title": "Weitere Verbände",
"fedimint.more.all": "Alle Verbände anzeigen", "fedimint.more.all": "Alle Verbände anzeigen",
"fedimint.disclaimer": "Einem Verband beizutreten bedeutet, seinen Verantwortlichen das eigene Geld anzuvertrauen. Bewertungen spiegeln die Meinungen der Community wider, und „bestätigt“ bedeutet lediglich, dass der Anbieter erreichbar ist – nicht, dass er vertrauenswürdig ist.", "fedimint.disclaimer": "Einem Verband beizutreten bedeutet, seinen Verantwortlichen das eigene Geld anzuvertrauen. Bewertungen spiegeln die Meinungen der Community wider, und „bestätigt“ bedeutet lediglich, dass der Anbieter erreichbar ist – nicht, dass er vertrauenswürdig ist.",
"fedimint.module.lightning": "Blitz", "fedimint.module.lightning": "Lightning",
"fedimint.module.mint": "Ecash-Prägeanstalt", "fedimint.module.mint": "ecash-Mint",
"fedimint.module.wallet": "On-Chain-Wallet", "fedimint.module.wallet": "On-Chain-Wallet",
"fedimint.module.meta": "Metadaten", "fedimint.module.meta": "Metadaten",
"fedimint.module.stability_pool": "Stabilitätspool", "fedimint.module.stability_pool": "Stabilitätspool",
@@ -635,8 +659,8 @@
"feed.ecosystem.all": "Alle", "feed.ecosystem.all": "Alle",
"feed.ecosystem.cashu": "Cashu", "feed.ecosystem.cashu": "Cashu",
"feed.ecosystem.fedimint": "Fedimint", "feed.ecosystem.fedimint": "Fedimint",
"feed.filterBySubject": "Nach Prägeanstalt oder Föderation filtern", "feed.filterBySubject": "Nach Mint oder Föderation filtern",
"feed.anySubject": "Jede Münzanstalt oder jeder Verband", "feed.anySubject": "Jedes Mint oder jede Föderation",
"feed.clearSubject": "Namensfilter löschen", "feed.clearSubject": "Namensfilter löschen",
"home.topFedimints.title": "Top-Fedimints", "home.topFedimints.title": "Top-Fedimints",
"home.topFedimints.sub": "nach Bewertungen und Sternebewertung sortiert", "home.topFedimints.sub": "nach Bewertungen und Sternebewertung sortiert",
@@ -652,7 +676,113 @@
"mint.warnings.neverConfirmed.body.quiet": "Diese Föderation wurde vor einiger Zeit auf Nostr unter {date}, {days} angekündigt; seitdem gibt es keine Bestätigung dafür, dass sie noch besteht, und niemand hat sie in letzter Zeit bewertet. Alle folgenden Informationen stammen aus der Ankündigung.", "mint.warnings.neverConfirmed.body.quiet": "Diese Föderation wurde vor einiger Zeit auf Nostr unter {date}, {days} angekündigt; seitdem gibt es keine Bestätigung dafür, dass sie noch besteht, und niemand hat sie in letzter Zeit bewertet. Alle folgenden Informationen stammen aus der Ankündigung.",
"mint.warnings.neverConfirmed.meta": "{date} angekündigt, nie bestätigt", "mint.warnings.neverConfirmed.meta": "{date} angekündigt, nie bestätigt",
"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 Minten, Melten und Support? Werde konkret.",
"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": "Minten ü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": "Mint-Gebü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 Minten, Melten 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 Minten / Melten",
"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": "Minten und Melten 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": "Minten und Melten nicht verfügbar",
"mint.warnings.also.noFunding": "Auch seine Lightning-Node war nicht erreichbar, es konnte also weder gemintet noch gemeltet werden."
} }
+158 -28
View File
@@ -1,5 +1,5 @@
{ {
"site.name": "Καταιγίδες. Διάστημα", "site.name": "Cashumints.space",
"common.close": "Κλείσιμο", "common.close": "Κλείσιμο",
"common.cancel": "Ακύρωση", "common.cancel": "Ακύρωση",
"common.done": "Τέλος", "common.done": "Τέλος",
@@ -33,26 +33,30 @@
"status.unknown": "Δεν έχει ελεγχθεί ακόμα", "status.unknown": "Δεν έχει ελεγχθεί ακόμα",
"nav.skip": "Μετάβαση στο περιεχόμενο", "nav.skip": "Μετάβαση στο περιεχόμενο",
"nav.main": "Κύριο", "nav.main": "Κύριο",
"nav.mints": "Κασσού νόμισματοκοπία", "nav.mints": "Cashu mints",
"nav.reviews": "Επανεξέταση", "nav.reviews": "Κριτικές",
"nav.wallets": "Πορτοφόλια", "nav.wallets": "Πορτοφόλια",
"nav.about": "Σχετικά", "nav.about": "Σχετικά",
"nav.searchMints": "Αναζήτηση{n}νόμισμα τοκετόπεια", "nav.searchMints": "Αναζήτηση σε {n} mints",
"lang.label": "Γλώσσα", "lang.label": "Γλώσσα",
"lang.switcher": "Επιλέξτε μια γλώσσα", "lang.switcher": "Επιλέξτε μια γλώσσα",
"footer.copyright": "©{year} Καταιγίδες. Διάστημα", "footer.copyright": "© {year} Cashumints.space",
"footer.allMints": "Όλα τα Cassu vomismatokopeia", "footer.allMints": "Όλα τα Cashu mints",
"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}· Powered by Cashu και NIP-87", "footer.madeBy": "Φτιαγμένο με ❤️ από {azzamo} · Με τη δύναμη των Cashu και NIP-87",
"footer.warning": "Ένα νόμισμα μπορεί να εξαφανιστεί με τα χρήματά σας.", "footer.warning": "Τα mints είναι θεματοφύλακες. Ένα mint μπορεί να εξαφανιστεί με τα χρήματά σας.",
"home.title": "Cashumints.space · Βρείτε ένα Cashu νόμισμα που μπορείτε να εμπιστευτείτε", "footer.tagline": "Κριτικές της κοινότητας για ecash mints, υπογεγραμμένες στο Nostr.",
"home.description": "Βρείτε ένα νομισματοκοπείο Cashu που μπορείτε να εμπιστευτείτε. {mints} νομισματοκοπεία καταχωρισμένα με {reviews} κριτικές της κοινότητας υπογεγραμμένες στο Nostr, καθώς και ζωντανή διαθεσιμότητα, υποστηριζόμενα NUTs και λογισμικό νομισματοκοπείου.", "footer.col.explore": "Εξερεύνηση",
"home.hero.title": "Βρείτε ένα Cassu vomismatokopopoo", "footer.col.site": "Ιστότοπος",
"footer.nav": "Υποσέλιδο",
"home.title": "Cashumints.space · Βρες ένα Cashu mint που μπορείς να εμπιστευτείς",
"home.description": "Βρες ένα Cashu mint που μπορείς να εμπιστευτείς. {mints} mints στο ευρετήριο με {reviews} κριτικές της κοινότητας υπογεγραμμένες στο Nostr, μαζί με ζωντανή διαθεσιμότητα, υποστηριζόμενα NUTs και λογισμικό του mint.",
"home.hero.title": "Βρες ένα Cashu mint",
"home.hero.titleEm": "Μπορείς να εμπιστευτείς.", "home.hero.titleEm": "Μπορείς να εμπιστευτείς.",
"home.hero.lede": "Κάθε νόμισμα στο δίκτυο Nostr, με πραγματικές κριτικές της κοινότητας. Χωρίς φύλακες, απλά υπογεγραμμένες κριτικές μπορείτε να επιβεβαιώσετε τον εαυτό σας.", "home.hero.lede": "Κάθε νόμισμα στο δίκτυο Nostr, με πραγματικές κριτικές της κοινότητας. Χωρίς φύλακες, απλά υπογεγραμμένες κριτικές μπορείτε να επιβεβαιώσετε τον εαυτό σας.",
"home.search.label": "Αναζήτηση ονόματος, domain, ή URL", "home.search.label": "Αναζήτηση ονόματος, domain, ή URL",
@@ -91,7 +95,7 @@
"pulse.stale": "μπαγιάτικο", "pulse.stale": "μπαγιάτικο",
"mints.title": "Όλα Κασσού νόμισμα · Cashumints.space", "mints.title": "Όλα Κασσού νόμισμα · Cashumints.space",
"mints.description": "Όλα{total}Cashu vomismatopopeia indexed from the Nostr network, Διεύθυνση:{online}online και{offline}offline, με την κοινότητα βαθμολογίες, uptime και vomismatopo λογισμικό. Αναζήτηση και ταξινόμηση της πλήρους λίστας.", "mints.description": "Όλα{total}Cashu vomismatopopeia indexed from the Nostr network, Διεύθυνση:{online}online και{offline}offline, με την κοινότητα βαθμολογίες, uptime και vomismatopo λογισμικό. Αναζήτηση και ταξινόμηση της πλήρους λίστας.",
"mints.heading": "Όλα τα Cassu vomismatokopeia", "mints.heading": "Όλα τα Cashu mints",
"mints.lede": "Κάθε νόμισμα που ανακαλύφθηκε στο δίκτυο Nostr, συμπεριλαμβανομένων και αυτών που έχουν φύγει. Offline νόμισμα που βρίσκεται στον κατάλογο ώστε να μπορείτε ακόμα να βρείτε και να τα ανασκοπήσετε.", "mints.lede": "Κάθε νόμισμα που ανακαλύφθηκε στο δίκτυο Nostr, συμπεριλαμβανομένων και αυτών που έχουν φύγει. Offline νόμισμα που βρίσκεται στον κατάλογο ώστε να μπορείτε ακόμα να βρείτε και να τα ανασκοπήσετε.",
"mints.search.label": "Αναζήτηση ονόματος ή domain", "mints.search.label": "Αναζήτηση ονόματος ή domain",
"mints.search.placeholder": "Αναζήτηση με όνομα ή τομέα...", "mints.search.placeholder": "Αναζήτηση με όνομα ή τομέα...",
@@ -114,6 +118,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",
@@ -261,6 +267,9 @@
"reviews.empty": "Δεν υπάρχουν κριτικές ακόμα.", "reviews.empty": "Δεν υπάρχουν κριτικές ακόμα.",
"reviews.emptyWritten": "Δεν υπάρχουν γραπτές κριτικές εδώ ακόμα.", "reviews.emptyWritten": "Δεν υπάρχουν γραπτές κριτικές εδώ ακόμα.",
"reviews.emptyFilter": "Καμία αξιολόγηση δεν ταιριάζει με αυτό το φίλτρο.", "reviews.emptyFilter": "Καμία αξιολόγηση δεν ταιριάζει με αυτό το φίλτρο.",
"reviews.rate.cashu": "Βαθμολογήστε αυτό το mint",
"reviews.rate.fedimint": "Βαθμολογήστε αυτή την ομοσπονδία",
"reviews.rate.lnurl": "Βαθμολογήστε αυτό το mint",
"reviews.noscript": "Οι κριτικές είναι υπογεγραμμένες στο Nostr και φορτώνουν στο πρόγραμμα περιήγησης σας, οπότε αυτή η λίστα χρειάζεται JavaScript. Η αξιολόγηση και οι μετρήσεις παραπάνω προέρχονται από την ίδια την ιστοσελίδα.", "reviews.noscript": "Οι κριτικές είναι υπογεγραμμένες στο Nostr και φορτώνουν στο πρόγραμμα περιήγησης σας, οπότε αυτή η λίστα χρειάζεται JavaScript. Η αξιολόγηση και οι μετρήσεις παραπάνω προέρχονται από την ίδια την ιστοσελίδα.",
"reviews.relayError": "Δεν μπόρεσε να φτάσει στο ρελέ. Οι κριτικές μπορεί να είναι ελλιπείς.", "reviews.relayError": "Δεν μπόρεσε να φτάσει στο ρελέ. Οι κριτικές μπορεί να είναι ελλιπείς.",
"reviews.srRating": "{rating} από 5", "reviews.srRating": "{rating} από 5",
@@ -273,7 +282,7 @@
"reviews.anonNote": "Πρώτη κριτική από αυτό το npub, δεν βρέθηκε άλλη δραστηριότητα", "reviews.anonNote": "Πρώτη κριτική από αυτό το npub, δεν βρέθηκε άλλη δραστηριότητα",
"reviews.published": "Δημοσίευση.", "reviews.published": "Δημοσίευση.",
"reviews.publishedPartial": "Δημοσίευση στο{accepted} του{total} ⁇ λεμάν.", "reviews.publishedPartial": "Δημοσίευση στο{accepted} του{total} ⁇ λεμάν.",
"reviews.propagating": "Διαδοχικά, μπορεί να πάρει μια στιγμή για να εμφανιστεί αλλού.", "reviews.propagating": "Δημοσίευση στα relays του Nostr",
"reviews.summary.line.one": "{count} αξιολόγηση χωρίς σχόλιο:{breakdown}", "reviews.summary.line.one": "{count} αξιολόγηση χωρίς σχόλιο:{breakdown}",
"reviews.summary.line.other": "{count} αξιολογήσεις χωρίς σχόλιο:{breakdown}", "reviews.summary.line.other": "{count} αξιολογήσεις χωρίς σχόλιο:{breakdown}",
"reviews.summary.lineDay.one": "{count} αξιολόγηση χωρίς σχόλιο σχετικά με{when}:{breakdown}", "reviews.summary.lineDay.one": "{count} αξιολόγηση χωρίς σχόλιο σχετικά με{when}:{breakdown}",
@@ -287,7 +296,12 @@
"reviews.breakdown.1": "{n} ένα αστέρι", "reviews.breakdown.1": "{n} ένα αστέρι",
"reviews.breakdown.none": "{n} χωρίς βαθμολογία", "reviews.breakdown.none": "{n} χωρίς βαθμολογία",
"reviews.dialog.title": "Επανάληψη{name}", "reviews.dialog.title": "Επανάληψη{name}",
"reviews.dialog.note": "Υπογραφή με το πλήκτρο Nostr και δημοσίευση σε δημόσιες αναμεταδόσεις. Δεν μπορεί να επεξεργαστεί ή να διαγραφεί.", "reviews.dialog.lede.cashu": "Μοιραστείτε την εμπειρία σας με αυτό το mint.",
"reviews.dialog.lede.fedimint": "Μοιραστείτε την εμπειρία σας με αυτή την ομοσπονδία.",
"reviews.dialog.lede.lnurl": "Μοιραστείτε την εμπειρία σας με αυτό το mint.",
"reviews.dialog.permanent": "Δημοσιεύεται μόνιμα σε δημόσια relays του Nostr.",
"reviews.dialog.gate.title": "Συνδεθείτε για να γράψετε μια κριτική",
"reviews.dialog.gate.lead": "Οι κριτικές υπογράφονται με το κλειδί σας Nostr. Παραμένει στον signer σας, αυτό το site δεν το βλέπει ποτέ.",
"reviews.dialog.ratingLegend": "Η βαθμολογία σας", "reviews.dialog.ratingLegend": "Η βαθμολογία σας",
"reviews.dialog.starsSr.one": "{n} άστρο", "reviews.dialog.starsSr.one": "{n} άστρο",
"reviews.dialog.starsSr.other": "{n} αστέρια", "reviews.dialog.starsSr.other": "{n} αστέρια",
@@ -308,7 +322,6 @@
"reviews.dialog.waitingSigner": "Περιμένω τον υπογράφοντα σου.", "reviews.dialog.waitingSigner": "Περιμένω τον υπογράφοντα σου.",
"reviews.dialog.confirmClose": "Κλείστε αυτή την κριτική; Το προσχέδιο σας διατηρείται μέχρι να φύγετε από το site.", "reviews.dialog.confirmClose": "Κλείστε αυτή την κριτική; Το προσχέδιο σας διατηρείται μέχρι να φύγετε από το site.",
"reviews.dialog.why.login": "Σύνδεση για δημοσίευση", "reviews.dialog.why.login": "Σύνδεση για δημοσίευση",
"reviews.dialog.why.rating": "Επιλέξτε μια βαθμολογία",
"reviews.dialog.why.words": "Γράψε μερικές λέξεις.", "reviews.dialog.why.words": "Γράψε μερικές λέξεις.",
"reviews.dialog.why.more.one": "{n} περισσότερο χαρακτήρα", "reviews.dialog.why.more.one": "{n} περισσότερο χαρακτήρα",
"reviews.dialog.why.more.other": "{n} περισσότεροι χαρακτήρες", "reviews.dialog.why.more.other": "{n} περισσότεροι χαρακτήρες",
@@ -325,7 +338,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": "Επανεξέταση",
@@ -369,6 +382,10 @@
"login.status.waitingSigner": "Περιμένω τον υπογράφοντα σου.", "login.status.waitingSigner": "Περιμένω τον υπογράφοντα σου.",
"login.status.connecting": "Σύνδεση με τον υπογράφοντα", "login.status.connecting": "Σύνδεση με τον υπογράφοντα",
"login.status.connected": "Συνδεδεμένο", "login.status.connected": "Συνδεδεμένο",
"login.toast.loggedIn": "Συνδεθήκατε",
"login.toast.fetchingProfile": "Λήψη του προφίλ σας στο Nostr από τα relays…",
"login.toast.profile": "Προφίλ: {name}",
"login.toast.noProfile": "Δεν βρέθηκε προφίλ στα relays",
"login.method.extension": "Επέκταση", "login.method.extension": "Επέκταση",
"login.method.remote": "Απομακρυσμένο", "login.method.remote": "Απομακρυσμένο",
"login.method.primal": "Πρωτεύων", "login.method.primal": "Πρωτεύων",
@@ -400,8 +417,15 @@
"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, με τις κριτικές από κάτω. Η πλήρης σελίδα εμφανίζεται στην επόμενη κατασκευή.",
"wallets.title": "Πορτοφόλια Κασού · Κασουμέντος.space", "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": "Πορτοφόλια Cashu · Cashumints.space",
"wallets.description": "Πορτοφόλια Cashu που λειτουργούν με τη νόμισμα που αναφέρεται εδώ, σε όλα τα κινητά, web, browser και τη γραμμή εντολών, με ό, τι ο καθένας είναι καλός σε.", "wallets.description": "Πορτοφόλια Cashu που λειτουργούν με τη νόμισμα που αναφέρεται εδώ, σε όλα τα κινητά, web, browser και τη γραμμή εντολών, με ό, τι ο καθένας είναι καλός σε.",
"wallets.heading": "Πορτοφόλια", "wallets.heading": "Πορτοφόλια",
"wallets.lede": "Χρειάζεται ένα πορτοφόλι για να κρατήσει εκάς από οποιοδήποτε από αυτά τα νόμισμα. Τα περισσότερα πορτοφόλια μιλούν με αρκετά νόμισμα ταυτόχρονα, έτσι ώστε να μην είναι δεμένα με το πρώτο που θα δοκιμάσετε.", "wallets.lede": "Χρειάζεται ένα πορτοφόλι για να κρατήσει εκάς από οποιοδήποτε από αυτά τα νόμισμα. Τα περισσότερα πορτοφόλια μιλούν με αρκετά νόμισμα ταυτόχρονα, έτσι ώστε να μην είναι δεμένα με το πρώτο που θα δοκιμάσετε.",
@@ -479,8 +503,8 @@
"terms.changes.p1": "Οι όροι αυτοί μπορούν να αλλάξουν. Η ημερομηνία στην κορυφή λέει πότε το έκαναν τελευταία φορά. Η ιστοσελίδα είναι ανοιχτή πηγή, έτσι κάθε αλλαγή σε αυτή τη σελίδα είναι στην ιστορία της δέσμευσης.", "terms.changes.p1": "Οι όροι αυτοί μπορούν να αλλάξουν. Η ημερομηνία στην κορυφή λέει πότε το έκαναν τελευταία φορά. Η ιστοσελίδα είναι ανοιχτή πηγή, έτσι κάθε αλλαγή σε αυτή τη σελίδα είναι στην ιστορία της δέσμευσης.",
"terms.contact.title": "Επαφή", "terms.contact.title": "Επαφή",
"terms.contact.p1": "Θέματα και αιτήσεις για λήψη είναι καλύτερα να υποβληθούν σε{github}. Το site διοικείται από{azzamo}.", "terms.contact.p1": "Θέματα και αιτήσεις για λήψη είναι καλύτερα να υποβληθούν σε{github}. Το site διοικείται από{azzamo}.",
"privacy.title": "Ιδιωτικότητα · Καsumints.space", "privacy.title": "Ιδιωτικότητα · Cashumints.space",
"privacy.description": "Προστασία προσωπικών δεδομένων στο χώρο Cashumints: δεν υπάρχουν λογαριασμοί, δεν υπάρχουν cookies παρακολούθησης, δεν υπάρχουν αναλυτικές πληροφορίες. Το τοπικόΑποθήκευση χρησιμοποιείται για το caching προφίλ και τίποτα δεν αποστέλλεται οπουδήποτε.", "privacy.description": "Ιδιωτικότητα στο Cashumints.space: χωρίς λογαριασμούς, χωρίς cookies παρακολούθησης, χωρίς αναλυτικά. Το localStorage χρησιμοποιείται για την προσωρινή αποθήκευση προφίλ και τίποτα δεν αποστέλλεται πουθενά. Οι κριτικές υπογράφονται στον δικό σας browser.",
"privacy.heading": "Ιδιωτικότητα", "privacy.heading": "Ιδιωτικότητα",
"privacy.lede": "Συλλέγουμε όσο λιγότερα μπορεί μια ιστοσελίδα.{date}.", "privacy.lede": "Συλλέγουμε όσο λιγότερα μπορεί μια ιστοσελίδα.{date}.",
"privacy.noAccounts.title": "Χωρίς λογαριασμούς", "privacy.noAccounts.title": "Χωρίς λογαριασμούς",
@@ -514,7 +538,7 @@
"privacy.permanent.p2": "Αν θέλετε να ανασκοπήσετε ένα νόμισμα χωρίς αυτό να είναι συνδεδεμένο με την κύρια ταυτότητά σας, χρησιμοποιήστε ένα ξεχωριστό πλήκτρο Nostr. Σημειώστε ότι ένα κλειδί χωρίς καμία άλλη δραστηριότητα εμφανίζεται ως τέτοιο σε σκληρές κριτικές, επειδή ένα ολοκαίνουργιο κλειδί που γράφει μια κριτική ενός αστεριού είναι ένα μοτίβο που αξίζει να δουν οι αναγνώστες.", "privacy.permanent.p2": "Αν θέλετε να ανασκοπήσετε ένα νόμισμα χωρίς αυτό να είναι συνδεδεμένο με την κύρια ταυτότητά σας, χρησιμοποιήστε ένα ξεχωριστό πλήκτρο Nostr. Σημειώστε ότι ένα κλειδί χωρίς καμία άλλη δραστηριότητα εμφανίζεται ως τέτοιο σε σκληρές κριτικές, επειδή ένα ολοκαίνουργιο κλειδί που γράφει μια κριτική ενός αστεριού είναι ένα μοτίβο που αξίζει να δουν οι αναγνώστες.",
"privacy.contact.title": "Ερωτήσεις", "privacy.contact.title": "Ερωτήσεις",
"privacy.contact.p1": "Ο ιστότοπος είναι ανοιχτός: αν θέλετε να ελέγξετε κάποιο από τα παραπάνω, διαβάστε τον κωδικό στο{github}. Ερωτήσεις και διορθώσεις μπορούν να υπεισέλθουν σε ένα θέμα.", "privacy.contact.p1": "Ο ιστότοπος είναι ανοιχτός: αν θέλετε να ελέγξετε κάποιο από τα παραπάνω, διαβάστε τον κωδικό στο{github}. Ερωτήσεις και διορθώσεις μπορούν να υπεισέλθουν σε ένα θέμα.",
"disclaimer.title": "Αποποίηση ευθύνης · Καsumints.space", "disclaimer.title": "Αποποίηση ευθύνης · Cashumints.space",
"disclaimer.description": "Τι πρέπει να ξέρετε πριν χρησιμοποιήσετε ένα Cassu vomismatokopoo: vomismatokopeia are stepterel, ένα vomismatokopoo μπορεί να εξαφανιστεί με τα χρήματά σας, οι κριτικές είναι απόψεις και σε απευθείας σύνδεση μέσα προσβάσιμα, όχι αξιόπιστα.", "disclaimer.description": "Τι πρέπει να ξέρετε πριν χρησιμοποιήσετε ένα Cassu vomismatokopoo: vomismatokopeia are stepterel, ένα vomismatokopoo μπορεί να εξαφανιστεί με τα χρήματά σας, οι κριτικές είναι απόψεις και σε απευθείας σύνδεση μέσα προσβάσιμα, όχι αξιόπιστα.",
"disclaimer.heading": "Αποποίηση ευθύνης", "disclaimer.heading": "Αποποίηση ευθύνης",
"disclaimer.lede": "Διαβάστε αυτό πριν βάλετε τα χρήματα σε ένα νόμισμα. Τελευταία ενημέρωση{date}.", "disclaimer.lede": "Διαβάστε αυτό πριν βάλετε τα χρήματα σε ένα νόμισμα. Τελευταία ενημέρωση{date}.",
@@ -548,12 +572,12 @@
"reviews.dialog.identity.extensionGone": "Η επέκταση του προγράμματος περιήγησης σας δεν απαντά. Συνδεθείτε ξανά ή χρησιμοποιήστε έναν απομακρυσμένο υπογράφοντα.", "reviews.dialog.identity.extensionGone": "Η επέκταση του προγράμματος περιήγησης σας δεν απαντά. Συνδεθείτε ξανά ή χρησιμοποιήστε έναν απομακρυσμένο υπογράφοντα.",
"seo.orgDescription": "Ένας κατάλογος της Cassu vomismatopopeia, με ζωντανή κατάσταση από κάθε vomismatopo και κοινοτικές κριτικές υπογεγραμμένες στο Nostr.", "seo.orgDescription": "Ένας κατάλογος της Cassu vomismatopopeia, με ζωντανή κατάσταση από κάθε vomismatopo και κοινοτικές κριτικές υπογεγραμμένες στο Nostr.",
"seo.ogImageAlt": "Cashumints.space: βρείτε ένα Cashu νόμισμα που μπορείτε να εμπιστευτείτε.", "seo.ogImageAlt": "Cashumints.space: βρείτε ένα Cashu νόμισμα που μπορείτε να εμπιστευτείτε.",
"seo.mint.title.one": "{name} αξιολογήσεις: βαθμολογημένες{rating} από{n} κριτική", "seo.mint.title.one": "{name} κριτικές: βαθμολογία {rating} από {n} κριτική | Cashumints.space",
"seo.mint.title.other": "{name} αξιολογήσεις: βαθμολογημένες{rating} από{n} σχόλια. Cashumints.space", "seo.mint.title.other": "{name} αξιολογήσεις: βαθμολογημένες{rating} από{n} σχόλια. Cashumints.space",
"seo.mint.titleOffline.one": "{name}: εκτός σύνδεσης για{n} ημέρα", "seo.mint.titleOffline.one": "{name}: εκτός σύνδεσης για {n} ημέρα | Cashumints.space",
"seo.mint.titleOffline.other": "{name}: εκτός σύνδεσης για{n} ημέρες. Cashumints.space", "seo.mint.titleOffline.other": "{name}: εκτός σύνδεσης για{n} ημέρες. Cashumints.space",
"seo.mint.titleNoReviews": "{name}: Κασού νόμισμα των πληροφοριών και σχόλια", "seo.mint.titleNoReviews": "{name}: πληροφορίες και κριτικές για το Cashu mint | Cashumints.space",
"seo.fedimint.titleNoReviews": "{name}: Fedimint Federation info and reviews", "seo.fedimint.titleNoReviews": "{name}: πληροφορίες και κριτικές για την ομοσπονδία Fedimint | Cashumints.space",
"seo.desc.fallback": "Κοινοτικές αναθεωρήσεις{name}({domain}).", "seo.desc.fallback": "Κοινοτικές αναθεωρήσεις{name}({domain}).",
"seo.desc.fallbackDomain": "Κοινοτικές κριτικές για το Cashu vomismatokopo στο{domain}.", "seo.desc.fallbackDomain": "Κοινοτικές κριτικές για το Cashu vomismatokopo στο{domain}.",
"seo.desc.rated.one": "Ονομαστική{rating}/5 από{n} υπογεγραμμένη αναθεώρηση Nostr.", "seo.desc.rated.one": "Ονομαστική{rating}/5 από{n} υπογεγραμμένη αναθεώρηση Nostr.",
@@ -565,11 +589,11 @@
"seo.desc.fedModulesNetwork": "{status} Ενότητες:{modules} Εμπρός{network}.", "seo.desc.fedModulesNetwork": "{status} Ενότητες:{modules} Εμπρός{network}.",
"seo.mint.ogAlt.one": "{name}:{rating} αστέρια από{n} ανασκόπηση του Cashumints.space", "seo.mint.ogAlt.one": "{name}:{rating} αστέρια από{n} ανασκόπηση του Cashumints.space",
"seo.mint.ogAlt.other": "{name}:{rating} αστέρια από{n} σχόλια για Cashumints.space", "seo.mint.ogAlt.other": "{name}:{rating} αστέρια από{n} σχόλια για Cashumints.space",
"seo.mint.ogAltNoReviews": "{name} στο χώρο Cashumints.", "seo.mint.ogAltNoReviews": "{name} στο Cashumints.space",
"nav.home": "Αρχική", "nav.home": "Αρχική",
"mint.more.title": "Περισσότερα νομισματοκοπεία", "mint.more.title": "Περισσότερα νομισματοκοπεία",
"mint.more.all": "Προβολή όλων των νομευμάτων", "mint.more.all": "Προβολή όλων των νομευμάτων",
"nav.fedimints": "Φοινικισμοί", "nav.fedimints": "Fedimints",
"status.announced": "Ανακοινώθηκε μόνο", "status.announced": "Ανακοινώθηκε μόνο",
"card.unnamedFederation": "Ανώνυμη ομοσπονδία", "card.unnamedFederation": "Ανώνυμη ομοσπονδία",
"card.notChecked": "δεν ελέγχθηκε ποτέ", "card.notChecked": "δεν ελέγχθηκε ποτέ",
@@ -654,5 +678,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 του ήταν απροσπέλαστος, οπότε τίποτα δεν μπορούσε να κοπεί ή να λιώσει."
} }
+136 -6
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",
@@ -261,6 +267,9 @@
"reviews.empty": "No reviews yet. Be the first to write one.", "reviews.empty": "No reviews yet. Be the first to write one.",
"reviews.emptyWritten": "No written reviews here yet. Be the first to write one.", "reviews.emptyWritten": "No written reviews here yet. Be the first to write one.",
"reviews.emptyFilter": "No reviews match this filter.", "reviews.emptyFilter": "No reviews match this filter.",
"reviews.rate.cashu": "Rate this mint",
"reviews.rate.fedimint": "Rate this federation",
"reviews.rate.lnurl": "Rate this mint",
"reviews.noscript": "Reviews are signed on Nostr and load in your browser, so this list needs JavaScript. The rating and counts above come from the site itself.", "reviews.noscript": "Reviews are signed on Nostr and load in your browser, so this list needs JavaScript. The rating and counts above come from the site itself.",
"reviews.relayError": "Could not reach the relay. Reviews may be incomplete.", "reviews.relayError": "Could not reach the relay. Reviews may be incomplete.",
"reviews.srRating": "{rating} out of 5", "reviews.srRating": "{rating} out of 5",
@@ -273,7 +282,7 @@
"reviews.anonNote": "First review from this npub, no other activity found", "reviews.anonNote": "First review from this npub, no other activity found",
"reviews.published": "Published.", "reviews.published": "Published.",
"reviews.publishedPartial": "Published to {accepted} of {total} relays.", "reviews.publishedPartial": "Published to {accepted} of {total} relays.",
"reviews.propagating": "Propagating, it may take a moment to appear elsewhere.", "reviews.propagating": "Publishing to Nostr relays",
"reviews.summary.line.one": "{count} rating without a comment: {breakdown}", "reviews.summary.line.one": "{count} rating without a comment: {breakdown}",
"reviews.summary.line.other": "{count} ratings without a comment: {breakdown}", "reviews.summary.line.other": "{count} ratings without a comment: {breakdown}",
"reviews.summary.lineDay.one": "{count} rating without a comment on {when}: {breakdown}", "reviews.summary.lineDay.one": "{count} rating without a comment on {when}: {breakdown}",
@@ -287,7 +296,12 @@
"reviews.breakdown.1": "{n} one star", "reviews.breakdown.1": "{n} one star",
"reviews.breakdown.none": "{n} with no rating", "reviews.breakdown.none": "{n} with no rating",
"reviews.dialog.title": "Review {name}", "reviews.dialog.title": "Review {name}",
"reviews.dialog.note": "Signed with your Nostr key and published to public relays. It cannot be edited or deleted.", "reviews.dialog.lede.cashu": "Share your experience with this mint.",
"reviews.dialog.lede.fedimint": "Share your experience with this federation.",
"reviews.dialog.lede.lnurl": "Share your experience with this mint.",
"reviews.dialog.permanent": "Published permanently to public Nostr relays.",
"reviews.dialog.gate.title": "Log in to write a review",
"reviews.dialog.gate.lead": "Reviews are signed with your Nostr key. It stays in your signer, this site never sees it.",
"reviews.dialog.ratingLegend": "Your rating", "reviews.dialog.ratingLegend": "Your rating",
"reviews.dialog.starsSr.one": "{n} star", "reviews.dialog.starsSr.one": "{n} star",
"reviews.dialog.starsSr.other": "{n} stars", "reviews.dialog.starsSr.other": "{n} stars",
@@ -308,7 +322,6 @@
"reviews.dialog.waitingSigner": "Waiting for your signer", "reviews.dialog.waitingSigner": "Waiting for your signer",
"reviews.dialog.confirmClose": "Close this review? Your draft is kept until you leave the site.", "reviews.dialog.confirmClose": "Close this review? Your draft is kept until you leave the site.",
"reviews.dialog.why.login": "Log in to publish", "reviews.dialog.why.login": "Log in to publish",
"reviews.dialog.why.rating": "Choose a rating",
"reviews.dialog.why.words": "Write a few words", "reviews.dialog.why.words": "Write a few words",
"reviews.dialog.why.more.one": "{n} more character", "reviews.dialog.why.more.one": "{n} more character",
"reviews.dialog.why.more.other": "{n} more characters", "reviews.dialog.why.more.other": "{n} more characters",
@@ -325,7 +338,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",
@@ -369,6 +382,10 @@
"login.status.waitingSigner": "Waiting for your signer", "login.status.waitingSigner": "Waiting for your signer",
"login.status.connecting": "Connecting to your signer", "login.status.connecting": "Connecting to your signer",
"login.status.connected": "Connected", "login.status.connected": "Connected",
"login.toast.loggedIn": "Logged in",
"login.toast.fetchingProfile": "Fetching your Nostr profile from relays…",
"login.toast.profile": "Profile: {name}",
"login.toast.noProfile": "No profile found on the relays",
"login.method.extension": "Extension", "login.method.extension": "Extension",
"login.method.remote": "Remote", "login.method.remote": "Remote",
"login.method.primal": "Primal", "login.method.primal": "Primal",
@@ -400,7 +417,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 +678,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."
} }
+136 -6
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",
@@ -265,6 +271,9 @@
"reviews.empty": "Todavía no hay reseñas. Sé la primera persona en escribir una.", "reviews.empty": "Todavía no hay reseñas. Sé la primera persona en escribir una.",
"reviews.emptyWritten": "Todavía no hay reseñas escritas aquí. Sé quien escriba la primera.", "reviews.emptyWritten": "Todavía no hay reseñas escritas aquí. Sé quien escriba la primera.",
"reviews.emptyFilter": "Ninguna reseña coincide con este filtro.", "reviews.emptyFilter": "Ninguna reseña coincide con este filtro.",
"reviews.rate.cashu": "Valora este mint",
"reviews.rate.fedimint": "Valora esta federación",
"reviews.rate.lnurl": "Valora este mint",
"reviews.noscript": "Las reseñas están firmadas en Nostr y se cargan en tu navegador, así que esta lista necesita JavaScript. La valoración y los recuentos de arriba vienen del propio sitio.", "reviews.noscript": "Las reseñas están firmadas en Nostr y se cargan en tu navegador, así que esta lista necesita JavaScript. La valoración y los recuentos de arriba vienen del propio sitio.",
"reviews.relayError": "No se ha podido llegar al relay. Puede que falten reseñas.", "reviews.relayError": "No se ha podido llegar al relay. Puede que falten reseñas.",
"reviews.srRating": "{rating} sobre 5", "reviews.srRating": "{rating} sobre 5",
@@ -277,7 +286,7 @@
"reviews.anonNote": "Primera reseña de este npub, no se ha encontrado ninguna otra actividad", "reviews.anonNote": "Primera reseña de este npub, no se ha encontrado ninguna otra actividad",
"reviews.published": "Publicada.", "reviews.published": "Publicada.",
"reviews.publishedPartial": "Publicada en {accepted} de {total} relays.", "reviews.publishedPartial": "Publicada en {accepted} de {total} relays.",
"reviews.propagating": "Propagándose, puede tardar un momento en aparecer en otros sitios.", "reviews.propagating": "Publicando en relés Nostr",
"reviews.summary.line.one": "{count} valoración sin comentario: {breakdown}", "reviews.summary.line.one": "{count} valoración sin comentario: {breakdown}",
"reviews.summary.line.other": "{count} valoraciones sin comentario: {breakdown}", "reviews.summary.line.other": "{count} valoraciones sin comentario: {breakdown}",
"reviews.summary.lineDay.one": "{count} valoración sin comentario del {when}: {breakdown}", "reviews.summary.lineDay.one": "{count} valoración sin comentario del {when}: {breakdown}",
@@ -291,7 +300,12 @@
"reviews.breakdown.1": "{n} de una estrella", "reviews.breakdown.1": "{n} de una estrella",
"reviews.breakdown.none": "{n} sin valoración", "reviews.breakdown.none": "{n} sin valoración",
"reviews.dialog.title": "Reseñar {name}", "reviews.dialog.title": "Reseñar {name}",
"reviews.dialog.note": "Se firma con tu clave Nostr y se publica en relays públicos. No se puede editar ni borrar.", "reviews.dialog.lede.cashu": "Cuenta tu experiencia con este mint.",
"reviews.dialog.lede.fedimint": "Cuenta tu experiencia con esta federación.",
"reviews.dialog.lede.lnurl": "Cuenta tu experiencia con este mint.",
"reviews.dialog.permanent": "Se publica de forma permanente en relays públicos de Nostr.",
"reviews.dialog.gate.title": "Inicia sesión para escribir una reseña",
"reviews.dialog.gate.lead": "Las reseñas se firman con tu clave Nostr. Se queda en tu firmante, este sitio no la ve nunca.",
"reviews.dialog.ratingLegend": "Tu valoración", "reviews.dialog.ratingLegend": "Tu valoración",
"reviews.dialog.starsSr.one": "{n} estrella", "reviews.dialog.starsSr.one": "{n} estrella",
"reviews.dialog.starsSr.other": "{n} estrellas", "reviews.dialog.starsSr.other": "{n} estrellas",
@@ -312,7 +326,6 @@
"reviews.dialog.waitingSigner": "Esperando a tu firmante", "reviews.dialog.waitingSigner": "Esperando a tu firmante",
"reviews.dialog.confirmClose": "¿Cerrar esta reseña? Tu borrador se guarda hasta que salgas del sitio.", "reviews.dialog.confirmClose": "¿Cerrar esta reseña? Tu borrador se guarda hasta que salgas del sitio.",
"reviews.dialog.why.login": "Inicia sesión para publicar", "reviews.dialog.why.login": "Inicia sesión para publicar",
"reviews.dialog.why.rating": "Elige una valoración",
"reviews.dialog.why.words": "Escribe unas palabras", "reviews.dialog.why.words": "Escribe unas palabras",
"reviews.dialog.why.more.one": "{n} carácter más", "reviews.dialog.why.more.one": "{n} carácter más",
"reviews.dialog.why.more.other": "{n} caracteres más", "reviews.dialog.why.more.other": "{n} caracteres más",
@@ -329,7 +342,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",
@@ -373,6 +386,10 @@
"login.status.waitingSigner": "Esperando a tu firmante", "login.status.waitingSigner": "Esperando a tu firmante",
"login.status.connecting": "Conectando con tu firmante", "login.status.connecting": "Conectando con tu firmante",
"login.status.connected": "Conectado", "login.status.connected": "Conectado",
"login.toast.loggedIn": "Sesión iniciada",
"login.toast.fetchingProfile": "Obteniendo tu perfil de Nostr desde los relays…",
"login.toast.profile": "Perfil: {name}",
"login.toast.noProfile": "No se encontró ningún perfil en los relays",
"login.method.extension": "Extensión", "login.method.extension": "Extensión",
"login.method.remote": "Remoto", "login.method.remote": "Remoto",
"login.method.primal": "Primal", "login.method.primal": "Primal",
@@ -404,7 +421,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 +682,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."
} }
+143 -13
View File
@@ -37,7 +37,7 @@
"nav.reviews": "Avis", "nav.reviews": "Avis",
"nav.wallets": "Portefeuilles", "nav.wallets": "Portefeuilles",
"nav.about": "À propos", "nav.about": "À propos",
"nav.searchMints": "Rechercher {n} mints", "nav.searchMints": "Rechercher parmi {n} mints",
"lang.label": "Langue", "lang.label": "Langue",
"lang.switcher": "Choisissez une langue", "lang.switcher": "Choisissez une langue",
"footer.copyright": "© {year} Cashumints.space", "footer.copyright": "© {year} Cashumints.space",
@@ -46,13 +46,17 @@
"footer.wallets": "Portefeuilles", "footer.wallets": "Portefeuilles",
"footer.about": "À propos", "footer.about": "À propos",
"footer.disclaimer": "Clause de non-responsabilité", "footer.disclaimer": "Clause de non-responsabilité",
"footer.terms": "Termes", "footer.terms": "Conditions",
"footer.privacy": "Confidentialité", "footer.privacy": "Confidentialité",
"footer.madeBy": "Réalisé avec ❤️ par {azzamo} · Propulsé par Cashu et NIP-87", "footer.madeBy": "Réalisé avec ❤️ par {azzamo} · Propulsé par Cashu et NIP-87",
"footer.warning": "Les Mints sont gardiens. Un mint peut disparaître avec votre argent.", "footer.warning": "Les mints sont custodiaux. Un mint peut disparaître avec votre argent.",
"home.title": "Cashumints.space · Trouvez un Cashu mint en qui vous pouvez avoir confiance", "footer.tagline": "Les avis de la communauté sur les mints ecash, signés sur Nostr.",
"home.description": "Trouvez un Cashu mint auquel vous pouvez faire confiance. {mints} mints indexé avec les avis de la communauté {reviews} signés sur Nostr, ainsi qu'une disponibilité en direct, les logiciels NUTs et mint pris en charge.", "footer.col.explore": "Explorer",
"home.hero.title": "Trouver un Cashu mint", "footer.col.site": "Site",
"footer.nav": "Pied de page",
"home.title": "Cashumints.space · Trouve un mint Cashu en qui tu peux avoir confiance",
"home.description": "Trouve un mint Cashu en qui tu peux avoir confiance. {mints} mints indexés, {reviews} avis de la communauté signés sur Nostr, plus la disponibilité en direct, les NUTs pris en charge et le logiciel du mint.",
"home.hero.title": "Trouver un mint Cashu",
"home.hero.titleEm": "tu peux faire confiance", "home.hero.titleEm": "tu peux faire confiance",
"home.hero.lede": "Chaque mint sur le réseau Nostr, avec de vrais avis communautaires. Pas de gardiens, juste des avis signés que vous pouvez vérifier vous-même.", "home.hero.lede": "Chaque mint sur le réseau Nostr, avec de vrais avis communautaires. Pas de gardiens, juste des avis signés que vous pouvez vérifier vous-même.",
"home.search.label": "Recherchez mints par nom, domaine ou URL", "home.search.label": "Recherchez mints par nom, domaine ou URL",
@@ -114,6 +118,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",
@@ -261,6 +267,9 @@
"reviews.empty": "Aucun avis pour l'instant. Soyez le premier à en écrire un.", "reviews.empty": "Aucun avis pour l'instant. Soyez le premier à en écrire un.",
"reviews.emptyWritten": "Pas encore de critiques écrites ici. Soyez le premier à en écrire un.", "reviews.emptyWritten": "Pas encore de critiques écrites ici. Soyez le premier à en écrire un.",
"reviews.emptyFilter": "Aucun avis ne correspond à ce filtre.", "reviews.emptyFilter": "Aucun avis ne correspond à ce filtre.",
"reviews.rate.cashu": "Évaluer ce mint",
"reviews.rate.fedimint": "Évaluer cette fédération",
"reviews.rate.lnurl": "Évaluer ce mint",
"reviews.noscript": "Les avis sont signés sur Nostr et chargés dans votre navigateur, cette liste nécessite donc JavaScript. La note et les décomptes ci-dessus proviennent du site lui-même.", "reviews.noscript": "Les avis sont signés sur Nostr et chargés dans votre navigateur, cette liste nécessite donc JavaScript. La note et les décomptes ci-dessus proviennent du site lui-même.",
"reviews.relayError": "Impossible d'atteindre le relais. Les avis peuvent être incomplets.", "reviews.relayError": "Impossible d'atteindre le relais. Les avis peuvent être incomplets.",
"reviews.srRating": "{rating} sur 5", "reviews.srRating": "{rating} sur 5",
@@ -273,7 +282,7 @@
"reviews.anonNote": "Premier avis sur ce npub, aucune autre activité trouvée", "reviews.anonNote": "Premier avis sur ce npub, aucune autre activité trouvée",
"reviews.published": "Publié.", "reviews.published": "Publié.",
"reviews.publishedPartial": "Publié sur {accepted} des relais {total}.", "reviews.publishedPartial": "Publié sur {accepted} des relais {total}.",
"reviews.propagating": "En se propageant, cela peut prendre un moment pour apparaître ailleurs.", "reviews.propagating": "Publication sur les relais Nostr",
"reviews.summary.line.one": "Note {count} sans commentaire : {breakdown}", "reviews.summary.line.one": "Note {count} sans commentaire : {breakdown}",
"reviews.summary.line.other": "Notes {count} sans commentaire : {breakdown}", "reviews.summary.line.other": "Notes {count} sans commentaire : {breakdown}",
"reviews.summary.lineDay.one": "Note {count} sans commentaire sur {when} : {breakdown}", "reviews.summary.lineDay.one": "Note {count} sans commentaire sur {when} : {breakdown}",
@@ -287,7 +296,12 @@
"reviews.breakdown.1": "{n} une étoile", "reviews.breakdown.1": "{n} une étoile",
"reviews.breakdown.none": "{n} sans note", "reviews.breakdown.none": "{n} sans note",
"reviews.dialog.title": "Avis {name}", "reviews.dialog.title": "Avis {name}",
"reviews.dialog.note": "Signé avec votre clé Nostr et publié auprès des relais publics. Il ne peut pas être modifié ou supprimé.", "reviews.dialog.lede.cashu": "Partagez votre expérience avec ce mint.",
"reviews.dialog.lede.fedimint": "Partagez votre expérience avec cette fédération.",
"reviews.dialog.lede.lnurl": "Partagez votre expérience avec ce mint.",
"reviews.dialog.permanent": "Publié de façon permanente sur des relais Nostr publics.",
"reviews.dialog.gate.title": "Connectez-vous pour écrire un avis",
"reviews.dialog.gate.lead": "Les avis sont signés avec votre clé Nostr. Elle reste dans votre signataire, ce site ne la voit jamais.",
"reviews.dialog.ratingLegend": "Votre note", "reviews.dialog.ratingLegend": "Votre note",
"reviews.dialog.starsSr.one": "Étoile {n}", "reviews.dialog.starsSr.one": "Étoile {n}",
"reviews.dialog.starsSr.other": "Étoiles {n}", "reviews.dialog.starsSr.other": "Étoiles {n}",
@@ -308,7 +322,6 @@
"reviews.dialog.waitingSigner": "En attente de votre signataire", "reviews.dialog.waitingSigner": "En attente de votre signataire",
"reviews.dialog.confirmClose": "Fermer cet avis ? Votre brouillon est conservé jusqu'à votre départ du site.", "reviews.dialog.confirmClose": "Fermer cet avis ? Votre brouillon est conservé jusqu'à votre départ du site.",
"reviews.dialog.why.login": "Connectez-vous pour publier", "reviews.dialog.why.login": "Connectez-vous pour publier",
"reviews.dialog.why.rating": "Choisissez une note",
"reviews.dialog.why.words": "Écrivez quelques mots", "reviews.dialog.why.words": "Écrivez quelques mots",
"reviews.dialog.why.more.one": "{n} plus de caractère", "reviews.dialog.why.more.one": "{n} plus de caractère",
"reviews.dialog.why.more.other": "{n} plus de caractères", "reviews.dialog.why.more.other": "{n} plus de caractères",
@@ -325,7 +338,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",
@@ -369,6 +382,10 @@
"login.status.waitingSigner": "En attente de votre signataire", "login.status.waitingSigner": "En attente de votre signataire",
"login.status.connecting": "Connexion à votre signataire", "login.status.connecting": "Connexion à votre signataire",
"login.status.connected": "Connecté", "login.status.connected": "Connecté",
"login.toast.loggedIn": "Connecté",
"login.toast.fetchingProfile": "Récupération de votre profil Nostr depuis les relais…",
"login.toast.profile": "Profil : {name}",
"login.toast.noProfile": "Aucun profil trouvé sur les relais",
"login.method.extension": "Extension", "login.method.extension": "Extension",
"login.method.remote": "Télécommande", "login.method.remote": "Télécommande",
"login.method.primal": "Primal", "login.method.primal": "Primal",
@@ -400,7 +417,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",
@@ -566,7 +590,7 @@
"seo.mint.ogAlt.one": "{name} : les étoiles du {rating} de l'examen du {n} sur le Cashumints.space", "seo.mint.ogAlt.one": "{name} : les étoiles du {rating} de l'examen du {n} sur le Cashumints.space",
"seo.mint.ogAlt.other": "{name} : Étoiles {rating} de {n} avis sur Cashumints.space", "seo.mint.ogAlt.other": "{name} : Étoiles {rating} de {n} avis sur Cashumints.space",
"seo.mint.ogAltNoReviews": "{name} sur Cashumints.space", "seo.mint.ogAltNoReviews": "{name} sur Cashumints.space",
"nav.home": "Maison", "nav.home": "Accueil",
"mint.more.title": "Plus mints", "mint.more.title": "Plus mints",
"mint.more.all": "Tout afficher mints", "mint.more.all": "Tout afficher mints",
"nav.fedimints": "Fedimints", "nav.fedimints": "Fedimints",
@@ -654,5 +678,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."
} }
+139 -9
View File
@@ -37,7 +37,7 @@
"nav.reviews": "समीक्षाएँ", "nav.reviews": "समीक्षाएँ",
"nav.wallets": "बटुए", "nav.wallets": "बटुए",
"nav.about": "बारे में", "nav.about": "बारे में",
"nav.searchMints": "खोज {n} मिंट्स", "nav.searchMints": "{n} मिंट्स में खोजें",
"lang.label": "भाषा", "lang.label": "भाषा",
"lang.switcher": "एक भाषा चुनें", "lang.switcher": "एक भाषा चुनें",
"footer.copyright": "© {year} Cashumints.space", "footer.copyright": "© {year} Cashumints.space",
@@ -49,7 +49,11 @@
"footer.terms": "शर्तें", "footer.terms": "शर्तें",
"footer.privacy": "गोपनीयता", "footer.privacy": "गोपनीयता",
"footer.madeBy": "❤️ से निर्मित {azzamo} द्वारा · Cashu और NIP-87 द्वारा संचालित", "footer.madeBy": "❤️ से निर्मित {azzamo} द्वारा · Cashu और NIP-87 द्वारा संचालित",
"footer.warning": "मिंट्स संरक्षक हैं। मिंट आपके पैसे के साथ गायब हो सकता है।", "footer.warning": "मिंट्स कस्टोडियल हैं। कोई मिंट आपके पैसों के साथ गायब हो सकता है।",
"footer.tagline": "ecash मिंट्स के लिए समुदाय की समीक्षाएँ, Nostr पर हस्ताक्षरित।",
"footer.col.explore": "एक्सप्लोर",
"footer.col.site": "साइट",
"footer.nav": "फ़ुटर",
"home.title": "Cashumints.space · एक ऐसा Cashu मिंट खोजें जिस पर आप भरोसा कर सकते हैं", "home.title": "Cashumints.space · एक ऐसा Cashu मिंट खोजें जिस पर आप भरोसा कर सकते हैं",
"home.description": "एक ऐसा Cashu मिंट खोजें जिस पर आप भरोसा कर सकें। {mints} मिंट्स {reviews} समुदाय समीक्षाओं के साथ अनुक्रमित, Nostr पर हस्ताक्षरित, साथ ही लाइव अपटाइम, समर्थित NUTs और मिंट सॉफ़्टवेयर।", "home.description": "एक ऐसा Cashu मिंट खोजें जिस पर आप भरोसा कर सकें। {mints} मिंट्स {reviews} समुदाय समीक्षाओं के साथ अनुक्रमित, Nostr पर हस्ताक्षरित, साथ ही लाइव अपटाइम, समर्थित NUTs और मिंट सॉफ़्टवेयर।",
"home.hero.title": "Cashu मिंट खोजें", "home.hero.title": "Cashu मिंट खोजें",
@@ -114,6 +118,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 मिंट की प्रतिलिपि बनाएँ",
@@ -261,6 +267,9 @@
"reviews.empty": "अभी तक कोई समीक्षा नहीं है। पहली समीक्षा लिखें।", "reviews.empty": "अभी तक कोई समीक्षा नहीं है। पहली समीक्षा लिखें।",
"reviews.emptyWritten": "यहाँ अभी तक कोई लिखित समीक्षा नहीं है। पहली समीक्षा लिखें।", "reviews.emptyWritten": "यहाँ अभी तक कोई लिखित समीक्षा नहीं है। पहली समीक्षा लिखें।",
"reviews.emptyFilter": "इस फ़िल्टर से कोई समीक्षा मेल नहीं खाती।", "reviews.emptyFilter": "इस फ़िल्टर से कोई समीक्षा मेल नहीं खाती।",
"reviews.rate.cashu": "इस मिंट को रेटिंग दें",
"reviews.rate.fedimint": "इस फ़ेडरेशन को रेटिंग दें",
"reviews.rate.lnurl": "इस मिंट को रेटिंग दें",
"reviews.noscript": "समीक्षाएँ Nostr पर साइन की जाती हैं और आपके ब्राउज़र में लोड होती हैं, इसलिए इस सूची के लिए JavaScript की आवश्यकता है। ऊपर दी गई रेटिंग और संख्याएँ स्वयं साइट से ली गई हैं।", "reviews.noscript": "समीक्षाएँ Nostr पर साइन की जाती हैं और आपके ब्राउज़र में लोड होती हैं, इसलिए इस सूची के लिए JavaScript की आवश्यकता है। ऊपर दी गई रेटिंग और संख्याएँ स्वयं साइट से ली गई हैं।",
"reviews.relayError": "रिले तक नहीं पहुँच सका। समीक्षाएँ अधूरी हो सकती हैं।", "reviews.relayError": "रिले तक नहीं पहुँच सका। समीक्षाएँ अधूरी हो सकती हैं।",
"reviews.srRating": "5 में से {rating}", "reviews.srRating": "5 में से {rating}",
@@ -273,7 +282,7 @@
"reviews.anonNote": "इस npub से पहली समीक्षा, कोई अन्य गतिविधि नहीं मिली।", "reviews.anonNote": "इस npub से पहली समीक्षा, कोई अन्य गतिविधि नहीं मिली।",
"reviews.published": "प्रकाशित", "reviews.published": "प्रकाशित",
"reviews.publishedPartial": "{total} रिलेज़ में से {accepted} पर प्रकाशित।", "reviews.publishedPartial": "{total} रिलेज़ में से {accepted} पर प्रकाशित।",
"reviews.propagating": "प्रसारित करते समय, कहीं और प्रकट होने में थोड़ा समय लग सकता है।", "reviews.propagating": "Nostr रिले पर प्रकाशित किया जा रहा है",
"reviews.summary.line.one": "बिना टिप्पणी के {count} रेटिंग: {breakdown}", "reviews.summary.line.one": "बिना टिप्पणी के {count} रेटिंग: {breakdown}",
"reviews.summary.line.other": "टिप्पणी के बिना रेटिंग: {count}; रेटिंग: {breakdown}", "reviews.summary.line.other": "टिप्पणी के बिना रेटिंग: {count}; रेटिंग: {breakdown}",
"reviews.summary.lineDay.one": "{count} रेटिंग, {when} पर बिना टिप्पणी के: {breakdown}", "reviews.summary.lineDay.one": "{count} रेटिंग, {when} पर बिना टिप्पणी के: {breakdown}",
@@ -287,7 +296,12 @@
"reviews.breakdown.1": "{n} एक सितारा", "reviews.breakdown.1": "{n} एक सितारा",
"reviews.breakdown.none": "{n} बिना रेटिंग के", "reviews.breakdown.none": "{n} बिना रेटिंग के",
"reviews.dialog.title": "समीक्षा {name}", "reviews.dialog.title": "समीक्षा {name}",
"reviews.dialog.note": "आपकी Nostr कुंजी से हस्ताक्षरित और सार्वजनिक रिले पर प्रकाशित। इसे संपादित या हटाया नहीं जा सकता।", "reviews.dialog.lede.cashu": "इस मिंट के बारे में अपना अनुभव साझा करें।",
"reviews.dialog.lede.fedimint": "इस फ़ेडरेशन के बारे में अपना अनुभव साझा करें।",
"reviews.dialog.lede.lnurl": "इस मिंट के बारे में अपना अनुभव साझा करें।",
"reviews.dialog.permanent": "सार्वजनिक Nostr रिले पर स्थायी रूप से प्रकाशित।",
"reviews.dialog.gate.title": "समीक्षा लिखने के लिए लॉग इन करें",
"reviews.dialog.gate.lead": "समीक्षाएँ आपकी Nostr कुंजी से हस्ताक्षरित होती हैं। कुंजी आपके हस्ताक्षरकर्ता में रहती है, यह साइट इसे कभी नहीं देखती।",
"reviews.dialog.ratingLegend": "आपकी रेटिंग", "reviews.dialog.ratingLegend": "आपकी रेटिंग",
"reviews.dialog.starsSr.one": "{n} सितारा", "reviews.dialog.starsSr.one": "{n} सितारा",
"reviews.dialog.starsSr.other": "{n} सितारे", "reviews.dialog.starsSr.other": "{n} सितारे",
@@ -308,7 +322,6 @@
"reviews.dialog.waitingSigner": "आपके हस्ताक्षरकर्ता की प्रतीक्षा", "reviews.dialog.waitingSigner": "आपके हस्ताक्षरकर्ता की प्रतीक्षा",
"reviews.dialog.confirmClose": "इस समीक्षा को बंद करें? आपका मसौदा तब तक रखा जाता है जब तक आप साइट छोड़ते नहीं हैं।", "reviews.dialog.confirmClose": "इस समीक्षा को बंद करें? आपका मसौदा तब तक रखा जाता है जब तक आप साइट छोड़ते नहीं हैं।",
"reviews.dialog.why.login": "प्रकाशित करने के लिए लॉग इन करें", "reviews.dialog.why.login": "प्रकाशित करने के लिए लॉग इन करें",
"reviews.dialog.why.rating": "एक रेटिंग चुनें",
"reviews.dialog.why.words": "कुछ शब्द लिखें", "reviews.dialog.why.words": "कुछ शब्द लिखें",
"reviews.dialog.why.more.one": "{n} और वर्ण", "reviews.dialog.why.more.one": "{n} और वर्ण",
"reviews.dialog.why.more.other": "{n} और अक्षर", "reviews.dialog.why.more.other": "{n} और अक्षर",
@@ -325,7 +338,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": "समीक्षाएँ",
@@ -369,6 +382,10 @@
"login.status.waitingSigner": "आपके हस्ताक्षरकर्ता की प्रतीक्षा", "login.status.waitingSigner": "आपके हस्ताक्षरकर्ता की प्रतीक्षा",
"login.status.connecting": "आपके हस्ताक्षरकर्ता से कनेक्ट हो रहा है", "login.status.connecting": "आपके हस्ताक्षरकर्ता से कनेक्ट हो रहा है",
"login.status.connected": "जुड़ा हुआ", "login.status.connected": "जुड़ा हुआ",
"login.toast.loggedIn": "लॉग इन हो गए",
"login.toast.fetchingProfile": "आपकी Nostr प्रोफ़ाइल रिले से लाई जा रही है…",
"login.toast.profile": "प्रोफ़ाइल: {name}",
"login.toast.noProfile": "रिले पर कोई प्रोफ़ाइल नहीं मिली",
"login.method.extension": "विस्तार", "login.method.extension": "विस्तार",
"login.method.remote": "दूरस्थ", "login.method.remote": "दूरस्थ",
"login.method.primal": "Primal", "login.method.primal": "Primal",
@@ -400,7 +417,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": "बटुए",
@@ -569,7 +593,7 @@
"nav.home": "होम", "nav.home": "होम",
"mint.more.title": "और मिंट्स", "mint.more.title": "और मिंट्स",
"mint.more.all": "सभी मिंट्स देखें", "mint.more.all": "सभी मिंट्स देखें",
"nav.fedimints": "फेडिमिंट्स", "nav.fedimints": "Fedimints",
"status.announced": "केवल घोषित", "status.announced": "केवल घोषित",
"card.unnamedFederation": "अनाम संघ", "card.unnamedFederation": "अनाम संघ",
"card.notChecked": "कभी जांचा नहीं", "card.notChecked": "कभी जांचा नहीं",
@@ -654,5 +678,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": "इसका लाइटनिंग नोड भी पहुँच से बाहर था, इसलिए कुछ भी मिंट या मेल्ट नहीं किया जा सकता था."
} }
+210 -80
View File
@@ -33,26 +33,30 @@
"status.unknown": "Belum diperiksa", "status.unknown": "Belum diperiksa",
"nav.skip": "Lewati ke isi", "nav.skip": "Lewati ke isi",
"nav.main": "Utama", "nav.main": "Utama",
"nav.mints": "Mints Cashu", "nav.mints": "Mint Cashu",
"nav.reviews": "Ulasan", "nav.reviews": "Ulasan",
"nav.wallets": "Dompet", "nav.wallets": "Dompet",
"nav.about": "Tentang", "nav.about": "Tentang",
"nav.searchMints": "Cari permen {n}", "nav.searchMints": "Cari di {n} mint",
"lang.label": "Bahasa", "lang.label": "Bahasa",
"lang.switcher": "Pilih sebuah bahasa", "lang.switcher": "Pilih sebuah bahasa",
"footer.copyright": "© {year} Cashumints.space", "footer.copyright": "© {year} Cashumints.space",
"footer.allMints": "Semua permen cashu", "footer.allMints": "Semua mint Cashu",
"footer.allReviews": "Semua ulasan", "footer.allReviews": "Semua ulasan",
"footer.wallets": "Dompet", "footer.wallets": "Dompet",
"footer.about": "Tentang", "footer.about": "Tentang",
"footer.disclaimer": "Discoverer", "footer.disclaimer": "Penafian",
"footer.terms": "Term", "footer.terms": "Ketentuan",
"footer.privacy": "Privasi", "footer.privacy": "Privasi",
"footer.madeBy": "Dibuat dengan {azzamo} yang dibuat oleh Cashu dan NIP-87", "footer.madeBy": "Dibuat dengan ❤️ oleh {azzamo} · Ditenagai oleh Cashu dan NIP-87",
"footer.warning": "Mints adalah kustodian. Mint bisa hilang dengan uangmu.", "footer.warning": "Mint bersifat kustodial. Sebuah mint bisa menghilang bersama uangmu.",
"home.title": "Cashumints.space", "footer.tagline": "Ulasan komunitas untuk mint ecash, ditandatangani di Nostr.",
"footer.col.explore": "Jelajahi",
"footer.col.site": "Situs",
"footer.nav": "Footer",
"home.title": "Cashumints.space · Temukan mint Cashu yang bisa kamu percaya",
"home.description": "Temukan mint Cashu yang dapat Anda percaya. {mints} mint diindeks dengan {reviews} ulasan komunitas yang ditandatangani di Nostr, beserta waktu aktif langsung, NUT yang didukung, dan perangkat lunak mint.", "home.description": "Temukan mint Cashu yang dapat Anda percaya. {mints} mint diindeks dengan {reviews} ulasan komunitas yang ditandatangani di Nostr, beserta waktu aktif langsung, NUT yang didukung, dan perangkat lunak mint.",
"home.hero.title": "Cari sebuah mint Cashu", "home.hero.title": "Temukan mint Cashu",
"home.hero.titleEm": "Anda dapat percaya", "home.hero.titleEm": "Anda dapat percaya",
"home.hero.lede": "Setiap mint pada jaringan Nostr, dengan ulasan komunitas yang nyata. Tidak ada penjaga gerbang, hanya menandatangani ulasan Anda dapat memverifikasi diri sendiri.", "home.hero.lede": "Setiap mint pada jaringan Nostr, dengan ulasan komunitas yang nyata. Tidak ada penjaga gerbang, hanya menandatangani ulasan Anda dapat memverifikasi diri sendiri.",
"home.search.label": "Cari mint dengan nama, domain, atau URL", "home.search.label": "Cari mint dengan nama, domain, atau URL",
@@ -61,7 +65,7 @@
"home.filters.highestRated": "Rating tertinggi", "home.filters.highestRated": "Rating tertinggi",
"home.filters.recentlyActive": "Baru-baru ini aktif", "home.filters.recentlyActive": "Baru-baru ini aktif",
"home.filters.websockets": "Mendukung Soket Web", "home.filters.websockets": "Mendukung Soket Web",
"home.top.title": "Permen mint Top Cashu", "home.top.title": "Mint Cashu teratas",
"home.top.sub": "peringkat dengan ulasan dan peringkat", "home.top.sub": "peringkat dengan ulasan dan peringkat",
"home.top.all": "Semua {n} Mints", "home.top.all": "Semua {n} Mints",
"home.latest.title": "Ulasan Terbaru", "home.latest.title": "Ulasan Terbaru",
@@ -80,7 +84,7 @@
"home.why.checked.body": "Kami menyelidiki setiap mint untuk uptime, versi perangkat lunak, batas, dan didukung NUTS, sehingga spesifikasi yang Anda lihat adalah saat ini, tidak diri-dilaporkan.", "home.why.checked.body": "Kami menyelidiki setiap mint untuk uptime, versi perangkat lunak, batas, dan didukung NUTS, sehingga spesifikasi yang Anda lihat adalah saat ini, tidak diri-dilaporkan.",
"home.cta.title": "Berpikir tentang mint? Baca dulu.", "home.cta.title": "Berpikir tentang mint? Baca dulu.",
"home.cta.body": "Sebuah mint memegang ecash Anda. Baca apa yang orang lain temukan sebelum kau memasukkan sats, dan tinggalkan ulasan ketika kau menggunakannya sendiri.", "home.cta.body": "Sebuah mint memegang ecash Anda. Baca apa yang orang lain temukan sebelum kau memasukkan sats, dan tinggalkan ulasan ketika kau menggunakannya sendiri.",
"home.cta.explore": "Menjelajahi semua permen", "home.cta.explore": "Jelajahi semua mint",
"home.cta.learn": "Pelajari tentang Cashu", "home.cta.learn": "Pelajari tentang Cashu",
"pulse.mintsIndexed": "mint diindeks", "pulse.mintsIndexed": "mint diindeks",
"pulse.reviewsOnNostr": "ulasan pada Nostr", "pulse.reviewsOnNostr": "ulasan pada Nostr",
@@ -89,43 +93,45 @@
"pulse.indexer": "pengindeks", "pulse.indexer": "pengindeks",
"pulse.connected": "tersambung", "pulse.connected": "tersambung",
"pulse.stale": "basi", "pulse.stale": "basi",
"mints.title": "Semua permen Cashu", "mints.title": "Semua mint Cashu · Cashumints.space",
"mints.description": "Semua {total} Cushu diindeks dari jaringan Nostr, {online} online dan {offline} offline, dengan peringkat masyarakat, perangkat lunak uptime dan mint. Cari dan urutkan daftar lengkapnya.", "mints.description": "Semua {total} mint Cashu yang diindeks dari jaringan Nostr, {online} online dan {offline} offline, dengan peringkat komunitas, waktu aktif, dan perangkat lunak mint. Cari dan urutkan daftar lengkapnya.",
"mints.heading": "Semua permen cashu", "mints.heading": "Semua mint Cashu",
"mints.lede": "Setiap mint ditemukan pada jaringan Nostr, termasuk orang-orang yang pergi. Mint offline tetap terdaftar sehingga Anda masih dapat menemukan dan meninjau mereka.", "mints.lede": "Setiap mint yang ditemukan di jaringan Nostr, termasuk yang sudah tidak ada. Mint yang offline tetap terdaftar sehingga kamu masih bisa menemukan dan mengulasnya.",
"mints.search.label": "Cari mint dengan nama atau domain", "mints.search.label": "Cari mint dengan nama atau domain",
"mints.search.placeholder": "Cari dengan nama atau domain...", "mints.search.placeholder": "Cari dengan nama atau domain...",
"mints.sort.label": "Urutkan permen", "mints.sort.label": "Urutkan mint",
"mints.sort.score": "Keseluruhan terbaik", "mints.sort.score": "Keseluruhan terbaik",
"mints.sort.rating": "Rating tertinggi", "mints.sort.rating": "Rating tertinggi",
"mints.sort.reviews": "Kebanyakan ditinjau", "mints.sort.reviews": "Kebanyakan ditinjau",
"mints.sort.recent": "Baru-baru ini ditinjau", "mints.sort.recent": "Baru-baru ini ditinjau",
"mints.sort.seen": "Baru-baru ini terlihat online", "mints.sort.seen": "Baru-baru ini terlihat online",
"mints.hideOffline": "Sembunyikan luring", "mints.hideOffline": "Sembunyikan luring",
"mints.showing": "Menampilkan {total} mints ({online} daring, {offline} offline)", "mints.showing": "Menampilkan {total} mint ({online} online, {offline} offline)",
"mints.showingFiltered": "Menampilkan mint {shown} dari {total} ({online} daring, {offline} offline)", "mints.showingFiltered": "Menampilkan mint {shown} dari {total} ({online} daring, {offline} offline)",
"mints.noResults": "Tidak ada permen yang cocok. Coba domain, atau paste sebuah URL mint.", "mints.noResults": "Tidak ada mint yang cocok. Coba sebuah domain, atau tempel URL mint.",
"mints.clearSearch": "Bersihkan pencarian", "mints.clearSearch": "Bersihkan pencarian",
"card.noRatings": "Belum ada peringkat", "card.noRatings": "Belum ada peringkat",
"card.reviews.one": "Ulasan {n}", "card.reviews.one": "{n} ulasan",
"card.reviews.other": "Ulasan {n}", "card.reviews.other": "{n} ulasan",
"card.lastSeen": "terakhir terlihat {when}", "card.lastSeen": "terakhir terlihat {when}",
"card.neverSeen": "tidak pernah terlihat online", "card.neverSeen": "tidak pernah terlihat online",
"card.reviewed": "{when}", "card.reviewed": "diulas {when}",
"card.noReviews": "belum ada ulasan", "card.noReviews": "belum ada ulasan",
"mint.title": "{name}", "mint.title": "{name} · ulasan mint Cashu",
"mint.titleFedimint": "{name} · Ulasan federasi Fedimint",
"mint.titleLnurl": "{name} · Ulasan mint LNURL",
"mint.breadcrumb": "Breadcrumb", "mint.breadcrumb": "Breadcrumb",
"mint.crumbMints": "Mints Cashu", "mint.crumbMints": "Mint Cashu",
"mint.copyUrl": "Salin URL mint", "mint.copyUrl": "Salin URL mint",
"mint.qrButton": "Mint QR", "mint.qrButton": "Mint QR",
"mint.writeReview": "Tulis suatu ulasan", "mint.writeReview": "Tulis suatu ulasan",
"mint.rankByScore": "# {rank} oleh skor", "mint.rankByScore": "#{rank} berdasarkan skor",
"mint.tagLightning": "Petir", "mint.tagLightning": "Lightning",
"mint.summary": "Ringkasan Mint", "mint.summary": "Ringkasan mint",
"mint.rating.label": "Rating komunitas", "mint.rating.label": "Rating komunitas",
"mint.rating.none": "Belum ada peringkat", "mint.rating.none": "Belum ada peringkat",
"mint.rating.reviews.one": "Ulasan {n}", "mint.rating.reviews.one": "{n} ulasan",
"mint.rating.reviews.other": "Ulasan {n}", "mint.rating.reviews.other": "{n} ulasan",
"mint.rating.unrated.one": "{n} tidak meninggalkan rating", "mint.rating.unrated.one": "{n} tidak meninggalkan rating",
"mint.rating.unrated.other": "{n} tidak meninggalkan rating", "mint.rating.unrated.other": "{n} tidak meninggalkan rating",
"mint.activity.label": "Aktivitas", "mint.activity.label": "Aktivitas",
@@ -138,14 +144,14 @@
"mint.limits.min": "min", "mint.limits.min": "min",
"mint.limits.max": "max", "mint.limits.max": "max",
"mint.limits.mintRow": "mint", "mint.limits.mintRow": "mint",
"mint.limits.meltRow": "meleleh", "mint.limits.meltRow": "melt",
"mint.limits.disabled": "dinonaktifkan", "mint.limits.disabled": "dinonaktifkan",
"mint.limits.range": "{min} ke {max} {unit}", "mint.limits.range": "{min} ke {max} {unit}",
"mint.limits.upTo": "hingga {max} {unit}", "mint.limits.upTo": "hingga {max} {unit}",
"mint.limits.from": "dari {min} {unit}", "mint.limits.from": "dari {min} {unit}",
"mint.software.label": "Perangkat lunak Mint", "mint.software.label": "Perangkat lunak mint",
"mint.software.unknown": "tidak diketahui", "mint.software.unknown": "tidak diketahui",
"mint.checked": "pemeriksaan {when}", "mint.checked": "diperiksa {when}",
"mint.lastSeen": "terakhir terlihat {when}", "mint.lastSeen": "terakhir terlihat {when}",
"mint.neverReached": "tidak pernah dicapai", "mint.neverReached": "tidak pernah dicapai",
"mint.uptime.checks": "{ok} dari pemeriksaan {total} terakhir yang dapat dicapai", "mint.uptime.checks": "{ok} dari pemeriksaan {total} terakhir yang dapat dicapai",
@@ -154,11 +160,11 @@
"mint.operator.title": "Dari operator", "mint.operator.title": "Dari operator",
"mint.operator.motd": "Pesan hari ini", "mint.operator.motd": "Pesan hari ini",
"mint.operator.beta": "Mint ini menggambarkan dirinya sebagai percobaan atau dalam beta. Baca kata-kata operator sendiri di bawah ini sebelum menyimpan apa pun yang Anda tidak dapat kehilangan.", "mint.operator.beta": "Mint ini menggambarkan dirinya sebagai percobaan atau dalam beta. Baca kata-kata operator sendiri di bawah ini sebelum menyimpan apa pun yang Anda tidak dapat kehilangan.",
"mint.nuts.title": "UTs Yang Didukung", "mint.nuts.title": "NUTs yang didukung",
"mint.nuts.noneOffline": "Tidak ada daftar NUT cached untuk mint ini, itu pergi offline sebelum salah satu direkam.", "mint.nuts.noneOffline": "Tidak ada daftar NUT cached untuk mint ini, itu pergi offline sebelum salah satu direkam.",
"mint.nuts.noneYet": "Tidak ada daftar NUT cached untuk mint ini belum.", "mint.nuts.noneYet": "Belum ada daftar NUT yang di-cache untuk mint ini.",
"mint.nuts.supported": "{n} dari {total} diketahui UTs didukung", "mint.nuts.supported": "{n} dari {total} NUTs yang dikenal didukung",
"mint.nuts.whatAre": "Apa itu NUTS?", "mint.nuts.whatAre": "apa itu NUTs?",
"mint.nutRow.disabled": "dinonaktifkan", "mint.nutRow.disabled": "dinonaktifkan",
"mint.nutRow.notSupported": "tidak didukung", "mint.nutRow.notSupported": "tidak didukung",
"mint.details.title": "Rincian mint", "mint.details.title": "Rincian mint",
@@ -197,8 +203,8 @@
"mint.warnings.meltDisabled.meta.online": "withdrawal saat ini tidak aktif", "mint.warnings.meltDisabled.meta.online": "withdrawal saat ini tidak aktif",
"mint.warnings.meltDisabled.meta.offline": "withdrawals dimatikan ketika terakhir terlihat", "mint.warnings.meltDisabled.meta.offline": "withdrawals dimatikan ketika terakhir terlihat",
"mint.warnings.frozen.lead": "Mint beku.", "mint.warnings.frozen.lead": "Mint beku.",
"mint.warnings.frozen.body.online": "Pemotongan dan pelelehan keduanya dinonaktifkan. Tak ada satelit yang bisa masuk atau keluar melalui Lightning.", "mint.warnings.frozen.body.online": "Minting dan melting keduanya dinonaktifkan. Tidak ada sats yang bisa masuk atau keluar lewat Lightning.",
"mint.warnings.frozen.body.offline": "Pemotongan dan pelelehan keduanya dinonaktifkan. Tak ada satelit yang bisa masuk atau keluar melalui Lightning.", "mint.warnings.frozen.body.offline": "Minting dan melting keduanya dinonaktifkan. Tidak ada sats yang bisa masuk atau keluar lewat Lightning.",
"mint.warnings.frozen.meta.online": "Saat ini membeku, tidak ada yang bergerak masuk atau keluar", "mint.warnings.frozen.meta.online": "Saat ini membeku, tidak ada yang bergerak masuk atau keluar",
"mint.warnings.frozen.meta.offline": "Membeku ketika terakhir terlihat", "mint.warnings.frozen.meta.offline": "Membeku ketika terakhir terlihat",
"mint.warnings.offlineLong.lead": "Offline untuk {days}, sejak {date}.", "mint.warnings.offlineLong.lead": "Offline untuk {days}, sejak {date}.",
@@ -219,11 +225,11 @@
"nut.1": "Pertukaran kunci publik Mint", "nut.1": "Pertukaran kunci publik Mint",
"nut.2": "Tombol dan biaya", "nut.2": "Tombol dan biaya",
"nut.3": "Bertukar token", "nut.3": "Bertukar token",
"nut.4": "Penerbitan melalui Lightning", "nut.4": "Minting lewat Lightning",
"nut.5": "Penarikan melalui Lightning", "nut.5": "Penarikan melalui Lightning",
"nut.6": "Informasi Mint", "nut.6": "Informasi Mint",
"nut.7": "Cek keadaan Token", "nut.7": "Cek keadaan Token",
"nut.8": "Biaya Petir dibayar terlalu mahal", "nut.8": "Biaya Lightning yang kelebihan bayar",
"nut.9": "Kembalikan tanda tangan", "nut.9": "Kembalikan tanda tangan",
"nut.10": "Kondisi pemborosan", "nut.10": "Kondisi pemborosan",
"nut.11": "Bayar ke kunci publik", "nut.11": "Bayar ke kunci publik",
@@ -261,6 +267,9 @@
"reviews.empty": "Belum ada ulasan. Jadilah orang pertama yang menulis.", "reviews.empty": "Belum ada ulasan. Jadilah orang pertama yang menulis.",
"reviews.emptyWritten": "Belum ada ulasan tertulis di sini. Jadilah orang pertama yang menulis.", "reviews.emptyWritten": "Belum ada ulasan tertulis di sini. Jadilah orang pertama yang menulis.",
"reviews.emptyFilter": "Tidak ada ulasan yang cocok dengan filter ini.", "reviews.emptyFilter": "Tidak ada ulasan yang cocok dengan filter ini.",
"reviews.rate.cashu": "Beri rating mint ini",
"reviews.rate.fedimint": "Beri rating federasi ini",
"reviews.rate.lnurl": "Beri rating mint ini",
"reviews.noscript": "Ulasan ditandatangani pada Nostr dan muat dalam peramban Anda, jadi daftar ini membutuhkan JavaScript. Rating dan jumlah di atas berasal dari situs itu sendiri.", "reviews.noscript": "Ulasan ditandatangani pada Nostr dan muat dalam peramban Anda, jadi daftar ini membutuhkan JavaScript. Rating dan jumlah di atas berasal dari situs itu sendiri.",
"reviews.relayError": "Tidak bisa mencapai relay. Ulasan mungkin tidak lengkap.", "reviews.relayError": "Tidak bisa mencapai relay. Ulasan mungkin tidak lengkap.",
"reviews.srRating": "{rating} dari 5", "reviews.srRating": "{rating} dari 5",
@@ -273,7 +282,7 @@
"reviews.anonNote": "Ulasan pertama dari npub ini, tidak ada aktivitas lain ditemukan", "reviews.anonNote": "Ulasan pertama dari npub ini, tidak ada aktivitas lain ditemukan",
"reviews.published": "Diterbitkan.", "reviews.published": "Diterbitkan.",
"reviews.publishedPartial": "Diterbitkan ke {accepted} dari {total} relay.", "reviews.publishedPartial": "Diterbitkan ke {accepted} dari {total} relay.",
"reviews.propagating": "Sedang disebarkan; mungkin perlu waktu untuk muncul di tempat lain.", "reviews.propagating": "Mempublikasikan ke relay Nostr",
"reviews.summary.line.one": "Rating {count} tanpa komentar: {breakdown}", "reviews.summary.line.one": "Rating {count} tanpa komentar: {breakdown}",
"reviews.summary.line.other": "Rating {count} tanpa komentar: {breakdown}", "reviews.summary.line.other": "Rating {count} tanpa komentar: {breakdown}",
"reviews.summary.lineDay.one": "{count} rating tanpa komentar di {when}: {breakdown}", "reviews.summary.lineDay.one": "{count} rating tanpa komentar di {when}: {breakdown}",
@@ -287,7 +296,12 @@
"reviews.breakdown.1": "{n} satu bintang", "reviews.breakdown.1": "{n} satu bintang",
"reviews.breakdown.none": "{n} tanpa rating", "reviews.breakdown.none": "{n} tanpa rating",
"reviews.dialog.title": "Ulasan {name}", "reviews.dialog.title": "Ulasan {name}",
"reviews.dialog.note": "Ditandatangani dengan kunci Nostr Anda dan diterbitkan ke relay publik. Tidak dapat disunting atau dihapus.", "reviews.dialog.lede.cashu": "Bagikan pengalaman Anda dengan mint ini.",
"reviews.dialog.lede.fedimint": "Bagikan pengalaman Anda dengan federasi ini.",
"reviews.dialog.lede.lnurl": "Bagikan pengalaman Anda dengan mint ini.",
"reviews.dialog.permanent": "Diterbitkan secara permanen ke relay Nostr publik.",
"reviews.dialog.gate.title": "Masuk untuk menulis ulasan",
"reviews.dialog.gate.lead": "Ulasan ditandatangani dengan kunci Nostr Anda. Kunci tetap di penandatangan Anda, situs ini tidak pernah melihatnya.",
"reviews.dialog.ratingLegend": "Rating Anda", "reviews.dialog.ratingLegend": "Rating Anda",
"reviews.dialog.starsSr.one": "Bintang {n}", "reviews.dialog.starsSr.one": "Bintang {n}",
"reviews.dialog.starsSr.other": "Bintang {n}", "reviews.dialog.starsSr.other": "Bintang {n}",
@@ -308,7 +322,6 @@
"reviews.dialog.waitingSigner": "Menunggu penandamu", "reviews.dialog.waitingSigner": "Menunggu penandamu",
"reviews.dialog.confirmClose": "Tutup ulasan ini? Draf Anda disimpan sampai Anda meninggalkan situs.", "reviews.dialog.confirmClose": "Tutup ulasan ini? Draf Anda disimpan sampai Anda meninggalkan situs.",
"reviews.dialog.why.login": "Log masuk untuk mempublikasikan", "reviews.dialog.why.login": "Log masuk untuk mempublikasikan",
"reviews.dialog.why.rating": "Pilih sebuah rating",
"reviews.dialog.why.words": "Tulis beberapa kata", "reviews.dialog.why.words": "Tulis beberapa kata",
"reviews.dialog.why.more.one": "{n} lebih banyak karakter", "reviews.dialog.why.more.one": "{n} lebih banyak karakter",
"reviews.dialog.why.more.other": "{n} lebih banyak karakter", "reviews.dialog.why.more.other": "{n} lebih banyak karakter",
@@ -324,8 +337,8 @@
"reviews.validate.rating": "Pilih peringkat 1 sampai 5 bintang.", "reviews.validate.rating": "Pilih peringkat 1 sampai 5 bintang.",
"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 · Cashumints.space",
"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",
@@ -369,6 +382,10 @@
"login.status.waitingSigner": "Menunggu penandamu", "login.status.waitingSigner": "Menunggu penandamu",
"login.status.connecting": "Menyambung ke penandatangan Anda", "login.status.connecting": "Menyambung ke penandatangan Anda",
"login.status.connected": "Terhubung", "login.status.connected": "Terhubung",
"login.toast.loggedIn": "Berhasil masuk",
"login.toast.fetchingProfile": "Mengambil profil Nostr Anda dari relay…",
"login.toast.profile": "Profil: {name}",
"login.toast.noProfile": "Tidak ada profil yang ditemukan di relay",
"login.method.extension": "Ekstensi", "login.method.extension": "Ekstensi",
"login.method.remote": "Jauh", "login.method.remote": "Jauh",
"login.method.primal": "Primal", "login.method.primal": "Primal",
@@ -389,22 +406,29 @@
"signer.error.timeout": "Penandamu tidak menjawab tepat waktu. Buka, periksa masih terhubung, lalu coba lagi.", "signer.error.timeout": "Penandamu tidak menjawab tepat waktu. Buka, periksa masih terhubung, lalu coba lagi.",
"signer.error.cancelled": "Koneksi dibatalkan.", "signer.error.cancelled": "Koneksi dibatalkan.",
"signer.error.declined": "Permintaan tanda tangan ditolak", "signer.error.declined": "Permintaan tanda tangan ditolak",
"notfound.title": "Halaman tak ditemukan", "notfound.title": "Halaman tidak ditemukan · Cashumints.space",
"notfound.description": "Halaman itu tidak ada di angkasa Cashumints.space.", "notfound.description": "Halaman itu tidak ada di angkasa Cashumints.space.",
"notfound.heading": "Halaman ini tidak ada", "notfound.heading": "Halaman ini tidak ada",
"notfound.lede": "Ini mungkin telah pindah, atau mint mungkin belum diindeks. Cari sebuah mint dengan nama atau domain, atau paste sebuah URL mint.", "notfound.lede": "Ini mungkin telah pindah, atau mint mungkin belum diindeks. Cari sebuah mint dengan nama atau domain, atau paste sebuah URL mint.",
"notfound.browse": "Ramban semua permen", "notfound.browse": "Jelajahi semua mint",
"notfound.home": "Pulanglah.", "notfound.home": "Pulanglah.",
"notfound.loading": "Memuat {host}", "notfound.loading": "Memuat {host}",
"notfound.fb.rating": "Rating", "notfound.fb.rating": "Rating",
"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.",
"wallets.title": "Dompet Cashu", "notfound.resolve.checking": "Kunjungan pertama: mint ini sedang diperiksa.",
"wallets.description": "Dompet Cashu yang bekerja dengan permen yang ada di sini, di ponsel, web, browser dan baris perintah, dengan keahlian masing-masing.", "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 · Cashumints.space",
"wallets.description": "Dompet Cashu yang bekerja dengan mint yang terdaftar di sini, di ponsel, web, peramban, dan baris perintah, beserta keunggulan masing-masing.",
"wallets.heading": "Dompet", "wallets.heading": "Dompet",
"wallets.lede": "Kau butuh dompet untuk menyimpan uang dari salah satu permen ini. Kebanyakan dompet berbicara dengan beberapa permen sekaligus, sehingga Anda tidak terikat dengan yang pertama Anda mencoba.", "wallets.lede": "Kamu butuh dompet untuk menyimpan ecash dari salah satu mint ini. Kebanyakan dompet bicara dengan beberapa mint sekaligus, jadi kamu tidak terikat pada yang pertama kamu coba.",
"wallets.note": "Daftar ini tidak melelahkan dan bukan dukungan. Proyek Cashu menyimpan indeks yang lebih luas di {link}. Dompet memegang kunci, mint memegang uang: membaca ulasan mint sebelum Anda memilih di mana Anda sats hidup.", "wallets.note": "Daftar ini tidak melelahkan dan bukan dukungan. Proyek Cashu menyimpan indeks yang lebih luas di {link}. Dompet memegang kunci, mint memegang uang: membaca ulasan mint sebelum Anda memilih di mana Anda sats hidup.",
"wallets.platform.android": "Android", "wallets.platform.android": "Android",
"wallets.platform.ios": "iOS", "wallets.platform.ios": "iOS",
@@ -413,26 +437,26 @@
"wallets.platform.cli": "CLI", "wallets.platform.cli": "CLI",
"wallets.platform.python": "Python", "wallets.platform.python": "Python",
"wallets.item.minibits": "Dompet ecash Mobile dengan mint sendiri, Lightning alamat dukungan dan Nostr kontak.", "wallets.item.minibits": "Dompet ecash Mobile dengan mint sendiri, Lightning alamat dukungan dan Nostr kontak.",
"wallets.item.nutstash": "Dompet peramban yang menangani beberapa permen sekaligus dan impor dan ekspor token mentah.", "wallets.item.nutstash": "Dompet peramban yang menangani beberapa mint sekaligus serta mengimpor dan mengekspor token mentah.",
"wallets.item.enuts": "Dompet mobil difokuskan pada saldo multimint dan mengirim e cash atas Nostr.", "wallets.item.enuts": "Dompet mobil difokuskan pada saldo multimint dan mengirim e cash atas Nostr.",
"wallets.item.cashume": "Referensi dompet web dari proyek Cashu. Berjalan sepenuhnya dalam browser, dipasang sebagai PWA.", "wallets.item.cashume": "Referensi dompet web dari proyek Cashu. Berjalan sepenuhnya dalam browser, dipasang sebagai PWA.",
"wallets.item.boardwalk": "Dompet web yang dibangun di sekitar alamat Lightning dan mengirim e-cash melalui link.", "wallets.item.boardwalk": "Dompet web yang dibangun di sekitar alamat Lightning dan mengirim e-cash melalui link.",
"wallets.item.coinos": "Dompet petir dan titik penjualan yang juga menjalankan sendiri mint Cashu.", "wallets.item.coinos": "Dompet petir dan titik penjualan yang juga menjalankan sendiri mint Cashu.",
"wallets.item.oxchat": "Utusan Nostr dengan dompet emcash dibangun di, untuk mengirim satelit di dalam obrolan.", "wallets.item.oxchat": "Messenger Nostr dengan dompet ecash bawaan, untuk mengirim sats di dalam obrolan.",
"wallets.item.nutshell": "Referensi implementasi. Berjalan sebagai dompet di baris perintah dan sebagai perangkat lunak mint.", "wallets.item.nutshell": "Referensi implementasi. Berjalan sebagai dompet di baris perintah dan sebagai perangkat lunak mint.",
"about.title": "Tentang Cashumints.space", "about.title": "Tentang Cashumints.space",
"about.description": "Apa Cashu adalah, apa mint benar-benar memegang, apa NIP-87 adalah, dan bagaimana situs ini index permen dan membaca ulasan dari Nostr relays.", "about.description": "Apa itu Cashu, apa yang sebenarnya dipegang sebuah mint, apa itu NIP-87, dan bagaimana situs ini mengindeks mint dan membaca ulasan dari relay Nostr.",
"about.heading": "Tentang", "about.heading": "Tentang",
"about.lede": "Cashumints.space daftar setiap mint Cashu kita dapat menemukan pada jaringan Nostr dan menunjukkan apa yang orang katakan tentang mereka. Ini dibuat oleh Azzamo.", "about.lede": "Cashumints.space daftar setiap mint Cashu kita dapat menemukan pada jaringan Nostr dan menunjukkan apa yang orang katakan tentang mereka. Ini dibuat oleh Azzamo.",
"about.cashu.title": "Apa itu Cashu?", "about.cashu.title": "Apa itu Cashu?",
"about.cashu.p1": "Cashu adalah protokol untuk Bitcoin. Sebuah isu mint buta token didukung oleh sats itu memegang, dan tanda-tanda bergerak antara orang tanpa pembelajaran mint yang dikirim apa kepada siapa. privasi itu nyata dan itu adalah titik: token bukan rekening, dan mint tidak dapat menghubungkan token itu ditandatangani ke token yang datang kembali.", "about.cashu.p1": "Cashu adalah protokol untuk Bitcoin. Sebuah isu mint buta token didukung oleh sats itu memegang, dan tanda-tanda bergerak antara orang tanpa pembelajaran mint yang dikirim apa kepada siapa. privasi itu nyata dan itu adalah titik: token bukan rekening, dan mint tidak dapat menghubungkan token itu ditandatangani ke token yang datang kembali.",
"about.cashu.p2": "Anda dapat membaca protokol dan spesifikasinya di {link}. Spesifikasi sendiri disebut NUTS, yang mana angka NUT pada setiap halaman mint berasal.", "about.cashu.p2": "Anda dapat membaca protokol dan spesifikasinya di {link}. Spesifikasi sendiri disebut NUTS, yang mana angka NUT pada setiap halaman mint berasal.",
"about.mints.title": "Apa mint adalah, dan tradeoff", "about.mints.title": "Apa mint adalah, dan tradeoff",
"about.mints.p1": "Mint adalah kustodian. Saat kau menyerahkan sats pada siapa pun yang menjalankan server itu, dan kau mempercayai mereka untuk menghormati token ketika kau lelehkan mereka. Tidak ada bukti penipuan, tidak ada escrow dan tidak ada jalan lain. Operator mint dapat menghilang dengan keseimbangan, dan permen memiliki.", "about.mints.p1": "Mint bersifat kustodial. Saat kamu minting ecash, kamu menyerahkan sats kepada siapa pun yang menjalankan server itu, dan kamu percaya mereka akan menghormati token itu saat kamu melt kembali. Tidak ada bukti penipuan, tidak ada escrow, dan tidak ada jalan banding. Operator mint bisa menghilang bersama saldonya, dan itu sudah pernah terjadi.",
"about.mints.p2": "Itulah mengapa ulasan ada di sini. Situs ini tidak pernah memberitahu Anda mint aman. Status pada halaman mint mengatakan {code}, bukan \"dipercaya\", dan itu hanya berarti server menjawab ketika kita terakhir bertanya. Perlakukan keseimbangan ecash seperti uang tunai dalam saku mantel: baik untuk pengeluaran, salah untuk tabungan.", "about.mints.p2": "Itulah mengapa ulasan ada di sini. Situs ini tidak pernah memberitahu Anda mint aman. Status pada halaman mint mengatakan {code}, bukan \"dipercaya\", dan itu hanya berarti server menjawab ketika kita terakhir bertanya. Perlakukan keseimbangan ecash seperti uang tunai dalam saku mantel: baik untuk pengeluaran, salah untuk tabungan.",
"about.nip87.title": "Apa itu NIP-87?", "about.nip87.title": "Apa itu NIP-87?",
"about.nip87.p1": "NIP-87 adalah konvensi Nostr untuk mengumumkan dan merekomendasikan cash mint. Dua peristiwa jenis materi di sini:", "about.nip87.p1": "NIP-87 adalah konvensi Nostr untuk mengumumkan dan merekomendasikan cash mint. Dua peristiwa jenis materi di sini:",
"about.nip87.li1": "{code} mengumumkan sebuah mint. URL mint dalam tag {tag}. Ini adalah bagaimana situs menemukan permen itu belum pernah terlihat.", "about.nip87.li1": "{code} mengumumkan sebuah mint. URL mint ada di tag {tag}. Beginilah situs ini menemukan mint yang belum pernah dilihatnya.",
"about.nip87.li2": "{code} adalah ulasan. Ini membawa peringkat dan teks tinjauan, dan poin pada mint itu adalah tentang dengan tag {utag} (URL mint) dan tag {dtag} (mint sendiri kunci publik).", "about.nip87.li2": "{code} adalah ulasan. Ini membawa peringkat dan teks tinjauan, dan poin pada mint itu adalah tentang dengan tag {utag} (URL mint) dan tag {dtag} (mint sendiri kunci publik).",
"about.nip87.p2": "Setiap ulasan ditandatangani oleh kunci Nostr penulisnya. Kami tidak pernah menyebut ulasan \"diverifikasi\", karena tidak ada yang diverifikasi: itu ditandatangani, yang merupakan klaim yang berbeda dan lebih berguna. Ini berarti Anda dapat memeriksa siapa yang menulis itu dan bahwa itu tidak berubah. Ini tidak berarti penulis adalah jujur atau pernah digunakan mint.", "about.nip87.p2": "Setiap ulasan ditandatangani oleh kunci Nostr penulisnya. Kami tidak pernah menyebut ulasan \"diverifikasi\", karena tidak ada yang diverifikasi: itu ditandatangani, yang merupakan klaim yang berbeda dan lebih berguna. Ini berarti Anda dapat memeriksa siapa yang menulis itu dan bahwa itu tidak berubah. Ini tidak berarti penulis adalah jujur atau pernah digunakan mint.",
"about.how.title": "Bagaimana situs ini bekerja", "about.how.title": "Bagaimana situs ini bekerja",
@@ -447,12 +471,12 @@
"about.relays.p1": "Ini adalah relay pengindexer dan peramban Anda berdua menggunakan:", "about.relays.p1": "Ini adalah relay pengindexer dan peramban Anda berdua menggunakan:",
"about.relays.p2": "Tunjukkan setiap klien Nostr pada relay yang sama dan Anda akan melihat peristiwa yang sama. Itu adalah seluruh janji: tidak ada di sini hanya benar karena kita mengatakan begitu.", "about.relays.p2": "Tunjukkan setiap klien Nostr pada relay yang sama dan Anda akan melihat peristiwa yang sama. Itu adalah seluruh janji: tidak ada di sini hanya benar karena kita mengatakan begitu.",
"about.nuts.title": "Apa itu NUTS?", "about.nuts.title": "Apa itu NUTS?",
"about.nuts.p1": "NUTs adalah nomor khusus Cashu, seperti BIPs untuk Bitcoin. Sebuah iklan mint yang mana yang diimplementasikan, dan yang memberitahu Anda apa yang sebenarnya dapat dilakukan: NUT-04 dan NUT-05 adalah minting dan mencair atas Petir (tanpa mereka mint tidak banyak digunakan), NUT-07 memungkinkan cek dompet apakah token sudah habis, NUT-17 memberikan dompet pembaruan langsung atas WebSockets bukan polling, dan NUT-20 memungkinkan sebuah dompet membuktikan kutipan mint benar-benar dikeluarkan untuk itu. Daftar lengkapnya ada di {link}.", "about.nuts.p1": "NUTs adalah spesifikasi Cashu yang bernomor, seperti BIP untuk Bitcoin. Sebuah mint mengumumkan mana saja yang diterapkannya, dan itu memberitahumu apa yang sebenarnya bisa dilakukannya: NUT-04 dan NUT-05 adalah minting dan melting lewat Lightning (tanpa keduanya sebuah mint tidak banyak gunanya), NUT-07 memungkinkan dompet memeriksa apakah sebuah token sudah terpakai, NUT-17 memberi dompet pembaruan langsung lewat WebSockets alih-alih polling, dan NUT-20 memungkinkan dompet membuktikan bahwa sebuah mint quote memang diterbitkan untuknya. Daftar lengkapnya ada di {link}.",
"about.numbers.title": "Sekarang", "about.numbers.title": "Sekarang",
"about.numbers.p1": "{mints} mint diindeks, {online} dari mereka menjawab, {reviews} ulasan dihitung dari relay di atas.", "about.numbers.p1": "{mints} mint diindeks, {online} dari mereka menjawab, {reviews} ulasan dihitung dari relay di atas.",
"about.source.title": "Sumber dan kredit", "about.source.title": "Sumber dan kredit",
"about.source.p1": "Situs ini open source di {github}. Dibangun dan dijalankan oleh {azzamo}. Jika halaman mint memiliki sesuatu yang salah di atasnya, memperbaikinya di sumber: metadata mint berasal dari mint sendiri {code}, dan ulasan berasal dari Nostr.", "about.source.p1": "Situs ini open source di {github}. Dibangun dan dijalankan oleh {azzamo}. Jika halaman mint memiliki sesuatu yang salah di atasnya, memperbaikinya di sumber: metadata mint berasal dari mint sendiri {code}, dan ulasan berasal dari Nostr.",
"terms.title": "Hukuman penggunaan", "terms.title": "Ketentuan penggunaan · Cashumints.space",
"terms.description": "Hukuman penggunaan bagi Cashumints.space: apa situs ini, apa yang tidak dilakukan, dan siapa yang bertanggung jawab untuk apa. Tidak ada rekening, tidak ada jaminan, tidak ada afiliasi dengan mint.", "terms.description": "Hukuman penggunaan bagi Cashumints.space: apa situs ini, apa yang tidak dilakukan, dan siapa yang bertanggung jawab untuk apa. Tidak ada rekening, tidak ada jaminan, tidak ada afiliasi dengan mint.",
"terms.heading": "Kalimat penggunaan", "terms.heading": "Kalimat penggunaan",
"terms.lede": "Istilah sederhana untuk situs yang hanya mengindeks data publik. Terakhir diperbarui {date}.", "terms.lede": "Istilah sederhana untuk situs yang hanya mengindeks data publik. Terakhir diperbarui {date}.",
@@ -479,7 +503,7 @@
"terms.changes.p1": "Istilah ini bisa berubah. Tanggal di atas mengatakan ketika mereka terakhir melakukannya. Situs ini open source, jadi setiap perubahan pada halaman ini ada dalam sejarah komit.", "terms.changes.p1": "Istilah ini bisa berubah. Tanggal di atas mengatakan ketika mereka terakhir melakukannya. Situs ini open source, jadi setiap perubahan pada halaman ini ada dalam sejarah komit.",
"terms.contact.title": "Kontak", "terms.contact.title": "Kontak",
"terms.contact.p1": "Masalah dan permintaan pengambilalihan paling baik diajukan pada {github}. Situs ini dijalankan oleh {azzamo}.", "terms.contact.p1": "Masalah dan permintaan pengambilalihan paling baik diajukan pada {github}. Situs ini dijalankan oleh {azzamo}.",
"privacy.title": "Privasi", "privacy.title": "Privasi · Cashumints.space",
"privacy.description": "Privasi pada Cashumints.space: tidak ada akun, tidak ada cookie pelacakan, tidak ada analitik. localStorage digunakan untuk caching profile dan tidak ada yang dikirim di mana saja. Ulasan ditandatangani dalam browser Anda sendiri.", "privacy.description": "Privasi pada Cashumints.space: tidak ada akun, tidak ada cookie pelacakan, tidak ada analitik. localStorage digunakan untuk caching profile dan tidak ada yang dikirim di mana saja. Ulasan ditandatangani dalam browser Anda sendiri.",
"privacy.heading": "Privasi", "privacy.heading": "Privasi",
"privacy.lede": "Kami mengumpulkan sesedikit situs web bisa. Terakhir diperbarui {date}.", "privacy.lede": "Kami mengumpulkan sesedikit situs web bisa. Terakhir diperbarui {date}.",
@@ -514,15 +538,15 @@
"privacy.permanent.p2": "Jika Anda ingin meninjau mint tanpa yang terikat dengan identitas utama Anda, menggunakan kunci Nostr terpisah. Perhatikan bahwa kunci tanpa aktivitas lain ditampilkan seperti pada ulasan kasar, karena kunci baru yang menulis tinjauan satu bintang adalah pembaca pola yang layak untuk dilihat.", "privacy.permanent.p2": "Jika Anda ingin meninjau mint tanpa yang terikat dengan identitas utama Anda, menggunakan kunci Nostr terpisah. Perhatikan bahwa kunci tanpa aktivitas lain ditampilkan seperti pada ulasan kasar, karena kunci baru yang menulis tinjauan satu bintang adalah pembaca pola yang layak untuk dilihat.",
"privacy.contact.title": "Pertanyaan", "privacy.contact.title": "Pertanyaan",
"privacy.contact.p1": "Situs ini terbuka: jika Anda ingin memeriksa salah satu di atas, baca kode di {github}. Pertanyaan dan koreksi dapat pergi dalam masalah di sana.", "privacy.contact.p1": "Situs ini terbuka: jika Anda ingin memeriksa salah satu di atas, baca kode di {github}. Pertanyaan dan koreksi dapat pergi dalam masalah di sana.",
"disclaimer.title": "Discopler", "disclaimer.title": "Penafian · Cashumints.space",
"disclaimer.description": "Apa yang harus diketahui sebelum Anda menggunakan mint Cashu: permen kustodian, mint dapat menghilang dengan uang Anda, ulasan adalah pendapat dan online berarti terjangkau, tidak dapat dipercaya.", "disclaimer.description": "Yang perlu diketahui sebelum kamu memakai mint Cashu: mint bersifat kustodial, sebuah mint bisa menghilang bersama uangmu, ulasan adalah pendapat, dan online berarti bisa dihubungi, bukan bisa dipercaya.",
"disclaimer.heading": "Discoverer", "disclaimer.heading": "Discoverer",
"disclaimer.lede": "Baca ini sebelum Anda menempatkan uang dalam mint. Terakhir diperbarui {date}.", "disclaimer.lede": "Baca ini sebelum Anda menempatkan uang dalam mint. Terakhir diperbarui {date}.",
"disclaimer.callout": "Sebuah mint Cashu memegang uang Anda untuk Anda. Jika itu menghilang, ecash Anda hilang, dan tidak ada orang untuk menarik. Simpan hanya apa yang Anda mampu untuk kehilangan.", "disclaimer.callout": "Sebuah mint Cashu memegang uang Anda untuk Anda. Jika itu menghilang, ecash Anda hilang, dan tidak ada orang untuk menarik. Simpan hanya apa yang Anda mampu untuk kehilangan.",
"disclaimer.custodial.title": "Mints adalah kustodian", "disclaimer.custodial.title": "Mints adalah kustodian",
"disclaimer.custodial.p1": "Ketika Anda mint ecash, Anda tangan bitcoin ke operator mint dan mendapatkan token kembali. Token itu adalah klaim pada operator itu. Mereka bukan bitcoin yang Anda pegang, dan operator adalah orang yang memutuskan apakah untuk menghormati mereka.", "disclaimer.custodial.p1": "Ketika Anda mint ecash, Anda tangan bitcoin ke operator mint dan mendapatkan token kembali. Token itu adalah klaim pada operator itu. Mereka bukan bitcoin yang Anda pegang, dan operator adalah orang yang memutuskan apakah untuk menghormati mereka.",
"disclaimer.custodial.p2": "Itu berarti setiap mint membawa risiko yang sama: operator dapat lenyap, dapat dipaksa untuk berhenti, dapat kehilangan dana melalui bug atau hack, atau hanya dapat mematikan server. Tidak ada asuransi deposit, tidak ada chargeback, tidak ada garis dukungan dan tidak ada jalur hukum yang layak diandalkan. Ini benar dari peringkat tertinggi mint di situs ini.", "disclaimer.custodial.p2": "Itu berarti setiap mint membawa risiko yang sama: operator dapat lenyap, dapat dipaksa untuk berhenti, dapat kehilangan dana melalui bug atau hack, atau hanya dapat mematikan server. Tidak ada asuransi deposit, tidak ada chargeback, tidak ada garis dukungan dan tidak ada jalur hukum yang layak diandalkan. Ini benar dari peringkat tertinggi mint di situs ini.",
"disclaimer.custodial.p3": "Cashu dirancang untuk sedikit bergerak cepat. Gunakan dengan cara itu: tahan apa yang akan Anda keluarkan, bukan tabungan Anda, dan menyebar di permen bukannya parkir segala sesuatu dalam satu.", "disclaimer.custodial.p3": "Cashu dirancang untuk jumlah kecil yang bergerak cepat. Pakai seperti itu: simpan yang akan segera kamu belanjakan, bukan tabunganmu, dan sebarkan ke beberapa mint alih-alih menaruh semuanya di satu tempat.",
"disclaimer.reviews.title": "Ulasan adalah pendapat, tidak diverifikasi klaim", "disclaimer.reviews.title": "Ulasan adalah pendapat, tidak diverifikasi klaim",
"disclaimer.reviews.p1": "Setiap ulasan di sini ditulis oleh seseorang di Nostr dan ditandatangani dengan kunci mereka. Ditandatangani berarti kita dapat memberitahu Anda kunci mana yang menulisnya. Ini tidak berarti ulasan benar, bahwa penulis menggunakan mint, atau bahwa penulis bukan operator, pesaing, atau satu orang dengan sepuluh kunci.", "disclaimer.reviews.p1": "Setiap ulasan di sini ditulis oleh seseorang di Nostr dan ditandatangani dengan kunci mereka. Ditandatangani berarti kita dapat memberitahu Anda kunci mana yang menulisnya. Ini tidak berarti ulasan benar, bahwa penulis menggunakan mint, atau bahwa penulis bukan operator, pesaing, atau satu orang dengan sepuluh kunci.",
"disclaimer.reviews.p2": "Kami tidak memverifikasi peninjau, dan kami tidak mengedit atau menghapus ulasan kita tidak setuju dengan. Baca beberapa, lihat apa yang sebenarnya mereka katakan, dan menimbang mint dengan tiga ulasan berbeda dari satu dengan tiga ratus.", "disclaimer.reviews.p2": "Kami tidak memverifikasi peninjau, dan kami tidak mengedit atau menghapus ulasan kita tidak setuju dengan. Baca beberapa, lihat apa yang sebenarnya mereka katakan, dan menimbang mint dengan tiga ulasan berbeda dari satu dengan tiga ratus.",
@@ -530,14 +554,14 @@
"disclaimer.status.p1": "Kami memeriksa setiap mint teratur dan menunjukkan apakah itu menjawab. Itu semua berarti status hijau: server menjawab. Ini mengatakan apa-apa tentang apakah operator adalah pelarut, jujur, atau masih berniat untuk menghormati token Anda besok.", "disclaimer.status.p1": "Kami memeriksa setiap mint teratur dan menunjukkan apakah itu menjawab. Itu semua berarti status hijau: server menjawab. Ini mengatakan apa-apa tentang apakah operator adalah pelarut, jujur, atau masih berniat untuk menghormati token Anda besok.",
"disclaimer.status.p2": "Mint A bisa online sampai saat ini tidak. Uptime sejarah, didukung NUTS dan versi perangkat lunak memberitahu Anda sesuatu tentang bagaimana mint dijalankan, tapi tidak satupun dari mereka adalah janji tentang uang Anda.", "disclaimer.status.p2": "Mint A bisa online sampai saat ini tidak. Uptime sejarah, didukung NUTS dan versi perangkat lunak memberitahu Anda sesuatu tentang bagaimana mint dijalankan, tapi tidak satupun dari mereka adalah janji tentang uang Anda.",
"disclaimer.ranking.title": "Bagaimana peringkat bekerja, dan apa itu tidak berarti", "disclaimer.ranking.title": "Bagaimana peringkat bekerja, dan apa itu tidak berarti",
"disclaimer.ranking.p1": "Mints diperintahkan oleh algoritma melalui data tinjauan publik: rating, jumlah ulasan dan aktivitas baru-baru ini, dengan permen online di depan dari yang offline secara default. Rinciannya ada di kode open source.", "disclaimer.ranking.p1": "Mint diurutkan oleh sebuah algoritma atas data ulasan publik: rating, jumlah ulasan, dan aktivitas terbaru, dengan mint yang online di depan yang offline secara bawaan. Rinciannya ada di kode sumber terbuka.",
"disclaimer.ranking.p2": "Tidak ada yang membayar untuk penempatan. Tidak ada slot yang disponsori, tidak ada daftar promosi dan tidak ada pengaturan dengan setiap mint di situs ini. Peringkat tinggi berarti algoritma menyukai data publik, dan tidak lebih. Ini bukan rekomendasi dan bukan rating keamanan.", "disclaimer.ranking.p2": "Tidak ada yang membayar untuk penempatan. Tidak ada slot yang disponsori, tidak ada daftar promosi dan tidak ada pengaturan dengan setiap mint di situs ini. Peringkat tinggi berarti algoritma menyukai data publik, dan tidak lebih. Ini bukan rekomendasi dan bukan rating keamanan.",
"disclaimer.notAdvice.title": "Ini bukan saran keuangan.", "disclaimer.notAdvice.title": "Ini bukan saran keuangan.",
"disclaimer.notAdvice.p1": "Tidak ada di situs ini adalah keuangan, investasi, hukum atau nasihat pajak. Kami tidak tahu situasi Anda dan kami tidak menyarankan Anda. Menggunakan mint, dan memilih yang mana, adalah keputusanmu.", "disclaimer.notAdvice.p1": "Tidak ada di situs ini adalah keuangan, investasi, hukum atau nasihat pajak. Kami tidak tahu situasi Anda dan kami tidak menyarankan Anda. Menggunakan mint, dan memilih yang mana, adalah keputusanmu.",
"disclaimer.dyor.title": "Apakah penelitian Anda sendiri", "disclaimer.dyor.title": "Apakah penelitian Anda sendiri",
"disclaimer.dyor.li1": "Cari operator. Sebuah mint dijalankan oleh seseorang dengan reputasi publik kehilangan adalah proposisi yang berbeda dari URL anonim.", "disclaimer.dyor.li1": "Cari operator. Sebuah mint dijalankan oleh seseorang dengan reputasi publik kehilangan adalah proposisi yang berbeda dari URL anonim.",
"disclaimer.dyor.li2": "Baca istilah mint sendiri dan MOTD nya. Sebuah mint yang menyebut dirinya tes atau beta berarti itu.", "disclaimer.dyor.li2": "Baca istilah mint sendiri dan MOTD nya. Sebuah mint yang menyebut dirinya tes atau beta berarti itu.",
"disclaimer.dyor.li3": "Mulai dari yang kecil. Mint sedikit, menghabiskan, meleleh kembali, dan melihat apa yang terjadi sebelum Anda percaya dengan lebih banyak.", "disclaimer.dyor.li3": "Mulai dari kecil. Mint sedikit, belanjakan, melt kembali, dan lihat apa yang terjadi sebelum kamu memercayakan lebih banyak.",
"disclaimer.dyor.li4": "Perhatikan batasnya. Maksimum rendah sering merupakan tanda dari operator yang hati-hati, dan yang tinggi bukanlah tanda dari yang aman.", "disclaimer.dyor.li4": "Perhatikan batasnya. Maksimum rendah sering merupakan tanda dari operator yang hati-hati, dan yang tinggi bukanlah tanda dari yang aman.",
"disclaimer.dyor.li5": "Dan kerjakanlah (amal yang baik) untuk dirimu, dan ketahuilah bahwa sesungguhnya tanda (Allah) itulah yang memberi (pahala) kepadanya.", "disclaimer.dyor.li5": "Dan kerjakanlah (amal yang baik) untuk dirimu, dan ketahuilah bahwa sesungguhnya tanda (Allah) itulah yang memberi (pahala) kepadanya.",
"disclaimer.site.title": "Tentang situs itu sendiri", "disclaimer.site.title": "Tentang situs itu sendiri",
@@ -546,34 +570,34 @@
"disclaimer.updated": "20 Agustus 2026", "disclaimer.updated": "20 Agustus 2026",
"login.getPrimal": "Dapatkan Primal", "login.getPrimal": "Dapatkan Primal",
"reviews.dialog.identity.extensionGone": "Ekstensi peramban Anda tidak menjawab. Masuk lagi, atau gunakan penandatangan jarak jauh.", "reviews.dialog.identity.extensionGone": "Ekstensi peramban Anda tidak menjawab. Masuk lagi, atau gunakan penandatangan jarak jauh.",
"seo.orgDescription": "Sebuah direktori dari permen Cashu, dengan status langsung dari setiap mint dan ulasan masyarakat yang ditandatangani pada Nostr.", "seo.orgDescription": "Direktori mint Cashu, dengan status langsung dari tiap mint dan ulasan komunitas yang ditandatangani di Nostr.",
"seo.ogImageAlt": "Cashumints.space: menemukan mint Cashu Anda dapat percaya.", "seo.ogImageAlt": "Cashumints.space: temukan mint Cashu yang bisa kamu percaya.",
"seo.mint.title.one": "Ulasan {name}: dinilai {rating} dari {n} ulasan | Cashumints.space", "seo.mint.title.one": "Ulasan {name}: dinilai {rating} dari {n} ulasan | Cashumints.space",
"seo.mint.title.other": "Ulasan {name}: dinilai {rating} dari {n} ulasan | Cashumints.space", "seo.mint.title.other": "Ulasan {name}: dinilai {rating} dari {n} ulasan | Cashumints.space",
"seo.mint.titleOffline.one": "{name}: offline selama {n} hari | Cashumints.space", "seo.mint.titleOffline.one": "{name}: offline selama {n} hari | Cashumints.space",
"seo.mint.titleOffline.other": "{name}: luring selama {n} hari | Cashumints.space", "seo.mint.titleOffline.other": "{name}: offline selama {n} hari | Cashumints.space",
"seo.mint.titleNoReviews": "{name}: Informasi mint Cashu dan ulasan 126; Cashumints.space", "seo.mint.titleNoReviews": "{name}: info dan ulasan mint Cashu | Cashumints.space",
"seo.fedimint.titleNoReviews": "{name}: Informasi federasi Fedimint dan ulasan 24; Cashumints.space", "seo.fedimint.titleNoReviews": "{name}: info dan ulasan federasi Fedimint | Cashumints.space",
"seo.desc.fallback": "Ulasan komunitas untuk {name} ({domain}).", "seo.desc.fallback": "Ulasan komunitas untuk {name} ({domain}).",
"seo.desc.fallbackDomain": "Ulasan masyarakat untuk mint Cashu di {domain}.", "seo.desc.fallbackDomain": "Ulasan komunitas untuk mint Cashu di {domain}.",
"seo.desc.rated.one": "Dinilai {rating}/5 dari {n} ulasan Nostr yang ditandatangani.", "seo.desc.rated.one": "Dinilai {rating}/5 dari {n} ulasan Nostr yang ditandatangani.",
"seo.desc.rated.other": "Dinilai {rating}/5 dari {n} ulasan Nostr yang ditandatangani.", "seo.desc.rated.other": "Dinilai {rating}/5 dari {n} ulasan Nostr yang ditandatangani.",
"seo.desc.noReviews": "Belum ada ulasan.", "seo.desc.noReviews": "Belum ada ulasan.",
"seo.desc.status": "{status}.", "seo.desc.status": "{status}.",
"seo.desc.statusLimits": "{status}, membatasi {limits}.", "seo.desc.statusLimits": "{status}, batas {limits}.",
"seo.desc.fedModules": "{status}. {modules}.", "seo.desc.fedModules": "{status}. Modul: {modules}.",
"seo.desc.fedModulesNetwork": "{status}. Modul: {modules} pada {network}.", "seo.desc.fedModulesNetwork": "{status}. Modul: {modules} pada {network}.",
"seo.mint.ogAlt.one": "{name}: {rating} bintang dari {n} ulasan di Cashumints.space", "seo.mint.ogAlt.one": "{name}: {rating} bintang dari {n} ulasan di Cashumints.space",
"seo.mint.ogAlt.other": "{name}: {rating} bintang dari {n} ulasan pada Cashumints.space", "seo.mint.ogAlt.other": "{name}: {rating} bintang dari {n} ulasan pada Cashumints.space",
"seo.mint.ogAltNoReviews": "{name} di ruang Cashumints.space", "seo.mint.ogAltNoReviews": "{name} di Cashumints.space",
"nav.home": "Rumah", "nav.home": "Beranda",
"mint.more.title": "Lebih permen", "mint.more.title": "Mint lainnya",
"mint.more.all": "Lihat semua permen", "mint.more.all": "Lihat semua mint",
"nav.fedimints": "Fedimints", "nav.fedimints": "Fedimints",
"status.announced": "Hanya diumumkan", "status.announced": "Hanya diumumkan",
"card.unnamedFederation": "Federasi Tanpa Nama", "card.unnamedFederation": "Federasi Tanpa Nama",
"card.notChecked": "tak pernah diperiksa", "card.notChecked": "tak pernah diperiksa",
"fedimints.title": "Semua Federasi Fedimint", "fedimints.title": "Semua federasi Fedimint · Cashumints.space",
"fedimints.description": "Semua {total} Fedimint federasi mengumumkan pada jaringan Nostr: {online} dikonfirmasi dijalankan oleh pemeriksaan luar, {announced} mengumumkan dan tidak pernah dikonfirmasi. Rating komunitas, modul dan kode undangan untuk masing-masing.", "fedimints.description": "Semua {total} Fedimint federasi mengumumkan pada jaringan Nostr: {online} dikonfirmasi dijalankan oleh pemeriksaan luar, {announced} mengumumkan dan tidak pernah dikonfirmasi. Rating komunitas, modul dan kode undangan untuk masing-masing.",
"fedimints.heading": "Semua federasi", "fedimints.heading": "Semua federasi",
"fedimints.lede": "Setiap federasi Fedimint mengumumkan pada jaringan Nostr. Sebuah federasi tidak memiliki status publik akhir sendiri, sehingga beberapa dari ini dikonfirmasi berjalan oleh cek luar dan sisanya terdaftar persis seperti yang diumumkan.", "fedimints.lede": "Setiap federasi Fedimint mengumumkan pada jaringan Nostr. Sebuah federasi tidak memiliki status publik akhir sendiri, sehingga beberapa dari ini dikonfirmasi berjalan oleh cek luar dan sisanya terdaftar persis seperti yang diumumkan.",
@@ -619,7 +643,7 @@
"fedimint.more.title": "Federasi lagi", "fedimint.more.title": "Federasi lagi",
"fedimint.more.all": "Lihat semua federasi", "fedimint.more.all": "Lihat semua federasi",
"fedimint.disclaimer": "Bergabung dengan federasi berarti mempercayai penjaganya dengan uangmu. Rating adalah pendapat masyarakat, dan dikonfirmasi berjalan berarti dapat dicapai, tidak dapat dipercaya.", "fedimint.disclaimer": "Bergabung dengan federasi berarti mempercayai penjaganya dengan uangmu. Rating adalah pendapat masyarakat, dan dikonfirmasi berjalan berarti dapat dicapai, tidak dapat dipercaya.",
"fedimint.module.lightning": "Petir", "fedimint.module.lightning": "Lightning",
"fedimint.module.mint": "Ecash mint", "fedimint.module.mint": "Ecash mint",
"fedimint.module.wallet": "Dompet On- chain", "fedimint.module.wallet": "Dompet On- chain",
"fedimint.module.meta": "Metadata", "fedimint.module.meta": "Metadata",
@@ -652,7 +676,113 @@
"mint.warnings.neverConfirmed.body.quiet": "federasi ini diumumkan pada Nostr di {date}, {days} yang lalu, tidak ada yang mengkonfirmasi sejak itu bahwa itu berjalan, dan tidak ada yang melihatnya baru-baru ini. Segala sesuatu di bawah ini berasal dari pengumuman.", "mint.warnings.neverConfirmed.body.quiet": "federasi ini diumumkan pada Nostr di {date}, {days} yang lalu, tidak ada yang mengkonfirmasi sejak itu bahwa itu berjalan, dan tidak ada yang melihatnya baru-baru ini. Segala sesuatu di bawah ini berasal dari pengumuman.",
"mint.warnings.neverConfirmed.meta": "Diumumkan {date}, tidak pernah dikonfirmasi", "mint.warnings.neverConfirmed.meta": "Diumumkan {date}, tidak pernah dikonfirmasi",
"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 pengalaman minting, melting, dan dukungannya? Ceritakan yang spesifik.",
"reviews.dialog.bodyPlaceholder.fedimint": "Bagaimana bergabung, pembayaran Petir dan dukungan berhasil? Lebih spesifik.", "reviews.dialog.bodyPlaceholder.fedimint": "Bagaimana pengalaman bergabung, pembayaran Lightning, dan dukungannya? Ceritakan yang 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."
} }
+59 -3
View File
@@ -2,14 +2,40 @@
* Build-time i18n entry point. Imported from .astro frontmatter, never from an island. * Build-time i18n entry point. Imported from .astro frontmatter, never from an island.
* *
* Every catalog is imported statically here, which is exactly what we want at build * Every catalog is imported statically here, which is exactly what we want at build
* time (one process renders all three locales) and exactly what we do not want in the * time (one process renders every locale) and exactly what we do not want in the
* browser. Islands import `./client` instead, which reads the one locale the page * browser. Islands import `./client` instead, which reads the one locale the page
* already carries. Nothing in this file reaches a bundle: Astro frontmatter runs on the * already carries. Nothing in this file reaches a bundle: Astro frontmatter runs on the
* build machine only. * build machine only.
*
* Every locale in `LOCALES` needs a line in both places below. A locale listed in the
* table with no catalog here does not fail: it silently renders English under its own
* `lang` and URL prefix, which is worse than failing. `test/i18n-wiring.test.mjs`
* fails instead.
*/ */
import en from './en.json'; import en from './en.json';
import es from './es.json'; import es from './es.json';
import nl from './nl.json'; import nl from './nl.json';
import zh from './zh.json';
import hi from './hi.json';
import ar from './ar.json';
import fr from './fr.json';
import pt from './pt.json';
import it from './it.json';
import tr from './tr.json';
import pl from './pl.json';
import uk from './uk.json';
import ro from './ro.json';
import el from './el.json';
import cs from './cs.json';
import sv from './sv.json';
import da from './da.json';
import id from './id.json';
import ur from './ur.json';
import ru from './ru.json';
import de from './de.json';
import ja from './ja.json';
import areg from './ar-eg.json';
import vi from './vi.json';
import { CLIENT_NAMESPACES, DEFAULT_LOCALE, LOCALE_CODES, isLocale, type Locale } from './config.js'; import { CLIENT_NAMESPACES, DEFAULT_LOCALE, LOCALE_CODES, isLocale, type Locale } from './config.js';
import { createTranslator, type Catalog, type Translator } from './translate.js'; import { createTranslator, type Catalog, type Translator } from './translate.js';
@@ -22,6 +48,27 @@ const CATALOGS: Record<string, Catalog> = {
en: en as Catalog, en: en as Catalog,
es: es as Catalog, es: es as Catalog,
nl: nl as Catalog, nl: nl as Catalog,
zh: zh as Catalog,
hi: hi as Catalog,
ar: ar as Catalog,
fr: fr as Catalog,
pt: pt as Catalog,
it: it as Catalog,
tr: tr as Catalog,
pl: pl as Catalog,
uk: uk as Catalog,
ro: ro as Catalog,
el: el as Catalog,
cs: cs as Catalog,
sv: sv as Catalog,
da: da as Catalog,
id: id as Catalog,
ur: ur as Catalog,
ru: ru as Catalog,
de: de as Catalog,
ja: ja as Catalog,
'ar-eg': areg as Catalog,
vi: vi as Catalog,
}; };
/** The English catalog, which is what every other locale falls back to key by key. */ /** The English catalog, which is what every other locale falls back to key by key. */
@@ -74,13 +121,22 @@ export function missingKeys(): Record<string, string[]> {
* key that survives is resolved: a key this locale is missing arrives already filled * key that survives is resolved: a key this locale is missing arrives already filled
* with the English string, so the browser needs no fallback catalog and ships exactly * with the English string, so the browser needs no fallback catalog and ships exactly
* one language. * one language.
*
* `extra` is for a namespace exactly one page's islands need. The home page's grids
* hydrate and have to rewrite their own "All 56 mints →" links, which live under
* `home.` — a namespace worth about 2KB that every other page, including 1,300 mint
* pages, has no use for. Passed per page through `Base.astro`'s `clientNamespaces`
* prop, it is inlined where it is read and nowhere else. `check-i18n.mjs` does not know
* about this, so a key reached this way must still be in a namespace the checker
* accepts, or listed in `CLIENT_NAMESPACES` — see the note there.
*/ */
export function clientCatalog(locale: Locale): Catalog { export function clientCatalog(locale: Locale, extra: readonly string[] = []): Catalog {
const catalog = catalogFor(locale); const catalog = catalogFor(locale);
const allowed = new Set<string>([...CLIENT_NAMESPACES, ...extra]);
const out: Catalog = {}; const out: Catalog = {};
for (const key of Object.keys(BASE_CATALOG)) { for (const key of Object.keys(BASE_CATALOG)) {
const namespace = key.split('.')[0] ?? ''; const namespace = key.split('.')[0] ?? '';
if (!(CLIENT_NAMESPACES as readonly string[]).includes(namespace)) continue; if (!allowed.has(namespace)) continue;
out[key] = catalog[key] ?? BASE_CATALOG[key]!; out[key] = catalog[key] ?? BASE_CATALOG[key]!;
} }
return out; return out;
+147 -17
View File
@@ -33,26 +33,30 @@
"status.unknown": "Non ancora controllato", "status.unknown": "Non ancora controllato",
"nav.skip": "Salta al contenuto", "nav.skip": "Salta al contenuto",
"nav.main": "Principali", "nav.main": "Principali",
"nav.mints": "Cashu mints", "nav.mints": "Mint Cashu",
"nav.reviews": "Recensioni", "nav.reviews": "Recensioni",
"nav.wallets": "Portafogli", "nav.wallets": "Portafogli",
"nav.about": "Informazioni", "nav.about": "Informazioni",
"nav.searchMints": "Ricerca {n} mints", "nav.searchMints": "Cerca tra {n} mint",
"lang.label": "Lingua", "lang.label": "Lingua",
"lang.switcher": "Scegli una lingua", "lang.switcher": "Scegli una lingua",
"footer.copyright": "© © {year} Cashumints.space", "footer.copyright": "© {year} Cashumints.space",
"footer.allMints": "Tutti Cashu mints", "footer.allMints": "Tutti i mint Cashu",
"footer.allReviews": "Tutte le recensioni", "footer.allReviews": "Tutte le recensioni",
"footer.wallets": "Portafogli", "footer.wallets": "Portafogli",
"footer.about": "Informazioni", "footer.about": "Informazioni",
"footer.disclaimer": "Avvertenza", "footer.disclaimer": "Avvertenza",
"footer.terms": "Termini", "footer.terms": "Termini",
"footer.privacy": "Privacy", "footer.privacy": "Privacy",
"footer.madeBy": "Realizzato con 😉 da {azzamo} · Alimentato da Cashu e NIP-87", "footer.madeBy": "Realizzato con ❤️ da {azzamo} · Basato su Cashu e NIP-87",
"footer.warning": "Mints sono custodi. A mint può scomparire con i vostri soldi.", "footer.warning": "I mint sono custodiali. Un mint può sparire con i tuoi soldi.",
"home.title": "Cashumints.space · Trova un Cashu mint si può fidare", "footer.tagline": "Recensioni della comunità sui mint ecash, firmate su Nostr.",
"home.description": "Trova un mint Cashu di cui fidarti. {mints} mints indicizzati con {reviews} recensioni della comunità firmate su Nostr, oltre a disponibilità in tempo reale, NUTs supportati e software del mint.", "footer.col.explore": "Esplora",
"home.hero.title": "Trova un Cashu mint", "footer.col.site": "Sito",
"footer.nav": "Piè di pagina",
"home.title": "Cashumints.space · Trova un mint Cashu di cui fidarti",
"home.description": "Trova un mint Cashu di cui fidarti. {mints} mint indicizzati con {reviews} recensioni della comunità firmate su Nostr, oltre a disponibilità in tempo reale, NUTs supportati e software del mint.",
"home.hero.title": "Trova un mint Cashu",
"home.hero.titleEm": "tu puoi fidarti", "home.hero.titleEm": "tu puoi fidarti",
"home.hero.lede": "Ogni mint sulla rete Nostr, con recensioni comuni reali. Nessun portiere, appena firmato recensioni è possibile verificare se stessi.", "home.hero.lede": "Ogni mint sulla rete Nostr, con recensioni comuni reali. Nessun portiere, appena firmato recensioni è possibile verificare se stessi.",
"home.search.label": "Cerca mints per nome, dominio o URL", "home.search.label": "Cerca mints per nome, dominio o URL",
@@ -91,7 +95,7 @@
"pulse.stale": "Stallo.", "pulse.stale": "Stallo.",
"mints.title": "Tutti Cashu mints · Cashumints.space", "mints.title": "Tutti Cashu mints · Cashumints.space",
"mints.description": "Tutti {total} Cashu mints indicizzato dalla rete Nostr, {online} online e {offline} offline, con valutazioni della comunità, uptime e mint software. Cerca e ordina la lista completa.", "mints.description": "Tutti {total} Cashu mints indicizzato dalla rete Nostr, {online} online e {offline} offline, con valutazioni della comunità, uptime e mint software. Cerca e ordina la lista completa.",
"mints.heading": "Tutti Cashu mints", "mints.heading": "Tutti i mint Cashu",
"mints.lede": "Ogni mint scoperto sulla rete Nostr, compresi quelli che sono andati. Offline mints rimanere elencato in modo da poter ancora trovare e rivederli.", "mints.lede": "Ogni mint scoperto sulla rete Nostr, compresi quelli che sono andati. Offline mints rimanere elencato in modo da poter ancora trovare e rivederli.",
"mints.search.label": "Cerca mints per nome o dominio", "mints.search.label": "Cerca mints per nome o dominio",
"mints.search.placeholder": "Ricerca per nome o dominio...", "mints.search.placeholder": "Ricerca per nome o dominio...",
@@ -114,6 +118,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",
@@ -261,6 +267,9 @@
"reviews.empty": "Ancora nessuna recensione. Sii il primo a scriverne uno.", "reviews.empty": "Ancora nessuna recensione. Sii il primo a scriverne uno.",
"reviews.emptyWritten": "Nessuna recensione scritta qui. Sii il primo a scriverne uno.", "reviews.emptyWritten": "Nessuna recensione scritta qui. Sii il primo a scriverne uno.",
"reviews.emptyFilter": "Nessuna recensione corrisponde a questo filtro.", "reviews.emptyFilter": "Nessuna recensione corrisponde a questo filtro.",
"reviews.rate.cashu": "Valuta questo mint",
"reviews.rate.fedimint": "Valuta questa federazione",
"reviews.rate.lnurl": "Valuta questo mint",
"reviews.noscript": "Le recensioni sono firmate su Nostr e caricare nel browser, quindi questa lista ha bisogno di JavaScript. La valutazione e conta sopra provengono dal sito stesso.", "reviews.noscript": "Le recensioni sono firmate su Nostr e caricare nel browser, quindi questa lista ha bisogno di JavaScript. La valutazione e conta sopra provengono dal sito stesso.",
"reviews.relayError": "Non poteva raggiungere il relè. Le recensioni possono essere incomplete.", "reviews.relayError": "Non poteva raggiungere il relè. Le recensioni possono essere incomplete.",
"reviews.srRating": "{rating} su 5", "reviews.srRating": "{rating} su 5",
@@ -273,7 +282,7 @@
"reviews.anonNote": "Prima recensione da questo npub, nessun'altra attività trovata", "reviews.anonNote": "Prima recensione da questo npub, nessun'altra attività trovata",
"reviews.published": "Pubblicato.", "reviews.published": "Pubblicato.",
"reviews.publishedPartial": "Pubblicato in {accepted} di {total} relè.", "reviews.publishedPartial": "Pubblicato in {accepted} di {total} relè.",
"reviews.propagating": "Propagando, può richiedere un momento per apparire altrove.", "reviews.propagating": "Pubblicazione sui relay Nostr",
"reviews.summary.line.one": "{count} valutazione senza un commento: {breakdown}", "reviews.summary.line.one": "{count} valutazione senza un commento: {breakdown}",
"reviews.summary.line.other": "{count} valutazioni senza un commento: {breakdown}", "reviews.summary.line.other": "{count} valutazioni senza un commento: {breakdown}",
"reviews.summary.lineDay.one": "{count} valutazione senza un commento su {when}: {breakdown}", "reviews.summary.lineDay.one": "{count} valutazione senza un commento su {when}: {breakdown}",
@@ -287,7 +296,12 @@
"reviews.breakdown.1": "{n} una stella", "reviews.breakdown.1": "{n} una stella",
"reviews.breakdown.none": "{n} senza classificazione", "reviews.breakdown.none": "{n} senza classificazione",
"reviews.dialog.title": "Recensione {name}", "reviews.dialog.title": "Recensione {name}",
"reviews.dialog.note": "Firmato con la chiave Nostr e pubblicato su relè pubblici. Non può essere modificato o cancellato.", "reviews.dialog.lede.cashu": "Racconta la tua esperienza con questo mint.",
"reviews.dialog.lede.fedimint": "Racconta la tua esperienza con questa federazione.",
"reviews.dialog.lede.lnurl": "Racconta la tua esperienza con questo mint.",
"reviews.dialog.permanent": "Pubblicato in modo permanente su relay Nostr pubblici.",
"reviews.dialog.gate.title": "Accedi per scrivere una recensione",
"reviews.dialog.gate.lead": "Le recensioni sono firmate con la tua chiave Nostr. Resta nel tuo signer, questo sito non la vede mai.",
"reviews.dialog.ratingLegend": "La tua valutazione", "reviews.dialog.ratingLegend": "La tua valutazione",
"reviews.dialog.starsSr.one": "{n} stella stella", "reviews.dialog.starsSr.one": "{n} stella stella",
"reviews.dialog.starsSr.other": "{n} stelle", "reviews.dialog.starsSr.other": "{n} stelle",
@@ -308,7 +322,6 @@
"reviews.dialog.waitingSigner": "Aspettando il tuo firmatario", "reviews.dialog.waitingSigner": "Aspettando il tuo firmatario",
"reviews.dialog.confirmClose": "Chiudi questa recensione? La tua bozza viene mantenuta fino a quando non esci dal sito.", "reviews.dialog.confirmClose": "Chiudi questa recensione? La tua bozza viene mantenuta fino a quando non esci dal sito.",
"reviews.dialog.why.login": "Accedi per pubblicare", "reviews.dialog.why.login": "Accedi per pubblicare",
"reviews.dialog.why.rating": "Scegli una valutazione",
"reviews.dialog.why.words": "Scrivi qualche parola", "reviews.dialog.why.words": "Scrivi qualche parola",
"reviews.dialog.why.more.one": "{n} più carattere", "reviews.dialog.why.more.one": "{n} più carattere",
"reviews.dialog.why.more.other": "{n} più caratteri", "reviews.dialog.why.more.other": "{n} più caratteri",
@@ -325,7 +338,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",
@@ -369,6 +382,10 @@
"login.status.waitingSigner": "Aspettando il tuo firmatario", "login.status.waitingSigner": "Aspettando il tuo firmatario",
"login.status.connecting": "Collegamento al tuo firmatario", "login.status.connecting": "Collegamento al tuo firmatario",
"login.status.connected": "Collegato", "login.status.connected": "Collegato",
"login.toast.loggedIn": "Accesso effettuato",
"login.toast.fetchingProfile": "Recupero del tuo profilo Nostr dalle relè…",
"login.toast.profile": "Profilo: {name}",
"login.toast.noProfile": "Nessun profilo trovato sulle relè",
"login.method.extension": "Estensione", "login.method.extension": "Estensione",
"login.method.remote": "Telecomando", "login.method.remote": "Telecomando",
"login.method.primal": "Primal", "login.method.primal": "Primal",
@@ -400,7 +417,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",
@@ -569,7 +593,7 @@
"nav.home": "Home", "nav.home": "Home",
"mint.more.title": "Altro mints", "mint.more.title": "Altro mints",
"mint.more.all": "Vedi tutto mints", "mint.more.all": "Vedi tutto mints",
"nav.fedimints": "Fediminti", "nav.fedimints": "Fedimints",
"status.announced": "Annunciato solo", "status.announced": "Annunciato solo",
"card.unnamedFederation": "federazione non nominata", "card.unnamedFederation": "federazione non nominata",
"card.notChecked": "mai controllato", "card.notChecked": "mai controllato",
@@ -654,5 +678,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."
} }
+167 -37
View File
@@ -1,5 +1,5 @@
{ {
"site.name": "ログイン スペース", "site.name": "Cashumints.space",
"common.close": "ふりがな", "common.close": "ふりがな",
"common.cancel": "キャンセル", "common.cancel": "キャンセル",
"common.done": "ログイン", "common.done": "ログイン",
@@ -33,26 +33,30 @@
"status.unknown": "未確認", "status.unknown": "未確認",
"nav.skip": "コンテンツへスキップ", "nav.skip": "コンテンツへスキップ",
"nav.main": "メインページ", "nav.main": "メインページ",
"nav.mints": "キャッシュミント", "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": "言語",
"lang.switcher": "言語を選択", "lang.switcher": "言語を選択",
"footer.copyright": "© 2018 {year} Cashumints.space 禁無断複写・転載を禁じます。", "footer.copyright": "© {year} Cashumints.space",
"footer.allMints": "すべてのカシューミント", "footer.allMints": "すべての Cashu ミント",
"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 および NIP-87 によって動力を与えられた", "footer.madeBy": "{azzamo} が ❤️ を込めて制作 · Cashu と NIP-87 で動作",
"footer.warning": "ミントはカストディアルです。 あなたのお金でマイナスが消えます。", "footer.warning": "ミントはカストディアル(資金を預かる)です。ミントがあなたのお金ごと消えることがあります。",
"home.title": "Cashumints.space · 信用できるキャッシュミントを見つける", "footer.tagline": "ecash ミントに対するコミュニティのレビュー。Nostr で署名されています。",
"home.description": "信頼できるキャッシュミントを見つけましょう。 {mints} mintsは、Nostrに署名された{reviews}コミュニティレビューと、ライブ稼働時間、NUTとミントソフトウェアをサポートしました。", "footer.col.explore": "探す",
"home.hero.title": "キャッシュミントを探す", "footer.col.site": "サイト",
"footer.nav": "フッター",
"home.title": "Cashumints.space · 信頼できる Cashu ミントを見つける",
"home.description": "信頼できる Cashu ミントを見つけましょう。{mints} 件のミントを収録し、Nostr で署名された {reviews} 件のコミュニティレビュー、稼働状況、対応 NUTs、ミントソフトウェアの情報を掲載しています。",
"home.hero.title": "Cashu ミントを探す",
"home.hero.titleEm": "信頼できます", "home.hero.titleEm": "信頼できます",
"home.hero.lede": "コミュニティレビューを実践するNostrネットワーク上ですべてのミント。 ゲートキーパーは、あなたが自分で確認できるレビューを署名しました。", "home.hero.lede": "コミュニティレビューを実践するNostrネットワーク上ですべてのミント。 ゲートキーパーは、あなたが自分で確認できるレビューを署名しました。",
"home.search.label": "名前、ドメイン、URLでミントを検索", "home.search.label": "名前、ドメイン、URLでミントを検索",
@@ -61,7 +65,7 @@
"home.filters.highestRated": "最高評価", "home.filters.highestRated": "最高評価",
"home.filters.recentlyActive": "最近の活動", "home.filters.recentlyActive": "最近の活動",
"home.filters.websockets": "WebSocketをサポート", "home.filters.websockets": "WebSocketをサポート",
"home.top.title": "トップカシューミント", "home.top.title": "人気の Cashu ミント",
"home.top.sub": "レビューと評価でランク付け", "home.top.sub": "レビューと評価でランク付け",
"home.top.all": "すべて {n} 分 →", "home.top.all": "すべて {n} 分 →",
"home.latest.title": "最新のレビュー", "home.latest.title": "最新のレビュー",
@@ -89,9 +93,9 @@
"pulse.indexer": "インデックス", "pulse.indexer": "インデックス",
"pulse.connected": "コネクティッド", "pulse.connected": "コネクティッド",
"pulse.stale": "ストール", "pulse.stale": "ストール",
"mints.title": "すべてのカシューミント・カシューティン。 スペース", "mints.title": "すべての Cashu ミント · Cashumints.space",
"mints.description": "すべて {total} Cashu は、Nostr のネットワーク、{online} オンラインおよび {offline} オフラインでインデックス化され、コミュニティの評価、稼働時間、ミント ソフトウェア。 完全なリストを検索してソートします。", "mints.description": "すべて {total} Cashu は、Nostr のネットワーク、{online} オンラインおよび {offline} オフラインでインデックス化され、コミュニティの評価、稼働時間、ミント ソフトウェア。 完全なリストを検索してソートします。",
"mints.heading": "すべてのカシューミント", "mints.heading": "すべての Cashu ミント",
"mints.lede": "ノストラネットワークで発見されたミントは、消えているものを含む。 オフラインのミントはリストされているので、まだそれらを見つけ、確認することができます。", "mints.lede": "ノストラネットワークで発見されたミントは、消えているものを含む。 オフラインのミントはリストされているので、まだそれらを見つけ、確認することができます。",
"mints.search.label": "名前またはドメインでミントを検索", "mints.search.label": "名前またはドメインでミントを検索",
"mints.search.placeholder": "名前やドメインで検索", "mints.search.placeholder": "名前やドメインで検索",
@@ -114,6 +118,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をコピーする",
@@ -261,6 +267,9 @@
"reviews.empty": "レビューはありません。 まずは書いてください。", "reviews.empty": "レビューはありません。 まずは書いてください。",
"reviews.emptyWritten": "まだレビューはありません。 まずは書いてください。", "reviews.emptyWritten": "まだレビューはありません。 まずは書いてください。",
"reviews.emptyFilter": "このフィルタに一致するレビューはありません。", "reviews.emptyFilter": "このフィルタに一致するレビューはありません。",
"reviews.rate.cashu": "この mint を評価する",
"reviews.rate.fedimint": "このフェデレーションを評価する",
"reviews.rate.lnurl": "この mint を評価する",
"reviews.noscript": "レビューはNostrに署名され、ブラウザに読み込まれるので、このリストにはJavaScriptが必要です。 上記の評価とカウントは、サイト自体から来ています。", "reviews.noscript": "レビューはNostrに署名され、ブラウザに読み込まれるので、このリストにはJavaScriptが必要です。 上記の評価とカウントは、サイト自体から来ています。",
"reviews.relayError": "リレーに到達できませんでした。 レビューは不完全かもしれません。", "reviews.relayError": "リレーに到達できませんでした。 レビューは不完全かもしれません。",
"reviews.srRating": "{rating} から 5", "reviews.srRating": "{rating} から 5",
@@ -273,7 +282,7 @@
"reviews.anonNote": "このnpubからの最初のレビュー、他の活動が見つかりません", "reviews.anonNote": "このnpubからの最初のレビュー、他の活動が見つかりません",
"reviews.published": "掲載情報", "reviews.published": "掲載情報",
"reviews.publishedPartial": "に公開 {accepted} の {total} リレー.", "reviews.publishedPartial": "に公開 {accepted} の {total} リレー.",
"reviews.propagating": "伝播は、他の場所で出現する瞬間を取るかもしれません。", "reviews.propagating": "Nostrリレーに公開中",
"reviews.summary.line.one": "{count} コメントのない評価: {breakdown}", "reviews.summary.line.one": "{count} コメントのない評価: {breakdown}",
"reviews.summary.line.other": "{count} コメントのない評価: {breakdown}", "reviews.summary.line.other": "{count} コメントのない評価: {breakdown}",
"reviews.summary.lineDay.one": "{count} 評価なし にコメント {when}: {breakdown}", "reviews.summary.lineDay.one": "{count} 評価なし にコメント {when}: {breakdown}",
@@ -287,7 +296,12 @@
"reviews.breakdown.1": "{n} ワンスター", "reviews.breakdown.1": "{n} ワンスター",
"reviews.breakdown.none": "{n} なしの評価", "reviews.breakdown.none": "{n} なしの評価",
"reviews.dialog.title": "レビュー {name}", "reviews.dialog.title": "レビュー {name}",
"reviews.dialog.note": "Nostrキーに署名し、パブリックリレーに公開します。 編集や削除はできません。", "reviews.dialog.lede.cashu": "この mint での体験を共有してください。",
"reviews.dialog.lede.fedimint": "このフェデレーションでの体験を共有してください。",
"reviews.dialog.lede.lnurl": "この mint での体験を共有してください。",
"reviews.dialog.permanent": "公開 Nostr リレーに恒久的に公開されます。",
"reviews.dialog.gate.title": "レビューを書くにはログインしてください",
"reviews.dialog.gate.lead": "レビューはあなたの Nostr 鍵で署名されます。鍵は署名アプリの中にとどまり、このサイトが見ることはありません。",
"reviews.dialog.ratingLegend": "あなたの評価", "reviews.dialog.ratingLegend": "あなたの評価",
"reviews.dialog.starsSr.one": "{n} スター", "reviews.dialog.starsSr.one": "{n} スター",
"reviews.dialog.starsSr.other": "{n} つ星ホテル", "reviews.dialog.starsSr.other": "{n} つ星ホテル",
@@ -308,7 +322,6 @@
"reviews.dialog.waitingSigner": "あなたの署名者を待っています", "reviews.dialog.waitingSigner": "あなたの署名者を待っています",
"reviews.dialog.confirmClose": "このレビューを閉じる? サイトを離れるまで、あなたのドラフトは保持されます。", "reviews.dialog.confirmClose": "このレビューを閉じる? サイトを離れるまで、あなたのドラフトは保持されます。",
"reviews.dialog.why.login": "ログインして公開する", "reviews.dialog.why.login": "ログインして公開する",
"reviews.dialog.why.rating": "評価を選ぶ",
"reviews.dialog.why.words": "いくつかの単語を書く", "reviews.dialog.why.words": "いくつかの単語を書く",
"reviews.dialog.why.more.one": "{n} キャラクター", "reviews.dialog.why.more.one": "{n} キャラクター",
"reviews.dialog.why.more.other": "{n} より多くの文字", "reviews.dialog.why.more.other": "{n} より多くの文字",
@@ -324,8 +337,8 @@
"reviews.validate.rating": "1から5星までの格付けを選択します。", "reviews.validate.rating": "1から5星までの格付けを選択します。",
"reviews.validate.short": "少なくとも{n}文字を書くと、レビューが便利です。", "reviews.validate.short": "少なくとも{n}文字を書くと、レビューが便利です。",
"reviews.validate.long": "レビューは{n}文字に制限されています。", "reviews.validate.long": "レビューは{n}文字に制限されています。",
"feed.title": "最新のミントとフェデレーションレビュー・カウンティン。 スペース", "feed.title": "最新のミントとフェデレーションのレビュー · Cashumints.space",
"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": "レビュー",
@@ -369,6 +382,10 @@
"login.status.waitingSigner": "あなたの署名者を待っています", "login.status.waitingSigner": "あなたの署名者を待っています",
"login.status.connecting": "署名者への接続", "login.status.connecting": "署名者への接続",
"login.status.connected": "コネクテッド", "login.status.connected": "コネクテッド",
"login.toast.loggedIn": "ログインしました",
"login.toast.fetchingProfile": "リレーから Nostr プロフィールを取得しています…",
"login.toast.profile": "プロフィール: {name}",
"login.toast.noProfile": "リレーにプロフィールが見つかりませんでした",
"login.method.extension": "エクステンション", "login.method.extension": "エクステンション",
"login.method.remote": "リモート", "login.method.remote": "リモート",
"login.method.primal": "プリマル", "login.method.primal": "プリマル",
@@ -389,8 +406,8 @@
"signer.error.timeout": "サイダーは時間内に答えませんでした。 開くと、まだ接続されていることを確認してください。", "signer.error.timeout": "サイダーは時間内に答えませんでした。 開くと、まだ接続されていることを確認してください。",
"signer.error.cancelled": "接続解除", "signer.error.cancelled": "接続解除",
"signer.error.declined": "署名が拒否されました", "signer.error.declined": "署名が拒否されました",
"notfound.title": "ページが見つかりません。 スペース", "notfound.title": "ページが見つかりません · Cashumints.space",
"notfound.description": "そのページは、Cashumintsに存在しません。 スペース。", "notfound.description": "そのページは Cashumints.space にありません。",
"notfound.heading": "このページは存在しない", "notfound.heading": "このページは存在しない",
"notfound.lede": "移動しているか、まだミントがインデックス化されていない可能性があります。 名前またはドメインでミントを検索するか、ミントURLを貼り付けます。", "notfound.lede": "移動しているか、まだミントがインデックス化されていない可能性があります。 名前またはドメインでミントを検索するか、ミントURLを貼り付けます。",
"notfound.browse": "すべてのミントを見る", "notfound.browse": "すべてのミントを見る",
@@ -400,8 +417,15 @@
"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 からの要約で、下にレビューがあります。完全なページは次回のビルドで表示されます。",
"wallets.title": "Cashuウォレット・Cashumints スペース", "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.description": "モバイル、ウェブ、ブラウザ、コマンドラインで、ここにリストされているミントで動作するキャッシュウォレットは、それぞれが得意です。", "wallets.description": "モバイル、ウェブ、ブラウザ、コマンドラインで、ここにリストされているミントで動作するキャッシュウォレットは、それぞれが得意です。",
"wallets.heading": "ウォレット", "wallets.heading": "ウォレット",
"wallets.lede": "これらのミントからecashを保持するウォレットが必要です。 ほとんどの財布は、一度に複数のミントに話しますので、最初に試してみることに気付きません。", "wallets.lede": "これらのミントからecashを保持するウォレットが必要です。 ほとんどの財布は、一度に複数のミントに話しますので、最初に試してみることに気付きません。",
@@ -420,7 +444,7 @@
"wallets.item.coinos": "また、独自のCashuミントを実行する財布と販売のポイントをLightning.", "wallets.item.coinos": "また、独自のCashuミントを実行する財布と販売のポイントをLightning.",
"wallets.item.oxchat": "ノスト・メッセンジャーは、チャット内でsatsを送信します。", "wallets.item.oxchat": "ノスト・メッセンジャーは、チャット内でsatsを送信します。",
"wallets.item.nutshell": "リファレンス実装。 コマンドラインとミントソフトウェアでウォレットとして実行します。", "wallets.item.nutshell": "リファレンス実装。 コマンドラインとミントソフトウェアでウォレットとして実行します。",
"about.title": "Cashumintsについて スペース", "about.title": "Cashumints.space について",
"about.description": "Cashuは、実際に保持するミント、NIP-87が何であるか、そしてこのサイトがNostrリレーからミントをインデックス化し、レビューを読み込む方法です。", "about.description": "Cashuは、実際に保持するミント、NIP-87が何であるか、そしてこのサイトがNostrリレーからミントをインデックス化し、レビューを読み込む方法です。",
"about.heading": "お問い合わせ", "about.heading": "お問い合わせ",
"about.lede": "Cashumints.spaceは、Nostrネットワークで見つけることができるすべてのCashuミントをリストし、人々がそれらについて言うことを示します。 Azzamoで作っています。", "about.lede": "Cashumints.spaceは、Nostrネットワークで見つけることができるすべてのCashuミントをリストし、人々がそれらについて言うことを示します。 Azzamoで作っています。",
@@ -452,8 +476,8 @@
"about.numbers.p1": "{mints} mints インデックス, {online} 回答の, {reviews} 上記リレーからカウントされたレビュー.", "about.numbers.p1": "{mints} mints インデックス, {online} 回答の, {reviews} 上記リレーからカウントされたレビュー.",
"about.source.title": "ソースとクレジット", "about.source.title": "ソースとクレジット",
"about.source.p1": "本サイトは{github}に公開されています。 {azzamo}でビルドして実行します。 ミントページが間違っている場合は、ソースで修正してください。ミントメタデータは、ミント独自の{code}から来て、レビューはノストから来ます。", "about.source.p1": "本サイトは{github}に公開されています。 {azzamo}でビルドして実行します。 ミントページが間違っている場合は、ソースで修正してください。ミントメタデータは、ミント独自の{code}から来て、レビューはノストから来ます。",
"terms.title": "利用条件・キャッシュミント スペース", "terms.title": "利用規約 · Cashumints.space",
"terms.description": "Cashumintsの利用規約 スペース:サイトが何であるか、それが何をしないのか、誰が責任を負います。 口座なし、保証なし、任意のミントとの提携なし。", "terms.description": "Cashumints.space の利用規約:このサイトが何であり、何をしないのか、誰が何に責任を負うのか。アカウントなし、保証なし、いかなるミントとも無関係です。",
"terms.heading": "利用規約", "terms.heading": "利用規約",
"terms.lede": "パブリックデータだけをインデックス化するサイトのための明白な条件。 最近更新された{date}。", "terms.lede": "パブリックデータだけをインデックス化するサイトのための明白な条件。 最近更新された{date}。",
"terms.what.title": "このサイトについて", "terms.what.title": "このサイトについて",
@@ -479,7 +503,7 @@
"terms.changes.p1": "これらの条件は変更できます。 一番上の日付は、最後にした時に言います。 本サイトはオープンソースなので、このページへの変更はすべてコミット履歴です。", "terms.changes.p1": "これらの条件は変更できます。 一番上の日付は、最後にした時に言います。 本サイトはオープンソースなので、このページへの変更はすべてコミット履歴です。",
"terms.contact.title": "お問い合わせ", "terms.contact.title": "お問い合わせ",
"terms.contact.p1": "{github} で発行と買収の要求が最適です。 このサイトは、{azzamo} が運営しています。", "terms.contact.p1": "{github} で発行と買収の要求が最適です。 このサイトは、{azzamo} が運営しています。",
"privacy.title": "プライバシー・カシュート スペース", "privacy.title": "プライバシー · Cashumints.space",
"privacy.description": "Cashumints.spaceのプライバシー:アカウントなし、トラッキングクッキーなし、分析なし。 localStorage は、プロファイルキャッシュに使用され、どこにも送信されません。 レビューは、あなた自身のブラウザで署名されます。", "privacy.description": "Cashumints.spaceのプライバシー:アカウントなし、トラッキングクッキーなし、分析なし。 localStorage は、プロファイルキャッシュに使用され、どこにも送信されません。 レビューは、あなた自身のブラウザで署名されます。",
"privacy.heading": "プロフィール", "privacy.heading": "プロフィール",
"privacy.lede": "弊社では、ウェブサイトが出来る限り少しだけ収集しております。 最近更新された{date}。", "privacy.lede": "弊社では、ウェブサイトが出来る限り少しだけ収集しております。 最近更新された{date}。",
@@ -514,7 +538,7 @@
"privacy.permanent.p2": "自分のアイデンティティに縛られたことなしでミントを見直したい場合は、別のNostrキーを使用します。 一方のスターレビューを書いているブランドの新しいキーはパターンリーダーが見るのに値するパターンリーダーであるため、他の活動がないキーは、過酷なレビューのように示されていることに注意してください。", "privacy.permanent.p2": "自分のアイデンティティに縛られたことなしでミントを見直したい場合は、別のNostrキーを使用します。 一方のスターレビューを書いているブランドの新しいキーはパターンリーダーが見るのに値するパターンリーダーであるため、他の活動がないキーは、過酷なレビューのように示されていることに注意してください。",
"privacy.contact.title": "お問い合わせ", "privacy.contact.title": "お問い合わせ",
"privacy.contact.p1": "サイトはオープンソースです。上記のいずれかをチェックしたい場合は、{github}でコードを読みます。 質問や修正は問題に遭う可能性があります。", "privacy.contact.p1": "サイトはオープンソースです。上記のいずれかをチェックしたい場合は、{github}でコードを読みます。 質問や修正は問題に遭う可能性があります。",
"disclaimer.title": "免責事項・ Cashumints スペース", "disclaimer.title": "免責事項 · Cashumints.space",
"disclaimer.description": "あなたはカシューミントを使用する前に知っておくべきこと:ミントはcustodialです、ミントはあなたのお金で消えることができます、レビューは意見であり、オンラインでは信頼できるものではありません。", "disclaimer.description": "あなたはカシューミントを使用する前に知っておくべきこと:ミントはcustodialです、ミントはあなたのお金で消えることができます、レビューは意見であり、オンラインでは信頼できるものではありません。",
"disclaimer.heading": "免責事項", "disclaimer.heading": "免責事項",
"disclaimer.lede": "あなたがミントにお金を入れる前にこれを読んでください。 最近更新された{date}。", "disclaimer.lede": "あなたがミントにお金を入れる前にこれを読んでください。 最近更新された{date}。",
@@ -552,8 +576,8 @@
"seo.mint.title.other": "{n} {name}のレビュー評価・口コミ{rating} Cashumints.space", "seo.mint.title.other": "{n} {name}のレビュー評価・口コミ{rating} Cashumints.space",
"seo.mint.titleOffline.one": "{name}: オフラインで {n} 日 | Cashumints.space", "seo.mint.titleOffline.one": "{name}: オフラインで {n} 日 | Cashumints.space",
"seo.mint.titleOffline.other": "{name}: オフラインで {n} 日間 | Cashumints.space", "seo.mint.titleOffline.other": "{name}: オフラインで {n} 日間 | Cashumints.space",
"seo.mint.titleNoReviews": "{name}: Cashu ミント情報とレビュー | Cashumints スペース", "seo.mint.titleNoReviews": "{name}: Cashu ミントの情報とレビュー | Cashumints.space",
"seo.fedimint.titleNoReviews": "{name}: 運命の送り状情報とレビュー | Cashumints. スペース", "seo.fedimint.titleNoReviews": "{name}: Fedimint フェデレーションの情報とレビュー | Cashumints.space",
"seo.desc.fallback": "コミュニティのレビュー {name} ({domain}).", "seo.desc.fallback": "コミュニティのレビュー {name} ({domain}).",
"seo.desc.fallbackDomain": "{domain}のキャッシュミントのコミュニティレビュー。", "seo.desc.fallbackDomain": "{domain}のキャッシュミントのコミュニティレビュー。",
"seo.desc.rated.one": "評価{rating}/5から{n}はノストラのレビューを署名しました。", "seo.desc.rated.one": "評価{rating}/5から{n}はノストラのレビューを署名しました。",
@@ -566,14 +590,14 @@
"seo.mint.ogAlt.one": "{name}: {rating} 星の {n} Cashumints.space のレビュー", "seo.mint.ogAlt.one": "{name}: {rating} 星の {n} Cashumints.space のレビュー",
"seo.mint.ogAlt.other": "{n} {name}: {rating} 星から Cashumints.space", "seo.mint.ogAlt.other": "{n} {name}: {rating} 星から Cashumints.space",
"seo.mint.ogAltNoReviews": "{name} に Cashumints.space", "seo.mint.ogAltNoReviews": "{name} に Cashumints.space",
"nav.home": "ホーム >", "nav.home": "ホーム",
"mint.more.title": "もっとミント", "mint.more.title": "もっとミント",
"mint.more.all": "すべてのミントを見る", "mint.more.all": "すべてのミントを見る",
"nav.fedimints": "フィードバック", "nav.fedimints": "Fedimints",
"status.announced": "開催概要", "status.announced": "開催概要",
"card.unnamedFederation": "名称変更", "card.unnamedFederation": "名称変更",
"card.notChecked": "決してチェックしない", "card.notChecked": "決してチェックしない",
"fedimints.title": "すべての Fedimint のフェデレーション · Cashumints. スペース", "fedimints.title": "すべての Fedimint フェデレーション · Cashumints.space",
"fedimints.description": "すべて {total} Nostrネットワークで発表されたFedimintのフェデレーション: {online} 外部チェックで実行を確認, {announced} 発表し、確認しませんでした. コミュニティの評価、モジュール、各コードを招待します。", "fedimints.description": "すべて {total} Nostrネットワークで発表されたFedimintのフェデレーション: {online} 外部チェックで実行を確認, {announced} 発表し、確認しませんでした. コミュニティの評価、モジュール、各コードを招待します。",
"fedimints.heading": "すべてのフェデレーション", "fedimints.heading": "すべてのフェデレーション",
"fedimints.lede": "ノスト・ネットワークにFedimintのフェデレーションが発表されました。 フェデレーションは、自身のパブリックなステータスのエンドポイントがないため、外部チェックで実行されていることが確認されており、発表した時点で、残りは正確にリストされています。", "fedimints.lede": "ノスト・ネットワークにFedimintのフェデレーションが発表されました。 フェデレーションは、自身のパブリックなステータスのエンドポイントがないため、外部チェックで実行されていることが確認されており、発表した時点で、残りは正確にリストされています。",
@@ -654,5 +678,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 ノードにも到達できず、ミントもメルトもできませんでした。"
} }
+5 -25
View File
@@ -7,33 +7,13 @@
* bare codes means the duplication is two short arrays that cannot drift far, and * bare codes means the duplication is two short arrays that cannot drift far, and
* `scripts/check-i18n.mjs` compares the two on every build so they cannot drift at all. * `scripts/check-i18n.mjs` compares the two on every build so they cannot drift at all.
* *
* Adding a language: add the code here and the full entry in `config.ts`. * Adding a language: the code here, the full entry in `config.ts`, and the import and
* `CATALOGS` line in `index.ts`. The last one is the one that gets forgotten, and the
* only symptom is the new language rendering in English.
*/ */
export const DEFAULT_LOCALE = 'en'; export const DEFAULT_LOCALE = 'en';
export const LOCALE_CODES = [ export const LOCALE_CODES = [
'en', 'en', 'es', 'nl', 'zh', 'hi', 'ar', 'fr', 'pt', 'it', 'tr', 'pl', 'uk', 'ro', 'el', 'cs',
'es', 'sv', 'da', 'id', 'ur', 'ru', 'de', 'ja', 'ar-eg', 'vi',
'nl',
'zh',
'hi',
'ar',
'fr',
'pt',
'it',
'tr',
'pl',
'uk',
'ro',
'el',
'cs',
'sv',
'da',
'id',
'ur',
'ru',
'de',
'ja',
'ar-eg',
'vi',
]; ];
+136 -6
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",
@@ -261,6 +267,9 @@
"reviews.empty": "Nog geen reviews. Schrijf als eerste er een.", "reviews.empty": "Nog geen reviews. Schrijf als eerste er een.",
"reviews.emptyWritten": "Hier staan nog geen geschreven reviews. Wees de eerste die er een schrijft.", "reviews.emptyWritten": "Hier staan nog geen geschreven reviews. Wees de eerste die er een schrijft.",
"reviews.emptyFilter": "Geen enkele review past bij dit filter.", "reviews.emptyFilter": "Geen enkele review past bij dit filter.",
"reviews.rate.cashu": "Beoordeel deze mint",
"reviews.rate.fedimint": "Beoordeel deze federatie",
"reviews.rate.lnurl": "Beoordeel deze mint",
"reviews.noscript": "Reviews worden op Nostr ondertekend en in je browser geladen, dus deze lijst heeft JavaScript nodig. De beoordeling en de aantallen hierboven komen van de site zelf.", "reviews.noscript": "Reviews worden op Nostr ondertekend en in je browser geladen, dus deze lijst heeft JavaScript nodig. De beoordeling en de aantallen hierboven komen van de site zelf.",
"reviews.relayError": "De relay was niet bereikbaar. De reviews zijn misschien niet compleet.", "reviews.relayError": "De relay was niet bereikbaar. De reviews zijn misschien niet compleet.",
"reviews.srRating": "{rating} van de 5", "reviews.srRating": "{rating} van de 5",
@@ -273,7 +282,7 @@
"reviews.anonNote": "Eerste review van deze npub, verder geen activiteit gevonden", "reviews.anonNote": "Eerste review van deze npub, verder geen activiteit gevonden",
"reviews.published": "Gepubliceerd.", "reviews.published": "Gepubliceerd.",
"reviews.publishedPartial": "Gepubliceerd op {accepted} van de {total} relays.", "reviews.publishedPartial": "Gepubliceerd op {accepted} van de {total} relays.",
"reviews.propagating": "Wordt verspreid, het kan even duren voordat hij elders verschijnt.", "reviews.propagating": "Publiceren naar Nostr-relays",
"reviews.summary.line.one": "{count} beoordeling zonder tekst: {breakdown}", "reviews.summary.line.one": "{count} beoordeling zonder tekst: {breakdown}",
"reviews.summary.line.other": "{count} beoordelingen zonder tekst: {breakdown}", "reviews.summary.line.other": "{count} beoordelingen zonder tekst: {breakdown}",
"reviews.summary.lineDay.one": "{count} beoordeling zonder tekst op {when}: {breakdown}", "reviews.summary.lineDay.one": "{count} beoordeling zonder tekst op {when}: {breakdown}",
@@ -287,7 +296,12 @@
"reviews.breakdown.1": "{n} van één ster", "reviews.breakdown.1": "{n} van één ster",
"reviews.breakdown.none": "{n} zonder beoordeling", "reviews.breakdown.none": "{n} zonder beoordeling",
"reviews.dialog.title": "{name} reviewen", "reviews.dialog.title": "{name} reviewen",
"reviews.dialog.note": "Wordt ondertekend met jouw Nostr-sleutel en gepubliceerd op publieke relays. Hij kan niet bewerkt of verwijderd worden.", "reviews.dialog.lede.cashu": "Deel je ervaring met deze mint.",
"reviews.dialog.lede.fedimint": "Deel je ervaring met deze federatie.",
"reviews.dialog.lede.lnurl": "Deel je ervaring met deze mint.",
"reviews.dialog.permanent": "Wordt permanent gepubliceerd op publieke Nostr-relays.",
"reviews.dialog.gate.title": "Log in om een review te schrijven",
"reviews.dialog.gate.lead": "Reviews worden ondertekend met je Nostr-sleutel. Die blijft in je signer, deze site ziet hem nooit.",
"reviews.dialog.ratingLegend": "Jouw beoordeling", "reviews.dialog.ratingLegend": "Jouw beoordeling",
"reviews.dialog.starsSr.one": "{n} ster", "reviews.dialog.starsSr.one": "{n} ster",
"reviews.dialog.starsSr.other": "{n} sterren", "reviews.dialog.starsSr.other": "{n} sterren",
@@ -308,7 +322,6 @@
"reviews.dialog.waitingSigner": "Wachten op je signer", "reviews.dialog.waitingSigner": "Wachten op je signer",
"reviews.dialog.confirmClose": "Deze review sluiten? Je concept blijft bewaard tot je de site verlaat.", "reviews.dialog.confirmClose": "Deze review sluiten? Je concept blijft bewaard tot je de site verlaat.",
"reviews.dialog.why.login": "Log in om te publiceren", "reviews.dialog.why.login": "Log in om te publiceren",
"reviews.dialog.why.rating": "Kies een beoordeling",
"reviews.dialog.why.words": "Schrijf een paar woorden", "reviews.dialog.why.words": "Schrijf een paar woorden",
"reviews.dialog.why.more.one": "nog {n} teken", "reviews.dialog.why.more.one": "nog {n} teken",
"reviews.dialog.why.more.other": "nog {n} tekens", "reviews.dialog.why.more.other": "nog {n} tekens",
@@ -325,7 +338,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",
@@ -369,6 +382,10 @@
"login.status.waitingSigner": "Wachten op je signer", "login.status.waitingSigner": "Wachten op je signer",
"login.status.connecting": "Verbinden met je signer", "login.status.connecting": "Verbinden met je signer",
"login.status.connected": "Verbonden", "login.status.connected": "Verbonden",
"login.toast.loggedIn": "Ingelogd",
"login.toast.fetchingProfile": "Je Nostr-profiel wordt opgehaald bij de relays…",
"login.toast.profile": "Profiel: {name}",
"login.toast.noProfile": "Geen profiel gevonden op de relays",
"login.method.extension": "Extensie", "login.method.extension": "Extensie",
"login.method.remote": "Op afstand", "login.method.remote": "Op afstand",
"login.method.primal": "Primal", "login.method.primal": "Primal",
@@ -400,7 +417,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 +678,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."
} }
+151 -17
View File
@@ -33,26 +33,30 @@
"status.unknown": "Jeszcze nie sprawdzone", "status.unknown": "Jeszcze nie sprawdzone",
"nav.skip": "Przejdź do treści", "nav.skip": "Przejdź do treści",
"nav.main": "Główne", "nav.main": "Główne",
"nav.mints": "Mennice cashu", "nav.mints": "Minty Cashu",
"nav.reviews": "Recenzje", "nav.reviews": "Recenzje",
"nav.wallets": "Portfele", "nav.wallets": "Portfele",
"nav.about": "O nas", "nav.about": "O nas",
"nav.searchMints": "Wyszukaj {n} mennicach", "nav.searchMints": "Szukaj wśród {n} mintów",
"lang.label": "Język", "lang.label": "Język",
"lang.switcher": "Wybierz język", "lang.switcher": "Wybierz język",
"footer.copyright": "© {year} Cashumints.space", "footer.copyright": "© {year} Cashumints.space",
"footer.allMints": "Wszystkie mennice Cashu", "footer.allMints": "Wszystkie minty Cashu",
"footer.allReviews": "Wszystkie recenzje", "footer.allReviews": "Wszystkie recenzje",
"footer.wallets": "Portfele", "footer.wallets": "Portfele",
"footer.about": "O nas", "footer.about": "O nas",
"footer.disclaimer": "Zastrzeżenie", "footer.disclaimer": "Zastrzeżenie",
"footer.terms": "Warunki", "footer.terms": "Warunki",
"footer.privacy": "Prywatność", "footer.privacy": "Prywatność",
"footer.madeBy": "Wykonane przez ❤️ {azzamo} · Napędzana przez Cashu i NIP-87", "footer.madeBy": "Zrobione z ❤️ przez {azzamo} · Napędzane przez Cashu i NIP-87",
"footer.warning": "Mennice są opiekunowe. Mennica może zniknąć razem z twoimi pieniędzmi.", "footer.warning": "Minty są powiernicze. Mint może zniknąć z twoimi pieniędzmi.",
"home.title": "Cashumints.space · Znajdź mennicę Cashu, której możesz zaufać", "footer.tagline": "Recenzje społeczności dotyczące mintów ecash, podpisane w Nostr.",
"home.description": "Znajdź mennicę Cashu, której możesz zaufać. Zindeksowano {mints} mennic z {reviews} opiniami społeczności podpisanymi w Nostr, wraz z bieżącą dostępnością, obsługiwanymi NUT-ami i oprogramowaniem mennicy.", "footer.col.explore": "Przeglądaj",
"home.hero.title": "Znajdź mennicę Cashu", "footer.col.site": "Strona",
"footer.nav": "Stopka",
"home.title": "Cashumints.space · Znajdź mint Cashu, któremu możesz zaufać",
"home.description": "Znajdź mint Cashu, któremu możesz zaufać. {mints} mintów w indeksie, {reviews} recenzji społeczności podpisanych w Nostr, do tego bieżąca dostępność, obsługiwane NUT-y i oprogramowanie mintu.",
"home.hero.title": "Znajdź mint Cashu",
"home.hero.titleEm": "Możesz zaufać", "home.hero.titleEm": "Możesz zaufać",
"home.hero.lede": "Każda moneta w sieci Nostr, z prawdziwymi recenzjami społeczności. Nie ma żadnych strażników, tylko podpisane recenzje, które możesz samodzielnie zweryfikować.", "home.hero.lede": "Każda moneta w sieci Nostr, z prawdziwymi recenzjami społeczności. Nie ma żadnych strażników, tylko podpisane recenzje, które możesz samodzielnie zweryfikować.",
"home.search.label": "Wyszukaj mennicy według nazwy, domeny lub adresu URL", "home.search.label": "Wyszukaj mennicy według nazwy, domeny lub adresu URL",
@@ -91,7 +95,7 @@
"pulse.stale": "Stale", "pulse.stale": "Stale",
"mints.title": "Wszystkie mennice Cashu · Cashumints.space", "mints.title": "Wszystkie mennice Cashu · Cashumints.space",
"mints.description": "Wszystkie mennice Cashu {total} indeksowane w sieci Nostr, {online} online i {offline} offline, z ocenami społecznościowymi, dostępnością i oprogramowaniem do mintowania. Wyszukaj i posortuj pełną listę.", "mints.description": "Wszystkie mennice Cashu {total} indeksowane w sieci Nostr, {online} online i {offline} offline, z ocenami społecznościowymi, dostępnością i oprogramowaniem do mintowania. Wyszukaj i posortuj pełną listę.",
"mints.heading": "Wszystkie mennice Cashu", "mints.heading": "Wszystkie minty Cashu",
"mints.lede": "Każda mennica odkryta w sieci Nostr, włącznie z tymi, które już nie istnieją. Offline minty pozostają na liście, więc nadal można je znaleźć i recenzować.", "mints.lede": "Każda mennica odkryta w sieci Nostr, włącznie z tymi, które już nie istnieją. Offline minty pozostają na liście, więc nadal można je znaleźć i recenzować.",
"mints.search.label": "Wyszukaj mennice według nazwy lub domeny", "mints.search.label": "Wyszukaj mennice według nazwy lub domeny",
"mints.search.placeholder": "Wyszukaj po imieniu lub domenie...", "mints.search.placeholder": "Wyszukaj po imieniu lub domenie...",
@@ -116,6 +120,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",
@@ -277,6 +283,9 @@
"reviews.empty": "Jeszcze brak recenzji. Bądź pierwszym, który go napisze.", "reviews.empty": "Jeszcze brak recenzji. Bądź pierwszym, który go napisze.",
"reviews.emptyWritten": "Na razie nie ma tu żadnych pisemnych recenzji. Bądź pierwszym, który go napisze.", "reviews.emptyWritten": "Na razie nie ma tu żadnych pisemnych recenzji. Bądź pierwszym, który go napisze.",
"reviews.emptyFilter": "Żadna recenzja nie pasuje do tego filtra.", "reviews.emptyFilter": "Żadna recenzja nie pasuje do tego filtra.",
"reviews.rate.cashu": "Oceń ten mint",
"reviews.rate.fedimint": "Oceń tę federację",
"reviews.rate.lnurl": "Oceń ten mint",
"reviews.noscript": "Recenzje są zalogowane w Nostr i ładują się w przeglądarce, więc ta lista wymaga JavaScript. Ocena i liczby powyżej pochodzą z samej strony.", "reviews.noscript": "Recenzje są zalogowane w Nostr i ładują się w przeglądarce, więc ta lista wymaga JavaScript. Ocena i liczby powyżej pochodzą z samej strony.",
"reviews.relayError": "Nie udało się dołączyć do przekaźnika. Recenzje mogą być niekompletne.", "reviews.relayError": "Nie udało się dołączyć do przekaźnika. Recenzje mogą być niekompletne.",
"reviews.srRating": "{rating} na 5", "reviews.srRating": "{rating} na 5",
@@ -289,7 +298,7 @@
"reviews.anonNote": "Pierwsza recenzja z tego npubu, nie znaleziono żadnej innej aktywności", "reviews.anonNote": "Pierwsza recenzja z tego npubu, nie znaleziono żadnej innej aktywności",
"reviews.published": "Opublikowano.", "reviews.published": "Opublikowano.",
"reviews.publishedPartial": "Opublikowano dla {accepted} sztafetyi {total}.", "reviews.publishedPartial": "Opublikowano dla {accepted} sztafetyi {total}.",
"reviews.propagating": "Rozmnażając, może potrzebować chwili, by pojawić się gdzie indziej.", "reviews.propagating": "Publikowanie na relayach Nostr",
"reviews.summary.line.one": "{count} ocena bez komentarza: {breakdown}", "reviews.summary.line.one": "{count} ocena bez komentarza: {breakdown}",
"reviews.summary.line.few": "{count} oceny bez komentarza: {breakdown}", "reviews.summary.line.few": "{count} oceny bez komentarza: {breakdown}",
"reviews.summary.line.many": "{count} ocen bez komentarza: {breakdown}", "reviews.summary.line.many": "{count} ocen bez komentarza: {breakdown}",
@@ -307,7 +316,12 @@
"reviews.breakdown.1": "{n} jedną gwiazdką", "reviews.breakdown.1": "{n} jedną gwiazdką",
"reviews.breakdown.none": "{n} bez oceny", "reviews.breakdown.none": "{n} bez oceny",
"reviews.dialog.title": "Recenzja {name}", "reviews.dialog.title": "Recenzja {name}",
"reviews.dialog.note": "Podpisany kluczem Nostr i opublikowany w publicznych przekaźnikach. Nie można go edytować ani usuwać.", "reviews.dialog.lede.cashu": "Podziel się swoim doświadczeniem z tym mintem.",
"reviews.dialog.lede.fedimint": "Podziel się swoim doświadczeniem z tą federacją.",
"reviews.dialog.lede.lnurl": "Podziel się swoim doświadczeniem z tym mintem.",
"reviews.dialog.permanent": "Publikowane na stałe w publicznych relayach Nostr.",
"reviews.dialog.gate.title": "Zaloguj się, aby napisać recenzję",
"reviews.dialog.gate.lead": "Recenzje są podpisywane twoim kluczem Nostr. Zostaje on w twoim signerze, ta strona nigdy go nie widzi.",
"reviews.dialog.ratingLegend": "Twoja ocena", "reviews.dialog.ratingLegend": "Twoja ocena",
"reviews.dialog.starsSr.one": "{n} gwiazda", "reviews.dialog.starsSr.one": "{n} gwiazda",
"reviews.dialog.starsSr.few": "{n} gwiazdki", "reviews.dialog.starsSr.few": "{n} gwiazdki",
@@ -330,7 +344,6 @@
"reviews.dialog.waitingSigner": "Czekam na swojego podpisującego", "reviews.dialog.waitingSigner": "Czekam na swojego podpisującego",
"reviews.dialog.confirmClose": "Zamknąć tę recenzję? Twój szkic jest przechowywany do momentu opuszczenia strony.", "reviews.dialog.confirmClose": "Zamknąć tę recenzję? Twój szkic jest przechowywany do momentu opuszczenia strony.",
"reviews.dialog.why.login": "Zaloguj się, aby opublikować", "reviews.dialog.why.login": "Zaloguj się, aby opublikować",
"reviews.dialog.why.rating": "Wybierz ocenę",
"reviews.dialog.why.words": "Napisz kilka słów", "reviews.dialog.why.words": "Napisz kilka słów",
"reviews.dialog.why.more.one": "{n} więcej charakteru", "reviews.dialog.why.more.one": "{n} więcej charakteru",
"reviews.dialog.why.more.few": "{n} kolejne postacie", "reviews.dialog.why.more.few": "{n} kolejne postacie",
@@ -351,7 +364,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",
@@ -397,6 +410,10 @@
"login.status.waitingSigner": "Czekam na swojego podpisującego", "login.status.waitingSigner": "Czekam na swojego podpisującego",
"login.status.connecting": "Łączenie się z Twoim sygnatariuszem", "login.status.connecting": "Łączenie się z Twoim sygnatariuszem",
"login.status.connected": "Połączone", "login.status.connected": "Połączone",
"login.toast.loggedIn": "Zalogowano",
"login.toast.fetchingProfile": "Pobieranie Twojego profilu Nostr z przekaźników…",
"login.toast.profile": "Profil: {name}",
"login.toast.noProfile": "Nie znaleziono profilu na przekaźnikach",
"login.method.extension": "Rozszerzenie", "login.method.extension": "Rozszerzenie",
"login.method.remote": "Zdalne", "login.method.remote": "Zdalne",
"login.method.primal": "Primal", "login.method.primal": "Primal",
@@ -428,7 +445,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",
@@ -602,10 +626,10 @@
"seo.mint.ogAlt.many": "{name}: {rating} gwiazdek z {n} recenzji na Cashumints.space", "seo.mint.ogAlt.many": "{name}: {rating} gwiazdek z {n} recenzji na Cashumints.space",
"seo.mint.ogAlt.other": "{name}: {rating} gwiazdek z {n} recenzji na Cashumints.space", "seo.mint.ogAlt.other": "{name}: {rating} gwiazdek z {n} recenzji na Cashumints.space",
"seo.mint.ogAltNoReviews": "{name} on Cashumints.space", "seo.mint.ogAltNoReviews": "{name} on Cashumints.space",
"nav.home": "Dom", "nav.home": "Strona główna",
"mint.more.title": "Więcej mennic", "mint.more.title": "Więcej mennic",
"mint.more.all": "Zobacz wszystkie mennice", "mint.more.all": "Zobacz wszystkie mennice",
"nav.fedimints": "Fediminty", "nav.fedimints": "Fedimints",
"status.announced": "Tylko ogłoszone", "status.announced": "Tylko ogłoszone",
"card.unnamedFederation": "Nienazwana federacja", "card.unnamedFederation": "Nienazwana federacja",
"card.notChecked": "Nigdy nie sprawdzałem", "card.notChecked": "Nigdy nie sprawdzałem",
@@ -696,5 +720,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ć."
} }
+143 -13
View File
@@ -37,11 +37,11 @@
"nav.reviews": "Avaliações", "nav.reviews": "Avaliações",
"nav.wallets": "Carteiras", "nav.wallets": "Carteiras",
"nav.about": "Sobre", "nav.about": "Sobre",
"nav.searchMints": "Pesquisar {n} mints", "nav.searchMints": "Pesquisar entre {n} mints",
"lang.label": "Linguagem", "lang.label": "Linguagem",
"lang.switcher": "Escolha um idioma", "lang.switcher": "Escolha um idioma",
"footer.copyright": "© {year} Cashumints.space", "footer.copyright": "© {year} Cashumints.space",
"footer.allMints": "Todos Cashu mints", "footer.allMints": "Todos os mints Cashu",
"footer.allReviews": "Todas as avaliações", "footer.allReviews": "Todas as avaliações",
"footer.wallets": "Carteiras", "footer.wallets": "Carteiras",
"footer.about": "Sobre", "footer.about": "Sobre",
@@ -50,9 +50,13 @@
"footer.privacy": "Privacidade", "footer.privacy": "Privacidade",
"footer.madeBy": "Feito com ❤️ por {azzamo} · Desenvolvido por Cashu e NIP-87", "footer.madeBy": "Feito com ❤️ por {azzamo} · Desenvolvido por Cashu e NIP-87",
"footer.warning": "Mints são de custódia. Um mint pode desaparecer com o seu dinheiro.", "footer.warning": "Mints são de custódia. Um mint pode desaparecer com o seu dinheiro.",
"home.title": "Cashumints.space · Encontre um Cashu mint em que você pode confiar", "footer.tagline": "Avaliações da comunidade sobre mints ecash, assinadas no Nostr.",
"home.description": "Encontre um Cashu mint em que você possa confiar. {mints} mints indexado com análises da comunidade {reviews} assinadas em Nostr, além de tempo de atividade ao vivo, software NUTs e mint compatível.", "footer.col.explore": "Explorar",
"home.hero.title": "Encontre um Cashu mint", "footer.col.site": "Site",
"footer.nav": "Rodapé",
"home.title": "Cashumints.space · Encontre um mint Cashu em que você possa confiar",
"home.description": "Encontre um mint Cashu em que você possa confiar. {mints} mints indexados com {reviews} avaliações da comunidade assinadas no Nostr, além de disponibilidade ao vivo, NUTs suportados e software do mint.",
"home.hero.title": "Encontre um mint Cashu",
"home.hero.titleEm": "você pode confiar", "home.hero.titleEm": "você pode confiar",
"home.hero.lede": "Cada mint na rede Nostr, com análises reais da comunidade. Sem guardiões, apenas comentários assinados que você mesmo pode verificar.", "home.hero.lede": "Cada mint na rede Nostr, com análises reais da comunidade. Sem guardiões, apenas comentários assinados que você mesmo pode verificar.",
"home.search.label": "Pesquise mints por nome, domínio ou URL", "home.search.label": "Pesquise mints por nome, domínio ou URL",
@@ -91,7 +95,7 @@
"pulse.stale": "obsoleto", "pulse.stale": "obsoleto",
"mints.title": "Todos Cashu mints · Cashumints.space", "mints.title": "Todos Cashu mints · Cashumints.space",
"mints.description": "Todos os {total} Cashu mints indexados na rede Nostr, {online} online e {offline} offline, com classificações da comunidade, tempo de atividade e software mint. Pesquise e classifique a lista completa.", "mints.description": "Todos os {total} Cashu mints indexados na rede Nostr, {online} online e {offline} offline, com classificações da comunidade, tempo de atividade e software mint. Pesquise e classifique a lista completa.",
"mints.heading": "Todos Cashu mints", "mints.heading": "Todos os mints Cashu",
"mints.lede": "Cada mint descoberto na rede Nostr, incluindo aqueles que desapareceram. mints off-line permanecem listados para que você ainda possa encontrá-los e revisá-los.", "mints.lede": "Cada mint descoberto na rede Nostr, incluindo aqueles que desapareceram. mints off-line permanecem listados para que você ainda possa encontrá-los e revisá-los.",
"mints.search.label": "Pesquise mints por nome ou domínio", "mints.search.label": "Pesquise mints por nome ou domínio",
"mints.search.placeholder": "Pesquise por nome ou domínio…", "mints.search.placeholder": "Pesquise por nome ou domínio…",
@@ -114,6 +118,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",
@@ -261,6 +267,9 @@
"reviews.empty": "Ainda não há comentários. Seja o primeiro a escrever um.", "reviews.empty": "Ainda não há comentários. Seja o primeiro a escrever um.",
"reviews.emptyWritten": "Ainda não há comentários escritos aqui. Seja o primeiro a escrever um.", "reviews.emptyWritten": "Ainda não há comentários escritos aqui. Seja o primeiro a escrever um.",
"reviews.emptyFilter": "Nenhuma avaliação corresponde a este filtro.", "reviews.emptyFilter": "Nenhuma avaliação corresponde a este filtro.",
"reviews.rate.cashu": "Avalie este mint",
"reviews.rate.fedimint": "Avalie esta federação",
"reviews.rate.lnurl": "Avalie este mint",
"reviews.noscript": "As avaliações são assinadas em Nostr e carregadas em seu navegador, portanto, esta lista precisa de JavaScript. A classificação e as contagens acima vêm do próprio site.", "reviews.noscript": "As avaliações são assinadas em Nostr e carregadas em seu navegador, portanto, esta lista precisa de JavaScript. A classificação e as contagens acima vêm do próprio site.",
"reviews.relayError": "Não foi possível alcançar o relé. As avaliações podem estar incompletas.", "reviews.relayError": "Não foi possível alcançar o relé. As avaliações podem estar incompletas.",
"reviews.srRating": "{rating} de 5", "reviews.srRating": "{rating} de 5",
@@ -273,7 +282,7 @@
"reviews.anonNote": "Primeira revisão deste npub, nenhuma outra atividade encontrada", "reviews.anonNote": "Primeira revisão deste npub, nenhuma outra atividade encontrada",
"reviews.published": "Publicado.", "reviews.published": "Publicado.",
"reviews.publishedPartial": "Publicado em {accepted} dos relés {total}.", "reviews.publishedPartial": "Publicado em {accepted} dos relés {total}.",
"reviews.propagating": "Propagando, pode demorar um pouco para aparecer em outro lugar.", "reviews.propagating": "Publicando nos relays Nostr",
"reviews.summary.line.one": "Classificação {count} sem comentários: {breakdown}", "reviews.summary.line.one": "Classificação {count} sem comentários: {breakdown}",
"reviews.summary.line.other": "Avaliações {count} sem comentários: {breakdown}", "reviews.summary.line.other": "Avaliações {count} sem comentários: {breakdown}",
"reviews.summary.lineDay.one": "Classificação {count} sem comentários em {when}: {breakdown}", "reviews.summary.lineDay.one": "Classificação {count} sem comentários em {when}: {breakdown}",
@@ -287,7 +296,12 @@
"reviews.breakdown.1": "{n} uma estrela", "reviews.breakdown.1": "{n} uma estrela",
"reviews.breakdown.none": "{n} sem classificação", "reviews.breakdown.none": "{n} sem classificação",
"reviews.dialog.title": "Revisão {name}", "reviews.dialog.title": "Revisão {name}",
"reviews.dialog.note": "Assinado com sua chave Nostr e publicado em retransmissores públicos. Não pode ser editado ou excluído.", "reviews.dialog.lede.cashu": "Compartilhe sua experiência com este mint.",
"reviews.dialog.lede.fedimint": "Compartilhe sua experiência com esta federação.",
"reviews.dialog.lede.lnurl": "Compartilhe sua experiência com este mint.",
"reviews.dialog.permanent": "Publicado permanentemente em relays públicos do Nostr.",
"reviews.dialog.gate.title": "Faça login para escrever um comentário",
"reviews.dialog.gate.lead": "Os comentários são assinados com sua chave Nostr. Ela permanece no seu assinante, este site nunca a vê.",
"reviews.dialog.ratingLegend": "Sua classificação", "reviews.dialog.ratingLegend": "Sua classificação",
"reviews.dialog.starsSr.one": "Estrela {n}", "reviews.dialog.starsSr.one": "Estrela {n}",
"reviews.dialog.starsSr.other": "Estrelas {n}", "reviews.dialog.starsSr.other": "Estrelas {n}",
@@ -308,7 +322,6 @@
"reviews.dialog.waitingSigner": "Esperando pelo seu signatário", "reviews.dialog.waitingSigner": "Esperando pelo seu signatário",
"reviews.dialog.confirmClose": "Fechar esta avaliação? Seu rascunho é mantido até você sair do site.", "reviews.dialog.confirmClose": "Fechar esta avaliação? Seu rascunho é mantido até você sair do site.",
"reviews.dialog.why.login": "Faça login para publicar", "reviews.dialog.why.login": "Faça login para publicar",
"reviews.dialog.why.rating": "Escolha uma classificação",
"reviews.dialog.why.words": "Escreva algumas palavras", "reviews.dialog.why.words": "Escreva algumas palavras",
"reviews.dialog.why.more.one": "{n} mais caráter", "reviews.dialog.why.more.one": "{n} mais caráter",
"reviews.dialog.why.more.other": "{n} mais caracteres", "reviews.dialog.why.more.other": "{n} mais caracteres",
@@ -325,7 +338,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",
@@ -369,6 +382,10 @@
"login.status.waitingSigner": "Esperando pelo seu signatário", "login.status.waitingSigner": "Esperando pelo seu signatário",
"login.status.connecting": "Conectando-se ao seu signatário", "login.status.connecting": "Conectando-se ao seu signatário",
"login.status.connected": "Conectado", "login.status.connected": "Conectado",
"login.toast.loggedIn": "Sessão iniciada",
"login.toast.fetchingProfile": "Obtendo o seu perfil Nostr dos relés…",
"login.toast.profile": "Perfil: {name}",
"login.toast.noProfile": "Nenhum perfil encontrado nos relés",
"login.method.extension": "Extensão", "login.method.extension": "Extensão",
"login.method.remote": "Remoto", "login.method.remote": "Remoto",
"login.method.primal": "Primal", "login.method.primal": "Primal",
@@ -400,7 +417,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",
@@ -566,7 +590,7 @@
"seo.mint.ogAlt.one": "{name}: {rating} estrelas da análise de {n} em Cashumints.space", "seo.mint.ogAlt.one": "{name}: {rating} estrelas da análise de {n} em Cashumints.space",
"seo.mint.ogAlt.other": "{name}: {rating} estrelas de comentários de {n} em Cashumints.space", "seo.mint.ogAlt.other": "{name}: {rating} estrelas de comentários de {n} em Cashumints.space",
"seo.mint.ogAltNoReviews": "{name} em Cashumints.space", "seo.mint.ogAltNoReviews": "{name} em Cashumints.space",
"nav.home": "Lar", "nav.home": "Início",
"mint.more.title": "Mais mints", "mint.more.title": "Mais mints",
"mint.more.all": "Ver todos os mints", "mint.more.all": "Ver todos os mints",
"nav.fedimints": "Fedimints", "nav.fedimints": "Fedimints",
@@ -654,5 +678,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."
} }
+166 -34
View File
@@ -1,5 +1,5 @@
{ {
"site.name": "Casumints.space", "site.name": "Cashumints.space",
"common.close": "Închide", "common.close": "Închide",
"common.cancel": "Anulează", "common.cancel": "Anulează",
"common.done": "Gata", "common.done": "Gata",
@@ -33,26 +33,30 @@
"status.unknown": "Încă nu s-a verificat.", "status.unknown": "Încă nu s-a verificat.",
"nav.skip": "Sari la conținut", "nav.skip": "Sari la conținut",
"nav.main": "Principal", "nav.main": "Principal",
"nav.mints": "Caju muntetării", "nav.mints": "Mint-uri Cashu",
"nav.reviews": "Revizuiri", "nav.reviews": "Recenzii",
"nav.wallets": "Portofele", "nav.wallets": "Portofele",
"nav.about": "Despre", "nav.about": "Despre",
"nav.searchMints": "Caută{n}Monetării", "nav.searchMints": "Caută printre {n} mint-uri",
"lang.label": "Limbă", "lang.label": "Limbă",
"lang.switcher": "Alege o limbă", "lang.switcher": "Alege o limbă",
"footer.copyright": "©{year}Casumints.space", "footer.copyright": "© {year} Cashumints.space",
"footer.allMints": "All Cashu monetării", "footer.allMints": "Toate mint-urile Cashu",
"footer.allReviews": "Toate recenziile", "footer.allReviews": "Toate recenziile",
"footer.wallets": "Portofele", "footer.wallets": "Portofele",
"footer.about": "Despre", "footer.about": "Despre",
"footer.disclaimer": "Retragere", "footer.disclaimer": "Declinarea responsabilității",
"footer.terms": "Termeni", "footer.terms": "Termeni",
"footer.privacy": "Confidenţialitatea", "footer.privacy": "Confidenţialitatea",
"footer.madeBy": "Fabricat cu {azzamo}· Alimentat de Cashu și NIP-87", "footer.madeBy": "Făcut cu ❤️ de {azzamo} · Susținut de Cashu și NIP-87",
"footer.warning": "Monetari sunt custodia. O monetărie poate dispărea cu banii tăi.", "footer.warning": "Mint-urile sunt custodiale. Un mint poate dispărea cu banii tăi.",
"home.title": "Casumints.space · Găsește un Cashu monetărie în care poți avea încredere", "footer.tagline": "Recenzii ale comunității pentru mint-urile ecash, semnate pe Nostr.",
"home.description": "Găsește o monetărie Cashu în care poți avea încredere. {mints} monetării indexate, cu {reviews} recenzii ale comunității semnate pe Nostr, plus disponibilitate în timp real, NUT-uri acceptate și software-ul monetăriei.", "footer.col.explore": "Explorează",
"home.hero.title": "Găsiți un Cashu monetărie", "footer.col.site": "Site",
"footer.nav": "Subsol",
"home.title": "Cashumints.space · Găsește un mint Cashu în care poți avea încredere",
"home.description": "Găsește un mint Cashu în care poți avea încredere. {mints} mint-uri indexate, cu {reviews} recenzii ale comunității semnate pe Nostr, plus disponibilitate în timp real, NUT-uri acceptate și software-ul mintului.",
"home.hero.title": "Găsește un mint Cashu",
"home.hero.titleEm": "Poţi avea încredere în tine.", "home.hero.titleEm": "Poţi avea încredere în tine.",
"home.hero.lede": "Fiecare monetărie de pe reţeaua Nostr, cu comentarii comunitare reale, fără paznici, doar recenzii semnate pe care le puteţi verifica.", "home.hero.lede": "Fiecare monetărie de pe reţeaua Nostr, cu comentarii comunitare reale, fără paznici, doar recenzii semnate pe care le puteţi verifica.",
"home.search.label": "Caută monetării după nume, domeniu sau URL", "home.search.label": "Caută monetării după nume, domeniu sau URL",
@@ -91,7 +95,7 @@
"pulse.stale": "vechi", "pulse.stale": "vechi",
"mints.title": "All Cashu monetării · Cashumints.space", "mints.title": "All Cashu monetării · Cashumints.space",
"mints.description": "Toate{total}Cashu monetării indexate din rețeaua Nostr;{online}online și{offline}Offline, cu evaluări comunitare, uptime și software-ul monetărie. Caută și sortează lista completă.", "mints.description": "Toate{total}Cashu monetării indexate din rețeaua Nostr;{online}online și{offline}Offline, cu evaluări comunitare, uptime și software-ul monetărie. Caută și sortează lista completă.",
"mints.heading": "All Cashu monetării", "mints.heading": "Toate mint-urile Cashu",
"mints.lede": "Fiecare monetărie descoperită pe rețeaua Nostr, inclusiv cele care au dispărut. Offline monetării se află pe listă, astfel încât să puteți găsi și să le revizuiască.", "mints.lede": "Fiecare monetărie descoperită pe rețeaua Nostr, inclusiv cele care au dispărut. Offline monetării se află pe listă, astfel încât să puteți găsi și să le revizuiască.",
"mints.search.label": "Caută după nume sau domeniu", "mints.search.label": "Caută după nume sau domeniu",
"mints.search.placeholder": "Caută după nume sau domeniu...", "mints.search.placeholder": "Caută după nume sau domeniu...",
@@ -115,6 +119,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",
@@ -269,6 +275,9 @@
"reviews.empty": "Fii primul care scrie una.", "reviews.empty": "Fii primul care scrie una.",
"reviews.emptyWritten": "Nu scriem recenzii aici încă.", "reviews.emptyWritten": "Nu scriem recenzii aici încă.",
"reviews.emptyFilter": "Nici o recenzie nu se potriveşte cu acest filtru.", "reviews.emptyFilter": "Nici o recenzie nu se potriveşte cu acest filtru.",
"reviews.rate.cashu": "Evaluează acest mint",
"reviews.rate.fedimint": "Evaluează această federație",
"reviews.rate.lnurl": "Evaluează acest mint",
"reviews.noscript": "Recenziile sunt semnate pe Nostr și încărcați în browser-ul dvs., astfel încât această listă are nevoie de JavaScript. Evaluarea și numărul de mai sus provin de la site-ul în sine.", "reviews.noscript": "Recenziile sunt semnate pe Nostr și încărcați în browser-ul dvs., astfel încât această listă are nevoie de JavaScript. Evaluarea și numărul de mai sus provin de la site-ul în sine.",
"reviews.relayError": "Nu am putut ajunge la releu. Recenziile pot fi incomplete.", "reviews.relayError": "Nu am putut ajunge la releu. Recenziile pot fi incomplete.",
"reviews.srRating": "{rating} din 5", "reviews.srRating": "{rating} din 5",
@@ -281,7 +290,7 @@
"reviews.anonNote": "Prima revizuire a acestei npub-uri, nicio altă activitate nu a fost găsită", "reviews.anonNote": "Prima revizuire a acestei npub-uri, nicio altă activitate nu a fost găsită",
"reviews.published": "Publicat.", "reviews.published": "Publicat.",
"reviews.publishedPartial": "Publicată în{accepted} din{total} relee.", "reviews.publishedPartial": "Publicată în{accepted} din{total} relee.",
"reviews.propagating": "Propaganda, poate dura un moment să apară în altă parte.", "reviews.propagating": "Se publică pe relayurile Nostr",
"reviews.summary.line.one": "{count}rating fără comentarii:{breakdown}", "reviews.summary.line.one": "{count}rating fără comentarii:{breakdown}",
"reviews.summary.line.other": "{count} ratinguri fără comentarii:{breakdown}", "reviews.summary.line.other": "{count} ratinguri fără comentarii:{breakdown}",
"reviews.summary.line.few": "{count} ratinguri fără comentarii:{breakdown}", "reviews.summary.line.few": "{count} ratinguri fără comentarii:{breakdown}",
@@ -297,7 +306,12 @@
"reviews.breakdown.1": "{n}o stea", "reviews.breakdown.1": "{n}o stea",
"reviews.breakdown.none": "{n} fără rating", "reviews.breakdown.none": "{n} fără rating",
"reviews.dialog.title": "Reexaminare{name}", "reviews.dialog.title": "Reexaminare{name}",
"reviews.dialog.note": "Semnat cu tasta Nostr și publicat la relee publice. Nu poate fi editat sau șters.", "reviews.dialog.lede.cashu": "Împărtășește experiența ta cu acest mint.",
"reviews.dialog.lede.fedimint": "Împărtășește experiența ta cu această federație.",
"reviews.dialog.lede.lnurl": "Împărtășește experiența ta cu acest mint.",
"reviews.dialog.permanent": "Publicat permanent pe relayuri Nostr publice.",
"reviews.dialog.gate.title": "Autentifică-te pentru a scrie un comentariu",
"reviews.dialog.gate.lead": "Recenziile sunt semnate cu cheia ta Nostr. Rămâne în signerul tău, acest site nu o vede niciodată.",
"reviews.dialog.ratingLegend": "Evaluarea dvs.", "reviews.dialog.ratingLegend": "Evaluarea dvs.",
"reviews.dialog.starsSr.one": "{n} stea", "reviews.dialog.starsSr.one": "{n} stea",
"reviews.dialog.starsSr.other": "{n} stele", "reviews.dialog.starsSr.other": "{n} stele",
@@ -319,7 +333,6 @@
"reviews.dialog.waitingSigner": "Aştept semnalul tău.", "reviews.dialog.waitingSigner": "Aştept semnalul tău.",
"reviews.dialog.confirmClose": "Ţi-ai păstrat proiectul până când ai plecat de pe site.", "reviews.dialog.confirmClose": "Ţi-ai păstrat proiectul până când ai plecat de pe site.",
"reviews.dialog.why.login": "Autentifică-te pentru publicare", "reviews.dialog.why.login": "Autentifică-te pentru publicare",
"reviews.dialog.why.rating": "Alegeți un rating",
"reviews.dialog.why.words": "Scrie câteva cuvinte.", "reviews.dialog.why.words": "Scrie câteva cuvinte.",
"reviews.dialog.why.more.one": "{n} mai mult caracter", "reviews.dialog.why.more.one": "{n} mai mult caracter",
"reviews.dialog.why.more.other": "{n} mai multe caractere", "reviews.dialog.why.more.other": "{n} mai multe caractere",
@@ -337,8 +350,8 @@
"reviews.validate.rating": "Alegeți un rating de la 1 la 5 stele.", "reviews.validate.rating": "Alegeți un rating de la 1 la 5 stele.",
"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": "Cele mai recente recenzii de mint-uri și federații · Cashumints.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",
@@ -383,6 +396,10 @@
"login.status.waitingSigner": "Aştept semnalul tău.", "login.status.waitingSigner": "Aştept semnalul tău.",
"login.status.connecting": "Conectarea la signer", "login.status.connecting": "Conectarea la signer",
"login.status.connected": "Conectat", "login.status.connected": "Conectat",
"login.toast.loggedIn": "Autentificat",
"login.toast.fetchingProfile": "Se preia profilul tău Nostr de la relee…",
"login.toast.profile": "Profil: {name}",
"login.toast.noProfile": "Niciun profil găsit pe relee",
"login.method.extension": "Extensie", "login.method.extension": "Extensie",
"login.method.remote": "Remote", "login.method.remote": "Remote",
"login.method.primal": "Primal", "login.method.primal": "Primal",
@@ -403,7 +420,7 @@
"signer.error.timeout": "Semnatorul nu a răspuns la timp. Deschide-l, verifică-l încă este conectat, apoi încearcă din nou.", "signer.error.timeout": "Semnatorul nu a răspuns la timp. Deschide-l, verifică-l încă este conectat, apoi încearcă din nou.",
"signer.error.cancelled": "Conexiune anulată.", "signer.error.cancelled": "Conexiune anulată.",
"signer.error.declined": "Cererea de semnătură a fost respinsă.", "signer.error.declined": "Cererea de semnătură a fost respinsă.",
"notfound.title": "Pagina nu a fost găsită · Casumints.space", "notfound.title": "Pagina nu a fost găsită · Cashumints.space",
"notfound.description": "Această pagină nu există pe Cashumints.space.", "notfound.description": "Această pagină nu există pe Cashumints.space.",
"notfound.heading": "Această pagină nu există", "notfound.heading": "Această pagină nu există",
"notfound.lede": "Este posibil să fi mutat, sau monetărie nu poate fi indexat încă. Caută o monetărie după nume sau domeniu, sau lipește un URL monetărie.", "notfound.lede": "Este posibil să fi mutat, sau monetărie nu poate fi indexat încă. Caută o monetărie după nume sau domeniu, sau lipește un URL monetărie.",
@@ -414,8 +431,15 @@
"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.",
"wallets.title": "Portofele caju · Casumints.space", "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 Cashu · Cashumints.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",
"wallets.lede": "Aveți nevoie de un portofel pentru a deține ecash de la oricare dintre aceste monetării. Cele mai multe portofele vorbesc cu mai multe monetării dintr-o dată, astfel încât să nu sunt legate de primul pe care încercați.", "wallets.lede": "Aveți nevoie de un portofel pentru a deține ecash de la oricare dintre aceste monetării. Cele mai multe portofele vorbesc cu mai multe monetării dintr-o dată, astfel încât să nu sunt legate de primul pe care încercați.",
@@ -437,7 +461,7 @@
"about.title": "Despre Cashumints.space", "about.title": "Despre Cashumints.space",
"about.description": "Ce este Cashu, ce deține de fapt o monetărie, ce este NIP-87, și cum acest site indexează monetării și citește recenzii de la releele Nostr.", "about.description": "Ce este Cashu, ce deține de fapt o monetărie, ce este NIP-87, și cum acest site indexează monetării și citește recenzii de la releele Nostr.",
"about.heading": "Despre", "about.heading": "Despre",
"about.lede": "Casumints.space enumeră fiecare Cashu monetărie pe care o putem găsi pe rețeaua Nostr și vă arată ce spun oamenii despre ele. Este făcută de Azzamo.", "about.lede": "Cashumints.space enumeră fiecare mint Cashu pe care îl putem găsi în rețeaua Nostr și îți arată ce spun oamenii despre ele. Este făcut de Azzamo.",
"about.cashu.title": "Ce este Cashu", "about.cashu.title": "Ce este Cashu",
"about.cashu.p1": "Cashu este un protocol de cash pentru Bitcoin. Un monetărie probleme orbite jetoane susținute de sat-uri ea deține, și acele jetoane se mișcă între oameni fără monetărie învățare care a trimis la ce. Confidențialitatea este reală și este punctul: jetoane nu sunt conturi, iar monetăria nu poate lega jetonul semnat cu jetonul care vine înapoi.", "about.cashu.p1": "Cashu este un protocol de cash pentru Bitcoin. Un monetărie probleme orbite jetoane susținute de sat-uri ea deține, și acele jetoane se mișcă între oameni fără monetărie învățare care a trimis la ce. Confidențialitatea este reală și este punctul: jetoane nu sunt conturi, iar monetăria nu poate lega jetonul semnat cu jetonul care vine înapoi.",
"about.cashu.p2": "Puteți citi protocolul și specificațiile sale la{link}. Specificaţiile în sine se numesc NUT-uri, de unde provin numerele NUT de pe fiecare pagină monetărie.", "about.cashu.p2": "Puteți citi protocolul și specificațiile sale la{link}. Specificaţiile în sine se numesc NUT-uri, de unde provin numerele NUT de pe fiecare pagină monetărie.",
@@ -466,7 +490,7 @@
"about.numbers.p1": "{mints} monetării indexate, dintre care {online} răspund, și {reviews} recenzii numărate din releele de mai sus.", "about.numbers.p1": "{mints} monetării indexate, dintre care {online} răspund, și {reviews} recenzii numărate din releele de mai sus.",
"about.source.title": "Sursa și creditul", "about.source.title": "Sursa și creditul",
"about.source.p1": "Site-ul este open source la{github} Este construit și condus de{azzamo}. Dacă o pagină de monetărie are ceva greșit pe ea, fixați-l la sursă: metadate monetărie vine de la propria monetărie{code}, și recenzii provin de la Nostr.", "about.source.p1": "Site-ul este open source la{github} Este construit și condus de{azzamo}. Dacă o pagină de monetărie are ceva greșit pe ea, fixați-l la sursă: metadate monetărie vine de la propria monetărie{code}, și recenzii provin de la Nostr.",
"terms.title": "Termeni de utilizare · Casumints.space", "terms.title": "Termeni de utilizare · Cashumints.space",
"terms.description": "Termeni de utilizare pentru Cashumints.space: ce este site-ul, ce nu face, și cine este responsabil pentru ce. Fără conturi, fără garanții, fără afiliere la orice monetărie.", "terms.description": "Termeni de utilizare pentru Cashumints.space: ce este site-ul, ce nu face, și cine este responsabil pentru ce. Fără conturi, fără garanții, fără afiliere la orice monetărie.",
"terms.heading": "Termeni de utilizare", "terms.heading": "Termeni de utilizare",
"terms.lede": "Termeni simpli pentru un site care indexează doar datele publice. Ultima actualizare{date}.", "terms.lede": "Termeni simpli pentru un site care indexează doar datele publice. Ultima actualizare{date}.",
@@ -493,7 +517,7 @@
"terms.changes.p1": "Acesti termeni se pot schimba. Data de la partea de sus spune când au făcut-o ultima dată. Site-ul este open source, astfel încât fiecare schimbare a acestei pagini este în istoria comite.", "terms.changes.p1": "Acesti termeni se pot schimba. Data de la partea de sus spune când au făcut-o ultima dată. Site-ul este open source, astfel încât fiecare schimbare a acestei pagini este în istoria comite.",
"terms.contact.title": "Contact", "terms.contact.title": "Contact",
"terms.contact.p1": "Problemele și cererile de preluare sunt cele mai bune depuse pe{github}Site-ul este condus de{azzamo}.", "terms.contact.p1": "Problemele și cererile de preluare sunt cele mai bune depuse pe{github}Site-ul este condus de{azzamo}.",
"privacy.title": "Privacy · Casumints.space", "privacy.title": "Confidențialitate · Cashumints.space",
"privacy.description": "Privacy on Cashumints.space: no accounts, no tracking cookies, no analytics. localStorage is used for profile caching and nothing is end anywhere. Reviews are signed in your own browser.", "privacy.description": "Privacy on Cashumints.space: no accounts, no tracking cookies, no analytics. localStorage is used for profile caching and nothing is end anywhere. Reviews are signed in your own browser.",
"privacy.heading": "Confidenţialitatea", "privacy.heading": "Confidenţialitatea",
"privacy.lede": "Noi colecta cât mai puțin ca un site web poate. Ultima actualizare{date}.", "privacy.lede": "Noi colecta cât mai puțin ca un site web poate. Ultima actualizare{date}.",
@@ -528,7 +552,7 @@
"privacy.permanent.p2": "Dacă doriți să revizuiți o monetărie fără ca aceasta să fie legată de identitatea dumneavoastră principală, utilizați o cheie Nost separat. Rețineți că o cheie fără altă activitate este afișată ca atare pe recenzii dure, pentru că o cheie de brand nou scris o recenzie o stea este un cititor de model merită să vadă.", "privacy.permanent.p2": "Dacă doriți să revizuiți o monetărie fără ca aceasta să fie legată de identitatea dumneavoastră principală, utilizați o cheie Nost separat. Rețineți că o cheie fără altă activitate este afișată ca atare pe recenzii dure, pentru că o cheie de brand nou scris o recenzie o stea este un cititor de model merită să vadă.",
"privacy.contact.title": "Întrebări", "privacy.contact.title": "Întrebări",
"privacy.contact.p1": "Site-ul este open source: dacă doriți să verificați oricare dintre cele de mai sus, citiți codul la{github} Întrebările şi corecţiile pot intra într-o problemă.", "privacy.contact.p1": "Site-ul este open source: dacă doriți să verificați oricare dintre cele de mai sus, citiți codul la{github} Întrebările şi corecţiile pot intra într-o problemă.",
"disclaimer.title": "Disclaimer · Casumints.space", "disclaimer.title": "Declinarea responsabilității · Cashumints.space",
"disclaimer.description": "Ce trebuie să știți înainte de a utiliza un Cashu montarie: banii sunt custodia, o monetărie poate dispărea cu banii, recenziile sunt opinii și mijloace online accesibile, nedemne de încredere.", "disclaimer.description": "Ce trebuie să știți înainte de a utiliza un Cashu montarie: banii sunt custodia, o monetărie poate dispărea cu banii, recenziile sunt opinii și mijloace online accesibile, nedemne de încredere.",
"disclaimer.heading": "Retragere", "disclaimer.heading": "Retragere",
"disclaimer.lede": "Citiți acest lucru înainte de a pune bani într-o monetărie. Ultima actualizare{date}.", "disclaimer.lede": "Citiți acest lucru înainte de a pune bani într-o monetărie. Ultima actualizare{date}.",
@@ -561,15 +585,15 @@
"login.getPrimal": "Ia-l pe Primal.", "login.getPrimal": "Ia-l pe Primal.",
"reviews.dialog.identity.extensionGone": "Extensia browser-ului nu răspunde. Autentifică-te din nou sau foloseşte un signer de la distanţă.", "reviews.dialog.identity.extensionGone": "Extensia browser-ului nu răspunde. Autentifică-te din nou sau foloseşte un signer de la distanţă.",
"seo.orgDescription": "Un director al Cashu monetării, cu statut live din fiecare monetărie și comentarii comunitare semnate pe Nostr.", "seo.orgDescription": "Un director al Cashu monetării, cu statut live din fiecare monetărie și comentarii comunitare semnate pe Nostr.",
"seo.ogImageAlt": "Casumints.space: găsiți un Cashu monetărie puteți avea încredere.", "seo.ogImageAlt": "Cashumints.space: găsește un mint Cashu în care poți avea încredere.",
"seo.mint.title.one": "{name} recenzii: evaluat{rating}de la{n}review", "seo.mint.title.one": "{name} recenzii: evaluat {rating} din {n} recenzie | Cashumints.space",
"seo.mint.title.other": "{name} recenzii: evaluat{rating}de la{n} recenzii bază.spaţiu", "seo.mint.title.other": "{name} recenzii: evaluat {rating} din {n} recenzii | Cashumints.space",
"seo.mint.title.few": "{name} recenzii: evaluat{rating}de la{n} recenzii bază.spaţiu", "seo.mint.title.few": "{name} recenzii: evaluat{rating}de la{n} recenzii bază.spaţiu",
"seo.mint.titleOffline.one": "{name}: offline for{n} zi", "seo.mint.titleOffline.one": "{name}: offline de {n} zi | Cashumints.space",
"seo.mint.titleOffline.other": "{name}: offline for{n} zile? Cajumints.space", "seo.mint.titleOffline.other": "{name}: offline de {n} zile | Cashumints.space",
"seo.mint.titleOffline.few": "{name}: offline for{n} zile? Cajumints.space", "seo.mint.titleOffline.few": "{name}: offline for{n} zile? Cajumints.space",
"seo.mint.titleNoReviews": "{name}: Cashu monetărie info and reviews - Cashumints.space", "seo.mint.titleNoReviews": "{name}: Cashu monetărie info and reviews - Cashumints.space",
"seo.fedimint.titleNoReviews": "{name}: Fedimint Federation info and reviews", "seo.fedimint.titleNoReviews": "{name}: informații și recenzii despre federația Fedimint | Cashumints.space",
"seo.desc.fallback": "Recenzii comunitare pentru{name}({domain}).", "seo.desc.fallback": "Recenzii comunitare pentru{name}({domain}).",
"seo.desc.fallbackDomain": "Recenzii comunitare pentru Cashu monetărie la{domain}.", "seo.desc.fallbackDomain": "Recenzii comunitare pentru Cashu monetărie la{domain}.",
"seo.desc.rated.one": "Evaluat{rating}/5 de la{n}a semnat revizuirea Nostr.", "seo.desc.rated.one": "Evaluat{rating}/5 de la{n}a semnat revizuirea Nostr.",
@@ -587,11 +611,11 @@
"nav.home": "Acasă", "nav.home": "Acasă",
"mint.more.title": "Mai multe monede", "mint.more.title": "Mai multe monede",
"mint.more.all": "A se vedea toate monetări", "mint.more.all": "A se vedea toate monetări",
"nav.fedimints": "Fediminți", "nav.fedimints": "Fedimints",
"status.announced": "Doar anunţat", "status.announced": "Doar anunţat",
"card.unnamedFederation": "federație fără nume", "card.unnamedFederation": "federație fără nume",
"card.notChecked": "Nu am verificat niciodată.", "card.notChecked": "Nu am verificat niciodată.",
"fedimints.title": "Toate federațiile Fedimint · Casumints.space", "fedimints.title": "Toate federațiile Fedimint · Cashumints.space",
"fedimints.description": "Toate{total} Federaţiile federale au anunţat pe reţeaua Nostr:{online} confirmat de un control extern,{announced} ratinguri comunitare, module şi coduri de invitaţie pentru fiecare.", "fedimints.description": "Toate{total} Federaţiile federale au anunţat pe reţeaua Nostr:{online} confirmat de un control extern,{announced} ratinguri comunitare, module şi coduri de invitaţie pentru fiecare.",
"fedimints.heading": "Toate federațiile", "fedimints.heading": "Toate federațiile",
"fedimints.lede": "Fiecare federație Fedimint anunțat pe rețeaua Nostr. O federație nu are un obiectiv de statut public propriu, astfel încât unele dintre acestea sunt confirmate de funcționare printr-un control extern și restul sunt enumerate exact așa cum au fost anunțate.", "fedimints.lede": "Fiecare federație Fedimint anunțat pe rețeaua Nostr. O federație nu are un obiectiv de statut public propriu, astfel încât unele dintre acestea sunt confirmate de funcționare printr-un control extern și restul sunt enumerate exact așa cum au fost anunțate.",
@@ -675,5 +699,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."
} }
+148 -14
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",
@@ -48,8 +48,12 @@
"footer.disclaimer": "Отказ от ответственности", "footer.disclaimer": "Отказ от ответственности",
"footer.terms": "Условия", "footer.terms": "Условия",
"footer.privacy": "Конфиденциальность", "footer.privacy": "Конфиденциальность",
"footer.madeBy": "Сделано с ❤️ автором {azzamo} · Работает на Cashu и NIP-87", "footer.madeBy": "Сделано с ❤️ {azzamo} · Работает на Cashu и NIP-87",
"footer.warning": "Минты хранят ваши средства. Минт может исчезнуть вместе с вашими деньгами.", "footer.warning": "Минты хранят ваши средства. Минт может исчезнуть вместе с вашими деньгами.",
"footer.tagline": "Отзывы сообщества о ecash-минтах, подписанные в Nostr.",
"footer.col.explore": "Обзор",
"footer.col.site": "Сайт",
"footer.nav": "Подвал сайта",
"home.title": "Cashumints.space · Найдите минт Cashu, которому можно доверять", "home.title": "Cashumints.space · Найдите минт Cashu, которому можно доверять",
"home.description": "Найдите минт Cashu, которому можно доверять. В каталоге {mints} минтов, {reviews} подписанных в Nostr отзывов сообщества, актуальная доступность, поддерживаемые NUT и ПО минтов.", "home.description": "Найдите минт Cashu, которому можно доверять. В каталоге {mints} минтов, {reviews} подписанных в Nostr отзывов сообщества, актуальная доступность, поддерживаемые NUT и ПО минтов.",
"home.hero.title": "Найдите минт Cashu", "home.hero.title": "Найдите минт Cashu",
@@ -116,6 +120,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 минта",
@@ -277,6 +283,9 @@
"reviews.empty": "Отзывов пока нет. Будьте первым, кто оставит отзыв.", "reviews.empty": "Отзывов пока нет. Будьте первым, кто оставит отзыв.",
"reviews.emptyWritten": "Здесь пока нет письменных отзывов. Будьте первым, кто оставит отзыв.", "reviews.emptyWritten": "Здесь пока нет письменных отзывов. Будьте первым, кто оставит отзыв.",
"reviews.emptyFilter": "По данному фильтру отзывов не найдено.", "reviews.emptyFilter": "По данному фильтру отзывов не найдено.",
"reviews.rate.cashu": "Оцените этот минт",
"reviews.rate.fedimint": "Оцените эту федерацию",
"reviews.rate.lnurl": "Оцените этот минт",
"reviews.noscript": "Отзывы на Nostr подписываются и загружаются в браузере, поэтому для просмотра этого списка требуется JavaScript. Оценки и количество просмотров, указанные выше, взяты с самого сайта.", "reviews.noscript": "Отзывы на Nostr подписываются и загружаются в браузере, поэтому для просмотра этого списка требуется JavaScript. Оценки и количество просмотров, указанные выше, взяты с самого сайта.",
"reviews.relayError": "Не удалось подключиться к relay. Часть отзывов может отсутствовать.", "reviews.relayError": "Не удалось подключиться к relay. Часть отзывов может отсутствовать.",
"reviews.srRating": "{rating} из 5", "reviews.srRating": "{rating} из 5",
@@ -289,7 +298,7 @@
"reviews.anonNote": "Первый отзыв с этого npub, другой активности не найдено", "reviews.anonNote": "Первый отзыв с этого npub, другой активности не найдено",
"reviews.published": "Опубликовано.", "reviews.published": "Опубликовано.",
"reviews.publishedPartial": "Опубликовано на {accepted} из {total} relays.", "reviews.publishedPartial": "Опубликовано на {accepted} из {total} relays.",
"reviews.propagating": "Распространяется по сети, в других местах отзыв может появиться не сразу.", "reviews.propagating": "Публикация на relays Nostr",
"reviews.summary.line.one": "Рейтинг «{count}» без комментариев: {breakdown}", "reviews.summary.line.one": "Рейтинг «{count}» без комментариев: {breakdown}",
"reviews.summary.line.other": "Рейтинги «{count}» без комментариев: {breakdown}", "reviews.summary.line.other": "Рейтинги «{count}» без комментариев: {breakdown}",
"reviews.summary.line.few": "Рейтинги «{count}» без комментариев: {breakdown}", "reviews.summary.line.few": "Рейтинги «{count}» без комментариев: {breakdown}",
@@ -307,7 +316,12 @@
"reviews.breakdown.1": "{n}, одна звезда", "reviews.breakdown.1": "{n}, одна звезда",
"reviews.breakdown.none": "{n}, без оценки", "reviews.breakdown.none": "{n}, без оценки",
"reviews.dialog.title": "Отзыв о {name}", "reviews.dialog.title": "Отзыв о {name}",
"reviews.dialog.note": "Отзыв будет подписан вашим ключом Nostr и опубликован на публичных relays. Его нельзя отредактировать или удалить.", "reviews.dialog.lede.cashu": "Расскажите о своём опыте с этим минтом.",
"reviews.dialog.lede.fedimint": "Расскажите о своём опыте с этой федерацией.",
"reviews.dialog.lede.lnurl": "Расскажите о своём опыте с этим минтом.",
"reviews.dialog.permanent": "Публикуется навсегда на публичных relays Nostr.",
"reviews.dialog.gate.title": "Войдите, чтобы написать отзыв",
"reviews.dialog.gate.lead": "Отзывы подписываются вашим ключом Nostr. Он остаётся в вашем signer, сайт никогда его не видит.",
"reviews.dialog.ratingLegend": "Ваша оценка", "reviews.dialog.ratingLegend": "Ваша оценка",
"reviews.dialog.starsSr.one": "{n} звезда", "reviews.dialog.starsSr.one": "{n} звезда",
"reviews.dialog.starsSr.other": "{n} звезд", "reviews.dialog.starsSr.other": "{n} звезд",
@@ -330,7 +344,6 @@
"reviews.dialog.waitingSigner": "Ожидание signer", "reviews.dialog.waitingSigner": "Ожидание signer",
"reviews.dialog.confirmClose": "Закрыть этот отзыв? Ваш черновик сохранится до тех пор, пока вы не покинете сайт.", "reviews.dialog.confirmClose": "Закрыть этот отзыв? Ваш черновик сохранится до тех пор, пока вы не покинете сайт.",
"reviews.dialog.why.login": "Войдите в систему, чтобы опубликовать", "reviews.dialog.why.login": "Войдите в систему, чтобы опубликовать",
"reviews.dialog.why.rating": "Выберите оценку",
"reviews.dialog.why.words": "Напишите несколько слов", "reviews.dialog.why.words": "Напишите несколько слов",
"reviews.dialog.why.more.one": "нужен еще {n} символ", "reviews.dialog.why.more.one": "нужен еще {n} символ",
"reviews.dialog.why.more.other": "{n} еще символов", "reviews.dialog.why.more.other": "{n} еще символов",
@@ -351,7 +364,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": "Отзывы",
@@ -397,6 +410,10 @@
"login.status.waitingSigner": "Ожидание signer", "login.status.waitingSigner": "Ожидание signer",
"login.status.connecting": "Подключение к signer", "login.status.connecting": "Подключение к signer",
"login.status.connected": "Подключено", "login.status.connected": "Подключено",
"login.toast.loggedIn": "Вход выполнен",
"login.toast.fetchingProfile": "Загрузка вашего профиля Nostr с relays…",
"login.toast.profile": "Профиль: {name}",
"login.toast.noProfile": "Профиль на relays не найден",
"login.method.extension": "Расширение", "login.method.extension": "Расширение",
"login.method.remote": "Удаленный", "login.method.remote": "Удаленный",
"login.method.primal": "Primal", "login.method.primal": "Primal",
@@ -428,7 +445,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": "Кошельки",
@@ -605,7 +629,7 @@
"nav.home": "Главная", "nav.home": "Главная",
"mint.more.title": "Другие минты", "mint.more.title": "Другие минты",
"mint.more.all": "Посмотреть все минты", "mint.more.all": "Посмотреть все минты",
"nav.fedimints": "Федиминтс", "nav.fedimints": "Fedimints",
"status.announced": "Только объявлено", "status.announced": "Только объявлено",
"card.unnamedFederation": "Федерация без названия", "card.unnamedFederation": "Федерация без названия",
"card.notChecked": "никогда не проверял", "card.notChecked": "никогда не проверял",
@@ -696,5 +720,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 тоже был недоступен, поэтому ничего нельзя было отчеканить или расплавить."
} }
+280 -150
View File
@@ -33,15 +33,15 @@
"status.unknown": "Har inte kontrollerats ännu", "status.unknown": "Har inte kontrollerats ännu",
"nav.skip": "Gå direkt till innehållet", "nav.skip": "Gå direkt till innehållet",
"nav.main": "Huvudsida", "nav.main": "Huvudsida",
"nav.mints": "Cashu-pastiller", "nav.mints": "Cashu-mints",
"nav.reviews": "Recensioner", "nav.reviews": "Recensioner",
"nav.wallets": "Plånböcker", "nav.wallets": "Plånböcker",
"nav.about": "Om", "nav.about": "Om",
"nav.searchMints": "Sök efter {n}-pastiller", "nav.searchMints": "Sök bland {n} mints",
"lang.label": "Språk", "lang.label": "Språk",
"lang.switcher": "Välj språk", "lang.switcher": "Välj språk",
"footer.copyright": "© {year} Cashumints.space", "footer.copyright": "© {year} Cashumints.space",
"footer.allMints": "Alla Cashu-mynt", "footer.allMints": "Alla Cashu-mints",
"footer.allReviews": "Alla recensioner", "footer.allReviews": "Alla recensioner",
"footer.wallets": "Plånböcker", "footer.wallets": "Plånböcker",
"footer.about": "Om", "footer.about": "Om",
@@ -49,21 +49,25 @@
"footer.terms": "Villkor", "footer.terms": "Villkor",
"footer.privacy": "Sekretess", "footer.privacy": "Sekretess",
"footer.madeBy": "Skapad med ❤️ av {azzamo} · Drivs av Cashu och NIP-87", "footer.madeBy": "Skapad med ❤️ av {azzamo} · Drivs av Cashu och NIP-87",
"footer.warning": "Myntverk är förvaringsinstitut. Ett myntverk kan försvinna med dina pengar.", "footer.warning": "Mints är förvaringsinstitut. Ett mint kan försvinna med dina pengar.",
"home.title": "Cashumints.space · Hitta ett Cashu i nyskick som du kan lita på", "footer.tagline": "Recensioner från communityn om ecash-mints, signerade på Nostr.",
"home.description": "Hitta en Cashu-mint som du kan lita på. {mints}-mints som är indexerade med {reviews}-användarrecensioner publicerade på Nostr, samt realtidsuppgifter om drifttid, stöd för NUTs och mint-programvara.", "footer.col.explore": "Utforska",
"home.hero.title": "Hitta en Cashu i nyskick", "footer.col.site": "Webbplats",
"footer.nav": "Sidfot",
"home.title": "Cashumints.space · Hitta ett Cashu-mint du kan lita på",
"home.description": "Hitta ett Cashu-mint du kan lita på. {mints} mints i indexet med {reviews} recensioner från communityn, signerade på Nostr, plus live drifttid, stöd för NUTs och mint-programvara.",
"home.hero.title": "Hitta ett Cashu-mint",
"home.hero.titleEm": "som du kan lita på", "home.hero.titleEm": "som du kan lita på",
"home.hero.lede": "Alla mynt i Nostr-nätverket, med äkta omdömen från användarna. Inga granskare – bara signerade omdömen som du själv kan verifiera.", "home.hero.lede": "Varje mint i Nostr-nätverket, med äkta recensioner från communityn. Inga grindvakter, bara signerade recensioner du kan verifiera själv.",
"home.search.label": "Sök efter webbplatser efter namn, domän eller URL", "home.search.label": "Sök mints på namn, domän eller URL",
"home.search.placeholder": "Sök efter namn, domän eller klistra in en Mint-URL…", "home.search.placeholder": "Sök efter namn, domän eller klistra in en Mint-URL…",
"home.filters.mostReviewed": "Mest omtalade", "home.filters.mostReviewed": "Mest omtalade",
"home.filters.highestRated": "Högst betygsatta", "home.filters.highestRated": "Högst betygsatta",
"home.filters.recentlyActive": "Nyligen aktiva", "home.filters.recentlyActive": "Nyligen aktiva",
"home.filters.websockets": "Stöder WebSockets", "home.filters.websockets": "Stöder WebSockets",
"home.top.title": "De bästa Cashu-mynterna", "home.top.title": "De bästa Cashu-mints",
"home.top.sub": "rangerade efter recensioner och betyg", "home.top.sub": "rangerade efter recensioner och betyg",
"home.top.all": "Alla {n}-mynt →", "home.top.all": "Alla {n} mints →",
"home.latest.title": "Senaste recensionerna", "home.latest.title": "Senaste recensionerna",
"home.latest.sub": "direkt från nätverket Nostr", "home.latest.sub": "direkt från nätverket Nostr",
"home.latest.prev": "Tidigare recensioner", "home.latest.prev": "Tidigare recensioner",
@@ -79,149 +83,151 @@
"home.why.checked.title": "Kontrolleras kontinuerligt", "home.why.checked.title": "Kontrolleras kontinuerligt",
"home.why.checked.body": "Vi kontrollerar varje mint med avseende på drifttid, programvaruversion, begränsningar och vilka NUTs-enheter som stöds, så de specifikationer du ser är aktuella och inte självrapporterade.", "home.why.checked.body": "Vi kontrollerar varje mint med avseende på drifttid, programvaruversion, begränsningar och vilka NUTs-enheter som stöds, så de specifikationer du ser är aktuella och inte självrapporterade.",
"home.cta.title": "Funderar du på att köpa en mint? Läs det här först.", "home.cta.title": "Funderar du på att köpa en mint? Läs det här först.",
"home.cta.body": "Här hittar du din ecash. Läs vad andra har skrivit innan du sätter in sats, och lämna en recension när du själv har använt en.", "home.cta.body": "Ett mint håller din ecash. Läs vad andra har kommit fram till innan du sätter in sats, och lämna en recension när du själv har använt ett.",
"home.cta.explore": "Upptäck alla mintsorter", "home.cta.explore": "Upptäck alla mintsorter",
"home.cta.learn": "Läs mer om Cashu", "home.cta.learn": "Läs mer om Cashu",
"pulse.mintsIndexed": "indexerade mynt", "pulse.mintsIndexed": "mints i indexet",
"pulse.reviewsOnNostr": "recensioner av Nostr", "pulse.reviewsOnNostr": "recensioner på Nostr",
"pulse.lastReview": "senaste granskningen", "pulse.lastReview": "senaste granskningen",
"pulse.mintsOnline": "mynta online", "pulse.mintsOnline": "mints online",
"pulse.indexer": "indexerare", "pulse.indexer": "indexerare",
"pulse.connected": "ansluten", "pulse.connected": "ansluten",
"pulse.stale": "föråldrad", "pulse.stale": "föråldrad",
"mints.title": "Alla Cashu-mynt · Cashumints.space", "mints.title": "Alla Cashu-mints · Cashumints.space",
"mints.description": "Alla {total}- och Cashu-minter som indexerats från nätverket Nostr, {online} online och {offline} offline, med användarbetyg, drifttid och minterprogramvara. Sök och sortera i hela listan.", "mints.description": "Alla {total} Cashu-mints indexerade från Nostr-nätverket, {online} online och {offline} offline, med betyg från communityn, drifttid och mint-programvara. Sök och sortera hela listan.",
"mints.heading": "Alla Cashu-mynt", "mints.heading": "Alla Cashu-mints",
"mints.lede": "Alla mynt som har upptäckts i nätverket Nostr, inklusive de som inte längre finns kvar. Mynt som är offline finns kvar i listan så att du fortfarande kan hitta och granska dem.", "mints.lede": "Varje mint som hittats i Nostr-nätverket, inklusive de som är borta. Mints som är offline står kvar i listan så att du fortfarande kan hitta och recensera dem.",
"mints.search.label": "Sök efter mynt efter namn eller domän", "mints.search.label": "Sök mints på namn eller domän",
"mints.search.placeholder": "Sök efter namn eller domän…", "mints.search.placeholder": "Sök efter namn eller domän…",
"mints.sort.label": "Sortera minttabletter", "mints.sort.label": "Sortera mints",
"mints.sort.score": "Bäst totalt sett", "mints.sort.score": "Bäst totalt sett",
"mints.sort.rating": "Högst betygsatta", "mints.sort.rating": "Högst betygsatta",
"mints.sort.reviews": "Mest omtalade", "mints.sort.reviews": "Mest omtalade",
"mints.sort.recent": "Nyligen granskade", "mints.sort.recent": "Nyligen granskade",
"mints.sort.seen": "Nyligen visade på nätet", "mints.sort.seen": "Nyligen visade på nätet",
"mints.hideOffline": "Dölj offline-status", "mints.hideOffline": "Dölj offline",
"mints.showing": "Visar {total}-mynt ({online} online, {offline} offline)", "mints.showing": "Visar {total} mints ({online} online, {offline} offline)",
"mints.showingFiltered": "Visar {shown} av {total}-mynt ({online} online, {offline} offline)", "mints.showingFiltered": "Visar {shown} av {total} mints ({online} online, {offline} offline)",
"mints.noResults": "Inga mintar stämmer överens. Prova med en domän eller klistra in en mint-URL.", "mints.noResults": "Inga mints matchar. Prova en domän, eller klistra in en mint-URL.",
"mints.clearSearch": "Rensa sökningen", "mints.clearSearch": "Rensa sökningen",
"card.noRatings": "Inga betyg ännu", "card.noRatings": "Inga betyg ännu",
"card.reviews.one": "Recension av {n}", "card.reviews.one": "{n} recension",
"card.reviews.other": "Recensioner av {n}", "card.reviews.other": "{n} recensioner",
"card.lastSeen": "senast sett {when}", "card.lastSeen": "senast sett {when}",
"card.neverSeen": "har aldrig setts på nätet", "card.neverSeen": "har aldrig setts på nätet",
"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} · recensioner av Cashu-mint",
"mint.titleFedimint": "{name} · recensioner av Fedimint-federation",
"mint.titleLnurl": "{name} · recensioner av LNURL-mint",
"mint.breadcrumb": "Navigationsspår", "mint.breadcrumb": "Navigationsspår",
"mint.crumbMints": "Cashu-pastiller", "mint.crumbMints": "Cashu-mints",
"mint.copyUrl": "Kopiera Mint-URL:en", "mint.copyUrl": "Kopiera mint-URL",
"mint.qrButton": "Mint QR", "mint.qrButton": "Mint QR",
"mint.writeReview": "Skriv en recension", "mint.writeReview": "Skriv en recension",
"mint.rankByScore": "#{rank} efter poäng", "mint.rankByScore": "#{rank} efter poäng",
"mint.tagLightning": "Lightning", "mint.tagLightning": "Lightning",
"mint.summary": "Sammanfattning av Mint", "mint.summary": "Mint-sammanfattning",
"mint.rating.label": "Gemenskapens betyg", "mint.rating.label": "Gemenskapens betyg",
"mint.rating.none": "Inga betyg ännu", "mint.rating.none": "Inga betyg ännu",
"mint.rating.reviews.one": "Recension av {n}", "mint.rating.reviews.one": "{n} recension",
"mint.rating.reviews.other": "Recensioner av {n}", "mint.rating.reviews.other": "{n} recensioner",
"mint.rating.unrated.one": "{n} har inte lämnat något betyg", "mint.rating.unrated.one": "{n} har inte lämnat något betyg",
"mint.rating.unrated.other": "{n} har inte lämnat något betyg", "mint.rating.unrated.other": "{n} har inte lämnat något betyg",
"mint.activity.label": "Aktivitet", "mint.activity.label": "Aktivitet",
"mint.activity.since": "sedan den senaste granskningen", "mint.activity.since": "sedan senaste recensionen",
"mint.activity.noReviews": "inga recensioner ännu", "mint.activity.noReviews": "inga recensioner ännu",
"mint.activity.recent.one": "Recensioner av {n} under de senaste 90 dagarna", "mint.activity.recent.one": "{n} recension de senaste 90 dagarna",
"mint.activity.recent.other": "Recensioner av {n} under de senaste 90 dagarna", "mint.activity.recent.other": "{n} recensioner de senaste 90 dagarna",
"mint.limits.label": "Gränser", "mint.limits.label": "Gränser",
"mint.limits.notPublished": "ej publicerad", "mint.limits.notPublished": "ej publicerad",
"mint.limits.min": "min", "mint.limits.min": "min",
"mint.limits.max": "max", "mint.limits.max": "max",
"mint.limits.mintRow": "mynta", "mint.limits.mintRow": "mint",
"mint.limits.meltRow": "smälta", "mint.limits.meltRow": "melt",
"mint.limits.disabled": "funktionshindrad", "mint.limits.disabled": "avstängd",
"mint.limits.range": "{min} till {max} {unit}", "mint.limits.range": "{min} till {max} {unit}",
"mint.limits.upTo": "upp till {max} {unit}", "mint.limits.upTo": "upp till {max} {unit}",
"mint.limits.from": "från {min} {unit}", "mint.limits.from": "från {min} {unit}",
"mint.software.label": "Mint-programvara", "mint.software.label": "Mint-programvara",
"mint.software.unknown": "okänt", "mint.software.unknown": "okänt",
"mint.checked": "markerad {when}", "mint.checked": "kontrollerad {when}",
"mint.lastSeen": "senast sett {when}", "mint.lastSeen": "senast sett {when}",
"mint.neverReached": "aldrig nåtts", "mint.neverReached": "aldrig nåtts",
"mint.uptime.checks": "{ok} av de senaste {total}-kontrollerna är tillgängliga", "mint.uptime.checks": "{ok} av de senaste {total} kontrollerna nåbara",
"mint.uptime.percent": "{percent}% tillgänglig under 30 dagar", "mint.uptime.percent": "{percent}% tillgänglig under 30 dagar",
"mint.uptime.description": "{ok} av de senaste {total}-kontrollerna har genomförts framgångsrikt", "mint.uptime.description": "{ok} av de senaste {total} kontrollerna lyckades",
"mint.operator.title": "Från operatören", "mint.operator.title": "Från operatören",
"mint.operator.motd": "Dagens meddelande", "mint.operator.motd": "Dagens meddelande",
"mint.operator.beta": "Denna mint beskriver sig själv som experimentell eller i betafas. Läs operatörens egna ord nedan innan du förvarar något som du inte har råd att förlora.", "mint.operator.beta": "Denna mint beskriver sig själv som experimentell eller i betafas. Läs operatörens egna ord nedan innan du förvarar något som du inte har råd att förlora.",
"mint.nuts.title": "Kompatibel med NUTs", "mint.nuts.title": "NUTs som stöds",
"mint.nuts.noneOffline": "Det finns ingen cachad lista med koden NUT för detta mynt; det togs bort från nätet innan någon lista hann registreras.", "mint.nuts.noneOffline": "Ingen NUT-lista är cachad för detta mint, det gick offline innan någon hann registreras.",
"mint.nuts.noneYet": "Det finns ännu ingen cachad lista med referenskoden NUT för detta mynt.", "mint.nuts.noneYet": "Ingen NUT-lista är cachad för detta mint ännu.",
"mint.nuts.supported": "{n} av {total}, även känt som NUTs, stöds", "mint.nuts.supported": "{n} av {total} kända NUTs stöds",
"mint.nuts.whatAre": "Vad är NUTs?", "mint.nuts.whatAre": "Vad är NUTs?",
"mint.nutRow.disabled": "funktionshindrad", "mint.nutRow.disabled": "avstängd",
"mint.nutRow.notSupported": "stöds inte", "mint.nutRow.notSupported": "stöds inte",
"mint.details.title": "Uppgifter om myntet", "mint.details.title": "Mint-detaljer",
"mint.details.pubkey": "Offentlig nyckel", "mint.details.pubkey": "Pubkey",
"mint.details.nostr": "Nostr", "mint.details.nostr": "Nostr",
"mint.details.email": "E-post", "mint.details.email": "E-post",
"mint.details.serverTime": "Servertid", "mint.details.serverTime": "Servertid",
"mint.details.firstSeen": "Första gången", "mint.details.firstSeen": "Första gången",
"mint.details.terms": "Villkor", "mint.details.terms": "Villkor",
"mint.kind.identifier": "identifierare", "mint.kind.identifier": "identifierare",
"mint.kind.mintPubkey": "skapa en offentlig nyckel", "mint.kind.mintPubkey": "mint-pubkey",
"mint.kind.nostrAddress": "Adressen Nostr", "mint.kind.nostrAddress": "Nostr-adress",
"mint.kind.emailAddress": "e-postadress", "mint.kind.emailAddress": "e-postadress",
"mint.kind.npub": "npub", "mint.kind.npub": "npub",
"mint.disclaimer": "Denna plattform fungerar som förvarare: den förvarar dina pengar och kan när som helst sluta göra det. Betygen är åsikter från användargemenskapen, och ”online” betyder endast att tjänsten är tillgänglig, inte att den är pålitlig.", "mint.disclaimer": "Detta mint är förvarande: det håller dina pengar, och det kan sluta göra det när som helst. Betyg är åsikter från communityn, och online betyder nåbar, inte pålitlig.",
"mint.disclaimerLink": "Läs hela ansvarsfriskrivningen", "mint.disclaimerLink": "Läs hela ansvarsfriskrivningen",
"mint.qr.title": "Lägg till {name} i din plånbok", "mint.qr.title": "Lägg till {name} i din plånbok",
"mint.qr.note": "Skanna den här koden med en Cashu-plånbok för att lägga till myntet, eller kopiera webbadressen och klistra in den.", "mint.qr.note": "Skanna den här med en Cashu-wallet för att lägga till mintet, eller kopiera URL:en och klistra in den.",
"mint.qr.warn": "Att lägga till en betalningsleverantör innebär att du anförtror den dina pengar.", "mint.qr.warn": "Att lägga till ett mint betyder att anförtro det dina pengar.",
"mint.qr.whatMeans": "Vad det innebär", "mint.qr.whatMeans": "Vad det innebär",
"mint.qr.wallets": "Plånböcker som stöder Cashu", "mint.qr.wallets": "Plånböcker som stöder Cashu",
"mint.chip.frozen": "Fryst", "mint.chip.frozen": "Fryst",
"mint.chip.noWithdrawals": "Inga uttag", "mint.chip.noWithdrawals": "Inga uttag",
"mint.chip.meltOnly": "Endast smältning", "mint.chip.meltOnly": "Endast melt",
"mint.warnings.dayCount.one": "{n}-dagen", "mint.warnings.dayCount.one": "{n} dag",
"mint.warnings.dayCount.other": "{n} dagar", "mint.warnings.dayCount.other": "{n} dagar",
"mint.warnings.gone.lead": "Förmodligen borta.", "mint.warnings.gone.lead": "Förmodligen borta.",
"mint.warnings.gone.body.since": "Denna plattform har varit otillgänglig sedan {date} ({days}). Betrakta de medel som finns där som förlorade och skicka inte in några nya medel. Nedan visas information om dess senast kända status.", "mint.warnings.gone.body.since": "Detta mint har varit onåbart sedan {date} ({days}). Betrakta medel som ligger där som utsatta, och skicka inte in nya medel. Detaljerna nedan visar dess senast kända tillstånd.",
"mint.warnings.gone.body.neverDated": "Denna myntverk har inte svarat på en enda kontroll sedan vi började övervaka den i {month}. Betrakta medel som finns där som utsatta för risk och skicka inte in nya medel. All information nedan kommer från meddelandet från Nostr, inte från myntverket.", "mint.warnings.gone.body.neverDated": "Detta mint har inte svarat på en enda kontroll sedan vi började bevaka det i {month}. Betrakta medel som ligger där som utsatta, och skicka inte in nya medel. Allt nedan kommer från Nostr-annonseringen, inte från mintet.",
"mint.warnings.gone.body.never": "Denna myntverk har inte besvarat en enda förfrågan sedan vi började bevaka den. Betrakta medel som finns där som utsatta för risk och skicka inte in nya medel. All information nedan kommer från meddelandet Nostr, inte från myntverket.", "mint.warnings.gone.body.never": "Detta mint har inte svarat på en enda kontroll sedan vi började bevaka det. Betrakta medel som ligger där som utsatta, och skicka inte in nya medel. Allt nedan kommer från Nostr-annonseringen, inte från mintet.",
"mint.warnings.gone.meta.since": "Offline sedan {date}", "mint.warnings.gone.meta.since": "Offline sedan {date}",
"mint.warnings.gone.meta.never": "Har aldrig gått att nå sedan den upptäcktes", "mint.warnings.gone.meta.never": "Har aldrig gått att nå sedan den upptäcktes",
"mint.warnings.meltDisabled.lead": "Uttag är inaktiverade.", "mint.warnings.meltDisabled.lead": "Uttag är inaktiverade.",
"mint.warnings.meltDisabled.body.online": "För denna myntmaskin är smältfunktionen för Lightning (NUT-05) avstängd: du kan sätta in sats, men du kan inte ta ut dem via Lightning. Gör inga insättningar förrän smältfunktionen är igång igen.", "mint.warnings.meltDisabled.body.online": "Detta mint har melting via Lightning (NUT-05) avstängt: du kan sätta in sats, men du kan inte ta ut dem via Lightning. Sätt inte in något förrän melting är tillbaka.",
"mint.warnings.meltDisabled.body.offline": "Denna myntautomat hade smältfunktionen för Lightning (NUT-05) avstängd: du kan sätta in sats, men du kan inte ta ut dem via Lightning. Gör inga insättningar förrän smältfunktionen är igång igen.", "mint.warnings.meltDisabled.body.offline": "Detta mint hade melting via Lightning (NUT-05) avstängt: du kan sätta in sats, men du kan inte ta ut dem via Lightning. Sätt inte in något förrän melting är tillbaka.",
"mint.warnings.meltDisabled.meta.online": "Uttag är för närvarande inaktiverade", "mint.warnings.meltDisabled.meta.online": "Uttag är för närvarande inaktiverade",
"mint.warnings.meltDisabled.meta.offline": "Uttag var inaktiverade när de senast visades", "mint.warnings.meltDisabled.meta.offline": "Uttag var inaktiverade när de senast visades",
"mint.warnings.frozen.lead": "Fryst mynta.", "mint.warnings.frozen.lead": "Mint fryst.",
"mint.warnings.frozen.body.online": "Både prägling och smältning är inaktiverade. Ingen sats kan flyttas in eller ut via Lightning.", "mint.warnings.frozen.body.online": "Både minting och melting är avstängt. Inga sats kan flyttas in eller ut via Lightning.",
"mint.warnings.frozen.body.offline": "Både myntning och smältning har inaktiverats. Ingen sats kan flyttas in eller ut via Lightning.", "mint.warnings.frozen.body.offline": "Både minting och melting var avstängt. Inga sats kan flyttas in eller ut via Lightning.",
"mint.warnings.frozen.meta.online": "För närvarande är verksamheten fryst, ingenting kommer in eller går ut", "mint.warnings.frozen.meta.online": "För närvarande är verksamheten fryst, ingenting kommer in eller går ut",
"mint.warnings.frozen.meta.offline": "Fryst när den senast sågs", "mint.warnings.frozen.meta.offline": "Fryst när den senast sågs",
"mint.warnings.offlineLong.lead": "Inaktiv för {days}, från och med {date}.", "mint.warnings.offlineLong.lead": "Inaktiv för {days}, från och med {date}.",
"mint.warnings.offlineLong.body": "Mynten kan ha tagit slut. Allt nedan visar dess senaste kända tillstånd, och recensionerna fungerar fortfarande.", "mint.warnings.offlineLong.body": "Mintet kan vara borta. Allt nedan är dess senast kända tillstånd, och recensioner fungerar fortfarande.",
"mint.warnings.offlineLong.meta": "Offline sedan {date}", "mint.warnings.offlineLong.meta": "Offline sedan {date}",
"mint.warnings.meltOnly.lead": "Endast smältning.", "mint.warnings.meltOnly.lead": "Endast melt.",
"mint.warnings.meltOnly.body.online": "Nyutgivning (NUT-04) är inaktiverad: du kan inte göra insättningar, men befintliga ecash kan fortfarande tas ut. Detta innebär ofta att en utgivning håller på att avvecklas, så om du innehar ecash bör du överväga att lösa in dem.", "mint.warnings.meltOnly.body.online": "Ny minting (NUT-04) är avstängd: du kan inte sätta in, men befintlig ecash kan fortfarande tas ut. Det betyder ofta att ett mint håller på att avvecklas, så om du håller dess ecash, överväg att melta ut.",
"mint.warnings.meltOnly.body.offline": "Nyutgivning (NUT-04) har inaktiverats: du kan inte göra insättningar, men befintliga ecash kan fortfarande tas ut. Detta innebär ofta att en utgivning håller på att avvecklas, så om du innehar ecash bör du överväga att lösa in dem.", "mint.warnings.meltOnly.body.offline": "Ny minting (NUT-04) var avstängd: du kan inte sätta in, men befintlig ecash kan fortfarande tas ut. Det betyder ofta att ett mint håller på att avvecklas, så om du håller dess ecash, överväg att melta ut.",
"mint.warnings.meltOnly.meta.online": "För närvarande endast smältning", "mint.warnings.meltOnly.meta.online": "För närvarande endast melt",
"mint.warnings.meltOnly.meta.offline": "Smält endast när den senast sågs", "mint.warnings.meltOnly.meta.offline": "Endast melt när den senast sågs",
"mint.warnings.offline.lead": "Offline sedan {date}.", "mint.warnings.offline.lead": "Offline sedan {date}.",
"mint.warnings.offline.body": "Detaljerna bekräftades senast då. Du kan fortfarande skriva en recension.", "mint.warnings.offline.body": "Detaljerna bekräftades senast då. Du kan fortfarande skriva en recension.",
"mint.warnings.offline.meta": "Offline sedan {date}", "mint.warnings.offline.meta": "Offline sedan {date}",
"mint.warnings.also.meltDisabled": "Innan tjänsten stängdes ner hade man dessutom inaktiverat uttagen.", "mint.warnings.also.meltDisabled": "Innan tjänsten stängdes ner hade man dessutom inaktiverat uttagen.",
"mint.warnings.also.mintDisabled": "Innan den gick offline hade den dessutom inaktiverat nyprägling.", "mint.warnings.also.mintDisabled": "Innan det gick offline hade det också stängt av ny minting.",
"mint.warnings.also.offline": "Den har dessutom varit ur drift sedan {date} ({days}).", "mint.warnings.also.offline": "Den har dessutom varit ur drift sedan {date} ({days}).",
"nut.0": "Beteckningar och modeller", "nut.0": "Beteckningar och modeller",
"nut.1": "Utbyte av offentliga nycklar i Mint", "nut.1": "Utbyte av offentliga nycklar i Mint",
"nut.2": "Nökkombinationer och avgifter", "nut.2": "Nökkombinationer och avgifter",
"nut.3": "Byta tokens", "nut.3": "Byta tokens",
"nut.4": "Myntprägling via Lightning", "nut.4": "Minting via Lightning",
"nut.5": "Smältning med Lightning", "nut.5": "Smältning med Lightning",
"nut.6": "Information om mynta", "nut.6": "Mint-information",
"nut.7": "Kontroll av tokenets status", "nut.7": "Kontroll av tokenets status",
"nut.8": "Överbetalda avgifter för Lightning", "nut.8": "Överbetalda avgifter för Lightning",
"nut.9": "Återställ signaturer", "nut.9": "Återställ signaturer",
@@ -235,7 +241,7 @@
"nut.17": "WebSocket-abonnemang", "nut.17": "WebSocket-abonnemang",
"nut.18": "Betalningsförfrågningar", "nut.18": "Betalningsförfrågningar",
"nut.19": "Cachelagrade svar", "nut.19": "Cachelagrade svar",
"nut.20": "Signerade citat i nyskick", "nut.20": "Signerade mint-quotes",
"nut.21": "Rensa autentisering", "nut.21": "Rensa autentisering",
"nut.22": "Autentisering utan inloggning", "nut.22": "Autentisering utan inloggning",
"nut.23": "BOLT11 – betalningssätt", "nut.23": "BOLT11 – betalningssätt",
@@ -261,6 +267,9 @@
"reviews.empty": "Inga recensioner ännu. Bli den första att skriva en.", "reviews.empty": "Inga recensioner ännu. Bli den första att skriva en.",
"reviews.emptyWritten": "Det finns inga skriftliga recensioner här än. Bli den första att skriva en.", "reviews.emptyWritten": "Det finns inga skriftliga recensioner här än. Bli den första att skriva en.",
"reviews.emptyFilter": "Det finns inga recensioner som stämmer överens med detta filter.", "reviews.emptyFilter": "Det finns inga recensioner som stämmer överens med detta filter.",
"reviews.rate.cashu": "Betygsätt denna mint",
"reviews.rate.fedimint": "Betygsätt denna federation",
"reviews.rate.lnurl": "Betygsätt denna mint",
"reviews.noscript": "Recensionerna är signerade med Nostr och laddas i din webbläsare, så den här listan kräver JavaScript. Betygen och antalet nedan hämtas direkt från webbplatsen.", "reviews.noscript": "Recensionerna är signerade med Nostr och laddas i din webbläsare, så den här listan kräver JavaScript. Betygen och antalet nedan hämtas direkt från webbplatsen.",
"reviews.relayError": "Det gick inte att nå reläet. Recensionerna kan vara ofullständiga.", "reviews.relayError": "Det gick inte att nå reläet. Recensionerna kan vara ofullständiga.",
"reviews.srRating": "{rating} av 5", "reviews.srRating": "{rating} av 5",
@@ -273,7 +282,7 @@
"reviews.anonNote": "Första recensionen av denna npub, inga andra aktiviteter har hittats", "reviews.anonNote": "Första recensionen av denna npub, inga andra aktiviteter har hittats",
"reviews.published": "Publicerad.", "reviews.published": "Publicerad.",
"reviews.publishedPartial": "Publicerad till reläet {accepted} i serien {total}.", "reviews.publishedPartial": "Publicerad till reläet {accepted} i serien {total}.",
"reviews.propagating": "När innehållet sprids kan det ta en stund innan det visas på andra ställen.", "reviews.propagating": "Publicerar till Nostr-relayer",
"reviews.summary.line.one": "{count}-betyg utan kommentar: {breakdown}", "reviews.summary.line.one": "{count}-betyg utan kommentar: {breakdown}",
"reviews.summary.line.other": "{count}-betyg utan kommentar: {breakdown}", "reviews.summary.line.other": "{count}-betyg utan kommentar: {breakdown}",
"reviews.summary.lineDay.one": "Betyg för {count} utan kommentar om {when}: {breakdown}", "reviews.summary.lineDay.one": "Betyg för {count} utan kommentar om {when}: {breakdown}",
@@ -287,7 +296,12 @@
"reviews.breakdown.1": "{n} en stjärna", "reviews.breakdown.1": "{n} en stjärna",
"reviews.breakdown.none": "{n} utan betyg", "reviews.breakdown.none": "{n} utan betyg",
"reviews.dialog.title": "Recension av {name}", "reviews.dialog.title": "Recension av {name}",
"reviews.dialog.note": "Signerad med din Nostr-nyckel och publicerad på offentliga reläer. Den kan varken redigeras eller raderas.", "reviews.dialog.lede.cashu": "Dela din upplevelse av denna mint.",
"reviews.dialog.lede.fedimint": "Dela din upplevelse av denna federation.",
"reviews.dialog.lede.lnurl": "Dela din upplevelse av denna mint.",
"reviews.dialog.permanent": "Publiceras permanent på publika Nostr-relayer.",
"reviews.dialog.gate.title": "Logga in för att skriva en recension",
"reviews.dialog.gate.lead": "Recensioner signeras med din Nostr-nyckel. Den stannar i din signer, den här webbplatsen ser den aldrig.",
"reviews.dialog.ratingLegend": "Ditt betyg", "reviews.dialog.ratingLegend": "Ditt betyg",
"reviews.dialog.starsSr.one": "{n}-stjärna", "reviews.dialog.starsSr.one": "{n}-stjärna",
"reviews.dialog.starsSr.other": "{n} stjärnor", "reviews.dialog.starsSr.other": "{n} stjärnor",
@@ -308,7 +322,6 @@
"reviews.dialog.waitingSigner": "Väntar på din undertecknare", "reviews.dialog.waitingSigner": "Väntar på din undertecknare",
"reviews.dialog.confirmClose": "Vill du stänga den här recensionen? Ditt utkast sparas tills du lämnar webbplatsen.", "reviews.dialog.confirmClose": "Vill du stänga den här recensionen? Ditt utkast sparas tills du lämnar webbplatsen.",
"reviews.dialog.why.login": "Logga in för att publicera", "reviews.dialog.why.login": "Logga in för att publicera",
"reviews.dialog.why.rating": "Välj ett betyg",
"reviews.dialog.why.words": "Skriv några ord", "reviews.dialog.why.words": "Skriv några ord",
"reviews.dialog.why.more.one": "{n} fler tecken", "reviews.dialog.why.more.one": "{n} fler tecken",
"reviews.dialog.why.more.other": "{n} fler tecken", "reviews.dialog.why.more.other": "{n} fler tecken",
@@ -324,8 +337,8 @@
"reviews.validate.rating": "Välj ett betyg mellan 1 och 5 stjärnor.", "reviews.validate.rating": "Välj ett betyg mellan 1 och 5 stjärnor.",
"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 recensionerna av mints 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",
@@ -333,12 +346,12 @@
"feed.showingBuild.other": "Här visas de senaste recensionerna av {n} från den senaste versionen", "feed.showingBuild.other": "Här visas de senaste recensionerna av {n} från den senaste versionen",
"feed.noscript": "Recensionerna är signerade med Nostr och laddas i din webbläsare, så för filtrering och den fullständiga listan krävs JavaScript. Recensionerna med {n} nedan hämtades från reläerna när denna sida skapades.", "feed.noscript": "Recensionerna är signerade med Nostr och laddas i din webbläsare, så för filtrering och den fullständiga listan krävs JavaScript. Recensionerna med {n} nedan hämtades från reläerna när denna sida skapades.",
"feed.emptyBuild": "Inga avläsningar kunde hämtas från reläerna när denna sida skapades. Listan uppdateras automatiskt från reläerna i din webbläsare.", "feed.emptyBuild": "Inga avläsningar kunde hämtas från reläerna när denna sida skapades. Listan uppdateras automatiskt från reläerna i din webbläsare.",
"feed.foot": "Recensioner är åsikter från användargemenskapen, inte verifierade påståenden. Läs om {link} innan du anförtror en kryptovalutabörs dina pengar.", "feed.foot": "Recensioner är åsikter från communityn, inte verifierade påståenden. Läs {link} innan du anförtror ett mint dina pengar.",
"feed.footLink": "ansvarsfriskrivning", "feed.footLink": "ansvarsfriskrivning",
"feed.showing": "Visar recensioner av {shown} och {total}", "feed.showing": "Visar recensioner av {shown} och {total}",
"feed.showingMatching": "Visar {shown} av {matching} matchande recensioner (totalt {total})", "feed.showingMatching": "Visar {shown} av {matching} matchande recensioner (totalt {total})",
"feed.emptyRelays": "Inga recensioner har kommit in från reläerna. Öppna valfri Mint-sida för att läsa och skriva recensioner där.", "feed.emptyRelays": "Inga recensioner har kommit in från reläerna. Öppna valfri Mint-sida för att läsa och skriva recensioner där.",
"feed.emptyFilter": "Inga recensioner matchar detta filter. Rensa rutan eller prova ”Alla”.", "feed.emptyFilter": "Inga recensioner matchar detta filter. Rensa mint-rutan, eller prova Alla.",
"feed.relayError": "Det gick inte att nå reläerna. Visar recensionerna från den senaste versionen.", "feed.relayError": "Det gick inte att nå reläerna. Visar recensionerna från den senaste versionen.",
"feed.relayEmpty": "Inga granskningar har kommit tillbaka från reläerna. Visar granskningarna från den senaste versionen.", "feed.relayEmpty": "Inga granskningar har kommit tillbaka från reläerna. Visar granskningarna från den senaste versionen.",
"feed.reading": "Avläsning av reläerna…", "feed.reading": "Avläsning av reläerna…",
@@ -369,6 +382,10 @@
"login.status.waitingSigner": "Väntar på din undertecknare", "login.status.waitingSigner": "Väntar på din undertecknare",
"login.status.connecting": "Ansluta till din signatär", "login.status.connecting": "Ansluta till din signatär",
"login.status.connected": "Ansluten", "login.status.connected": "Ansluten",
"login.toast.loggedIn": "Inloggad",
"login.toast.fetchingProfile": "Hämtar din Nostr-profil från reläerna…",
"login.toast.profile": "Profil: {name}",
"login.toast.noProfile": "Ingen profil hittades på reläerna",
"login.method.extension": "Förlängning", "login.method.extension": "Förlängning",
"login.method.remote": "Fjärrstyrning", "login.method.remote": "Fjärrstyrning",
"login.method.primal": "Primal", "login.method.primal": "Primal",
@@ -392,7 +409,7 @@
"notfound.title": "Sidan hittades inte · Cashumints.space", "notfound.title": "Sidan hittades inte · Cashumints.space",
"notfound.description": "Den sidan finns inte på Cashumints.space.", "notfound.description": "Den sidan finns inte på Cashumints.space.",
"notfound.heading": "Den här sidan finns inte", "notfound.heading": "Den här sidan finns inte",
"notfound.lede": "Den kan ha flyttats, eller så har webbplatsen kanske inte indexerats ännu. Sök efter en webbplats efter namn eller domän, eller klistra in webbadressen till webbplatsen.", "notfound.lede": "Den kan ha flyttat, eller så är mintet inte indexerat än. Sök efter ett mint på namn eller domän, eller klistra in en mint-URL.",
"notfound.browse": "Se alla mints", "notfound.browse": "Se alla mints",
"notfound.home": "Gå hem", "notfound.home": "Gå hem",
"notfound.loading": "Laddar {host}", "notfound.loading": "Laddar {host}",
@@ -400,12 +417,19 @@
"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-wallets som fungerar med de mints som listas här, för mobil, webb, webbläsare och kommandoraden, med vad var och en är bra på.",
"wallets.heading": "Plånböcker", "wallets.heading": "Plånböcker",
"wallets.lede": "Du behöver en plånbok för att förvara ecash från någon av dessa utgivare. De flesta plånböcker kan kommunicera med flera utgivare samtidigt, så du är inte bunden till den första du provar.", "wallets.lede": "Du behöver en wallet för att hålla ecash från något av dessa mints. De flesta wallets pratar med flera mints samtidigt, så du är inte bunden till det första du provar.",
"wallets.note": "Denna lista är inte uttömmande och utgör inte någon rekommendation. Projektet Cashu för ett mer omfattande register på {link}. En plånbok innehåller nycklar, medan en myntverk förvarar pengarna: läs recensionerna av myntverken innan du väljer var du vill förvara dina sats.", "wallets.note": "Den här listan är inte uttömmande och är ingen rekommendation. Cashu-projektet har ett bredare register på {link}. En wallet håller nycklar, ett mint håller pengarna: läs recensionerna av mintet innan du väljer var dina sats ska bo.",
"wallets.platform.android": "Android", "wallets.platform.android": "Android",
"wallets.platform.ios": "iOS", "wallets.platform.ios": "iOS",
"wallets.platform.web": "Webb", "wallets.platform.web": "Webb",
@@ -413,61 +437,61 @@
"wallets.platform.cli": "CLI", "wallets.platform.cli": "CLI",
"wallets.platform.python": "Python", "wallets.platform.python": "Python",
"wallets.item.minibits": "Mobilplånboken ecash med egen mint-funktion, stöd för adresser av typen Lightning och kontakter av typen Nostr.", "wallets.item.minibits": "Mobilplånboken ecash med egen mint-funktion, stöd för adresser av typen Lightning och kontakter av typen Nostr.",
"wallets.item.nutstash": "En webbläsarbaserad plånbok som hanterar flera mynt samtidigt och importerar och exporterar råa tokens.", "wallets.item.nutstash": "Webbläsar-wallet som hanterar flera mints samtidigt och importerar och exporterar råa tokens.",
"wallets.item.enuts": "Mobilplånbok med fokus på saldon i flera valutor och överföring av ecash till Nostr.", "wallets.item.enuts": "Mobilplånbok med fokus på saldon i flera valutor och överföring av ecash till Nostr.",
"wallets.item.cashume": "Referenswebbplånbok från projektet Cashu. Körs helt i webbläsaren och kan installeras som PWA.", "wallets.item.cashume": "Referenswebbplånbok från projektet Cashu. Körs helt i webbläsaren och kan installeras som PWA.",
"wallets.item.boardwalk": "Webbplånbok baserad på Lightning-adresser och överföring av ecash via länk.", "wallets.item.boardwalk": "Webbplånbok baserad på Lightning-adresser och överföring av ecash via länk.",
"wallets.item.coinos": "Lightning – en plånbok och kassasystem som även driver sin egen Cashu-myntverk.", "wallets.item.coinos": "Lightning-wallet och kassasystem som också kör sitt eget Cashu-mint.",
"wallets.item.oxchat": "Nostr-messenger med en inbyggd ecash-plånbok, för att skicka sats i chattar.", "wallets.item.oxchat": "Nostr-messenger med en inbyggd ecash-plånbok, för att skicka sats i chattar.",
"wallets.item.nutshell": "Referensimplementeringen. Körs som en plånbok på kommandoraden och som myntprogramvara.", "wallets.item.nutshell": "Referensimplementationen. Körs som wallet på kommandoraden och som mint-programvara.",
"about.title": "Om Cashumints.space", "about.title": "Om Cashumints.space",
"about.description": "Vad Cashu är, vad en mint egentligen innehåller, vad NIP-87 är och hur den här webbplatsen indexerar mints och läser av recensioner från Nostr-reläer.", "about.description": "Vad Cashu är, vad en mint egentligen innehåller, vad NIP-87 är och hur den här webbplatsen indexerar mints och läser av recensioner från Nostr-reläer.",
"about.heading": "Om", "about.heading": "Om",
"about.lede": "Cashumints.space listar alla Cashu-mynt som vi kan hitta i Nostr-nätverket och visar vad folk säger om dem. Den är utvecklad av Azzamo.", "about.lede": "Cashumints.space listar varje Cashu-mint vi kan hitta i Nostr-nätverket och visar vad folk säger om dem. Den är byggd av Azzamo.",
"about.cashu.title": "Vad Cashu är", "about.cashu.title": "Vad Cashu är",
"about.cashu.p1": "Cashu är ett ecash-protokoll för Bitcoin. En myntverk utfärdar anonymiserade tokens som backas upp av de sats som det innehar, och dessa tokens överförs mellan användare utan att myntverket får reda på vem som skickade vad till vem. Sekretessen är verklig och det är just poängen: tokens är inte konton, och myntverket kan inte koppla samman den token som det har signerat med den token som kommer tillbaka.", "about.cashu.p1": "Cashu är ett ecash-protokoll för Bitcoin. Ett mint ger ut blindade tokens som backas av de sats det håller, och de tokens rör sig mellan människor utan att mintet får veta vem som skickade vad till vem. Integriteten är verklig och den är hela poängen: tokens är inte konton, och mintet kan inte koppla den token det signerade till den token som kommer tillbaka.",
"about.cashu.p2": "Du kan läsa protokollet och dess specifikationer på {link}. Specifikationerna själva kallas NUTs, och det är därifrån som siffrorna NUT på varje myntsida härstammar.", "about.cashu.p2": "Du kan läsa protokollet och dess specifikationer på {link}. Specifikationerna kallas NUTs, och det är därifrån NUT-numren på varje mint-sida kommer.",
"about.mints.title": "Vad en mynta är, och avvägningen", "about.mints.title": "Vad ett mint är, och avvägningen",
"about.mints.p1": "En myntning är en förvaringsprocess. När du myntar ecash överlämnar du sats till den som driver servern, och du litar på att denne kommer att respektera token när du smälter ut dem igen. Det finns inget skydd mot bedrägeri, ingen deponering och ingen möjlighet till regress. En myntningsoperatör kan försvinna med saldot, och det har hänt.", "about.mints.p1": "Ett mint är förvarande. När du mintar ecash lämnar du sats till den som driver servern, och du litar på att den infriar dina tokens när du meltar ut dem igen. Det finns inget bedrägeribevis, ingen deposition och ingen möjlighet till upprättelse. En mint-operatör kan försvinna med saldot, och mints har gjort det.",
"about.mints.p2": "Det är just därför som det finns recensioner här. Den här webbplatsen påstår aldrig att en kryptovaluta är säker. Statusen på en kryptovalutasida visar {code}, inte ”betrodd”, och det betyder bara att servern svarade när vi senast frågade. Behandla saldon med ecash som kontanter i en rockficka: bra att spendera, men inte lämpliga att spara.", "about.mints.p2": "Det är precis därför recensioner finns här. Den här webbplatsen säger aldrig att ett mint är säkert. Statusen på en mint-sida säger {code}, inte ”betrodd”, och den betyder bara att servern svarade när vi frågade senast. Behandla ecash-saldon som kontanter i en rockficka: bra att spendera, fel att spara.",
"about.nip87.title": "Vad NIP-87 är", "about.nip87.title": "Vad NIP-87 är",
"about.nip87.p1": "NIP-87 är Nostr-konventionen för att tillkännage och rekommendera ecash-mynt. Två typer av händelser är relevanta här:", "about.nip87.p1": "NIP-87 är Nostr-konventionen för att annonsera och rekommendera ecash-mints. Två händelsetyper spelar roll här:",
"about.nip87.li1": "{code} meddelar en ny utgivning. URL:en till den nya utgivningen finns i taggen {tag}. Det är så webbplatsen upptäcker nya utgivningar som den aldrig har sett tidigare.", "about.nip87.li1": "{code} annonserar ett mint. Mint-URL:en ligger i {tag}-taggen. Det är så webbplatsen hittar mints den aldrig sett.",
"about.nip87.li2": "{code} är en recension. Den innehåller betyg och recensionstext, och hänvisar till det mynt den handlar om med en {utag}-tagg (myntets URL) och en {dtag}-tagg (myntets egen publika nyckel).", "about.nip87.li2": "{code} är en recension. Den bär betyget och recensionstexten, och pekar på det mint den handlar om med en {utag}-tagg (mintets URL) och en {dtag}-tagg (mintets egen publika nyckel).",
"about.nip87.p2": "Varje recension är signerad med författarens nyckel Nostr. Vi betecknar aldrig en recension som ”verifierad”, eftersom ingen har verifierat den: den är signerad, vilket är ett annat och mer användbart påstående. Det innebär att du kan kontrollera vem som har skrivit den och att den inte har ändrats. Det innebär inte att författaren är ärlig eller att hen någonsin har använt myntet.", "about.nip87.p2": "Varje recension är signerad med författarens Nostr-nyckel. Vi kallar aldrig en recension ”verifierad”, för ingen har verifierat den: den är signerad, vilket är ett annat och mer användbart påstående. Det betyder att du kan kontrollera vem som skrev den och att den inte har ändrats. Det betyder inte att författaren är ärlig eller någonsin har använt mintet.",
"about.how.title": "Så här fungerar den här webbplatsen", "about.how.title": "Så här fungerar den här webbplatsen",
"about.how.p1": "Det finns två slingor och en regel. Regeln är att myntverket äger sina metadata och att Nostr äger recensionerna, så den här webbplatsen lagrar varken det ena eller det andra som en auktoritet.", "about.how.p1": "Det finns två slingor och en regel. Regeln är att mintet äger sina metadata och Nostr äger recensionerna, så den här webbplatsen lagrar ingetdera som auktoritet.",
"about.how.li1": "{strong} skickar en förfrågan till alla kända myntverk om {code} var tionde minut och registrerar svaret: namn, beskrivning, version, stödda NUTs, begränsningar, kontaktuppgifter. Den registrerar även om myntverket svarade överhuvudtaget, vilket är grunden för drifttiden.", "about.how.li1": "{strong} frågar varje känt mint efter {code} var tionde minut och registrerar vad det säger: namn, beskrivning, version, stödda NUTs, gränser, kontaktuppgifter. Den registrerar också om mintet svarade alls, vilket är varifrån drifttiden kommer.",
"about.how.li1Label": "Mätkretsen", "about.how.li1Label": "Mätkretsen",
"about.how.li2": "{strong} ansluter till Nostr-reläerna varje timme, läser meddelanden och recensioner om mynt och lägger till alla mynt som den inte har sett tidigare.", "about.how.li2": "{strong} ansluter till Nostr-reläerna varje timme, läser mint-annonseringar och recensioner, och lägger till varje mint den inte sett förut.",
"about.how.li2Label": "Upptäcktscykeln", "about.how.li2Label": "Upptäcktscykeln",
"about.how.p2": "Det viktiga är vad som händer när en myntbutik slutar svara. Dess cachelagrade metadata skrivs aldrig över av en misslyckad förfrågan, så en nedlagd myntbutik behåller sin fullständiga sida: specifikationer, historik och recensioner, tillsammans med en banner som anger när den stängdes ner. Den tidigare versionen av den här webbplatsen visade istället en felsida, vilket innebar att just de myntbutiker som användarna mest behövde varna varandra för var de som inte kunde recenseras. Det var den främsta anledningen till ombyggnaden.", "about.how.p2": "Det viktiga är vad som händer när ett mint slutar svara. Dess cachade metadata skrivs aldrig över av en misslyckad kontroll, så ett dött mint behåller hela sin sida: sina specifikationer, sin historik och sina recensioner, med en banner som säger när det tystnade. Den tidigare versionen av webbplatsen visade en felsida istället, vilket innebar att just de mints som folk mest behövde varna varandra för var de som inte gick att recensera. Det var huvudskälet till ombyggnaden.",
"about.how.p3": "Recensionstexter lagras aldrig hos oss. När du öppnar en mint-sida kommunicerar din webbläsare direkt med reläerna och hämtar recensionerna själv, och när du skriver en recension signeras den av din egen Nostr-förlängning och skickas direkt till dessa reläer. Vi håller reda på antalet så att sidorna kan rangordnas och sorteras, men inget mer än så. Ingenting här kan redigeras eller raderas av oss, inte heller recensioner som vi helst skulle vilja slippa.", "about.how.p3": "Recensionstexter lagras aldrig hos oss. När du öppnar en mint-sida kommunicerar din webbläsare direkt med reläerna och hämtar recensionerna själv, och när du skriver en recension signeras den av din egen Nostr-förlängning och skickas direkt till dessa reläer. Vi håller reda på antalet så att sidorna kan rangordnas och sorteras, men inget mer än så. Ingenting här kan redigeras eller raderas av oss, inte heller recensioner som vi helst skulle vilja slippa.",
"about.relays.title": "Reläer som vi läser om", "about.relays.title": "Reläer som vi läser om",
"about.relays.p1": "Det här är de reläer som både indexeraren och din webbläsare använder:", "about.relays.p1": "Det här är de reläer som både indexeraren och din webbläsare använder:",
"about.relays.p2": "Rikta vilken Nostr eller cli som helst mot samma reläer så kommer du att se samma händelser. Det är hela löftet: ingenting här är sant bara för att vi säger det.", "about.relays.p2": "Rikta vilken Nostr eller cli som helst mot samma reläer så kommer du att se samma händelser. Det är hela löftet: ingenting här är sant bara för att vi säger det.",
"about.nuts.title": "Vad är NUTs?", "about.nuts.title": "Vad är NUTs?",
"about.nuts.p1": "NUTs är de numrerade specifikationerna för Cashu, precis som BIP:er för Bitcoin. En myntverk anger vilka av dessa den implementerar, och det visar vad den faktiskt kan göra: NUT-04 och NUT-05 hanterar prägling och smältning via Lightning (utan dem är ett myntverk inte till mycket nytta), NUT-07 låter en plånbok kontrollera om en token redan har använts, NUT-17 ger plånböcker realtidsuppdateringar via WebSockets istället för polling, och NUT-20 låter en plånbok bevisa att en myntkvot verkligen utfärdades till den. Den fullständiga listan finns på {link}.", "about.nuts.p1": "NUTs är de numrerade Cashu-specifikationerna, som BIP:er för Bitcoin. Ett mint anger vilka det implementerar, och det säger dig vad det faktiskt kan göra: NUT-04 och NUT-05 är minting och melting via Lightning (utan dem är ett mint inte till mycket nytta), NUT-07 låter en wallet kontrollera om en token redan är spenderad, NUT-17 ger wallets liveuppdateringar via WebSockets istället för polling, och NUT-20 låter en wallet bevisa att en mint-quote verkligen utfärdades till den. Hela listan finns på {link}.",
"about.numbers.title": "Just nu", "about.numbers.title": "Just nu",
"about.numbers.p1": "{mints}-enheterna är indexerade, {online} av dem svarar, {reviews}-enheterna räknas utifrån reläerna ovan.", "about.numbers.p1": "{mints}-enheterna är indexerade, {online} av dem svarar, {reviews}-enheterna räknas utifrån reläerna ovan.",
"about.source.title": "Källa och upphovsangivelse", "about.source.title": "Källa och upphovsangivelse",
"about.source.p1": "Webbplatsen är öppen källkod och finns på {github}. Den har skapats och drivs av {azzamo}. Om det finns något fel på en myntsida, korrigera det i källkoden: myntets metadata hämtas från myntets egen {code}, och recensionerna hämtas från Nostr.", "about.source.p1": "Webbplatsen är öppen källkod på {github}. Den är byggd och driven av {azzamo}. Om något är fel på en mint-sida, fixa det vid källan: mint-metadata kommer från mintets eget {code}, och recensioner kommer från Nostr.",
"terms.title": "Användarvillkor · Cashumints.space", "terms.title": "Användarvillkor · Cashumints.space",
"terms.description": "Användarvillkor för Cashumints.space: vad webbplatsen är, vad den inte gör och vem som ansvarar för vad. Inga konton, inga garantier, ingen anknytning till något myntverk.", "terms.description": "Användarvillkor för Cashumints.space: vad webbplatsen är, vad den inte gör och vem som ansvarar för vad. Inga konton, inga garantier, ingen anknytning till något mint.",
"terms.heading": "Användarvillkor", "terms.heading": "Användarvillkor",
"terms.lede": "Enkel förklaring av en webbplats som endast indexerar offentliga data. Senast uppdaterad {date}.", "terms.lede": "Enkel förklaring av en webbplats som endast indexerar offentliga data. Senast uppdaterad {date}.",
"terms.what.title": "Vad den här webbplatsen handlar om", "terms.what.title": "Vad den här webbplatsen handlar om",
"terms.what.p1": "Cashumints.space är ett index över offentligt tillgängliga uppgifter om Cashu-mynt. Två typer av uppgifter, båda hämtade från andra källor:", "terms.what.p1": "Cashumints.space är ett index över offentligt tillgängliga data om Cashu-mints. Två sorters data, båda från någon annanstans:",
"terms.what.li1": "Metadata om myntutgivare, hämtade från varje myntutgivares egen {code}-ändpunkt: namn, beskrivning, programvaruversion, stöd för NUTs, kontaktuppgifter och om myntutgivaren svarade när vi senast ställde en fråga.", "terms.what.li1": "Mint-metadata, läst från varje mints eget {code}-endpoint: namn, beskrivning, programvaruversion, stödda NUTs, kontaktuppgifter och om mintet svarade när vi frågade senast.",
"terms.what.li2": "Recensioner, hämtade från de offentliga Nostr-reläerna. Det rör sig om NIP-87-händelser som publicerats av sina författare till reläer som vem som helst kan söka information från.", "terms.what.li2": "Recensioner, hämtade från de offentliga Nostr-reläerna. Det rör sig om NIP-87-händelser som publicerats av sina författare till reläer som vem som helst kan söka information från.",
"terms.what.p2": "Vi samlar in dessa uppgifter, normaliserar dem, rangordnar dem och visar dem. Vi skapar dem inte.", "terms.what.p2": "Vi samlar in dessa uppgifter, normaliserar dem, rangordnar dem och visar dem. Vi skapar dem inte.",
"terms.noAccounts.title": "Inga konton, inga garantier", "terms.noAccounts.title": "Inga konton, inga garantier",
"terms.noAccounts.p1": "Det finns inget att registrera sig för. Webbplatsen tillhandahålls i befintligt skick, utan någon som helst garanti: varken för att uppgifterna är korrekta, att de är aktuella eller att webbplatsen kommer att vara tillgänglig. Uppgifterna om mynt kan vara minutes eller timmar gamla, ett mynt kan ha förändrats sedan vi senast kontrollerade det, och en relä kan sluta svara, vilket innebär att du ser mindre än vad som faktiskt finns.", "terms.noAccounts.p1": "Det finns inget att registrera sig för. Webbplatsen tillhandahålls som den är, utan garanti av något slag: inte att data stämmer, inte att de är aktuella, inte att webbplatsen är tillgänglig. Mint-data kan vara minuter eller timmar gamla, ett mint kan ha ändrats sedan vi kontrollerade senast, och ett relä kan låta bli att svara, och då ser du mindre än vad som finns.",
"terms.noAccounts.p2": "Använd den på egen risk. Vi tar inget ansvar för eventuella förluster som uppstår till följd av att du använder denna webbplats eller förlitar dig på informationen här. Se {link} för information om de specifika riskerna med att använda en Cashu-myntmaskin.", "terms.noAccounts.p2": "Använd den på egen risk. Vi ansvarar inte för förluster som följer av att använda den här webbplatsen eller av att förlita sig på något du läser här. Se {link} för de specifika riskerna med att använda ett Cashu-mint.",
"terms.noAccounts.link": "ansvarsfriskrivning", "terms.noAccounts.link": "ansvarsfriskrivning",
"terms.noCustody.title": "Vi förvarar inte dina pengar", "terms.noCustody.title": "Vi förvarar inte dina pengar",
"terms.noCustody.p1": "Denna webbplats hanterar aldrig några medel. Den har ingen plånbok, kommer inte i kontakt med ecash och kan inte flytta, spärra eller återställa någonting. Om du har problem med en myntverk är det endast myntverkets operatör som kan hjälpa dig, och vi kan inte kontakta dem å dina vägnar.", "terms.noCustody.p1": "Den här webbplatsen tar aldrig hand om medel. Den har ingen wallet, den rör inte ecash, och den kan inte flytta, frysa eller återställa något. Om du har problem med ett mint är mint-operatören den enda part som kan hjälpa dig, och vi kan inte kontakta dem åt dig.",
"terms.noCustody.p2": "Vi har inga kopplingar till något av de myntverk som listas här, och inget myntverk betalar för att bli listat, för sin placering eller för sitt betyg. Myntverken hämtas från offentliga Nostr-meddelanden och rangordnas av en algoritm utifrån offentliga omdömen.", "terms.noCustody.p2": "Vi har ingen koppling till något mint som listas här, och inget mint betalar för sin listning, sin placering eller sitt betyg. Mints hittas via offentliga Nostr-annonseringar och rangordnas av en algoritm över offentliga recensionsdata.",
"terms.reviews.title": "Recensioner är andras ord", "terms.reviews.title": "Recensioner är andras ord",
"terms.reviews.p1": "Recensioner är användargenererat innehåll som publiceras på de offentliga Nostr-reläerna av sina författare. Vi visar dem, men vi kontrollerar dem inte. Vi har inte skrivit dem, vi kan inte redigera dem och vi kan inte ta bort dem från de reläer där de finns: de är signerade händelser i ett nätverk som vi inte äger.", "terms.reviews.p1": "Recensioner är användargenererat innehåll som publiceras på de offentliga Nostr-reläerna av sina författare. Vi visar dem, men vi kontrollerar dem inte. Vi har inte skrivit dem, vi kan inte redigera dem och vi kan inte ta bort dem från de reläer där de finns: de är signerade händelser i ett nätverk som vi inte äger.",
"terms.reviews.p2": "En recension är en enskild persons upplevelse, inte ett verifierat faktum. Om du publicerar en recension är du ansvarig för dess innehåll. Publicera inte något olagligt, och tänk på att det du publicerar är offentligt och blir kvar för alltid (se {link}).", "terms.reviews.p2": "En recension är en enskild persons upplevelse, inte ett verifierat faktum. Om du publicerar en recension är du ansvarig för dess innehåll. Publicera inte något olagligt, och tänk på att det du publicerar är offentligt och blir kvar för alltid (se {link}).",
@@ -511,33 +535,33 @@
"privacy.permanent.li2": "Den är permanent kopplad till din publika nyckel Nostr, tillsammans med allt annat som den nyckeln någonsin har publicerat.", "privacy.permanent.li2": "Den är permanent kopplad till din publika nyckel Nostr, tillsammans med allt annat som den nyckeln någonsin har publicerat.",
"privacy.permanent.li3": "Att ta bort den är en begäran, inte en order. Du kan publicera en borttagningshändelse och vissa reläer kommer att följa den, men ett relä kan välja att ignorera den och alla som redan har kopierat händelsen har den kvar. Betrakta en publicerad recension som permanent.", "privacy.permanent.li3": "Att ta bort den är en begäran, inte en order. Du kan publicera en borttagningshändelse och vissa reläer kommer att följa den, men ett relä kan välja att ignorera den och alla som redan har kopierat händelsen har den kvar. Betrakta en publicerad recension som permanent.",
"privacy.permanent.li4": "Vi kan inte ta bort det åt dig. Vi kan sluta visa det här, men det påverkar inte reläerna på något sätt.", "privacy.permanent.li4": "Vi kan inte ta bort det åt dig. Vi kan sluta visa det här, men det påverkar inte reläerna på något sätt.",
"privacy.permanent.p2": "Om du vill recensera en myntbutik utan att det kopplas till din huvudidentitet, använd en separat nyckel med koden Nostr. Observera att en nyckel utan någon annan aktivitet framgår tydligt vid hårda recensioner, eftersom en helt ny nyckel som skriver en enstjärnig recension är ett mönster som läsarna förtjänar att se.", "privacy.permanent.p2": "Om du vill recensera ett mint utan att det knyts till din huvudidentitet, använd en separat Nostr-nyckel. Observera att en nyckel utan annan aktivitet visas som sådan vid hårda recensioner, eftersom en helt ny nyckel som skriver en enstjärnig recension är ett mönster läsare förtjänar att se.",
"privacy.contact.title": "Frågor", "privacy.contact.title": "Frågor",
"privacy.contact.p1": "Webbplatsen är öppen källkod: om du vill kontrollera något av ovanstående kan du läsa koden på {github}. Frågor och rättelser kan lämnas som ett ärende där.", "privacy.contact.p1": "Webbplatsen är öppen källkod: om du vill kontrollera något av ovanstående kan du läsa koden på {github}. Frågor och rättelser kan lämnas som ett ärende där.",
"disclaimer.title": "Ansvarsfriskrivning · Cashumints.space", "disclaimer.title": "Ansvarsfriskrivning · Cashumints.space",
"disclaimer.description": "Vad du bör veta innan du använder en Cashu-mynta: mynt är förvaringsbaserade, en mynta kan försvinna tillsammans med dina pengar, recensioner är åsikter och ”online” betyder endast att de är tillgängliga, inte att de är tillförlitliga.", "disclaimer.description": "Vad du bör veta innan du använder ett Cashu-mint: mints är förvarande, ett mint kan försvinna med dina pengar, recensioner är åsikter, och online betyder nåbar, inte pålitlig.",
"disclaimer.heading": "Ansvarsfriskrivning", "disclaimer.heading": "Ansvarsfriskrivning",
"disclaimer.lede": "Läs detta innan du satsar pengar på ett mynt. Senast uppdaterad {date}.", "disclaimer.lede": "Läs det här innan du lägger pengar i ett mint. Senast uppdaterad {date}.",
"disclaimer.callout": "En Cashu-mynt förvarar dina pengar åt dig. Om den försvinner är din ecash borta, och det finns ingen att vända sig till. Förvara bara det du har råd att förlora.", "disclaimer.callout": "Ett Cashu-mint förvarar dina pengar åt dig. Om det försvinner är din ecash borta, och det finns ingen att vända sig till. Ha bara kvar det du har råd att förlora.",
"disclaimer.custodial.title": "Mint-mynt är förvaringsmynt", "disclaimer.custodial.title": "Mints är förvarande",
"disclaimer.custodial.p1": "När du präglar ecash överlämnar du bitcoin till präglingsoperatören och får tillbaka tokens. Dessa tokens utgör en fordran på den operatören. De är inte bitcoin som du innehar, och det är operatören som avgör om de ska godtas.", "disclaimer.custodial.p1": "När du mintar ecash lämnar du bitcoin till mint-operatören och får tillbaka tokens. De tokens är en fordran på den operatören. De är inte bitcoin du håller, och operatören är den som avgör om de infrias.",
"disclaimer.custodial.p2": "Det innebär att alla myntverk medför samma risker: operatören kan försvinna, tvingas upphöra med verksamheten, förlora medlen på grund av ett programfel eller en hackerattack, eller helt enkelt stänga av servern. Det finns ingen insättningsgaranti, ingen återbetalning vid transaktionsåterkallelse, ingen supportlinje och inga rättsliga möjligheter att förlita sig på. Detta gäller även det högst rankade myntverket på denna webbplats.", "disclaimer.custodial.p2": "Det betyder att varje mint bär samma risker: operatören kan försvinna, kan tvingas sluta, kan förlora medlen genom en bugg eller ett intrång, eller kan helt enkelt stänga av servern. Det finns ingen insättningsgaranti, ingen återbetalning, ingen supportlinje och ingen rättslig väg värd att räkna med. Detta gäller även det högst rankade mintet på den här webbplatsen.",
"disclaimer.custodial.p3": "Cashu är avsett för små belopp som omsätts snabbt. Använd det på följande sätt: placera där de pengar du snart ska spendera, inte dina besparingar, och sprid ut dem på flera olika plattformar istället för att lägga allt på en enda.", "disclaimer.custodial.p3": "Cashu är gjort för små belopp som rör sig snabbt. Använd det så: håll det du är på väg att spendera, inte ditt sparande, och sprid över flera mints i stället för att parkera allt i ett.",
"disclaimer.reviews.title": "Recensioner är åsikter, inte verifierade påståenden", "disclaimer.reviews.title": "Recensioner är åsikter, inte verifierade påståenden",
"disclaimer.reviews.p1": "Varje recension här har skrivits av någon med användarnamnet Nostr och signerats med den personens nyckel. Att den är signerad innebär att vi kan se vilken nyckel som har skrivit den. Det innebär inte att recensionen är sann, att författaren har använt myntverket eller att författaren inte är operatören, en konkurrent eller en och samma person med tio nycklar.", "disclaimer.reviews.p1": "Varje recension här skrevs av någon på Nostr och signerades med deras nyckel. Signerad betyder att vi kan tala om vilken nyckel som skrev den. Det betyder inte att recensionen är sann, att författaren använde mintet, eller att författaren inte är operatören, en konkurrent eller en person med tio nycklar.",
"disclaimer.reviews.p2": "Vi kontrollerar inte de som skriver recensioner, och vi redigerar eller tar inte bort recensioner som vi inte håller med om. Läs några stycken, se vad de faktiskt säger, och bedöm en produkt med tre recensioner annorlunda än en med trehundra.", "disclaimer.reviews.p2": "Vi verifierar inte recensenter, och vi redigerar eller tar inte bort recensioner vi inte håller med om. Läs några, se vad de faktiskt säger, och väg ett mint med tre recensioner annorlunda än ett med trehundra.",
"disclaimer.status.title": "”Online” betyder tillgänglig, inte pålitlig", "disclaimer.status.title": "”Online” betyder tillgänglig, inte pålitlig",
"disclaimer.status.p1": "Vi kontrollerar varje mint regelbundet och visar om den har svarat. Det är allt som en grön status betyder: servern har svarat. Det säger ingenting om huruvida operatören är solvent, ärlig eller fortfarande har för avsikt att infria dina tokens i morgon.", "disclaimer.status.p1": "Vi kontrollerar varje mint regelbundet och visar om den har svarat. Det är allt som en grön status betyder: servern har svarat. Det säger ingenting om huruvida operatören är solvent, ärlig eller fortfarande har för avsikt att infria dina tokens i morgon.",
"disclaimer.status.p2": "En myntverk kan vara online ända fram till det ögonblick då den inte längre är det. Uppehållshistorik, stöd för NUTs och programvaruversion ger dig viss information om hur ett myntverk drivs, men ingen av dessa faktorer utgör någon garanti för dina pengar.", "disclaimer.status.p2": "Ett mint kan vara online ända fram till det ögonblick det inte är det. Drifttidshistorik, stödda NUTs och programvaruversion säger något om hur ett mint drivs, men inget av det är ett löfte om dina pengar.",
"disclaimer.ranking.title": "Hur rankningen fungerar, och vad den inte innebär", "disclaimer.ranking.title": "Hur rankningen fungerar, och vad den inte innebär",
"disclaimer.ranking.p1": "Mints rangordnas enligt en algoritm baserad på data från offentliga recensioner: betyg, antal recensioner och senaste aktivitet, där online-mints som standard prioriteras framför offline-mints. Mer information finns i den öppna källkoden.", "disclaimer.ranking.p1": "Mints rangordnas enligt en algoritm baserad på data från offentliga recensioner: betyg, antal recensioner och senaste aktivitet, där online-mints som standard prioriteras framför offline-mints. Mer information finns i den öppna källkoden.",
"disclaimer.ranking.p2": "Ingen betalar för sin placering. Det finns inga sponsrade platser, inga marknadsförda listningar och inga avtal med något myntverk på denna webbplats. En hög placering innebär endast att algoritmen uppskattade de offentliga uppgifterna – inget mer. Det är varken en rekommendation eller ett säkerhetsbetyg.", "disclaimer.ranking.p2": "Ingen betalar för placering. Det finns ingen sponsrad plats, ingen marknadsförd listning och ingen överenskommelse med något mint på den här webbplatsen. En hög placering betyder att algoritmen gillade de offentliga uppgifterna, inget mer. Det är ingen rekommendation och det är inget säkerhetsbetyg.",
"disclaimer.notAdvice.title": "Detta är inte finansiell rådgivning", "disclaimer.notAdvice.title": "Detta är inte finansiell rådgivning",
"disclaimer.notAdvice.p1": "Ingenting på denna webbplats utgör finansiell, investerings-, juridisk eller skatterådgivning. Vi känner inte till din situation och ger dig inga råd. Det är du som bestämmer om du vill använda en myntpress och vilken du ska välja.", "disclaimer.notAdvice.p1": "Ingenting på den här webbplatsen är finansiell, investerings-, juridisk eller skatterådgivning. Vi känner inte till din situation och vi ger dig inga råd. Att använda ett mint, och att välja vilket, är ditt beslut.",
"disclaimer.dyor.title": "Gör din egen efterforskning", "disclaimer.dyor.title": "Gör din egen efterforskning",
"disclaimer.dyor.li1": "Kolla upp operatören. En sajt som drivs av någon som har ett offentligt rykte att förlora är en helt annan sak än en anonym webbadress.", "disclaimer.dyor.li1": "Slå upp operatören. Ett mint som drivs av någon med ett offentligt rykte att förlora är något annat än en anonym URL.",
"disclaimer.dyor.li2": "Läs myntets egna villkor och dess MOTD. Ett mynt som kallar sig för ett test eller en betaversion menar just det.", "disclaimer.dyor.li2": "Läs mintets egna villkor och dess MOTD. Ett mint som kallar sig ett test eller en beta menar det.",
"disclaimer.dyor.li3": "Börja i liten skala. Prägla lite, spendera det, smält ner det igen och se vad som händer innan du satsar mer på det.", "disclaimer.dyor.li3": "Börja smått. Minta lite, spendera det, melta ut det igen, och se vad som händer innan du anförtror det mer.",
"disclaimer.dyor.li4": "Håll koll på gränsvärdena. Ett lågt maximivärde är ofta ett tecken på en försiktig operatör, medan ett högt värde inte nödvändigtvis innebär att operatören är säker.", "disclaimer.dyor.li4": "Håll koll på gränsvärdena. Ett lågt maximivärde är ofta ett tecken på en försiktig operatör, medan ett högt värde inte nödvändigtvis innebär att operatören är säker.",
"disclaimer.dyor.li5": "Se till att du har en säkerhetskopia av dina tokens och kom ihåg att en token är ett innehavarvärdepapper: den som innehar den kan använda den.", "disclaimer.dyor.li5": "Se till att du har en säkerhetskopia av dina tokens och kom ihåg att en token är ett innehavarvärdepapper: den som innehar den kan använda den.",
"disclaimer.site.title": "Om själva webbplatsen", "disclaimer.site.title": "Om själva webbplatsen",
@@ -546,32 +570,32 @@
"disclaimer.updated": "20 augusti 2026", "disclaimer.updated": "20 augusti 2026",
"login.getPrimal": "Skaffa Primal", "login.getPrimal": "Skaffa Primal",
"reviews.dialog.identity.extensionGone": "Ditt webbläsartillägg svarar inte. Logga in igen eller använd en fjärrsignaturtjänst.", "reviews.dialog.identity.extensionGone": "Ditt webbläsartillägg svarar inte. Logga in igen eller använd en fjärrsignaturtjänst.",
"seo.orgDescription": "En förteckning över Cashu-minter, med aktuell status för varje mint och användarrecensioner signerade på Nostr.", "seo.orgDescription": "En katalog över Cashu-mints, med live-status från varje mint och recensioner från communityn signerade på Nostr.",
"seo.ogImageAlt": "Cashumints.space: hitta ett Cashu i nyskick som du kan lita på.", "seo.ogImageAlt": "Cashumints.space: hitta ett Cashu-mint du kan lita på.",
"seo.mint.title.one": "Recensioner av {name}: betyg på {rating} från recensionen av {n} | Cashumints.space", "seo.mint.title.one": "{name} recensioner: {rating} från {n} recension | Cashumints.space",
"seo.mint.title.other": "Recensioner av {name}: betyg på {rating} utifrån recensioner av {n} | Cashumints.space", "seo.mint.title.other": "{name} recensioner: {rating} från {n} recensioner | Cashumints.space",
"seo.mint.titleOffline.one": "{name}: offline för {n}-dagen | Cashumints.space", "seo.mint.titleOffline.one": "{name}: offline i {n} dag | Cashumints.space",
"seo.mint.titleOffline.other": "{name}: har varit offline i {n} dagar | Cashumints.space", "seo.mint.titleOffline.other": "{name}: offline i {n} dagar | Cashumints.space",
"seo.mint.titleNoReviews": "{name}: Information och recensioner om Cashu | Cashumints.space", "seo.mint.titleNoReviews": "{name}: info och recensioner om Cashu-mint | Cashumints.space",
"seo.fedimint.titleNoReviews": "{name}: Information och recensioner om Fedimint | Cashumints.space", "seo.fedimint.titleNoReviews": "{name}: Information och recensioner om Fedimint | Cashumints.space",
"seo.desc.fallback": "Användarrecensioner för {name} ({domain}).", "seo.desc.fallback": "Recensioner från communityn av {name} ({domain}).",
"seo.desc.fallbackDomain": "Användarrecensioner för Cashu mint på {domain}.", "seo.desc.fallbackDomain": "Recensioner från communityn av Cashu-mintet på {domain}.",
"seo.desc.rated.one": "Betygsatt {rating}/5 från {n}, undertecknad av Nostr – recension.", "seo.desc.rated.one": "{rating}/5 från {n} signerad Nostr-recension.",
"seo.desc.rated.other": "Betygsatta recensioner av {rating}/5 från {n}, signerade av Nostr.", "seo.desc.rated.other": "{rating}/5 från {n} signerade Nostr-recensioner.",
"seo.desc.noReviews": "Inga recensioner ännu.", "seo.desc.noReviews": "Inga recensioner ännu.",
"seo.desc.status": "{status}", "seo.desc.status": "{status}.",
"seo.desc.statusLimits": "{status}, gränser {limits}.", "seo.desc.statusLimits": "{status}, gränser {limits}.",
"seo.desc.fedModules": "{status}. Moduler: {modules}.", "seo.desc.fedModules": "{status}. Moduler: {modules}.",
"seo.desc.fedModulesNetwork": "{status}. Moduler: {modules} och {network}.", "seo.desc.fedModulesNetwork": "{status}. Moduler: {modules} på {network}.",
"seo.mint.ogAlt.one": "{name}: {rating} utgår från recensionen av {n} på Cashumints.space", "seo.mint.ogAlt.one": "{name}: {rating} stjärnor från {n} recension på Cashumints.space",
"seo.mint.ogAlt.other": "{name}: {rating} utgår från recensioner av {n} på Cashumints.space", "seo.mint.ogAlt.other": "{name}: {rating} stjärnor från {n} recensioner på Cashumints.space",
"seo.mint.ogAltNoReviews": "{name} på Cashumints.space", "seo.mint.ogAltNoReviews": "{name} på Cashumints.space",
"nav.home": "Hem", "nav.home": "Hem",
"mint.more.title": "Fler minttabletter", "mint.more.title": "Fler mints",
"mint.more.all": "Visa alla mints", "mint.more.all": "Visa alla mints",
"nav.fedimints": "Fedimints", "nav.fedimints": "Fedimints",
"status.announced": "Endast tillkännagivet", "status.announced": "Endast tillkännagivet",
"card.unnamedFederation": "Namnlös förbund", "card.unnamedFederation": "Namnlös federation",
"card.notChecked": "har aldrig kontrollerats", "card.notChecked": "har aldrig kontrollerats",
"fedimints.title": "Alla Fedimint-förbund · Cashumints.space", "fedimints.title": "Alla Fedimint-förbund · Cashumints.space",
"fedimints.description": "Alla {total}- och Fedimint-federationer som har tillkännagivits i Nostr-nätverket: {online} har bekräftats vara i drift genom en extern kontroll, {announced} har tillkännagivits men aldrig bekräftats. Användarbetyg, moduler och inbjudningskoder för var och en.", "fedimints.description": "Alla {total}- och Fedimint-federationer som har tillkännagivits i Nostr-nätverket: {online} har bekräftats vara i drift genom en extern kontroll, {announced} har tillkännagivits men aldrig bekräftats. Användarbetyg, moduler och inbjudningskoder för var och en.",
@@ -620,7 +644,7 @@
"fedimint.more.all": "Visa alla förbund", "fedimint.more.all": "Visa alla förbund",
"fedimint.disclaimer": "Att gå med i en sammanslutning innebär att man anförtror sina pengar åt dess förvaltare. Betygen är åsikter från gemenskapen, och att en verksamhet är bekräftad innebär att den är tillgänglig, inte att den är pålitlig.", "fedimint.disclaimer": "Att gå med i en sammanslutning innebär att man anförtror sina pengar åt dess förvaltare. Betygen är åsikter från gemenskapen, och att en verksamhet är bekräftad innebär att den är tillgänglig, inte att den är pålitlig.",
"fedimint.module.lightning": "Lightning", "fedimint.module.lightning": "Lightning",
"fedimint.module.mint": "Ecash i nyskick", "fedimint.module.mint": "Ecash-mint",
"fedimint.module.wallet": "Plånbok på blockkedjan", "fedimint.module.wallet": "Plånbok på blockkedjan",
"fedimint.module.meta": "Metadata", "fedimint.module.meta": "Metadata",
"fedimint.module.stability_pool": "Stabilitetsreserv", "fedimint.module.stability_pool": "Stabilitetsreserv",
@@ -635,8 +659,8 @@
"feed.ecosystem.all": "Alla", "feed.ecosystem.all": "Alla",
"feed.ecosystem.cashu": "Cashu", "feed.ecosystem.cashu": "Cashu",
"feed.ecosystem.fedimint": "Fedimint", "feed.ecosystem.fedimint": "Fedimint",
"feed.filterBySubject": "Filtrera efter mynt eller federation", "feed.filterBySubject": "Filtrera på mint eller federation",
"feed.anySubject": "Vilken mynt eller vilken myntförening som helst", "feed.anySubject": "Vilket mint eller vilken federation som helst",
"feed.clearSubject": "Rensa namnfiltret", "feed.clearSubject": "Rensa namnfiltret",
"home.topFedimints.title": "De bästa Fedimint-modellerna", "home.topFedimints.title": "De bästa Fedimint-modellerna",
"home.topFedimints.sub": "rangerade efter recensioner och betyg", "home.topFedimints.sub": "rangerade efter recensioner och betyg",
@@ -652,7 +676,113 @@
"mint.warnings.neverConfirmed.body.quiet": "Denna sammanslutning tillkännagavs på Nostr för {date} och {days} sedan; sedan dess har inget bekräftat att den är i drift, och ingen har granskat den på senare tid. All information nedan hämtas från tillkännagivandet.", "mint.warnings.neverConfirmed.body.quiet": "Denna sammanslutning tillkännagavs på Nostr för {date} och {days} sedan; sedan dess har inget bekräftat att den är i drift, och ingen har granskat den på senare tid. All information nedan hämtas från tillkännagivandet.",
"mint.warnings.neverConfirmed.meta": "{date} tillkännagavs, men bekräftades aldrig", "mint.warnings.neverConfirmed.meta": "{date} tillkännagavs, men bekräftades aldrig",
"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 minting, melting och supporten? Var konkret.",
"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-mint | Cashumints.space",
"seo.desc.statusWithdraw": "{status}, uttag {min}–{max} sat.",
"mint.chip.noFunding": "Ingen minting / melting",
"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": "Minting och melting ä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": "Minting och melting otillgängliga",
"mint.warnings.also.noFunding": "Dess Lightning-nod var också onåbar, så ingenting kunde mintas eller smältas."
} }
+242 -112
View File
@@ -33,7 +33,7 @@
"status.unknown": "Henüz kontrol edilmedi", "status.unknown": "Henüz kontrol edilmedi",
"nav.skip": "İçeriğe geç", "nav.skip": "İçeriğe geç",
"nav.main": "Ana", "nav.main": "Ana",
"nav.mints": "Cashu darphaneleri", "nav.mints": "Cashu mint'leri",
"nav.reviews": "İncelemeler", "nav.reviews": "İncelemeler",
"nav.wallets": "Cüzdanlar", "nav.wallets": "Cüzdanlar",
"nav.about": "Hakkında", "nav.about": "Hakkında",
@@ -41,22 +41,26 @@
"lang.label": "Dil", "lang.label": "Dil",
"lang.switcher": "Bir dil seçin", "lang.switcher": "Bir dil seçin",
"footer.copyright": "© {year} Cashumints.space", "footer.copyright": "© {year} Cashumints.space",
"footer.allMints": "Tüm Cashu mint ürünleri", "footer.allMints": "Tüm Cashu mint'leri",
"footer.allReviews": "Tüm incelemeler", "footer.allReviews": "Tüm incelemeler",
"footer.wallets": "Cüzdanlar", "footer.wallets": "Cüzdanlar",
"footer.about": "Hakkında", "footer.about": "Hakkında",
"footer.disclaimer": "Feragatname", "footer.disclaimer": "Feragatname",
"footer.terms": "Terimler", "footer.terms": "Koşullar",
"footer.privacy": "Gizlilik", "footer.privacy": "Gizlilik",
"footer.madeBy": "❤️ Yapım: {azzamo} · Cashu ve NIP-87 ile güçlendirilmiştir", "footer.madeBy": "{azzamo} tarafından ❤️ ile yapıldı · Cashu ve NIP-87 ile güçlendirilmiştir",
"footer.warning": "Mint ürünleri bakımlıdır. Bir mint parayla birlikte yok olabilir.", "footer.warning": "Mint'ler emanetçidir. Bir mint paranızla birlikte kaybolabilir.",
"footer.tagline": "ecash mint'leri hakkında topluluk incelemeleri, Nostr üzerinde imzalanmış.",
"footer.col.explore": "Keşfet",
"footer.col.site": "Site",
"footer.nav": "Alt bilgi",
"home.title": "Cashumints.space · Güvenebileceğiniz bir Cashu mint bulun", "home.title": "Cashumints.space · Güvenebileceğiniz bir Cashu mint bulun",
"home.description": "Güvenebileceğiniz bir Cashu minti bulun. Nostr'da imzalanmış {reviews} topluluk incelemesiyle {mints} mint indekslendi; ayrıca canlı çalışma durumu, desteklenen NUT'lar ve mint yazılımı bilgileri sunuluyor.", "home.description": "Güvenebileceğiniz bir Cashu minti bulun. Nostr'da imzalanmış {reviews} topluluk incelemesiyle {mints} mint indekslendi; ayrıca canlı çalışma durumu, desteklenen NUT'lar ve mint yazılımı bilgileri sunuluyor.",
"home.hero.title": "Bir Cashu mint bul", "home.hero.title": "Bir Cashu mint bul",
"home.hero.titleEm": "Güvenebilirsin", "home.hero.titleEm": "Güvenebilirsin",
"home.hero.lede": "Nostr ağındaki her bir mint, gerçek topluluk incelemeleriyle. Kapı bekçisi yok, sadece imzalı incelemeler kendiniz doğrulayabilirsiniz.", "home.hero.lede": "Nostr ağındaki her bir mint, gerçek topluluk incelemeleriyle. Kapı bekçisi yok, sadece imzalı incelemeler kendiniz doğrulayabilirsiniz.",
"home.search.label": "Madeleri isim, alan alanı veya URL ile aramak", "home.search.label": "Mint'leri ada, alan adına veya URL'ye göre arayın",
"home.search.placeholder": "İsim, alan adı ile arama yapmak veya yeni bir URL yapıştırmak için...", "home.search.placeholder": "Ada veya alan adına göre arayın, ya da bir mint URL'si yapıştırın…",
"home.filters.mostReviewed": "En çok inceleme", "home.filters.mostReviewed": "En çok inceleme",
"home.filters.highestRated": "En yüksek puan", "home.filters.highestRated": "En yüksek puan",
"home.filters.recentlyActive": "Son zamanlarda aktif", "home.filters.recentlyActive": "Son zamanlarda aktif",
@@ -79,101 +83,103 @@
"home.why.checked.title": "Sürekli kontrol edildi", "home.why.checked.title": "Sürekli kontrol edildi",
"home.why.checked.body": "Tüm mintlerde çalışma süresi, yazılım sürümü, limitler ve desteklenen NUT'ları inceliyoruz, böylece gördüğünüz özellikler güncel ve kendi raporlarımız değil.", "home.why.checked.body": "Tüm mintlerde çalışma süresi, yazılım sürümü, limitler ve desteklenen NUT'ları inceliyoruz, böylece gördüğünüz özellikler güncel ve kendi raporlarımız değil.",
"home.cta.title": "Mint mi düşünüyorsunuz? Önce oku.", "home.cta.title": "Mint mi düşünüyorsunuz? Önce oku.",
"home.cta.body": "Bir darphane, e-nakitinizi tutar. Saturasyon koymadan önce başkalarının ne bulduğunu okuyun ve kendiniz kullandığınızda bir yorum bırakın.", "home.cta.body": "Bir mint, ecash'inizi tutar. Sats yatırmadan önce başkalarının neler bulduğunu okuyun, kendiniz bir tanesini kullandığınızda da bir inceleme bırakın.",
"home.cta.explore": "Tüm mint fabrikalarını keşfedin", "home.cta.explore": "Tüm mint'leri keşfedin",
"home.cta.learn": "Cashu hakkında bilgi edinin", "home.cta.learn": "Cashu hakkında bilgi edinin",
"pulse.mintsIndexed": "Indekslenmiş Darphaneler", "pulse.mintsIndexed": "mint indekslendi",
"pulse.reviewsOnNostr": "Nostr'daki incelemeler", "pulse.reviewsOnNostr": "Nostr'daki incelemeler",
"pulse.lastReview": "Son inceleme", "pulse.lastReview": "Son inceleme",
"pulse.mintsOnline": "Mint Çevrimiçi", "pulse.mintsOnline": "mint çevrimiçi",
"pulse.indexer": "İndeksleyici", "pulse.indexer": "İndeksleyici",
"pulse.connected": "bağlantılı", "pulse.connected": "bağlantılı",
"pulse.stale": "bayat", "pulse.stale": "bayat",
"mints.title": "Tüm Cashu mint fabrikaları · Cashumints.space", "mints.title": "Tüm Cashu mint'leri · Cashumints.space",
"mints.description": "Tüm {total} Cashu darphaneleri Nostr ağından indekslenmiş, çevrimiçi ve çevrimdışı {online} topluluk dereceleri, çalışma süresi ve mint yazılımlarıyla {offline}. Tam listeyi arayın ve sıralayın.", "mints.description": "Nostr ağından indekslenmiş {total} Cashu mint'inin tamamı, {online} çevrimiçi ve {offline} çevrimdışı, topluluk puanları, çalışma süresi ve mint yazılımıyla birlikte. Tam listeyi arayın ve sıralayın.",
"mints.heading": "Tüm Cashu mint ürünleri", "mints.heading": "Tüm Cashu mint'leri",
"mints.lede": "Nostr ağında bulunan her mint, hatta yok olanlar dahil. Çevrimdışı mint ürünleri listede kalır, böylece onları bulup incelemeye devam edebilirsiniz.", "mints.lede": "Nostr ağında bulunan her mint, artık var olmayanlar dahil. Çevrimdışı mint'ler listede kalır, böylece onları bulmaya ve incelemeye devam edebilirsiniz.",
"mints.search.label": "Madenlerde isim veya alan adına göre arama", "mints.search.label": "Mint'leri ada veya alan adına göre arayın",
"mints.search.placeholder": "İsim veya alan adına göre ara...", "mints.search.placeholder": "İsim veya alan adına göre ara...",
"mints.sort.label": "Mint çeşitleri", "mints.sort.label": "Mint'leri sırala",
"mints.sort.score": "Genel olarak en iyi", "mints.sort.score": "Genel olarak en iyi",
"mints.sort.rating": "En yüksek puan", "mints.sort.rating": "En yüksek puan",
"mints.sort.reviews": "En çok inceleme", "mints.sort.reviews": "En çok inceleme",
"mints.sort.recent": "Son zamanlarda incelendi", "mints.sort.recent": "Son zamanlarda incelendi",
"mints.sort.seen": "Son zamanlarda internette görüldü", "mints.sort.seen": "Son zamanlarda internette görüldü",
"mints.hideOffline": "Çevrimdışı gizle", "mints.hideOffline": "Çevrimdışı gizle",
"mints.showing": "{total} mint göstermek ({online} çevrimiçi, {offline} çevrimdışı)", "mints.showing": "{total} mint gösteriliyor ({online} çevrimiçi, {offline} çevrimdışı)",
"mints.showingFiltered": "{total} mint mintinin {shown} gösteriliyor ({online} çevrimiçi, {offline} çevrimdışı)", "mints.showingFiltered": "{total} mint'in {shown} tanesi gösteriliyor ({online} çevrimiçi, {offline} çevrimdışı)",
"mints.noResults": "Mint uyumu yok. Bir alan adı deneyin ya da bir mint URL'sini yapıştırın.", "mints.noResults": "Eşleşen mint yok. Bir alan adı deneyin ya da bir mint URL'si yapıştırın.",
"mints.clearSearch": "Aramayı kapat", "mints.clearSearch": "Aramayı temizle",
"card.noRatings": "Henüz reytingler yok", "card.noRatings": "Henüz reytingler yok",
"card.reviews.one": "{n} incelemesi", "card.reviews.one": "{n} inceleme",
"card.reviews.other": "{n} incelemeleri", "card.reviews.other": "{n} inceleme",
"card.lastSeen": "Son Görülme {when}", "card.lastSeen": "Son Görülme {when}",
"card.neverSeen": "Hiç çevrimiçi görünmedi", "card.neverSeen": "Hiç çevrimiçi görünmedi",
"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.breadcrumb": "Ekmek kırıntısı", "mint.titleFedimint": "{name} · Fedimint federasyonu değerlendirmeleri",
"mint.crumbMints": "Cashu darphaneleri", "mint.titleLnurl": "{name} · LNURL mint değerlendirmeleri",
"mint.copyUrl": "Kopyala URL", "mint.breadcrumb": "Gezinme yolu",
"mint.crumbMints": "Cashu mint'leri",
"mint.copyUrl": "Mint URL'sini kopyala",
"mint.qrButton": "Mint QR kodu", "mint.qrButton": "Mint QR kodu",
"mint.writeReview": "Bir inceleme yaz", "mint.writeReview": "Bir inceleme yaz",
"mint.rankByScore": "#{rank} skoru", "mint.rankByScore": "#{rank} skoru",
"mint.tagLightning": "Lightning", "mint.tagLightning": "Lightning",
"mint.summary": "Darphanenin özeti", "mint.summary": "Mint özeti",
"mint.rating.label": "Topluluk puanı", "mint.rating.label": "Topluluk puanı",
"mint.rating.none": "Henüz reytingler yok", "mint.rating.none": "Henüz puan yok",
"mint.rating.reviews.one": "{n} incelemesi", "mint.rating.reviews.one": "{n} inceleme",
"mint.rating.reviews.other": "{n} incelemeleri", "mint.rating.reviews.other": "{n} inceleme",
"mint.rating.unrated.one": "{n} puan bırakmadı", "mint.rating.unrated.one": "{n} puan bırakmadı",
"mint.rating.unrated.other": "{n} puan bırakmadı", "mint.rating.unrated.other": "{n} puan bırakmadı",
"mint.activity.label": "Faaliyetler", "mint.activity.label": "Etkinlik",
"mint.activity.since": "Son incelemeden beri", "mint.activity.since": "Son incelemeden beri",
"mint.activity.noReviews": "Henüz inceleme yok", "mint.activity.noReviews": "Henüz inceleme yok",
"mint.activity.recent.one": "{n} son 90 gündeki incelemeler", "mint.activity.recent.one": "son 90 günde {n} inceleme",
"mint.activity.recent.other": "{n} son 90 günde yapılan yorumlar", "mint.activity.recent.other": "son 90 günde {n} inceleme",
"mint.limits.label": "Sınırlar", "mint.limits.label": "Sınırlar",
"mint.limits.notPublished": "Yayınlanmadı", "mint.limits.notPublished": "Yayınlanmadı",
"mint.limits.min": "min", "mint.limits.min": "min",
"mint.limits.max": "max", "mint.limits.max": "max",
"mint.limits.mintRow": "Mint", "mint.limits.mintRow": "mint",
"mint.limits.meltRow": "erime", "mint.limits.meltRow": "melt",
"mint.limits.disabled": "Engelli", "mint.limits.disabled": "devre dışı",
"mint.limits.range": "{min} ile {max} {unit} arası", "mint.limits.range": "{min} ile {max} {unit} arası",
"mint.limits.upTo": "{max} {unit}", "mint.limits.upTo": "{max} {unit} kadar",
"mint.limits.from": "{min} {unit}", "mint.limits.from": "{min} {unit} itibaren",
"mint.software.label": "Mint yazılımı", "mint.software.label": "Mint yazılımı",
"mint.software.unknown": "bilinmeyen", "mint.software.unknown": "bilinmeyen",
"mint.checked": "kontrol {when}", "mint.checked": "{when} kontrol edildi",
"mint.lastSeen": "Son Görülme {when}", "mint.lastSeen": "Son Görülme {when}",
"mint.neverReached": "Hiç ulaşılmadı", "mint.neverReached": "Hiç ulaşılmadı",
"mint.uptime.checks": "{ok} son {total} çek ulaşılabilir", "mint.uptime.checks": "son {total} kontrolün {ok} tanesi ulaşılabilir",
"mint.uptime.percent": "%{percent} 30 gün boyunca ulaşılabilir", "mint.uptime.percent": "%{percent} 30 gün boyunca ulaşılabilir",
"mint.uptime.description": "{ok} son {total} kontrolün başarılı olduğu", "mint.uptime.description": "son {total} kontrolün {ok} tanesi başarılı",
"mint.operator.title": "Operatörden", "mint.operator.title": "Operatörden",
"mint.operator.motd": "Günün mesajı", "mint.operator.motd": "Günün mesajı",
"mint.operator.beta": "Bu darphane kendini deneysel veya beta sürümünde olarak tanımlar. Kaybedemeyeceğiniz bir şeyi depolamadan önce operatörün kendi sözlerini aşağıda okuyun.", "mint.operator.beta": "Bu mint kendini deneysel ya da beta olarak tanımlıyor. Kaybetmeyi göze alamayacağınız bir şeyi burada tutmadan önce işletmecinin kendi sözlerini aşağıda okuyun.",
"mint.nuts.title": "Desteklenen NUT'lar", "mint.nuts.title": "Desteklenen NUT'lar",
"mint.nuts.noneOffline": "Bu mint için önbellek yapılmayan NUT listesi yoktu, kaydedilmeden önce çevrimdışı oldu.", "mint.nuts.noneOffline": "Bu mint için önbellek yapılmayan NUT listesi yoktu, kaydedilmeden önce çevrimdışı oldu.",
"mint.nuts.noneYet": "Bu nadrah için henüz önbellek bir NUT listesi yok.", "mint.nuts.noneYet": "Bu mint için henüz önbelleğe alınmış bir NUT listesi yok.",
"mint.nuts.supported": "{n} bilinen {total} NUT'tan destekleniyor", "mint.nuts.supported": "{n} bilinen {total} NUT'tan destekleniyor",
"mint.nuts.whatAre": "NUT'lar nedir?", "mint.nuts.whatAre": "NUT'lar nedir?",
"mint.nutRow.disabled": "Engelli", "mint.nutRow.disabled": "devre dışı",
"mint.nutRow.notSupported": "desteklenmiyor", "mint.nutRow.notSupported": "desteklenmiyor",
"mint.details.title": "Darphane detayları", "mint.details.title": "Mint detayları",
"mint.details.pubkey": "Pubkey", "mint.details.pubkey": "Pubkey",
"mint.details.nostr": "Nostr", "mint.details.nostr": "Nostr",
"mint.details.email": "E-posta", "mint.details.email": "E-posta",
"mint.details.serverTime": "Server zamanı", "mint.details.serverTime": "Sunucu zamanı",
"mint.details.firstSeen": "İlk kez görüldü", "mint.details.firstSeen": "İlk kez görüldü",
"mint.details.terms": "Terimler", "mint.details.terms": "Koşullar",
"mint.kind.identifier": "Tanımlayıcı", "mint.kind.identifier": "Tanımlayıcı",
"mint.kind.mintPubkey": "Mint Pubkey", "mint.kind.mintPubkey": "Mint Pubkey",
"mint.kind.nostrAddress": "Nostr adresi", "mint.kind.nostrAddress": "Nostr adresi",
"mint.kind.emailAddress": "E-posta adresi", "mint.kind.emailAddress": "E-posta adresi",
"mint.kind.npub": "npub", "mint.kind.npub": "npub",
"mint.disclaimer": "Bu darpe tasarruf edicidir: paranızı saklar ve bunu istediğiniz zaman durdurabilir. Derecelendirmeler topluluk görüşleridir ve çevrimiçi ulaşılabilir demektir, güvenilir değil.", "mint.disclaimer": "Bu mint emanetçidir: paranızı tutar ve bunu istediği an bırakabilir. Puanlar topluluk görüşleridir ve çevrimiçi olmak ulaşılabilir demektir, güvenilir demek değil.",
"mint.disclaimerLink": "Tam feragatnameyi okuyun", "mint.disclaimerLink": "Tam feragatnameyi okuyun",
"mint.qr.title": "Cüzdanınıza {name} ekleyin", "mint.qr.title": "Cüzdanınıza {name} ekleyin",
"mint.qr.note": "Bunu bir Cashu cüzdanıyla tarayıp mint ekleyin ya da URL'yi kopyalayıp yapıştırın.", "mint.qr.note": "Bunu bir Cashu cüzdanıyla tarayıp mint ekleyin ya da URL'yi kopyalayıp yapıştırın.",
@@ -182,11 +188,11 @@
"mint.qr.wallets": "Cashu'yu destekleyen cüzdanlar", "mint.qr.wallets": "Cashu'yu destekleyen cüzdanlar",
"mint.chip.frozen": "Donmuş", "mint.chip.frozen": "Donmuş",
"mint.chip.noWithdrawals": "Çekim yok", "mint.chip.noWithdrawals": "Çekim yok",
"mint.chip.meltOnly": "Sadece erime", "mint.chip.meltOnly": "Sadece melt",
"mint.warnings.dayCount.one": "{n} gün", "mint.warnings.dayCount.one": "{n} gün",
"mint.warnings.dayCount.other": "{n} günler", "mint.warnings.dayCount.other": "{n} günler",
"mint.warnings.gone.lead": "Muhtemelen gitmiş.", "mint.warnings.gone.lead": "Muhtemelen gitmiş.",
"mint.warnings.gone.body.since": "Bu darphane, {date} ({days}) yılından beri ulaşılamamıştır. Orada tutulan fonları risk altında olarak kabul edin ve yeni fonlar göndermeyin. Aşağıdaki detaylar onun son bilinen durumunu göstermektedir.", "mint.warnings.gone.body.since": "Bu mint {date} tarihinden beri ({days}) ulaşılamıyor. Orada tutulan fonları risk altında sayın ve yeni fon göndermeyin. Aşağıdaki detaylar son bilinen durumunu gösteriyor.",
"mint.warnings.gone.body.neverDated": "Bu mint, {month} yılında izlemeye başladığımızdan beri tek bir çeke bile cevap vermedi. Orada tutulan fonları risk altında olarak kabul edin ve yeni fonlar göndermeyin. Aşağıdaki her şey Nostr duyurusundan geliyor, mintten değil.", "mint.warnings.gone.body.neverDated": "Bu mint, {month} yılında izlemeye başladığımızdan beri tek bir çeke bile cevap vermedi. Orada tutulan fonları risk altında olarak kabul edin ve yeni fonlar göndermeyin. Aşağıdaki her şey Nostr duyurusundan geliyor, mintten değil.",
"mint.warnings.gone.body.never": "Bu mint, izlemeye başladığımızdan beri tek bir çeke bile cevap vermedi. Orada tutulan fonları risk altında olarak kabul edin ve yeni fonlar göndermeyin. Aşağıdaki her şey Nostr duyurusundan geliyor, mintten değil.", "mint.warnings.gone.body.never": "Bu mint, izlemeye başladığımızdan beri tek bir çeke bile cevap vermedi. Orada tutulan fonları risk altında olarak kabul edin ve yeni fonlar göndermeyin. Aşağıdaki her şey Nostr duyurusundan geliyor, mintten değil.",
"mint.warnings.gone.meta.since": "{date} beri çevrimdışı", "mint.warnings.gone.meta.since": "{date} beri çevrimdışı",
@@ -197,31 +203,31 @@
"mint.warnings.meltDisabled.meta.online": "Şu anda devre dışı bırakılan çekimler", "mint.warnings.meltDisabled.meta.online": "Şu anda devre dışı bırakılan çekimler",
"mint.warnings.meltDisabled.meta.offline": "Son görüldüğünde çekimler devre dışı bırakılmıştı", "mint.warnings.meltDisabled.meta.offline": "Son görüldüğünde çekimler devre dışı bırakılmıştı",
"mint.warnings.frozen.lead": "Mint dondurulmuş.", "mint.warnings.frozen.lead": "Mint dondurulmuş.",
"mint.warnings.frozen.body.online": "Hem darpma hem de eritme devre dışı bırakılmıştır. Hiçbir sat Lightning ile girip çıkamaz.", "mint.warnings.frozen.body.online": "Hem minting hem melting devre dışı. Lightning üzerinden hiçbir sats girip çıkamaz.",
"mint.warnings.frozen.body.offline": "Hem darpma hem de eritme devre dışı bırakıldı. Hiçbir sat Lightning ile girip çıkamaz.", "mint.warnings.frozen.body.offline": "Hem minting hem melting devre dışı bırakılmıştı. Lightning üzerinden hiçbir sats girip çıkamaz.",
"mint.warnings.frozen.meta.online": "Şu anda donmuş, hiçbir şey girip çıkmıyor", "mint.warnings.frozen.meta.online": "Şu anda donmuş, hiçbir şey girip çıkmıyor",
"mint.warnings.frozen.meta.offline": "Son kez görüldüğünde donmuştu", "mint.warnings.frozen.meta.offline": "Son kez görüldüğünde donmuştu",
"mint.warnings.offlineLong.lead": "{days} çevrimiçi, {date}'den beri.", "mint.warnings.offlineLong.lead": "{days} çevrimiçi, {date}'den beri.",
"mint.warnings.offlineLong.body": "Mint gitmiş olabilir. Aşağıdaki her şey son bilinen durumu ve incelemeler hâlâ çalışıyor.", "mint.warnings.offlineLong.body": "Mint gitmiş olabilir. Aşağıdaki her şey son bilinen durumu ve incelemeler hâlâ çalışıyor.",
"mint.warnings.offlineLong.meta": "{date} beri çevrimdışı", "mint.warnings.offlineLong.meta": "{date} beri çevrimdışı",
"mint.warnings.meltOnly.lead": "Sadece eriyecek.", "mint.warnings.meltOnly.lead": "Sadece eriyecek.",
"mint.warnings.meltOnly.body.online": "Yeni basma (NUT-04) devre dışı bırakılmıştır: yatıramazsınız, ancak mevcut e-nakit yine de çekilebilir. Bu genellikle bir mintin sona ermek üzere olduğu anlamına gelir, bu yüzden e-nakit pulunu tutarsanız erimeyi düşünün.", "mint.warnings.meltOnly.body.online": "Yeni minting (NUT-04) devre dışı: yatıramazsınız, ancak mevcut ecash hâlâ çekilebilir. Bu genellikle bir mint'in kapanmakta olduğu anlamına gelir, dolayısıyla onun ecash'ini tutuyorsanız melt etmeyi düşünün.",
"mint.warnings.meltOnly.body.offline": "Yeni basma (NUT-04) devre dışı bırakıldı: yatırılanamaz ama mevcut e-nakit yine de çekilebilir. Bu genellikle bir mintin sona ermek üzere olduğu anlamına gelir, bu yüzden e-nakit pulunu tutarsanız erimeyi düşünün.", "mint.warnings.meltOnly.body.offline": "Yeni minting (NUT-04) devre dışı bırakılmıştı: yatıramazsınız, ancak mevcut ecash hâlâ çekilebilir. Bu genellikle bir mint'in kapanmakta olduğu anlamına gelir, dolayısıyla onun ecash'ini tutuyorsanız melt etmeyi düşünün.",
"mint.warnings.meltOnly.meta.online": "Şu anda sadece erime", "mint.warnings.meltOnly.meta.online": "Şu anda sadece melt",
"mint.warnings.meltOnly.meta.offline": "Sadece son görüldüğünde eriyip gider", "mint.warnings.meltOnly.meta.offline": "Sadece son görüldüğünde eriyip gider",
"mint.warnings.offline.lead": "{date}'dan beri çevrimdış.", "mint.warnings.offline.lead": "{date}'dan beri çevrimdış.",
"mint.warnings.offline.body": "Detaylar en son o zaman doğrulandı. Yine de bir inceleme yazabilirsiniz.", "mint.warnings.offline.body": "Detaylar en son o zaman doğrulandı. Yine de bir inceleme yazabilirsiniz.",
"mint.warnings.offline.meta": "{date} beri çevrimdışı", "mint.warnings.offline.meta": "{date} beri çevrimdışı",
"mint.warnings.also.meltDisabled": "Çevrimdışı olmadan önce para çekme işlemlerini de devre dışı bırakmıştı.", "mint.warnings.also.meltDisabled": "Çevrimdışı olmadan önce para çekme işlemlerini de devre dışı bırakmıştı.",
"mint.warnings.also.mintDisabled": "Çevrimdışı olmadan önce yeni basma sistemini de devre dışı bırakmıştı.", "mint.warnings.also.mintDisabled": "Çevrimdışı olmadan önce yeni minting'i de devre dışı bırakmıştı.",
"mint.warnings.also.offline": "Ayrıca {date} ({days}) yılından beri çevrimdışıdır.", "mint.warnings.also.offline": "Ayrıca {date} ({days}) yılından beri çevrimdışıdır.",
"nut.0": "Notasyon ve modeller", "nut.0": "Notasyon ve modeller",
"nut.1": "Mint açık anahtar değişimi", "nut.1": "Mint açık anahtar değişimi",
"nut.2": "Anahtar setleri ve ücretler", "nut.2": "Anahtar setleri ve ücretler",
"nut.3": "Token takası", "nut.3": "Token takası",
"nut.4": "Lightning ile Basım", "nut.4": "Lightning ile minting",
"nut.5": "Lightningla Erime", "nut.5": "Lightningla Erime",
"nut.6": "Darphane bilgileri", "nut.6": "Mint bilgileri",
"nut.7": "Token durum kontrolü", "nut.7": "Token durum kontrolü",
"nut.8": "Fazla ödenen Lightning ücretleri", "nut.8": "Fazla ödenen Lightning ücretleri",
"nut.9": "İmzaları geri getir", "nut.9": "İmzaları geri getir",
@@ -235,7 +241,7 @@
"nut.17": "WebSocket abonelikleri", "nut.17": "WebSocket abonelikleri",
"nut.18": "Ödeme talepleri", "nut.18": "Ödeme talepleri",
"nut.19": "Önbelleğe alınmış yanıtlar", "nut.19": "Önbelleğe alınmış yanıtlar",
"nut.20": "İmzalı darphaneler", "nut.20": "İmzalı mint quote'ları",
"nut.21": "Temiz kimlik doğrulama", "nut.21": "Temiz kimlik doğrulama",
"nut.22": "Kör kimlik doğrulama", "nut.22": "Kör kimlik doğrulama",
"nut.23": "BOLT11 ödeme yöntemi", "nut.23": "BOLT11 ödeme yöntemi",
@@ -261,6 +267,9 @@
"reviews.empty": "Henüz inceleme yok. İlk yazan kişi ol.", "reviews.empty": "Henüz inceleme yok. İlk yazan kişi ol.",
"reviews.emptyWritten": "Henüz yazılı bir inceleme yok. İlk yazan kişi ol.", "reviews.emptyWritten": "Henüz yazılı bir inceleme yok. İlk yazan kişi ol.",
"reviews.emptyFilter": "Bu filtreyle eşleşen hiçbir yorum yok.", "reviews.emptyFilter": "Bu filtreyle eşleşen hiçbir yorum yok.",
"reviews.rate.cashu": "Bu mint'i puanla",
"reviews.rate.fedimint": "Bu federasyonu puanla",
"reviews.rate.lnurl": "Bu mint'i puanla",
"reviews.noscript": "İncelemeler Nostr'da imzalanır ve tarayıcınızda yüklenir, bu yüzden bu liste JavaScript gerektirir. Yukarıdaki puan ve sayımlar sitenin kendisinden geliyor.", "reviews.noscript": "İncelemeler Nostr'da imzalanır ve tarayıcınızda yüklenir, bu yüzden bu liste JavaScript gerektirir. Yukarıdaki puan ve sayımlar sitenin kendisinden geliyor.",
"reviews.relayError": "Röleye ulaşamadım. İncelemeler eksik olabilir.", "reviews.relayError": "Röleye ulaşamadım. İncelemeler eksik olabilir.",
"reviews.srRating": "{rating} 5 üzerinden", "reviews.srRating": "{rating} 5 üzerinden",
@@ -273,7 +282,7 @@
"reviews.anonNote": "Bu npub'dan ilk inceleme, başka bir faaliyet bulunmadı", "reviews.anonNote": "Bu npub'dan ilk inceleme, başka bir faaliyet bulunmadı",
"reviews.published": "Yayınlandı.", "reviews.published": "Yayınlandı.",
"reviews.publishedPartial": "{accepted} {total} rölesine yayımlandı.", "reviews.publishedPartial": "{accepted} {total} rölesine yayımlandı.",
"reviews.propagating": "Yayılırken başka bir yerde ortaya çıkması biraz zaman alabilir.", "reviews.propagating": "Nostr relay'lerine yayınlanıyor",
"reviews.summary.line.one": "{count} yorumsuz puan: {breakdown}", "reviews.summary.line.one": "{count} yorumsuz puan: {breakdown}",
"reviews.summary.line.other": "{count} yorumsuz puanlar: {breakdown}", "reviews.summary.line.other": "{count} yorumsuz puanlar: {breakdown}",
"reviews.summary.lineDay.one": "{when} yorumsuz {count} puanı: {breakdown}", "reviews.summary.lineDay.one": "{when} yorumsuz {count} puanı: {breakdown}",
@@ -287,7 +296,12 @@
"reviews.breakdown.1": "{n} bir yıldız", "reviews.breakdown.1": "{n} bir yıldız",
"reviews.breakdown.none": "{n} puanı olmayan", "reviews.breakdown.none": "{n} puanı olmayan",
"reviews.dialog.title": "İnceleme {name}", "reviews.dialog.title": "İnceleme {name}",
"reviews.dialog.note": "Nostr anahtarınızla imzalandı ve halka aktarıldı. Düzenlenemiyor veya silinmiyor.", "reviews.dialog.lede.cashu": "Bu mint ile deneyiminizi paylaşın.",
"reviews.dialog.lede.fedimint": "Bu federasyon ile deneyiminizi paylaşın.",
"reviews.dialog.lede.lnurl": "Bu mint ile deneyiminizi paylaşın.",
"reviews.dialog.permanent": "Herkese açık Nostr relay'lerinde kalıcı olarak yayınlanır.",
"reviews.dialog.gate.title": "İnceleme yazmak için giriş yapın",
"reviews.dialog.gate.lead": "İncelemeler Nostr anahtarınızla imzalanır. Anahtar imzalayıcınızda kalır, bu site onu asla görmez.",
"reviews.dialog.ratingLegend": "Puanınız", "reviews.dialog.ratingLegend": "Puanınız",
"reviews.dialog.starsSr.one": "{n} yıldızı", "reviews.dialog.starsSr.one": "{n} yıldızı",
"reviews.dialog.starsSr.other": "{n} yıldızlar", "reviews.dialog.starsSr.other": "{n} yıldızlar",
@@ -308,7 +322,6 @@
"reviews.dialog.waitingSigner": "İmzalanını bekliyorum", "reviews.dialog.waitingSigner": "İmzalanını bekliyorum",
"reviews.dialog.confirmClose": "Bu incelemeyi kapatmak mı? Taslağınız site terk edene kadar saklanır.", "reviews.dialog.confirmClose": "Bu incelemeyi kapatmak mı? Taslağınız site terk edene kadar saklanır.",
"reviews.dialog.why.login": "Yayınlamak için giriş yapın", "reviews.dialog.why.login": "Yayınlamak için giriş yapın",
"reviews.dialog.why.rating": "Bir puan seçin",
"reviews.dialog.why.words": "Birkaç kelime yaz", "reviews.dialog.why.words": "Birkaç kelime yaz",
"reviews.dialog.why.more.one": "{n} daha karakter", "reviews.dialog.why.more.one": "{n} daha karakter",
"reviews.dialog.why.more.other": "{n} daha fazla karakter", "reviews.dialog.why.more.other": "{n} daha fazla karakter",
@@ -324,16 +337,16 @@
"reviews.validate.rating": "1 ile 5 yıldız arasında bir değerlendirme seç.", "reviews.validate.rating": "1 ile 5 yıldız arasında bir değerlendirme seç.",
"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 mint 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 mint'i ve Fedimint federasyonu hakkındaki her inceleme, en yenisi başta, Nostr rölelerinden canlı okunur. Her biri yazarı tarafından imzalanmıştır ve veritabanımızda değil, rölelerde durur.",
"feed.label": "İncelemeler", "feed.label": "İncelemeler",
"feed.showingBuild.one": "Son sürümden en güncel incelemeyi gösteriyor", "feed.showingBuild.one": "Son sürümden en güncel incelemeyi gösteriyor",
"feed.showingBuild.other": "Son sürümden {n} en son incelemeleri gösteriyor", "feed.showingBuild.other": "Son sürümden {n} en son incelemeleri gösteriyor",
"feed.noscript": "Yorumlar Nostr'da imzalanır ve tarayıcınızda yüklenir, bu yüzden filtreleme ve tam liste JavaScript gerektirir. Aşağıdaki {n} incelemeler, bu sayfa oluşturulduğunda rölelerden okunmuştur.", "feed.noscript": "Yorumlar Nostr'da imzalanır ve tarayıcınızda yüklenir, bu yüzden filtreleme ve tam liste JavaScript gerektirir. Aşağıdaki {n} incelemeler, bu sayfa oluşturulduğunda rölelerden okunmuştur.",
"feed.emptyBuild": "Bu sayfa oluşturulduğunda rölelerden hiçbir yorum okunamıyordu. Liste, tarayıcınızdaki rölelerden yeniden yüklenir.", "feed.emptyBuild": "Bu sayfa oluşturulduğunda rölelerden hiçbir yorum okunamıyordu. Liste, tarayıcınızdaki rölelerden yeniden yüklenir.",
"feed.foot": "İncelemeler topluluk görüşleridir, doğrulanmış iddialar değildir. Bir darpeye para emanet etmeden önce {link} okuyun.", "feed.foot": "İncelemeler topluluk görüşleridir, doğrulanmış iddialar değil. Bir mint'e para emanet etmeden önce {link} okuyun.",
"feed.footLink": "Feragatname", "feed.footLink": "Feragatname",
"feed.showing": "{total} incelemelerin {shown} gösterilmesi", "feed.showing": "{total} incelemelerin {shown} gösterilmesi",
"feed.showingMatching": "{shown} {matching} eşleşen inceleme (toplamda {total}) gösteriliyor", "feed.showingMatching": "{shown} {matching} eşleşen inceleme (toplamda {total}) gösteriliyor",
@@ -369,6 +382,10 @@
"login.status.waitingSigner": "İmzalanını bekliyorum", "login.status.waitingSigner": "İmzalanını bekliyorum",
"login.status.connecting": "İmzacınıza bağlanmak", "login.status.connecting": "İmzacınıza bağlanmak",
"login.status.connected": "Bağlantılı", "login.status.connected": "Bağlantılı",
"login.toast.loggedIn": "Giriş yapıldı",
"login.toast.fetchingProfile": "Nostr profiliniz rölelerden alınıyor…",
"login.toast.profile": "Profil: {name}",
"login.toast.noProfile": "Rölelerde profil bulunamadı",
"login.method.extension": "Genişletme", "login.method.extension": "Genişletme",
"login.method.remote": "Uzaktan", "login.method.remote": "Uzaktan",
"login.method.primal": "İlkel", "login.method.primal": "İlkel",
@@ -392,7 +409,7 @@
"notfound.title": "Sayfa bulunmadı · Cashumints.space", "notfound.title": "Sayfa bulunmadı · Cashumints.space",
"notfound.description": "O sayfa Cashumints.space'da yok.", "notfound.description": "O sayfa Cashumints.space'da yok.",
"notfound.heading": "Bu sayfa mevcut değildir", "notfound.heading": "Bu sayfa mevcut değildir",
"notfound.lede": "Taşınmış olabilir ya da darphane henüz indekslenmemiş olabilir. Bir mint adını isim veya alan adı ile arayın ya da bir mint URL'sini yapıştırın.", "notfound.lede": "Taşınmış olabilir ya da mint henüz indekslenmemiş olabilir. Bir mint'i adına veya alan adına göre arayın ya da bir mint URL'si yapıştırın.",
"notfound.browse": "Tüm mintlere göz atın", "notfound.browse": "Tüm mintlere göz atın",
"notfound.home": "Eve git", "notfound.home": "Eve git",
"notfound.loading": "Yükleniyor {host}", "notfound.loading": "Yükleniyor {host}",
@@ -400,11 +417,18 @@
"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",
"wallets.lede": "Bu mintlerden herhangi birinin e-nakit taşımak için cüzdana ihtiyacınız var. Çoğu cüzdan aynı anda birden fazla mint ile konuşur, yani ilk denediğiniz yere bağlı değilsiniz.", "wallets.lede": "Bu mint'lerin herhangi birinden ecash tutmak için bir cüzdana ihtiyacınız var. Çoğu cüzdan aynı anda birden fazla mint ile konuşur, yani ilk denediğinize bağlı kalmazsınız.",
"wallets.note": "Bu liste kapsamlı değildir ve bir onay değildir. Cashu projesi daha geniş bir endeksi {link} olarak tutuyor. Bir cüzdan anahtarları tutar, bir mint parayı tutar: satsların nerede yaşadığını seçmeden önce mint incelemelerini okuyun.", "wallets.note": "Bu liste kapsamlı değildir ve bir onay değildir. Cashu projesi daha geniş bir endeksi {link} olarak tutuyor. Bir cüzdan anahtarları tutar, bir mint parayı tutar: satsların nerede yaşadığını seçmeden önce mint incelemelerini okuyun.",
"wallets.platform.android": "Android", "wallets.platform.android": "Android",
"wallets.platform.ios": "iOS", "wallets.platform.ios": "iOS",
@@ -412,53 +436,53 @@
"wallets.platform.extension": "Tarayıcı uzantısı", "wallets.platform.extension": "Tarayıcı uzantısı",
"wallets.platform.cli": "CLI", "wallets.platform.cli": "CLI",
"wallets.platform.python": "Python", "wallets.platform.python": "Python",
"wallets.item.minibits": "Kendi mint'i, Lightning adres desteği ve Nostr bağlantıları olan mobil e-nakit cüzdanı.", "wallets.item.minibits": "Kendi mint'i, Lightning adres desteği ve Nostr kişileri olan mobil ecash cüzdanı.",
"wallets.item.nutstash": "Birden fazla mint'i aynı anda yöneten ve ham tokenları içe ve dışa aktaran tarayıcı cüzdanı.", "wallets.item.nutstash": "Birden fazla mint'i aynı anda yöneten ve ham tokenları içe ve dışa aktaran tarayıcı cüzdanı.",
"wallets.item.enuts": "Mobil cüzdan, çoklu minnet bakiyelerine ve Nostr üzerinden e-nakit göndermeye odaklanıyordu.", "wallets.item.enuts": "Birden fazla mint'teki bakiyelere ve Nostr üzerinden ecash göndermeye odaklanan mobil cüzdan.",
"wallets.item.cashume": "Cashu projesinden referans web cüzdanı. Tamamen tarayıcıda çalışıyor, PWA olarak yüklenebilir.", "wallets.item.cashume": "Cashu projesinden referans web cüzdanı. Tamamen tarayıcıda çalışıyor, PWA olarak yüklenebilir.",
"wallets.item.boardwalk": "Lightning adresleri ve e-nakit gönderme etrafında oluşturulmuş web cüzdanı.", "wallets.item.boardwalk": "Lightning adresleri ve bağlantıyla ecash gönderme etrafında kurulmuş web cüzdanı.",
"wallets.item.coinos": "Lightning cüzdanı ve satış noktası, ayrıca kendi Cashu darphanesini de işletiyor.", "wallets.item.coinos": "Kendi Cashu mint'ini de işleten Lightning cüzdanı ve satış noktası.",
"wallets.item.oxchat": "Sohbet içinde sats göndermek için yerleşik bir ecash cüzdanı bulunan Nostr messenger.", "wallets.item.oxchat": "Sohbet içinde sats göndermek için yerleşik bir ecash cüzdanı bulunan Nostr messenger.",
"wallets.item.nutshell": "Referans uygulaması. Komut satırında cüzdan olarak ve mint yazılımı olarak çalışıyor.", "wallets.item.nutshell": "Referans uygulaması. Komut satırında cüzdan olarak ve mint yazılımı olarak çalışıyor.",
"about.title": "Hakkında Cashumints.space", "about.title": "Hakkında Cashumints.space",
"about.description": "Cashu'nun ne olduğu, bir darphanenin aslında ne taşıdığı, NIP-87'nin ne olduğu ve bu sitenin mint parçalarını nasıl indekslediği ve Nostr rölelerinden gelen incelemeleri nasıl okuduğu konular.", "about.description": "Cashu'nun ne olduğu, bir mint'in aslında ne tuttuğu, NIP-87'nin ne olduğu ve bu sitenin mint'leri nasıl indekslediği ve Nostr rölelerinden incelemeleri nasıl okuduğu.",
"about.heading": "Hakkında", "about.heading": "Hakkında",
"about.lede": "Cashumints.space, Nostr ağında bulabildiğimiz tüm Cashu mintlerini listeliyor ve insanların onlar hakkında ne söylediğini gösteriyor. Azzamo tarafından üretilmiştir.", "about.lede": "Cashumints.space, Nostr ağında bulabildiğimiz tüm Cashu mintlerini listeliyor ve insanların onlar hakkında ne söylediğini gösteriyor. Azzamo tarafından üretilmiştir.",
"about.cashu.title": "Cashu Nedir", "about.cashu.title": "Cashu Nedir",
"about.cashu.p1": "Cashu, Bitcoin için bir ecash protokolüdür. Bir darpçe, elindeki satlarla desteklenen kör jetonlar çıkarır ve bu tokenlar, kimin kime ne gönderdiğini öğrenmeden insanlar arasında hareket eder. Gizlilik gerçektir ve mesele de budur: tokenlar hesap değildir ve darpevçe, imzaladığı tokenı geri dönen tokenla bağlayamaz.", "about.cashu.p1": "Cashu, Bitcoin için bir ecash protokolüdür. Bir mint, elinde tuttuğu sats ile desteklenen körleştirilmiş tokenlar çıkarır ve bu tokenlar, mint kimin kime ne gönderdiğini öğrenmeden insanlar arasında dolaşır. Gizlilik gerçektir ve mesele de budur: tokenlar hesap değildir ve mint, imzaladığı tokenı geri gelen tokenla ilişkilendiremez.",
"about.cashu.p2": "Protokolü ve teknik özellikleri {link} üzerinden okuyabilirsiniz. Teknik özelliklerin kendisi NUT olarak adlandırılır, her mint sayfasındaki NUT numaraları da buradan gelir.", "about.cashu.p2": "Protokolü ve teknik özellikleri {link} üzerinden okuyabilirsiniz. Teknik özelliklerin kendisi NUT olarak adlandırılır, her mint sayfasındaki NUT numaraları da buradan gelir.",
"about.mints.title": "Mint nedir ve bunun karşılığında", "about.mints.title": "Mint nedir ve bunun karşılığında",
"about.mints.p1": "Mint koruyucudur. Ecash bastığınızda, sat'ları o sunucuyu işleten kişiye veriyor oluyorsunuz ve jetonları tekrar erittiğinizde onları onurlandıracaklarına güveniyorsunuz. Dolandırıcılık kanıtı, emanet ve geri ödeme yolu yoktur. Bir darphane operatörü bakayla birlikte ortadan kaybolabilir ve darphaneler de kaybolur.", "about.mints.p1": "Mint emanetçidir. Ecash mint ettiğinizde sats'ları o sunucuyu işleten kişiye verirsiniz ve tokenları geri melt ettiğinizde bunları karşılayacaklarına güvenmiş olursunuz. Dolandırıcılık kanıtı, emanet hesabı ve başvuru yolu yoktur. Bir mint işletmecisi bakiyeyle birlikte ortadan kaybolabilir, ve kaybolan mint'ler olmuştur.",
"about.mints.p2": "İşte bu yüzden burada incelemeler var. Bu site size bir mintin güvenli olduğunu asla söylemez. Bir mint sayfasındaki durum {code} yazıyor, \"güvenilen\" değil ve bu sadece sunucunun son sorduğumuzda cevap verdiği anlamına geliyor. E-nakit bakiyelerini bir ceket cebindeki nakit gibi ele alın: harcamalar için ceza, tasarruf için yanlış.", "about.mints.p2": "İşte bu yüzden burada incelemeler var. Bu site size bir mintin güvenli olduğunu asla söylemez. Bir mint sayfasındaki durum {code} yazıyor, \"güvenilen\" değil ve bu sadece sunucunun son sorduğumuzda cevap verdiği anlamına geliyor. E-nakit bakiyelerini bir ceket cebindeki nakit gibi ele alın: harcamalar için ceza, tasarruf için yanlış.",
"about.nip87.title": "NIP-87 nedir", "about.nip87.title": "NIP-87 nedir",
"about.nip87.p1": "NIP-87, ecash darphanelerinin duyurulması ve önerilmesi için Nostr geleneğidir. Burada iki tür etkinlik önemlidir:", "about.nip87.p1": "NIP-87, ecash mint'lerini duyurmak ve önermek için kullanılan Nostr geleneğidir. Burada iki olay türü önemlidir:",
"about.nip87.li1": "{code} bir mint duyuruyor. Mint URL'si {tag} etiketinde. Site, daha önce hiç görmediği mint parçalarını böyle keşfediyor.", "about.nip87.li1": "{code} bir mint duyuruyor. Mint URL'si {tag} etiketinde. Site, daha önce hiç görmediği mint parçalarını böyle keşfediyor.",
"about.nip87.li2": "{code} bir inceleme. Derecelendirme ve inceleme metni taşır ve üzerindeki darpeye {utag} etiketi (mint URL) ve {dtag} etiketi (darphanenin kendi açık anahtarı) işaret eder.", "about.nip87.li2": "{code} bir incelemedir. Puanı ve inceleme metnini taşır ve konu olduğu mint'i bir {utag} etiketi (mint URL'si) ve bir {dtag} etiketi (mint'in kendi açık anahtarı) ile işaret eder.",
"about.nip87.p2": "Her inceleme, yazarının Nostr anahtarıyla imzalanır. Bir incelemeye asla \"doğrulanmış\" demiriz, çünkü kimse doğrulamadı: imzalanmıştır, bu da farklı ve daha faydalı bir iddiadır. Bu, kimin yazdığını ve değiştirilmediğini kontrol edebildiğiniz anlamına gelir. Bu, yazarın dürüst olduğu ya da mint kullandığı anlamına gelmez.", "about.nip87.p2": "Her inceleme, yazarının Nostr anahtarıyla imzalanır. Bir incelemeye asla \"doğrulanmış\" demiriz, çünkü kimse doğrulamadı: imzalanmıştır, bu da farklı ve daha faydalı bir iddiadır. Bu, kimin yazdığını ve değiştirilmediğini kontrol edebildiğiniz anlamına gelir. Bu, yazarın dürüst olduğu ya da mint kullandığı anlamına gelmez.",
"about.how.title": "Bu site nasıl çalışır", "about.how.title": "Bu site nasıl çalışır",
"about.how.p1": "İki döngü ve bir kural var. Kural, darphanenin meta verilerine, Nostr'un ise incelemelere sahip olduğu, bu yüzden bu site ikisini de otorite olarak saklıyor.", "about.how.p1": "İki döngü ve bir kural var. Kural şu: meta veriler mint'in, incelemeler Nostr'undur, bu yüzden bu site ikisini de otorite olarak saklamaz.",
"about.how.li1": "{strong}, bilinen her mint her on dakikada bir {code} ister ve ne yazdığını kaydeder: isim, açıklama, sürüm, desteklenen NUT'lar, limitler, iletişim bilgileri. Ayrıca darphanenin cevap verip vermediğini de kaydediyor, ki bu da uptime'ın kaynağıdır.", "about.how.li1": "{strong}, bilinen her mint'ten on dakikada bir {code} ister ve söylediklerini kaydeder: ad, açıklama, sürüm, desteklenen NUT'lar, limitler, iletişim bilgileri. Ayrıca mint'in yanıt verip vermediğini de kaydeder, çalışma süresi buradan gelir.",
"about.how.li1Label": "Prob döngüsü", "about.how.li1Label": "Prob döngüsü",
"about.how.li2": "{strong}, her saat Nostr rölelerine bağlanıyor, mint duyurularını ve incelemelerini okuyor, daha önce görmediği herhangi bir mint ekliyor.", "about.how.li2": "{strong}, her saat Nostr rölelerine bağlanıyor, mint duyurularını ve incelemelerini okuyor, daha önce görmediği herhangi bir mint ekliyor.",
"about.how.li2Label": "Keşif döngüsü", "about.how.li2Label": "Keşif döngüsü",
"about.how.p2": "Önemli olan, bir mint cevap vermeyi bıraktığında ne olacağıdır. Önbelleğe alınan meta verileri, başarısız bir prob tarafından asla üzerine yazılmaz, bu yüzden ölü bir mint tam sayfasını korur: özelliklerini, geçmişini ve incelemelerini, bir pankartla ne zaman yayına girdiğini belirtir. Önceki versiyonda ise hata sayfası vardı, yani insanların birbirlerini en çok uyarması gereken darphaneler incelenemeyenler oldu. Yeniden inşa edilmenin ana nedeni buydu.", "about.how.p2": "Önemli olan, bir mint yanıt vermeyi bıraktığında ne olduğudur. Önbelleğe alınmış meta verileri başarısız bir yoklama tarafından asla üzerine yazılmaz, böylece ölü bir mint tam sayfasını korur: özellikleri, geçmişi ve incelemeleri, ne zaman sustuğunu söyleyen bir bant ile birlikte. Bu sitenin önceki sürümü bunun yerine bir hata sayfası gösteriyordu; bu da insanların birbirini en çok uyarması gereken mint'lerin incelenemeyen mint'ler olması demekti. Yeniden yazmanın ana nedeni buydu.",
"about.how.p3": "İnceleme metni bizim tarafımızda asla saklanmaz. Bir mint sayfası açtığınızda, tarayıcınız doğrudan rölelerle konuşur ve incelemeleri kendisi okur, bir sayfa yazdığınızda ise kendi Nostr uzantınızla imzalanır ve doğrudan o rölelere gönderilir. Sayfalar sıralanıp sıralanabilmesi için sayıyı tutuyoruz, başka bir şey yok. Buradaki hiçbir şey bizim tarafından düzenlenemeyebilir veya silinemez, istemediğimiz incelemeler dahil.", "about.how.p3": "İnceleme metni bizim tarafımızda asla saklanmaz. Bir mint sayfası açtığınızda, tarayıcınız doğrudan rölelerle konuşur ve incelemeleri kendisi okur, bir sayfa yazdığınızda ise kendi Nostr uzantınızla imzalanır ve doğrudan o rölelere gönderilir. Sayfalar sıralanıp sıralanabilmesi için sayıyı tutuyoruz, başka bir şey yok. Buradaki hiçbir şey bizim tarafından düzenlenemeyebilir veya silinemez, istemediğimiz incelemeler dahil.",
"about.relays.title": "Okuduğumuz röleler", "about.relays.title": "Okuduğumuz röleler",
"about.relays.p1": "İndeksleyicinin ve tarayıcınızın kullandığı röleler şunlardır:", "about.relays.p1": "İndeksleyicinin ve tarayıcınızın kullandığı röleler şunlardır:",
"about.relays.p2": "Herhangi bir Nostr istemcisini aynı rölelere yönlendirirseniz, aynı olayları göreceksiniz. Bütün söz budur: burada hiçbir şey sadece bizim söylediğimiz için doğru değildir.", "about.relays.p2": "Herhangi bir Nostr istemcisini aynı rölelere yönlendirirseniz, aynı olayları göreceksiniz. Bütün söz budur: burada hiçbir şey sadece bizim söylediğimiz için doğru değildir.",
"about.nuts.title": "NUT'lar nedir?", "about.nuts.title": "NUT'lar nedir?",
"about.nuts.p1": "NUT'lar, Bitcoin için BIP'ler gibi numaralandırılmış Cashu spesifikasyonlarıdır. Bir darpevçe, hangi tokenleri uyguladığını duyuruyor ve bu size aslında ne yapabileceğini gösterir: NUT-04 ve NUT-05 Lightning üzerinde basıp eriyor (onlar olmadan bir mint çok işe yaramaz), NUT-07 cüzdanın bir tokenın zaten harcanıp harcanmadığını kontrol etmesine izin verir, NUT-17 cüzdanlara anket yerine WebSockets üzerinden canlı güncellemeler verir, NUT-20 ise cüzdanın gerçekten bir mint teklifi verildiğini kanıtlamasına izin verir. Tam liste {link}.", "about.nuts.p1": "NUT'lar, Bitcoin'deki BIP'ler gibi numaralandırılmış Cashu spesifikasyonlarıdır. Bir mint hangilerini uyguladığını duyurur ve bu size onun aslında ne yapabileceğini söyler: NUT-04 ve NUT-05 Lightning üzerinden minting ve melting'dir (bunlar olmadan bir mint pek işe yaramaz), NUT-07 bir cüzdanın tokenın zaten harcanıp harcanmadığını kontrol etmesini sağlar, NUT-17 cüzdanlara yoklama yerine WebSockets üzerinden canlı güncelleme verir, NUT-20 ise bir cüzdanın mint quote'unun gerçekten kendisine verildiğini kanıtlamasını sağlar. Tam liste {link} adresinde.",
"about.numbers.title": "Şu anda", "about.numbers.title": "Şu anda",
"about.numbers.p1": "{mints} darphaneler indekslenmiş, {online} cevap vermiş, {reviews} yukarıdaki rölelerden sayılan incelemeler.", "about.numbers.p1": "{mints} mint indekslendi, bunların {online} tanesi yanıt veriyor, yukarıdaki rölelerden {reviews} inceleme sayıldı.",
"about.source.title": "Kaynak ve kredi", "about.source.title": "Kaynak ve kredi",
"about.source.p1": "Site {github} adresinde açık kaynaklıdır. Tesis {azzamo} tarafından inşa edilip işletilmektedir. Bir mint sayfasında bir sorun varsa, kaynağında düzeltin: mint meta verileri kendi {code} gelir, incelemeler ise Nostr'den gelir.", "about.source.p1": "Site {github} adresinde açık kaynaklıdır. Tesis {azzamo} tarafından inşa edilip işletilmektedir. Bir mint sayfasında bir sorun varsa, kaynağında düzeltin: mint meta verileri kendi {code} gelir, incelemeler ise Nostr'den gelir.",
"terms.title": "Kullanım Şartları · Cashumints.space", "terms.title": "Kullanım Şartları · Cashumints.space",
"terms.description": "Cashumints.space kullanım şartları: sitenin ne olduğu, ne yapmadığı ve kim neden sorumlu. Hesap yok, garanti yok, hiçbir darphaneyle bağlantım yok.", "terms.description": "Cashumints.space kullanım şartları: sitenin ne olduğu, ne yapmadığı ve kimin neden sorumlu olduğu. Hesap yok, garanti yok, hiçbir mint ile bağlantı yok.",
"terms.heading": "Kullanım Şartları", "terms.heading": "Kullanım Şartları",
"terms.lede": "Sadece halka açık verileri indeksleyen bir site için açık. Son güncelleme {date}.", "terms.lede": "Sadece halka açık verileri indeksleyen bir site için açık. Son güncelleme {date}.",
"terms.what.title": "Bu site nedir", "terms.what.title": "Bu site nedir",
"terms.what.p1": "Cashumints.space, Cashu mint mintler hakkında halka açık verilerin bir dizinidir. İki tür veri, ikisi de başka bir yerden:", "terms.what.p1": "Cashumints.space, Cashu mint mintler hakkında halka açık verilerin bir dizinidir. İki tür veri, ikisi de başka bir yerden:",
"terms.what.li1": "Mint meta verileri, her darphanenin kendi {code} uç noktasından okunuyor: isim, açıklama, yazılım sürümü, desteklenen NUT'lar, iletişim bilgileri ve mint'in son sorduğumuzda yanıt verip vermediği.", "terms.what.li1": "Mint meta verileri, her mint'in kendi {code} uç noktasından okunur: ad, açıklama, yazılım sürümü, desteklenen NUT'lar, iletişim bilgileri ve son sorduğumuzda mint'in yanıt verip vermediği.",
"terms.what.li2": "Kamuya açık Nostr rölelerinden okunan incelemeler. Bunlar, yazarları tarafından herkesin sorgulayabileceği NIP-87 etkinlikleridir.", "terms.what.li2": "Kamuya açık Nostr rölelerinden okunan incelemeler. Bunlar, yazarları tarafından herkesin sorgulayabileceği NIP-87 etkinlikleridir.",
"terms.what.p2": "O verileri topluyoruz, normalleştiriyoruz, sıralıyoruz ve gösteriyoruz. Biz yaratmıyoruz.", "terms.what.p2": "O verileri topluyoruz, normalleştiriyoruz, sıralıyoruz ve gösteriyoruz. Biz yaratmıyoruz.",
"terms.noAccounts.title": "Hesap yok, garanti yok", "terms.noAccounts.title": "Hesap yok, garanti yok",
@@ -466,8 +490,8 @@
"terms.noAccounts.p2": "Kendi riskinizle kullanın. Bu siteyi kullanmaktan veya burada okuduklarınıza güvenmekten kaynaklanan herhangi bir kayıptan sorumlu değiliz. Cashu mint kullanımının özel riskleri için {link} bölümüne bakınız.", "terms.noAccounts.p2": "Kendi riskinizle kullanın. Bu siteyi kullanmaktan veya burada okuduklarınıza güvenmekten kaynaklanan herhangi bir kayıptan sorumlu değiliz. Cashu mint kullanımının özel riskleri için {link} bölümüne bakınız.",
"terms.noAccounts.link": "Feragatname", "terms.noAccounts.link": "Feragatname",
"terms.noCustody.title": "Paranızı elimizde tutmuyoruz", "terms.noCustody.title": "Paranızı elimizde tutmuyoruz",
"terms.noCustody.p1": "Bu site hiçbir zaman fonların gözetimini almıyor. Cüzdanı yok, e-paraya dokunmuyor ve hiçbir şeyi hareket ettiremiyor, donduramıyor veya geri kazanamıyor. Bir mint ile ilgili sorununuz varsa, size yardımcı olabilecek tek taraf darphane işletmecisidir ve biz sizin adınıza onlarla iletişime geçemeyiz.", "terms.noCustody.p1": "Bu site fonları asla emanet almaz. Cüzdanı yoktur, ecash'e dokunmaz ve hiçbir şeyi taşıyamaz, donduramaz veya kurtaramaz. Bir mint ile sorununuz varsa size yardım edebilecek tek taraf mint işletmecisidir ve biz sizin adınıza onlarla iletişime geçemeyiz.",
"terms.noCustody.p2": "Burada listelenen hiçbir darphaneyle bağlantımız yok ve hiçbir darpme listesi, sıralaması veya derecesi için ödeme yapmaz. Darphaneler kamuya açık Nostr duyurularından keşfedilir ve bir algoritma tarafından kamu inceleme verileri üzerinden sıralanır.", "terms.noCustody.p2": "Burada listelenen hiçbir mint ile bağlantımız yok ve hiçbir mint listelenmesi, sırası veya puanı için ödeme yapmaz. Mint'ler kamuya açık Nostr duyurularından keşfedilir ve kamuya açık inceleme verileri üzerinden bir algoritma tarafından sıralanır.",
"terms.reviews.title": "İnceleler başkalarının sözleridir", "terms.reviews.title": "İnceleler başkalarının sözleridir",
"terms.reviews.p1": "İncelemeler, yazarları tarafından kamuya açık Nostr aktarıcılarına yayımlanan kullanıcı tarafından oluşturulan içeriktir. Onları sergileriz, kontrol etmiyoruz. Onları biz yazmadık, düzenleyemeyiz ve onları tutan aktarıcılardan silemeyiz: bunlar bizim sahip olmadığımız bir ağda imzalanmış etkinliklerdir.", "terms.reviews.p1": "İncelemeler, yazarları tarafından kamuya açık Nostr aktarıcılarına yayımlanan kullanıcı tarafından oluşturulan içeriktir. Onları sergileriz, kontrol etmiyoruz. Onları biz yazmadık, düzenleyemeyiz ve onları tutan aktarıcılardan silemeyiz: bunlar bizim sahip olmadığımız bir ağda imzalanmış etkinliklerdir.",
"terms.reviews.p2": "Bir inceleme, bir kişinin deneyimidir, doğrulanmış bir gerçek değildir. Bir tane yayımlarsanız, ne yazdığından sorumlusunuz. Yasa dışı hiçbir şey yayınlamayın ve yayınladığınız şeyin kamuya açık ve kalıcı olduğunu unutmayın (bkz. {link}).", "terms.reviews.p2": "Bir inceleme, bir kişinin deneyimidir, doğrulanmış bir gerçek değildir. Bir tane yayımlarsanız, ne yazdığından sorumlusunuz. Yasa dışı hiçbir şey yayınlamayın ve yayınladığınız şeyin kamuya açık ve kalıcı olduğunu unutmayın (bkz. {link}).",
@@ -517,26 +541,26 @@
"disclaimer.title": "Feragatname · Cashumints.space", "disclaimer.title": "Feragatname · Cashumints.space",
"disclaimer.description": "Cashu mint kullanmadan önce bilmeniz gerekenler: mint ürünleri koruma altındadır, bir mint paranizla birlikte kaybolabilir, incelemeler görüştür ve çevrimiçi olarak ulaşılabilir, güvenilir değildir.", "disclaimer.description": "Cashu mint kullanmadan önce bilmeniz gerekenler: mint ürünleri koruma altındadır, bir mint paranizla birlikte kaybolabilir, incelemeler görüştür ve çevrimiçi olarak ulaşılabilir, güvenilir değildir.",
"disclaimer.heading": "Feragatname", "disclaimer.heading": "Feragatname",
"disclaimer.lede": "Bir darveye para yatırmadan önce bunu okuyun. Son güncelleme {date}.", "disclaimer.lede": "Bir mint'e para koymadan önce bunu okuyun. Son güncelleme {date}.",
"disclaimer.callout": "Bir Cashu darve paranızı sizin için saklar. Eğer kaybolursa, e-nakitiniz kaybolur ve başvuracak kimse kalmaz. Sadece kaybedebileceğin şeyi sakla.", "disclaimer.callout": "Bir Cashu mint'i paranızı sizin için tutar. Kaybolursa ecash'iniz gider ve başvurabileceğiniz kimse olmaz. Yalnızca kaybetmeyi göze alabileceğiniz kadarını tutun.",
"disclaimer.custodial.title": "Darphaneler muhafızdır", "disclaimer.custodial.title": "Mint'ler emanetçidir",
"disclaimer.custodial.p1": "E-cash bastığınızda, bitcoin'i mint operatörüne verirsiniz ve tokenları geri alırsınız. Bu tokenlar o operatör üzerinde bir hak hakkıdır. Bunlar sizin sahip olduğunuz bitcoin değildir ve onları takdir edip etmeyeceğinize karar veren operatördür.", "disclaimer.custodial.p1": "E-cash bastığınızda, bitcoin'i mint operatörüne verirsiniz ve tokenları geri alırsınız. Bu tokenlar o operatör üzerinde bir hak hakkıdır. Bunlar sizin sahip olduğunuz bitcoin değildir ve onları takdir edip etmeyeceğinize karar veren operatördür.",
"disclaimer.custodial.p2": "Bu, her darphanenin aynı riskleri taşıdığı anlamına gelir: operatör ortadan kaybolabilir, durdurmaya zorlanabilir, bir hata veya hack yoluyla fonları kaybedebilir ya da sunucuyu kapatabilir. Depozito sigortası yok, geri ödeme yok, destek hattı yok ve güvenmeye değer yasal bir yol yok. Bu sitedeki en yüksek puanlı mint için de geçerli.", "disclaimer.custodial.p2": "Bu, her mint'in aynı riskleri taşıdığı anlamına gelir: işletmeci ortadan kaybolabilir, durdurulmaya zorlanabilir, bir hata veya saldırı yüzünden fonları kaybedebilir ya da sunucuyu kapatabilir. Mevduat sigortası yok, ters ibraz yok, destek hattı yok ve güvenmeye değer yasal bir yol yok. Bu, bu sitedeki en yüksek puanlı mint için de geçerlidir.",
"disclaimer.custodial.p3": "Cashu, küçük miktarların hızlı hareket etmesini sağlamak için tasarlanmıştır. Bunu şöyle kullanın: harcamak üzere olduğunuz şeyi tutun, birikimlerinizi değil, ve her şeyi bir arada park etmek yerine mint mağazalarına yayın.", "disclaimer.custodial.p3": "Cashu, küçük miktarların hızlı hareket etmesini sağlamak için tasarlanmıştır. Bunu şöyle kullanın: harcamak üzere olduğunuz şeyi tutun, birikimlerinizi değil, ve her şeyi bir arada park etmek yerine mint mağazalarına yayın.",
"disclaimer.reviews.title": "İncelemeler görüşdür, doğrulanmış iddialar değildir", "disclaimer.reviews.title": "İncelemeler görüşdür, doğrulanmış iddialar değildir",
"disclaimer.reviews.p1": "Buradaki her inceleme Nostr'dan biri tarafından yazılmış ve anahtarıyla imzalanmıştır. İmzalı olmak, hangi anahtarın yazdığını söyleyebileceğimiz anlamına gelir. Bu, incelemenin doğru olduğu, yazarın darphaneyi kullandığı ya da yazarın operatör, rakip ya da on anahtara sahip bir kişi olmadığı anlamına gelmez.", "disclaimer.reviews.p1": "Buradaki her inceleme Nostr'daki biri tarafından yazılmış ve anahtarıyla imzalanmıştır. İmzalı olması, hangi anahtarın yazdığını size söyleyebileceğimiz anlamına gelir. İncelemenin doğru olduğu, yazarın mint'i kullandığı ya da yazarın işletmeci, bir rakip veya on anahtarı olan tek bir kişi olmadığı anlamına gelmez.",
"disclaimer.reviews.p2": "İnceleme yapanları doğrulamayız ve katılmadığımız yorumları düzenlemez veya kaldırmıyoruz. Birkaçını okuyun, aslında ne söylediklerine bakın ve üç incelemeli bir mint tartı, üç yüz inceleme ile farklı bir tartı tartın.", "disclaimer.reviews.p2": "İnceleme yapanları doğrulamayız ve katılmadığımız yorumları düzenlemez veya kaldırmıyoruz. Birkaçını okuyun, aslında ne söylediklerine bakın ve üç incelemeli bir mint tartı, üç yüz inceleme ile farklı bir tartı tartın.",
"disclaimer.status.title": "Çevrim, ulaşılabilir demektir, güvenilir değil", "disclaimer.status.title": "Çevrim, ulaşılabilir demektir, güvenilir değil",
"disclaimer.status.p1": "Her minti düzenli olarak kontrol ediyoruz ve cevap verip vermediğini gösteriyoruz. Yeşil durumun dediği tek şey budur: sunucu yanıt verdi. Operatörün ödeme yapıp etmediği, dürüst olup olmadığı ya da yarın tokenlarınızı hâlâ onurlandırmayı düşünüp düşünmediği hakkında hiçbir şey belirtmiyor.", "disclaimer.status.p1": "Her minti düzenli olarak kontrol ediyoruz ve cevap verip vermediğini gösteriyoruz. Yeşil durumun dediği tek şey budur: sunucu yanıt verdi. Operatörün ödeme yapıp etmediği, dürüst olup olmadığı ya da yarın tokenlarınızı hâlâ onurlandırmayı düşünüp düşünmediği hakkında hiçbir şey belirtmiyor.",
"disclaimer.status.p2": "Bir mint artık çevrimiçi olmayana kadar olabilir. Çalışma süresi geçmişi, desteklenen NUT'lar ve yazılım sürümleri bir mintin nasıl işletildiği hakkında bir şeyler söyler, ama hiçbiri paranizle ilgili bir söz değildir.", "disclaimer.status.p2": "Bir mint artık çevrimiçi olmayana kadar olabilir. Çalışma süresi geçmişi, desteklenen NUT'lar ve yazılım sürümleri bir mintin nasıl işletildiği hakkında bir şeyler söyler, ama hiçbiri paranizle ilgili bir söz değildir.",
"disclaimer.ranking.title": "Sıralama nasıl çalışır ve ne anlama gelmez", "disclaimer.ranking.title": "Sıralama nasıl çalışır ve ne anlama gelmez",
"disclaimer.ranking.p1": "Nathanalar, kamu inceleme verileri üzerinden algoritma tarafından sıralanır: derecelendirme, inceleme sayısı ve son aktiviteler; çevrimiçi darphaneler varsayılan olarak çevrimdışı olanların önünde olur. Detaylar açık kaynak kodunda.", "disclaimer.ranking.p1": "Mint'ler kamuya açık inceleme verileri üzerinden bir algoritma ile sıralanır: puan, inceleme sayısı ve son etkinlik; çevrimiçi mint'ler varsayılan olarak çevrimdışı olanların önünde gelir. Detaylar açık kaynak kodunda.",
"disclaimer.ranking.p2": "Kimse yerleştirme için ödeme yapmaz. Bu sitede sponsorlu bir slot, promosyonlu liste ve herhangi bir darphaneyle anlaşma yoktur. Yüksek sıralama, algoritmanın kamuya açık verileri beğendiği anlamına gelir, başka bir şey değildir. Bu bir tavsiye değildir ve güvenlik derecesi değildir.", "disclaimer.ranking.p2": "Kimse sıralama için ödeme yapmaz. Bu sitede sponsorlu alan, öne çıkarılmış liste veya herhangi bir mint ile anlaşma yoktur. Yüksek sıra, algoritmanın kamuya açık verileri beğendiği anlamına gelir, fazlası değil. Bu bir tavsiye değildir ve bir güvenlik puanı değildir.",
"disclaimer.notAdvice.title": "Bu finansal tavsiye değil", "disclaimer.notAdvice.title": "Bu finansal tavsiye değil",
"disclaimer.notAdvice.p1": "Bu sitedeki hiçbir şey finansal, yatırım, hukuki veya vergi tavsiyesi değildir. Durumunuzu bilmiyoruz ve size tavsiye vermiyoruz. Mint kullanmak ve hangisini seçmek sizin kararınız.", "disclaimer.notAdvice.p1": "Bu sitedeki hiçbir şey finansal, yatırım, hukuki veya vergi tavsiyesi değildir. Durumunuzu bilmiyoruz ve size tavsiye vermiyoruz. Mint kullanmak ve hangisini seçmek sizin kararınız.",
"disclaimer.dyor.title": "Kendi araştırmanızı yapın", "disclaimer.dyor.title": "Kendi araştırmanızı yapın",
"disclaimer.dyor.li1": "Operatörü araştır. Kamu itibarı olan birinin işlettiği bir darpence, anonim bir URL'den farklı bir öneridir.", "disclaimer.dyor.li1": "İşletmeciyi araştırın. Kaybedecek kamusal itibarı olan birinin işlettiği bir mint, anonim bir URL'den başka bir şeydir.",
"disclaimer.dyor.li2": "Darphanenin kendi şartlarını ve MOTD'sini okuyun. Kendine test veya beta diyen bir mint ise demektir.", "disclaimer.dyor.li2": "Mint'in kendi şartlarını ve MOTD'sini okuyun. Kendine test ya da beta diyen bir mint bunu ciddi söylüyordur.",
"disclaimer.dyor.li3": "Küçük başlayın. Biraz mint yap, harca, tekrar erit, ve daha fazlasını emanet etmeden önce ne olacağını gör.", "disclaimer.dyor.li3": "Küçük başlayın. Biraz mint yap, harca, tekrar erit, ve daha fazlasını emanet etmeden önce ne olacağını gör.",
"disclaimer.dyor.li4": "Sınırlara dikkat et. Düşük maksimum genellikle dikkatli bir operatörün işaretidir, yüksek maksimum ise güvenli bir operatörün işareti değildir.", "disclaimer.dyor.li4": "Sınırlara dikkat et. Düşük maksimum genellikle dikkatli bir operatörün işaretidir, yüksek maksimum ise güvenli bir operatörün işareti değildir.",
"disclaimer.dyor.li5": "Tokenlarınızı yedeği tutun ve unutmayın ki token taşıyıcı varlıktır: kim varsa onu harcayabilir.", "disclaimer.dyor.li5": "Tokenlarınızı yedeği tutun ve unutmayın ki token taşıyıcı varlıktır: kim varsa onu harcayabilir.",
@@ -546,30 +570,30 @@
"disclaimer.updated": "20 Ağustos 2026", "disclaimer.updated": "20 Ağustos 2026",
"login.getPrimal": "Primal'ı al", "login.getPrimal": "Primal'ı al",
"reviews.dialog.identity.extensionGone": "Tarayıcı uzantınız cevap vermiyor. Tekrar giriş yapın ya da uzaktan imzacıyı kullanın.", "reviews.dialog.identity.extensionGone": "Tarayıcı uzantınız cevap vermiyor. Tekrar giriş yapın ya da uzaktan imzacıyı kullanın.",
"seo.orgDescription": "Her mintin canlı durumu ve topluluk incelemelerinin Nostr'da imzaladığı Cashu mint dizini.", "seo.orgDescription": "Her mint'in canlı durumu ve Nostr'da imzalanmış topluluk incelemeleriyle birlikte bir Cashu mint dizini.",
"seo.ogImageAlt": "Cashumints.space: güvenebileceğiniz bir Cashu mint bul.", "seo.ogImageAlt": "Cashumints.space: güvenebileceğiniz bir Cashu mint'i bulun.",
"seo.mint.title.one": "{name} incelemeleri: {n} incelemesinden {rating} derecelendirildi | Cashumints.space", "seo.mint.title.one": "{name} incelemeleri: {n} incelemeden {rating} | Cashumints.space",
"seo.mint.title.other": "{name} yorumları: {n} yorumlarından {rating} derecelendirildi | Cashumints.space", "seo.mint.title.other": "{name} incelemeleri: {n} incelemeden {rating} | Cashumints.space",
"seo.mint.titleOffline.one": "{name}: {n} gün boyunca çevrimdışı | Cashumints.space", "seo.mint.titleOffline.one": "{name}: {n} gün boyunca çevrimdışı | Cashumints.space",
"seo.mint.titleOffline.other": "{name}: {n} gün çevrimdışı | Cashumints.space", "seo.mint.titleOffline.other": "{name}: {n} gün çevrimdışı | Cashumints.space",
"seo.mint.titleNoReviews": "{name}: Cashu mint bilgisi ve incelemeleri | Cashumints.space", "seo.mint.titleNoReviews": "{name}: Cashu mint bilgisi ve incelemeleri | Cashumints.space",
"seo.fedimint.titleNoReviews": "{name}: Fedimint federasyonu bilgileri ve incelemeleri | Cashumints.space", "seo.fedimint.titleNoReviews": "{name}: Fedimint federasyonu bilgileri ve incelemeleri | Cashumints.space",
"seo.desc.fallback": "{name} için topluluk yorumları ({domain}).", "seo.desc.fallback": "{name} ({domain}) için topluluk incelemeleri.",
"seo.desc.fallbackDomain": "Cashu darphanesi için topluluk yorumları {domain}.", "seo.desc.fallbackDomain": "{domain} adresindeki Cashu mint'i için topluluk incelemeleri.",
"seo.desc.rated.one": "{n} imzalı Nostr incelemesinden {rating}/5 derecesi alındı.", "seo.desc.rated.one": "{n} imzalı Nostr incelemesinden {rating}/5 derecesi alındı.",
"seo.desc.rated.other": "{n} imzalı Nostr incelemelerinden {rating}/5 derecelendirildi.", "seo.desc.rated.other": "{n} imzalı Nostr incelemelerinden {rating}/5 derecelendirildi.",
"seo.desc.noReviews": "Henüz inceleme yok.", "seo.desc.noReviews": "Henüz inceleme yok.",
"seo.desc.status": "{status}.", "seo.desc.status": "{status}.",
"seo.desc.statusLimits": "{status}, sınırlar {limits}.", "seo.desc.statusLimits": "{status}, sınırlar {limits}.",
"seo.desc.fedModules": "{status}. Modüller: {modules}.", "seo.desc.fedModules": "{status}. Modüller: {modules}.",
"seo.desc.fedModulesNetwork": "{status}. Modüller: {modules} on {network}.", "seo.desc.fedModulesNetwork": "{status}. Modüller: {network} üzerinde {modules}.",
"seo.mint.ogAlt.one": "{name}: {rating} {n} incelemesinden Cashumints.space", "seo.mint.ogAlt.one": "{name}: Cashumints.space üzerinde {n} incelemeden {rating} yıldız",
"seo.mint.ogAlt.other": "{name}: {rating} {n} incelemelerinden yıldızlar Cashumints.space", "seo.mint.ogAlt.other": "{name}: Cashumints.space üzerinde {n} incelemeden {rating} yıldız",
"seo.mint.ogAltNoReviews": "{name} on Cashumints.space", "seo.mint.ogAltNoReviews": "Cashumints.space üzerinde {name}",
"nav.home": "Ana Sayfa", "nav.home": "Ana Sayfa",
"mint.more.title": "Daha fazla mint", "mint.more.title": "Daha fazla mint",
"mint.more.all": "Tüm mint ürünlerini gör", "mint.more.all": "Tüm mint ürünlerini gör",
"nav.fedimints": "Fedimintler", "nav.fedimints": "Fedimints",
"status.announced": "Sadece duyuruldu", "status.announced": "Sadece duyuruldu",
"card.unnamedFederation": "İsimsiz federasyon", "card.unnamedFederation": "İsimsiz federasyon",
"card.notChecked": "hiç kontrol edilmedi", "card.notChecked": "hiç kontrol edilmedi",
@@ -620,7 +644,7 @@
"fedimint.more.all": "Tüm federasyonları gör", "fedimint.more.all": "Tüm federasyonları gör",
"fedimint.disclaimer": "Bir federasyona katılmak, paranızı koruyucularına emanet etmek demektir. Derecelendirmeler topluluk görüşleridir ve onaylanmış koşu ulaşılabilir demektir, güvenilir değil.", "fedimint.disclaimer": "Bir federasyona katılmak, paranızı koruyucularına emanet etmek demektir. Derecelendirmeler topluluk görüşleridir ve onaylanmış koşu ulaşılabilir demektir, güvenilir değil.",
"fedimint.module.lightning": "Lightning", "fedimint.module.lightning": "Lightning",
"fedimint.module.mint": "Ecash darphanesi", "fedimint.module.mint": "Ecash mint'i",
"fedimint.module.wallet": "Zincir içi cüzdan", "fedimint.module.wallet": "Zincir içi cüzdan",
"fedimint.module.meta": "Meta veriler", "fedimint.module.meta": "Meta veriler",
"fedimint.module.stability_pool": "Stabilite havuzu", "fedimint.module.stability_pool": "Stabilite havuzu",
@@ -635,8 +659,8 @@
"feed.ecosystem.all": "Hepsi", "feed.ecosystem.all": "Hepsi",
"feed.ecosystem.cashu": "Cashu", "feed.ecosystem.cashu": "Cashu",
"feed.ecosystem.fedimint": "Fedimint", "feed.ecosystem.fedimint": "Fedimint",
"feed.filterBySubject": "Darphane veya federasyona göre filtre", "feed.filterBySubject": "Mint veya federasyona göre filtrele",
"feed.anySubject": "Herhangi bir darphane veya federasyon", "feed.anySubject": "Herhangi bir mint veya federasyon",
"feed.clearSubject": "İsim filtresini temizle", "feed.clearSubject": "İsim filtresini temizle",
"home.topFedimints.title": "En İyi Fedimintler", "home.topFedimints.title": "En İyi Fedimintler",
"home.topFedimints.sub": "İncelemeler ve puanlamaya göre sıralanır", "home.topFedimints.sub": "İncelemeler ve puanlamaya göre sıralanır",
@@ -652,7 +676,113 @@
"mint.warnings.neverConfirmed.body.quiet": "Bu federasyon {date} tarihinde Nostr'da duyuruldu, {days} önce, o zamandan beri hiçbir şey doğrulanmadı ve son zamanlarda kimse incelemedi. Aşağıdaki her şey duyurudan alındı.", "mint.warnings.neverConfirmed.body.quiet": "Bu federasyon {date} tarihinde Nostr'da duyuruldu, {days} önce, o zamandan beri hiçbir şey doğrulanmadı ve son zamanlarda kimse incelemedi. Aşağıdaki her şey duyurudan alındı.",
"mint.warnings.neverConfirmed.meta": "{date} duyuruldu, hiç doğrulanmadı", "mint.warnings.neverConfirmed.meta": "{date} duyuruldu, hiç doğrulanmadı",
"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": "Minting, melting 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 incelemeleri | 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."
} }
+142 -8
View File
@@ -48,8 +48,12 @@
"footer.disclaimer": "Відмова від відповідальності", "footer.disclaimer": "Відмова від відповідальності",
"footer.terms": "Умови", "footer.terms": "Умови",
"footer.privacy": "Конфіденційність", "footer.privacy": "Конфіденційність",
"footer.madeBy": "Створено з ❤️ автором {azzamo} · Працює на Cashu та NIP-87", "footer.madeBy": "Створено з ❤️ {azzamo} · Працює на Cashu та NIP-87",
"footer.warning": "Мінти є кастодіальними. Мінт може зникнути разом із вашими грошима.", "footer.warning": "Мінти є кастодіальними. Мінт може зникнути разом із вашими грошима.",
"footer.tagline": "Відгуки спільноти про ecash-мінти, підписані в Nostr.",
"footer.col.explore": "Огляд",
"footer.col.site": "Сайт",
"footer.nav": "Підвал сайту",
"home.title": "Cashumints.space · Знайдіть мінт Cashu, якому можна довіряти", "home.title": "Cashumints.space · Знайдіть мінт Cashu, якому можна довіряти",
"home.description": "Знайдіть мінт Cashu, якому можна довіряти. Проіндексовано {mints} мінтів із {reviews} відгуками спільноти, підписаними в Nostr, а також актуальними даними про доступність, підтримувані NUT і програмне забезпечення мінтів.", "home.description": "Знайдіть мінт Cashu, якому можна довіряти. Проіндексовано {mints} мінтів із {reviews} відгуками спільноти, підписаними в Nostr, а також актуальними даними про доступність, підтримувані NUT і програмне забезпечення мінтів.",
"home.hero.title": "Знайдіть мінт Cashu", "home.hero.title": "Знайдіть мінт Cashu",
@@ -116,6 +120,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 мінту",
@@ -277,6 +283,9 @@
"reviews.empty": "Поки що відгуків немає. Будьте першими, хто напише такий.", "reviews.empty": "Поки що відгуків немає. Будьте першими, хто напише такий.",
"reviews.emptyWritten": "Поки що немає письмових відгуків. Будьте першими, хто напише такий.", "reviews.emptyWritten": "Поки що немає письмових відгуків. Будьте першими, хто напише такий.",
"reviews.emptyFilter": "Жоден відгук не відповідає цьому фільтру.", "reviews.emptyFilter": "Жоден відгук не відповідає цьому фільтру.",
"reviews.rate.cashu": "Оцініть цей мінт",
"reviews.rate.fedimint": "Оцініть цю федерацію",
"reviews.rate.lnurl": "Оцініть цей мінт",
"reviews.noscript": "Відгуки підписуються на Nostr і завантажуються у вашому браузері, тому цей список потребує JavaScript. Вищезазначений рейтинг і підрахунок походять із самого сайту.", "reviews.noscript": "Відгуки підписуються на Nostr і завантажуються у вашому браузері, тому цей список потребує JavaScript. Вищезазначений рейтинг і підрахунок походять із самого сайту.",
"reviews.relayError": "Не вдалося підключитися до relay. Відгуки можуть бути неповними.", "reviews.relayError": "Не вдалося підключитися до relay. Відгуки можуть бути неповними.",
"reviews.srRating": "{rating} з 5", "reviews.srRating": "{rating} з 5",
@@ -289,7 +298,7 @@
"reviews.anonNote": "Перший відгук від цього npub, іншої активності не знайдено", "reviews.anonNote": "Перший відгук від цього npub, іншої активності не знайдено",
"reviews.published": "Опубліковано.", "reviews.published": "Опубліковано.",
"reviews.publishedPartial": "Опубліковано на {accepted} із {total} relays.", "reviews.publishedPartial": "Опубліковано на {accepted} із {total} relays.",
"reviews.propagating": "Поширюється мережею, поява в інших місцях може тривати деякий час.", "reviews.propagating": "Публікація на relays Nostr",
"reviews.summary.line.one": "{count} рейтинг без коментаря: {breakdown}", "reviews.summary.line.one": "{count} рейтинг без коментаря: {breakdown}",
"reviews.summary.line.few": "{count} оцінки без коментарів: {breakdown}", "reviews.summary.line.few": "{count} оцінки без коментарів: {breakdown}",
"reviews.summary.line.many": "{count} оцінок без коментарів: {breakdown}", "reviews.summary.line.many": "{count} оцінок без коментарів: {breakdown}",
@@ -307,7 +316,12 @@
"reviews.breakdown.1": "{n} одну зірку", "reviews.breakdown.1": "{n} одну зірку",
"reviews.breakdown.none": "{n} без рейтингу", "reviews.breakdown.none": "{n} без рейтингу",
"reviews.dialog.title": "Відгук про {name}", "reviews.dialog.title": "Відгук про {name}",
"reviews.dialog.note": "Підписується вашим Nostr-ключем і публікується на загальнодоступних relays. Відгук не можна відредагувати або видалити.", "reviews.dialog.lede.cashu": "Розкажіть про свій досвід із цим мінтом.",
"reviews.dialog.lede.fedimint": "Розкажіть про свій досвід із цією федерацією.",
"reviews.dialog.lede.lnurl": "Розкажіть про свій досвід із цим мінтом.",
"reviews.dialog.permanent": "Публікується назавжди на загальнодоступних relays Nostr.",
"reviews.dialog.gate.title": "Увійдіть, щоб написати відгук",
"reviews.dialog.gate.lead": "Відгуки підписуються вашим ключем Nostr. Він залишається у вашому signer, сайт ніколи його не бачить.",
"reviews.dialog.ratingLegend": "Ваш рейтинг", "reviews.dialog.ratingLegend": "Ваш рейтинг",
"reviews.dialog.starsSr.one": "{n} зірка", "reviews.dialog.starsSr.one": "{n} зірка",
"reviews.dialog.starsSr.few": "{n} зірки", "reviews.dialog.starsSr.few": "{n} зірки",
@@ -330,7 +344,6 @@
"reviews.dialog.waitingSigner": "Очікування signer", "reviews.dialog.waitingSigner": "Очікування signer",
"reviews.dialog.confirmClose": "Закрити цей відгук? Чернетка зберігатиметься, доки ви не залишите сайт.", "reviews.dialog.confirmClose": "Закрити цей відгук? Чернетка зберігатиметься, доки ви не залишите сайт.",
"reviews.dialog.why.login": "Увійти для публікації", "reviews.dialog.why.login": "Увійти для публікації",
"reviews.dialog.why.rating": "Виберіть рейтинг",
"reviews.dialog.why.words": "Напишіть кілька слів", "reviews.dialog.why.words": "Напишіть кілька слів",
"reviews.dialog.why.more.one": "Ще {n} символ", "reviews.dialog.why.more.one": "Ще {n} символ",
"reviews.dialog.why.more.few": "Ще {n} символи", "reviews.dialog.why.more.few": "Ще {n} символи",
@@ -351,7 +364,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": "Відгуки",
@@ -397,6 +410,10 @@
"login.status.waitingSigner": "Очікування signer", "login.status.waitingSigner": "Очікування signer",
"login.status.connecting": "Підключення до signer", "login.status.connecting": "Підключення до signer",
"login.status.connected": "Підключено", "login.status.connected": "Підключено",
"login.toast.loggedIn": "Вхід виконано",
"login.toast.fetchingProfile": "Завантаження вашого профілю Nostr з relays…",
"login.toast.profile": "Профіль: {name}",
"login.toast.noProfile": "Профіль на relays не знайдено",
"login.method.extension": "Розширення", "login.method.extension": "Розширення",
"login.method.remote": "Віддалений", "login.method.remote": "Віддалений",
"login.method.primal": "Primal", "login.method.primal": "Primal",
@@ -428,7 +445,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": "Гаманці",
@@ -605,7 +629,7 @@
"nav.home": "Головна", "nav.home": "Головна",
"mint.more.title": "Інші мінти", "mint.more.title": "Інші мінти",
"mint.more.all": "Переглянути всі мінти", "mint.more.all": "Переглянути всі мінти",
"nav.fedimints": "Fedimint", "nav.fedimints": "Fedimints",
"status.announced": "Лише оголошено", "status.announced": "Лише оголошено",
"card.unnamedFederation": "Безіменна федерація", "card.unnamedFederation": "Безіменна федерація",
"card.notChecked": "ніколи не перевірено", "card.notChecked": "ніколи не перевірено",
@@ -696,5 +720,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 теж був недоступний, тож нічого не можна було відкарбувати чи розплавити."
} }
+199 -69
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,44 +15,48 @@
"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": "زبان",
"lang.switcher": "زبان منتخب کریں", "lang.switcher": "زبان منتخب کریں",
"footer.copyright": "نصب شدہ:{year}Cashumints. space", "footer.copyright": "© {year} Cashumints.space",
"footer.allMints": "تمام کیوشو منتر", "footer.allMints": "تمام Cashu منٹس",
"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 اور NIP-87 پر مبنی",
"footer.warning": "آپ کے پیسوں سے منٹ ختم ہو سکتے ہیں ۔", "footer.warning": "منٹس تحویلی ہیں۔ کوئی منٹ آپ کے پیسوں کے ساتھ غائب ہو سکتا ہے۔",
"home.title": "Cashumints. space · Cashu منٹ پر آپ اعتماد کر سکتے ہیں۔", "footer.tagline": "ecash منٹس کے لیے کمیونٹی کے جائزے، Nostr پر دستخط شدہ۔",
"home.description": "Cashuہ منٹ تلاش کر سکتے ہیں ۔{mints}منٹوں کا حساب{reviews}نوسٹر پر دستخط کیے گئے، اس کے علاوہ اپ ٹائم جیتے، این یو ٹی اور منٹ سافٹ ویئر کی حمایت کی۔", "footer.col.explore": "دریافت کریں",
"home.hero.title": "ایک جیسی جو ڑیاں تلا ش کریں", "footer.col.site": "سائٹ",
"footer.nav": "فوٹر",
"home.title": "Cashumints.space · ایک ایسا Cashu منٹ تلاش کریں جس پر آپ بھروسا کر سکیں",
"home.description": "ایک ایسا Cashu منٹ تلاش کریں جس پر آپ بھروسا کر سکیں۔ {mints} منٹس انڈیکس میں، {reviews} کمیونٹی جائزے جن پر Nostr پر دستخط ہوئے، ساتھ ہی لائیو اپ ٹائم، معاون NUTs اور منٹ سافٹ ویئر۔",
"home.hero.title": "ایک Cashu منٹ تلاش کریں",
"home.hero.titleEm": "تم اعتماد کر سکتے ہیں", "home.hero.titleEm": "تم اعتماد کر سکتے ہیں",
"home.hero.lede": "نوسٹر نیٹ ورک پر ہر منٹ، اصلی کمیونٹی کے تجزیے کے ساتھ.", "home.hero.lede": "نوسٹر نیٹ ورک پر ہر منٹ، اصلی کمیونٹی کے تجزیے کے ساتھ.",
"home.search.label": "تلاش منٹس اسم، ڈومین یا اسم سے ہوتے ہیں۔", "home.search.label": "تلاش منٹس اسم، ڈومین یا اسم سے ہوتے ہیں۔",
@@ -89,9 +93,9 @@
"pulse.indexer": "غیر درج شدہ", "pulse.indexer": "غیر درج شدہ",
"pulse.connected": "متصل", "pulse.connected": "متصل",
"pulse.stale": "مصر", "pulse.stale": "مصر",
"mints.title": "All Cashu Mints · Cashumints. space –", "mints.title": "تمام Cashu منٹس · Cashumints.space",
"mints.description": "تمام{total}Cashu منٹس نے نوسٹر نیٹ ورک سے انڈیکس کیا،{online}آن لائن{offline}اب، کمیونٹی کی شرح، وقت اور منٹ سافٹ وئیر کے ساتھ. تلاش کریں اور مکمل فہرست کی طرح.", "mints.description": "تمام{total}Cashu منٹس نے نوسٹر نیٹ ورک سے انڈیکس کیا،{online}آن لائن{offline}اب، کمیونٹی کی شرح، وقت اور منٹ سافٹ وئیر کے ساتھ. تلاش کریں اور مکمل فہرست کی طرح.",
"mints.heading": "تمام کیوشو منتر", "mints.heading": "تمام Cashu منٹس",
"mints.lede": "نیوسٹر نیٹ ورک پر ہر منٹ کی دریافت ہوئی جس میں وہ لوگ بھی شامل ہیں جو چلے جا رہے ہیں ۔", "mints.lede": "نیوسٹر نیٹ ورک پر ہر منٹ کی دریافت ہوئی جس میں وہ لوگ بھی شامل ہیں جو چلے جا رہے ہیں ۔",
"mints.search.label": "تلاش منٹس اسم یا ڈومین سے ہوتے ہیں۔", "mints.search.label": "تلاش منٹس اسم یا ڈومین سے ہوتے ہیں۔",
"mints.search.placeholder": "نام یا ڈومین سے تلاش کرو...", "mints.search.placeholder": "نام یا ڈومین سے تلاش کرو...",
@@ -114,6 +118,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": "منٹ کاپی کریں",
@@ -261,6 +267,9 @@
"reviews.empty": "ابھی تک کوئی تجزیے نہیں کیے۔ سب سے پہلے ایک لکھنے والے کو لکھیں۔", "reviews.empty": "ابھی تک کوئی تجزیے نہیں کیے۔ سب سے پہلے ایک لکھنے والے کو لکھیں۔",
"reviews.emptyWritten": "یہاں ابھی تک کوئی تحریری تجزیے نہیں لکھے۔ سب سے پہلے ایک لکھیں۔", "reviews.emptyWritten": "یہاں ابھی تک کوئی تحریری تجزیے نہیں لکھے۔ سب سے پہلے ایک لکھیں۔",
"reviews.emptyFilter": "کوئی تجزیے اس فیلڈر سے مل کر نہیں ملتے.", "reviews.emptyFilter": "کوئی تجزیے اس فیلڈر سے مل کر نہیں ملتے.",
"reviews.rate.cashu": "اس منٹ کو ریٹنگ دیں",
"reviews.rate.fedimint": "اس فیڈریشن کو ریٹنگ دیں",
"reviews.rate.lnurl": "اس منٹ کو ریٹنگ دیں",
"reviews.noscript": "آپ کے براؤزر میں نوسٹر اور لوڈ پر Reviews دستخط کیے گئے ہیں، چنانچہ اس فہرست میں جاوا ایسکریپٹ کی ضرورت ہوتی ہے. شرح خواندگی اور حساب سے اوپر آتا ہے خود سائٹ سے۔", "reviews.noscript": "آپ کے براؤزر میں نوسٹر اور لوڈ پر Reviews دستخط کیے گئے ہیں، چنانچہ اس فہرست میں جاوا ایسکریپٹ کی ضرورت ہوتی ہے. شرح خواندگی اور حساب سے اوپر آتا ہے خود سائٹ سے۔",
"reviews.relayError": "ریلیز تک رسائی حاصل نہیں ہو سکی۔ دوبارہ تجدید ہو سکتی ہے۔", "reviews.relayError": "ریلیز تک رسائی حاصل نہیں ہو سکی۔ دوبارہ تجدید ہو سکتی ہے۔",
"reviews.srRating": "{rating}5 میں سے", "reviews.srRating": "{rating}5 میں سے",
@@ -273,7 +282,7 @@
"reviews.anonNote": "اس ناول سے پہلا جائزہ، کوئی اور سرگرمی نہیں پائی گئی", "reviews.anonNote": "اس ناول سے پہلا جائزہ، کوئی اور سرگرمی نہیں پائی گئی",
"reviews.published": "شائع ہوا۔", "reviews.published": "شائع ہوا۔",
"reviews.publishedPartial": "شائع شدہ{accepted}کا مطلب{total}ریلویز.", "reviews.publishedPartial": "شائع شدہ{accepted}کا مطلب{total}ریلویز.",
"reviews.propagating": "اِس لئے شاید آپ کسی اَور ملک میں جا کر اُس کے بارے میں بات کریں ۔", "reviews.propagating": "Nostr ریلے پر شائع ہو رہا ہے",
"reviews.summary.line.one": "{count}بغیر تبصرے کے شرحیں:{breakdown}", "reviews.summary.line.one": "{count}بغیر تبصرے کے شرحیں:{breakdown}",
"reviews.summary.line.other": "{count}بغیر تبصرے کے شرحیں:{breakdown}", "reviews.summary.line.other": "{count}بغیر تبصرے کے شرحیں:{breakdown}",
"reviews.summary.lineDay.one": "{count}بغیر تبصرے کے خواندگی{when}:{breakdown}", "reviews.summary.lineDay.one": "{count}بغیر تبصرے کے خواندگی{when}:{breakdown}",
@@ -287,7 +296,12 @@
"reviews.breakdown.1": "{n}ایک ستارہ", "reviews.breakdown.1": "{n}ایک ستارہ",
"reviews.breakdown.none": "{n}اور نہ ہی (اس میں) کمی ہوتی ہے۔", "reviews.breakdown.none": "{n}اور نہ ہی (اس میں) کمی ہوتی ہے۔",
"reviews.dialog.title": "اعادہ{name}", "reviews.dialog.title": "اعادہ{name}",
"reviews.dialog.note": "آپ کی نوسٹر کلید کے ساتھ دستخط کیے اور عوامی ریلیز کے لیے شائع کیے۔اس میں ترمیم یا حذف نہیں کی جا سکتی۔", "reviews.dialog.lede.cashu": "اس منٹ کے بارے میں اپنا تجربہ بتائیں۔",
"reviews.dialog.lede.fedimint": "اس فیڈریشن کے بارے میں اپنا تجربہ بتائیں۔",
"reviews.dialog.lede.lnurl": "اس منٹ کے بارے میں اپنا تجربہ بتائیں۔",
"reviews.dialog.permanent": "عوامی Nostr ریلیز پر مستقل طور پر شائع ہوتا ہے۔",
"reviews.dialog.gate.title": "جائزہ لکھنے کے لیے لاگ ان کریں",
"reviews.dialog.gate.lead": "جائزے آپ کی Nostr کلید سے دستخط ہوتے ہیں۔ کلید آپ کے سائنر میں رہتی ہے، یہ سائٹ اسے کبھی نہیں دیکھتی۔",
"reviews.dialog.ratingLegend": "آپ کی شرح", "reviews.dialog.ratingLegend": "آپ کی شرح",
"reviews.dialog.starsSr.one": "{n}ستارہ", "reviews.dialog.starsSr.one": "{n}ستارہ",
"reviews.dialog.starsSr.other": "{n}ستاروں", "reviews.dialog.starsSr.other": "{n}ستاروں",
@@ -308,7 +322,6 @@
"reviews.dialog.waitingSigner": "آپ کے دستخط کا انتظار", "reviews.dialog.waitingSigner": "آپ کے دستخط کا انتظار",
"reviews.dialog.confirmClose": "اِس جائزے کو بند کریں ؟ آپ کا ڈرافٹ اُس وقت تک برقرار رہتا ہے جب تک آپ ویب‌سائٹ سے باہر نہیں جاتے ۔", "reviews.dialog.confirmClose": "اِس جائزے کو بند کریں ؟ آپ کا ڈرافٹ اُس وقت تک برقرار رہتا ہے جب تک آپ ویب‌سائٹ سے باہر نہیں جاتے ۔",
"reviews.dialog.why.login": "شائع کرنے کے لیے لاگ ان", "reviews.dialog.why.login": "شائع کرنے کے لیے لاگ ان",
"reviews.dialog.why.rating": "فہرست منتخب کریں",
"reviews.dialog.why.words": "چند الفاظ لکھے ہیں۔", "reviews.dialog.why.words": "چند الفاظ لکھے ہیں۔",
"reviews.dialog.why.more.one": "{n}مزید حروف", "reviews.dialog.why.more.one": "{n}مزید حروف",
"reviews.dialog.why.more.other": "{n}مزید حروف", "reviews.dialog.why.more.other": "{n}مزید حروف",
@@ -324,8 +337,8 @@
"reviews.validate.rating": "1 سے 5 تاروں تک کی شرح منتخب کیجئے۔", "reviews.validate.rating": "1 سے 5 تاروں تک کی شرح منتخب کیجئے۔",
"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": "تازہ ترین منٹ اور فیڈریشن جائزے · 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": "اعادہ",
@@ -369,6 +382,10 @@
"login.status.waitingSigner": "آپ کے دستخط کا انتظار", "login.status.waitingSigner": "آپ کے دستخط کا انتظار",
"login.status.connecting": "آپ کے دستخط سے وابستہ", "login.status.connecting": "آپ کے دستخط سے وابستہ",
"login.status.connected": "غیر متصل", "login.status.connected": "غیر متصل",
"login.toast.loggedIn": "لاگ اِن ہو گئے",
"login.toast.fetchingProfile": "آپ کا Nostr پروفائل ریلے سے حاصل کیا جا رہا ہے…",
"login.toast.profile": "پروفائل: {name}",
"login.toast.noProfile": "ریلے پر کوئی پروفائل نہیں ملا",
"login.method.extension": "بند", "login.method.extension": "بند",
"login.method.remote": "دور دراز", "login.method.remote": "دور دراز",
"login.method.primal": "پریمل", "login.method.primal": "پریمل",
@@ -390,7 +407,7 @@
"signer.error.cancelled": "اتصال حذف کردیا گیا.", "signer.error.cancelled": "اتصال حذف کردیا گیا.",
"signer.error.declined": "اشاروں کی درخواست بند کر دی گئی", "signer.error.declined": "اشاروں کی درخواست بند کر دی گئی",
"notfound.title": "صفحہ نہیں پایا گیا * Cashumints.space", "notfound.title": "صفحہ نہیں پایا گیا * Cashumints.space",
"notfound.description": "یہ صفحہ کیس‌مینس پر موجود نہیں ہے ۔", "notfound.description": "یہ صفحہ Cashumints.space پر موجود نہیں ہے۔",
"notfound.heading": "یہ صفحہ موجود نہیں ہے", "notfound.heading": "یہ صفحہ موجود نہیں ہے",
"notfound.lede": "شاید اسے منتقل کِیا گیا ہو یا پھر منٹ‌بھر انڈیکس نہیں کِیا جا سکتا ۔", "notfound.lede": "شاید اسے منتقل کِیا گیا ہو یا پھر منٹ‌بھر انڈیکس نہیں کِیا جا سکتا ۔",
"notfound.browse": "تمام منتر", "notfound.browse": "تمام منتر",
@@ -400,8 +417,15 @@
"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 سے ایک خلاصہ ہے اور نیچے اس کے ریویو ہیں۔ مکمل صفحہ اگلے بلڈ پر آئے گا۔",
"wallets.title": "Cashu Citutes · Cashumints. space", "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.description": "کیش چندر گپت جو یہاں کے منٹوں کے ساتھ کام کرتے ہیں، موبائل، ویب، براؤزر اور کمانڈ لائن کے ساتھ،", "wallets.description": "کیش چندر گپت جو یہاں کے منٹوں کے ساتھ کام کرتے ہیں، موبائل، ویب، براؤزر اور کمانڈ لائن کے ساتھ،",
"wallets.heading": "دیواری", "wallets.heading": "دیواری",
"wallets.lede": "آپ کو ان میں سے کسی بھی منٹوں سے Ecash رکھنے کے لئے کی ضرورت ہے.", "wallets.lede": "آپ کو ان میں سے کسی بھی منٹوں سے Ecash رکھنے کے لئے کی ضرورت ہے.",
@@ -420,10 +444,10 @@
"wallets.item.coinos": "نورنگ کتھانگ اور نقطہ فروخت جو خود Cashu منٹ بھی چلاتا ہے۔", "wallets.item.coinos": "نورنگ کتھانگ اور نقطہ فروخت جو خود Cashu منٹ بھی چلاتا ہے۔",
"wallets.item.oxchat": "نوسٹر رسول نے گفتگو کے دوران بیٹھنے کیلئے ایک ایاس‌میل تعمیر کِیا ۔", "wallets.item.oxchat": "نوسٹر رسول نے گفتگو کے دوران بیٹھنے کیلئے ایک ایاس‌میل تعمیر کِیا ۔",
"wallets.item.nutshell": "حوالہ جات عمل آوری۔ کمانڈ لائن اور منٹ سافٹ ویئر کے طور پر دوڑتا ہے۔", "wallets.item.nutshell": "حوالہ جات عمل آوری۔ کمانڈ لائن اور منٹ سافٹ ویئر کے طور پر دوڑتا ہے۔",
"about.title": "کاس‌مینس ۔", "about.title": "Cashumints.space کے بارے میں",
"about.description": "کیا Cashu اصل میں ایک منٹ ہے، کیا ہے این آئی پی-87، اور کس طرح یہ سائٹ انڈیکسز منٹس پڑھ کر نوسٹر ریلویز کی طرف سے تجزیے پڑھتی ہے۔", "about.description": "کیا Cashu اصل میں ایک منٹ ہے، کیا ہے این آئی پی-87، اور کس طرح یہ سائٹ انڈیکسز منٹس پڑھ کر نوسٹر ریلویز کی طرف سے تجزیے پڑھتی ہے۔",
"about.heading": "کے بارے میں", "about.heading": "کے بارے میں",
"about.lede": "Cashumints. space Lists ہر Cashuہ منٹ ہم نوسٹر نیٹ ورک پر تلاش کر سکتے ہیں اور آپ کو دکھائی دے سکتے ہیں کہ لوگ ان کے بارے میں کیا کہتے ہیں. اسے Azamo نے بنایا ہے۔", "about.lede": "Cashumints.space ہر اُس Cashu منٹ کی فہرست دیتا ہے جو ہمیں Nostr نیٹ ورک پر مل سکے، اور دکھاتا ہے کہ لوگ اُن کے بارے میں کیا کہتے ہیں۔ اسے Azzamo نے بنایا ہے۔",
"about.cashu.title": "کیوشو کیا ہے؟", "about.cashu.title": "کیوشو کیا ہے؟",
"about.cashu.p1": "Cashu ایک اسکاش پروٹوکول ہے Bitcoin کے لئے ایک منٹ کے مسائل اندھے نشانوں کے پیچھے بیٹھ جاتے ہیں", "about.cashu.p1": "Cashu ایک اسکاش پروٹوکول ہے Bitcoin کے لئے ایک منٹ کے مسائل اندھے نشانوں کے پیچھے بیٹھ جاتے ہیں",
"about.cashu.p2": "آپ پروٹوکول اور اس کا اندراج بیک وقت پڑھ سکتے ہیں۔{link}. خود مختاری کو NUT کہا جاتا ہے، یہ وہ جگہ ہے جہاں ہر منٹ کے صفحے پر موجود NUT نمبر آتے ہیں۔", "about.cashu.p2": "آپ پروٹوکول اور اس کا اندراج بیک وقت پڑھ سکتے ہیں۔{link}. خود مختاری کو NUT کہا جاتا ہے، یہ وہ جگہ ہے جہاں ہر منٹ کے صفحے پر موجود NUT نمبر آتے ہیں۔",
@@ -452,12 +476,12 @@
"about.numbers.p1": "{mints}ملین انڈیکس،{online}ان میں سے بعض جواب دے سکتے ہیں{reviews}بالائی ریلوں سے تجزیے شمار کیے جاتے ہیں۔", "about.numbers.p1": "{mints}ملین انڈیکس،{online}ان میں سے بعض جواب دے سکتے ہیں{reviews}بالائی ریلوں سے تجزیے شمار کیے جاتے ہیں۔",
"about.source.title": "ماخذ اور تعریف", "about.source.title": "ماخذ اور تعریف",
"about.source.p1": "سائٹ کھلے چشمہ پر ہے۔{github}. اسے تعمیر اور چلایا جاتا ہے۔{azzamo}. اگر کوئی منٹ کا صفحہ اس پر کچھ غلط ہو تو ماخذ پر اس کی اصلاح کریں : منٹ میٹراڈاٹا خود منٹو کی جانب سے آتا ہے۔{code}. . . . . . . .", "about.source.p1": "سائٹ کھلے چشمہ پر ہے۔{github}. اسے تعمیر اور چلایا جاتا ہے۔{azzamo}. اگر کوئی منٹ کا صفحہ اس پر کچھ غلط ہو تو ماخذ پر اس کی اصلاح کریں : منٹ میٹراڈاٹا خود منٹو کی جانب سے آتا ہے۔{code}. . . . . . . .",
"terms.title": "استعمال کی میعاد × Cashumints. space", "terms.title": "استعمال کی شرائط · Cashumints.space",
"terms.description": "Cashumints کے لیے استعمال کی میعاد: سائٹ کیا ہے، کیا نہیں کرتا اور کون ذمہ دار ہوتا ہے.", "terms.description": "Cashumints.space کے استعمال کی شرائط: یہ سائٹ کیا ہے، کیا نہیں کرتی، اور کس چیز کا ذمہ دار کون ہے۔ کوئی اکاؤنٹ نہیں، کوئی ضمانت نہیں، کسی منٹ سے کوئی وابستگی نہیں۔",
"terms.heading": "استعمال کی میعاد", "terms.heading": "استعمال کی میعاد",
"terms.lede": "ایک ایسی سائٹ کے لیے Plan اصطلاحات جو صرف انڈیکس کو ہی عوامی اعداد و شمار کے لیے مہیا کرتی ہیں۔{date}. .", "terms.lede": "ایک ایسی سائٹ کے لیے Plan اصطلاحات جو صرف انڈیکس کو ہی عوامی اعداد و شمار کے لیے مہیا کرتی ہیں۔{date}. .",
"terms.what.title": "یہ سائٹ کیا ہے؟", "terms.what.title": "یہ سائٹ کیا ہے؟",
"terms.what.p1": "Cashumints. space space about Cashu Mints کے بارے میں عوامی دستیاب اعداد و شمار کا ایک انڈیکس ہے. دو طرح کے اعداد و شمار کسی اور جگہ سے۔", "terms.what.p1": "Cashumints.space ایک انڈیکس ہے جو Cashu منٹس کے بارے میں عوامی طور پر دستیاب ڈیٹا رکھتا ہے۔ ڈیٹا کی دو قسمیں، دونوں کہیں اور سے:",
"terms.what.li1": "منٹگمٹا، ہر منٹ کے اپنے ہی حصے سے پڑھی جاتی ہے۔{code}اختتامی نقطہ: نام، تشریح، سافٹ ویئر ورژن، این یو ٹیس کی حمایت، تفصیلات سے رابطہ کرنا اور آخری سوال پر منٹ نے جواب دیا یا نہیں۔", "terms.what.li1": "منٹگمٹا، ہر منٹ کے اپنے ہی حصے سے پڑھی جاتی ہے۔{code}اختتامی نقطہ: نام، تشریح، سافٹ ویئر ورژن، این یو ٹیس کی حمایت، تفصیلات سے رابطہ کرنا اور آخری سوال پر منٹ نے جواب دیا یا نہیں۔",
"terms.what.li2": "تجزیے، عوامی نوسٹر ریلویز سے پڑھے۔ یہ این آئی پی-87 واقعات ہیں جو ان کے مصنفین کی جانب سے شائع کیے گئے ہیں تاکہ کسی کو ریلیز کیا جا سکے۔", "terms.what.li2": "تجزیے، عوامی نوسٹر ریلویز سے پڑھے۔ یہ این آئی پی-87 واقعات ہیں جو ان کے مصنفین کی جانب سے شائع کیے گئے ہیں تاکہ کسی کو ریلیز کیا جا سکے۔",
"terms.what.p2": "ہم یہ اعداد و شمار جمع کرتے ہیں، اسے عام طور پر مرتب کرتے ہیں، اسے ظاہر کرتے ہیں. ہم اسے پیدا نہیں کرتے.", "terms.what.p2": "ہم یہ اعداد و شمار جمع کرتے ہیں، اسے عام طور پر مرتب کرتے ہیں، اسے ظاہر کرتے ہیں. ہم اسے پیدا نہیں کرتے.",
@@ -479,8 +503,8 @@
"terms.changes.p1": "یہ اصطلاحیں بدل سکتی ہیں۔ اوپر کے اوپر کی تاریخ کہتے ہیں جب انہوں نے آخری کام کیا تو سائٹ کھلے ماخذ ہے لہذا اس صفحے کی ہر تبدیلی عمل میں آتی ہے۔", "terms.changes.p1": "یہ اصطلاحیں بدل سکتی ہیں۔ اوپر کے اوپر کی تاریخ کہتے ہیں جب انہوں نے آخری کام کیا تو سائٹ کھلے ماخذ ہے لہذا اس صفحے کی ہر تبدیلی عمل میں آتی ہے۔",
"terms.contact.title": "رابطہ", "terms.contact.title": "رابطہ",
"terms.contact.p1": "مسائل اور ڈاؤن لوڈ درخواستوں کو بہترین فہرست میں درج کیا گیا ہے۔{github}. سائٹ کے ذریعے چلایا جاتا ہے۔{azzamo}. .", "terms.contact.p1": "مسائل اور ڈاؤن لوڈ درخواستوں کو بہترین فہرست میں درج کیا گیا ہے۔{github}. سائٹ کے ذریعے چلایا جاتا ہے۔{azzamo}. .",
"privacy.title": "Privay · Cashumints. space –", "privacy.title": "رازداری · Cashumints.space",
"privacy.description": "Cashumints پر Privay on Cashumints. space: nocounts, no special cooies, no no no analytics. مقامی اسٹیورج کو profile Caching کے لیے استعمال کیا جاتا ہے اور کہیں بھی نہیں بھیجا جاتا. اپنے ہی براؤزر میں جائزہ لیا جاتا ہے۔", "privacy.description": "Cashumints.space پر رازداری: کوئی اکاؤنٹ نہیں، کوئی ٹریکنگ کوکیز نہیں، کوئی اینالیٹکس نہیں۔ localStorage صرف پروفائل کیشنگ کے لیے استعمال ہوتا ہے اور کچھ بھی کہیں نہیں بھیجا جاتا۔ جائزوں پر آپ کے اپنے براؤزر میں دستخط ہوتے ہیں۔",
"privacy.heading": "پُرتشدد", "privacy.heading": "پُرتشدد",
"privacy.lede": "ہم ایک ویب سائٹ کے طور پر تھوڑا سا جمع کر سکتے ہیں.{date}. .", "privacy.lede": "ہم ایک ویب سائٹ کے طور پر تھوڑا سا جمع کر سکتے ہیں.{date}. .",
"privacy.noAccounts.title": "کوئی اکاؤنٹ نہیں", "privacy.noAccounts.title": "کوئی اکاؤنٹ نہیں",
@@ -507,7 +531,7 @@
"privacy.publishing.p1": "آپ کے براؤزر میں ایک جائزہ لکھیں، ہمارے سرور پر نہیں، اس واقعے پر آپ کے NAP-07 توسیع (Alby، nos2x یا مشابہ) دستخط ہیں اور آپ کے براؤزر سے براہ راست نشر کیا جاتا ہے. آپ کی نجی کلید کبھی توسیع نہیں چھوڑتی اور یہ سائٹ کبھی نہیں دیکھتی اور نہ ہی ہم کلیدی انتظام کے لئے درخواست کرتے ہیں اور نہ ہی کسی کلیدی اصطلاح کے لئے درخواست کرتے ہیں۔", "privacy.publishing.p1": "آپ کے براؤزر میں ایک جائزہ لکھیں، ہمارے سرور پر نہیں، اس واقعے پر آپ کے NAP-07 توسیع (Alby، nos2x یا مشابہ) دستخط ہیں اور آپ کے براؤزر سے براہ راست نشر کیا جاتا ہے. آپ کی نجی کلید کبھی توسیع نہیں چھوڑتی اور یہ سائٹ کبھی نہیں دیکھتی اور نہ ہی ہم کلیدی انتظام کے لئے درخواست کرتے ہیں اور نہ ہی کسی کلیدی اصطلاح کے لئے درخواست کرتے ہیں۔",
"privacy.permanent.title": "جو بھی آپ نوسٹر کو شائع کرتے ہیں وہ عوامی اور مستقل ہے۔", "privacy.permanent.title": "جو بھی آپ نوسٹر کو شائع کرتے ہیں وہ عوامی اور مستقل ہے۔",
"privacy.permanent.p1": "یہ حصہ دو بار پڑھنے کے قابل ہے. ایک جائزے عوامی ریلویز پر دستخط شدہ واقعہ ہے. جب یہ باہر نکلے تو، یہ باہر ہے:", "privacy.permanent.p1": "یہ حصہ دو بار پڑھنے کے قابل ہے. ایک جائزے عوامی ریلویز پر دستخط شدہ واقعہ ہے. جب یہ باہر نکلے تو، یہ باہر ہے:",
"privacy.permanent.li1": "کوئی بھی اسے پڑھ سکتا ہے، اب اور بعد میں کسی بھی نوسٹر کلائنٹ استعمال کر سکتا ہے. یہ \"Cashumints. space\" نہیں ہے، یہ نیٹ ورک پر عوامی ہے۔", "privacy.permanent.li1": "کوئی بھی اسے ابھی اور بعد میں، کسی بھی Nostr کلائنٹ سے پڑھ سکتا ہے۔ یہ ”Cashumints.space پر عوامی“ نہیں ہے، یہ نیٹ ورک پر عوامی ہے۔",
"privacy.permanent.li2": "یہ آپ کے نوسٹر عوامی کُل‌وقتی کُل‌وقتی طور پر شائع ہونے والی تمام چیزوں کے ساتھ مستقل طور پر وابستہ ہے ۔", "privacy.permanent.li2": "یہ آپ کے نوسٹر عوامی کُل‌وقتی کُل‌وقتی طور پر شائع ہونے والی تمام چیزوں کے ساتھ مستقل طور پر وابستہ ہے ۔",
"privacy.permanent.li3": "اس کو مد نظر رکھتے ہوئے یہ درخواست نہیں ہے کہ آپ کوئی متبادل واقعہ شائع کر سکتے ہیں اور کچھ ریلیز اسے عزت دیں گے لیکن ایک ریلیز اسے نظر انداز کرنے کے لیے آزاد ہے اور جس نے پہلے ہی سے اس واقعہ کو نقل کیا ہے اس کا مستقل جائزہ لیں۔", "privacy.permanent.li3": "اس کو مد نظر رکھتے ہوئے یہ درخواست نہیں ہے کہ آپ کوئی متبادل واقعہ شائع کر سکتے ہیں اور کچھ ریلیز اسے عزت دیں گے لیکن ایک ریلیز اسے نظر انداز کرنے کے لیے آزاد ہے اور جس نے پہلے ہی سے اس واقعہ کو نقل کیا ہے اس کا مستقل جائزہ لیں۔",
"privacy.permanent.li4": "ہم اسے آپ کے لئے منسوخ نہیں کر سکتے، ہم اسے یہاں ظاہر کرنے سے روک سکتے ہیں، جسے ریل گاڑیوں پر کچھ بھی نہیں بدل سکتے.", "privacy.permanent.li4": "ہم اسے آپ کے لئے منسوخ نہیں کر سکتے، ہم اسے یہاں ظاہر کرنے سے روک سکتے ہیں، جسے ریل گاڑیوں پر کچھ بھی نہیں بدل سکتے.",
@@ -547,13 +571,13 @@
"login.getPrimal": "پریمل حاصل کریں", "login.getPrimal": "پریمل حاصل کریں",
"reviews.dialog.identity.extensionGone": "آپ کا براؤزر توسیعی جواب نہیں دے رہا. لاگ میں یا پھر سے بعید دستخط کرنے والا استعمال.", "reviews.dialog.identity.extensionGone": "آپ کا براؤزر توسیعی جواب نہیں دے رہا. لاگ میں یا پھر سے بعید دستخط کرنے والا استعمال.",
"seo.orgDescription": "Cashu منٹس کی ایک ڈائریکٹر ، جس پر ہر منٹ اور کمیونٹی کے جائزے نے نوسٹر پر دستخط کئے ۔", "seo.orgDescription": "Cashu منٹس کی ایک ڈائریکٹر ، جس پر ہر منٹ اور کمیونٹی کے جائزے نے نوسٹر پر دستخط کئے ۔",
"seo.ogImageAlt": "Cashumints. space: کسی Cashu منٹ پر اعتماد کر سکتے ہیں.", "seo.ogImageAlt": "Cashumints.space: ایک ایسا Cashu منٹ تلاش کریں جس پر آپ بھروسا کر سکیں۔",
"seo.mint.title.one": "{name}تجزیے : سوال{rating}سے{n}جائزہ ⁇ Cashumints. space -", "seo.mint.title.one": "{name} جائزے: {n} جائزے میں {rating} درجہ بندی | Cashumints.space",
"seo.mint.title.other": "{name}تجزیے : سوال{rating}سے{n}تجزیے - Cashumints. space -", "seo.mint.title.other": "{name} جائزے: {n} جائزوں میں {rating} درجہ بندی | Cashumints.space",
"seo.mint.titleOffline.one": "{name}سوال :{n}Day ⁇ Cashumints. space -", "seo.mint.titleOffline.one": "{name}: {n} دن سے آف لائن | Cashumints.space",
"seo.mint.titleOffline.other": "{name}سوال :{n}Dayshumints. space", "seo.mint.titleOffline.other": "{name}: {n} دن سے آف لائن | Cashumints.space",
"seo.mint.titleNoReviews": "{name}: Cashu Mint Info and Reviews ⁇ Cashumints. space -", "seo.mint.titleNoReviews": "{name}: Cashu منٹ کی معلومات اور جائزے | Cashumints.space",
"seo.fedimint.titleNoReviews": "{name}: Fedimint Federation Infration and reviews ⁇ Cashumints. space -", "seo.fedimint.titleNoReviews": "{name}: Fedimint فیڈریشن کی معلومات اور جائزے | Cashumints.space",
"seo.desc.fallback": "کمیونٹی کے لئے تجزیے{name}({domain})", "seo.desc.fallback": "کمیونٹی کے لئے تجزیے{name}({domain})",
"seo.desc.fallbackDomain": "Cashu منٹ کے لئے کمیونٹی کے جائزے{domain}. .", "seo.desc.fallbackDomain": "Cashu منٹ کے لئے کمیونٹی کے جائزے{domain}. .",
"seo.desc.rated.one": "رُخ{rating}/5 سے{n}نوسٹر جائزے پر دستخط کیے۔", "seo.desc.rated.one": "رُخ{rating}/5 سے{n}نوسٹر جائزے پر دستخط کیے۔",
@@ -563,17 +587,17 @@
"seo.desc.statusLimits": "{status}، حدود{limits}. .", "seo.desc.statusLimits": "{status}، حدود{limits}. .",
"seo.desc.fedModules": "{status}. مدراس :{modules}. .", "seo.desc.fedModules": "{status}. مدراس :{modules}. .",
"seo.desc.fedModulesNetwork": "{status}. مدراس :{modules}پر{network}. .", "seo.desc.fedModulesNetwork": "{status}. مدراس :{modules}پر{network}. .",
"seo.mint.ogAlt.one": "{name}:{rating}ستاروں سے{n}کاس‌مینٹس پر غور کریں ۔", "seo.mint.ogAlt.one": "{name}: Cashumints.space پر {n} جائزے سے {rating} ستارے",
"seo.mint.ogAlt.other": "{name}:{rating}ستاروں سے{n}Cashumints. space پر تجزیات -", "seo.mint.ogAlt.other": "{name}: Cashumints.space پر {n} جائزوں سے {rating} ستارے",
"seo.mint.ogAltNoReviews": "{name}کیس‌مینس پر ۔", "seo.mint.ogAltNoReviews": "{name} Cashumints.space پر",
"nav.home": "گھر", "nav.home": "ہوم",
"mint.more.title": "زیادہ‌تر منٹ", "mint.more.title": "زیادہ‌تر منٹ",
"mint.more.all": "تمام منٹ دیکھیں", "mint.more.all": "تمام منٹ دیکھیں",
"nav.fedimints": "فِدَمِنس", "nav.fedimints": "Fedimints",
"status.announced": "صرف اطلاع", "status.announced": "صرف اطلاع",
"card.unnamedFederation": "غیر درج‌ذیل رعایت", "card.unnamedFederation": "غیر درج‌ذیل رعایت",
"card.notChecked": "کبھی نہیں", "card.notChecked": "کبھی نہیں",
"fedimints.title": "All Fedmint Federations · Cashumints. space -", "fedimints.title": "تمام Fedimint فیڈریشنز · Cashumints.space",
"fedimints.description": "تمام{total}نوسٹر نیٹ ورک پر متعارف کرانے کا اعلان فیمینٹ فیڈریشن نے کیا-{online}ایک بیرونی چیک سے چلنے کی تصدیق کی،{announced}ہر ایک کے لئے کوڈ کی دعوت دیتا ہے اور کبھی تصدیق نہیں کرتا.", "fedimints.description": "تمام{total}نوسٹر نیٹ ورک پر متعارف کرانے کا اعلان فیمینٹ فیڈریشن نے کیا-{online}ایک بیرونی چیک سے چلنے کی تصدیق کی،{announced}ہر ایک کے لئے کوڈ کی دعوت دیتا ہے اور کبھی تصدیق نہیں کرتا.",
"fedimints.heading": "تمام رعایتیں", "fedimints.heading": "تمام رعایتیں",
"fedimints.lede": "نوسٹر نیٹ ورک پر اعلان کیے جانے والے ہر فیمینٹ فیڈریشن کے پاس کوئی عوامی حیثیت ختم نہیں ہوتی اس لیے ان میں سے کچھ کو بیرونی چیک کے ذریعے چلاتے ہیں اور باقی سب کو بالکل اسی طرح شمار کیا جاتا ہے جس طرح اعلان کیا گیا تھا۔", "fedimints.lede": "نوسٹر نیٹ ورک پر اعلان کیے جانے والے ہر فیمینٹ فیڈریشن کے پاس کوئی عوامی حیثیت ختم نہیں ہوتی اس لیے ان میں سے کچھ کو بیرونی چیک کے ذریعے چلاتے ہیں اور باقی سب کو بالکل اسی طرح شمار کیا جاتا ہے جس طرح اعلان کیا گیا تھا۔",
@@ -651,8 +675,114 @@
"mint.warnings.neverConfirmed.body.reviewed": "اس فیڈریشن کا اعلان نوسٹر پر کیا گیا تھا۔{date}:{days}پہلے اور کچھ اس وقت سے تصدیق نہیں ہوئی کہ یہ چل رہا ہے. ذیل کے تجزیے اس صفحے پر زندگی کی واحد علامت ہیں؛", "mint.warnings.neverConfirmed.body.reviewed": "اس فیڈریشن کا اعلان نوسٹر پر کیا گیا تھا۔{date}:{days}پہلے اور کچھ اس وقت سے تصدیق نہیں ہوئی کہ یہ چل رہا ہے. ذیل کے تجزیے اس صفحے پر زندگی کی واحد علامت ہیں؛",
"mint.warnings.neverConfirmed.body.quiet": "اس فیڈریشن کا اعلان نوسٹر پر کیا گیا تھا۔{date}:{days}پہلے، اس وقت سے کچھ بھی تصدیق نہیں کر رہا ہے کہ یہ چل رہا ہے، اور کوئی بھی حال ہی میں اس کا جائزہ نہیں لے رہا.", "mint.warnings.neverConfirmed.body.quiet": "اس فیڈریشن کا اعلان نوسٹر پر کیا گیا تھا۔{date}:{days}پہلے، اس وقت سے کچھ بھی تصدیق نہیں کر رہا ہے کہ یہ چل رہا ہے، اور کوئی بھی حال ہی میں اس کا جائزہ نہیں لے رہا.",
"mint.warnings.neverConfirmed.meta": "اطلاع{date}، کبھی تصدیق نہیں کی گئی", "mint.warnings.neverConfirmed.meta": "اطلاع{date}، کبھی تصدیق نہیں کی گئی",
"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": "اس کا لائٹننگ نوڈ بھی ناقابلِ رسائی تھا، اس لیے کچھ بھی منٹ یا میلٹ نہیں کیا جا سکتا تھا۔"
} }
+264 -134
View File
@@ -1,5 +1,5 @@
{ {
"site.name": "Tiền mặt. không gian", "site.name": "Cashumints.space",
"common.close": "Đóng", "common.close": "Đóng",
"common.cancel": "Thôi", "common.cancel": "Thôi",
"common.done": "Hoàn tất", "common.done": "Hoàn tất",
@@ -33,9 +33,9 @@
"status.unknown": "Chưa kiểm tra", "status.unknown": "Chưa kiểm tra",
"nav.skip": "Bỏ qua nội dung", "nav.skip": "Bỏ qua nội dung",
"nav.main": "Chính", "nav.main": "Chính",
"nav.mints": "mint", "nav.mints": "Mint Cashu",
"nav.reviews": "Xem lại", "nav.reviews": "Đánh giá",
"nav.wallets": "Comment", "nav.wallets": "Ví",
"nav.about": "Giới thiệu", "nav.about": "Giới thiệu",
"nav.searchMints": "Tìm kiếm {n} mint", "nav.searchMints": "Tìm kiếm {n} mint",
"lang.label": "Ngôn ngữ", "lang.label": "Ngôn ngữ",
@@ -43,16 +43,20 @@
"footer.copyright": "© {year} Cashumints.space", "footer.copyright": "© {year} Cashumints.space",
"footer.allMints": "Tất cả mint Cashu", "footer.allMints": "Tất cả mint Cashu",
"footer.allReviews": "Mọi đánh giá", "footer.allReviews": "Mọi đánh giá",
"footer.wallets": "Comment", "footer.wallets": "Ví",
"footer.about": "Giới thiệu", "footer.about": "Giới thiệu",
"footer.disclaimer": "Không nhận diện", "footer.disclaimer": "Tuyên bố miễn trừ trách nhiệm",
"footer.terms": "Kì", "footer.terms": "Điều khoản",
"footer.privacy": "Riêng tư", "footer.privacy": "Riêng tư",
"footer.madeBy": "Được sản xuất với tôm hùm {azzamo} · Powered bởi Cashu và NIP-87", "footer.madeBy": "Được tạo bằng ❤️ bởi {azzamo} · Vận hành bởi Cashu và NIP-87",
"footer.warning": "mint là bảo quản. Một cục mint có thể biến mất với tiền của anh.", "footer.warning": "Mint giữ tiền hộ bạn. Một mint có thể biến mất cùng với tiền của bạn.",
"home.title": "Cashumint. space # Hãy tìm một cây mint Cashu bạn có thể tin tưởng", "footer.tagline": "Đánh giá từ cộng đồng cho các mint ecash, được ký trên Nostr.",
"home.description": "Tìm một cây mint Cashu mà anh có thể tin tưởng. {mints} loại mint được liệt kê với {reviews} bài phê bình cộng đồng được ký trên Nostr, cộng thêm việc sống đúng giờ, ủng hộ NUTs và phần mềm mint.", "footer.col.explore": "Khám phá",
"home.hero.title": "Tìm một cây mint", "footer.col.site": "Trang",
"footer.nav": "Chân trang",
"home.title": "Cashumints.space · Tìm một mint Cashu bạn có thể tin tưởng",
"home.description": "Tìm một mint Cashu bạn có thể tin tưởng. {mints} mint được lập chỉ mục với {reviews} đánh giá từ cộng đồng được ký trên Nostr, cùng thời gian hoạt động trực tiếp, các NUTs được hỗ trợ và phần mềm mint.",
"home.hero.title": "Tìm một mint Cashu",
"home.hero.titleEm": "Cậu có thể tin tưởng.", "home.hero.titleEm": "Cậu có thể tin tưởng.",
"home.hero.lede": "Mọi loại mint trên mạng Nostr, với đánh giá cộng đồng thực sự. Không có người gác cổng, chỉ cần ký giấy phê bình anh có thể chứng minh bản thân.", "home.hero.lede": "Mọi loại mint trên mạng Nostr, với đánh giá cộng đồng thực sự. Không có người gác cổng, chỉ cần ký giấy phê bình anh có thể chứng minh bản thân.",
"home.search.label": "Tìm kiếm mint bằng tên, miền hoặc URL", "home.search.label": "Tìm kiếm mint bằng tên, miền hoặc URL",
@@ -77,70 +81,72 @@
"home.why.verify.title": "Tự kiểm tra đi.", "home.why.verify.title": "Tự kiểm tra đi.",
"home.why.verify.body": "Các bản ôn lại trực tiếp trên các bản chuyển tiếp công cộng, không phải cơ sở dữ liệu. Chỉ vào bất cứ khách hàng nào ở cùng một rơle và bạn sẽ thấy cùng dữ liệu.", "home.why.verify.body": "Các bản ôn lại trực tiếp trên các bản chuyển tiếp công cộng, không phải cơ sở dữ liệu. Chỉ vào bất cứ khách hàng nào ở cùng một rơle và bạn sẽ thấy cùng dữ liệu.",
"home.why.checked.title": "Đã kiểm tra liên tục", "home.why.checked.title": "Đã kiểm tra liên tục",
"home.why.checked.body": "Chúng tôi thăm dò tất cả các loại kẹo mint trong thời gian đầu, phiên bản phần mềm, giới hạn, và hỗ trợ NUT, vì vậy các số liệu bạn thấy là hiện tại, chứ không phải là tự xuất bản.", "home.why.checked.body": "Chúng tôi thăm dò từng mint để biết thời gian hoạt động, phiên bản phần mềm, giới hạn và các NUTs được hỗ trợ, nên thông số bạn thấy là hiện tại, không phải do mint tự khai.",
"home.cta.title": "Nghĩ về kẹo mint? Đọc trước đi.", "home.cta.title": "Đang cân nhắc một mint? Đọc trước đã.",
"home.cta.body": "Kẹo mint giữ cho anh. Hãy xem những gì người khác tìm thấy trước khi bạn đặt ghế, và để lại lời phê bình khi chính bạn đã dùng.", "home.cta.body": "Kẹo mint giữ cho anh. Hãy xem những gì người khác tìm thấy trước khi bạn đặt ghế, và để lại lời phê bình khi chính bạn đã dùng.",
"home.cta.explore": "Khám phá mọi loại mint", "home.cta.explore": "Khám phá mọi loại mint",
"home.cta.learn": "Học về Cashu", "home.cta.learn": "Học về Cashu",
"pulse.mintsIndexed": "chỉ mục mint", "pulse.mintsIndexed": "chỉ mục mint",
"pulse.reviewsOnNostr": "Comment", "pulse.reviewsOnNostr": "đánh giá trên Nostr",
"pulse.lastReview": "Xem xét cuối", "pulse.lastReview": "đánh giá gần nhất",
"pulse.mintsOnline": "mint online", "pulse.mintsOnline": "mint online",
"pulse.indexer": "chỉ mục", "pulse.indexer": "chỉ mục",
"pulse.connected": "đã kết nối", "pulse.connected": "đã kết nối",
"pulse.stale": "cũ", "pulse.stale": "cũ",
"mints.title": "Tất cả đều là kẹo mint. không gian", "mints.title": "Tất cả mint Cashu · Cashumints.space",
"mints.description": "Tất cả {total} Tiền bạc được chỉ định từ mạng Nostr, {online} online và {offline} phút tắt máy, với tỷ lệ cộng đồng, thời gian tăng và phần mềm mint. Tìm kiếm và sắp xếp danh sách đầy đủ.", "mints.description": "Tất cả {total} mint Cashu được lập chỉ mục từ mạng Nostr, {online} online và {offline} ngoại tuyến, cùng đánh giá từ cộng đồng, thời gian hoạt động và phần mềm mint. Tìm kiếm và sắp xếp toàn bộ danh sách.",
"mints.heading": "Tất cả mint Cashu", "mints.heading": "Tất cả mint Cashu",
"mints.lede": "Mọi loại mint được phát hiện trên mạng Nostr, kể cả loại đã mất. Kẹo mint được liệt kê để bạn vẫn có thể tìm và xem xét chúng.", "mints.lede": "Mọi mint được tìm thấy trên mạng Nostr, kể cả những mint đã biến mất. Mint ngoại tuyến vẫn nằm trong danh sách để bạn còn tìm và đánh giá được.",
"mints.search.label": "Tìm kiếm mint theo tên hay miền", "mints.search.label": "Tìm kiếm mint theo tên hay miền",
"mints.search.placeholder": "Tìm kiếm theo tên hay miền...", "mints.search.placeholder": "Tìm kiếm theo tên hay miền...",
"mints.sort.label": "Sắp xếp mint", "mints.sort.label": "Sắp xếp mint",
"mints.sort.score": "Tổng quát", "mints.sort.score": "Tốt nhất toàn diện",
"mints.sort.rating": "Cao nhất", "mints.sort.rating": "Điểm cao nhất",
"mints.sort.reviews": "Comment", "mints.sort.reviews": "Nhiều đánh giá nhất",
"mints.sort.recent": "Mới xem lại", "mints.sort.recent": "Vừa được đánh giá",
"mints.sort.seen": "Gần đây được xem trực tuyến", "mints.sort.seen": "Gần đây được xem trực tuyến",
"mints.hideOffline": "Ẩn ngoài cửa sổ", "mints.hideOffline": "Ẩn mint ngoại tuyến",
"mints.showing": "Hiển thị {total} mint ({online} online, {offline} ngoại tuyến)", "mints.showing": "Hiển thị {total} mint ({online} online, {offline} ngoại tuyến)",
"mints.showingFiltered": "Hiện {shown} của {total} mint ({online} trực tuyến, {offline} offline)", "mints.showingFiltered": "Hiện {shown} của {total} mint ({online} trực tuyến, {offline} offline)",
"mints.noResults": "Không có kẹo mint khớp. Thử một miền, hoặc dán một địa chỉ URL mint.", "mints.noResults": "Không có mint nào khớp. Thử một tên miền, hoặc dán một URL mint.",
"mints.clearSearch": "Xoá tìm kiếm", "mints.clearSearch": "Xoá tìm kiếm",
"card.noRatings": "Chưa đánh giá", "card.noRatings": "Chưa đánh giá",
"card.reviews.one": "{n} ôn lại", "card.reviews.one": "{n} đánh giá",
"card.reviews.other": "{n} bài phê bình", "card.reviews.other": "{n} đánh giá",
"card.lastSeen": "Lần cuối thấy {when}", "card.lastSeen": "Lần cuối thấy {when}",
"card.neverSeen": "chưa bao giờ thấy trực tuyến", "card.neverSeen": "chưa bao giờ thấy trực tuyến",
"card.reviewed": "Xem lại {when}", "card.reviewed": "đánh giá {when}",
"card.noReviews": "chưa xem lại", "card.noReviews": "chưa có đánh giá",
"mint.title": "{name} · Cashu bình luận mint", "mint.title": "{name} · đánh giá mint Cashu",
"mint.breadcrumb": "Mảnh bánh mì", "mint.titleFedimint": "{name} · đánh giá liên đoàn Fedimint",
"mint.crumbMints": "mint", "mint.titleLnurl": "{name} · Đánh giá mint LNURL",
"mint.breadcrumb": "Đường dẫn",
"mint.crumbMints": "Mint Cashu",
"mint.copyUrl": "Chép URL mint", "mint.copyUrl": "Chép URL mint",
"mint.qrButton": "Mã QR của mint", "mint.qrButton": "Mã QR của mint",
"mint.writeReview": "Viết bài phê bình", "mint.writeReview": "Viết đánh giá",
"mint.rankByScore": "#{rank} theo số", "mint.rankByScore": "#{rank} theo điểm",
"mint.tagLightning": "Ánh sáng", "mint.tagLightning": "Lightning",
"mint.summary": "Bản tóm tắt", "mint.summary": "Tóm tắt mint",
"mint.rating.label": "đánh giá công cộng", "mint.rating.label": "Đánh giá của cộng đồng",
"mint.rating.none": "Chưa đánh giá", "mint.rating.none": "Chưa đánh giá",
"mint.rating.reviews.one": "{n} ôn lại", "mint.rating.reviews.one": "{n} đánh giá",
"mint.rating.reviews.other": "{n} bài phê bình", "mint.rating.reviews.other": "{n} đánh giá",
"mint.rating.unrated.one": "{n} không đánh giá", "mint.rating.unrated.one": "{n} không đánh giá",
"mint.rating.unrated.other": "{n} không đánh giá", "mint.rating.unrated.other": "{n} không đánh giá",
"mint.activity.label": "Hoạt động", "mint.activity.label": "Hoạt động",
"mint.activity.since": "Từ lần đánh giá cuối", "mint.activity.since": "Từ lần đánh giá cuối",
"mint.activity.noReviews": "chưa xem lại", "mint.activity.noReviews": "chưa có đánh giá",
"mint.activity.recent.one": "{n} ôn lại trong 90 ngày qua", "mint.activity.recent.one": "{n} đánh giá trong 90 ngày qua",
"mint.activity.recent.other": "{n} bài phê bình trong 90 ngày qua", "mint.activity.recent.other": "{n} đánh giá trong 90 ngày qua",
"mint.limits.label": "Giới hạn", "mint.limits.label": "Giới hạn",
"mint.limits.notPublished": "chưa xuất bản", "mint.limits.notPublished": "chưa xuất bản",
"mint.limits.min": "phút", "mint.limits.min": "min",
"mint.limits.max": "cực đại", "mint.limits.max": "max",
"mint.limits.mintRow": "mint", "mint.limits.mintRow": "mint",
"mint.limits.meltRow": "Chảy", "mint.limits.meltRow": "melt",
"mint.limits.disabled": "bị tắt", "mint.limits.disabled": "bị tắt",
"mint.limits.range": "{unit} {min} đến {max} 9182743", "mint.limits.range": "{min} đến {max} {unit}",
"mint.limits.upTo": "lên tới {max} {unit}", "mint.limits.upTo": "lên tới {max} {unit}",
"mint.limits.from": "Từ {min} {unit}", "mint.limits.from": "Từ {min} {unit}",
"mint.software.label": "Phần mềm Mint", "mint.software.label": "Phần mềm Mint",
@@ -148,41 +154,41 @@
"mint.checked": "đã kiểm tra {when}", "mint.checked": "đã kiểm tra {when}",
"mint.lastSeen": "Lần cuối thấy {when}", "mint.lastSeen": "Lần cuối thấy {when}",
"mint.neverReached": "chưa bao giờ tới", "mint.neverReached": "chưa bao giờ tới",
"mint.uptime.checks": "{ok}41 trong {total} lần kiểm tra cuối cùng", "mint.uptime.checks": "{ok} trong {total} lần kiểm tra gần nhất kết nối được",
"mint.uptime.percent": "{percent}% có thể đạt được trong 30 ngày", "mint.uptime.percent": "kết nối được {percent}% trong 30 ngày",
"mint.uptime.description": "{ok} trong {total} ngân phiếu đã thành công", "mint.uptime.description": "{ok} trong {total} lần kiểm tra gần nhất thành công",
"mint.operator.title": "Từ tổng đài", "mint.operator.title": "Từ người vận hành",
"mint.operator.motd": "Thông điệp của ngày", "mint.operator.motd": "Thông điệp của ngày",
"mint.operator.beta": "Loại mint này tự mô tả như là thử nghiệm hoặc trong thử nghiệm. Hãy đọc lời của người điều hành trước khi lưu trữ những gì bạn không thể mất.", "mint.operator.beta": "Loại mint này tự mô tả như là thử nghiệm hoặc trong thử nghiệm. Hãy đọc lời của người điều hành trước khi lưu trữ những gì bạn không thể mất.",
"mint.nuts.title": "Comment", "mint.nuts.title": "NUTs được hỗ trợ",
"mint.nuts.noneOffline": "Không có danh sách NUT lưu trữ cho loại mint này, nó đã bị tắt trước khi có một loại được ghi nhận.", "mint.nuts.noneOffline": "Không có danh sách NUT lưu trữ cho loại mint này, nó đã bị tắt trước khi có một loại được ghi nhận.",
"mint.nuts.noneYet": "Chưa có danh sách NUT lưu trữ cho loại mint này.", "mint.nuts.noneYet": "Chưa có danh sách NUT nào được lưu cho mint này.",
"mint.nuts.supported": "{n} của {total} được tài trợ", "mint.nuts.supported": "{n} trong {total} NUTs đã biết được hỗ trợ",
"mint.nuts.whatAre": "NUTs là gì?", "mint.nuts.whatAre": "NUTs là gì?",
"mint.nutRow.disabled": "bị tắt", "mint.nutRow.disabled": "bị tắt",
"mint.nutRow.notSupported": "không hỗ trợ", "mint.nutRow.notSupported": "không hỗ trợ",
"mint.details.title": "Chi tiết Bóng loáng", "mint.details.title": "Chi tiết mint",
"mint.details.pubkey": "Pbkey", "mint.details.pubkey": "Pubkey",
"mint.details.nostr": "mũi", "mint.details.nostr": "Nostr",
"mint.details.email": "Email", "mint.details.email": "Email",
"mint.details.serverTime": "Thời gian máy phục vụ", "mint.details.serverTime": "Thời gian máy phục vụ",
"mint.details.firstSeen": "Phát hiện đầu tiên", "mint.details.firstSeen": "Phát hiện đầu tiên",
"mint.details.terms": "Kì", "mint.details.terms": "Điều khoản",
"mint.kind.identifier": "Bộ nhận diện", "mint.kind.identifier": "Bộ nhận diện",
"mint.kind.mintPubkey": "đồng hồ mint", "mint.kind.mintPubkey": "mint pubkey",
"mint.kind.nostrAddress": "Địa chỉ mũi tên", "mint.kind.nostrAddress": "Địa chỉ Nostr",
"mint.kind.emailAddress": "Địa chỉ thư", "mint.kind.emailAddress": "địa chỉ email",
"mint.kind.npub": "npub", "mint.kind.npub": "npub",
"mint.disclaimer": "Loại mint này có tính bảo quản: nó giữ tiền của bạn, và nó có thể dừng lại bất cứ lúc nào. Đánh giá là ý kiến cộng đồng và có nghĩa là có thể truy cập trực tuyến, không đáng tin cậy.", "mint.disclaimer": "Loại mint này có tính bảo quản: nó giữ tiền của bạn, và nó có thể dừng lại bất cứ lúc nào. Đánh giá là ý kiến cộng đồng và có nghĩa là có thể truy cập trực tuyến, không đáng tin cậy.",
"mint.disclaimerLink": "Đọc toàn bộ lời tuyên bố", "mint.disclaimerLink": "Đọc toàn bộ tuyên bố miễn trừ",
"mint.qr.title": "Thêm {name} vào ví", "mint.qr.title": "Thêm {name} vào ví",
"mint.qr.note": "Quét này với ví Cashu để thêm mint, hoặc sao chép địa chỉ URL và dán nó vào.", "mint.qr.note": "Quét này với ví Cashu để thêm mint, hoặc sao chép địa chỉ URL và dán nó vào.",
"mint.qr.warn": "Thêm một cục mint có nghĩa là tin nó bằng tiền của bạn.", "mint.qr.warn": "Thêm một mint nghĩa là tin tưởng giao tiền của bạn cho nó.",
"mint.qr.whatMeans": "Nghĩa là sao?", "mint.qr.whatMeans": "Nghĩa là sao?",
"mint.qr.wallets": "Name", "mint.qr.wallets": "Ví hỗ trợ Cashu",
"mint.chip.frozen": "Đóng băng", "mint.chip.frozen": "Đóng băng",
"mint.chip.noWithdrawals": "Không rút lui", "mint.chip.noWithdrawals": "Không rút được",
"mint.chip.meltOnly": "Chảy Chỉ", "mint.chip.meltOnly": "Chỉ melt",
"mint.warnings.dayCount.one": "{n} ngày", "mint.warnings.dayCount.one": "{n} ngày",
"mint.warnings.dayCount.other": "{n} ngày", "mint.warnings.dayCount.other": "{n} ngày",
"mint.warnings.gone.lead": "Có thể mất rồi.", "mint.warnings.gone.lead": "Có thể mất rồi.",
@@ -192,36 +198,36 @@
"mint.warnings.gone.meta.since": "Ngoại tuyến từ {date}", "mint.warnings.gone.meta.since": "Ngoại tuyến từ {date}",
"mint.warnings.gone.meta.never": "Không bao giờ lấy được kể từ khi phát hiện ra", "mint.warnings.gone.meta.never": "Không bao giờ lấy được kể từ khi phát hiện ra",
"mint.warnings.meltDisabled.lead": "Đã rút lui.", "mint.warnings.meltDisabled.lead": "Đã rút lui.",
"mint.warnings.meltDisabled.body.online": "Loại mint này có khả năng làm tan chảy tia Lightning (NUT-05) tắt: bạn có thể đặt vệ tinh vào, nhưng bạn không thể rút chúng qua tia Lightning. Đừng gửi tiền cho đến khi tan chảy trở lại.", "mint.warnings.meltDisabled.body.online": "Mint này đã tắt melting qua Lightning (NUT-05): bạn có thể nạp sats vào, nhưng không thể rút ra qua Lightning. Đừng nạp vào cho đến khi melting hoạt động trở lại.",
"mint.warnings.meltDisabled.body.offline": "Loại mint này có sự tan chảy tia Lightning (NUT-05) tắt: bạn có thể đặt ngồi vào, nhưng bạn không thể rút chúng qua tia Lightning. Đừng gửi tiền cho đến khi tan chảy trở lại.", "mint.warnings.meltDisabled.body.offline": "Mint này đã tắt melting qua Lightning (NUT-05): bạn có thể nạp sats vào, nhưng không thể rút ra qua Lightning. Đừng nạp vào cho đến khi melting hoạt động trở lại.",
"mint.warnings.meltDisabled.meta.online": "Comment", "mint.warnings.meltDisabled.meta.online": "Comment",
"mint.warnings.meltDisabled.meta.offline": "Rút lui bị tắt khi nhìn lần cuối", "mint.warnings.meltDisabled.meta.offline": "Rút lui bị tắt khi nhìn lần cuối",
"mint.warnings.frozen.lead": "Đóng băng rồi.", "mint.warnings.frozen.lead": "Mint bị đóng băng.",
"mint.warnings.frozen.body.online": "Cả việc đúc và nấu chảy đều bị vô hiệu hóa. Không vệ tinh nào có thể di chuyển qua tia Lightning.", "mint.warnings.frozen.body.online": "Cả minting và melting đều bị tắt. Không sats nào có thể vào hoặc ra qua Lightning.",
"mint.warnings.frozen.body.offline": "Cả việc đúc và nấu chảy đều bị vô hiệu hóa. Không vệ tinh nào có thể di chuyển qua tia Lightning.", "mint.warnings.frozen.body.offline": "Cả minting và melting đều đã bị tắt. Không sats nào có thể vào hoặc ra qua Lightning.",
"mint.warnings.frozen.meta.online": "Đang đóng băng, không có gì di chuyển vào hoặc ra", "mint.warnings.frozen.meta.online": "Đang đóng băng, không có gì di chuyển vào hoặc ra",
"mint.warnings.frozen.meta.offline": "♪ Băng giá khi nhìn lần cuối ♪", "mint.warnings.frozen.meta.offline": "♪ Băng giá khi nhìn lần cuối ♪",
"mint.warnings.offlineLong.lead": "Đường dây ngoài cho {days}, từ {date}.", "mint.warnings.offlineLong.lead": "Đường dây ngoài cho {days}, từ {date}.",
"mint.warnings.offlineLong.body": "Kẹo mint có thể biến mất. Mọi thứ bên dưới là trạng thái được biết đến cuối cùng, và đánh giá vẫn còn hiệu quả.", "mint.warnings.offlineLong.body": "Kẹo mint có thể biến mất. Mọi thứ bên dưới là trạng thái được biết đến cuối cùng, và đánh giá vẫn còn hiệu quả.",
"mint.warnings.offlineLong.meta": "Ngoại tuyến từ {date}", "mint.warnings.offlineLong.meta": "Ngoại tuyến từ {date}",
"mint.warnings.meltOnly.lead": "Chỉ chảy ra thôi.", "mint.warnings.meltOnly.lead": "Chỉ chảy ra thôi.",
"mint.warnings.meltOnly.body.online": "Việc đúc kết mới (NUT-04) bị tắt: bạn không thể gửi, nhưng vẫn có thể rút ra. Điều này thường có nghĩa là một cây mint đang cạn dần, vì vậy nếu bạn cầm nó, hãy xem xét việc tan chảy.", "mint.warnings.meltOnly.body.online": "Việc minting mới (NUT-04) đang bị tắt: bạn không thể gửi vào, nhưng ecash hiện có vẫn rút ra được. Điều này thường có nghĩa là một mint đang ngừng hoạt động, nên nếu bạn đang giữ ecash của nó, hãy cân nhắc melt ra.",
"mint.warnings.meltOnly.body.offline": "Việc đúc kết mới (NUT-04) bị tắt: bạn không thể gửi tiền, nhưng vẫn có thể rút ra. Điều này thường có nghĩa là một cây mint đang cạn dần, vì vậy nếu bạn cầm nó, hãy xem xét việc tan chảy.", "mint.warnings.meltOnly.body.offline": "Việc minting mới (NUT-04) đã bị tắt: bạn không thể gửi vào, nhưng ecash hiện có vẫn rút ra được. Điều này thường có nghĩa là một mint đang ngừng hoạt động, nên nếu bạn đang giữ ecash của nó, hãy cân nhắc melt ra.",
"mint.warnings.meltOnly.meta.online": "Hiện tại chỉ làm tan chảy", "mint.warnings.meltOnly.meta.online": "Hiện tại chỉ làm tan chảy",
"mint.warnings.meltOnly.meta.offline": "Chảy Chỉ khi nhìn lần cuối", "mint.warnings.meltOnly.meta.offline": "Chỉ melt khi được thấy lần cuối",
"mint.warnings.offline.lead": "Ngoại tuyến từ {date}.", "mint.warnings.offline.lead": "Ngoại tuyến từ {date}.",
"mint.warnings.offline.body": "Chi tiết được xác nhận lần cuối. Anh vẫn có thể viết bài phê bình.", "mint.warnings.offline.body": "Chi tiết được xác nhận lần cuối. Anh vẫn có thể viết bài phê bình.",
"mint.warnings.offline.meta": "Ngoại tuyến từ {date}", "mint.warnings.offline.meta": "Ngoại tuyến từ {date}",
"mint.warnings.also.meltDisabled": "Trước khi tắt nó cũng đã vô hiệu hóa việc rút tiền.", "mint.warnings.also.meltDisabled": "Trước khi tắt nó cũng đã vô hiệu hóa việc rút tiền.",
"mint.warnings.also.mintDisabled": "Trước khi tắt nó cũng đã làm hỏng việc đúc kết mới.", "mint.warnings.also.mintDisabled": "Trước khi offline, nó cũng đã tắt việc minting mới.",
"mint.warnings.also.offline": "{days} Nó cũng ngưng hoạt động từ {date} (9182742).", "mint.warnings.also.offline": "{days} Nó cũng ngưng hoạt động từ {date} (9182742).",
"nut.0": "Ghi chú và mô hình", "nut.0": "Ghi chú và mô hình",
"nut.1": "Trao đổi phím trước công chúng mint", "nut.1": "Trao đổi phím trước công chúng mint",
"nut.2": "Chìa khóa và phí tổn", "nut.2": "Chìa khóa và phí tổn",
"nut.3": "Đang trao đổi hiệu bài", "nut.3": "Đang trao đổi hiệu bài",
"nut.4": "Khai thác bằng Lightning", "nut.4": "Minting qua Lightning",
"nut.5": "Chảy qua Lightning", "nut.5": "Melting qua Lightning",
"nut.6": "Thông tin tiểu dụng", "nut.6": "Thông tin mint",
"nut.7": "Kiểm tra tình trạng Token", "nut.7": "Kiểm tra tình trạng Token",
"nut.8": "Phí ánh sáng quá cao", "nut.8": "Phí ánh sáng quá cao",
"nut.9": "Phục hồi chữ ký", "nut.9": "Phục hồi chữ ký",
@@ -261,6 +267,9 @@
"reviews.empty": "Vẫn chưa có đánh giá. Là người đầu tiên viết ra.", "reviews.empty": "Vẫn chưa có đánh giá. Là người đầu tiên viết ra.",
"reviews.emptyWritten": "Chưa có bài phê bình nào ở đây. Là người đầu tiên viết ra.", "reviews.emptyWritten": "Chưa có bài phê bình nào ở đây. Là người đầu tiên viết ra.",
"reviews.emptyFilter": "Không có đánh giá khớp với bộ lọc này.", "reviews.emptyFilter": "Không có đánh giá khớp với bộ lọc này.",
"reviews.rate.cashu": "Đánh giá mint này",
"reviews.rate.fedimint": "Đánh giá liên minh này",
"reviews.rate.lnurl": "Đánh giá mint này",
"reviews.noscript": "Các bản ôn lại đã được ký trên Nostr và tải lên trình duyệt của bạn, vì vậy danh sách này cần JavaScript. Điểm đánh giá và điểm số ở trên đến từ chính trang web.", "reviews.noscript": "Các bản ôn lại đã được ký trên Nostr và tải lên trình duyệt của bạn, vì vậy danh sách này cần JavaScript. Điểm đánh giá và điểm số ở trên đến từ chính trang web.",
"reviews.relayError": "Không thể tới phần tiếp theo. Xem lại có thể chưa đầy đủ.", "reviews.relayError": "Không thể tới phần tiếp theo. Xem lại có thể chưa đầy đủ.",
"reviews.srRating": "{rating} 9182741 trong số 5", "reviews.srRating": "{rating} 9182741 trong số 5",
@@ -273,7 +282,7 @@
"reviews.anonNote": "Xem lại lần đầu từ npb này, không tìm thấy hoạt động nào khác", "reviews.anonNote": "Xem lại lần đầu từ npb này, không tìm thấy hoạt động nào khác",
"reviews.published": "Xuất bản.", "reviews.published": "Xuất bản.",
"reviews.publishedPartial": "Do {accepted} của {total} rơle.", "reviews.publishedPartial": "Do {accepted} của {total} rơle.",
"reviews.propagating": "Tuyên truyền, có thể phải mất một lúc mới xuất hiện.", "reviews.propagating": "Đang đăng lên các relay Nostr",
"reviews.summary.line.one": "{count} đánh giá mà không bình luận: {breakdown}", "reviews.summary.line.one": "{count} đánh giá mà không bình luận: {breakdown}",
"reviews.summary.line.other": "{breakdown} {count} đánh giá mà không bình luận: 918274242", "reviews.summary.line.other": "{breakdown} {count} đánh giá mà không bình luận: 918274242",
"reviews.summary.lineDay.one": "{count} đánh giá mà không bình luận gì về {when}: {breakdown}", "reviews.summary.lineDay.one": "{count} đánh giá mà không bình luận gì về {when}: {breakdown}",
@@ -287,7 +296,12 @@
"reviews.breakdown.1": "{n} Một ngôi sao", "reviews.breakdown.1": "{n} Một ngôi sao",
"reviews.breakdown.none": "{n} mà không đánh giá", "reviews.breakdown.none": "{n} mà không đánh giá",
"reviews.dialog.title": "Xem lại {name}", "reviews.dialog.title": "Xem lại {name}",
"reviews.dialog.note": "Ký với chìa khóa mũi và được xuất bản để tiếp âm công cộng. Không thể sửa hay xoá nó.", "reviews.dialog.lede.cashu": "Chia sẻ trải nghiệm của bạn với mint này.",
"reviews.dialog.lede.fedimint": "Chia sẻ trải nghiệm của bạn với liên minh này.",
"reviews.dialog.lede.lnurl": "Chia sẻ trải nghiệm của bạn với mint này.",
"reviews.dialog.permanent": "Được đăng vĩnh viễn lên các relay Nostr công khai.",
"reviews.dialog.gate.title": "Đăng nhập để viết bài đánh giá",
"reviews.dialog.gate.lead": "Bài đánh giá được ký bằng khoá Nostr của bạn. Khoá luôn nằm trong trình ký, trang web này không bao giờ thấy nó.",
"reviews.dialog.ratingLegend": "Đánh giá của bạn", "reviews.dialog.ratingLegend": "Đánh giá của bạn",
"reviews.dialog.starsSr.one": "{n} sao", "reviews.dialog.starsSr.one": "{n} sao",
"reviews.dialog.starsSr.other": "{n} sao", "reviews.dialog.starsSr.other": "{n} sao",
@@ -308,7 +322,6 @@
"reviews.dialog.waitingSigner": "Đang đợi người ký", "reviews.dialog.waitingSigner": "Đang đợi người ký",
"reviews.dialog.confirmClose": "Đóng bài phê bình này? Bản nháp của anh được giữ lại cho đến khi anh rời khỏi trang web.", "reviews.dialog.confirmClose": "Đóng bài phê bình này? Bản nháp của anh được giữ lại cho đến khi anh rời khỏi trang web.",
"reviews.dialog.why.login": "Đăng nhập để xuất bản", "reviews.dialog.why.login": "Đăng nhập để xuất bản",
"reviews.dialog.why.rating": "Chọn đánh giá",
"reviews.dialog.why.words": "Viết vài từ", "reviews.dialog.why.words": "Viết vài từ",
"reviews.dialog.why.more.one": "{n} nhân vật", "reviews.dialog.why.more.one": "{n} nhân vật",
"reviews.dialog.why.more.other": "{n} ký tự", "reviews.dialog.why.more.other": "{n} ký tự",
@@ -324,8 +337,8 @@
"reviews.validate.rating": "Chọn đánh giá từ 1 đến 5 sao.", "reviews.validate.rating": "Chọn đánh giá từ 1 đến 5 sao.",
"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": "Đánh giá mint và liên đoàn mới nhất · Cashumints.space",
"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",
@@ -369,6 +382,10 @@
"login.status.waitingSigner": "Đang đợi người ký", "login.status.waitingSigner": "Đang đợi người ký",
"login.status.connecting": "Kết nối với người ký của bạn", "login.status.connecting": "Kết nối với người ký của bạn",
"login.status.connected": "Đã kết nối", "login.status.connected": "Đã kết nối",
"login.toast.loggedIn": "Đã đăng nhập",
"login.toast.fetchingProfile": "Đang lấy hồ sơ Nostr của bạn từ các rơle…",
"login.toast.profile": "Hồ sơ: {name}",
"login.toast.noProfile": "Không tìm thấy hồ sơ trên các rơle",
"login.method.extension": "Mặt trời", "login.method.extension": "Mặt trời",
"login.method.remote": "Từ xa", "login.method.remote": "Từ xa",
"login.method.primal": "Primal", "login.method.primal": "Primal",
@@ -389,8 +406,8 @@
"signer.error.timeout": "Người ký của anh không trả lời đúng lúc. Mở ra, kiểm tra nó vẫn còn kết nối, sau đó thử lại.", "signer.error.timeout": "Người ký của anh không trả lời đúng lúc. Mở ra, kiểm tra nó vẫn còn kết nối, sau đó thử lại.",
"signer.error.cancelled": "Kết nối bị hủy bỏ.", "signer.error.cancelled": "Kết nối bị hủy bỏ.",
"signer.error.declined": "Yêu cầu chữ ký bị từ chối", "signer.error.declined": "Yêu cầu chữ ký bị từ chối",
"notfound.title": "Không tìm thấy dấu ngoặc. không gian", "notfound.title": "Không tìm thấy trang · Cashumints.space",
"notfound.description": "Trang đó không có trên Cashumins. không gian.", "notfound.description": "Trang đó không tồn tại trên Cashumints.space.",
"notfound.heading": "Trang này không tồn tại", "notfound.heading": "Trang này không tồn tại",
"notfound.lede": "Nó có thể đã chuyển đi, hoặc có thể chưa có chỉ số. Tìm kiếm tên hoặc miền mint, hoặc dán một địa chỉ URL mint.", "notfound.lede": "Nó có thể đã chuyển đi, hoặc có thể chưa có chỉ số. Tìm kiếm tên hoặc miền mint, hoặc dán một địa chỉ URL mint.",
"notfound.browse": "Duyệt tất cả các mint", "notfound.browse": "Duyệt tất cả các mint",
@@ -400,12 +417,19 @@
"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.",
"wallets.title": "Tiền trong ví. không gian", "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": "Ví Cashu · Cashumints.space",
"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",
"wallets.lede": "Anh cần một cái ví để giữ tiền trong đống mint này. Hầu hết các ví tiền nói chuyện với một số mint cùng một lúc, vì vậy bạn không bị trói buộc vào cái đầu tiên bạn thử.", "wallets.lede": "Anh cần một cái ví để giữ tiền trong đống mint này. Hầu hết các ví tiền nói chuyện với một số mint cùng một lúc, vì vậy bạn không bị trói buộc vào cái đầu tiên bạn thử.",
"wallets.note": "Danh sách này không phải là một sự tán thành. Dự án Cashu có chỉ số rộng hơn vào {link}. Một cái ví cầm chìa khóa, một cây mint giữ tiền: hãy đọc những bài phê bình mint trước khi chọn chỗ ngồi.", "wallets.note": "Danh sách này chưa đầy đủ và không phải là một sự chứng thực. Dự án Cashu có một danh mục rộng hơn tại {link}. Ví giữ khóa, mint giữ tiền: hãy đọc các đánh giá về mint trước khi chọn nơi cất sats của bạn.",
"wallets.platform.android": "Ando", "wallets.platform.android": "Ando",
"wallets.platform.ios": "iOS", "wallets.platform.ios": "iOS",
"wallets.platform.web": "Mạng", "wallets.platform.web": "Mạng",
@@ -418,10 +442,10 @@
"wallets.item.cashume": "Ví có tham khảo từ dự án Cashu. Chạy hoàn toàn trong trình duyệt, có thể cài đặt như là PWA.", "wallets.item.cashume": "Ví có tham khảo từ dự án Cashu. Chạy hoàn toàn trong trình duyệt, có thể cài đặt như là PWA.",
"wallets.item.boardwalk": "Ví tiền được xây dựng xung quanh địa chỉ Tia Lightning và gửi ecash bằng đường dẫn.", "wallets.item.boardwalk": "Ví tiền được xây dựng xung quanh địa chỉ Tia Lightning và gửi ecash bằng đường dẫn.",
"wallets.item.coinos": "Ví Lightning nhoáng và điểm bán cũng có mint Cashu riêng.", "wallets.item.coinos": "Ví Lightning nhoáng và điểm bán cũng có mint Cashu riêng.",
"wallets.item.oxchat": "Người đưa tin với một cái ví tiền được xây dựng, để gửi tin nhắn vệ tinh vào trong.", "wallets.item.oxchat": "Messenger Nostr có sẵn ví ecash, để gửi sats ngay trong cuộc trò chuyện.",
"wallets.item.nutshell": "Việc thực hiện tham khảo. Chạy như ví trên dòng lệnh và phần mềm mint.", "wallets.item.nutshell": "Việc thực hiện tham khảo. Chạy như ví trên dòng lệnh và phần mềm mint.",
"about.title": "Về Cashumins. không gian", "about.title": "Giới thiệu về Cashumints.space",
"about.description": "Cashu là gì, một cây mint thực sự chứa gì, NIP-87 là gì, và làm thế nào địa điểm này chỉ mục mint và đọc bài phê bình từ các rơ-le Nostr.", "about.description": "Cashu là gì, một mint thực sự giữ gì, NIP-87 là gì, và trang này lập chỉ mục mint cũng như đọc đánh giá từ các relay Nostr ra sao.",
"about.heading": "Giới thiệu", "about.heading": "Giới thiệu",
"about.lede": "Cashumints.space liệt kê tất cả các loại mint mà chúng ta có thể tìm thấy trên mạng Nostr và cho bạn thấy những gì mọi người nói về họ. Nó được làm bởi Azzamo.", "about.lede": "Cashumints.space liệt kê tất cả các loại mint mà chúng ta có thể tìm thấy trên mạng Nostr và cho bạn thấy những gì mọi người nói về họ. Nó được làm bởi Azzamo.",
"about.cashu.title": "Cashu là gì?", "about.cashu.title": "Cashu là gì?",
@@ -431,7 +455,7 @@
"about.mints.p1": "Kẹo mint là bảo quản. Khi bạn đang đưa cho bất cứ ai điều hành máy chủ đó, và bạn tin tưởng họ sẽ tôn trọng những biểu tượng khi bạn làm tan chảy chúng. Không có bằng chứng gian lận, không có thử thách và không có giải pháp. Một người vận hành mint có thể biến mất với sự cân bằng, và mint thì có.", "about.mints.p1": "Kẹo mint là bảo quản. Khi bạn đang đưa cho bất cứ ai điều hành máy chủ đó, và bạn tin tưởng họ sẽ tôn trọng những biểu tượng khi bạn làm tan chảy chúng. Không có bằng chứng gian lận, không có thử thách và không có giải pháp. Một người vận hành mint có thể biến mất với sự cân bằng, và mint thì có.",
"about.mints.p2": "Đó chính là lý do tại sao có những bài phê bình ở đây. Trang này không bao giờ cho bạn biết một mint là an toàn. Tình trạng trên trang mint ghi {code}, không phải \"tin cậy\", và nó chỉ có nghĩa là máy chủ trả lời khi chúng tôi hỏi lần cuối. Hãy đối xử với sự cân bằng thực phẩm như tiền mặt trong túi áo khoác: trả tiền mặt, tiền tiết kiệm là sai.", "about.mints.p2": "Đó chính là lý do tại sao có những bài phê bình ở đây. Trang này không bao giờ cho bạn biết một mint là an toàn. Tình trạng trên trang mint ghi {code}, không phải \"tin cậy\", và nó chỉ có nghĩa là máy chủ trả lời khi chúng tôi hỏi lần cuối. Hãy đối xử với sự cân bằng thực phẩm như tiền mặt trong túi áo khoác: trả tiền mặt, tiền tiết kiệm là sai.",
"about.nip87.title": "NIP-87 là gì?", "about.nip87.title": "NIP-87 là gì?",
"about.nip87.p1": "NIP-87 là hội nghị về việc thông báo và giới thiệu kẹo mint. Hai sự kiện kiểu như thế này:", "about.nip87.p1": "NIP-87 là quy ước Nostr để công bố và giới thiệu các mint ecash. Hai loại sự kiện quan trọng ở đây:",
"about.nip87.li1": "{code} thông báo mint. Địa chỉ URL mint nằm trong thẻ {tag}. Đây là cách trang web khám phá ra mint mà nó chưa bao giờ thấy.", "about.nip87.li1": "{code} thông báo mint. Địa chỉ URL mint nằm trong thẻ {tag}. Đây là cách trang web khám phá ra mint mà nó chưa bao giờ thấy.",
"about.nip87.li2": "{code} là một bài phê bình. Nó mang theo đánh giá và các văn bản đánh giá, và điểm ở mint nó là khoảng {utag} thẻ (địa chỉ URL mint) và thẻ {dtag} ( Khóa công cộng của mint).", "about.nip87.li2": "{code} là một bài phê bình. Nó mang theo đánh giá và các văn bản đánh giá, và điểm ở mint nó là khoảng {utag} thẻ (địa chỉ URL mint) và thẻ {dtag} ( Khóa công cộng của mint).",
"about.nip87.p2": "Mọi bài phê bình đều được ký bởi chìa khóa của tác giả. Chúng tôi không bao giờ gọi đó là một bài phê bình, bởi vì không ai xác nhận nó: nó được ký, một tuyên bố khác và hữu ích hơn. Nó có nghĩa là bạn có thể kiểm tra ai đã viết nó và nó không bị thay đổi. Điều này không có nghĩa là tác giả trung thực hoặc từng dùng mint.", "about.nip87.p2": "Mọi bài phê bình đều được ký bởi chìa khóa của tác giả. Chúng tôi không bao giờ gọi đó là một bài phê bình, bởi vì không ai xác nhận nó: nó được ký, một tuyên bố khác và hữu ích hơn. Nó có nghĩa là bạn có thể kiểm tra ai đã viết nó và nó không bị thay đổi. Điều này không có nghĩa là tác giả trung thực hoặc từng dùng mint.",
@@ -441,7 +465,7 @@
"about.how.li1Label": "Vòng thăm dò", "about.how.li1Label": "Vòng thăm dò",
"about.how.li2": "{strong} kết nối với các tiếp tân Nostr mỗi giờ, đọc những thông báo và bài phê bình mint, và thêm vào bất cứ loại mint nào chưa từng thấy trước đó.", "about.how.li2": "{strong} kết nối với các tiếp tân Nostr mỗi giờ, đọc những thông báo và bài phê bình mint, và thêm vào bất cứ loại mint nào chưa từng thấy trước đó.",
"about.how.li2Label": "Vòng lặp khám phá", "about.how.li2Label": "Vòng lặp khám phá",
"about.how.p2": "Phần quan trọng là những gì xảy ra khi kẹo mint ngừng trả lời. Siêu dữ liệu được lưu trữ của nó không bao giờ được viết quá tải bởi một máy dò thất bại, vì vậy một cây mint đã chết giữ toàn bộ trang của nó: những thông số, lịch sử và những bài phê bình của nó, với một biểu tượng nói khi trời tối. Phiên bản trước của trang web này đã hiển thị một trang lỗi, có nghĩa là những người mint cần thiết nhất để cảnh báo nhau về những người không thể xem xét. Đó là lý do chính để xây dựng lại.", "about.how.p2": "Phần quan trọng là điều gì xảy ra khi một mint ngừng trả lời. Siêu dữ liệu đã lưu của nó không bao giờ bị ghi đè bởi một lần thăm dò thất bại, nên một mint đã chết vẫn giữ nguyên trang của nó: thông số, lịch sử và các đánh giá, kèm một biểu ngữ cho biết nó tắt tiếng từ khi nào. Phiên bản trước của trang này hiển thị trang lỗi thay vào đó, nghĩa là chính những mint mà mọi người cần cảnh báo nhau nhất lại là những mint không thể đánh giá được. Đó là lý do chính cho việc xây dựng lại.",
"about.how.p3": "Đoạn ôn lại không bao giờ được lưu bên cạnh chúng ta. Khi bạn mở một trang mint mà trình duyệt của bạn nói chuyện trực tiếp với các rơ-le và tự đọc các bài phê bình, và khi bạn viết một trang nó được ký bởi chính phần mở rộng Nostr của bạn và gửi thẳng đến các rơ-le đó. Chúng tôi giữ số lượng để có thể xếp hạng và sắp xếp, không gì hơn. Không có gì ở đây có thể được biên tập hoặc xóa bởi chúng tôi, bao gồm cả những bài phê bình mà chúng tôi không muốn.", "about.how.p3": "Đoạn ôn lại không bao giờ được lưu bên cạnh chúng ta. Khi bạn mở một trang mint mà trình duyệt của bạn nói chuyện trực tiếp với các rơ-le và tự đọc các bài phê bình, và khi bạn viết một trang nó được ký bởi chính phần mở rộng Nostr của bạn và gửi thẳng đến các rơ-le đó. Chúng tôi giữ số lượng để có thể xếp hạng và sắp xếp, không gì hơn. Không có gì ở đây có thể được biên tập hoặc xóa bởi chúng tôi, bao gồm cả những bài phê bình mà chúng tôi không muốn.",
"about.relays.title": "Chúng ta đọc lại được", "about.relays.title": "Chúng ta đọc lại được",
"about.relays.p1": "Đây là những bản chuyển tiếp mà bộ chỉ mục và trình duyệt của bạn đều dùng:", "about.relays.p1": "Đây là những bản chuyển tiếp mà bộ chỉ mục và trình duyệt của bạn đều dùng:",
@@ -452,13 +476,13 @@
"about.numbers.p1": "{mints} loại mint, {online} trong số đó trả lời, {reviews} bản đánh giá được tính từ các tiếp tân trên.", "about.numbers.p1": "{mints} loại mint, {online} trong số đó trả lời, {reviews} bản đánh giá được tính từ các tiếp tân trên.",
"about.source.title": "Nguồn và uy tín", "about.source.title": "Nguồn và uy tín",
"about.source.p1": "Địa điểm này là nguồn mở tại {github}. Nó được xây và chạy bởi {azzamo}. Nếu một trang mint có gì đó không ổn, hãy sửa nó ở nguồn: siêu dữ liệu mint đến từ số {code}, và những bài phê bình đến từ Nostr.", "about.source.p1": "Địa điểm này là nguồn mở tại {github}. Nó được xây và chạy bởi {azzamo}. Nếu một trang mint có gì đó không ổn, hãy sửa nó ở nguồn: siêu dữ liệu mint đến từ số {code}, và những bài phê bình đến từ Nostr.",
"terms.title": "Điều kiện dùng . Cashumins. không gian", "terms.title": "Điều khoản sử dụng · Cashumints.space",
"terms.description": "Hợp đồng dùng cho Cashumins. không gian: những gì trang web là, những gì nó không làm, và ai chịu trách nhiệm cho những gì. Không tài khoản, không bảo hành, không liên quan đến mint.", "terms.description": "Điều khoản sử dụng của Cashumints.space: trang web này là gì, không làm gì, và ai chịu trách nhiệm về điều gì. Không tài khoản, không bảo đảm, không liên kết với bất kỳ mint nào.",
"terms.heading": "Điều kiện dùng", "terms.heading": "Điều kiện dùng",
"terms.lede": "Điều khoản bình thường cho một trang web mà chỉ có chỉ số dữ liệu công cộng. Cập nhật {date}.", "terms.lede": "Điều khoản bình thường cho một trang web mà chỉ có chỉ số dữ liệu công cộng. Cập nhật {date}.",
"terms.what.title": "Nơi này là gì?", "terms.what.title": "Nơi này là gì?",
"terms.what.p1": "Cashumints.space là chỉ số của dữ liệu công khai về mint Cashu. Hai loại dữ liệu, cả hai từ nơi khác:", "terms.what.p1": "Cashumints.space là chỉ số của dữ liệu công khai về mint Cashu. Hai loại dữ liệu, cả hai từ nơi khác:",
"terms.what.li1": "Siêu dữ liệu tối thiểu, đọc từ {code}41 điểm cuối của mỗi cây mint: tên, mô tả, phiên bản phần mềm, hỗ trợ NUTs, liên lạc chi tiết và liệu thanh mint có trả lời khi chúng tôi hỏi lần cuối.", "terms.what.li1": "Siêu dữ liệu mint, đọc từ endpoint {code} của chính mỗi mint: tên, mô tả, phiên bản phần mềm, các NUTs được hỗ trợ, thông tin liên hệ và liệu mint có trả lời khi chúng tôi hỏi lần cuối hay không.",
"terms.what.li2": "Xem lại, đọc từ các thông tin chính. Đây là những sự kiện NIP-87 do tác giả của họ xuất bản để chuyển tiếp mọi người có thể truy vấn.", "terms.what.li2": "Xem lại, đọc từ các thông tin chính. Đây là những sự kiện NIP-87 do tác giả của họ xuất bản để chuyển tiếp mọi người có thể truy vấn.",
"terms.what.p2": "Chúng tôi thu thập dữ liệu đó, bình thường hóa nó, xếp hạng và cho nó thấy. Chúng ta không tạo ra nó.", "terms.what.p2": "Chúng tôi thu thập dữ liệu đó, bình thường hóa nó, xếp hạng và cho nó thấy. Chúng ta không tạo ra nó.",
"terms.noAccounts.title": "Không tài khoản, không bảo hành.", "terms.noAccounts.title": "Không tài khoản, không bảo hành.",
@@ -466,7 +490,7 @@
"terms.noAccounts.p2": "Sử dụng nó với rủi ro của riêng bạn. Chúng tôi không phải chịu trách nhiệm cho bất kỳ mất mát nào do sử dụng trang web này hoặc dựa vào bất cứ điều gì bạn đọc ở đây. Hãy xem số {link}, với rủi ro cụ thể là dùng một loại mint Cashu.", "terms.noAccounts.p2": "Sử dụng nó với rủi ro của riêng bạn. Chúng tôi không phải chịu trách nhiệm cho bất kỳ mất mát nào do sử dụng trang web này hoặc dựa vào bất cứ điều gì bạn đọc ở đây. Hãy xem số {link}, với rủi ro cụ thể là dùng một loại mint Cashu.",
"terms.noAccounts.link": "Không gọi", "terms.noAccounts.link": "Không gọi",
"terms.noCustody.title": "Chúng tôi không giữ tiền của anh.", "terms.noCustody.title": "Chúng tôi không giữ tiền của anh.",
"terms.noCustody.p1": "Trang web này không bao giờ nhận tiền bảo hộ. Nó không có ví, không đụng đến tiền, không thể di chuyển, đóng băng hoặc lấy lại bất cứ thứ gì. Nếu bạn có vấn đề với kẹo mint, người điều hành mint là người duy nhất có thể giúp bạn, và chúng tôi không thể liên lạc với họ thay mặt bạn.", "terms.noCustody.p1": "Trang này không bao giờ giữ hộ tiền. Nó không có ví, không đụng đến ecash, và không thể chuyển, đóng băng hay khôi phục bất cứ thứ gì. Nếu bạn gặp vấn đề với một mint, người vận hành mint là bên duy nhất có thể giúp bạn, và chúng tôi không thể liên hệ với họ thay bạn.",
"terms.noCustody.p2": "Chúng ta không có bất kỳ loại mint nào ở đây, và không có loại mint nào trả cho danh sách, cấp bậc hay đánh giá của nó. mint được phát hiện từ những thông báo công cộng và được xếp hạng bởi một thuật toán trên dữ liệu duyệt công cộng.", "terms.noCustody.p2": "Chúng ta không có bất kỳ loại mint nào ở đây, và không có loại mint nào trả cho danh sách, cấp bậc hay đánh giá của nó. mint được phát hiện từ những thông báo công cộng và được xếp hạng bởi một thuật toán trên dữ liệu duyệt công cộng.",
"terms.reviews.title": "Kết luận là từ của người khác.", "terms.reviews.title": "Kết luận là từ của người khác.",
"terms.reviews.p1": "Xem lại là nội dung do người dùng cung cấp cho công chúng, được tác giả của họ xuất bản. Chúng ta thể hiện chúng, chúng ta không kiểm soát chúng. Chúng tôi không viết chúng, chúng tôi không thể chỉnh sửa chúng, và chúng tôi không thể xóa chúng khỏi các rơ-le giữ chúng: chúng được ký kết các sự kiện trên một mạng lưới mà chúng tôi không sở hữu.", "terms.reviews.p1": "Xem lại là nội dung do người dùng cung cấp cho công chúng, được tác giả của họ xuất bản. Chúng ta thể hiện chúng, chúng ta không kiểm soát chúng. Chúng tôi không viết chúng, chúng tôi không thể chỉnh sửa chúng, và chúng tôi không thể xóa chúng khỏi các rơ-le giữ chúng: chúng được ký kết các sự kiện trên một mạng lưới mà chúng tôi không sở hữu.",
@@ -474,12 +498,12 @@
"terms.reviews.link": "Trang riêng", "terms.reviews.link": "Trang riêng",
"terms.reviews.p3": "Chúng tôi có thể dừng hiển thị một đánh giá cụ thể trên trang web này, và chúng tôi sẽ làm điều đó cho nội dung mà là bất hợp pháp. Loại bỏ nó ra khỏi những trang giấy này. Sự kiện vẫn tiếp tục diễn ra và bất cứ khách hàng nào khác cũng có thể đọc nó.", "terms.reviews.p3": "Chúng tôi có thể dừng hiển thị một đánh giá cụ thể trên trang web này, và chúng tôi sẽ làm điều đó cho nội dung mà là bất hợp pháp. Loại bỏ nó ra khỏi những trang giấy này. Sự kiện vẫn tiếp tục diễn ra và bất cứ khách hàng nào khác cũng có thể đọc nó.",
"terms.links.title": "Liên kết tới nơi Mạng khác", "terms.links.title": "Liên kết tới nơi Mạng khác",
"terms.links.p1": "Các URL hạt nhân, liên kết ví, liên lạc viên điều hành và mọi thứ khác chỉ ra trang web này là có bởi vì chúng là một phần của dữ liệu. Liên kết không phải là sự tán thành và chúng ta không xem xét những gì ở đầu bên kia.", "terms.links.p1": "URL của mint, liên kết ví, thông tin liên hệ của người vận hành và mọi thứ khác trỏ ra ngoài trang này đều có mặt vì chúng là một phần của dữ liệu. Một liên kết không phải là sự chứng thực và chúng tôi không thẩm định những gì ở đầu bên kia.",
"terms.changes.title": "Thay đổi", "terms.changes.title": "Thay đổi",
"terms.changes.p1": "Những điều khoản này có thể thay đổi. Cuộc hẹn ở trên báo nói khi họ làm lần cuối. Trang web là nguồn mở, nên mọi thay đổi trong trang này đều nằm trong lịch sử.", "terms.changes.p1": "Những điều khoản này có thể thay đổi. Cuộc hẹn ở trên báo nói khi họ làm lần cuối. Trang web là nguồn mở, nên mọi thay đổi trong trang này đều nằm trong lịch sử.",
"terms.contact.title": "Contact & mới", "terms.contact.title": "Contact & mới",
"terms.contact.p1": "Vấn đề và yêu cầu giảm án là tốt nhất trên {github}41. Nơi này được điều hành bởi {azzamo}.", "terms.contact.p1": "Vấn đề và yêu cầu giảm án là tốt nhất trên {github}41. Nơi này được điều hành bởi {azzamo}.",
"privacy.title": "Riêng tư. không gian", "privacy.title": "Riêng tư · Cashumints.space",
"privacy.description": "Quyền riêng tư trên Cashumints.space: không tài khoản, không theo dõi cookie, không phân tích. Cơ cấu địa phương được dùng để đốt hồ sơ và không gửi đi đâu cả. Các bản thảo được ký vào trình duyệt của bạn.", "privacy.description": "Quyền riêng tư trên Cashumints.space: không tài khoản, không theo dõi cookie, không phân tích. Cơ cấu địa phương được dùng để đốt hồ sơ và không gửi đi đâu cả. Các bản thảo được ký vào trình duyệt của bạn.",
"privacy.heading": "Riêng tư", "privacy.heading": "Riêng tư",
"privacy.lede": "Chúng tôi thu thập càng ít càng tốt. Cập nhật {date}.", "privacy.lede": "Chúng tôi thu thập càng ít càng tốt. Cập nhật {date}.",
@@ -511,33 +535,33 @@
"privacy.permanent.li2": "Nó được liên kết vĩnh viễn với chìa khóa công cộng của bạn, cùng với tất cả những thứ khác mà khóa đã được xuất bản.", "privacy.permanent.li2": "Nó được liên kết vĩnh viễn với chìa khóa công cộng của bạn, cùng với tất cả những thứ khác mà khóa đã được xuất bản.",
"privacy.permanent.li3": "Xóa bỏ nó là một yêu cầu, không phải một mệnh lệnh. Bạn có thể công bố một sự kiện xóa bỏ và một số rơ-le sẽ được vinh danh nó, nhưng một tiếp âm miễn phí để bỏ qua nó và bất cứ ai đã sao chép sự kiện này vẫn còn giữ nó. Hãy xem một bài phê bình được xuất bản là vĩnh viễn.", "privacy.permanent.li3": "Xóa bỏ nó là một yêu cầu, không phải một mệnh lệnh. Bạn có thể công bố một sự kiện xóa bỏ và một số rơ-le sẽ được vinh danh nó, nhưng một tiếp âm miễn phí để bỏ qua nó và bất cứ ai đã sao chép sự kiện này vẫn còn giữ nó. Hãy xem một bài phê bình được xuất bản là vĩnh viễn.",
"privacy.permanent.li4": "Chúng tôi không thể xóa nó cho anh. Chúng ta có thể ngừng trưng bày nó ở đây, mà không thay đổi gì trên các rơ-le.", "privacy.permanent.li4": "Chúng tôi không thể xóa nó cho anh. Chúng ta có thể ngừng trưng bày nó ở đây, mà không thay đổi gì trên các rơ-le.",
"privacy.permanent.p2": "Nếu bạn muốn xem lại một cây mint mà không bị trói vào nhân dạng chính của mình, hãy dùng chìa khóa mũi khác. Hãy lưu ý rằng một chìa khóa mà không có hoạt động nào khác được hiển thị như trên các bài phê bình khắc nghiệt, bởi vì một dấu hiệu mới của một bài phê bình là một người đọc mẫu.", "privacy.permanent.p2": "Nếu bạn muốn đánh giá một mint mà không gắn với danh tính chính của mình, hãy dùng một khóa Nostr riêng. Lưu ý rằng một khóa không có hoạt động nào khác sẽ được hiển thị như vậy trên các đánh giá gay gắt, vì một khóa hoàn toàn mới viết đánh giá một sao là một dấu hiệu mà người đọc xứng đáng được thấy.",
"privacy.contact.title": "Câu hỏi", "privacy.contact.title": "Câu hỏi",
"privacy.contact.p1": "Trang web là mã nguồn mở: nếu bạn muốn kiểm tra bất cứ cái nào ở trên, hãy đọc mã ở {github}. Câu hỏi và sự sửa trị có thể được nêu lên trong một vấn đề ở đó.", "privacy.contact.p1": "Trang web là mã nguồn mở: nếu bạn muốn kiểm tra bất cứ cái nào ở trên, hãy đọc mã ở {github}. Câu hỏi và sự sửa trị có thể được nêu lên trong một vấn đề ở đó.",
"disclaimer.title": "Không nhận dạng được. không gian", "disclaimer.title": "Tuyên bố miễn trừ trách nhiệm · Cashumints.space",
"disclaimer.description": "Điều cần biết trước khi dùng kẹo Cashu: mint là tiền bảo quản, mint có thể biến mất với tiền của bạn, đánh giá là ý kiến và phương tiện trực tuyến có thể đạt được, không đáng tin cậy.", "disclaimer.description": "Điều cần biết trước khi bạn dùng một mint Cashu: mint giữ tiền hộ bạn, một mint có thể biến mất cùng tiền của bạn, đánh giá là ý kiến cá nhân, và online nghĩa là kết nối được, không phải đáng tin.",
"disclaimer.heading": "Không nhận diện", "disclaimer.heading": "Không nhận diện",
"disclaimer.lede": "Đọc cái này trước khi bỏ tiền vô mint. Cập nhật {date}.", "disclaimer.lede": "Đọc cái này trước khi bỏ tiền vô mint. Cập nhật {date}.",
"disclaimer.callout": "Một cây mint Cashu giữ tiền cho anh. Nếu nó biến mất, tiền của anh sẽ biến mất, và không còn ai để kháng cáo. Chỉ giữ những gì có thể để mất.", "disclaimer.callout": "Một mint Cashu giữ tiền hộ bạn. Nếu nó biến mất, ecash của bạn mất theo, và không có ai để khiếu nại. Chỉ giữ số tiền bạn có thể chấp nhận mất.",
"disclaimer.custodial.title": "mint là quản lý", "disclaimer.custodial.title": "mint là quản lý",
"disclaimer.custodial.p1": "Khi anh ăn kẹo cao su, anh sẽ đưa cho người điều hành mint và lấy lại kim cương. Những biểu tượng đó là yêu cầu của người điều hành. Họ không cắn bạn giữ, và người điều hành là người quyết định có nên tôn trọng họ hay không.", "disclaimer.custodial.p1": "Khi bạn mint ecash, bạn giao bitcoin cho người vận hành mint và nhận lại token. Những token đó là một quyền đòi đối với người vận hành ấy. Chúng không phải bitcoin bạn đang nắm giữ, và người vận hành là bên quyết định có thực hiện nghĩa vụ hay không.",
"disclaimer.custodial.p2": "Điều đó có nghĩa là mọi loại mint đều có cùng nguy cơ: người điều hành có thể biến mất, có thể bị buộc phải dừng lại, có thể mất tiền thông qua một con bọ hay một cú hack, hoặc đơn giản là tắt máy chủ. Không có bảo hiểm tiền gửi, không tính phí, không có đường dây hỗ trợ và không có giải pháp hợp pháp đáng tin cậy. Điều này đúng với loại mint cao nhất trên trang này.", "disclaimer.custodial.p2": "Điều đó có nghĩa là mọi loại mint đều có cùng nguy cơ: người điều hành có thể biến mất, có thể bị buộc phải dừng lại, có thể mất tiền thông qua một con bọ hay một cú hack, hoặc đơn giản là tắt máy chủ. Không có bảo hiểm tiền gửi, không tính phí, không có đường dây hỗ trợ và không có giải pháp hợp pháp đáng tin cậy. Điều này đúng với loại mint cao nhất trên trang này.",
"disclaimer.custodial.p3": "Cashu được thiết kế để vận chuyển một cách nhanh chóng số lượng nhỏ. Hãy dùng nó như thế này: Hãy giữ những gì bạn sắp tiêu, chứ không phải tiền tiết kiệm, và rải rác trên những cây mint thay vì đỗ mọi thứ trong một cái.", "disclaimer.custodial.p3": "Cashu được thiết kế cho những khoản nhỏ luân chuyển nhanh. Hãy dùng đúng như vậy: giữ số bạn sắp tiêu, không phải tiền tiết kiệm, và chia ra nhiều mint thay vì để hết vào một chỗ.",
"disclaimer.reviews.title": "Xem lại là ý kiến, chưa xác nhận lời tuyên bố", "disclaimer.reviews.title": "Xem lại là ý kiến, chưa xác nhận lời tuyên bố",
"disclaimer.reviews.p1": "Mọi bài phê bình ở đây đều được viết bởi ai đó trên Nostr và đã ký bằng chìa khóa của họ. Ký tên nghĩa là chúng tôi có thể cho anh biết chìa khóa nào đã viết nó. Điều này không có nghĩa là lời phê bình là đúng, tác giả đã dùng mint, hoặc tác giả không phải là người điều hành, một đối thủ cạnh tranh, hoặc một người có mười chìa khóa.", "disclaimer.reviews.p1": "Mọi bài phê bình ở đây đều được viết bởi ai đó trên Nostr và đã ký bằng chìa khóa của họ. Ký tên nghĩa là chúng tôi có thể cho anh biết chìa khóa nào đã viết nó. Điều này không có nghĩa là lời phê bình là đúng, tác giả đã dùng mint, hoặc tác giả không phải là người điều hành, một đối thủ cạnh tranh, hoặc một người có mười chìa khóa.",
"disclaimer.reviews.p2": "Chúng ta không xác nhận những người phê bình, và chúng ta không chỉnh sửa hoặc loại bỏ những lời phê bình mà chúng ta không đồng ý. Hãy đọc một vài, hãy nhìn vào những gì họ thực sự nói, và cân một mint với ba đánh giá khác nhau so với một với ba trăm.", "disclaimer.reviews.p2": "Chúng ta không xác nhận những người phê bình, và chúng ta không chỉnh sửa hoặc loại bỏ những lời phê bình mà chúng ta không đồng ý. Hãy đọc một vài, hãy nhìn vào những gì họ thực sự nói, và cân một mint với ba đánh giá khác nhau so với một với ba trăm.",
"disclaimer.status.title": "Comment", "disclaimer.status.title": "Comment",
"disclaimer.status.p1": "Chúng tôi kiểm tra mọi loại mint đều đặn và cho thấy nó có trả lời không. Tất cả đều có nghĩa là: máy chủ trả lời. Nó không nói gì về việc người điều hành giải quyết, trung thực, hay vẫn có ý định tôn trọng vật kỷ niệm ngày mai.", "disclaimer.status.p1": "Chúng tôi kiểm tra mọi loại mint đều đặn và cho thấy nó có trả lời không. Tất cả đều có nghĩa là: máy chủ trả lời. Nó không nói gì về việc người điều hành giải quyết, trung thực, hay vẫn có ý định tôn trọng vật kỷ niệm ngày mai.",
"disclaimer.status.p2": "Một cục mint có thể được trực tuyến cho đến khi nó không còn. Lịch sử hiện đại, được hỗ trợ bởi NUTs và phiên bản phần mềm cho bạn biết về cách một cây mint được chạy, nhưng không ai trong số chúng là một lời hứa về tiền của bạn.", "disclaimer.status.p2": "Một mint có thể online cho đến đúng khoảnh khắc nó không còn online. Lịch sử thời gian hoạt động, các NUTs được hỗ trợ và phiên bản phần mềm cho bạn biết đôi điều về cách một mint được vận hành, nhưng không điều nào là lời hứa về tiền của bạn.",
"disclaimer.ranking.title": "Làm thế nào để xếp hạng hoạt động, và nó không có nghĩa là", "disclaimer.ranking.title": "Làm thế nào để xếp hạng hoạt động, và nó không có nghĩa là",
"disclaimer.ranking.p1": "mint được đặt hàng bởi một thuật toán thông qua dữ liệu xem xét công cộng: đánh giá, số lượng đánh giá và hoạt động gần đây, với mint trực tuyến trước khi tắt máy theo mặc định. Chi tiết nằm trong mã nguồn mở.", "disclaimer.ranking.p1": "mint được đặt hàng bởi một thuật toán thông qua dữ liệu xem xét công cộng: đánh giá, số lượng đánh giá và hoạt động gần đây, với mint trực tuyến trước khi tắt máy theo mặc định. Chi tiết nằm trong mã nguồn mở.",
"disclaimer.ranking.p2": "Không ai trả tiền đặt chỗ cả. Không có quỹ bảo trợ, không có danh sách quảng cáo và không có sự sắp đặt nào về mint trên trang này. Một cấp cao có nghĩa là thuật toán thích dữ liệu công cộng, và không có gì hơn. Đây không phải là một lời đề nghị và cũng không phải là một đánh giá an toàn.", "disclaimer.ranking.p2": "Không ai trả tiền đặt chỗ cả. Không có quỹ bảo trợ, không có danh sách quảng cáo và không có sự sắp đặt nào về mint trên trang này. Một cấp cao có nghĩa là thuật toán thích dữ liệu công cộng, và không có gì hơn. Đây không phải là một lời đề nghị và cũng không phải là một đánh giá an toàn.",
"disclaimer.notAdvice.title": "Đây không phải là lời khuyên tài chính.", "disclaimer.notAdvice.title": "Đây không phải là lời khuyên tài chính.",
"disclaimer.notAdvice.p1": "Không có gì trên trang này là tài chính, đầu tư, tư vấn pháp lý hay thuế vụ. Chúng tôi không biết tình hình của anh và cũng không khuyên anh. Dùng kẹo mint, và lựa chọn viên nào là quyết định của anh.", "disclaimer.notAdvice.p1": "Không có gì trên trang này là tư vấn tài chính, đầu tư, pháp lý hay thuế. Chúng tôi không biết hoàn cảnh của bạn và không tư vấn cho bạn. Dùng một mint, và chọn mint nào, là quyết định của bạn.",
"disclaimer.dyor.title": "Tự nghiên cứu đi", "disclaimer.dyor.title": "Tự nghiên cứu đi",
"disclaimer.dyor.li1": "Tìm tổng đài đi. Một người có tiếng tăm công chúng để mất tiền bạc là một đề xuất khác từ một địa chỉ URL vô danh.", "disclaimer.dyor.li1": "Tra cứu người vận hành. Một mint do người có danh tiếng công khai để mất vận hành là chuyện khác hẳn một URL vô danh.",
"disclaimer.dyor.li2": "Hãy đọc những điều khoản của chính mint và MOTD của nó. Một cây mint tự gọi mình là một thử nghiệm hoặc một beta có nghĩa là nó.", "disclaimer.dyor.li2": "Đọc điều khoản của chính mint và MOTD của nó. Một mint tự gọi mình là bản thử nghiệm hay beta là có ý như vậy.",
"disclaimer.dyor.li3": "Bắt đầu từ việc nhỏ. Chỉ một chút thôi, dùng nó, nấu chảy nó ra, và xem chuyện gì xảy ra trước khi bạn tin tưởng nó nhiều hơn.", "disclaimer.dyor.li3": "Bắt đầu nhỏ. Mint một ít, tiêu nó, melt nó ra lại, và xem chuyện gì xảy ra trước khi bạn tin tưởng giao nhiều hơn.",
"disclaimer.dyor.li4": "Coi chừng giới hạn. Mức tối đa thường là dấu hiệu của người điều hành cẩn thận, và cao không phải là dấu hiệu của người an toàn.", "disclaimer.dyor.li4": "Coi chừng giới hạn. Mức tối đa thường là dấu hiệu của người điều hành cẩn thận, và cao không phải là dấu hiệu của người an toàn.",
"disclaimer.dyor.li5": "Hãy giữ những vật kỷ niệm của các ngươi , và nhớ rằng của - cải các ngươi phải phó cho ; ai cầm có thể tiêu đi .", "disclaimer.dyor.li5": "Hãy giữ những vật kỷ niệm của các ngươi , và nhớ rằng của - cải các ngươi phải phó cho ; ai cầm có thể tiêu đi .",
"disclaimer.site.title": "Về chính trang web", "disclaimer.site.title": "Về chính trang web",
@@ -546,34 +570,34 @@
"disclaimer.updated": "20 tháng 8 năm 2026", "disclaimer.updated": "20 tháng 8 năm 2026",
"login.getPrimal": "Nhận được Primal", "login.getPrimal": "Nhận được Primal",
"reviews.dialog.identity.extensionGone": "Phần mở rộng trình duyệt của bạn không trả lời. Hãy đăng nhập lại, hoặc sử dụng một tín hiệu từ xa.", "reviews.dialog.identity.extensionGone": "Phần mở rộng trình duyệt của bạn không trả lời. Hãy đăng nhập lại, hoặc sử dụng một tín hiệu từ xa.",
"seo.orgDescription": "Một thư mục của các loại mint Cashu, với uy tín trực tiếp từ mỗi bản đánh giá cộng đồng và mint được ký trên Nostr.", "seo.orgDescription": "Danh mục các mint Cashu, với trạng thái trực tiếp của từng mint và đánh giá từ cộng đồng được ký trên Nostr.",
"seo.ogImageAlt": "Cashumints.space: Tìm một cây mint Cashu mà bạn có thể tin tưởng.", "seo.ogImageAlt": "Cashumints.space: tìm một mint Cashu bạn có thể tin tưởng.",
"seo.mint.title.one": "{name} bài phê bình: đánh giá {rating} từ {n} đánh giá không gian", "seo.mint.title.one": "{name} đánh giá: xếp hạng {rating} từ {n} đánh giá | Cashumints.space",
"seo.mint.title.other": "{name} bài phê bình: đánh giá {rating} từ {n} bản đánh giá", "seo.mint.title.other": "{name} đánh giá: xếp hạng {rating} từ {n} đánh giá | Cashumints.space",
"seo.mint.titleOffline.one": "{name}: Không gian trong không gian {n} ngày.", "seo.mint.titleOffline.one": "{name}: ngoại tuyến {n} ngày | Cashumints.space",
"seo.mint.titleOffline.other": "{name}: ngưng hoạt động {n} ngày không gian", "seo.mint.titleOffline.other": "{name}: ngoại tuyến {n} ngày | Cashumints.space",
"seo.mint.titleNoReviews": "{name}: Cashu đủ thông tin về mint và các bài bình luận về những người nhận tiền mặt. không gian", "seo.mint.titleNoReviews": "{name}: thông tin và đánh giá mint Cashu | Cashumints.space",
"seo.fedimint.titleNoReviews": "{name}: Fedimint Femination Intels và đánh giá vội vàng. không gian", "seo.fedimint.titleNoReviews": "{name}: thông tin và đánh giá liên đoàn Fedimint | Cashumints.space",
"seo.desc.fallback": "Các bài phê bình cộng đồng {name} ({domain}).", "seo.desc.fallback": "Đánh giá từ cộng đồng cho {name} ({domain}).",
"seo.desc.fallbackDomain": "Báo cáo công chúng về mint Cashu {domain}.", "seo.desc.fallbackDomain": "Đánh giá từ cộng đồng cho mint Cashu tại {domain}.",
"seo.desc.rated.one": "Đánh giá {rating}/5 từ {n} đã ký kết.", "seo.desc.rated.one": "Xếp hạng {rating}/5 từ {n} đánh giá Nostr đã ký.",
"seo.desc.rated.other": "Đánh giá {rating}/5 từ {n} đã ký kết.", "seo.desc.rated.other": "Xếp hạng {rating}/5 từ {n} đánh giá Nostr đã ký.",
"seo.desc.noReviews": "Vẫn chưa có đánh giá.", "seo.desc.noReviews": "Vẫn chưa có đánh giá.",
"seo.desc.status": "{status}.", "seo.desc.status": "{status}.",
"seo.desc.statusLimits": "{status}, giới hạn {limits}.", "seo.desc.statusLimits": "{status}, giới hạn {limits}.",
"seo.desc.fedModules": "{modules} {status}.", "seo.desc.fedModules": "{status}. Mô-đun: {modules}.",
"seo.desc.fedModulesNetwork": "{modules} {network} {status}.", "seo.desc.fedModulesNetwork": "{status}. Mô-đun: {modules} trên {network}.",
"seo.mint.ogAlt.one": "{name}: {rating} vì sao từ {n} xem xét về Cashumints.", "seo.mint.ogAlt.one": "{name}: {rating} sao từ {n} đánh giá trên Cashumints.space",
"seo.mint.ogAlt.other": "{name}: {rating} sao từ {n} bài phê bình về Cashumints.space", "seo.mint.ogAlt.other": "{name}: {rating} sao từ {n} đánh giá trên Cashumints.space",
"seo.mint.ogAltNoReviews": "{name} trên đường Cashumint.", "seo.mint.ogAltNoReviews": "{name} trên Cashumints.space",
"nav.home": "Nhà", "nav.home": "Trang chủ",
"mint.more.title": "Thêm mint", "mint.more.title": "Thêm mint",
"mint.more.all": "Xem tất cả kẹo mint", "mint.more.all": "Xem tất cả mint",
"nav.fedimints": "Fedimins", "nav.fedimints": "Fedimints",
"status.announced": "Chỉ thông báo", "status.announced": "Chỉ thông báo",
"card.unnamedFederation": "Liên kết không tên", "card.unnamedFederation": "Liên đoàn chưa đặt tên",
"card.notChecked": "chưa bao giờ kiểm tra", "card.notChecked": "chưa bao giờ kiểm tra",
"fedimints.title": "Tất cả liên bang. không gian", "fedimints.title": "Tất cả liên đoàn Fedimint · Cashumints.space",
"fedimints.description": "Tất cả {total} Fedimint FBI đã thông báo trên mạng: {online} đã được kiểm tra từ bên ngoài, {announced} thông báo và chưa bao giờ xác nhận. Đánh giá cộng đồng, mô-đun và mời mật mã cho mỗi người.", "fedimints.description": "Tất cả {total} Fedimint FBI đã thông báo trên mạng: {online} đã được kiểm tra từ bên ngoài, {announced} thông báo và chưa bao giờ xác nhận. Đánh giá cộng đồng, mô-đun và mời mật mã cho mỗi người.",
"fedimints.heading": "Mọi liên kết", "fedimints.heading": "Mọi liên kết",
"fedimints.lede": "Mọi nguồn tin liên bang đều thông báo trên mạng Nostr. Một tổ chức liên bang không có địa vị chung của riêng mình, vì vậy một số được xác nhận chạy bởi một kiểm tra bên ngoài và phần còn lại được liệt kê chính xác như những gì đã được thông báo.", "fedimints.lede": "Mọi nguồn tin liên bang đều thông báo trên mạng Nostr. Một tổ chức liên bang không có địa vị chung của riêng mình, vì vậy một số được xác nhận chạy bởi một kiểm tra bên ngoài và phần còn lại được liệt kê chính xác như những gì đã được thông báo.",
@@ -651,8 +675,114 @@
"mint.warnings.neverConfirmed.body.reviewed": "{date} {days} Liên bang này đã được thông báo trên đường số 918274141, 9182742, và không có gì xác nhận từ đó. Những bài phê bình dưới đây là dấu hiệu duy nhất của sự sống trên trang này; mọi thứ khác đến từ thông báo.", "mint.warnings.neverConfirmed.body.reviewed": "{date} {days} Liên bang này đã được thông báo trên đường số 918274141, 9182742, và không có gì xác nhận từ đó. Những bài phê bình dưới đây là dấu hiệu duy nhất của sự sống trên trang này; mọi thứ khác đến từ thông báo.",
"mint.warnings.neverConfirmed.body.quiet": "{date} Liên bang này đã được thông báo trên đường số 918274141, {days} trước đây, không có gì được xác nhận kể từ đó, và không ai xem xét nó gần đây. Mọi thứ bên dưới đều đến từ thông báo.", "mint.warnings.neverConfirmed.body.quiet": "{date} Liên bang này đã được thông báo trên đường số 918274141, {days} trước đây, không có gì được xác nhận kể từ đó, và không ai xem xét nó gần đây. Mọi thứ bên dưới đều đến từ thông báo.",
"mint.warnings.neverConfirmed.meta": "{date} Thông báo 918274141, chưa xác nhận", "mint.warnings.neverConfirmed.meta": "{date} Thông báo 918274141, chưa xác nhận",
"footer.allFedimints": "Mọi liên kết", "footer.allFedimints": "Tất cả liên đoà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.cashu": "Việc minting, melting và hỗ trợ diễn ra thế nào? Hãy cụ thể.",
"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ì."
} }
+137 -7
View File
@@ -46,10 +46,14 @@
"footer.wallets": "钱包", "footer.wallets": "钱包",
"footer.about": "关于", "footer.about": "关于",
"footer.disclaimer": "免责声明", "footer.disclaimer": "免责声明",
"footer.terms": "术语", "footer.terms": "条款",
"footer.privacy": "隐私", "footer.privacy": "隐私",
"footer.madeBy": "由 {azzamo} 倾情制作 ❤️ · 由 Cashu 和 NIP-87 提供技术支持", "footer.madeBy": "由 {azzamo} 倾情制作 ❤️ · 由 Cashu 和 NIP-87 提供技术支持",
"footer.warning": "铸币服务属于托管方;它可能会带着你的钱消失。", "footer.warning": "铸币服务属于托管方;它可能会带着你的钱消失。",
"footer.tagline": "社区对 ecash 铸币服务的评价,由 Nostr 签名。",
"footer.col.explore": "探索",
"footer.col.site": "站点",
"footer.nav": "页脚",
"home.title": "Cashumints.space · 寻找值得信赖的 Cashu 铸币服务", "home.title": "Cashumints.space · 寻找值得信赖的 Cashu 铸币服务",
"home.description": "寻找值得信赖的 Cashu 铸币服务。已收录 {mints} 个铸币服务和 {reviews} 条在 Nostr 上签名的社区评论,并提供实时在线状态、支持的 NUTs 和铸币软件信息。", "home.description": "寻找值得信赖的 Cashu 铸币服务。已收录 {mints} 个铸币服务和 {reviews} 条在 Nostr 上签名的社区评论,并提供实时在线状态、支持的 NUTs 和铸币软件信息。",
"home.hero.title": "查找 Cashu 铸币服务", "home.hero.title": "查找 Cashu 铸币服务",
@@ -114,6 +118,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": "复制 铸币服务 的网址",
@@ -261,6 +267,9 @@
"reviews.empty": "目前尚无评论。快来成为首位评论者吧。", "reviews.empty": "目前尚无评论。快来成为首位评论者吧。",
"reviews.emptyWritten": "这里还没有书面评论。快来成为第一个发表评论的人吧。", "reviews.emptyWritten": "这里还没有书面评论。快来成为第一个发表评论的人吧。",
"reviews.emptyFilter": "没有符合此筛选条件的评论。", "reviews.emptyFilter": "没有符合此筛选条件的评论。",
"reviews.rate.cashu": "为该铸币服务评分",
"reviews.rate.fedimint": "为该联邦评分",
"reviews.rate.lnurl": "为该铸币服务评分",
"reviews.noscript": "评论是在 Nostr 上签署的,并在您的浏览器中加载,因此此列表需要 JavaScript。上方的评分和数量来自该网站本身。", "reviews.noscript": "评论是在 Nostr 上签署的,并在您的浏览器中加载,因此此列表需要 JavaScript。上方的评分和数量来自该网站本身。",
"reviews.relayError": "无法连接到中继服务器。评论可能不完整。", "reviews.relayError": "无法连接到中继服务器。评论可能不完整。",
"reviews.srRating": "{rating}(满分5分)", "reviews.srRating": "{rating}(满分5分)",
@@ -273,7 +282,7 @@
"reviews.anonNote": "这是关于这款npub的首条评论,未发现其他相关活动", "reviews.anonNote": "这是关于这款npub的首条评论,未发现其他相关活动",
"reviews.published": "已发布。", "reviews.published": "已发布。",
"reviews.publishedPartial": "已发布至 {total} 继电器中的 {accepted}。", "reviews.publishedPartial": "已发布至 {total} 继电器中的 {accepted}。",
"reviews.propagating": "正在同步,内容可能需要片刻时间才会显示在其他地方。", "reviews.propagating": "正在发布到 Nostr 中继",
"reviews.summary.line.one": "{count} 的评分(无评论):{breakdown}", "reviews.summary.line.one": "{count} 的评分(无评论):{breakdown}",
"reviews.summary.line.other": "{count} 的评分(无评论):{breakdown}", "reviews.summary.line.other": "{count} 的评分(无评论):{breakdown}",
"reviews.summary.lineDay.one": "{count}的评分,未对{when}发表评论:{breakdown}", "reviews.summary.lineDay.one": "{count}的评分,未对{when}发表评论:{breakdown}",
@@ -287,7 +296,12 @@
"reviews.breakdown.1": "{n} 一星", "reviews.breakdown.1": "{n} 一星",
"reviews.breakdown.none": "{n},无评分", "reviews.breakdown.none": "{n},无评分",
"reviews.dialog.title": "评论 {name}", "reviews.dialog.title": "评论 {name}",
"reviews.dialog.note": "已使用您的 Nostr 密钥签名,并发布到公共中继节点。该条目无法编辑或删除。", "reviews.dialog.lede.cashu": "分享你在该铸币服务的使用体验。",
"reviews.dialog.lede.fedimint": "分享你在该联邦的使用体验。",
"reviews.dialog.lede.lnurl": "分享你在该铸币服务的使用体验。",
"reviews.dialog.permanent": "将永久发布到公共 Nostr 中继节点。",
"reviews.dialog.gate.title": "登录后即可撰写评论",
"reviews.dialog.gate.lead": "评论使用您的 Nostr 密钥签名。密钥始终保存在您的签名器中,本网站绝不会看到它。",
"reviews.dialog.ratingLegend": "您的评分", "reviews.dialog.ratingLegend": "您的评分",
"reviews.dialog.starsSr.one": "{n} 星", "reviews.dialog.starsSr.one": "{n} 星",
"reviews.dialog.starsSr.other": "{n} 星", "reviews.dialog.starsSr.other": "{n} 星",
@@ -308,7 +322,6 @@
"reviews.dialog.waitingSigner": "等待您的签字人", "reviews.dialog.waitingSigner": "等待您的签字人",
"reviews.dialog.confirmClose": "关闭此评论?您的草稿将保留至您离开本网站为止。", "reviews.dialog.confirmClose": "关闭此评论?您的草稿将保留至您离开本网站为止。",
"reviews.dialog.why.login": "登录以发布", "reviews.dialog.why.login": "登录以发布",
"reviews.dialog.why.rating": "选择评分",
"reviews.dialog.why.words": "写几句话", "reviews.dialog.why.words": "写几句话",
"reviews.dialog.why.more.one": "{n} 更多字符", "reviews.dialog.why.more.one": "{n} 更多字符",
"reviews.dialog.why.more.other": "{n} 更多字符", "reviews.dialog.why.more.other": "{n} 更多字符",
@@ -325,7 +338,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": "评论",
@@ -369,6 +382,10 @@
"login.status.waitingSigner": "等待您的签字人", "login.status.waitingSigner": "等待您的签字人",
"login.status.connecting": "与您的签署人建立联系", "login.status.connecting": "与您的签署人建立联系",
"login.status.connected": "已连接", "login.status.connected": "已连接",
"login.toast.loggedIn": "已登录",
"login.toast.fetchingProfile": "正在从中继服务器获取你的 Nostr 资料…",
"login.toast.profile": "资料:{name}",
"login.toast.noProfile": "中继服务器上没有找到资料",
"login.method.extension": "扩展", "login.method.extension": "扩展",
"login.method.remote": "远程", "login.method.remote": "远程",
"login.method.primal": "Primal", "login.method.primal": "Primal",
@@ -400,7 +417,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 +678,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": "它的闪电节点当时同样无法访问,因此既不能铸造也不能熔回。"
} }
+17 -3
View File
@@ -23,8 +23,16 @@ 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;
/**
* Catalog namespaces this page's islands need on top of `CLIENT_NAMESPACES`.
*
* The home page passes `['home']`: its three grids refresh from the API and rewrite
* their own "All 56 mints →" links, so those strings have to reach the browser. They
* are inlined on the one page that reads them rather than on all 1,300.
*/
clientNamespaces?: readonly string[];
ogType?: string; ogType?: string;
/** /**
* A real page that should not be in the index. * A real page that should not be in the index.
@@ -70,7 +78,7 @@ interface Props {
const { const {
title, description, current, mintCount, ogType = 'website', title, description, current, mintCount, ogType = 'website',
noindex = false, offGraph = false, schema = [], image = OG_IMAGE, noindex = false, offGraph = false, schema = [], image = OG_IMAGE,
imageAlt, imageAlt, clientNamespaces = [],
} = Astro.props; } = Astro.props;
/* /*
@@ -118,7 +126,7 @@ const ogAlternates = LOCALES.filter((l) => l.code !== locale).map((l) => l.og);
* travels in the HTML the page was sending anyway, costs no extra request, and is on * travels in the HTML the page was sending anyway, costs no extra request, and is on
* screen before the first island has finished downloading. * screen before the first island has finished downloading.
*/ */
const i18nPayload = JSON.stringify({ locale, catalog: clientCatalog(locale) }).replace(/</g, '\\u003c'); const i18nPayload = JSON.stringify({ locale, catalog: clientCatalog(locale, clientNamespaces) }).replace(/</g, '\\u003c');
/** /**
* The social card, absolute. * The social card, absolute.
@@ -130,6 +138,9 @@ const i18nPayload = JSON.stringify({ locale, catalog: clientCatalog(locale) }).r
*/ */
const imageUrl = new URL(image, site).href; const imageUrl = new URL(image, site).href;
const plausibleUrl = import.meta.env.PLAUSIBLE_URL;
const plausibleDomain = import.meta.env.PLAUSIBLE_DOMAIN;
/** /**
* This page's JSON-LD, assembled here and nowhere else. * This page's JSON-LD, assembled here and nowhere else.
* *
@@ -267,6 +278,9 @@ const noscriptMotion =
{graph.length > 0 && ( {graph.length > 0 && (
<script type="application/ld+json" is:inline set:html={serializeGraph(graph)} /> <script type="application/ld+json" is:inline set:html={serializeGraph(graph)} />
)} )}
{plausibleUrl && plausibleDomain && (
<script is:inline defer data-domain={plausibleDomain} src={plausibleUrl}></script>
)}
</head> </head>
<body> <body>
{/* {/*
+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 {
+51 -4
View File
@@ -14,6 +14,7 @@ import { escapeHtml, iconGradient, shortNpub, truncateMiddle, wireCopyButtons }
import { useI18n } from '../i18n/client'; import { useI18n } from '../i18n/client';
import type { Translator } from '../i18n/translate'; import type { Translator } from '../i18n/translate';
import { closeModal, openModal, wireModal } from './modal'; import { closeModal, openModal, wireModal } from './modal';
import { showToast, type Toast } from './toast';
import { import {
clearSession, clearSession,
connectBunker, connectBunker,
@@ -310,14 +311,51 @@ function loggedInHtml(session: Session, npub: string, profile: Profile | null):
); );
} }
/** How long the login toast stays once it has nothing left to wait for. */
const TOAST_SETTLED_MS = 3000;
/**
* Say that the login landed, while the header cannot yet say it.
*
* The popup closes as soon as the signer answers, but the control it closes onto
* still reads "Log in" until a kind 0 comes back from the relays: a round trip that
* takes seconds on a cold cache and up to `PROFILE_TIMEOUT_MS` when a relay is dead.
* Those seconds looked like a login that quietly failed, which is the one reading
* that must never be available. So the toast confirms the half that is already true
* and names what the other half is waiting on.
*/
function announceLogin(): Toast {
const t = useI18n();
return showToast({
title: t('login.toast.loggedIn'),
body: t('login.toast.fetchingProfile'),
tone: 'busy',
duration: null, // Pinned: the profile lookup below is what ends it.
});
}
/** Close the login toast out, saying who the relays turned out to name. */
function settleLogin(toast: Toast, profile: Profile | null, t: Translator): void {
const name = profileName(profile);
toast.update({
// The name is the reader's own, out of their kind 0. `showToast` sets it as text.
body: name ? t('login.toast.profile', { name }) : t('login.toast.noProfile'),
tone: 'done',
duration: TOAST_SETTLED_MS,
});
}
/** /**
* Draw the header for the session as it stands. * Draw the header for the session as it stands.
* *
* Runs twice on a restored session: once from the cache, immediately, so the avatar * Runs twice on a restored session: once from the cache, immediately, so the avatar
* and name are there at first paint, and again if the relays turn up something newer. * and name are there at first paint, and again if the relays turn up something newer.
* Neither pass changes the control's height. * Neither pass changes the control's height.
*
* `announce` is set only by a fresh login, never by a restore or a redraw: a toast on
* every page arrival would be noise, and the wait it explains only exists once.
*/ */
async function renderHeader(): Promise<void> { async function renderHeader(announce = false): Promise<void> {
const root = headerRoot(); const root = headerRoot();
if (!root) return; if (!root) return;
@@ -328,10 +366,17 @@ async function renderHeader(): Promise<void> {
return; return;
} }
const toast = announce ? announceLogin() : null;
const npub = await toNpub(session.pubkey); const npub = await toNpub(session.pubkey);
const cached = await resolveProfile(session.pubkey); const cached = await resolveProfile(session.pubkey);
if (readSession()?.pubkey !== session.pubkey) return; // Logged out while we waited. if (readSession()?.pubkey !== session.pubkey) {
// Logged out while we waited. Nothing left to confirm, so take the toast with it.
toast?.dismiss();
return;
}
root.innerHTML = loggedInHtml(session, npub, cached); root.innerHTML = loggedInHtml(session, npub, cached);
if (toast) settleLogin(toast, cached, useI18n());
} }
function setMenu(open: boolean): void { function setMenu(open: boolean): void {
@@ -397,8 +442,10 @@ export function initAuth(): void {
if (wired) return; if (wired) return;
wired = true; wired = true;
onSessionChange(() => { onSessionChange((session) => {
void renderHeader(); // A session arriving here is always a fresh login: it is emitted on write, and a
// restored session is read, not written. A logout redraws without a toast.
void renderHeader(session !== null);
}); });
// The signer talks back through here: reconnecting, waiting, approve-in-a-window. // The signer talks back through here: reconnecting, waiting, approve-in-a-window.
+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(' + ');
}
+316
View File
@@ -0,0 +1,316 @@
/**
* The mint card, as HTML strings, and the hydration that puts them on a page.
*
* `MintCard.astro` renders the same card at build time and cannot run in the browser, so
* this is its parallel renderer — the same relationship `review-cards.ts` has with the
* reviews panel. The two must agree on every class name and every `data-*` attribute,
* because the sort, the filter, the rank chips and the shared-element view transitions
* on the three index pages all read the DOM rather than any model:
*
* data-mint-card what the sort collects and the transition arms
* data-name / data-domain what the search box matches
* data-status "hide offline", and the online/offline counts
* data-score / -rating / -reviews the sort keys
* data-last-review / -last-online the other two sort keys
* data-vt-icon / data-vt-name shared-element names, applied on click
*
* Why this exists at all: /mints was a snapshot of whatever the API held when `astro
* build` ran, and stayed that until the next build. A mint indexed at noon was reviewable
* immediately — the 404 resolver saw to that — and simply had no card until the nightly
* rebuild. The list now refetches after paint. The prerendered cards stay exactly as they
* were: they are the first paint, they are what a crawler and a reader with no JavaScript
* get, and this only ever replaces them with something newer.
*
* Nothing here talks to a relay. Card counts come from the API's ingested aggregates,
* which is what they always were; review *bodies* remain a detail-page and /reviews
* concern. See docs/dynamic-mint-data.md.
*
* Everything interpolated goes through `escapeHtml`. A mint's name comes from its own
* `/v1/info` — a string an operator controls — and this builds markup with strings.
*/
import {
baseUrlFromKey,
federationIdFromSlug,
getMintWarnings,
mintChip,
type MintListItem,
} from '@cashumints/shared';
import { apiBase, escapeHtml, iconGradient } from './client';
import { targetPath } from './feed-resolve';
import { displayDomain, initials, transitionName } from './format';
import { localePath, splitLocale } from '../i18n/routing';
import type { Locale } from '../i18n/config';
import { useI18n } from '../i18n/client';
import { formatters, starString, type Formatters } from '../i18n/format';
import { chipStrings, statusLabel, warningOptions } from '../i18n/mint';
import { initReveal } from '../scripts/reveal';
export interface CardOptions {
/** 1-based position in the default order. Omitted, the card has no rank chip. */
rank?: number;
/** Entrance delay in ms, for a grid of known size that arrives as one gesture. */
revealDelay?: number;
/**
* Render already revealed, with no entrance.
*
* Set for a card replacing one the reader is already looking at. `[data-reveal]` is
* `opacity: 0` until `.in` lands, so without this a hydration would fade the whole
* visible grid back in a second after load — an animation that says "something
* changed" about forty cards where at most one did.
*/
revealed?: boolean;
}
/**
* One card's markup.
*
* `f` carries both the translator and the locale's number and date formatting, and
* `locale` is what prefixes the href. Both are passed in rather than read here, because
* a grid renders dozens of these and rebuilding the formatter per card would be the
* expensive part of the whole hydration.
*/
export function mintCardHtml(
mint: MintListItem,
locale: Locale,
f: Formatters,
options: CardOptions = {},
): string {
const t = f.t;
const { rank, revealDelay, revealed } = options;
/*
* One card, all three ecosystems — the same reasoning as MintCard.astro.
*
* A federation has no URL, so its second line is a shortened federation id rather than
* `fedimint:aeca6c…`, which is a database key. An LNURL mint's row key carries an
* `lnurl:` scheme in front of its URL, so the domain comes out of the key.
*/
const fedimint = mint.type === 'fedimint';
const lnurlBase = baseUrlFromKey(mint.url);
const domain = fedimint
? federationIdFromSlug(mint.host)
: displayDomain(lnurlBase ?? mint.url);
const name = fedimint
? mint.name ?? t('card.unnamedFederation')
: mint.name ?? domain.split('/')[0] ?? domain;
// Never `API_URL`: that is a build-machine address and a visitor's browser cannot
// reach it. `apiBase` is PUBLIC_API_URL, empty in production, which resolves /icons
// against whatever origin is serving the page.
const icon = mint.icon ? `${apiBase}${mint.icon}` : null;
const offline = mint.status === 'offline';
const announced = mint.status === 'announced';
const href = localePath(targetPath(mint), locale);
/*
* The chip, from facts the list payload now carries.
*
* It used to take one `GET /api/mints/:host` per mint to read these — fine for a build
* machine rendering the grid once a night, ruinous as an N+1 in every visitor's
* browser. `capabilities` and the two LNURL fields were added to `MintListItem` for
* exactly this. A federation has no chip and never will: `mintChip` returns null for
* one, because a federation publishes no capability list to draw a claim from.
*/
const chipSource =
mint.capabilities || mint.max_withdrawable_msat !== undefined || mint.funding_available !== undefined
? {
type: mint.type,
status: mint.status,
last_online: mint.last_online,
capabilities: mint.capabilities ?? null,
max_withdrawable_msat: mint.max_withdrawable_msat ?? null,
funding_available: mint.funding_available ?? null,
}
: null;
const chip = chipSource
? mintChip(getMintWarnings(chipSource, warningOptions(f)), chipStrings(t))
: null;
// Falls back to the rating split when there is no distribution, exactly as the build
// does for /mints: a 4.6 average is roughly 92% positive, which is what the bar says.
const pos =
mint.rating_avg === null ? 0 : Math.round(((mint.rating_avg - 1) / 4) * 100);
const neg = mint.rating_avg === null ? 0 : 100 - pos;
const vtIcon = escapeHtml(transitionName('icon', mint.host));
const vtName = escapeHtml(transitionName('name', mint.host));
const iconHtml = icon
? `<img class="mc-icon" src="${escapeHtml(icon)}" alt="" width="42" height="42" ` +
`loading="lazy" decoding="async" data-vt-icon="${vtIcon}">`
: `<span class="mc-icon" style="background:${escapeHtml(iconGradient(domain))}" ` +
`aria-hidden="true" data-vt-icon="${vtIcon}">${escapeHtml(initials(name))}</span>`;
const statsHtml =
mint.rating_avg === null
? `<span class="mc-none">${escapeHtml(t('card.noRatings'))}</span>`
: `<span class="mc-rating">` +
`<span class="mc-score">${escapeHtml(f.decimal(mint.rating_avg))}</span>` +
`<span class="mc-stars" aria-hidden="true">${starString(mint.rating_avg)}</span>` +
`</span>`;
/*
* "Announced" has no "last seen" to print, and printing one anyway is exactly the fake
* status this site refuses to show. What it has instead is the fact that nothing has
* checked it, said in as many words.
*/
const last = announced
? t('card.notChecked')
: offline
? mint.last_online
? t('card.lastSeen', { when: f.relative(mint.last_online) })
: t('card.neverSeen')
: mint.last_review_at
? t('card.reviewed', { when: f.relative(mint.last_review_at) })
: t('card.noReviews');
const classes = ['mint-card'];
if (offline) classes.push('is-offline');
if (revealed) classes.push('in');
return (
`<a class="${classes.join(' ')}" href="${escapeHtml(href)}" data-reveal` +
(revealDelay === undefined ? '' : ` data-reveal-delay="${revealDelay}"`) +
` data-mint-card` +
` data-name="${escapeHtml(name.toLowerCase())}"` +
` data-domain="${escapeHtml(domain.toLowerCase())}"` +
` data-status="${escapeHtml(mint.status)}"` +
` data-score="${mint.score}"` +
` data-rating="${mint.rating_avg ?? 0}"` +
` data-reviews="${mint.review_count}"` +
` data-last-review="${mint.last_review_at ?? 0}"` +
` data-last-online="${mint.last_online ?? 0}">` +
`<div class="mc-top">` +
iconHtml +
`<span class="mc-id">` +
`<span class="mc-name" data-vt-name="${vtName}">${escapeHtml(name)}</span>` +
`<span class="mc-domain${fedimint ? ' mono' : ''}">${escapeHtml(domain)}</span>` +
`</span>` +
(rank === undefined
? ''
: `<span class="mc-rank${rank === 1 ? ' gold' : ''}">#${rank}</span>`) +
`</div>` +
`<div class="mc-stats">` +
statsHtml +
`<span class="mc-reviews">${escapeHtml(t('card.reviews', { n: mint.review_count }))}</span>` +
`</div>` +
`<div class="mc-bar" aria-hidden="true"><span class="mc-bar-fill">` +
(pos > 0 ? `<span class="pos" style="width:${pos}%"></span>` : '') +
(neg > 0 ? `<span class="neg" style="width:${neg}%"></span>` : '') +
`</span></div>` +
`<div class="mc-foot">` +
`<span class="mc-dot ${escapeHtml(mint.status)}"></span>` +
`<span class="mc-status">${escapeHtml(statusLabel(mint.status, t))}</span>` +
(chip ? `<span class="mc-chip ${escapeHtml(chip.severity)}">${escapeHtml(chip.label)}</span>` : '') +
`<span class="last">${escapeHtml(last)}</span>` +
`</div>` +
`</a>`
);
}
/**
* One ecosystem's full listing, or null.
*
* Null on anything at all going wrong, and the caller's job is then to do nothing: the
* prerendered grid is already on screen and correct as of the last build, so a failed
* refresh should be invisible rather than an error message about a list the reader can
* see. This is the same rule the reviews panel and the pulse ticker follow.
*/
export async function fetchListing(type: string): Promise<MintListItem[] | null> {
try {
const res = await fetch(`${apiBase}/api/mints?type=${encodeURIComponent(type)}`, {
headers: { Accept: 'application/json' },
});
if (!res.ok) return null;
const items = (await res.json()) as MintListItem[];
// A well-formed empty answer is still not a reason to empty a grid that has cards in
// it. An API serving nothing is the failure the build gate exists to catch, and a
// page that renders it as "no mints" would be this bug wearing a different hat.
return Array.isArray(items) && items.length > 0 ? items : null;
} catch {
return null;
}
}
/**
* Replace a grid's cards with freshly rendered ones.
*
* Cards whose host was already on screen and revealed are rendered revealed, so the
* common case — the list is the same list, with newer numbers — is a silent swap rather
* than forty cards fading in again. Genuinely new hosts get the ordinary entrance from
* `initReveal`, which is also what reveals anything below the fold on scroll.
*
* Returns the new card elements, in DOM order, for the caller to re-apply its sort and
* filter to.
*/
export function renderMintGrid(
grid: HTMLElement,
items: MintListItem[],
options: { ranked?: boolean; revealDelayStep?: number } = {},
): HTMLElement[] {
const { ranked = true, revealDelayStep } = options;
const t = useI18n();
const f = formatters(t);
const { locale } = splitLocale(window.location.pathname);
// Which hosts the reader can already see. Keyed by host rather than by index: the list
// may have grown, shrunk or reordered, and the question is per mint.
const revealed = new Set<string>();
for (const card of grid.querySelectorAll<HTMLAnchorElement>('[data-mint-card]')) {
if (card.classList.contains('in')) {
const host = card.getAttribute('href')?.split('/').pop();
if (host) revealed.add(decodeURIComponent(host));
}
}
grid.innerHTML = items
.map((mint, i) =>
mintCardHtml(mint, locale, f, {
...(ranked ? { rank: i + 1 } : {}),
...(revealDelayStep === undefined ? {} : { revealDelay: i * revealDelayStep }),
revealed: revealed.has(mint.host),
}),
)
.join('');
initReveal(grid);
return [...grid.querySelectorAll<HTMLElement>('[data-mint-card]')];
}
export interface HydrateOptions {
/** `cashu`, `fedimint` or `lnurl`. */
type: string;
/** The grid to rebuild. */
grid: HTMLElement;
/** Keep only the first N, for the home page's top-six strips. */
limit?: number;
ranked?: boolean;
revealDelayStep?: number;
/** Called with the new cards and the payload they were built from, on success only. */
onReplaced?: (cards: HTMLElement[], items: MintListItem[]) => void;
}
/**
* Fetch one ecosystem and rebuild its grid, after paint.
*
* Deliberately silent on failure — see `fetchListing`. Deliberately unconditional on
* success: the API is the newer of the two by construction, since the prerendered grid
* is a copy of what this same endpoint said at build time.
*/
export async function hydrateMintGrid(options: HydrateOptions): Promise<void> {
const { type, grid, limit, ranked, revealDelayStep, onReplaced } = options;
const all = await fetchListing(type);
if (!all) return;
// The reader may have navigated away while the fetch was in flight. The router has
// already swapped this grid out of the document, so rendering into it (and telling the
// caller to re-apply filters against cards nobody can see) is wasted work.
if (!grid.isConnected) return;
const items = limit === undefined ? all : all.slice(0, limit);
const cards = renderMintGrid(grid, items, {
...(ranked === undefined ? {} : { ranked }),
...(revealDelayStep === undefined ? {} : { revealDelayStep }),
});
onReplaced?.(cards, all);
}
+16 -28
View File
@@ -55,19 +55,6 @@ export interface BuildReview {
* characters or matching a common test string do not qualify. They still appear in * characters or matching a common test string do not qualify. They still appear in
* full on the mint page, nothing is hidden, this strip is just a shop window. * full on the mint page, nothing is hidden, this strip is just a shop window.
*/ */
/** How many candidates to gather per card shown, so the ordering has a real choice. */
const POOL_FACTOR = 12;
/**
* How recent a review must still be to be promoted for having a named author.
*
* The ordering below prefers reviews whose author has a kind 0, and this is the leash
* on that preference. Without it the strip would headline a named review from two
* years ago under a heading that says "Latest reviews"; with it, a named review from
* within the season can lead and anything older cannot.
*/
const PROMOTE_MAX_AGE_S = 90 * 24 * 60 * 60;
const TEST_STRINGS = new Set([ const TEST_STRINGS = new Set([
'test', 'testing', 'test test', 'hello', 'hi', 'ok', 'okay', 'good', 'nice', 'test', 'testing', 'test test', 'hello', 'hi', 'ok', 'okay', 'good', 'nice',
'great', 'cool', 'gm', 'a', '.', '..', '...', 'asdf', 'qwerty', '123', 'great', 'cool', 'gm', 'a', '.', '..', '...', 'asdf', 'qwerty', '123',
@@ -227,29 +214,30 @@ export async function fetchLatestReviews(
profile: null, profile: null,
}); });
if (candidates.length >= limit * POOL_FACTOR) break; // The scan runs newest first, so the first `limit` survivors are the latest
// `limit` reviews. Nothing further down the list can outrank them.
if (candidates.length >= limit) break;
} }
const profiles = await fetchProfiles(candidates.map((c) => c.pubkey)); const profiles = await fetchProfiles(candidates.map((c) => c.pubkey));
for (const review of candidates) review.profile = profiles.get(review.pubkey) ?? null; for (const review of candidates) review.profile = profiles.get(review.pubkey) ?? null;
/* /*
* Recent reviews with a named author come first; everything else stays newest * Strict recency, newest first, as the last thing that happens to this list.
* first behind them.
* *
* Most review keys have published exactly one event in their life: the review * The heading says "Latest reviews", so the order under it is the event
* itself. Nothing can be fetched for them, so a strict recency order fills this * `created_at` and nothing else. This used to float reviews whose author had a
* strip with anonymous npubs while a named review sits a few rows below the * kind 0 to the front, which read as broken on the page: a named review from two
* cut. This is the home page shop window, which already drops junk bodies, so * months ago sat between two reviews from this week, and the card feet said so,
* it prefers a review a reader can attach a person to. Nothing is hidden: * because the "2mo ago" label is formatted from the very same `created_at` this
* every review is on its mint page, and PROMOTE_MAX_AGE_S keeps "latest" * sorts on. Author names are still resolved and still shown — they just no longer
* meaning latest. * decide the order.
*
* Sorted here rather than left to the scan above, so that the invariant holds
* whatever the gathering loop does later, and `id` breaks ties between two events
* that share a second so two builds of the same events agree.
*/ */
const cutoff = Math.floor(Date.now() / 1000) - PROMOTE_MAX_AGE_S; candidates.sort((a, b) => b.created_at - a.created_at || a.id.localeCompare(b.id));
const promoted = (review: BuildReview): number =>
review.profile?.found && review.created_at >= cutoff ? 1 : 0;
candidates.sort((a, b) => promoted(b) - promoted(a) || b.created_at - a.created_at);
return candidates.slice(0, limit); return candidates.slice(0, limit);
} catch { } catch {

Some files were not shown because too many files have changed in this diff Show More