Build 78: honest transport labels, request decline/cancel, NIP-05 on requests, full localization
Settings now says "Manual transaction" and the privacy row reads "Messages & lookups" opening a new Network privacy page that tells the truth: messages, names, price and avatars ride the Nym mixnet; the grin node connects directly. README and lander updated to match. Requests are messages, payments are final: declining a request now sends the requester a void control message (NIP-17), a requester can cancel a request they sent (cancels the local invoice and notifies the payer), and incoming requests resolve the sender's verified @username instead of a bare npub. The Requested amount on the success screen is centered. New NostrDecline/NostrCancel tasks and a goblin-action control message carry it, bound to the stored counterparty. Localization: every Goblin-screen string moved to t!() keys (370 keys) and translated into de/fr/ru/tr/zh-CN, guarded by a key/placeholder drift test. System-locale auto-detect now matches region locales like zh-CN.
This commit is contained in:
Generated
+1
@@ -4531,6 +4531,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"sha2 0.10.9",
|
||||
"sys-locale",
|
||||
"thiserror 2.0.18",
|
||||
|
||||
@@ -172,3 +172,4 @@ tokio = { version = "1.49.0", features = ["full"] }
|
||||
base64 = "0.22"
|
||||
sha2 = "0.10"
|
||||
hex = "0.4"
|
||||
serde_yaml = "0.9"
|
||||
|
||||
@@ -15,7 +15,7 @@ Goblin is a fork of the **Grim** egui GRIN wallet: it keeps Grim's full GRIN nod
|
||||
- **Send to people** — pay a `@username` or `npub`; the GRIN slatepack travels as a [NIP-17](https://nips.nostr.com/17) gift-wrapped DM ([kind 1059](https://nostrbook.dev/kinds/1059)) over the Nym mixnet and is applied automatically by the recipient's wallet. No files to swap, no need to both be online at once.
|
||||
- **Manual slatepacks too** — when you need to pay or get paid without a handle, **Settings → Wallet → Slatepacks** exposes the classic by-hand flow: create a slatepack to send, or paste one to receive, finalize, or pay.
|
||||
- **In-app identity** — a nostr payment key that is deliberately *not* part of your seed, so you can rotate it any time to stay unlinkable without touching your funds. An optional human-readable `@name` (and hosted avatar) comes from the goblin.st identity service.
|
||||
- **Private by construction** — GRIN's address-less, confidential chain; every relay and HTTP request (relays, NIP-05 lookups, price, avatars) is routed through the [Nym mixnet](https://nym.com), so nothing touches the clear net; keys, names and history stay on your device.
|
||||
- **Private by construction** — GRIN's address-less, confidential chain; your payments and identity (nostr relays, NIP-05 lookups, price, avatars) are routed through the [Nym mixnet](https://nym.com), so who-pays-whom never touches the clear net. The GRIN node connection — block sync and broadcasting your transaction — is direct: public chain data, the same for everyone, and not tied to your identity. Keys, names and history stay on your device.
|
||||
- **Configurable amount pairing** — show balances against a world currency, Bitcoin, or sats (rates fetched over the mixnet), or turn the preview off.
|
||||
- **Cross-platform** — Linux, macOS, Windows, Android, built in pure Rust on [egui](https://github.com/emilk/egui).
|
||||
|
||||
@@ -50,7 +50,7 @@ cargo build --release
|
||||
./target/release/goblin
|
||||
```
|
||||
|
||||
All wallet traffic — nostr relays, NIP-05 lookups, price and avatar fetches — is routed over the mixnet through a network requester (the default is baked into `NETWORK_REQUESTER` in `src/nym/sidecar.rs`); the SDK's SOCKS5 listener is run in-process on `127.0.0.1:1080`. If something is already listening there, Goblin reuses it.
|
||||
Goblin's identity and payment traffic — nostr relays, NIP-05 lookups, price and avatar fetches — is routed over the mixnet through a network requester (the default is baked into `NETWORK_REQUESTER` in `src/nym/sidecar.rs`); the SDK's SOCKS5 listener is run in-process on `127.0.0.1:1080`. If something is already listening there, Goblin reuses it. The GRIN node connection (block sync and transaction broadcast) is **not** mixed — it connects directly, as it carries only public chain data that isn't linked to your wallet.
|
||||
|
||||
### Android
|
||||
|
||||
|
||||
+394
-1
@@ -355,4 +355,397 @@ keyboard:
|
||||
m: m
|
||||
m1: ','
|
||||
m2: .
|
||||
m3: '/'
|
||||
m3: '/'
|
||||
goblin:
|
||||
home:
|
||||
anonymous: "Anonym"
|
||||
connected_nym: "Über Nym verbunden"
|
||||
nym_ready: "Nym bereit · Relays…"
|
||||
connecting_nym: "Verbinde mit Nym…"
|
||||
cant_reach_node: "Node nicht erreichbar"
|
||||
node_synced: "Node synchronisiert"
|
||||
syncing: "Synchronisiere…"
|
||||
block: "Block %{height}"
|
||||
waiting_for_chain: "Warte auf Chain…"
|
||||
nav_wallet: "Wallet"
|
||||
nav_pay: "Zahlen"
|
||||
nav_activity: "Aktivität"
|
||||
nav_receive: "Empfangen"
|
||||
nav_settings: "Einstellungen"
|
||||
activity: "Aktivität"
|
||||
empty_title: "Noch keine Aktivität"
|
||||
empty_sub: "Sende oder empfange grin, um zu starten."
|
||||
recent: "Zuletzt"
|
||||
scan_to_pay: "Zum Zahlen scannen"
|
||||
type_amount: "Betrag eingeben"
|
||||
request: "Anfordern"
|
||||
pay: "Zahlen"
|
||||
enter_amount: "Betrag zum Zahlen oder Anfordern eingeben"
|
||||
activity:
|
||||
canceled: "abgebrochen"
|
||||
pending: "ausstehend"
|
||||
earlier: "Früher"
|
||||
today: "Heute"
|
||||
yesterday: "Gestern"
|
||||
title: "Aktivität"
|
||||
requests: "Anfragen"
|
||||
empty_title: "Noch keine Aktivität"
|
||||
empty_sub: "Deine Zahlungen erscheinen hier."
|
||||
pending_header: "Ausstehend"
|
||||
receipt:
|
||||
title: "Beleg"
|
||||
not_found: "Transaktion nicht gefunden"
|
||||
for_note: "Für %{note}"
|
||||
details: "Transaktionsdetails"
|
||||
canceled: "Abgebrochen"
|
||||
expired: "Abgelaufen"
|
||||
funds_returned: "Guthaben zurückerstattet"
|
||||
complete: "Abgeschlossen"
|
||||
payment_received: "Zahlung empfangen"
|
||||
payment_sent: "Zahlung erfolgreich gesendet"
|
||||
pending: "Ausstehend"
|
||||
confs: "%{c}/%{r} Bestätigungen"
|
||||
waiting_to_confirm: "Warte auf Bestätigung"
|
||||
you: "Du"
|
||||
to: "An"
|
||||
from: "Von"
|
||||
nostr: "nostr"
|
||||
fee_none: "Keine"
|
||||
network_fee: "Netzwerkgebühr"
|
||||
privacy: "Privatsphäre"
|
||||
privacy_value: "Mimblewimble + Nym"
|
||||
transaction: "Transaktion"
|
||||
cancel_request: "Anfrage abbrechen"
|
||||
request:
|
||||
title: "%{name} fordert an"
|
||||
approve: "Annehmen"
|
||||
decline: "Ablehnen"
|
||||
receive:
|
||||
title: "Empfangen"
|
||||
requesting: "Fordere %{amt}%{tsu} an — teilen, um bezahlt zu werden"
|
||||
clear_request: "Anfrage löschen"
|
||||
share_handle: "Teile deinen Handle, um bezahlt zu werden"
|
||||
copied: "Kopiert"
|
||||
copy_nostr_id: "nostr-ID kopieren"
|
||||
copy_address: "Adresse kopieren"
|
||||
privacy_note: "Dein Benutzername ist öffentlich. Zahlungsinhalte bleiben im Netzwerk verschlüsselt."
|
||||
profile:
|
||||
title: "Profil"
|
||||
activity: "Aktivität"
|
||||
no_activity: "Noch keine Aktivität mit ihnen."
|
||||
unblock: "Entsperren"
|
||||
block: "Sperren"
|
||||
blocked_blurb: "Gesperrt — ihre Zahlungen und Anfragen werden verworfen."
|
||||
block_blurb: "Sperren verwirft eingehende Zahlungen und Anfragen von ihnen."
|
||||
settings:
|
||||
title: "Einstellungen"
|
||||
connected_nostr: "Mit nostr verbunden"
|
||||
connecting_relays: "Verbinde mit Relays…"
|
||||
pic_updated: "Profilbild aktualisiert"
|
||||
uploading_pic: "Lade Bild hoch…"
|
||||
claim_first: "Erst einen Benutzernamen sichern — Bilder hängen daran"
|
||||
identity: "Identität"
|
||||
copy_npub: "npub kopieren (öffentlich)"
|
||||
backup_nsec: "Geheimen Schlüssel sichern (nsec)"
|
||||
export_identity: "Identitäts-Backup exportieren (verschlüsselt)"
|
||||
rotate_key: "nostr-Schlüssel wechseln"
|
||||
import_identity: "Identität importieren (nsec / Backup)"
|
||||
backup_note: "Gerätewechsel? Sichere BEIDES: deine Seed-Phrase (Guthaben) und ein Identitäts-Backup (Name + Schlüssel)."
|
||||
wallet: "Wallet"
|
||||
display_unit: "Anzeigeeinheit"
|
||||
relays: "Relays"
|
||||
node: "Node"
|
||||
slatepacks: "Slatepacks"
|
||||
slatepacks_value: "Manuelle Transaktion"
|
||||
lock_wallet: "Wallet sperren"
|
||||
privacy: "Privatsphäre"
|
||||
mixnet_routing: "Mixnet-Routing"
|
||||
messages_lookups: "Nachrichten & Abfragen"
|
||||
auto_accept: "Automatisch annehmen"
|
||||
pairing: "Kopplung"
|
||||
accept_anyone: "Jeder"
|
||||
accept_contacts: "Nur Kontakte"
|
||||
accept_ask: "Immer fragen"
|
||||
requests: "Anfragen"
|
||||
incoming_requests: "Eingehende Anfragen"
|
||||
incoming_requests_sub: "Erlaube anderen, Geld von dir anzufordern"
|
||||
appearance: "Erscheinungsbild"
|
||||
theme: "Design"
|
||||
theme_light: "Hell"
|
||||
theme_dark: "Dunkel"
|
||||
theme_yellow: "Gelb"
|
||||
archive: "Archiv"
|
||||
export_archive: "Archiv exportieren"
|
||||
wipe_history: "Zahlungsverlauf löschen"
|
||||
about: "Über"
|
||||
goblin: "Goblin"
|
||||
build: "Build %{build}"
|
||||
network: "Netzwerk"
|
||||
network_value: "MW + Nym mixnet + nostr"
|
||||
third_party: "Drittanbieter"
|
||||
grim: "GRIM (Upstream-Wallet)"
|
||||
grin_node: "Grin-Node"
|
||||
sp_intro: "Erweitert — rohe slatepacks von Hand austauschen, so wie GRIM es macht. Nur nutzen, wenn du nicht über einen @username zahlen oder bezahlt werden kannst."
|
||||
sp_receive_group: "Empfangen oder abschließen"
|
||||
sp_receive_blurb: "Füge einen slatepack ein, den dir jemand gegeben hat. Goblin empfängt die Zahlung, begleicht die Rechnung oder schließt sie ab und sendet sie."
|
||||
sp_process: "Slatepack verarbeiten"
|
||||
sp_paste_first: "Füge zuerst einen slatepack ein."
|
||||
sp_reply_ready: "Antwort bereit — sende sie an den Absender zurück."
|
||||
sp_finalizing: "Schließe ab und sende an die Chain…"
|
||||
sp_create_group: "Zahlung erstellen"
|
||||
sp_create_blurb: "Erstelle einen slatepack zum Übergeben. Der Empfänger nimmt ihn an, sendet die Antwort zurück, und du schließt sie oben ab."
|
||||
sp_amount_hint: "Betrag in grin"
|
||||
sp_addr_hint: "Empfängeradresse (optional)"
|
||||
sp_create: "Slatepack erstellen"
|
||||
sp_ready: "Slatepack bereit — übergib ihn dem Empfänger."
|
||||
sp_amount_gt_zero: "Gib einen Betrag größer als null ein."
|
||||
sp_to_send: "Zu sendender slatepack"
|
||||
sp_copy: "Slatepack kopieren"
|
||||
rotate_line1: "• Du bekommst einen brandneuen ZUFÄLLIGEN Schlüssel; das alte npub empfängt nichts mehr. Es gibt keine Ableitungskette zwischen beiden."
|
||||
rotate_line2: "• Der neue Schlüssel ist NICHT aus deinem Seed wiederherstellbar — sichere die neue nsec direkt nach dem Wechsel."
|
||||
rotate_line3: "• Dein @username wird FREIGEGEBEN und dein Profilbild gelöscht — sichere danach denselben oder einen neuen Namen (sobald frei, kann ihn jeder andere ebenfalls greifen)."
|
||||
rotate_line4: "• Zahlungen, die noch an den alten Schlüssel unterwegs sind, WERDEN gestört — warte zuerst, bis ausstehende Zahlungen abgeschlossen sind."
|
||||
rotate_line5: "• Kontakte, die dein npub direkt gespeichert haben, müssen dich neu finden — teile dein neues npub oder den neu gesicherten @username."
|
||||
cancel: "Abbrechen"
|
||||
continue: "Weiter"
|
||||
final_confirmation: "Endgültige Bestätigung"
|
||||
rotate_confirm_blurb: "Dies kann in der App nicht rückgängig gemacht werden. Tippe RESET und gib dein Wallet-Passwort ein, um zu wechseln."
|
||||
type_reset: "RESET tippen"
|
||||
wallet_password: "Wallet-Passwort"
|
||||
rotate_key_btn: "Schlüssel wechseln"
|
||||
rotating_key: "Wechsle Schlüssel…"
|
||||
key_rotated: "Schlüssel gewechselt"
|
||||
new_npub: "Neues npub: %{npub}"
|
||||
backup_new_key: "Sichere jetzt den NEUEN geheimen Schlüssel — dein Seed kann ihn nicht wiederherstellen."
|
||||
copy_new_nsec: "Neues nsec-Backup kopieren"
|
||||
done: "Fertig"
|
||||
rotation_failed: "Wechsel fehlgeschlagen"
|
||||
close: "Schließen"
|
||||
import_identity_title: "Identität importieren"
|
||||
import_blurb: "Ersetzt die nostr-Identität dieses Wallets — füge eine reine nsec oder ein exportiertes Identitäts-Backup ein (das Backup stellt auch deinen Benutzernamen und Verlauf wieder her). Sichere zuerst den aktuellen Schlüssel, falls du ihn noch brauchst."
|
||||
import_nsec_hint: "nsec1… oder Identitäts-Backup-JSON"
|
||||
backup_password_hint: "Backup-Passwort (nur wenn anderswo exportiert)"
|
||||
import_btn: "Importieren"
|
||||
importing: "Importiere…"
|
||||
identity_replaced: "Identität ersetzt"
|
||||
now_using: "Jetzt aktiv: %{npub}"
|
||||
import_failed: "Import fehlgeschlagen"
|
||||
registered: "%{name} registriert"
|
||||
released_msg: "Freigegeben — der Name ist frei"
|
||||
release_confirm: "@%{name} freigeben?"
|
||||
release_blurb: "Er ist frei, sobald er verfügbar wird — jeder kann ihn beanspruchen, auch der nächste Schlüssel, zu dem du wechselst. Dein Profilbild wird damit gelöscht. Du kannst 10 Minuten lang keinen anderen Benutzernamen registrieren."
|
||||
releasing: "Gebe frei…"
|
||||
keep_it: "Behalten"
|
||||
release_it: "Freigeben"
|
||||
username: "Benutzername"
|
||||
username_note: "Wird als @you angezeigt. Öffentlich auf goblin.st. Zahlungen bleiben verschlüsselt."
|
||||
release_username: "Benutzername freigeben"
|
||||
pick_username: "Benutzernamen wählen — optional"
|
||||
working: "Arbeite…"
|
||||
claim: "Sichern"
|
||||
err_just_taken: "Dieser Benutzername wurde gerade vergeben"
|
||||
err_cooldown: "Du hast kürzlich einen Benutzernamen freigegeben — du kannst innerhalb von 10 Minuten einen neuen registrieren."
|
||||
err_unreachable: "goblin.st nicht erreichbar — Verbindungsproblem. Versuche es erneut."
|
||||
err_release: "Freigabe fehlgeschlagen: %{err}"
|
||||
avail_available: "Verfügbar!"
|
||||
avail_taken: "Vergeben"
|
||||
avail_reserved: "Reserviert"
|
||||
avail_invalid: "Namen haben 3–30 Zeichen: a–z, 0–9, _ oder -"
|
||||
avail_quarantined: "Nicht verfügbar"
|
||||
avail_unknown: "Prüfung fehlgeschlagen — Verbindungsproblem. Versuche es erneut."
|
||||
privacy:
|
||||
title: "Netzwerk-Privatsphäre"
|
||||
intro: "Goblin sendet seinen privaten Datenverkehr durch das Nym mixnet — ein Netzwerk mit fünf Sprüngen, das verbirgt, wer mit wem kommuniziert, sodass ein Relay eine Zahlung nicht zu dir zurückverfolgen kann."
|
||||
payments: "Zahlungen"
|
||||
payments_blurb: "Jede nostr-Nachricht, die einen slatepack trägt."
|
||||
usernames: "@usernames"
|
||||
usernames_blurb: "NIP-05-Namensabfragen zu und von goblin.st."
|
||||
price_avatars: "Kurs & Avatare"
|
||||
price_avatars_blurb: "Die Kursvorschau und Kontaktbilder."
|
||||
over_mixnet: "Über das mixnet"
|
||||
direct_connection: "Direkte Verbindung"
|
||||
grin_node: "Grin-Node"
|
||||
grin_node_blurb: "Block-Synchronisierung und Übertragung deiner Transaktion ins Netzwerk. Dies sind öffentliche Chain-Daten, für alle gleich, und nicht mit deiner Identität verknüpft."
|
||||
pairing:
|
||||
title: "Kopplung"
|
||||
intro: "Womit dein Guthaben und deine Beträge verglichen werden."
|
||||
pair_with: "Koppeln mit"
|
||||
rates_note: "Kurse werden über das Nym mixnet abgerufen, nur solange eine Kopplung aktiv ist — aus bedeutet, dass keine Kursanfrage dein Gerät verlässt."
|
||||
relays:
|
||||
title: "Relays"
|
||||
intro: "Zahlungsnachrichten werden an jedes Relay unten gespiegelt; ein erreichbares Relay genügt zum Empfangen."
|
||||
your_relays: "Deine Relays"
|
||||
add_relay: "Relay hinzufügen"
|
||||
add_relay_btn: "Relay hinzufügen"
|
||||
save_reconnect: "Speichern & neu verbinden"
|
||||
none: "keine"
|
||||
count: "%{n} Relays"
|
||||
node:
|
||||
title: "Node"
|
||||
connection: "Verbindung"
|
||||
integrated: "Integrierter Node"
|
||||
applies_after: "Wird wirksam, nachdem das Wallet gesperrt und wieder entsperrt wurde."
|
||||
add_external: "Externen Node hinzufügen"
|
||||
api_secret_hint: "API-Secret (optional)"
|
||||
add_node: "Node hinzufügen"
|
||||
integrated_host: "integrierter Node"
|
||||
summary_syncing: "%{conn} · synchronisiere"
|
||||
summary_block: "Block %{height} · %{conn}"
|
||||
nips:
|
||||
title: "nostr & NIPs"
|
||||
intro1: "Goblin spricht nostr — ein offenes Protokoll signierter Nachrichten, die über einfache Relay-Server weitergereicht werden. Dein Wallet trägt seine eigene nostr-Identität: einen eigenständigen Zufallsschlüssel, bewusst unabhängig von deinem Guthaben und Seed gehalten. Jede Zahlung reist als Ende-zu-Ende-verschlüsselte Direktnachricht zwischen Identitäten, mit dem slatepack im Inneren."
|
||||
intro2: "goblin.st ist Goblins Namensdienst: Das Sichern eines Benutzernamens veröffentlicht dort eine Name → Schlüssel-Zuordnung (NIP-05), sodass Leute @you statt eines langen npub bezahlen können. Der Benutzername ist öffentlich; Zahlungsinhalte sind es nie. NIPs sind die Bausteine des Protokolls — tippe auf einen, um die Spezifikation zu lesen."
|
||||
n05_title: "Namen"
|
||||
n05_blurb: "Ordnet @username@goblin.st deinem Schlüssel zu, sodass Handles wie Adressen funktionieren."
|
||||
n17_title: "Private Nachrichten"
|
||||
n17_blurb: "Die verschlüsselte DM-Hülle, in der jede Zahlung reist."
|
||||
n44_title: "Verschlüsselung"
|
||||
n44_blurb: "Die authentifizierte Chiffre, die in diesen Nachrichten verwendet wird."
|
||||
n49_title: "Schlüsselverschlüsselung"
|
||||
n49_blurb: "Wie der geheime Schlüssel im Ruhezustand gespeichert wird, gesperrt durch dein Passwort."
|
||||
n59_title: "Gift Wrap"
|
||||
n59_blurb: "Verpackt Nachrichten, sodass Relays nicht sehen können, wer mit wem kommuniziert."
|
||||
n98_title: "HTTP-Auth"
|
||||
n98_blurb: "Signiert die Benutzernamen-Registrierungsanfrage an goblin.st."
|
||||
onboarding:
|
||||
intro:
|
||||
private_money_head: "Privates Geld"
|
||||
private_money_body: "Goblin ist ein Wallet für grin — digitales Bargeld ohne Beträge oder Adressen auf seiner Chain."
|
||||
send_like_message_head: "Senden wie eine Nachricht"
|
||||
send_like_message_body: "Zahle an einen @username oder npub und es kommt als Ende-zu-Ende-verschlüsselte Nachricht über nostr und das Nym mixnet an — niemand dazwischen sieht den Betrag oder die Beteiligten."
|
||||
yours_alone_head: "Nur deins"
|
||||
yours_alone_body: "Schlüssel, Namen und Verlauf bleiben auf diesem Gerät. Basiert auf dem GRIM-Wallet."
|
||||
get_started: "Loslegen"
|
||||
footnote: "Dauert etwa eine Minute. Du kannst später alles ändern."
|
||||
node:
|
||||
kicker: "SCHRITT 1 VON 3 · NETZWERK"
|
||||
title: "Wie soll Goblin\ndie Chain beobachten?"
|
||||
own_title: "Eigenen Node betreiben"
|
||||
own_badge: "Privat"
|
||||
own_body: "Vertraut niemandem — dein Wallet prüft die Chain selbst. Synchronisiert im Hintergrund, während du die Einrichtung beendest."
|
||||
connect_title: "Mit einem Node verbinden"
|
||||
connect_badge: "Sofort"
|
||||
connect_body: "Kein Warten auf Sync. Der gewählte Node kann die Abfragen deines Wallets sehen."
|
||||
changeable: "Jederzeit änderbar unter Einstellungen → Node."
|
||||
continue: "Weiter"
|
||||
url_invalid: "Node-URL muss mit http:// oder https:// beginnen"
|
||||
wallet:
|
||||
kicker: "SCHRITT 2 VON 3 · WALLET"
|
||||
title: "Richte dein Wallet ein"
|
||||
create_new: "Neu erstellen"
|
||||
restore_from_seed: "Aus Seed wiederherstellen"
|
||||
name_hint: "Wallet-Name"
|
||||
password_hint: "Passwort"
|
||||
repeat_password_hint: "Passwort wiederholen"
|
||||
restore_hint: "Halte deine Seed-Wörter bereit — du gibst sie als Nächstes ein."
|
||||
create_hint: "Als Nächstes erhältst du 24 Seed-Wörter zum Aufschreiben. Sie sind das Geld — wer sie hat, hält dein Guthaben."
|
||||
continue: "Weiter"
|
||||
passwords_no_match: "Passwörter stimmen nicht überein"
|
||||
words:
|
||||
kicker: "SCHRITT 2 VON 3 · WALLET"
|
||||
title_restore: "Gib deine Seed-Wörter ein"
|
||||
title_create: "Schreibe diese Wörter auf"
|
||||
write_down_hint: "Auf Papier, in Reihenfolge. Wer diese Wörter hat, kann dein Guthaben nehmen; ohne sie bedeutet ein verlorenes Gerät verlorenes Guthaben."
|
||||
paste: "Einfügen"
|
||||
scan_qr: "QR scannen"
|
||||
copy_clipboard: "In Zwischenablage kopieren (vermeiden)"
|
||||
restore_wallet: "Wallet wiederherstellen"
|
||||
wrote_them_down: "Ich habe sie aufgeschrieben"
|
||||
fill_every_word: "Fülle jedes Wort aus — tippe ein Wort an, um es zu bearbeiten, oder füge die Phrase ein."
|
||||
confirm:
|
||||
kicker: "SCHRITT 2 VON 3 · WALLET"
|
||||
title: "Jetzt beweise es"
|
||||
enter_hint: "Gib die soeben aufgeschriebenen Wörter ein. Tippe ein Wort an, um es zu tippen."
|
||||
paste: "Einfügen"
|
||||
create_wallet: "Wallet erstellen"
|
||||
keep_going: "Weiter so — jedes Wort, in Reihenfolge."
|
||||
identity:
|
||||
kicker: "SCHRITT 3 VON 3 · IDENTITÄT"
|
||||
title: "Deine Zahlungsidentität"
|
||||
key_being_made: "Schlüssel wird erstellt…"
|
||||
connected_nym: "über Nym verbunden"
|
||||
connecting_nym: "verbinde über Nym…"
|
||||
fresh_key_blurb: "Ein frischer Schlüssel, für Zahlungen gemacht — bewusst nicht Teil deines Seeds, sodass du ihn jederzeit wechseln kannst, um deine Privatsphäre zu wahren, ohne je dein Guthaben zu berühren. Sichere ihn unter Einstellungen → Identität."
|
||||
clean_slate_blurb: "Lust auf einen Neuanfang? Tausche jederzeit einen brandneuen Schlüssel ein — das neue Du ist nicht mit dem alten verknüpft. Gleiches Wallet, frisches Gesicht."
|
||||
pick_username: "Benutzernamen wählen — optional"
|
||||
username_blurb: "Freunde zahlen an @you statt an einen langen Schlüssel. Öffentlich auf goblin.st; Zahlungen bleiben verschlüsselt. Überspringe es und du bist einfach anonym — sichere jederzeit später einen."
|
||||
username_field_hint: "deinname"
|
||||
working: "Arbeite…"
|
||||
claim_username: "Benutzernamen sichern"
|
||||
available_when_connected: "Verfügbar, sobald das mixnet verbindet — oder überspringen und später sichern."
|
||||
youre: "Du bist @%{name}"
|
||||
open_wallet: "Mein Wallet öffnen"
|
||||
skip_for_now: "Vorerst überspringen"
|
||||
errors:
|
||||
cant_open: "Wallet konnte nicht geöffnet werden: %{err}"
|
||||
cant_create: "Wallet konnte nicht erstellt werden: %{err}"
|
||||
send:
|
||||
scan_to_request: "Zum Anfordern scannen"
|
||||
scan_to_pay: "Zum Zahlen scannen"
|
||||
tab_scan: "Scannen"
|
||||
tab_my_code: "Mein Code"
|
||||
request_from: "Anfordern von"
|
||||
send_to: "Senden an"
|
||||
search_hint: "@handle, npub oder Name"
|
||||
suggested: "%{icon} Vorgeschlagen"
|
||||
no_contacts: "Noch keine Kontakte. Finde jemanden über seinen @handle."
|
||||
no_profile: "kein Profil"
|
||||
tag_contact: "Kontakt"
|
||||
tag_on_nostr: "auf nostr"
|
||||
searching_nostr: "Durchsuche nostr…"
|
||||
unverified_title: "Unverifizierten Schlüssel bezahlen?"
|
||||
unverified_body: "Für diesen Schlüssel ist kein nostr-Profil veröffentlicht — er könnte brandneu, anonym oder vertippt sein. Prüfe genau, ob es der richtige ist, bevor du sendest."
|
||||
keep_looking: "Weitersuchen"
|
||||
pay_anyway: "Trotzdem zahlen"
|
||||
scan_not_recipient: "Dieser QR ist kein goblin-Empfänger — erwartet wurde ein npub oder @handle"
|
||||
scan_prompt: "Halte einen goblin-Code ins Bild, um zu aktivieren"
|
||||
scan_to_pay_me: "Scannen, um mich zu bezahlen"
|
||||
share_btn: "%{icon} Teilen"
|
||||
share_message: "Bezahl mich auf Goblin — %{handle}\n%{link}\nnpub: %{npub}"
|
||||
none_found: "Niemand gefunden für %{label}"
|
||||
enter_recipient: "Gib einen @handle, npub oder Namen ein"
|
||||
amount_title: "Betrag"
|
||||
to_name: "An %{name}"
|
||||
not_enough: "Du hast nicht genug grin"
|
||||
max: "Max"
|
||||
note_label: "Notiz"
|
||||
note_hint: "Notiz hinzufügen…"
|
||||
review_btn: "Prüfen"
|
||||
confirm_request: "Anfrage bestätigen"
|
||||
review_title: "Prüfen"
|
||||
requesting_from: "Fordere an von %{name}"
|
||||
youre_sending: "Du sendest %{name}"
|
||||
row_from: "Von"
|
||||
row_to: "An"
|
||||
row_note: "Notiz"
|
||||
row_they_pay: "Sie zahlen"
|
||||
row_they_pay_val: "Nur wenn sie zustimmen"
|
||||
row_delivery: "Zustellung"
|
||||
row_delivery_val: "NIP-44-verschlüsselt, über Nym"
|
||||
row_network_fee: "Netzwerkgebühr"
|
||||
row_network_fee_val: "Von deinem Guthaben abgezogen"
|
||||
row_privacy: "Privatsphäre"
|
||||
row_privacy_val: "Mimblewimble + Nym"
|
||||
send_request_btn: "Anfrage senden"
|
||||
request_approve_hint: "Sie erhalten eine Anfrage zum Zustimmen"
|
||||
hold_to_send: "Zum Senden halten"
|
||||
lower_amount: "Zurückgehen und Betrag verringern"
|
||||
hold_confirm_hint: "Gedrückt halten zum Bestätigen"
|
||||
requesting: "Fordere an…"
|
||||
sending: "Sende…"
|
||||
they: "Sie"
|
||||
request_blocked: "%{who} nimmt keine Anfragen an. Bitte sie, dir stattdessen grin zu senden."
|
||||
failed_request_title: "Anfrage fehlgeschlagen"
|
||||
failed_send_title: "Senden fehlgeschlagen"
|
||||
failed_request_body: "Die Anfrage konnte nicht zugestellt werden. Bitte sie, dir stattdessen grin zu senden."
|
||||
failed_send_body: "Die Zahlung wurde nicht zugestellt. Dein grin ist sicher — versuche es erneut."
|
||||
try_again_btn: "Erneut versuchen"
|
||||
close_btn: "Schließen"
|
||||
success:
|
||||
requested: "Angefordert"
|
||||
sent: "Gesendet"
|
||||
from: "von"
|
||||
to: "an"
|
||||
subtitle: "%{dir} %{who} · gerade eben"
|
||||
done_btn: "Fertig"
|
||||
receipt_btn: "Beleg"
|
||||
+394
-1
@@ -355,4 +355,397 @@ keyboard:
|
||||
m: m
|
||||
m1: ','
|
||||
m2: .
|
||||
m3: /
|
||||
m3: /
|
||||
goblin:
|
||||
home:
|
||||
anonymous: "Anonymous"
|
||||
connected_nym: "Connected over Nym"
|
||||
nym_ready: "Nym ready · relays…"
|
||||
connecting_nym: "Connecting to Nym…"
|
||||
cant_reach_node: "Can't reach node"
|
||||
node_synced: "Node synced"
|
||||
syncing: "Syncing…"
|
||||
block: "Block %{height}"
|
||||
waiting_for_chain: "Waiting for chain…"
|
||||
nav_wallet: "Wallet"
|
||||
nav_pay: "Pay"
|
||||
nav_activity: "Activity"
|
||||
nav_receive: "Receive"
|
||||
nav_settings: "Settings"
|
||||
activity: "Activity"
|
||||
empty_title: "No activity yet"
|
||||
empty_sub: "Send or receive grin to get started."
|
||||
recent: "Recent"
|
||||
scan_to_pay: "Scan to pay"
|
||||
type_amount: "Type an amount"
|
||||
request: "Request"
|
||||
pay: "Pay"
|
||||
enter_amount: "Enter an amount to pay or request"
|
||||
activity:
|
||||
canceled: "canceled"
|
||||
pending: "pending"
|
||||
earlier: "Earlier"
|
||||
today: "Today"
|
||||
yesterday: "Yesterday"
|
||||
title: "Activity"
|
||||
requests: "Requests"
|
||||
empty_title: "No activity yet"
|
||||
empty_sub: "Your payments will appear here."
|
||||
pending_header: "Pending"
|
||||
receipt:
|
||||
title: "Receipt"
|
||||
not_found: "Transaction not found"
|
||||
for_note: "For %{note}"
|
||||
details: "Transaction details"
|
||||
canceled: "Canceled"
|
||||
expired: "Expired"
|
||||
funds_returned: "Funds returned"
|
||||
complete: "Complete"
|
||||
payment_received: "Payment received"
|
||||
payment_sent: "Payment sent successfully"
|
||||
pending: "Pending"
|
||||
confs: "%{c}/%{r} confirmations"
|
||||
waiting_to_confirm: "Waiting to confirm"
|
||||
you: "You"
|
||||
to: "To"
|
||||
from: "From"
|
||||
nostr: "nostr"
|
||||
fee_none: "None"
|
||||
network_fee: "Network fee"
|
||||
privacy: "Privacy"
|
||||
privacy_value: "Mimblewimble + Nym"
|
||||
transaction: "Transaction"
|
||||
cancel_request: "Cancel request"
|
||||
request:
|
||||
title: "%{name} requests"
|
||||
approve: "Approve"
|
||||
decline: "Decline"
|
||||
receive:
|
||||
title: "Receive"
|
||||
requesting: "Requesting %{amt}%{tsu} — share to get paid"
|
||||
clear_request: "Clear request"
|
||||
share_handle: "Share your handle to get paid"
|
||||
copied: "Copied"
|
||||
copy_nostr_id: "Copy nostr ID"
|
||||
copy_address: "Copy address"
|
||||
privacy_note: "Your username is public. Payment contents stay encrypted over the network."
|
||||
profile:
|
||||
title: "Profile"
|
||||
activity: "Activity"
|
||||
no_activity: "No activity with them yet."
|
||||
unblock: "Unblock"
|
||||
block: "Block"
|
||||
blocked_blurb: "Blocked — their payments and requests are dropped."
|
||||
block_blurb: "Blocking drops their incoming payments and requests."
|
||||
settings:
|
||||
title: "Settings"
|
||||
connected_nostr: "Connected to nostr"
|
||||
connecting_relays: "Connecting to relays…"
|
||||
pic_updated: "Profile picture updated"
|
||||
uploading_pic: "Uploading picture…"
|
||||
claim_first: "Claim a username first — pictures ride on it"
|
||||
identity: "Identity"
|
||||
copy_npub: "Copy npub (public)"
|
||||
backup_nsec: "Back up secret key (nsec)"
|
||||
export_identity: "Export identity backup (encrypted)"
|
||||
rotate_key: "Rotate nostr key"
|
||||
import_identity: "Import identity (nsec / backup)"
|
||||
backup_note: "Moving devices? Back up BOTH: your seed phrase (funds) and an identity backup (name + key)."
|
||||
wallet: "Wallet"
|
||||
display_unit: "Display unit"
|
||||
relays: "Relays"
|
||||
node: "Node"
|
||||
slatepacks: "Slatepacks"
|
||||
slatepacks_value: "Manual transaction"
|
||||
lock_wallet: "Lock wallet"
|
||||
privacy: "Privacy"
|
||||
mixnet_routing: "Mixnet routing"
|
||||
messages_lookups: "Messages & lookups"
|
||||
auto_accept: "Auto-accept"
|
||||
pairing: "Pairing"
|
||||
accept_anyone: "Anyone"
|
||||
accept_contacts: "Contacts only"
|
||||
accept_ask: "Always ask"
|
||||
requests: "Requests"
|
||||
incoming_requests: "Incoming requests"
|
||||
incoming_requests_sub: "Let others request money from you"
|
||||
appearance: "Appearance"
|
||||
theme: "Theme"
|
||||
theme_light: "Light"
|
||||
theme_dark: "Dark"
|
||||
theme_yellow: "Yellow"
|
||||
archive: "Archive"
|
||||
export_archive: "Export archive"
|
||||
wipe_history: "Wipe payment history"
|
||||
about: "About"
|
||||
goblin: "Goblin"
|
||||
build: "Build %{build}"
|
||||
network: "Network"
|
||||
network_value: "MW + Nym mixnet + nostr"
|
||||
third_party: "Third party"
|
||||
grim: "GRIM (upstream wallet)"
|
||||
grin_node: "Grin node"
|
||||
sp_intro: "Advanced — exchange raw slatepacks by hand, the way GRIM does. Use this only when you can't pay or get paid through a @username."
|
||||
sp_receive_group: "Receive or finalize"
|
||||
sp_receive_blurb: "Paste a slatepack someone gave you. Goblin receives the payment, pays the invoice, or finalizes and posts it."
|
||||
sp_process: "Process slatepack"
|
||||
sp_paste_first: "Paste a slatepack first."
|
||||
sp_reply_ready: "Reply ready — send it back to the sender."
|
||||
sp_finalizing: "Finalizing and posting to the chain…"
|
||||
sp_create_group: "Create a payment"
|
||||
sp_create_blurb: "Make a slatepack to hand to someone. They receive it, send the reply back, and you finalize it above."
|
||||
sp_amount_hint: "Amount in grin"
|
||||
sp_addr_hint: "Recipient address (optional)"
|
||||
sp_create: "Create slatepack"
|
||||
sp_ready: "Slatepack ready — hand it to the recipient."
|
||||
sp_amount_gt_zero: "Enter an amount greater than zero."
|
||||
sp_to_send: "Slatepack to send"
|
||||
sp_copy: "Copy slatepack"
|
||||
rotate_line1: "• You get a brand-new RANDOM key; the old npub stops receiving. There is no derivation chain between them."
|
||||
rotate_line2: "• The new key is NOT recoverable from your seed — back up the new nsec right after rotating."
|
||||
rotate_line3: "• Your @username is RELEASED and your profile picture deleted — claim the same or a new name right after (anyone else can grab it too once it's free)."
|
||||
rotate_line4: "• Payments still in flight to the old key WILL be disrupted — wait for pending payments to finish first."
|
||||
rotate_line5: "• Contacts who saved your npub directly must re-find you — share your new npub or re-claimed @username."
|
||||
cancel: "Cancel"
|
||||
continue: "Continue"
|
||||
final_confirmation: "Final confirmation"
|
||||
rotate_confirm_blurb: "This cannot be undone from the app. Type RESET and enter your wallet password to rotate."
|
||||
type_reset: "Type RESET"
|
||||
wallet_password: "Wallet password"
|
||||
rotate_key_btn: "Rotate key"
|
||||
rotating_key: "Rotating key…"
|
||||
key_rotated: "Key rotated"
|
||||
new_npub: "New npub: %{npub}"
|
||||
backup_new_key: "Back up the NEW secret key now — your seed cannot recover it."
|
||||
copy_new_nsec: "Copy new nsec backup"
|
||||
done: "Done"
|
||||
rotation_failed: "Rotation failed"
|
||||
close: "Close"
|
||||
import_identity_title: "Import identity"
|
||||
import_blurb: "Replaces this wallet's nostr identity — paste a bare nsec or an exported identity backup (the backup also restores your username and history). Back up the current key first if you still need it."
|
||||
import_nsec_hint: "nsec1… or identity backup JSON"
|
||||
backup_password_hint: "Backup password (only if exported elsewhere)"
|
||||
import_btn: "Import"
|
||||
importing: "Importing…"
|
||||
identity_replaced: "Identity replaced"
|
||||
now_using: "Now using: %{npub}"
|
||||
import_failed: "Import failed"
|
||||
registered: "Registered %{name}"
|
||||
released_msg: "Released — the name is up for grabs"
|
||||
release_confirm: "Release @%{name}?"
|
||||
release_blurb: "It's up for grabs the moment it's free — anyone can claim it, including the next key you rotate to. Your profile picture is deleted with it. You won't be able to register another username for 10 minutes."
|
||||
releasing: "Releasing…"
|
||||
keep_it: "Keep it"
|
||||
release_it: "Release it"
|
||||
username: "Username"
|
||||
username_note: "Shown as @you. Public on goblin.st. Payments stay encrypted."
|
||||
release_username: "Release username"
|
||||
pick_username: "Pick a username — optional"
|
||||
working: "Working…"
|
||||
claim: "Claim"
|
||||
err_just_taken: "That username was just taken"
|
||||
err_cooldown: "You recently released a username — you can register a new one within 10 minutes."
|
||||
err_unreachable: "Couldn't reach goblin.st — connection hiccup. Try again."
|
||||
err_release: "Couldn't release: %{err}"
|
||||
avail_available: "Available!"
|
||||
avail_taken: "Taken"
|
||||
avail_reserved: "Reserved"
|
||||
avail_invalid: "Names are 3–30 chars: a–z, 0–9, _ or -"
|
||||
avail_quarantined: "Not available"
|
||||
avail_unknown: "Couldn't check — connection hiccup. Try again."
|
||||
privacy:
|
||||
title: "Network privacy"
|
||||
intro: "Goblin sends its private traffic through the Nym mixnet — a five-hop network that hides who is talking to whom, so a relay can't link a payment back to you."
|
||||
payments: "Payments"
|
||||
payments_blurb: "Every nostr message carrying a slatepack."
|
||||
usernames: "@usernames"
|
||||
usernames_blurb: "NIP-05 name lookups to and from goblin.st."
|
||||
price_avatars: "Price & avatars"
|
||||
price_avatars_blurb: "The rate preview and contact pictures."
|
||||
over_mixnet: "Over the mixnet"
|
||||
direct_connection: "Direct connection"
|
||||
grin_node: "Grin node"
|
||||
grin_node_blurb: "Block sync and broadcasting your transaction to the network. This is public chain data, the same for everyone, and isn't linked to your identity."
|
||||
pairing:
|
||||
title: "Pairing"
|
||||
intro: "What your balance and amounts are shown against."
|
||||
pair_with: "Pair with"
|
||||
rates_note: "Rates fetch over the Nym mixnet, only while a pairing is on — off means no rate request leaves your device."
|
||||
relays:
|
||||
title: "Relays"
|
||||
intro: "Payment messages are mirrored to every relay below; one reachable relay is enough to receive."
|
||||
your_relays: "Your relays"
|
||||
add_relay: "Add relay"
|
||||
add_relay_btn: "Add relay"
|
||||
save_reconnect: "Save & reconnect"
|
||||
none: "none"
|
||||
count: "%{n} relays"
|
||||
node:
|
||||
title: "Node"
|
||||
connection: "Connection"
|
||||
integrated: "Integrated node"
|
||||
applies_after: "Applies after the wallet is locked and unlocked again."
|
||||
add_external: "Add external node"
|
||||
api_secret_hint: "API secret (optional)"
|
||||
add_node: "Add node"
|
||||
integrated_host: "integrated node"
|
||||
summary_syncing: "%{conn} · syncing"
|
||||
summary_block: "Block %{height} · %{conn}"
|
||||
nips:
|
||||
title: "nostr & NIPs"
|
||||
intro1: "Goblin speaks nostr — an open protocol of signed messages passed through simple relay servers. Your wallet carries its own nostr identity: a standalone random key, kept deliberately independent of your funds and seed. Every payment travels as an end-to-end encrypted direct message between identities, with the slatepack riding inside."
|
||||
intro2: "goblin.st is Goblin's name service: claiming a username publishes a name → key mapping there (NIP-05), so people can pay @you instead of a long npub. The username is public; payment contents never are. NIPs are the protocol's building blocks — tap one to read the spec."
|
||||
n05_title: "Names"
|
||||
n05_blurb: "Maps @username@goblin.st to your key, so handles work like addresses."
|
||||
n17_title: "Private messages"
|
||||
n17_blurb: "The encrypted DM envelope every payment travels in."
|
||||
n44_title: "Encryption"
|
||||
n44_blurb: "The authenticated cipher used inside those messages."
|
||||
n49_title: "Key encryption"
|
||||
n49_blurb: "How the secret key is stored at rest, locked by your password."
|
||||
n59_title: "Gift wrap"
|
||||
n59_blurb: "Wraps messages so relays can't see who is talking to whom."
|
||||
n98_title: "HTTP auth"
|
||||
n98_blurb: "Signs the username registration request to goblin.st."
|
||||
onboarding:
|
||||
intro:
|
||||
private_money_head: "Private money"
|
||||
private_money_body: "Goblin is a wallet for grin — digital cash with no amounts or addresses on its chain."
|
||||
send_like_message_head: "Send like a message"
|
||||
send_like_message_body: "Pay a @username or npub and it arrives as an end-to-end encrypted message over nostr and the Nym mixnet — no one in between can see the amount or who's involved."
|
||||
yours_alone_head: "Yours alone"
|
||||
yours_alone_body: "Keys, names and history live on this device. Built on the GRIM wallet."
|
||||
get_started: "Get started"
|
||||
footnote: "Takes about a minute. You can change everything later."
|
||||
node:
|
||||
kicker: "STEP 1 OF 3 · NETWORK"
|
||||
title: "How should Goblin\nwatch the chain?"
|
||||
own_title: "Run my own node"
|
||||
own_badge: "Private"
|
||||
own_body: "Trusts no one — your wallet checks the chain itself. Syncs in the background while you finish setup."
|
||||
connect_title: "Connect to a node"
|
||||
connect_badge: "Instant"
|
||||
connect_body: "No sync wait. The node you pick can see your wallet's queries."
|
||||
changeable: "Changeable any time in Settings → Node."
|
||||
continue: "Continue"
|
||||
url_invalid: "Node URL must start with http:// or https://"
|
||||
wallet:
|
||||
kicker: "STEP 2 OF 3 · WALLET"
|
||||
title: "Set up your wallet"
|
||||
create_new: "Create new"
|
||||
restore_from_seed: "Restore from seed"
|
||||
name_hint: "Wallet name"
|
||||
password_hint: "Password"
|
||||
repeat_password_hint: "Repeat password"
|
||||
restore_hint: "Have your seed words ready — you'll enter them next."
|
||||
create_hint: "Next you'll get 24 seed words to write down. They are the money — anyone holding them holds your funds."
|
||||
continue: "Continue"
|
||||
passwords_no_match: "Passwords don't match"
|
||||
words:
|
||||
kicker: "STEP 2 OF 3 · WALLET"
|
||||
title_restore: "Enter your seed words"
|
||||
title_create: "Write these words down"
|
||||
write_down_hint: "On paper, in order. Anyone with these words can take your funds; without them a lost device means lost funds."
|
||||
paste: "Paste"
|
||||
scan_qr: "Scan QR"
|
||||
copy_clipboard: "Copy to clipboard (avoid this)"
|
||||
restore_wallet: "Restore wallet"
|
||||
wrote_them_down: "I wrote them down"
|
||||
fill_every_word: "Fill every word — tap a word to edit it, or paste the phrase."
|
||||
confirm:
|
||||
kicker: "STEP 2 OF 3 · WALLET"
|
||||
title: "Now prove it"
|
||||
enter_hint: "Enter the words you just wrote down. Tap a word to type it."
|
||||
paste: "Paste"
|
||||
create_wallet: "Create wallet"
|
||||
keep_going: "Keep going — every word, in order."
|
||||
identity:
|
||||
kicker: "STEP 3 OF 3 · IDENTITY"
|
||||
title: "Your payment identity"
|
||||
key_being_made: "key being made…"
|
||||
connected_nym: "connected over Nym"
|
||||
connecting_nym: "connecting over Nym…"
|
||||
fresh_key_blurb: "A fresh key, made for payments — deliberately not part of your seed, so you can rotate it anytime to maintain your privacy, without ever touching your funds. Back it up in Settings → Identity."
|
||||
clean_slate_blurb: "Want a clean slate? Swap in a brand-new key any time — the new you isn't linked to the old one. Same wallet, fresh face."
|
||||
pick_username: "Pick a username — optional"
|
||||
username_blurb: "Friends pay @you instead of a long key. Public on goblin.st; payments stay encrypted. Skip it and you're simply anonymous — claim one any time later."
|
||||
username_field_hint: "yourname"
|
||||
working: "Working…"
|
||||
claim_username: "Claim username"
|
||||
available_when_connected: "Available once the mixnet connects — or skip and claim later."
|
||||
youre: "You're @%{name}"
|
||||
open_wallet: "Open my wallet"
|
||||
skip_for_now: "Skip for now"
|
||||
errors:
|
||||
cant_open: "Couldn't open the wallet: %{err}"
|
||||
cant_create: "Couldn't create the wallet: %{err}"
|
||||
send:
|
||||
scan_to_request: "Scan to request"
|
||||
scan_to_pay: "Scan to pay"
|
||||
tab_scan: "Scan"
|
||||
tab_my_code: "My Code"
|
||||
request_from: "Request from"
|
||||
send_to: "Send to"
|
||||
search_hint: "@handle, npub, or name"
|
||||
suggested: "%{icon} Suggested"
|
||||
no_contacts: "No contacts yet. Find someone by their @handle."
|
||||
no_profile: "no profile"
|
||||
tag_contact: "contact"
|
||||
tag_on_nostr: "on nostr"
|
||||
searching_nostr: "Searching nostr…"
|
||||
unverified_title: "Pay an unverified key?"
|
||||
unverified_body: "No nostr profile is published for this key — it may be brand new, anonymous, or mistyped. Double-check it's the right one before sending."
|
||||
keep_looking: "Keep looking"
|
||||
pay_anyway: "Pay anyway"
|
||||
scan_not_recipient: "That QR isn't a goblin recipient — expected an npub or @handle"
|
||||
scan_prompt: "Position a goblin code in view to activate"
|
||||
scan_to_pay_me: "Scan to pay me"
|
||||
share_btn: "%{icon} Share"
|
||||
share_message: "Pay me on Goblin — %{handle}\n%{link}\nnpub: %{npub}"
|
||||
none_found: "No one found for %{label}"
|
||||
enter_recipient: "Enter an @handle, npub, or name"
|
||||
amount_title: "Amount"
|
||||
to_name: "To %{name}"
|
||||
not_enough: "You don't have enough grin"
|
||||
max: "Max"
|
||||
note_label: "Note"
|
||||
note_hint: "Add a note…"
|
||||
review_btn: "Review"
|
||||
confirm_request: "Confirm request"
|
||||
review_title: "Review"
|
||||
requesting_from: "Requesting from %{name}"
|
||||
youre_sending: "You're sending %{name}"
|
||||
row_from: "From"
|
||||
row_to: "To"
|
||||
row_note: "Note"
|
||||
row_they_pay: "They pay"
|
||||
row_they_pay_val: "Only if they approve"
|
||||
row_delivery: "Delivery"
|
||||
row_delivery_val: "NIP-44 encrypted, over Nym"
|
||||
row_network_fee: "Network fee"
|
||||
row_network_fee_val: "Deducted from your balance"
|
||||
row_privacy: "Privacy"
|
||||
row_privacy_val: "Mimblewimble + Nym"
|
||||
send_request_btn: "Send request"
|
||||
request_approve_hint: "They'll get a request to approve"
|
||||
hold_to_send: "Hold to send"
|
||||
lower_amount: "Go back and lower the amount"
|
||||
hold_confirm_hint: "Press and hold to confirm"
|
||||
requesting: "Requesting…"
|
||||
sending: "Sending…"
|
||||
they: "They"
|
||||
request_blocked: "%{who} isn't accepting requests. Ask them to send you grin instead."
|
||||
failed_request_title: "Couldn't request"
|
||||
failed_send_title: "Couldn't send"
|
||||
failed_request_body: "We couldn't deliver the request. Ask them to send you grin instead."
|
||||
failed_send_body: "The payment wasn't delivered. Your grin is safe — try again."
|
||||
try_again_btn: "Try again"
|
||||
close_btn: "Close"
|
||||
success:
|
||||
requested: "Requested"
|
||||
sent: "Sent"
|
||||
from: "from"
|
||||
to: "to"
|
||||
subtitle: "%{dir} %{who} · just now"
|
||||
done_btn: "Done"
|
||||
receipt_btn: "Receipt"
|
||||
+394
-1
@@ -355,4 +355,397 @@ keyboard:
|
||||
m: ','
|
||||
m1: .
|
||||
m2: ':'
|
||||
m3: /
|
||||
m3: /
|
||||
goblin:
|
||||
home:
|
||||
anonymous: "Anonyme"
|
||||
connected_nym: "Connecté via Nym"
|
||||
nym_ready: "Nym prêt · relais…"
|
||||
connecting_nym: "Connexion à Nym…"
|
||||
cant_reach_node: "Nœud injoignable"
|
||||
node_synced: "Nœud synchronisé"
|
||||
syncing: "Synchronisation…"
|
||||
block: "Bloc %{height}"
|
||||
waiting_for_chain: "En attente de la chaîne…"
|
||||
nav_wallet: "Portefeuille"
|
||||
nav_pay: "Payer"
|
||||
nav_activity: "Activité"
|
||||
nav_receive: "Recevoir"
|
||||
nav_settings: "Réglages"
|
||||
activity: "Activité"
|
||||
empty_title: "Aucune activité"
|
||||
empty_sub: "Envoyez ou recevez des grin pour commencer."
|
||||
recent: "Récent"
|
||||
scan_to_pay: "Scanner pour payer"
|
||||
type_amount: "Saisir un montant"
|
||||
request: "Demander"
|
||||
pay: "Payer"
|
||||
enter_amount: "Saisissez un montant à payer ou demander"
|
||||
activity:
|
||||
canceled: "annulé"
|
||||
pending: "en attente"
|
||||
earlier: "Plus tôt"
|
||||
today: "Aujourd'hui"
|
||||
yesterday: "Hier"
|
||||
title: "Activité"
|
||||
requests: "Demandes"
|
||||
empty_title: "Aucune activité"
|
||||
empty_sub: "Vos paiements apparaîtront ici."
|
||||
pending_header: "En attente"
|
||||
receipt:
|
||||
title: "Reçu"
|
||||
not_found: "Transaction introuvable"
|
||||
for_note: "Pour %{note}"
|
||||
details: "Détails de la transaction"
|
||||
canceled: "Annulé"
|
||||
expired: "Expiré"
|
||||
funds_returned: "Fonds retournés"
|
||||
complete: "Terminé"
|
||||
payment_received: "Paiement reçu"
|
||||
payment_sent: "Paiement envoyé avec succès"
|
||||
pending: "En attente"
|
||||
confs: "%{c}/%{r} confirmations"
|
||||
waiting_to_confirm: "En attente de confirmation"
|
||||
you: "Vous"
|
||||
to: "À"
|
||||
from: "De"
|
||||
nostr: "nostr"
|
||||
fee_none: "Aucun"
|
||||
network_fee: "Frais de réseau"
|
||||
privacy: "Confidentialité"
|
||||
privacy_value: "Mimblewimble + Nym"
|
||||
transaction: "Transaction"
|
||||
cancel_request: "Annuler la demande"
|
||||
request:
|
||||
title: "%{name} demande"
|
||||
approve: "Approuver"
|
||||
decline: "Refuser"
|
||||
receive:
|
||||
title: "Recevoir"
|
||||
requesting: "Demande de %{amt}%{tsu} — partagez pour être payé"
|
||||
clear_request: "Effacer la demande"
|
||||
share_handle: "Partagez votre identifiant pour être payé"
|
||||
copied: "Copié"
|
||||
copy_nostr_id: "Copier l'ID nostr"
|
||||
copy_address: "Copier l'adresse"
|
||||
privacy_note: "Votre nom d'utilisateur est public. Le contenu des paiements reste chiffré sur le réseau."
|
||||
profile:
|
||||
title: "Profil"
|
||||
activity: "Activité"
|
||||
no_activity: "Aucune activité avec cette personne."
|
||||
unblock: "Débloquer"
|
||||
block: "Bloquer"
|
||||
blocked_blurb: "Bloqué — ses paiements et demandes sont ignorés."
|
||||
block_blurb: "Le blocage ignore ses paiements et demandes entrants."
|
||||
settings:
|
||||
title: "Réglages"
|
||||
connected_nostr: "Connecté à nostr"
|
||||
connecting_relays: "Connexion aux relais…"
|
||||
pic_updated: "Photo de profil mise à jour"
|
||||
uploading_pic: "Envoi de la photo…"
|
||||
claim_first: "Réservez d'abord un nom d'utilisateur — la photo en dépend"
|
||||
identity: "Identité"
|
||||
copy_npub: "Copier le npub (public)"
|
||||
backup_nsec: "Sauvegarder la clé secrète (nsec)"
|
||||
export_identity: "Exporter la sauvegarde d'identité (chiffrée)"
|
||||
rotate_key: "Renouveler la clé nostr"
|
||||
import_identity: "Importer une identité (nsec / sauvegarde)"
|
||||
backup_note: "Changement d'appareil ? Sauvegardez LES DEUX : votre phrase de récupération (fonds) et une sauvegarde d'identité (nom + clé)."
|
||||
wallet: "Portefeuille"
|
||||
display_unit: "Unité d'affichage"
|
||||
relays: "Relais"
|
||||
node: "Nœud"
|
||||
slatepacks: "Slatepacks"
|
||||
slatepacks_value: "Transaction manuelle"
|
||||
lock_wallet: "Verrouiller le portefeuille"
|
||||
privacy: "Confidentialité"
|
||||
mixnet_routing: "Routage par mixnet"
|
||||
messages_lookups: "Messages et recherches"
|
||||
auto_accept: "Acceptation auto"
|
||||
pairing: "Appairage"
|
||||
accept_anyone: "Tout le monde"
|
||||
accept_contacts: "Contacts seulement"
|
||||
accept_ask: "Toujours demander"
|
||||
requests: "Demandes"
|
||||
incoming_requests: "Demandes entrantes"
|
||||
incoming_requests_sub: "Laisser les autres vous demander de l'argent"
|
||||
appearance: "Apparence"
|
||||
theme: "Thème"
|
||||
theme_light: "Clair"
|
||||
theme_dark: "Sombre"
|
||||
theme_yellow: "Jaune"
|
||||
archive: "Archive"
|
||||
export_archive: "Exporter l'archive"
|
||||
wipe_history: "Effacer l'historique des paiements"
|
||||
about: "À propos"
|
||||
goblin: "Goblin"
|
||||
build: "Build %{build}"
|
||||
network: "Réseau"
|
||||
network_value: "MW + mixnet Nym + nostr"
|
||||
third_party: "Tiers"
|
||||
grim: "GRIM (portefeuille amont)"
|
||||
grin_node: "Nœud grin"
|
||||
sp_intro: "Avancé — échangez des slatepacks bruts à la main, comme le fait GRIM. À utiliser seulement si vous ne pouvez pas payer ou être payé via un @username."
|
||||
sp_receive_group: "Recevoir ou finaliser"
|
||||
sp_receive_blurb: "Collez un slatepack qu'on vous a donné. Goblin reçoit le paiement, règle la facture, ou le finalise et le publie."
|
||||
sp_process: "Traiter le slatepack"
|
||||
sp_paste_first: "Collez d'abord un slatepack."
|
||||
sp_reply_ready: "Réponse prête — renvoyez-la à l'expéditeur."
|
||||
sp_finalizing: "Finalisation et publication sur la chaîne…"
|
||||
sp_create_group: "Créer un paiement"
|
||||
sp_create_blurb: "Créez un slatepack à remettre à quelqu'un. Il le reçoit, vous renvoie la réponse, et vous le finalisez ci-dessus."
|
||||
sp_amount_hint: "Montant en grin"
|
||||
sp_addr_hint: "Adresse du destinataire (facultatif)"
|
||||
sp_create: "Créer un slatepack"
|
||||
sp_ready: "Slatepack prêt — remettez-le au destinataire."
|
||||
sp_amount_gt_zero: "Saisissez un montant supérieur à zéro."
|
||||
sp_to_send: "Slatepack à envoyer"
|
||||
sp_copy: "Copier le slatepack"
|
||||
rotate_line1: "• Vous obtenez une toute nouvelle clé ALÉATOIRE ; l'ancien npub cesse de recevoir. Il n'y a aucune chaîne de dérivation entre eux."
|
||||
rotate_line2: "• La nouvelle clé n'est PAS récupérable depuis votre phrase de récupération — sauvegardez le nouveau nsec juste après le renouvellement."
|
||||
rotate_line3: "• Votre @username est LIBÉRÉ et votre photo de profil supprimée — réservez le même nom ou un nouveau juste après (n'importe qui peut le prendre une fois libre)."
|
||||
rotate_line4: "• Les paiements encore en cours vers l'ancienne clé SERONT interrompus — attendez d'abord la fin des paiements en attente."
|
||||
rotate_line5: "• Les contacts qui ont enregistré votre npub directement doivent vous retrouver — partagez votre nouveau npub ou votre @username re-réservé."
|
||||
cancel: "Annuler"
|
||||
continue: "Continuer"
|
||||
final_confirmation: "Confirmation finale"
|
||||
rotate_confirm_blurb: "Cette action est irréversible depuis l'app. Tapez RESET et saisissez le mot de passe du portefeuille pour renouveler."
|
||||
type_reset: "Tapez RESET"
|
||||
wallet_password: "Mot de passe du portefeuille"
|
||||
rotate_key_btn: "Renouveler la clé"
|
||||
rotating_key: "Renouvellement de la clé…"
|
||||
key_rotated: "Clé renouvelée"
|
||||
new_npub: "Nouveau npub : %{npub}"
|
||||
backup_new_key: "Sauvegardez la NOUVELLE clé secrète maintenant — votre phrase de récupération ne peut pas la restaurer."
|
||||
copy_new_nsec: "Copier la sauvegarde du nouveau nsec"
|
||||
done: "Terminé"
|
||||
rotation_failed: "Échec du renouvellement"
|
||||
close: "Fermer"
|
||||
import_identity_title: "Importer une identité"
|
||||
import_blurb: "Remplace l'identité nostr de ce portefeuille — collez un nsec brut ou une sauvegarde d'identité exportée (la sauvegarde restaure aussi votre nom d'utilisateur et votre historique). Sauvegardez d'abord la clé actuelle si vous en avez encore besoin."
|
||||
import_nsec_hint: "nsec1… ou JSON de sauvegarde d'identité"
|
||||
backup_password_hint: "Mot de passe de sauvegarde (uniquement si exportée ailleurs)"
|
||||
import_btn: "Importer"
|
||||
importing: "Importation…"
|
||||
identity_replaced: "Identité remplacée"
|
||||
now_using: "Utilise maintenant : %{npub}"
|
||||
import_failed: "Échec de l'importation"
|
||||
registered: "%{name} enregistré"
|
||||
released_msg: "Libéré — le nom est disponible"
|
||||
release_confirm: "Libérer @%{name} ?"
|
||||
release_blurb: "Il est disponible dès qu'il est libre — n'importe qui peut le réserver, y compris la prochaine clé vers laquelle vous renouvelez. Votre photo de profil est supprimée avec lui. Vous ne pourrez pas enregistrer un autre nom d'utilisateur pendant 10 minutes."
|
||||
releasing: "Libération…"
|
||||
keep_it: "Le garder"
|
||||
release_it: "Le libérer"
|
||||
username: "Nom d'utilisateur"
|
||||
username_note: "Affiché comme @you. Public sur goblin.st. Les paiements restent chiffrés."
|
||||
release_username: "Libérer le nom d'utilisateur"
|
||||
pick_username: "Choisir un nom d'utilisateur — facultatif"
|
||||
working: "En cours…"
|
||||
claim: "Réserver"
|
||||
err_just_taken: "Ce nom d'utilisateur vient d'être pris"
|
||||
err_cooldown: "Vous avez récemment libéré un nom d'utilisateur — vous pouvez en enregistrer un nouveau dans les 10 minutes."
|
||||
err_unreachable: "Impossible de joindre goblin.st — souci de connexion. Réessayez."
|
||||
err_release: "Impossible de libérer : %{err}"
|
||||
avail_available: "Disponible !"
|
||||
avail_taken: "Pris"
|
||||
avail_reserved: "Réservé"
|
||||
avail_invalid: "Les noms font 3 à 30 caractères : a–z, 0–9, _ ou -"
|
||||
avail_quarantined: "Indisponible"
|
||||
avail_unknown: "Vérification impossible — souci de connexion. Réessayez."
|
||||
privacy:
|
||||
title: "Confidentialité réseau"
|
||||
intro: "Goblin envoie son trafic privé via le mixnet Nym — un réseau à cinq sauts qui masque qui parle à qui, afin qu'un relais ne puisse pas relier un paiement à vous."
|
||||
payments: "Paiements"
|
||||
payments_blurb: "Chaque message nostr transportant un slatepack."
|
||||
usernames: "@usernames"
|
||||
usernames_blurb: "Recherches de noms NIP-05 vers et depuis goblin.st."
|
||||
price_avatars: "Cours et avatars"
|
||||
price_avatars_blurb: "L'aperçu du cours et les photos de contacts."
|
||||
over_mixnet: "Via le mixnet"
|
||||
direct_connection: "Connexion directe"
|
||||
grin_node: "Nœud grin"
|
||||
grin_node_blurb: "Synchronisation des blocs et diffusion de votre transaction sur le réseau. Ce sont des données de chaîne publiques, identiques pour tous, et non liées à votre identité."
|
||||
pairing:
|
||||
title: "Appairage"
|
||||
intro: "Ce à quoi votre solde et vos montants sont comparés."
|
||||
pair_with: "Apparier avec"
|
||||
rates_note: "Les cours sont récupérés via le mixnet Nym, uniquement tant qu'un appairage est actif — désactivé, aucune requête de cours ne quitte votre appareil."
|
||||
relays:
|
||||
title: "Relais"
|
||||
intro: "Les messages de paiement sont répliqués sur tous les relais ci-dessous ; un seul relais joignable suffit pour recevoir."
|
||||
your_relays: "Vos relais"
|
||||
add_relay: "Ajouter un relais"
|
||||
add_relay_btn: "Ajouter un relais"
|
||||
save_reconnect: "Enregistrer et reconnecter"
|
||||
none: "aucun"
|
||||
count: "%{n} relais"
|
||||
node:
|
||||
title: "Nœud"
|
||||
connection: "Connexion"
|
||||
integrated: "Nœud intégré"
|
||||
applies_after: "S'applique après le verrouillage puis déverrouillage du portefeuille."
|
||||
add_external: "Ajouter un nœud externe"
|
||||
api_secret_hint: "Secret API (facultatif)"
|
||||
add_node: "Ajouter le nœud"
|
||||
integrated_host: "nœud intégré"
|
||||
summary_syncing: "%{conn} · synchronisation"
|
||||
summary_block: "Bloc %{height} · %{conn}"
|
||||
nips:
|
||||
title: "nostr et NIP"
|
||||
intro1: "Goblin parle nostr — un protocole ouvert de messages signés transmis via de simples serveurs relais. Votre portefeuille porte sa propre identité nostr : une clé aléatoire autonome, gardée délibérément indépendante de vos fonds et de votre phrase de récupération. Chaque paiement voyage comme un message direct chiffré de bout en bout entre identités, le slatepack à l'intérieur."
|
||||
intro2: "goblin.st est le service de noms de Goblin : réserver un nom d'utilisateur y publie une correspondance nom → clé (NIP-05), pour qu'on puisse payer @you au lieu d'un long npub. Le nom d'utilisateur est public ; le contenu des paiements ne l'est jamais. Les NIP sont les briques du protocole — touchez-en un pour lire la spécification."
|
||||
n05_title: "Noms"
|
||||
n05_blurb: "Associe @username@goblin.st à votre clé, pour que les identifiants fonctionnent comme des adresses."
|
||||
n17_title: "Messages privés"
|
||||
n17_blurb: "L'enveloppe de DM chiffré dans laquelle voyage chaque paiement."
|
||||
n44_title: "Chiffrement"
|
||||
n44_blurb: "Le chiffrement authentifié utilisé à l'intérieur de ces messages."
|
||||
n49_title: "Chiffrement de clé"
|
||||
n49_blurb: "Comment la clé secrète est stockée au repos, verrouillée par votre mot de passe."
|
||||
n59_title: "Emballage cadeau"
|
||||
n59_blurb: "Enveloppe les messages pour que les relais ne voient pas qui parle à qui."
|
||||
n98_title: "Auth HTTP"
|
||||
n98_blurb: "Signe la demande d'enregistrement du nom d'utilisateur auprès de goblin.st."
|
||||
onboarding:
|
||||
intro:
|
||||
private_money_head: "Argent privé"
|
||||
private_money_body: "Goblin est un portefeuille pour grin — de l'argent numérique sans montants ni adresses sur sa chaîne."
|
||||
send_like_message_head: "Envoyer comme un message"
|
||||
send_like_message_body: "Payez un @username ou un npub et cela arrive comme un message chiffré de bout en bout via nostr et le mixnet Nym — personne entre les deux ne voit le montant ni les personnes impliquées."
|
||||
yours_alone_head: "À vous seul"
|
||||
yours_alone_body: "Clés, noms et historique vivent sur cet appareil. Construit sur le portefeuille GRIM."
|
||||
get_started: "Commencer"
|
||||
footnote: "Environ une minute. Vous pourrez tout changer plus tard."
|
||||
node:
|
||||
kicker: "ÉTAPE 1 SUR 3 · RÉSEAU"
|
||||
title: "Comment Goblin doit-il\nsurveiller la chaîne ?"
|
||||
own_title: "Lancer mon propre nœud"
|
||||
own_badge: "Privé"
|
||||
own_body: "Ne fait confiance à personne — votre portefeuille vérifie la chaîne lui-même. Se synchronise en arrière-plan pendant que vous terminez la configuration."
|
||||
connect_title: "Se connecter à un nœud"
|
||||
connect_badge: "Instantané"
|
||||
connect_body: "Aucune attente de synchronisation. Le nœud que vous choisissez peut voir les requêtes de votre portefeuille."
|
||||
changeable: "Modifiable à tout moment dans Réglages → Nœud."
|
||||
continue: "Continuer"
|
||||
url_invalid: "L'URL du nœud doit commencer par http:// ou https://"
|
||||
wallet:
|
||||
kicker: "ÉTAPE 2 SUR 3 · PORTEFEUILLE"
|
||||
title: "Configurez votre portefeuille"
|
||||
create_new: "Créer un nouveau"
|
||||
restore_from_seed: "Restaurer depuis la phrase"
|
||||
name_hint: "Nom du portefeuille"
|
||||
password_hint: "Mot de passe"
|
||||
repeat_password_hint: "Répéter le mot de passe"
|
||||
restore_hint: "Préparez vos mots de récupération — vous les saisirez ensuite."
|
||||
create_hint: "Vous obtiendrez ensuite 24 mots de récupération à noter. Ce sont l'argent — quiconque les détient détient vos fonds."
|
||||
continue: "Continuer"
|
||||
passwords_no_match: "Les mots de passe ne correspondent pas"
|
||||
words:
|
||||
kicker: "ÉTAPE 2 SUR 3 · PORTEFEUILLE"
|
||||
title_restore: "Saisissez vos mots de récupération"
|
||||
title_create: "Notez ces mots"
|
||||
write_down_hint: "Sur papier, dans l'ordre. Quiconque a ces mots peut prendre vos fonds ; sans eux, un appareil perdu signifie des fonds perdus."
|
||||
paste: "Coller"
|
||||
scan_qr: "Scanner le QR"
|
||||
copy_clipboard: "Copier dans le presse-papiers (à éviter)"
|
||||
restore_wallet: "Restaurer le portefeuille"
|
||||
wrote_them_down: "Je les ai notés"
|
||||
fill_every_word: "Remplissez chaque mot — touchez un mot pour le modifier, ou collez la phrase."
|
||||
confirm:
|
||||
kicker: "ÉTAPE 2 SUR 3 · PORTEFEUILLE"
|
||||
title: "Maintenant prouvez-le"
|
||||
enter_hint: "Saisissez les mots que vous venez de noter. Touchez un mot pour le taper."
|
||||
paste: "Coller"
|
||||
create_wallet: "Créer le portefeuille"
|
||||
keep_going: "Continuez — chaque mot, dans l'ordre."
|
||||
identity:
|
||||
kicker: "ÉTAPE 3 SUR 3 · IDENTITÉ"
|
||||
title: "Votre identité de paiement"
|
||||
key_being_made: "clé en cours de création…"
|
||||
connected_nym: "connecté via Nym"
|
||||
connecting_nym: "connexion via Nym…"
|
||||
fresh_key_blurb: "Une clé neuve, créée pour les paiements — délibérément hors de votre phrase de récupération, pour la renouveler à tout moment et préserver votre confidentialité, sans jamais toucher à vos fonds. Sauvegardez-la dans Réglages → Identité."
|
||||
clean_slate_blurb: "Envie de repartir à zéro ? Remplacez par une toute nouvelle clé à tout moment — le nouveau vous n'est pas lié à l'ancien. Même portefeuille, nouveau visage."
|
||||
pick_username: "Choisir un nom d'utilisateur — facultatif"
|
||||
username_blurb: "Vos amis paient @you au lieu d'une longue clé. Public sur goblin.st ; les paiements restent chiffrés. Passez et vous restez simplement anonyme — réservez-en un à tout moment plus tard."
|
||||
username_field_hint: "votrenom"
|
||||
working: "En cours…"
|
||||
claim_username: "Réserver le nom d'utilisateur"
|
||||
available_when_connected: "Disponible une fois le mixnet connecté — ou passez et réservez plus tard."
|
||||
youre: "Vous êtes @%{name}"
|
||||
open_wallet: "Ouvrir mon portefeuille"
|
||||
skip_for_now: "Passer pour l'instant"
|
||||
errors:
|
||||
cant_open: "Impossible d'ouvrir le portefeuille : %{err}"
|
||||
cant_create: "Impossible de créer le portefeuille : %{err}"
|
||||
send:
|
||||
scan_to_request: "Scanner pour demander"
|
||||
scan_to_pay: "Scanner pour payer"
|
||||
tab_scan: "Scanner"
|
||||
tab_my_code: "Mon code"
|
||||
request_from: "Demander à"
|
||||
send_to: "Envoyer à"
|
||||
search_hint: "@handle, npub ou nom"
|
||||
suggested: "%{icon} Suggéré"
|
||||
no_contacts: "Aucun contact pour l'instant. Trouvez quelqu'un par son @handle."
|
||||
no_profile: "pas de profil"
|
||||
tag_contact: "contact"
|
||||
tag_on_nostr: "sur nostr"
|
||||
searching_nostr: "Recherche sur nostr…"
|
||||
unverified_title: "Payer une clé non vérifiée ?"
|
||||
unverified_body: "Aucun profil nostr n'est publié pour cette clé — elle peut être toute neuve, anonyme ou mal saisie. Vérifiez bien qu'il s'agit de la bonne avant d'envoyer."
|
||||
keep_looking: "Continuer à chercher"
|
||||
pay_anyway: "Payer quand même"
|
||||
scan_not_recipient: "Ce QR n'est pas un destinataire goblin — un npub ou @handle est attendu"
|
||||
scan_prompt: "Placez un code goblin dans le champ pour activer"
|
||||
scan_to_pay_me: "Scannez pour me payer"
|
||||
share_btn: "%{icon} Partager"
|
||||
share_message: "Payez-moi sur Goblin — %{handle}\n%{link}\nnpub : %{npub}"
|
||||
none_found: "Personne trouvé pour %{label}"
|
||||
enter_recipient: "Saisissez un @handle, un npub ou un nom"
|
||||
amount_title: "Montant"
|
||||
to_name: "À %{name}"
|
||||
not_enough: "Vous n'avez pas assez de grin"
|
||||
max: "Max"
|
||||
note_label: "Note"
|
||||
note_hint: "Ajouter une note…"
|
||||
review_btn: "Vérifier"
|
||||
confirm_request: "Confirmer la demande"
|
||||
review_title: "Vérification"
|
||||
requesting_from: "Demande à %{name}"
|
||||
youre_sending: "Vous envoyez %{name}"
|
||||
row_from: "De"
|
||||
row_to: "À"
|
||||
row_note: "Note"
|
||||
row_they_pay: "Ils paient"
|
||||
row_they_pay_val: "Seulement s'ils approuvent"
|
||||
row_delivery: "Livraison"
|
||||
row_delivery_val: "Chiffré NIP-44, via Nym"
|
||||
row_network_fee: "Frais de réseau"
|
||||
row_network_fee_val: "Déduit de votre solde"
|
||||
row_privacy: "Confidentialité"
|
||||
row_privacy_val: "Mimblewimble + Nym"
|
||||
send_request_btn: "Envoyer la demande"
|
||||
request_approve_hint: "Ils recevront une demande à approuver"
|
||||
hold_to_send: "Maintenir pour envoyer"
|
||||
lower_amount: "Revenez et baissez le montant"
|
||||
hold_confirm_hint: "Appuyez et maintenez pour confirmer"
|
||||
requesting: "Demande en cours…"
|
||||
sending: "Envoi…"
|
||||
they: "Ils"
|
||||
request_blocked: "%{who} n'accepte pas les demandes. Demandez-lui de vous envoyer des grin à la place."
|
||||
failed_request_title: "Échec de la demande"
|
||||
failed_send_title: "Échec de l'envoi"
|
||||
failed_request_body: "Impossible de livrer la demande. Demandez-lui de vous envoyer des grin à la place."
|
||||
failed_send_body: "Le paiement n'a pas été livré. Vos grin sont en sécurité — réessayez."
|
||||
try_again_btn: "Réessayer"
|
||||
close_btn: "Fermer"
|
||||
success:
|
||||
requested: "Demandé"
|
||||
sent: "Envoyé"
|
||||
from: "de"
|
||||
to: "à"
|
||||
subtitle: "%{dir} %{who} · à l'instant"
|
||||
done_btn: "Terminé"
|
||||
receipt_btn: "Reçu"
|
||||
+394
-1
@@ -355,4 +355,397 @@ keyboard:
|
||||
m: ь
|
||||
m1: б
|
||||
m2: ю
|
||||
m3: ё
|
||||
m3: ё
|
||||
goblin:
|
||||
home:
|
||||
anonymous: "Аноним"
|
||||
connected_nym: "Подключено через Nym"
|
||||
nym_ready: "Nym готов · реле…"
|
||||
connecting_nym: "Подключение к Nym…"
|
||||
cant_reach_node: "Нет связи с узлом"
|
||||
node_synced: "Узел синхронизирован"
|
||||
syncing: "Синхронизация…"
|
||||
block: "Блок %{height}"
|
||||
waiting_for_chain: "Ожидание цепочки…"
|
||||
nav_wallet: "Кошелёк"
|
||||
nav_pay: "Оплатить"
|
||||
nav_activity: "Действия"
|
||||
nav_receive: "Получить"
|
||||
nav_settings: "Настройки"
|
||||
activity: "Действия"
|
||||
empty_title: "Пока нет действий"
|
||||
empty_sub: "Отправьте или получите grin, чтобы начать."
|
||||
recent: "Недавние"
|
||||
scan_to_pay: "Сканируйте для оплаты"
|
||||
type_amount: "Введите сумму"
|
||||
request: "Запросить"
|
||||
pay: "Оплатить"
|
||||
enter_amount: "Введите сумму для оплаты или запроса"
|
||||
activity:
|
||||
canceled: "отменено"
|
||||
pending: "в ожидании"
|
||||
earlier: "Ранее"
|
||||
today: "Сегодня"
|
||||
yesterday: "Вчера"
|
||||
title: "Действия"
|
||||
requests: "Запросы"
|
||||
empty_title: "Пока нет действий"
|
||||
empty_sub: "Здесь появятся ваши платежи."
|
||||
pending_header: "В ожидании"
|
||||
receipt:
|
||||
title: "Квитанция"
|
||||
not_found: "Транзакция не найдена"
|
||||
for_note: "За %{note}"
|
||||
details: "Детали транзакции"
|
||||
canceled: "Отменено"
|
||||
expired: "Истекло"
|
||||
funds_returned: "Средства возвращены"
|
||||
complete: "Завершено"
|
||||
payment_received: "Платёж получен"
|
||||
payment_sent: "Платёж успешно отправлен"
|
||||
pending: "В ожидании"
|
||||
confs: "%{c}/%{r} подтверждений"
|
||||
waiting_to_confirm: "Ожидание подтверждения"
|
||||
you: "Вы"
|
||||
to: "Кому"
|
||||
from: "От"
|
||||
nostr: "nostr"
|
||||
fee_none: "Нет"
|
||||
network_fee: "Сетевая комиссия"
|
||||
privacy: "Приватность"
|
||||
privacy_value: "Mimblewimble + Nym"
|
||||
transaction: "Транзакция"
|
||||
cancel_request: "Отменить запрос"
|
||||
request:
|
||||
title: "%{name} запрашивает"
|
||||
approve: "Принять"
|
||||
decline: "Отклонить"
|
||||
receive:
|
||||
title: "Получить"
|
||||
requesting: "Запрос %{amt}%{tsu} — поделитесь, чтобы получить оплату"
|
||||
clear_request: "Очистить запрос"
|
||||
share_handle: "Поделитесь именем, чтобы получить оплату"
|
||||
copied: "Скопировано"
|
||||
copy_nostr_id: "Копировать nostr ID"
|
||||
copy_address: "Копировать адрес"
|
||||
privacy_note: "Ваше имя публично. Содержимое платежей остаётся зашифрованным в сети."
|
||||
profile:
|
||||
title: "Профиль"
|
||||
activity: "Действия"
|
||||
no_activity: "Пока нет действий с ними."
|
||||
unblock: "Разблокировать"
|
||||
block: "Заблокировать"
|
||||
blocked_blurb: "Заблокирован — их платежи и запросы отклоняются."
|
||||
block_blurb: "Блокировка отклоняет их входящие платежи и запросы."
|
||||
settings:
|
||||
title: "Настройки"
|
||||
connected_nostr: "Подключено к nostr"
|
||||
connecting_relays: "Подключение к реле…"
|
||||
pic_updated: "Фото профиля обновлено"
|
||||
uploading_pic: "Загрузка фото…"
|
||||
claim_first: "Сначала займите имя — фото привязывается к нему"
|
||||
identity: "Личность"
|
||||
copy_npub: "Копировать npub (публичный)"
|
||||
backup_nsec: "Резерв секретного ключа (nsec)"
|
||||
export_identity: "Экспорт резерва личности (зашифр.)"
|
||||
rotate_key: "Сменить ключ nostr"
|
||||
import_identity: "Импорт личности (nsec / резерв)"
|
||||
backup_note: "Меняете устройство? Сохраните ОБА: seed-фразу (средства) и резерв личности (имя + ключ)."
|
||||
wallet: "Кошелёк"
|
||||
display_unit: "Единица отображения"
|
||||
relays: "Реле"
|
||||
node: "Узел"
|
||||
slatepacks: "Slatepacks"
|
||||
slatepacks_value: "Ручная транзакция"
|
||||
lock_wallet: "Заблокировать кошелёк"
|
||||
privacy: "Приватность"
|
||||
mixnet_routing: "Маршрутизация через mixnet"
|
||||
messages_lookups: "Сообщения и поиск"
|
||||
auto_accept: "Автоприём"
|
||||
pairing: "Привязка"
|
||||
accept_anyone: "Любой"
|
||||
accept_contacts: "Только контакты"
|
||||
accept_ask: "Всегда спрашивать"
|
||||
requests: "Запросы"
|
||||
incoming_requests: "Входящие запросы"
|
||||
incoming_requests_sub: "Разрешить другим запрашивать у вас деньги"
|
||||
appearance: "Внешний вид"
|
||||
theme: "Тема"
|
||||
theme_light: "Светлая"
|
||||
theme_dark: "Тёмная"
|
||||
theme_yellow: "Жёлтая"
|
||||
archive: "Архив"
|
||||
export_archive: "Экспорт архива"
|
||||
wipe_history: "Стереть историю платежей"
|
||||
about: "О приложении"
|
||||
goblin: "Goblin"
|
||||
build: "Сборка %{build}"
|
||||
network: "Сеть"
|
||||
network_value: "MW + mixnet Nym + nostr"
|
||||
third_party: "Сторонние"
|
||||
grim: "GRIM (исходный кошелёк)"
|
||||
grin_node: "Узел Grin"
|
||||
sp_intro: "Для опытных — обмен сырыми slatepacks вручную, как в GRIM. Используйте только если не можете платить или получать через @username."
|
||||
sp_receive_group: "Получить или завершить"
|
||||
sp_receive_blurb: "Вставьте slatepack, который вам дали. Goblin получит платёж, оплатит счёт или завершит и опубликует его."
|
||||
sp_process: "Обработать slatepack"
|
||||
sp_paste_first: "Сначала вставьте slatepack."
|
||||
sp_reply_ready: "Ответ готов — отправьте его обратно отправителю."
|
||||
sp_finalizing: "Завершение и публикация в цепочку…"
|
||||
sp_create_group: "Создать платёж"
|
||||
sp_create_blurb: "Создайте slatepack для передачи кому-то. Они получат его, отправят ответ, а вы завершите его выше."
|
||||
sp_amount_hint: "Сумма в grin"
|
||||
sp_addr_hint: "Адрес получателя (необязательно)"
|
||||
sp_create: "Создать slatepack"
|
||||
sp_ready: "Slatepack готов — передайте его получателю."
|
||||
sp_amount_gt_zero: "Введите сумму больше нуля."
|
||||
sp_to_send: "Slatepack для отправки"
|
||||
sp_copy: "Копировать slatepack"
|
||||
rotate_line1: "• Вы получите совершенно новый СЛУЧАЙНЫЙ ключ; старый npub перестанет принимать. Между ними нет цепочки вывода."
|
||||
rotate_line2: "• Новый ключ НЕЛЬЗЯ восстановить из seed — сохраните новый nsec сразу после смены."
|
||||
rotate_line3: "• Ваш @username ОСВОБОЖДАЕТСЯ, а фото профиля удаляется — займите то же или новое имя сразу после (любой другой тоже может его занять, как только оно свободно)."
|
||||
rotate_line4: "• Платежи, всё ещё идущие к старому ключу, БУДУТ нарушены — сначала дождитесь завершения ожидающих платежей."
|
||||
rotate_line5: "• Контакты, сохранившие ваш npub напрямую, должны найти вас заново — поделитесь новым npub или заново занятым @username."
|
||||
cancel: "Отмена"
|
||||
continue: "Продолжить"
|
||||
final_confirmation: "Финальное подтверждение"
|
||||
rotate_confirm_blurb: "Это нельзя отменить из приложения. Введите RESET и пароль кошелька, чтобы сменить."
|
||||
type_reset: "Введите RESET"
|
||||
wallet_password: "Пароль кошелька"
|
||||
rotate_key_btn: "Сменить ключ"
|
||||
rotating_key: "Смена ключа…"
|
||||
key_rotated: "Ключ сменён"
|
||||
new_npub: "Новый npub: %{npub}"
|
||||
backup_new_key: "Сохраните НОВЫЙ секретный ключ сейчас — seed не сможет его восстановить."
|
||||
copy_new_nsec: "Копировать резерв нового nsec"
|
||||
done: "Готово"
|
||||
rotation_failed: "Смена не удалась"
|
||||
close: "Закрыть"
|
||||
import_identity_title: "Импорт личности"
|
||||
import_blurb: "Заменяет nostr-личность этого кошелька — вставьте чистый nsec или экспортированный резерв личности (резерв также восстанавливает имя и историю). Сначала сохраните текущий ключ, если он ещё нужен."
|
||||
import_nsec_hint: "nsec1… или JSON резерва личности"
|
||||
backup_password_hint: "Пароль резерва (только если экспортирован в другом месте)"
|
||||
import_btn: "Импорт"
|
||||
importing: "Импорт…"
|
||||
identity_replaced: "Личность заменена"
|
||||
now_using: "Сейчас используется: %{npub}"
|
||||
import_failed: "Импорт не удался"
|
||||
registered: "Зарегистрировано %{name}"
|
||||
released_msg: "Освобождено — имя свободно для занятия"
|
||||
release_confirm: "Освободить @%{name}?"
|
||||
release_blurb: "Имя свободно для занятия сразу после освобождения — любой может его занять, включая следующий ключ, на который вы смените. Фото профиля удаляется вместе с ним. Вы не сможете зарегистрировать другое имя в течение 10 минут."
|
||||
releasing: "Освобождение…"
|
||||
keep_it: "Оставить"
|
||||
release_it: "Освободить"
|
||||
username: "Имя пользователя"
|
||||
username_note: "Показывается как @you. Публично на goblin.st. Платежи остаются зашифрованными."
|
||||
release_username: "Освободить имя"
|
||||
pick_username: "Выберите имя — необязательно"
|
||||
working: "Обработка…"
|
||||
claim: "Занять"
|
||||
err_just_taken: "Это имя только что заняли"
|
||||
err_cooldown: "Вы недавно освободили имя — можно зарегистрировать новое в течение 10 минут."
|
||||
err_unreachable: "Не удалось связаться с goblin.st — сбой соединения. Попробуйте снова."
|
||||
err_release: "Не удалось освободить: %{err}"
|
||||
avail_available: "Доступно!"
|
||||
avail_taken: "Занято"
|
||||
avail_reserved: "Зарезервировано"
|
||||
avail_invalid: "Имена 3–30 символов: a–z, 0–9, _ или -"
|
||||
avail_quarantined: "Недоступно"
|
||||
avail_unknown: "Не удалось проверить — сбой соединения. Попробуйте снова."
|
||||
privacy:
|
||||
title: "Сетевая приватность"
|
||||
intro: "Goblin отправляет приватный трафик через mixnet Nym — сеть из пяти переходов, скрывающую, кто с кем общается, чтобы реле не могло связать платёж с вами."
|
||||
payments: "Платежи"
|
||||
payments_blurb: "Каждое nostr-сообщение, несущее slatepack."
|
||||
usernames: "@usernames"
|
||||
usernames_blurb: "Поиск имён NIP-05 к и от goblin.st."
|
||||
price_avatars: "Курс и аватары"
|
||||
price_avatars_blurb: "Предпросмотр курса и фото контактов."
|
||||
over_mixnet: "Через mixnet"
|
||||
direct_connection: "Прямое соединение"
|
||||
grin_node: "Узел Grin"
|
||||
grin_node_blurb: "Синхронизация блоков и трансляция транзакции в сеть. Это публичные данные цепочки, одинаковые для всех, и они не связаны с вашей личностью."
|
||||
pairing:
|
||||
title: "Привязка"
|
||||
intro: "К чему привязаны отображаемые баланс и суммы."
|
||||
pair_with: "Привязать к"
|
||||
rates_note: "Курсы загружаются через mixnet Nym только при включённой привязке — выключено означает, что запрос курса не покидает устройство."
|
||||
relays:
|
||||
title: "Реле"
|
||||
intro: "Сообщения о платежах дублируются на каждое реле ниже; для получения достаточно одного доступного реле."
|
||||
your_relays: "Ваши реле"
|
||||
add_relay: "Добавить реле"
|
||||
add_relay_btn: "Добавить реле"
|
||||
save_reconnect: "Сохранить и переподключить"
|
||||
none: "нет"
|
||||
count: "%{n} реле"
|
||||
node:
|
||||
title: "Узел"
|
||||
connection: "Соединение"
|
||||
integrated: "Встроенный узел"
|
||||
applies_after: "Применяется после блокировки и повторной разблокировки кошелька."
|
||||
add_external: "Добавить внешний узел"
|
||||
api_secret_hint: "API-секрет (необязательно)"
|
||||
add_node: "Добавить узел"
|
||||
integrated_host: "встроенный узел"
|
||||
summary_syncing: "%{conn} · синхронизация"
|
||||
summary_block: "Блок %{height} · %{conn}"
|
||||
nips:
|
||||
title: "nostr и NIPs"
|
||||
intro1: "Goblin говорит на nostr — открытом протоколе подписанных сообщений, передаваемых через простые реле-серверы. Ваш кошелёк несёт собственную nostr-личность: отдельный случайный ключ, намеренно независимый от ваших средств и seed. Каждый платёж идёт как сквозно зашифрованное личное сообщение между личностями, со slatepack внутри."
|
||||
intro2: "goblin.st — это служба имён Goblin: занятие имени публикует там сопоставление имя → ключ (NIP-05), чтобы вам платили на @you вместо длинного npub. Имя публично; содержимое платежей — никогда. NIPs — это строительные блоки протокола; коснитесь одного, чтобы прочитать спецификацию."
|
||||
n05_title: "Имена"
|
||||
n05_blurb: "Сопоставляет @username@goblin.st с вашим ключом, чтобы имена работали как адреса."
|
||||
n17_title: "Личные сообщения"
|
||||
n17_blurb: "Зашифрованный конверт DM, в котором идёт каждый платёж."
|
||||
n44_title: "Шифрование"
|
||||
n44_blurb: "Аутентифицированный шифр, используемый внутри этих сообщений."
|
||||
n49_title: "Шифрование ключа"
|
||||
n49_blurb: "Как секретный ключ хранится в покое, защищённый вашим паролем."
|
||||
n59_title: "Gift wrap"
|
||||
n59_blurb: "Оборачивает сообщения, чтобы реле не видели, кто с кем общается."
|
||||
n98_title: "HTTP-авторизация"
|
||||
n98_blurb: "Подписывает запрос регистрации имени на goblin.st."
|
||||
onboarding:
|
||||
intro:
|
||||
private_money_head: "Приватные деньги"
|
||||
private_money_body: "Goblin — кошелёк для grin: цифровая наличность без сумм и адресов в её цепочке."
|
||||
send_like_message_head: "Отправляйте как сообщение"
|
||||
send_like_message_body: "Заплатите на @username или npub, и платёж придёт как сквозно зашифрованное сообщение через nostr и mixnet Nym — никто посередине не увидит сумму или участников."
|
||||
yours_alone_head: "Только ваше"
|
||||
yours_alone_body: "Ключи, имена и история живут на этом устройстве. На базе кошелька GRIM."
|
||||
get_started: "Начать"
|
||||
footnote: "Займёт около минуты. Всё можно изменить позже."
|
||||
node:
|
||||
kicker: "ШАГ 1 ИЗ 3 · СЕТЬ"
|
||||
title: "Как Goblin должен\nследить за цепочкой?"
|
||||
own_title: "Запустить свой узел"
|
||||
own_badge: "Приватно"
|
||||
own_body: "Никому не доверяет — ваш кошелёк проверяет цепочку сам. Синхронизируется в фоне, пока вы завершаете настройку."
|
||||
connect_title: "Подключиться к узлу"
|
||||
connect_badge: "Мгновенно"
|
||||
connect_body: "Без ожидания синхронизации. Выбранный узел может видеть запросы вашего кошелька."
|
||||
changeable: "Меняется в любой момент в Настройки → Узел."
|
||||
continue: "Продолжить"
|
||||
url_invalid: "URL узла должен начинаться с http:// или https://"
|
||||
wallet:
|
||||
kicker: "ШАГ 2 ИЗ 3 · КОШЕЛЁК"
|
||||
title: "Настройте кошелёк"
|
||||
create_new: "Создать новый"
|
||||
restore_from_seed: "Восстановить из seed"
|
||||
name_hint: "Имя кошелька"
|
||||
password_hint: "Пароль"
|
||||
repeat_password_hint: "Повторите пароль"
|
||||
restore_hint: "Подготовьте seed-слова — вы введёте их далее."
|
||||
create_hint: "Далее вы получите 24 seed-слова для записи. Они — это деньги: кто владеет ими, владеет вашими средствами."
|
||||
continue: "Продолжить"
|
||||
passwords_no_match: "Пароли не совпадают"
|
||||
words:
|
||||
kicker: "ШАГ 2 ИЗ 3 · КОШЕЛЁК"
|
||||
title_restore: "Введите seed-слова"
|
||||
title_create: "Запишите эти слова"
|
||||
write_down_hint: "На бумаге, по порядку. Любой с этими словами может забрать ваши средства; без них потеря устройства означает потерю средств."
|
||||
paste: "Вставить"
|
||||
scan_qr: "Сканировать QR"
|
||||
copy_clipboard: "Копировать в буфер (избегайте этого)"
|
||||
restore_wallet: "Восстановить кошелёк"
|
||||
wrote_them_down: "Я записал их"
|
||||
fill_every_word: "Заполните каждое слово — коснитесь слова для редактирования или вставьте фразу."
|
||||
confirm:
|
||||
kicker: "ШАГ 2 ИЗ 3 · КОШЕЛЁК"
|
||||
title: "Теперь подтвердите"
|
||||
enter_hint: "Введите слова, которые только что записали. Коснитесь слова, чтобы ввести его."
|
||||
paste: "Вставить"
|
||||
create_wallet: "Создать кошелёк"
|
||||
keep_going: "Продолжайте — каждое слово, по порядку."
|
||||
identity:
|
||||
kicker: "ШАГ 3 ИЗ 3 · ЛИЧНОСТЬ"
|
||||
title: "Ваша платёжная личность"
|
||||
key_being_made: "ключ создаётся…"
|
||||
connected_nym: "подключено через Nym"
|
||||
connecting_nym: "подключение через Nym…"
|
||||
fresh_key_blurb: "Новый ключ, созданный для платежей — намеренно не часть вашего seed, поэтому вы можете менять его в любой момент для сохранения приватности, не затрагивая средства. Сохраните его в Настройки → Личность."
|
||||
clean_slate_blurb: "Хотите начать с чистого листа? Подставьте совершенно новый ключ в любой момент — новый вы не связан со старым. Тот же кошелёк, новое лицо."
|
||||
pick_username: "Выберите имя — необязательно"
|
||||
username_blurb: "Друзья платят на @you вместо длинного ключа. Публично на goblin.st; платежи остаются зашифрованными. Пропустите — и вы просто аноним; имя можно занять позже."
|
||||
username_field_hint: "yourname"
|
||||
working: "Обработка…"
|
||||
claim_username: "Занять имя"
|
||||
available_when_connected: "Доступно после подключения mixnet — или пропустите и займите позже."
|
||||
youre: "Вы @%{name}"
|
||||
open_wallet: "Открыть кошелёк"
|
||||
skip_for_now: "Пропустить пока"
|
||||
errors:
|
||||
cant_open: "Не удалось открыть кошелёк: %{err}"
|
||||
cant_create: "Не удалось создать кошелёк: %{err}"
|
||||
send:
|
||||
scan_to_request: "Сканируйте для запроса"
|
||||
scan_to_pay: "Сканируйте для оплаты"
|
||||
tab_scan: "Сканировать"
|
||||
tab_my_code: "Мой код"
|
||||
request_from: "Запросить у"
|
||||
send_to: "Отправить"
|
||||
search_hint: "@handle, npub или имя"
|
||||
suggested: "%{icon} Рекомендуемые"
|
||||
no_contacts: "Пока нет контактов. Найдите кого-то по их @handle."
|
||||
no_profile: "нет профиля"
|
||||
tag_contact: "контакт"
|
||||
tag_on_nostr: "в nostr"
|
||||
searching_nostr: "Поиск в nostr…"
|
||||
unverified_title: "Заплатить непроверенному ключу?"
|
||||
unverified_body: "Для этого ключа не опубликован nostr-профиль — он может быть совсем новым, анонимным или с опечаткой. Дважды проверьте перед отправкой."
|
||||
keep_looking: "Продолжить поиск"
|
||||
pay_anyway: "Всё равно оплатить"
|
||||
scan_not_recipient: "Этот QR — не получатель goblin; ожидался npub или @handle"
|
||||
scan_prompt: "Наведите на код goblin, чтобы активировать"
|
||||
scan_to_pay_me: "Сканируйте, чтобы заплатить мне"
|
||||
share_btn: "%{icon} Поделиться"
|
||||
share_message: "Заплатите мне в Goblin — %{handle}\n%{link}\nnpub: %{npub}"
|
||||
none_found: "Никого не найдено по %{label}"
|
||||
enter_recipient: "Введите @handle, npub или имя"
|
||||
amount_title: "Сумма"
|
||||
to_name: "Кому %{name}"
|
||||
not_enough: "Недостаточно grin"
|
||||
max: "Макс"
|
||||
note_label: "Заметка"
|
||||
note_hint: "Добавить заметку…"
|
||||
review_btn: "Проверить"
|
||||
confirm_request: "Подтвердить запрос"
|
||||
review_title: "Проверка"
|
||||
requesting_from: "Запрос у %{name}"
|
||||
youre_sending: "Вы отправляете %{name}"
|
||||
row_from: "От"
|
||||
row_to: "Кому"
|
||||
row_note: "Заметка"
|
||||
row_they_pay: "Они платят"
|
||||
row_they_pay_val: "Только если они одобрят"
|
||||
row_delivery: "Доставка"
|
||||
row_delivery_val: "Зашифровано NIP-44, через Nym"
|
||||
row_network_fee: "Сетевая комиссия"
|
||||
row_network_fee_val: "Списывается с вашего баланса"
|
||||
row_privacy: "Приватность"
|
||||
row_privacy_val: "Mimblewimble + Nym"
|
||||
send_request_btn: "Отправить запрос"
|
||||
request_approve_hint: "Они получат запрос на одобрение"
|
||||
hold_to_send: "Удерживайте для отправки"
|
||||
lower_amount: "Вернуться и уменьшить сумму"
|
||||
hold_confirm_hint: "Нажмите и удерживайте для подтверждения"
|
||||
requesting: "Запрос…"
|
||||
sending: "Отправка…"
|
||||
they: "Они"
|
||||
request_blocked: "%{who} не принимает запросы. Попросите их отправить вам grin вместо этого."
|
||||
failed_request_title: "Не удалось запросить"
|
||||
failed_send_title: "Не удалось отправить"
|
||||
failed_request_body: "Не удалось доставить запрос. Попросите их отправить вам grin вместо этого."
|
||||
failed_send_body: "Платёж не доставлен. Ваш grin в безопасности — попробуйте снова."
|
||||
try_again_btn: "Попробовать снова"
|
||||
close_btn: "Закрыть"
|
||||
success:
|
||||
requested: "Запрошено"
|
||||
sent: "Отправлено"
|
||||
from: "от"
|
||||
to: "кому"
|
||||
subtitle: "%{dir} %{who} · только что"
|
||||
done_btn: "Готово"
|
||||
receipt_btn: "Квитанция"
|
||||
+394
-1
@@ -355,4 +355,397 @@ keyboard:
|
||||
m: m
|
||||
m1: ','
|
||||
m2: .
|
||||
m3: /
|
||||
m3: /
|
||||
goblin:
|
||||
home:
|
||||
anonymous: "Anonim"
|
||||
connected_nym: "Nym üzerinden bağlı"
|
||||
nym_ready: "Nym hazır · relaylar…"
|
||||
connecting_nym: "Nym'e bağlanılıyor…"
|
||||
cant_reach_node: "Düğüme ulaşılamıyor"
|
||||
node_synced: "Düğüm eşitlendi"
|
||||
syncing: "Eşitleniyor…"
|
||||
block: "Blok %{height}"
|
||||
waiting_for_chain: "Zincir bekleniyor…"
|
||||
nav_wallet: "Cüzdan"
|
||||
nav_pay: "Öde"
|
||||
nav_activity: "Etkinlik"
|
||||
nav_receive: "Al"
|
||||
nav_settings: "Ayarlar"
|
||||
activity: "Etkinlik"
|
||||
empty_title: "Henüz etkinlik yok"
|
||||
empty_sub: "Başlamak için grin gönder ya da al."
|
||||
recent: "Son işlemler"
|
||||
scan_to_pay: "Ödemek için tara"
|
||||
type_amount: "Bir tutar gir"
|
||||
request: "İste"
|
||||
pay: "Öde"
|
||||
enter_amount: "Ödemek ya da istemek için bir tutar gir"
|
||||
activity:
|
||||
canceled: "iptal edildi"
|
||||
pending: "beklemede"
|
||||
earlier: "Daha önce"
|
||||
today: "Bugün"
|
||||
yesterday: "Dün"
|
||||
title: "Etkinlik"
|
||||
requests: "İstekler"
|
||||
empty_title: "Henüz etkinlik yok"
|
||||
empty_sub: "Ödemelerin burada görünecek."
|
||||
pending_header: "Beklemede"
|
||||
receipt:
|
||||
title: "Makbuz"
|
||||
not_found: "İşlem bulunamadı"
|
||||
for_note: "%{note} için"
|
||||
details: "İşlem ayrıntıları"
|
||||
canceled: "İptal edildi"
|
||||
expired: "Süresi doldu"
|
||||
funds_returned: "Para iade edildi"
|
||||
complete: "Tamamlandı"
|
||||
payment_received: "Ödeme alındı"
|
||||
payment_sent: "Ödeme başarıyla gönderildi"
|
||||
pending: "Beklemede"
|
||||
confs: "%{c}/%{r} onay"
|
||||
waiting_to_confirm: "Onay bekleniyor"
|
||||
you: "Sen"
|
||||
to: "Alıcı"
|
||||
from: "Gönderen"
|
||||
nostr: "nostr"
|
||||
fee_none: "Yok"
|
||||
network_fee: "Ağ ücreti"
|
||||
privacy: "Gizlilik"
|
||||
privacy_value: "Mimblewimble + Nym"
|
||||
transaction: "İşlem"
|
||||
cancel_request: "İsteği iptal et"
|
||||
request:
|
||||
title: "%{name} istiyor"
|
||||
approve: "Onayla"
|
||||
decline: "Reddet"
|
||||
receive:
|
||||
title: "Al"
|
||||
requesting: "%{amt}%{tsu} isteniyor — ödeme almak için paylaş"
|
||||
clear_request: "İsteği temizle"
|
||||
share_handle: "Ödeme almak için kullanıcı adını paylaş"
|
||||
copied: "Kopyalandı"
|
||||
copy_nostr_id: "nostr kimliğini kopyala"
|
||||
copy_address: "Adresi kopyala"
|
||||
privacy_note: "Kullanıcı adın herkese açıktır. Ödeme içeriği ağ üzerinde şifreli kalır."
|
||||
profile:
|
||||
title: "Profil"
|
||||
activity: "Etkinlik"
|
||||
no_activity: "Henüz onlarla etkinlik yok."
|
||||
unblock: "Engeli kaldır"
|
||||
block: "Engelle"
|
||||
blocked_blurb: "Engellendi — ödemeleri ve istekleri reddediliyor."
|
||||
block_blurb: "Engellemek, gelen ödeme ve isteklerini düşürür."
|
||||
settings:
|
||||
title: "Ayarlar"
|
||||
connected_nostr: "nostr'a bağlı"
|
||||
connecting_relays: "Relaylara bağlanılıyor…"
|
||||
pic_updated: "Profil fotoğrafı güncellendi"
|
||||
uploading_pic: "Fotoğraf yükleniyor…"
|
||||
claim_first: "Önce bir kullanıcı adı al — fotoğraflar ona bağlıdır"
|
||||
identity: "Kimlik"
|
||||
copy_npub: "npub kopyala (genel)"
|
||||
backup_nsec: "Gizli anahtarı yedekle (nsec)"
|
||||
export_identity: "Kimlik yedeğini dışa aktar (şifreli)"
|
||||
rotate_key: "nostr anahtarını değiştir"
|
||||
import_identity: "Kimlik içe aktar (nsec / yedek)"
|
||||
backup_note: "Cihaz mı değiştiriyorsun? İKİSİNİ de yedekle: tohum kelimeleri (para) ve kimlik yedeği (ad + anahtar)."
|
||||
wallet: "Cüzdan"
|
||||
display_unit: "Görüntüleme birimi"
|
||||
relays: "Relaylar"
|
||||
node: "Düğüm"
|
||||
slatepacks: "Slatepackler"
|
||||
slatepacks_value: "Manuel işlem"
|
||||
lock_wallet: "Cüzdanı kilitle"
|
||||
privacy: "Gizlilik"
|
||||
mixnet_routing: "Mixnet yönlendirme"
|
||||
messages_lookups: "Mesajlar ve aramalar"
|
||||
auto_accept: "Otomatik kabul"
|
||||
pairing: "Eşleştirme"
|
||||
accept_anyone: "Herkes"
|
||||
accept_contacts: "Yalnızca kişiler"
|
||||
accept_ask: "Her zaman sor"
|
||||
requests: "İstekler"
|
||||
incoming_requests: "Gelen istekler"
|
||||
incoming_requests_sub: "Başkalarının senden para istemesine izin ver"
|
||||
appearance: "Görünüm"
|
||||
theme: "Tema"
|
||||
theme_light: "Açık"
|
||||
theme_dark: "Koyu"
|
||||
theme_yellow: "Sarı"
|
||||
archive: "Arşiv"
|
||||
export_archive: "Arşivi dışa aktar"
|
||||
wipe_history: "Ödeme geçmişini sil"
|
||||
about: "Hakkında"
|
||||
goblin: "Goblin"
|
||||
build: "Sürüm %{build}"
|
||||
network: "Ağ"
|
||||
network_value: "MW + Nym mixnet + nostr"
|
||||
third_party: "Üçüncü taraf"
|
||||
grim: "GRIM (üst kaynak cüzdan)"
|
||||
grin_node: "Grin düğümü"
|
||||
sp_intro: "Gelişmiş — GRIM'in yaptığı gibi ham slatepackleri elle değiş tokuş et. Bunu yalnızca bir @username üzerinden ödeme yapamadığında ya da alamadığında kullan."
|
||||
sp_receive_group: "Al ya da tamamla"
|
||||
sp_receive_blurb: "Birinin sana verdiği bir slatepack'i yapıştır. Goblin ödemeyi alır, faturayı öder ya da tamamlayıp zincire gönderir."
|
||||
sp_process: "Slatepack işle"
|
||||
sp_paste_first: "Önce bir slatepack yapıştır."
|
||||
sp_reply_ready: "Yanıt hazır — gönderene geri yolla."
|
||||
sp_finalizing: "Tamamlanıp zincire gönderiliyor…"
|
||||
sp_create_group: "Ödeme oluştur"
|
||||
sp_create_blurb: "Birine vermek için bir slatepack oluştur. Onlar alır, yanıtı geri gönderir, sen de yukarıda tamamlarsın."
|
||||
sp_amount_hint: "Grin cinsinden tutar"
|
||||
sp_addr_hint: "Alıcı adresi (isteğe bağlı)"
|
||||
sp_create: "Slatepack oluştur"
|
||||
sp_ready: "Slatepack hazır — alıcıya ver."
|
||||
sp_amount_gt_zero: "Sıfırdan büyük bir tutar gir."
|
||||
sp_to_send: "Gönderilecek slatepack"
|
||||
sp_copy: "Slatepack kopyala"
|
||||
rotate_line1: "• Tamamen yeni RASTGELE bir anahtar alırsın; eski npub artık almaz. Aralarında türetme zinciri yoktur."
|
||||
rotate_line2: "• Yeni anahtar tohumundan kurtarılamaz — anahtarı değiştirdikten hemen sonra yeni nsec'i yedekle."
|
||||
rotate_line3: "• @username'in BIRAKILIR ve profil fotoğrafın silinir — hemen aynı ya da yeni bir adı al (boştayken başkası da kapabilir)."
|
||||
rotate_line4: "• Eski anahtara hâlâ yoldaki ödemeler KESİNTİYE uğrar — önce bekleyen ödemelerin bitmesini bekle."
|
||||
rotate_line5: "• npub'unu doğrudan kaydeden kişiler seni yeniden bulmalı — yeni npub'unu ya da yeniden aldığın @username'i paylaş."
|
||||
cancel: "İptal"
|
||||
continue: "Devam"
|
||||
final_confirmation: "Son onay"
|
||||
rotate_confirm_blurb: "Bu işlem uygulamadan geri alınamaz. Değiştirmek için RESET yaz ve cüzdan parolanı gir."
|
||||
type_reset: "RESET yaz"
|
||||
wallet_password: "Cüzdan parolası"
|
||||
rotate_key_btn: "Anahtarı değiştir"
|
||||
rotating_key: "Anahtar değiştiriliyor…"
|
||||
key_rotated: "Anahtar değiştirildi"
|
||||
new_npub: "Yeni npub: %{npub}"
|
||||
backup_new_key: "YENİ gizli anahtarı şimdi yedekle — tohumun onu kurtaramaz."
|
||||
copy_new_nsec: "Yeni nsec yedeğini kopyala"
|
||||
done: "Bitti"
|
||||
rotation_failed: "Değiştirme başarısız"
|
||||
close: "Kapat"
|
||||
import_identity_title: "Kimlik içe aktar"
|
||||
import_blurb: "Bu cüzdanın nostr kimliğini değiştirir — çıplak bir nsec ya da dışa aktarılmış kimlik yedeği yapıştır (yedek ayrıca kullanıcı adını ve geçmişini de geri yükler). Hâlâ gerekiyorsa önce mevcut anahtarı yedekle."
|
||||
import_nsec_hint: "nsec1… ya da kimlik yedeği JSON"
|
||||
backup_password_hint: "Yedek parolası (yalnızca başka yerde dışa aktarıldıysa)"
|
||||
import_btn: "İçe aktar"
|
||||
importing: "İçe aktarılıyor…"
|
||||
identity_replaced: "Kimlik değiştirildi"
|
||||
now_using: "Şu an kullanılan: %{npub}"
|
||||
import_failed: "İçe aktarma başarısız"
|
||||
registered: "%{name} kaydedildi"
|
||||
released_msg: "Bırakıldı — ad artık alınabilir"
|
||||
release_confirm: "@%{name} bırakılsın mı?"
|
||||
release_blurb: "Boşaldığı an alınabilir — değiştireceğin yeni anahtar dahil herkes kapabilir. Profil fotoğrafın da onunla silinir. 10 dakika boyunca başka kullanıcı adı kaydedemezsin."
|
||||
releasing: "Bırakılıyor…"
|
||||
keep_it: "Vazgeç"
|
||||
release_it: "Bırak"
|
||||
username: "Kullanıcı adı"
|
||||
username_note: "@you olarak gösterilir. goblin.st'de herkese açık. Ödemeler şifreli kalır."
|
||||
release_username: "Kullanıcı adını bırak"
|
||||
pick_username: "Bir kullanıcı adı seç — isteğe bağlı"
|
||||
working: "Çalışıyor…"
|
||||
claim: "Al"
|
||||
err_just_taken: "O kullanıcı adı az önce alındı"
|
||||
err_cooldown: "Yakın zamanda bir kullanıcı adı bıraktın — 10 dakika içinde yenisini kaydedebilirsin."
|
||||
err_unreachable: "goblin.st'ye ulaşılamadı — bağlantı sorunu. Tekrar dene."
|
||||
err_release: "Bırakılamadı: %{err}"
|
||||
avail_available: "Müsait!"
|
||||
avail_taken: "Alınmış"
|
||||
avail_reserved: "Ayrılmış"
|
||||
avail_invalid: "Adlar 3–30 karakter: a–z, 0–9, _ ya da -"
|
||||
avail_quarantined: "Müsait değil"
|
||||
avail_unknown: "Kontrol edilemedi — bağlantı sorunu. Tekrar dene."
|
||||
privacy:
|
||||
title: "Ağ gizliliği"
|
||||
intro: "Goblin özel trafiğini Nym mixnet üzerinden gönderir — kimin kiminle konuştuğunu gizleyen beş atlamalı bir ağ, böylece bir relay bir ödemeyi sana bağlayamaz."
|
||||
payments: "Ödemeler"
|
||||
payments_blurb: "Slatepack taşıyan her nostr mesajı."
|
||||
usernames: "@usernamelar"
|
||||
usernames_blurb: "goblin.st'ye ve oradan NIP-05 ad aramaları."
|
||||
price_avatars: "Fiyat ve avatarlar"
|
||||
price_avatars_blurb: "Kur önizlemesi ve kişi fotoğrafları."
|
||||
over_mixnet: "Mixnet üzerinden"
|
||||
direct_connection: "Doğrudan bağlantı"
|
||||
grin_node: "Grin düğümü"
|
||||
grin_node_blurb: "Blok eşitleme ve işlemini ağa yayma. Bu, herkes için aynı olan genel zincir verisidir ve kimliğinle ilişkilendirilmez."
|
||||
pairing:
|
||||
title: "Eşleştirme"
|
||||
intro: "Bakiyenin ve tutarların neye göre gösterildiği."
|
||||
pair_with: "Eşleştir"
|
||||
rates_note: "Kurlar yalnızca bir eşleştirme açıkken Nym mixnet üzerinden alınır — kapalıysa cihazından hiçbir kur isteği çıkmaz."
|
||||
relays:
|
||||
title: "Relaylar"
|
||||
intro: "Ödeme mesajları aşağıdaki her relay'e yansıtılır; almak için ulaşılabilir tek bir relay yeterlidir."
|
||||
your_relays: "Relaylarn"
|
||||
add_relay: "Relay ekle"
|
||||
add_relay_btn: "Relay ekle"
|
||||
save_reconnect: "Kaydet ve yeniden bağlan"
|
||||
none: "yok"
|
||||
count: "%{n} relay"
|
||||
node:
|
||||
title: "Düğüm"
|
||||
connection: "Bağlantı"
|
||||
integrated: "Tümleşik düğüm"
|
||||
applies_after: "Cüzdan kilitlenip yeniden açıldıktan sonra geçerli olur."
|
||||
add_external: "Harici düğüm ekle"
|
||||
api_secret_hint: "API gizli anahtarı (isteğe bağlı)"
|
||||
add_node: "Düğüm ekle"
|
||||
integrated_host: "tümleşik düğüm"
|
||||
summary_syncing: "%{conn} · eşitleniyor"
|
||||
summary_block: "Blok %{height} · %{conn}"
|
||||
nips:
|
||||
title: "nostr ve NIPler"
|
||||
intro1: "Goblin nostr konuşur — basit relay sunucuları üzerinden geçen imzalı mesajların açık bir protokolü. Cüzdanın kendi nostr kimliğini taşır: bağımsız rastgele bir anahtar, paran ve tohumundan kasıtlı olarak ayrı tutulur. Her ödeme, slatepack içinde olacak şekilde, kimlikler arasında uçtan uca şifreli bir doğrudan mesaj olarak gider."
|
||||
intro2: "goblin.st, Goblin'in ad servisidir: bir kullanıcı adı almak orada bir ad → anahtar eşlemesi yayımlar (NIP-05), böylece insanlar uzun bir npub yerine @you'ya ödeme yapabilir. Kullanıcı adı herkese açıktır; ödeme içeriği asla değil. NIPler protokolün yapı taşlarıdır — özelliği okumak için birine dokun."
|
||||
n05_title: "Adlar"
|
||||
n05_blurb: "@username@goblin.st'yi anahtarına eşler, böylece kullanıcı adları adres gibi çalışır."
|
||||
n17_title: "Özel mesajlar"
|
||||
n17_blurb: "Her ödemenin içinde gittiği şifreli DM zarfı."
|
||||
n44_title: "Şifreleme"
|
||||
n44_blurb: "Bu mesajların içinde kullanılan kimlik doğrulamalı şifreleme."
|
||||
n49_title: "Anahtar şifreleme"
|
||||
n49_blurb: "Gizli anahtarın parolanla kilitli olarak nasıl depolandığı."
|
||||
n59_title: "Hediye paketi"
|
||||
n59_blurb: "Mesajları sarar, böylece relaylar kimin kiminle konuştuğunu göremez."
|
||||
n98_title: "HTTP kimlik doğrulama"
|
||||
n98_blurb: "goblin.st'ye gönderilen kullanıcı adı kayıt isteğini imzalar."
|
||||
onboarding:
|
||||
intro:
|
||||
private_money_head: "Özel para"
|
||||
private_money_body: "Goblin, grin için bir cüzdan — zincirinde tutar ya da adres bulunmayan dijital nakit."
|
||||
send_like_message_head: "Mesaj gibi gönder"
|
||||
send_like_message_body: "Bir @username ya da npub'a öde, nostr ve Nym mixnet üzerinden uçtan uca şifreli bir mesaj olarak ulaşır — aradaki hiç kimse tutarı ya da kimlerin dahil olduğunu göremez."
|
||||
yours_alone_head: "Yalnızca senin"
|
||||
yours_alone_body: "Anahtarlar, adlar ve geçmiş bu cihazda yaşar. GRIM cüzdanı üzerine kuruludur."
|
||||
get_started: "Başla"
|
||||
footnote: "Yaklaşık bir dakika sürer. Her şeyi sonradan değiştirebilirsin."
|
||||
node:
|
||||
kicker: "ADIM 1 / 3 · AĞ"
|
||||
title: "Goblin zinciri nasıl\nizlesin?"
|
||||
own_title: "Kendi düğümümü çalıştır"
|
||||
own_badge: "Özel"
|
||||
own_body: "Kimseye güvenmez — cüzdanın zinciri kendisi kontrol eder. Sen kurulumu bitirirken arka planda eşitlenir."
|
||||
connect_title: "Bir düğüme bağlan"
|
||||
connect_badge: "Anında"
|
||||
connect_body: "Eşitleme beklemesi yok. Seçtiğin düğüm cüzdanının sorgularını görebilir."
|
||||
changeable: "Ayarlar → Düğüm'den istediğin zaman değiştirilebilir."
|
||||
continue: "Devam"
|
||||
url_invalid: "Düğüm URL'si http:// ya da https:// ile başlamalı"
|
||||
wallet:
|
||||
kicker: "ADIM 2 / 3 · CÜZDAN"
|
||||
title: "Cüzdanını kur"
|
||||
create_new: "Yeni oluştur"
|
||||
restore_from_seed: "Tohumdan geri yükle"
|
||||
name_hint: "Cüzdan adı"
|
||||
password_hint: "Parola"
|
||||
repeat_password_hint: "Parolayı tekrarla"
|
||||
restore_hint: "Tohum kelimelerini hazır tut — onları sonra gireceksin."
|
||||
create_hint: "Sırada yazman için 24 tohum kelimesi var. Onlar paradır — onları elinde tutan paranı elinde tutar."
|
||||
continue: "Devam"
|
||||
passwords_no_match: "Parolalar eşleşmiyor"
|
||||
words:
|
||||
kicker: "ADIM 2 / 3 · CÜZDAN"
|
||||
title_restore: "Tohum kelimelerini gir"
|
||||
title_create: "Bu kelimeleri yaz"
|
||||
write_down_hint: "Kâğıda, sırayla. Bu kelimelere sahip olan paranı alabilir; onlar olmadan kaybolan bir cihaz kaybolan para demektir."
|
||||
paste: "Yapıştır"
|
||||
scan_qr: "QR tara"
|
||||
copy_clipboard: "Panoya kopyala (bundan kaçın)"
|
||||
restore_wallet: "Cüzdanı geri yükle"
|
||||
wrote_them_down: "Onları yazdım"
|
||||
fill_every_word: "Her kelimeyi doldur — düzenlemek için bir kelimeye dokun ya da ifadeyi yapıştır."
|
||||
confirm:
|
||||
kicker: "ADIM 2 / 3 · CÜZDAN"
|
||||
title: "Şimdi kanıtla"
|
||||
enter_hint: "Az önce yazdığın kelimeleri gir. Yazmak için bir kelimeye dokun."
|
||||
paste: "Yapıştır"
|
||||
create_wallet: "Cüzdan oluştur"
|
||||
keep_going: "Devam et — her kelime, sırayla."
|
||||
identity:
|
||||
kicker: "ADIM 3 / 3 · KİMLİK"
|
||||
title: "Ödeme kimliğin"
|
||||
key_being_made: "anahtar oluşturuluyor…"
|
||||
connected_nym: "Nym üzerinden bağlı"
|
||||
connecting_nym: "Nym üzerinden bağlanılıyor…"
|
||||
fresh_key_blurb: "Ödemeler için yapılmış yepyeni bir anahtar — kasıtlı olarak tohumunun parçası değildir, böylece paranıza dokunmadan, gizliliğini korumak için onu istediğin zaman değiştirebilirsin. Ayarlar → Kimlik'ten yedekle."
|
||||
clean_slate_blurb: "Temiz bir sayfa mı istiyorsun? İstediğin zaman yepyeni bir anahtar tak — yeni sen eskisine bağlı değil. Aynı cüzdan, yeni yüz."
|
||||
pick_username: "Bir kullanıcı adı seç — isteğe bağlı"
|
||||
username_blurb: "Arkadaşların uzun bir anahtar yerine @you'ya öder. goblin.st'de herkese açık; ödemeler şifreli kalır. Atla, basitçe anonim olursun — istediğin zaman sonra bir tane alabilirsin."
|
||||
username_field_hint: "adınız"
|
||||
working: "Çalışıyor…"
|
||||
claim_username: "Kullanıcı adı al"
|
||||
available_when_connected: "Mixnet bağlandığında müsait — ya da atla ve sonra al."
|
||||
youre: "Sen @%{name}'sin"
|
||||
open_wallet: "Cüzdanımı aç"
|
||||
skip_for_now: "Şimdilik atla"
|
||||
errors:
|
||||
cant_open: "Cüzdan açılamadı: %{err}"
|
||||
cant_create: "Cüzdan oluşturulamadı: %{err}"
|
||||
send:
|
||||
scan_to_request: "İstemek için tara"
|
||||
scan_to_pay: "Ödemek için tara"
|
||||
tab_scan: "Tara"
|
||||
tab_my_code: "Kodum"
|
||||
request_from: "Şundan iste"
|
||||
send_to: "Şuna gönder"
|
||||
search_hint: "@handle, npub ya da ad"
|
||||
suggested: "%{icon} Önerilen"
|
||||
no_contacts: "Henüz kişi yok. Birini @handle ile bul."
|
||||
no_profile: "profil yok"
|
||||
tag_contact: "kişi"
|
||||
tag_on_nostr: "nostr'da"
|
||||
searching_nostr: "nostr aranıyor…"
|
||||
unverified_title: "Doğrulanmamış bir anahtara ödeme yapılsın mı?"
|
||||
unverified_body: "Bu anahtar için yayımlanmış bir nostr profili yok — yepyeni, anonim ya da yanlış yazılmış olabilir. Göndermeden önce doğru olduğunu iki kez kontrol et."
|
||||
keep_looking: "Aramaya devam et"
|
||||
pay_anyway: "Yine de öde"
|
||||
scan_not_recipient: "O QR bir goblin alıcısı değil — bir npub ya da @handle bekleniyordu"
|
||||
scan_prompt: "Etkinleştirmek için bir goblin kodunu görüntüye getir"
|
||||
scan_to_pay_me: "Bana ödemek için tara"
|
||||
share_btn: "%{icon} Paylaş"
|
||||
share_message: "Goblin'de bana öde — %{handle}\n%{link}\nnpub: %{npub}"
|
||||
none_found: "%{label} için kimse bulunamadı"
|
||||
enter_recipient: "Bir @handle, npub ya da ad gir"
|
||||
amount_title: "Tutar"
|
||||
to_name: "%{name} için"
|
||||
not_enough: "Yeterli grin yok"
|
||||
max: "Maks"
|
||||
note_label: "Not"
|
||||
note_hint: "Bir not ekle…"
|
||||
review_btn: "İncele"
|
||||
confirm_request: "İsteği onayla"
|
||||
review_title: "İncele"
|
||||
requesting_from: "%{name} kişisinden isteniyor"
|
||||
youre_sending: "%{name} kişisine gönderiyorsun"
|
||||
row_from: "Gönderen"
|
||||
row_to: "Alıcı"
|
||||
row_note: "Not"
|
||||
row_they_pay: "Onlar öder"
|
||||
row_they_pay_val: "Yalnızca onaylarlarsa"
|
||||
row_delivery: "Teslimat"
|
||||
row_delivery_val: "NIP-44 şifreli, Nym üzerinden"
|
||||
row_network_fee: "Ağ ücreti"
|
||||
row_network_fee_val: "Bakiyenden düşülür"
|
||||
row_privacy: "Gizlilik"
|
||||
row_privacy_val: "Mimblewimble + Nym"
|
||||
send_request_btn: "İstek gönder"
|
||||
request_approve_hint: "Onaylamaları için bir istek alacaklar"
|
||||
hold_to_send: "Göndermek için basılı tut"
|
||||
lower_amount: "Geri dön ve tutarı düşür"
|
||||
hold_confirm_hint: "Onaylamak için basılı tut"
|
||||
requesting: "İsteniyor…"
|
||||
sending: "Gönderiliyor…"
|
||||
they: "Onlar"
|
||||
request_blocked: "%{who} istek kabul etmiyor. Bunun yerine sana grin göndermesini iste."
|
||||
failed_request_title: "İstenemedi"
|
||||
failed_send_title: "Gönderilemedi"
|
||||
failed_request_body: "İsteği teslim edemedik. Bunun yerine sana grin göndermesini iste."
|
||||
failed_send_body: "Ödeme teslim edilemedi. Grin'in güvende — tekrar dene."
|
||||
try_again_btn: "Tekrar dene"
|
||||
close_btn: "Kapat"
|
||||
success:
|
||||
requested: "İstendi"
|
||||
sent: "Gönderildi"
|
||||
from: "şuradan"
|
||||
to: "şuraya"
|
||||
subtitle: "%{dir} %{who} · az önce"
|
||||
done_btn: "Bitti"
|
||||
receipt_btn: "Makbuz"
|
||||
+394
-1
@@ -355,4 +355,397 @@ keyboard:
|
||||
m: 一
|
||||
m1: ','
|
||||
m2: .
|
||||
m3: /
|
||||
m3: /
|
||||
goblin:
|
||||
home:
|
||||
anonymous: "匿名"
|
||||
connected_nym: "已通过 Nym 连接"
|
||||
nym_ready: "Nym 就绪 · 连接中继…"
|
||||
connecting_nym: "正在连接 Nym…"
|
||||
cant_reach_node: "无法连接节点"
|
||||
node_synced: "节点已同步"
|
||||
syncing: "同步中…"
|
||||
block: "区块 %{height}"
|
||||
waiting_for_chain: "等待链数据…"
|
||||
nav_wallet: "钱包"
|
||||
nav_pay: "支付"
|
||||
nav_activity: "动态"
|
||||
nav_receive: "收款"
|
||||
nav_settings: "设置"
|
||||
activity: "动态"
|
||||
empty_title: "暂无动态"
|
||||
empty_sub: "收发 grin 即可开始。"
|
||||
recent: "最近"
|
||||
scan_to_pay: "扫码支付"
|
||||
type_amount: "输入金额"
|
||||
request: "请求"
|
||||
pay: "支付"
|
||||
enter_amount: "输入要支付或请求的金额"
|
||||
activity:
|
||||
canceled: "已取消"
|
||||
pending: "待处理"
|
||||
earlier: "更早"
|
||||
today: "今天"
|
||||
yesterday: "昨天"
|
||||
title: "动态"
|
||||
requests: "请求"
|
||||
empty_title: "暂无动态"
|
||||
empty_sub: "你的付款将显示在这里。"
|
||||
pending_header: "待处理"
|
||||
receipt:
|
||||
title: "收据"
|
||||
not_found: "未找到交易"
|
||||
for_note: "用于 %{note}"
|
||||
details: "交易详情"
|
||||
canceled: "已取消"
|
||||
expired: "已过期"
|
||||
funds_returned: "资金已退回"
|
||||
complete: "已完成"
|
||||
payment_received: "已收到付款"
|
||||
payment_sent: "付款发送成功"
|
||||
pending: "待处理"
|
||||
confs: "%{c}/%{r} 次确认"
|
||||
waiting_to_confirm: "等待确认"
|
||||
you: "你"
|
||||
to: "收款方"
|
||||
from: "付款方"
|
||||
nostr: "nostr"
|
||||
fee_none: "无"
|
||||
network_fee: "网络费用"
|
||||
privacy: "隐私"
|
||||
privacy_value: "Mimblewimble + Nym"
|
||||
transaction: "交易"
|
||||
cancel_request: "取消请求"
|
||||
request:
|
||||
title: "%{name} 发起请求"
|
||||
approve: "同意"
|
||||
decline: "拒绝"
|
||||
receive:
|
||||
title: "收款"
|
||||
requesting: "正在请求 %{amt}%{tsu} — 分享以收款"
|
||||
clear_request: "清除请求"
|
||||
share_handle: "分享你的用户名以收款"
|
||||
copied: "已复制"
|
||||
copy_nostr_id: "复制 nostr ID"
|
||||
copy_address: "复制地址"
|
||||
privacy_note: "你的用户名是公开的。付款内容在网络中保持加密。"
|
||||
profile:
|
||||
title: "资料"
|
||||
activity: "动态"
|
||||
no_activity: "尚无往来记录。"
|
||||
unblock: "取消屏蔽"
|
||||
block: "屏蔽"
|
||||
blocked_blurb: "已屏蔽 — 其付款和请求会被丢弃。"
|
||||
block_blurb: "屏蔽后将丢弃对方发来的付款和请求。"
|
||||
settings:
|
||||
title: "设置"
|
||||
connected_nostr: "已连接 nostr"
|
||||
connecting_relays: "正在连接中继…"
|
||||
pic_updated: "头像已更新"
|
||||
uploading_pic: "正在上传头像…"
|
||||
claim_first: "请先注册用户名 — 头像依附于它"
|
||||
identity: "身份"
|
||||
copy_npub: "复制 npub(公开)"
|
||||
backup_nsec: "备份私钥(nsec)"
|
||||
export_identity: "导出身份备份(加密)"
|
||||
rotate_key: "轮换 nostr 密钥"
|
||||
import_identity: "导入身份(nsec / 备份)"
|
||||
backup_note: "更换设备?请同时备份:助记词(资金)和身份备份(用户名 + 密钥)。"
|
||||
wallet: "钱包"
|
||||
display_unit: "显示单位"
|
||||
relays: "中继"
|
||||
node: "节点"
|
||||
slatepacks: "Slatepack"
|
||||
slatepacks_value: "手动交易"
|
||||
lock_wallet: "锁定钱包"
|
||||
privacy: "隐私"
|
||||
mixnet_routing: "mixnet 路由"
|
||||
messages_lookups: "消息和查询"
|
||||
auto_accept: "自动接受"
|
||||
pairing: "配对"
|
||||
accept_anyone: "任何人"
|
||||
accept_contacts: "仅联系人"
|
||||
accept_ask: "每次询问"
|
||||
requests: "请求"
|
||||
incoming_requests: "收到的请求"
|
||||
incoming_requests_sub: "允许他人向你请求付款"
|
||||
appearance: "外观"
|
||||
theme: "主题"
|
||||
theme_light: "浅色"
|
||||
theme_dark: "深色"
|
||||
theme_yellow: "黄色"
|
||||
archive: "存档"
|
||||
export_archive: "导出存档"
|
||||
wipe_history: "清除付款记录"
|
||||
about: "关于"
|
||||
goblin: "Goblin"
|
||||
build: "构建 %{build}"
|
||||
network: "网络"
|
||||
network_value: "MW + Nym mixnet + nostr"
|
||||
third_party: "第三方"
|
||||
grim: "GRIM(上游钱包)"
|
||||
grin_node: "Grin 节点"
|
||||
sp_intro: "高级功能 — 像 GRIM 那样手动交换原始 slatepack。仅在无法通过 @username 收付款时使用。"
|
||||
sp_receive_group: "接收或确认"
|
||||
sp_receive_blurb: "粘贴别人给你的 slatepack。Goblin 会接收付款、支付账单,或确认并广播到链上。"
|
||||
sp_process: "处理 slatepack"
|
||||
sp_paste_first: "请先粘贴 slatepack。"
|
||||
sp_reply_ready: "回复已就绪 — 发回给发送方。"
|
||||
sp_finalizing: "正在确认并广播到链上…"
|
||||
sp_create_group: "创建付款"
|
||||
sp_create_blurb: "生成一个 slatepack 交给他人。对方接收后将回复发回,你在上方确认即可。"
|
||||
sp_amount_hint: "金额(grin)"
|
||||
sp_addr_hint: "收款地址(可选)"
|
||||
sp_create: "创建 slatepack"
|
||||
sp_ready: "slatepack 已就绪 — 交给收款方。"
|
||||
sp_amount_gt_zero: "请输入大于零的金额。"
|
||||
sp_to_send: "待发送的 slatepack"
|
||||
sp_copy: "复制 slatepack"
|
||||
rotate_line1: "• 你会得到一个全新的随机密钥;旧 npub 将停止接收。两者之间没有任何派生关系。"
|
||||
rotate_line2: "• 新密钥无法从助记词恢复 — 轮换后请立即备份新的 nsec。"
|
||||
rotate_line3: "• 你的 @username 将被释放,头像也会被删除 — 请立即重新注册同名或新用户名(一旦释放,任何人都可抢注)。"
|
||||
rotate_line4: "• 正在发往旧密钥的付款将受影响 — 请先等待待处理付款完成。"
|
||||
rotate_line5: "• 直接保存了你 npub 的联系人需要重新查找你 — 分享你的新 npub 或重新注册的 @username。"
|
||||
cancel: "取消"
|
||||
continue: "继续"
|
||||
final_confirmation: "最终确认"
|
||||
rotate_confirm_blurb: "此操作在应用内无法撤销。输入 RESET 并输入钱包密码以进行轮换。"
|
||||
type_reset: "输入 RESET"
|
||||
wallet_password: "钱包密码"
|
||||
rotate_key_btn: "轮换密钥"
|
||||
rotating_key: "正在轮换密钥…"
|
||||
key_rotated: "密钥已轮换"
|
||||
new_npub: "新 npub:%{npub}"
|
||||
backup_new_key: "立即备份新私钥 — 助记词无法恢复它。"
|
||||
copy_new_nsec: "复制新 nsec 备份"
|
||||
done: "完成"
|
||||
rotation_failed: "轮换失败"
|
||||
close: "关闭"
|
||||
import_identity_title: "导入身份"
|
||||
import_blurb: "将替换此钱包的 nostr 身份 — 粘贴纯 nsec 或导出的身份备份(备份还会恢复你的用户名和历史记录)。若仍需要当前密钥,请先备份。"
|
||||
import_nsec_hint: "nsec1… 或身份备份 JSON"
|
||||
backup_password_hint: "备份密码(仅当在他处导出时需要)"
|
||||
import_btn: "导入"
|
||||
importing: "正在导入…"
|
||||
identity_replaced: "身份已替换"
|
||||
now_using: "当前使用:%{npub}"
|
||||
import_failed: "导入失败"
|
||||
registered: "已注册 %{name}"
|
||||
released_msg: "已释放 — 用户名可被抢注"
|
||||
release_confirm: "释放 @%{name}?"
|
||||
release_blurb: "释放后立即可被抢注 — 任何人都能注册,包括你下次轮换到的密钥。头像也会一并删除。10 分钟内你无法注册新用户名。"
|
||||
releasing: "正在释放…"
|
||||
keep_it: "保留"
|
||||
release_it: "释放"
|
||||
username: "用户名"
|
||||
username_note: "显示为 @you。在 goblin.st 上公开。付款保持加密。"
|
||||
release_username: "释放用户名"
|
||||
pick_username: "选择用户名 — 可选"
|
||||
working: "处理中…"
|
||||
claim: "注册"
|
||||
err_just_taken: "该用户名刚被占用"
|
||||
err_cooldown: "你刚释放了一个用户名 — 10 分钟内无法注册新用户名。"
|
||||
err_unreachable: "无法连接 goblin.st — 连接中断。请重试。"
|
||||
err_release: "无法释放:%{err}"
|
||||
avail_available: "可用!"
|
||||
avail_taken: "已被占用"
|
||||
avail_reserved: "已保留"
|
||||
avail_invalid: "用户名为 3–30 个字符:a–z、0–9、_ 或 -"
|
||||
avail_quarantined: "不可用"
|
||||
avail_unknown: "无法检查 — 连接中断。请重试。"
|
||||
privacy:
|
||||
title: "网络隐私"
|
||||
intro: "Goblin 通过 Nym mixnet 发送其私密流量 — 这是一个五跳网络,可隐藏通信双方的身份,使中继无法将付款关联到你。"
|
||||
payments: "付款"
|
||||
payments_blurb: "每条携带 slatepack 的 nostr 消息。"
|
||||
usernames: "@用户名"
|
||||
usernames_blurb: "往返 goblin.st 的 NIP-05 名称查询。"
|
||||
price_avatars: "汇率和头像"
|
||||
price_avatars_blurb: "汇率预览和联系人头像。"
|
||||
over_mixnet: "经由 mixnet"
|
||||
direct_connection: "直接连接"
|
||||
grin_node: "Grin 节点"
|
||||
grin_node_blurb: "区块同步及向网络广播你的交易。这是公开的链上数据,对所有人都一样,且不与你的身份关联。"
|
||||
pairing:
|
||||
title: "配对"
|
||||
intro: "你的余额和金额以何种货币显示。"
|
||||
pair_with: "配对货币"
|
||||
rates_note: "汇率仅在开启配对时通过 Nym mixnet 获取 — 关闭后不会有任何汇率请求离开你的设备。"
|
||||
relays:
|
||||
title: "中继"
|
||||
intro: "付款消息会镜像到下方每个中继;只要有一个可达的中继即可收款。"
|
||||
your_relays: "你的中继"
|
||||
add_relay: "添加中继"
|
||||
add_relay_btn: "添加中继"
|
||||
save_reconnect: "保存并重新连接"
|
||||
none: "无"
|
||||
count: "%{n} 个中继"
|
||||
node:
|
||||
title: "节点"
|
||||
connection: "连接"
|
||||
integrated: "集成节点"
|
||||
applies_after: "在钱包锁定并再次解锁后生效。"
|
||||
add_external: "添加外部节点"
|
||||
api_secret_hint: "API 密钥(可选)"
|
||||
add_node: "添加节点"
|
||||
integrated_host: "集成节点"
|
||||
summary_syncing: "%{conn} · 同步中"
|
||||
summary_block: "区块 %{height} · %{conn}"
|
||||
nips:
|
||||
title: "nostr 与 NIPs"
|
||||
intro1: "Goblin 使用 nostr — 一种通过简单中继服务器传递签名消息的开放协议。你的钱包拥有自己的 nostr 身份:一个独立的随机密钥,刻意与你的资金和助记词保持独立。每笔付款都作为身份之间的端到端加密私信传输,slatepack 就包含在其中。"
|
||||
intro2: "goblin.st 是 Goblin 的名称服务:注册用户名会在此发布名称 → 密钥的映射(NIP-05),让人们可以付款给 @you 而不必使用冗长的 npub。用户名是公开的;付款内容则永不公开。NIPs 是该协议的构建模块 — 点击任意一项可阅读规范。"
|
||||
n05_title: "名称"
|
||||
n05_blurb: "将 @username@goblin.st 映射到你的密钥,让用户名像地址一样使用。"
|
||||
n17_title: "私密消息"
|
||||
n17_blurb: "每笔付款传输所用的加密私信信封。"
|
||||
n44_title: "加密"
|
||||
n44_blurb: "这些消息内部使用的认证加密算法。"
|
||||
n49_title: "密钥加密"
|
||||
n49_blurb: "私钥静态存储的方式,由你的密码锁定。"
|
||||
n59_title: "礼物包装"
|
||||
n59_blurb: "包装消息,使中继无法看到通信双方是谁。"
|
||||
n98_title: "HTTP 认证"
|
||||
n98_blurb: "为向 goblin.st 注册用户名的请求签名。"
|
||||
onboarding:
|
||||
intro:
|
||||
private_money_head: "私密货币"
|
||||
private_money_body: "Goblin 是一个 grin 钱包 — 链上无金额、无地址的数字现金。"
|
||||
send_like_message_head: "像发消息一样付款"
|
||||
send_like_message_body: "向 @username 或 npub 付款,款项会作为端到端加密消息通过 nostr 和 Nym mixnet 送达 — 中间任何人都看不到金额或参与者。"
|
||||
yours_alone_head: "完全属于你"
|
||||
yours_alone_body: "密钥、用户名和历史记录都存于本设备。基于 GRIM 钱包构建。"
|
||||
get_started: "开始使用"
|
||||
footnote: "约需一分钟。之后一切均可更改。"
|
||||
node:
|
||||
kicker: "步骤 1 / 3 · 网络"
|
||||
title: "Goblin 该如何\n监视链?"
|
||||
own_title: "运行我自己的节点"
|
||||
own_badge: "私密"
|
||||
own_body: "无需信任任何人 — 钱包自行验证链。完成设置时在后台同步。"
|
||||
connect_title: "连接到节点"
|
||||
connect_badge: "即时"
|
||||
connect_body: "无需等待同步。你选择的节点可看到钱包的查询。"
|
||||
changeable: "随时可在 设置 → 节点 中更改。"
|
||||
continue: "继续"
|
||||
url_invalid: "节点 URL 必须以 http:// 或 https:// 开头"
|
||||
wallet:
|
||||
kicker: "步骤 2 / 3 · 钱包"
|
||||
title: "设置你的钱包"
|
||||
create_new: "新建"
|
||||
restore_from_seed: "从助记词恢复"
|
||||
name_hint: "钱包名称"
|
||||
password_hint: "密码"
|
||||
repeat_password_hint: "重复密码"
|
||||
restore_hint: "准备好你的助记词 — 下一步将输入。"
|
||||
create_hint: "接下来你会得到 24 个助记词以供抄写。它们就是钱 — 谁持有它们,谁就掌握你的资金。"
|
||||
continue: "继续"
|
||||
passwords_no_match: "密码不一致"
|
||||
words:
|
||||
kicker: "步骤 2 / 3 · 钱包"
|
||||
title_restore: "输入你的助记词"
|
||||
title_create: "抄下这些词"
|
||||
write_down_hint: "按顺序写在纸上。任何持有这些词的人都能取走你的资金;丢失这些词又丢失设备,资金将无法找回。"
|
||||
paste: "粘贴"
|
||||
scan_qr: "扫描二维码"
|
||||
copy_clipboard: "复制到剪贴板(不建议)"
|
||||
restore_wallet: "恢复钱包"
|
||||
wrote_them_down: "我已抄好"
|
||||
fill_every_word: "填写每个词 — 点击某个词进行编辑,或粘贴整个短语。"
|
||||
confirm:
|
||||
kicker: "步骤 2 / 3 · 钱包"
|
||||
title: "现在来验证"
|
||||
enter_hint: "输入你刚抄下的词。点击某个词进行输入。"
|
||||
paste: "粘贴"
|
||||
create_wallet: "创建钱包"
|
||||
keep_going: "继续 — 每个词,按顺序。"
|
||||
identity:
|
||||
kicker: "步骤 3 / 3 · 身份"
|
||||
title: "你的付款身份"
|
||||
key_being_made: "正在生成密钥…"
|
||||
connected_nym: "已通过 Nym 连接"
|
||||
connecting_nym: "正在通过 Nym 连接…"
|
||||
fresh_key_blurb: "一个为付款生成的全新密钥 — 刻意不属于你的助记词,因此你可随时轮换以保护隐私,而不会触及你的资金。请在 设置 → 身份 中备份它。"
|
||||
clean_slate_blurb: "想要全新开始?随时换上一个全新密钥 — 新的你与旧的毫无关联。同一个钱包,焕然一新。"
|
||||
pick_username: "选择用户名 — 可选"
|
||||
username_blurb: "朋友付款给 @you 而非冗长的密钥。在 goblin.st 上公开;付款保持加密。跳过则你只是匿名 — 之后随时可注册。"
|
||||
username_field_hint: "你的用户名"
|
||||
working: "处理中…"
|
||||
claim_username: "注册用户名"
|
||||
available_when_connected: "mixnet 连接后可用 — 或跳过,稍后注册。"
|
||||
youre: "你是 @%{name}"
|
||||
open_wallet: "打开我的钱包"
|
||||
skip_for_now: "暂时跳过"
|
||||
errors:
|
||||
cant_open: "无法打开钱包:%{err}"
|
||||
cant_create: "无法创建钱包:%{err}"
|
||||
send:
|
||||
scan_to_request: "扫码请求"
|
||||
scan_to_pay: "扫码支付"
|
||||
tab_scan: "扫描"
|
||||
tab_my_code: "我的二维码"
|
||||
request_from: "向谁请求"
|
||||
send_to: "发送给"
|
||||
search_hint: "@handle、npub 或名称"
|
||||
suggested: "%{icon} 建议"
|
||||
no_contacts: "暂无联系人。通过 @handle 查找某人。"
|
||||
no_profile: "无资料"
|
||||
tag_contact: "联系人"
|
||||
tag_on_nostr: "在 nostr 上"
|
||||
searching_nostr: "正在搜索 nostr…"
|
||||
unverified_title: "向未验证的密钥付款?"
|
||||
unverified_body: "此密钥未发布 nostr 资料 — 它可能是全新的、匿名的或输错的。发送前请仔细核对是否正确。"
|
||||
keep_looking: "继续查找"
|
||||
pay_anyway: "仍然付款"
|
||||
scan_not_recipient: "该二维码不是 goblin 收款方 — 应为 npub 或 @handle"
|
||||
scan_prompt: "将 goblin 二维码对准取景框以激活"
|
||||
scan_to_pay_me: "扫码向我付款"
|
||||
share_btn: "%{icon} 分享"
|
||||
share_message: "在 Goblin 上向我付款 — %{handle}\n%{link}\nnpub:%{npub}"
|
||||
none_found: "未找到与 %{label} 匹配的人"
|
||||
enter_recipient: "输入 @handle、npub 或名称"
|
||||
amount_title: "金额"
|
||||
to_name: "发送给 %{name}"
|
||||
not_enough: "你的 grin 余额不足"
|
||||
max: "最大"
|
||||
note_label: "备注"
|
||||
note_hint: "添加备注…"
|
||||
review_btn: "查看"
|
||||
confirm_request: "确认请求"
|
||||
review_title: "查看"
|
||||
requesting_from: "向 %{name} 请求"
|
||||
youre_sending: "你正在发送给 %{name}"
|
||||
row_from: "付款方"
|
||||
row_to: "收款方"
|
||||
row_note: "备注"
|
||||
row_they_pay: "对方支付"
|
||||
row_they_pay_val: "仅当对方同意时"
|
||||
row_delivery: "传输"
|
||||
row_delivery_val: "NIP-44 加密,经由 Nym"
|
||||
row_network_fee: "网络费用"
|
||||
row_network_fee_val: "从你的余额中扣除"
|
||||
row_privacy: "隐私"
|
||||
row_privacy_val: "Mimblewimble + Nym"
|
||||
send_request_btn: "发送请求"
|
||||
request_approve_hint: "对方将收到一条待同意的请求"
|
||||
hold_to_send: "长按发送"
|
||||
lower_amount: "返回并降低金额"
|
||||
hold_confirm_hint: "按住以确认"
|
||||
requesting: "正在请求…"
|
||||
sending: "正在发送…"
|
||||
they: "对方"
|
||||
request_blocked: "%{who} 不接受请求。请对方改为向你发送 grin。"
|
||||
failed_request_title: "请求失败"
|
||||
failed_send_title: "发送失败"
|
||||
failed_request_body: "我们无法送达请求。请对方改为向你发送 grin。"
|
||||
failed_send_body: "付款未送达。你的 grin 是安全的 — 请重试。"
|
||||
try_again_btn: "重试"
|
||||
close_btn: "关闭"
|
||||
success:
|
||||
requested: "已请求"
|
||||
sent: "已发送"
|
||||
from: "来自"
|
||||
to: "发往"
|
||||
subtitle: "%{dir} %{who} · 刚刚"
|
||||
done_btn: "完成"
|
||||
receipt_btn: "收据"
|
||||
+564
-345
File diff suppressed because it is too large
Load Diff
+119
-101
@@ -185,22 +185,18 @@ impl OnboardingContent {
|
||||
);
|
||||
});
|
||||
ui.add_space(26.0);
|
||||
let lines: [(&str, &str); 3] = [
|
||||
let lines: [(String, String); 3] = [
|
||||
(
|
||||
"Private money",
|
||||
"Goblin is a wallet for grin — digital cash with no amounts \
|
||||
or addresses on its chain.",
|
||||
t!("goblin.onboarding.intro.private_money_head").to_string(),
|
||||
t!("goblin.onboarding.intro.private_money_body").to_string(),
|
||||
),
|
||||
(
|
||||
"Send like a message",
|
||||
"Pay a @username or npub and it arrives as an end-to-end \
|
||||
encrypted message over nostr and the Nym mixnet — no one in \
|
||||
between can see the amount or who's involved.",
|
||||
t!("goblin.onboarding.intro.send_like_message_head").to_string(),
|
||||
t!("goblin.onboarding.intro.send_like_message_body").to_string(),
|
||||
),
|
||||
(
|
||||
"Yours alone",
|
||||
"Keys, names and history live on this device. Built on the \
|
||||
GRIM wallet.",
|
||||
t!("goblin.onboarding.intro.yours_alone_head").to_string(),
|
||||
t!("goblin.onboarding.intro.yours_alone_body").to_string(),
|
||||
),
|
||||
];
|
||||
for (head, body) in lines {
|
||||
@@ -221,13 +217,13 @@ impl OnboardingContent {
|
||||
ui.add_space(10.0);
|
||||
}
|
||||
ui.add_space(16.0);
|
||||
if w::big_action(ui, "Get started", false).clicked() {
|
||||
if w::big_action(ui, &t!("goblin.onboarding.intro.get_started"), false).clicked() {
|
||||
self.step = Step::Node;
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
RichText::new("Takes about a minute. You can change everything later.")
|
||||
RichText::new(t!("goblin.onboarding.intro.footnote"))
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.text_mute),
|
||||
);
|
||||
@@ -293,17 +289,16 @@ impl OnboardingContent {
|
||||
let t = theme::tokens();
|
||||
self.step_header(
|
||||
ui,
|
||||
"STEP 1 OF 3 · NETWORK",
|
||||
"How should Goblin\nwatch the chain?",
|
||||
&t!("goblin.onboarding.node.kicker"),
|
||||
&t!("goblin.onboarding.node.title"),
|
||||
Step::Intro,
|
||||
);
|
||||
if Self::node_card(
|
||||
ui,
|
||||
self.integrated,
|
||||
"Run my own node",
|
||||
"Private",
|
||||
"Trusts no one — your wallet checks the chain itself. Syncs in \
|
||||
the background while you finish setup.",
|
||||
&t!("goblin.onboarding.node.own_title"),
|
||||
&t!("goblin.onboarding.node.own_badge"),
|
||||
&t!("goblin.onboarding.node.own_body"),
|
||||
) {
|
||||
self.integrated = true;
|
||||
}
|
||||
@@ -311,9 +306,9 @@ impl OnboardingContent {
|
||||
if Self::node_card(
|
||||
ui,
|
||||
!self.integrated,
|
||||
"Connect to a node",
|
||||
"Instant",
|
||||
"No sync wait. The node you pick can see your wallet's queries.",
|
||||
&t!("goblin.onboarding.node.connect_title"),
|
||||
&t!("goblin.onboarding.node.connect_badge"),
|
||||
&t!("goblin.onboarding.node.connect_body"),
|
||||
) {
|
||||
self.integrated = false;
|
||||
}
|
||||
@@ -330,7 +325,7 @@ impl OnboardingContent {
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
RichText::new("Changeable any time in Settings → Node.")
|
||||
RichText::new(t!("goblin.onboarding.node.changeable"))
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.text_mute),
|
||||
);
|
||||
@@ -338,13 +333,13 @@ impl OnboardingContent {
|
||||
let url_ok = self.integrated
|
||||
|| self.ext_url.trim().starts_with("http://")
|
||||
|| self.ext_url.trim().starts_with("https://");
|
||||
if w::big_action(ui, "Continue", false).clicked() && url_ok {
|
||||
if w::big_action(ui, &t!("goblin.onboarding.node.continue"), false).clicked() && url_ok {
|
||||
self.step = Step::WalletSetup;
|
||||
}
|
||||
if !url_ok {
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
RichText::new("Node URL must start with http:// or https://")
|
||||
RichText::new(t!("goblin.onboarding.node.url_invalid"))
|
||||
.font(FontId::new(13.0, fonts::regular()))
|
||||
.color(t.neg),
|
||||
);
|
||||
@@ -355,12 +350,20 @@ impl OnboardingContent {
|
||||
|
||||
fn wallet_setup_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
let t = theme::tokens();
|
||||
self.step_header(ui, "STEP 2 OF 3 · WALLET", "Set up your wallet", Step::Node);
|
||||
self.step_header(
|
||||
ui,
|
||||
&t!("goblin.onboarding.wallet.kicker"),
|
||||
&t!("goblin.onboarding.wallet.title"),
|
||||
Step::Node,
|
||||
);
|
||||
|
||||
// Create / Restore segmented choice.
|
||||
ui.horizontal(|ui| {
|
||||
let half = (ui.available_width() - 10.0) / 2.0;
|
||||
for (restore, label) in [(false, "Create new"), (true, "Restore from seed")] {
|
||||
for (restore, label) in [
|
||||
(false, t!("goblin.onboarding.wallet.create_new")),
|
||||
(true, t!("goblin.onboarding.wallet.restore_from_seed")),
|
||||
] {
|
||||
ui.scope_builder(
|
||||
egui::UiBuilder::new().max_rect(egui::Rect::from_min_size(
|
||||
ui.cursor().min,
|
||||
@@ -368,7 +371,7 @@ impl OnboardingContent {
|
||||
)),
|
||||
|ui| {
|
||||
let active = self.restore == restore;
|
||||
let resp = w::chip(ui, label, active);
|
||||
let resp = w::chip(ui, &label, active);
|
||||
if resp.clicked() {
|
||||
self.restore = restore;
|
||||
}
|
||||
@@ -382,7 +385,7 @@ impl OnboardingContent {
|
||||
w::field_well(ui, |ui| {
|
||||
TextEdit::new(egui::Id::from("onb_name"))
|
||||
.focus(false)
|
||||
.hint_text("Wallet name")
|
||||
.hint_text(t!("goblin.onboarding.wallet.name_hint"))
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
.ui(ui, &mut self.name, cb);
|
||||
@@ -391,7 +394,7 @@ impl OnboardingContent {
|
||||
w::field_well(ui, |ui| {
|
||||
TextEdit::new(egui::Id::from("onb_pass"))
|
||||
.focus(false)
|
||||
.hint_text("Password")
|
||||
.hint_text(t!("goblin.onboarding.wallet.password_hint"))
|
||||
.password()
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
@@ -401,7 +404,7 @@ impl OnboardingContent {
|
||||
w::field_well(ui, |ui| {
|
||||
TextEdit::new(egui::Id::from("onb_pass2"))
|
||||
.focus(false)
|
||||
.hint_text("Repeat password")
|
||||
.hint_text(t!("goblin.onboarding.wallet.repeat_password_hint"))
|
||||
.password()
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
@@ -410,10 +413,9 @@ impl OnboardingContent {
|
||||
ui.add_space(10.0);
|
||||
ui.label(
|
||||
RichText::new(if self.restore {
|
||||
"Have your seed words ready — you'll enter them next."
|
||||
t!("goblin.onboarding.wallet.restore_hint")
|
||||
} else {
|
||||
"Next you'll get 24 seed words to write down. They are the \
|
||||
money — anyone holding them holds your funds."
|
||||
t!("goblin.onboarding.wallet.create_hint")
|
||||
})
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.text_mute),
|
||||
@@ -422,7 +424,10 @@ impl OnboardingContent {
|
||||
|
||||
let pass_ok = !self.pass.is_empty() && self.pass == self.pass2;
|
||||
let name_ok = !self.name.trim().is_empty();
|
||||
if w::big_action(ui, "Continue", false).clicked() && pass_ok && name_ok {
|
||||
if w::big_action(ui, &t!("goblin.onboarding.wallet.continue"), false).clicked()
|
||||
&& pass_ok
|
||||
&& name_ok
|
||||
{
|
||||
self.mnemonic_setup.reset();
|
||||
self.mnemonic_setup.mnemonic.set_mode(if self.restore {
|
||||
PhraseMode::Import
|
||||
@@ -436,7 +441,7 @@ impl OnboardingContent {
|
||||
if !self.pass.is_empty() && self.pass != self.pass2 {
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
RichText::new("Passwords don't match")
|
||||
RichText::new(t!("goblin.onboarding.wallet.passwords_no_match"))
|
||||
.font(FontId::new(13.0, fonts::regular()))
|
||||
.color(t.neg),
|
||||
);
|
||||
@@ -453,14 +458,15 @@ impl OnboardingContent {
|
||||
) {
|
||||
let t = theme::tokens();
|
||||
let restore = self.mnemonic_setup.mnemonic.mode() == PhraseMode::Import;
|
||||
let words_title = if restore {
|
||||
t!("goblin.onboarding.words.title_restore")
|
||||
} else {
|
||||
t!("goblin.onboarding.words.title_create")
|
||||
};
|
||||
self.step_header(
|
||||
ui,
|
||||
"STEP 2 OF 3 · WALLET",
|
||||
if restore {
|
||||
"Enter your seed words"
|
||||
} else {
|
||||
"Write these words down"
|
||||
},
|
||||
&t!("goblin.onboarding.words.kicker"),
|
||||
&words_title,
|
||||
Step::WalletSetup,
|
||||
);
|
||||
if restore {
|
||||
@@ -478,12 +484,9 @@ impl OnboardingContent {
|
||||
ui.add_space(10.0);
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new(
|
||||
"On paper, in order. Anyone with these words can take \
|
||||
your funds; without them a lost device means lost funds.",
|
||||
)
|
||||
.font(FontId::new(13.0, fonts::regular()))
|
||||
.color(t.text_dim),
|
||||
RichText::new(t!("goblin.onboarding.words.write_down_hint"))
|
||||
.font(FontId::new(13.0, fonts::regular()))
|
||||
.color(t.text_dim),
|
||||
);
|
||||
ui.add_space(10.0);
|
||||
}
|
||||
@@ -501,7 +504,7 @@ impl OnboardingContent {
|
||||
Vec2::new(half, 44.0),
|
||||
)),
|
||||
|ui| {
|
||||
if w::chip(ui, "Paste", false).clicked() {
|
||||
if w::chip(ui, &t!("goblin.onboarding.words.paste"), false).clicked() {
|
||||
let data = ZeroingString::from(cb.get_string_from_buffer());
|
||||
self.mnemonic_setup.mnemonic.import(&data);
|
||||
}
|
||||
@@ -514,7 +517,7 @@ impl OnboardingContent {
|
||||
Vec2::new(half, 44.0),
|
||||
)),
|
||||
|ui| {
|
||||
if w::chip(ui, "Scan QR", false).clicked() {
|
||||
if w::chip(ui, &t!("goblin.onboarding.words.scan_qr"), false).clicked() {
|
||||
self.scan_modal = Some(CameraScanContent::default());
|
||||
Modal::new(OB_PHRASE_SCAN_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
@@ -527,7 +530,7 @@ impl OnboardingContent {
|
||||
);
|
||||
});
|
||||
ui.add_space(14.0);
|
||||
} else if w::chip(ui, "Copy to clipboard (avoid this)", false).clicked() {
|
||||
} else if w::chip(ui, &t!("goblin.onboarding.words.copy_clipboard"), false).clicked() {
|
||||
cb.copy_string_to_buffer(self.mnemonic_setup.mnemonic.get_phrase());
|
||||
}
|
||||
if !restore {
|
||||
@@ -540,12 +543,12 @@ impl OnboardingContent {
|
||||
true
|
||||
};
|
||||
let label = if restore {
|
||||
"Restore wallet"
|
||||
t!("goblin.onboarding.words.restore_wallet")
|
||||
} else {
|
||||
"I wrote them down"
|
||||
t!("goblin.onboarding.words.wrote_them_down")
|
||||
};
|
||||
if ready {
|
||||
if w::big_action(ui, label, false).clicked() {
|
||||
if w::big_action(ui, &label, false).clicked() {
|
||||
if restore {
|
||||
self.create_wallet(wallets);
|
||||
} else {
|
||||
@@ -554,7 +557,7 @@ impl OnboardingContent {
|
||||
}
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new("Fill every word — tap a word to edit it, or paste the phrase.")
|
||||
RichText::new(t!("goblin.onboarding.words.fill_every_word"))
|
||||
.font(FontId::new(13.0, fonts::regular()))
|
||||
.color(t.text_mute),
|
||||
);
|
||||
@@ -569,27 +572,32 @@ impl OnboardingContent {
|
||||
cb: &dyn PlatformCallbacks,
|
||||
) {
|
||||
let t = theme::tokens();
|
||||
self.step_header(ui, "STEP 2 OF 3 · WALLET", "Now prove it", Step::Words);
|
||||
self.step_header(
|
||||
ui,
|
||||
&t!("goblin.onboarding.confirm.kicker"),
|
||||
&t!("goblin.onboarding.confirm.title"),
|
||||
Step::Words,
|
||||
);
|
||||
ui.label(
|
||||
RichText::new("Enter the words you just wrote down. Tap a word to type it.")
|
||||
RichText::new(t!("goblin.onboarding.confirm.enter_hint"))
|
||||
.font(FontId::new(13.0, fonts::regular()))
|
||||
.color(t.text_dim),
|
||||
);
|
||||
ui.add_space(10.0);
|
||||
self.mnemonic_setup.word_list_ui(ui, true);
|
||||
ui.add_space(14.0);
|
||||
if w::chip(ui, "Paste", false).clicked() {
|
||||
if w::chip(ui, &t!("goblin.onboarding.confirm.paste"), false).clicked() {
|
||||
let data = ZeroingString::from(cb.get_string_from_buffer());
|
||||
self.mnemonic_setup.mnemonic.import(&data);
|
||||
}
|
||||
ui.add_space(14.0);
|
||||
if !self.mnemonic_setup.mnemonic.has_empty_or_invalid() {
|
||||
if w::big_action(ui, "Create wallet", false).clicked() {
|
||||
if w::big_action(ui, &t!("goblin.onboarding.confirm.create_wallet"), false).clicked() {
|
||||
self.create_wallet(wallets);
|
||||
}
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new("Keep going — every word, in order.")
|
||||
RichText::new(t!("goblin.onboarding.confirm.keep_going"))
|
||||
.font(FontId::new(13.0, fonts::regular()))
|
||||
.color(t.text_mute),
|
||||
);
|
||||
@@ -650,10 +658,20 @@ impl OnboardingContent {
|
||||
self.error = None;
|
||||
self.step = Step::Identity;
|
||||
}
|
||||
Err(e) => self.error = Some(format!("Couldn't open the wallet: {:?}", e)),
|
||||
Err(e) => {
|
||||
self.error = Some(
|
||||
t!("goblin.onboarding.errors.cant_open", err => format!("{:?}", e))
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => self.error = Some(format!("Couldn't create the wallet: {:?}", e)),
|
||||
Err(e) => {
|
||||
self.error = Some(
|
||||
t!("goblin.onboarding.errors.cant_create", err => format!("{:?}", e))
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -663,13 +681,13 @@ impl OnboardingContent {
|
||||
let t = theme::tokens();
|
||||
// No back from here: the wallet exists now.
|
||||
ui.label(
|
||||
RichText::new("STEP 3 OF 3 · IDENTITY")
|
||||
RichText::new(t!("goblin.onboarding.identity.kicker"))
|
||||
.font(fonts::kicker())
|
||||
.color(t.text_mute),
|
||||
);
|
||||
ui.add_space(18.0);
|
||||
ui.label(
|
||||
RichText::new("Your payment identity")
|
||||
RichText::new(t!("goblin.onboarding.identity.title"))
|
||||
.font(FontId::new(26.0, fonts::bold()))
|
||||
.color(t.text),
|
||||
);
|
||||
@@ -698,7 +716,7 @@ impl OnboardingContent {
|
||||
let short = if npub.len() > 20 {
|
||||
format!("{}…{}", &npub[..12], &npub[npub.len() - 6..])
|
||||
} else if npub.is_empty() {
|
||||
"key being made…".to_string()
|
||||
t!("goblin.onboarding.identity.key_being_made").to_string()
|
||||
} else {
|
||||
npub.clone()
|
||||
};
|
||||
@@ -709,9 +727,9 @@ impl OnboardingContent {
|
||||
);
|
||||
ui.label(
|
||||
RichText::new(if connected {
|
||||
"connected over Nym"
|
||||
t!("goblin.onboarding.identity.connected_nym")
|
||||
} else {
|
||||
"connecting over Nym…"
|
||||
t!("goblin.onboarding.identity.connecting_nym")
|
||||
})
|
||||
.font(FontId::new(12.0, fonts::regular()))
|
||||
.color(t.surface_text_mute),
|
||||
@@ -720,24 +738,15 @@ impl OnboardingContent {
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
RichText::new(
|
||||
"A fresh key, made for payments — deliberately not part \
|
||||
of your seed, so you can rotate it anytime to maintain \
|
||||
your privacy, without ever touching your funds. Back it \
|
||||
up in Settings → Identity.",
|
||||
)
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.surface_text_dim),
|
||||
RichText::new(t!("goblin.onboarding.identity.fresh_key_blurb"))
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.surface_text_dim),
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
ui.label(
|
||||
RichText::new(
|
||||
"Want a clean slate? Swap in a brand-new key any time — \
|
||||
the new you isn't linked to the old one. Same wallet, \
|
||||
fresh face.",
|
||||
)
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.surface_text_dim),
|
||||
RichText::new(t!("goblin.onboarding.identity.clean_slate_blurb"))
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.surface_text_dim),
|
||||
);
|
||||
});
|
||||
ui.add_space(14.0);
|
||||
@@ -752,8 +761,13 @@ impl OnboardingContent {
|
||||
self.claim.message = Some(msg.to_string());
|
||||
}
|
||||
ClaimMsg::Registered(nip05) => {
|
||||
self.claim.message =
|
||||
Some(format!("You're @{}", nip05.split('@').next().unwrap_or("")));
|
||||
self.claim.message = Some(
|
||||
t!(
|
||||
"goblin.onboarding.identity.youre",
|
||||
name => nip05.split('@').next().unwrap_or("")
|
||||
)
|
||||
.to_string(),
|
||||
);
|
||||
self.claim.available = Some(true);
|
||||
if let Some(s) = wallet.nostr_service() {
|
||||
{
|
||||
@@ -779,19 +793,15 @@ impl OnboardingContent {
|
||||
w::card(ui, |ui| {
|
||||
ui.set_min_width(ui.available_width());
|
||||
ui.label(
|
||||
RichText::new("Pick a username — optional")
|
||||
RichText::new(t!("goblin.onboarding.identity.pick_username"))
|
||||
.font(FontId::new(15.0, fonts::semibold()))
|
||||
.color(t.surface_text),
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
ui.label(
|
||||
RichText::new(
|
||||
"Friends pay @you instead of a long key. Public on \
|
||||
goblin.st; payments stay encrypted. Skip it and \
|
||||
you're simply anonymous — claim one any time later.",
|
||||
)
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.surface_text_dim),
|
||||
RichText::new(t!("goblin.onboarding.identity.username_blurb"))
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.surface_text_dim),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
w::field_well(ui, |ui| {
|
||||
@@ -804,7 +814,7 @@ impl OnboardingContent {
|
||||
let before = self.claim.input.clone();
|
||||
TextEdit::new(egui::Id::from("onb_claim"))
|
||||
.focus(false)
|
||||
.hint_text("yourname")
|
||||
.hint_text(t!("goblin.onboarding.identity.username_field_hint"))
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
.ui(ui, &mut self.claim.input, cb);
|
||||
@@ -833,21 +843,29 @@ impl OnboardingContent {
|
||||
ui.horizontal(|ui| {
|
||||
View::small_loading_spinner(ui);
|
||||
ui.add_space(8.0);
|
||||
ui.label(RichText::new("Working…").color(t.surface_text_dim));
|
||||
ui.label(
|
||||
RichText::new(t!("goblin.onboarding.identity.working"))
|
||||
.color(t.surface_text_dim),
|
||||
);
|
||||
});
|
||||
ui.ctx().request_repaint();
|
||||
} else {
|
||||
ui.add_enabled_ui(valid && connected, |ui| {
|
||||
if w::big_action_on_card(ui, "Claim username").clicked() {
|
||||
if w::big_action_on_card(
|
||||
ui,
|
||||
&t!("goblin.onboarding.identity.claim_username"),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
start_claim_flow(&mut self.claim, &name, &wallet);
|
||||
}
|
||||
});
|
||||
if !connected {
|
||||
ui.add_space(6.0);
|
||||
ui.label(
|
||||
RichText::new(
|
||||
"Available once the mixnet connects — or skip and claim later.",
|
||||
)
|
||||
RichText::new(t!(
|
||||
"goblin.onboarding.identity.available_when_connected"
|
||||
))
|
||||
.font(FontId::new(12.0, fonts::regular()))
|
||||
.color(t.surface_text_mute),
|
||||
);
|
||||
@@ -865,11 +883,11 @@ impl OnboardingContent {
|
||||
}
|
||||
|
||||
let main_label = if registered {
|
||||
"Open my wallet"
|
||||
t!("goblin.onboarding.identity.open_wallet")
|
||||
} else {
|
||||
"Skip for now"
|
||||
t!("goblin.onboarding.identity.skip_for_now")
|
||||
};
|
||||
if w::big_action(ui, main_label, false).clicked() {
|
||||
if w::big_action(ui, &main_label, false).clicked() {
|
||||
return Some(wallet);
|
||||
}
|
||||
None
|
||||
|
||||
+150
-111
@@ -288,21 +288,21 @@ impl SendFlow {
|
||||
// Scan-to-pay screen: a Scan | My Code toggle over the camera or your own
|
||||
// payment QR. Replaces the picker until closed.
|
||||
if self.scan_open {
|
||||
if self.back_header(
|
||||
ui,
|
||||
if self.request {
|
||||
"Scan to request"
|
||||
} else {
|
||||
"Scan to pay"
|
||||
},
|
||||
) {
|
||||
let title = if self.request {
|
||||
t!("goblin.send.scan_to_request")
|
||||
} else {
|
||||
t!("goblin.send.scan_to_pay")
|
||||
};
|
||||
if self.back_header(ui, &title) {
|
||||
cb.stop_camera();
|
||||
self.scan = None;
|
||||
self.scan_open = false;
|
||||
return false;
|
||||
}
|
||||
let sel = if self.scan_tab == ScanTab::Scan { 0 } else { 1 };
|
||||
if let Some(i) = w::segmented(ui, &["Scan", "My Code"], sel) {
|
||||
let (tab_scan, tab_my_code) =
|
||||
(t!("goblin.send.tab_scan"), t!("goblin.send.tab_my_code"));
|
||||
if let Some(i) = w::segmented(ui, &[&tab_scan, &tab_my_code], sel) {
|
||||
self.scan_tab = if i == 0 {
|
||||
ScanTab::Scan
|
||||
} else {
|
||||
@@ -331,14 +331,12 @@ impl SendFlow {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.back_header(
|
||||
ui,
|
||||
if self.request {
|
||||
"Request from"
|
||||
} else {
|
||||
"Send to"
|
||||
},
|
||||
) {
|
||||
let title = if self.request {
|
||||
t!("goblin.send.request_from")
|
||||
} else {
|
||||
t!("goblin.send.send_to")
|
||||
};
|
||||
if self.back_header(ui, &title) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -365,7 +363,7 @@ impl SendFlow {
|
||||
ui.add_space(8.0);
|
||||
let mut te = TextEdit::new(egui::Id::from("send_search"))
|
||||
.focus(false)
|
||||
.hint_text("@handle, npub, or name")
|
||||
.hint_text(t!("goblin.send.search_hint"))
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
.scan_qr();
|
||||
@@ -405,7 +403,7 @@ impl SendFlow {
|
||||
if query.is_empty() {
|
||||
// Empty query → suggested recent peers, as before.
|
||||
ui.label(
|
||||
RichText::new(format!("{} Suggested", USERS))
|
||||
RichText::new(t!("goblin.send.suggested", "icon" => USERS))
|
||||
.font(fonts::kicker())
|
||||
.color(t.text_mute),
|
||||
);
|
||||
@@ -421,7 +419,7 @@ impl SendFlow {
|
||||
if peers.is_empty() {
|
||||
ui.add_space(20.0);
|
||||
ui.label(
|
||||
RichText::new("No contacts yet. Find someone by their @handle.")
|
||||
RichText::new(t!("goblin.send.no_contacts"))
|
||||
.font(FontId::new(14.0, fonts::regular()))
|
||||
.color(t.text_dim),
|
||||
);
|
||||
@@ -480,9 +478,16 @@ impl SendFlow {
|
||||
.show(ui, |ui| {
|
||||
for (c, tex) in cands.iter().zip(texs.iter()) {
|
||||
let tag = if c.verified {
|
||||
format!("✓ {}", c.tag)
|
||||
// Localize the human-worded provenance tags; domain / protocol
|
||||
// tags (@goblin.st, nip-05) display verbatim.
|
||||
let label = match c.tag {
|
||||
"contact" => t!("goblin.send.tag_contact"),
|
||||
"on nostr" => t!("goblin.send.tag_on_nostr"),
|
||||
other => std::borrow::Cow::Borrowed(other),
|
||||
};
|
||||
format!("✓ {}", label)
|
||||
} else {
|
||||
"no profile".to_string()
|
||||
t!("goblin.send.no_profile").to_string()
|
||||
};
|
||||
if w::activity_row(
|
||||
ui,
|
||||
@@ -506,7 +511,7 @@ impl SendFlow {
|
||||
View::small_loading_spinner(ui);
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
RichText::new("Searching nostr…")
|
||||
RichText::new(t!("goblin.send.searching_nostr"))
|
||||
.font(FontId::new(14.0, fonts::regular()))
|
||||
.color(t.text_dim),
|
||||
);
|
||||
@@ -550,19 +555,15 @@ impl SendFlow {
|
||||
w::card(ui, |ui| {
|
||||
ui.set_min_width(ui.available_width());
|
||||
ui.label(
|
||||
RichText::new("Pay an unverified key?")
|
||||
RichText::new(t!("goblin.send.unverified_title"))
|
||||
.font(FontId::new(15.0, fonts::semibold()))
|
||||
.color(t.surface_text),
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
ui.label(
|
||||
RichText::new(
|
||||
"No nostr profile is published for this key — it may be \
|
||||
brand new, anonymous, or mistyped. Double-check it's the \
|
||||
right one before sending.",
|
||||
)
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.surface_text_dim),
|
||||
RichText::new(t!("goblin.send.unverified_body"))
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.surface_text_dim),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
@@ -579,7 +580,7 @@ impl SendFlow {
|
||||
Vec2::new(half, 44.0),
|
||||
)),
|
||||
|ui| {
|
||||
if w::big_action_on_card(ui, "Keep looking").clicked() {
|
||||
if w::big_action_on_card(ui, &t!("goblin.send.keep_looking")).clicked() {
|
||||
self.confirm_unverified = None;
|
||||
}
|
||||
},
|
||||
@@ -591,7 +592,7 @@ impl SendFlow {
|
||||
Vec2::new(half, 44.0),
|
||||
)),
|
||||
|ui| {
|
||||
if w::big_action(ui, "Pay anyway", false).clicked() {
|
||||
if w::big_action(ui, &t!("goblin.send.pay_anyway"), false).clicked() {
|
||||
let mut c = cand.clone();
|
||||
c.verified = true;
|
||||
self.confirm_unverified = None;
|
||||
@@ -605,7 +606,6 @@ impl SendFlow {
|
||||
|
||||
/// Camera feed scanning for a recipient QR (npub / nostr: URI / @handle).
|
||||
fn scan_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
|
||||
const NO_RECIPIENT: &str = "That QR isn't a goblin recipient — expected an npub or @handle";
|
||||
let t = theme::tokens();
|
||||
let result = self.scan.as_mut().and_then(|cam| {
|
||||
let res = cam.qr_scan_result();
|
||||
@@ -637,14 +637,14 @@ impl SendFlow {
|
||||
self.net_candidate = None;
|
||||
let _ = wallet;
|
||||
}
|
||||
_ => self.error = Some(NO_RECIPIENT.to_string()),
|
||||
_ => self.error = Some(t!("goblin.send.scan_not_recipient").to_string()),
|
||||
}
|
||||
return;
|
||||
}
|
||||
ui.add_space(14.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
RichText::new("Position a goblin code in view to activate")
|
||||
RichText::new(t!("goblin.send.scan_prompt"))
|
||||
.font(FontId::new(14.0, fonts::regular()))
|
||||
.color(t.text_dim),
|
||||
);
|
||||
@@ -676,7 +676,7 @@ impl SendFlow {
|
||||
);
|
||||
ui.add_space(2.0);
|
||||
ui.label(
|
||||
RichText::new("Scan to pay me")
|
||||
RichText::new(t!("goblin.send.scan_to_pay_me"))
|
||||
.font(FontId::new(13.0, fonts::regular()))
|
||||
.color(t.surface_text_dim),
|
||||
);
|
||||
@@ -688,7 +688,7 @@ impl SendFlow {
|
||||
});
|
||||
|
||||
ui.add_space(12.0);
|
||||
if w::big_action(ui, &format!("{} Share", SHARE), false).clicked() {
|
||||
if w::big_action(ui, &t!("goblin.send.share_btn", "icon" => SHARE), false).clicked() {
|
||||
// Share the full nostr identity (npub + relay hints), with the bare
|
||||
// npub as a fallback line, via the platform's native share sheet.
|
||||
let link = if nprofile.is_empty() {
|
||||
@@ -696,7 +696,13 @@ impl SendFlow {
|
||||
} else {
|
||||
format!("nostr:{}", nprofile)
|
||||
};
|
||||
let msg = format!("Pay me on Goblin — {}\n{}\nnpub: {}", handle, link, npub);
|
||||
let msg = t!(
|
||||
"goblin.send.share_message",
|
||||
"handle" => handle,
|
||||
"link" => link,
|
||||
"npub" => npub
|
||||
)
|
||||
.to_string();
|
||||
cb.share_text(msg);
|
||||
}
|
||||
}
|
||||
@@ -716,7 +722,8 @@ impl SendFlow {
|
||||
}
|
||||
LookupResult::NotFound(label) => {
|
||||
self.net_candidate = None;
|
||||
self.error = Some(format!("No one found for {label}"));
|
||||
self.error =
|
||||
Some(t!("goblin.send.none_found", "label" => label).to_string());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -841,7 +848,7 @@ impl SendFlow {
|
||||
*slot.lock().unwrap() = Some(res);
|
||||
});
|
||||
} else {
|
||||
self.error = Some("Enter an @handle, npub, or name".to_string());
|
||||
self.error = Some(t!("goblin.send.enter_recipient").to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -853,7 +860,7 @@ impl SendFlow {
|
||||
cb: &dyn PlatformCallbacks,
|
||||
) -> bool {
|
||||
let t = theme::tokens();
|
||||
if self.back_header(ui, "Amount") {
|
||||
if self.back_header(ui, &t!("goblin.send.amount_title")) {
|
||||
self.stage = Stage::Recipient;
|
||||
return false;
|
||||
}
|
||||
@@ -870,7 +877,7 @@ impl SendFlow {
|
||||
.unwrap_or(false);
|
||||
|
||||
// Recipient chip, centered per the design.
|
||||
let name_label = format!("To {}", recipient.name);
|
||||
let name_label = t!("goblin.send.to_name", "name" => recipient.name).to_string();
|
||||
let name_galley = ui.painter().layout_no_wrap(
|
||||
name_label.clone(),
|
||||
FontId::new(14.0, fonts::semibold()),
|
||||
@@ -924,7 +931,7 @@ impl SendFlow {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
RichText::new("You don't have enough grin")
|
||||
RichText::new(t!("goblin.send.not_enough"))
|
||||
.font(FontId::new(14.0, fonts::regular()))
|
||||
.color(t.neg),
|
||||
);
|
||||
@@ -936,7 +943,12 @@ impl SendFlow {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space((ui.available_width() - 220.0).max(0.0) / 2.0);
|
||||
for v in ["1", "10", "100", "Max"] {
|
||||
if w::chip_outline(ui, v).clicked() {
|
||||
let label = if v == "Max" {
|
||||
t!("goblin.send.max")
|
||||
} else {
|
||||
std::borrow::Cow::Borrowed(v)
|
||||
};
|
||||
if w::chip_outline(ui, &label).clicked() {
|
||||
if v == "Max" {
|
||||
let max = wallet
|
||||
.get_data()
|
||||
@@ -957,7 +969,7 @@ impl SendFlow {
|
||||
w::card(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(
|
||||
RichText::new("Note")
|
||||
RichText::new(t!("goblin.send.note_label"))
|
||||
.font(FontId::new(14.0, fonts::regular()))
|
||||
.color(t.surface_text_dim),
|
||||
);
|
||||
@@ -965,7 +977,7 @@ impl SendFlow {
|
||||
let note_id = egui::Id::from("send_note");
|
||||
TextEdit::new(note_id)
|
||||
.focus(false)
|
||||
.hint_text("Add a note…")
|
||||
.hint_text(t!("goblin.send.note_hint"))
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
.ui(ui, &mut self.note, cb);
|
||||
@@ -988,7 +1000,7 @@ impl SendFlow {
|
||||
.map(|a| a > 0)
|
||||
.unwrap_or(false);
|
||||
ui.add_enabled_ui(valid, |ui| {
|
||||
if w::big_action(ui, "Review", false).clicked() {
|
||||
if w::big_action(ui, &t!("goblin.send.review_btn"), false).clicked() {
|
||||
if over {
|
||||
cb.vibrate_error();
|
||||
} else {
|
||||
@@ -1006,14 +1018,12 @@ impl SendFlow {
|
||||
avatars: &mut AvatarTextures,
|
||||
) -> bool {
|
||||
let t = theme::tokens();
|
||||
if self.back_header(
|
||||
ui,
|
||||
if self.request {
|
||||
"Confirm request"
|
||||
} else {
|
||||
"Review"
|
||||
},
|
||||
) {
|
||||
let title = if self.request {
|
||||
t!("goblin.send.confirm_request")
|
||||
} else {
|
||||
t!("goblin.send.review_title")
|
||||
};
|
||||
if self.back_header(ui, &title) {
|
||||
// Requests fix the amount on the Pay tab, so back returns to the
|
||||
// recipient picker rather than the send-style amount step.
|
||||
self.stage = if self.request {
|
||||
@@ -1045,9 +1055,9 @@ impl SendFlow {
|
||||
ui.set_min_width(ui.available_width());
|
||||
ui.add_space(8.0);
|
||||
let label = if self.request {
|
||||
format!("Requesting from {}", recipient.name)
|
||||
t!("goblin.send.requesting_from", "name" => recipient.name).to_string()
|
||||
} else {
|
||||
format!("You're sending {}", recipient.name)
|
||||
t!("goblin.send.youre_sending", "name" => recipient.name).to_string()
|
||||
};
|
||||
// Centered avatar + caption. A long counterparty (a bare npub) wraps
|
||||
// and stays centered instead of overflowing the card.
|
||||
@@ -1073,34 +1083,59 @@ impl SendFlow {
|
||||
});
|
||||
ui.add_space(16.0);
|
||||
|
||||
w::info_row(
|
||||
ui,
|
||||
if self.request { "From" } else { "To" },
|
||||
&recipient.name,
|
||||
);
|
||||
let from_to = if self.request {
|
||||
t!("goblin.send.row_from")
|
||||
} else {
|
||||
t!("goblin.send.row_to")
|
||||
};
|
||||
w::info_row(ui, &from_to, &recipient.name);
|
||||
if !self.note.trim().is_empty() {
|
||||
w::info_row(ui, "Note", &format!("\u{201C}{}\u{201D}", self.note.trim()));
|
||||
w::info_row(
|
||||
ui,
|
||||
&t!("goblin.send.row_note"),
|
||||
&format!("\u{201C}{}\u{201D}", self.note.trim()),
|
||||
);
|
||||
}
|
||||
if self.request {
|
||||
w::info_row(ui, "They pay", "Only if they approve");
|
||||
w::info_row(ui, "Delivery", "NIP-44 encrypted, over Nym");
|
||||
w::info_row(
|
||||
ui,
|
||||
&t!("goblin.send.row_they_pay"),
|
||||
&t!("goblin.send.row_they_pay_val"),
|
||||
);
|
||||
w::info_row(
|
||||
ui,
|
||||
&t!("goblin.send.row_delivery"),
|
||||
&t!("goblin.send.row_delivery_val"),
|
||||
);
|
||||
} else {
|
||||
w::info_row(ui, "Network fee", "Deducted from your balance");
|
||||
w::info_row(ui, "Privacy", "Mimblewimble + Nym");
|
||||
w::info_row(ui, "Delivery", "NIP-44 encrypted, over Nym");
|
||||
w::info_row(
|
||||
ui,
|
||||
&t!("goblin.send.row_network_fee"),
|
||||
&t!("goblin.send.row_network_fee_val"),
|
||||
);
|
||||
w::info_row(
|
||||
ui,
|
||||
&t!("goblin.send.row_privacy"),
|
||||
&t!("goblin.send.row_privacy_val"),
|
||||
);
|
||||
w::info_row(
|
||||
ui,
|
||||
&t!("goblin.send.row_delivery"),
|
||||
&t!("goblin.send.row_delivery_val"),
|
||||
);
|
||||
}
|
||||
ui.add_space(16.0);
|
||||
|
||||
// Requests are not a spend: one tap sends the ask, no hold-to-confirm.
|
||||
if self.request {
|
||||
if w::big_action(ui, "Send request", false).clicked() {
|
||||
if w::big_action(ui, &t!("goblin.send.send_request_btn"), false).clicked() {
|
||||
self.dispatch(wallet);
|
||||
self.stage = Stage::Sending;
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
RichText::new("They'll get a request to approve")
|
||||
RichText::new(t!("goblin.send.request_approve_hint"))
|
||||
.font(FontId::new(12.0, fonts::regular()))
|
||||
.color(t.text_mute),
|
||||
);
|
||||
@@ -1111,7 +1146,7 @@ impl SendFlow {
|
||||
if over {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
RichText::new("You don't have enough grin")
|
||||
RichText::new(t!("goblin.send.not_enough"))
|
||||
.font(FontId::new(14.0, fonts::regular()))
|
||||
.color(t.neg),
|
||||
);
|
||||
@@ -1121,7 +1156,7 @@ impl SendFlow {
|
||||
// Greyed out while over balance; the `&& !over` also refuses the send in
|
||||
// case the hold widget ignores the disabled state.
|
||||
ui.add_enabled_ui(!over, |ui| {
|
||||
if self.hold.ui(ui, "Hold to send") && !over {
|
||||
if self.hold.ui(ui, &t!("goblin.send.hold_to_send")) && !over {
|
||||
self.dispatch(wallet);
|
||||
self.stage = Stage::Sending;
|
||||
}
|
||||
@@ -1130,9 +1165,9 @@ impl SendFlow {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
RichText::new(if over {
|
||||
"Go back and lower the amount"
|
||||
t!("goblin.send.lower_amount")
|
||||
} else {
|
||||
"Press and hold to confirm"
|
||||
t!("goblin.send.hold_confirm_hint")
|
||||
})
|
||||
.font(FontId::new(12.0, fonts::regular()))
|
||||
.color(t.text_mute),
|
||||
@@ -1183,9 +1218,9 @@ impl SendFlow {
|
||||
ui.add_space(16.0);
|
||||
ui.label(
|
||||
RichText::new(if self.request {
|
||||
"Requesting…"
|
||||
t!("goblin.send.requesting")
|
||||
} else {
|
||||
"Sending…"
|
||||
t!("goblin.send.sending")
|
||||
})
|
||||
.font(FontId::new(18.0, fonts::semibold()))
|
||||
.color(t.text),
|
||||
@@ -1204,11 +1239,8 @@ impl SendFlow {
|
||||
.recipient
|
||||
.as_ref()
|
||||
.map(|r| r.name.clone())
|
||||
.unwrap_or_else(|| "They".to_string());
|
||||
self.error = Some(format!(
|
||||
"{} isn't accepting requests. Ask them to send you grin instead.",
|
||||
who
|
||||
));
|
||||
.unwrap_or_else(|| t!("goblin.send.they").to_string());
|
||||
self.error = Some(t!("goblin.send.request_blocked", "who" => who).to_string());
|
||||
self.stage = Stage::Failed;
|
||||
}
|
||||
crate::nostr::send_phase::FAILED => self.stage = Stage::Failed,
|
||||
@@ -1230,9 +1262,9 @@ impl SendFlow {
|
||||
ui.add_space(16.0);
|
||||
ui.label(
|
||||
RichText::new(if self.request {
|
||||
"Couldn't request"
|
||||
t!("goblin.send.failed_request_title")
|
||||
} else {
|
||||
"Couldn't send"
|
||||
t!("goblin.send.failed_send_title")
|
||||
})
|
||||
.font(FontId::new(22.0, fonts::bold()))
|
||||
.color(t.text),
|
||||
@@ -1241,10 +1273,9 @@ impl SendFlow {
|
||||
ui.label(
|
||||
RichText::new(self.error.clone().unwrap_or_else(|| {
|
||||
if self.request {
|
||||
"We couldn't deliver the request. Ask them to send you grin instead."
|
||||
.to_string()
|
||||
t!("goblin.send.failed_request_body").to_string()
|
||||
} else {
|
||||
"The payment wasn't delivered. Your grin is safe — try again.".to_string()
|
||||
t!("goblin.send.failed_send_body").to_string()
|
||||
}
|
||||
}))
|
||||
.font(FontId::new(14.0, fonts::regular()))
|
||||
@@ -1252,12 +1283,12 @@ impl SendFlow {
|
||||
);
|
||||
});
|
||||
ui.add_space(24.0);
|
||||
if w::big_action(ui, "Try again", false).clicked() {
|
||||
if w::big_action(ui, &t!("goblin.send.try_again_btn"), false).clicked() {
|
||||
self.dispatch(wallet);
|
||||
self.stage = Stage::Sending;
|
||||
}
|
||||
ui.add_space(10.0);
|
||||
if w::big_action(ui, "Close", true).clicked() {
|
||||
if w::big_action(ui, &t!("goblin.send.close_btn"), true).clicked() {
|
||||
done = true;
|
||||
}
|
||||
done
|
||||
@@ -1281,32 +1312,40 @@ impl SendFlow {
|
||||
);
|
||||
ui.add_space(24.0);
|
||||
ui.label(
|
||||
RichText::new(if self.request { "Requested" } else { "Sent" })
|
||||
RichText::new(if self.request { t!("goblin.send.success.requested") } else { t!("goblin.send.success.sent") })
|
||||
.font(FontId::new(34.0, fonts::bold()))
|
||||
.color(t.accent_ink),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
let total_w = ui.available_width();
|
||||
ui.add_space(total_w / 2.0 - 60.0);
|
||||
ui.label(
|
||||
RichText::new(&self.amount)
|
||||
.font(FontId::new(40.0, fonts::mono_semibold()))
|
||||
.color(t.accent_ink),
|
||||
);
|
||||
ui.label(
|
||||
RichText::new(w::TSU)
|
||||
.font(FontId::new(20.0, fonts::medium()))
|
||||
.color(t.accent_ink),
|
||||
);
|
||||
});
|
||||
// Amount + ツ as one layout job so vertical_centered centers it exactly,
|
||||
// independent of the number's width (a fixed offset drifts off-center).
|
||||
let mut job = egui::text::LayoutJob::default();
|
||||
job.append(
|
||||
&self.amount,
|
||||
0.0,
|
||||
egui::text::TextFormat {
|
||||
font_id: FontId::new(40.0, fonts::mono_semibold()),
|
||||
color: t.accent_ink,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
job.append(
|
||||
w::TSU,
|
||||
0.0,
|
||||
egui::text::TextFormat {
|
||||
font_id: FontId::new(20.0, fonts::medium()),
|
||||
color: t.accent_ink,
|
||||
valign: Align::BOTTOM,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
ui.label(job);
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
RichText::new(format!(
|
||||
"{} {} · just now",
|
||||
if self.request { "from" } else { "to" },
|
||||
recipient.name
|
||||
RichText::new(t!(
|
||||
"goblin.send.success.subtitle",
|
||||
"dir" => if self.request { t!("goblin.send.success.from") } else { t!("goblin.send.success.to") },
|
||||
"who" => recipient.name
|
||||
))
|
||||
.font(FontId::new(15.0, fonts::regular()))
|
||||
.color(t.accent_ink.gamma_multiply(0.7)),
|
||||
@@ -1330,7 +1369,7 @@ impl SendFlow {
|
||||
ui.painter().text(
|
||||
rect.center(),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
"Done",
|
||||
t!("goblin.send.success.done_btn"),
|
||||
FontId::new(17.0, fonts::semibold()),
|
||||
t.accent,
|
||||
);
|
||||
@@ -1352,7 +1391,7 @@ impl SendFlow {
|
||||
ui.painter().text(
|
||||
r2.center(),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
"Receipt",
|
||||
t!("goblin.send.success.receipt_btn"),
|
||||
FontId::new(17.0, fonts::semibold()),
|
||||
t.accent_ink,
|
||||
);
|
||||
|
||||
+15
-12
@@ -335,18 +335,21 @@ fn setup_i18n() {
|
||||
}
|
||||
} else {
|
||||
let locale = sys_locale::get_locale().unwrap_or(String::from(AppConfig::DEFAULT_LOCALE));
|
||||
let locale_str = if locale.contains("-") {
|
||||
locale
|
||||
.split("-")
|
||||
.next()
|
||||
.unwrap_or(AppConfig::DEFAULT_LOCALE)
|
||||
} else {
|
||||
locale.as_str()
|
||||
};
|
||||
|
||||
// Set best possible locale.
|
||||
if rust_i18n::available_locales!().contains(&locale_str) {
|
||||
rust_i18n::set_locale(locale_str);
|
||||
// sys_locale may hand back either `zh-CN` or `zh_CN`; normalize the
|
||||
// separator so a region-specific locale can match its file name.
|
||||
let normalized = locale.replace('_', "-");
|
||||
let available = rust_i18n::available_locales!();
|
||||
// Prefer an exact region match (e.g. `zh-CN`, the only CJK locale and one
|
||||
// the bare-subtag fallback could never reach), then the language subtag
|
||||
// (e.g. `de` from `de-DE`), else the default.
|
||||
let primary = normalized
|
||||
.split('-')
|
||||
.next()
|
||||
.unwrap_or(AppConfig::DEFAULT_LOCALE);
|
||||
if available.contains(&normalized.as_str()) {
|
||||
rust_i18n::set_locale(normalized.as_str());
|
||||
} else if available.contains(&primary) {
|
||||
rust_i18n::set_locale(primary);
|
||||
} else {
|
||||
rust_i18n::set_locale(AppConfig::DEFAULT_LOCALE);
|
||||
}
|
||||
|
||||
+144
-1
@@ -444,6 +444,46 @@ impl NostrService {
|
||||
Ok(res.val.to_hex())
|
||||
}
|
||||
|
||||
/// Dispatch a control DM that voids a pending request (a decline by the payer
|
||||
/// or a cancel by the requester) to `receiver_hex`, referencing `slate_id`.
|
||||
/// Same routing as a payment DM, but the message carries no slatepack.
|
||||
pub async fn send_control_dm(
|
||||
&self,
|
||||
receiver_hex: &str,
|
||||
slate_id: &str,
|
||||
relay_hints: &[String],
|
||||
) -> Result<String, String> {
|
||||
let client = {
|
||||
let r_client = self.client.read();
|
||||
r_client.clone().ok_or("nostr client is not running")?
|
||||
};
|
||||
let receiver =
|
||||
PublicKey::from_hex(receiver_hex).map_err(|e| format!("invalid receiver: {e}"))?;
|
||||
let content = protocol::build_control_content();
|
||||
let tags = protocol::build_control_tags(slate_id);
|
||||
|
||||
let mut urls = self.fetch_dm_relays(&client, &receiver).await;
|
||||
for r in relay_hints {
|
||||
if !urls.contains(r) {
|
||||
urls.push(r.clone());
|
||||
}
|
||||
}
|
||||
for r in self.relays() {
|
||||
if !urls.contains(&r) {
|
||||
urls.push(r);
|
||||
}
|
||||
}
|
||||
|
||||
let res = tokio::time::timeout(
|
||||
SEND_TIMEOUT,
|
||||
client.send_private_msg_to(urls, receiver, content, tags),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| "send timeout".to_string())?
|
||||
.map_err(|e| format!("send failed: {e}"))?;
|
||||
Ok(res.val.to_hex())
|
||||
}
|
||||
|
||||
/// Fetch a contact's kind 10050 DM relay list from our relays.
|
||||
async fn fetch_dm_relays(&self, client: &Client, pk: &PublicKey) -> Vec<String> {
|
||||
// Use cached relays first.
|
||||
@@ -497,6 +537,51 @@ impl NostrService {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Best-effort: resolve a contact's published `@username`. Incoming messages
|
||||
/// only carry the sender's key, so a fresh contact shows as a bare npub. This
|
||||
/// fetches their kind-0 profile and, if it advertises a NIP-05 handle that
|
||||
/// verifies back to their key, records it so the UI can show `@username`.
|
||||
/// Spawns a worker; fail-open (any miss just leaves the npub). Skips contacts
|
||||
/// that already carry a verified handle.
|
||||
pub fn resolve_contact_identity(self: &Arc<Self>, sender_hex: &str) {
|
||||
if let Some(c) = self.store.contact(sender_hex) {
|
||||
if c.nip05.is_some() && c.nip05_verified_at.is_some() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let svc = self.clone();
|
||||
let hex = sender_hex.to_string();
|
||||
thread::spawn(move || {
|
||||
let Some(profile) = svc.fetch_profile_blocking(&hex) else {
|
||||
return;
|
||||
};
|
||||
let Some(nip05) = profile.nip05 else {
|
||||
return;
|
||||
};
|
||||
let Some((name, domain)) = nip05.split_once('@') else {
|
||||
return;
|
||||
};
|
||||
let Ok(pk) = PublicKey::from_hex(&hex) else {
|
||||
return;
|
||||
};
|
||||
// Trust the handle only if it maps back to this key.
|
||||
let verified = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.ok()
|
||||
.map(|rt| rt.block_on(crate::nostr::nip05::verify(&pk, name, domain)))
|
||||
.unwrap_or(false);
|
||||
if !verified {
|
||||
return;
|
||||
}
|
||||
if let Some(mut c) = svc.store.contact(&hex) {
|
||||
c.nip05 = Some(nip05.clone());
|
||||
c.nip05_verified_at = Some(unix_time());
|
||||
svc.store.save_contact(&c);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Main service loop: connect, publish identity, catch up, listen.
|
||||
@@ -803,6 +888,52 @@ async fn reconcile(svc: &Arc<NostrService>, wallet: &Wallet) {
|
||||
}
|
||||
|
||||
/// Full guarded pipeline for one incoming gift wrap event.
|
||||
/// Apply a request-void control message. Two roles, distinguished by what we
|
||||
/// hold for `slate_id`; in both the `sender` must match the stored counterparty,
|
||||
/// so an attacker can't void a request they're not party to.
|
||||
fn handle_request_void(svc: &Arc<NostrService>, wallet: &Wallet, slate_id: &str, sender: &str) {
|
||||
// Role A — we are the payer and the requester withdrew. Drop the pending card.
|
||||
let mut voided = false;
|
||||
for req in svc.store.pending_requests() {
|
||||
if req.slate_id == slate_id && req.npub == sender {
|
||||
info!(
|
||||
"nostr: incoming request {} withdrawn by requester",
|
||||
req.rumor_id
|
||||
);
|
||||
svc.store
|
||||
.update_request_status(&req.rumor_id, RequestStatus::Cancelled);
|
||||
svc.has_new_requests.store(true, Ordering::Relaxed);
|
||||
voided = true;
|
||||
}
|
||||
}
|
||||
if voided {
|
||||
return;
|
||||
}
|
||||
// Role B — we are the requester and the payer declined. Cancel our invoice tx
|
||||
// and mark the meta cancelled so it leaves the pending state.
|
||||
if let Some(meta) = svc.store.tx_meta(slate_id) {
|
||||
if meta.direction == NostrTxDirection::RequestedByUs
|
||||
&& matches!(
|
||||
meta.status,
|
||||
NostrSendStatus::Created | NostrSendStatus::AwaitingI2
|
||||
) && meta.npub == sender
|
||||
{
|
||||
info!("nostr: outgoing request {} declined by payer", slate_id);
|
||||
if let Some(tx_id) = wallet.get_data().and_then(|d| d.txs).and_then(|txs| {
|
||||
txs.iter()
|
||||
.find(|t| {
|
||||
t.data.tx_slate_id.map(|u| u.to_string()).as_deref() == Some(slate_id)
|
||||
})
|
||||
.map(|t| t.data.id)
|
||||
}) {
|
||||
wallet.task(WalletTask::Cancel(tx_id));
|
||||
}
|
||||
svc.store
|
||||
.update_tx_status(slate_id, NostrSendStatus::Cancelled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_wrap(svc: &Arc<NostrService>, wallet: &Wallet, client: &Client, event: Event) {
|
||||
// 0. Only gift wraps.
|
||||
if event.kind != Kind::GiftWrap {
|
||||
@@ -879,7 +1010,17 @@ async fn handle_wrap(svc: &Arc<NostrService>, wallet: &Wallet, client: &Client,
|
||||
svc.store.mark_processed(&wrap_id);
|
||||
return;
|
||||
}
|
||||
// 8. Extract the slatepack; non-payment DMs are ignored entirely.
|
||||
// 8. Request-void control message (a decline by the payer or a cancel by the
|
||||
// requester): it carries no slatepack, just an action tag naming a slate id.
|
||||
// Handle it before slatepack extraction; the sender is bound to the stored
|
||||
// counterparty inside, so a stranger can't void someone else's request.
|
||||
if let Some(void_slate_id) = protocol::extract_control(&rumor.tags) {
|
||||
handle_request_void(svc, wallet, &void_slate_id, &sender_hex);
|
||||
svc.store.mark_processed(&wrap_id);
|
||||
svc.store.mark_processed(&rumor_id);
|
||||
return;
|
||||
}
|
||||
// 8b. Extract the slatepack; non-payment DMs are ignored entirely.
|
||||
let Some(armor) = protocol::extract_slatepack(&rumor.content) else {
|
||||
svc.store.mark_processed(&wrap_id);
|
||||
svc.store.mark_processed(&rumor_id);
|
||||
@@ -973,6 +1114,8 @@ async fn handle_wrap(svc: &Arc<NostrService>, wallet: &Wallet, client: &Client,
|
||||
}
|
||||
IngestDecision::SurfaceIncoming | IngestDecision::SurfaceRequest => {
|
||||
svc.ensure_contact(&sender_hex);
|
||||
// Resolve the requester's @username so the card isn't a bare npub.
|
||||
svc.resolve_contact_identity(&sender_hex);
|
||||
svc.store.save_request(&PaymentRequest {
|
||||
ver: 1,
|
||||
rumor_id: rumor_id.clone(),
|
||||
|
||||
@@ -35,6 +35,12 @@ pub const MAX_NOTE_CHARS: usize = 256;
|
||||
pub const GOBLIN_TAG: &str = "goblin";
|
||||
/// Protocol version value.
|
||||
pub const PROTOCOL_VERSION: &str = "1";
|
||||
/// Control-message tag name: carries `[action, slate_id]` for a request that is
|
||||
/// being voided (a decline by the payer or a cancel by the requester).
|
||||
pub const GOBLIN_ACTION_TAG: &str = "goblin-action";
|
||||
/// The one control action: void an unpaid request. Decline and cancel are the
|
||||
/// same wire message — "this request is off" — they only differ by who sends it.
|
||||
pub const ACTION_VOID: &str = "void";
|
||||
|
||||
/// Human readable preamble other NIP-17 clients render.
|
||||
pub const PREAMBLE: &str =
|
||||
@@ -76,6 +82,44 @@ pub fn build_rumor_tags(note: Option<&str>) -> Vec<Tag> {
|
||||
tags
|
||||
}
|
||||
|
||||
/// Build the kind-14 rumor content for a request-void control message. Carries
|
||||
/// NO slatepack — other NIP-17 clients render the human-readable line.
|
||||
pub fn build_control_content() -> String {
|
||||
"[Goblin] Payment request withdrawn — open in Goblin (https://goblin.st).".to_string()
|
||||
}
|
||||
|
||||
/// Build rumor tags for a control message: protocol marker plus the action +
|
||||
/// slate id the control refers to.
|
||||
pub fn build_control_tags(slate_id: &str) -> Vec<Tag> {
|
||||
vec![
|
||||
Tag::custom(TagKind::custom(GOBLIN_TAG), [PROTOCOL_VERSION.to_string()]),
|
||||
Tag::custom(
|
||||
TagKind::custom(GOBLIN_ACTION_TAG),
|
||||
[ACTION_VOID.to_string(), slate_id.to_string()],
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/// Read a control action from rumor tags. Returns the referenced slate id when a
|
||||
/// well-formed `goblin-action` void tag is present, else `None`. Classification
|
||||
/// NEVER trusts this for payment processing — it only voids a pending request,
|
||||
/// and the caller still binds the sender to the stored counterparty.
|
||||
pub fn extract_control(tags: &Tags) -> Option<String> {
|
||||
for tag in tags.iter() {
|
||||
let parts = tag.as_slice();
|
||||
if parts.first().map(|s| s.as_str()) == Some(GOBLIN_ACTION_TAG) {
|
||||
let action = parts.get(1).map(|s| s.as_str());
|
||||
let slate_id = parts.get(2).map(|s| s.to_string());
|
||||
if action == Some(ACTION_VOID) {
|
||||
if let Some(id) = slate_id.filter(|s| !s.is_empty()) {
|
||||
return Some(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Extract exactly one slatepack armor block from rumor content.
|
||||
/// More than one block, none at all, or an oversized block returns `None`.
|
||||
pub fn extract_slatepack(content: &str) -> Option<String> {
|
||||
@@ -195,4 +239,22 @@ mod tests {
|
||||
assert!(c.starts_with(PREAMBLE));
|
||||
assert!(extract_slatepack(&c).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_round_trips_slate_id() {
|
||||
let tags = Tags::from_list(build_control_tags("abc-123"));
|
||||
assert_eq!(extract_control(&tags), Some("abc-123".to_string()));
|
||||
// A control message carries no slatepack.
|
||||
assert!(extract_slatepack(&build_control_content()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_absent_or_malformed_returns_none() {
|
||||
// Ordinary payment tags have no action tag.
|
||||
let tags = Tags::from_list(build_rumor_tags(Some("lunch")));
|
||||
assert!(extract_control(&tags).is_none());
|
||||
// Action present but empty slate id is rejected.
|
||||
let bad = Tags::from_list(build_control_tags(""));
|
||||
assert!(extract_control(&bad).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,8 @@ pub enum RequestStatus {
|
||||
Approved,
|
||||
Declined,
|
||||
Expired,
|
||||
/// Withdrawn by the requester (we received their cancel control message).
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// An incoming Invoice1 payment request awaiting explicit user approval.
|
||||
|
||||
@@ -450,4 +450,12 @@ pub enum WalletTask {
|
||||
/// Republish our kind-0 profile (e.g. after toggling the incoming-requests
|
||||
/// preference) so the change propagates to relays immediately.
|
||||
NostrRepublishProfile,
|
||||
/// Decline an incoming payment request: mark it declined and send the
|
||||
/// requester a void control message so their side clears too.
|
||||
/// * request id (rumor event id hex)
|
||||
NostrDeclineRequest(String),
|
||||
/// Cancel a request WE sent: cancel the local invoice tx and send the payer a
|
||||
/// void control message so the pending card disappears on their side.
|
||||
/// * slate id (uuid string)
|
||||
NostrCancelOutgoing(String),
|
||||
}
|
||||
|
||||
@@ -2364,6 +2364,59 @@ async fn handle_task(w: &Wallet, t: WalletTask) {
|
||||
service.republish_identity().await;
|
||||
}
|
||||
}
|
||||
WalletTask::NostrDeclineRequest(rumor_id) => {
|
||||
let Some(service) = w.nostr_service() else {
|
||||
return;
|
||||
};
|
||||
let Some(mut request) = service.store.request(rumor_id) else {
|
||||
error!("nostr decline: request not found");
|
||||
return;
|
||||
};
|
||||
// Mark declined locally (idempotent) so the card stays gone, then tell
|
||||
// the requester. Requests are messages; payments are final.
|
||||
request.status = crate::nostr::RequestStatus::Declined;
|
||||
service.store.save_request(&request);
|
||||
if let Err(e) = service
|
||||
.send_control_dm(&request.npub, &request.slate_id, &[])
|
||||
.await
|
||||
{
|
||||
error!("nostr decline: control dispatch failed: {e}");
|
||||
}
|
||||
}
|
||||
WalletTask::NostrCancelOutgoing(slate_id) => {
|
||||
let Some(service) = w.nostr_service() else {
|
||||
return;
|
||||
};
|
||||
let Some(meta) = service.store.tx_meta(slate_id) else {
|
||||
error!("nostr cancel: no metadata for slate {slate_id}");
|
||||
return;
|
||||
};
|
||||
if meta.direction != crate::nostr::NostrTxDirection::RequestedByUs {
|
||||
error!("nostr cancel: slate {slate_id} is not an outgoing request");
|
||||
return;
|
||||
}
|
||||
// Cancel the underlying grin invoice tx (an issued invoice locks no
|
||||
// outputs, but cancelling keeps the wallet ledger tidy).
|
||||
if let Some(tx_id) = w.get_data().and_then(|d| d.txs).and_then(|txs| {
|
||||
txs.iter()
|
||||
.find(|t| {
|
||||
t.data.tx_slate_id.map(|u| u.to_string()).as_deref()
|
||||
== Some(slate_id.as_str())
|
||||
})
|
||||
.map(|t| t.data.id)
|
||||
}) {
|
||||
if let Err(e) = w.cancel(tx_id) {
|
||||
error!("nostr cancel: wallet cancel failed: {e}");
|
||||
}
|
||||
}
|
||||
service
|
||||
.store
|
||||
.update_tx_status(slate_id, crate::nostr::NostrSendStatus::Cancelled);
|
||||
if let Err(e) = service.send_control_dm(&meta.npub, slate_id, &[]).await {
|
||||
error!("nostr cancel: control dispatch failed: {e}");
|
||||
}
|
||||
sync_wallet_data(&w, false);
|
||||
}
|
||||
WalletTask::NostrResend(id) => {
|
||||
let Some(service) = w.nostr_service() else {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
// Copyright 2026 The Goblin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Localization drift guard. en.yml is the source of truth: every `goblin.*`
|
||||
//! key (and its `%{...}` interpolation placeholders) must exist, identically,
|
||||
//! in every other locale, so the language picker never falls back to a raw key
|
||||
//! or a string that drops a value. Fails CI the moment a translation lags.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::Path;
|
||||
|
||||
/// The locales shipped alongside English.
|
||||
const OTHER_LOCALES: &[&str] = &["de", "fr", "ru", "tr", "zh-CN"];
|
||||
|
||||
/// Flatten a YAML mapping into dotted leaf keys → string value.
|
||||
fn flatten(value: &serde_yaml::Value, prefix: &str, out: &mut BTreeMap<String, String>) {
|
||||
match value {
|
||||
serde_yaml::Value::Mapping(map) => {
|
||||
for (k, v) in map {
|
||||
let key = k.as_str().unwrap_or_default();
|
||||
let next = if prefix.is_empty() {
|
||||
key.to_string()
|
||||
} else {
|
||||
format!("{prefix}.{key}")
|
||||
};
|
||||
flatten(v, &next, out);
|
||||
}
|
||||
}
|
||||
other => {
|
||||
let s = match other {
|
||||
serde_yaml::Value::String(s) => s.clone(),
|
||||
serde_yaml::Value::Bool(b) => b.to_string(),
|
||||
serde_yaml::Value::Number(n) => n.to_string(),
|
||||
_ => String::new(),
|
||||
};
|
||||
out.insert(prefix.to_string(), s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a locale file flattened to `goblin.*` keys only.
|
||||
fn load_goblin(locale: &str) -> BTreeMap<String, String> {
|
||||
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("locales")
|
||||
.join(format!("{locale}.yml"));
|
||||
let text = std::fs::read_to_string(&path)
|
||||
.unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
|
||||
let doc: serde_yaml::Value =
|
||||
serde_yaml::from_str(&text).unwrap_or_else(|e| panic!("invalid YAML in {locale}.yml: {e}"));
|
||||
let mut all = BTreeMap::new();
|
||||
flatten(&doc, "", &mut all);
|
||||
all.into_iter()
|
||||
.filter(|(k, _)| k.starts_with("goblin."))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `%{name}` placeholders contained in a value, sorted.
|
||||
fn placeholders(s: &str) -> BTreeSet<String> {
|
||||
let mut out = BTreeSet::new();
|
||||
let bytes = s.as_bytes();
|
||||
let mut i = 0;
|
||||
while i + 1 < bytes.len() {
|
||||
if bytes[i] == b'%' && bytes[i + 1] == b'{' {
|
||||
if let Some(end) = s[i..].find('}') {
|
||||
out.insert(s[i..i + end + 1].to_string());
|
||||
i += end + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_locale_has_all_goblin_keys() {
|
||||
let en = load_goblin("en");
|
||||
assert!(
|
||||
en.len() > 300,
|
||||
"en.yml goblin block looks too small ({} keys) — did it load?",
|
||||
en.len()
|
||||
);
|
||||
let en_keys: BTreeSet<&String> = en.keys().collect();
|
||||
|
||||
let mut problems = Vec::new();
|
||||
for &loc in OTHER_LOCALES {
|
||||
let other = load_goblin(loc);
|
||||
let other_keys: BTreeSet<&String> = other.keys().collect();
|
||||
for missing in en_keys.difference(&other_keys) {
|
||||
problems.push(format!("{loc}: MISSING key {missing}"));
|
||||
}
|
||||
for extra in other_keys.difference(&en_keys) {
|
||||
problems.push(format!("{loc}: EXTRA key {extra} (not in en.yml)"));
|
||||
}
|
||||
// Placeholder parity: a translation must carry the same %{...} args.
|
||||
for (k, en_val) in &en {
|
||||
if let Some(other_val) = other.get(k) {
|
||||
if placeholders(en_val) != placeholders(other_val) {
|
||||
problems.push(format!(
|
||||
"{loc}: placeholder mismatch in {k} (en {:?} vs {:?})",
|
||||
placeholders(en_val),
|
||||
placeholders(other_val)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
problems.is_empty(),
|
||||
"localization drift detected:\n{}",
|
||||
problems.join("\n")
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user