@@ -0,0 +1,73 @@
|
||||
# Translation glossary
|
||||
|
||||
The terms this site keeps consistent across every language. Decide a term once here,
|
||||
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.
|
||||
|
||||
Adding a fourth language starts here, not in the catalog. Fill in the column, then
|
||||
translate `en.json` with it beside you.
|
||||
|
||||
## House rules, in every language
|
||||
|
||||
- **Sentence case.** Headings, buttons, labels. Not Title Case, not ALL CAPS.
|
||||
(Small caps on section labels come from CSS, so the source string stays sentence case.)
|
||||
- **No em dashes.** Ever, in any language. Use a comma, a colon, or two sentences.
|
||||
Spanish and Dutch both reach for the em dash naturally; do not.
|
||||
- **Specific over clever.** "Offline since 3 Aug 2026" beats "this mint has gone quiet".
|
||||
- **Second person, informal.** Spanish `tú`, Dutch `je`. Never `usted`, never `u`.
|
||||
- **Protocol terms stay English.** See the table.
|
||||
- **Never translate data.** Review text, reviewer names, mint names, mint descriptions,
|
||||
MOTD, URLs, npubs, version strings. Those are source data, not site copy.
|
||||
|
||||
## The terms
|
||||
|
||||
| English | Spanish | Dutch | Note |
|
||||
| --- | --- | --- | --- |
|
||||
| mint (noun) | el mint | de mint | Never "casa de la moneda" / "munt". It is the Cashu term. |
|
||||
| mint (verb) | acuñar | minten | Spanish spells it out once per page then uses it plainly. |
|
||||
| melt (verb) | fundir | melten | Same rule as mint. |
|
||||
| minting | la acuñación | minten | |
|
||||
| melting | el fundido | melten | |
|
||||
| ecash | ecash | ecash | Never "dinero electrónico". |
|
||||
| sat / sats | sat / sats | sat / sats | Never translated, never inflected. |
|
||||
| Lightning | Lightning | Lightning | Capital L, no article games. |
|
||||
| NUT / NUT-04 | NUT / NUT-04 | NUT / NUT-04 | The number is the name. The plain-language label beside it IS translated. |
|
||||
| Nostr | Nostr | Nostr | |
|
||||
| NIP-87, NIP-07, NIP-46 | unchanged | unchanged | |
|
||||
| relay | el relay | de relay | Plural: relays. Not "retransmisor" / "doorgeefluik". |
|
||||
| signer | el firmante | de signer | Dutch keeps the loanword: NL Nostr users say "signer". |
|
||||
| review (the written thing) | la reseña | de review | |
|
||||
| rating (the stars) | la valoración | de beoordeling | Deliberately a different word from review in all three. |
|
||||
| reviewer | quien escribió la reseña | wie de review schreef | Avoid a noun that implies a role or an account. |
|
||||
| wallet | el monedero | de wallet | Spanish follows bitcoin.org/es. Dutch keeps the loanword. |
|
||||
| key (private/public) | la clave | de sleutel | |
|
||||
| npub / pubkey | npub / pubkey | npub / pubkey | |
|
||||
| custodial | custodial | custodiaal | Always followed by what it means, never left bare. |
|
||||
| online (a mint) | en línea | online | |
|
||||
| offline (a mint) | sin conexión | offline | Shorter than "fuera de línea" and standard for a status. |
|
||||
| unreachable | no responde | onbereikbaar | |
|
||||
| uptime | disponibilidad | uptime | |
|
||||
| indexed | indexado | geïndexeerd | |
|
||||
| to probe (a mint) | comprobar | controleren | |
|
||||
| mint URL | la URL del mint | de mint-URL | |
|
||||
| bunker link | el enlace bunker | de bunker-link | `bunker://` itself is never translated. |
|
||||
| seed phrase | la frase semilla | de seed phrase | |
|
||||
|
||||
## Two decisions worth knowing
|
||||
|
||||
**Spanish spells out mint and melt on first use, then stops.** `acuñar (mint)` the first
|
||||
time a page uses it, `acuñar` after that. Spanish-speaking Bitcoin readers know the
|
||||
English verbs, but a page that only ever says "mint" reads as untranslated. Dutch does
|
||||
not need this: `minten` and `melten` are what NL and BE Nostr users actually write.
|
||||
|
||||
**Dutch keeps more English than Spanish does.** `review`, `wallet`, `signer`, `online`,
|
||||
`uptime` are all ordinary Dutch now, and translating them ("beoordeling" for the written
|
||||
review, "portemonnee", "ondertekenaar") reads as stiff or, worse, as a different concept.
|
||||
Spanish translates more, because Spanish tech writing does.
|
||||
|
||||
## Safety copy
|
||||
|
||||
The warning banners (`mint.warnings.*`) and the disclaimer are the copy someone reads
|
||||
before they risk money. Translate them for **plain, unambiguous meaning**, not word for
|
||||
word. If a literal translation is ambiguous about whether money can get out of a mint,
|
||||
the translation is wrong even if every word is right. Short sentences. No hedging.
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Island-side i18n. This is the file that ships.
|
||||
*
|
||||
* The page carries its own copy: `Base.astro` emits one `<script type="application/json"
|
||||
* data-i18n>` holding the current locale's client-facing keys, already resolved against
|
||||
* English. So a Spanish page ships Spanish strings and no others, in the HTML it was
|
||||
* going to send anyway. No second request, no catalog chunk per language, no waterfall
|
||||
* before an island can draw a label, and nothing in a shared JS chunk that has to be
|
||||
* one language for everybody.
|
||||
*
|
||||
* The alternative, a dynamic `import('./es.json')` keyed on locale, also avoids
|
||||
* shipping three languages, but it costs a round trip on the critical path of every
|
||||
* island and it leaves all three catalogs sitting in `dist/_astro/`. This does not.
|
||||
*/
|
||||
import { DEFAULT_LOCALE, type Locale } from './config.js';
|
||||
import { createTranslator, type Catalog, type Translator } from './translate.js';
|
||||
|
||||
interface Payload {
|
||||
locale: Locale;
|
||||
catalog: Catalog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the payload out of the current document.
|
||||
*
|
||||
* Read every time rather than cached at module scope, because the view transition
|
||||
* router swaps the document under this module without reloading it. An island that
|
||||
* cached its translator on first load would keep drawing Spanish after the reader
|
||||
* clicked through to a Dutch page. Islands call `useI18n()` inside their `onReady`
|
||||
* setup, which runs again on every arrival, so this reads the page that is actually on
|
||||
* screen.
|
||||
*/
|
||||
function readPayload(): Payload {
|
||||
const tag = document.querySelector<HTMLScriptElement>('script[data-i18n]');
|
||||
if (!tag) return { locale: DEFAULT_LOCALE, catalog: {} };
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(tag.textContent ?? '{}') as Partial<Payload>;
|
||||
return {
|
||||
locale: parsed.locale ?? DEFAULT_LOCALE,
|
||||
catalog: parsed.catalog ?? {},
|
||||
};
|
||||
} catch {
|
||||
// A payload that will not parse is a build bug, not a runtime condition. Falling
|
||||
// back to the key humaniser beats throwing inside every island on the page.
|
||||
return { locale: DEFAULT_LOCALE, catalog: {} };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One translator per document, rebuilt when the router swaps in a new one.
|
||||
*
|
||||
* Keyed on the script element itself: a new page brings a new element, and identity
|
||||
* comparison is cheaper and more honest than re-parsing the JSON on every `t()`.
|
||||
*/
|
||||
let cachedTag: Element | null = null;
|
||||
let cached: Translator | null = null;
|
||||
|
||||
export function useI18n(): Translator {
|
||||
const tag = document.querySelector('script[data-i18n]');
|
||||
if (cached && tag === cachedTag) return cached;
|
||||
|
||||
const { locale, catalog } = readPayload();
|
||||
// The payload is already resolved against English at build time, so the fallback
|
||||
// catalog here is the same object: there is nothing further to fall back to.
|
||||
cached = createTranslator(locale, catalog, catalog);
|
||||
cachedTag = tag;
|
||||
return cached;
|
||||
}
|
||||
|
||||
/** The locale this page is in, for `Intl` calls that do not go through `t()`. */
|
||||
export function currentLocale(): Locale {
|
||||
return useI18n().locale;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* The locale table. Adding a language is this file, `xx.json`, and nothing else.
|
||||
*
|
||||
* Everything downstream reads from `LOCALES`: the Astro i18n config, the language
|
||||
* switcher, the hreflang set, the sitemap, the suggestion bar and the catalog loader.
|
||||
* There is deliberately no second list to keep in step.
|
||||
*/
|
||||
|
||||
export const DEFAULT_LOCALE = 'en' as const;
|
||||
|
||||
export interface LocaleInfo {
|
||||
/** URL prefix and catalog filename. Also the value of `<html lang>`. */
|
||||
code: string;
|
||||
/**
|
||||
* The language's own name for itself, for the switcher. Never a flag: a flag is a
|
||||
* country, and Spanish is not Spain.
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* BCP 47 tag handed to Intl. Region included because number and date formatting
|
||||
* differ by region even when the language does not.
|
||||
*/
|
||||
intl: string;
|
||||
/** `og:locale`. Underscored, region required by the OpenGraph spec. */
|
||||
og: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Order matters: this is the order the switcher renders in, and the default locale
|
||||
* comes first.
|
||||
*/
|
||||
export const LOCALES: LocaleInfo[] = [
|
||||
{ code: 'en', label: 'English', intl: 'en-GB', og: 'en_US' },
|
||||
{ code: 'es', label: 'Español', intl: 'es-ES', og: 'es_ES' },
|
||||
{ code: 'nl', label: 'Nederlands', intl: 'nl-NL', og: 'nl_NL' },
|
||||
];
|
||||
|
||||
export type Locale = string;
|
||||
|
||||
/** Just the codes, for the Astro config and for validating a URL segment. */
|
||||
export const LOCALE_CODES: string[] = LOCALES.map((l) => l.code);
|
||||
|
||||
/** Every locale except the default, which is the set that carries a URL prefix. */
|
||||
export const PREFIXED_LOCALES: string[] = LOCALE_CODES.filter((code) => code !== DEFAULT_LOCALE);
|
||||
|
||||
export function isLocale(value: unknown): value is Locale {
|
||||
return typeof value === 'string' && LOCALE_CODES.includes(value);
|
||||
}
|
||||
|
||||
export function localeInfo(locale: Locale): LocaleInfo {
|
||||
return LOCALES.find((l) => l.code === locale) ?? LOCALES[0]!;
|
||||
}
|
||||
|
||||
/** The Intl tag for a locale, for `Intl.NumberFormat` and friends. */
|
||||
export function intlTag(locale: Locale): string {
|
||||
return localeInfo(locale).intl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Namespaces whose keys are sent to the browser.
|
||||
*
|
||||
* Islands render strings at runtime, so their copy has to reach the client. The static
|
||||
* half of the site (home page sections, about, the legal pages) is rendered at build
|
||||
* time and its copy never needs to ship. Splitting on the namespace keeps the inline
|
||||
* payload to roughly a third of the catalog.
|
||||
*
|
||||
* `scripts/check-i18n.mjs` fails the build if an island reaches for a key outside this
|
||||
* list, so the split cannot silently rot into a missing string on a real page.
|
||||
*/
|
||||
export const CLIENT_NAMESPACES = [
|
||||
'common',
|
||||
'time',
|
||||
'status',
|
||||
'pulse',
|
||||
'mints',
|
||||
'mint',
|
||||
'nut',
|
||||
'reviews',
|
||||
'feed',
|
||||
'login',
|
||||
'signer',
|
||||
'notfound',
|
||||
] as const;
|
||||
@@ -0,0 +1,550 @@
|
||||
{
|
||||
"site.name": "Cashumints.space",
|
||||
"common.close": "Close",
|
||||
"common.cancel": "Cancel",
|
||||
"common.done": "Done",
|
||||
"common.back": "Back",
|
||||
"common.retry": "Retry",
|
||||
"common.connect": "Connect",
|
||||
"common.copied": "Copied",
|
||||
"common.copyFull": "Copy full {kind}",
|
||||
"common.previousPage": "Previous page",
|
||||
"common.nextPage": "Next page",
|
||||
"common.reviewPages": "Review pages",
|
||||
"time.never": "never",
|
||||
"time.justNow": "just now",
|
||||
"time.none": "-",
|
||||
"time.unknownDate": "an unknown date",
|
||||
"time.ago.minute": "{n}m ago",
|
||||
"time.ago.hour": "{n}h ago",
|
||||
"time.ago.day": "{n}d ago",
|
||||
"time.ago.week": "{n}w ago",
|
||||
"time.ago.month": "{n}mo ago",
|
||||
"time.ago.year": "{n}y ago",
|
||||
"time.short.minute": "{n}m",
|
||||
"time.short.hour": "{n}h",
|
||||
"time.short.day": "{n}d",
|
||||
"time.short.week": "{n}w",
|
||||
"time.short.month": "{n}mo",
|
||||
"time.short.year": "{n}y",
|
||||
"status.online": "Online",
|
||||
"status.offline": "Offline",
|
||||
"status.degraded": "Unreachable",
|
||||
"status.unknown": "Not checked yet",
|
||||
"nav.skip": "Skip to content",
|
||||
"nav.main": "Main",
|
||||
"nav.mints": "Mints",
|
||||
"nav.reviews": "Reviews",
|
||||
"nav.wallets": "Wallets",
|
||||
"nav.about": "About",
|
||||
"nav.searchMints": "Search {n} mints",
|
||||
"lang.label": "Language",
|
||||
"lang.switcher": "Choose a language",
|
||||
"footer.copyright": "© {year} Cashumints.space",
|
||||
"footer.allMints": "All mints",
|
||||
"footer.allReviews": "All reviews",
|
||||
"footer.wallets": "Wallets",
|
||||
"footer.about": "About",
|
||||
"footer.disclaimer": "Disclaimer",
|
||||
"footer.terms": "Terms",
|
||||
"footer.privacy": "Privacy",
|
||||
"footer.madeBy": "Made with ❤️ by {azzamo} · Powered by Cashu and NIP-87",
|
||||
"footer.warning": "Mints are custodial. A mint can disappear with your money.",
|
||||
"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.hero.title": "Find a Cashu mint",
|
||||
"home.hero.titleEm": "you can trust",
|
||||
"home.hero.lede": "Every mint on the Nostr network, with real community reviews. No gatekeepers, just signed reviews you can verify yourself.",
|
||||
"home.search.label": "Search mints by name, domain, or URL",
|
||||
"home.search.placeholder": "Search by name, domain, or paste a mint URL…",
|
||||
"home.filters.mostReviewed": "Most reviewed",
|
||||
"home.filters.highestRated": "Highest rated",
|
||||
"home.filters.recentlyActive": "Recently active",
|
||||
"home.filters.websockets": "Supports WebSockets",
|
||||
"home.top.title": "Top mints",
|
||||
"home.top.sub": "ranked by reviews and rating",
|
||||
"home.top.all": "All {n} mints →",
|
||||
"home.latest.title": "Latest reviews",
|
||||
"home.latest.sub": "live from the Nostr network",
|
||||
"home.latest.prev": "Previous reviews",
|
||||
"home.latest.next": "More reviews",
|
||||
"home.latest.all": "All reviews →",
|
||||
"home.latest.empty": "No reviews could be read from the relays just now. Open any mint page to read and write reviews directly.",
|
||||
"home.why.title": "Why trust these reviews?",
|
||||
"home.why.signed.title": "Signed, not submitted",
|
||||
"home.why.signed.body": "Every review is a {link} event signed by its author's Nostr key. Nothing here is edited, ranked, or removed by us.",
|
||||
"home.why.signed.link": "NIP-87",
|
||||
"home.why.verify.title": "Verify it yourself",
|
||||
"home.why.verify.body": "Reviews live on public Nostr relays, not our database. Point any client at the same relays and you'll see the same data.",
|
||||
"home.why.checked.title": "Checked continuously",
|
||||
"home.why.checked.body": "We probe every mint for uptime, software version, limits, and supported NUTs, so the specs you see are current, not self-reported.",
|
||||
"home.cta.title": "Thinking about a mint? Read it first.",
|
||||
"home.cta.body": "A mint holds your ecash. Read what other people found before you put sats in, and leave a review when you have used one yourself.",
|
||||
"home.cta.explore": "Explore all mints",
|
||||
"home.cta.learn": "Learn about Cashu",
|
||||
"pulse.mintsIndexed": "mints indexed",
|
||||
"pulse.reviewsOnNostr": "reviews on Nostr",
|
||||
"pulse.lastReview": "last review",
|
||||
"pulse.mintsOnline": "mints online",
|
||||
"pulse.indexer": "indexer",
|
||||
"pulse.connected": "connected",
|
||||
"pulse.stale": "stale",
|
||||
"mints.title": "All Cashu mints · Cashumints.space",
|
||||
"mints.description": "All {total} Cashu mints indexed from the Nostr network, {online} online and {offline} offline, with community ratings, uptime and mint software. Search and sort the full list.",
|
||||
"mints.heading": "All mints",
|
||||
"mints.lede": "Every mint discovered on the Nostr network, including the ones that are gone. Offline mints stay listed so you can still find and review them.",
|
||||
"mints.search.label": "Search mints by name or domain",
|
||||
"mints.search.placeholder": "Search by name or domain…",
|
||||
"mints.sort.label": "Sort mints",
|
||||
"mints.sort.score": "Best overall",
|
||||
"mints.sort.rating": "Highest rated",
|
||||
"mints.sort.reviews": "Most reviewed",
|
||||
"mints.sort.recent": "Recently reviewed",
|
||||
"mints.sort.seen": "Recently seen online",
|
||||
"mints.hideOffline": "Hide offline",
|
||||
"mints.showing": "Showing {total} mints ({online} online, {offline} offline)",
|
||||
"mints.showingFiltered": "Showing {shown} of {total} mints ({online} online, {offline} offline)",
|
||||
"mints.noResults": "No mints match. Try a domain, or paste a mint URL.",
|
||||
"mints.clearSearch": "Clear the search",
|
||||
"card.noRatings": "No ratings yet",
|
||||
"card.reviews.one": "{n} review",
|
||||
"card.reviews.other": "{n} reviews",
|
||||
"card.lastSeen": "last seen {when}",
|
||||
"card.neverSeen": "never seen online",
|
||||
"card.reviewed": "reviewed {when}",
|
||||
"card.noReviews": "no reviews yet",
|
||||
"mint.title": "{name} · Cashu mint reviews",
|
||||
"mint.description.fallback": "{name} is a Cashu mint at {domain}.",
|
||||
"mint.description.rated.one": "Rated {rating} out of 5 from {n} review signed on Nostr.",
|
||||
"mint.description.rated.other": "Rated {rating} out of 5 from {n} reviews signed on Nostr.",
|
||||
"mint.description.noReviews": "No community reviews yet.",
|
||||
"mint.description.online": "Currently online.",
|
||||
"mint.description.unreachable": "Currently unreachable.",
|
||||
"mint.breadcrumb": "Breadcrumb",
|
||||
"mint.crumbMints": "Mints",
|
||||
"mint.copyUrl": "Copy mint URL",
|
||||
"mint.qrButton": "Mint QR",
|
||||
"mint.writeReview": "Write a review",
|
||||
"mint.rankByScore": "#{rank} by score",
|
||||
"mint.tagLightning": "Lightning",
|
||||
"mint.summary": "Mint summary",
|
||||
"mint.rating.label": "Community rating",
|
||||
"mint.rating.none": "No ratings yet",
|
||||
"mint.rating.reviews.one": "{n} review",
|
||||
"mint.rating.reviews.other": "{n} reviews",
|
||||
"mint.rating.unrated.one": "{n} left no rating",
|
||||
"mint.rating.unrated.other": "{n} left no rating",
|
||||
"mint.activity.label": "Activity",
|
||||
"mint.activity.since": "since last review",
|
||||
"mint.activity.noReviews": "no reviews yet",
|
||||
"mint.activity.recent.one": "{n} review in the last 90 days",
|
||||
"mint.activity.recent.other": "{n} reviews in the last 90 days",
|
||||
"mint.limits.label": "Limits",
|
||||
"mint.limits.notPublished": "not published",
|
||||
"mint.limits.min": "min",
|
||||
"mint.limits.max": "max",
|
||||
"mint.limits.mintRow": "mint",
|
||||
"mint.limits.meltRow": "melt",
|
||||
"mint.limits.disabled": "disabled",
|
||||
"mint.limits.range": "{min} to {max} {unit}",
|
||||
"mint.limits.upTo": "up to {max} {unit}",
|
||||
"mint.limits.from": "from {min} {unit}",
|
||||
"mint.software.label": "Mint software",
|
||||
"mint.software.unknown": "unknown",
|
||||
"mint.checked": "checked {when}",
|
||||
"mint.lastSeen": "last seen {when}",
|
||||
"mint.neverReached": "never reached",
|
||||
"mint.uptime.checks": "{ok} of the last {total} checks reachable",
|
||||
"mint.uptime.percent": "{percent}% reachable over 30 days",
|
||||
"mint.uptime.description": "{ok} of the last {total} checks succeeded",
|
||||
"mint.operator.title": "From the operator",
|
||||
"mint.operator.motd": "Message of the day",
|
||||
"mint.operator.beta": "This mint describes itself as experimental or in beta. Read the operator's own words below before storing anything you cannot lose.",
|
||||
"mint.nuts.title": "Supported NUTs",
|
||||
"mint.nuts.noneOffline": "No NUT list cached for this mint, it went offline before one was recorded.",
|
||||
"mint.nuts.noneYet": "No NUT list cached for this mint yet.",
|
||||
"mint.nuts.supported": "{n} of {total} known NUTs supported",
|
||||
"mint.nuts.whatAre": "what are NUTs?",
|
||||
"mint.nutRow.disabled": "disabled",
|
||||
"mint.nutRow.notSupported": "not supported",
|
||||
"mint.details.title": "Mint details",
|
||||
"mint.details.pubkey": "Pubkey",
|
||||
"mint.details.nostr": "Nostr",
|
||||
"mint.details.email": "Email",
|
||||
"mint.details.serverTime": "Server time",
|
||||
"mint.details.firstSeen": "First seen",
|
||||
"mint.details.terms": "Terms",
|
||||
"mint.kind.identifier": "identifier",
|
||||
"mint.kind.mintPubkey": "mint pubkey",
|
||||
"mint.kind.nostrAddress": "Nostr address",
|
||||
"mint.kind.emailAddress": "email address",
|
||||
"mint.kind.npub": "npub",
|
||||
"mint.disclaimer": "This mint is custodial: it holds your money, and it can stop doing so at any time. Ratings are community opinions and online means reachable, not trustworthy.",
|
||||
"mint.disclaimerLink": "Read the full disclaimer",
|
||||
"mint.qr.title": "Add {name} to your wallet",
|
||||
"mint.qr.note": "Scan this with a Cashu wallet to add the mint, or copy the URL and paste it in.",
|
||||
"mint.qr.warn": "Adding a mint means trusting it with your money.",
|
||||
"mint.qr.whatMeans": "What that means",
|
||||
"mint.qr.wallets": "Wallets that support Cashu",
|
||||
"mint.chip.frozen": "Frozen",
|
||||
"mint.chip.noWithdrawals": "No withdrawals",
|
||||
"mint.chip.meltOnly": "Melt only",
|
||||
"mint.warnings.dayCount.one": "{n} day",
|
||||
"mint.warnings.dayCount.other": "{n} days",
|
||||
"mint.warnings.gone.lead": "Likely gone.",
|
||||
"mint.warnings.gone.body.since": "This mint has been unreachable since {date} ({days}). Treat funds held there as at risk, and do not send new funds. Details below show its last known state.",
|
||||
"mint.warnings.gone.body.neverDated": "This mint has not answered a single check since we started watching it in {month}. Treat funds held there as at risk, and do not send new funds. Anything below came from the Nostr announcement, not from the mint.",
|
||||
"mint.warnings.gone.body.never": "This mint has not answered a single check since we started watching it. Treat funds held there as at risk, and do not send new funds. Anything below came from the Nostr announcement, not from the mint.",
|
||||
"mint.warnings.gone.meta.since": "Offline since {date}",
|
||||
"mint.warnings.gone.meta.never": "Never reachable since it was discovered",
|
||||
"mint.warnings.meltDisabled.lead": "Withdrawals disabled.",
|
||||
"mint.warnings.meltDisabled.body.online": "This mint has Lightning melting (NUT-05) turned off: you can put sats in, but you cannot withdraw them via Lightning. Do not deposit until melting is back.",
|
||||
"mint.warnings.meltDisabled.body.offline": "This mint had Lightning melting (NUT-05) turned off: you can put sats in, but you cannot withdraw them via Lightning. Do not deposit until melting is back.",
|
||||
"mint.warnings.meltDisabled.meta.online": "Withdrawals currently disabled",
|
||||
"mint.warnings.meltDisabled.meta.offline": "Withdrawals were disabled when last seen",
|
||||
"mint.warnings.frozen.lead": "Mint frozen.",
|
||||
"mint.warnings.frozen.body.online": "Both minting and melting are disabled. No sats can move in or out via Lightning.",
|
||||
"mint.warnings.frozen.body.offline": "Both minting and melting were disabled. No sats can move in or out via Lightning.",
|
||||
"mint.warnings.frozen.meta.online": "Currently frozen, nothing moves in or out",
|
||||
"mint.warnings.frozen.meta.offline": "Frozen when last seen",
|
||||
"mint.warnings.offlineLong.lead": "Offline for {days}, since {date}.",
|
||||
"mint.warnings.offlineLong.body": "The mint may be gone. Everything below is its last known state, and reviews still work.",
|
||||
"mint.warnings.offlineLong.meta": "Offline since {date}",
|
||||
"mint.warnings.meltOnly.lead": "Melt only.",
|
||||
"mint.warnings.meltOnly.body.online": "New minting (NUT-04) is disabled: you cannot deposit, but existing ecash can still be withdrawn. This often means a mint is winding down, so if you hold its ecash, consider melting out.",
|
||||
"mint.warnings.meltOnly.body.offline": "New minting (NUT-04) was disabled: you cannot deposit, but existing ecash can still be withdrawn. This often means a mint is winding down, so if you hold its ecash, consider melting out.",
|
||||
"mint.warnings.meltOnly.meta.online": "Currently melt only",
|
||||
"mint.warnings.meltOnly.meta.offline": "Melt only when last seen",
|
||||
"mint.warnings.offline.lead": "Offline since {date}.",
|
||||
"mint.warnings.offline.body": "Details were last confirmed then. You can still write a review.",
|
||||
"mint.warnings.offline.meta": "Offline since {date}",
|
||||
"mint.warnings.also.meltDisabled": "Before going offline it had also disabled withdrawals.",
|
||||
"mint.warnings.also.mintDisabled": "Before going offline it had also disabled new minting.",
|
||||
"mint.warnings.also.offline": "It has also been offline since {date} ({days}).",
|
||||
"nut.0": "Notation and models",
|
||||
"nut.1": "Mint public key exchange",
|
||||
"nut.2": "Keysets and fees",
|
||||
"nut.3": "Swapping tokens",
|
||||
"nut.4": "Minting via Lightning",
|
||||
"nut.5": "Melting via Lightning",
|
||||
"nut.6": "Mint information",
|
||||
"nut.7": "Token state check",
|
||||
"nut.8": "Overpaid Lightning fees",
|
||||
"nut.9": "Restore signatures",
|
||||
"nut.10": "Spending conditions",
|
||||
"nut.11": "Pay to public key",
|
||||
"nut.12": "DLEQ proofs",
|
||||
"nut.13": "Deterministic secrets",
|
||||
"nut.14": "Hashed timelock contracts",
|
||||
"nut.15": "Multipath payments",
|
||||
"nut.16": "Animated QR codes",
|
||||
"nut.17": "WebSocket subscriptions",
|
||||
"nut.18": "Payment requests",
|
||||
"nut.19": "Cached responses",
|
||||
"nut.20": "Signed mint quotes",
|
||||
"nut.21": "Clear authentication",
|
||||
"nut.22": "Blind authentication",
|
||||
"nut.23": "BOLT11 payment method",
|
||||
"nut.24": "HTTP 402 payment required",
|
||||
"nut.25": "BOLT12 payment method",
|
||||
"reviews.title": "Reviews",
|
||||
"reviews.anon": "Anon",
|
||||
"reviews.filter.group": "Filter reviews",
|
||||
"reviews.filter.all": "All",
|
||||
"reviews.filter.five": "5★",
|
||||
"reviews.filter.critical": "Critical",
|
||||
"reviews.filter.recent": "Recent",
|
||||
"reviews.loading.one": "Loading {n} review from Nostr relays",
|
||||
"reviews.loading.other": "Loading {n} reviews from Nostr relays",
|
||||
"reviews.empty": "No reviews yet. Be the first to review this mint.",
|
||||
"reviews.emptyWritten": "No written reviews here yet. Be the first to write one.",
|
||||
"reviews.emptyFilter": "No reviews match this filter.",
|
||||
"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.srRating": "{rating} out of 5",
|
||||
"reviews.srNoRating": "No rating given",
|
||||
"reviews.anonNote": "First review from this npub, no other activity found",
|
||||
"reviews.published": "Published.",
|
||||
"reviews.publishedPartial": "Published to {accepted} of {total} relays.",
|
||||
"reviews.propagating": "Propagating, it may take a moment to appear elsewhere.",
|
||||
"reviews.summary.line.one": "{count} rating 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.other": "{count} ratings without a comment on {when}: {breakdown}",
|
||||
"reviews.summary.showOne": "Show it",
|
||||
"reviews.summary.showMany": "Show them",
|
||||
"reviews.breakdown.5": "{n} five star",
|
||||
"reviews.breakdown.4": "{n} four star",
|
||||
"reviews.breakdown.3": "{n} three star",
|
||||
"reviews.breakdown.2": "{n} two star",
|
||||
"reviews.breakdown.1": "{n} one star",
|
||||
"reviews.breakdown.none": "{n} with no rating",
|
||||
"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.ratingLegend": "Your rating",
|
||||
"reviews.dialog.starsSr.one": "{n} star",
|
||||
"reviews.dialog.starsSr.other": "{n} stars",
|
||||
"reviews.dialog.chooseRating": "Choose a rating",
|
||||
"reviews.dialog.starLabel": "{n}: {phrase}",
|
||||
"reviews.dialog.stars.1": "avoid it",
|
||||
"reviews.dialog.stars.2": "had problems",
|
||||
"reviews.dialog.stars.3": "worked, with caveats",
|
||||
"reviews.dialog.stars.4": "good",
|
||||
"reviews.dialog.stars.5": "works great",
|
||||
"reviews.dialog.bodyLabel": "What should other people know?",
|
||||
"reviews.dialog.bodyPlaceholder": "How did minting, melting and support work out? Be specific.",
|
||||
"reviews.dialog.charCount": "{n} / {max}",
|
||||
"reviews.dialog.submit": "Sign and publish",
|
||||
"reviews.dialog.signing": "Signing…",
|
||||
"reviews.dialog.publishing": "Publishing…",
|
||||
"reviews.dialog.publishingTo": "Publishing to relays ({accepted}/{total})",
|
||||
"reviews.dialog.waitingExtension": "Waiting for your extension",
|
||||
"reviews.dialog.waitingSigner": "Waiting for your signer",
|
||||
"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.rating": "Choose a rating",
|
||||
"reviews.dialog.why.words": "Write a few words",
|
||||
"reviews.dialog.why.more.one": "{n} more character",
|
||||
"reviews.dialog.why.more.other": "{n} more characters",
|
||||
"reviews.dialog.why.over.one": "{n} character over the limit",
|
||||
"reviews.dialog.why.over.other": "{n} characters over the limit",
|
||||
"reviews.dialog.identity.needSigner": "You need a Nostr signer to publish. Your key stays in it, this site never sees it.",
|
||||
"reviews.dialog.identity.loginToPublish": "Log in to publish",
|
||||
"reviews.dialog.identity.switch": "Switch",
|
||||
"reviews.dialog.identity.extension": "Signing with your browser extension",
|
||||
"reviews.dialog.identity.readingKey": "Reading your public key…",
|
||||
"reviews.dialog.identity.willAsk": "Your extension will be asked to sign",
|
||||
"reviews.dialog.error.noRelay": "No relay accepted the review. Check your connection, then try again.",
|
||||
"reviews.validate.rating": "Choose a rating from 1 to 5 stars.",
|
||||
"reviews.validate.short": "Write at least {n} characters so the review is useful.",
|
||||
"reviews.validate.long": "Reviews are limited to {n} characters.",
|
||||
"feed.title": "Latest Cashu mint reviews · Cashumints.space",
|
||||
"feed.description": "Every Cashu mint review as it is published: {reviews} reviews across {mints} mints, signed on Nostr and readable here newest first. Filter by rating or by mint.",
|
||||
"feed.heading": "Latest Cashu mint reviews",
|
||||
"feed.lede": "Every review of every mint, 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.filterByMint": "Filter by mint",
|
||||
"feed.anyMint": "Any mint",
|
||||
"feed.clearMint": "Clear the mint filter",
|
||||
"feed.showingBuild.one": "Showing the most recent review from the last build",
|
||||
"feed.showingBuild.other": "Showing the {n} most recent reviews from the last build",
|
||||
"feed.noscript": "Reviews are signed on Nostr and load in your browser, so filtering and the full list need JavaScript. The {n} reviews below were read from the relays when this page was built.",
|
||||
"feed.emptyBuild": "No reviews could be read from the relays when this page was built. The list reloads from the relays in your browser.",
|
||||
"feed.foot": "Reviews are community opinions, not verified claims. Read the {link} before you trust a mint with money.",
|
||||
"feed.footLink": "disclaimer",
|
||||
"feed.showing": "Showing {shown} of {total} reviews",
|
||||
"feed.showingMatching": "Showing {shown} of {matching} matching reviews ({total} in total)",
|
||||
"feed.emptyRelays": "No reviews came back from the relays. Open any mint page to read and write reviews there.",
|
||||
"feed.emptyFilter": "No reviews match this filter. Clear the mint box, or try All.",
|
||||
"feed.relayError": "Could not reach the relays. Showing the reviews from the last build.",
|
||||
"feed.relayEmpty": "No reviews came back from the relays. Showing the reviews from the last build.",
|
||||
"feed.reading": "Reading the relays…",
|
||||
"login.open": "Log in",
|
||||
"login.title": "Log in with Nostr",
|
||||
"login.note": "Your key stays in your signer. This site can never see it.",
|
||||
"login.extension.name": "Browser extension",
|
||||
"login.extension.desc": "Sign with Alby, nos2x or another NIP-07 extension",
|
||||
"login.extension.none": "No extension found",
|
||||
"login.extension.getOne": "Get one:",
|
||||
"login.remote.name": "Remote signer",
|
||||
"login.remote.desc": "Scan a code with a signer app, or paste a bunker link",
|
||||
"login.primal.name": "Primal",
|
||||
"login.primal.desc": "Approve the login in the Primal app on your phone",
|
||||
"login.paste.label": "Or paste the connection link your signer gave you",
|
||||
"login.copyLink": "Copy the connection link",
|
||||
"login.whatIsNostr": "What is Nostr?",
|
||||
"login.connect.remote.title": "Connect a remote signer",
|
||||
"login.connect.remote.lead": "Scan this with your signer app, or copy the link and paste it there.",
|
||||
"login.connect.remote.hint": "Works with any NIP-46 signer: nsec.app, Amber, Nostrify, your own bunker.",
|
||||
"login.connect.primal.title": "Connect with Primal",
|
||||
"login.connect.primal.lead": "Open Primal on your phone, find remote signing in its settings, and scan this code.",
|
||||
"login.connect.primal.hint": "Primal signs for other apps over NIP-46, so this is the same connection any signer uses, and your key stays in Primal. {link}",
|
||||
"login.status.buildingLink": "Building a connection link",
|
||||
"login.status.waitingExtension": "Waiting for your extension",
|
||||
"login.status.waitingSigner": "Waiting for your signer",
|
||||
"login.status.connecting": "Connecting to your signer",
|
||||
"login.status.connected": "Connected",
|
||||
"login.method.extension": "Extension",
|
||||
"login.method.remote": "Remote",
|
||||
"login.method.primal": "Primal",
|
||||
"login.account": "Your Nostr account",
|
||||
"login.copyNpub": "Copy your full npub",
|
||||
"login.logout": "Log out",
|
||||
"signer.status.waitingExtension": "Waiting for your extension",
|
||||
"signer.status.signed": "Signed",
|
||||
"signer.status.reconnecting": "Reconnecting to your signer",
|
||||
"signer.status.approveWindow": "Your signer needs you to approve this in its own window",
|
||||
"signer.status.waitingSigner": "Waiting for your signer",
|
||||
"signer.status.stillWaiting": "Still waiting for your signer to answer",
|
||||
"signer.status.connected": "Connected",
|
||||
"signer.error.noExtension": "No signing extension found. Install Alby or nos2x, then reload this page.",
|
||||
"signer.error.privateKey": "That looks like a private key, not a connection link. This site never takes a key. Paste the bunker:// link your signer gave you.",
|
||||
"signer.error.badLink": "That is not a connection link this site understands. Paste the bunker:// link from your signer.",
|
||||
"signer.error.noAnswer": "Your signer did not answer. It may be closed, or its relay may be down. Open it and try again.",
|
||||
"signer.error.timeout": "Your signer did not answer in time. Open it, check it is still connected, then try again.",
|
||||
"signer.error.cancelled": "Connection cancelled.",
|
||||
"signer.error.declined": "Signature request was declined",
|
||||
"notfound.title": "Page not found · Cashumints.space",
|
||||
"notfound.description": "That page does not exist on Cashumints.space.",
|
||||
"notfound.heading": "This page does not exist",
|
||||
"notfound.lede": "It may have moved, or the mint may not be indexed yet. Search for a mint by name or domain, or paste a mint URL.",
|
||||
"notfound.browse": "Browse all mints",
|
||||
"notfound.home": "Go home",
|
||||
"notfound.loading": "Loading {host}",
|
||||
"notfound.fb.rating": "Rating",
|
||||
"notfound.fb.reviews": "Reviews",
|
||||
"notfound.fb.software": "Software",
|
||||
"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.",
|
||||
"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.heading": "Wallets",
|
||||
"wallets.lede": "You need a wallet to hold ecash from any of these mints. Most wallets talk to several mints at once, so you are not tied to the first one you try.",
|
||||
"wallets.note": "This list is not exhaustive and is not an endorsement. The Cashu project keeps a broader index at {link}. A wallet holds keys, a mint holds the money: read the mint reviews before you pick where your sats live.",
|
||||
"wallets.platform.android": "Android",
|
||||
"wallets.platform.ios": "iOS",
|
||||
"wallets.platform.web": "Web",
|
||||
"wallets.platform.extension": "Browser extension",
|
||||
"wallets.platform.cli": "CLI",
|
||||
"wallets.platform.python": "Python",
|
||||
"wallets.item.minibits": "Mobile ecash wallet with its own mint, Lightning address support and Nostr contacts.",
|
||||
"wallets.item.nutstash": "Browser wallet that handles multiple mints at once and imports and exports raw tokens.",
|
||||
"wallets.item.enuts": "Mobile wallet focused on multimint balances and sending ecash over Nostr.",
|
||||
"wallets.item.cashume": "Reference web wallet from the Cashu project. Runs entirely in the browser, installable as a PWA.",
|
||||
"wallets.item.boardwalk": "Web wallet built around Lightning addresses and sending ecash by link.",
|
||||
"wallets.item.coinos": "Lightning wallet and point of sale that also runs its own Cashu mint.",
|
||||
"wallets.item.oxchat": "Nostr messenger with an ecash wallet built in, for sending sats inside chats.",
|
||||
"wallets.item.nutshell": "The reference implementation. Runs as a wallet on the command line and as mint software.",
|
||||
"about.title": "About Cashumints.space",
|
||||
"about.description": "What Cashu is, what a mint actually holds, what NIP-87 is, and how this site indexes mints and reads reviews from Nostr relays.",
|
||||
"about.heading": "About",
|
||||
"about.lede": "Cashumints.space lists every Cashu mint we can find on the Nostr network and shows you what people say about them. It is made by Azzamo.",
|
||||
"about.cashu.title": "What Cashu is",
|
||||
"about.cashu.p1": "Cashu is an ecash protocol for Bitcoin. A mint issues blinded tokens backed by sats it holds, and those tokens move between people without the mint learning who sent what to whom. The privacy is real and it is the point: tokens are not accounts, and the mint cannot link the token it signed to the token that comes back.",
|
||||
"about.cashu.p2": "You can read the protocol and its specifications at {link}. The specifications themselves are called NUTs, which is where the NUT numbers on every mint page come from.",
|
||||
"about.mints.title": "What a mint is, and the tradeoff",
|
||||
"about.mints.p1": "A mint is custodial. When you mint ecash you are handing sats to whoever runs that server, and you are trusting them to honour the tokens when you melt them back out. There is no fraud proof, no escrow and no recourse. A mint operator can disappear with the balance, and mints have.",
|
||||
"about.mints.p2": "That is exactly why reviews exist here. This site never tells you a mint is safe. The status on a mint page says {code}, not \"trusted\", and it only means the server answered when we last asked. Treat ecash balances like cash in a coat pocket: fine for spending, wrong for savings.",
|
||||
"about.nip87.title": "What NIP-87 is",
|
||||
"about.nip87.p1": "NIP-87 is the Nostr convention for announcing and recommending ecash mints. Two event kinds matter here:",
|
||||
"about.nip87.li1": "{code} announces a mint. The mint URL is in the {tag} tag. This is how the site discovers mints it has never seen.",
|
||||
"about.nip87.li2": "{code} is a review. It carries the rating and the review text, and points at the mint it is about with a {utag} tag (the mint URL) and a {dtag} tag (the mint's own public key).",
|
||||
"about.nip87.p2": "Every review is signed by its author's Nostr key. We never call a review \"verified\", because nobody verified it: it is signed, which is a different and more useful claim. It means you can check who wrote it and that it was not altered. It does not mean the author is honest or ever used the mint.",
|
||||
"about.how.title": "How this site works",
|
||||
"about.how.p1": "There are two loops and one rule. The rule is that the mint owns its metadata and Nostr owns the reviews, so this site stores neither as an authority.",
|
||||
"about.how.li1": "{strong} asks every known mint for {code} every ten minutes and records what it says: name, description, version, supported NUTs, limits, contact details. It also records whether the mint answered at all, which is where uptime comes from.",
|
||||
"about.how.li1Label": "The probe loop",
|
||||
"about.how.li2": "{strong} connects to Nostr relays every hour, reads mint announcements and reviews, and adds any mint it has not seen before.",
|
||||
"about.how.li2Label": "The discovery loop",
|
||||
"about.how.p2": "The important part is what happens when a mint stops answering. Its cached metadata is never overwritten by a failed probe, so a dead mint keeps its full page: its specs, its history and its reviews, with a banner saying when it went dark. The previous version of this site showed an error page instead, which meant the mints people most needed to warn each other about were the ones that could not be reviewed. That was the main reason for the rebuild.",
|
||||
"about.how.p3": "Review text is never stored on our side. When you open a mint page your browser talks to the relays directly and reads the reviews itself, and when you write one it is signed by your own Nostr extension and sent straight to those relays. We keep a count so the pages can be ranked and sorted, nothing more. Nothing here can be edited or deleted by us, including reviews we would rather not have.",
|
||||
"about.relays.title": "Relays we read",
|
||||
"about.relays.p1": "These are the relays the indexer and your browser both use:",
|
||||
"about.relays.p2": "Point any Nostr client at the same relays and you will see the same events. That is the whole promise: nothing here is only true because we say so.",
|
||||
"about.nuts.title": "What are NUTs?",
|
||||
"about.nuts.p1": "NUTs are the numbered Cashu specifications, like BIPs for Bitcoin. A mint advertises which ones it implements, and that tells you what it can actually do: NUT-04 and NUT-05 are minting and melting over Lightning (without them a mint is not much use), NUT-07 lets a wallet check whether a token was already spent, NUT-17 gives wallets live updates over WebSockets instead of polling, and NUT-20 lets a wallet prove a mint quote was really issued to it. The full list is at {link}.",
|
||||
"about.numbers.title": "Right now",
|
||||
"about.numbers.p1": "{mints} mints indexed, {online} of them answering, {reviews} reviews counted from the relays above.",
|
||||
"about.source.title": "Source and credit",
|
||||
"about.source.p1": "The site is open source at {github}. It is built and run by {azzamo}. If a mint page has something wrong on it, fix it at the source: mint metadata comes from the mint's own {code}, and reviews come from Nostr.",
|
||||
"terms.title": "Terms of use · Cashumints.space",
|
||||
"terms.description": "Terms of use for Cashumints.space: what the site is, what it does not do, and who is responsible for what. No accounts, no warranties, no affiliation with any mint.",
|
||||
"terms.heading": "Terms of use",
|
||||
"terms.lede": "Plain terms for a site that only indexes public data. Last updated {date}.",
|
||||
"terms.what.title": "What this site is",
|
||||
"terms.what.p1": "Cashumints.space is an index of publicly available data about Cashu mints. Two kinds of data, both from somewhere else:",
|
||||
"terms.what.li1": "Mint metadata, read from each mint's own {code} endpoint: name, description, software version, supported NUTs, contact details and whether the mint answered when we last asked.",
|
||||
"terms.what.li2": "Reviews, read from public Nostr relays. These are NIP-87 events published by their authors to relays anyone can query.",
|
||||
"terms.what.p2": "We collect that data, normalise it, rank it and show it. We do not create it.",
|
||||
"terms.noAccounts.title": "No accounts, no warranties",
|
||||
"terms.noAccounts.p1": "There is nothing to sign up for. The site is provided as it is, with no warranty of any kind: not that the data is accurate, not that it is current, not that the site will be available. Mint data can be minutes or hours old, a mint can change after we last checked, and a relay can fail to answer, in which case you are seeing less than exists.",
|
||||
"terms.noAccounts.p2": "Use it at your own risk. We are not liable for any loss that follows from using this site or relying on anything you read here. See the {link} for the specific risks of using a Cashu mint.",
|
||||
"terms.noAccounts.link": "disclaimer",
|
||||
"terms.noCustody.title": "We do not hold your money",
|
||||
"terms.noCustody.p1": "This site never takes custody of funds. It has no wallet, it does not touch ecash, and it cannot move, freeze or recover anything. If you have a problem with a mint, the mint operator is the only party who can help you, and we cannot contact them on your behalf.",
|
||||
"terms.noCustody.p2": "We are not affiliated with any mint listed here, and no mint pays for its listing, its rank or its rating. Mints are discovered from public Nostr announcements and ranked by an algorithm over public review data.",
|
||||
"terms.reviews.title": "Reviews are other people's words",
|
||||
"terms.reviews.p1": "Reviews are user-generated content published to public Nostr relays by their authors. We display them, we do not control them. We did not write them, we cannot edit them, and we cannot delete them from the relays that hold them: they are signed events on a network we do not own.",
|
||||
"terms.reviews.p2": "A review is one person's experience, not a verified fact. If you publish one, you are responsible for what it says. Do not publish anything unlawful, and be aware that what you publish is public and permanent (see the {link}).",
|
||||
"terms.reviews.link": "privacy page",
|
||||
"terms.reviews.p3": "We can stop showing a specific review on this site, and we will do that for content that is unlawful. That removes it from these pages only. The event stays on the relays and any other Nostr client can still read it.",
|
||||
"terms.links.title": "Links to other sites",
|
||||
"terms.links.p1": "Mint URLs, wallet links, operator contacts and everything else that points off this site are there because they are part of the data. A link is not an endorsement and we do not vet what is on the other end.",
|
||||
"terms.changes.title": "Changes",
|
||||
"terms.changes.p1": "These terms can change. The date at the top says when they last did. The site is open source, so every change to this page is in the commit history.",
|
||||
"terms.contact.title": "Contact",
|
||||
"terms.contact.p1": "Issues and takedown requests are best filed on {github}. The site is run by {azzamo}.",
|
||||
"privacy.title": "Privacy · Cashumints.space",
|
||||
"privacy.description": "Privacy on Cashumints.space: no accounts, no tracking cookies, no analytics. localStorage is used for profile caching and nothing is sent anywhere. Reviews are signed in your own browser.",
|
||||
"privacy.heading": "Privacy",
|
||||
"privacy.lede": "We collect as little as a website can. Last updated {date}.",
|
||||
"privacy.noAccounts.title": "No accounts",
|
||||
"privacy.noAccounts.p1": "There is no sign up, no login and no profile. Nothing on this site asks you who you are, and there is no user record to leak because there are no users.",
|
||||
"privacy.noTracking.title": "No tracking cookies, no analytics",
|
||||
"privacy.noTracking.p1": "The site sets no cookies for tracking. There is no analytics script, no tag manager, no pixel, no session recording and no advertising network. Nothing on the page reports your behaviour to us or to anyone else.",
|
||||
"privacy.logs.title": "Server logs",
|
||||
"privacy.logs.p1": "The web server keeps standard access logs, which is what any web server does by default. A log line records your IP address, the time, the page you asked for, the response status, and the user agent and referrer your browser sent. The logs exist to debug errors and spot abuse. They are not joined to anything else and they are not sold or shared.",
|
||||
"privacy.logs.operatorLabel": "Operator note:",
|
||||
"privacy.logs.operator": "if you run this site, state your real retention here. The deployment in the README uses nginx with its default access log and whatever rotation your distribution ships (on Debian and Ubuntu that is logrotate, daily, keeping 14 days). Confirm what your server does and replace this paragraph with the number.",
|
||||
"privacy.storage.title": "What is stored in your browser",
|
||||
"privacy.storage.p1": "The site uses localStorage for exactly two things, both of which stay on your device and are never sent anywhere:",
|
||||
"privacy.storage.li1": "A cache of Nostr profile metadata (kind 0 events: names, avatars, NIP-05 addresses) for the reviewers whose reviews you have looked at. This saves re-querying relays on every page. Entries expire, and the cache is capped and evicted oldest first.",
|
||||
"privacy.storage.li2": "Interface preferences, so the site behaves the same way next time.",
|
||||
"privacy.storage.p2": "Clearing your browser's site data removes both. Nothing breaks, the profile lookups just run again.",
|
||||
"privacy.thirdParties.title": "What your browser talks to",
|
||||
"privacy.thirdParties.p1": "Loading a page connects your browser to a few places besides this site. That is how a site with no server-side tracking still reads live data:",
|
||||
"privacy.thirdParties.li1Label": "Public Nostr relays",
|
||||
"privacy.thirdParties.li1": "{strong}, over WebSocket, to read reviews and reviewer profiles. The relay sees your IP and what you asked for, as any server you connect to does. The relays are listed on the {link}.",
|
||||
"privacy.thirdParties.li1Link": "about page",
|
||||
"privacy.thirdParties.li2Label": "Reviewer avatar images",
|
||||
"privacy.thirdParties.li2": "{strong}, loaded from wherever the reviewer's profile points. These are sent with a no-referrer policy, so the host does not learn which page you were on.",
|
||||
"privacy.publishing.title": "Publishing a review",
|
||||
"privacy.publishing.p1": "Writing a review happens in your browser, not on our server. The event is signed by your NIP-07 extension (Alby, nos2x or similar) and broadcast from your browser straight to the relays. Your private key never leaves the extension and this site never sees it. We do not build custom key management and we never ask for a key or a seed phrase.",
|
||||
"privacy.permanent.title": "Anything you publish to Nostr is public and permanent",
|
||||
"privacy.permanent.p1": "This is the part worth reading twice. A review is a signed event on public relays. Once it is out, it is out:",
|
||||
"privacy.permanent.li1": "Anyone can read it, now and later, using any Nostr client. It is not \"public on Cashumints.space\", it is public on the network.",
|
||||
"privacy.permanent.li2": "It is permanently linked to your Nostr public key, alongside everything else that key has ever published.",
|
||||
"privacy.permanent.li3": "Deleting it is a request, not a command. You can publish a deletion event and some relays will honour it, but a relay is free to ignore it and anyone who already copied the event still has it. Treat a published review as permanent.",
|
||||
"privacy.permanent.li4": "We cannot delete it for you. We can stop displaying it here; that changes nothing on the relays.",
|
||||
"privacy.permanent.p2": "If you want to review a mint without that being tied to your main identity, use a separate Nostr key. Note that a key with no other activity is shown as such on harsh reviews, because a brand new key writing a one star review is a pattern readers deserve to see.",
|
||||
"privacy.contact.title": "Questions",
|
||||
"privacy.contact.p1": "The site is open source: if you want to check any of the above, read the code at {github}. Questions and corrections can go in an issue there.",
|
||||
"disclaimer.title": "Disclaimer · Cashumints.space",
|
||||
"disclaimer.description": "What to know before you use a Cashu mint: mints are custodial, a mint can disappear with your money, reviews are opinions and online means reachable, not trustworthy.",
|
||||
"disclaimer.heading": "Disclaimer",
|
||||
"disclaimer.lede": "Read this before you put money in a mint. Last updated {date}.",
|
||||
"disclaimer.callout": "A Cashu mint holds your money for you. If it disappears, your ecash is gone, and there is nobody to appeal to. Keep only what you can afford to lose.",
|
||||
"disclaimer.custodial.title": "Mints are custodial",
|
||||
"disclaimer.custodial.p1": "When you mint ecash, you hand bitcoin to the mint operator and get tokens back. Those tokens are a claim on that operator. They are not bitcoin you hold, and the operator is the one who decides whether to honour them.",
|
||||
"disclaimer.custodial.p2": "That means every mint carries the same risks: the operator can vanish, can be compelled to stop, can lose the funds through a bug or a hack, or can simply turn the server off. There is no deposit insurance, no chargeback, no support line and no legal recourse worth counting on. This is true of the highest rated mint on this site.",
|
||||
"disclaimer.custodial.p3": "Cashu is designed for small amounts moving quickly. Use it that way: hold what you are about to spend, not your savings, and spread across mints rather than parking everything in one.",
|
||||
"disclaimer.reviews.title": "Reviews are opinions, not verified claims",
|
||||
"disclaimer.reviews.p1": "Every review here was written by someone on Nostr and signed with their key. Signed means we can tell you which key wrote it. It does not mean the review is true, that the author used the mint, or that the author is not the operator, a competitor, or one person with ten keys.",
|
||||
"disclaimer.reviews.p2": "We do not verify reviewers, and we do not edit or remove reviews we disagree with. Read a few, look at what they actually say, and weigh a mint with three reviews differently from one with three hundred.",
|
||||
"disclaimer.status.title": "Online means reachable, not trustworthy",
|
||||
"disclaimer.status.p1": "We check every mint regularly and show whether it answered. That is all a green status means: the server responded. It says nothing about whether the operator is solvent, honest, or still intends to honour your tokens tomorrow.",
|
||||
"disclaimer.status.p2": "A mint can be online right up to the moment it is not. Uptime history, supported NUTs and software version tell you something about how a mint is run, but none of them are a promise about your money.",
|
||||
"disclaimer.ranking.title": "How ranking works, and what it does not mean",
|
||||
"disclaimer.ranking.p1": "Mints are ordered by an algorithm over public review data: rating, number of reviews and recent activity, with online mints ahead of offline ones by default. The details are in the open source code.",
|
||||
"disclaimer.ranking.p2": "Nobody pays for placement. There is no sponsored slot, no promoted listing and no arrangement with any mint on this site. A high rank means the algorithm liked the public data, and nothing more. It is not a recommendation and it is not a safety rating.",
|
||||
"disclaimer.notAdvice.title": "This is not financial advice",
|
||||
"disclaimer.notAdvice.p1": "Nothing on this site is financial, investment, legal or tax advice. We do not know your situation and we are not advising you. Using a mint, and choosing which one, is your decision.",
|
||||
"disclaimer.dyor.title": "Do your own research",
|
||||
"disclaimer.dyor.li1": "Look up the operator. A mint run by someone with a public reputation to lose is a different proposition from an anonymous URL.",
|
||||
"disclaimer.dyor.li2": "Read the mint's own terms and its MOTD. A mint that calls itself a test or a beta means it.",
|
||||
"disclaimer.dyor.li3": "Start small. Mint a little, spend it, melt it back out, and see what happens before you trust it with more.",
|
||||
"disclaimer.dyor.li4": "Watch the limits. A low maximum is often a sign of a careful operator, and a high one is not a sign of a safe one.",
|
||||
"disclaimer.dyor.li5": "Keep your tokens backed up, and remember that a token is a bearer asset: whoever holds it can spend it.",
|
||||
"disclaimer.site.title": "About the site itself",
|
||||
"disclaimer.site.p1": "Data here can be wrong or out of date. Mints are probed on a schedule, relays sometimes fail to answer, and a page you are reading was built at some point in the past. The site is provided as it is, with no warranty. See the {link}.",
|
||||
"disclaimer.site.link": "terms of use",
|
||||
"disclaimer.updated": "20 August 2026",
|
||||
"login.getPrimal": "Get Primal",
|
||||
"reviews.dialog.identity.extensionGone": "Your browser extension is not answering. Log in again, or use a remote signer.",
|
||||
"seo.orgDescription": "A directory of Cashu mints, with live status from each mint and community reviews signed on Nostr.",
|
||||
"seo.ogImageAlt": "Cashumints.space: find a Cashu mint you can trust.",
|
||||
"nav.home": "Home",
|
||||
"mint.description.fallbackDomain": "A Cashu mint running at {domain}.",
|
||||
"mint.more.title": "More mints",
|
||||
"mint.more.all": "See all mints"
|
||||
}
|
||||
@@ -0,0 +1,554 @@
|
||||
{
|
||||
"site.name": "Cashumints.space",
|
||||
"common.close": "Cerrar",
|
||||
"common.cancel": "Cancelar",
|
||||
"common.done": "Listo",
|
||||
"common.back": "Atrás",
|
||||
"common.retry": "Reintentar",
|
||||
"common.connect": "Conectar",
|
||||
"common.copied": "Copiado",
|
||||
"common.copyFull": "Copiar {kind} completo",
|
||||
"common.previousPage": "Página anterior",
|
||||
"common.nextPage": "Página siguiente",
|
||||
"common.reviewPages": "Páginas de reseñas",
|
||||
"time.never": "nunca",
|
||||
"time.justNow": "ahora mismo",
|
||||
"time.none": "-",
|
||||
"time.unknownDate": "una fecha desconocida",
|
||||
"time.ago.minute": "hace {n} min",
|
||||
"time.ago.hour": "hace {n} h",
|
||||
"time.ago.day": "hace {n} d",
|
||||
"time.ago.week": "hace {n} sem",
|
||||
"time.ago.month.one": "hace {n} mes",
|
||||
"time.ago.month.other": "hace {n} meses",
|
||||
"time.ago.year.one": "hace {n} año",
|
||||
"time.ago.year.other": "hace {n} años",
|
||||
"time.short.minute": "{n} min",
|
||||
"time.short.hour": "{n} h",
|
||||
"time.short.day": "{n} d",
|
||||
"time.short.week": "{n} sem",
|
||||
"time.short.month.one": "{n} mes",
|
||||
"time.short.month.other": "{n} meses",
|
||||
"time.short.year.one": "{n} año",
|
||||
"time.short.year.other": "{n} años",
|
||||
"status.online": "En línea",
|
||||
"status.offline": "Sin conexión",
|
||||
"status.degraded": "No responde",
|
||||
"status.unknown": "Sin comprobar aún",
|
||||
"nav.skip": "Saltar al contenido",
|
||||
"nav.main": "Principal",
|
||||
"nav.mints": "Mints",
|
||||
"nav.reviews": "Reseñas",
|
||||
"nav.wallets": "Monederos",
|
||||
"nav.about": "Acerca de",
|
||||
"nav.searchMints": "Buscar en {n} mints",
|
||||
"lang.label": "Idioma",
|
||||
"lang.switcher": "Elige un idioma",
|
||||
"footer.copyright": "© {year} Cashumints.space",
|
||||
"footer.allMints": "Todos los mints",
|
||||
"footer.allReviews": "Todas las reseñas",
|
||||
"footer.wallets": "Monederos",
|
||||
"footer.about": "Acerca de",
|
||||
"footer.disclaimer": "Aviso legal",
|
||||
"footer.terms": "Términos",
|
||||
"footer.privacy": "Privacidad",
|
||||
"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.",
|
||||
"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.hero.title": "Encuentra un mint Cashu",
|
||||
"home.hero.titleEm": "en el que confiar",
|
||||
"home.hero.lede": "Todos los mints de la red Nostr, con reseñas reales de la comunidad. Sin intermediarios, solo reseñas firmadas que puedes verificar tú mismo.",
|
||||
"home.search.label": "Buscar mints por nombre, dominio o URL",
|
||||
"home.search.placeholder": "Busca por nombre, dominio, o pega la URL de un mint…",
|
||||
"home.filters.mostReviewed": "Más reseñados",
|
||||
"home.filters.highestRated": "Mejor valorados",
|
||||
"home.filters.recentlyActive": "Activos hace poco",
|
||||
"home.filters.websockets": "Compatibles con WebSockets",
|
||||
"home.top.title": "Mejores mints",
|
||||
"home.top.sub": "ordenados por reseñas y valoración",
|
||||
"home.top.all": "Ver los {n} mints →",
|
||||
"home.latest.title": "Últimas reseñas",
|
||||
"home.latest.sub": "en directo desde la red Nostr",
|
||||
"home.latest.prev": "Reseñas anteriores",
|
||||
"home.latest.next": "Más reseñas",
|
||||
"home.latest.all": "Ver todas las reseñas →",
|
||||
"home.latest.empty": "Ahora mismo no se ha podido leer ninguna reseña de los relays. Abre la página de cualquier mint para leer y escribir reseñas directamente.",
|
||||
"home.why.title": "¿Por qué fiarte de estas reseñas?",
|
||||
"home.why.signed.title": "Firmadas, no enviadas",
|
||||
"home.why.signed.body": "Cada reseña es un evento {link} firmado con la clave Nostr de quien la escribió. Aquí no editamos, ordenamos a mano ni borramos nada.",
|
||||
"home.why.signed.link": "NIP-87",
|
||||
"home.why.verify.title": "Compruébalo tú mismo",
|
||||
"home.why.verify.body": "Las reseñas viven en relays públicos de Nostr, no en nuestra base de datos. Apunta cualquier cliente a esos mismos relays y verás los mismos datos.",
|
||||
"home.why.checked.title": "Comprobados continuamente",
|
||||
"home.why.checked.body": "Comprobamos cada mint para saber su disponibilidad, versión del software, límites y NUTs compatibles, así que lo que ves está al día y no lo dice el propio mint.",
|
||||
"home.cta.title": "¿Estás pensando en un mint? Léelo primero.",
|
||||
"home.cta.body": "Un mint guarda tu ecash. Lee lo que ha encontrado otra gente antes de meter sats, y deja una reseña cuando hayas usado uno tú.",
|
||||
"home.cta.explore": "Explorar todos los mints",
|
||||
"home.cta.learn": "Aprender sobre Cashu",
|
||||
"pulse.mintsIndexed": "mints indexados",
|
||||
"pulse.reviewsOnNostr": "reseñas en Nostr",
|
||||
"pulse.lastReview": "última reseña",
|
||||
"pulse.mintsOnline": "mints en línea",
|
||||
"pulse.indexer": "indexador",
|
||||
"pulse.connected": "conectado",
|
||||
"pulse.stale": "desactualizado",
|
||||
"mints.title": "Todos los mints Cashu · Cashumints.space",
|
||||
"mints.description": "Los {total} mints Cashu indexados desde la red Nostr, {online} en línea y {offline} sin conexión, con valoraciones de la comunidad, disponibilidad y software del mint. Busca y ordena la lista completa.",
|
||||
"mints.heading": "Todos los mints",
|
||||
"mints.lede": "Todos los mints descubiertos en la red Nostr, incluidos los que ya no están. Los mints sin conexión siguen en la lista para que puedas encontrarlos y reseñarlos.",
|
||||
"mints.search.label": "Buscar mints por nombre o dominio",
|
||||
"mints.search.placeholder": "Busca por nombre o dominio…",
|
||||
"mints.sort.label": "Ordenar mints",
|
||||
"mints.sort.score": "Mejores en general",
|
||||
"mints.sort.rating": "Mejor valorados",
|
||||
"mints.sort.reviews": "Más reseñados",
|
||||
"mints.sort.recent": "Reseñados hace poco",
|
||||
"mints.sort.seen": "Vistos en línea hace poco",
|
||||
"mints.hideOffline": "Ocultar sin conexión",
|
||||
"mints.showing": "Mostrando {total} mints ({online} en línea, {offline} sin conexión)",
|
||||
"mints.showingFiltered": "Mostrando {shown} de {total} mints ({online} en línea, {offline} sin conexión)",
|
||||
"mints.noResults": "Ningún mint coincide. Prueba con un dominio, o pega la URL de un mint.",
|
||||
"mints.clearSearch": "Borrar la búsqueda",
|
||||
"card.noRatings": "Sin valoraciones aún",
|
||||
"card.reviews.one": "{n} reseña",
|
||||
"card.reviews.other": "{n} reseñas",
|
||||
"card.lastSeen": "visto {when}",
|
||||
"card.neverSeen": "nunca visto en línea",
|
||||
"card.reviewed": "reseñado {when}",
|
||||
"card.noReviews": "sin reseñas aún",
|
||||
"mint.title": "{name} · Reseñas del mint Cashu",
|
||||
"mint.description.fallback": "{name} es un mint Cashu en {domain}.",
|
||||
"mint.description.rated.one": "Valorado con {rating} sobre 5 a partir de {n} reseña firmada en Nostr.",
|
||||
"mint.description.rated.other": "Valorado con {rating} sobre 5 a partir de {n} reseñas firmadas en Nostr.",
|
||||
"mint.description.noReviews": "Todavía no hay reseñas de la comunidad.",
|
||||
"mint.description.online": "Ahora mismo en línea.",
|
||||
"mint.description.unreachable": "Ahora mismo no responde.",
|
||||
"mint.breadcrumb": "Ruta de navegación",
|
||||
"mint.crumbMints": "Mints",
|
||||
"mint.copyUrl": "Copiar la URL del mint",
|
||||
"mint.qrButton": "QR del mint",
|
||||
"mint.writeReview": "Escribir una reseña",
|
||||
"mint.rankByScore": "#{rank} por puntuación",
|
||||
"mint.tagLightning": "Lightning",
|
||||
"mint.summary": "Resumen del mint",
|
||||
"mint.rating.label": "Valoración de la comunidad",
|
||||
"mint.rating.none": "Sin valoraciones aún",
|
||||
"mint.rating.reviews.one": "{n} reseña",
|
||||
"mint.rating.reviews.other": "{n} reseñas",
|
||||
"mint.rating.unrated.one": "{n} no dejó valoración",
|
||||
"mint.rating.unrated.other": "{n} no dejaron valoración",
|
||||
"mint.activity.label": "Actividad",
|
||||
"mint.activity.since": "desde la última reseña",
|
||||
"mint.activity.noReviews": "sin reseñas aún",
|
||||
"mint.activity.recent.one": "{n} reseña en los últimos 90 días",
|
||||
"mint.activity.recent.other": "{n} reseñas en los últimos 90 días",
|
||||
"mint.limits.label": "Límites",
|
||||
"mint.limits.notPublished": "sin publicar",
|
||||
"mint.limits.min": "mín",
|
||||
"mint.limits.max": "máx",
|
||||
"mint.limits.mintRow": "acuñar",
|
||||
"mint.limits.meltRow": "fundir",
|
||||
"mint.limits.disabled": "desactivado",
|
||||
"mint.limits.range": "de {min} a {max} {unit}",
|
||||
"mint.limits.upTo": "hasta {max} {unit}",
|
||||
"mint.limits.from": "desde {min} {unit}",
|
||||
"mint.software.label": "Software del mint",
|
||||
"mint.software.unknown": "desconocido",
|
||||
"mint.checked": "comprobado {when}",
|
||||
"mint.lastSeen": "visto {when}",
|
||||
"mint.neverReached": "nunca alcanzado",
|
||||
"mint.uptime.checks": "{ok} de las últimas {total} comprobaciones respondieron",
|
||||
"mint.uptime.percent": "{percent}% de disponibilidad en 30 días",
|
||||
"mint.uptime.description": "{ok} de las últimas {total} comprobaciones salieron bien",
|
||||
"mint.operator.title": "De quien lo opera",
|
||||
"mint.operator.motd": "Mensaje del día",
|
||||
"mint.operator.beta": "Este mint se describe a sí mismo como experimental o en beta. Lee abajo lo que dice quien lo opera antes de guardar ahí nada que no puedas perder.",
|
||||
"mint.nuts.title": "NUTs compatibles",
|
||||
"mint.nuts.noneOffline": "No hay lista de NUTs guardada para este mint, se quedó sin conexión antes de que se registrara una.",
|
||||
"mint.nuts.noneYet": "Todavía no hay lista de NUTs guardada para este mint.",
|
||||
"mint.nuts.supported": "{n} de {total} NUTs conocidos son compatibles",
|
||||
"mint.nuts.whatAre": "¿qué son los NUTs?",
|
||||
"mint.nutRow.disabled": "desactivado",
|
||||
"mint.nutRow.notSupported": "no compatible",
|
||||
"mint.details.title": "Datos del mint",
|
||||
"mint.details.pubkey": "Pubkey",
|
||||
"mint.details.nostr": "Nostr",
|
||||
"mint.details.email": "Correo",
|
||||
"mint.details.serverTime": "Hora del servidor",
|
||||
"mint.details.firstSeen": "Visto por primera vez",
|
||||
"mint.details.terms": "Términos",
|
||||
"mint.kind.identifier": "identificador",
|
||||
"mint.kind.mintPubkey": "pubkey del mint",
|
||||
"mint.kind.nostrAddress": "dirección Nostr",
|
||||
"mint.kind.emailAddress": "dirección de correo",
|
||||
"mint.kind.npub": "npub",
|
||||
"mint.disclaimer": "Este mint es custodial: guarda tu dinero, y puede dejar de hacerlo en cualquier momento. Las valoraciones son opiniones de la comunidad, y en línea significa accesible, no fiable.",
|
||||
"mint.disclaimerLink": "Leer el aviso legal completo",
|
||||
"mint.qr.title": "Añade {name} a tu monedero",
|
||||
"mint.qr.note": "Escanea esto con un monedero Cashu para añadir el mint, o copia la URL y pégala ahí.",
|
||||
"mint.qr.warn": "Añadir un mint significa confiarle tu dinero.",
|
||||
"mint.qr.whatMeans": "Qué significa eso",
|
||||
"mint.qr.wallets": "Monederos compatibles con Cashu",
|
||||
"mint.chip.frozen": "Congelado",
|
||||
"mint.chip.noWithdrawals": "Sin retiradas",
|
||||
"mint.chip.meltOnly": "Solo fundir",
|
||||
"mint.warnings.dayCount.one": "{n} día",
|
||||
"mint.warnings.dayCount.other": "{n} días",
|
||||
"mint.warnings.gone.lead": "Probablemente ha desaparecido.",
|
||||
"mint.warnings.gone.body.since": "Este mint lleva sin responder desde el {date} ({days}). Da por perdido el dinero que tengas ahí y no envíes más. Los datos de abajo son su último estado conocido.",
|
||||
"mint.warnings.gone.body.neverDated": "Este mint no ha respondido a ni una sola comprobación desde que empezamos a vigilarlo en {month}. Da por perdido el dinero que tengas ahí y no envíes más. Todo lo de abajo viene del anuncio en Nostr, no del propio mint.",
|
||||
"mint.warnings.gone.body.never": "Este mint no ha respondido a ni una sola comprobación desde que empezamos a vigilarlo. Da por perdido el dinero que tengas ahí y no envíes más. Todo lo de abajo viene del anuncio en Nostr, no del propio mint.",
|
||||
"mint.warnings.gone.meta.since": "Sin conexión desde el {date}",
|
||||
"mint.warnings.gone.meta.never": "Nunca ha respondido desde que se descubrió",
|
||||
"mint.warnings.meltDisabled.lead": "Retiradas desactivadas.",
|
||||
"mint.warnings.meltDisabled.body.online": "Este mint tiene desactivado el fundido por Lightning (melt, NUT-05): puedes meter sats, pero no puedes sacarlos por Lightning. No deposites hasta que el fundido vuelva a funcionar.",
|
||||
"mint.warnings.meltDisabled.body.offline": "Este mint tenía desactivado el fundido por Lightning (melt, NUT-05): puedes meter sats, pero no puedes sacarlos por Lightning. No deposites hasta que el fundido vuelva a funcionar.",
|
||||
"mint.warnings.meltDisabled.meta.online": "Retiradas desactivadas ahora mismo",
|
||||
"mint.warnings.meltDisabled.meta.offline": "Las retiradas estaban desactivadas la última vez que respondió",
|
||||
"mint.warnings.frozen.lead": "Mint congelado.",
|
||||
"mint.warnings.frozen.body.online": "La acuñación y el fundido están los dos desactivados. No entra ni sale ni un sat por Lightning.",
|
||||
"mint.warnings.frozen.body.offline": "La acuñación y el fundido estaban los dos desactivados. No entraba ni salía ni un sat por Lightning.",
|
||||
"mint.warnings.frozen.meta.online": "Congelado ahora mismo, no entra ni sale nada",
|
||||
"mint.warnings.frozen.meta.offline": "Estaba congelado la última vez que respondió",
|
||||
"mint.warnings.offlineLong.lead": "Sin conexión desde hace {days}, desde el {date}.",
|
||||
"mint.warnings.offlineLong.body": "Puede que el mint haya desaparecido. Todo lo de abajo es su último estado conocido, y las reseñas siguen funcionando.",
|
||||
"mint.warnings.offlineLong.meta": "Sin conexión desde el {date}",
|
||||
"mint.warnings.meltOnly.lead": "Solo fundir.",
|
||||
"mint.warnings.meltOnly.body.online": "La acuñación de ecash nuevo (mint, NUT-04) está desactivada: no puedes depositar, pero el ecash que ya tienes todavía se puede retirar. Esto suele querer decir que el mint está cerrando, así que si tienes ecash suyo, plantéate fundirlo y sacarlo.",
|
||||
"mint.warnings.meltOnly.body.offline": "La acuñación de ecash nuevo (mint, NUT-04) estaba desactivada: no puedes depositar, pero el ecash que ya tienes todavía se puede retirar. Esto suele querer decir que el mint está cerrando, así que si tienes ecash suyo, plantéate fundirlo y sacarlo.",
|
||||
"mint.warnings.meltOnly.meta.online": "Ahora mismo solo se puede fundir",
|
||||
"mint.warnings.meltOnly.meta.offline": "Solo se podía fundir la última vez que respondió",
|
||||
"mint.warnings.offline.lead": "Sin conexión desde el {date}.",
|
||||
"mint.warnings.offline.body": "Los datos se confirmaron por última vez entonces. Todavía puedes escribir una reseña.",
|
||||
"mint.warnings.offline.meta": "Sin conexión desde el {date}",
|
||||
"mint.warnings.also.meltDisabled": "Antes de quedarse sin conexión también había desactivado las retiradas.",
|
||||
"mint.warnings.also.mintDisabled": "Antes de quedarse sin conexión también había desactivado la acuñación de ecash nuevo.",
|
||||
"mint.warnings.also.offline": "Además lleva sin conexión desde el {date} ({days}).",
|
||||
"nut.0": "Notación y modelos",
|
||||
"nut.1": "Intercambio de la clave pública del mint",
|
||||
"nut.2": "Conjuntos de claves y comisiones",
|
||||
"nut.3": "Intercambio de tokens",
|
||||
"nut.4": "Acuñación por Lightning",
|
||||
"nut.5": "Fundido por Lightning",
|
||||
"nut.6": "Información del mint",
|
||||
"nut.7": "Comprobación del estado de un token",
|
||||
"nut.8": "Comisiones de Lightning pagadas de más",
|
||||
"nut.9": "Restaurar firmas",
|
||||
"nut.10": "Condiciones de gasto",
|
||||
"nut.11": "Pago a clave pública",
|
||||
"nut.12": "Pruebas DLEQ",
|
||||
"nut.13": "Secretos deterministas",
|
||||
"nut.14": "Contratos con bloqueo temporal por hash",
|
||||
"nut.15": "Pagos por varias rutas",
|
||||
"nut.16": "Códigos QR animados",
|
||||
"nut.17": "Suscripciones por WebSocket",
|
||||
"nut.18": "Solicitudes de pago",
|
||||
"nut.19": "Respuestas en caché",
|
||||
"nut.20": "Cotizaciones de acuñación firmadas",
|
||||
"nut.21": "Autenticación en claro",
|
||||
"nut.22": "Autenticación ciega",
|
||||
"nut.23": "Método de pago BOLT11",
|
||||
"nut.24": "HTTP 402, pago requerido",
|
||||
"nut.25": "Método de pago BOLT12",
|
||||
"reviews.title": "Reseñas",
|
||||
"reviews.anon": "Anón",
|
||||
"reviews.filter.group": "Filtrar reseñas",
|
||||
"reviews.filter.all": "Todas",
|
||||
"reviews.filter.five": "5★",
|
||||
"reviews.filter.critical": "Críticas",
|
||||
"reviews.filter.recent": "Recientes",
|
||||
"reviews.loading.one": "Cargando {n} reseña de los relays de Nostr",
|
||||
"reviews.loading.other": "Cargando {n} reseñas de los relays de Nostr",
|
||||
"reviews.empty": "Todavía no hay reseñas. 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.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.srRating": "{rating} sobre 5",
|
||||
"reviews.srNoRating": "Sin valoración",
|
||||
"reviews.anonNote": "Primera reseña de este npub, no se ha encontrado ninguna otra actividad",
|
||||
"reviews.published": "Publicada.",
|
||||
"reviews.publishedPartial": "Publicada en {accepted} de {total} relays.",
|
||||
"reviews.propagating": "Propagándose, puede tardar un momento en aparecer en otros sitios.",
|
||||
"reviews.summary.line.one": "{count} valoración 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.other": "{count} valoraciones sin comentario del {when}: {breakdown}",
|
||||
"reviews.summary.showOne": "Mostrarla",
|
||||
"reviews.summary.showMany": "Mostrarlas",
|
||||
"reviews.breakdown.5": "{n} de cinco estrellas",
|
||||
"reviews.breakdown.4": "{n} de cuatro estrellas",
|
||||
"reviews.breakdown.3": "{n} de tres estrellas",
|
||||
"reviews.breakdown.2": "{n} de dos estrellas",
|
||||
"reviews.breakdown.1": "{n} de una estrella",
|
||||
"reviews.breakdown.none": "{n} sin valoración",
|
||||
"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.ratingLegend": "Tu valoración",
|
||||
"reviews.dialog.starsSr.one": "{n} estrella",
|
||||
"reviews.dialog.starsSr.other": "{n} estrellas",
|
||||
"reviews.dialog.chooseRating": "Elige una valoración",
|
||||
"reviews.dialog.starLabel": "{n}: {phrase}",
|
||||
"reviews.dialog.stars.1": "evítalo",
|
||||
"reviews.dialog.stars.2": "tuve problemas",
|
||||
"reviews.dialog.stars.3": "funcionó, con reparos",
|
||||
"reviews.dialog.stars.4": "bien",
|
||||
"reviews.dialog.stars.5": "funciona genial",
|
||||
"reviews.dialog.bodyLabel": "¿Qué debería saber la gente?",
|
||||
"reviews.dialog.bodyPlaceholder": "¿Qué tal fue acuñar, fundir y el soporte? Sé concreto.",
|
||||
"reviews.dialog.charCount": "{n} / {max}",
|
||||
"reviews.dialog.submit": "Firmar y publicar",
|
||||
"reviews.dialog.signing": "Firmando…",
|
||||
"reviews.dialog.publishing": "Publicando…",
|
||||
"reviews.dialog.publishingTo": "Publicando en los relays ({accepted}/{total})",
|
||||
"reviews.dialog.waitingExtension": "Esperando a tu extensión",
|
||||
"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.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.more.one": "{n} carácter más",
|
||||
"reviews.dialog.why.more.other": "{n} caracteres más",
|
||||
"reviews.dialog.why.over.one": "{n} carácter de más",
|
||||
"reviews.dialog.why.over.other": "{n} caracteres de más",
|
||||
"reviews.dialog.identity.needSigner": "Necesitas un firmante de Nostr para publicar. Tu clave se queda en él, este sitio no la ve nunca.",
|
||||
"reviews.dialog.identity.loginToPublish": "Inicia sesión para publicar",
|
||||
"reviews.dialog.identity.switch": "Cambiar",
|
||||
"reviews.dialog.identity.extension": "Firmando con la extensión de tu navegador",
|
||||
"reviews.dialog.identity.readingKey": "Leyendo tu clave pública…",
|
||||
"reviews.dialog.identity.willAsk": "Se le pedirá a tu extensión que firme",
|
||||
"reviews.dialog.error.noRelay": "Ningún relay ha aceptado la reseña. Comprueba tu conexión y vuelve a intentarlo.",
|
||||
"reviews.validate.rating": "Elige una valoración de 1 a 5 estrellas.",
|
||||
"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.",
|
||||
"feed.title": "Últimas reseñas de mints Cashu · Cashumints.space",
|
||||
"feed.description": "Cada reseña de un mint Cashu según se publica: {reviews} reseñas de {mints} mints, firmadas en Nostr y legibles aquí de la más nueva a la más antigua. Filtra por valoración o por mint.",
|
||||
"feed.heading": "Últimas reseñas de mints Cashu",
|
||||
"feed.lede": "Todas las reseñas de todos los mints, 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.filterByMint": "Filtrar por mint",
|
||||
"feed.anyMint": "Cualquier mint",
|
||||
"feed.clearMint": "Borrar el filtro de mint",
|
||||
"feed.showingBuild.one": "Mostrando la reseña más reciente de la última compilación",
|
||||
"feed.showingBuild.other": "Mostrando las {n} reseñas más recientes de la última compilación",
|
||||
"feed.noscript": "Las reseñas están firmadas en Nostr y se cargan en tu navegador, así que los filtros y la lista completa necesitan JavaScript. Las {n} reseñas de abajo se leyeron de los relays cuando se compiló esta página.",
|
||||
"feed.emptyBuild": "No se pudo leer ninguna reseña de los relays cuando se compiló esta página. La lista se vuelve a cargar desde los relays en tu navegador.",
|
||||
"feed.foot": "Las reseñas son opiniones de la comunidad, no hechos verificados. Lee el {link} antes de confiarle dinero a un mint.",
|
||||
"feed.footLink": "aviso legal",
|
||||
"feed.showing": "Mostrando {shown} de {total} reseñas",
|
||||
"feed.showingMatching": "Mostrando {shown} de {matching} reseñas que coinciden ({total} en total)",
|
||||
"feed.emptyRelays": "Los relays no han devuelto ninguna reseña. Abre la página de cualquier mint para leer y escribir reseñas ahí.",
|
||||
"feed.emptyFilter": "Ninguna reseña coincide con este filtro. Borra la caja del mint, o prueba con Todas.",
|
||||
"feed.relayError": "No se ha podido llegar a los relays. Mostrando las reseñas de la última compilación.",
|
||||
"feed.relayEmpty": "Los relays no han devuelto ninguna reseña. Mostrando las reseñas de la última compilación.",
|
||||
"feed.reading": "Leyendo los relays…",
|
||||
"login.open": "Iniciar sesión",
|
||||
"login.title": "Inicia sesión con Nostr",
|
||||
"login.note": "Tu clave se queda en tu firmante. Este sitio no puede verla nunca.",
|
||||
"login.extension.name": "Extensión del navegador",
|
||||
"login.extension.desc": "Firma con Alby, nos2x u otra extensión NIP-07",
|
||||
"login.extension.none": "No se ha encontrado ninguna extensión",
|
||||
"login.extension.getOne": "Consigue una:",
|
||||
"login.remote.name": "Firmante remoto",
|
||||
"login.remote.desc": "Escanea un código con una app de firma, o pega un enlace bunker",
|
||||
"login.primal.name": "Primal",
|
||||
"login.primal.desc": "Aprueba el inicio de sesión en la app de Primal en tu móvil",
|
||||
"login.paste.label": "O pega el enlace de conexión que te dio tu firmante",
|
||||
"login.copyLink": "Copiar el enlace de conexión",
|
||||
"login.whatIsNostr": "¿Qué es Nostr?",
|
||||
"login.connect.remote.title": "Conectar un firmante remoto",
|
||||
"login.connect.remote.lead": "Escanea esto con tu app de firma, o copia el enlace y pégalo ahí.",
|
||||
"login.connect.remote.hint": "Funciona con cualquier firmante NIP-46: nsec.app, Amber, Nostrify, tu propio bunker.",
|
||||
"login.connect.primal.title": "Conectar con Primal",
|
||||
"login.connect.primal.lead": "Abre Primal en tu móvil, busca la firma remota en sus ajustes y escanea este código.",
|
||||
"login.connect.primal.hint": "Primal firma para otras apps por NIP-46, así que esta es la misma conexión que usa cualquier firmante, y tu clave se queda en Primal. {link}",
|
||||
"login.status.buildingLink": "Creando un enlace de conexión",
|
||||
"login.status.waitingExtension": "Esperando a tu extensión",
|
||||
"login.status.waitingSigner": "Esperando a tu firmante",
|
||||
"login.status.connecting": "Conectando con tu firmante",
|
||||
"login.status.connected": "Conectado",
|
||||
"login.method.extension": "Extensión",
|
||||
"login.method.remote": "Remoto",
|
||||
"login.method.primal": "Primal",
|
||||
"login.account": "Tu cuenta de Nostr",
|
||||
"login.copyNpub": "Copiar tu npub completo",
|
||||
"login.logout": "Cerrar sesión",
|
||||
"signer.status.waitingExtension": "Esperando a tu extensión",
|
||||
"signer.status.signed": "Firmado",
|
||||
"signer.status.reconnecting": "Reconectando con tu firmante",
|
||||
"signer.status.approveWindow": "Tu firmante necesita que lo apruebes en su propia ventana",
|
||||
"signer.status.waitingSigner": "Esperando a tu firmante",
|
||||
"signer.status.stillWaiting": "Seguimos esperando a que responda tu firmante",
|
||||
"signer.status.connected": "Conectado",
|
||||
"signer.error.noExtension": "No se ha encontrado ninguna extensión de firma. Instala Alby o nos2x y recarga esta página.",
|
||||
"signer.error.privateKey": "Eso parece una clave privada, no un enlace de conexión. Este sitio nunca acepta una clave. Pega el enlace bunker:// que te dio tu firmante.",
|
||||
"signer.error.badLink": "Ese no es un enlace de conexión que este sitio entienda. Pega el enlace bunker:// de tu firmante.",
|
||||
"signer.error.noAnswer": "Tu firmante no ha respondido. Puede que esté cerrado, o que su relay esté caído. Ábrelo y vuelve a intentarlo.",
|
||||
"signer.error.timeout": "Tu firmante no ha respondido a tiempo. Ábrelo, comprueba que sigue conectado y vuelve a intentarlo.",
|
||||
"signer.error.cancelled": "Conexión cancelada.",
|
||||
"signer.error.declined": "Se rechazó la petición de firma",
|
||||
"notfound.title": "Página no encontrada · Cashumints.space",
|
||||
"notfound.description": "Esa página no existe en Cashumints.space.",
|
||||
"notfound.heading": "Esta página no existe",
|
||||
"notfound.lede": "Puede que se haya movido, o que el mint todavía no esté indexado. Busca un mint por nombre o dominio, o pega la URL de un mint.",
|
||||
"notfound.browse": "Ver todos los mints",
|
||||
"notfound.home": "Ir al inicio",
|
||||
"notfound.loading": "Cargando {host}",
|
||||
"notfound.fb.rating": "Valoración",
|
||||
"notfound.fb.reviews": "Reseñas",
|
||||
"notfound.fb.software": "Software",
|
||||
"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.",
|
||||
"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.heading": "Monederos",
|
||||
"wallets.lede": "Necesitas un monedero para guardar el ecash de cualquiera de estos mints. La mayoría hablan con varios mints a la vez, así que no te atas al primero que pruebes.",
|
||||
"wallets.note": "Esta lista no está completa y no es una recomendación. El proyecto Cashu mantiene un índice más amplio en {link}. Un monedero guarda claves, un mint guarda el dinero: lee las reseñas de los mints antes de elegir dónde viven tus sats.",
|
||||
"wallets.platform.android": "Android",
|
||||
"wallets.platform.ios": "iOS",
|
||||
"wallets.platform.web": "Web",
|
||||
"wallets.platform.extension": "Extensión de navegador",
|
||||
"wallets.platform.cli": "Línea de comandos",
|
||||
"wallets.platform.python": "Python",
|
||||
"wallets.item.minibits": "Monedero de ecash para móvil con su propio mint, direcciones Lightning y contactos de Nostr.",
|
||||
"wallets.item.nutstash": "Monedero de navegador que maneja varios mints a la vez e importa y exporta tokens en crudo.",
|
||||
"wallets.item.enuts": "Monedero móvil centrado en saldos repartidos entre mints y en enviar ecash por Nostr.",
|
||||
"wallets.item.cashume": "El monedero web de referencia del proyecto Cashu. Funciona entero en el navegador y se puede instalar como PWA.",
|
||||
"wallets.item.boardwalk": "Monedero web pensado para direcciones Lightning y para enviar ecash con un enlace.",
|
||||
"wallets.item.coinos": "Monedero Lightning y TPV que además opera su propio mint Cashu.",
|
||||
"wallets.item.oxchat": "Mensajería Nostr con un monedero de ecash integrado, para enviar sats dentro del chat.",
|
||||
"wallets.item.nutshell": "La implementación de referencia. Funciona como monedero en línea de comandos y como software de mint.",
|
||||
"about.title": "Acerca de Cashumints.space",
|
||||
"about.description": "Qué es Cashu, qué guarda de verdad un mint, qué es NIP-87 y cómo este sitio indexa mints y lee reseñas de los relays de Nostr.",
|
||||
"about.heading": "Acerca de",
|
||||
"about.lede": "Cashumints.space lista todos los mints Cashu que encontramos en la red Nostr y te enseña lo que dice la gente sobre ellos. Lo hace Azzamo.",
|
||||
"about.cashu.title": "Qué es Cashu",
|
||||
"about.cashu.p1": "Cashu es un protocolo de ecash para Bitcoin. Un mint emite tokens ciegos respaldados por los sats que guarda, y esos tokens pasan de una persona a otra sin que el mint sepa quién le envió qué a quién. La privacidad es real y es el objetivo: los tokens no son cuentas, y el mint no puede enlazar el token que firmó con el token que le vuelve.",
|
||||
"about.cashu.p2": "Puedes leer el protocolo y sus especificaciones en {link}. Las especificaciones se llaman NUTs, y de ahí salen los números NUT que ves en cada página de mint.",
|
||||
"about.mints.title": "Qué es un mint, y qué estás cediendo",
|
||||
"about.mints.p1": "Un mint es custodial. Cuando acuñas ecash le estás entregando sats a quien opere ese servidor, y confías en que respete los tokens cuando los fundas para sacarlos. No hay prueba de fraude, ni depósito en garantía, ni forma de reclamar. Quien opera un mint puede desaparecer con el saldo, y ha pasado.",
|
||||
"about.mints.p2": "Justo por eso existen aquí las reseñas. Este sitio nunca te dice que un mint sea seguro. El estado de la página de un mint dice {code}, no \"de fiar\", y solo significa que el servidor respondió la última vez que preguntamos. Trata los saldos de ecash como el dinero suelto de un bolsillo: bien para gastar, mal para ahorrar.",
|
||||
"about.nip87.title": "Qué es NIP-87",
|
||||
"about.nip87.p1": "NIP-87 es la convención de Nostr para anunciar y recomendar mints de ecash. Aquí importan dos tipos de evento:",
|
||||
"about.nip87.li1": "{code} anuncia un mint. La URL del mint va en la etiqueta {tag}. Así es como el sitio descubre mints que no ha visto nunca.",
|
||||
"about.nip87.li2": "{code} es una reseña. Lleva la valoración y el texto, y apunta al mint del que habla con una etiqueta {utag} (la URL del mint) y una etiqueta {dtag} (la clave pública del propio mint).",
|
||||
"about.nip87.p2": "Cada reseña está firmada con la clave Nostr de quien la escribió. Nunca decimos que una reseña esté \"verificada\", porque nadie la ha verificado: está firmada, que es otra cosa y más útil. Significa que puedes comprobar quién la escribió y que no se ha alterado. No significa que quien la escribió sea honesto ni que haya usado el mint.",
|
||||
"about.how.title": "Cómo funciona este sitio",
|
||||
"about.how.p1": "Hay dos bucles y una regla. La regla es que el mint es dueño de sus metadatos y Nostr es dueño de las reseñas, así que este sitio no guarda ninguna de las dos cosas como autoridad.",
|
||||
"about.how.li1": "{strong} le pide {code} a cada mint conocido cada diez minutos y anota lo que dice: nombre, descripción, versión, NUTs compatibles, límites, datos de contacto. También anota si el mint respondió, y de ahí sale la disponibilidad.",
|
||||
"about.how.li1Label": "El bucle de comprobación",
|
||||
"about.how.li2": "{strong} se conecta a los relays de Nostr cada hora, lee anuncios de mints y reseñas, y añade cualquier mint que no haya visto antes.",
|
||||
"about.how.li2Label": "El bucle de descubrimiento",
|
||||
"about.how.p2": "Lo importante es qué pasa cuando un mint deja de responder. Sus metadatos guardados nunca se sobrescriben con una comprobación fallida, así que un mint muerto conserva su página entera: sus especificaciones, su historial y sus reseñas, con un aviso que dice cuándo se apagó. La versión anterior de este sitio mostraba una página de error, lo que significaba que los mints sobre los que más falta hacía avisarse eran justo los que no se podían reseñar. Esa fue la razón principal para rehacerlo.",
|
||||
"about.how.p3": "El texto de las reseñas no se guarda nunca de nuestro lado. Cuando abres la página de un mint, tu navegador habla directamente con los relays y lee las reseñas él mismo, y cuando escribes una la firma tu propia extensión de Nostr y va derecha a esos relays. Nosotros guardamos un recuento para poder ordenar y clasificar las páginas, nada más. Aquí no podemos editar ni borrar nada, incluidas las reseñas que preferiríamos no tener.",
|
||||
"about.relays.title": "Relays que leemos",
|
||||
"about.relays.p1": "Estos son los relays que usan tanto el indexador como tu navegador:",
|
||||
"about.relays.p2": "Apunta cualquier cliente de Nostr a esos mismos relays y verás los mismos eventos. Esa es toda la promesa: aquí nada es cierto solo porque lo digamos nosotros.",
|
||||
"about.nuts.title": "¿Qué son los NUTs?",
|
||||
"about.nuts.p1": "Los NUTs son las especificaciones numeradas de Cashu, como los BIPs de Bitcoin. Un mint anuncia cuáles implementa, y eso te dice lo que puede hacer de verdad: NUT-04 y NUT-05 son acuñar y fundir por Lightning (sin ellos un mint no sirve de mucho), NUT-07 deja que un monedero compruebe si un token ya se gastó, NUT-17 da a los monederos avisos en directo por WebSockets en vez de preguntar una y otra vez, y NUT-20 deja que un monedero demuestre que una cotización de acuñación se le emitió a él. La lista completa está en {link}.",
|
||||
"about.numbers.title": "Ahora mismo",
|
||||
"about.numbers.p1": "{mints} mints indexados, {online} de ellos respondiendo, {reviews} reseñas contadas de los relays de arriba.",
|
||||
"about.source.title": "Código y créditos",
|
||||
"about.source.p1": "El sitio es de código abierto en {github}. Lo hace y lo mantiene {azzamo}. Si la página de un mint tiene algo mal, arréglalo en el origen: los metadatos del mint vienen de su propio {code}, y las reseñas vienen de Nostr.",
|
||||
"terms.title": "Términos de uso · Cashumints.space",
|
||||
"terms.description": "Términos de uso de Cashumints.space: qué es el sitio, qué no hace y quién responde de qué. Sin cuentas, sin garantías, sin vínculo con ningún mint.",
|
||||
"terms.heading": "Términos de uso",
|
||||
"terms.lede": "Términos sencillos para un sitio que solo indexa datos públicos. Última actualización: {date}.",
|
||||
"terms.what.title": "Qué es este sitio",
|
||||
"terms.what.p1": "Cashumints.space es un índice de datos públicos sobre mints Cashu. Dos tipos de datos, los dos de fuera:",
|
||||
"terms.what.li1": "Metadatos del mint, leídos del endpoint {code} de cada mint: nombre, descripción, versión del software, NUTs compatibles, datos de contacto y si el mint respondió la última vez que preguntamos.",
|
||||
"terms.what.li2": "Reseñas, leídas de relays públicos de Nostr. Son eventos NIP-87 que sus autores publican en relays que cualquiera puede consultar.",
|
||||
"terms.what.p2": "Recogemos esos datos, los normalizamos, los ordenamos y los mostramos. No los creamos.",
|
||||
"terms.noAccounts.title": "Sin cuentas, sin garantías",
|
||||
"terms.noAccounts.p1": "No hay nada a lo que registrarse. El sitio se ofrece tal cual, sin garantía de ningún tipo: ni de que los datos sean exactos, ni de que estén al día, ni de que el sitio vaya a estar disponible. Los datos de un mint pueden tener minutos u horas, un mint puede cambiar después de nuestra última comprobación, y un relay puede no responder, y entonces estás viendo menos de lo que hay.",
|
||||
"terms.noAccounts.p2": "Úsalo bajo tu responsabilidad. No respondemos de ninguna pérdida derivada de usar este sitio o de fiarte de lo que leas aquí. Consulta el {link} para los riesgos concretos de usar un mint Cashu.",
|
||||
"terms.noAccounts.link": "aviso legal",
|
||||
"terms.noCustody.title": "No guardamos tu dinero",
|
||||
"terms.noCustody.p1": "Este sitio nunca tiene la custodia de ningún fondo. No tiene monedero, no toca ecash, y no puede mover, congelar ni recuperar nada. Si tienes un problema con un mint, quien lo opera es la única parte que puede ayudarte, y nosotros no podemos contactar con esa persona en tu nombre.",
|
||||
"terms.noCustody.p2": "No estamos vinculados a ningún mint de los listados, y ningún mint paga por aparecer, por su posición ni por su valoración. Los mints se descubren desde anuncios públicos de Nostr y se ordenan con un algoritmo sobre datos públicos de reseñas.",
|
||||
"terms.reviews.title": "Las reseñas son palabras de otra gente",
|
||||
"terms.reviews.p1": "Las reseñas son contenido de usuarios publicado en relays públicos de Nostr por sus autores. Las mostramos, no las controlamos. No las escribimos, no podemos editarlas, y no podemos borrarlas de los relays que las guardan: son eventos firmados en una red que no es nuestra.",
|
||||
"terms.reviews.p2": "Una reseña es la experiencia de una persona, no un hecho verificado. Si publicas una, respondes de lo que diga. No publiques nada ilegal, y ten claro que lo que publicas es público y permanente (consulta la {link}).",
|
||||
"terms.reviews.link": "página de privacidad",
|
||||
"terms.reviews.p3": "Podemos dejar de mostrar una reseña concreta en este sitio, y lo haremos con contenido ilegal. Eso la quita solo de estas páginas. El evento sigue en los relays y cualquier otro cliente de Nostr puede seguir leyéndolo.",
|
||||
"terms.links.title": "Enlaces a otros sitios",
|
||||
"terms.links.p1": "Las URLs de los mints, los enlaces a monederos, los contactos de quienes los operan y todo lo demás que apunta fuera de este sitio está ahí porque forma parte de los datos. Un enlace no es una recomendación y no revisamos lo que hay al otro lado.",
|
||||
"terms.changes.title": "Cambios",
|
||||
"terms.changes.p1": "Estos términos pueden cambiar. La fecha de arriba dice cuándo lo hicieron por última vez. El sitio es de código abierto, así que cada cambio de esta página está en el historial de commits.",
|
||||
"terms.contact.title": "Contacto",
|
||||
"terms.contact.p1": "Las incidencias y las peticiones de retirada se presentan mejor en {github}. El sitio lo mantiene {azzamo}.",
|
||||
"privacy.title": "Privacidad · Cashumints.space",
|
||||
"privacy.description": "Privacidad en Cashumints.space: sin cuentas, sin cookies de seguimiento, sin analítica. localStorage se usa para guardar perfiles en caché y no se envía nada a ningún sitio. Las reseñas se firman en tu propio navegador.",
|
||||
"privacy.heading": "Privacidad",
|
||||
"privacy.lede": "Recogemos lo menos que puede recoger una web. Última actualización: {date}.",
|
||||
"privacy.noAccounts.title": "Sin cuentas",
|
||||
"privacy.noAccounts.p1": "No hay registro, ni inicio de sesión, ni perfil. Nada en este sitio te pregunta quién eres, y no hay ningún registro de usuario que se pueda filtrar porque no hay usuarios.",
|
||||
"privacy.noTracking.title": "Sin cookies de seguimiento, sin analítica",
|
||||
"privacy.noTracking.p1": "El sitio no pone ninguna cookie de seguimiento. No hay script de analítica, ni gestor de etiquetas, ni píxel, ni grabación de sesión, ni red publicitaria. Nada en la página informa de lo que haces, ni a nosotros ni a nadie.",
|
||||
"privacy.logs.title": "Registros del servidor",
|
||||
"privacy.logs.p1": "El servidor web guarda registros de acceso normales, que es lo que hace cualquier servidor web por defecto. Una línea de registro anota tu dirección IP, la hora, la página que pediste, el estado de la respuesta, y el agente de usuario y el referente que envió tu navegador. Los registros existen para depurar errores y detectar abusos. No se cruzan con nada más y no se venden ni se comparten.",
|
||||
"privacy.logs.operatorLabel": "Nota para quien opere el sitio:",
|
||||
"privacy.logs.operator": "si tú lo operas, indica aquí tu retención real. El despliegue del README usa nginx con su registro de acceso por defecto y la rotación que traiga tu distribución (en Debian y Ubuntu es logrotate, a diario, guardando 14 días). Confirma lo que hace tu servidor y sustituye este párrafo por el número.",
|
||||
"privacy.storage.title": "Qué se guarda en tu navegador",
|
||||
"privacy.storage.p1": "El sitio usa localStorage para exactamente dos cosas, y las dos se quedan en tu dispositivo y no se envían a ningún sitio:",
|
||||
"privacy.storage.li1": "Una caché de metadatos de perfiles de Nostr (eventos de tipo 0: nombres, avatares, direcciones NIP-05) de quienes escribieron las reseñas que has mirado. Así no hay que preguntar a los relays en cada página. Las entradas caducan, y la caché tiene un tope y se vacía por orden de antigüedad.",
|
||||
"privacy.storage.li2": "Preferencias de la interfaz, para que el sitio se comporte igual la próxima vez.",
|
||||
"privacy.storage.p2": "Borrar los datos del sitio en tu navegador quita las dos cosas. No se rompe nada, simplemente se vuelven a buscar los perfiles.",
|
||||
"privacy.thirdParties.title": "Con qué habla tu navegador",
|
||||
"privacy.thirdParties.p1": "Cargar una página conecta tu navegador con unos cuantos sitios además de este. Así es como una web sin seguimiento en el servidor sigue leyendo datos en directo:",
|
||||
"privacy.thirdParties.li1Label": "Relays públicos de Nostr",
|
||||
"privacy.thirdParties.li1": "{strong}, por WebSocket, para leer reseñas y perfiles. El relay ve tu IP y lo que pediste, como cualquier servidor al que te conectes. Los relays están listados en la {link}.",
|
||||
"privacy.thirdParties.li1Link": "página Acerca de",
|
||||
"privacy.thirdParties.li2Label": "Imágenes de avatar",
|
||||
"privacy.thirdParties.li2": "{strong}, cargadas desde donde apunte el perfil de quien escribió la reseña. Se piden con una política de no enviar referente, así que el servidor no sabe en qué página estabas.",
|
||||
"privacy.publishing.title": "Publicar una reseña",
|
||||
"privacy.publishing.p1": "Escribir una reseña ocurre en tu navegador, no en nuestro servidor. El evento lo firma tu extensión NIP-07 (Alby, nos2x o similar) y se emite desde tu navegador directamente a los relays. Tu clave privada no sale nunca de la extensión y este sitio no la ve. No hacemos gestión de claves propia y nunca pedimos una clave ni una frase semilla.",
|
||||
"privacy.permanent.title": "Todo lo que publiques en Nostr es público y permanente",
|
||||
"privacy.permanent.p1": "Esta es la parte que merece leerse dos veces. Una reseña es un evento firmado en relays públicos. Una vez fuera, está fuera:",
|
||||
"privacy.permanent.li1": "Cualquiera puede leerla, ahora y más adelante, con cualquier cliente de Nostr. No es \"pública en Cashumints.space\", es pública en la red.",
|
||||
"privacy.permanent.li2": "Queda ligada para siempre a tu clave pública de Nostr, junto a todo lo demás que haya publicado esa clave.",
|
||||
"privacy.permanent.li3": "Borrarla es una petición, no una orden. Puedes publicar un evento de borrado y algunos relays lo respetarán, pero un relay es libre de ignorarlo y quien ya copiara el evento lo sigue teniendo. Da una reseña publicada por permanente.",
|
||||
"privacy.permanent.li4": "No podemos borrarla por ti. Podemos dejar de mostrarla aquí, y eso no cambia nada en los relays.",
|
||||
"privacy.permanent.p2": "Si quieres reseñar un mint sin que quede ligado a tu identidad principal, usa una clave Nostr aparte. Ten en cuenta que una clave sin ninguna otra actividad se señala como tal en las reseñas duras, porque una clave recién hecha escribiendo una reseña de una estrella es un patrón que quien lee merece ver.",
|
||||
"privacy.contact.title": "Preguntas",
|
||||
"privacy.contact.p1": "El sitio es de código abierto: si quieres comprobar cualquier cosa de las de arriba, lee el código en {github}. Las preguntas y correcciones pueden ir en una incidencia ahí.",
|
||||
"disclaimer.title": "Aviso legal · Cashumints.space",
|
||||
"disclaimer.description": "Lo que hay que saber antes de usar un mint Cashu: los mints son custodiales, un mint puede desaparecer con tu dinero, las reseñas son opiniones y en línea significa accesible, no fiable.",
|
||||
"disclaimer.heading": "Aviso legal",
|
||||
"disclaimer.lede": "Lee esto antes de meter dinero en un mint. Última actualización: {date}.",
|
||||
"disclaimer.callout": "Un mint Cashu guarda tu dinero por ti. Si desaparece, tu ecash se va con él, y no hay nadie a quien reclamar. Guarda solo lo que te puedas permitir perder.",
|
||||
"disclaimer.custodial.title": "Los mints son custodiales",
|
||||
"disclaimer.custodial.p1": "Cuando acuñas ecash, le entregas bitcoin a quien opera el mint y recibes tokens a cambio. Esos tokens son un derecho frente a esa persona. No son bitcoin que tengas tú, y es quien opera el mint quien decide si los respeta.",
|
||||
"disclaimer.custodial.p2": "Eso quiere decir que todos los mints tienen los mismos riesgos: quien lo opera puede esfumarse, puede verse obligado a parar, puede perder los fondos por un fallo o un ataque, o puede simplemente apagar el servidor. No hay seguro de depósitos, ni devolución del cargo, ni soporte, ni vía legal con la que contar de verdad. Esto vale también para el mint mejor valorado de este sitio.",
|
||||
"disclaimer.custodial.p3": "Cashu está pensado para cantidades pequeñas que se mueven rápido. Úsalo así: guarda lo que estás a punto de gastar, no tus ahorros, y repártelo entre varios mints en vez de dejarlo todo en uno.",
|
||||
"disclaimer.reviews.title": "Las reseñas son opiniones, no hechos verificados",
|
||||
"disclaimer.reviews.p1": "Cada reseña de aquí la escribió alguien en Nostr y la firmó con su clave. Firmada quiere decir que podemos decirte qué clave la escribió. No quiere decir que la reseña sea cierta, ni que quien la escribió usara el mint, ni que no sea quien lo opera, la competencia, o una sola persona con diez claves.",
|
||||
"disclaimer.reviews.p2": "No verificamos a quien reseña, y no editamos ni quitamos reseñas con las que no estemos de acuerdo. Lee unas cuantas, fíjate en lo que dicen de verdad, y no midas igual un mint con tres reseñas y uno con trescientas.",
|
||||
"disclaimer.status.title": "En línea significa accesible, no fiable",
|
||||
"disclaimer.status.p1": "Comprobamos cada mint con regularidad y mostramos si respondió. Eso es todo lo que significa un estado en verde: el servidor contestó. No dice nada sobre si quien lo opera es solvente, honesto, o si mañana seguirá respetando tus tokens.",
|
||||
"disclaimer.status.p2": "Un mint puede estar en línea hasta el momento justo en que deja de estarlo. El historial de disponibilidad, los NUTs compatibles y la versión del software te dicen algo sobre cómo se lleva un mint, pero ninguno es una promesa sobre tu dinero.",
|
||||
"disclaimer.ranking.title": "Cómo funciona el orden, y qué no significa",
|
||||
"disclaimer.ranking.p1": "Los mints se ordenan con un algoritmo sobre datos públicos de reseñas: valoración, número de reseñas y actividad reciente, con los mints en línea por delante de los que no lo están. Los detalles están en el código abierto.",
|
||||
"disclaimer.ranking.p2": "Nadie paga por su posición. No hay hueco patrocinado, ni listado promocionado, ni acuerdo con ningún mint de este sitio. Estar arriba significa que al algoritmo le gustaron los datos públicos, y nada más. No es una recomendación y no es una calificación de seguridad.",
|
||||
"disclaimer.notAdvice.title": "Esto no es asesoramiento financiero",
|
||||
"disclaimer.notAdvice.p1": "Nada de este sitio es asesoramiento financiero, de inversión, legal ni fiscal. No conocemos tu situación y no te estamos aconsejando. Usar un mint, y cuál, es decisión tuya.",
|
||||
"disclaimer.dyor.title": "Infórmate por tu cuenta",
|
||||
"disclaimer.dyor.li1": "Busca quién lo opera. Un mint llevado por alguien con una reputación pública que perder no es lo mismo que una URL anónima.",
|
||||
"disclaimer.dyor.li2": "Lee los términos del propio mint y su mensaje del día. Un mint que se llama a sí mismo prueba o beta lo dice en serio.",
|
||||
"disclaimer.dyor.li3": "Empieza con poco. Acuña un poco, gástalo, fúndelo para sacarlo, y mira qué pasa antes de confiarle más.",
|
||||
"disclaimer.dyor.li4": "Fíjate en los límites. Un máximo bajo suele ser señal de que se opera con cuidado, y uno alto no es señal de que sea seguro.",
|
||||
"disclaimer.dyor.li5": "Guarda una copia de tus tokens, y recuerda que un token es un activo al portador: quien lo tiene, lo puede gastar.",
|
||||
"disclaimer.site.title": "Sobre el propio sitio",
|
||||
"disclaimer.site.p1": "Los datos de aquí pueden estar mal o desactualizados. Los mints se comprueban según un horario, los relays a veces no responden, y la página que estás leyendo se compiló en algún momento del pasado. El sitio se ofrece tal cual, sin garantía. Consulta los {link}.",
|
||||
"disclaimer.site.link": "términos de uso",
|
||||
"disclaimer.updated": "20 de agosto de 2026",
|
||||
"login.getPrimal": "Consigue Primal",
|
||||
"reviews.dialog.identity.extensionGone": "La extensión de tu navegador no responde. Vuelve a iniciar sesión, o usa un firmante remoto.",
|
||||
"seo.orgDescription": "Un directorio de mints Cashu, con el estado en vivo de cada mint y reseñas de la comunidad firmadas en Nostr.",
|
||||
"seo.ogImageAlt": "Cashumints.space: encuentra un mint Cashu en el que puedas confiar.",
|
||||
"nav.home": "Inicio",
|
||||
"mint.description.fallbackDomain": "Un mint Cashu que funciona en {domain}.",
|
||||
"mint.more.title": "Más mints",
|
||||
"mint.more.all": "Ver todos los mints"
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Locale-aware dates, times and numbers, for both halves of the site.
|
||||
*
|
||||
* One object per locale, built from that locale's translator, so a page and the island
|
||||
* that updates it in place format the same timestamp the same way. Everything that used
|
||||
* to be a hand-rolled `toLocaleDateString('en-GB', …)` goes through here now.
|
||||
*
|
||||
* This file ships to the browser, so it stays small: three cached Intl instances and
|
||||
* some arithmetic.
|
||||
*/
|
||||
import { intlTag, type Locale } from './config.js';
|
||||
import type { Translator } from './translate.js';
|
||||
|
||||
const MINUTE = 60;
|
||||
const HOUR = 3600;
|
||||
const DAY = 86_400;
|
||||
const WEEK = 604_800;
|
||||
const MONTH = 2_592_000;
|
||||
const YEAR = 31_536_000;
|
||||
|
||||
/** Largest first: the first threshold a difference clears is the unit it is said in. */
|
||||
const UNITS: Array<{ key: string; intl: Intl.RelativeTimeFormatUnit; seconds: number }> = [
|
||||
{ key: 'year', intl: 'year', seconds: YEAR },
|
||||
{ key: 'month', intl: 'month', seconds: MONTH },
|
||||
{ key: 'week', intl: 'week', seconds: WEEK },
|
||||
{ key: 'day', intl: 'day', seconds: DAY },
|
||||
{ key: 'hour', intl: 'hour', seconds: HOUR },
|
||||
{ key: 'minute', intl: 'minute', seconds: MINUTE },
|
||||
];
|
||||
|
||||
const relativeFormats = new Map<string, Intl.RelativeTimeFormat>();
|
||||
const dateFormats = new Map<string, Intl.DateTimeFormat>();
|
||||
const numberFormats = new Map<string, Intl.NumberFormat>();
|
||||
|
||||
function relativeFormat(tag: string): Intl.RelativeTimeFormat {
|
||||
let found = relativeFormats.get(tag);
|
||||
if (!found) {
|
||||
found = new Intl.RelativeTimeFormat(tag, { style: 'short', numeric: 'always' });
|
||||
relativeFormats.set(tag, found);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
function dateFormat(tag: string, options: Intl.DateTimeFormatOptions, id: string): Intl.DateTimeFormat {
|
||||
const cacheKey = `${tag}:${id}`;
|
||||
let found = dateFormats.get(cacheKey);
|
||||
if (!found) {
|
||||
found = new Intl.DateTimeFormat(tag, options);
|
||||
dateFormats.set(cacheKey, found);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
function numberFormat(tag: string): Intl.NumberFormat {
|
||||
let found = numberFormats.get(tag);
|
||||
if (!found) {
|
||||
found = new Intl.NumberFormat(tag);
|
||||
numberFormats.set(tag, found);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
export interface Formatters {
|
||||
locale: Locale;
|
||||
t: Translator;
|
||||
/** "4d ago", "hace 4 d", "4 d geleden". The dense form, used in rows and card feet. */
|
||||
relative(unix: number | null, now?: number): string;
|
||||
/** Magnitude only: "4d". The big number in the verdict strip. */
|
||||
shortDuration(unix: number | null, now?: number): string;
|
||||
/** "3 Aug 2026". Precise, for a banner where a fuzzy date is not good enough. */
|
||||
date(unix: number | null): string;
|
||||
/** "3 August 2026". The long form, for a day heading in the review feed. */
|
||||
longDate(unix: number): string;
|
||||
/** "Mar 2024", for "first seen". */
|
||||
monthYear(unix: number): string;
|
||||
/** "03/08/2026, 14:20 UTC". The mint's own clock, always in UTC. */
|
||||
serverTime(unix: number): string;
|
||||
/** Grouped: 1,000,000 in English, 1.000.000 in Spanish and Dutch. */
|
||||
number(n: number): string;
|
||||
/** A rating to one decimal, with the locale's own decimal separator. */
|
||||
decimal(n: number, digits?: number): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the formatter set for one locale.
|
||||
*
|
||||
* Cheap enough to call per component: the Intl instances behind it are cached by tag,
|
||||
* and the object itself is a handful of closures.
|
||||
*/
|
||||
export function formatters(t: Translator): Formatters {
|
||||
const tag = intlTag(t.locale);
|
||||
|
||||
/**
|
||||
* The relative form, catalog first.
|
||||
*
|
||||
* `Intl.RelativeTimeFormat` knows every language's grammar and is the right default,
|
||||
* but its short forms are not short enough for this layout: English gives "4 days
|
||||
* ago" where the design has room for "4d ago", and these strings sit in card feet and
|
||||
* dense review rows that were measured against the compact form.
|
||||
*
|
||||
* So the catalog gets first refusal, per unit, and Intl is the fallback. A fourth
|
||||
* language that never adds a single `time.ago.*` key still renders correct, natural
|
||||
* relative times from day one; the three launch languages tune theirs for width.
|
||||
*/
|
||||
const relative = (unix: number | null, now = Date.now() / 1000): string => {
|
||||
if (unix === null) return t('time.never');
|
||||
const diff = Math.max(0, Math.floor(now - unix));
|
||||
if (diff < MINUTE) return t('time.justNow');
|
||||
|
||||
for (const unit of UNITS) {
|
||||
if (diff < unit.seconds) continue;
|
||||
const n = Math.floor(diff / unit.seconds);
|
||||
const key = `time.ago.${unit.key}`;
|
||||
if (t.has(key)) return t(key, { n });
|
||||
return relativeFormat(tag).format(-n, unit.intl);
|
||||
}
|
||||
|
||||
return t('time.justNow');
|
||||
};
|
||||
|
||||
const shortDuration = (unix: number | null, now = Date.now() / 1000): string => {
|
||||
if (unix === null) return t('time.none');
|
||||
const diff = Math.max(0, Math.floor(now - unix));
|
||||
// Under a minute still reads as one minute here: this is a magnitude in a big
|
||||
// display number, and "0m" says less than "1m".
|
||||
if (diff < HOUR) return t('time.short.minute', { n: Math.max(1, Math.floor(diff / MINUTE)) });
|
||||
|
||||
for (const unit of UNITS) {
|
||||
if (diff < unit.seconds) continue;
|
||||
return t(`time.short.${unit.key}`, { n: Math.floor(diff / unit.seconds) });
|
||||
}
|
||||
|
||||
return t('time.short.minute', { n: 1 });
|
||||
};
|
||||
|
||||
return {
|
||||
locale: t.locale,
|
||||
t,
|
||||
relative,
|
||||
shortDuration,
|
||||
|
||||
date: (unix) =>
|
||||
unix === null
|
||||
? t('time.unknownDate')
|
||||
: dateFormat(tag, { day: 'numeric', month: 'short', year: 'numeric' }, 'short').format(unix * 1000),
|
||||
|
||||
longDate: (unix) =>
|
||||
dateFormat(tag, { day: 'numeric', month: 'long', year: 'numeric' }, 'long').format(unix * 1000),
|
||||
|
||||
monthYear: (unix) =>
|
||||
dateFormat(tag, { month: 'short', year: 'numeric' }, 'monthYear').format(unix * 1000),
|
||||
|
||||
serverTime: (unix) =>
|
||||
dateFormat(
|
||||
tag,
|
||||
{ dateStyle: 'short', timeStyle: 'short', timeZone: 'UTC' },
|
||||
'serverTime',
|
||||
).format(unix * 1000) + ' UTC',
|
||||
|
||||
number: (n) => numberFormat(tag).format(n),
|
||||
|
||||
decimal: (n, digits = 1) =>
|
||||
new Intl.NumberFormat(tag, {
|
||||
minimumFractionDigits: digits,
|
||||
maximumFractionDigits: digits,
|
||||
}).format(n),
|
||||
};
|
||||
}
|
||||
|
||||
/** Locale-free, so it lives here beside the rest rather than in two places. */
|
||||
export function starString(rating: number | null): string {
|
||||
if (rating === null) return '☆☆☆☆☆';
|
||||
const filled = Math.round(rating);
|
||||
return '★'.repeat(filled) + '☆'.repeat(Math.max(0, 5 - filled));
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 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
|
||||
* time (one process renders all three locales) and exactly what we do not want in the
|
||||
* 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
|
||||
* build machine only.
|
||||
*/
|
||||
import en from './en.json';
|
||||
import es from './es.json';
|
||||
import nl from './nl.json';
|
||||
|
||||
import { CLIENT_NAMESPACES, DEFAULT_LOCALE, LOCALE_CODES, isLocale, type Locale } from './config.js';
|
||||
import { createTranslator, type Catalog, type Translator } from './translate.js';
|
||||
|
||||
export * from './config.js';
|
||||
export * from './routing.js';
|
||||
export type { Translator, Vars } from './translate.js';
|
||||
|
||||
const CATALOGS: Record<string, Catalog> = {
|
||||
en: en as Catalog,
|
||||
es: es as Catalog,
|
||||
nl: nl as Catalog,
|
||||
};
|
||||
|
||||
/** The English catalog, which is what every other locale falls back to key by key. */
|
||||
export const BASE_CATALOG: Catalog = CATALOGS[DEFAULT_LOCALE]!;
|
||||
|
||||
export function catalogFor(locale: Locale): Catalog {
|
||||
return CATALOGS[locale] ?? BASE_CATALOG;
|
||||
}
|
||||
|
||||
const translators = new Map<string, Translator>();
|
||||
|
||||
/**
|
||||
* The translator for one locale. Cached, because every component on every page asks
|
||||
* for it and there are ~180 pages.
|
||||
*/
|
||||
export function useI18n(locale: Locale): Translator {
|
||||
const code = isLocale(locale) ? locale : DEFAULT_LOCALE;
|
||||
let t = translators.get(code);
|
||||
if (!t) {
|
||||
t = createTranslator(code, catalogFor(code), BASE_CATALOG);
|
||||
translators.set(code, t);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* Which keys are missing from which locale, as the build reports them.
|
||||
*
|
||||
* English is the source of truth: a key it does not have is not a key. Exported so
|
||||
* both the build warning in `astro.config.mjs` and `scripts/check-i18n.mjs` answer the
|
||||
* question the same way.
|
||||
*/
|
||||
export function missingKeys(): Record<string, string[]> {
|
||||
const expected = Object.keys(BASE_CATALOG);
|
||||
const out: Record<string, string[]> = {};
|
||||
for (const code of LOCALE_CODES) {
|
||||
if (code === DEFAULT_LOCALE) continue;
|
||||
const catalog = catalogFor(code);
|
||||
const missing = expected.filter((key) => catalog[key] === undefined);
|
||||
if (missing.length > 0) out[code] = missing;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* The subset of a catalog an island can reach, resolved against English first.
|
||||
*
|
||||
* Two things happen here that matter. Keys outside `CLIENT_NAMESPACES` are dropped, so
|
||||
* the Spanish home page does not inline the Dutch legal pages or its own. And every
|
||||
* 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
|
||||
* one language.
|
||||
*/
|
||||
export function clientCatalog(locale: Locale): Catalog {
|
||||
const catalog = catalogFor(locale);
|
||||
const out: Catalog = {};
|
||||
for (const key of Object.keys(BASE_CATALOG)) {
|
||||
const namespace = key.split('.')[0] ?? '';
|
||||
if (!(CLIENT_NAMESPACES as readonly string[]).includes(namespace)) continue;
|
||||
out[key] = catalog[key] ?? BASE_CATALOG[key]!;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* The locale codes, as plain JavaScript.
|
||||
*
|
||||
* `config.ts` is the real table and everything the site renders reads from it. This
|
||||
* exists because two things run before TypeScript does: `astro.config.mjs`, which needs
|
||||
* the codes to build the i18n config, and the build-time checker. Keeping it to the
|
||||
* 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.
|
||||
*
|
||||
* Adding a language: add the code here and the full entry in `config.ts`.
|
||||
*/
|
||||
export const DEFAULT_LOCALE = 'en';
|
||||
|
||||
export const LOCALE_CODES = ['en', 'es', 'nl'];
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* The bridge between `shared/warnings.ts` and the catalogs.
|
||||
*
|
||||
* `getMintWarnings` decides what is wrong with a mint and asks for the sentences by
|
||||
* key; this hands it the sentences. Keeping the decision in shared and the words here
|
||||
* is what lets the mint page, the mint card and the live-status island say the same
|
||||
* thing in the same language without any of them knowing how the others render.
|
||||
*
|
||||
* Ships to the browser, so it stays to two small functions.
|
||||
*/
|
||||
import type { MintWarningOptions, WarningStrings } from '@cashumints/shared';
|
||||
import type { Formatters } from './format.js';
|
||||
import type { Translator } from './translate.js';
|
||||
|
||||
/** The catalog prefix the warning keys live under. */
|
||||
const PREFIX = 'mint.warnings.';
|
||||
|
||||
/**
|
||||
* A resolver over one locale's catalog.
|
||||
*
|
||||
* The plural handling that matters is already in `t()`: it uses `Intl.PluralRules`, so
|
||||
* `dayCount` is right in a language with six plural categories without this function or
|
||||
* shared/ knowing anything about it.
|
||||
*/
|
||||
export function warningStrings(t: Translator): WarningStrings {
|
||||
return (key, vars) => t(PREFIX + key, vars);
|
||||
}
|
||||
|
||||
/**
|
||||
* Everything `getMintWarnings` needs to speak one locale: its sentences, and the date
|
||||
* formats the sentences interpolate.
|
||||
*
|
||||
* Passed by the mint page at build time and by the live island at runtime, so a banner
|
||||
* that a fresh probe replaces is in the same language as the one it replaced.
|
||||
*/
|
||||
export function warningOptions(f: Formatters): MintWarningOptions {
|
||||
return {
|
||||
strings: warningStrings(f.t),
|
||||
formatDate: (unix) => f.date(unix),
|
||||
formatMonth: (unix) => f.monthYear(unix),
|
||||
};
|
||||
}
|
||||
|
||||
/** The chip on a mint card, whose three labels sit under their own prefix. */
|
||||
export function chipStrings(t: Translator): WarningStrings {
|
||||
return (key) => t(`mint.chip.${key}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* "Online", "Sin conexión", "Onbereikbaar".
|
||||
*
|
||||
* An unknown status renders as itself rather than as a humanised key: if the API ever
|
||||
* grows a fifth state, showing the raw word is honest and showing a made-up label is
|
||||
* not.
|
||||
*/
|
||||
export function statusLabel(status: string, t: Translator): string {
|
||||
const key = `status.${status}`;
|
||||
return t.has(key) ? t(key) : status;
|
||||
}
|
||||
@@ -0,0 +1,550 @@
|
||||
{
|
||||
"site.name": "Cashumints.space",
|
||||
"common.close": "Sluiten",
|
||||
"common.cancel": "Annuleren",
|
||||
"common.done": "Klaar",
|
||||
"common.back": "Terug",
|
||||
"common.retry": "Opnieuw",
|
||||
"common.connect": "Verbinden",
|
||||
"common.copied": "Gekopieerd",
|
||||
"common.copyFull": "Volledige {kind} kopiëren",
|
||||
"common.previousPage": "Vorige pagina",
|
||||
"common.nextPage": "Volgende pagina",
|
||||
"common.reviewPages": "Reviewpagina's",
|
||||
"time.never": "nooit",
|
||||
"time.justNow": "net",
|
||||
"time.none": "-",
|
||||
"time.unknownDate": "een onbekende datum",
|
||||
"time.ago.minute": "{n} min geleden",
|
||||
"time.ago.hour": "{n} u geleden",
|
||||
"time.ago.day": "{n} d geleden",
|
||||
"time.ago.week": "{n} wk geleden",
|
||||
"time.ago.month": "{n} mnd geleden",
|
||||
"time.ago.year": "{n} jr geleden",
|
||||
"time.short.minute": "{n} min",
|
||||
"time.short.hour": "{n} u",
|
||||
"time.short.day": "{n} d",
|
||||
"time.short.week": "{n} wk",
|
||||
"time.short.month": "{n} mnd",
|
||||
"time.short.year": "{n} jr",
|
||||
"status.online": "Online",
|
||||
"status.offline": "Offline",
|
||||
"status.degraded": "Onbereikbaar",
|
||||
"status.unknown": "Nog niet gecontroleerd",
|
||||
"nav.skip": "Naar de inhoud",
|
||||
"nav.main": "Hoofdmenu",
|
||||
"nav.mints": "Mints",
|
||||
"nav.reviews": "Reviews",
|
||||
"nav.wallets": "Wallets",
|
||||
"nav.about": "Over",
|
||||
"nav.searchMints": "Zoek in {n} mints",
|
||||
"lang.label": "Taal",
|
||||
"lang.switcher": "Kies een taal",
|
||||
"footer.copyright": "© {year} Cashumints.space",
|
||||
"footer.allMints": "Alle mints",
|
||||
"footer.allReviews": "Alle reviews",
|
||||
"footer.wallets": "Wallets",
|
||||
"footer.about": "Over",
|
||||
"footer.disclaimer": "Disclaimer",
|
||||
"footer.terms": "Voorwaarden",
|
||||
"footer.privacy": "Privacy",
|
||||
"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.",
|
||||
"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.hero.title": "Vind een Cashu mint",
|
||||
"home.hero.titleEm": "die je kunt vertrouwen",
|
||||
"home.hero.lede": "Elke mint op het Nostr-netwerk, met echte reviews uit de community. Geen poortwachters, alleen ondertekende reviews die je zelf kunt controleren.",
|
||||
"home.search.label": "Zoek mints op naam, domein of URL",
|
||||
"home.search.placeholder": "Zoek op naam of domein, of plak een mint-URL…",
|
||||
"home.filters.mostReviewed": "Meeste reviews",
|
||||
"home.filters.highestRated": "Best beoordeeld",
|
||||
"home.filters.recentlyActive": "Recent actief",
|
||||
"home.filters.websockets": "Ondersteunt WebSockets",
|
||||
"home.top.title": "Beste mints",
|
||||
"home.top.sub": "gerangschikt op reviews en beoordeling",
|
||||
"home.top.all": "Alle {n} mints →",
|
||||
"home.latest.title": "Laatste reviews",
|
||||
"home.latest.sub": "live van het Nostr-netwerk",
|
||||
"home.latest.prev": "Vorige reviews",
|
||||
"home.latest.next": "Meer reviews",
|
||||
"home.latest.all": "Alle reviews →",
|
||||
"home.latest.empty": "Er kwamen op dit moment geen reviews van de relays. Open een willekeurige mintpagina om daar reviews te lezen en te schrijven.",
|
||||
"home.why.title": "Waarom zou je deze reviews vertrouwen?",
|
||||
"home.why.signed.title": "Ondertekend, niet ingediend",
|
||||
"home.why.signed.body": "Elke review is een {link}-event, ondertekend met de Nostr-sleutel van wie het schreef. Wij bewerken, rangschikken of verwijderen hier niets.",
|
||||
"home.why.signed.link": "NIP-87",
|
||||
"home.why.verify.title": "Controleer het zelf",
|
||||
"home.why.verify.body": "Reviews staan op publieke Nostr-relays, niet in onze database. Richt een willekeurige client op dezelfde relays en je ziet dezelfde gegevens.",
|
||||
"home.why.checked.title": "Doorlopend gecontroleerd",
|
||||
"home.why.checked.body": "We controleren elke mint op uptime, softwareversie, limieten en ondersteunde NUTs, dus wat je ziet is actueel en komt niet van de mint zelf.",
|
||||
"home.cta.title": "Denk je aan een mint? Lees er eerst over.",
|
||||
"home.cta.body": "Een mint houdt jouw ecash vast. Lees wat anderen tegenkwamen voordat je er sats in stopt, en laat zelf een review achter als je er een gebruikt hebt.",
|
||||
"home.cta.explore": "Alle mints bekijken",
|
||||
"home.cta.learn": "Meer over Cashu",
|
||||
"pulse.mintsIndexed": "mints geïndexeerd",
|
||||
"pulse.reviewsOnNostr": "reviews op Nostr",
|
||||
"pulse.lastReview": "laatste review",
|
||||
"pulse.mintsOnline": "mints online",
|
||||
"pulse.indexer": "indexer",
|
||||
"pulse.connected": "verbonden",
|
||||
"pulse.stale": "verouderd",
|
||||
"mints.title": "Alle Cashu mints · Cashumints.space",
|
||||
"mints.description": "Alle {total} Cashu mints die op het Nostr-netwerk zijn geïndexeerd, {online} online en {offline} offline, met beoordelingen uit de community, uptime en mint-software. Doorzoek en sorteer de volledige lijst.",
|
||||
"mints.heading": "Alle mints",
|
||||
"mints.lede": "Elke mint die op het Nostr-netwerk gevonden is, ook de mints die er niet meer zijn. Offline mints blijven in de lijst staan zodat je ze nog kunt vinden en reviewen.",
|
||||
"mints.search.label": "Zoek mints op naam of domein",
|
||||
"mints.search.placeholder": "Zoek op naam of domein…",
|
||||
"mints.sort.label": "Mints sorteren",
|
||||
"mints.sort.score": "Algemeen beste",
|
||||
"mints.sort.rating": "Best beoordeeld",
|
||||
"mints.sort.reviews": "Meeste reviews",
|
||||
"mints.sort.recent": "Recent gereviewd",
|
||||
"mints.sort.seen": "Recent online gezien",
|
||||
"mints.hideOffline": "Offline verbergen",
|
||||
"mints.showing": "{total} mints te zien ({online} online, {offline} offline)",
|
||||
"mints.showingFiltered": "{shown} van {total} mints te zien ({online} online, {offline} offline)",
|
||||
"mints.noResults": "Geen enkele mint komt overeen. Probeer een domein, of plak een mint-URL.",
|
||||
"mints.clearSearch": "Zoekopdracht wissen",
|
||||
"card.noRatings": "Nog geen beoordelingen",
|
||||
"card.reviews.one": "{n} review",
|
||||
"card.reviews.other": "{n} reviews",
|
||||
"card.lastSeen": "laatst gezien {when}",
|
||||
"card.neverSeen": "nooit online gezien",
|
||||
"card.reviewed": "laatste review {when}",
|
||||
"card.noReviews": "nog geen reviews",
|
||||
"mint.title": "{name} · Reviews van deze Cashu mint",
|
||||
"mint.description.fallback": "{name} is een Cashu mint op {domain}.",
|
||||
"mint.description.rated.one": "Beoordeeld met {rating} van de 5 op basis van {n} review, ondertekend op Nostr.",
|
||||
"mint.description.rated.other": "Beoordeeld met {rating} van de 5 op basis van {n} reviews, ondertekend op Nostr.",
|
||||
"mint.description.noReviews": "Nog geen reviews uit de community.",
|
||||
"mint.description.online": "Nu online.",
|
||||
"mint.description.unreachable": "Nu onbereikbaar.",
|
||||
"mint.breadcrumb": "Kruimelpad",
|
||||
"mint.crumbMints": "Mints",
|
||||
"mint.copyUrl": "Mint-URL kopiëren",
|
||||
"mint.qrButton": "Mint-QR",
|
||||
"mint.writeReview": "Schrijf een review",
|
||||
"mint.rankByScore": "#{rank} op score",
|
||||
"mint.tagLightning": "Lightning",
|
||||
"mint.summary": "Samenvatting van de mint",
|
||||
"mint.rating.label": "Beoordeling uit de community",
|
||||
"mint.rating.none": "Nog geen beoordelingen",
|
||||
"mint.rating.reviews.one": "{n} review",
|
||||
"mint.rating.reviews.other": "{n} reviews",
|
||||
"mint.rating.unrated.one": "{n} gaf geen beoordeling",
|
||||
"mint.rating.unrated.other": "{n} gaven geen beoordeling",
|
||||
"mint.activity.label": "Activiteit",
|
||||
"mint.activity.since": "sinds de laatste review",
|
||||
"mint.activity.noReviews": "nog geen reviews",
|
||||
"mint.activity.recent.one": "{n} review in de laatste 90 dagen",
|
||||
"mint.activity.recent.other": "{n} reviews in de laatste 90 dagen",
|
||||
"mint.limits.label": "Limieten",
|
||||
"mint.limits.notPublished": "niet gepubliceerd",
|
||||
"mint.limits.min": "min",
|
||||
"mint.limits.max": "max",
|
||||
"mint.limits.mintRow": "minten",
|
||||
"mint.limits.meltRow": "melten",
|
||||
"mint.limits.disabled": "uit",
|
||||
"mint.limits.range": "{min} tot {max} {unit}",
|
||||
"mint.limits.upTo": "tot {max} {unit}",
|
||||
"mint.limits.from": "vanaf {min} {unit}",
|
||||
"mint.software.label": "Mint-software",
|
||||
"mint.software.unknown": "onbekend",
|
||||
"mint.checked": "gecontroleerd {when}",
|
||||
"mint.lastSeen": "laatst gezien {when}",
|
||||
"mint.neverReached": "nooit bereikt",
|
||||
"mint.uptime.checks": "{ok} van de laatste {total} controles bereikbaar",
|
||||
"mint.uptime.percent": "{percent}% bereikbaar over 30 dagen",
|
||||
"mint.uptime.description": "{ok} van de laatste {total} controles slaagden",
|
||||
"mint.operator.title": "Van de beheerder",
|
||||
"mint.operator.motd": "Bericht van de dag",
|
||||
"mint.operator.beta": "Deze mint noemt zichzelf experimenteel of beta. Lees hieronder wat de beheerder er zelf over zegt voordat je er iets in bewaart dat je niet kunt missen.",
|
||||
"mint.nuts.title": "Ondersteunde NUTs",
|
||||
"mint.nuts.noneOffline": "Er is geen NUT-lijst voor deze mint bewaard, hij ging offline voordat er een vastgelegd was.",
|
||||
"mint.nuts.noneYet": "Er is nog geen NUT-lijst voor deze mint bewaard.",
|
||||
"mint.nuts.supported": "{n} van de {total} bekende NUTs ondersteund",
|
||||
"mint.nuts.whatAre": "wat zijn NUTs?",
|
||||
"mint.nutRow.disabled": "uit",
|
||||
"mint.nutRow.notSupported": "niet ondersteund",
|
||||
"mint.details.title": "Gegevens van de mint",
|
||||
"mint.details.pubkey": "Pubkey",
|
||||
"mint.details.nostr": "Nostr",
|
||||
"mint.details.email": "E-mail",
|
||||
"mint.details.serverTime": "Servertijd",
|
||||
"mint.details.firstSeen": "Eerst gezien",
|
||||
"mint.details.terms": "Voorwaarden",
|
||||
"mint.kind.identifier": "identificatie",
|
||||
"mint.kind.mintPubkey": "pubkey van de mint",
|
||||
"mint.kind.nostrAddress": "Nostr-adres",
|
||||
"mint.kind.emailAddress": "e-mailadres",
|
||||
"mint.kind.npub": "npub",
|
||||
"mint.disclaimer": "Deze mint is custodiaal: hij houdt jouw geld vast, en hij kan daar op elk moment mee stoppen. Beoordelingen zijn meningen uit de community, en online betekent bereikbaar, niet betrouwbaar.",
|
||||
"mint.disclaimerLink": "Lees de volledige disclaimer",
|
||||
"mint.qr.title": "{name} aan je wallet toevoegen",
|
||||
"mint.qr.note": "Scan dit met een Cashu wallet om de mint toe te voegen, of kopieer de URL en plak hem daar.",
|
||||
"mint.qr.warn": "Een mint toevoegen betekent dat je hem je geld toevertrouwt.",
|
||||
"mint.qr.whatMeans": "Wat dat betekent",
|
||||
"mint.qr.wallets": "Wallets die Cashu ondersteunen",
|
||||
"mint.chip.frozen": "Bevroren",
|
||||
"mint.chip.noWithdrawals": "Geen opnames",
|
||||
"mint.chip.meltOnly": "Alleen melten",
|
||||
"mint.warnings.dayCount.one": "{n} dag",
|
||||
"mint.warnings.dayCount.other": "{n} dagen",
|
||||
"mint.warnings.gone.lead": "Waarschijnlijk verdwenen.",
|
||||
"mint.warnings.gone.body.since": "Deze mint is onbereikbaar sinds {date} ({days}). Beschouw geld dat je er hebt staan als verloren, en stuur er niets nieuws heen. Wat hieronder staat is de laatst bekende staat.",
|
||||
"mint.warnings.gone.body.neverDated": "Deze mint heeft geen enkele controle beantwoord sinds we hem in {month} zijn gaan volgen. Beschouw geld dat je er hebt staan als verloren, en stuur er niets nieuws heen. Alles hieronder komt uit de aankondiging op Nostr, niet van de mint zelf.",
|
||||
"mint.warnings.gone.body.never": "Deze mint heeft geen enkele controle beantwoord sinds we hem zijn gaan volgen. Beschouw geld dat je er hebt staan als verloren, en stuur er niets nieuws heen. Alles hieronder komt uit de aankondiging op Nostr, niet van de mint zelf.",
|
||||
"mint.warnings.gone.meta.since": "Offline sinds {date}",
|
||||
"mint.warnings.gone.meta.never": "Nooit bereikbaar geweest sinds hij gevonden werd",
|
||||
"mint.warnings.meltDisabled.lead": "Opnemen staat uit.",
|
||||
"mint.warnings.meltDisabled.body.online": "Deze mint heeft melten via Lightning (NUT-05) uitgezet: je kunt er sats in stoppen, maar je krijgt ze er via Lightning niet uit. Stort niets tot melten weer werkt.",
|
||||
"mint.warnings.meltDisabled.body.offline": "Deze mint had melten via Lightning (NUT-05) uitgezet: je kunt er sats in stoppen, maar je krijgt ze er via Lightning niet uit. Stort niets tot melten weer werkt.",
|
||||
"mint.warnings.meltDisabled.meta.online": "Opnemen staat nu uit",
|
||||
"mint.warnings.meltDisabled.meta.offline": "Opnemen stond uit toen hij voor het laatst gezien werd",
|
||||
"mint.warnings.frozen.lead": "Mint bevroren.",
|
||||
"mint.warnings.frozen.body.online": "Minten en melten staan allebei uit. Er gaat geen enkele sat in of uit via Lightning.",
|
||||
"mint.warnings.frozen.body.offline": "Minten en melten stonden allebei uit. Er ging geen enkele sat in of uit via Lightning.",
|
||||
"mint.warnings.frozen.meta.online": "Nu bevroren, er gaat niets in of uit",
|
||||
"mint.warnings.frozen.meta.offline": "Bevroren toen hij voor het laatst gezien werd",
|
||||
"mint.warnings.offlineLong.lead": "Al {days} offline, sinds {date}.",
|
||||
"mint.warnings.offlineLong.body": "Deze mint is misschien weg. Alles hieronder is zijn laatst bekende staat, en reviews werken nog gewoon.",
|
||||
"mint.warnings.offlineLong.meta": "Offline sinds {date}",
|
||||
"mint.warnings.meltOnly.lead": "Alleen melten.",
|
||||
"mint.warnings.meltOnly.body.online": "Nieuw minten (NUT-04) staat uit: je kunt niet storten, maar ecash dat je al hebt kun je nog wel opnemen. Vaak betekent dit dat een mint aan het afbouwen is, dus als je er ecash van hebt, overweeg het eruit te melten.",
|
||||
"mint.warnings.meltOnly.body.offline": "Nieuw minten (NUT-04) stond uit: je kunt niet storten, maar ecash dat je al hebt kun je nog wel opnemen. Vaak betekent dit dat een mint aan het afbouwen is, dus als je er ecash van hebt, overweeg het eruit te melten.",
|
||||
"mint.warnings.meltOnly.meta.online": "Nu alleen melten",
|
||||
"mint.warnings.meltOnly.meta.offline": "Alleen melten toen hij voor het laatst gezien werd",
|
||||
"mint.warnings.offline.lead": "Offline sinds {date}.",
|
||||
"mint.warnings.offline.body": "De gegevens zijn toen voor het laatst bevestigd. Je kunt nog steeds een review schrijven.",
|
||||
"mint.warnings.offline.meta": "Offline sinds {date}",
|
||||
"mint.warnings.also.meltDisabled": "Voordat hij offline ging had hij opnemen ook al uitgezet.",
|
||||
"mint.warnings.also.mintDisabled": "Voordat hij offline ging had hij nieuw minten ook al uitgezet.",
|
||||
"mint.warnings.also.offline": "Hij is bovendien offline sinds {date} ({days}).",
|
||||
"nut.0": "Notatie en modellen",
|
||||
"nut.1": "Uitwisseling van de publieke sleutel van de mint",
|
||||
"nut.2": "Keysets en kosten",
|
||||
"nut.3": "Tokens wisselen",
|
||||
"nut.4": "Minten via Lightning",
|
||||
"nut.5": "Melten via Lightning",
|
||||
"nut.6": "Mint-informatie",
|
||||
"nut.7": "Tokenstatus controleren",
|
||||
"nut.8": "Te veel betaalde Lightning-kosten",
|
||||
"nut.9": "Handtekeningen herstellen",
|
||||
"nut.10": "Uitgavevoorwaarden",
|
||||
"nut.11": "Betalen aan publieke sleutel",
|
||||
"nut.12": "DLEQ-bewijzen",
|
||||
"nut.13": "Deterministische secrets",
|
||||
"nut.14": "Hashed timelock contracts",
|
||||
"nut.15": "Betalingen via meerdere paden",
|
||||
"nut.16": "Bewegende QR-codes",
|
||||
"nut.17": "WebSocket-abonnementen",
|
||||
"nut.18": "Betaalverzoeken",
|
||||
"nut.19": "Antwoorden uit cache",
|
||||
"nut.20": "Ondertekende mint-quotes",
|
||||
"nut.21": "Open authenticatie",
|
||||
"nut.22": "Blinde authenticatie",
|
||||
"nut.23": "BOLT11-betaalmethode",
|
||||
"nut.24": "HTTP 402, betaling vereist",
|
||||
"nut.25": "BOLT12-betaalmethode",
|
||||
"reviews.title": "Reviews",
|
||||
"reviews.anon": "Anon",
|
||||
"reviews.filter.group": "Reviews filteren",
|
||||
"reviews.filter.all": "Alle",
|
||||
"reviews.filter.five": "5★",
|
||||
"reviews.filter.critical": "Kritisch",
|
||||
"reviews.filter.recent": "Recent",
|
||||
"reviews.loading.one": "{n} review van Nostr-relays laden",
|
||||
"reviews.loading.other": "{n} reviews van Nostr-relays laden",
|
||||
"reviews.empty": "Nog geen reviews. Wees de eerste die deze mint reviewt.",
|
||||
"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.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.srRating": "{rating} van de 5",
|
||||
"reviews.srNoRating": "Geen beoordeling gegeven",
|
||||
"reviews.anonNote": "Eerste review van deze npub, verder geen activiteit gevonden",
|
||||
"reviews.published": "Gepubliceerd.",
|
||||
"reviews.publishedPartial": "Gepubliceerd op {accepted} van de {total} relays.",
|
||||
"reviews.propagating": "Wordt verspreid, het kan even duren voordat hij elders verschijnt.",
|
||||
"reviews.summary.line.one": "{count} beoordeling 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.other": "{count} beoordelingen zonder tekst op {when}: {breakdown}",
|
||||
"reviews.summary.showOne": "Laat zien",
|
||||
"reviews.summary.showMany": "Laat zien",
|
||||
"reviews.breakdown.5": "{n} van vijf sterren",
|
||||
"reviews.breakdown.4": "{n} van vier sterren",
|
||||
"reviews.breakdown.3": "{n} van drie sterren",
|
||||
"reviews.breakdown.2": "{n} van twee sterren",
|
||||
"reviews.breakdown.1": "{n} van één ster",
|
||||
"reviews.breakdown.none": "{n} zonder beoordeling",
|
||||
"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.ratingLegend": "Jouw beoordeling",
|
||||
"reviews.dialog.starsSr.one": "{n} ster",
|
||||
"reviews.dialog.starsSr.other": "{n} sterren",
|
||||
"reviews.dialog.chooseRating": "Kies een beoordeling",
|
||||
"reviews.dialog.starLabel": "{n}: {phrase}",
|
||||
"reviews.dialog.stars.1": "blijf er weg",
|
||||
"reviews.dialog.stars.2": "had problemen",
|
||||
"reviews.dialog.stars.3": "werkte, met kanttekeningen",
|
||||
"reviews.dialog.stars.4": "goed",
|
||||
"reviews.dialog.stars.5": "werkt prima",
|
||||
"reviews.dialog.bodyLabel": "Wat moeten anderen weten?",
|
||||
"reviews.dialog.bodyPlaceholder": "Hoe ging het minten, het melten en de support? Wees concreet.",
|
||||
"reviews.dialog.charCount": "{n} / {max}",
|
||||
"reviews.dialog.submit": "Ondertekenen en publiceren",
|
||||
"reviews.dialog.signing": "Ondertekenen…",
|
||||
"reviews.dialog.publishing": "Publiceren…",
|
||||
"reviews.dialog.publishingTo": "Publiceren op relays ({accepted}/{total})",
|
||||
"reviews.dialog.waitingExtension": "Wachten op je extensie",
|
||||
"reviews.dialog.waitingSigner": "Wachten op je signer",
|
||||
"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.rating": "Kies een beoordeling",
|
||||
"reviews.dialog.why.words": "Schrijf een paar woorden",
|
||||
"reviews.dialog.why.more.one": "nog {n} teken",
|
||||
"reviews.dialog.why.more.other": "nog {n} tekens",
|
||||
"reviews.dialog.why.over.one": "{n} teken te veel",
|
||||
"reviews.dialog.why.over.other": "{n} tekens te veel",
|
||||
"reviews.dialog.identity.needSigner": "Je hebt een Nostr-signer nodig om te publiceren. Je sleutel blijft daarin, deze site ziet hem nooit.",
|
||||
"reviews.dialog.identity.loginToPublish": "Log in om te publiceren",
|
||||
"reviews.dialog.identity.switch": "Wisselen",
|
||||
"reviews.dialog.identity.extension": "Ondertekenen met je browserextensie",
|
||||
"reviews.dialog.identity.readingKey": "Je publieke sleutel lezen…",
|
||||
"reviews.dialog.identity.willAsk": "Je extensie krijgt de vraag om te ondertekenen",
|
||||
"reviews.dialog.error.noRelay": "Geen enkele relay heeft de review aangenomen. Controleer je verbinding en probeer het opnieuw.",
|
||||
"reviews.validate.rating": "Kies een beoordeling van 1 tot 5 sterren.",
|
||||
"reviews.validate.short": "Schrijf minstens {n} tekens, anders heeft niemand iets aan de review.",
|
||||
"reviews.validate.long": "Reviews mogen maximaal {n} tekens lang zijn.",
|
||||
"feed.title": "Laatste reviews van Cashu mints · Cashumints.space",
|
||||
"feed.description": "Elke review van een Cashu mint zodra hij verschijnt: {reviews} reviews over {mints} mints, ondertekend op Nostr en hier te lezen met de nieuwste bovenaan. Filter op beoordeling of op mint.",
|
||||
"feed.heading": "Laatste reviews van Cashu mints",
|
||||
"feed.lede": "Elke review van elke mint, 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.filterByMint": "Filteren op mint",
|
||||
"feed.anyMint": "Elke mint",
|
||||
"feed.clearMint": "Mintfilter wissen",
|
||||
"feed.showingBuild.one": "De meest recente review van de laatste build te zien",
|
||||
"feed.showingBuild.other": "De {n} meest recente reviews van de laatste build te zien",
|
||||
"feed.noscript": "Reviews worden op Nostr ondertekend en in je browser geladen, dus filteren en de volledige lijst hebben JavaScript nodig. De {n} reviews hieronder zijn van de relays gelezen toen deze pagina gebouwd werd.",
|
||||
"feed.emptyBuild": "Er konden geen reviews van de relays gelezen worden toen deze pagina gebouwd werd. De lijst wordt in je browser opnieuw van de relays geladen.",
|
||||
"feed.foot": "Reviews zijn meningen uit de community, geen gecontroleerde feiten. Lees de {link} voordat je een mint je geld toevertrouwt.",
|
||||
"feed.footLink": "disclaimer",
|
||||
"feed.showing": "{shown} van {total} reviews te zien",
|
||||
"feed.showingMatching": "{shown} van {matching} passende reviews te zien ({total} in totaal)",
|
||||
"feed.emptyRelays": "Er kwamen geen reviews terug van de relays. Open een willekeurige mintpagina om daar reviews te lezen en te schrijven.",
|
||||
"feed.emptyFilter": "Geen enkele review past bij dit filter. Maak het mintveld leeg, of probeer Alle.",
|
||||
"feed.relayError": "De relays waren niet bereikbaar. De reviews van de laatste build zijn te zien.",
|
||||
"feed.relayEmpty": "Er kwamen geen reviews terug van de relays. De reviews van de laatste build zijn te zien.",
|
||||
"feed.reading": "De relays lezen…",
|
||||
"login.open": "Inloggen",
|
||||
"login.title": "Inloggen met Nostr",
|
||||
"login.note": "Je sleutel blijft in je signer. Deze site kan hem nooit zien.",
|
||||
"login.extension.name": "Browserextensie",
|
||||
"login.extension.desc": "Onderteken met Alby, nos2x of een andere NIP-07-extensie",
|
||||
"login.extension.none": "Geen extensie gevonden",
|
||||
"login.extension.getOne": "Haal er een:",
|
||||
"login.remote.name": "Signer op afstand",
|
||||
"login.remote.desc": "Scan een code met een signer-app, of plak een bunker-link",
|
||||
"login.primal.name": "Primal",
|
||||
"login.primal.desc": "Keur het inloggen goed in de Primal-app op je telefoon",
|
||||
"login.paste.label": "Of plak de verbindingslink die je signer je gaf",
|
||||
"login.copyLink": "De verbindingslink kopiëren",
|
||||
"login.whatIsNostr": "Wat is Nostr?",
|
||||
"login.connect.remote.title": "Een signer op afstand verbinden",
|
||||
"login.connect.remote.lead": "Scan dit met je signer-app, of kopieer de link en plak hem daar.",
|
||||
"login.connect.remote.hint": "Werkt met elke NIP-46-signer: nsec.app, Amber, Nostrify, je eigen bunker.",
|
||||
"login.connect.primal.title": "Verbinden met Primal",
|
||||
"login.connect.primal.lead": "Open Primal op je telefoon, zoek ondertekenen op afstand in de instellingen, en scan deze code.",
|
||||
"login.connect.primal.hint": "Primal ondertekent via NIP-46 voor andere apps, dus dit is dezelfde verbinding die elke signer gebruikt, en je sleutel blijft in Primal. {link}",
|
||||
"login.status.buildingLink": "Een verbindingslink maken",
|
||||
"login.status.waitingExtension": "Wachten op je extensie",
|
||||
"login.status.waitingSigner": "Wachten op je signer",
|
||||
"login.status.connecting": "Verbinden met je signer",
|
||||
"login.status.connected": "Verbonden",
|
||||
"login.method.extension": "Extensie",
|
||||
"login.method.remote": "Op afstand",
|
||||
"login.method.primal": "Primal",
|
||||
"login.account": "Jouw Nostr-account",
|
||||
"login.copyNpub": "Je volledige npub kopiëren",
|
||||
"login.logout": "Uitloggen",
|
||||
"signer.status.waitingExtension": "Wachten op je extensie",
|
||||
"signer.status.signed": "Ondertekend",
|
||||
"signer.status.reconnecting": "Opnieuw verbinden met je signer",
|
||||
"signer.status.approveWindow": "Je signer wil dat je dit in zijn eigen venster goedkeurt",
|
||||
"signer.status.waitingSigner": "Wachten op je signer",
|
||||
"signer.status.stillWaiting": "Nog steeds wachten tot je signer antwoordt",
|
||||
"signer.status.connected": "Verbonden",
|
||||
"signer.error.noExtension": "Geen ondertekenextensie gevonden. Installeer Alby of nos2x en herlaad deze pagina.",
|
||||
"signer.error.privateKey": "Dat lijkt op een privésleutel, niet op een verbindingslink. Deze site neemt nooit een sleutel aan. Plak de bunker://-link die je signer je gaf.",
|
||||
"signer.error.badLink": "Dat is geen verbindingslink die deze site begrijpt. Plak de bunker://-link van je signer.",
|
||||
"signer.error.noAnswer": "Je signer antwoordde niet. Hij is misschien afgesloten, of zijn relay ligt eruit. Open hem en probeer het opnieuw.",
|
||||
"signer.error.timeout": "Je signer antwoordde niet op tijd. Open hem, kijk of hij nog verbonden is, en probeer het opnieuw.",
|
||||
"signer.error.cancelled": "Verbinding geannuleerd.",
|
||||
"signer.error.declined": "Het ondertekenverzoek is geweigerd",
|
||||
"notfound.title": "Pagina niet gevonden · Cashumints.space",
|
||||
"notfound.description": "Die pagina bestaat niet op Cashumints.space.",
|
||||
"notfound.heading": "Deze pagina bestaat niet",
|
||||
"notfound.lede": "Misschien is hij verplaatst, of is de mint nog niet geïndexeerd. Zoek een mint op naam of domein, of plak een mint-URL.",
|
||||
"notfound.browse": "Alle mints bekijken",
|
||||
"notfound.home": "Naar de startpagina",
|
||||
"notfound.loading": "{host} laden",
|
||||
"notfound.fb.rating": "Beoordeling",
|
||||
"notfound.fb.reviews": "Reviews",
|
||||
"notfound.fb.software": "Software",
|
||||
"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.",
|
||||
"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.heading": "Wallets",
|
||||
"wallets.lede": "Je hebt een wallet nodig om ecash van deze mints in te bewaren. De meeste wallets praten met meerdere mints tegelijk, dus je zit niet vast aan de eerste die je probeert.",
|
||||
"wallets.note": "Deze lijst is niet volledig en is geen aanbeveling. Het Cashu-project houdt een bredere index bij op {link}. Een wallet bewaart sleutels, een mint bewaart het geld: lees de reviews van de mints voordat je kiest waar je sats wonen.",
|
||||
"wallets.platform.android": "Android",
|
||||
"wallets.platform.ios": "iOS",
|
||||
"wallets.platform.web": "Web",
|
||||
"wallets.platform.extension": "Browserextensie",
|
||||
"wallets.platform.cli": "Opdrachtregel",
|
||||
"wallets.platform.python": "Python",
|
||||
"wallets.item.minibits": "Mobiele ecash wallet met een eigen mint, ondersteuning voor Lightning-adressen en Nostr-contacten.",
|
||||
"wallets.item.nutstash": "Browserwallet die meerdere mints tegelijk aankan en ruwe tokens importeert en exporteert.",
|
||||
"wallets.item.enuts": "Mobiele wallet gericht op saldo's bij meerdere mints en het versturen van ecash via Nostr.",
|
||||
"wallets.item.cashume": "De referentiewallet voor het web van het Cashu-project. Draait volledig in de browser en is te installeren als PWA.",
|
||||
"wallets.item.boardwalk": "Webwallet gebouwd rond Lightning-adressen en het versturen van ecash via een link.",
|
||||
"wallets.item.coinos": "Lightning wallet en kassasysteem dat ook een eigen Cashu mint draait.",
|
||||
"wallets.item.oxchat": "Nostr-messenger met een ingebouwde ecash wallet, om sats in een gesprek te versturen.",
|
||||
"wallets.item.nutshell": "De referentie-implementatie. Draait als wallet op de opdrachtregel en als mint-software.",
|
||||
"about.title": "Over Cashumints.space",
|
||||
"about.description": "Wat Cashu is, wat een mint werkelijk vasthoudt, wat NIP-87 is, en hoe deze site mints indexeert en reviews van Nostr-relays leest.",
|
||||
"about.heading": "Over",
|
||||
"about.lede": "Cashumints.space toont elke Cashu mint die we op het Nostr-netwerk kunnen vinden en laat zien wat mensen erover zeggen. De site is gemaakt door Azzamo.",
|
||||
"about.cashu.title": "Wat Cashu is",
|
||||
"about.cashu.p1": "Cashu is een ecash-protocol voor Bitcoin. Een mint geeft blinde tokens uit die gedekt zijn door sats die hij vasthoudt, en die tokens gaan van persoon naar persoon zonder dat de mint te weten komt wie wat aan wie stuurde. De privacy is echt en is het hele punt: tokens zijn geen rekeningen, en de mint kan het token dat hij ondertekende niet koppelen aan het token dat terugkomt.",
|
||||
"about.cashu.p2": "Je kunt het protocol en de specificaties lezen op {link}. De specificaties heten NUTs, en daar komen de NUT-nummers op elke mintpagina vandaan.",
|
||||
"about.mints.title": "Wat een mint is, en wat het je kost",
|
||||
"about.mints.p1": "Een mint is custodiaal. Als je ecash mint geef je sats aan wie die server draait, en vertrouw je erop dat die de tokens honoreert als je ze er weer uit melt. Er is geen fraudebewijs, geen escrow en geen verhaal. Wie een mint beheert kan er met het saldo vandoor gaan, en dat is gebeurd.",
|
||||
"about.mints.p2": "Precies daarom bestaan reviews hier. Deze site vertelt je nooit dat een mint veilig is. De status op een mintpagina zegt {code}, niet \"betrouwbaar\", en betekent alleen dat de server antwoordde toen we het voor het laatst vroegen. Behandel ecash-saldo's als los geld in een jaszak: prima om uit te geven, verkeerd om te sparen.",
|
||||
"about.nip87.title": "Wat NIP-87 is",
|
||||
"about.nip87.p1": "NIP-87 is de Nostr-afspraak voor het aankondigen en aanbevelen van ecash mints. Twee soorten events doen er hier toe:",
|
||||
"about.nip87.li1": "{code} kondigt een mint aan. De mint-URL staat in de {tag}-tag. Zo vindt de site mints die hij nog nooit gezien heeft.",
|
||||
"about.nip87.li2": "{code} is een review. Hij bevat de beoordeling en de tekst, en wijst naar de mint waar hij over gaat met een {utag}-tag (de mint-URL) en een {dtag}-tag (de eigen publieke sleutel van de mint).",
|
||||
"about.nip87.p2": "Elke review is ondertekend met de Nostr-sleutel van wie hem schreef. We noemen een review nooit \"geverifieerd\", want niemand heeft hem geverifieerd: hij is ondertekend, en dat is iets anders en nuttiger. Het betekent dat je kunt nagaan wie hem schreef en dat er niets aan veranderd is. Het betekent niet dat wie hem schreef eerlijk is of de mint ooit gebruikt heeft.",
|
||||
"about.how.title": "Hoe deze site werkt",
|
||||
"about.how.p1": "Er zijn twee lussen en één regel. De regel is dat de mint eigenaar is van zijn eigen gegevens en Nostr eigenaar is van de reviews, dus deze site bewaart geen van beide als autoriteit.",
|
||||
"about.how.li1": "{strong} vraagt elke bekende mint om de tien minuten om {code} en legt vast wat die zegt: naam, omschrijving, versie, ondersteunde NUTs, limieten, contactgegevens. Hij legt ook vast of de mint überhaupt antwoordde, en daar komt de uptime vandaan.",
|
||||
"about.how.li1Label": "De controlelus",
|
||||
"about.how.li2": "{strong} maakt elk uur verbinding met Nostr-relays, leest mintaankondigingen en reviews, en voegt elke mint toe die hij nog niet gezien had.",
|
||||
"about.how.li2Label": "De ontdekkingslus",
|
||||
"about.how.p2": "Het belangrijkste is wat er gebeurt als een mint stopt met antwoorden. Zijn bewaarde gegevens worden nooit overschreven door een mislukte controle, dus een dode mint houdt zijn volledige pagina: zijn specificaties, zijn geschiedenis en zijn reviews, met een waarschuwing die zegt wanneer hij donker werd. De vorige versie van deze site liet een foutpagina zien, en dat betekende dat juist de mints waar mensen elkaar het hardst voor moesten waarschuwen niet te reviewen waren. Dat was de belangrijkste reden om de site opnieuw te bouwen.",
|
||||
"about.how.p3": "Reviewteksten worden bij ons nooit bewaard. Als je een mintpagina opent praat je browser rechtstreeks met de relays en leest hij de reviews zelf, en als je er een schrijft wordt hij ondertekend door je eigen Nostr-extensie en gaat hij regelrecht naar die relays. Wij houden een telling bij zodat de pagina's gerangschikt en gesorteerd kunnen worden, meer niet. Wij kunnen hier niets bewerken of verwijderen, ook geen reviews die we liever niet hadden gehad.",
|
||||
"about.relays.title": "Relays die we lezen",
|
||||
"about.relays.p1": "Dit zijn de relays die zowel de indexer als jouw browser gebruiken:",
|
||||
"about.relays.p2": "Richt een willekeurige Nostr-client op dezelfde relays en je ziet dezelfde events. Dat is de hele belofte: hier is niets waar alleen omdat wij het zeggen.",
|
||||
"about.nuts.title": "Wat zijn NUTs?",
|
||||
"about.nuts.p1": "NUTs zijn de genummerde Cashu-specificaties, zoals BIPs voor Bitcoin. Een mint geeft aan welke hij toepast, en dat vertelt je wat hij echt kan: NUT-04 en NUT-05 zijn minten en melten via Lightning (zonder die twee heb je weinig aan een mint), NUT-07 laat een wallet nagaan of een token al uitgegeven is, NUT-17 geeft wallets live updates via WebSockets in plaats van steeds te moeten vragen, en NUT-20 laat een wallet bewijzen dat een mint-quote echt aan hem is uitgegeven. De volledige lijst staat op {link}.",
|
||||
"about.numbers.title": "Op dit moment",
|
||||
"about.numbers.p1": "{mints} mints geïndexeerd, waarvan er {online} antwoorden, {reviews} reviews geteld van de relays hierboven.",
|
||||
"about.source.title": "Broncode en credits",
|
||||
"about.source.p1": "De site is open source op {github}. Hij is gebouwd en wordt beheerd door {azzamo}. Staat er iets fout op een mintpagina, los het dan bij de bron op: de gegevens van een mint komen uit zijn eigen {code}, en reviews komen van Nostr.",
|
||||
"terms.title": "Gebruiksvoorwaarden · Cashumints.space",
|
||||
"terms.description": "Gebruiksvoorwaarden voor Cashumints.space: wat de site is, wat hij niet doet, en wie waarvoor verantwoordelijk is. Geen accounts, geen garanties, geen band met welke mint dan ook.",
|
||||
"terms.heading": "Gebruiksvoorwaarden",
|
||||
"terms.lede": "Nuchtere voorwaarden voor een site die alleen publieke gegevens indexeert. Laatst bijgewerkt op {date}.",
|
||||
"terms.what.title": "Wat deze site is",
|
||||
"terms.what.p1": "Cashumints.space is een index van openbaar beschikbare gegevens over Cashu mints. Twee soorten gegevens, allebei van elders:",
|
||||
"terms.what.li1": "Gegevens van de mint, gelezen van het {code}-endpoint van de mint zelf: naam, omschrijving, softwareversie, ondersteunde NUTs, contactgegevens en of de mint antwoordde toen we het voor het laatst vroegen.",
|
||||
"terms.what.li2": "Reviews, gelezen van publieke Nostr-relays. Dat zijn NIP-87-events die door hun auteurs zijn gepubliceerd op relays die iedereen kan bevragen.",
|
||||
"terms.what.p2": "Wij verzamelen die gegevens, maken ze uniform, rangschikken ze en tonen ze. Wij maken ze niet.",
|
||||
"terms.noAccounts.title": "Geen accounts, geen garanties",
|
||||
"terms.noAccounts.p1": "Er valt nergens iets aan te melden. De site wordt geleverd zoals hij is, zonder enige garantie: niet dat de gegevens kloppen, niet dat ze actueel zijn, en niet dat de site beschikbaar zal zijn. Gegevens van een mint kunnen minuten of uren oud zijn, een mint kan veranderd zijn na onze laatste controle, en een relay kan niet antwoorden, en dan zie je minder dan er is.",
|
||||
"terms.noAccounts.p2": "Gebruik hem op eigen risico. Wij zijn niet aansprakelijk voor verlies dat volgt uit het gebruik van deze site of uit het vertrouwen op wat je hier leest. Lees de {link} voor de specifieke risico's van het gebruik van een Cashu mint.",
|
||||
"terms.noAccounts.link": "disclaimer",
|
||||
"terms.noCustody.title": "Wij houden jouw geld niet vast",
|
||||
"terms.noCustody.p1": "Deze site neemt nooit geld in bewaring. Hij heeft geen wallet, raakt geen ecash aan, en kan niets verplaatsen, bevriezen of terughalen. Heb je een probleem met een mint, dan is de beheerder van die mint de enige partij die je kan helpen, en wij kunnen die niet namens jou benaderen.",
|
||||
"terms.noCustody.p2": "Wij hebben geen band met welke mint hier ook staat, en geen enkele mint betaalt voor zijn vermelding, zijn plek of zijn beoordeling. Mints worden gevonden via publieke aankondigingen op Nostr en gerangschikt door een algoritme over publieke reviewgegevens.",
|
||||
"terms.reviews.title": "Reviews zijn de woorden van anderen",
|
||||
"terms.reviews.p1": "Reviews zijn door gebruikers gemaakte inhoud die hun auteurs op publieke Nostr-relays publiceren. Wij tonen ze, wij hebben er geen zeggenschap over. Wij hebben ze niet geschreven, wij kunnen ze niet bewerken, en wij kunnen ze niet verwijderen van de relays die ze bewaren: het zijn ondertekende events op een netwerk dat niet van ons is.",
|
||||
"terms.reviews.p2": "Een review is de ervaring van één persoon, geen gecontroleerd feit. Publiceer je er een, dan ben je verantwoordelijk voor wat erin staat. Publiceer niets dat onwettig is, en besef dat wat je publiceert openbaar en blijvend is (zie de {link}).",
|
||||
"terms.reviews.link": "privacypagina",
|
||||
"terms.reviews.p3": "Wij kunnen een bepaalde review op deze site niet meer tonen, en dat doen we bij inhoud die onwettig is. Dat haalt hem alleen van deze pagina's af. Het event blijft op de relays staan en elke andere Nostr-client kan het nog steeds lezen.",
|
||||
"terms.links.title": "Links naar andere sites",
|
||||
"terms.links.p1": "Mint-URL's, links naar wallets, contactgegevens van beheerders en al het andere dat van deze site af wijst staat er omdat het bij de gegevens hoort. Een link is geen aanbeveling en wij controleren niet wat er aan de andere kant staat.",
|
||||
"terms.changes.title": "Wijzigingen",
|
||||
"terms.changes.p1": "Deze voorwaarden kunnen veranderen. De datum bovenaan zegt wanneer dat voor het laatst gebeurde. De site is open source, dus elke wijziging van deze pagina staat in de commitgeschiedenis.",
|
||||
"terms.contact.title": "Contact",
|
||||
"terms.contact.p1": "Meldingen en verzoeken om iets te verwijderen kun je het beste indienen op {github}. De site wordt beheerd door {azzamo}.",
|
||||
"privacy.title": "Privacy · Cashumints.space",
|
||||
"privacy.description": "Privacy op Cashumints.space: geen accounts, geen trackingcookies, geen analytics. localStorage wordt gebruikt om profielen te cachen en er gaat niets ergens naartoe. Reviews worden in je eigen browser ondertekend.",
|
||||
"privacy.heading": "Privacy",
|
||||
"privacy.lede": "We verzamelen zo weinig als een website maar kan. Laatst bijgewerkt op {date}.",
|
||||
"privacy.noAccounts.title": "Geen accounts",
|
||||
"privacy.noAccounts.p1": "Er is geen aanmelding, geen login en geen profiel. Niets op deze site vraagt wie je bent, en er is geen gebruikersgegeven dat kan uitlekken omdat er geen gebruikers zijn.",
|
||||
"privacy.noTracking.title": "Geen trackingcookies, geen analytics",
|
||||
"privacy.noTracking.p1": "De site zet geen cookies om je te volgen. Er is geen analyticsscript, geen tagmanager, geen pixel, geen sessieopname en geen advertentienetwerk. Niets op de pagina rapporteert jouw gedrag aan ons of aan iemand anders.",
|
||||
"privacy.logs.title": "Serverlogs",
|
||||
"privacy.logs.p1": "De webserver houdt gewone toegangslogs bij, zoals elke webserver standaard doet. Een logregel legt je IP-adres vast, het tijdstip, de pagina die je opvroeg, de responsstatus, en de user agent en de referrer die je browser meestuurde. De logs bestaan om fouten op te sporen en misbruik te zien. Ze worden nergens aan gekoppeld en ze worden niet verkocht of gedeeld.",
|
||||
"privacy.logs.operatorLabel": "Noot voor de beheerder:",
|
||||
"privacy.logs.operator": "draai jij deze site, zet hier dan je echte bewaartermijn neer. De opzet in de README gebruikt nginx met het standaard toegangslog en de rotatie die jouw distributie meelevert (op Debian en Ubuntu is dat logrotate, dagelijks, met 14 dagen bewaring). Kijk na wat jouw server doet en vervang deze alinea door dat getal.",
|
||||
"privacy.storage.title": "Wat er in je browser wordt bewaard",
|
||||
"privacy.storage.p1": "De site gebruikt localStorage voor precies twee dingen, en die blijven allebei op je apparaat en gaan nergens heen:",
|
||||
"privacy.storage.li1": "Een cache van Nostr-profielgegevens (kind 0-events: namen, avatars, NIP-05-adressen) van de mensen wier reviews je bekeken hebt. Zo hoeven de relays niet op elke pagina opnieuw bevraagd te worden. Regels verlopen, en de cache heeft een maximum en gooit de oudste er als eerste uit.",
|
||||
"privacy.storage.li2": "Voorkeuren voor de interface, zodat de site zich de volgende keer hetzelfde gedraagt.",
|
||||
"privacy.storage.p2": "Als je de sitegegevens in je browser wist verdwijnen ze allebei. Er gaat niets stuk, de profielen worden gewoon opnieuw opgezocht.",
|
||||
"privacy.thirdParties.title": "Waar je browser mee praat",
|
||||
"privacy.thirdParties.p1": "Een pagina laden verbindt je browser met een paar plekken buiten deze site. Zo leest een site zonder tracking aan de serverkant toch live gegevens:",
|
||||
"privacy.thirdParties.li1Label": "Publieke Nostr-relays",
|
||||
"privacy.thirdParties.li1": "{strong}, via WebSocket, om reviews en profielen te lezen. De relay ziet je IP en wat je opvroeg, zoals elke server waarmee je verbinding maakt. De relays staan op de {link}.",
|
||||
"privacy.thirdParties.li1Link": "pagina Over",
|
||||
"privacy.thirdParties.li2Label": "Avatarafbeeldingen",
|
||||
"privacy.thirdParties.li2": "{strong}, geladen van waar het profiel van wie de review schreef naar wijst. Ze worden opgevraagd met een no-referrer-beleid, dus die host komt niet te weten op welke pagina je was.",
|
||||
"privacy.publishing.title": "Een review publiceren",
|
||||
"privacy.publishing.p1": "Een review schrijven gebeurt in je browser, niet op onze server. Het event wordt ondertekend door je NIP-07-extensie (Alby, nos2x of iets dergelijks) en vanuit je browser rechtstreeks naar de relays gestuurd. Je privésleutel verlaat de extensie nooit en deze site ziet hem nooit. Wij bouwen geen eigen sleutelbeheer en wij vragen nooit om een sleutel of een seed phrase.",
|
||||
"privacy.permanent.title": "Alles wat je op Nostr publiceert is openbaar en blijvend",
|
||||
"privacy.permanent.p1": "Dit is het stuk dat het waard is om twee keer te lezen. Een review is een ondertekend event op publieke relays. Eenmaal weg is weg:",
|
||||
"privacy.permanent.li1": "Iedereen kan hem lezen, nu en later, met elke Nostr-client. Hij is niet \"openbaar op Cashumints.space\", hij is openbaar op het netwerk.",
|
||||
"privacy.permanent.li2": "Hij is voorgoed gekoppeld aan jouw publieke Nostr-sleutel, naast al het andere dat die sleutel ooit gepubliceerd heeft.",
|
||||
"privacy.permanent.li3": "Hem verwijderen is een verzoek, geen bevel. Je kunt een verwijderevent publiceren en sommige relays houden zich daaraan, maar een relay mag het negeren en wie het event al gekopieerd had heeft het nog steeds. Beschouw een gepubliceerde review als blijvend.",
|
||||
"privacy.permanent.li4": "Wij kunnen hem niet voor je verwijderen. Wij kunnen hem hier niet meer tonen, en dat verandert niets op de relays.",
|
||||
"privacy.permanent.p2": "Wil je een mint reviewen zonder dat het aan je hoofdidentiteit vastzit, gebruik dan een aparte Nostr-sleutel. Let wel: bij harde reviews wordt vermeld als een sleutel verder geen activiteit heeft, want een gloednieuwe sleutel die een review van één ster schrijft is een patroon dat lezers verdienen te zien.",
|
||||
"privacy.contact.title": "Vragen",
|
||||
"privacy.contact.p1": "De site is open source: wil je iets van het bovenstaande nagaan, lees dan de code op {github}. Vragen en correcties kunnen daar in een issue.",
|
||||
"disclaimer.title": "Disclaimer · Cashumints.space",
|
||||
"disclaimer.description": "Wat je moet weten voordat je een Cashu mint gebruikt: mints zijn custodiaal, een mint kan er met jouw geld vandoor gaan, reviews zijn meningen en online betekent bereikbaar, niet betrouwbaar.",
|
||||
"disclaimer.heading": "Disclaimer",
|
||||
"disclaimer.lede": "Lees dit voordat je geld in een mint stopt. Laatst bijgewerkt op {date}.",
|
||||
"disclaimer.callout": "Een Cashu mint houdt jouw geld voor je vast. Verdwijnt hij, dan is je ecash weg, en is er niemand bij wie je kunt aankloppen. Bewaar er alleen wat je kunt missen.",
|
||||
"disclaimer.custodial.title": "Mints zijn custodiaal",
|
||||
"disclaimer.custodial.p1": "Als je ecash mint geef je bitcoin aan de beheerder van de mint en krijg je tokens terug. Die tokens zijn een aanspraak op die beheerder. Het is geen bitcoin die jij vasthoudt, en de beheerder is degene die beslist of hij ze honoreert.",
|
||||
"disclaimer.custodial.p2": "Dat betekent dat elke mint dezelfde risico's draagt: de beheerder kan verdwijnen, kan gedwongen worden te stoppen, kan het geld kwijtraken door een bug of een hack, of kan de server gewoon uitzetten. Er is geen depositogarantie, geen terugboeking, geen helpdesk en geen juridische weg waar je op kunt rekenen. Dit geldt ook voor de best beoordeelde mint op deze site.",
|
||||
"disclaimer.custodial.p3": "Cashu is bedoeld voor kleine bedragen die snel bewegen. Gebruik het zo: bewaar wat je op het punt staat uit te geven, niet je spaargeld, en spreid over meerdere mints in plaats van alles bij één te parkeren.",
|
||||
"disclaimer.reviews.title": "Reviews zijn meningen, geen gecontroleerde feiten",
|
||||
"disclaimer.reviews.p1": "Elke review hier is geschreven door iemand op Nostr en ondertekend met diens sleutel. Ondertekend betekent dat we je kunnen vertellen welke sleutel hem schreef. Het betekent niet dat de review klopt, dat wie hem schreef de mint gebruikt heeft, of dat het niet de beheerder is, een concurrent, of één persoon met tien sleutels.",
|
||||
"disclaimer.reviews.p2": "Wij controleren niemand die een review schrijft, en wij bewerken of verwijderen geen reviews waar we het niet mee eens zijn. Lees er een paar, kijk wat er werkelijk staat, en weeg een mint met drie reviews anders dan een met driehonderd.",
|
||||
"disclaimer.status.title": "Online betekent bereikbaar, niet betrouwbaar",
|
||||
"disclaimer.status.p1": "We controleren elke mint regelmatig en laten zien of hij antwoordde. Meer betekent een groene status niet: de server gaf antwoord. Het zegt niets over of de beheerder solvabel is, eerlijk is, of morgen nog van plan is jouw tokens te honoreren.",
|
||||
"disclaimer.status.p2": "Een mint kan online zijn tot precies het moment dat hij het niet meer is. De uptime-geschiedenis, de ondersteunde NUTs en de softwareversie zeggen iets over hoe een mint gerund wordt, maar geen van drieën is een belofte over jouw geld.",
|
||||
"disclaimer.ranking.title": "Hoe de rangschikking werkt, en wat het niet betekent",
|
||||
"disclaimer.ranking.p1": "Mints worden geordend door een algoritme over publieke reviewgegevens: beoordeling, aantal reviews en recente activiteit, met online mints standaard boven offline mints. De details staan in de open source code.",
|
||||
"disclaimer.ranking.p2": "Niemand betaalt voor een plek. Er is geen gesponsorde plaats, geen uitgelichte vermelding en geen afspraak met welke mint op deze site dan ook. Hoog staan betekent dat het algoritme de publieke gegevens goed vond, meer niet. Het is geen aanbeveling en het is geen veiligheidsoordeel.",
|
||||
"disclaimer.notAdvice.title": "Dit is geen financieel advies",
|
||||
"disclaimer.notAdvice.p1": "Niets op deze site is financieel, beleggings-, juridisch of fiscaal advies. Wij kennen jouw situatie niet en wij adviseren je niet. Of je een mint gebruikt, en welke, is jouw beslissing.",
|
||||
"disclaimer.dyor.title": "Zoek het zelf uit",
|
||||
"disclaimer.dyor.li1": "Zoek uit wie hem beheert. Een mint van iemand met een publieke reputatie te verliezen is iets anders dan een anonieme URL.",
|
||||
"disclaimer.dyor.li2": "Lees de voorwaarden van de mint zelf en zijn bericht van de dag. Een mint die zichzelf een test of een beta noemt meent dat.",
|
||||
"disclaimer.dyor.li3": "Begin klein. Mint een beetje, geef het uit, melt het er weer uit, en kijk wat er gebeurt voordat je hem meer toevertrouwt.",
|
||||
"disclaimer.dyor.li4": "Let op de limieten. Een laag maximum is vaak een teken van een voorzichtige beheerder, en een hoog maximum is geen teken dat het veilig is.",
|
||||
"disclaimer.dyor.li5": "Maak een back-up van je tokens, en onthoud dat een token een waardepapier aan toonder is: wie het heeft, kan het uitgeven.",
|
||||
"disclaimer.site.title": "Over de site zelf",
|
||||
"disclaimer.site.p1": "De gegevens hier kunnen fout of verouderd zijn. Mints worden volgens een schema gecontroleerd, relays antwoorden soms niet, en de pagina die je leest is ergens in het verleden gebouwd. De site wordt geleverd zoals hij is, zonder garantie. Lees de {link}.",
|
||||
"disclaimer.site.link": "gebruiksvoorwaarden",
|
||||
"disclaimer.updated": "20 augustus 2026",
|
||||
"login.getPrimal": "Primal downloaden",
|
||||
"reviews.dialog.identity.extensionGone": "Je browserextensie antwoordt niet. Log opnieuw in, of gebruik een signer op afstand.",
|
||||
"seo.orgDescription": "Een overzicht van Cashu-mints, met de actuele status van elke mint en reviews uit de gemeenschap, ondertekend op Nostr.",
|
||||
"seo.ogImageAlt": "Cashumints.space: vind een Cashu-mint die je kunt vertrouwen.",
|
||||
"nav.home": "Home",
|
||||
"mint.description.fallbackDomain": "Een Cashu-mint die draait op {domain}.",
|
||||
"mint.more.title": "Meer mints",
|
||||
"mint.more.all": "Bekijk alle mints"
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* The `getStaticPaths` half of path-prefix routing.
|
||||
*
|
||||
* Every page lives under `src/pages/[...locale]/`, and a rest parameter of `undefined`
|
||||
* emits the bare path. So one file emits `/mints`, `/es/mints` and `/nl/mints`, and
|
||||
* there is exactly one copy of each page's markup, translated by `t()` rather than by
|
||||
* being written out three times. Astro's own i18n config knows the same locale list and
|
||||
* resolves `Astro.currentLocale` from the URL, but it does not generate pages, which is
|
||||
* what this does.
|
||||
*
|
||||
* ~55 mints plus 9 static routes, times three locales, is around 190 prerendered pages.
|
||||
* The whole set builds in one pass off one API fetch, so the cost is disk, not time.
|
||||
*/
|
||||
import type { AstroGlobal } from 'astro';
|
||||
import { DEFAULT_LOCALE, LOCALES, isLocale, type Locale } from './config.js';
|
||||
import { splitLocale } from './routing.js';
|
||||
|
||||
/** The rest-parameter value for a locale: `undefined` for English, the code otherwise. */
|
||||
export function localeParam(locale: Locale): string | undefined {
|
||||
return locale === DEFAULT_LOCALE ? undefined : locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* One entry per locale, for a page with no other dynamic segment.
|
||||
*
|
||||
* The locale is passed through props as well as params so a page can read it without
|
||||
* re-deriving it from a parameter that is `undefined` for English.
|
||||
*/
|
||||
export function localePaths(): Array<{
|
||||
params: { locale: string | undefined };
|
||||
props: { locale: Locale };
|
||||
}> {
|
||||
return LOCALES.map(({ code }) => ({
|
||||
params: { locale: localeParam(code) },
|
||||
props: { locale: code },
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* The cross product of every locale with some other set of routes.
|
||||
*
|
||||
* Used by the mint pages: one entry per mint per locale, each carrying the mint detail
|
||||
* the page renders from, fetched once and shared across the three locales rather than
|
||||
* fetched three times.
|
||||
*/
|
||||
export function localePathsFor<T extends Record<string, unknown>>(
|
||||
items: Array<{ params: Record<string, string>; props: T }>,
|
||||
): Array<{ params: Record<string, string | undefined>; props: T & { locale: Locale } }> {
|
||||
return LOCALES.flatMap(({ code }) =>
|
||||
items.map((item) => ({
|
||||
params: { ...item.params, locale: localeParam(code) },
|
||||
props: { ...item.props, locale: code },
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The locale and the unprefixed path for the page being rendered.
|
||||
*
|
||||
* Derived from the URL rather than from props, so a layout or a component deep in the
|
||||
* tree can ask without every page above it threading two more props through. The path
|
||||
* it returns is the key the whole hreflang set is built from: `/mint/kashu.me`, the
|
||||
* same string for all three locales, which is exactly what makes them alternates of
|
||||
* each other.
|
||||
*/
|
||||
export function pageLocale(astro: AstroGlobal): { locale: Locale; path: string } {
|
||||
const fromUrl = splitLocale(astro.url.pathname);
|
||||
|
||||
/*
|
||||
* In dev, Astro serves the route before `getStaticPaths` props are in hand for some
|
||||
* requests, and `Astro.params.locale` is the more direct answer when it is set.
|
||||
* They agree in the build; preferring the param means dev cannot disagree with it.
|
||||
*/
|
||||
const param = astro.params['locale'];
|
||||
if (typeof param === 'string' && isLocale(param)) {
|
||||
return { locale: param, path: fromUrl.path };
|
||||
}
|
||||
|
||||
return fromUrl;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Path arithmetic for path-prefix locales with the default language at the root.
|
||||
*
|
||||
* `/mints` is English, `/es/mints` is Spanish, `/nl/mints` is Dutch. Route slugs stay
|
||||
* English in every language: `/es/mints`, never `/es/mentas`. That is a deliberate
|
||||
* trade. Translated slugs would read marginally better in a browser's address bar and
|
||||
* would cost a permanent, growing table of slug-per-route-per-language that every
|
||||
* internal link, the link checker, the sitemap and the language switcher would have to
|
||||
* agree on, forever. Stable URLs are worth more here than translated ones, and the
|
||||
* hreflang set is what actually tells a crawler these pages are the same page.
|
||||
*/
|
||||
import { DEFAULT_LOCALE, LOCALE_CODES, isLocale, type Locale } from './config.js';
|
||||
|
||||
/**
|
||||
* The locale a path is in, and the path with its prefix removed.
|
||||
*
|
||||
* `/es/mint/kashu.me` is `{ locale: 'es', path: '/mint/kashu.me' }`. An unprefixed path
|
||||
* is the default locale. The path always starts with `/` and never ends with one,
|
||||
* except for the root, which is exactly `/`.
|
||||
*/
|
||||
export function splitLocale(pathname: string): { locale: Locale; path: string } {
|
||||
const clean = normalise(pathname);
|
||||
const segments = clean.split('/').filter(Boolean);
|
||||
const first = segments[0];
|
||||
|
||||
if (first !== undefined && isLocale(first) && first !== DEFAULT_LOCALE) {
|
||||
const rest = segments.slice(1).join('/');
|
||||
return { locale: first, path: rest ? `/${rest}` : '/' };
|
||||
}
|
||||
|
||||
return { locale: DEFAULT_LOCALE, path: clean };
|
||||
}
|
||||
|
||||
/** Just the locale, for a page that only needs to know which language it is. */
|
||||
export function localeFromPath(pathname: string): Locale {
|
||||
return splitLocale(pathname).locale;
|
||||
}
|
||||
|
||||
/** Just the unprefixed path, which is the key a page's translations are keyed by. */
|
||||
export function stripLocale(pathname: string): string {
|
||||
return splitLocale(pathname).path;
|
||||
}
|
||||
|
||||
/**
|
||||
* The same page in another language.
|
||||
*
|
||||
* `localePath('/mint/kashu.me', 'nl')` is `/nl/mint/kashu.me`. The default locale keeps
|
||||
* the bare path, which is the whole point of `prefixDefaultLocale: false`.
|
||||
*/
|
||||
export function localePath(path: string, locale: Locale): string {
|
||||
const clean = normalise(path);
|
||||
if (locale === DEFAULT_LOCALE) return clean;
|
||||
return clean === '/' ? `/${locale}` : `/${locale}${clean}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute URL for one locale's version of a page.
|
||||
*
|
||||
* hreflang and canonical both require absolute URLs, and a relative one there is not a
|
||||
* lesser signal, it is an ignored one.
|
||||
*/
|
||||
export function localeUrl(path: string, locale: Locale, site: URL | string): string {
|
||||
const origin = typeof site === 'string' ? new URL(site).origin : site.origin;
|
||||
return origin + localePath(path, locale);
|
||||
}
|
||||
|
||||
export interface Alternate {
|
||||
/** The `hreflang` value: a locale code, or `x-default`. */
|
||||
hreflang: string;
|
||||
href: string;
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* The complete reciprocal alternate set for one page, x-default included.
|
||||
*
|
||||
* Every locale's version of this page lists every locale's version of this page,
|
||||
* itself among them, plus one `x-default` pointing at English. That reciprocity is the
|
||||
* part crawlers actually check: a page that lists alternates which do not list it back
|
||||
* has its whole set discarded, so a partial set is worth less than none.
|
||||
*
|
||||
* `x-default` is the English URL rather than a language selector, because there is no
|
||||
* selector page here and English is what an unmatched visitor gets.
|
||||
*/
|
||||
export function alternates(path: string, site: URL | string): Alternate[] {
|
||||
const out: Alternate[] = LOCALE_CODES.map((locale) => ({
|
||||
hreflang: locale,
|
||||
href: localeUrl(path, locale, site),
|
||||
locale,
|
||||
}));
|
||||
|
||||
out.push({
|
||||
hreflang: 'x-default',
|
||||
href: localeUrl(path, DEFAULT_LOCALE, site),
|
||||
locale: DEFAULT_LOCALE,
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Leading slash, no trailing slash, no double slashes, no query or hash. */
|
||||
function normalise(pathname: string): string {
|
||||
const withoutQuery = pathname.split(/[?#]/)[0] ?? '';
|
||||
const trimmed = withoutQuery.replace(/\/+$/, '').replace(/\/{2,}/g, '/');
|
||||
if (trimmed === '') return '/';
|
||||
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* The `t()` core: one lookup, one interpolation, one plural rule.
|
||||
*
|
||||
* Deliberately tiny and dependency free, because it runs in two places with very
|
||||
* different budgets: in .astro frontmatter at build time, where nothing it does costs a
|
||||
* visitor anything, and inside an island, where every byte ships. There is no ICU
|
||||
* parser here. What ICU is actually needed for on this site is plurals, and
|
||||
* `Intl.PluralRules` is already in every browser and in node.
|
||||
*/
|
||||
import type { Locale } from './config.js';
|
||||
import { DEFAULT_LOCALE, intlTag } from './config.js';
|
||||
|
||||
/** A catalog is flat: dotted keys to strings. No nesting, so no path walking. */
|
||||
export type Catalog = Record<string, string>;
|
||||
|
||||
export type Vars = Record<string, string | number>;
|
||||
|
||||
/**
|
||||
* The interpolation placeholder. `{name}` and nothing else: no formats, no nested
|
||||
* selects. A string that needs more than a value dropped into it is two strings.
|
||||
*/
|
||||
const PLACEHOLDER = /\{(\w+)\}/g;
|
||||
|
||||
/** Cached per locale, because constructing one of these is not free. */
|
||||
const pluralRules = new Map<string, Intl.PluralRules>();
|
||||
const numberFormats = new Map<string, Intl.NumberFormat>();
|
||||
|
||||
function pluralRule(locale: Locale): Intl.PluralRules {
|
||||
const tag = intlTag(locale);
|
||||
let rule = pluralRules.get(tag);
|
||||
if (!rule) {
|
||||
rule = new Intl.PluralRules(tag);
|
||||
pluralRules.set(tag, rule);
|
||||
}
|
||||
return rule;
|
||||
}
|
||||
|
||||
function numberFormat(locale: Locale): Intl.NumberFormat {
|
||||
const tag = intlTag(locale);
|
||||
let format = numberFormats.get(tag);
|
||||
if (!format) {
|
||||
format = new Intl.NumberFormat(tag);
|
||||
numberFormats.set(tag, format);
|
||||
}
|
||||
return format;
|
||||
}
|
||||
|
||||
/**
|
||||
* The plural category for a count, as a catalog key suffix.
|
||||
*
|
||||
* English, Spanish and Dutch share one rule (`1` is singular, everything else is not),
|
||||
* so this looks like overkill for the three launch languages. It is not: it is what
|
||||
* makes language four a one-file job even when that language is Polish, which has three
|
||||
* categories, or Arabic, which has six. A catalog that only ships `.one` and `.other`
|
||||
* still resolves, because a missing category falls back to `.other`.
|
||||
*/
|
||||
function pluralKey(locale: Locale, key: string, n: number, catalog: Catalog): string | null {
|
||||
const category = pluralRule(locale).select(n);
|
||||
if (catalog[`${key}.${category}`] !== undefined) return `${key}.${category}`;
|
||||
if (catalog[`${key}.other`] !== undefined) return `${key}.other`;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitute `{name}` placeholders.
|
||||
*
|
||||
* Numbers are formatted for the locale on the way in, so `{n}` in "1,000,000 sats"
|
||||
* comes out as `1.000.000` in Spanish and Dutch without every caller remembering to
|
||||
* format first. Pass a string when a raw value is wanted (a NUT number, a year, a
|
||||
* rating already fixed to one decimal).
|
||||
*/
|
||||
function interpolate(template: string, vars: Vars | undefined, locale: Locale): string {
|
||||
if (!vars) return template;
|
||||
return template.replace(PLACEHOLDER, (whole, name: string) => {
|
||||
const value = vars[name];
|
||||
if (value === undefined) return whole;
|
||||
return typeof value === 'number' ? numberFormat(locale).format(value) : value;
|
||||
});
|
||||
}
|
||||
|
||||
export interface Translator {
|
||||
(key: string, vars?: Vars): string;
|
||||
locale: Locale;
|
||||
/** True when this key resolves in this locale without falling back. */
|
||||
has: (key: string) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a translator over one locale's catalog, with English behind it.
|
||||
*
|
||||
* A missing key renders the English string rather than the key itself: a visitor
|
||||
* reading a Spanish page should meet an English sentence at worst, never
|
||||
* `mint.warnings.meltOnly`. The build is what complains about the gap, loudly and by
|
||||
* name, in `scripts/check-i18n.mjs`.
|
||||
*/
|
||||
export function createTranslator(locale: Locale, catalog: Catalog, fallback: Catalog): Translator {
|
||||
const lookup = (key: string, n: number | undefined): string | undefined => {
|
||||
if (n !== undefined) {
|
||||
const plural = pluralKey(locale, key, n, catalog);
|
||||
if (plural) return catalog[plural];
|
||||
// The locale has no plural forms for this key. Take English's, picked with
|
||||
// English's own rule, rather than dropping to the bare key.
|
||||
const englishPlural = pluralKey(DEFAULT_LOCALE, key, n, fallback);
|
||||
if (englishPlural) return fallback[englishPlural];
|
||||
}
|
||||
return catalog[key] ?? fallback[key];
|
||||
};
|
||||
|
||||
const t = ((key: string, vars?: Vars): string => {
|
||||
const n = typeof vars?.['n'] === 'number' ? (vars['n'] as number) : undefined;
|
||||
const found = lookup(key, n);
|
||||
// Nothing anywhere. Render the last segment of the key as words rather than the
|
||||
// key: "meltOnly" is a bad string, "mint.warnings.meltOnly" is a bug on screen.
|
||||
if (found === undefined) return humanise(key);
|
||||
return interpolate(found, vars, locale);
|
||||
}) as Translator;
|
||||
|
||||
t.locale = locale;
|
||||
t.has = (key: string): boolean =>
|
||||
catalog[key] !== undefined ||
|
||||
catalog[`${key}.other`] !== undefined ||
|
||||
catalog[`${key}.one`] !== undefined;
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* The last resort, for a key that exists in no catalog at all.
|
||||
*
|
||||
* This should be unreachable: `check-i18n` fails the build on a key that is used and
|
||||
* not defined in English. It is here because "unreachable" and "never renders" are
|
||||
* different claims, and the difference is what a visitor sees.
|
||||
*/
|
||||
function humanise(key: string): string {
|
||||
const last = key.split('.').pop() ?? key;
|
||||
const spaced = last.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[-_]+/g, ' ');
|
||||
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
||||
}
|
||||
Reference in New Issue
Block a user