Repository: cashubtc/cashu.me Branch: main Commit: 5979114b1a39 Files: 228 Total size: 1.7 MB Directory structure: gitextract_4w51wde5/ ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github/ │ └── workflows/ │ ├── build.yaml │ ├── docker.yaml │ ├── format.yaml │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .npmrc ├── .postcssrc.js ├── .prettierignore ├── .vscode/ │ ├── extensions.json │ └── settings.json ├── AGENTS.md ├── Dockerfile ├── LICENSE.md ├── README.md ├── android/ │ ├── .gitignore │ ├── app/ │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── capacitor.build.gradle │ │ ├── proguard-rules.pro │ │ └── src/ │ │ ├── androidTest/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── getcapacitor/ │ │ │ └── myapp/ │ │ │ └── ExampleInstrumentedTest.java │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── java/ │ │ │ │ └── me/ │ │ │ │ └── cashu/ │ │ │ │ └── wallet/ │ │ │ │ └── MainActivity.java │ │ │ └── res/ │ │ │ ├── drawable/ │ │ │ │ └── ic_launcher_background.xml │ │ │ ├── drawable-v24/ │ │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── layout/ │ │ │ │ └── activity_main.xml │ │ │ ├── mipmap-anydpi-v26/ │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── values/ │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ │ └── xml/ │ │ │ └── file_paths.xml │ │ └── test/ │ │ └── java/ │ │ └── com/ │ │ └── getcapacitor/ │ │ └── myapp/ │ │ └── ExampleUnitTest.java │ ├── build.gradle │ ├── capacitor.settings.gradle │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle │ └── variables.gradle ├── capacitor.config.ts ├── docker-compose.yaml ├── extension/ │ ├── embedder.html │ ├── manifest.json │ └── style.css ├── index.html ├── ios/ │ ├── .gitignore │ └── App/ │ ├── App/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ └── Splash.imageset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ └── Info.plist │ ├── App.xcodeproj/ │ │ └── project.pbxproj │ ├── App.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ └── Podfile ├── jsconfig.json ├── package.json ├── quasar.config.js ├── quasar.extensions.json ├── scripts/ │ └── check-i18n.js ├── src/ │ ├── App.vue │ ├── boot/ │ │ ├── .gitkeep │ │ ├── axios.js │ │ ├── base.js │ │ ├── cashu.js │ │ ├── global-components.js │ │ └── i18n.js │ ├── components/ │ │ ├── ActivityOrb.vue │ │ ├── AddMintDialog.vue │ │ ├── AmountInputComponent.vue │ │ ├── AndroidPWAPrompt.vue │ │ ├── AnimatedNumber.vue │ │ ├── BalanceView.vue │ │ ├── ChooseMint.vue │ │ ├── CreateInvoiceDialog.vue │ │ ├── DisplayTokenComponent.vue │ │ ├── EditMintDialog.vue │ │ ├── EssentialLink.vue │ │ ├── FullscreenHeader.vue │ │ ├── HistoryTable.vue │ │ ├── InvoiceDetailDialog.vue │ │ ├── MainHeader.vue │ │ ├── MeltQuoteInformation.vue │ │ ├── MintAuditInfo.vue │ │ ├── MintAuditSwapsBarChart.vue │ │ ├── MintAuditWarningBox.vue │ │ ├── MintDiscovery.vue │ │ ├── MintInfoContainer.vue │ │ ├── MintMotdMessage.vue │ │ ├── MintQuoteInformation.vue │ │ ├── MintRatingsComponent.vue │ │ ├── MintSettings.vue │ │ ├── MultinutPaymentDialog.vue │ │ ├── NWCDialog.vue │ │ ├── NoMintWarnBanner.vue │ │ ├── NostrMintRestore.vue │ │ ├── NumericKeyboard.vue │ │ ├── P2PKDialog.vue │ │ ├── ParseInputComponent.vue │ │ ├── PayInvoiceDialog.vue │ │ ├── PaymentRequestDialog.vue │ │ ├── PaymentRequestInfo.vue │ │ ├── PaymentRequestPayments.vue │ │ ├── QrcodeReader.vue │ │ ├── ReceiveDialog.vue │ │ ├── ReceiveEcashDrawer.vue │ │ ├── ReceiveTokenDialog.vue │ │ ├── RemoveMintDialog.vue │ │ ├── RestoreView.vue │ │ ├── SendDialog.vue │ │ ├── SendPaymentRequest.vue │ │ ├── SendTokenDialog.vue │ │ ├── SettingsView.vue │ │ ├── SwapIncomingTokenToKnownMint.vue │ │ ├── ToggleUnit.vue │ │ ├── TokenInformation.vue │ │ ├── TokenStringRender.vue │ │ ├── ToolTipInfo.vue │ │ ├── WelcomeDialog.vue │ │ └── iOSPWAPrompt.vue │ ├── css/ │ │ ├── app.scss │ │ ├── base.scss │ │ ├── mintlist.css │ │ └── quasar.variables.scss │ ├── i18n/ │ │ ├── ar-SA/ │ │ │ └── index.ts │ │ ├── cs-CZ/ │ │ │ └── index.ts │ │ ├── de-DE/ │ │ │ └── index.ts │ │ ├── el-GR/ │ │ │ └── index.ts │ │ ├── en-US/ │ │ │ └── index.ts │ │ ├── es-ES/ │ │ │ └── index.ts │ │ ├── fr-FR/ │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── it-IT/ │ │ │ └── index.ts │ │ ├── ja-JP/ │ │ │ └── index.ts │ │ ├── pt-BR/ │ │ │ └── index.ts │ │ ├── sv-SE/ │ │ │ └── index.ts │ │ ├── th-TH/ │ │ │ └── index.ts │ │ ├── tr-TR/ │ │ │ └── index.ts │ │ └── zh-CN/ │ │ └── index.ts │ ├── icons.js │ ├── js/ │ │ ├── __tests__/ │ │ │ ├── legacy-qr.test.js │ │ │ └── token.test.js │ │ ├── base64.js │ │ ├── dhke.js │ │ ├── eventBus.js │ │ ├── legacy-qr.js │ │ ├── notify.ts │ │ ├── string-utils.js │ │ ├── token.ts │ │ ├── utils.js │ │ └── wallet-helpers.js │ ├── layouts/ │ │ ├── BlankLayout.vue │ │ ├── FullscreenLayout.vue │ │ └── MainLayout.vue │ ├── main.js │ ├── pages/ │ │ ├── AlreadyRunning.vue │ │ ├── CreateMintReviewPage.vue │ │ ├── ErrorNotFound.vue │ │ ├── MintDetailsPage.vue │ │ ├── MintDiscoveryPage.vue │ │ ├── MintRatingsPage.vue │ │ ├── Restore.vue │ │ ├── Settings.vue │ │ ├── TermsPage.vue │ │ ├── WalletPage.vue │ │ ├── WelcomePage.vue │ │ └── welcome/ │ │ ├── WelcomeMintSetup.vue │ │ ├── WelcomeRecoverSeed.vue │ │ ├── WelcomeRestoreEcash.vue │ │ ├── WelcomeSlide1.vue │ │ ├── WelcomeSlide2.vue │ │ ├── WelcomeSlide3.vue │ │ ├── WelcomeSlide4.vue │ │ └── WelcomeSlideChoice.vue │ ├── router/ │ │ ├── index.js │ │ └── routes.js │ └── stores/ │ ├── __tests__/ │ │ └── wallet.test.js │ ├── camera.ts │ ├── dexie.ts │ ├── index.js │ ├── invoicesWorker.ts │ ├── migrations.ts │ ├── mintRecommendations.ts │ ├── mints.ts │ ├── nostr.ts │ ├── nostrMintBackup.ts │ ├── nostrUser.ts │ ├── npcv2.ts │ ├── npubcash.ts │ ├── nwc.ts │ ├── p2pk.ts │ ├── payment-request.ts │ ├── price.ts │ ├── proofs.ts │ ├── receiveTokensStore.ts │ ├── restore.ts │ ├── sendTokensStore.ts │ ├── settings.ts │ ├── storage.ts │ ├── store-flag.d.ts │ ├── swap.ts │ ├── tokens.ts │ ├── ui.ts │ ├── wallet.ts │ ├── welcome.ts │ └── workers.ts ├── src-electron/ │ ├── electron-env.d.ts │ ├── electron-flag.d.ts │ ├── electron-main.ts │ ├── electron-preload.ts │ └── icons/ │ └── icon.icns ├── src-pwa/ │ ├── custom-service-worker.js │ ├── manifest.json │ ├── pwa-flag.d.ts │ └── register-service-worker.js ├── test/ │ └── vitest/ │ ├── __tests__/ │ │ └── bip39seed.test.ts │ └── setup-file.js ├── tsconfig.json ├── types/ │ └── light-bolt11-decoder/ │ └── index.d.ts └── vitest.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 indent_style = space indent_size = 2 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true ================================================ FILE: .eslintignore ================================================ /dist /src-bex/www /src-capacitor /src-cordova /.quasar /node_modules .eslintrc.js babel.config.js **/cashu-ts/dist/** ================================================ FILE: .eslintrc.js ================================================ module.exports = { // https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy // This option interrupts the configuration hierarchy at this file // Remove this if you have an higher level ESLint config file (it usually happens into a monorepos) root: true, parserOptions: { parser: require.resolve("@typescript-eslint/parser"), ecmaVersion: "2021", // Allows for the parsing of modern ECMAScript features }, env: { browser: true, "vue/setup-compiler-macros": true, }, // Rules order is important, please avoid shuffling them extends: [ // Base ESLint recommended rules "eslint:recommended", // Uncomment any of the lines below to choose desired strictness, // but leave only one uncommented! // See https://eslint.vuejs.org/rules/#available-rules "plugin:vue/vue3-essential", // Priority A: Essential (Error Prevention) // 'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability) // 'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead) // https://github.com/prettier/eslint-config-prettier#installation // usage with Prettier, provided by 'eslint-config-prettier'. "prettier", ], plugins: [ // https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-files // required to lint *.vue files "vue", // https://github.com/typescript-eslint/typescript-eslint/issues/389#issuecomment-509292674 // Prettier has not been included as plugin to avoid performance impact // add it as an extension for your IDE ], globals: { ga: "readonly", // Google Analytics cordova: "readonly", __statics: "readonly", __QUASAR_SSR__: "readonly", __QUASAR_SSR_SERVER__: "readonly", __QUASAR_SSR_CLIENT__: "readonly", __QUASAR_SSR_PWA__: "readonly", process: "readonly", Capacitor: "readonly", chrome: "readonly", }, // add your custom rules here rules: { "prefer-promise-reject-errors": "off", // allow debugger during development only "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off", "no-var": "error", "no-const-assign": "error", "prefer-const": [ "error", { destructuring: "any", ignoreReadBeforeAssign: false, }, ], // remove some warnings/errors from eslint:recommended for now // which are quite common in the current codebase // we will deal with them later on "no-unused-vars": "off", "no-undef": "off", "no-empty": "off", "no-useless-catch": "off", "no-constant-condition": "off", }, overrides: [ { files: ["**/*.{js,ts}"], // If the `script` part of a Vue component is stored in a separate JS/TS file, // as is the case when using DFC (https://testing.quasar.dev/packages/unit-jest/#double-file-components-dfc), // Vue ESLint plugin will highlight all public properties as unused // as it's not able to detect their usage into the template // We disable this rule and only keep it for Vue files rules: { "vue/no-unused-properties": "off" }, }, { files: ["*.vue"], parser: "vue-eslint-parser", parserOptions: { parser: "@typescript-eslint/parser", }, rules: { // Disallow ================================================ FILE: src/boot/.gitkeep ================================================ ================================================ FILE: src/boot/axios.js ================================================ // src/boot/axios.js import { boot } from "quasar/wrappers"; import axios from "axios"; // const api = axios.create({ baseURL: 'https://api.example.com' }) export default boot(({ app }) => { // for use inside Vue files (Options API) through this.$axios and this.$api app.config.globalProperties.$axios = axios; // ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form) // so you won't necessarily have to import axios in each vue file // app.config.globalProperties.$api = api // ^ ^ ^ this will allow you to use this.$api (for Vue Options API form) // so you can easily perform requests against your app's API }); // export { axios, api } export { axios }; ================================================ FILE: src/boot/base.js ================================================ import { copyToClipboard } from "quasar"; import { useUiStore } from "stores/ui"; import { Clipboard } from "@capacitor/clipboard"; import { SafeArea } from "capacitor-plugin-safe-area"; import { useSettingsStore } from "stores/settings"; window.LOCALE = "en"; // window.EventHub = new Vue(); // Ensure we capture the PWA install prompt as early as possible if (typeof window !== "undefined") { if (!window.__deferredBeforeInstallPrompt) { window.__deferredBeforeInstallPrompt = null; } window.addEventListener( "beforeinstallprompt", (e) => { // Allow custom install UI by deferring the prompt e.preventDefault(); window.__deferredBeforeInstallPrompt = e; // Notify any listeners that install is available try { window.dispatchEvent(new CustomEvent("bip-available")); } catch (err) { // noop } }, { once: false } ); window.addEventListener("appinstalled", () => { window.__deferredBeforeInstallPrompt = null; }); } // ---- PWA status bar color sync helpers ---- function ensureMetaTag(name, initialContent) { let el = document.querySelector(`meta[name="${name}"]`); if (!el) { el = document.createElement("meta"); el.setAttribute("name", name); if (initialContent != null) { el.setAttribute("content", initialContent); } document.head.appendChild(el); } return el; } function resolveEffectiveTopBackgroundColor() { const header = document.querySelector(".q-header"); const isTransparent = (val) => !val || val === "transparent" || (val.startsWith("rgba") && parseFloat(val.split(",")[3]) === 0); const getBg = (el) => el ? window.getComputedStyle(el).backgroundColor || "" : ""; let color = getBg(header); if (isTransparent(color)) { const layout = document.querySelector(".q-layout"); color = getBg(layout); if (isTransparent(color)) { const pageContainer = document.querySelector(".q-page-container"); color = getBg(pageContainer); } if (isTransparent(color)) { color = getBg(document.body); } } return color || "#000000"; } function updateStatusBarMeta() { try { const iosBar = ensureMetaTag( "apple-mobile-web-app-status-bar-style", "black-translucent" ); iosBar.setAttribute("content", "black-translucent"); const themeMeta = ensureMetaTag("theme-color", "#000000"); const color = resolveEffectiveTopBackgroundColor(); themeMeta.setAttribute("content", color); } catch { // noop } } // ------------------------------------------- window.windowMixin = { data: function () { return { g: { offline: !navigator.onLine, visibleDrawer: false, extensions: [], user: null, wallet: null, payments: [], allowedThemes: null, }, }; }, methods: { changeColor: function (newValue) { document.body.setAttribute("data-theme", newValue); this.$q.localStorage.set("cashu.theme", newValue); updateStatusBarMeta(); }, changeLanguage: function (e) { this.$q.localStorage.set("cashu.language", e.target.value); }, toggleDarkMode: function () { this.$q.dark.toggle(); this.$q.localStorage.set("cashu.darkMode", this.$q.dark.isActive); updateStatusBarMeta(); }, copyText: function (text, message, position) { const notify = this.$q.notify; const i18n = this.$i18n; copyToClipboard(text).then(function () { notify({ message: message || (i18n && i18n.t("global.copy_to_clipboard.success")) || "Copied to clipboard!", position: position || "bottom", }); }); }, pasteFromClipboard: async function () { let text = ""; if (window?.Capacitor) { const { value } = await Clipboard.read(); text = value; } else { text = await navigator.clipboard.readText(); } return text; }, formatCurrency: function (value, currency, showBalance = false) { if (currency == undefined) { currency = "sat"; } if (useUiStore().hideBalance && !showBalance) { return "****"; } if (currency == "sat") return this.formatSat(value); if (currency == "msat") return this.fromMsat(value); if (currency == "usd") value = value / 100; if (currency == "eur") value = value / 100; return new Intl.NumberFormat(window.LOCALE, { style: "currency", currency: currency, }).format(value); // + " " + // currency.toUpperCase() }, formatSat: function (value) { // convert value to integer value = parseInt(value); if (useSettingsStore().bip177BitcoinSymbol) { if (value >= 0) { return "₿" + new Intl.NumberFormat(window.LOCALE).format(value); } else { return ( "-₿" + new Intl.NumberFormat(window.LOCALE).format(Math.abs(value)) ); } } return new Intl.NumberFormat(window.LOCALE).format(value) + " sat"; }, fromMsat: function (value) { value = parseInt(value); return new Intl.NumberFormat(window.LOCALE).format(value) + " msat"; }, notifyApiError: function (error) { const types = { 400: "warning", 401: "warning", 500: "negative", }; this.$q.notify({ timeout: 5000, type: types[error.response.status] || "warning", message: error.message || error.response.data.message || error.response.data.detail || null, caption: [error.response.status, " ", error.response.statusText] .join("") .toUpperCase() || null, icon: null, }); }, notifySuccess: async function (message, position = "top") { this.$q.notify({ timeout: 5000, type: "positive", message: message, position: position, progress: true, actions: [ { icon: "close", color: "white", handler: () => {}, }, ], }); }, notifyRefreshed: async function (message, position = "top") { this.$q.notify({ timeout: 500, type: "positive", message: message, position: position, actions: [ { color: "white", handler: () => {}, }, ], }); }, notifyError: async function (message, caption = null) { this.$q.notify({ color: "red", message: message, caption: caption, position: "top", progress: true, actions: [ { icon: "close", color: "white", handler: () => {}, }, ], }); }, notifyWarning: async function (message, caption = null, timeout = 5000) { this.$q.notify({ timeout: timeout, type: "warning", message: message, caption: caption, position: "top", progress: true, actions: [ { icon: "close", color: "black", handler: () => {}, }, ], }); }, notify: async function ( message, type = "null", position = "top", caption = null, color = null ) { // failure this.$q.notify({ timeout: 5000, type: "nuill", color: "grey", message: message, caption: null, position: "top", actions: [ { icon: "close", color: "white", handler: () => {}, }, ], }); }, }, created: function () { if ( this.$q.localStorage.getItem("cashu.darkMode") == true || this.$q.localStorage.getItem("cashu.darkMode") == false ) { this.$q.dark.set(this.$q.localStorage.getItem("cashu.darkMode")); } else { this.$q.dark.set(true); } this.g.allowedThemes = window.allowedThemes ?? ["classic"]; addEventListener("offline", (event) => { this.g.offline = true; }); addEventListener("online", (event) => { this.g.offline = false; }); // addEventListener("beforeunload", (event) => { // event.preventDefault(); // const dialogText = "Are you sure about this?"; // event.returnValue = dialogText; // return dialogText; // }); if (this.$q.localStorage.getItem("cashu.theme")) { document.body.setAttribute( "data-theme", this.$q.localStorage.getItem("cashu.theme") ); } else { this.changeColor("monochrome"); } // Initial status bar sync and observers for changes updateStatusBarMeta(); window.addEventListener("resize", updateStatusBarMeta); window.addEventListener("orientationchange", updateStatusBarMeta); try { const header = document.querySelector(".q-header"); const observer = new MutationObserver(updateStatusBarMeta); if (header) { observer.observe(header, { attributes: true, attributeFilter: ["class", "style"], }); } observer.observe(document.body, { attributes: true, attributeFilter: ["class", "style", "data-theme"], }); } catch { // noop } const language = this.$q.localStorage.getItem("cashu.language"); if (language) { this.$i18n.locale = language; } // only for iOS if (window.Capacitor && Capacitor.getPlatform() === "ios") { SafeArea.getStatusBarHeight().then(({ statusBarHeight }) => { document.documentElement.style.setProperty( `--safe-area-inset-top`, `${statusBarHeight}px` ); }); SafeArea.removeAllListeners(); // when safe-area changed SafeArea.addListener("safeAreaChanged", (data) => { const { insets } = data; for (const [key, value] of Object.entries(insets)) { document.documentElement.style.setProperty( `--safe-area-inset-${key}`, `${value}px` ); } }); } }, }; ================================================ FILE: src/boot/cashu.js ================================================ /** * Configures the Cashu-ts library axios client */ // import { setupAxios } from "@cashu/cashu-ts"; // export default () => { // setupAxios({ // // Default timeout for any interaction using the cashu-ts library to interact with a mint // timeout: 15 * 1000, // 15 seconds // }); // }; ================================================ FILE: src/boot/global-components.js ================================================ import { boot } from "quasar/wrappers"; import VueQrcode from "@chenfengyuan/vue-qrcode"; // "async" is optional; // more info on params: https://v2.quasar.dev/quasar-cli/boot-files export default boot(async ({ app }) => { app.component(VueQrcode.name, VueQrcode); }); ================================================ FILE: src/boot/i18n.js ================================================ import { boot } from "quasar/wrappers"; import { createI18n } from "vue-i18n"; import messages from "src/i18n"; // Get stored locale from localStorage or fallback to browser language or en-US const storedLocale = localStorage.getItem("cashu.language") || navigator.language || "en-US"; export const i18n = createI18n({ locale: storedLocale, fallbackLocale: "en-US", globalInjection: true, messages, }); export default boot(async ({ app }) => { app.use(i18n); }); ================================================ FILE: src/components/ActivityOrb.vue ================================================ ================================================ FILE: src/components/AddMintDialog.vue ================================================ ================================================ FILE: src/components/AmountInputComponent.vue ================================================ ================================================ FILE: src/components/AndroidPWAPrompt.vue ================================================ ================================================ FILE: src/components/AnimatedNumber.vue ================================================ ================================================ FILE: src/components/BalanceView.vue ================================================ ================================================ FILE: src/components/ChooseMint.vue ================================================ ================================================ FILE: src/components/CreateInvoiceDialog.vue ================================================ ================================================ FILE: src/components/DisplayTokenComponent.vue ================================================ ================================================ FILE: src/components/EditMintDialog.vue ================================================ ================================================ FILE: src/components/EssentialLink.vue ================================================ ================================================ FILE: src/components/FullscreenHeader.vue ================================================ ================================================ FILE: src/components/HistoryTable.vue ================================================ ================================================ FILE: src/components/InvoiceDetailDialog.vue ================================================ ================================================ FILE: src/components/MainHeader.vue ================================================ ================================================ FILE: src/components/MeltQuoteInformation.vue ================================================ ================================================ FILE: src/components/MintAuditInfo.vue ================================================ ================================================ FILE: src/components/MintAuditSwapsBarChart.vue ================================================ ================================================ FILE: src/components/MintAuditWarningBox.vue ================================================ ================================================ FILE: src/components/MintDiscovery.vue ================================================ ================================================ FILE: src/components/MintInfoContainer.vue ================================================ ================================================ FILE: src/components/MintMotdMessage.vue ================================================ ================================================ FILE: src/components/MintQuoteInformation.vue ================================================ ================================================ FILE: src/components/MintRatingsComponent.vue ================================================ ================================================ FILE: src/components/MintSettings.vue ================================================ ================================================ FILE: src/components/MultinutPaymentDialog.vue ================================================ ================================================ FILE: src/components/NWCDialog.vue ================================================ ================================================ FILE: src/components/NoMintWarnBanner.vue ================================================ ================================================ FILE: src/components/NostrMintRestore.vue ================================================ ================================================ FILE: src/components/NumericKeyboard.vue ================================================ ================================================ FILE: src/components/P2PKDialog.vue ================================================ ================================================ FILE: src/components/ParseInputComponent.vue ================================================ ================================================ FILE: src/components/PayInvoiceDialog.vue ================================================ ================================================ FILE: src/components/PaymentRequestDialog.vue ================================================ ================================================ FILE: src/components/PaymentRequestInfo.vue ================================================ ================================================ FILE: src/components/PaymentRequestPayments.vue ================================================ ================================================ FILE: src/components/QrcodeReader.vue ================================================ ================================================ FILE: src/components/ReceiveDialog.vue ================================================ ================================================ FILE: src/components/ReceiveEcashDrawer.vue ================================================ ================================================ FILE: src/components/ReceiveTokenDialog.vue ================================================ ================================================ FILE: src/components/RemoveMintDialog.vue ================================================ ================================================ FILE: src/components/RestoreView.vue ================================================ ================================================ FILE: src/components/SendDialog.vue ================================================ ================================================ FILE: src/components/SendPaymentRequest.vue ================================================ ================================================ FILE: src/components/SendTokenDialog.vue ================================================ ================================================ FILE: src/components/SettingsView.vue ================================================ ================================================ FILE: src/components/SwapIncomingTokenToKnownMint.vue ================================================ ================================================ FILE: src/components/ToggleUnit.vue ================================================ ================================================ FILE: src/components/TokenInformation.vue ================================================ ================================================ FILE: src/components/TokenStringRender.vue ================================================ ================================================ FILE: src/components/ToolTipInfo.vue ================================================ ================================================ FILE: src/components/WelcomeDialog.vue ================================================ ================================================ FILE: src/components/iOSPWAPrompt.vue ================================================ ================================================ FILE: src/css/app.scss ================================================ // app global css in SCSS form // Import Inter font from Google Fonts @import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"); // Apply Inter as the primary font throughout the application body { font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; } // Dialog header typography // This class provides proper letter-spacing for dialog titles // The default Quasar 'overline' class has excessive letter-spacing for headers .dialog-header { font-size: 1.2rem; // 16px font-weight: 500; // Medium weight for headers letter-spacing: -0.01em; // Tight letter spacing for better readability on headers text-transform: none; // No uppercase transformation line-height: 1.5; // Good line height for readability } // prevent pull to refresh html, body { overscroll-behavior: none; } // Map CSS env() safe area to our custom variable for PWA iOS :root { --safe-area-inset-top: env(safe-area-inset-top); } // margin for top elements for ios safe area body, .q-drawer, .q-header, .q-dialog__inner > div { margin-top: var(--safe-area-inset-top); } .q-dialog__inner > div { border-top-left-radius: 0px; border-top-right-radius: 0px; border-bottom-right-radius: 20px !important; border-bottom-left-radius: 20px !important; } .q-card { border-top-left-radius: 20px; border-top-right-radius: 20px; } .q-dialog__inner--minimized > div { border-top-left-radius: 20px; border-top-right-radius: 20px; border-bottom-right-radius: 20px; border-bottom-left-radius: 20px; max-width: 650px; } .custom-btn { background: $grey-9; color: white; border-radius: 8px; height: 80px; box-shadow: none; font-size: 18px; } .custom-btn:hover { background: $grey-8; } .custom-btn-text { font-size: 18px !important; color: white; padding-top: 2px; } .full-width-card { width: 100%; max-width: 600px; margin: 0 auto; } .animated.tada { animation-duration: 1s; } .animated.bounceIn { animation-duration: 1s; } .animated.bounce { animation-duration: 1s; } .q-checkbox__svg { color: var(--q-dark); } ================================================ FILE: src/css/base.scss ================================================ $themes: ( "classic": ( primary: #935af5, secondary: #b45af5, dark: #1f2234, info: #333646, marginal-bg: #1f2234, marginal-text: #fff, ), "bitcoin": ( primary: #ff9853, secondary: #ff8753, dark: #2d293b, info: #333646, marginal-bg: #2d293b, marginal-text: #fff, ), "freedom": ( primary: #e22156, secondary: #b91a45, dark: #000, info: #1b1b1b, marginal-bg: #000, marginal-text: #fff, ), "salvador": ( primary: #2d68d5, secondary: #1366cb, dark: #242424, info: #333646, marginal-bg: #242424, marginal-text: #fff, ), "mint": ( primary: #3ab77d, secondary: #27b065, dark: #1f342b, info: #334642, marginal-bg: #1f342b, marginal-text: #fff, ), "autumn": ( primary: #b7763a, secondary: #b07927, dark: #34291f, info: #463f33, marginal-bg: #342a1f, marginal-text: rgb(255, 255, 255), ), "flamingo": ( primary: #ff64b4, secondary: #ff61b3, dark: #56353f, info: #56353a, marginal-bg: #56353a, marginal-text: rgb(255, 255, 255), ), "monochrome": ( primary: #ededed, secondary: #d5d5d5, dark: #000, info: rgb(39, 39, 39), marginal-bg: #000, marginal-text: rgb(255, 255, 255), ), "cyber": ( primary: #00ff00, secondary: #00ff00, dark: #000, info: #1b1b1b, marginal-bg: #000, marginal-text: rgb(255, 255, 255), ), ); @each $theme, $colors in $themes { @each $name, $color in $colors { @if $name== "dark" { [data-theme="#{$theme}"] .q-drawer--dark, body[data-theme="#{$theme}"].body--dark, [data-theme="#{$theme}"] .q-menu--dark { background: $color !important; } // set a darker body bg for all themes, when in "dark mode" body[data-theme="#{$theme}"].body--dark { background: scale-color($color, $lightness: -60%); } } @if $name== "info" { [data-theme="#{$theme}"] .q-card--dark, [data-theme="#{$theme}"] .q-stepper--dark { background: $color !important; } } } [data-theme="#{$theme}"] { @each $name, $color in $colors { .bg-#{$name} { background: $color !important; } .text-#{$name} { color: $color !important; } } } } @each $theme, $colors in $themes { [data-theme="#{$theme}"] { @each $name, $color in $colors { @if $name == "primary" { --q-primary: #{$color}; } @if $name == "secondary" { --q-secondary: #{$color}; } } @each $name, $color in $colors { .bg-#{$name} { background: $color !important; } .text-#{$name} { color: $color !important; } } } } [data-theme="monochrome"] .q-badge.bg-primary, [data-theme="cyber"] .q-badge.bg-primary { background: primary !important; color: #0a0a0a !important; } [data-theme="monochrome"] .q-btn.bg-primary, [data-theme="cyber"] .q-btn.bg-primary { background: primary !important; color: #0a0a0a !important; } [data-theme="freedom"] .q-drawer--dark { background: #0a0a0a !important; } [data-theme="freedom"] .q-header { background: #0a0a0a !important; } [v-cloak] { display: none; } body.body--dark .q-table--dark { background: transparent; } body.body--dark .q-field--error { .text-negative, .q-field__messages { color: yellow !important; } } .qcard { width: 500px; } .q-table--dense { th:first-child, td:first-child, .q-table__bottom { padding-left: 6px !important; } th:last-child, td:last-child, .q-table__bottom { padding-right: 6px !important; } } a { color: var(--primary-color); text-decoration: underline; font-weight: bold; } a.inherit { color: inherit; text-decoration: none; } // QR video video { border-radius: 3px; } // Material icons font @font-face { font-family: "Material Icons"; font-style: normal; font-weight: 400; src: url(../fonts/material-icons-v50.woff2) format("woff2"); } .material-icons { font-family: "Material Icons"; font-weight: normal; font-style: normal; font-size: 24px; line-height: 1; letter-spacing: normal; text-transform: none; display: inline-block; white-space: nowrap; word-wrap: normal; direction: ltr; -moz-font-feature-settings: "liga"; font-feature-settings: "liga"; -moz-osx-font-smoothing: grayscale; } .q-rating__icon { font-size: 1em; } // text-wrap .text-wrap { word-break: break-word; } .q-card { code { overflow-wrap: break-word; } } .q-card { box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2), 0 2px 2px rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.12); border-radius: 4px; vertical-align: top; } .shadow-2 { box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2), 0 2px 2px rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.12); } ::-webkit-scrollbar { display: none; } ================================================ FILE: src/css/mintlist.css ================================================ /* Shared styles for mint list items across MintSettings, RestoreView, and NostrMintRestore */ /* Common mint card styling */ .mint-card { border-radius: 10px; border: 1px solid rgba(128, 128, 128, 0.2); padding: 0px; position: relative; transition: border-color 0.2s ease; } .mint-card:hover { border-color: rgba(128, 128, 128, 0.4) !important; } .mint-card.q-loading { opacity: 0.5; pointer-events: none; } /* Mint information container */ .mint-info-container { display: flex; flex-direction: column; min-width: 0; /* This is crucial for text wrapping */ flex: 1; } /* Mint name styling with proper text wrapping */ .mint-name { text-align: left; font-size: 16px; font-weight: 600; line-height: 20px; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; white-space: normal; max-width: 100%; } /* Mint URL styling with proper text wrapping */ .mint-url { text-align: left; font-size: 12px; line-height: 16px; font-family: monospace !important; margin-top: 4px; word-wrap: break-word; overflow-wrap: break-word; word-break: break-all; /* For URLs, break-all is better */ white-space: normal; max-width: 100%; } /* Currency unit badges */ .currency-unit-badge { border-radius: 4px; background-color: #1d1d1d; display: inline-block; padding: 4px 8px; margin: 4px 4px 4px 0; } .currency-unit-text { color: white; font-size: 14px; font-weight: 500; } /* Loading spinner positioning */ .mint-loading-spinner { position: absolute; top: 12px; right: 30px; z-index: 10; } /* Error badge positioning */ .error-badge { position: absolute; top: 4px; right: 30px; z-index: 10; } /* Transition animations */ .fade-enter-active, .fade-leave-active { transition: transform 1s ease, opacity 1s ease; } /* Selection buttons styling */ .selection-buttons { display: flex; align-items: center; justify-content: flex-start; } /* Primary action section */ .primary-action-section { display: flex; align-items: center; justify-content: flex-start; } /* Clickable checkbox */ .clickable-checkbox { cursor: pointer; } /* Mint item selected state */ .mint-item-selected { border-color: var(--q-primary) !important; background-color: rgba(var(--q-primary-rgb), 0.1); } /* Ensure text alignment is consistent */ .text-left { text-align: left !important; } ================================================ FILE: src/css/quasar.variables.scss ================================================ // Quasar SCSS (& Sass) Variables // -------------------------------------------------- // To customize the look and feel of this app, you can override // the Sass/SCSS variables found in Quasar's source Sass/SCSS files. // Check documentation for full list of Quasar variables // Your own variables (that are declared here) and Quasar's own // ones will be available out of the box in your .vue/.scss/.sass files // It's highly recommended to change the default colors // to match your app's branding. // Tip: Use the "Theme Builder" on Quasar's documentation website. $primary: #1976d2; $secondary: #26a69a; $accent: #9c27b0; $dark: #1d1d1d; $dark-page: #121212; $positive: #21ba45; $negative: #c10015; $info: #31ccec; $warning: #f2c037; ================================================ FILE: src/i18n/ar-SA/index.ts ================================================ export default { MultinutPicker: { payment: "دفع متعدد الجوز", selectMints: "حدد واحدًا أو أكثر من mints لتنفيذ الدفع منه.", totalSelectedBalance: "إجمالي الرصيد المحدد", multiMintPay: "دفع متعدد Mint", balanceNotEnough: "رصيد متعدد mints غير كافٍ لتلبية هذه الفاتورة", failed: "فشل في المعالجة: {error}", paid: "تم دفع {amount} عبر Lightning", }, global: { copy_to_clipboard: { success: "تم النسخ إلى الحافظة!", }, actions: { add_mint: { label: "إضافة Mint", }, cancel: { label: "إلغاء", }, copy: { label: "نسخ", }, close: { label: "إغلاق", }, enter: { label: "إدخال", }, lock: { label: "قفل", }, paste: { label: "لصق", }, receive: { label: "استلام", }, scan: { label: "مسح ضوئي", }, send: { label: "إرسال", }, swap: { label: "تبديل", }, update: { label: "تحديث", }, }, inputs: { mint_url: { label: "عنوان URL للـ Mint", }, }, }, wallet: { notifications: { balance_too_low: "الرصيد منخفض جدًا", received: "تم استلام {amount}", fee: " (رسوم: {fee})", could_not_request_mint: "لا يمكن طلب الإنشاء", invoice_still_pending: "الفاتورة لا تزال معلقة", paid_lightning: "تم دفع {amount} عبر شبكة لايتنينج", payment_pending_refresh: "الدفع معلق. قم بتحديث الفاتورة يدويًا.", sent: "تم إرسال {amount}", token_still_pending: "الرمز لا يزال معلقًا", received_lightning: "تم استلام {amount} عبر شبكة لايتنينج", lightning_payment_failed: "فشل الدفع عبر لايتنينج", failed_to_decode_invoice: "فشل فك ترميز الفاتورة", invalid_lnurl: "LNURL غير صالح", lnurl_error: "خطأ في LNURL", no_amount: "لا يوجد مبلغ", no_lnurl_data: "لا توجد بيانات LNURL", no_price_data: "لا توجد بيانات سعرية.", please_try_again: "يرجى المحاولة مرة أخرى.", }, mint: { notifications: { already_added: "تمت إضافة الـ Mint بالفعل", added: "تمت إضافة الـ Mint", not_found: "لم يتم العثور على الـ Mint", activation_failed: "فشل تنشيط الـ Mint", no_active_mint: "لا يوجد Mint نشط", unit_activation_failed: "فشل تنشيط الوحدة", unit_not_supported: "الوحدة غير مدعومة من قبل الـ Mint", activated: "تم تنشيط الـ Mint", could_not_connect: "تعذر الاتصال بالـ Mint", could_not_get_info: "تعذر الحصول على معلومات الـ Mint", could_not_get_keys: "تعذر الحصول على مفاتيح الـ Mint", could_not_get_keysets: "تعذر الحصول على مجموعات مفاتيح الـ Mint", mint_validation_error: "خطأ في التحقق من صحة mint", removed: "تمت إزالة الـ Mint", error: "خطأ في الـ Mint", }, }, }, MainHeader: { menu: { settings: { title: "الإعدادات", settings: { title: "الإعدادات", caption: "تهيئة المحفظة", }, }, terms: { title: "الشروط", terms: { title: "الشروط", caption: "شروط الخدمة", }, }, links: { title: "روابط", cashuSpace: { title: "Cashu.space", caption: "cashu.space", }, github: { title: "Github", caption: "github.com/cashubtc", }, telegram: { title: "Telegram", caption: "t.me/CashuMe", }, twitter: { title: "Twitter", caption: "{'@'}CashuBTC", }, donate: { title: "تبرع", caption: "دعم Cashu", }, }, }, offline: { warning: { text: "غير متصل", }, }, reload: { warning: { text: "إعادة التحميل في { countdown }", }, }, staging: { warning: { text: "المرحلة التجريبية – لا تستخدم مع أموال حقيقية!", }, }, }, FullscreenHeader: { actions: { back: { label: "المحفظة", }, }, }, Settings: { language: { title: "اللغة", description: "الرجاء اختيار لغتك المفضلة من القائمة أدناه.", }, sections: { backup_restore: "النسخ الاحتياطي والاستعادة", lightning_address: "عنوان LIGHTNING", nostr_keys: "مفاتيح NOSTR", nostr: { title: "NOSTR", relays: { expand_label: "انقر لتعديل المرحلات", add: { title: "إضافة مُرحِل", description: "تستخدم محفظتك هذه المُرحِلات لعمليات nostr مثل طلبات الدفع وربط محفظة nostr والنسخ الاحتياطية.", }, list: { title: "المُرحِلات", description: "ستتصل محفظتك بهذه المُرحِلات.", copy_tooltip: "نسخ المُرحِل", remove_tooltip: "إزالة المُرحِل", }, }, }, payment_requests: "طلبات الدفع", nostr_wallet_connect: "اتصال محفظة NOSTR", hardware_features: "ميزات الأجهزة", p2pk_features: "ميزات P2PK", privacy: "الخصوصية", experimental: "تجريبي", appearance: "المظهر", }, backup_restore: { backup_seed: { title: "نسخ عبارة الاستعادة احتياطيًا", description: "يمكن لعبارة الاستعادة الخاصة بك استعادة محفظتك. احتفظ بها آمنة وخصوصية.", seed_phrase_label: "عبارة الاستعادة", }, restore_ecash: { title: "استعادة ecash", description: "يتيح لك معالج الاستعادة استرداد ecash المفقود من عبارة استعادة. لن تتأثر عبارة استعادة محفظتك الحالية، وسيسمح لك المعالج فقط باستعادة ecash من عبارة استعادة أخرى.", button: "استعادة", }, }, lightning_address: { title: "عنوان Lightning", description: "استلام المدفوعات إلى عنوان Lightning الخاص بك.", enable: { toggle: "تمكين", description: "عنوان Lightning مع npub.cash", }, address: { copy_tooltip: "نسخ عنوان Lightning", }, automatic_claim: { toggle: "المطالبة تلقائيًا", description: "استلام المدفوعات الواردة تلقائيًا.", }, npc_v2: { choose_mint_title: "اختر mint لـ npub.cash v2", choose_mint_placeholder: "حدد mint...", }, }, nostr_keys: { title: "مفاتيح nostr الخاصة بك", description: "قم بتعيين مفاتيح nostr لعنوان Lightning الخاص بك.", wallet_seed: { title: "عبارة استعادة المحفظة", description: "إنشاء زوج مفاتيح nostr من عبارة استعادة المحفظة", copy_nsec: "نسخ nsec", }, nsec_bunker: { title: "Nsec Bunker", description: "استخدام مخبأ NIP-46", delete_tooltip: "حذف الاتصال", }, use_nsec: { title: "استخدام nsec الخاص بك", description: "هذه الطريقة خطيرة ولا ينصح بها", delete_tooltip: "حذف nsec", }, signing_extension: { title: "ملحق التوقيع", description: "استخدام ملحق التوقيع NIP-07", not_found: "لم يتم العثور على ملحق التوقيع NIP-07", }, }, payment_requests: { title: "طلبات الدفع", description: "تسمح لك طلبات الدفع بتلقي المدفوعات عبر nostr. إذا قمت بتمكين هذا، ستقوم محفظتك بالاشتراك في مرحلات nostr الخاصة بك.", enable_toggle: "تمكين طلبات الدفع", claim_automatically: { toggle: "المطالبة تلقائيًا", description: "استلام المدفوعات الواردة تلقائيًا.", }, }, nostr_wallet_connect: { title: "اتصال محفظة Nostr (NWC)", description: "استخدم NWC للتحكم في محفظتك من أي تطبيق آخر.", enable_toggle: "تمكين NWC", payments_note: "يمكنك فقط استخدام NWC للمدفوعات من رصيد Bitcoin الخاص بك. ستتم المدفوعات من mint النشط الخاص بك.", connection: { copy_tooltip: "نسخ سلسلة الاتصال", qr_tooltip: "إظهار رمز QR", allowance_label: "المسموح به المتبقي (سات)", }, }, hardware_features: { webnfc: { title: "WebNFC", description: "اختر الترميز للكتابة على بطاقات NFC", text: { title: "نص", description: "تخزين الرمز كنص عادي", }, weburl: { title: "URL", description: "تخزين URL لهذه المحفظة مع الرمز", }, binary: { title: "ثنائي", description: "تخزين الرموز كبيانات ثنائية", }, quick_access: { toggle: "وصول سريع إلى NFC", description: "مسح بطاقات NFC بسرعة في قائمة استلام Ecash. يضيف هذا الخيار زر NFC إلى قائمة استلام Ecash.", }, }, }, p2pk_features: { title: "P2PK", description: "إنشاء زوج مفاتيح لاستلام ecash مقفلة بـ P2PK. تحذير: هذه الميزة تجريبية. استخدمها بكميات صغيرة فقط. إذا فقدت مفاتيحك الخاصة، فلن يتمكن أحد من فتح ecash المقفلة بها بعد الآن.", generate_button: "إنشاء مفتاح", import_button: "استيراد nsec", quick_access: { toggle: "وصول سريع للقفل", description: "استخدم هذا لعرض مفتاح قفل P2PK الخاص بك بسرعة في قائمة استلام ecash.", }, keys_expansion: { label: "انقر لاستعراض {count} مفتاح", used_badge: "مستخدم", }, }, privacy: { title: "الخصوصية", description: "تؤثر هذه الإعدادات على خصوصيتك.", check_incoming: { toggle: "التحقق من الفاتورة الواردة", description: "إذا تم تمكين هذا، ستقوم المحفظة بفحص أحدث فاتورة في الخلفية. يزيد هذا من استجابة المحفظة مما يسهل عملية البصمة. يمكنك التحقق من الفواتير غير المدفوعة يدويًا في علامة التبويب الفواتير.", }, check_startup: { toggle: "التحقق من الفواتير المعلقة عند بدء التشغيل", description: "إذا تم تمكين هذا، ستقوم المحفظة بفحص الفواتير المعلقة من آخر 24 ساعة عند بدء التشغيل.", }, check_all: { toggle: "التحقق من جميع الفواتير", description: "إذا تم تمكين هذا، ستقوم المحفظة بالتحقق بشكل دوري من الفواتير غير المدفوعة في الخلفية لمدة تصل إلى أسبوعين. يزيد هذا من نشاط المحفظة عبر الإنترنت مما يسهل عملية البصمة. يمكنك التحقق من الفواتير غير المدفوعة يدويًا في علامة التبويب الفواتير.", }, check_sent: { toggle: "التحقق من ecash المرسلة", description: "إذا تم تمكين هذا، ستقوم المحفظة باستخدام فحوصات خلفية دورية لتحديد ما إذا تم استرداد الرموز المرسلة. يزيد هذا من نشاط المحفظة عبر الإنترنت مما يسهل عملية البصمة.", }, websockets: { toggle: "استخدام WebSockets", description: "إذا تم تمكين هذا، ستقوم المحفظة باستخدام اتصالات WebSocket طويلة الأمد لاستلام التحديثات حول الفواتير المدفوعة والرموز المستخدمة من mints. يزيد هذا من استجابة المحفظة ولكنه يسهل أيضًا عملية البصمة.", }, bitcoin_price: { toggle: "الحصول على سعر الصرف من Coinbase", description: "إذا تم تمكين هذا، سيتم جلب سعر صرف Bitcoin الحالي من coinbase.com وسيتم عرض رصيدك المحول.", currency: { title: "العملة الورقية", description: "اختر العملة الورقية لعرض سعر البيتكوين.", }, }, }, experimental: { title: "تجريبي", description: "هذه الميزات تجريبية.", receive_swaps: { toggle: "استلام عمليات التبديل", badge: "بيتا", description: "خيار تبديل Ecash المستلم إلى mint النشط الخاص بك في مربع حوار استلام Ecash.", }, auto_paste: { toggle: "لصق Ecash تلقائيًا", description: "لصق ecash تلقائيًا في الحافظة الخاصة بك عند الضغط على استلام، ثم Ecash، ثم لصق. قد يتسبب اللصق التلقائي في مشاكل في واجهة المستخدم في iOS، قم بإيقاف تشغيله إذا واجهت مشاكل.", }, auditor: { toggle: "تمكين المدقق", badge: "بيتا", description: "إذا تم تمكين هذا، ستعرض المحفظة معلومات المدقق في مربع حوار تفاصيل mint. المدقق هو خدمة طرف ثالث تراقب موثوقية mints.", url_label: "عنوان URL للمدقق", api_url_label: "عنوان URL لواجهة برمجة تطبيقات المدقق", }, multinut: { toggle: "تمكين Multinut", description: "إذا تم تمكينه، ستستخدم المحفظة Multinut لدفع الفواتير من عدة mints في وقت واحد.", }, nostr_mint_backup: { toggle: "النسخ الاحتياطي لقائمة mint على Nostr", description: "إذا تم تمكينه، سيتم نسخ قائمة mint الخاصة بك احتياطيًا تلقائيًا إلى مرحلات Nostr باستخدام مفاتيح Nostr التي تم تكوينها. يتيح لك هذا استعادة قائمة mint الخاصة بك عبر الأجهزة.", notifications: { enabled: "تمكين النسخ الاحتياطي لـ Nostr mint", disabled: "تعطيل النسخ الاحتياطي لـ Nostr mint", failed: "فشل تمكين النسخ الاحتياطي لـ Nostr mint", }, }, }, appearance: { keyboard: { title: "لوحة مفاتيح على الشاشة", description: "استخدم لوحة المفاتيح الرقمية لإدخال المبالغ.", toggle: "استخدام لوحة المفاتيح الرقمية", toggle_description: "إذا تم تمكين هذا، سيتم استخدام لوحة المفاتيح الرقمية لإدخال المبالغ.", }, theme: { title: "المظهر", description: "تغيير مظهر محفظتك.", tooltips: { mono: "أحادي", cyber: "سايبر", freedom: "حرية", nostr: "نوستر", bitcoin: "بيتكوين", mint: "مينت", nut: "جوز", blu: "أزرق", flamingo: "فلامينجو", }, }, bip177: { title: "رمز البيتكوين", description: "استخدم رمز ₿ بدلاً من sats.", toggle: "استخدام رمز ₿", }, }, web_of_trust: { title: "شبكة الثقة", known_pubkeys: "المفاتيح العامة المعروفة: {wotCount}", continue_crawl: "متابعة الزحف", crawl_odell: "الزحف إلى شبكة ثقة ODELL", crawl_wot: "الزحف إلى شبكة الثقة", pause: "إيقاف مؤقت", reset: "إعادة تعيين", progress: "{crawlProcessed} / {crawlTotal}", }, npub_cash: { use_npubx: "استخدام npubx.cash", copy_lightning_address: "نسخ عنوان Lightning", v2_mint: "npub.cash v2 mint", }, multinut: { use_multinut: "استخدام Multinut", }, advanced: { title: "متقدم", developer: { title: "إعدادات المطور", description: "الإعدادات التالية هي للتطوير والتصحيح.", new_seed: { button: "إنشاء عبارة استعادة جديدة", description: "سيؤدي هذا إلى إنشاء عبارة استعادة جديدة. يجب عليك إرسال رصيدك بالكامل إلى نفسك لتتمكن من استعادته باستخدام عبارة استعادة جديدة.", confirm_question: "هل أنت متأكد أنك تريد إنشاء عبارة استعادة جديدة؟", cancel: "إلغاء", confirm: "تأكيد", }, remove_spent: { button: "إزالة البراهين المستخدمة", description: "تحقق مما إذا كانت رموز ecash من mints النشطة الخاصة بك قد تم استخدامها وإزالة المستخدمة من محفظتك. استخدم هذا فقط إذا كانت محفظتك عالقة.", }, debug_console: { button: "تبديل وحدة تحكم التصحيح", description: "افتح طرفية تصحيح Javascript. لا تلصق أبدًا أي شيء في هذه الطرفية لا تفهمه. قد يحاول لص خداعك للصق رمز ضار هنا.", }, export_proofs: { button: "تصدير البراهين النشطة", description: "نسخ رصيدك بالكامل من mint النشط كرمز Cashu إلى الحافظة الخاصة بك. سيؤدي هذا فقط إلى تصدير الرموز من mint والوحدة المحددة. لتصدير كامل، حدد mint ووحدة مختلفين وقم بالتصدير مرة أخرى.", }, keyset_counters: { title: "زيادة عدادات مجموعة المفاتيح", description: 'انقر على معرّف مجموعة المفاتيح لزيادة عدادات مسار الاشتقاق لمجموعات المفاتيح في محفظتك. هذا مفيد إذا رأيت خطأ "النواتج تم توقيعها بالفعل".', counter: "العداد: {count}", }, unset_reserved: { button: "إلغاء تعيين جميع الرموز المحجوزة", description: 'تضع هذه المحفظة علامة على ecash الصادرة المعلقة كمحجوزة (وتطرحها من رصيدك) لمنع محاولات الإنفاق المزدوج. هذا الزر سيلغي تعيين جميع الرموز المحجوزة بحيث يمكن استخدامها مرة أخرى. إذا قمت بذلك، قد تتضمن محفظتك براهين مستخدمة. اضغط على زر "إزالة البراهين المستخدمة" للتخلص منها.', }, show_onboarding: { button: "إظهار شاشة الترحيب", description: "إظهار شاشة الترحيب مرة أخرى.", }, reset_wallet: { button: "إعادة تعيين بيانات المحفظة", description: "إعادة تعيين بيانات محفظتك. تحذير: سيؤدي هذا إلى حذف كل شيء! تأكد من إنشاء نسخة احتياطية أولاً.", confirm_question: "هل أنت متأكد أنك تريد حذف بيانات محفظتك؟", cancel: "إلغاء", confirm: "حذف المحفظة", }, export_wallet: { button: "تصدير بيانات المحفظة", description: "تنزيل نسخة من محفظتك. يمكنك استعادة محفظتك من هذا الملف في شاشة الترحيب لمحفظة جديدة. سيكون هذا الملف غير متزامن إذا واصلت استخدام محفظتك بعد تصديره.", }, }, }, }, NoMintWarnBanner: { title: "انضم إلى mint", subtitle: "لم تنضم إلى أي Cashu mint بعد. أضف عنوان URL لـ mint في الإعدادات أو استلم ecash من mint جديد للبدء.", actions: { add_mint: { label: "@:global.actions.add_mint.label", }, receive: { label: "استلام Ecash", }, }, }, WalletPage: { actions: { send: { label: "@:global.actions.send.label", }, receive: { label: "@:global.actions.receive.label", }, }, tabs: { history: { label: "السجل", }, invoices: { label: "الفواتير", }, mints: { label: "Mints", }, }, install: { text: "تثبيت", tooltip: "تثبيت Cashu", }, }, AlreadyRunning: { title: "لا.", text: "علامة تبويب أخرى قيد التشغيل بالفعل. أغلق هذه العلامة وحاول مرة أخرى.", actions: { retry: { label: "إعادة المحاولة", }, }, }, ErrorNotFound: { title: "404", text: "عذرًا، لا يوجد شيء هنا…", actions: { home: { label: "العودة إلى الصفحة الرئيسية", }, }, }, BalanceView: { mintUrl: { label: "Mint", }, mintBalance: { label: "الرصيد", }, mintError: { label: "خطأ في Mint", }, pending: { label: "معلق", tooltip: "التحقق من جميع الرموز المعلقة", }, }, WelcomePage: { actions: { previous: { label: "السابق", }, next: { label: "التالي", }, }, }, WelcomeSlide1: { title: "أهلاً بك في Cashu", text: "Cashu.me هي محفظة Bitcoin مجانية ومفتوحة المصدر تستخدم ecash للحفاظ على أموالك آمنة وخصوصية.", actions: { more: { label: "انقر لمعرفة المزيد", }, }, p1: { text: "Cashu هو بروتوكول ecash مجاني ومفتوح المصدر لـ Bitcoin. يمكنك معرفة المزيد عنه على { link }.", link: { text: "cashu.space", }, }, p2: { text: "هذه المحفظة غير تابعة لأي mint. لاستخدام هذه المحفظة، تحتاج إلى الاتصال بواحد أو أكثر من Cashu mints التي تثق بها.", }, p3: { text: "تخزن هذه المحفظة ecash لا يمكنك الوصول إليها إلا أنت. إذا قمت بحذف بيانات المتصفح الخاص بك دون نسخ احتياطي لعبارة الاستعادة، فستفقد رموزك.", }, p4: { text: "هذه المحفظة في مرحلة بيتا. لا نتحمل أي مسؤولية عن فقدان الأشخاص الوصول إلى أموالهم. استخدم على مسؤوليتك الخاصة! هذا الكود مفتوح المصدر ومرخص تحت رخصة MIT.", }, }, WelcomeSlide2: { title: "تثبيت PWA", alt: { pwa_example: "مثال تثبيت PWA" }, installing: "جارٍ التثبيت…", instruction: { intro: { text: "للحصول على أفضل تجربة، استخدم هذه المحفظة مع متصفح الويب الأصلي لجهازك لتثبيتها كتطبيق ويب تقدمي. افعل هذا الآن.", }, android: { title: "Android (Chrome)", step1: { item: "1. { icon } { text }", text: "انقر على القائمة (أعلى اليمين)", }, step2: { item: "2. { icon } { text }", text: "اضغط على { buttonText }", buttonText: "@:AndroidPWAPrompt.buttonText", }, }, ios: { title: "iOS (Safari)", step1: { item: "1. { icon } { text }", text: "انقر على مشاركة (أسفل)", }, step2: { item: "2. { icon } { text }", text: "اضغط على { buttonText }", buttonText: "@:iOSPWAPrompt.buttonText", }, }, outro: { text: "بعد تثبيت هذا التطبيق على جهازك، أغلق نافذة المتصفح هذه واستخدم التطبيق من شاشتك الرئيسية.", }, }, pwa: { success: { title: "نجاح!", text: "أنت تستخدم Cashu كتطبيق PWA. أغلق أي نوافذ متصفح أخرى مفتوحة واستخدم التطبيق من شاشتك الرئيسية.", nextSteps: "يمكنك الآن إغلاق هذا اللسان وفتح التطبيق من الشاشة الرئيسية.", }, }, }, iOSPWAPrompt: { text: "انقر على { icon } و { buttonText }", buttonText: "إضافة إلى الشاشة الرئيسية", }, AndroidPWAPrompt: { text: "انقر على { icon } و { buttonText }", buttonText: "إضافة إلى الشاشة الرئيسية", }, WelcomeSlide3: { title: "عبارة الاستعادة الخاصة بك", text: "احفظ عبارة الاستعادة الخاصة بك في مدير كلمات مرور أو على الورق. عبارة الاستعادة الخاصة بك هي الطريقة الوحيدة لاستعادة أموالك إذا فقدت الوصول إلى هذا الجهاز.", inputs: { seed_phrase: { label: "عبارة الاستعادة", caption: "يمكنك رؤية عبارة الاستعادة الخاصة بك في الإعدادات.", }, checkbox: { label: "لقد كتبتها", }, }, }, WelcomeSlide4: { title: "الشروط", actions: { more: { label: "قراءة شروط الخدمة", }, }, inputs: { checkbox: { label: "لقد قرأت وأقبل هذه الشروط والأحكام", }, }, }, WelcomeSlideChoice: { title: "إعداد محفظتك", text: "هل تريد الاستعادة من عبارة الاستعادة أم إنشاء محفظة جديدة؟", options: { new: { title: "إنشاء محفظة جديدة", subtitle: "توليد عبارة استعادة جديدة وإضافة mints.", }, recover: { title: "استعادة المحفظة", subtitle: "أدخل عبارة الاستعادة، واستعد المِنْت و ecash.", }, }, }, WelcomeMintSetup: { title: "إضافة mints", text: "المِنْت خوادم تساعدك على إرسال واستلام ecash. اختر mint مكتشفًا أو أضِف واحدًا يدويًا. يمكنك التخطي والإضافة لاحقًا.", sections: { your_mints: "المِنْت الخاصة بك" }, restoring: "جارٍ استعادة المِنْت…", placeholder: { mint_url: "https://" }, }, WelcomeRecoverSeed: { title: "أدخل عبارة الاستعادة", text: "الصق أو اكتب عبارة من 12 كلمة للاستعادة.", inputs: { word: "الكلمة { index }" }, actions: { paste_all: "لصق الكل" }, disclaimer: "تُستخدم عبارة الاستعادة محليًا فقط لاشتقاق مفاتيح محفظتك.", }, WelcomeRestoreEcash: { title: "استعد ecash الخاصة بك", text: "ابحث عن البراهين غير المصروفة على المِنْت المُكوَّنة لديك وأضِفها إلى محفظتك.", }, MintRatings: { title: "مراجعات المِنْت", reviews: "مراجعات", ratings: "التقييمات", no_reviews: "لا توجد مراجعات", your_review: "مراجعتك", no_reviews_to_display: "لا توجد مراجعات للعرض.", no_rating: "لا يوجد تقييم", out_of: "من", rows: "Reviews", sort: "ترتيب", sort_options: { newest: "الأحدث", oldest: "الأقدم", highest: "الأعلى", lowest: "الأقل", }, actions: { write_review: "اكتب مراجعة" }, empty_state_subtitle: "ساعد من خلال ترك مراجعة. شارك تجربتك مع هذا المِنْت وساعد الآخرين من خلال ترك مراجعة.", }, CreateMintReview: { title: "مراجعة المِنْت", publishing_as: "النشر باسم", inputs: { rating: { label: "التقييم" }, review: { label: "مراجعة (اختياري)" }, }, actions: { publish: { label: "نشر", in_progress: "جارٍ النشر…" }, }, }, RestoreView: { seed_phrase: { label: "استعادة من عبارة الاستعادة", caption: "أدخل عبارة الاستعادة الخاصة بك لاستعادة محفظتك. قبل الاستعادة، تأكد من أنك أضفت جميع mints التي استخدمتها من قبل.", inputs: { seed_phrase: { label: "عبارة الاستعادة", caption: "يمكنك رؤية عبارة الاستعادة الخاصة بك في الإعدادات.", }, }, }, information: { label: "معلومات", caption: "سيقوم المعالج فقط باستعادة ecash من عبارة استعادة أخرى، ولن تتمكن من استخدام عبارة الاستعادة هذه أو تغيير عبارة استعادة المحفظة التي تستخدمها حاليًا. هذا يعني أن ecash المستعادة لن تكون محمية بواسطة عبارة الاستعادة الحالية طالما لم ترسل ecash إلى نفسك مرة واحدة.", }, restore_mints: { label: "استعادة Mints", caption: 'حدد mint للاستعادة. يمكنك إضافة المزيد من mints في الشاشة الرئيسية تحت "Mints" واستعادتها هنا.', }, actions: { paste: { error: "فشل قراءة محتويات الحافظة.", }, validate: { error: "يجب أن تكون الكلمة التذكيرية 12 كلمة على الأقل.", }, select_all: { label: "تحديد الكل", }, deselect_all: { label: "إلغاء تحديد الكل", }, restore: { label: "استعادة", in_progress: "استعادة mint…", error: "خطأ في استعادة mint: { error }", }, restore_all_mints: { label: "استعادة جميع Mints", in_progress: "استعادة mint { index } من { length } …", success: "تمت الاستعادة بنجاح", error: "خطأ في استعادة mints: { error }", }, restore_selected_mints: { label: "استعادة المِنْتات المحددة ({count})", in_progress: "استعادة mint { index } من { length } …", success: "تمت استعادة {count} mint(s) بنجاح", error: "خطأ في استعادة mints المحددة: { error }", }, }, nostr_mints: { label: "استعادة Mints من Nostr", caption: "ابحث عن نسخ احتياطية لـ mint مخزنة على مرحلات Nostr باستخدام عبارة الاستعادة الخاصة بك. سيساعدك هذا على اكتشاف mints التي استخدمتها سابقًا.", search_button: "البحث عن نسخ احتياطية لـ Mint", select_all: "تحديد الكل", deselect_all: "إلغاء تحديد الكل", backed_up: "تم النسخ الاحتياطي", already_added: "تمت الإضافة بالفعل", add_selected: "إضافة المحدد ({count})", no_backups_found: "لم يتم العثور على نسخ احتياطية لـ mint", no_backups_hint: "تأكد من تمكين النسخ الاحتياطي لـ Nostr mint في الإعدادات لنسخ قائمة mint الخاصة بك احتياطيًا تلقائيًا.", invalid_mnemonic: "الرجاء إدخال عبارة استعادة صالحة قبل البحث.", search_error: "فشل البحث عن نسخ احتياطية لـ mint.", add_error: "فشل إضافة mints المحددة.", }, }, MintSettings: { add: { title: "إضافة mint", description: "أدخل عنوان URL لـ Cashu mint للاتصال به. هذه المحفظة غير تابعة لأي mint.", inputs: { nickname: { placeholder: "الاسم المستعار (مثلاً Testnet)", }, }, actions: { add_mint: { label: "@:global.actions.add_mint.label", error_invalid_url: "عنوان URL غير صالح", }, scan: { label: "مسح رمز QR", }, }, }, discover: { title: "اكتشف mints", overline: "اكتشف", caption: "اكتشف mints التي أوصى بها المستخدمون الآخرون على nostr.", actions: { discover: { label: "اكتشف mints", in_progress: "جارٍ التحميل…", error_no_mints: "لم يتم العثور على mints", success: "تم العثور على { length } mints", }, }, recommendations: { overline: "تم العثور على { length } mints", caption: "تمت التوصية بهذه mints من قبل مستخدمي Nostr الآخرين. كن حذرًا وقم ببحثك الخاص قبل استخدام أي mint.", actions: { browse: { label: "انقر لاستعراض mints", }, }, }, }, swap: { title: "تبديل", overline: "تبديل بين عدة Mints", caption: "تبديل الأموال بين mints عبر Lightning. ملاحظة: اترك مساحة لرسوم Lightning المحتملة. إذا لم تنجح الدفعة الواردة، فتحقق من الفاتورة يدويًا.", inputs: { from: { label: "من", }, to: { label: "إلى", }, amount: { label: "المبلغ ({ ticker })", }, }, actions: { swap: { label: "@:global.actions.swap.label", in_progress: "@:MintSettings.swap.actions.swap.label", }, }, }, error_badge: "خطأ", reviews_text: "مراجعات", no_reviews_yet: "لا توجد مراجعات بعد", discover_mints_button: "اكتشف mints", }, QrcodeReader: { progress: { text: "{ percentage }{ addon }", percentage: "{ percentage }%", keep_scanning_text: " - استمر في المسح الضوئي", }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, }, }, InvoiceDetailDialog: { title: "استلام Lightning", create_invoice_title: "إنشاء فاتورة", inputs: { amount: { label: "المبلغ ({ ticker }) *", }, }, actions: { close: { label: "@:global.actions.close.label", }, create: { label: "إنشاء فاتورة", label_blocked: "جارٍ إنشاء الفاتورة…", in_progress: "إنشاء", }, }, invoice: { caption: "فاتورة Lightning", status_paid_text: "تم الدفع!", actions: { close: { label: "@:global.actions.close.label", }, copy: { label: "@:global.actions.copy.label", }, }, }, }, SendDialog: { title: "إرسال", actions: { ecash: { label: "Ecash", error_no_mints: "لا يوجد mints متوفرة", }, lightning: { label: "Lightning", error_no_mints: "لا يوجد mints متوفرة", }, }, }, SendTokenDialog: { title: "إرسال Ecash", title_ecash_text: "Ecash", badge_offline_text: "غير متصل", inputs: { amount: { label: "المبلغ ({ ticker }) *", invalid_too_much_error_text: "أكثر من اللازم", }, p2pk_pubkey: { label: "المفتاح العام للمستلم", label_invalid: "المفتاح العام للمستلم", }, }, actions: { close: { label: "@:global.actions.close.label", }, close_card_scanner: { label: "@:global.actions.close.label", }, copy_emoji: { label: "🥜", tooltip_text: "نسخ الرمز التعبيري", }, copy_tokens: { label: "@:global.actions.copy.label", }, copy_link: { tooltip_text: "نسخ الرابط", }, share: { tooltip_text: "مشاركة ecash", }, lock: { label: "@:global.actions.lock.label", }, paste_p2pk_pubkey: { tooltip_text: "@:global.actions.paste.label", }, send: { label: "@:global.actions.send.label", }, delete: { tooltip_text: "حذف من السجل", }, write_tokens_to_card: { tooltips: { ndef_supported_text: "فلاش إلى بطاقة NFC", ndef_unsupported_text: "NDEF غير مدعوم", }, }, }, }, ReceiveDialog: { title: "استلام", actions: { ecash: { label: "Ecash", error_no_mints: "لا يوجد mints متوفرة", }, lightning: { label: "Lightning", error_no_mints: "تحتاج إلى الاتصال بـ mint لاستلام عبر Lightning", }, }, }, ReceiveEcashDrawer: { title: "استلام Ecash", actions: { paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, request: { label: "طلب", }, lock: { label: "@:global.actions.lock.label", }, nfc: { label: "NFC", scanning_text: "مسح ضوئي…", }, }, }, ReceiveTokenDialog: { title: "استلام Ecash", title_ecash_text: "Ecash", inputs: { tokens_base64: { label: "لصق رمز Cashu", }, }, errors: { invalid_token: { label: "رمز غير صالح", }, p2pk_lock_mismatch: { label: "غير قادر على الاستلام. قفل P2PK لهذا الرمز لا يطابق المفتاح العام الخاص بك.", }, }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, scan: { label: "@:global.actions.scan.label", }, receive: { label: "@:global.actions.receive.label", label_known_mint: "@:ReceiveTokenDialog.actions.receive.label", label_adding_mint: "إضافة mint…", }, swap: { label: "@:global.actions.swap.label", tooltip_text: "التبديل إلى mint موثوق به", caption: "تبديل { value }", }, cancel_swap: { label: "@:global.actions.cancel.label", tooltip_text: "إلغاء التبديل", }, confirm_swap: { label: "@:ReceiveTokenDialog.actions.swap.label", tooltip_text: "@:ReceiveTokenDialog.actions.swap.tooltip_text", in_progress: "@:ReceiveTokenDialog.actions.confirm_swap.label", }, later: { label: "استلام لاحقًا", tooltip_text: "أضف إلى السجل للاستلام لاحقًا", already_in_history_success_text: "Ecash موجود بالفعل في السجل", added_to_history_success_text: "تمت إضافة Ecash إلى السجل", }, nfc: { label: "NFC", tooltips: { ndef_supported_text: "القراءة من بطاقة NFC", ndef_unsupported_text: "NDEF غير مدعوم", }, }, }, }, P2PKDialog: { p2pk: { caption: "مفتاح P2PK", description: "استلام ecash مقفلة بهذا المفتاح", used_warning_text: "تحذير: تم استخدام هذا المفتاح من قبل. استخدم مفتاحًا جديدًا لخصوصية أفضل.", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_key: { label: "إنشاء مفتاح جديد", }, }, }, PaymentRequestDialog: { payment_request: { caption: "طلب دفع", description: "استلام المدفوعات عبر Nostr", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_request: { label: "طلب جديد", }, add_amount: { label: "إضافة مبلغ", }, use_active_mint: { label: "أي mint", }, }, inputs: { amount: { placeholder: "أدخل المبلغ", }, }, }, NumericKeyboard: { actions: { close: { label: "@:global.actions.close.label", closed_info_text: "تم تعطيل لوحة المفاتيح. يمكنك إعادة تمكين لوحة المفاتيح في الإعدادات.", }, enter: { label: "@:global.actions.enter.label", }, }, }, NWCDialog: { nwc: { caption: "اتصال محفظة Nostr", description: "تحكم في محفظتك عن بعد باستخدام NWC. اضغط على رمز الاستجابة السريعة لربط محفظتك بتطبيق متوافق.", warning_text: "تحذير: أي شخص لديه حق الوصول إلى سلسلة الاتصال هذه يمكنه بدء مدفوعات من محفظتك. لا تشاركها!", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, }, }, MintMotdMessage: { title: "رسالة Mint", }, MintDetailsDialog: { contact: { title: "جهة الاتصال", }, details: { title: "تفاصيل Mint", url: { label: "URL", }, nuts: { label: "Nuts", actions: { show: { label: "عرض الكل", }, hide: { label: "إخفاء", }, }, }, currency: { label: "العملة", }, currencies: { label: "@:MintDetailsDialog.details.currency.label", }, version: { label: "الإصدار", }, }, actions: { title: "الإجراءات", copy_mint_url: { label: "نسخ عنوان URL للـ mint", }, delete: { label: "حذف mint", }, edit: { label: "تعديل mint", }, }, }, ChooseMint: { title: "حدد mint", badge_mint_error_text: "خطأ", badge_option_mint_error_text: "@:ChooseMint.badge_mint_error_text", }, HistoryTable: { empty_text: "لا يوجد سجل حتى الآن", row: { type_label: "Ecash", date_label: "منذ { value }", }, actions: { check_status: { tooltip_text: "التحقق من الحالة", }, receive: { tooltip_text: "استلام", }, filter_pending: { label: "تصفية المعلقة", }, show_all: { label: "عرض الكل", }, }, old_token_not_found_error_text: "لم يتم العثور على الرمز القديم", }, InvoiceTable: { empty_text: "لا يوجد فواتير حتى الآن", row: { type_label: "Lightning", type_tooltip_text: "انقر للنسخ", date_label: "منذ { value }", }, actions: { check_status: { tooltip_text: "التحقق من الحالة", }, filter_pending: { label: "تصفية المعلقة", }, show_all: { label: "عرض الكل", }, }, }, RemoveMintDialog: { title: "هل أنت متأكد أنك تريد حذف هذا الـ mint؟", nickname: { label: "الاسم المستعار", }, balances: { label: "الأرصدة", }, warning_text: "ملاحظة: نظرًا لأن هذه المحفظة حذرة، فلن يتم حذف ecash الخاص بك من هذا الـ mint فعليًا ولكنه سيظل مخزنًا على جهازك. ستراه يظهر مرة أخرى إذا قمت بإعادة إضافة هذا الـ mint لاحقًا.", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { confirm: { label: "إزالة mint", }, cancel: { label: "@:global.actions.cancel.label", }, }, }, ParseInputComponent: { placeholder: { default: "رمز Cashu أو عنوان Lightning", receive: "رمز Cashu", pay: "عنوان Lightning أو فاتورة", }, qr_scanner: { title: "مسح رمز QR", description: "اضغط للمسح عنوان", }, paste_button: { label: "@:global.actions.paste.label", }, }, PayInvoiceDialog: { input_data: { title: "دفع Lightning", inputs: { invoice_data: { label: "فاتورة Lightning أو عنوان", }, }, actions: { close: { label: "@:global.actions.close.label", }, enter: { label: "@:global.actions.enter.label", }, paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, }, }, lnurlpay: { amount_exact_label: "{ payee } يطلب { value } { ticker }", amount_range_label: "{ payee } يطلب{br}بين { min } و { max } { ticker }", sending_to_lightning_address: "إرسال إلى { address }", inputs: { amount: { label: "المبلغ ({ ticker }) *", }, comment: { label: "تعليق (اختياري)", }, }, actions: { close: { label: "@:global.actions.close.label", }, send: { label: "@:global.actions.send.label", }, }, }, invoice: { title: "دفع { value }", paying: "جاري الدفع", paid: "تم الدفع", fee: "الرسوم", memo: { label: "مذكرة", }, processing_info_text: "جاري المعالجة…", balance_too_low_warning_text: "الرصيد منخفض جدًا", actions: { close: { label: "@:global.actions.close.label", }, pay: { label: "دفع", in_progress: "@:PayInvoiceDialog.invoice.processing_info_text", error: "خطأ", }, }, }, }, EditMintDialog: { title: "تعديل mint", inputs: { nickname: { label: "الاسم المستعار", }, mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, update: { label: "@:global.actions.update.label", }, }, }, AddMintDialog: { title: "هل تثق في هذا الـ mint؟", description: "قبل استخدام هذا الـ mint، تأكد من أنك تثق به. قد يصبح الـ mints ضارًا أو يتوقف عن العمل في أي وقت.", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, add_mint: { label: "@:global.actions.add_mint.label", in_progress: "إضافة mint", }, }, }, restore: { mnemonic_error_text: "الرجاء إدخال كلمة تذكيرية", restore_mint_error_text: "خطأ في استعادة mint: { error }", prepare_info_text: "تحضير عملية الاستعادة…", restored_proofs_for_keyset_info_text: "تم استعادة { restoreCounter } برهان لمجموعة المفاتيح { keysetId }", checking_proofs_for_keyset_info_text: "التحقق من البراهين من { startIndex } إلى { endIndex } لمجموعة المفاتيح { keysetId }", no_proofs_info_text: "لم يتم العثور على براهين للاستعادة", restored_amount_success_text: "تم استعادة { amount }", }, swap: { in_progress_warning_text: "التبديل قيد التقدم", invalid_swap_data_error_text: "بيانات تبديل غير صالحة", swap_error_text: "خطأ في التبديل", }, TokenInformation: { fee: "الرسوم", unit: "الوحدة", fiat: "العملة الورقية", p2pk: "P2PK", locked: "مقفل", locked_to_you: "مقفل لك", mint: "دار السك", memo: "مذكرة", payment_request: "طلب دفع", nostr: "Nostr", token_copied: "تم نسخ الرمز المميز إلى الحافظة", }, }; ================================================ FILE: src/i18n/cs-CZ/index.ts ================================================ export default { global: { copy_to_clipboard: { success: "Zkopírováno do schránky!", }, actions: { add_mint: { label: "Přidat mint", }, cancel: { label: "Zrušit", }, copy: { label: "Kopírovat", }, close: { label: "Zavřít", }, enter: { label: "Potvrdit", }, lock: { label: "Uzamknout", }, paste: { label: "Vložit", }, receive: { label: "Přijmout", }, scan: { label: "Skenovat", }, send: { label: "Odeslat", }, pay: { label: "Zaplatit", }, swap: { label: "Směnit", }, update: { label: "Aktualizovat", }, }, inputs: { mint_url: { label: "URL mintu", }, }, }, common: { fee: "Poplatek", }, MultinutPicker: { payment: "Platba Multinut", selectMints: "Vyberte jeden nebo více mintů, ze kterých se má platba provést.", totalSelectedBalance: "Celkový vybraný zůstatek", multiMintPay: "Platba z více mintů", balanceNotEnough: "Zůstatek napříč minty nestačí k uhrazení této faktury", failed: "Zpracování selhalo: {error}", paid: "Zaplaceno {amount} přes Lightning", }, wallet: { notifications: { balance_too_low: "Zůstatek je příliš nízký", received: "Přijato {amount}", fee: " (poplatek: {fee})", could_not_request_mint: "Nepodařilo se požádat mint", invoice_still_pending: "Faktura zatím nevyřízena", paid_lightning: "Zaplaceno {amount} přes Lightning", payment_pending_refresh: "Platba nevyřízena. Obnovte fakturu ručně.", sent: "Odesláno {amount}", token_still_pending: "Token stále nevyřízen", received_lightning: "Přijato {amount} přes Lightning", lightning_payment_failed: "Lightning platba selhala", failed_to_decode_invoice: "Nepodařilo se dekódovat fakturu", invalid_lnurl: "Neplatné LNURL", lnurl_error: "Chyba LNURL", no_amount: "Nebyla zadána částka", no_lnurl_data: "Žádná LNURL data", no_price_data: "Žádná cenová data.", please_try_again: "Zkuste to prosím znovu.", }, mint: { notifications: { already_added: "Mint je již přidán", added: "Mint přidán", not_found: "Mint nebyl nalezen", activation_failed: "Aktivace mintu selhala", no_active_mint: "Žádný aktivní mint", unit_activation_failed: "Aktivace jednotky selhala", unit_not_supported: "Jednotka není mintem podporována", activated: "Mint aktivován", could_not_connect: "Nelze se připojit k mintu", could_not_get_info: "Nelze získat informace o mintu", could_not_get_keys: "Nelze získat klíče mintu", could_not_get_keysets: "Nelze získat keysety mintu", mint_validation_error: "Chyba ověření mintu", removed: "Mint odebrán", error: "Chyba mintu", }, }, }, MainHeader: { menu: { settings: { title: "Nastavení", settings: { title: "Nastavení", caption: "Nastavení peněženky", }, }, terms: { title: "Podmínky", terms: { title: "Podmínky", caption: "Podmínky služby", }, }, links: { title: "Odkazy", cashuSpace: { title: "Cashu.space", caption: "cashu.space", }, github: { title: "GitHub", caption: "github.com/cashubtc", }, telegram: { title: "Telegram", caption: "t.me/CashuMe", }, twitter: { title: "Twitter", caption: "{'@'}CashuBTC", }, donate: { title: "Darovat", caption: "Podpořit Cashu", }, }, }, offline: { warning: { text: "Offline", }, }, reload: { warning: { text: "Obnovení za { countdown }", }, }, staging: { warning: { text: "Staging – nepoužívejte se skutečnými prostředky!", }, }, }, FullscreenHeader: { actions: { back: { label: "Peněženka", }, }, }, Settings: { language: { title: "Jazyk", description: "Vyberte si preferovaný jazyk ze seznamu níže.", }, sections: { backup_restore: "ZÁLOHA A OBNOVA", lightning_address: "LIGHTNING ADRESA", nostr_keys: "NOSTR KLÍČE", nostr: { title: "NOSTR", relays: { expand_label: "Klikněte pro úpravu relayů", add: { title: "Přidat relay", description: "Vaše peněženka používá tyto relaye pro Nostr operace, jako jsou žádosti o platbu, Nostr Wallet Connect a zálohy.", }, list: { title: "Relaye", description: "Vaše peněženka se připojí k těmto relayům.", copy_tooltip: "Kopírovat relay", remove_tooltip: "Odebrat relay", }, }, }, payment_requests: "ŽÁDOSTI O PLATBU", nostr_wallet_connect: "NOSTR WALLET CONNECT", hardware_features: "HARDWAROVÉ FUNKCE", p2pk_features: "P2PK FUNKCE", privacy: "SOUKROMÍ", experimental: "EXPERIMENTÁLNÍ", appearance: "VZHLED", }, backup_restore: { backup_seed: { title: "Záloha seed fráze", description: "Seed fráze umožňuje obnovit vaši peněženku. Uchovávejte ji v bezpečí a v soukromí.", seed_phrase_label: "Seed fráze", }, restore_ecash: { title: "Obnovit ecash", description: "Průvodce obnovou vám umožní obnovit ztracený ecash pomocí mnemotechnické seed fráze. Seed fráze vaší aktuální peněženky zůstane nedotčena – průvodce umožňuje obnovu ecashe pouze z jiné seed fráze.", button: "Obnovit", }, }, lightning_address: { title: "Lightning adresa", description: "Přijímejte platby na svou Lightning adresu.", enable: { toggle: "Povolit", description: "Lightning adresa s npub.cash", }, address: { copy_tooltip: "Kopírovat Lightning adresu", }, automatic_claim: { toggle: "Přijímat automaticky", description: "Automaticky přijímat příchozí platby.", }, npc_v2: { choose_mint_title: "Vyberte mint pro npub.cash v2", choose_mint_placeholder: "Vyberte mint…", }, }, nostr_keys: { title: "Vaše Nostr klíče", description: "Vaše Nostr klíče budou použity k určení vaší Lightning adresy.", wallet_seed: { title: "Seed fráze peněženky", description: "Vygenerovat Nostr klíčový pár ze seedu peněženky", copy_nsec: "Kopírovat nsec", }, nsec_bunker: { title: "Nsec Bunker", description: "Použít NIP-46 bunker", delete_tooltip: "Odstranit připojení", }, use_nsec: { title: "Použít vlastní nsec", description: "Tato metoda je nebezpečná a nedoporučuje se", delete_tooltip: "Odstranit nsec", }, signing_extension: { title: "Podepisovací rozšíření", description: "Použít podepisovací rozšíření NIP-07", not_found: "Nenalezeno žádné podepisovací rozšíření NIP-07", }, }, payment_requests: { title: "Žádosti o platbu", description: "Žádosti o platbu vám umožňují přijímat platby přes Nostr. Pokud tuto funkci povolíte, vaše peněženka se přihlásí k odběru vašich Nostr relayů.", enable_toggle: "Povolit žádosti o platbu", claim_automatically: { toggle: "Přijímat automaticky", description: "Automaticky přijímat příchozí platby.", }, }, nostr_wallet_connect: { title: "Nostr Wallet Connect (NWC)", description: "Použijte NWC k ovládání své peněženky z jakékoli jiné aplikace.", enable_toggle: "Povolit NWC", payments_note: "NWC lze použít pouze pro platby z vašeho bitcoinového zůstatku. Platby budou prováděny z aktivního mintu.", connection: { copy_tooltip: "Kopírovat připojovací řetězec", qr_tooltip: "Zobrazit QR kód", allowance_label: "Zbývající limit (sat)", }, }, hardware_features: { webnfc: { title: "WebNFC", description: "Vyberte kódování pro zápis na NFC karty", text: { title: "Text", description: "Uložit token jako prostý text", }, weburl: { title: "URL", description: "Uložit URL této peněženky spolu s tokenem", }, binary: { title: "Binární", description: "Uložit tokeny jako binární data", }, quick_access: { toggle: "Rychlý přístup k NFC", description: "Rychlé skenování NFC karet v nabídce Přijmout ecash. Tato volba přidá tlačítko NFC do nabídky Přijmout ecash.", }, }, }, p2pk_features: { title: "P2PK", description: "Vygenerujte klíčový pár pro příjem ecashe uzamčeného pomocí P2PK. Varování: Tato funkce je experimentální. Používejte ji pouze s malými částkami. Pokud ztratíte své soukromé klíče, nikdo už nebude schopen ecash uzamčený k nim odemknout.", generate_button: "Vygenerovat klíč", import_button: "Importovat nsec", quick_access: { toggle: "Rychlý přístup k uzamčení", description: "Použijte tuto možnost pro rychlé zobrazení vašeho P2PK uzamykacího klíče v nabídce Přijmout ecash.", }, keys_expansion: { label: "Klikněte pro zobrazení {count} klíčů", used_badge: "použito", }, }, privacy: { title: "Soukromí", description: "Tato nastavení ovlivňují vaše soukromí.", check_incoming: { toggle: "Kontrolovat příchozí faktury", description: "Pokud je povoleno, peněženka bude na pozadí kontrolovat nejnovější fakturu. To zvyšuje odezvu peněženky, ale také usnadňuje fingerprinting. Nezaplacené faktury můžete kontrolovat ručně na kartě Faktury.", }, check_startup: { toggle: "Kontrolovat čekající faktury při spuštění", description: "Pokud je povoleno, peněženka při spuštění zkontroluje čekající faktury z posledních 24 hodin.", }, check_all: { toggle: "Kontrolovat všechny faktury", description: "Pokud je povoleno, peněženka bude až po dobu dvou týdnů periodicky kontrolovat nezaplacené faktury na pozadí. To zvyšuje síťovou aktivitu peněženky a usnadňuje fingerprinting. Nezaplacené faktury můžete kontrolovat ručně na kartě Faktury.", }, check_sent: { toggle: "Kontrolovat odeslaný ecash", description: "Pokud je povoleno, peněženka bude pomocí periodických kontrol na pozadí zjišťovat, zda byly odeslané tokeny uplatněny. To zvyšuje síťovou aktivitu peněženky a usnadňuje fingerprinting.", }, websockets: { toggle: "Používat WebSockety", description: "Pokud je povoleno, peněženka bude používat dlouhodobá WebSocket připojení pro přijímání aktualizací o zaplacených fakturách a utracených tokenech z mintů. To zvyšuje odezvu peněženky, ale také usnadňuje fingerprinting.", }, bitcoin_price: { toggle: "Získávat kurz z Coinbase", description: "Pokud je povoleno, aktuální kurz Bitcoinu bude načítán z coinbase.com a převedený zůstatek bude zobrazen.", currency: { title: "Fiat měna", description: "Vyberte fiat měnu pro zobrazení ceny Bitcoinu.", }, }, }, experimental: { title: "Experimentální", description: "Tyto funkce jsou experimentální.", receive_swaps: { toggle: "Přijímat swapy", badge: "Beta", description: "Možnost směnit přijatý ecash na váš aktivní mint v dialogu Přijmout ecash.", }, auto_paste: { toggle: "Automaticky vkládat ecash", description: "Automaticky vloží ecash ze schránky při stisknutí Přijmout → Ecash → Vložit. Automatické vkládání může na iOS způsobovat problémy v UI – pokud k nim dochází, vypněte tuto funkci.", }, auditor: { toggle: "Povolit auditora", badge: "Beta", description: "Pokud je povoleno, peněženka zobrazí informace o auditorovi v dialogu detailů mintu. Auditor je služba třetí strany, která sleduje spolehlivost mintů.", url_label: "URL auditora", api_url_label: "API URL auditora", }, multinut: { toggle: "Povolit Multinut", description: "Pokud je povoleno, peněženka použije Multinut k placení faktur z více mintů najednou.", }, nostr_mint_backup: { toggle: "Zálohovat seznam mintů na Nostr", description: "Pokud je povoleno, váš seznam mintů bude automaticky zálohován na Nostr relaye pomocí nakonfigurovaných Nostr klíčů. To umožňuje obnovu seznamu mintů napříč zařízeními.", notifications: { enabled: "Záloha mintů na Nostr povolena", disabled: "Záloha mintů na Nostr zakázána", failed: "Nepodařilo se povolit zálohu mintů na Nostr", }, }, }, appearance: { keyboard: { title: "Klávesnice na obrazovce", description: "Použít číselnou klávesnici pro zadávání částek.", toggle: "Použít číselnou klávesnici", toggle_description: "Pokud je povoleno, pro zadávání částek bude použita číselná klávesnice.", }, theme: { title: "Vzhled", description: "Změňte vzhled své peněženky.", tooltips: { mono: "mono", cyber: "cyber", freedom: "freedom", nostr: "nostr", bitcoin: "bitcoin", mint: "mint", nut: "nut", blu: "blu", flamingo: "flamingo", }, }, bip177: { title: "Symbol Bitcoinu", description: "Používat symbol ₿ místo satů.", toggle: "Používat symbol ₿", }, }, web_of_trust: { title: "Web of trust", known_pubkeys: "Známé pubkey: {wotCount}", continue_crawl: "Pokračovat v procházení", crawl_odell: "Procházet ODELLŮV WEB OF TRUST", crawl_wot: "Procházet web of trust", pause: "Pozastavit", reset: "Resetovat", progress: "{crawlProcessed} / {crawlTotal}", }, npub_cash: { use_npubx: "Použít npubx.cash", copy_lightning_address: "Kopírovat Lightning adresu", v2_mint: "mint pro npub.cash v2", }, multinut: { use_multinut: "Použít Multinut", }, advanced: { title: "Pokročilé", developer: { title: "Vývojářská nastavení", description: "Následující nastavení slouží pro vývoj a ladění.", new_seed: { button: "Vygenerovat novou seed frázi", description: "Tímto se vygeneruje nová seed fráze. Abyste ji mohli později obnovit, musíte si nejprve poslat celý zůstatek sami sobě.", confirm_question: "Opravdu chcete vygenerovat novou seed frázi?", cancel: "Zrušit", confirm: "Potvrdit", }, remove_spent: { button: "Odstranit utracené proofy", description: "Zkontroluje, zda jsou ecash tokeny z vašich aktivních mintů utracené, a odstraní ty utracené z peněženky. Používejte pouze v případě, že je peněženka zaseknutá.", }, debug_console: { button: "Přepnout ladicí konzoli", description: "Otevře ladicí Javascript konzoli. Nikdy sem nevkládejte nic, čemu nerozumíte. Útočník by vás mohl přimět vložit sem škodlivý kód.", }, export_proofs: { button: "Exportovat aktivní proofy", description: "Zkopíruje celý váš zůstatek z aktivního mintu jako Cashu token do schránky. Exportuje se pouze vybraný mint a jednotka. Pro kompletní export vyberte jiný mint a jednotku a export opakujte.", }, keyset_counters: { title: "Navýšit čítače keysetů", description: "Kliknutím na ID keysetu zvýšíte čítače derivační cesty pro keysety ve vaší peněžence. To je užitečné, pokud se zobrazí chyba „outputs have already been signed“.", counter: "čítač: {count}", }, unset_reserved: { button: "Zrušit rezervaci všech tokenů", description: "Tato peněženka označuje čekající odchozí ecash jako rezervovaný (a odečítá jej ze zůstatku), aby zabránila pokusům o double-spend. Toto tlačítko zruší rezervaci všech tokenů, aby je bylo možné znovu použít. Pokud tak učiníte, peněženka může obsahovat utracené proofy. Pro jejich odstranění použijte tlačítko „Odstranit utracené proofy“.", }, show_onboarding: { button: "Zobrazit onboarding", description: "Znovu zobrazí úvodní obrazovky.", }, reset_wallet: { button: "Resetovat data peněženky", description: "Resetuje data vaší peněženky. Varování: Tímto dojde ke smazání všeho! Nezapomeňte si nejprve vytvořit zálohu.", confirm_question: "Opravdu chcete smazat data peněženky?", cancel: "Zrušit", confirm: "Smazat peněženku", }, export_wallet: { button: "Exportovat data peněženky", description: "Stáhne výpis vaší peněženky. Z tohoto souboru můžete obnovit peněženku na úvodní obrazovce nové peněženky. Pokud budete peněženku po exportu dál používat, soubor nebude aktuální.", }, import_wallet: { button: "Importovat zálohu peněženky", description: "Obnoví peněženku z dříve exportovaného záložního souboru. Tímto nahradíte aktuální data peněženky daty ze zálohy.", confirm_question: "Opravdu chcete obnovit data peněženky?", cancel: "Zrušit", confirm: "IMPORTOVAT ZÁLOHU PENĚŽENKY", }, }, }, }, NoMintWarnBanner: { title: "Připojte mint", subtitle: "Ještě jste se nepřipojili k žádnému Cashu mintu. Přidejte URL mintu v nastavení nebo přijměte ecash z nového mintu, abyste mohli začít.", actions: { add_mint: { label: "@:global.actions.add_mint.label", }, receive: { label: "Přijmout Ecash", }, }, }, WalletPage: { actions: { send: { label: "@:global.actions.send.label", }, receive: { label: "@:global.actions.receive.label", }, }, tabs: { history: { label: "Historie", }, invoices: { label: "Faktury", }, mints: { label: "Minty", }, }, install: { text: "Instalovat", tooltip: "Nainstalovat Cashu", }, }, AlreadyRunning: { title: "Ne.", text: "Jiná karta už běží. Zavřete ji a zkuste to znovu.", actions: { retry: { label: "Zkusit znovu", }, }, }, ErrorNotFound: { title: "404", text: "Oops. Nic tu není…", actions: { home: { label: "Zpět domů", }, }, }, BalanceView: { mintUrl: { label: "Mint", }, mintBalance: { label: "Zůstatek", }, mintError: { label: "Chyba mintu", }, pending: { label: "Čekající", tooltip: "Zkontrolovat všechny čekající tokeny", }, }, WelcomePage: { actions: { previous: { label: "Předchozí", }, next: { label: "Další", }, }, }, WelcomeSlide1: { title: "Vítejte v Cashu", text: "Cashu.me je bezplatná a open-source bitcoinová peněženka, která používá ecash pro bezpečné a soukromé uchování vašich prostředků.", actions: { more: { label: "Klikněte pro více informací", }, }, p1: { text: "Cashu je bezplatný a open-source ecash protokol pro Bitcoin. Více informací najdete na { link }.", link: { text: "cashu.space", }, }, p2: { text: "Tato peněženka není spojena s žádným mintem. Chcete-li ji používat, musíte se připojit k jednomu nebo více Cashu mintům, kterým důvěřujete.", }, p3: { text: "Tato peněženka uchovává ecash, ke kterému máte přístup pouze vy. Pokud smažete data prohlížeče bez zálohy seed fráze, ztratíte své tokeny.", }, p4: { text: "Tato peněženka je v beta verzi. Nenese odpovědnost za ztrátu přístupu k prostředkům. Používejte na vlastní riziko! Tento kód je open-source a licencován pod licencí MIT.", }, }, WelcomeSlide2: { title: "Nainstalujte PWA", alt: { pwa_example: "Příklad instalace PWA", }, installing: "Instaluje se…", instruction: { intro: { text: "Pro nejlepší zážitek používejte tuto peněženku ve webovém prohlížeči vašeho zařízení a nainstalujte ji jako Progressive Web App.", }, android: { title: "Android (Chrome)", step1: { item: "1. { icon } { text }", text: "Klepněte na menu (vpravo nahoře)", }, step2: { item: "2. { icon } { text }", text: "Stiskněte { buttonText }", buttonText: "@:AndroidPWAPrompt.buttonText", }, }, ios: { title: "iOS (Safari)", step1: { item: "1. { icon } { text }", text: "Klepněte na sdílení (dole)", }, step2: { item: "2. { icon } { text }", text: "Stiskněte { buttonText }", buttonText: "@:iOSPWAPrompt.buttonText", }, }, outro: { text: "Jakmile tuto aplikaci nainstalujete, zavřete okno prohlížeče a používejte aplikaci z domovské obrazovky.", }, }, pwa: { success: { title: "Úspěch!", text: "Používáte Cashu jako PWA. Zavřete všechny ostatní okna prohlížeče a používejte aplikaci z domovské obrazovky.", nextSteps: "Nyní můžete zavřít tuto kartu prohlížeče a otevřít aplikaci z domovské obrazovky.", }, }, }, iOSPWAPrompt: { text: "Klepněte na { icon } a { buttonText }", buttonText: "Přidat na domovskou obrazovku", }, AndroidPWAPrompt: { text: "Klepněte na { icon } a { buttonText }", buttonText: "Přidat na domovskou obrazovku", }, WelcomeSlide3: { title: "Vaše seed fráze", text: "Uložte svou seed frázi do správce hesel nebo na papír. Vaše seed fráze je jediný způsob, jak obnovit prostředky, pokud ztratíte přístup k tomuto zařízení.", inputs: { seed_phrase: { label: "Seed fráze", caption: "Seed frázi najdete v nastavení.", }, checkbox: { label: "Zapsal(a) jsem si ji", }, }, }, WelcomeSlide4: { title: "Podmínky", actions: { more: { label: "Přečíst Podmínky služby", }, }, inputs: { checkbox: { label: "Přečetl(a) jsem a souhlasím s těmito podmínkami", }, }, }, WelcomeSlideChoice: { title: "Nastavte svou peněženku", text: "Chcete obnovit ze seed fráze nebo vytvořit novou peněženku?", options: { new: { title: "Vytvořit novou peněženku", subtitle: "Vygenerovat nový seed a přidat minty.", }, recover: { title: "Obnovit peněženku", subtitle: "Zadejte svou seed frázi, obnovte minty a ecash.", }, }, }, WelcomeMintSetup: { title: "Přidat minty", text: "Minty jsou servery, které vám pomáhají odesílat a přijímat ecash. Vyberte nalezený mint nebo přidejte ručně. Přeskočte a přidejte minty později.", sections: { your_mints: "Vaše minty", }, restoring: "Obnovují se minty…", placeholder: { mint_url: "https://", }, }, WelcomeRecoverSeed: { title: "Zadejte seed frázi", text: "Vložte nebo napište svou 12slovnou seed frázi pro obnovení.", inputs: { word: "Slovo { index }", }, actions: { paste_all: "Vložit vše", }, disclaimer: "Vaše seed fráze se používá pouze lokálně pro odvození klíčů peněženky.", }, WelcomeRestoreEcash: { title: "Obnovte svůj ecash", text: "Skenujte nevyužité proofy na nakonfigurovaných mintech a přidejte je do peněženky.", }, MintRatings: { title: "Recenze mintu", reviews: "recenze", ratings: "Hodnocení", no_reviews: "Žádné recenze nebyly nalezeny", your_review: "Vaše recenze", no_reviews_to_display: "Žádné recenze k zobrazení.", no_rating: "Bez hodnocení", out_of: "z", rows: "Recenze", sort: "Třídit", sort_options: { newest: "Nejnovější", oldest: "Nejstarší", highest: "Nejvyšší", lowest: "Nejnižší", }, actions: { write_review: "Napsat recenzi", }, empty_state_subtitle: "Pomozte tím, že napíšete recenzi. Sdílejte svou zkušenost s tímto mintem a pomozte ostatním.", }, CreateMintReview: { title: "Recenze mintu", publishing_as: "Publikujete jako", inputs: { rating: { label: "Hodnocení" }, review: { label: "Recenze (volitelně)" }, }, actions: { publish: { label: "Odeslat recenzi", in_progress: "Odesílání…" }, }, }, RestoreView: { seed_phrase: { label: "Obnovit ze seed fráze", caption: "Zadejte svou seed frázi pro obnovení peněženky. Před obnovením se ujistěte, že jste přidali všechny minty, které jste dříve používali.", inputs: { seed_phrase: { label: "Seed fráze", caption: "Seed frázi najdete v nastavení.", }, }, }, information: { label: "Informace", caption: "Tento průvodce obnoví pouze ecash z jiné seed fráze, nebudete moci tuto seed frázi používat ani měnit seed frázi aktuální peněženky. Obnovený ecash tak nebude chráněn vaší aktuální seed frází, dokud ho jednou nepošlete sami sobě.", }, restore_mints: { label: "Obnovit minty", caption: 'Vyberte mint k obnovení. Další minty můžete přidat na hlavní obrazovce pod "Minty" a obnovit je zde.', }, actions: { paste: { error: "Nepodařilo se načíst obsah schránky.", }, validate: { error: "Mnemonic musí mít alespoň 12 slov.", }, select_all: { label: "Vybrat vše", }, deselect_all: { label: "Odznačit vše", }, restore: { label: "Obnovit", in_progress: "Obnovuje se mint …", error: "Chyba při obnově mintu: { error }", }, restore_all_mints: { label: "Obnovit všechny minty", in_progress: "Obnovuje se mint { index } z { length } …", success: "Obnova dokončena úspěšně", error: "Chyba při obnově mintů: { error }", }, restore_selected_mints: { label: "Obnovit vybrané minty ({count})", in_progress: "Obnovuje se mint { index } z { length } …", success: "Úspěšně obnoven(a) {count} mint(y)", error: "Chyba při obnově vybraných mintů: { error }", }, }, nostr_mints: { label: "Obnovit minty z Nostr", caption: "Hledejte zálohy mintů uložené na Nostr relays pomocí vaší seed fráze. Pomůže to objevit minty, které jste dříve používali.", search_button: "Hledat zálohy mintů", select_all: "Vybrat vše", deselect_all: "Odznačit vše", backed_up: "Zálohováno", already_added: "Již přidáno", add_selected: "Přidat vybrané ({count})", no_backups_found: "Nenalezeny žádné zálohy mintů", no_backups_hint: "Ujistěte se, že záloha mintů na Nostr je v nastavení povolena, aby se váš seznam mintů automaticky zálohoval.", invalid_mnemonic: "Než budete hledat, zadejte platnou seed frázi.", search_error: "Nepodařilo se vyhledat zálohy mintů.", add_error: "Nepodařilo se přidat vybrané minty.", }, }, MintSettings: { add: { title: "Přidat mint", description: "Zadejte URL Cashu mintu, ke kterému se chcete připojit. Tato peněženka není spojena s žádným mintem.", inputs: { nickname: { placeholder: "Přezdívka (např. Testnet)", }, }, actions: { add_mint: { label: "@:global.actions.add_mint.label", error_invalid_url: "Neplatná URL", }, scan: { label: "Skenovat QR kód", }, }, }, discover: { title: "Objevte minty", overline: "Objevovat", caption: "Objevte minty, které doporučili jiní uživatelé na Nostr.", actions: { discover: { label: "Objevovat minty", in_progress: "Načítá se…", error_no_mints: "Nenalezeny žádné minty", success: "Nalezeno { length } mintů", }, }, recommendations: { overline: "Nalezeno { length } mintů", caption: "Tyto minty doporučili ostatní uživatelé Nostr. Buďte opatrní a prověřte mint před použitím.", actions: { browse: { label: "Klikněte pro prohlížení mintů", }, }, }, }, swap: { title: "Swap", overline: "Multimint Swapy", actions: { receove_to_trusted_mint: { label: "Přijmout do důvěryhodného mintu", }, swap: { label: "@:global.actions.swap.label", in_progress: "@:MintSettings.swap.actions.swap.label", }, }, caption: "Prohození prostředků mezi minty přes Lightning. Poznámka: Nechte rezervu na poplatky Lightning. Pokud příchozí platba neproběhne, zkontrolujte fakturu manuálně.", inputs: { from: { label: "Odesílatel", }, to: { label: "Příjemce", }, amount: { label: "Částka ({ ticker })", }, }, }, error_badge: "Chyba", reviews_text: "recenze", no_reviews_yet: "Žádné recenze", discover_mints_button: "Objevovat minty", }, QrcodeReader: { progress: { text: "{ percentage }{ addon }", percentage: "{ percentage }%", keep_scanning_text: " - Pokračovat ve skenování", }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, }, }, InvoiceDetailDialog: { title: "Přijmout Lightning", create_invoice_title: "Vytvořit fakturu", inputs: { amount: { label: "Částka ({ ticker }) *", }, }, actions: { close: { label: "@:global.actions.close.label", }, create: { label: "Vytvořit fakturu", label_blocked: "Vytváří se faktura…", in_progress: "Vytváří se", }, }, invoice: { caption: "Faktura Lightning", status_paid_text: "Zaplaceno!", actions: { close: { label: "@:global.actions.close.label", }, copy: { label: "@:global.actions.copy.label", }, }, }, }, SendDialog: { title: "Odeslat", actions: { ecash: { label: "Ecash", error_no_mints: "Žádné minty k dispozici", }, lightning: { label: "Lightning", error_no_mints: "Žádné minty k dispozici", }, }, }, SendTokenDialog: { title: "Odeslat Ecash", title_ecash_text: "Ecash", badge_offline_text: "Offline", inputs: { amount: { label: "Částka ({ ticker }) *", invalid_too_much_error_text: "Příliš mnoho", }, p2pk_pubkey: { label: "Veřejný klíč příjemce", label_invalid: "Neplatný veřejný klíč příjemce", }, }, actions: { close: { label: "@:global.actions.close.label", }, close_card_scanner: { label: "@:global.actions.close.label", }, copy_emoji: { label: "🥜", tooltip_text: "Kopírovat emoji", }, copy_tokens: { label: "@:global.actions.copy.label", }, copy_link: { tooltip_text: "Kopírovat odkaz", }, share: { tooltip_text: "Sdílet ecash", }, lock: { label: "@:global.actions.lock.label", }, paste_p2pk_pubkey: { tooltip_text: "@:global.actions.paste.label", }, pay: { label: "@:global.actions.pay.label", }, send: { label: "@:global.actions.send.label", }, delete: { tooltip_text: "Smazat z historie", }, write_tokens_to_card: { tooltips: { ndef_supported_text: "Zapsat na NFC kartu", ndef_unsupported_text: "NDEF není podporováno", }, }, }, errors: { amount_required: "Nejprve zadejte částku.", serialization_failed: "Nepodařilo se připravit ecash token.", }, }, SendPaymentRequest: { actions: { pay: { label: "Zaplatit", }, pay_via: { label: "Zaplatit přes {transport}", }, }, info: { pay_to: "Zaplatit {target}", invalid_url: "Neplatná URL", }, }, PaymentRequestInfo: { title_with_transport: "Platební požadavek přes {transport}", title: "Platební požadavek", subtitle: "Zaplatit {target}", subtitle_fallback: "Platební požadavek", invalid_url: "Neplatná URL", }, ReceiveDialog: { title: "Přijmout", actions: { ecash: { label: "Ecash", error_no_mints: "Žádné minty k dispozici", }, lightning: { label: "Lightning", error_no_mints: "Pro příjem přes Lightning se musíte připojit k mintu", }, }, }, ReceiveEcashDrawer: { title: "Přijmout Ecash", actions: { paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, request: { label: "Požádat", }, lock: { label: "@:global.actions.lock.label", }, nfc: { label: "NFC", scanning_text: "Skenuje se…", }, }, }, ReceiveTokenDialog: { title: "Přijmout Ecash", title_ecash_text: "Ecash", inputs: { tokens_base64: { label: "Vložit Cashu token", }, }, errors: { invalid_token: { label: "Neplatný token", }, p2pk_lock_mismatch: { label: "Nelze přijmout. P2PK zámek tohoto tokenu neodpovídá vašemu veřejnému klíči.", }, }, unknown_mint_info_text: "Neznámý mint. Bude přidán po přijetí tohoto tokenu.", swap_section: { title: "Swap", source_label: "Odesílatel", destination_label: "Příjemce", fee_info: "Tento swap podléhá poplatkům Lightning sítě.", }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, scan: { label: "@:global.actions.scan.label", }, receive: { label: "@:global.actions.receive.label", label_known_mint: "@:ReceiveTokenDialog.actions.receive.label", label_adding_mint: "Přidává se mint…", }, swap: { label: "Přijmout do důvěryhodného mintu", tooltip_text: "Swap do důvěryhodného mintu", caption: "Swap { value }", processing: "Probíhá swap…", failed: "Swap selhal", }, cancel_swap: { label: "@:global.actions.cancel.label", tooltip_text: "Zrušit swap", }, confirm_swap: { label: "@:ReceiveTokenDialog.actions.swap.label", tooltip_text: "@:ReceiveTokenDialog.actions.swap.tooltip_text", in_progress: "@:ReceiveTokenDialog.actions.confirm_swap.label", }, receive_to_selected_mint: { label: "Přijmout do vybraného mintu", }, later: { label: "Přijmout později", tooltip_text: "Přidat do historie pro pozdější příjem", already_in_history_success_text: "Ecash již v historii", added_to_history_success_text: "Ecash přidán do historie", }, nfc: { label: "NFC", tooltips: { ndef_supported_text: "Přečíst z NFC karty", ndef_unsupported_text: "NDEF není podporováno", }, }, }, }, P2PKDialog: { p2pk: { caption: "P2PK klíč", description: "Přijímat ecash uzamčený tímto klíčem", used_warning_text: "Varování: Tento klíč byl již použit. Pro lepší soukromí použijte nový klíč.", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_key: { label: "Vygenerovat nový klíč", }, }, }, PaymentRequestDialog: { payment_request: { caption: "Platební požadavek", description: "Přijímat platby přes Nostr", }, received_total: "Celkem přijato", no_payments_yet: "Žádné platby dosud", actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_request: { label: "Nový požadavek", }, add_amount: { label: "Přidat částku", }, use_active_mint: { label: "Libovolný mint", }, }, inputs: { amount: { placeholder: "Zadejte částku", }, }, }, NumericKeyboard: { actions: { close: { label: "@:global.actions.close.label", closed_info_text: "Klávesnice zakázána. Opět ji můžete povolit v nastavení.", }, enter: { label: "@:global.actions.enter.label", }, }, }, NWCDialog: { nwc: { caption: "Nostr Wallet Connect", description: "Ovládejte svou peněženku vzdáleně pomocí NWC. Stiskněte QR kód pro propojení peněženky s kompatibilní aplikací.", warning_text: "Varování: kdokoli s přístupem k tomuto spojovacímu řetězci může provádět platby z vaší peněženky. Nesdílejte!", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, }, }, MintMotdMessage: { title: "Zpráva mintu", }, MintDetailsDialog: { contact: { title: "Kontakt", }, details: { title: "Detaily mintu", url: { label: "URL", }, nuts: { label: "Nuts", actions: { show: { label: "Zobrazit vše", }, hide: { label: "Skrýt", }, }, }, currency: { label: "Měna", }, currencies: { label: "@:MintDetailsDialog.details.currency.label", }, version: { label: "Verze", }, }, actions: { title: "Akce", copy_mint_url: { label: "Kopírovat URL mintu", }, delete: { label: "Smazat mint", }, edit: { label: "Upravit mint", }, }, }, ChooseMint: { title: "Vyberte mint", placeholder: "Vyberte mint", available_text: "dostupný", sheet_title: "Vybrat mint", badge_mint_error_text: "Chyba", badge_option_mint_error_text: "@:ChooseMint.badge_mint_error_text", }, HistoryTable: { empty_text: "Žádná historie zatím", row: { type_label: "Ecash", date_label: "před { value }", }, actions: { check_status: { tooltip_text: "Zkontrolovat stav", }, receive: { tooltip_text: "Přijmout", }, filter_pending: { label: "Filtrovat čekající", }, show_all: { label: "Zobrazit vše", }, }, old_token_not_found_error_text: "Starý token nenalezen", }, InvoiceTable: { empty_text: "Žádné faktury zatím", row: { type_label: "Lightning", type_tooltip_text: "Klikněte pro kopírování", date_label: "před { value }", }, actions: { check_status: { tooltip_text: "Zkontrolovat stav", }, filter_pending: { label: "Filtrovat čekající", }, show_all: { label: "Zobrazit vše", }, }, }, RemoveMintDialog: { title: "Opravdu chcete smazat tento mint?", nickname: { label: "Přezdívka", }, balances: { label: "Zůstatky", }, warning_text: "Poznámka: Protože je tato peněženka paranoidní, váš ecash z tohoto mintu nebude skutečně smazán, ale zůstane uložen na zařízení. Uvidíte ho znovu, pokud tento mint později znovu přidáte.", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { confirm: { label: "Odstranit mint", }, cancel: { label: "@:global.actions.cancel.label", }, }, }, ParseInputComponent: { placeholder: { default: "Cashu token nebo Lightning adresa", receive: "Cashu token", pay: "Lightning adresa nebo faktura", }, qr_scanner: { title: "Skenovat QR kód", description: "Klikněte pro skenování adresy", }, paste_button: { label: "@:global.actions.paste.label", }, }, PayInvoiceDialog: { input_data: { title: "Zaplatit Lightning", inputs: { invoice_data: { label: "Lightning faktura nebo adresa", }, }, actions: { close: { label: "@:global.actions.close.label", }, enter: { label: "@:global.actions.enter.label", }, paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, }, }, lnurlpay: { amount_exact_label: "{ payee } požaduje { value } { ticker }", amount_range_label: "{ payee } požaduje{br}mezi { min } a { max } { ticker }", sending_to_lightning_address: "Odesílám na { address }", inputs: { amount: { label: "Částka ({ ticker }) *", }, comment: { label: "Komentář (volitelné)", }, }, actions: { close: { label: "@:global.actions.close.label", }, send: { label: "@:global.actions.send.label", }, }, }, invoice: { title: "Zaplatit { value }", paying: "Probíhá platba", paid: "Zaplaceno", fee: "Poplatek", memo: { label: "Poznámka", }, processing_info_text: "Zpracovává se…", balance_too_low_warning_text: "Zůstatek příliš nízký", actions: { close: { label: "@:global.actions.close.label", }, pay: { label: "Zaplatit", in_progress: "@:PayInvoiceDialog.invoice.processing_info_text", error: "Chyba", }, }, }, }, EditMintDialog: { title: "Upravit mint", inputs: { nickname: { label: "Přezdívka", }, mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, update: { label: "@:global.actions.update.label", }, }, }, AddMintDialog: { title: "Důvěřujete tomuto mintu?", description: "Před použitím tohoto mintu se ujistěte, že mu důvěřujete. Mints mohou kdykoli přestat fungovat nebo se stát škodlivými.", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, add_mint: { label: "@:global.actions.add_mint.label", in_progress: "Přidává se mint", }, }, }, restore: { mnemonic_error_text: "Zadejte mnemotechnickou frázi", restore_mint_error_text: "Chyba při obnově mintu: { error }", prepare_info_text: "Připravuji proces obnovy…", restored_proofs_for_keyset_info_text: "Obnoveno { restoreCounter } důkazů pro keyset { keysetId }", checking_proofs_for_keyset_info_text: "Kontroluji důkazy { startIndex } až { endIndex } pro keyset { keysetId }", no_proofs_info_text: "Nebyly nalezeny žádné důkazy k obnově", restored_amount_success_text: "Obnoveno { amount }", }, swap: { in_progress_warning_text: "Probíhá swap", invalid_swap_data_error_text: "Neplatná data pro swap", swap_error_text: "Chyba při swapu", }, TokenInformation: { fee: "Poplatek", unit: "Jednotka", fiat: "Fiat", p2pk: "P2PK", locked: "Uzamčeno", locked_to_you: "Uzamčeno pro vás", mint: "Mint", memo: "Poznámka", payment_request: "Platební požadavek", nostr: "Nostr", token_copied: "Token zkopírován do schránky", }, }; ================================================ FILE: src/i18n/de-DE/index.ts ================================================ export default { MultinutPicker: { payment: "Multinut-Zahlung", selectMints: "Wählen Sie eine oder mehrere Mints aus, um eine Zahlung auszuführen.", totalSelectedBalance: "Gesamtes ausgewähltes Guthaben", multiMintPay: "Multi-Mint-Zahlung", balanceNotEnough: "Das Multi-Mint-Guthaben reicht nicht aus, um diese Rechnung zu begleichen", failed: "Verarbeitung fehlgeschlagen: {error}", paid: "{amount} über Lightning bezahlt", }, global: { copy_to_clipboard: { success: "In die Zwischenablage kopiert!", }, actions: { add_mint: { label: "Mint hinzufügen", }, cancel: { label: "Abbrechen", }, copy: { label: "Kopieren", }, close: { label: "Schließen", }, enter: { label: "Eingeben", }, lock: { label: "Sperren", }, paste: { label: "Einfügen", }, receive: { label: "Empfangen", }, scan: { label: "Scannen", }, send: { label: "Senden", }, swap: { label: "Tauschen", }, update: { label: "Aktualisieren", }, }, inputs: { mint_url: { label: "Mint URL", }, }, }, wallet: { notifications: { balance_too_low: "Guthaben ist zu niedrig", received: "{amount} empfangen", fee: " (Gebühr: {fee})", could_not_request_mint: "Mint konnte nicht angefordert werden", invoice_still_pending: "Rechnung noch ausstehend", paid_lightning: "{amount} über Lightning bezahlt", payment_pending_refresh: "Zahlung ausstehend. Rechnung manuell aktualisieren.", sent: "{amount} gesendet", token_still_pending: "Token noch ausstehend", received_lightning: "{amount} über Lightning empfangen", lightning_payment_failed: "Lightning-Zahlung fehlgeschlagen", failed_to_decode_invoice: "Rechnung konnte nicht dekodiert werden", invalid_lnurl: "Ungültige LNURL", lnurl_error: "LNURL Fehler", no_amount: "Kein Betrag", no_lnurl_data: "Keine LNURL-Daten", no_price_data: "Keine Preisdaten.", please_try_again: "Bitte versuchen Sie es erneut.", }, mint: { notifications: { already_added: "Mint bereits hinzugefügt", added: "Mint hinzugefügt", not_found: "Mint nicht gefunden", activation_failed: "Mint-Aktivierung fehlgeschlagen", no_active_mint: "Kein aktives Mint", unit_activation_failed: "Einheiten-Aktivierung fehlgeschlagen", unit_not_supported: "Einheit wird von Mint nicht unterstützt", activated: "Mint aktiviert", could_not_connect: "Verbindung zum Mint nicht möglich", could_not_get_info: "Mint-Information konnte nicht abgerufen werden", could_not_get_keys: "Mint-Schlüssel konnten nicht abgerufen werden", could_not_get_keysets: "Mint-Keysets konnten nicht abgerufen werden", mint_validation_error: "Mint-Validierungsfehler", removed: "Mint entfernt", error: "Mint-Fehler", }, }, }, MainHeader: { menu: { settings: { title: "Einstellungen", settings: { title: "Einstellungen", caption: "Wallet-Konfiguration", }, }, terms: { title: "Bedingungen", terms: { title: "Bedingungen", caption: "Nutzungsbedingungen", }, }, links: { title: "Links", cashuSpace: { title: "Cashu.space", caption: "cashu.space", }, github: { title: "Github", caption: "github.com/cashubtc", }, telegram: { title: "Telegram", caption: "t.me/CashuMe", }, twitter: { title: "Twitter", caption: "{'@'}CashuBTC", }, donate: { title: "Spenden", caption: "Cashu unterstützen", }, }, }, offline: { warning: { text: "Offline", }, }, reload: { warning: { text: "Neu laden in { countdown }", }, }, staging: { warning: { text: "Staging – nicht mit echten Geldern verwenden!", }, }, }, FullscreenHeader: { actions: { back: { label: "Wallet", }, }, }, Settings: { language: { title: "Sprache", description: "Bitte wählen Sie Ihre bevorzugte Sprache aus der Liste unten.", }, sections: { backup_restore: "SICHERUNG & WIEDERHERSTELLUNG", lightning_address: "LIGHTNING ADRESSE", nostr_keys: "NOSTR SCHLÜSSEL", nostr: { title: "NOSTR", relays: { expand_label: "Klicken, um Relays zu bearbeiten", add: { title: "Relay hinzufügen", description: "Ihre Wallet verwendet diese Relays für Nostr‑Operationen wie Zahlungsanforderungen, Nostr Wallet Connect und Backups.", }, list: { title: "Relays", description: "Ihre Wallet verbindet sich mit diesen Relays.", copy_tooltip: "Relay kopieren", remove_tooltip: "Relay entfernen", }, }, }, payment_requests: "ZAHLUNGSANFORDERUNGEN", nostr_wallet_connect: "NOSTR WALLET CONNECT", hardware_features: "HARDWARE FUNKTIONEN", p2pk_features: "P2PK FUNKTIONEN", privacy: "DATENSCHUTZ", experimental: "EXPERIMENTELL", appearance: "AUSSEHEN", }, backup_restore: { backup_seed: { title: "Seed-Phrase sichern", description: "Ihre Seed-Phrase kann Ihre Wallet wiederherstellen. Bewahren Sie sie sicher und privat auf.", seed_phrase_label: "Seed-Phrase", }, restore_ecash: { title: "Ecash wiederherstellen", description: "Der Wiederherstellungs-Assistent ermöglicht es Ihnen, verlorenes Ecash von einer mnemonischen Seed-Phrase wiederherzustellen. Die Seed-Phrase Ihrer aktuellen Wallet bleibt unverändert, der Assistent erlaubt es Ihnen lediglich, Ecash von einer anderen Seed-Phrase zu restaurieren.", button: "Wiederherstellen", }, }, lightning_address: { title: "Lightning Adresse", description: "Zahlungen an Ihre Lightning Adresse empfangen.", enable: { toggle: "Aktivieren", description: "Lightning Adresse mit npub.cash", }, address: { copy_tooltip: "Lightning Adresse kopieren", }, automatic_claim: { toggle: "Automatisch beanspruchen", description: "Eingehende Zahlungen automatisch empfangen.", }, npc_v2: { choose_mint_title: "Wählen Sie eine Mint für npub.cash v2", choose_mint_placeholder: "Wählen Sie eine Mint...", }, }, web_of_trust: { title: "Vertrauensnetzwerk", known_pubkeys: "Bekannte Pubkeys: {wotCount}", continue_crawl: "Crawl fortsetzen", crawl_odell: "ODELL'S WEB OF TRUST crawlen", crawl_wot: "Web of Trust crawlen", pause: "Pausieren", reset: "Zurücksetzen", progress: "{crawlProcessed} / {crawlTotal}", }, npub_cash: { use_npubx: "npubx.cash verwenden", copy_lightning_address: "Lightning-Adresse kopieren", v2_mint: "npub.cash v2 Mint", }, multinut: { use_multinut: "Multinut verwenden", }, nostr_keys: { title: "Ihre Nostr-Schlüssel", description: "Legen Sie die Nostr-Schlüssel für Ihre Lightning-Adresse fest.", wallet_seed: { title: "Wallet Seed-Phrase", description: "Nostr-Schlüsselpaar aus Wallet-Seed generieren", copy_nsec: "Nsec kopieren", }, nsec_bunker: { title: "Nsec Bunker", description: "Einen NIP-46 Bunker verwenden", delete_tooltip: "Verbindung löschen", }, use_nsec: { title: "Ihren Nsec verwenden", description: "Diese Methode ist gefährlich und wird nicht empfohlen", delete_tooltip: "Nsec löschen", }, signing_extension: { title: "Signaturerweiterung", description: "Eine NIP-07 Signaturerweiterung verwenden", not_found: "Keine NIP-07 Signaturerweiterung gefunden", }, }, payment_requests: { title: "Zahlungsanforderungen", description: "Zahlungsanforderungen ermöglichen es Ihnen, Zahlungen über Nostr zu empfangen. Wenn Sie dies aktivieren, abonniert Ihre Wallet Ihre Nostr-Relays.", enable_toggle: "Zahlungsanforderungen aktivieren", claim_automatically: { toggle: "Automatisch beanspruchen", description: "Eingehende Zahlungen automatisch empfangen.", }, }, nostr_wallet_connect: { title: "Nostr Wallet Connect (NWC)", description: "Verwenden Sie NWC, um Ihre Wallet von jeder anderen Anwendung aus zu steuern.", enable_toggle: "NWC aktivieren", payments_note: "Sie können NWC nur für Zahlungen von Ihrem Bitcoin-Guthaben verwenden. Zahlungen werden von Ihrer aktiven Mint vorgenommen.", connection: { copy_tooltip: "Verbindungsstring kopieren", qr_tooltip: "QR-Code anzeigen", allowance_label: "Restliches Guthaben (sat)", }, }, hardware_features: { webnfc: { title: "WebNFC", description: "Wählen Sie die Kodierung für das Schreiben auf NFC-Karten", text: { title: "Text", description: "Token als Klartext speichern", }, weburl: { title: "URL", description: "URL zu dieser Wallet mit Token speichern", }, binary: { title: "Binary", description: "Token als Binärdaten speichern", }, quick_access: { toggle: "Schnellzugriff auf NFC", description: "Schnelles Scannen von NFC-Karten im Ecash empfangen-Menü. Diese Option fügt einen NFC-Button zum Ecash empfangen-Menü hinzu.", }, }, }, p2pk_features: { title: "P2PK", description: "Generieren Sie ein Schlüsselpaar, um P2PK-gesperrten Ecash zu erhalten. Warnung: Diese Funktion ist experimentell. Nur mit kleinen Beträgen verwenden. Wenn Sie Ihre privaten Schlüssel verlieren, kann niemand mehr den darauf gesperrten Ecash freischalten.", generate_button: "Schlüssel generieren", import_button: "Nsec importieren", quick_access: { toggle: "Schnellzugriff auf Sperre", description: "Verwenden Sie dies, um Ihren P2PK-Sperrschlüssel schnell im Ecash empfangen-Menü anzuzeigen.", }, keys_expansion: { label: "Klicken, um {count} Schlüssel zu durchsuchen", used_badge: "verwendet", }, }, privacy: { title: "Datenschutz", description: "Diese Einstellungen beeinflussen Ihren Datenschutz.", check_incoming: { toggle: "Eingehende Rechnung prüfen", description: "Wenn aktiviert, prüft die Wallet die neueste Rechnung im Hintergrund. Dies erhöht die Reaktionsfähigkeit der Wallet, was das Fingerprinting erleichtert. Sie können unbezahlte Rechnungen manuell im Reiter 'Rechnungen' prüfen.", }, check_startup: { toggle: "Ausstehende Rechnungen beim Start prüfen", description: "Wenn aktiviert, prüft die Wallet beim Start ausstehende Rechnungen der letzten 24 Stunden.", }, check_all: { toggle: "Alle Rechnungen prüfen", description: "Wenn aktiviert, prüft die Wallet unbezahlte Rechnungen im Hintergrund für bis zu zwei Wochen. Dies erhöht die Online-Aktivität der Wallet, was das Fingerprinting erleichtert. Sie können unbezahlte Rechnungen manuell im Reiter 'Rechnungen' prüfen.", }, check_sent: { toggle: "Gesendeten Ecash prüfen", description: "Wenn aktiviert, verwendet die Wallet periodische Hintergrundprüfungen, um festzustellen, ob gesendete Token eingelöst wurden. Dies erhöht die Online-Aktivität der Wallet, was das Fingerprinting erleichtert.", }, websockets: { toggle: "WebSockets verwenden", description: "Wenn aktiviert, verwendet die Wallet langlebige WebSocket-Verbindungen, um Updates zu bezahlten Rechnungen und ausgegebenen Token von Mints zu erhalten. Dies erhöht die Reaktionsfähigkeit der Wallet, macht aber auch das Fingerprinting einfacher.", }, bitcoin_price: { toggle: "Wechselkurs von Coinbase abrufen", description: "Wenn aktiviert, wird der aktuelle Bitcoin-Wechselkurs von coinbase.com abgerufen und Ihr umgerechnetes Guthaben angezeigt.", currency: { title: "Fiat-Währung", description: "Wählen Sie die Fiat-Währung für die Bitcoin-Preisanzeige.", }, }, }, experimental: { title: "Experimentell", description: "Diese Funktionen sind experimentell.", receive_swaps: { toggle: "Swaps empfangen", badge: "Beta", description: "Option, empfangenen Ecash in Ihrer aktiven Mint im Dialog 'Ecash empfangen' zu tauschen.", }, auto_paste: { toggle: "Ecash automatisch einfügen", description: "Fügt Ecash automatisch aus Ihrer Zwischenablage ein, wenn Sie auf 'Empfangen', dann 'Ecash', dann 'Einfügen' drücken. Automatisches Einfügen kann auf iOS zu UI-Fehlern führen. Deaktivieren Sie dies, wenn Sie Probleme haben.", }, auditor: { toggle: "Auditor aktivieren", badge: "Beta", description: "Wenn aktiviert, zeigt die Wallet Auditor-Informationen im Dialog 'Mint-Details' an. Der Auditor ist ein Drittanbieter-Service, der die Zuverlässigkeit von Mints überwacht.", url_label: "Auditor URL", api_url_label: "Auditor API URL", }, multinut: { toggle: "Multinut aktivieren", description: "Wenn aktiviert, verwendet die Wallet Multinut, um Rechnungen von mehreren Mints gleichzeitig zu bezahlen.", }, nostr_mint_backup: { toggle: "Mint-Liste auf Nostr sichern", description: "Wenn aktiviert, wird Ihre Mint-Liste automatisch auf Nostr-Relays mit Ihren konfigurierten Nostr-Schlüsseln gesichert. Dies ermöglicht es Ihnen, Ihre Mint-Liste auf verschiedenen Geräten wiederherzustellen.", notifications: { enabled: "Nostr-Mint-Backup aktiviert", disabled: "Nostr-Mint-Backup deaktiviert", failed: "Fehler beim Aktivieren des Nostr-Mint-Backups", }, }, }, appearance: { keyboard: { title: "Bildschirmtastatur", description: "Verwenden Sie die Zifferntastatur zur Eingabe von Beträgen.", toggle: "Numerische Tastatur verwenden", toggle_description: "Wenn aktiviert, wird die numerische Tastatur zur Eingabe von Beträgen verwendet.", }, theme: { title: "Aussehen", description: "Ändern Sie das Aussehen Ihrer Wallet.", tooltips: { mono: "mono", cyber: "cyber", freedom: "freedom", nostr: "nostr", bitcoin: "bitcoin", mint: "mint", nut: "nut", blu: "blu", flamingo: "flamingo", }, }, bip177: { title: "Bitcoin-Symbol", description: "Verwenden Sie das ₿-Symbol anstelle von sats.", toggle: "₿-Symbol verwenden", }, }, advanced: { title: "Erweitert", developer: { title: "Entwickler-Einstellungen", description: "Die folgenden Einstellungen sind für Entwicklung und Debugging.", new_seed: { button: "Neue Seed-Phrase generieren", description: "Dies generiert eine neue Seed-Phrase. Sie müssen Ihr gesamtes Guthaben an sich selbst senden, um es mit einer neuen Seed wiederherstellen zu können.", confirm_question: "Sind Sie sicher, dass Sie eine neue Seed-Phrase generieren möchten?", cancel: "Abbrechen", confirm: "Bestätigen", }, remove_spent: { button: "Ausgegebene Nachweise entfernen", description: "Überprüfen Sie, ob die Ecash-Token von Ihren aktiven Mints ausgegeben wurden und entfernen Sie die ausgegebenen aus Ihrer Wallet. Verwenden Sie dies nur, wenn Ihre Wallet festhängt.", }, debug_console: { button: "Debug-Konsole umschalten", description: "Öffnen Sie das Javascript-Debug-Terminal. Fügen Sie niemals etwas in dieses Terminal ein, das Sie nicht verstehen. Ein Dieb könnte versuchen, Sie dazu zu bringen, bösartigen Code hier einzufügen.", }, export_proofs: { button: "Aktive Nachweise exportieren", description: "Kopieren Sie Ihr gesamtes Guthaben von der aktiven Mint als Cashu-Token in Ihre Zwischenablage. Dies exportiert nur die Token der ausgewählten Mint und Einheit. Für einen vollständigen Export wählen Sie eine andere Mint und Einheit und exportieren Sie erneut.", }, keyset_counters: { title: "Keyset-Zähler erhöhen", description: 'Klicken Sie auf die Keyset-ID, um die Ableitungspfad-Zähler für die Keysets in Ihrer Wallet zu erhöhen. Dies ist nützlich, wenn Sie die Fehlermeldung "outputs have already been signed" sehen.', counter: "Zähler: {count}", }, unset_reserved: { button: "Alle reservierten Token freigeben", description: 'Diese Wallet markiert ausstehenden ausgehenden Ecash als reserviert (und zieht es von Ihrem Guthaben ab), um Double-Spend-Versuche zu verhindern. Dieser Button gibt alle reservierten Token frei, damit sie wieder verwendet werden können. Wenn Sie dies tun, könnte Ihre Wallet ausgegebene Nachweise enthalten. Drücken Sie auf den Button "Ausgegebene Nachweise entfernen", um sie loszuwerden.', }, show_onboarding: { button: "Onboarding anzeigen", description: "Zeigen Sie den Onboarding-Bildschirm erneut an.", }, reset_wallet: { button: "Wallet-Daten zurücksetzen", description: "Setzen Sie Ihre Wallet-Daten zurück. Warnung: Dies löscht alles! Stellen Sie sicher, dass Sie vorher eine Sicherung erstellen.", confirm_question: "Sind Sie sicher, dass Sie Ihre Wallet-Daten löschen möchten?", cancel: "Abbrechen", confirm: "Wallet löschen", }, export_wallet: { button: "Wallet-Daten exportieren", description: "Laden Sie einen Dump Ihrer Wallet herunter. Sie können Ihre Wallet aus dieser Datei auf dem Willkommensbildschirm einer neuen Wallet wiederherstellen. Diese Datei ist nicht synchron, wenn Sie Ihre Wallet nach dem Export weiter verwenden.", }, }, }, }, NoMintWarnBanner: { title: "Einer Mint beitreten", subtitle: "Sie sind noch keiner Cashu Mint beigetreten. Fügen Sie eine Mint URL in den Einstellungen hinzu oder empfangen Sie Ecash von einer neuen Mint, um zu beginnen.", actions: { add_mint: { label: "@:global.actions.add_mint.label", }, receive: { label: "Ecash empfangen", }, }, }, WalletPage: { actions: { send: { label: "@:global.actions.send.label", }, receive: { label: "@:global.actions.receive.label", }, }, tabs: { history: { label: "Verlauf", }, invoices: { label: "Rechnungen", }, mints: { label: "Mints", }, }, install: { text: "Installieren", tooltip: "Cashu installieren", }, }, AlreadyRunning: { title: "Nein.", text: "Ein anderer Tab läuft bereits. Schließen Sie diesen Tab und versuchen Sie es erneut.", actions: { retry: { label: "Erneut versuchen", }, }, }, ErrorNotFound: { title: "404", text: "Ups. Nichts gefunden…", actions: { home: { label: "Zurück zur Startseite", }, }, }, BalanceView: { mintUrl: { label: "Mint", }, mintBalance: { label: "Guthaben", }, mintError: { label: "Mint-Fehler", }, pending: { label: "Ausstehend", tooltip: "Alle ausstehenden Token prüfen", }, }, WelcomePage: { actions: { previous: { label: "Zurück", }, next: { label: "Weiter", }, }, }, WelcomeSlide1: { title: "Willkommen bei Cashu", text: "Cashu.me ist eine kostenlose und quelloffene Bitcoin-Wallet, die Ecash verwendet, um Ihre Gelder sicher und privat zu halten.", actions: { more: { label: "Klicken, um mehr zu erfahren", }, }, p1: { text: "Cashu ist ein kostenloses und quelloffenes Ecash-Protokoll für Bitcoin. Mehr dazu erfahren Sie unter { link }.", link: { text: "cashu.space", }, }, p2: { text: "Diese Wallet ist nicht mit einer Mint affiliiert. Um diese Wallet zu nutzen, müssen Sie sich mit einer oder mehreren Cashu Mints verbinden, denen Sie vertrauen.", }, p3: { text: "Diese Wallet speichert Ecash, auf das nur Sie Zugriff haben. Wenn Sie Ihre Browserdaten ohne Seed-Phrase-Sicherung löschen, verlieren Sie Ihre Token.", }, p4: { text: "Diese Wallet ist in Beta. Wir übernehmen keine Verantwortung für den Verlust des Zugangs zu Geldern. Nutzung auf eigenes Risiko! Dieser Code ist quelloffen und unter der MIT-Lizenz lizenziert.", }, }, WelcomeSlide2: { title: "PWA installieren", alt: { pwa_example: "PWA Installationsbeispiel" }, installing: "Installiere…", instruction: { intro: { text: "Für die beste Erfahrung verwenden Sie diese Wallet mit dem nativen Webbrowser Ihres Geräts, um sie als Progressive Web App zu installieren. Machen Sie dies jetzt.", }, android: { title: "Android (Chrome)", step1: { item: "1. { icon } { text }", text: "Tippen Sie auf das Menü (oben rechts)", }, step2: { item: "2. { icon } { text }", text: "Drücken Sie { buttonText }", buttonText: "@:AndroidPWAPrompt.buttonText", }, }, ios: { title: "iOS (Safari)", step1: { item: "1. { icon } { text }", text: "Tippen Sie auf Teilen (unten)", }, step2: { item: "2. { icon } { text }", text: "Drücken Sie { buttonText }", buttonText: "@:iOSPWAPrompt.buttonText", }, }, outro: { text: "Nachdem Sie diese App auf Ihrem Gerät installiert haben, schließen Sie dieses Browserfenster und verwenden Sie die App von Ihrem Startbildschirm aus.", }, }, pwa: { success: { title: "Erfolg!", text: "Sie verwenden Cashu als PWA. Schließen Sie alle anderen geöffneten Browserfenster und verwenden Sie die App von Ihrem Startbildschirm aus.", nextSteps: "Sie können nun diesen Tab schließen und die App vom Startbildschirm öffnen.", }, }, }, iOSPWAPrompt: { text: "Tippen Sie auf { icon } und { buttonText }", buttonText: "Zum Home-Bildschirm", }, AndroidPWAPrompt: { text: "Tippen Sie auf { icon } und { buttonText }", buttonText: "Zum Startbildschirm hinzufügen", }, WelcomeSlide3: { title: "Ihre Seed-Phrase", text: "Speichern Sie Ihre Seed-Phrase in einem Passwortmanager oder auf Papier. Ihre Seed-Phrase ist der einzige Weg, Ihre Gelder wiederherzustellen, wenn Sie den Zugriff auf dieses Gerät verlieren.", inputs: { seed_phrase: { label: "Seed-Phrase", caption: "Sie können Ihre Seed-Phrase in den Einstellungen sehen.", }, checkbox: { label: "Ich habe sie aufgeschrieben", }, }, }, WelcomeSlide4: { title: "Bedingungen", actions: { more: { label: "Nutzungsbedingungen lesen", }, }, inputs: { checkbox: { label: "Ich habe diese Bedingungen gelesen und akzeptiere sie", }, }, }, WelcomeSlideChoice: { title: "Richten Sie Ihre Wallet ein", text: "Möchten Sie aus einer Seed-Phrase wiederherstellen oder eine neue Wallet erstellen?", options: { new: { title: "Neue Wallet erstellen", subtitle: "Neue Seed erzeugen und Mints hinzufügen.", }, recover: { title: "Wallet wiederherstellen", subtitle: "Seed-Phrase eingeben, Mints und Ecash wiederherstellen.", }, }, }, WelcomeMintSetup: { title: "Mints hinzufügen", text: "Mints sind Server, die beim Senden und Empfangen von Ecash helfen. Wählen Sie eine gefundene Mint oder fügen Sie manuell eine hinzu. Sie können dies auch später tun.", sections: { your_mints: "Ihre Mints" }, restoring: "Mints werden wiederhergestellt…", placeholder: { mint_url: "https://" }, }, WelcomeRecoverSeed: { title: "Seed-Phrase eingeben", text: "Fügen Sie Ihre 12 Wörter ein oder tippen Sie sie ein, um wiederherzustellen.", inputs: { word: "Wort { index }" }, actions: { paste_all: "Alle einfügen" }, disclaimer: "Ihre Seed-Phrase wird nur lokal verwendet, um Ihre Wallet-Schlüssel abzuleiten.", }, WelcomeRestoreEcash: { title: "Ihr Ecash wiederherstellen", text: "Scannen Sie nach nicht ausgegebenen Nachweisen auf Ihren konfigurierten Mints und fügen Sie sie Ihrer Wallet hinzu.", }, MintRatings: { title: "Mint-Bewertungen", reviews: "Bewertungen", ratings: "Bewertungen", no_reviews: "Keine Bewertungen gefunden", your_review: "Ihre Bewertung", no_reviews_to_display: "Keine Bewertungen anzuzeigen.", no_rating: "Keine Bewertung", out_of: "von", rows: "Reviews", sort: "Sortieren", sort_options: { newest: "Neueste", oldest: "Älteste", highest: "Höchste", lowest: "Niedrigste", }, actions: { write_review: "Bewertung schreiben" }, empty_state_subtitle: "Helfen Sie, indem Sie eine Bewertung hinterlassen. Teilen Sie Ihre Erfahrungen mit diesem Mint und helfen Sie anderen, indem Sie eine Bewertung hinterlassen.", }, CreateMintReview: { title: "Mint bewerten", publishing_as: "Veröffentlichen als", inputs: { rating: { label: "Bewertung" }, review: { label: "Rezension (optional)" }, }, actions: { publish: { label: "Veröffentlichen", in_progress: "Veröffentlichen…" }, }, }, RestoreView: { seed_phrase: { label: "Aus Seed-Phrase wiederherstellen", caption: "Geben Sie Ihre Seed-Phrase ein, um Ihre Wallet wiederherzustellen. Stellen Sie vor der Wiederherstellung sicher, dass Sie alle Mints hinzugefügt haben, die Sie zuvor verwendet haben.", inputs: { seed_phrase: { label: "Seed-Phrase", caption: "Sie können Ihre Seed-Phrase in den Einstellungen sehen.", }, }, }, information: { label: "Information", caption: "Der Assistent stellt nur Ecash von einer anderen Seed-Phrase wieder her. Sie können diese Seed-Phrase nicht verwenden oder die Seed-Phrase der aktuell verwendeten Wallet ändern. Das bedeutet, dass wiederhergestellter Ecash nicht durch Ihre aktuelle Seed-Phrase geschützt ist, solange Sie den Ecash nicht einmal an sich selbst senden.", }, restore_mints: { label: "Mints wiederherstellen", caption: 'Wählen Sie die Mint zur Wiederherstellung aus. Sie können weitere Mints im Hauptbildschirm unter "Mints" hinzufügen und sie hier wiederherstellen.', }, actions: { paste: { error: "Zwischenablage-Inhalt konnte nicht gelesen werden.", }, validate: { error: "Mnemonisch muss mindestens 12 Wörter enthalten.", }, select_all: { label: "Alle auswählen", }, deselect_all: { label: "Alle abwählen", }, restore: { label: "Wiederherstellen", in_progress: "Mint wird wiederhergestellt…", error: "Fehler beim Wiederherstellen der Mint: { error }", }, restore_all_mints: { label: "Alle Mints wiederherstellen", in_progress: "Mint { index } von { length } wird wiederhergestellt…", success: "Wiederherstellung erfolgreich abgeschlossen", error: "Fehler beim Wiederherstellen der Mints: { error }", }, restore_selected_mints: { label: "Ausgewählte Mints wiederherstellen ({count})", in_progress: "Mint { index } von { length } wird wiederhergestellt…", success: "{count} Mint(s) erfolgreich wiederhergestellt", error: "Fehler beim Wiederherstellen ausgewählter Mints: { error }", }, }, nostr_mints: { label: "Mints von Nostr wiederherstellen", caption: "Suchen Sie nach Mint-Backups, die auf Nostr-Relays mit Ihrer Seed-Phrase gespeichert sind. Dies hilft Ihnen, Mints zu entdecken, die Sie zuvor verwendet haben.", search_button: "Nach Mint-Backups suchen", select_all: "Alle auswählen", deselect_all: "Alle abwählen", backed_up: "Gesichert", already_added: "Bereits hinzugefügt", add_selected: "Ausgewählte hinzufügen ({count})", no_backups_found: "Keine Mint-Backups gefunden", no_backups_hint: "Stellen Sie sicher, dass das Nostr-Mint-Backup in den Einstellungen aktiviert ist, um Ihre Mint-Liste automatisch zu sichern.", invalid_mnemonic: "Bitte geben Sie eine gültige Seed-Phrase ein, bevor Sie suchen.", search_error: "Fehler bei der Suche nach Mint-Backups.", add_error: "Fehler beim Hinzufügen ausgewählter Mints.", }, }, MintSettings: { add: { title: "Mint hinzufügen", description: "Geben Sie die URL einer Cashu Mint ein, um sich mit ihr zu verbinden. Diese Wallet ist nicht mit einer Mint affiliiert.", inputs: { nickname: { placeholder: "Spitzname (z.B. Testnet)", }, }, actions: { add_mint: { label: "@:global.actions.add_mint.label", error_invalid_url: "Ungültige URL", }, scan: { label: "QR-Code scannen", }, }, }, discover: { title: "Mints entdecken", overline: "Entdecken", caption: "Entdecken Sie Mints, die andere Benutzer auf Nostr empfohlen haben.", actions: { discover: { label: "Mints entdecken", in_progress: "Lädt…", error_no_mints: "Keine Mints gefunden", success: "{ length } Mints gefunden", }, }, recommendations: { overline: "{ length } Mints gefunden", caption: "Diese Mints wurden von anderen Nostr-Benutzern empfohlen. Seien Sie vorsichtig und recherchieren Sie selbst, bevor Sie eine Mint verwenden.", actions: { browse: { label: "Klicken, um Mints zu durchsuchen", }, }, }, }, swap: { title: "Tauschen", overline: "Multimint-Swaps", caption: "Tauschen Sie Gelder zwischen Mints über Lightning. Hinweis: Lassen Sie Platz für potenzielle Lightning-Gebühren. Wenn die eingehende Zahlung nicht erfolgreich ist, überprüfen Sie die Rechnung manuell.", inputs: { from: { label: "Von", }, to: { label: "Nach", }, amount: { label: "Betrag ({ ticker }))", }, }, actions: { swap: { label: "@:global.actions.swap.label", in_progress: "@:MintSettings.swap.actions.swap.label", }, }, }, error_badge: "Fehler", reviews_text: "Bewertungen", no_reviews_yet: "Noch keine Bewertungen", discover_mints_button: "Mints entdecken", }, QrcodeReader: { progress: { text: "{ percentage }{ addon }", percentage: "{ percentage }%", keep_scanning_text: " - Weiter scannen", }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, }, }, InvoiceDetailDialog: { title: "Lightning empfangen", create_invoice_title: "Rechnung erstellen", inputs: { amount: { label: "Betrag ({ ticker }) *", }, }, actions: { close: { label: "@:global.actions.close.label", }, create: { label: "Rechnung erstellen", label_blocked: "Rechnung wird erstellt…", in_progress: "Erstellt", }, }, invoice: { caption: "Lightning Rechnung", status_paid_text: "Bezahlt!", actions: { close: { label: "@:global.actions.close.label", }, copy: { label: "@:global.actions.copy.label", }, }, }, }, SendDialog: { title: "Senden", actions: { ecash: { label: "Ecash", error_no_mints: "Keine Mints verfügbar", }, lightning: { label: "Lightning", error_no_mints: "Keine Mints verfügbar", }, }, }, SendTokenDialog: { title: "Ecash senden", title_ecash_text: "Ecash", badge_offline_text: "Offline", inputs: { amount: { label: "Betrag ({ ticker }) *", invalid_too_much_error_text: "Zu viel", }, p2pk_pubkey: { label: "Öffentlicher Schlüssel des Empfängers", label_invalid: "Öffentlicher Schlüssel des Empfängers", }, }, actions: { close: { label: "@:global.actions.close.label", }, close_card_scanner: { label: "@:global.actions.close.label", }, copy_emoji: { label: "🥜", tooltip_text: "Emoji kopieren", }, copy_tokens: { label: "@:global.actions.copy.label", }, copy_link: { tooltip_text: "Link kopieren", }, share: { tooltip_text: "Ecash teilen", }, lock: { label: "@:global.actions.lock.label", }, paste_p2pk_pubkey: { tooltip_text: "@:global.actions.paste.label", }, send: { label: "@:global.actions.send.label", }, delete: { tooltip_text: "Aus Verlauf löschen", }, write_tokens_to_card: { tooltips: { ndef_supported_text: "Auf NFC-Karte schreiben", ndef_unsupported_text: "NDEF nicht unterstützt", }, }, }, }, ReceiveDialog: { title: "Empfangen", actions: { ecash: { label: "Ecash", error_no_mints: "Keine Mints verfügbar", }, lightning: { label: "Lightning", error_no_mints: "Sie müssen sich mit einer Mint verbinden, um über Lightning zu empfangen", }, }, }, ReceiveEcashDrawer: { title: "Ecash empfangen", actions: { paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, request: { label: "Anfordern", }, lock: { label: "@:global.actions.lock.label", }, nfc: { label: "NFC", scanning_text: "Scannt…", }, }, }, ReceiveTokenDialog: { title: "Ecash empfangen", title_ecash_text: "Ecash", inputs: { tokens_base64: { label: "Cashu Token einfügen", }, }, errors: { invalid_token: { label: "Ungültiger Token", }, p2pk_lock_mismatch: { label: "Kann nicht empfangen werden. Die P2PK-Sperre dieses Tokens stimmt nicht mit Ihrem öffentlichen Schlüssel überein.", }, }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, scan: { label: "@:global.actions.scan.label", }, receive: { label: "@:global.actions.receive.label", label_known_mint: "@:ReceiveTokenDialog.actions.receive.label", label_adding_mint: "Mint wird hinzugefügt…", }, swap: { label: "@:global.actions.swap.label", tooltip_text: "Zu einer vertrauenswürdigen Mint tauschen", caption: "Tauschen { value }", }, cancel_swap: { label: "@:global.actions.cancel.label", tooltip_text: "Swap abbrechen", }, confirm_swap: { label: "@:ReceiveTokenDialog.actions.swap.label", tooltip_text: "@:ReceiveTokenDialog.actions.swap.tooltip_text", in_progress: "@:ReceiveTokenDialog.actions.confirm_swap.label", }, later: { label: "Später empfangen", tooltip_text: "Zum Verlauf hinzufügen, um später zu empfangen", already_in_history_success_text: "Ecash bereits im Verlauf", added_to_history_success_text: "Ecash zum Verlauf hinzugefügt", }, nfc: { label: "NFC", tooltips: { ndef_supported_text: "Von NFC-Karte lesen", ndef_unsupported_text: "NDEF nicht unterstützt", }, }, }, }, P2PKDialog: { p2pk: { caption: "P2PK Schlüssel", description: "Ecash empfangen, der auf diesen Schlüssel gesperrt ist", used_warning_text: "Warnung: Dieser Schlüssel wurde bereits verwendet. Verwenden Sie einen neuen Schlüssel für besseren Datenschutz.", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_key: { label: "Neuen Schlüssel generieren", }, }, }, PaymentRequestDialog: { payment_request: { caption: "Zahlungsanforderung", description: "Zahlungen über Nostr empfangen", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_request: { label: "Neue Anforderung", }, add_amount: { label: "Betrag hinzufügen", }, use_active_mint: { label: "Beliebige Mint", }, }, inputs: { amount: { placeholder: "Betrag eingeben", }, }, }, NumericKeyboard: { actions: { close: { label: "@:global.actions.close.label", closed_info_text: "Tastatur deaktiviert. Sie können die Tastatur in den Einstellungen wieder aktivieren.", }, enter: { label: "@:global.actions.enter.label", }, }, }, NWCDialog: { nwc: { caption: "Nostr Wallet Connect", description: "Steuern Sie Ihre Wallet per Fernzugriff mit NWC. Tippen Sie auf den QR-Code, um Ihre Wallet mit einer kompatiblen App zu verknüpfen.", warning_text: "Warnung: Jeder, der Zugriff auf diesen Verbindungsstring hat, kann Zahlungen von Ihrer Wallet initiieren. Nicht teilen!", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, }, }, MintMotdMessage: { title: "Mint Nachricht", }, MintDetailsDialog: { contact: { title: "Kontakt", }, details: { title: "Mint Details", url: { label: "URL", }, nuts: { label: "Nuts", actions: { show: { label: "Alle anzeigen", }, hide: { label: "Ausblenden", }, }, }, currency: { label: "Währung", }, currencies: { label: "@:MintDetailsDialog.details.currency.label", }, version: { label: "Version", }, }, actions: { title: "Aktionen", copy_mint_url: { label: "Mint URL kopieren", }, delete: { label: "Mint löschen", }, edit: { label: "Mint bearbeiten", }, }, }, ChooseMint: { title: "Wählen Sie eine Mint", badge_mint_error_text: "Fehler", badge_option_mint_error_text: "@:ChooseMint.badge_mint_error_text", }, HistoryTable: { empty_text: "Noch kein Verlauf vorhanden", row: { type_label: "Ecash", date_label: "Vor { value }", }, actions: { check_status: { tooltip_text: "Status prüfen", }, receive: { tooltip_text: "Empfangen", }, filter_pending: { label: "Ausstehende filtern", }, show_all: { label: "Alle anzeigen", }, }, old_token_not_found_error_text: "Alter Token nicht gefunden", }, InvoiceTable: { empty_text: "Noch keine Rechnungen vorhanden", row: { type_label: "Lightning", type_tooltip_text: "Zum Kopieren klicken", date_label: "Vor { value }", }, actions: { check_status: { tooltip_text: "Status prüfen", }, filter_pending: { label: "Ausstehende filtern", }, show_all: { label: "Alle anzeigen", }, }, }, RemoveMintDialog: { title: "Sind Sie sicher, dass Sie diese Mint löschen möchten?", nickname: { label: "Spitzname", }, balances: { label: "Guthaben", }, warning_text: "Hinweis: Da diese Wallet paranoid ist, wird Ihr Ecash von dieser Mint nicht wirklich gelöscht, sondern auf Ihrem Gerät gespeichert bleiben. Sie werden ihn wieder sehen, wenn Sie diese Mint später erneut hinzufügen.", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { confirm: { label: "Mint entfernen", }, cancel: { label: "@:global.actions.cancel.label", }, }, }, ParseInputComponent: { placeholder: { default: "Cashu Token oder Lightning-Adresse", receive: "Cashu Token", pay: "Lightning-Adresse oder Rechnung", }, qr_scanner: { title: "QR-Code scannen", description: "Tippen Sie, um eine Adresse zu scannen", }, paste_button: { label: "@:global.actions.paste.label", }, }, PayInvoiceDialog: { input_data: { title: "Lightning bezahlen", inputs: { invoice_data: { label: "Lightning-Rechnung oder Adresse", }, }, actions: { close: { label: "@:global.actions.close.label", }, enter: { label: "@:global.actions.enter.label", }, paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, }, }, lnurlpay: { amount_exact_label: "{ payee } fordert { value } { ticker } an", amount_range_label: "{ payee } fordert{br}zwischen { min } und { max } { ticker } an", sending_to_lightning_address: "Senden an { address }", inputs: { amount: { label: "Betrag ({ ticker }) *", }, comment: { label: "Kommentar (optional)", }, }, actions: { close: { label: "@:global.actions.close.label", }, send: { label: "@:global.actions.send.label", }, }, }, invoice: { title: "{ value } bezahlen", paying: "Wird bezahlt", paid: "Bezahlt", fee: "Gebühr", memo: { label: "Memo", }, processing_info_text: "Wird verarbeitet…", balance_too_low_warning_text: "Guthaben zu niedrig", actions: { close: { label: "@:global.actions.close.label", }, pay: { label: "Bezahlen", in_progress: "@:PayInvoiceDialog.invoice.processing_info_text", error: "Fehler", }, }, }, }, EditMintDialog: { title: "Mint bearbeiten", inputs: { nickname: { label: "Spitzname", }, mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, update: { label: "@:global.actions.update.label", }, }, }, AddMintDialog: { title: "Vertrauen Sie dieser Mint?", description: "Bevor Sie diese Mint verwenden, stellen Sie sicher, dass Sie ihr vertrauen. Mints könnten bösartig werden oder jederzeit den Betrieb einstellen.", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, add_mint: { label: "@:global.actions.add_mint.label", in_progress: "Mint wird hinzugefügt", }, }, }, restore: { mnemonic_error_text: "Bitte geben Sie ein Mnemonisch ein", restore_mint_error_text: "Fehler beim Wiederherstellen der Mint: { error }", prepare_info_text: "Wiederherstellungsprozess wird vorbereitet…", restored_proofs_for_keyset_info_text: "{ restoreCounter } Nachweise für Keyset { keysetId } wiederhergestellt", checking_proofs_for_keyset_info_text: "Prüfe Nachweise { startIndex } bis { endIndex } für Keyset { keysetId }", no_proofs_info_text: "Keine Nachweise zum Wiederherstellen gefunden", restored_amount_success_text: "{ amount } wiederhergestellt", }, swap: { in_progress_warning_text: "Swap läuft", invalid_swap_data_error_text: "Ungültige Swap-Daten", swap_error_text: "Fehler beim Tauschen", }, TokenInformation: { fee: "Gebühr", unit: "Einheit", fiat: "Fiat", p2pk: "P2PK", locked: "Gesperrt", locked_to_you: "An dich gesperrt", mint: "Münzstätte", memo: "Notiz", payment_request: "Zahlungsanforderung", nostr: "Nostr", token_copied: "Token in Zwischenablage kopiert", }, }; ================================================ FILE: src/i18n/el-GR/index.ts ================================================ export default { MultinutPicker: { payment: "Πληρωμή Multinut", selectMints: "Επιλέξτε ένα ή περισσότερα mints για να εκτελέσετε μια πληρωμή.", totalSelectedBalance: "Συνολικό επιλεγμένο υπόλοιπο", multiMintPay: "Πληρωμή Multi-Mint", balanceNotEnough: "Το υπόλοιπο πολλών νομισματοκοπείων δεν επαρκεί για την κάλυψη αυτού του τιμολογίου", failed: "Η επεξεργασία απέτυχε: {error}", paid: "Πληρώθηκε {amount} μέσω Lightning", }, global: { copy_to_clipboard: { success: "Αντιγράφηκε στο πρόχειρο!", }, actions: { add_mint: { label: "Προσθήκη mint", }, cancel: { label: "Ακύρωση", }, copy: { label: "Αντιγραφή", }, close: { label: "Κλείσιμο", }, enter: { label: "Είσοδος", }, lock: { label: "Κλείδωμα", }, paste: { label: "Επικόλληση", }, receive: { label: "Λήψη", }, scan: { label: "Σάρωση", }, send: { label: "Αποστολή", }, swap: { label: "Ανταλλαγή", }, update: { label: "Ενημέρωση", }, }, inputs: { mint_url: { label: "URL Mint", }, }, }, wallet: { notifications: { balance_too_low: "Το υπόλοιπο είναι πολύ χαμηλό", received: "Λήφθηκε {amount}", fee: " (προμήθεια: {fee})", could_not_request_mint: "Δεν ήταν δυνατή η αίτηση στο mint", invoice_still_pending: "Το τιμολόγιο εκκρεμεί ακόμα", paid_lightning: "Πληρώθηκε {amount} μέσω Lightning", payment_pending_refresh: "Πληρωμή σε εκκρεμότητα. Ανανεώστε το τιμολόγιο χειροκίνητα.", sent: "Στάλθηκε {amount}", token_still_pending: "Το token εκκρεμεί ακόμα", received_lightning: "Λήφθηκε {amount} μέσω Lightning", lightning_payment_failed: "Η πληρωμή Lightning απέτυχε", failed_to_decode_invoice: "Αποτυχία αποκωδικοποίησης τιμολογίου", invalid_lnurl: "Μη έγκυρο LNURL", lnurl_error: "Σφάλμα LNURL", no_amount: "Δεν υπάρχει ποσό", no_lnurl_data: "Δεν υπάρχουν δεδομένα LNURL", no_price_data: "Δεν υπάρχουν δεδομένα τιμής.", please_try_again: "Παρακαλώ προσπαθήστε ξανά.", }, mint: { notifications: { already_added: "Το mint έχει ήδη προστεθεί", added: "Το mint προστέθηκε", not_found: "Το mint δεν βρέθηκε", activation_failed: "Η ενεργοποίηση του mint απέτυχε", no_active_mint: "Δεν υπάρχει ενεργό mint", unit_activation_failed: "Η ενεργοποίηση μονάδας απέτυχε", unit_not_supported: "Η μονάδα δεν υποστηρίζεται από το mint", activated: "Το mint ενεργοποιήθηκε", could_not_connect: "Δεν ήταν δυνατή η σύνδεση στο mint", could_not_get_info: "Δεν ήταν δυνατή η λήψη πληροφοριών mint", could_not_get_keys: "Δεν ήταν δυνατή η λήψη κλειδιών mint", could_not_get_keysets: "Δεν ήταν δυνατή η λήψη συνόλων κλειδιών mint", mint_validation_error: "Σφάλμα επικύρωσης Mint", removed: "Το mint αφαιρέθηκε", error: "Σφάλμα mint", }, }, }, MainHeader: { menu: { settings: { title: "Ρυθμίσεις", settings: { title: "Ρυθμίσεις", caption: "Διαμόρφωση πορτοφολιού", }, }, terms: { title: "Όροι", terms: { title: "Όροι", caption: "Όροι Παροχής Υπηρεσιών", }, }, links: { title: "Σύνδεσμοι", cashuSpace: { title: "Cashu.space", caption: "cashu.space", }, github: { title: "Github", caption: "github.com/cashubtc", }, telegram: { title: "Telegram", caption: "t.me/CashuMe", }, twitter: { title: "Twitter", caption: "{'@'}CashuBTC", }, donate: { title: "Δωρεά", caption: "Υποστήριξη Cashu", }, }, }, offline: { warning: { text: "Εκτός σύνδεσης", }, }, reload: { warning: { text: "Επαναφόρτωση σε { countdown }", }, }, staging: { warning: { text: "Staging – μην το χρησιμοποιείτε με πραγματικά κεφάλαια!", }, }, }, FullscreenHeader: { actions: { back: { label: "Πορτοφόλι", }, }, }, Settings: { language: { title: "Γλώσσα", description: "Παρακαλώ επιλέξτε την προτιμώμενη γλώσσα σας από την παρακάτω λίστα.", }, sections: { backup_restore: "ΑΝΤΙΓΡΑΦΟ ΑΣΦΑΛΕΙΑΣ & ΕΠΑΝΑΦΟΡΑ", lightning_address: "ΔΙΕΥΘΥΝΣΗ LIGHTNING", nostr_keys: "ΚΛΕΙΔΙΑ NOSTR", nostr: { title: "NOSTR", relays: { expand_label: "Κάντε κλικ για να επεξεργαστείτε τα relays", add: { title: "Προσθήκη relay", description: "Το πορτοφόλι σας χρησιμοποιεί αυτά τα relays για λειτουργίες nostr όπως αιτήματα πληρωμής, nostr wallet connect και αντίγραφα ασφαλείας.", }, list: { title: "Relays", description: "Το πορτοφόλι σας θα συνδεθεί σε αυτά τα relays.", copy_tooltip: "Αντιγραφή relay", remove_tooltip: "Κατάργηση relay", }, }, }, payment_requests: "ΑΙΤΗΜΑΤΑ ΠΛΗΡΩΜΗΣ", nostr_wallet_connect: "NOSTR WALLET CONNECT", hardware_features: "ΧΑΡΑΚΤΗΡΙΣΤΙΚΑ ΥΛΙΚΟΥ", p2pk_features: "ΧΑΡΑΚΤΗΡΙΣΤΙΚΑ P2PK", privacy: "ΑΠΟΡΡΗΤΟ", experimental: "ΠΕΙΡΑΜΑΤΙΚΟ", appearance: "ΕΜΦΑΝΙΣΗ", }, backup_restore: { backup_seed: { title: "Φράση seed αντιγράφου ασφαλείας", description: "Η φράση seed σας μπορεί να επαναφέρει το πορτοφόλι σας. Κρατήστε την ασφαλή και ιδιωτική.", seed_phrase_label: "Φράση seed", }, restore_ecash: { title: "Επαναφορά ecash", description: "Ο οδηγός επαναφοράς σάς επιτρέπει να ανακτήσετε χαμένο ecash από μια μνημονική φράση seed. Η φράση seed του τρέχοντος πορτοφολιού σας θα παραμείνει ανεπηρέαστη, ο οδηγός θα σας επιτρέψει μόνο να επαναφέρετε ecash από μια άλλη φράση seed.", button: "Επαναφορά", }, }, lightning_address: { title: "Διεύθυνση Lightning", description: "Λάβετε πληρωμές στη διεύθυνση Lightning σας.", enable: { toggle: "Ενεργοποίηση", description: "Διεύθυνση Lightning με npub.cash", }, address: { copy_tooltip: "Αντιγραφή διεύθυνσης Lightning", }, automatic_claim: { toggle: "Αυτόματη διεκδίκηση", description: "Λήψη εισερχόμενων πληρωμών αυτόματα.", }, npc_v2: { choose_mint_title: "Επιλέξτε mint για npub.cash v2", choose_mint_placeholder: "Επιλέξτε ένα mint...", }, }, nostr_keys: { title: "Τα κλειδιά σας nostr", description: "Ορίστε τα κλειδιά nostr για τη διεύθυνση Lightning σας.", wallet_seed: { title: "Φράση seed πορτοφολιού", description: "Δημιουργία ζεύγους κλειδιών nostr από τη seed του πορτοφολιού", copy_nsec: "Αντιγραφή nsec", }, nsec_bunker: { title: "Nsec Bunker", description: "Χρήση bunker NIP-46", delete_tooltip: "Διαγραφή σύνδεσης", }, use_nsec: { title: "Χρησιμοποιήστε το nsec σας", description: "Αυτή η μέθοδος είναι επικίνδυνη και δεν συνιστάται", delete_tooltip: "Διαγραφή nsec", }, signing_extension: { title: "Επέκταση υπογραφής", description: "Χρήση επέκτασης υπογραφής NIP-07", not_found: "Δεν βρέθηκε επέκταση υπογραφής NIP-07", }, }, payment_requests: { title: "Αιτήματα πληρωμής", description: "Τα αιτήματα πληρωμής σάς επιτρέπουν να λαμβάνετε πληρωμές μέσω nostr. Εάν το ενεργοποιήσετε, το πορτοφόλι σας θα εγγραφεί στα relays nostr σας.", enable_toggle: "Ενεργοποίηση Αιτημάτων Πληρωμής", claim_automatically: { toggle: "Αυτόματη διεκδίκηση", description: "Λήψη εισερχόμενων πληρωμών αυτόματα.", }, }, nostr_wallet_connect: { title: "Nostr Wallet Connect (NWC)", description: "Χρησιμοποιήστε το NWC για να ελέγξετε το πορτοφόλι σας από οποιαδήποτε άλλη εφαρμογή.", enable_toggle: "Ενεργοποίηση NWC", payments_note: "Μπορείτε να χρησιμοποιήσετε το NWC μόνο για πληρωμές από το υπόλοιπο Bitcoin σας. Οι πληρωμές θα γίνονται από το ενεργό σας mint.", connection: { copy_tooltip: "Αντιγραφή συμβολοσειράς σύνδεσης", qr_tooltip: "Εμφάνιση κωδικού QR", allowance_label: "Υπολειπόμενο όριο (sat)", }, }, hardware_features: { webnfc: { title: "WebNFC", description: "Επιλέξτε την κωδικοποίηση για εγγραφή σε κάρτες NFC", text: { title: "Κείμενο", description: "Αποθήκευση token σε απλό κείμενο", }, weburl: { title: "URL", description: "Αποθήκευση URL σε αυτό το πορτοφόλι με token", }, binary: { title: "Δυαδικό", description: "Αποθήκευση tokens ως δυαδικά δεδομένα", }, quick_access: { toggle: "Γρήγορη πρόσβαση σε NFC", description: "Γρήγορη σάρωση καρτών NFC στο μενού Λήψη Ecash. Αυτή η επιλογή προσθέτει ένα κουμπί NFC στο μενού Λήψη Ecash.", }, }, }, p2pk_features: { title: "P2PK", description: "Δημιουργία ζεύγους κλειδιών για λήψη ecash κλειδωμένου με P2PK. Προειδοποίηση: Αυτή η δυνατότητα είναι πειραματική. Χρησιμοποιήστε μόνο με μικρά ποσά. Εάν χάσετε τα ιδιωτικά σας κλειδιά, κανείς δεν θα μπορεί πλέον να ξεκλειδώσει το ecash που είναι κλειδωμένο σε αυτά.", generate_button: "Δημιουργία κλειδιού", import_button: "Εισαγωγή nsec", quick_access: { toggle: "Γρήγορη πρόσβαση στο κλείδωμα", description: "Χρησιμοποιήστε το για να εμφανίσετε γρήγορα το κλειδί κλειδώματος P2PK στο μενού λήψης ecash.", }, keys_expansion: { label: "Κάντε κλικ για περιήγηση σε {count} κλειδιά", used_badge: "χρησιμοποιημένο", }, }, privacy: { title: "Απόρρητο", description: "Αυτές οι ρυθμίσεις επηρεάζουν το απόρρητό σας.", check_incoming: { toggle: "Έλεγχος εισερχόμενου τιμολογίου", description: "Εάν είναι ενεργοποιημένο, το πορτοφόλι θα ελέγχει το τελευταίο τιμολόγιο στο παρασκήνιο. Αυτό αυξάνει την απόκριση του πορτοφολιού, καθιστώντας ευκολότερο το fingerprinting. Μπορείτε να ελέγξετε μη αυτόματα τα απλήρωτα τιμολόγια στην καρτέλα Τιμολόγια.", }, check_startup: { toggle: "Έλεγχος εκκρεμών τιμολογίων κατά την εκκίνηση", description: "Εάν είναι ενεργοποιημένο, το πορτοφόλι θα ελέγχει τα εκκρεμή τιμολόγια των τελευταίων 24 ωρών κατά την εκκίνηση.", }, check_all: { toggle: "Έλεγχος όλων των τιμολογίων", description: "Εάν είναι ενεργοποιημένο, το πορτοφόλι θα ελέγχει περιοδικά τα απλήρωτα τιμολόγια στο παρασκήνιο για έως και δύο εβδομάδες. Αυτό αυξάνει τη διαδικτυακή δραστηριότητα του πορτοφολιού, καθιστώντας ευκολότερο το fingerprinting. Μπορείτε να ελέγξετε μη αυτόματα τα απλήρωτα τιμολόγια στην καρτέλα Τιμολόγια.", }, check_sent: { toggle: "Έλεγχος απεσταλμένου ecash", description: "Εάν είναι ενεργοποιημένο, το πορτοφόλι θα χρησιμοποιεί περιοδικούς ελέγχους παρασκηνίου για να προσδιορίσει εάν τα απεσταλμένα token έχουν εξαργυρωθεί. Αυτό αυξάνει τη διαδικτυακή δραστηριότητα του πορτοφολιού, καθιστώντας ευκολότερο το fingerprinting.", }, websockets: { toggle: "Χρήση WebSockets", description: "Εάν είναι ενεργοποιημένο, το πορτοφόλι θα χρησιμοποιεί μακροχρόνιες συνδέσεις WebSocket για λήψη ενημερώσεων σχετικά με πληρωμένα τιμολόγια και δαπανημένα token από τα mints. Αυτό αυξάνει την απόκριση του πορτοφολιού αλλά καθιστά επίσης ευκολότερο το fingerprinting.", }, bitcoin_price: { toggle: "Λήψη συναλλαγματικής ισοτιμίας από Coinbase", description: "Εάν είναι ενεργοποιημένο, η τρέχουσα συναλλαγματική ισοτιμία Bitcoin θα ληφθεί από το coinbase.com και θα εμφανιστεί το μετατραπέν υπόλοιπό σας.", currency: { title: "Νόμισμα Fiat", description: "Επιλέξτε το νόμισμα fiat για την εμφάνιση της τιμής Bitcoin.", }, }, }, experimental: { title: "Πειραματικό", description: "Αυτές οι δυνατότητες είναι πειραματικές.", receive_swaps: { toggle: "Λήψη ανταλλαγών", badge: "Beta", description: "Επιλογή ανταλλαγής του ληφθέντος Ecash στο ενεργό σας mint στον διάλογο Λήψη Ecash.", }, auto_paste: { toggle: "Αυτόματη επικόλληση Ecash", description: "Αυτόματη επικόλληση του ecash στο πρόχειρό σας όταν πατάτε Λήψη, μετά Ecash, μετά Επικόλληση. Η αυτόματη επικόλληση μπορεί να προκαλέσει δυσλειτουργίες στο UI στο iOS, απενεργοποιήστε την εάν αντιμετωπίζετε προβλήματα.", }, auditor: { toggle: "Ενεργοποίηση ελεγκτή", badge: "Beta", description: "Εάν είναι ενεργοποιημένο, το πορτοφόλι θα εμφανίζει πληροφορίες ελεγκτή στον διάλογο λεπτομερειών του mint. Ο ελεγκτής είναι μια υπηρεσία τρίτου μέρους που παρακολουθεί την αξιοπιστία των mints.", url_label: "URL Ελεγκτή", api_url_label: "URL API Ελεγκτή", }, multinut: { toggle: "Ενεργοποίηση Multinut", description: "Εάν είναι ενεργοποιημένο, το πορτοφόλι θα χρησιμοποιεί το Multinut για την πληρωμή τιμολογίων από πολλαπλά mints ταυτόχρονα.", }, nostr_mint_backup: { toggle: "Δημιουργία αντιγράφων ασφαλείας της λίστας mint στο Nostr", description: "Εάν είναι ενεργοποιημένο, η λίστα mint σας θα δημιουργείται αυτόματα αντίγραφα ασφαλείας στα ρελέ Nostr χρησιμοποιώντας τα διαμορφωμένα κλειδιά Nostr. Αυτό σας επιτρέπει να επαναφέρετε τη λίστα mint σας σε όλες τις συσκευές.", notifications: { enabled: "Ενεργοποιήθηκε το αντίγραφο ασφαλείας Nostr mint", disabled: "Απενεργοποιήθηκε το αντίγραφο ασφαλείας Nostr mint", failed: "Αποτυχία ενεργοποίησης του αντιγράφου ασφαλείας Nostr mint", }, }, }, appearance: { keyboard: { title: "Πληκτρολόγιο οθόνης", description: "Χρησιμοποιήστε το αριθμητικό πληκτρολόγιο για την εισαγωγή ποσών.", toggle: "Χρήση αριθμητικού πληκτρολογίου", toggle_description: "Εάν είναι ενεργοποιημένο, θα χρησιμοποιείται το αριθμητικό πληκτρολόγιο για την εισαγωγή ποσών.", }, theme: { title: "Εμφάνιση", description: "Αλλάξτε την εμφάνιση του πορτοφολιού σας.", tooltips: { mono: "mono", cyber: "cyber", freedom: "freedom", nostr: "nostr", bitcoin: "bitcoin", mint: "mint", nut: "nut", blu: "blu", flamingo: "flamingo", }, }, bip177: { title: "Σύμβολο Bitcoin", description: "Χρησιμοποιήστε το σύμβολο ₿ αντί για sats.", toggle: "Χρήση συμβόλου ₿", }, }, web_of_trust: { title: "Δίκτυο εμπιστοσύνης", known_pubkeys: "Γνωστά pubkeys: {wotCount}", continue_crawl: "Συνέχιση ανίχνευσης", crawl_odell: "Ανίχνευση ODELL'S WEB OF TRUST", crawl_wot: "Ανίχνευση web of trust", pause: "Παύση", reset: "Επαναφορά", progress: "{crawlProcessed} / {crawlTotal}", }, npub_cash: { use_npubx: "Χρήση npubx.cash", copy_lightning_address: "Αντιγραφή διεύθυνσης Lightning", v2_mint: "npub.cash v2 mint", }, multinut: { use_multinut: "Χρήση Multinut", }, advanced: { title: "Για προχωρημένους", developer: { title: "Ρυθμίσεις προγραμματιστή", description: "Οι ακόλουθες ρυθμίσεις είναι για ανάπτυξη και εντοπισμό σφαλμάτων.", new_seed: { button: "Δημιουργία νέας φράσης seed", description: "Αυτό θα δημιουργήσει μια νέα φράση seed. Πρέπει να στείλετε ολόκληρο το υπόλοιπό σας στον εαυτό σας για να μπορέσετε να το επαναφέρετε με μια νέα seed.", confirm_question: "Είστε βέβαιοι ότι θέλετε να δημιουργήσετε μια νέα φράση seed;", cancel: "Ακύρωση", confirm: "Επιβεβαίωση", }, remove_spent: { button: "Αφαίρεση δαπανημένων αποδείξεων", description: "Ελέγξτε εάν τα token ecash από τα ενεργά σας mints έχουν δαπανηθεί και αφαιρέστε τα δαπανημένα από το πορτοφόλι σας. Χρησιμοποιήστε το μόνο εάν το πορτοφόλι σας έχει κολλήσει.", }, debug_console: { button: "Εναλλαγή Κονσόλας Εντοπισμού Σφαλμάτων", description: "Ανοίξτε το τερματικό εντοπισμού σφαλμάτων Javascript. Ποτέ μην επικολλάτε τίποτα σε αυτό το τερματικό που δεν καταλαβαίνετε. Ένας κλέφτης μπορεί να προσπαθήσει να σας εξαπατήσει για να επικολλήσετε κακόβουλο κώδικα εδώ.", }, export_proofs: { button: "Εξαγωγή ενεργών αποδείξεων", description: "Αντιγράψτε ολόκληρο το υπόλοιπό σας από το ενεργό mint ως token Cashu στο πρόχειρό σας. Αυτό θα εξάγει μόνο τα token από το επιλεγμένο mint και μονάδα. Για πλήρη εξαγωγή, επιλέξτε διαφορετικό mint και μονάδα και εξάγετε ξανά.", }, keyset_counters: { title: "Αύξηση μετρητών keyset", description: 'Κάντε κλικ στο ID του keyset για να αυξήσετε τους μετρητές διαδρομής παραγωγής για τα keysets στο πορτοφόλι σας. Αυτό είναι χρήσιμο εάν βλέπετε το σφάλμα "οι εξόδοι έχουν ήδη υπογραφεί".', counter: "μετρητής: {count}", }, unset_reserved: { button: "Κατάργηση δέσμευσης όλων των δεσμευμένων token", description: 'Αυτό το πορτοφόλι επισημαίνει το εκκρεμές εξερχόμενο ecash ως δεσμευμένο (και το αφαιρεί από το υπόλοιπό σας) για να αποτρέψει προσπάθειες διπλής δαπάνης. Αυτό το κουμπί θα καταργήσει τη δέσμευση όλων των δεσμευμένων token ώστε να μπορούν να χρησιμοποιηθούν ξανά. Εάν το κάνετε αυτό, το πορτοφόλι σας μπορεί να περιλαμβάνει δαπανημένες αποδείξεις. Πατήστε το κουμπί "Αφαίρεση δαπανημένων αποδείξεων" για να τις αφαιρέσετε.', }, show_onboarding: { button: "Εμφάνιση onboarding", description: "Εμφάνιση ξανά της οθόνης onboarding.", }, reset_wallet: { button: "Επαναφορά δεδομένων πορτοφολιού", description: "Επαναφορά των δεδομένων του πορτοφολιού σας. Προειδοποίηση: Αυτό θα διαγράψει τα πάντα! Βεβαιωθείτε ότι έχετε δημιουργήσει πρώτα ένα αντίγραφο ασφαλείας.", confirm_question: "Είστε βέβαιοι ότι θέλετε να διαγράψετε τα δεδομένα του πορτοφολιού σας;", cancel: "Ακύρωση", confirm: "Διαγραφή πορτοφολιού", }, export_wallet: { button: "Εξαγωγή δεδομένων πορτοφολιού", description: "Λήψη ενός dump του πορτοφολιού σας. Μπορείτε να επαναφέρετε το πορτοφόλι σας από αυτό το αρχείο στην οθόνη καλωσορίσματος ενός νέου πορτοφολιού. Αυτό το αρχείο θα είναι εκτός συγχρονισμού εάν συνεχίσετε να χρησιμοποιείτε το πορτοφόλι σας μετά την εξαγωγή του.", }, }, }, }, NoMintWarnBanner: { title: "Συμμετοχή σε mint", subtitle: "Δεν έχετε συνδεθεί ακόμα σε κανένα Cashu mint. Προσθέστε ένα URL mint στις ρυθμίσεις ή λάβετε ecash από ένα νέο mint για να ξεκινήσετε.", actions: { add_mint: { label: "@:global.actions.add_mint.label", }, receive: { label: "Λήψη Ecash", }, }, }, WalletPage: { actions: { send: { label: "@:global.actions.send.label", }, receive: { label: "@:global.actions.receive.label", }, }, tabs: { history: { label: "Ιστορικό", }, invoices: { label: "Τιμολόγια", }, mints: { label: "Mints", }, }, install: { text: "Εγκατάσταση", tooltip: "Εγκατάσταση Cashu", }, }, AlreadyRunning: { title: "Όχι.", text: "Μια άλλη καρτέλα εκτελείται ήδη. Κλείστε αυτήν την καρτέλα και δοκιμάστε ξανά.", actions: { retry: { label: "Επανάληψη", }, }, }, ErrorNotFound: { title: "404", text: "Ωχ. Τίποτα εδώ…", actions: { home: { label: "Επιστροφή στην αρχική σελίδα", }, }, }, BalanceView: { mintUrl: { label: "Mint", }, mintBalance: { label: "Υπόλοιπο", }, mintError: { label: "Σφάλμα mint", }, pending: { label: "Εκκρεμεί", tooltip: "Έλεγχος όλων των εκκρεμών token", }, }, WelcomePage: { actions: { previous: { label: "Προηγούμενο", }, next: { label: "Επόμενο", }, }, }, WelcomeSlide1: { title: "Καλώς ήρθατε στο Cashu", text: "Το Cashu.me είναι ένα δωρεάν και ανοιχτού κώδικα πορτοφόλι Bitcoin που χρησιμοποιεί ecash για να διατηρεί τα κεφάλαιά σας ασφαλή και ιδιωτικά.", actions: { more: { label: "Κάντε κλικ για να μάθετε περισσότερα", }, }, p1: { text: "Το Cashu είναι ένα δωρεάν και ανοιχτού κώδικα πρωτόκολλο ecash για Bitcoin. Μπορείτε να μάθετε περισσότερα γι' αυτό στο { link }.", link: { text: "cashu.space", }, }, p2: { text: "Αυτό το πορτοφόλι δεν είναι συνδεδεμένο με κανένα mint. Για να χρησιμοποιήσετε αυτό το πορτοφόλι, πρέπει να συνδεθείτε σε ένα ή περισσότερα Cashu mints που εμπιστεύεστε.", }, p3: { text: "Αυτό το πορτοφόλι αποθηκεύει ecash στο οποίο έχετε πρόσβαση μόνο εσείς. Εάν διαγράψετε τα δεδομένα του προγράμματος περιήγησής σας χωρίς αντίγραφο ασφαλείας της φράσης seed, θα χάσετε τα token σας.", }, p4: { text: "Αυτό το πορτοφόλι βρίσκεται σε έκδοση beta. Δεν φέρουμε καμία ευθύνη για άτομα που χάνουν την πρόσβαση στα κεφάλαιά τους. Χρησιμοποιήστε το με δική σας ευθύνη! Αυτός ο κώδικας είναι ανοιχτού κώδικα και διατίθεται με άδεια MIT.", }, }, WelcomeSlide2: { title: "Εγκατάσταση PWA", alt: { pwa_example: "Παράδειγμα εγκατάστασης PWA" }, installing: "Γίνεται εγκατάσταση…", instruction: { intro: { text: "Για την καλύτερη εμπειρία, χρησιμοποιήστε αυτό το πορτοφόλι με το εγγενές πρόγραμμα περιήγησης ιστού της συσκευής σας για να το εγκαταστήσετε ως Προοδευτική Εφαρμογή Ιστού. Κάντε το αυτό τώρα.", }, android: { title: "Android (Chrome)", step1: { item: "1. { icon } { text }", text: "Πατήστε το μενού (πάνω δεξιά)", }, step2: { item: "2. { icon } { text }", text: "Πατήστε { buttonText }", buttonText: "@:AndroidPWAPrompt.buttonText", }, }, ios: { title: "iOS (Safari)", step1: { item: "1. { icon } { text }", text: "Πατήστε κοινοποίηση (κάτω)", }, step2: { item: "2. { icon } { text }", text: "Πατήστε { buttonText }", buttonText: "@:iOSPWAPrompt.buttonText", }, }, outro: { text: "Μόλις εγκαταστήσετε αυτήν την εφαρμογή στη συσκευή σας, κλείστε αυτό το παράθυρο του προγράμματος περιήγησης και χρησιμοποιήστε την εφαρμογή από την αρχική σας οθόνη.", }, }, pwa: { success: { title: "Επιτυχία!", text: "Χρησιμοποιείτε το Cashu ως PWA. Κλείστε τυχόν άλλα ανοιχτά παράθυρα προγράμματος περιήγησης και χρησιμοποιήστε την εφαρμογή από την αρχική σας οθόνη.", nextSteps: "Τώρα μπορείτε να κλείσετε αυτήν την καρτέλα και να ανοίξετε την εφαρμογή από την αρχική οθόνη.", }, }, }, iOSPWAPrompt: { text: "Πατήστε { icon } και { buttonText }", buttonText: "Προσθήκη στην αρχική οθόνη", }, AndroidPWAPrompt: { text: "Πατήστε { icon } και { buttonText }", buttonText: "Προσθήκη στην αρχική οθόνη", }, WelcomeSlide3: { title: "Η Φράση Seed σας", text: "Αποθηκεύστε τη φράση seed σας σε έναν διαχειριστή κωδικών πρόσβασης ή σε χαρτί. Η φράση seed σας είναι ο μόνος τρόπος για να ανακτήσετε τα κεφάλαιά σας εάν χάσετε την πρόσβαση σε αυτήν τη συσκευή.", inputs: { seed_phrase: { label: "Φράση Seed", caption: "Μπορείτε να δείτε τη φράση seed σας στις ρυθμίσεις.", }, checkbox: { label: "Την έχω γράψει", }, }, }, WelcomeSlide4: { title: "Όροι", actions: { more: { label: "Διαβάστε τους Όρους Παροχής Υπηρεσιών", }, }, inputs: { checkbox: { label: "Έχω διαβάσει και αποδέχομαι αυτούς τους όρους και προϋποθέσεις", }, }, }, WelcomeSlideChoice: { title: "Ρύθμιση πορτοφολιού", text: "Θέλετε να επαναφέρετε από φράση seed ή να δημιουργήσετε νέο πορτοφόλι;", options: { new: { title: "Δημιουργία νέου πορτοφολιού", subtitle: "Δημιουργήστε νέα seed και προσθέστε mints.", }, recover: { title: "Επαναφορά πορτοφολιού", subtitle: "Εισαγάγετε τη φράση seed, επαναφέρετε mints και ecash.", }, }, }, WelcomeMintSetup: { title: "Προσθήκη mints", text: "Τα mints είναι διακομιστές που βοηθούν στην αποστολή και λήψη ecash. Επιλέξτε ένα εντοπισμένο mint ή προσθέστε ένα χειροκίνητα. Μπορείτε να παραλείψετε και να προσθέσετε αργότερα.", sections: { your_mints: "Τα mints σας" }, restoring: "Επαναφορά mints…", placeholder: { mint_url: "https://" }, }, WelcomeRecoverSeed: { title: "Εισαγάγετε τη φράση seed", text: "Επικολλήστε ή πληκτρολογήστε τη φράση 12 λέξεων για επαναφορά.", inputs: { word: "Λέξη { index }" }, actions: { paste_all: "Επικόλληση όλων" }, disclaimer: "Η φράση seed χρησιμοποιείται μόνο τοπικά για παραγωγή κλειδιών πορτοφολιού.", }, WelcomeRestoreEcash: { title: "Επαναφορά ecash", text: "Σαρώστε για μη δαπανημένες αποδείξεις (proofs) στα ρυθμισμένα mints και προσθέστε τες στο πορτοφόλι σας.", }, MintRatings: { title: "Κριτικές mint", reviews: "κριτικές", ratings: "Βαθμολογίες", no_reviews: "Δεν βρέθηκαν κριτικές", your_review: "Η κριτική σας", no_reviews_to_display: "Καμία κριτική προς εμφάνιση.", no_rating: "Χωρίς βαθμολογία", out_of: "από", rows: "Reviews", sort: "Ταξινόμηση", sort_options: { newest: "Νεότερες", oldest: "Παλαιότερες", highest: "Υψηλότερες", lowest: "Χαμηλότερες", }, actions: { write_review: "Γράψτε κριτική" }, empty_state_subtitle: "Βοηθήστε αφήνοντας μια κριτική. Μοιραστείτε την εμπειρία σας με αυτό το mint και βοηθήστε άλλους αφήνοντας μια κριτική.", }, CreateMintReview: { title: "Κριτική mint", publishing_as: "Δημοσίευση ως", inputs: { rating: { label: "Βαθμολογία" }, review: { label: "Κριτική (προαιρετικά)" }, }, actions: { publish: { label: "Δημοσίευση", in_progress: "Γίνεται δημοσίευση…" }, }, }, RestoreView: { seed_phrase: { label: "Επαναφορά από Φράση Seed", caption: "Εισαγάγετε τη φράση seed σας για να επαναφέρετε το πορτοφόλι σας. Πριν την επαναφορά, βεβαιωθείτε ότι έχετε προσθέσει όλα τα mints που έχετε χρησιμοποιήσει στο παρελθόν.", inputs: { seed_phrase: { label: "Φράση seed", caption: "Μπορείτε να δείτε τη φράση seed σας στις ρυθμίσεις.", }, }, }, information: { label: "Πληροφορίες", caption: "Ο οδηγός θα επαναφέρει μόνο ecash από μια άλλη φράση seed, δεν θα μπορείτε να χρησιμοποιήσετε αυτήν τη φράση seed ή να αλλάξετε τη φράση seed του πορτοφολιού που χρησιμοποιείτε αυτήν τη στιγμή. Αυτό σημαίνει ότι το επαναφερμένο ecash δεν θα προστατεύεται από την τρέχουσα φράση seed σας εφόσον δεν στείλετε το ecash στον εαυτό σας μία φορά.", }, restore_mints: { label: "Επαναφορά Mints", caption: 'Επιλέξτε το mint για επαναφορά. Μπορείτε να προσθέσετε περισσότερα mints στην κύρια οθόνη κάτω από το "Mints" και να τα επαναφέρετε εδώ.', }, actions: { paste: { error: "Αποτυχία ανάγνωσης περιεχομένων προχείρου.", }, validate: { error: "Το μνημονικό πρέπει να είναι τουλάχιστον 12 λέξεις.", }, select_all: { label: "Επιλογή όλων", }, deselect_all: { label: "Αποεπιλογή όλων", }, restore: { label: "Επαναφορά", in_progress: "Επαναφορά mint…", error: "Σφάλμα κατά την επαναφορά του mint: { error }", }, restore_all_mints: { label: "Επαναφορά Όλων των Mints", in_progress: "Επαναφορά mint { index } από { length }…", success: "Η επαναφορά ολοκληρώθηκε με επιτυχία", error: "Σφάλμα κατά την επαναφορά των mints: { error }", }, restore_selected_mints: { label: "Επαναφορά επιλεγμένων Mints ({count})", in_progress: "Επαναφορά mint { index } από { length }…", success: "Επιτυχής επαναφορά {count} mint(s)", error: "Σφάλμα κατά την επαναφορά των mints: { error }", }, }, nostr_mints: { label: "Επαναφορά Mints από το Nostr", caption: "Αναζητήστε αντίγραφα ασφαλείας mint που είναι αποθηκευμένα σε ρελέ Nostr χρησιμοποιώντας τη φράση-κλειδί σας. Αυτό θα σας βοηθήσει να ανακαλύψετε νομισματοκοπεία που χρησιμοποιήσατε προηγουμένως.", search_button: "Αναζήτηση για αντίγραφα ασφαλείας Mint", select_all: "Επιλογή όλων", deselect_all: "Αποεπιλογή όλων", backed_up: "Δημιουργήθηκαν αντίγραφα ασφαλείας", already_added: "Έχει ήδη προστεθεί", add_selected: "Προσθήκη επιλεγμένων ({count})", no_backups_found: "Δεν βρέθηκαν αντίγραφα ασφαλείας mint", no_backups_hint: "Βεβαιωθείτε ότι η δημιουργία αντιγράφων ασφαλείας του Nostr mint είναι ενεργοποιημένη στις ρυθμίσεις για την αυτόματη δημιουργία αντιγράφων ασφαλείας της λίστας mint σας.", invalid_mnemonic: "Εισαγάγετε μια έγκυρη φράση-κλειδί πριν από την αναζήτηση.", search_error: "Αποτυχία αναζήτησης αντιγράφων ασφαλείας mint.", add_error: "Αποτυχία προσθήκης επιλεγμένων mints.", }, }, MintSettings: { add: { title: "Προσθήκη mint", description: "Εισαγάγετε το URL ενός Cashu mint για να συνδεθείτε σε αυτό. Αυτό το πορτοφόλι δεν είναι συνδεδεμένο με κανένα mint.", inputs: { nickname: { placeholder: "Ψευδώνυμο (π.χ. Testnet)", }, }, actions: { add_mint: { label: "@:global.actions.add_mint.label", error_invalid_url: "Μη έγκυρο URL", }, scan: { label: "Σάρωση Κωδικού QR", }, }, }, discover: { title: "Ανακάλυψη mints", overline: "Ανακάλυψη", caption: "Ανακαλύψτε mints που άλλοι χρήστες έχουν προτείνει στο nostr.", actions: { discover: { label: "Ανακάλυψη mints", in_progress: "Φόρτωση…", error_no_mints: "Δεν βρέθηκαν mints", success: "Βρέθηκαν { length } mints", }, }, recommendations: { overline: "Βρέθηκαν { length } mints", caption: "Αυτά τα mints προτάθηκαν από άλλους χρήστες Nostr. Να είστε προσεκτικοί και κάντε τη δική σας έρευνα πριν χρησιμοποιήσετε ένα mint.", actions: { browse: { label: "Κάντε κλικ για περιήγηση στα mints", }, }, }, }, swap: { title: "Ανταλλαγή", overline: "Ανταλλαγές Multimint", caption: "Ανταλλάξτε κεφάλαια μεταξύ mints μέσω Lightning. Σημείωση: Αφήστε χώρο για πιθανές χρεώσεις Lightning. Εάν η εισερχόμενη πληρωμή δεν επιτύχει, ελέγξτε το τιμολόγιο μη αυτόματα.", inputs: { from: { label: "Από", }, to: { label: "Προς", }, amount: { label: "Ποσό ({ ticker })", }, }, actions: { swap: { label: "@:global.actions.swap.label", in_progress: "@:MintSettings.swap.actions.swap.label", }, }, }, error_badge: "Σφάλμα", reviews_text: "κριτικές", no_reviews_yet: "Δεν υπάρχουν κριτικές ακόμα", discover_mints_button: "Ανακαλύψτε mints", }, QrcodeReader: { progress: { text: "{ percentage }{ addon }", percentage: "{ percentage }%", keep_scanning_text: " - Συνέχεια σάρωσης", }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, }, }, InvoiceDetailDialog: { title: "Λήψη Lightning", create_invoice_title: "Δημιουργία Τιμολογίου", inputs: { amount: { label: "Ποσό ({ ticker }) *", }, }, actions: { close: { label: "@:global.actions.close.label", }, create: { label: "Δημιουργία Τιμολογίου", label_blocked: "Δημιουργία τιμολογίου…", in_progress: "Δημιουργία", }, }, invoice: { caption: "Τιμολόγιο Lightning", status_paid_text: "Πληρώθηκε!", actions: { close: { label: "@:global.actions.close.label", }, copy: { label: "@:global.actions.copy.label", }, }, }, }, SendDialog: { title: "Αποστολή", actions: { ecash: { label: "Ecash", error_no_mints: "Δεν υπάρχουν διαθέσιμα mints", }, lightning: { label: "Lightning", error_no_mints: "Δεν υπάρχουν διαθέσιμα mints", }, }, }, SendTokenDialog: { title: "Αποστολή Ecash", title_ecash_text: "Ecash", badge_offline_text: "Εκτός σύνδεσης", inputs: { amount: { label: "Ποσό ({ ticker }) *", invalid_too_much_error_text: "Πάρα πολύ", }, p2pk_pubkey: { label: "Δημόσιο κλειδί παραλήπτη", label_invalid: "Μη έγκυρο δημόσιο κλειδί παραλήπτη", }, }, actions: { close: { label: "@:global.actions.close.label", }, close_card_scanner: { label: "@:global.actions.close.label", }, copy_emoji: { label: "🥜", tooltip_text: "Αντιγραφή Emoji", }, copy_tokens: { label: "@:global.actions.copy.label", }, copy_link: { tooltip_text: "Αντιγραφή συνδέσμου", }, share: { tooltip_text: "Κοινοποίηση ecash", }, lock: { label: "@:global.actions.lock.label", }, paste_p2pk_pubkey: { tooltip_text: "@:global.actions.paste.label", }, send: { label: "@:global.actions.send.label", }, delete: { tooltip_text: "Διαγραφή από το ιστορικό", }, write_tokens_to_card: { tooltips: { ndef_supported_text: "Εγγραφή στην κάρτα NFC", ndef_unsupported_text: "Το NDEF δεν υποστηρίζεται", }, }, }, }, ReceiveDialog: { title: "Λήψη", actions: { ecash: { label: "Ecash", error_no_mints: "Δεν υπάρχουν διαθέσιμα mints", }, lightning: { label: "Lightning", error_no_mints: "Πρέπει να συνδεθείτε σε ένα mint για λήψη μέσω Lightning", }, }, }, ReceiveEcashDrawer: { title: "Λήψη Ecash", actions: { paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, request: { label: "Αίτημα", }, lock: { label: "@:global.actions.lock.label", }, nfc: { label: "NFC", scanning_text: "Σάρωση…", }, }, }, ReceiveTokenDialog: { title: "Λήψη Ecash", title_ecash_text: "Ecash", inputs: { tokens_base64: { label: "Επικολλήστε το token Cashu", }, }, errors: { invalid_token: { label: "Μη έγκυρο token", }, p2pk_lock_mismatch: { label: "Δεν είναι δυνατή η λήψη. Το κλείδωμα P2PK αυτού του token δεν ταιριάζει με το δημόσιο κλειδί σας.", }, }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, scan: { label: "@:global.actions.scan.label", }, receive: { label: "@:global.actions.receive.label", label_known_mint: "@:ReceiveTokenDialog.actions.receive.label", label_adding_mint: "Προσθήκη mint…", }, swap: { label: "@:global.actions.swap.label", tooltip_text: "Ανταλλαγή σε αξιόπιστο mint", caption: "Ανταλλαγή { value }", }, cancel_swap: { label: "@:global.actions.cancel.label", tooltip_text: "Ακύρωση ανταλλαγής", }, confirm_swap: { label: "@:ReceiveTokenDialog.actions.swap.label", tooltip_text: "@:ReceiveTokenDialog.actions.swap.tooltip_text", in_progress: "@:ReceiveTokenDialog.actions.confirm_swap.label", }, later: { label: "Λήψη αργότερα", tooltip_text: "Προσθήκη στο ιστορικό για λήψη αργότερα", already_in_history_success_text: "Το Ecash είναι ήδη στο Ιστορικό", added_to_history_success_text: "Το Ecash προστέθηκε στο Ιστορικό", }, nfc: { label: "NFC", tooltips: { ndef_supported_text: "Ανάγνωση από κάρτα NFC", ndef_unsupported_text: "Το NDEF δεν υποστηρίζεται", }, }, }, }, P2PKDialog: { p2pk: { caption: "Κλειδί P2PK", description: "Λήψη ecash κλειδωμένο σε αυτό το κλειδί", used_warning_text: "Προειδοποίηση: Αυτό το κλειδί χρησιμοποιήθηκε στο παρελθόν. Χρησιμοποιήστε ένα νέο κλειδί για καλύτερο απόρρητο.", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_key: { label: "Δημιουργία νέου κλειδιού", }, }, }, PaymentRequestDialog: { payment_request: { caption: "Αίτημα Πληρωμής", description: "Λήψη πληρωμών μέσω Nostr", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_request: { label: "Νέο αίτημα", }, add_amount: { label: "Προσθήκη ποσού", }, use_active_mint: { label: "Οποιοδήποτε mint", }, }, inputs: { amount: { placeholder: "Εισαγωγή ποσού", }, }, }, NumericKeyboard: { actions: { close: { label: "@:global.actions.close.label", closed_info_text: "Πληκτρολόγιο απενεργοποιημένο. Μπορείτε να ενεργοποιήσετε ξανά το πληκτρολόγιο στις ρυθμίσεις.", }, enter: { label: "@:global.actions.enter.label", }, }, }, NWCDialog: { nwc: { caption: "Nostr Wallet Connect", description: "Ελέγξτε το πορτοφόλι σας απομακρυσμένα με το NWC. Πατήστε τον κωδικό QR για να συνδέσετε το πορτοφόλι σας με μια συμβατή εφαρμογή.", warning_text: "Προειδοποίηση: οποιοσδήποτε με πρόσβαση σε αυτήν τη συμβολοσειρά σύνδεσης μπορεί να ξεκινήσει πληρωμές από το πορτοφόλι σας. Μην την κοινοποιείτε!", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, }, }, MintMotdMessage: { title: "Μήνυμα Mint", }, MintDetailsDialog: { contact: { title: "Επαφή", }, details: { title: "Λεπτομέρειες mint", url: { label: "URL", }, nuts: { label: "Nuts", actions: { show: { label: "Προβολή όλων", }, hide: { label: "Απόκρυψη", }, }, }, currency: { label: "Νόμισμα", }, currencies: { label: "@:MintDetailsDialog.details.currency.label", }, version: { label: "Έκδοση", }, }, actions: { title: "Ενέργειες", copy_mint_url: { label: "Αντιγραφή URL mint", }, delete: { label: "Διαγραφή mint", }, edit: { label: "Επεξεργασία mint", }, }, }, ChooseMint: { title: "Επιλέξτε ένα mint", badge_mint_error_text: "Σφάλμα", badge_option_mint_error_text: "@:ChooseMint.badge_mint_error_text", }, HistoryTable: { empty_text: "Δεν υπάρχει ακόμη ιστορικό", row: { type_label: "Ecash", date_label: "πριν από { value }", }, actions: { check_status: { tooltip_text: "Έλεγχος κατάστασης", }, receive: { tooltip_text: "Λήψη", }, filter_pending: { label: "Φιλτράρισμα εκκρεμών", }, show_all: { label: "Εμφάνιση όλων", }, }, old_token_not_found_error_text: "Παλιό token δεν βρέθηκε", }, InvoiceTable: { empty_text: "Δεν υπάρχουν ακόμη τιμολόγια", row: { type_label: "Lightning", type_tooltip_text: "Κάντε κλικ για αντιγραφή", date_label: "πριν από { value }", }, actions: { check_status: { tooltip_text: "Έλεγχος κατάστασης", }, filter_pending: { label: "Φιλτράρισμα εκκρεμών", }, show_all: { label: "Εμφάνιση όλων", }, }, }, RemoveMintDialog: { title: "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το mint?", nickname: { label: "Ψευδώνυμο", }, balances: { label: "Υπόλοιπα", }, warning_text: "Σημείωση: Επειδή αυτό το πορτοφόλι είναι παρανοϊκό, το ecash σας από αυτό το mint δεν θα διαγραφεί στην πραγματικότητα, αλλά θα παραμείνει αποθηκευμένο στη συσκευή σας. Θα το δείτε να εμφανίζεται ξανά εάν προσθέσετε ξανά αυτό το mint αργότερα.", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { confirm: { label: "Αφαίρεση mint", }, cancel: { label: "@:global.actions.cancel.label", }, }, }, ParseInputComponent: { placeholder: { default: "Token Cashu ή διεύθυνση Lightning", receive: "Token Cashu", pay: "Διεύθυνση Lightning ή τιμολόγιο", }, qr_scanner: { title: "Σάρωση Κωδικού QR", description: "Πατήστε για σάρωση διεύθυνσης", }, paste_button: { label: "@:global.actions.paste.label", }, }, PayInvoiceDialog: { input_data: { title: "Πληρωμή με Lightning", inputs: { invoice_data: { label: "Τιμολόγιο ή διεύθυνση Lightning", }, }, actions: { close: { label: "@:global.actions.close.label", }, enter: { label: "@:global.actions.enter.label", }, paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, }, }, lnurlpay: { amount_exact_label: "Ο { payee } ζητά { value } { ticker }", amount_range_label: "Ο { payee } ζητά{br}μεταξύ { min } και { max } { ticker }", sending_to_lightning_address: "Αποστολή σε { address }", inputs: { amount: { label: "Ποσό ({ ticker }) *", }, comment: { label: "Σχόλιο (προαιρετικό)", }, }, actions: { close: { label: "@:global.actions.close.label", }, send: { label: "@:global.actions.send.label", }, }, }, invoice: { title: "Πληρωμή { value }", paying: "Πληρώνεται", paid: "Πληρώθηκε", fee: "Χρέωση", memo: { label: "Σημείωμα", }, processing_info_text: "Επεξεργασία…", balance_too_low_warning_text: "Το υπόλοιπο είναι πολύ χαμηλό", actions: { close: { label: "@:global.actions.close.label", }, pay: { label: "Πληρωμή", in_progress: "@:PayInvoiceDialog.invoice.processing_info_text", error: "Σφάλμα", }, }, }, }, EditMintDialog: { title: "Επεξεργασία mint", inputs: { nickname: { label: "Ψευδώνυμο", }, mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, update: { label: "@:global.actions.update.label", }, }, }, AddMintDialog: { title: "Εμπιστεύεστε αυτό το mint;", description: "Πριν χρησιμοποιήσετε αυτό το mint, βεβαιωθείτε ότι το εμπιστεύεστε. Τα mints μπορεί να γίνουν κακόβουλα ή να σταματήσουν τη λειτουργία τους ανά πάσα στιγμή.", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, add_mint: { label: "@:global.actions.add_mint.label", in_progress: "Προσθήκη mint", }, }, }, restore: { mnemonic_error_text: "Παρακαλώ εισαγάγετε ένα μνημονικό", restore_mint_error_text: "Σφάλμα κατά την επαναφορά του mint: { error }", prepare_info_text: "Προετοιμασία διαδικασίας επαναφοράς…", restored_proofs_for_keyset_info_text: "Επαναφέρθηκαν { restoreCounter } αποδείξεις για το keyset { keysetId }", checking_proofs_for_keyset_info_text: "Έλεγχος αποδείξεων { startIndex } έως { endIndex } για το keyset { keysetId }", no_proofs_info_text: "Δεν βρέθηκαν αποδείξεις για επαναφορά", restored_amount_success_text: "Επαναφέρθηκε { amount }", }, swap: { in_progress_warning_text: "Η ανταλλαγή βρίσκεται σε εξέλιξη", invalid_swap_data_error_text: "Μη έγκυρα δεδομένα ανταλλαγής", swap_error_text: "Σφάλμα κατά την ανταλλαγή", }, TokenInformation: { fee: "Χρέωση", unit: "Μονάδα", fiat: "Fiat", p2pk: "P2PK", locked: "Κλειδωμένο", locked_to_you: "Κλειδωμένο για εσάς", mint: "Νομισματοκοπείο", memo: "Σημείωση", payment_request: "Αίτημα πληρωμής", nostr: "Nostr", token_copied: "Το token αντιγράφηκε στο πρόχειρο", }, }; ================================================ FILE: src/i18n/en-US/index.ts ================================================ export default { global: { copy_to_clipboard: { success: "Copied to clipboard!", }, actions: { add_mint: { label: "Add mint", }, cancel: { label: "Cancel", }, copy: { label: "Copy", }, close: { label: "Close", }, enter: { label: "Enter", }, lock: { label: "Lock", }, paste: { label: "Paste", }, receive: { label: "Receive", }, scan: { label: "Scan", }, send: { label: "Send", }, pay: { label: "Pay", }, swap: { label: "Swap", }, update: { label: "Update", }, }, inputs: { mint_url: { label: "Mint URL", }, }, }, common: { fee: "Fee", }, MultinutPicker: { payment: "Multinut payment", selectMints: "Select one or multiple mints to execute a payment from.", totalSelectedBalance: "Total Selected Balance", multiMintPay: "Multi-Mint Pay", balanceNotEnough: "Multi-mint balance not enough to satisfy this invoice", failed: "Failed to process: {error}", paid: "Paid {amount} via Lightning", }, wallet: { notifications: { balance_too_low: "Balance is too low", received: "Received {amount}", fee: " (fee: {fee})", could_not_request_mint: "Could not request mint", invoice_still_pending: "Invoice still pending", paid_lightning: "Paid {amount} via Lightning", payment_pending_refresh: "Payment pending. Refresh invoice manually.", sent: "Sent {amount}", token_still_pending: "Token still pending", received_lightning: "Received {amount} via Lightning", lightning_payment_failed: "Lightning payment failed", failed_to_decode_invoice: "Failed to decode invoice", unsupported_legacy_qr: "Unsupported Legacy QR code", legacy_qr_not_supported: "This Legacy QR code is not from a supported merchant", invalid_lnurl: "Invalid LNURL", lnurl_error: "LNURL Error", no_amount: "No amount", no_lnurl_data: "No LNURL data", no_price_data: "No price data.", please_try_again: "Please try again.", }, mint: { notifications: { already_added: "Mint already added", added: "Mint added", not_found: "Mint not found", activation_failed: "Mint activation failed", no_active_mint: "No active mint", unit_activation_failed: "Unit activation failed", unit_not_supported: "Unit not supported by mint", activated: "Mint activated", could_not_connect: "Could not connect to mint", could_not_get_info: "Could not get mint info", could_not_get_keys: "Could not get mint keys", could_not_get_keysets: "Could not get mint keysets", mint_validation_error: "Mint validation error", removed: "Mint removed", error: "Mint error", }, }, }, MainHeader: { menu: { settings: { title: "Settings", settings: { title: "Settings", caption: "Wallet configuration", }, }, terms: { title: "Terms", terms: { title: "Terms", caption: "Terms of Service", }, }, links: { title: "Links", cashuSpace: { title: "Cashu.space", caption: "cashu.space", }, github: { title: "Github", caption: "github.com/cashubtc", }, telegram: { title: "Telegram", caption: "t.me/CashuMe", }, twitter: { title: "Twitter", caption: "{'@'}CashuBTC", }, donate: { title: "Donate", caption: "Support Cashu", }, }, }, offline: { warning: { text: "Offline", }, }, reload: { warning: { text: "Reload in { countdown }", }, }, staging: { warning: { text: "Staging – don't use with real funds!", }, }, }, FullscreenHeader: { actions: { back: { label: "Wallet", }, }, }, Settings: { language: { title: "Language", description: "Please choose your preferred language from the list below.", }, sections: { backup_restore: "BACKUP & RESTORE", lightning_address: "LIGHTNING ADDRESS", nostr_keys: "NOSTR KEYS", nostr: { title: "NOSTR", relays: { expand_label: "Click to edit relays", add: { title: "Add relay", description: "Your wallet uses these relays for nostr operations such as payment requests, nostr wallet connect, and backups.", }, list: { title: "Relays", description: "Your wallet will connect to these relays.", copy_tooltip: "Copy relay", remove_tooltip: "Remove relay", }, }, }, payment_requests: "PAYMENT REQUESTS", nostr_wallet_connect: "NOSTR WALLET CONNECT", hardware_features: "HARDWARE FEATURES", p2pk_features: "P2PK FEATURES", privacy: "PRIVACY", experimental: "EXPERIMENTAL", appearance: "APPEARANCE", }, backup_restore: { backup_seed: { title: "Backup seed phrase", description: "Your seed phrase can restore your wallet. Keep it safe and private.", seed_phrase_label: "Seed phrase", }, restore_ecash: { title: "Restore ecash", description: "The restore wizard lets you recover lost ecash from a mnemonic seed phrase. The seed phrase of your current wallet will remain unaffected, the wizard will only allow you to restore ecash from another seed phrase.", button: "Restore", }, }, lightning_address: { title: "Lightning address", description: "Receive payments to your Lightning address.", enable: { toggle: "Enable", description: "Lightning address with npub.cash", }, address: { copy_tooltip: "Copy Lightning address", }, automatic_claim: { toggle: "Claim automatically", description: "Receive incoming payments automatically.", }, npc_v2: { choose_mint_title: "Choose mint for npub.cash v2", choose_mint_placeholder: "Select a mint...", }, }, nostr_keys: { title: "Your nostr keys", description: "Your nostr keys will be used to determine your Lightning address.", wallet_seed: { title: "Wallet seed phrase", description: "Generate nostr key pair from wallet seed", copy_nsec: "Copy nsec", }, nsec_bunker: { title: "Nsec Bunker", description: "Use a NIP-46 bunker", delete_tooltip: "Delete connection", }, use_nsec: { title: "Use your nsec", description: "This method is dangerous and not recommended", delete_tooltip: "Delete nsec", }, signing_extension: { title: "Signing extension", description: "Use a NIP-07 signing extension", not_found: "No NIP-07 signing extension found", }, }, payment_requests: { title: "Payment requests", description: "Payment requests allow you to receive payments via nostr. If you enable this, your wallet will subscribe to your nostr relays.", enable_toggle: "Enable Payment Requests", claim_automatically: { toggle: "Claim automatically", description: "Receive incoming payments automatically.", }, }, nostr_wallet_connect: { title: "Nostr Wallet Connect (NWC)", description: "Use NWC to control your wallet from any other application.", enable_toggle: "Enable NWC", payments_note: "You can only use NWC for payments from your Bitcoin balance. Payments will be made from your active mint.", connection: { copy_tooltip: "Copy connection string", qr_tooltip: "Show QR code", allowance_label: "Allowance left (sat)", }, }, hardware_features: { webnfc: { title: "WebNFC", description: "Choose the encoding for writing to NFC cards", text: { title: "Text", description: "Store token in plain text", }, weburl: { title: "URL", description: "Store URL to this wallet with token", }, binary: { title: "Binary", description: "Store tokens as binary data", }, quick_access: { toggle: "Quick access to NFC", description: "Quickly scan NFC cards in the Receive Ecash menu. This option adds an NFC button the Receive Ecash menu.", }, }, }, p2pk_features: { title: "P2PK", description: "Generate a key pair to receive P2PK-locked ecash. Warning: This feature is experimental. Only use with small amounts. If you lose your private keys, nobody will be able to unlock the ecash locked to it anymore.", generate_button: "Generate key", import_button: "Import nsec", quick_access: { toggle: "Quick access to lock", description: "Use this to quickly show your P2PK locking key in the receive ecash menu.", }, keys_expansion: { label: "Click to browse {count} keys", used_badge: "used", }, }, privacy: { title: "Privacy", description: "These settings affect your privacy.", check_incoming: { toggle: "Check incoming invoice", description: "If enabled, the wallet will check the latest invoice in the background. This increases the wallet's responsiveness which makes fingerprinting easier. You can manually check unpaid invoices in the Invoices tab.", }, check_startup: { toggle: "Check pending invoices on startup", description: "If enabled, the wallet will check pending invoices from the last 24 hours on startup.", }, check_all: { toggle: "Check all invoices", description: "If enabled, the wallet will periodically check unpaid invoices in the background for up to two weeks. This increases the wallet's online activity which makes fingerprinting easier. You can manually check unpaid invoices in the Invoices tab.", }, check_sent: { toggle: "Check sent ecash", description: "If enabled, the wallet will use periodic background checks to determine if sent tokens have been redeemed. This increases the wallet's online activity which makes fingerprinting easier.", }, websockets: { toggle: "Use WebSockets", description: "If enabled, the wallet will use long-lived WebSocket connections to receive updates on paid invoices and spent tokens from mints. This increases the wallet's responsiveness but also makes fingerprinting easier.", }, bitcoin_price: { toggle: "Get exchange rate from Coinbase", description: "If enabled, the current Bitcoin exchange rate will be fetched from coinbase.com and your converted balance will be displayed.", currency: { title: "Fiat Currency", description: "Choose the fiat currency for Bitcoin price display.", }, }, }, experimental: { title: "Experimental", description: "These features are experimental.", receive_swaps: { toggle: "Receive swaps", badge: "Beta", description: "Option to swap received Ecash to your active mint in the Receive Ecash dialog.", }, auto_paste: { toggle: "Paste Ecash automatically", description: "Automatically paste ecash in your clipboard when you press Receive, then Ecash, then Paste. Automatic pasting can cause UI glitches in iOS, turn it off if you experience issues.", }, auditor: { toggle: "Enable auditor", badge: "Beta", description: "If enabled, the wallet will display auditor information in the mint details dialog. The auditor is a third party service that monitors the reliability of mints.", url_label: "Auditor URL", api_url_label: "Auditor API URL", }, multinut: { toggle: "Enable Multinut", description: "If enabled, the wallet will use Multinut to pay invoices from multiple mints at once.", }, nostr_mint_backup: { toggle: "Backup mint list on Nostr", description: "If enabled, your mint list will be automatically backed up to Nostr relays using your configured Nostr keys. This allows you to restore your mint list across devices.", notifications: { enabled: "Nostr mint backup enabled", disabled: "Nostr mint backup disabled", failed: "Failed to enable Nostr mint backup", }, }, }, appearance: { keyboard: { title: "On-screen keyboard", description: "Use the numeric keyboard for entering amounts.", toggle: "Use numeric keyboard", toggle_description: "If enabled, the numeric keyboard will be used for entering amounts.", }, theme: { title: "Appearance", description: "Change how your wallet looks.", tooltips: { mono: "mono", cyber: "cyber", freedom: "freedom", nostr: "nostr", bitcoin: "bitcoin", mint: "mint", nut: "nut", blu: "blu", flamingo: "flamingo", }, }, bip177: { title: "Bitcoin symbol", description: "Use ₿ symbol instead of sats.", toggle: "Use ₿ symbol", }, }, web_of_trust: { title: "Web of trust", known_pubkeys: "Known pubkeys: {wotCount}", continue_crawl: "Continue crawl", crawl_odell: "Crawl ODELL'S WEB OF TRUST", crawl_wot: "Crawl web of trust", pause: "Pause", reset: "Reset", progress: "{crawlProcessed} / {crawlTotal}", }, npub_cash: { use_npubx: "Use npubx.cash", copy_lightning_address: "Copy Lightning address", v2_mint: "npub.cash v2 mint", }, multinut: { use_multinut: "Use Multinut", }, advanced: { title: "Advanced", developer: { title: "Developer settings", description: "The following settings are for development and debugging.", new_seed: { button: "Generate new seed phrase", description: "This will generate a new seed phrase. You must send your entire balance to yourself in order to be able to restore it with a new seed.", confirm_question: "Are you sure you want to generate a new seed phrase?", cancel: "Cancel", confirm: "Confirm", }, remove_spent: { button: "Remove spent proofs", description: "Check if the ecash tokens from your active mints are spent and remove the spent ones from your wallet. Only use this if your wallet is stuck.", }, debug_console: { button: "Toggle Debug Console", description: "Open the Javascript debug terminal. Never paste anything into this terminal that you don't understand. A thief might try to trick you into pasting malicious code here.", }, export_proofs: { button: "Export active proofs", description: "Copy your entire balance from the active mint as a Cashu token into your clipboard. This will only export the tokens from the selected mint and unit. For a full export, select a different mint and unit and export again.", }, keyset_counters: { title: "Increment keyset counters", description: 'Click the keyset ID to increment the derivation path counters for the keysets in your wallet. This is useful if you see the "outputs have already been signed" error.', counter: "counter: {count}", }, unset_reserved: { button: "Unset all reserved tokens", description: 'This wallet marks pending outgoing ecash as reserved (and subtracts it from your balance) to prevent double-spend attempts. This button will unset all reserved tokens so they can be used again. If you do this, your wallet might include spent proofs. Press the "Remove spent proofs" button to get rid of them.', }, show_onboarding: { button: "Show onboarding", description: "Show the onboarding screen again.", }, reset_wallet: { button: "Reset wallet data", description: "Reset your wallet data. Warning: This will delete everything! Make sure you create a backup first.", confirm_question: "Are you sure you want to delete your wallet data?", cancel: "Cancel", confirm: "Delete wallet", }, export_wallet: { button: "Export wallet data", description: "Download a dump of your wallet. You can restore your wallet from this file in the welcome screen of a new wallet. This file will be out of sync if you keep using your wallet after exporting it.", }, import_wallet: { button: "Import wallet backup", description: "Restore your wallet from a previously exported backup file. This will replace your current wallet data with the backup.", confirm_question: "Are you sure you want to restore your wallet data?", cancel: "Cancel", confirm: "IMPORT WALLET BACKUP", }, }, }, }, NoMintWarnBanner: { title: "Join a mint", subtitle: "You haven't joined any Cashu mint yet. Add a mint URL in the settings or receive ecash from a new mint to get started.", actions: { add_mint: { label: "@:global.actions.add_mint.label", }, receive: { label: "Receive Ecash", }, }, }, WalletPage: { actions: { send: { label: "@:global.actions.send.label", }, receive: { label: "@:global.actions.receive.label", }, }, tabs: { history: { label: "History", }, invoices: { label: "Invoices", }, mints: { label: "Mints", }, }, install: { text: "Install", tooltip: "Install Cashu", }, }, AlreadyRunning: { title: "Nope.", text: "Another tab is already running. Close this tab and try again.", actions: { retry: { label: "Retry", }, }, }, ErrorNotFound: { title: "404", text: "Oops. Nothing here…", actions: { home: { label: "Go back home", }, }, }, BalanceView: { mintUrl: { label: "Mint", }, mintBalance: { label: "Balance", }, mintError: { label: "Mint error", }, pending: { label: "Pending", tooltip: "Check all pending tokens", }, }, WelcomePage: { actions: { previous: { label: "Previous", }, next: { label: "Next", }, }, }, WelcomeSlide1: { title: "Welcome to Cashu", text: "Cashu.me is a free and open-source Bitcoin wallet that uses ecash to keep your funds secure and private.", actions: { more: { label: "Click to learn more", }, }, p1: { text: "Cashu is a free and open-source ecash protocol for Bitcoin. You can learn more about it at { link }.", link: { text: "cashu.space", }, }, p2: { text: "This wallet is not affiliated with any mint. To use this wallet, you need to connect to one or more Cashu mints that you trust.", }, p3: { text: "This wallet stores ecash that only you have access to. If you delete your browser data without a seed phrase backup, you will lose your tokens.", }, p4: { text: "This wallet is in beta. We hold no responsibility for people losing access to funds. Use at your own risk! This code is open-source and licensed under the MIT license.", }, }, WelcomeSlide2: { title: "Install PWA", alt: { pwa_example: "PWA Installation Example", }, installing: "Installing…", instruction: { intro: { text: "For the best experience, use this wallet with your device's native web browser to install it as a Progressive Web App.", }, android: { title: "Android (Chrome)", step1: { item: "1. { icon } { text }", text: "Tap the menu (top right)", }, step2: { item: "2. { icon } { text }", text: "Press { buttonText }", buttonText: "@:AndroidPWAPrompt.buttonText", }, }, ios: { title: "iOS (Safari)", step1: { item: "1. { icon } { text }", text: "Tap share (bottom)", }, step2: { item: "2. { icon } { text }", text: "Press { buttonText }", buttonText: "@:iOSPWAPrompt.buttonText", }, }, outro: { text: "Once you installed this app on your device, close this browser window and use the app from your home screen.", }, }, pwa: { success: { title: "Success!", text: "You are using Cashu as a PWA. Close any other open browser windows and use the app from your home screen.", nextSteps: "You can now close this browser tab and open the app from your home screen.", }, }, }, iOSPWAPrompt: { text: "Tap { icon } and { buttonText }", buttonText: "Add to Home Screen", }, AndroidPWAPrompt: { text: "Tap { icon } and { buttonText }", buttonText: "Add to Home Screen", }, WelcomeSlide3: { title: "Your Seed Phrase", text: "Store your seed phrase in a password manager or on paper. Your seed phrase is the only way to recover your funds if you lose access to this device.", inputs: { seed_phrase: { label: "Seed Phrase", caption: "You can see your seed phrase in the settings.", }, checkbox: { label: "I have written it down", }, }, }, WelcomeSlide4: { title: "Terms", actions: { more: { label: "Read Terms of Service", }, }, inputs: { checkbox: { label: "I've read and accept these terms and conditions", }, }, }, WelcomeSlideChoice: { title: "Set up your wallet", text: "Do you want to recover from a seed phrase or create a new wallet?", options: { new: { title: "Create new wallet", subtitle: "Generate a new seed and add mints.", }, recover: { title: "Recover wallet", subtitle: "Enter your seed phrase, restore mints and ecash.", }, }, }, WelcomeMintSetup: { title: "Add mints", text: "Mints are servers that help you send and receive ecash. Choose a discovered mint or add one manually. Skip to add mints later.", sections: { your_mints: "Your mints", }, restoring: "Restoring mints…", placeholder: { mint_url: "https://", }, }, WelcomeRecoverSeed: { title: "Enter your seed phrase", text: "Paste or type your 12 word seed phrase to recover.", inputs: { word: "Word { index }", }, actions: { paste_all: "Paste all", }, disclaimer: "Your seed phrase is only used locally to derive your wallet keys.", }, WelcomeRestoreEcash: { title: "Restore your ecash", text: "Scan for unspent proofs on your configured mints and add them to your wallet.", }, MintRatings: { title: "Mint Reviews", reviews: "reviews", ratings: "Ratings", no_reviews: "No reviews found", your_review: "Your review", no_reviews_to_display: "No reviews to display.", no_rating: "No rating", out_of: "out of", rows: "Reviews", sort: "Sort", sort_options: { newest: "Newest", oldest: "Oldest", highest: "Highest", lowest: "Lowest", }, actions: { write_review: "Write a review", }, empty_state_subtitle: "Help by leaving a review. Share your experience with this mint and help others by leaving a review.", }, CreateMintReview: { title: "Review Mint", publishing_as: "Publishing as", inputs: { rating: { label: "Rating" }, review: { label: "Review (optional)" }, }, actions: { publish: { label: "Submit Review", in_progress: "Submitting…" }, }, }, RestoreView: { seed_phrase: { label: "Restore from Seed Phrase", caption: "Enter your seed phrase to restore your wallet. Before you restore, make sure you have added all the mints that you have used before.", inputs: { seed_phrase: { label: "Seed phrase", caption: "You can see your seed phrase in the settings.", }, }, }, information: { label: "Information", caption: "The wizard will only restore ecash from another seed phrase, you will not be able to use this seed phrase or change the seed phrase of the wallet that you're currently using. This means that restored ecash will not be protected by your current seed phrase as long as you don't send the ecash to yourself once.", }, restore_mints: { label: "Restore Mints", caption: 'Select the mint to restore. You can add more mints in the main screen under "Mints" and restore them here.', }, actions: { paste: { error: "Failed to read clipboard contents.", }, validate: { error: "Mnemonic is not a valid BIP39 seed phrase.", }, select_all: { label: "Select All", }, deselect_all: { label: "Deselect All", }, restore: { label: "Restore", in_progress: "Restoring mint …", error: "Error restoring mint: { error }", }, restore_all_mints: { label: "Restore All Mints", in_progress: "Restoring mint { index } of { length } …", success: "Restore finished successfully", error: "Error restoring mints: { error }", }, restore_selected_mints: { label: "Restore Selected Mints ({count})", in_progress: "Restoring mint { index } of { length } …", success: "Successfully restored {count} mint(s)", error: "Error restoring selected mints: { error }", }, }, nostr_mints: { label: "Restore Mints from Nostr", caption: "Search for mint backups stored on Nostr relays using your seed phrase. This will help you discover mints you previously used.", search_button: "Search for Mint Backups", select_all: "Select All", deselect_all: "Deselect All", backed_up: "Backed up", already_added: "Already Added", add_selected: "Add Selected ({count})", no_backups_found: "No mint backups found", no_backups_hint: "Make sure Nostr mint backup is enabled in settings to automatically backup your mint list.", invalid_mnemonic: "Please enter a valid seed phrase before searching.", search_error: "Failed to search for mint backups.", add_error: "Failed to add selected mints.", }, }, MintSettings: { add: { title: "Add mint", description: "Enter the URL of a Cashu mint to connect to it. This wallet is not affiliated with any mint.", inputs: { nickname: { placeholder: "Nickname (e.g. Testnet)", }, }, actions: { add_mint: { label: "@:global.actions.add_mint.label", error_invalid_url: "Invalid URL", }, scan: { label: "Scan QR Code", }, }, }, discover: { title: "Discover mints", overline: "Discover", caption: "Discover mints other users have recommended on nostr.", actions: { discover: { label: "Discover mints", in_progress: "Loading…", error_no_mints: "No mints found", success: "Found { length } mints", }, }, recommendations: { overline: "Found { length } mints", caption: "These mints were recommended by other Nostr users. Be careful and do your own research before using a mint.", actions: { browse: { label: "Click to browse mints", }, }, }, }, swap: { title: "Swap", overline: "Multimint Swaps", actions: { receove_to_trusted_mint: { label: "Receive to trusted mint", }, swap: { label: "@:global.actions.swap.label", in_progress: "@:MintSettings.swap.actions.swap.label", }, }, caption: "Swap funds between mints via Lightning. Note: Leave room for potential Lightning fees. If the incoming payment does not succeed, check the invoice manually.", inputs: { from: { label: "From", }, to: { label: "To", }, amount: { label: "Amount ({ ticker })", }, }, }, error_badge: "Error", reviews_text: "reviews", no_reviews_yet: "No reviews yet", discover_mints_button: "Discover mints", }, QrcodeReader: { progress: { text: "{ percentage }{ addon }", percentage: "{ percentage }%", keep_scanning_text: " - Keep scanning", }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, }, }, InvoiceDetailDialog: { title: "Receive Lightning", create_invoice_title: "Create Invoice", inputs: { amount: { label: "Amount ({ ticker }) *", }, }, actions: { close: { label: "@:global.actions.close.label", }, create: { label: "Create Invoice", label_blocked: "Creating invoice…", in_progress: "Creating", }, }, invoice: { caption: "Lightning invoice", status_paid_text: "Paid!", actions: { close: { label: "@:global.actions.close.label", }, copy: { label: "@:global.actions.copy.label", }, }, }, }, SendDialog: { title: "Send", actions: { ecash: { label: "Ecash", error_no_mints: "No mints available", }, lightning: { label: "Lightning", error_no_mints: "No mints available", }, }, }, SendTokenDialog: { title: "Send Ecash", title_ecash_text: "Ecash", badge_offline_text: "Offline", inputs: { amount: { label: "Amount ({ ticker }) *", invalid_too_much_error_text: "Too much", }, p2pk_pubkey: { label: "Receiver public key", label_invalid: "Receiver public key", }, }, actions: { close: { label: "@:global.actions.close.label", }, close_card_scanner: { label: "@:global.actions.close.label", }, copy_emoji: { label: "🥜", tooltip_text: "Copy Emoji", }, copy_tokens: { label: "@:global.actions.copy.label", }, copy_link: { tooltip_text: "Copy link", }, share: { tooltip_text: "Share ecash", }, lock: { label: "@:global.actions.lock.label", }, paste_p2pk_pubkey: { tooltip_text: "@:global.actions.paste.label", }, pay: { label: "@:global.actions.pay.label", }, send: { label: "@:global.actions.send.label", }, delete: { tooltip_text: "Delete from history", }, write_tokens_to_card: { tooltips: { ndef_supported_text: "Flash to NFC card", ndef_unsupported_text: "NDEF unsupported", }, }, }, errors: { amount_required: "Enter an amount first.", serialization_failed: "Could not prepare ecash token.", }, }, SendPaymentRequest: { actions: { pay: { label: "Pay", }, pay_via: { label: "Pay via {transport}", }, }, info: { pay_to: "Pay to {target}", invalid_url: "Invalid URL", }, }, PaymentRequestInfo: { title_with_transport: "Payment request via {transport}", title: "Payment request", subtitle: "Pay to {target}", subtitle_fallback: "Payment request", invalid_url: "Invalid URL", }, ReceiveDialog: { title: "Receive", actions: { ecash: { label: "Ecash", error_no_mints: "No mints available", }, lightning: { label: "Lightning", error_no_mints: "You need to connect to a mint to receive via Lightning", }, }, }, ReceiveEcashDrawer: { title: "Receive Ecash", actions: { paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, request: { label: "Request", }, lock: { label: "@:global.actions.lock.label", }, nfc: { label: "NFC", scanning_text: "Scanning…", }, }, }, ReceiveTokenDialog: { title: "Receive Ecash", title_ecash_text: "Ecash", inputs: { tokens_base64: { label: "Paste Cashu token", }, }, errors: { invalid_token: { label: "Invalid token", }, p2pk_lock_mismatch: { label: "Unable to receive. This token's P2PK lock doesn't match your public key.", }, }, unknown_mint_info_text: "Unknown mint. It will be added after you receive this token.", swap_section: { title: "Swap", source_label: "From", destination_label: "To", fee_info: "This swap will incur Lightning network fees.", }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, scan: { label: "@:global.actions.scan.label", }, receive: { label: "@:global.actions.receive.label", label_known_mint: "@:ReceiveTokenDialog.actions.receive.label", label_adding_mint: "Adding mint…", }, swap: { label: "Receive to trusted mint", tooltip_text: "Swap to a trusted mint", caption: "Swap { value }", processing: "Processing swap...", failed: "Swap failed", }, cancel_swap: { label: "@:global.actions.cancel.label", tooltip_text: "Cancel swap", }, confirm_swap: { label: "@:ReceiveTokenDialog.actions.swap.label", tooltip_text: "@:ReceiveTokenDialog.actions.swap.tooltip_text", in_progress: "@:ReceiveTokenDialog.actions.confirm_swap.label", }, receive_to_selected_mint: { label: "Receive to selected mint", }, later: { label: "Receive later", tooltip_text: "Add to history to receive later", already_in_history_success_text: "Ecash already in History", added_to_history_success_text: "Ecash added to History", }, nfc: { label: "NFC", tooltips: { ndef_supported_text: "Read from NFC card", ndef_unsupported_text: "NDEF unsupported", }, }, }, }, P2PKDialog: { p2pk: { caption: "P2PK Key", description: "Receive ecash locked to this key", used_warning_text: "Warning: This key was used before. Use a new key for better privacy.", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_key: { label: "Generate new key", }, }, }, PaymentRequestDialog: { payment_request: { caption: "Payment Request", description: "Receive payments via Nostr", }, received_total: "Received total", no_payments_yet: "No payments yet", actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_request: { label: "New request", }, add_amount: { label: "Add amount", }, use_active_mint: { label: "Any mint", }, }, inputs: { amount: { placeholder: "Enter amount", }, }, }, NumericKeyboard: { actions: { close: { label: "@:global.actions.close.label", closed_info_text: "Keyboard disabled. You can re-enable the keyboard in the settings.", }, enter: { label: "@:global.actions.enter.label", }, }, }, NWCDialog: { nwc: { caption: "Nostr Wallet Connect", description: "Control your wallet remotely with NWC. Press the QR code to link your wallet with a compatible app.", warning_text: "Warning: anyone with access to this connection string can initiate payments from your wallet. Do not share!", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, }, }, MintMotdMessage: { title: "Mint Message", }, MintDetailsDialog: { contact: { title: "Contact", }, details: { title: "Mint details", url: { label: "URL", }, nuts: { label: "Nuts", actions: { show: { label: "View all", }, hide: { label: "Hide", }, }, }, currency: { label: "Currency", }, currencies: { label: "@:MintDetailsDialog.details.currency.label", }, version: { label: "Version", }, }, actions: { title: "Actions", copy_mint_url: { label: "Copy mint URL", }, delete: { label: "Delete mint", }, edit: { label: "Edit mint", }, }, }, ChooseMint: { title: "Select a mint", placeholder: "Select a mint", available_text: "available", sheet_title: "Select Mint", badge_mint_error_text: "Error", badge_option_mint_error_text: "@:ChooseMint.badge_mint_error_text", }, HistoryTable: { empty_text: "No history yet", row: { type_label: "Ecash", date_label: "{ value } ago", }, actions: { check_status: { tooltip_text: "Check status", }, receive: { tooltip_text: "Receive", }, filter_pending: { label: "Filter pending", }, show_all: { label: "Show all", }, }, old_token_not_found_error_text: "Old token not found", }, InvoiceTable: { empty_text: "No invoices yet", row: { type_label: "Lightning", type_tooltip_text: "Click to copy", date_label: "{ value } ago", }, actions: { check_status: { tooltip_text: "Check status", }, filter_pending: { label: "Filter pending", }, show_all: { label: "Show all", }, }, }, RemoveMintDialog: { title: "Are you sure you want to delete this mint?", nickname: { label: "Nickname", }, balances: { label: "Balances", }, warning_text: "Note: Because this wallet is paranoid, your ecash from this mint will not be actually deleted but will remain stored on your device. You will see it reappear if you re-add this mint later again.", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { confirm: { label: "Remove mint", }, cancel: { label: "@:global.actions.cancel.label", }, }, }, ParseInputComponent: { placeholder: { default: "Cashu token or Lightning address", receive: "Cashu token", pay: "Lightning address or invoice", }, qr_scanner: { title: "Scan QR Code", description: "Tap to scan an address", }, paste_button: { label: "@:global.actions.paste.label", }, }, PayInvoiceDialog: { input_data: { title: "Pay Lightning", inputs: { invoice_data: { label: "Lightning invoice or address", }, }, actions: { close: { label: "@:global.actions.close.label", }, enter: { label: "@:global.actions.enter.label", }, paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, }, }, lnurlpay: { amount_exact_label: "{ payee } is requesting { value } { ticker }", amount_range_label: "{ payee } is requesting{br}between { min } and { max } { ticker }", sending_to_lightning_address: "Sending to { address }", inputs: { amount: { label: "Amount ({ ticker }) *", }, comment: { label: "Comment (optional)", }, }, actions: { close: { label: "@:global.actions.close.label", }, send: { label: "@:global.actions.send.label", }, }, }, invoice: { title: "Pay { value }", paying: "Paying", paid: "Paid", fee: "Fee", memo: { label: "Memo", }, processing_info_text: "Processing…", balance_too_low_warning_text: "Balance too low", actions: { close: { label: "@:global.actions.close.label", }, pay: { label: "Pay", in_progress: "@:PayInvoiceDialog.invoice.processing_info_text", error: "Error", }, }, }, }, EditMintDialog: { title: "Edit mint", inputs: { nickname: { label: "Nickname", }, mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, update: { label: "@:global.actions.update.label", }, }, }, AddMintDialog: { title: "Do you trust this mint?", description: "Before using this mint, make sure you trust it. Mints could become malicious or cease operation at any time.", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, add_mint: { label: "@:global.actions.add_mint.label", in_progress: "Adding mint", }, }, }, restore: { mnemonic_error_text: "Please enter a mnemonic", restore_mint_error_text: "Error restoring mint: { error }", prepare_info_text: "Preparing restore process …", restored_proofs_for_keyset_info_text: "Restored { restoreCounter } proofs for keyset { keysetId }", checking_proofs_for_keyset_info_text: "Checking proofs { startIndex } to { endIndex } for keyset { keysetId }", no_proofs_info_text: "No proofs found to restore", restored_amount_success_text: "Restored { amount }", }, swap: { in_progress_warning_text: "Swap in progress", invalid_swap_data_error_text: "Invalid swap data", swap_error_text: "Error swapping", }, TokenInformation: { fee: "Fee", unit: "Unit", fiat: "Fiat", p2pk: "P2PK", locked: "Locked", locked_to_you: "Locked to you", mint: "Mint", memo: "Memo", payment_request: "Payment request", nostr: "Nostr", token_copied: "Token copied to clipboard", }, }; ================================================ FILE: src/i18n/es-ES/index.ts ================================================ export default { global: { copy_to_clipboard: { success: "¡Copiado al portapapeles!", }, actions: { add_mint: { label: "Añadir mint", }, cancel: { label: "Cancelar", }, copy: { label: "Copiar", }, close: { label: "Cerrar", }, enter: { label: "Entrar", }, lock: { label: "Bloquear", }, paste: { label: "Pegar", }, receive: { label: "Recibir", }, scan: { label: "Escanear", }, send: { label: "Enviar", }, swap: { label: "Intercambiar", }, update: { label: "Actualizar", }, }, inputs: { mint_url: { label: "URL del mint", }, }, }, MultinutPicker: { payment: "Pago Multinut", selectMints: "Seleccione una o varias casas de moneda para ejecutar un pago.", totalSelectedBalance: "Saldo Total Seleccionado", multiMintPay: "Pago Multi-Mint", balanceNotEnough: "El saldo de multi-mint no es suficiente para satisfacer esta factura", failed: "Error al procesar: {error}", paid: "Pagado {amount} a través de Lightning", }, wallet: { notifications: { balance_too_low: "El saldo es demasiado bajo", received: "Recibido {amount}", fee: " (comisión: {fee})", could_not_request_mint: "No se pudo solicitar acuñación", invoice_still_pending: "Factura aún pendiente", paid_lightning: "Pagado {amount} a través de Lightning", payment_pending_refresh: "Pago pendiente. Actualice la factura manualmente.", sent: "Enviado {amount}", token_still_pending: "Token aún pendiente", received_lightning: "Recibido {amount} a través de Lightning", lightning_payment_failed: "Pago Lightning fallido", failed_to_decode_invoice: "No se pudo decodificar la factura", invalid_lnurl: "LNURL inválido", lnurl_error: "Error LNURL", no_amount: "Sin cantidad", no_lnurl_data: "Sin datos LNURL", no_price_data: "Sin datos de precio.", please_try_again: "Por favor, inténtelo de nuevo.", }, mint: { notifications: { already_added: "Mint ya añadido", added: "Mint añadido", not_found: "Mint no encontrado", activation_failed: "Fallo en la activación del mint", no_active_mint: "No hay mint activo", unit_activation_failed: "Fallo en la activación de la unidad", unit_not_supported: "Unidad no soportada por el mint", activated: "Mint activado", could_not_connect: "No se pudo conectar al mint", could_not_get_info: "No se pudo obtener información del mint", could_not_get_keys: "No se pudieron obtener las claves del mint", could_not_get_keysets: "No se pudieron obtener los keysets del mint", mint_validation_error: "Error de validación de Mint", removed: "Mint eliminado", error: "Error del mint", }, }, }, MainHeader: { menu: { settings: { title: "Configuración", settings: { title: "Configuración", caption: "Configuración de la billetera", }, }, terms: { title: "Términos", terms: { title: "Términos", caption: "Términos de Servicio", }, }, links: { title: "Enlaces", cashuSpace: { title: "Cashu.space", caption: "cashu.space", }, github: { title: "Github", caption: "github.com/cashubtc", }, telegram: { title: "Telegram", caption: "t.me/CashuMe", }, twitter: { title: "Twitter", caption: "{'@'}CashuBTC", }, donate: { title: "Donar", caption: "Apoyar a Cashu", }, }, }, offline: { warning: { text: "Sin conexión", }, }, reload: { warning: { text: "Recargar en { countdown }", }, }, staging: { warning: { text: "Staging – ¡no usar con fondos reales!", }, }, }, FullscreenHeader: { actions: { back: { label: "Billetera", }, }, }, Settings: { language: { title: "Idioma", description: "Por favor, elige tu idioma preferido de la lista de abajo.", }, sections: { backup_restore: "COPIA DE SEGURIDAD Y RESTAURACIÓN", lightning_address: "DIRECCIÓN LIGHTNING", nostr_keys: "CLAVES NOSTR", nostr: { title: "NOSTR", relays: { expand_label: "Haga clic para editar relays", add: { title: "Añadir relay", description: "Su billetera usa estos relays para operaciones de nostr como solicitudes de pago, NWC y copias de seguridad.", }, list: { title: "Relays", description: "Su billetera se conectará a estos relays.", copy_tooltip: "Copiar relay", remove_tooltip: "Eliminar relay", }, }, }, payment_requests: "SOLICITUDES DE PAGO", nostr_wallet_connect: "NOSTR WALLET CONNECT", hardware_features: "CARACTERÍSTICAS DE HARDWARE", p2pk_features: "CARACTERÍSTICAS P2PK", privacy: "PRIVACIDAD", experimental: "EXPERIMENTAL", appearance: "APARIENCIA", }, backup_restore: { backup_seed: { title: "Frase semilla de respaldo", description: "Tu frase semilla puede restaurar tu billetera. Mantenla segura y privada.", seed_phrase_label: "Frase semilla", }, restore_ecash: { title: "Restaurar ecash", description: "El asistente de restauración te permite recuperar ecash perdido desde una frase semilla mnemónica. La frase semilla de tu billetera actual no se verá afectada, el asistente solo te permitirá restaurar ecash desde otra frase semilla.", button: "Restaurar", }, }, lightning_address: { title: "Dirección Lightning", description: "Recibe pagos a tu dirección Lightning.", enable: { toggle: "Habilitar", description: "Dirección Lightning con npub.cash", }, address: { copy_tooltip: "Copiar dirección Lightning", }, automatic_claim: { toggle: "Reclamar automáticamente", description: "Recibir pagos entrantes automáticamente.", }, npc_v2: { choose_mint_title: "Elegir mint para npub.cash v2", choose_mint_placeholder: "Seleccionar un mint...", }, }, nostr_keys: { title: "Tus claves nostr", description: "Establece las claves nostr para tu dirección Lightning.", wallet_seed: { title: "Frase semilla de la billetera", description: "Generar par de claves nostr desde la semilla de la billetera", copy_nsec: "Copiar nsec", }, nsec_bunker: { title: "Nsec Bunker", description: "Usar un bunker NIP-46", delete_tooltip: "Eliminar conexión", }, use_nsec: { title: "Usa tu nsec", description: "Este método es peligroso y no se recomienda", delete_tooltip: "Eliminar nsec", }, signing_extension: { title: "Extensión de firma", description: "Usar una extensión de firma NIP-07", not_found: "No se encontró ninguna extensión de firma NIP-07", }, }, payment_requests: { title: "Solicitudes de pago", description: "Las solicitudes de pago te permiten recibir pagos vía nostr. Si habilitas esto, tu billetera se suscribirá a tus relays nostr.", enable_toggle: "Habilitar Solicitudes de Pago", claim_automatically: { toggle: "Reclamar automáticamente", description: "Recibir pagos entrantes automáticamente.", }, }, nostr_wallet_connect: { title: "Nostr Wallet Connect (NWC)", description: "Usa NWC para controlar tu billetera desde cualquier otra aplicación.", enable_toggle: "Habilitar NWC", payments_note: "Solo puedes usar NWC para pagos desde tu saldo de Bitcoin. Los pagos se realizarán desde tu mint activo.", connection: { copy_tooltip: "Copiar cadena de conexión", qr_tooltip: "Mostrar código QR", allowance_label: "Límite restante (sat)", }, }, hardware_features: { webnfc: { title: "WebNFC", description: "Elige la codificación para escribir en tarjetas NFC", text: { title: "Texto", description: "Almacenar token en texto plano", }, weburl: { title: "URL", description: "Almacenar URL a esta billetera con el token", }, binary: { title: "Binario", description: "Almacenar tokens como datos binarios", }, quick_access: { toggle: "Acceso rápido a NFC", description: "Escanea rápidamente tarjetas NFC en el menú Recibir Ecash. Esta opción añade un botón NFC al menú Recibir Ecash.", }, }, }, p2pk_features: { title: "P2PK", description: "Genera un par de claves para recibir ecash bloqueado con P2PK. Advertencia: Esta característica es experimental. Úsala solo con cantidades pequeñas. Si pierdes tus claves privadas, nadie podrá desbloquear el ecash bloqueado con ellas.", generate_button: "Generar clave", import_button: "Importar nsec", quick_access: { toggle: "Acceso rápido para bloquear", description: "Usa esto para mostrar rápidamente tu clave de bloqueo P2PK en el menú recibir ecash.", }, keys_expansion: { label: "Haz clic para ver {count} claves", used_badge: "usada", }, }, privacy: { title: "Privacidad", description: "Estas configuraciones afectan tu privacidad.", check_incoming: { toggle: "Verificar factura entrante", description: "Si está habilitado, la billetera verificará la última factura en segundo plano. Esto aumenta la capacidad de respuesta de la billetera, lo que facilita la toma de huellas digitales. Puedes verificar manualmente las facturas no pagadas en la pestaña Facturas.", }, check_startup: { toggle: "Verificar facturas pendientes al inicio", description: "Si está habilitado, la billetera verificará las facturas pendientes de las últimas 24 horas al inicio.", }, check_all: { toggle: "Verificar todas las facturas", description: "Si está habilitado, la billetera verificará periódicamente las facturas no pagadas en segundo plano durante hasta dos semanas. Esto aumenta la actividad en línea de la billetera, lo que facilita la toma de huellas digitales. Puedes verificar manualmente las facturas no pagadas en la pestaña Facturas.", }, check_sent: { toggle: "Verificar ecash enviado", description: "Si está habilitado, la billetera usará verificaciones periódicas en segundo plano para determinar si los tokens enviados han sido canjeados. Esto aumenta la actividad en línea de la billetera, lo que facilita la toma de huellas digitales.", }, websockets: { toggle: "Usar WebSockets", description: "Si está habilitado, la billetera usará conexiones WebSocket de larga duración para recibir actualizaciones sobre facturas pagadas y tokens gastados de los mints. Esto aumenta la capacidad de respuesta de la billetera pero también facilita la toma de huellas digitales.", }, bitcoin_price: { toggle: "Obtener tasa de cambio de Coinbase", description: "Si está habilitado, se obtendrá la tasa de cambio actual de Bitcoin de coinbase.com y se mostrará tu saldo convertido.", currency: { title: "Moneda Fiat", description: "Elija la moneda fiat para mostrar el precio de Bitcoin.", }, }, }, experimental: { title: "Experimental", description: "Estas características son experimentales.", receive_swaps: { toggle: "Recibir intercambios", badge: "Beta", description: "Opción para intercambiar Ecash recibido a tu mint activo en el diálogo Recibir Ecash.", }, auto_paste: { toggle: "Pegar Ecash automáticamente", description: "Pega automáticamente ecash de tu portapapeles cuando presionas Recibir, luego Ecash, luego Pegar. El pegado automático puede causar fallos en la interfaz de usuario en iOS, desactívalo si experimentas problemas.", }, auditor: { toggle: "Habilitar auditor", badge: "Beta", description: "Si está habilitado, la billetera mostrará información del auditor en el diálogo de detalles del mint. El auditor es un servicio de terceros que monitorea la fiabilidad de los mints.", url_label: "URL del Auditor", api_url_label: "URL API del Auditor", }, multinut: { toggle: "Habilitar Multinut", description: "Si está habilitado, el monedero utilizará Multinut para pagar facturas de varias cecas a la vez.", }, nostr_mint_backup: { toggle: "Copia de seguridad de la lista de cecas en Nostr", description: "Si está activada, se realizará una copia de seguridad automática de su lista de cecas en los relés de Nostr utilizando sus claves de Nostr configuradas. Esto le permite restaurar su lista de cecas en todos los dispositivos.", notifications: { enabled: "Copia de seguridad de la ceca de Nostr activada", disabled: "Copia de seguridad de la ceca de Nostr desactivada", failed: "Error al activar la copia de seguridad de la ceca de Nostr", }, }, }, appearance: { keyboard: { title: "Teclado en pantalla", description: "Usa el teclado numérico para ingresar cantidades.", toggle: "Usar teclado numérico", toggle_description: "Si está habilitado, se usará el teclado numérico para ingresar cantidades.", }, theme: { title: "Apariencia", description: "Cambia cómo se ve tu billetera.", tooltips: { mono: "mono", cyber: "ciber", freedom: "libertad", nostr: "nostr", bitcoin: "bitcoin", mint: "mint", nut: "nuez", blu: "azul", flamingo: "flamenco", }, }, bip177: { title: "Símbolo de Bitcoin", description: "Utilice el símbolo ₿ en lugar de sats.", toggle: "Utilice el símbolo ₿", }, }, web_of_trust: { title: "Red de confianza", known_pubkeys: "Claves públicas conocidas: {wotCount}", continue_crawl: "Continuar rastreo", crawl_odell: "RASTREAR LA RED DE CONFIANZA DE ODELL", crawl_wot: "Rastrear red de confianza", pause: "Pausa", reset: "Reiniciar", progress: "{crawlProcessed} / {crawlTotal}", }, npub_cash: { use_npubx: "Utilice npubx.cash", copy_lightning_address: "Copiar dirección Lightning", v2_mint: "npub.cash v2 mint", }, multinut: { use_multinut: "Usar Multinut", }, advanced: { title: "Avanzado", developer: { title: "Configuración de desarrollador", description: "Las siguientes configuraciones son para desarrollo y depuración.", new_seed: { button: "Generar nueva frase semilla", description: "Esto generará una nueva frase semilla. Debes enviar tu saldo completo a ti mismo para poder restaurarlo con una nueva semilla.", confirm_question: "¿Estás seguro de que quieres generar una nueva frase semilla?", cancel: "Cancelar", confirm: "Confirmar", }, remove_spent: { button: "Eliminar pruebas gastadas", description: "Verifica si los tokens ecash de tus mints activos están gastados y elimina los gastados de tu billetera. Solo usa esto si tu billetera está bloqueada.", }, debug_console: { button: "Alternar Consola de Depuración", description: "Abre la terminal de depuración de Javascript. Nunca pegues nada en esta terminal que no entiendas. Un ladrón podría intentar engañarte para que pegues código malicioso aquí.", }, export_proofs: { button: "Exportar pruebas activas", description: "Copia tu saldo completo del mint activo como un token Cashu en tu portapapeles. Esto solo exportará los tokens del mint y unidad seleccionados. Para una exportación completa, selecciona un mint y unidad diferente y exporta de nuevo.", }, keyset_counters: { title: "Incrementar contadores de keyset", description: 'Haz clic en el ID del keyset para incrementar los contadores de la ruta de derivación para los keysets en tu billetera. Esto es útil si ves el error "las salidas ya han sido firmadas".', counter: "contador: {count}", }, unset_reserved: { button: "Desmarcar todos los tokens reservados", description: 'Esta billetera marca el ecash saliente pendiente como reservado (y lo resta de tu saldo) para prevenir intentos de doble gasto. Este botón desmarcará todos los tokens reservados para que puedan usarse de nuevo. Si haces esto, tu billetera podría incluir pruebas gastadas. Presiona el botón "Eliminar pruebas gastadas" para deshacerte de ellas.', }, show_onboarding: { button: "Mostrar bienvenida", description: "Mostrar la pantalla de bienvenida de nuevo.", }, reset_wallet: { button: "Restablecer datos de la billetera", description: "Restablece los datos de tu billetera. Advertencia: ¡Esto borrará todo! Asegúrate de crear una copia de seguridad primero.", confirm_question: "¿Estás seguro de que quieres eliminar los datos de tu billetera?", cancel: "Cancelar", confirm: "Eliminar billetera", }, export_wallet: { button: "Exportar datos de la billetera", description: "Descarga un volcado de tu billetera. Puedes restaurar tu billetera desde este archivo en la pantalla de bienvenida de una nueva billetera. Este archivo estará desactualizado si sigues usando tu billetera después de exportarlo.", }, }, }, }, NoMintWarnBanner: { title: "Únete a un mint", subtitle: "Todavía no te has unido a ningún mint de Cashu. Añade una URL de mint en la configuración o recibe ecash de un nuevo mint para empezar.", actions: { add_mint: { label: "@:global.actions.add_mint.label", }, receive: { label: "Recibir Ecash", }, }, }, WalletPage: { actions: { send: { label: "@:global.actions.send.label", }, receive: { label: "@:global.actions.receive.label", }, }, tabs: { history: { label: "Historial", }, invoices: { label: "Facturas", }, mints: { label: "Mints", }, }, install: { text: "Instalar", tooltip: "Instalar Cashu", }, }, AlreadyRunning: { title: "Nop.", text: "Otra pestaña ya está en ejecución. Cierra esta pestaña e inténtalo de nuevo.", actions: { retry: { label: "Reintentar", }, }, }, ErrorNotFound: { title: "404", text: "Oops. Nada por aquí…", actions: { home: { label: "Volver al inicio", }, }, }, BalanceView: { mintUrl: { label: "Mint", }, mintBalance: { label: "Saldo", }, mintError: { label: "Error del mint", }, pending: { label: "Pendiente", tooltip: "Verificar todos los tokens pendientes", }, }, WelcomePage: { actions: { previous: { label: "Anterior", }, next: { label: "Siguiente", }, }, }, WelcomeSlide1: { title: "Bienvenido a Cashu", text: "Cashu.me es una billetera de Bitcoin gratuita y de código abierto que utiliza ecash para mantener tus fondos seguros y privados.", actions: { more: { label: "Haz clic para saber más", }, }, p1: { text: "Cashu es un protocolo ecash gratuito y de código abierto para Bitcoin. Puedes aprender más en { link }.", link: { text: "cashu.space", }, }, p2: { text: "Esta billetera no está afiliada a ningún mint. Para usar esta billetera, necesitas conectarte a uno o más mints de Cashu en los que confíes.", }, p3: { text: "Esta billetera almacena ecash al que solo tú tienes acceso. Si eliminas los datos de tu navegador sin una copia de seguridad de la frase semilla, perderás tus tokens.", }, p4: { text: "Esta billetera está en beta. No nos hacemos responsables de las personas que pierdan el acceso a sus fondos. ¡Úsala bajo tu propio riesgo! Este código es de código abierto y está licenciado bajo la licencia MIT.", }, }, WelcomeSlide2: { title: "Instalar PWA", alt: { pwa_example: "Ejemplo de instalación PWA" }, installing: "Instalando…", instruction: { intro: { text: "Para la mejor experiencia, usa esta billetera con el navegador web nativo de tu dispositivo para instalarla como una Aplicación Web Progresiva. Haz esto ahora mismo.", }, android: { title: "Android (Chrome)", step1: { item: "1. { icon } { text }", text: "Toca el menú (arriba a la derecha)", }, step2: { item: "2. { icon } { text }", text: "Presiona { buttonText }", buttonText: "@:AndroidPWAPrompt.buttonText", }, }, ios: { title: "iOS (Safari)", step1: { item: "1. { icon } { text }", text: "Toca compartir (abajo)", }, step2: { item: "2. { icon } { text }", text: "Presiona { buttonText }", buttonText: "@:iOSPWAPrompt.buttonText", }, }, outro: { text: "Una vez que hayas instalado esta aplicación en tu dispositivo, cierra esta ventana del navegador y usa la aplicación desde tu pantalla de inicio.", }, }, pwa: { success: { title: "¡Éxito!", text: "Estás usando Cashu como una PWA. Cierra cualquier otra ventana del navegador abierta y usa la aplicación desde tu pantalla de inicio.", nextSteps: "Ahora puedes cerrar esta pestaña del navegador y abrir la app desde tu pantalla de inicio.", }, }, }, iOSPWAPrompt: { text: "Toca { icon } y { buttonText }", buttonText: "Añadir a pantalla de inicio", }, AndroidPWAPrompt: { text: "Toca { icon } y { buttonText }", buttonText: "Añadir a pantalla de inicio", }, WelcomeSlide3: { title: "Tu Frase Semilla", text: "Guarda tu frase semilla en un gestor de contraseñas o en papel. Tu frase semilla es la única forma de recuperar tus fondos si pierdes el acceso a este dispositivo.", inputs: { seed_phrase: { label: "Frase Semilla", caption: "Puedes ver tu frase semilla en la configuración.", }, checkbox: { label: "La he anotado", }, }, }, WelcomeSlide4: { title: "Términos", actions: { more: { label: "Leer Términos de Servicio", }, }, inputs: { checkbox: { label: "He leído y acepto estos términos y condiciones", }, }, }, WelcomeSlideChoice: { title: "Configura tu billetera", text: "¿Quieres recuperar desde una frase semilla o crear una billetera nueva?", options: { new: { title: "Crear billetera nueva", subtitle: "Genera una semilla nueva y añade mints.", }, recover: { title: "Recuperar billetera", subtitle: "Ingresa tu frase semilla, restaura mints y ecash.", }, }, }, WelcomeMintSetup: { title: "Añadir mints", text: "Los mints son servidores que te ayudan a enviar y recibir ecash. Elige un mint descubierto o añade uno manualmente. Puedes hacerlo más tarde.", sections: { your_mints: "Tus mints" }, restoring: "Restaurando mints…", placeholder: { mint_url: "https://" }, }, WelcomeRecoverSeed: { title: "Ingresa tu frase semilla", text: "Pega o escribe tu frase semilla de 12 palabras para recuperar.", inputs: { word: "Palabra { index }" }, actions: { paste_all: "Pegar todo" }, disclaimer: "Tu frase semilla solo se usa localmente para derivar las claves de tu billetera.", }, WelcomeRestoreEcash: { title: "Restaura tu ecash", text: "Busca comprobantes no gastados en tus mints configurados y agrégalos a tu billetera.", }, MintRatings: { title: "Reseñas del mint", reviews: "reseñas", ratings: "Calificaciones", no_reviews: "No se encontraron reseñas", your_review: "Tu reseña", no_reviews_to_display: "No hay reseñas para mostrar.", no_rating: "Sin calificación", out_of: "de", rows: "Reviews", sort: "Ordenar", sort_options: { newest: "Más recientes", oldest: "Más antiguas", highest: "Más altas", lowest: "Más bajas", }, actions: { write_review: "Escribir una reseña" }, empty_state_subtitle: "Ayuda dejando una reseña. Comparte tu experiencia con este mint y ayuda a otros dejando una reseña.", }, CreateMintReview: { title: "Reseñar mint", publishing_as: "Publicando como", inputs: { rating: { label: "Calificación" }, review: { label: "Reseña (opcional)" }, }, actions: { publish: { label: "Publicar", in_progress: "Publicando…" } }, }, RestoreView: { seed_phrase: { label: "Restaurar desde Frase Semilla", caption: "Ingresa tu frase semilla para restaurar tu billetera. Antes de restaurar, asegúrate de haber añadido todos los mints que has usado antes.", inputs: { seed_phrase: { label: "Frase semilla", caption: "Puedes ver tu frase semilla en la configuración.", }, }, }, information: { label: "Información", caption: "El asistente solo restaurará ecash desde otra frase semilla, no podrás usar esta frase semilla ni cambiar la frase semilla de la billetera que estás usando actualmente. Esto significa que el ecash restaurado no estará protegido por tu frase semilla actual mientras no te envíes el ecash a ti mismo una vez.", }, restore_mints: { label: "Restaurar Mints", caption: 'Selecciona el mint para restaurar. Puedes añadir más mints en la pantalla principal bajo "Mints" y restaurarlos aquí.', }, nostr_mints: { label: "Restaurar Mints de Nostr", caption: "Busque copias de seguridad de mints almacenadas en relés de Nostr utilizando su frase semilla. Esto le ayudará a descubrir mints que utilizó anteriormente.", search_button: "Buscar copias de seguridad de Mint", select_all: "Seleccionar todo", deselect_all: "Deseleccionar todo", backed_up: "Copia de seguridad realizada", already_added: "Ya añadido", add_selected: "Añadir seleccionados ({count})", no_backups_found: "No se han encontrado copias de seguridad de mints", no_backups_hint: "Asegúrese de que la copia de seguridad de Nostr mint está activada en los ajustes para hacer una copia de seguridad automática de su lista de mints.", invalid_mnemonic: "Por favor, introduzca una frase semilla válida antes de buscar.", search_error: "Error al buscar copias de seguridad de mints.", add_error: "Error al añadir los mints seleccionados.", }, actions: { paste: { error: "Error al leer el contenido del portapapeles.", }, validate: { error: "El mnemónico debe tener al menos 12 palabras.", }, select_all: { label: "Seleccionar todo", }, deselect_all: { label: "Deseleccionar todo", }, restore: { label: "Restaurar", in_progress: "Restaurando mint…", error: "Error restaurando mint: { error }", }, restore_all_mints: { label: "Restaurar Todos los Mints", in_progress: "Restaurando mint { index } de { length }…", success: "Restauración finalizada con éxito", error: "Error restaurando mints: { error }", }, restore_selected_mints: { label: "Restaurar Mints seleccionados ({count})", in_progress: "Restaurando mint { index } de { length }…", success: "Se han restaurado correctamente {count} mint(s)", error: "Error al restaurar los mints seleccionados: { error }", }, }, }, MintSettings: { add: { title: "Añadir mint", description: "Ingresa la URL de un mint de Cashu para conectarte a él. Esta billetera no está afiliada a ningún mint.", inputs: { nickname: { placeholder: "Apodo (ej. Testnet)", }, }, actions: { add_mint: { label: "@:global.actions.add_mint.label", error_invalid_url: "URL inválida", }, scan: { label: "Escanear Código QR", }, }, }, discover: { title: "Descubrir mints", overline: "Descubrir", caption: "Descubre mints que otros usuarios han recomendado en nostr.", actions: { discover: { label: "Descubrir mints", in_progress: "Cargando…", error_no_mints: "No se encontraron mints", success: "Encontrados { length } mints", }, }, recommendations: { overline: "Encontrados { length } mints", caption: "Estos mints fueron recomendados por otros usuarios de Nostr. Ten cuidado y haz tu propia investigación antes de usar un mint.", actions: { browse: { label: "Haz clic para explorar mints", }, }, }, }, swap: { title: "Intercambiar", overline: "Intercambios Multimint", caption: "Intercambia fondos entre mints vía Lightning. Nota: Deja espacio para posibles comisiones de Lightning. Si el pago entrante no tiene éxito, verifica la factura manualmente.", inputs: { from: { label: "Desde", }, to: { label: "Hasta", }, amount: { label: "Cantidad ({ ticker })", }, }, actions: { swap: { label: "@:global.actions.swap.label", in_progress: "@:MintSettings.swap.actions.swap.label", }, }, }, error_badge: "Error", reviews_text: "reseñas", no_reviews_yet: "Aún no hay reseñas", discover_mints_button: "Descubrir mints", }, QrcodeReader: { progress: { text: "{ percentage }{ addon }", percentage: "{ percentage }%", keep_scanning_text: " - Sigue escaneando", }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, }, }, InvoiceDetailDialog: { title: "Recibir Lightning", create_invoice_title: "Crear Factura", inputs: { amount: { label: "Cantidad ({ ticker }) *", }, }, actions: { close: { label: "@:global.actions.close.label", }, create: { label: "Crear Factura", label_blocked: "Creando factura…", in_progress: "Creando", }, }, invoice: { caption: "Factura Lightning", status_paid_text: "¡Pagada!", actions: { close: { label: "@:global.actions.close.label", }, copy: { label: "@:global.actions.copy.label", }, }, }, }, SendDialog: { title: "Enviar", actions: { ecash: { label: "Ecash", error_no_mints: "No hay mints disponibles", }, lightning: { label: "Lightning", error_no_mints: "No hay mints disponibles", }, }, }, SendTokenDialog: { title: "Enviar Ecash", title_ecash_text: "Ecash", badge_offline_text: "Sin conexión", inputs: { amount: { label: "Cantidad ({ ticker }) *", invalid_too_much_error_text: "Demasiado", }, p2pk_pubkey: { label: "Clave pública del receptor", label_invalid: "Clave pública del receptor inválida", }, }, actions: { close: { label: "@:global.actions.close.label", }, close_card_scanner: { label: "@:global.actions.close.label", }, copy_emoji: { label: "🥜", tooltip_text: "Copiar Emoji", }, copy_tokens: { label: "@:global.actions.copy.label", }, copy_link: { tooltip_text: "Copiar enlace", }, share: { tooltip_text: "Compartir ecash", }, lock: { label: "@:global.actions.lock.label", }, paste_p2pk_pubkey: { tooltip_text: "@:global.actions.paste.label", }, send: { label: "@:global.actions.send.label", }, delete: { tooltip_text: "Eliminar del historial", }, write_tokens_to_card: { tooltips: { ndef_supported_text: "Grabar en tarjeta NFC", ndef_unsupported_text: "NDEF no soportado", }, }, }, }, ReceiveDialog: { title: "Recibir", actions: { ecash: { label: "Ecash", error_no_mints: "No hay mints disponibles", }, lightning: { label: "Lightning", error_no_mints: "Necesitas conectarte a un mint para recibir vía Lightning", }, }, }, ReceiveEcashDrawer: { title: "Recibir Ecash", actions: { paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, request: { label: "Solicitar", }, lock: { label: "@:global.actions.lock.label", }, nfc: { label: "NFC", scanning_text: "Escaneando…", }, }, }, ReceiveTokenDialog: { title: "Recibir Ecash", title_ecash_text: "Ecash", inputs: { tokens_base64: { label: "Pegar token Cashu", }, }, errors: { invalid_token: { label: "Token inválido", }, p2pk_lock_mismatch: { label: "No se puede recibir. El bloqueo P2PK de este token no coincide con su clave pública.", }, }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, scan: { label: "@:global.actions.scan.label", }, receive: { label: "@:global.actions.receive.label", label_known_mint: "@:ReceiveTokenDialog.actions.receive.label", label_adding_mint: "Añadiendo mint…", }, swap: { label: "@:global.actions.swap.label", tooltip_text: "Intercambiar a un mint de confianza", caption: "Intercambiar { value }", }, cancel_swap: { label: "@:global.actions.cancel.label", tooltip_text: "Cancelar intercambio", }, confirm_swap: { label: "@:ReceiveTokenDialog.actions.swap.label", tooltip_text: "@:ReceiveTokenDialog.actions.swap.tooltip_text", in_progress: "@:ReceiveTokenDialog.actions.confirm_swap.label", }, later: { label: "Recibir más tarde", tooltip_text: "Añadir al historial para recibir más tarde", already_in_history_success_text: "Ecash ya está en el Historial", added_to_history_success_text: "Ecash añadido al Historial", }, nfc: { label: "NFC", tooltips: { ndef_supported_text: "Leer desde tarjeta NFC", ndef_unsupported_text: "NDEF no soportado", }, }, }, }, P2PKDialog: { p2pk: { caption: "Clave P2PK", description: "Recibir ecash bloqueado con esta clave", used_warning_text: "Advertencia: Esta clave se usó antes. Usa una clave nueva para mayor privacidad.", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_key: { label: "Generar nueva clave", }, }, }, PaymentRequestDialog: { payment_request: { caption: "Solicitud de Pago", description: "Recibir pagos vía Nostr", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_request: { label: "Nueva solicitud", }, add_amount: { label: "Añadir cantidad", }, use_active_mint: { label: "Cualquier mint", }, }, inputs: { amount: { placeholder: "Ingresar cantidad", }, }, }, NumericKeyboard: { actions: { close: { label: "@:global.actions.close.label", closed_info_text: "Teclado desactivado. Puedes reactivar el teclado en la configuración.", }, enter: { label: "@:global.actions.enter.label", }, }, }, NWCDialog: { nwc: { caption: "Nostr Wallet Connect", description: "Controla tu billetera remotamente con NWC. Presiona el código QR para vincular tu billetera con una aplicación compatible.", warning_text: "Advertencia: cualquiera con acceso a esta cadena de conexión puede iniciar pagos desde tu billetera. ¡No la compartas!", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, }, }, MintMotdMessage: { title: "Mensaje del Mint", }, MintDetailsDialog: { contact: { title: "Contacto", }, details: { title: "Detalles del mint", url: { label: "URL", }, nuts: { label: "Nuts", actions: { show: { label: "Ver todo", }, hide: { label: "Ocultar", }, }, }, currency: { label: "Moneda", }, currencies: { label: "@:MintDetailsDialog.details.currency.label", }, version: { label: "Versión", }, }, actions: { title: "Acciones", copy_mint_url: { label: "Copiar URL del mint", }, delete: { label: "Eliminar mint", }, edit: { label: "Editar mint", }, }, }, ChooseMint: { title: "Selecciona un mint", badge_mint_error_text: "Error", badge_option_mint_error_text: "@:ChooseMint.badge_mint_error_text", }, HistoryTable: { empty_text: "Aún no hay historial", row: { type_label: "Ecash", date_label: "hace { value }", }, actions: { check_status: { tooltip_text: "Verificar estado", }, receive: { tooltip_text: "Recibir", }, filter_pending: { label: "Filtrar pendientes", }, show_all: { label: "Mostrar todo", }, }, old_token_not_found_error_text: "Token antiguo no encontrado", }, InvoiceTable: { empty_text: "Aún no hay facturas", row: { type_label: "Lightning", type_tooltip_text: "Haz clic para copiar", date_label: "hace { value }", }, actions: { check_status: { tooltip_text: "Verificar estado", }, filter_pending: { label: "Filtrar pendientes", }, show_all: { label: "Mostrar todo", }, }, }, RemoveMintDialog: { title: "¿Estás seguro de que quieres eliminar este mint?", nickname: { label: "Apodo", }, balances: { label: "Saldos", }, warning_text: "Nota: Debido a que esta billetera es paranoica, tu ecash de este mint no se eliminará realmente, sino que permanecerá almacenado en tu dispositivo. Lo verás reaparecer si vuelves a añadir este mint más tarde.", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { confirm: { label: "Eliminar mint", }, cancel: { label: "@:global.actions.cancel.label", }, }, }, ParseInputComponent: { placeholder: { default: "Token Cashu o dirección Lightning", receive: "Token Cashu", pay: "Dirección Lightning o factura", }, qr_scanner: { title: "Escanear Código QR", description: "Toca para escanear una dirección", }, paste_button: { label: "@:global.actions.paste.label", }, }, PayInvoiceDialog: { input_data: { title: "Pagar con Lightning", inputs: { invoice_data: { label: "Factura o dirección Lightning", }, }, actions: { close: { label: "@:global.actions.close.label", }, enter: { label: "@:global.actions.enter.label", }, paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, }, }, lnurlpay: { amount_exact_label: "{ payee } está solicitando { value } { ticker }", amount_range_label: "{ payee } está solicitando{br}entre { min } y { max } { ticker }", sending_to_lightning_address: "Enviando a { address }", inputs: { amount: { label: "Cantidad ({ ticker }) *", }, comment: { label: "Comentario (opcional)", }, }, actions: { close: { label: "@:global.actions.close.label", }, send: { label: "@:global.actions.send.label", }, }, }, invoice: { title: "Pagar { value }", paying: "Pagando", paid: "Pagado", fee: "Tarifa", memo: { label: "Memo", }, processing_info_text: "Procesando…", balance_too_low_warning_text: "Saldo demasiado bajo", actions: { close: { label: "@:global.actions.close.label", }, pay: { label: "Pagar", in_progress: "@:PayInvoiceDialog.invoice.processing_info_text", error: "Error", }, }, }, }, EditMintDialog: { title: "Editar mint", inputs: { nickname: { label: "Apodo", }, mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, update: { label: "@:global.actions.update.label", }, }, }, AddMintDialog: { title: "¿Confías en este mint?", description: "Antes de usar este mint, asegúrate de confiar en él. Los mints podrían volverse maliciosos o dejar de operar en cualquier momento.", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, add_mint: { label: "@:global.actions.add_mint.label", in_progress: "Añadiendo mint", }, }, }, restore: { mnemonic_error_text: "Por favor, ingresa un mnemónico", restore_mint_error_text: "Error restaurando mint: { error }", prepare_info_text: "Preparando proceso de restauración…", restored_proofs_for_keyset_info_text: "Restauradas { restoreCounter } pruebas para el keyset { keysetId }", checking_proofs_for_keyset_info_text: "Verificando pruebas { startIndex } a { endIndex } para el keyset { keysetId }", no_proofs_info_text: "No se encontraron pruebas para restaurar", restored_amount_success_text: "Restaurado { amount }", }, swap: { in_progress_warning_text: "Intercambio en progreso", invalid_swap_data_error_text: "Datos de intercambio inválidos", swap_error_text: "Error al intercambiar", }, TokenInformation: { fee: "Tarifa", unit: "Unidad", fiat: "Fiat", p2pk: "P2PK", locked: "Bloqueado", locked_to_you: "Bloqueado para ti", mint: "Casa de moneda", memo: "Memo", payment_request: "Solicitud de pago", nostr: "Nostr", token_copied: "Token copiado al portapapeles", }, }; ================================================ FILE: src/i18n/fr-FR/index.ts ================================================ export default { MultinutPicker: { payment: "Paiement Multinut", selectMints: "Sélectionnez une ou plusieurs mints pour exécuter un paiement.", totalSelectedBalance: "Solde total sélectionné", multiMintPay: "Paiement Multi-Mint", balanceNotEnough: "Le solde multi-mint n’est pas suffisant pour satisfaire cette facture", failed: "Échec du traitement: {error}", paid: "Payé {amount} via Lightning", }, global: { copy_to_clipboard: { success: "Copié dans le presse-papiers !", }, actions: { add_mint: { label: "Ajouter une mint", }, cancel: { label: "Annuler", }, copy: { label: "Copier", }, close: { label: "Fermer", }, enter: { label: "Entrer", }, lock: { label: "Verrouiller", }, paste: { label: "Coller", }, receive: { label: "Recevoir", }, scan: { label: "Scanner", }, send: { label: "Envoyer", }, swap: { label: "Échanger", }, update: { label: "Mettre à jour", }, }, inputs: { mint_url: { label: "URL de la mint", }, }, }, wallet: { notifications: { balance_too_low: "Le solde est trop bas", received: "Reçu {amount}", fee: " (frais: {fee})", could_not_request_mint: "Impossible de demander à la Mint", invoice_still_pending: "Facture toujours en attente", paid_lightning: "Payé {amount} via Lightning", payment_pending_refresh: "Paiement en attente. Rafraîchissez la facture manuellement.", sent: "Envoyé {amount}", token_still_pending: "Token toujours en attente", received_lightning: "Reçu {amount} via Lightning", lightning_payment_failed: "Paiement Lightning échoué", failed_to_decode_invoice: "Impossible de décoder la facture", invalid_lnurl: "LNURL invalide", lnurl_error: "Erreur LNURL", no_amount: "Pas de montant", no_lnurl_data: "Pas de données LNURL", no_price_data: "Pas de données de prix.", please_try_again: "Veuillez réessayer.", }, mint: { notifications: { already_added: "Mint déjà ajoutée", added: "Mint ajoutée", not_found: "Mint introuvable", activation_failed: "L'activation de la mint a échoué", no_active_mint: "Aucune mint active", unit_activation_failed: "L'activation de l'unité a échoué", unit_not_supported: "Unité non prise en charge par la mint", activated: "Mint activée", could_not_connect: "Impossible de se connecter à la mint", could_not_get_info: "Impossible d'obtenir les informations de la mint", could_not_get_keys: "Impossible d'obtenir les clés de la mint", could_not_get_keysets: "Impossible d'obtenir les keysets de la mint", mint_validation_error: "Erreur de validation de la menthe", removed: "Mint supprimée", error: "Erreur de la mint", }, }, }, MainHeader: { menu: { settings: { title: "Paramètres", settings: { title: "Paramètres", caption: "Configuration du portefeuille", }, }, terms: { title: "Conditions", terms: { title: "Conditions", caption: "Conditions de Service", }, }, links: { title: "Liens", cashuSpace: { title: "Cashu.space", caption: "cashu.space", }, github: { title: "Github", caption: "github.com/cashubtc", }, telegram: { title: "Telegram", caption: "t.me/CashuMe", }, twitter: { title: "Twitter", caption: "{'@'}CashuBTC", }, donate: { title: "Faire un don", caption: "Soutenir Cashu", }, }, }, offline: { warning: { text: "Hors ligne", }, }, reload: { warning: { text: "Recharger dans { countdown }", }, }, staging: { warning: { text: "Staging – ne pas utiliser avec de vrais fonds !", }, }, }, FullscreenHeader: { actions: { back: { label: "Portefeuille", }, }, }, Settings: { sections: { backup_restore: "SAUVEGARDE & RESTAURATION", lightning_address: "ADRESSE LIGHTNING", nostr_keys: "CLÉS NOSTR", nostr: { title: "NOSTR", relays: { expand_label: "Cliquer pour modifier les relais", add: { title: "Ajouter un relais", description: "Votre portefeuille utilise ces relais pour les opérations Nostr comme les demandes de paiement, NWC et les sauvegardes.", }, list: { title: "Relais", description: "Votre portefeuille se connectera à ces relais.", copy_tooltip: "Copier le relais", remove_tooltip: "Supprimer le relais", }, }, }, payment_requests: "DEMANDES DE PAIEMENT", nostr_wallet_connect: "NOSTR WALLET CONNECT", hardware_features: "FONCTIONNALITÉS MATÉRIELLES", p2pk_features: "FONCTIONNALITÉS P2PK", privacy: "CONFIDENTIALITÉ", experimental: "EXPÉRIMENTAL", appearance: "APPARENCE", }, language: { title: "Langue", description: "Veuillez choisir votre langue préférée dans la liste ci-dessous.", }, backup_restore: { backup_seed: { title: "Sauvegarder la phrase de départ", description: "Votre phrase de départ peut restaurer votre portefeuille. Gardez-la en sécurité et privée.", seed_phrase_label: "Phrase de départ", }, restore_ecash: { title: "Restaurer ecash", description: "L'assistant de restauration vous permet de récupérer l'ecash perdu à partir d'une phrase mnémonique. La phrase de départ de votre portefeuille actuel ne sera pas affectée, l'assistant vous permettra uniquement de restaurer l'ecash à partir d'une autre phrase de départ.", button: "Restaurer", }, }, lightning_address: { title: "Adresse Lightning", description: "Recevez des paiements sur votre adresse Lightning.", enable: { toggle: "Activer", description: "Adresse Lightning avec npub.cash", }, address: { copy_tooltip: "Copier l'adresse Lightning", }, automatic_claim: { toggle: "Réclamer automatiquement", description: "Recevez les paiements entrants automatiquement.", }, npc_v2: { choose_mint_title: "Choisissez une menthe pour npub.cash v2", choose_mint_placeholder: "Sélectionnez une menthe...", }, }, nostr_keys: { title: "Vos clés Nostr", description: "Définissez les clés nostr pour votre adresse Lightning.", wallet_seed: { title: "Phrase de départ du portefeuille", description: "Générer une paire de clés nostr à partir de la phrase de départ du portefeuille", copy_nsec: "Copier nsec", }, nsec_bunker: { title: "Nsec Bunker", description: "Utiliser un bunker NIP-46", delete_tooltip: "Supprimer la connexion", }, use_nsec: { title: "Utiliser votre nsec", description: "Cette méthode est dangereuse et non recommandée", delete_tooltip: "Supprimer nsec", }, signing_extension: { title: "Extension de signature", description: "Utiliser une extension de signature NIP-07", not_found: "Aucune extension de signature NIP-07 trouvée", }, }, payment_requests: { title: "Demandes de paiement", description: "Les demandes de paiement vous permettent de recevoir des paiements via nostr. Si vous activez cela, votre portefeuille s'abonnera à vos relais nostr.", enable_toggle: "Activer les demandes de paiement", claim_automatically: { toggle: "Réclamer automatiquement", description: "Recevez les paiements entrants automatiquement.", }, }, nostr_wallet_connect: { title: "Nostr Wallet Connect (NWC)", description: "Utilisez NWC pour contrôler votre portefeuille depuis n'importe quelle autre application.", enable_toggle: "Activer NWC", payments_note: "Vous ne pouvez utiliser NWC que pour les paiements à partir de votre solde Bitcoin. Les paiements seront effectués à partir de votre mint active.", connection: { copy_tooltip: "Copier la chaîne de connexion", qr_tooltip: "Afficher le code QR", allowance_label: "Montant restant (sat)", }, }, hardware_features: { webnfc: { title: "WebNFC", description: "Choisissez l'encodage pour l'écriture sur les cartes NFC", text: { title: "Texte", description: "Stocker le jeton en texte brut", }, weburl: { title: "URL", description: "Stocker l'URL de ce portefeuille avec le jeton", }, binary: { title: "Binaire", description: "Stocker les jetons comme données binaires", }, quick_access: { toggle: "Accès rapide à la NFC", description: "Scannez rapidement les cartes NFC dans le menu Recevoir Ecash. Cette option ajoute un bouton NFC au menu Recevoir Ecash.", }, }, }, p2pk_features: { title: "P2PK", description: "Générez une paire de clés pour recevoir de l'ecash verrouillé P2PK. Attention : Cette fonctionnalité est expérimentale. N'utilisez qu'avec de petits montants. Si vous perdez vos clés privées, personne ne pourra plus déverrouiller l'ecash qui y est verrouillé.", generate_button: "Générer une clé", import_button: "Importer nsec", quick_access: { toggle: "Accès rapide au verrouillage", description: "Utilisez ceci pour afficher rapidement votre clé de verrouillage P2PK dans le menu Recevoir Ecash.", }, keys_expansion: { label: "Cliquez pour parcourir {count} clés", used_badge: "utilisée", }, }, privacy: { title: "Confidentialité", description: "Ces paramètres affectent votre confidentialité.", check_incoming: { toggle: "Vérifier la facture entrante", description: "Si activé, le portefeuille vérifiera la dernière facture en arrière-plan. Cela augmente la réactivité du portefeuille, ce qui rend le fingerprinting plus facile. Vous pouvez vérifier manuellement les factures impayées dans l'onglet Factures.", }, check_startup: { toggle: "Vérifier les factures en attente au démarrage", description: "Si activé, le portefeuille vérifiera les factures en attente des dernières 24 heures au démarrage.", }, check_all: { toggle: "Vérifier toutes les factures", description: "Si activé, le portefeuille vérifiera périodiquement les factures impayées en arrière-plan pendant jusqu'à deux semaines. Cela augmente l'activité en ligne du portefeuille, ce qui rend le fingerprinting plus facile. Vous pouvez vérifier manuellement les factures impayées dans l'onglet Factures.", }, check_sent: { toggle: "Vérifier l'ecash envoyé", description: "Si activé, le portefeuille utilisera des vérifications périodiques en arrière-plan pour déterminer si les jetons envoyés ont été utilisés. Cela augmente l'activité en ligne du portefeuille, ce qui rend le fingerprinting plus facile.", }, websockets: { toggle: "Utiliser les WebSockets", description: "Si activé, le portefeuille utilisera des connexions WebSocket de longue durée pour recevoir des mises à jour sur les factures payées et les jetons dépensés des mints. Cela augmente la réactivité du portefeuille mais rend également le fingerprinting plus facile.", }, bitcoin_price: { toggle: "Obtenir le taux de change de Coinbase", description: "Si activé, le taux de change actuel du Bitcoin sera récupéré de coinbase.com et votre solde converti sera affiché.", currency: { title: "Devise Fiat", description: "Choisissez la devise fiat pour l'affichage du prix Bitcoin.", }, }, }, experimental: { title: "Expérimental", description: "Ces fonctionnalités sont expérimentales.", receive_swaps: { toggle: "Recevoir des échanges", badge: "Bêta", description: "Option pour échanger l'Ecash reçu vers votre mint active dans la boîte de dialogue Recevoir Ecash.", }, auto_paste: { toggle: "Coller l'Ecash automatiquement", description: "Coller automatiquement l'ecash dans votre presse-papiers lorsque vous appuyez sur Recevoir, puis Ecash, puis Coller. Le collage automatique peut causer des problèmes d'interface utilisateur dans iOS, désactivez-le si vous rencontrez des problèmes.", }, auditor: { toggle: "Activer l'auditeur", badge: "Bêta", description: "Si activé, le portefeuille affichera les informations de l'auditeur dans la boîte de dialogue des détails de la mint. L'auditeur est un service tiers qui surveille la fiabilité des mints.", url_label: "URL de l'auditeur", api_url_label: "URL de l'API de l'auditeur", }, multinut: { toggle: "Activer Multinut", description: "Si cette option est activée, le portefeuille utilisera Multinut pour payer les factures de plusieurs टकसाल à la fois.", }, nostr_mint_backup: { toggle: "Sauvegarder la liste des टकसाल sur Nostr", description: "Si cette option est activée, votre liste de टकसाल sera automatiquement sauvegardée sur les relais Nostr à l'aide de vos clés Nostr configurées. Cela vous permet de restaurer votre liste de टकसाल sur plusieurs appareils.", notifications: { enabled: "Sauvegarde de la टकसाल Nostr activée", disabled: "Sauvegarde de la टकसाल Nostr désactivée", failed: "Échec de l'activation de la sauvegarde de la टकसाल Nostr", }, }, }, multinut: { use_multinut: "Utiliser Multinut", }, appearance: { keyboard: { title: "Clavier à l'écran", description: "Utilisez le clavier numérique pour saisir les montants.", toggle: "Utiliser le clavier numérique", toggle_description: "Si activé, le clavier numérique sera utilisé pour saisir les montants.", }, theme: { title: "Apparence", description: "Changez l'apparence de votre portefeuille.", tooltips: { mono: "mono", cyber: "cyber", freedom: "liberté", nostr: "nostr", bitcoin: "bitcoin", mint: "mint", nut: "nut", blu: "blu", flamingo: "flamingo", }, }, bip177: { title: "Symbole Bitcoin", description: "Utilisez le symbole ₿ au lieu de sats.", toggle: "Utiliser le symbole ₿", }, }, web_of_trust: { title: "Réseau de confiance", known_pubkeys: "Clés publiques connues: {wotCount}", continue_crawl: "Poursuivre l'exploration", crawl_odell: "EXPLORER LE RÉSEAU DE CONFIANCE D'ODELL", crawl_wot: "Explorer le réseau de confiance", pause: "Pause", reset: "Réinitialiser", progress: "{crawlProcessed} / {crawlTotal}", }, npub_cash: { use_npubx: "Utiliser npubx.cash", copy_lightning_address: "Copier l'adresse Lightning", v2_mint: "npub.cash v2 mint", }, advanced: { title: "Avancé", developer: { title: "Paramètres développeur", description: "Les paramètres suivants sont pour le développement et le débogage.", keyset_counters: { title: "Incrémenter les compteurs de keyset", description: "Cliquez sur l'ID du keyset pour incrémenter les compteurs de chemin de dérivation pour les keysets de votre portefeuille. Ceci est utile si vous voyez l'erreur \"les sorties ont déjà été signées\".", counter: "compteur : {count}", }, new_seed: { button: "Générer une nouvelle phrase de départ", description: "Cela générera une nouvelle phrase de départ. Vous devez vous envoyer votre solde entier pour pouvoir le restaurer avec une nouvelle phrase de départ.", confirm_question: "Êtes-vous sûr de vouloir générer une nouvelle phrase de départ ?", cancel: "Annuler", confirm: "Confirmer", }, remove_spent: { button: "Supprimer les preuves dépensées", description: "Vérifiez si les jetons ecash de vos mints actives sont dépensés et supprimez les jetons dépensés de votre portefeuille. N'utilisez ceci que si votre portefeuille est bloqué.", }, debug_console: { button: "Basculer la console de débogage", description: "Ouvrez le terminal de débogage Javascript. Ne collez jamais rien dans ce terminal que vous ne comprenez pas. Un voleur pourrait essayer de vous piéger en y collant du code malveillant.", }, export_proofs: { button: "Exporter les preuves actives", description: "Copiez votre solde entier de la mint active en tant que jeton Cashu dans votre presse-papiers. Cela n'exportera que les jetons de la mint et de l'unité sélectionnées. Pour un export complet, sélectionnez une mint et une unité différentes et exportez à nouveau.", }, unset_reserved: { button: "Annuler la réservation de tous les jetons réservés", description: "Ce portefeuille marque l'ecash sortant en attente comme réservé (et le soustrait de votre solde) pour éviter les tentatives de double dépense. Ce bouton annulera la réservation de tous les jetons réservés afin qu'ils puissent être utilisés à nouveau. Si vous faites cela, votre portefeuille pourrait inclure des preuves dépensées. Appuyez sur le bouton \"Supprimer les preuves dépensées\" pour vous en débarrasser.", }, show_onboarding: { button: "Afficher l'accueil", description: "Afficher à nouveau l'écran d'accueil.", }, reset_wallet: { button: "Réinitialiser les données du portefeuille", description: "Réinitialisez les données de votre portefeuille. Attention : Cela supprimera tout ! Assurez-vous de faire une sauvegarde d'abord.", confirm_question: "Êtes-vous sûr de vouloir supprimer les données de votre portefeuille ?", cancel: "Annuler", confirm: "Supprimer le portefeuille", }, export_wallet: { button: "Exporter les données du portefeuille", description: "Téléchargez un dump de votre portefeuille. Vous pouvez restaurer votre portefeuille à partir de ce fichier sur l'écran d'accueil d'un nouveau portefeuille. Ce fichier sera désynchronisé si vous continuez à utiliser votre portefeuille après l'exportation.", }, }, }, }, NoMintWarnBanner: { title: "Rejoindre une mint", subtitle: "Vous n'avez pas encore rejoint de mint Cashu. Ajoutez une URL de mint dans les paramètres ou recevez de l'ecash d'une nouvelle mint pour commencer.", actions: { add_mint: { label: "@:global.actions.add_mint.label", }, receive: { label: "Recevoir Ecash", }, }, }, WalletPage: { actions: { send: { label: "@:global.actions.send.label", }, receive: { label: "@:global.actions.receive.label", }, }, tabs: { history: { label: "Historique", }, invoices: { label: "Factures", }, mints: { label: "Mints", }, }, install: { text: "Installer", tooltip: "Installer Cashu", }, }, AlreadyRunning: { title: "Non.", text: "Un autre onglet est déjà en cours d'exécution. Fermez cet onglet et réessayez.", actions: { retry: { label: "Réessayer", }, }, }, ErrorNotFound: { title: "404", text: "Oups. Rien ici…", actions: { home: { label: "Retour à l'accueil", }, }, }, BalanceView: { mintUrl: { label: "Mint", }, mintBalance: { label: "Solde", }, mintError: { label: "Erreur de la mint", }, pending: { label: "En attente", tooltip: "Vérifier tous les jetons en attente", }, }, WelcomePage: { actions: { previous: { label: "Précédent", }, next: { label: "Suivant", }, }, }, WelcomeSlide1: { title: "Bienvenue sur Cashu", text: "Cashu.me est un portefeuille Bitcoin gratuit et open-source qui utilise l'ecash pour garder vos fonds sécurisés et privés.", actions: { more: { label: "Cliquez pour en savoir plus", }, }, p1: { text: "Cashu est un protocole ecash gratuit et open-source pour Bitcoin. Vous pouvez en savoir plus sur { link }.", link: { text: "cashu.space", }, }, p2: { text: "Ce portefeuille n'est affilié à aucune mint. Pour utiliser ce portefeuille, vous devez vous connecter à une ou plusieurs mints Cashu auxquelles vous faites confiance.", }, p3: { text: "Ce portefeuille stocke l'ecash auquel vous seul avez accès. Si vous supprimez les données de votre navigateur sans sauvegarde de la phrase de départ, vous perdrez vos jetons.", }, p4: { text: "Ce portefeuille est en version bêta. Nous déclinons toute responsabilité en cas de perte d'accès aux fonds. Utilisez à vos propres risques ! Ce code est open-source et sous licence MIT.", }, }, WelcomeSlide2: { title: "Installer la PWA", alt: { pwa_example: "Exemple d’installation PWA" }, installing: "Installation…", instruction: { intro: { text: "Pour la meilleure expérience, utilisez ce portefeuille avec le navigateur web natif de votre appareil pour l'installer en tant que Progressive Web App. Faites-le maintenant.", }, android: { title: "Android (Chrome)", step1: { item: "1. { icon } { text }", text: "Appuyez sur le menu (en haut à droite)", }, step2: { item: "2. { icon } { text }", text: "Appuyez sur { buttonText }", buttonText: "@:AndroidPWAPrompt.buttonText", }, }, ios: { title: "iOS (Safari)", step1: { item: "1. { icon } { text }", text: "Appuyez sur partager (en bas)", }, step2: { item: "2. { icon } { text }", text: "Appuyez sur { buttonText }", buttonText: "@:iOSPWAPrompt.buttonText", }, }, outro: { text: "Une fois que vous avez installé cette application sur votre appareil, fermez cette fenêtre de navigateur et utilisez l'application depuis votre écran d'accueil.", }, }, pwa: { success: { title: "Succès !", text: "Vous utilisez Cashu comme PWA. Fermez les autres fenêtres de navigateur ouvertes et utilisez l'application depuis votre écran d'accueil.", nextSteps: "Vous pouvez maintenant fermer cet onglet et ouvrir l’application depuis votre écran d’accueil.", }, }, }, iOSPWAPrompt: { text: "Appuyez sur { icon } et { buttonText }", buttonText: "Sur l'écran d'accueil", }, AndroidPWAPrompt: { text: "Appuyez sur { icon } et { buttonText }", buttonText: "Ajouter à l'écran d'accueil", }, WelcomeSlide3: { title: "Votre Phrase de Départ", text: "Stockez votre phrase de départ dans un gestionnaire de mots de passe ou sur papier. Votre phrase de départ est le seul moyen de récupérer vos fonds si vous perdez l'accès à cet appareil.", inputs: { seed_phrase: { label: "Phrase de départ", caption: "Vous pouvez voir votre phrase de départ dans les paramètres.", }, checkbox: { label: "Je l'ai écrite", }, }, }, WelcomeSlide4: { title: "Conditions", actions: { more: { label: "Lire les Conditions de Service", }, }, inputs: { checkbox: { label: "J'ai lu et j'accepte ces termes et conditions", }, }, }, WelcomeSlideChoice: { title: "Configurez votre portefeuille", text: "Souhaitez-vous restaurer à partir d’une phrase de récupération ou créer un nouveau portefeuille ?", options: { new: { title: "Créer un nouveau portefeuille", subtitle: "Générez une nouvelle seed et ajoutez des mints.", }, recover: { title: "Restaurer le portefeuille", subtitle: "Saisissez votre phrase de récupération, restaurez les mints et l’ecash.", }, }, }, WelcomeMintSetup: { title: "Ajouter des mints", text: "Les mints sont des serveurs qui vous aident à envoyer et recevoir de l’ecash. Choisissez un mint découvert ou ajoutez-en un manuellement. Vous pouvez passer pour ajouter des mints plus tard.", sections: { your_mints: "Vos mints" }, restoring: "Restauration des mints…", placeholder: { mint_url: "https://" }, }, WelcomeRecoverSeed: { title: "Saisissez votre phrase de récupération", text: "Collez ou saisissez votre phrase de 12 mots pour restaurer.", inputs: { word: "Mot { index }" }, actions: { paste_all: "Tout coller" }, disclaimer: "Votre phrase de récupération est uniquement utilisée localement pour dériver les clés de votre portefeuille.", }, WelcomeRestoreEcash: { title: "Restaurez votre ecash", text: "Recherchez les preuves non dépensées sur vos mints configurés et ajoutez-les à votre portefeuille.", }, MintRatings: { title: "Avis sur le mint", reviews: "avis", ratings: "Notes", no_reviews: "Aucun avis trouvé", your_review: "Votre avis", no_reviews_to_display: "Aucun avis à afficher.", no_rating: "Aucune note", out_of: "sur", rows: "Reviews", sort: "Trier", sort_options: { newest: "Plus récents", oldest: "Plus anciens", highest: "Plus élevées", lowest: "Plus basses", }, actions: { write_review: "Rédiger un avis" }, empty_state_subtitle: "Aidez en laissant un avis. Partagez votre expérience avec ce mint et aidez les autres en laissant un avis.", }, CreateMintReview: { title: "Évaluer le mint", publishing_as: "Publier en tant que", inputs: { rating: { label: "Note" }, review: { label: "Avis (facultatif)" }, }, actions: { publish: { label: "Publier", in_progress: "Publication…" }, }, }, RestoreView: { seed_phrase: { label: "Restaurer à partir de la Phrase de Départ", caption: "Entrez votre phrase de départ pour restaurer votre portefeuille. Avant de restaurer, assurez-vous d'avoir ajouté toutes les mints que vous avez utilisées auparavant.", inputs: { seed_phrase: { label: "Phrase de départ", caption: "Vous pouvez voir votre phrase de départ dans les paramètres.", }, }, }, information: { label: "Information", caption: "L'assistant ne restaurera que l'ecash d'une autre phrase de départ, vous ne pourrez pas utiliser cette phrase de départ ni changer la phrase de départ du portefeuille que vous utilisez actuellement. Cela signifie que l'ecash restauré ne sera pas protégé par votre phrase de départ actuelle tant que vous ne vous enverrez pas l'ecash une fois.", }, restore_mints: { label: "Restaurer les Mints", caption: "Sélectionnez la mint à restaurer. Vous pouvez ajouter d'autres mints dans l'écran principal sous \"Mints\" et les restaurer ici.", }, actions: { paste: { error: "Échec de la lecture du contenu du presse-papiers.", }, validate: { error: "Le mnémonique doit comporter au moins 12 mots.", }, select_all: { label: "Tout sélectionner", }, deselect_all: { label: "Tout désélectionner", }, restore: { label: "Restaurer", in_progress: "Restauration de la mint…", error: "Erreur lors de la restauration de la mint : { error }", }, restore_all_mints: { label: "Restaurer toutes les mints", in_progress: "Restauration de la mint { index } sur { length }…", success: "Restauration terminée avec succès", error: "Erreur lors de la restauration des mints : { error }", }, restore_selected_mints: { label: "Restaurer les Mints sélectionnées ({count})", in_progress: "Restauration de la menthe {index} de {length}…", success: "{count} mint(s) restaurée(s) avec succès", error: "Erreur lors de la restauration des mints sélectionnées: {error}", }, }, nostr_mints: { label: "Restaurer les Mints de Nostr", caption: "Recherchez les sauvegardes de mints stockées sur les relais Nostr en utilisant votre phrase de départ. Cela vous aidera à découvrir les mints que vous avez précédemment utilisées.", search_button: "Rechercher les sauvegardes de Mint", select_all: "Tout sélectionner", deselect_all: "Tout désélectionner", backed_up: "Sauvegardé", already_added: "Déjà ajouté", add_selected: "Ajouter la sélection ({count})", no_backups_found: "Aucune sauvegarde de mint trouvée", no_backups_hint: "Assurez-vous que la sauvegarde de la liste des mints Nostr est activée dans les paramètres pour sauvegarder automatiquement votre liste de mints.", invalid_mnemonic: "Veuillez entrer une phrase de départ valide avant de rechercher.", search_error: "Échec de la recherche des sauvegardes de mints.", add_error: "Échec de l'ajout des mints sélectionnées.", }, }, MintSettings: { add: { title: "Ajouter une mint", description: "Entrez l'URL d'une mint Cashu pour vous y connecter. Ce portefeuille n'est affilié à aucune mint.", inputs: { nickname: { placeholder: "Surnom (ex. Testnet)", }, }, actions: { add_mint: { label: "@:global.actions.add_mint.label", error_invalid_url: "URL invalide", }, scan: { label: "Scanner le Code QR", }, }, }, discover: { title: "Découvrir les mints", overline: "Découvrir", caption: "Découvrez les mints que d'autres utilisateurs ont recommandées sur nostr.", actions: { discover: { label: "Découvrir les mints", in_progress: "Chargement…", error_no_mints: "Aucune mint trouvée", success: "{ length } mints trouvées", }, }, recommendations: { overline: "{ length } mints trouvées", caption: "Ces mints ont été recommandées par d'autres utilisateurs Nostr. Soyez prudent et faites vos propres recherches avant d'utiliser une mint.", actions: { browse: { label: "Cliquez pour parcourir les mints", }, }, }, }, swap: { title: "Échanger", overline: "Échanges Multi-mints", caption: "Échangez des fonds entre mints via Lightning. Note : Prévoyez de la place pour les frais Lightning potentiels. Si le paiement entrant ne réussit pas, vérifiez la facture manuellement.", inputs: { from: { label: "De", }, to: { label: "À", }, amount: { label: "Montant ({ ticker }) )", }, }, actions: { swap: { label: "@:global.actions.swap.label", in_progress: "@:MintSettings.swap.actions.swap.label", }, }, }, error_badge: "Erreur", reviews_text: "avis", no_reviews_yet: "Aucun avis pour l'instant", discover_mints_button: "Découvrir les mints", }, QrcodeReader: { progress: { text: "{ percentage }{ addon }", percentage: "{ percentage }%", keep_scanning_text: " - Continuer à scanner", }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, }, }, InvoiceDetailDialog: { title: "Recevoir Lightning", create_invoice_title: "Créer une Facture", inputs: { amount: { label: "Montant ({ ticker }) *", }, }, actions: { close: { label: "@:global.actions.close.label", }, create: { label: "Créer une Facture", label_blocked: "Création en cours…", in_progress: "Création", }, }, invoice: { caption: "Facture Lightning", status_paid_text: "Payée !", actions: { close: { label: "@:global.actions.close.label", }, copy: { label: "@:global.actions.copy.label", }, }, }, }, SendDialog: { title: "Envoyer", actions: { ecash: { label: "Ecash", error_no_mints: "Aucune mint disponible", }, lightning: { label: "Lightning", error_no_mints: "Aucune mint disponible", }, }, }, SendTokenDialog: { title: "Envoyer Ecash", title_ecash_text: "Ecash", badge_offline_text: "Hors ligne", inputs: { amount: { label: "Montant ({ ticker }) *", invalid_too_much_error_text: "Trop élevé", }, p2pk_pubkey: { label: "Clé publique du destinataire", label_invalid: "Clé publique du destinataire", }, }, actions: { close: { label: "@:global.actions.close.label", }, close_card_scanner: { label: "@:global.actions.close.label", }, copy_emoji: { label: "🥜", tooltip_text: "Copier l'Emoji", }, copy_tokens: { label: "@:global.actions.copy.label", }, copy_link: { tooltip_text: "Copier le lien", }, share: { tooltip_text: "Partager ecash", }, lock: { label: "@:global.actions.lock.label", }, paste_p2pk_pubkey: { tooltip_text: "@:global.actions.paste.label", }, send: { label: "@:global.actions.send.label", }, delete: { tooltip_text: "Supprimer de l'historique", }, write_tokens_to_card: { tooltips: { ndef_supported_text: "Flasher sur carte NFC", ndef_unsupported_text: "NDEF non pris en charge", }, }, }, }, ReceiveDialog: { title: "Recevoir", actions: { ecash: { label: "Ecash", error_no_mints: "Aucune mint disponible", }, lightning: { label: "Lightning", error_no_mints: "Vous devez vous connecter à une mint pour recevoir via Lightning", }, }, }, ReceiveEcashDrawer: { title: "Recevoir Ecash", actions: { paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, request: { label: "Demander", }, lock: { label: "@:global.actions.lock.label", }, nfc: { label: "NFC", scanning_text: "Scan en cours…", }, }, }, ReceiveTokenDialog: { title: "Recevoir Ecash", title_ecash_text: "Ecash", inputs: { tokens_base64: { label: "Coller le jeton Cashu", }, }, errors: { invalid_token: { label: "Jeton invalide", }, p2pk_lock_mismatch: { label: "Impossible de recevoir. Le verrouillage P2PK de ce jeton ne correspond pas à votre clé publique.", }, }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, scan: { label: "@:global.actions.scan.label", }, receive: { label: "@:global.actions.receive.label", label_known_mint: "@:ReceiveTokenDialog.actions.receive.label", label_adding_mint: "Ajout de la mint…", }, swap: { label: "@:global.actions.swap.label", tooltip_text: "Échanger vers une mint de confiance", caption: "Échanger { value }", }, cancel_swap: { label: "@:global.actions.cancel.label", tooltip_text: "Annuler l'échange", }, confirm_swap: { label: "@:ReceiveTokenDialog.actions.swap.label", tooltip_text: "@:ReceiveTokenDialog.actions.swap.tooltip_text", in_progress: "@:ReceiveTokenDialog.actions.confirm_swap.label", }, later: { label: "Recevoir plus tard", tooltip_text: "Ajouter à l'historique pour recevoir plus tard", already_in_history_success_text: "Ecash déjà dans l'historique", added_to_history_success_text: "Ecash ajouté à l'historique", }, nfc: { label: "NFC", tooltips: { ndef_supported_text: "Lire à partir de la carte NFC", ndef_unsupported_text: "NDEF non pris en charge", }, }, }, }, P2PKDialog: { p2pk: { caption: "Clé P2PK", description: "Recevoir de l'ecash verrouillé sur cette clé", used_warning_text: "Attention : Cette clé a déjà été utilisée. Utilisez une nouvelle clé pour une meilleure confidentialité.", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_key: { label: "Générer une nouvelle clé", }, }, }, PaymentRequestDialog: { payment_request: { caption: "Demande de Paiement", description: "Recevoir des paiements via Nostr", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_request: { label: "Nouvelle demande", }, add_amount: { label: "Ajouter un montant", }, use_active_mint: { label: "N'importe quelle mint", }, }, inputs: { amount: { placeholder: "Entrez le montant", }, }, }, NumericKeyboard: { actions: { close: { label: "@:global.actions.close.label", closed_info_text: "Clavier désactivé. Vous pouvez réactiver le clavier dans les paramètres.", }, enter: { label: "@:global.actions.enter.label", }, }, }, NWCDialog: { nwc: { caption: "Nostr Wallet Connect", description: "Contrôlez votre portefeuille à distance avec NWC. Appuyez sur le code QR pour lier votre portefeuille à une application compatible.", warning_text: "Attention : toute personne ayant accès à cette chaîne de connexion peut initier des paiements depuis votre portefeuille. Ne la partagez pas !", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, }, }, MintMotdMessage: { title: "Message de la Mint", }, MintDetailsDialog: { contact: { title: "Contact", }, details: { title: "Détails de la mint", url: { label: "URL", }, nuts: { label: "Nuts", actions: { show: { label: "Tout afficher", }, hide: { label: "Masquer", }, }, }, currency: { label: "Devise", }, currencies: { label: "@:MintDetailsDialog.details.currency.label", }, version: { label: "Version", }, }, actions: { title: "Actions", copy_mint_url: { label: "Copier l'URL de la mint", }, delete: { label: "Supprimer la mint", }, edit: { label: "Modifier la mint", }, }, }, ChooseMint: { title: "Sélectionnez une mint", badge_mint_error_text: "Erreur", badge_option_mint_error_text: "@:ChooseMint.badge_mint_error_text", }, HistoryTable: { empty_text: "Aucun historique pour l'instant", row: { type_label: "Ecash", date_label: "Il y a { value }", }, actions: { check_status: { tooltip_text: "Vérifier le statut", }, receive: { tooltip_text: "Recevoir", }, filter_pending: { label: "Filtrer en attente", }, show_all: { label: "Tout afficher", }, }, old_token_not_found_error_text: "Ancien jeton introuvable", }, InvoiceTable: { empty_text: "Aucune facture pour l'instant", row: { type_label: "Lightning", type_tooltip_text: "Cliquez pour copier", date_label: "Il y a { value }", }, actions: { check_status: { tooltip_text: "Vérifier le statut", }, filter_pending: { label: "Filtrer en attente", }, show_all: { label: "Tout afficher", }, }, }, RemoveMintDialog: { title: "Êtes-vous sûr de vouloir supprimer cette mint ?", nickname: { label: "Surnom", }, balances: { label: "Soldes", }, warning_text: "Note : Comme ce portefeuille est paranoïaque, votre ecash de cette mint ne sera pas réellement supprimé mais restera stocké sur votre appareil. Vous le verrez réapparaître si vous réajoutez cette mint plus tard.", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { confirm: { label: "Supprimer la mint", }, cancel: { label: "@:global.actions.cancel.label", }, }, }, ParseInputComponent: { placeholder: { default: "Token Cashu ou adresse Lightning", receive: "Token Cashu", pay: "Adresse Lightning ou facture", }, qr_scanner: { title: "Scanner le Code QR", description: "Appuyez pour scanner une adresse", }, paste_button: { label: "@:global.actions.paste.label", }, }, PayInvoiceDialog: { input_data: { title: "Payer Lightning", inputs: { invoice_data: { label: "Facture ou adresse Lightning", }, }, actions: { close: { label: "@:global.actions.close.label", }, enter: { label: "@:global.actions.enter.label", }, paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, }, }, lnurlpay: { amount_exact_label: "{ payee } demande { value } { ticker }", amount_range_label: "{ payee } demande{br}entre { min } et { max } { ticker }", sending_to_lightning_address: "Envoi à { address }", inputs: { amount: { label: "Montant ({ ticker }) *", }, comment: { label: "Commentaire (optionnel)", }, }, actions: { close: { label: "@:global.actions.close.label", }, send: { label: "@:global.actions.send.label", }, }, }, invoice: { title: "Payer { value }", paying: "Paiement en cours", paid: "Payé", fee: "Frais", memo: { label: "Mémo", }, processing_info_text: "Traitement…", balance_too_low_warning_text: "Solde trop faible", actions: { close: { label: "@:global.actions.close.label", }, pay: { label: "Payer", in_progress: "@:PayInvoiceDialog.invoice.processing_info_text", error: "Erreur", }, }, }, }, EditMintDialog: { title: "Modifier la mint", inputs: { nickname: { label: "Surnom", }, mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, update: { label: "@:global.actions.update.label", }, }, }, AddMintDialog: { title: "Faites-vous confiance à cette mint ?", description: "Avant d'utiliser cette mint, assurez-vous de lui faire confiance. Les mints pourraient devenir malveillantes ou cesser leurs opérations à tout moment.", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, add_mint: { label: "@:global.actions.add_mint.label", in_progress: "Ajout de la mint", }, }, }, restore: { mnemonic_error_text: "Veuillez entrer un mnémonique", restore_mint_error_text: "Erreur lors de la restauration de la mint : { error }", prepare_info_text: "Préparation du processus de restauration…", restored_proofs_for_keyset_info_text: "{ restoreCounter } preuves restaurées pour le keyset { keysetId }", checking_proofs_for_keyset_info_text: "Vérification des preuves { startIndex } à { endIndex } pour le keyset { keysetId }", no_proofs_info_text: "Aucune preuve trouvée à restaurer", restored_amount_success_text: "{ amount } restauré", }, swap: { in_progress_warning_text: "Échange en cours", invalid_swap_data_error_text: "Données d'échange invalides", swap_error_text: "Erreur lors de l'échange", }, TokenInformation: { fee: "Frais", unit: "Unité", fiat: "Fiat", p2pk: "P2PK", locked: "Verrouillé", locked_to_you: "Verrouillé pour vous", mint: "Monnaie", memo: "Mémo", payment_request: "Demande de paiement", nostr: "Nostr", token_copied: "Token copié dans le presse-papiers", }, }; ================================================ FILE: src/i18n/index.ts ================================================ import enUS from "./en-US"; import esES from "./es-ES"; import itIT from "./it-IT"; import deDE from "./de-DE"; import frFR from "./fr-FR"; import csCZ from "./cs-CZ"; import svSE from "./sv-SE"; import elGR from "./el-GR"; import trTR from "./tr-TR"; import thTH from "./th-TH"; import arSA from "./ar-SA"; import zhCN from "./zh-CN"; import jaJP from "./ja-JP"; import ptBR from "./pt-BR"; export default { "en-US": enUS, "es-ES": esES, "it-IT": itIT, "de-DE": deDE, "fr-FR": frFR, "cs-CZ": csCZ, "sv-SE": svSE, "el-GR": elGR, "tr-TR": trTR, "th-TH": thTH, "ar-SA": arSA, "zh-CN": zhCN, "ja-JP": jaJP, "pt-BR": ptBR, }; ================================================ FILE: src/i18n/it-IT/index.ts ================================================ export default { MultinutPicker: { payment: "Pagamento Multinut", selectMints: "Seleziona una o più mint da cui eseguire un pagamento.", totalSelectedBalance: "Saldo totale selezionato", multiMintPay: "Pagamento Multi-Mint", balanceNotEnough: "Il saldo multi-mint non è sufficiente per questa fattura", failed: "Impossibile elaborare: {error}", paid: "Pagato {amount} via Lightning", }, global: { // merged global keys copy_to_clipboard: { success: "Copiato negli appunti!", }, actions: { add_mint: { label: "Aggiungi mint" }, cancel: { label: "Annulla" }, copy: { label: "Copia" }, close: { label: "Chiudi" }, enter: { label: "Invio" }, lock: { label: "Blocca" }, paste: { label: "Incolla" }, receive: { label: "Ricevi" }, scan: { label: "Scansiona" }, send: { label: "Invia" }, swap: { label: "Scambia" }, update: { label: "Aggiorna" }, }, inputs: { mint_url: { label: "URL Mint" } }, }, wallet: { notifications: { balance_too_low: "Il saldo è troppo basso", received: "Ricevuto {amount}", fee: " (commissione: {fee})", could_not_request_mint: "Impossibile richiedere coniazione", invoice_still_pending: "Fattura ancora in attesa", paid_lightning: "Pagato {amount} tramite Lightning", payment_pending_refresh: "Pagamento in attesa. Aggiorna la fattura manualmente.", sent: "Inviato {amount}", token_still_pending: "Token ancora in attesa", received_lightning: "Ricevuto {amount} tramite Lightning", lightning_payment_failed: "Pagamento Lightning fallito", failed_to_decode_invoice: "Impossibile decodificare la fattura", invalid_lnurl: "LNURL non valido", lnurl_error: "Errore LNURL", no_amount: "Nessun importo", no_lnurl_data: "Nessun dato LNURL", no_price_data: "Nessun dato di prezzo.", please_try_again: "Si prega di riprovare.", }, mint: { notifications: { already_added: "Mint già aggiunto", added: "Mint aggiunto", not_found: "Mint non trovato", activation_failed: "Attivazione mint fallita", no_active_mint: "Nessun mint attivo", unit_activation_failed: "Attivazione unità fallita", unit_not_supported: "Unità non supportata dal mint", activated: "Mint attivato", could_not_connect: "Impossibile connettersi al mint", could_not_get_info: "Impossibile ottenere informazioni sul mint", could_not_get_keys: "Impossibile ottenere le chiavi del mint", could_not_get_keysets: "Impossibile ottenere i keysets del mint", mint_validation_error: "Errore di validazione del mint", removed: "Mint rimosso", error: "Errore mint", }, }, }, MainHeader: { menu: { settings: { title: "Impostazioni", settings: { title: "Impostazioni", caption: "Configurazione portafoglio", }, }, terms: { title: "Termini", terms: { title: "Termini", caption: "Termini di Servizio", }, }, links: { title: "Link", cashuSpace: { title: "Cashu.space", caption: "cashu.space", }, github: { title: "Github", caption: "github.com/cashubtc", }, telegram: { title: "Telegram", caption: "t.me/CashuMe", }, twitter: { title: "Twitter", caption: "{'@'}CashuBTC", }, donate: { title: "Dona", caption: "Supporta Cashu", }, }, }, offline: { warning: { text: "Offline", }, }, reload: { warning: { text: "Ricarica tra { countdown }", }, }, staging: { warning: { text: "Staging – non usare con fondi reali!", }, }, }, FullscreenHeader: { actions: { back: { label: "Portafoglio", }, }, }, Settings: { language: { title: "Lingua", description: "Seleziona la tua lingua preferita dall'elenco sottostante.", }, sections: { backup_restore: "BACKUP E RIPRISTINO", lightning_address: "INDIRIZZO LIGHTNING", nostr_keys: "CHIAVI NOSTR", nostr: { title: "NOSTR", relays: { expand_label: "Clicca per modificare i relay", add: { title: "Aggiungi relay", description: "Il tuo portafoglio usa questi relay per operazioni nostr come richieste di pagamento, NWC e backup.", }, list: { title: "Relay", description: "Il tuo portafoglio si connetterà a questi relay.", copy_tooltip: "Copia relay", remove_tooltip: "Rimuovi relay", }, }, }, payment_requests: "RICHIESTE DI PAGAMENTO", nostr_wallet_connect: "NOSTR WALLET CONNECT", hardware_features: "FUNZIONALITÀ HARDWARE", p2pk_features: "FUNZIONALITÀ P2PK", privacy: "PRIVACY", experimental: "SPERIMENTALE", appearance: "ASPETTO", }, backup_restore: { backup_seed: { title: "Backup frase seed", description: "La tua frase seed può ripristinare il tuo portafoglio. Conservala al sicuro e privata.", seed_phrase_label: "Frase seed", }, restore_ecash: { title: "Ripristina ecash", description: "La procedura guidata di ripristino consente di recuperare ecash perso da una frase mnemonica. La frase seed del tuo portafoglio attuale rimarrà inalterata, la procedura guidata ti consentirà solo di ripristinare ecash da un'altra frase seed.", button: "Ripristina", }, }, lightning_address: { title: "Indirizzo Lightning", description: "Ricevi pagamenti al tuo indirizzo Lightning.", enable: { toggle: "Abilita", description: "Indirizzo Lightning con npub.cash", }, address: { copy_tooltip: "Copia indirizzo Lightning", }, automatic_claim: { toggle: "Richiedi automaticamente", description: "Ricevi pagamenti in entrata automaticamente.", }, npc_v2: { choose_mint_title: "Scegli mint per npub.cash v2", choose_mint_placeholder: "Seleziona un mint...", }, }, nostr_keys: { title: "Le tue chiavi nostr", description: "Imposta le chiavi nostr per il tuo indirizzo Lightning.", wallet_seed: { title: "Frase seed del portafoglio", description: "Genera coppia di chiavi nostr dalla seed del portafoglio", copy_nsec: "Copia nsec", }, nsec_bunker: { title: "Nsec Bunker", description: "Usa un bunker NIP-46", delete_tooltip: "Elimina connessione", }, use_nsec: { title: "Usa la tua nsec", description: "Questo metodo è pericoloso e non raccomandato", delete_tooltip: "Elimina nsec", }, signing_extension: { title: "Estensione di firma", description: "Usa un'estensione di firma NIP-07", not_found: "Nessuna estensione di firma NIP-07 trovata", }, }, payment_requests: { title: "Richieste di pagamento", description: "Le richieste di pagamento ti permettono di ricevere pagamenti via nostr. Se abiliti questa opzione, il tuo portafoglio si iscriverà ai tuoi relay nostr.", enable_toggle: "Abilita Richieste di Pagamento", claim_automatically: { toggle: "Richiedi automaticamente", description: "Ricevi pagamenti in entrata automaticamente.", }, }, nostr_wallet_connect: { title: "Nostr Wallet Connect (NWC)", description: "Usa NWC per controllare il tuo portafoglio da qualsiasi altra applicazione.", enable_toggle: "Abilita NWC", payments_note: "Puoi usare NWC solo per pagamenti dal tuo saldo Bitcoin. I pagamenti verranno effettuati dal tuo mint attivo.", connection: { copy_tooltip: "Copia stringa di connessione", qr_tooltip: "Mostra codice QR", allowance_label: "Limite rimasto (sat)", }, }, hardware_features: { webnfc: { title: "WebNFC", description: "Scegli la codifica per scrivere su schede NFC", text: { title: "Testo", description: "Memorizza token in testo semplice", }, weburl: { title: "URL", description: "Memorizza URL a questo portafoglio con token", }, binary: { title: "Binario", description: "Memorizza i token come dati binari", }, quick_access: { toggle: "Accesso rapido NFC", description: "Scansiona rapidamente schede NFC nel menu Ricevi Ecash. Questa opzione aggiunge un pulsante NFC al menu Ricevi Ecash.", }, }, }, p2pk_features: { title: "P2PK", description: "Genera una coppia di chiavi per ricevere ecash bloccato con P2PK. Attenzione: Questa funzionalità è sperimentale. Usare solo con piccole somme. Se perdi le tue chiavi private, nessuno sarà più in grado di sbloccare l'ecash ad esse associato.", generate_button: "Genera chiave", import_button: "Importa nsec", quick_access: { toggle: "Accesso rapido al blocco", description: "Usa questo per mostrare rapidamente la tua chiave di blocco P2PK nel menu ricevi ecash.", }, keys_expansion: { label: "Clicca per sfogliare {count} chiavi", used_badge: "usata", }, }, privacy: { title: "Privacy", description: "Queste impostazioni influenzano la tua privacy.", check_incoming: { toggle: "Verifica fattura in entrata", description: "Se abilitato, il portafoglio verificherà l'ultima fattura in background. Questo aumenta la reattività del portafoglio, rendendo più facile il fingerprinting. Puoi verificare manualmente le fatture non pagate nella scheda Fatture.", }, check_startup: { toggle: "Verifica fatture pendenti all'avvio", description: "Se abilitato, il portafoglio verificherà le fatture pendenti delle ultime 24 ore all'avvio.", }, check_all: { toggle: "Verifica tutte le fatture", description: "Se abilitato, il portafoglio verificherà periodicamente le fatture non pagate in background per un massimo di due settimane. Questo aumenta l'attività online del portafoglio, rendendo più facile il fingerprinting. Puoi verificare manualmente le fatture non pagate nella scheda Fatture.", }, check_sent: { toggle: "Verifica ecash inviato", description: "Se abilitato, il portafoglio userà controlli periodici in background per determinare se i token inviati sono stati riscattati. Questo aumenta l'attività online del portafoglio, rendendo più facile il fingerprinting.", }, websockets: { toggle: "Usa WebSockets", description: "Se abilitato, il portafoglio userà connessioni WebSocket a lunga durata per ricevere aggiornamenti su fatture pagate e token spesi dai mints. Questo aumenta la reattività del portafoglio ma rende anche più facile il fingerprinting.", }, bitcoin_price: { toggle: "Ottieni tasso di cambio da Coinbase", description: "Se abilitato, il tasso di cambio attuale di Bitcoin verrà recuperato da coinbase.com e verrà visualizzato il tuo saldo convertito.", currency: { title: "Valuta Fiat", description: "Scegli la valuta fiat per la visualizzazione del prezzo Bitcoin.", }, }, }, experimental: { title: "Sperimentale", description: "Queste funzionalità sono sperimentali.", receive_swaps: { toggle: "Ricevi scambi", badge: "Beta", description: "Opzione per scambiare Ecash ricevuto al tuo mint attivo nella finestra di dialogo Ricevi Ecash.", }, auto_paste: { toggle: "Incolla Ecash automaticamente", description: "Incolla automaticamente ecash nei tuoi appunti quando premi Ricevi, poi Ecash, poi Incolla. L'incollaggio automatico può causare problemi all'interfaccia utente su iOS, disattivalo se riscontri problemi.", }, auditor: { toggle: "Abilita revisore", badge: "Beta", description: "Se abilitato, il portafoglio mostrerà le informazioni del revisore nella finestra di dialogo dei dettagli del mint. Il revisore è un servizio di terze parti che monitora l'affidabilità dei mints.", url_label: "URL Revisore", api_url_label: "URL API Revisore", }, multinut: { toggle: "Abilita Multinut", description: "Se abilitato, il portafoglio userà Multinut per pagare le fatture da più mint contemporaneamente.", }, nostr_mint_backup: { toggle: "Backup lista mint su Nostr", description: "Se abilitato, la tua lista mint verrà automaticamente sottoposta a backup sui relay Nostr usando le tue chiavi Nostr configurate. Questo ti permette di ripristinare la tua lista mint su tutti i dispositivi.", notifications: { enabled: "Backup mint Nostr abilitato", disabled: "Backup mint Nostr disabilitato", failed: "Impossibile abilitare il backup mint Nostr", }, }, }, appearance: { keyboard: { title: "Tastiera su schermo", description: "Usa la tastiera numerica per inserire importi.", toggle: "Usa tastiera numerica", toggle_description: "Se abilitato, verrà utilizzata la tastiera numerica per inserire gli importi.", }, theme: { title: "Aspetto", description: "Cambia l'aspetto del tuo portafoglio.", tooltips: { mono: "mono", cyber: "cyber", freedom: "freedom", nostr: "nostr", bitcoin: "bitcoin", mint: "mint", nut: "nut", blu: "blu", flamingo: "flamingo", }, }, bip177: { title: "Simbolo Bitcoin", description: "Usa il simbolo ₿ invece di sats.", toggle: "Usa il simbolo ₿", }, }, web_of_trust: { title: "Rete di fiducia", known_pubkeys: "Chiavi pubbliche conosciute: {wotCount}", continue_crawl: "Continua scansione", crawl_odell: "Scansiona WEB OF TRUST DI ODELL", crawl_wot: "Scansiona web of trust", pause: "Pausa", reset: "Reset", progress: "{crawlProcessed} / {crawlTotal}", }, npub_cash: { use_npubx: "Usa npubx.cash", copy_lightning_address: "Copia indirizzo Lightning", v2_mint: "npub.cash v2 mint", }, multinut: { use_multinut: "Usa Multinut", }, advanced: { title: "Avanzato", developer: { title: "Impostazioni sviluppatore", description: "Le seguenti impostazioni sono per sviluppo e debug.", new_seed: { button: "Genera nuova frase seed", description: "Questo genererà una nuova frase seed. Devi inviare l'intero saldo a te stesso per poterlo ripristinare con una nuova seed.", confirm_question: "Sei sicuro di voler generare una nuova frase seed?", cancel: "Annulla", confirm: "Conferma", }, remove_spent: { button: "Rimuovi prove spese", description: "Verifica se i token ecash dai tuoi mints attivi sono spesi e rimuovi quelli spesi dal tuo portafoglio. Usalo solo se il tuo portafoglio è bloccato.", }, debug_console: { button: "Attiva/disattiva Console di Debug", description: "Apri il terminale di debug Javascript. Non incollare mai nulla in questo terminale che non capisci. Un ladro potrebbe provare a ingannarti facendoti incollare codice malevolo qui.", }, export_proofs: { button: "Esporta prove attive", description: "Copia l'intero saldo dal mint attivo come token Cashu nei tuoi appunti. Questo esporterà solo i token dal mint e unità selezionati. Per un'esportazione completa, seleziona un mint e un'unità diversi ed esporta di nuovo.", }, keyset_counters: { title: "Incrementa contatori keyset", description: "Clicca l'ID del keyset per incrementare i contatori del percorso di derivazione per i keyset nel tuo portafoglio. Questo è utile se vedi l'errore \"le uscite sono già state firmate\".", counter: "contatore: {count}", }, unset_reserved: { button: "Annulla prenotazione tutti i token riservati", description: 'Questo portafoglio marca l\'ecash in uscita pendente come riservato (e lo sottrae dal tuo saldo) per prevenire tentativi di doppia spesa. Questo pulsante annullerà la prenotazione di tutti i token riservati così potranno essere usati di nuovo. Se fai questo, il tuo portafoglio potrebbe includere prove spese. Premi il pulsante "Rimuovi prove spese" per eliminarle.', }, show_onboarding: { button: "Mostra onboarding", description: "Mostra di nuovo la schermata di onboarding.", }, reset_wallet: { button: "Resetta dati portafoglio", description: "Resetta i dati del tuo portafoglio. Attenzione: Questo eliminerà tutto! Assicurati di creare prima un backup.", confirm_question: "Sei sicuro di voler eliminare i dati del tuo portafoglio?", cancel: "Annulla", confirm: "Elimina portafoglio", }, export_wallet: { button: "Esporta dati portafoglio", description: "Scarica un dump del tuo portafoglio. Puoi ripristinare il tuo portafoglio da questo file nella schermata di benvenuto di un nuovo portafoglio. Questo file non sarà sincronizzato se continui a usare il tuo portafoglio dopo averlo esportato.", }, }, }, }, NoMintWarnBanner: { title: "Unisciti a un mint", subtitle: "Non ti sei ancora unito a nessun mint Cashu. Aggiungi un URL mint nelle impostazioni o ricevi ecash da un nuovo mint per iniziare.", actions: { add_mint: { label: "@:global.actions.add_mint.label", }, receive: { label: "Ricevi Ecash", }, }, }, WalletPage: { actions: { send: { label: "@:global.actions.send.label", }, receive: { label: "@:global.actions.receive.label", }, }, tabs: { history: { label: "Cronologia", }, invoices: { label: "Fatture", }, mints: { label: "Mints", }, }, install: { text: "Installa", tooltip: "Installa Cashu", }, }, AlreadyRunning: { title: "Nope.", text: "Un'altra scheda è già in esecuzione. Chiudi questa scheda e riprova.", actions: { retry: { label: "Riprova", }, }, }, ErrorNotFound: { title: "404", text: "Oops. Niente qui…", actions: { home: { label: "Torna alla home", }, }, }, BalanceView: { mintUrl: { label: "Mint", }, mintBalance: { label: "Saldo", }, mintError: { label: "Errore mint", }, pending: { label: "In attesa", tooltip: "Verifica tutti i token in attesa", }, }, WelcomePage: { actions: { previous: { label: "Precedente", }, next: { label: "Successivo", }, }, }, WelcomeSlide1: { title: "Benvenuto in Cashu", text: "Cashu.me è un portafoglio Bitcoin gratuito e open-source che utilizza ecash per mantenere i tuoi fondi sicuri e privati.", actions: { more: { label: "Clicca per saperne di più", }, }, p1: { text: "Cashu è un protocollo ecash gratuito e open-source per Bitcoin. Puoi saperne di più su { link }.", link: { text: "cashu.space", }, }, p2: { text: "Questo portafoglio non è affiliato a nessun mint. Per usare questo portafoglio, devi connetterti a uno o più mints Cashu di cui ti fidi.", }, p3: { text: "Questo portafoglio conserva ecash a cui solo tu hai accesso. Se elimini i dati del tuo browser senza un backup della frase seed, perderai i tuoi token.", }, p4: { text: "Questo portafoglio è in beta. Non ci assumiamo alcuna responsabilità per le persone che perdono l'accesso ai fondi. Usalo a tuo rischio! Questo codice è open-source e licenziato sotto la licenza MIT.", }, }, WelcomeSlide2: { title: "Installa PWA", alt: { pwa_example: "Esempio installazione PWA" }, installing: "Installazione…", instruction: { intro: { text: "Per la migliore esperienza, usa questo portafoglio con il browser web nativo del tuo dispositivo per installarlo come App Web Progressiva. Fallo subito.", }, android: { title: "Android (Chrome)", step1: { item: "1. { icon } { text }", text: "Tocca il menu (in alto a destra)", }, step2: { item: "2. { icon } { text }", text: "Premi { buttonText }", buttonText: "@:AndroidPWAPrompt.buttonText", }, }, ios: { title: "iOS (Safari)", step1: { item: "1. { icon } { text }", text: "Tocca condividi (in basso)", }, step2: { item: "2. { icon } { text }", text: "Premi { buttonText }", buttonText: "@:iOSPWAPrompt.buttonText", }, }, outro: { text: "Una volta installata questa app sul tuo dispositivo, chiudi questa finestra del browser e usa l'app dalla tua schermata home.", }, }, pwa: { success: { title: "Successo!", text: "Stai usando Cashu come PWA. Chiudi qualsiasi altra finestra del browser aperta e usa l'app dalla tua schermata home.", nextSteps: "Ora puoi chiudere questa scheda del browser e aprire l’app dalla schermata Home.", }, }, }, iOSPWAPrompt: { text: "Tocca { icon } e { buttonText }", buttonText: "Aggiungi alla schermata Home", }, AndroidPWAPrompt: { text: "Tocca { icon } e { buttonText }", buttonText: "Aggiungi alla schermata Home", }, WelcomeSlide3: { title: "La tua Frase Seed", text: "Conserva la tua frase seed in un gestore di password o su carta. La tua frase seed è l'unico modo per recuperare i tuoi fondi se perdi l'accesso a questo dispositivo.", inputs: { seed_phrase: { label: "Frase Seed", caption: "Puoi vedere la tua frase seed nelle impostazioni.", }, checkbox: { label: "L'ho scritta", }, }, }, WelcomeSlide4: { title: "Termini", actions: { more: { label: "Leggi i Termini di Servizio", }, }, inputs: { checkbox: { label: "Ho letto e accetto questi termini e condizioni", }, }, }, WelcomeSlideChoice: { title: "Configura il tuo portafoglio", text: "Vuoi recuperare da una frase seed o creare un nuovo portafoglio?", options: { new: { title: "Crea nuovo portafoglio", subtitle: "Genera una nuova seed e aggiungi mints.", }, recover: { title: "Recupera portafoglio", subtitle: "Inserisci la tua frase seed, ripristina mints ed ecash.", }, }, }, WelcomeMintSetup: { title: "Aggiungi mints", text: "I mints sono server che ti aiutano a inviare e ricevere ecash. Scegli un mint scoperto o aggiungine uno manualmente. Puoi saltare per aggiungere mints più tardi.", sections: { your_mints: "I tuoi mints" }, restoring: "Ripristino mints…", placeholder: { mint_url: "https://" }, }, WelcomeRecoverSeed: { title: "Inserisci la tua frase seed", text: "Incolla o digita la tua frase seed di 12 parole per recuperare.", inputs: { word: "Parola { index }" }, actions: { paste_all: "Incolla tutto" }, disclaimer: "La tua frase seed è usata solo localmente per derivare le chiavi del portafoglio.", }, WelcomeRestoreEcash: { title: "Ripristina il tuo ecash", text: "Scansiona le proofs non spese sui mints configurati e aggiungile al tuo portafoglio.", }, MintRatings: { title: "Recensioni del mint", reviews: "recensioni", ratings: "Valutazioni", no_reviews: "Nessuna recensione trovata", your_review: "La tua recensione", no_reviews_to_display: "Nessuna recensione da mostrare.", no_rating: "Nessuna valutazione", out_of: "su", rows: "Reviews", sort: "Ordina", sort_options: { newest: "Più recenti", oldest: "Più vecchie", highest: "Più alte", lowest: "Più basse", }, actions: { write_review: "Scrivi una recensione" }, empty_state_subtitle: "Aiuta lasciando una recensione. Condividi la tua esperienza con questo mint e aiuta gli altri lasciando una recensione.", }, CreateMintReview: { title: "Recensisci il mint", publishing_as: "Pubblicazione come", inputs: { rating: { label: "Valutazione" }, review: { label: "Recensione (opzionale)" }, }, actions: { publish: { label: "Pubblica", in_progress: "Pubblicazione…" }, }, }, RestoreView: { seed_phrase: { label: "Ripristina da Frase Seed", caption: "Inserisci la tua frase seed per ripristinare il tuo portafoglio. Prima di ripristinare, assicurati di aver aggiunto tutti i mints che hai usato in precedenza.", inputs: { seed_phrase: { label: "Frase seed", caption: "Puoi vedere la tua frase seed nelle impostazioni.", }, }, }, information: { label: "Informazioni", caption: "L'assistente ripristinerà solo ecash da un'altra frase seed, non potrai usare questa frase seed o cambiare la frase seed del portafoglio che stai usando attualmente. Ciò significa che l'ecash ripristinato non sarà protetto dalla tua frase seed attuale finché non invii l'ecash a te stesso una volta.", }, restore_mints: { label: "Ripristina Mints", caption: 'Seleziona il mint da ripristinare. Puoi aggiungere altri mints nella schermata principale sotto "Mints" e ripristinarli qui.', }, actions: { paste: { error: "Impossibile leggere il contenuto degli appunti.", }, validate: { error: "Il mnemonico dovrebbe essere di almeno 12 parole.", }, select_all: { label: "Seleziona tutto", }, deselect_all: { label: "Deseleziona tutto", }, restore: { label: "Ripristina", in_progress: "Ripristino mint in corso…", error: "Errore nel ripristino del mint: { error }", }, restore_all_mints: { label: "Ripristina Tutti i Mints", in_progress: "Ripristino mint { index } di { length }…", success: "Ripristino completato con successo", error: "Errore nel ripristino dei mints: { error }", }, restore_selected_mints: { label: "Ripristina Mints selezionati ({count})", in_progress: "Ripristino mint { index } di { length }…", success: "{count} mint(s) ripristinati con successo", error: "Errore nel ripristino dei mints selezionati: { error }", }, }, nostr_mints: { label: "Ripristina Mints da Nostr", caption: "Cerca i backup dei mints memorizzati sui relay Nostr usando la tua frase seed. Questo ti aiuterà a scoprire i mints che hai usato in precedenza.", search_button: "Cerca backup Mint", select_all: "Seleziona tutto", deselect_all: "Deseleziona tutto", backed_up: "Backup effettuato", already_added: "Già aggiunto", add_selected: "Aggiungi selezionati ({count})", no_backups_found: "Nessun backup mint trovato", no_backups_hint: "Assicurati che il backup mint Nostr sia abilitato nelle impostazioni per eseguire automaticamente il backup della tua lista mint.", invalid_mnemonic: "Inserisci una frase seed valida prima di cercare.", search_error: "Impossibile cercare i backup dei mints.", add_error: "Impossibile aggiungere i mints selezionati.", }, }, MintSettings: { add: { title: "Aggiungi mint", description: "Inserisci l'URL di un mint Cashu per connetterti ad esso. Questo portafoglio non è affiliato a nessun mint.", inputs: { nickname: { placeholder: "Nickname (es. Testnet)", }, }, actions: { add_mint: { label: "@:global.actions.add_mint.label", error_invalid_url: "URL non valido", }, scan: { label: "Scansiona Codice QR", }, }, }, discover: { title: "Scopri mints", overline: "Scopri", caption: "Scopri mints che altri utenti hanno raccomandato su nostr.", actions: { discover: { label: "Scopri mints", in_progress: "Caricamento…", error_no_mints: "Nessun mint trovato", success: "Trovati { length } mints", }, }, recommendations: { overline: "Trovati { length } mints", caption: "Questi mints sono stati raccomandati da altri utenti Nostr. Sii cauto e fai le tue ricerche prima di usare un mint.", actions: { browse: { label: "Clicca per sfogliare i mints", }, }, }, }, swap: { title: "Scambia", overline: "Scambi Multimint", caption: "Scambia fondi tra mints via Lightning. Nota: Lascia spazio per potenziali commissioni Lightning. Se il pagamento in entrata non riesce, controlla manualmente la fattura.", inputs: { from: { label: "Da", }, to: { label: "A", }, amount: { label: "Importo ({ ticker })", }, }, actions: { swap: { label: "@:global.actions.swap.label", in_progress: "@:MintSettings.swap.actions.swap.label", }, }, }, error_badge: "Errore", reviews_text: "recensioni", no_reviews_yet: "Nessuna recensione ancora", discover_mints_button: "Scopri mints", }, QrcodeReader: { progress: { text: "{ percentage }{ addon }", percentage: "{ percentage }%", keep_scanning_text: " - Continua a scansionare", }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, }, }, InvoiceDetailDialog: { title: "Ricevi Lightning", create_invoice_title: "Crea Fattura", inputs: { amount: { label: "Importo ({ ticker }) *", }, }, actions: { close: { label: "@:global.actions.close.label", }, create: { label: "Crea Fattura", label_blocked: "Creazione fattura…", in_progress: "Creazione", }, }, invoice: { caption: "Fattura Lightning", status_paid_text: "Pagata!", actions: { close: { label: "@:global.actions.close.label", }, copy: { label: "@:global.actions.copy.label", }, }, }, }, SendDialog: { title: "Invia", actions: { ecash: { label: "Ecash", error_no_mints: "Nessun mint disponibile", }, lightning: { label: "Lightning", error_no_mints: "Nessun mint disponibile", }, }, }, SendTokenDialog: { title: "Invia Ecash", title_ecash_text: "Ecash", badge_offline_text: "Offline", inputs: { amount: { label: "Importo ({ ticker }) *", invalid_too_much_error_text: "Troppo", }, p2pk_pubkey: { label: "Chiave pubblica ricevitore", label_invalid: "Chiave pubblica ricevitore non valida", }, }, actions: { close: { label: "@:global.actions.close.label", }, close_card_scanner: { label: "@:global.actions.close.label", }, copy_emoji: { label: "🥜", tooltip_text: "Copia Emoji", }, copy_tokens: { label: "@:global.actions.copy.label", }, copy_link: { tooltip_text: "Copia link", }, share: { tooltip_text: "Condividi ecash", }, lock: { label: "@:global.actions.lock.label", }, paste_p2pk_pubkey: { tooltip_text: "@:global.actions.paste.label", }, send: { label: "@:global.actions.send.label", }, delete: { tooltip_text: "Elimina dalla cronologia", }, write_tokens_to_card: { tooltips: { ndef_supported_text: "Scrivi su scheda NFC", ndef_unsupported_text: "NDEF non supportato", }, }, }, }, ReceiveDialog: { title: "Ricevi", actions: { ecash: { label: "Ecash", error_no_mints: "Nessun mint disponibile", }, lightning: { label: "Lightning", error_no_mints: "Devi connetterti a un mint per ricevere via Lightning", }, }, }, ReceiveEcashDrawer: { title: "Ricevi Ecash", actions: { paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, request: { label: "Richiedi", }, lock: { label: "@:global.actions.lock.label", }, nfc: { label: "NFC", scanning_text: "Scansione…", }, }, }, ReceiveTokenDialog: { title: "Ricevi Ecash", title_ecash_text: "Ecash", inputs: { tokens_base64: { label: "Incolla token Cashu", }, }, errors: { invalid_token: { label: "Token non valido", }, p2pk_lock_mismatch: { label: "Impossibile ricevere. Il blocco P2PK di questo token non corrisponde alla tua chiave pubblica.", }, }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, scan: { label: "@:global.actions.scan.label", }, receive: { label: "@:global.actions.receive.label", label_known_mint: "@:ReceiveTokenDialog.actions.receive.label", label_adding_mint: "Aggiunta mint…", }, swap: { label: "@:global.actions.swap.label", tooltip_text: "Scambia verso un mint fidato", caption: "Scambia { value }", }, cancel_swap: { label: "@:global.actions.cancel.label", tooltip_text: "Annulla scambio", }, confirm_swap: { label: "@:ReceiveTokenDialog.actions.swap.label", tooltip_text: "@:ReceiveTokenDialog.actions.swap.tooltip_text", in_progress: "@:ReceiveTokenDialog.actions.confirm_swap.label", }, later: { label: "Ricevi più tardi", tooltip_text: "Aggiungi alla cronologia per ricevere dopo", already_in_history_success_text: "Ecash già nella Cronologia", added_to_history_success_text: "Ecash aggiunto alla Cronologia", }, nfc: { label: "NFC", tooltips: { ndef_supported_text: "Leggi da scheda NFC", ndef_unsupported_text: "NDEF non supportato", }, }, }, }, P2PKDialog: { p2pk: { caption: "Chiave P2PK", description: "Ricevi ecash bloccato su questa chiave", used_warning_text: "Attenzione: Questa chiave è stata usata prima. Usa una chiave nuova per maggiore privacy.", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_key: { label: "Genera nuova chiave", }, }, }, PaymentRequestDialog: { payment_request: { caption: "Richiesta di Pagamento", description: "Ricevi pagamenti via Nostr", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_request: { label: "Nuova richiesta", }, add_amount: { label: "Aggiungi importo", }, use_active_mint: { label: "Qualsiasi mint", }, }, inputs: { amount: { placeholder: "Inserisci importo", }, }, }, NumericKeyboard: { actions: { close: { label: "@:global.actions.close.label", closed_info_text: "Tastiera disabilitata. Puoi riabilitare la tastiera nelle impostazioni.", }, enter: { label: "@:global.actions.enter.label", }, }, }, NWCDialog: { nwc: { caption: "Nostr Wallet Connect", description: "Controlla il tuo portafoglio remotamente con NWC. Premi il codice QR per collegare il tuo portafoglio con un'app compatibile.", warning_text: "Attenzione: chiunque abbia accesso a questa stringa di connessione può avviare pagamenti dal tuo portafoglio. Non condividerla!", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, }, }, MintMotdMessage: { title: "Messaggio del Mint", }, MintDetailsDialog: { contact: { title: "Contatto", }, details: { title: "Dettagli mint", url: { label: "URL", }, nuts: { label: "Nuts", actions: { show: { label: "Vedi tutto", }, hide: { label: "Nascondi", }, }, }, currency: { label: "Valuta", }, currencies: { label: "@:MintDetailsDialog.details.currency.label", }, version: { label: "Versione", }, }, actions: { title: "Azioni", copy_mint_url: { label: "Copia URL mint", }, delete: { label: "Elimina mint", }, edit: { label: "Modifica mint", }, }, }, ChooseMint: { title: "Seleziona un mint", badge_mint_error_text: "Errore", badge_option_mint_error_text: "@:ChooseMint.badge_mint_error_text", }, HistoryTable: { empty_text: "Nessuna cronologia ancora", row: { type_label: "Ecash", date_label: "{ value } fa", }, actions: { check_status: { tooltip_text: "Verifica stato", }, receive: { tooltip_text: "Ricevi", }, filter_pending: { label: "Filtra pendenti", }, show_all: { label: "Mostra tutto", }, }, old_token_not_found_error_text: "Vecchio token non trovato", }, InvoiceTable: { empty_text: "Nessuna fattura ancora", row: { type_label: "Lightning", type_tooltip_text: "Clicca per copiare", date_label: "{ value } fa", }, actions: { check_status: { tooltip_text: "Verifica stato", }, filter_pending: { label: "Filtra pendenti", }, show_all: { label: "Mostra tutto", }, }, }, RemoveMintDialog: { title: "Sei sicuro di voler eliminare questo mint?", nickname: { label: "Nickname", }, balances: { label: "Saldi", }, warning_text: "Nota: Poiché questo portafoglio è paranoico, il tuo ecash da questo mint non verrà effettivamente eliminato ma rimarrà memorizzato sul tuo dispositivo. Lo vedrai riapparire se aggiungerai nuovamente questo mint in seguito.", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { confirm: { label: "Rimuovi mint", }, cancel: { label: "@:global.actions.cancel.label", }, }, }, ParseInputComponent: { placeholder: { default: "Token Cashu o indirizzo Lightning", receive: "Token Cashu", pay: "Indirizzo Lightning o fattura", }, qr_scanner: { title: "Scansiona Codice QR", description: "Tocca per scansionare un indirizzo", }, paste_button: { label: "@:global.actions.paste.label", }, }, PayInvoiceDialog: { input_data: { title: "Paga con Lightning", inputs: { invoice_data: { label: "Fattura o indirizzo Lightning", }, }, actions: { close: { label: "@:global.actions.close.label", }, enter: { label: "@:global.actions.enter.label", }, paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, }, }, lnurlpay: { amount_exact_label: "{ payee } richiede { value } { ticker }", amount_range_label: "{ payee } richiede{br}tra { min } e { max } { ticker }", sending_to_lightning_address: "Invio a { address }", inputs: { amount: { label: "Importo ({ ticker }) *", }, comment: { label: "Commento (opzionale)", }, }, actions: { close: { label: "@:global.actions.close.label", }, send: { label: "@:global.actions.send.label", }, }, }, invoice: { title: "Paga { value }", paying: "Pagamento in corso", paid: "Pagato", fee: "Commissione", memo: { label: "Memo", }, processing_info_text: "Elaborazione…", balance_too_low_warning_text: "Saldo troppo basso", actions: { close: { label: "@:global.actions.close.label", }, pay: { label: "Paga", in_progress: "@:PayInvoiceDialog.invoice.processing_info_text", error: "Errore", }, }, }, }, EditMintDialog: { title: "Modifica mint", inputs: { nickname: { label: "Nickname", }, mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, update: { label: "@:global.actions.update.label", }, }, }, AddMintDialog: { title: "Ti fidi di questo mint?", description: "Prima di usare questo mint, assicurati di fidarti. I mints potrebbero diventare malevoli o cessare l'attività in qualsiasi momento.", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, add_mint: { label: "@:global.actions.add_mint.label", in_progress: "Aggiunta mint", }, }, }, restore: { mnemonic_error_text: "Inserisci un mnemonico", restore_mint_error_text: "Errore nel ripristino del mint: { error }", prepare_info_text: "Preparazione processo di ripristino…", restored_proofs_for_keyset_info_text: "Ripristinate { restoreCounter } prove per keyset { keysetId }", checking_proofs_for_keyset_info_text: "Verifica prove da { startIndex } a { endIndex } per keyset { keysetId }", no_proofs_info_text: "Nessuna prova trovata da ripristinare", restored_amount_success_text: "Ripristinato { amount }", }, swap: { in_progress_warning_text: "Scambio in corso", invalid_swap_data_error_text: "Dati di scambio non validi", swap_error_text: "Errore durante lo scambio", }, TokenInformation: { fee: "Commissione", unit: "Unità", fiat: "Fiat", p2pk: "P2PK", locked: "Bloccato", locked_to_you: "Bloccato per te", mint: "Zecca", memo: "Memo", payment_request: "Richiesta di pagamento", nostr: "Nostr", token_copied: "Token copiato negli appunti", }, }; ================================================ FILE: src/i18n/ja-JP/index.ts ================================================ export default { MultinutPicker: { payment: "マルチナット支払い", selectMints: "支払いに使用するミントを一つ以上選択してください。", totalSelectedBalance: "選択した合計残高", multiMintPay: "マルチミント支払い", balanceNotEnough: "複数ミントの残高がこの請求書を満たすには不十分です", failed: "処理に失敗しました: {error}", paid: "Lightningで{amount}を支払いました", }, global: { copy_to_clipboard: { success: "クリップボードにコピーしました!", }, actions: { add_mint: { label: "ミントを追加", }, cancel: { label: "キャンセル", }, copy: { label: "コピー", }, close: { label: "閉じる", }, enter: { label: "入力", }, lock: { label: "ロック", }, paste: { label: "貼り付け", }, receive: { label: "受け取る", }, scan: { label: "スキャン", }, send: { label: "送る", }, swap: { label: "スワップ", }, update: { label: "更新", }, }, inputs: { mint_url: { label: "ミントURL", }, }, }, wallet: { notifications: { balance_too_low: "残高が不足しています", received: "{amount}を受け取りました", fee: " (手数料: {fee})", could_not_request_mint: "ミントをリクエストできませんでした", invoice_still_pending: "請求書はまだ処理中です", paid_lightning: "Lightningで{amount}を支払いました", payment_pending_refresh: "支払いは保留中です。請求書を手動で更新してください。", sent: "{amount}を送信しました", token_still_pending: "トークンはまだ処理中です", received_lightning: "Lightningで{amount}を受け取りました", lightning_payment_failed: "Lightning支払いに失敗しました", failed_to_decode_invoice: "請求書をデコードできませんでした", invalid_lnurl: "無効なLNURL", lnurl_error: "LNURLエラー", no_amount: "金額がありません", no_lnurl_data: "LNURLデータがありません", no_price_data: "価格データがありません。", please_try_again: "もう一度お試しください。", }, mint: { notifications: { already_added: "ミントはすでに追加されています", added: "ミントが追加されました", not_found: "ミントが見つかりません", activation_failed: "ミントの有効化に失敗しました", no_active_mint: "アクティブなミントがありません", unit_activation_failed: "単位の有効化に失敗しました", unit_not_supported: "この単位はミントでサポートされていません", activated: "ミントが有効化されました", could_not_connect: "ミントに接続できませんでした", could_not_get_info: "ミント情報を取得できませんでした", could_not_get_keys: "ミントキーを取得できませんでした", could_not_get_keysets: "ミントキーセットを取得できませんでした", mint_validation_error: "ミントの検証エラー", removed: "ミントが削除されました", error: "ミントエラー", }, }, }, MainHeader: { menu: { settings: { title: "設定", settings: { title: "設定", caption: "ウォレット構成", }, }, terms: { title: "規約", terms: { title: "規約", caption: "利用規約", }, }, links: { title: "リンク集", cashuSpace: { title: "Cashu.space", caption: "cashu.space", }, github: { title: "Github", caption: "github.com/cashubtc", }, telegram: { title: "Telegram", caption: "t.me/CashuMe", }, twitter: { title: "Twitter", caption: "{'@'}CashuBTC", }, donate: { title: "寄付する", caption: "Cashuをサポートする", }, }, }, offline: { warning: { text: "オフライン", }, }, reload: { warning: { text: "{ countdown }後に再読み込み", }, }, staging: { warning: { text: "ステージング環境 – 実際の資金では使用しないでください!", }, }, }, FullscreenHeader: { actions: { back: { label: "ウォレット", }, }, }, Settings: { language: { title: "言語", description: "以下のリストから希望の言語を選択してください。", }, sections: { backup_restore: "バックアップと復元", lightning_address: "ライトニングアドレス", nostr_keys: "ノストルキー", nostr: { title: "NOSTR", relays: { expand_label: "クリックしてリレーを編集", add: { title: "リレーを追加", description: "あなたのウォレットは、支払い要求、nostr wallet connect、バックアップなどのnostr操作にこれらのリレーを使用します。", }, list: { title: "リレー", description: "あなたのウォレットはこれらのリレーに接続します。", copy_tooltip: "リレーをコピー", remove_tooltip: "リレーを削除", }, }, }, payment_requests: "支払いリクエスト", nostr_wallet_connect: "ノストルウォレットコネクト", hardware_features: "ハードウェア機能", p2pk_features: "P2PK機能", privacy: "プライバシー", experimental: "実験的な機能", appearance: "外観", }, backup_restore: { backup_seed: { title: "シードフレーズをバックアップ", description: "シードフレーズでウォレットを復元できます。安全に保管してください。", seed_phrase_label: "シードフレーズ", }, restore_ecash: { title: "ecashを復元", description: "復元ウィザードを使用すると、ニーモニックシードフレーズから失われたecashを回復できます。現在のウォレットのシードフレーズは影響を受けず、ウィザードでは別のシードフレーズからecashを復元することのみが可能です。", button: "復元", }, }, lightning_address: { title: "ライトニングアドレス", description: "Lightningアドレスで支払いを受け取ります。", enable: { toggle: "有効にする", description: "npub.cash付きLightningアドレス", }, address: { copy_tooltip: "Lightningアドレスをコピー", }, automatic_claim: { toggle: "自動的に請求", description: "着信支払いを自動的に受け取ります。", }, npc_v2: { choose_mint_title: "npub.cash v2のミントを選択", choose_mint_placeholder: "ミントを選択...", }, }, nostr_keys: { title: "あなたのnostrキー", description: "Lightningアドレスのnostrキーを設定します。", wallet_seed: { title: "ウォレットシードフレーズ", description: "ウォレットシードからnostrキーペアを生成", copy_nsec: "nsecをコピー", }, nsec_bunker: { title: "Nsec Bunker", description: "NIP-46バンカーを使用", delete_tooltip: "接続を削除", }, use_nsec: { title: "nsecを使用", description: "この方法は危険であり推奨されません", delete_tooltip: "nsecを削除", }, signing_extension: { title: "署名拡張機能", description: "NIP-07署名拡張機能を使用", not_found: "NIP-07署名拡張機能が見つかりません", }, }, payment_requests: { title: "支払いリクエスト", description: "支払いリクエストを使用すると、nostr経由で支払いを受け取ることができます。これを有効にすると、ウォレットはあなたのnostrリレーに購読します。", enable_toggle: "支払いリクエストを有効にする", claim_automatically: { toggle: "自動的に請求", description: "着信支払いを自動的に受け取ります。", }, }, nostr_wallet_connect: { title: "Nostrウォレットコネクト (NWC)", description: "NWCを使用して、他のどのアプリケーションからでもウォレットを制御できます。", enable_toggle: "NWCを有効にする", payments_note: "NWCはBitcoin残高からの支払いにのみ使用できます。支払いはアクティブなミントから行われます。", connection: { copy_tooltip: "接続文字列をコピー", qr_tooltip: "QRコードを表示", allowance_label: "残りアローワンス (sat)", }, }, hardware_features: { webnfc: { title: "WebNFC", description: "NFCカードへの書き込みエンコーディングを選択", text: { title: "テキスト", description: "トークンをプレーンテキストで保存", }, weburl: { title: "URL", description: "トークン付きでこのウォレットへのURLを保存", }, binary: { title: "バイナリ", description: "トークンをバイナリデータとして保存", }, quick_access: { toggle: "NFCへのクイックアクセス", description: "Ecash受信メニューでNFCカードをすばやくスキャンします。このオプションは、Ecash受信メニューにNFCボタンを追加します。", }, }, }, p2pk_features: { title: "P2PK", description: "このキーにロックされたecashを受け取るためのキーペアを生成します。警告: この機能は実験的です。少額のみに使用してください。秘密鍵を紛失した場合、誰にもそれにロックされたecashのロックを解除できなくなります。", generate_button: "キーを生成", import_button: "nsecをインポート", quick_access: { toggle: "ロックへのクイックアクセス", description: "これを使用して、ecash受信メニューでP2PKロックキーをすばやく表示します。", }, keys_expansion: { label: "{count}個のキーをブラウズするにはクリック", used_badge: "使用済み", }, }, privacy: { title: "プライバシー", description: "これらの設定はプライバシーに影響します。", check_incoming: { toggle: "着信請求書をチェック", description: "有効にすると、ウォレットはバックグラウンドで最新の請求書をチェックします。これによりウォレットの応答性が向上し、フィンガープリンティングが容易になります。未払い請求書は請求書タブで手動で確認できます。", }, check_startup: { toggle: "起動時に保留中の請求書をチェック", description: "有効にすると、ウォレットは起動時に過去24時間の保留中の請求書をチェックします。", }, check_all: { toggle: "すべての請求書をチェック", description: "有効にすると、ウォレットは最大2週間、未払い請求書をバックグラウンドで定期的にチェックします。これによりウォレットのオンラインアクティビティが増加し、フィンガープリンティングが容易になります。未払い請求書は請求書タブで手動で確認できます。", }, check_sent: { toggle: "送信されたecashをチェック", description: "有効にすると、ウォレットは定期的なバックグラウンドチェックを使用して、送信されたトークンが償還されたかどうかを判断します。これによりウォレットのオンラインアクティビティが増加し、フィンガープリンティングが容易になります。", }, websockets: { toggle: "WebSocketsを使用", description: "有効にすると、ウォレットは長期間のWebSocket接続を使用して、ミントから支払われた請求書や使用済みトークンに関する更新を受け取ります。これによりウォレットの応答性は向上しますが、フィンガープリンティングも容易になります。", }, bitcoin_price: { toggle: "Coinbaseから為替レートを取得", description: "有効にすると、現在のBitcoin為替レートがcoinbase.comから取得され、換算された残高が表示されます。", currency: { title: "法定通貨", description: "Bitcoin価格表示用の法定通貨を選択してください。", }, }, }, experimental: { title: "実験的な機能", description: "これらの機能は実験的です。", receive_swaps: { toggle: "スワップを受け取る", badge: "ベータ", description: "Ecash受信ダイアログで、受信したEcashをアクティブなミントにスワップするオプション。", }, auto_paste: { toggle: "Ecashを自動的に貼り付け", description: "受信、Ecash、貼り付けを押すと、クリップボードのecashを自動的に貼り付けます。自動貼り付けはiOSでUIグリッチを引き起こす可能性があります。問題が発生した場合はオフにしてください。", }, auditor: { toggle: "監査人を有効にする", badge: "ベータ", description: "有効にすると、ウォレットはミントの詳細ダイアログに監査人情報を表示します。監査人はミントの信頼性を監視するサードパーティサービスです。", url_label: "監査人URL", api_url_label: "監査人API URL", }, multinut: { toggle: "マルチナットを有効にする", description: "有効にすると、ウォレットはマルチナットを使用して複数のミントから一度に請求書を支払います。", }, nostr_mint_backup: { toggle: "Nostrでミントリストをバックアップ", description: "有効にすると、設定されたNostrキーを使用して、ミントリストがNostrリレーに自動的にバックアップされます。これにより、デバイス間でミントリストを復元できます。", notifications: { enabled: "Nostrミントのバックアップが有効になりました", disabled: "Nostrミントのバックアップが無効になりました", failed: "Nostrミントのバックアップを有効にできませんでした", }, }, }, appearance: { keyboard: { title: "オンスクリーンキーボード", description: "金額入力に数字キーボードを使用します。", toggle: "数字キーボードを使用", toggle_description: "有効にすると、金額入力に数字キーボードが使用されます。", }, theme: { title: "外観", description: "ウォレットの外観を変更します。", tooltips: { mono: "モノラル", cyber: "サイバー", freedom: "自由", nostr: "ノストル", bitcoin: "ビットコイン", mint: "ミント", nut: "ナッツ", blu: "ブルー", flamingo: "フラミンゴ", }, }, bip177: { title: "ビットコインシンボル", description: "satsの代わりに₿シンボルを使用します。", toggle: "₿シンボルを使用", }, }, web_of_trust: { title: "信頼のウェブ", known_pubkeys: "既知の公開鍵: {wotCount}", continue_crawl: "クロールを続行", crawl_odell: "ODELLの信頼のウェブをクロール", crawl_wot: "信頼のウェブをクロール", pause: "一時停止", reset: "リセット", progress: "{crawlProcessed} / {crawlTotal}", }, npub_cash: { use_npubx: "npubx.cashを使用", copy_lightning_address: "Lightningアドレスをコピー", v2_mint: "npub.cash v2ミント", }, multinut: { use_multinut: "マルチナットを使用", }, advanced: { title: "高度な設定", developer: { title: "開発者設定", description: "以下の設定は開発およびデバッグ用です。", new_seed: { button: "新しいシードフレーズを生成", description: "これにより新しいシードフレーズが生成されます。新しいシードで復元できるように、すべての残高を自分自身に送金する必要があります。", confirm_question: "新しいシードフレーズを生成してもよろしいですか?", cancel: "キャンセル", confirm: "確認", }, remove_spent: { button: "使用済み証明書を削除", description: "アクティブなミントからのecashトークンが使用されているかチェックし、使用済みのものをウォレットから削除します。ウォレットが詰まった場合にのみ使用してください。", }, debug_console: { button: "デバッグコンソールを切り替え", description: "Javascriptデバッグターミナルを開きます。理解できないものをこのターミナルに貼り付けないでください。泥棒が悪意のあるコードを貼り付けさせようとする可能性があります。", }, export_proofs: { button: "アクティブな証明書をエクスポート", description: "アクティブなミントからの全残高をCashuトークンとしてクリップボードにコピーします。これは選択したミントと単位のトークンのみをエクスポートします。完全なエクスポートを行うには、別のミントと単位を選択して再度エクスポートしてください。", }, keyset_counters: { title: "キーセットカウンターをインクリメント", description: "キーセットIDをクリックして、ウォレット内のキーセットの導出パスカウンターをインクリメントします。「出力はすでに署名されています」というエラーが表示される場合に便利です。", counter: "カウンター: {count}", }, unset_reserved: { button: "すべての予約済みトークンを解除", description: "このウォレットは、二重支払いを防ぐために、保留中の出金ecashを予約済みとしてマークします(そして残高から差し引きます)。このボタンはすべての予約済みトークンを解除し、再び使用できるようにします。これを行うと、ウォレットに消費済みの証明書が含まれる可能性があります。「消費済み証明書を削除」ボタンを押してそれらを取り除いてください。", }, show_onboarding: { button: "オンボーディングを表示", description: "オンボーディング画面を再度表示します。", }, reset_wallet: { button: "ウォレットデータをリセット", description: "ウォレットデータをリセットします。警告: これによりすべてが削除されます!まずバックアップを作成してください。", confirm_question: "ウォレットデータを削除してもよろしいですか?", cancel: "キャンセル", confirm: "ウォレットを削除", }, export_wallet: { button: "ウォレットデータをエクスポート", description: "ウォレットのダンプをダウンロードします。新しいウォレットのウェルカム画面でこのファイルからウォレットを復元できます。このファイルは、エクスポート後にウォレットを使い続けると同期がずれます。", }, }, }, }, NoMintWarnBanner: { title: "ミントに参加する", subtitle: "まだCashuミントに参加していません。始めるには、設定でミントURLを追加するか、新しいミントからecashを受け取ります。", actions: { add_mint: { label: "@:global.actions.add_mint.label", }, receive: { label: "Ecashを受け取る", }, }, }, WalletPage: { actions: { send: { label: "@:global.actions.send.label", }, receive: { label: "@:global.actions.receive.label", }, }, tabs: { history: { label: "履歴", }, invoices: { label: "請求書", }, mints: { label: "ミント", }, }, install: { text: "インストール", tooltip: "Cashuをインストール", }, }, AlreadyRunning: { title: "ダメです。", text: "別のタブがすでに実行中です。このタブを閉じて再試行してください。", actions: { retry: { label: "再試行", }, }, }, ErrorNotFound: { title: "404", text: "おっと、何もありません…", actions: { home: { label: "ホームに戻る", }, }, }, BalanceView: { mintUrl: { label: "ミント", }, mintBalance: { label: "残高", }, mintError: { label: "ミントエラー", }, pending: { label: "保留中", tooltip: "すべての保留中のトークンを確認", }, }, WelcomePage: { actions: { previous: { label: "前へ", }, next: { label: "次へ", }, }, }, WelcomeSlide1: { title: "Cashuへようこそ", text: "Cashu.meは、ecashを使用して資金を安全かつプライベートに保つための無料のオープンソースBitcoinウォレットです。", actions: { more: { label: "もっと詳しく", }, }, p1: { text: "CashuはBitcoinのための無料のオープンソースecashプロトコルです。{ link }で詳しく知ることができます。", link: { text: "cashu.space", }, }, p2: { text: "このウォレットはどのミントにも関連付けられていません。このウォレットを使用するには、信頼する1つ以上のCashuミントに接続する必要があります。", }, p3: { text: "このウォレットは、あなただけがアクセスできるecashを保存します。シードフレーズのバックアップなしにブラウザデータを削除すると、トークンを失います。", }, p4: { text: "このウォレットはベータ版です。資金へのアクセスを失うことについて、当社は一切の責任を負いません。ご自身の責任で使用してください!このコードはオープンソースであり、MITライセンスの下でライセンスされています。", }, }, WelcomeSlide2: { title: "PWAをインストール", alt: { pwa_example: "PWA インストール例" }, installing: "インストール中…", instruction: { intro: { text: "最高の体験のため、このウォレットをデバイスのネイティブWebブラウザでProgressive Web Appとしてインストールしてください。今すぐこれを行ってください。", }, android: { title: "Android (Chrome)", step1: { item: "1. { icon } { text }", text: "メニューをタップ (右上)", }, step2: { item: "2. { icon } { text }", text: "{ buttonText }を押す", buttonText: "@:AndroidPWAPrompt.buttonText", }, }, ios: { title: "iOS (Safari)", step1: { item: "1. { icon } { text }", text: "共有をタップ (下)", }, step2: { item: "2. { icon } { text }", text: "{ buttonText }を押す", buttonText: "@:iOSPWAPrompt.buttonText", }, }, outro: { text: "このアプリをデバイスにインストールしたら、このブラウザウィンドウを閉じてホーム画面からアプリを使用してください。", }, }, pwa: { success: { title: "成功!", text: "CashuをPWAとして使用しています。他の開いているブラウザウィンドウをすべて閉じ、ホーム画面からアプリを使用してください。", nextSteps: "このブラウザタブを閉じて、ホーム画面からアプリを開けます。", }, }, }, iOSPWAPrompt: { text: "{ icon }と{ buttonText }をタップ", buttonText: "ホーム画面に追加", }, AndroidPWAPrompt: { text: "{ icon }と{ buttonText }をタップ", buttonText: "ホーム画面に追加", }, WelcomeSlide3: { title: "あなたのシードフレーズ", text: "シードフレーズをパスワードマネージャーまたは紙に保管してください。シードフレーズは、このデバイスへのアクセスを失った場合に資金を回復する唯一の方法です。", inputs: { seed_phrase: { label: "シードフレーズ", caption: "設定でシードフレーズを確認できます。", }, checkbox: { label: "書き留めました", }, }, }, WelcomeSlide4: { title: "規約", actions: { more: { label: "利用規約を読む", }, }, inputs: { checkbox: { label: "これらの規約と条件を読み、同意します", }, }, }, WelcomeSlideChoice: { title: "ウォレットをセットアップ", text: "シードフレーズから復元しますか? それとも新しいウォレットを作成しますか?", options: { new: { title: "新しいウォレットを作成", subtitle: "新しいシードを生成してミントを追加します。", }, recover: { title: "ウォレットを復元", subtitle: "シードフレーズを入力し、ミントとecashを復元します。", }, }, }, WelcomeMintSetup: { title: "ミントを追加", text: "ミントはecashの送受信を助けるサーバーです。検出されたミントを選ぶか、手動で追加できます。後で追加することもできます。", sections: { your_mints: "あなたのミント" }, restoring: "ミントを復元中…", placeholder: { mint_url: "https://" }, }, WelcomeRecoverSeed: { title: "シードフレーズを入力", text: "復元のために12語のシードフレーズを貼り付けるか入力してください。", inputs: { word: "単語 { index }" }, actions: { paste_all: "すべて貼り付け" }, disclaimer: "シードフレーズはローカルでのみ使用され、ウォレットの鍵を導出します。", }, WelcomeRestoreEcash: { title: "ecash を復元", text: "設定済みミントで未使用のproofをスキャンし、ウォレットに追加します。", }, MintRatings: { title: "ミントのレビュー", reviews: "レビュー", ratings: "評価", no_reviews: "レビューが見つかりません", your_review: "あなたのレビュー", no_reviews_to_display: "表示するレビューはありません。", no_rating: "評価なし", out_of: "のうち", rows: "Reviews", sort: "並び替え", sort_options: { newest: "新しい順", oldest: "古い順", highest: "高い順", lowest: "低い順", }, actions: { write_review: "レビューを書く" }, empty_state_subtitle: "レビューを残して助けてください。このミントでの経験を共有し、レビューを残して他の人を助けてください。", }, CreateMintReview: { title: "ミントをレビュー", publishing_as: "次として公開", inputs: { rating: { label: "評価" }, review: { label: "レビュー(任意)" }, }, actions: { publish: { label: "公開", in_progress: "公開中…" }, }, }, RestoreView: { seed_phrase: { label: "シードフレーズから復元", caption: "ウォレットを復元するには、シードフレーズを入力してください。復元する前に、以前に使用したすべてのミントを追加したことを確認してください。", inputs: { seed_phrase: { label: "シードフレーズ", caption: "設定でシードフレーズを確認できます。", }, }, }, information: { label: "情報", caption: "ウィザードは別のシードフレーズからのみecashを復元し、現在使用しているウォレットのシードフレーズを使用したり変更したりすることはできません。これは、一度ecashを自分自身に送金しない限り、復元されたecashが現在のシードフレーズによって保護されないことを意味します。", }, restore_mints: { label: "ミントを復元", caption: "復元するミントを選択します。メイン画面の「ミント」でさらにミントを追加し、ここで復元できます。", }, actions: { paste: { error: "クリップボードの内容の読み取りに失敗しました。", }, validate: { error: "ニーモニックは少なくとも12単語である必要があります。", }, select_all: { label: "すべて選択", }, deselect_all: { label: "すべて解除", }, restore: { label: "復元", in_progress: "ミントを復元中…", error: "ミントの復元エラー: { error }", }, restore_all_mints: { label: "すべてのミントを復元", in_progress: "{ length }個のミントのうち{ index }個目を復元中…", success: "復元が正常に完了しました", error: "ミントの復元エラー: { error }", }, restore_selected_mints: { label: "選択したミントを復元 ({count})", in_progress: "{length}個のミントのうち{index}個を復元しています…", success: "{count}個のミントを正常に復元しました", error: "選択したミントの復元中にエラーが発生しました: {error}", }, }, nostr_mints: { label: "Nostrからミントを復元", caption: "シードフレーズを使用してNostrリレーに保存されているミントのバックアップを検索します。これにより、以前使用したミントを発見できます。", search_button: "ミントのバックアップを検索", select_all: "すべて選択", deselect_all: "すべて選択解除", backed_up: "バックアップ済み", already_added: "既に追加済み", add_selected: "選択したものを追加 ({count})", no_backups_found: "ミントのバックアップが見つかりません", no_backups_hint: "ミントリストを自動的にバックアップするには、設定でNostrミントのバックアップが有効になっていることを確認してください。", invalid_mnemonic: "検索する前に有効なシードフレーズを入力してください。", search_error: "ミントのバックアップの検索に失敗しました。", add_error: "選択したミントの追加に失敗しました。", }, }, MintSettings: { add: { title: "ミントを追加", description: "接続するCashuミントのURLを入力します。このウォレットはどのミントにも関連付けられていません。", inputs: { nickname: { placeholder: "ニックネーム(例:Testnet)", }, }, actions: { add_mint: { label: "@:global.actions.add_mint.label", error_invalid_url: "無効なURL", }, scan: { label: "QRコードをスキャン", }, }, }, discover: { title: "ミントを発見", overline: "発見", caption: "他のユーザーがnostrで推奨したミントを発見します。", actions: { discover: { label: "ミントを発見", in_progress: "読み込み中…", error_no_mints: "ミントが見つかりませんでした", success: "{ length }個のミントが見つかりました", }, }, recommendations: { overline: "{ length }個のミントが見つかりました", caption: "これらのミントは他のNostrユーザーによって推奨されました。ミントを使用する前に注意し、ご自身の調査を行ってください。", actions: { browse: { label: "ミントをブラウズするにはクリック", }, }, }, }, swap: { title: "スワップ", overline: "マルチミントスワップ", caption: "Lightning経由でミント間で資金をスワップします。注意:Lightningの手数料の可能性に備えて余裕を持たせてください。着信支払いが成功しない場合は、手動で請求書を確認してください。", inputs: { from: { label: "から", }, to: { label: "へ", }, amount: { label: "金額 ({ ticker })", }, }, actions: { swap: { label: "@:global.actions.swap.label", in_progress: "@:MintSettings.swap.actions.swap.label", }, }, }, error_badge: "エラー", reviews_text: "レビュー", no_reviews_yet: "まだレビューはありません", discover_mints_button: "ミントを発見", }, QrcodeReader: { progress: { text: "{ percentage }{ addon }", percentage: "{ percentage }%", keep_scanning_text: " - スキャンを続行", }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, }, }, InvoiceDetailDialog: { title: "Lightningを受け取る", create_invoice_title: "請求書の作成", inputs: { amount: { label: "金額 ({ ticker }) *", }, }, actions: { close: { label: "@:global.actions.close.label", }, create: { label: "請求書を作成", label_blocked: "請求書作成中…", in_progress: "作成中", }, }, invoice: { caption: "Lightning請求書", status_paid_text: "支払い済み!", actions: { close: { label: "@:global.actions.close.label", }, copy: { label: "@:global.actions.copy.label", }, }, }, }, SendDialog: { title: "送る", actions: { ecash: { label: "Ecash", error_no_mints: "利用可能なミントがありません", }, lightning: { label: "Lightning", error_no_mints: "利用可能なミントがありません", }, }, }, SendTokenDialog: { title: "Ecashを送る", title_ecash_text: "Ecash", badge_offline_text: "オフライン", inputs: { amount: { label: "金額 ({ ticker }) *", invalid_too_much_error_text: "多すぎます", }, p2pk_pubkey: { label: "受信者の公開鍵", label_invalid: "受信者の公開鍵", }, }, actions: { close: { label: "@:global.actions.close.label", }, close_card_scanner: { label: "@:global.actions.close.label", }, copy_emoji: { label: "🥜", tooltip_text: "絵文字をコピー", }, copy_tokens: { label: "@:global.actions.copy.label", }, copy_link: { tooltip_text: "リンクをコピー", }, share: { tooltip_text: "ecashトークンを共有", }, lock: { label: "@:global.actions.lock.label", }, paste_p2pk_pubkey: { tooltip_text: "@:global.actions.paste.label", }, send: { label: "@:global.actions.send.label", }, delete: { tooltip_text: "履歴から削除", }, write_tokens_to_card: { tooltips: { ndef_supported_text: "NFCカードに書き込み", ndef_unsupported_text: "NDEF非対応", }, }, }, }, ReceiveDialog: { title: "受け取る", actions: { ecash: { label: "Ecash", error_no_mints: "利用可能なミントがありません", }, lightning: { label: "Lightning", error_no_mints: "Lightning経由で受け取るにはミントに接続する必要があります", }, }, }, ReceiveEcashDrawer: { title: "Ecashを受け取る", actions: { paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, request: { label: "リクエスト", }, lock: { label: "@:global.actions.lock.label", }, nfc: { label: "NFC", scanning_text: "スキャン中…", }, }, }, ReceiveTokenDialog: { title: "Ecashを受け取る", title_ecash_text: "Ecash", inputs: { tokens_base64: { label: "Cashuトークンを貼り付け", }, }, errors: { invalid_token: { label: "無効なトークン", }, p2pk_lock_mismatch: { label: "受信できません。このトークンのP2PKロックがあなたの公開鍵と一致しません。", }, }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, scan: { label: "@:global.actions.scan.label", }, receive: { label: "@:global.actions.receive.label", label_known_mint: "@:ReceiveTokenDialog.actions.receive.label", label_adding_mint: "ミント追加中…", }, swap: { label: "@:global.actions.swap.label", tooltip_text: "信頼できるミントにスワップ", caption: "{ value }をスワップ", }, cancel_swap: { label: "@:global.actions.cancel.label", tooltip_text: "スワップをキャンセル", }, confirm_swap: { label: "@:ReceiveTokenDialog.actions.swap.label", tooltip_text: "@:ReceiveTokenDialog.actions.swap.tooltip_text", in_progress: "@:ReceiveTokenDialog.actions.confirm_swap.label", }, later: { label: "後で受け取る", tooltip_text: "後で受け取るために履歴に追加", already_in_history_success_text: "Ecashはすでに履歴にあります", added_to_history_success_text: "Ecashを履歴に追加しました", }, nfc: { label: "NFC", tooltips: { ndef_supported_text: "NFCカードから読み取り", ndef_unsupported_text: "NDEF非対応", }, }, }, }, P2PKDialog: { p2pk: { caption: "P2PKキー", description: "このキーにロックされたecashを受け取る", used_warning_text: "警告: このキーは以前に使用されています。プライバシー向上のため新しいキーを使用してください。", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_key: { label: "新しいキーを生成", }, }, }, PaymentRequestDialog: { payment_request: { caption: "支払いリクエスト", description: "Nostr経由で支払いを受け取る", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_request: { label: "新しいリクエスト", }, add_amount: { label: "金額を追加", }, use_active_mint: { label: "任意のミント", }, }, inputs: { amount: { placeholder: "金額を入力", }, }, }, NumericKeyboard: { actions: { close: { label: "@:global.actions.close.label", closed_info_text: "キーボードが無効になりました。設定でキーボードを再度有効にできます。", }, enter: { label: "@:global.actions.enter.label", }, }, }, NWCDialog: { nwc: { caption: "Nostrウォレットコネクト", description: "NWCを使用してリモートでウォレットを制御します。互換性のあるアプリでウォレットをリンクするには、QRコードを押してください。", warning_text: "警告: この接続文字列にアクセスできる人は誰でもウォレットから支払いを開始できます。共有しないでください!", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, }, }, MintMotdMessage: { title: "ミントメッセージ", }, MintDetailsDialog: { contact: { title: "連絡先", }, details: { title: "ミント詳細", url: { label: "URL", }, nuts: { label: "Nuts", actions: { show: { label: "すべて表示", }, hide: { label: "隠す", }, }, }, currency: { label: "通貨", }, currencies: { label: "@:MintDetailsDialog.details.currency.label", }, version: { label: "バージョン", }, }, actions: { title: "アクション", copy_mint_url: { label: "ミントURLをコピー", }, delete: { label: "ミントを削除", }, edit: { label: "ミントを編集", }, }, }, ChooseMint: { title: "ミントを選択", badge_mint_error_text: "エラー", badge_option_mint_error_text: "@:ChooseMint.badge_mint_error_text", }, HistoryTable: { empty_text: "履歴はまだありません", row: { type_label: "Ecash", date_label: "{ value }前", }, actions: { check_status: { tooltip_text: "ステータスを確認", }, receive: { tooltip_text: "受け取る", }, filter_pending: { label: "保留中をフィルタリング", }, show_all: { label: "すべて表示", }, }, old_token_not_found_error_text: "古いトークンが見つかりません", }, InvoiceTable: { empty_text: "請求書はまだありません", row: { type_label: "Lightning", type_tooltip_text: "クリックしてコピー", date_label: "{ value }前", }, actions: { check_status: { tooltip_text: "ステータスを確認", }, filter_pending: { label: "保留中をフィルタリング", }, show_all: { label: "すべて表示", }, }, }, RemoveMintDialog: { title: "このミントを削除してもよろしいですか?", nickname: { label: "ニックネーム", }, balances: { label: "残高", }, warning_text: "注: このウォレットは偏執的であるため、このミントからのecashは実際には削除されず、デバイスに保存されたままになります。後でこのミントを再度追加すると、再び表示されます。", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { confirm: { label: "ミントを削除", }, cancel: { label: "@:global.actions.cancel.label", }, }, }, ParseInputComponent: { placeholder: { default: "CashuトークンまたはLightningアドレス", receive: "Cashuトークン", pay: "Lightningアドレスまたは請求書", }, qr_scanner: { title: "QRコードをスキャン", description: "タップしてアドレスをスキャン", }, paste_button: { label: "@:global.actions.paste.label", }, }, PayInvoiceDialog: { input_data: { title: "Lightningで支払う", inputs: { invoice_data: { label: "Lightning請求書またはアドレス", }, }, actions: { close: { label: "@:global.actions.close.label", }, enter: { label: "@:global.actions.enter.label", }, paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, }, }, lnurlpay: { amount_exact_label: "{ payee }が{ value } { ticker }をリクエストしています", amount_range_label: "{ payee }が{ min }から{ max } { ticker }の間をリクエストしています", sending_to_lightning_address: "{ address }に送信中", inputs: { amount: { label: "金額 ({ ticker }) *", }, comment: { label: "コメント (任意)", }, }, actions: { close: { label: "@:global.actions.close.label", }, send: { label: "@:global.actions.send.label", }, }, }, invoice: { title: "{ value }を支払う", paying: "支払い中", paid: "支払い済み", fee: "手数料", memo: { label: "メモ", }, processing_info_text: "処理中…", balance_too_low_warning_text: "残高不足", actions: { close: { label: "@:global.actions.close.label", }, pay: { label: "支払う", in_progress: "@:PayInvoiceDialog.invoice.processing_info_text", error: "エラー", }, }, }, }, EditMintDialog: { title: "ミントを編集", inputs: { nickname: { label: "ニックネーム", }, mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, update: { label: "@:global.actions.update.label", }, }, }, AddMintDialog: { title: "このミントを信頼しますか?", description: "このミントを使用する前に、信頼できることを確認してください。ミントはいつでも悪意のあるものになるか、運営を停止する可能性があります。", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, add_mint: { label: "@:global.actions.add_mint.label", in_progress: "ミントを追加中", }, }, }, restore: { mnemonic_error_text: "ニーモニックを入力してください", restore_mint_error_text: "ミントの復元エラー: { error }", prepare_info_text: "復元処理を準備中…", restored_proofs_for_keyset_info_text: "{ keysetId }キーセットの{ restoreCounter }個の証明書を復元しました", checking_proofs_for_keyset_info_text: "{ keysetId }キーセットの{ startIndex }から{ endIndex }までの証明書を確認中", no_proofs_info_text: "復元する証明書が見つかりませんでした", restored_amount_success_text: "{ amount }復元しました", }, swap: { in_progress_warning_text: "スワップ進行中", invalid_swap_data_error_text: "無効なスワップデータ", swap_error_text: "スワップエラー", }, TokenInformation: { fee: "手数料", unit: "単位", fiat: "フィアット", p2pk: "P2PK", locked: "ロック済み", locked_to_you: "あなたにロック済み", mint: "ミント", memo: "メモ", payment_request: "支払いリクエスト", nostr: "Nostr", token_copied: "トークンをクリップボードにコピーしました", }, }; ================================================ FILE: src/i18n/pt-BR/index.ts ================================================ export default { global: { copy_to_clipboard: { success: "Copiado para a área de transferência!", }, actions: { add_mint: { label: "Adicionar mint", }, cancel: { label: "Cancelar", }, copy: { label: "Copiar", }, close: { label: "Fechar", }, enter: { label: "Confirmar", }, lock: { label: "Bloquear", }, paste: { label: "Colar", }, receive: { label: "Receber", }, scan: { label: "Escanear", }, send: { label: "Enviar", }, pay: { label: "Pagar", }, swap: { label: "Swap", }, update: { label: "Atualizar", }, }, inputs: { mint_url: { label: "URL do mint", }, }, }, common: { fee: "Taxa", }, MultinutPicker: { payment: "Pagamento multi-mint", selectMints: "Selecione um ou mais mints para realizar um pagamento.", totalSelectedBalance: "Saldo total selecionado", multiMintPay: "Pagamento multi-mint", balanceNotEnough: "Saldo multi-mint insuficiente para cobrir esta fatura", failed: "Falha ao processar: {error}", paid: "Pago {amount} via Lightning", }, wallet: { notifications: { balance_too_low: "Saldo insuficiente", received: "Recebido {amount}", fee: " (taxa: {fee})", could_not_request_mint: "Não foi possível solicitar ao mint", invoice_still_pending: "Fatura ainda pendente", paid_lightning: "Pago {amount} via Lightning", payment_pending_refresh: "Pagamento pendente. Atualize a fatura manualmente.", sent: "Enviado {amount}", token_still_pending: "Token ainda pendente", received_lightning: "Recebido {amount} via Lightning", lightning_payment_failed: "Pagamento Lightning falhou", failed_to_decode_invoice: "Falha ao decodificar fatura", unsupported_legacy_qr: "QR code legado não suportado", legacy_qr_not_supported: "Este QR code legado não é de um comerciante suportado", invalid_lnurl: "LNURL inválido", lnurl_error: "Erro de LNURL", no_amount: "Sem valor", no_lnurl_data: "Sem dados LNURL", no_price_data: "Sem dados de preço.", please_try_again: "Por favor, tente novamente.", }, mint: { notifications: { already_added: "Mint já adicionado", added: "Mint adicionado", not_found: "Mint não encontrado", activation_failed: "Falha ao ativar mint", no_active_mint: "Nenhum mint ativo", unit_activation_failed: "Falha ao ativar unidade", unit_not_supported: "Unidade não suportada pelo mint", activated: "Mint ativado", could_not_connect: "Não foi possível conectar ao mint", could_not_get_info: "Não foi possível obter informações do mint", could_not_get_keys: "Não foi possível obter chaves do mint", could_not_get_keysets: "Não foi possível obter keysets do mint", mint_validation_error: "Erro de validação do mint", removed: "Mint removido", error: "Erro no mint", }, }, }, MainHeader: { menu: { settings: { title: "Configurações", settings: { title: "Configurações", caption: "Configuração da carteira", }, }, terms: { title: "Termos", terms: { title: "Termos", caption: "Termos de Serviço", }, }, links: { title: "Links", cashuSpace: { title: "Cashu.space", caption: "cashu.space", }, github: { title: "Github", caption: "github.com/cashubtc", }, telegram: { title: "Telegram", caption: "t.me/CashuMe", }, twitter: { title: "Twitter", caption: "{'@'}CashuBTC", }, donate: { title: "Doar", caption: "Apoie o Cashu", }, }, }, offline: { warning: { text: "Offline", }, }, reload: { warning: { text: "Recarregando em { countdown }", }, }, staging: { warning: { text: "Staging – não use com fundos reais!", }, }, }, FullscreenHeader: { actions: { back: { label: "Carteira", }, }, }, Settings: { language: { title: "Idioma", description: "Por favor, escolha o idioma de sua preferência na lista abaixo.", }, sections: { backup_restore: "BACKUP E RESTAURAÇÃO", lightning_address: "ENDEREÇO LIGHTNING", nostr_keys: "CHAVES NOSTR", nostr: { title: "NOSTR", relays: { expand_label: "Clique para editar os relays", add: { title: "Adicionar relay", description: "Sua carteira usa esses relays para operações Nostr como solicitações de pagamento, Nostr Wallet Connect e backups.", }, list: { title: "Relays", description: "Sua carteira se conectará a esses relays.", copy_tooltip: "Copiar relay", remove_tooltip: "Remover relay", }, }, }, payment_requests: "SOLICITAÇÕES DE PAGAMENTO", nostr_wallet_connect: "NOSTR WALLET CONNECT", hardware_features: "RECURSOS DE HARDWARE", p2pk_features: "RECURSOS P2PK", privacy: "PRIVACIDADE", experimental: "EXPERIMENTAL", appearance: "APARÊNCIA", }, backup_restore: { backup_seed: { title: "Fazer backup da frase de recuperação", description: "Sua frase de recuperação pode restaurar sua carteira. Mantenha-a segura e privada.", seed_phrase_label: "Frase de recuperação", }, restore_ecash: { title: "Restaurar ecash", description: "O assistente de restauração permite recuperar ecash perdido a partir de uma frase de recuperação mnemônica. A frase de recuperação da sua carteira atual permanecerá inalterada; o assistente só permitirá restaurar ecash de outra frase de recuperação.", button: "Restaurar", }, }, lightning_address: { title: "Endereço Lightning", description: "Receba pagamentos no seu endereço Lightning.", enable: { toggle: "Ativar", description: "Endereço Lightning com npub.cash", }, address: { copy_tooltip: "Copiar endereço Lightning", }, automatic_claim: { toggle: "Reivindicar automaticamente", description: "Receba pagamentos recebidos automaticamente.", }, npc_v2: { choose_mint_title: "Escolha o mint para npub.cash v2", choose_mint_placeholder: "Selecione um mint...", }, }, nostr_keys: { title: "Suas chaves Nostr", description: "Suas chaves Nostr serão usadas para determinar seu endereço Lightning.", wallet_seed: { title: "Frase de recuperação da carteira", description: "Gerar par de chaves Nostr a partir da frase de recuperação", copy_nsec: "Copiar nsec", }, nsec_bunker: { title: "Nsec Bunker", description: "Usar um bunker NIP-46", delete_tooltip: "Excluir conexão", }, use_nsec: { title: "Usar seu nsec", description: "Este método é perigoso e não recomendado", delete_tooltip: "Excluir nsec", }, signing_extension: { title: "Extensão de assinatura", description: "Usar uma extensão de assinatura NIP-07", not_found: "Nenhuma extensão de assinatura NIP-07 encontrada", }, }, payment_requests: { title: "Solicitações de pagamento", description: "As solicitações de pagamento permitem receber pagamentos via Nostr. Se ativado, sua carteira se inscreverá nos seus relays Nostr.", enable_toggle: "Ativar Solicitações de Pagamento", claim_automatically: { toggle: "Reivindicar automaticamente", description: "Receba pagamentos recebidos automaticamente.", }, }, nostr_wallet_connect: { title: "Nostr Wallet Connect (NWC)", description: "Use NWC para controlar sua carteira a partir de qualquer outro aplicativo.", enable_toggle: "Ativar NWC", payments_note: "Você só pode usar NWC para pagamentos do seu saldo em Bitcoin. Os pagamentos serão feitos a partir do seu mint ativo.", connection: { copy_tooltip: "Copiar string de conexão", qr_tooltip: "Mostrar QR code", allowance_label: "Limite restante (sat)", }, }, hardware_features: { webnfc: { title: "WebNFC", description: "Escolha a codificação para gravar em cartões NFC", text: { title: "Texto", description: "Armazenar token em texto simples", }, weburl: { title: "URL", description: "Armazenar URL desta carteira com o token", }, binary: { title: "Binário", description: "Armazenar tokens como dados binários", }, quick_access: { toggle: "Acesso rápido ao NFC", description: "Escaneie rapidamente cartões NFC no menu Receber Ecash. Esta opção adiciona um botão NFC ao menu Receber Ecash.", }, }, }, p2pk_features: { title: "P2PK", description: "Gere um par de chaves para receber ecash bloqueado por P2PK. Aviso: Este recurso é experimental. Use apenas com pequenas quantias. Se perder suas chaves privadas, ninguém poderá desbloquear o ecash vinculado a elas.", generate_button: "Gerar chave", import_button: "Importar nsec", quick_access: { toggle: "Acesso rápido ao bloqueio", description: "Use isto para mostrar rapidamente sua chave de bloqueio P2PK no menu de recebimento de ecash.", }, keys_expansion: { label: "Clique para ver {count} chaves", used_badge: "usada", }, }, privacy: { title: "Privacidade", description: "Estas configurações afetam sua privacidade.", check_incoming: { toggle: "Verificar fatura recebida", description: "Se ativado, a carteira verificará a última fatura em segundo plano. Isso aumenta a responsividade da carteira, facilitando a identificação. Você pode verificar manualmente as faturas não pagas na aba Faturas.", }, check_startup: { toggle: "Verificar faturas pendentes na inicialização", description: "Se ativado, a carteira verificará faturas pendentes das últimas 24 horas na inicialização.", }, check_all: { toggle: "Verificar todas as faturas", description: "Se ativado, a carteira verificará periodicamente em segundo plano faturas não pagas por até duas semanas. Isso aumenta a atividade online da carteira, facilitando a identificação. Você pode verificar manualmente as faturas não pagas na aba Faturas.", }, check_sent: { toggle: "Verificar ecash enviado", description: "Se ativado, a carteira usará verificações periódicas em segundo plano para determinar se os tokens enviados foram resgatados. Isso aumenta a atividade online da carteira, facilitando a identificação.", }, websockets: { toggle: "Usar WebSockets", description: "Se ativado, a carteira usará conexões WebSocket de longa duração para receber atualizações sobre faturas pagas e tokens gastos dos mints. Isso aumenta a responsividade da carteira, mas também facilita a identificação.", }, bitcoin_price: { toggle: "Obter taxa de câmbio da Coinbase", description: "Se ativado, a taxa de câmbio atual do Bitcoin será obtida de coinbase.com e seu saldo convertido será exibido.", currency: { title: "Moeda Fiduciária", description: "Escolha a moeda fiduciária para exibição do preço do Bitcoin.", }, }, }, experimental: { title: "Experimental", description: "Estes recursos são experimentais.", receive_swaps: { toggle: "Receber swaps", badge: "Beta", description: "Opção para fazer swap do ecash recebido para o seu mint ativo no diálogo Receber Ecash.", }, auto_paste: { toggle: "Colar ecash automaticamente", description: "Colar automaticamente o ecash da sua área de transferência ao pressionar Receber, depois Ecash, depois Colar. A colagem automática pode causar falhas na interface no iOS; desative se tiver problemas.", }, auditor: { toggle: "Ativar auditor", badge: "Beta", description: "Se ativado, a carteira exibirá informações do auditor no diálogo de detalhes do mint. O auditor é um serviço de terceiros que monitora a confiabilidade dos mints.", url_label: "URL do auditor", api_url_label: "URL da API do auditor", }, multinut: { toggle: "Ativar Multinut", description: "Se ativado, a carteira usará o Multinut para pagar faturas de múltiplos mints simultaneamente.", }, nostr_mint_backup: { toggle: "Fazer backup da lista de mints no Nostr", description: "Se ativado, sua lista de mints será automaticamente salva nos relays Nostr usando suas chaves Nostr configuradas. Isso permite restaurar sua lista de mints em outros dispositivos.", notifications: { enabled: "Backup de mints no Nostr ativado", disabled: "Backup de mints no Nostr desativado", failed: "Falha ao ativar backup de mints no Nostr", }, }, }, appearance: { keyboard: { title: "Teclado na tela", description: "Use o teclado numérico para inserir valores.", toggle: "Usar teclado numérico", toggle_description: "Se ativado, o teclado numérico será usado para inserir valores.", }, theme: { title: "Aparência", description: "Altere a aparência da sua carteira.", tooltips: { mono: "mono", cyber: "cyber", freedom: "freedom", nostr: "nostr", bitcoin: "bitcoin", mint: "mint", nut: "nut", blu: "blu", flamingo: "flamingo", }, }, bip177: { title: "Símbolo do Bitcoin", description: "Usar o símbolo ₿ em vez de sats.", toggle: "Usar símbolo ₿", }, }, web_of_trust: { title: "Rede de confiança", known_pubkeys: "Chaves públicas conhecidas: {wotCount}", continue_crawl: "Continuar varredura", crawl_odell: "Varrer a TEIA DE CONFIANÇA DO ODELL", crawl_wot: "Varrer teia de confiança", pause: "Pausar", reset: "Redefinir", progress: "{crawlProcessed} / {crawlTotal}", }, npub_cash: { use_npubx: "Usar npubx.cash", copy_lightning_address: "Copiar endereço Lightning", v2_mint: "Mint npub.cash v2", }, multinut: { use_multinut: "Usar Multinut", }, advanced: { title: "Avançado", developer: { title: "Configurações de desenvolvedor", description: "As configurações a seguir são para desenvolvimento e depuração.", new_seed: { button: "Gerar nova frase de recuperação", description: "Isso gerará uma nova frase de recuperação. Você deve enviar todo o seu saldo para si mesmo para poder restaurá-lo com uma nova frase de recuperação.", confirm_question: "Tem certeza de que deseja gerar uma nova frase de recuperação?", cancel: "Cancelar", confirm: "Confirmar", }, remove_spent: { button: "Remover provas gastas", description: "Verifique se os tokens ecash dos seus mints ativos foram gastos e remova os gastos da sua carteira. Use isso somente se sua carteira estiver travada.", }, debug_console: { button: "Alternar Console de Depuração", description: "Abra o terminal de depuração JavaScript. Nunca cole nada neste terminal que você não entenda. Um ladrão pode tentar induzi-lo a colar código malicioso aqui.", }, export_proofs: { button: "Exportar provas ativas", description: "Copie seu saldo completo do mint ativo como um token Cashu para sua área de transferência. Isso exportará apenas os tokens do mint e unidade selecionados. Para uma exportação completa, selecione um mint e unidade diferentes e exporte novamente.", }, keyset_counters: { title: "Incrementar contadores de keyset", description: 'Clique no ID do keyset para incrementar os contadores de caminho de derivação dos keysets na sua carteira. Isso é útil se você ver o erro "outputs have already been signed".', counter: "contador: {count}", }, unset_reserved: { button: "Desmarcar todos os tokens reservados", description: 'Esta carteira marca o ecash sainte pendente como reservado (e o subtrai do seu saldo) para evitar tentativas de gasto duplo. Este botão desmarcará todos os tokens reservados para que possam ser usados novamente. Se fizer isso, sua carteira pode incluir provas gastas. Pressione o botão "Remover provas gastas" para eliminá-las.', }, show_onboarding: { button: "Mostrar integração", description: "Mostrar a tela de integração novamente.", }, reset_wallet: { button: "Redefinir dados da carteira", description: "Redefina os dados da sua carteira. Aviso: Isso apagará tudo! Certifique-se de criar um backup primeiro.", confirm_question: "Tem certeza de que deseja excluir os dados da sua carteira?", cancel: "Cancelar", confirm: "Excluir carteira", }, export_wallet: { button: "Exportar dados da carteira", description: "Baixe um dump da sua carteira. Você pode restaurar sua carteira a partir deste arquivo na tela de boas-vindas de uma nova carteira. Este arquivo ficará desatualizado se você continuar usando a carteira após a exportação.", }, import_wallet: { button: "Importar backup da carteira", description: "Restaure sua carteira a partir de um arquivo de backup exportado anteriormente. Isso substituirá os dados atuais da sua carteira pelo backup.", confirm_question: "Tem certeza de que deseja restaurar os dados da sua carteira?", cancel: "Cancelar", confirm: "IMPORTAR BACKUP DA CARTEIRA", }, }, }, }, NoMintWarnBanner: { title: "Entre em um mint", subtitle: "Você ainda não entrou em nenhum mint Cashu. Adicione uma URL de mint nas configurações ou receba ecash de um novo mint para começar.", actions: { add_mint: { label: "@:global.actions.add_mint.label", }, receive: { label: "Receber Ecash", }, }, }, WalletPage: { actions: { send: { label: "@:global.actions.send.label", }, receive: { label: "@:global.actions.receive.label", }, }, tabs: { history: { label: "Histórico", }, invoices: { label: "Faturas", }, mints: { label: "Mints", }, }, install: { text: "Instalar", tooltip: "Instalar Cashu", }, }, AlreadyRunning: { title: "Ops.", text: "Outra aba já está em execução. Feche esta aba e tente novamente.", actions: { retry: { label: "Tentar novamente", }, }, }, ErrorNotFound: { title: "404", text: "Ops. Nada aqui…", actions: { home: { label: "Voltar ao início", }, }, }, BalanceView: { mintUrl: { label: "Mint", }, mintBalance: { label: "Saldo", }, mintError: { label: "Erro no mint", }, pending: { label: "Pendente", tooltip: "Verificar todos os tokens pendentes", }, }, WelcomePage: { actions: { previous: { label: "Anterior", }, next: { label: "Próximo", }, }, }, WelcomeSlide1: { title: "Bem-vindo ao Cashu", text: "Cashu.me é uma carteira Bitcoin gratuita e de código aberto que usa ecash para manter seus fundos seguros e privados.", actions: { more: { label: "Clique para saber mais", }, }, p1: { text: "Cashu é um protocolo de ecash gratuito e de código aberto para Bitcoin. Saiba mais em { link }.", link: { text: "cashu.space", }, }, p2: { text: "Esta carteira não é afiliada a nenhum mint. Para usá-la, você precisa se conectar a um ou mais mints Cashu em que confia.", }, p3: { text: "Esta carteira armazena ecash ao qual somente você tem acesso. Se você excluir os dados do navegador sem um backup da frase de recuperação, perderá seus tokens.", }, p4: { text: "Esta carteira está em beta. Não nos responsabilizamos por pessoas que perdem acesso a fundos. Use por sua conta e risco! Este código é de código aberto e licenciado sob a licença MIT.", }, }, WelcomeSlide2: { title: "Instalar PWA", alt: { pwa_example: "Exemplo de Instalação de PWA", }, installing: "Instalando…", instruction: { intro: { text: "Para a melhor experiência, use esta carteira com o navegador nativo do seu dispositivo para instalá-la como um Aplicativo Web Progressivo.", }, android: { title: "Android (Chrome)", step1: { item: "1. { icon } { text }", text: "Toque no menu (canto superior direito)", }, step2: { item: "2. { icon } { text }", text: "Pressione { buttonText }", buttonText: "@:AndroidPWAPrompt.buttonText", }, }, ios: { title: "iOS (Safari)", step1: { item: "1. { icon } { text }", text: "Toque em compartilhar (parte inferior)", }, step2: { item: "2. { icon } { text }", text: "Pressione { buttonText }", buttonText: "@:iOSPWAPrompt.buttonText", }, }, outro: { text: "Após instalar o aplicativo no seu dispositivo, feche esta janela do navegador e use o aplicativo pela tela inicial.", }, }, pwa: { success: { title: "Sucesso!", text: "Você está usando o Cashu como PWA. Feche qualquer outra janela do navegador aberta e use o aplicativo pela tela inicial.", nextSteps: "Agora você pode fechar esta aba do navegador e abrir o aplicativo pela tela inicial.", }, }, }, iOSPWAPrompt: { text: "Toque em { icon } e { buttonText }", buttonText: "Adicionar à Tela de Início", }, AndroidPWAPrompt: { text: "Toque em { icon } e { buttonText }", buttonText: "Adicionar à Tela de Início", }, WelcomeSlide3: { title: "Sua Frase de Recuperação", text: "Guarde sua frase de recuperação em um gerenciador de senhas ou no papel. Sua frase de recuperação é a única forma de recuperar seus fundos caso perca o acesso a este dispositivo.", inputs: { seed_phrase: { label: "Frase de Recuperação", caption: "Você pode ver sua frase de recuperação nas configurações.", }, checkbox: { label: "Já a anotei", }, }, }, WelcomeSlide4: { title: "Termos", actions: { more: { label: "Ler Termos de Serviço", }, }, inputs: { checkbox: { label: "Li e aceito estes termos e condições", }, }, }, WelcomeSlideChoice: { title: "Configure sua carteira", text: "Você deseja recuperar a partir de uma frase de recuperação ou criar uma nova carteira?", options: { new: { title: "Criar nova carteira", subtitle: "Gerar uma nova frase de recuperação e adicionar mints.", }, recover: { title: "Recuperar carteira", subtitle: "Digite sua frase de recuperação, restaure mints e ecash.", }, }, }, WelcomeMintSetup: { title: "Adicionar mints", text: "Mints são servidores que ajudam você a enviar e receber ecash. Escolha um mint descoberto ou adicione um manualmente. Pule para adicionar mints depois.", sections: { your_mints: "Seus mints", }, restoring: "Restaurando mints…", placeholder: { mint_url: "https://", }, }, WelcomeRecoverSeed: { title: "Digite sua frase de recuperação", text: "Cole ou digite sua frase de recuperação de 12 palavras para recuperar.", inputs: { word: "Palavra { index }", }, actions: { paste_all: "Colar tudo", }, disclaimer: "Sua frase de recuperação é usada apenas localmente para derivar as chaves da sua carteira.", }, WelcomeRestoreEcash: { title: "Restaurar seu ecash", text: "Busque provas não gastas nos seus mints configurados e adicione-as à sua carteira.", }, MintRatings: { title: "Avaliações do Mint", reviews: "avaliações", ratings: "Avaliações", no_reviews: "Nenhuma avaliação encontrada", your_review: "Sua avaliação", no_reviews_to_display: "Nenhuma avaliação para exibir.", no_rating: "Sem avaliação", out_of: "de", rows: "Avaliações", sort: "Ordenar", sort_options: { newest: "Mais recente", oldest: "Mais antiga", highest: "Maior", lowest: "Menor", }, actions: { write_review: "Escrever uma avaliação", }, empty_state_subtitle: "Ajude deixando uma avaliação. Compartilhe sua experiência com este mint e ajude outros deixando uma avaliação.", }, CreateMintReview: { title: "Avaliar Mint", publishing_as: "Publicando como", inputs: { rating: { label: "Avaliação" }, review: { label: "Comentário (opcional)" }, }, actions: { publish: { label: "Enviar Avaliação", in_progress: "Enviando…" }, }, }, RestoreView: { seed_phrase: { label: "Restaurar a partir da Frase de Recuperação", caption: "Digite sua frase de recuperação para restaurar sua carteira. Antes de restaurar, certifique-se de ter adicionado todos os mints que usou anteriormente.", inputs: { seed_phrase: { label: "Frase de recuperação", caption: "Você pode ver sua frase de recuperação nas configurações.", }, }, }, information: { label: "Informação", caption: "O assistente só restaurará ecash de outra frase de recuperação; você não poderá usar esta frase de recuperação nem alterar a frase de recuperação da carteira que está usando atualmente. Isso significa que o ecash restaurado não estará protegido pela sua frase de recuperação atual enquanto você não o enviar para si mesmo uma vez.", }, restore_mints: { label: "Restaurar Mints", caption: 'Selecione o mint para restaurar. Você pode adicionar mais mints na tela principal em "Mints" e restaurá-los aqui.', }, actions: { paste: { error: "Falha ao ler o conteúdo da área de transferência.", }, validate: { error: "O mnemônico não é uma frase de recuperação BIP39 válida.", }, select_all: { label: "Selecionar Todos", }, deselect_all: { label: "Desselecionar Todos", }, restore: { label: "Restaurar", in_progress: "Restaurando mint …", error: "Erro ao restaurar mint: { error }", }, restore_all_mints: { label: "Restaurar Todos os Mints", in_progress: "Restaurando mint { index } de { length } …", success: "Restauração concluída com sucesso", error: "Erro ao restaurar mints: { error }", }, restore_selected_mints: { label: "Restaurar Mints Selecionados ({count})", in_progress: "Restaurando mint { index } de { length } …", success: "Restaurado(s) com sucesso {count} mint(s)", error: "Erro ao restaurar mints selecionados: { error }", }, }, nostr_mints: { label: "Restaurar Mints do Nostr", caption: "Busque backups de mints armazenados nos relays Nostr usando sua frase de recuperação. Isso ajudará você a buscar os mints que usou anteriormente.", search_button: "Buscar Backups de Mints", select_all: "Selecionar Todos", deselect_all: "Desselecionar Todos", backed_up: "Com backup", already_added: "Já adicionado", add_selected: "Adicionar Selecionados ({count})", no_backups_found: "Nenhum backup de mint encontrado", no_backups_hint: "Certifique-se de que o backup de mints no Nostr está ativado nas configurações para fazer backup automático da sua lista de mints.", invalid_mnemonic: "Por favor, insira uma frase de recuperação válida antes de buscar.", search_error: "Falha ao buscar backups de mints.", add_error: "Falha ao adicionar os mints selecionados.", }, }, MintSettings: { add: { title: "Adicionar mint", description: "Digite a URL de um mint Cashu para se conectar. Esta carteira não é afiliada a nenhum mint.", inputs: { nickname: { placeholder: "Apelido (ex.: Testnet)", }, }, actions: { add_mint: { label: "@:global.actions.add_mint.label", error_invalid_url: "URL inválida", }, scan: { label: "Escanear QR Code", }, }, }, discover: { title: "Buscar mints", overline: "Buscar", caption: "Descubra mints que outros usuários recomendaram no Nostr.", actions: { discover: { label: "Buscar mints", in_progress: "Carregando…", error_no_mints: "Nenhum mint encontrado", success: "Encontrado(s) { length } mint(s)", }, }, recommendations: { overline: "Encontrado(s) { length } mint(s)", caption: "Esses mints foram recomendados por outros usuários do Nostr. Tome cuidado e faça sua própria pesquisa antes de usar um mint.", actions: { browse: { label: "Clique para ver os mints", }, }, }, }, swap: { title: "Swap", overline: "Swap Multi-mint", actions: { receove_to_trusted_mint: { label: "Receber para mint confiável", }, swap: { label: "@:global.actions.swap.label", in_progress: "@:MintSettings.swap.actions.swap.label", }, }, caption: "Faça swap de fundos entre mints via Lightning. Nota: Deixe margem para possíveis taxas Lightning. Se o pagamento recebido não for bem-sucedido, verifique a fatura manualmente.", inputs: { from: { label: "De", }, to: { label: "Para", }, amount: { label: "Valor ({ ticker })", }, }, }, error_badge: "Erro", reviews_text: "avaliações", no_reviews_yet: "Ainda sem avaliações", discover_mints_button: "Buscar mints", }, QrcodeReader: { progress: { text: "{ percentage }{ addon }", percentage: "{ percentage }%", keep_scanning_text: " - Continue escaneando", }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, }, }, InvoiceDetailDialog: { title: "Receber Lightning", create_invoice_title: "Criar Fatura", inputs: { amount: { label: "Valor ({ ticker }) *", }, }, actions: { close: { label: "@:global.actions.close.label", }, create: { label: "Criar Fatura", label_blocked: "Criando fatura…", in_progress: "Criando", }, }, invoice: { caption: "Fatura Lightning", status_paid_text: "Pago!", actions: { close: { label: "@:global.actions.close.label", }, copy: { label: "@:global.actions.copy.label", }, }, }, }, SendDialog: { title: "Enviar", actions: { ecash: { label: "Ecash", error_no_mints: "Nenhum mint disponível", }, lightning: { label: "Lightning", error_no_mints: "Nenhum mint disponível", }, }, }, SendTokenDialog: { title: "Enviar Ecash", title_ecash_text: "Ecash", badge_offline_text: "Offline", inputs: { amount: { label: "Valor ({ ticker }) *", invalid_too_much_error_text: "Valor muito alto", }, p2pk_pubkey: { label: "Chave pública do destinatário", label_invalid: "Chave pública do destinatário", }, }, actions: { close: { label: "@:global.actions.close.label", }, close_card_scanner: { label: "@:global.actions.close.label", }, copy_emoji: { label: "🥜", tooltip_text: "Copiar Emoji", }, copy_tokens: { label: "@:global.actions.copy.label", }, copy_link: { tooltip_text: "Copiar link", }, share: { tooltip_text: "Compartilhar ecash", }, lock: { label: "@:global.actions.lock.label", }, paste_p2pk_pubkey: { tooltip_text: "@:global.actions.paste.label", }, pay: { label: "@:global.actions.pay.label", }, send: { label: "@:global.actions.send.label", }, delete: { tooltip_text: "Excluir do histórico", }, write_tokens_to_card: { tooltips: { ndef_supported_text: "Gravar no cartão NFC", ndef_unsupported_text: "NDEF não suportado", }, }, }, errors: { amount_required: "Insira um valor primeiro.", serialization_failed: "Não foi possível preparar o token ecash.", }, }, SendPaymentRequest: { actions: { pay: { label: "Pagar", }, pay_via: { label: "Pagar via {transport}", }, }, info: { pay_to: "Pagar para {target}", invalid_url: "URL inválida", }, }, PaymentRequestInfo: { title_with_transport: "Solicitação de pagamento via {transport}", title: "Solicitação de pagamento", subtitle: "Pagar para {target}", subtitle_fallback: "Solicitação de pagamento", invalid_url: "URL inválida", }, ReceiveDialog: { title: "Receber", actions: { ecash: { label: "Ecash", error_no_mints: "Nenhum mint disponível", }, lightning: { label: "Lightning", error_no_mints: "Você precisa se conectar a um mint para receber via Lightning", }, }, }, ReceiveEcashDrawer: { title: "Receber Ecash", actions: { paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, request: { label: "Solicitar", }, lock: { label: "@:global.actions.lock.label", }, nfc: { label: "NFC", scanning_text: "Escaneando…", }, }, }, ReceiveTokenDialog: { title: "Receber Ecash", title_ecash_text: "Ecash", inputs: { tokens_base64: { label: "Colar token Cashu", }, }, errors: { invalid_token: { label: "Token inválido", }, p2pk_lock_mismatch: { label: "Não foi possível receber. O bloqueio P2PK deste token não corresponde à sua chave pública.", }, }, unknown_mint_info_text: "Mint desconhecido. Será adicionado após você receber este token.", swap_section: { title: "Swap", source_label: "De", destination_label: "Para", fee_info: "Este swap incorrerá em taxas da rede Lightning.", }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, scan: { label: "@:global.actions.scan.label", }, receive: { label: "@:global.actions.receive.label", label_known_mint: "@:ReceiveTokenDialog.actions.receive.label", label_adding_mint: "Adicionando mint…", }, swap: { label: "Receber para mint confiável", tooltip_text: "Swap para um mint confiável", caption: "Swap { value }", processing: "Processando swap...", failed: "Swap falhou", }, cancel_swap: { label: "@:global.actions.cancel.label", tooltip_text: "Cancelar swap", }, confirm_swap: { label: "@:ReceiveTokenDialog.actions.swap.label", tooltip_text: "@:ReceiveTokenDialog.actions.swap.tooltip_text", in_progress: "@:ReceiveTokenDialog.actions.confirm_swap.label", }, receive_to_selected_mint: { label: "Receber para o mint selecionado", }, later: { label: "Receber depois", tooltip_text: "Adicionar ao histórico para receber depois", already_in_history_success_text: "Ecash já está no Histórico", added_to_history_success_text: "Ecash adicionado ao Histórico", }, nfc: { label: "NFC", tooltips: { ndef_supported_text: "Ler do cartão NFC", ndef_unsupported_text: "NDEF não suportado", }, }, }, }, P2PKDialog: { p2pk: { caption: "Chave P2PK", description: "Receber ecash bloqueado a esta chave", used_warning_text: "Aviso: Esta chave já foi usada. Use uma nova chave para melhor privacidade.", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_key: { label: "Gerar nova chave", }, }, }, PaymentRequestDialog: { payment_request: { caption: "Solicitação de Pagamento", description: "Receba pagamentos via Nostr", }, received_total: "Total recebido", no_payments_yet: "Nenhum pagamento ainda", actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_request: { label: "Nova solicitação", }, add_amount: { label: "Adicionar valor", }, use_active_mint: { label: "Qualquer mint", }, }, inputs: { amount: { placeholder: "Digite o valor", }, }, }, NumericKeyboard: { actions: { close: { label: "@:global.actions.close.label", closed_info_text: "Teclado desativado. Você pode reativar o teclado nas configurações.", }, enter: { label: "@:global.actions.enter.label", }, }, }, NWCDialog: { nwc: { caption: "Nostr Wallet Connect", description: "Controle sua carteira remotamente com NWC. Pressione o QR code para vincular sua carteira a um aplicativo compatível.", warning_text: "Aviso: qualquer pessoa com acesso a esta string de conexão pode iniciar pagamentos da sua carteira. Não compartilhe!", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, }, }, MintMotdMessage: { title: "Mensagem do Mint", }, MintDetailsDialog: { contact: { title: "Contato", }, details: { title: "Detalhes do mint", url: { label: "URL", }, nuts: { label: "Nuts", actions: { show: { label: "Ver todos", }, hide: { label: "Ocultar", }, }, }, currency: { label: "Moeda", }, currencies: { label: "@:MintDetailsDialog.details.currency.label", }, version: { label: "Versão", }, }, actions: { title: "Ações", copy_mint_url: { label: "Copiar URL do mint", }, delete: { label: "Excluir mint", }, edit: { label: "Editar mint", }, }, }, ChooseMint: { title: "Selecione um mint", placeholder: "Selecione um mint", available_text: "disponível", sheet_title: "Selecionar Mint", badge_mint_error_text: "Erro", badge_option_mint_error_text: "@:ChooseMint.badge_mint_error_text", }, HistoryTable: { empty_text: "Nenhum histórico ainda", row: { type_label: "Ecash", date_label: "há { value }", }, actions: { check_status: { tooltip_text: "Verificar status", }, receive: { tooltip_text: "Receber", }, filter_pending: { label: "Filtrar pendentes", }, show_all: { label: "Mostrar todos", }, }, old_token_not_found_error_text: "Token antigo não encontrado", }, InvoiceTable: { empty_text: "Nenhuma fatura ainda", row: { type_label: "Lightning", type_tooltip_text: "Clique para copiar", date_label: "há { value }", }, actions: { check_status: { tooltip_text: "Verificar status", }, filter_pending: { label: "Filtrar pendentes", }, show_all: { label: "Mostrar todos", }, }, }, RemoveMintDialog: { title: "Tem certeza de que deseja excluir este mint?", nickname: { label: "Apelido", }, balances: { label: "Saldos", }, warning_text: "Nota: Como esta carteira é precavida, seu ecash deste mint não será realmente excluído, mas permanecerá armazenado no seu dispositivo. Você o verá reaparecer se adicionar novamente este mint.", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { confirm: { label: "Remover mint", }, cancel: { label: "@:global.actions.cancel.label", }, }, }, ParseInputComponent: { placeholder: { default: "Token Cashu ou endereço Lightning", receive: "Token Cashu", pay: "Endereço Lightning ou fatura", }, qr_scanner: { title: "Escanear QR Code", description: "Toque para escanear um endereço", }, paste_button: { label: "@:global.actions.paste.label", }, }, PayInvoiceDialog: { input_data: { title: "Pagar Lightning", inputs: { invoice_data: { label: "Fatura ou endereço Lightning", }, }, actions: { close: { label: "@:global.actions.close.label", }, enter: { label: "@:global.actions.enter.label", }, paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, }, }, lnurlpay: { amount_exact_label: "{ payee } está solicitando { value } { ticker }", amount_range_label: "{ payee } está solicitando{br}entre { min } e { max } { ticker }", sending_to_lightning_address: "Enviando para { address }", inputs: { amount: { label: "Valor ({ ticker }) *", }, comment: { label: "Comentário (opcional)", }, }, actions: { close: { label: "@:global.actions.close.label", }, send: { label: "@:global.actions.send.label", }, }, }, invoice: { title: "Pagar { value }", paying: "Pagando", paid: "Pago", fee: "Taxa", memo: { label: "Memo", }, processing_info_text: "Processando…", balance_too_low_warning_text: "Saldo insuficiente", actions: { close: { label: "@:global.actions.close.label", }, pay: { label: "Pagar", in_progress: "@:PayInvoiceDialog.invoice.processing_info_text", error: "Erro", }, }, }, }, EditMintDialog: { title: "Editar mint", inputs: { nickname: { label: "Apelido", }, mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, update: { label: "@:global.actions.update.label", }, }, }, AddMintDialog: { title: "Você confia neste mint?", description: "Antes de usar este mint, certifique-se de que confia nele. Os mints podem se tornar maliciosos ou encerrar as operações a qualquer momento.", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, add_mint: { label: "@:global.actions.add_mint.label", in_progress: "Adicionando mint", }, }, }, restore: { mnemonic_error_text: "Por favor, insira um mnemônico", restore_mint_error_text: "Erro ao restaurar mint: { error }", prepare_info_text: "Preparando processo de restauração …", restored_proofs_for_keyset_info_text: "Restaurado(s) { restoreCounter } prova(s) para o keyset { keysetId }", checking_proofs_for_keyset_info_text: "Verificando provas { startIndex } a { endIndex } para o keyset { keysetId }", no_proofs_info_text: "Nenhuma prova encontrada para restaurar", restored_amount_success_text: "Restaurado { amount }", }, swap: { in_progress_warning_text: "Swap em andamento", invalid_swap_data_error_text: "Dados de swap inválidos", swap_error_text: "Erro no swap", }, TokenInformation: { fee: "Taxa", unit: "Unidade", fiat: "Moeda fiduciária", p2pk: "P2PK", locked: "Bloqueado", locked_to_you: "Bloqueado para você", mint: "Mint", memo: "Memo", payment_request: "Solicitação de pagamento", nostr: "Nostr", token_copied: "Token copiado para a área de transferência", }, }; ================================================ FILE: src/i18n/sv-SE/index.ts ================================================ export default { MultinutPicker: { payment: "Multinut-betalning", selectMints: "Välj en eller flera mints att betala från.", totalSelectedBalance: "Totalt valt saldo", multiMintPay: "Multi-Mint-betalning", balanceNotEnough: "Multi-mint-saldo räcker inte för denna faktura", failed: "Misslyckades att behandla: {error}", paid: "Betalat {amount} via Lightning", }, global: { copy_to_clipboard: { success: "Kopierat till urklipp!", }, actions: { add_mint: { label: "Lägg till mint", }, cancel: { label: "Avbryt", }, copy: { label: "Kopiera", }, close: { label: "Stäng", }, enter: { label: "Ange", }, lock: { label: "Lås", }, paste: { label: "Klistra in", }, receive: { label: "Ta emot", }, scan: { label: "Skanna", }, send: { label: "Skicka", }, swap: { label: "Byt", }, update: { label: "Uppdatera", }, }, inputs: { mint_url: { label: "Mint URL", }, }, }, wallet: { notifications: { balance_too_low: "Saldot är för lågt", received: "Mottaget {amount}", fee: " (avgift: {fee})", could_not_request_mint: "Kunde inte begära prägling", invoice_still_pending: "Fakturan väntar fortfarande", paid_lightning: "Betalat {amount} via Lightning", payment_pending_refresh: "Betalning väntar. Uppdatera fakturan manuellt.", sent: "Skickat {amount}", token_still_pending: "Token väntar fortfarande", received_lightning: "Mottaget {amount} via Lightning", lightning_payment_failed: "Lightning-betalning misslyckades", failed_to_decode_invoice: "Kunde inte avkoda fakturan", invalid_lnurl: "Ogiltig LNURL", lnurl_error: "LNURL-fel", no_amount: "Inget belopp", no_lnurl_data: "Ingen LNURL-data", no_price_data: "Ingen prisdata.", please_try_again: "Försök igen.", }, mint: { notifications: { already_added: "Mint redan tillagd", added: "Mint tillagd", not_found: "Mint hittades inte", activation_failed: "Aktivering av mint misslyckades", no_active_mint: "Ingen aktiv mint", unit_activation_failed: "Aktivering av enhet misslyckades", unit_not_supported: "Enheten stöds inte av mint", activated: "Mint aktiverad", could_not_connect: "Kunde inte ansluta till mint", could_not_get_info: "Kunde inte hämta mint-information", could_not_get_keys: "Kunde inte hämta mint-nycklar", could_not_get_keysets: "Kunde inte hämta mint-nyckeluppsättningar", mint_validation_error: "Mint-valideringsfel", removed: "Mint borttagen", error: "Mint-fel", }, }, }, MainHeader: { menu: { settings: { title: "Inställningar", settings: { title: "Inställningar", caption: "Plånboksinställningar", }, }, terms: { title: "Villkor", terms: { title: "Villkor", caption: "Användarvillkor", }, }, links: { title: "Länkar", cashuSpace: { title: "Cashu.space", caption: "cashu.space", }, github: { title: "Github", caption: "github.com/cashubtc", }, telegram: { title: "Telegram", caption: "t.me/CashuMe", }, twitter: { title: "Twitter", caption: "{'@'}CashuBTC", }, donate: { title: "Donera", caption: "Stöd Cashu", }, }, }, offline: { warning: { text: "Offline", }, }, reload: { warning: { text: "Laddar om om { countdown }", }, }, staging: { warning: { text: "Staging – använd inte med riktiga pengar!", }, }, }, FullscreenHeader: { actions: { back: { label: "Plånbok", }, }, }, Settings: { web_of_trust: { title: "Förtroendenätverk", known_pubkeys: "Kända pubkeys: {wotCount}", continue_crawl: "Fortsätt genomsökning", crawl_odell: "Genomsök ODELL'S WEB OF TRUST", crawl_wot: "Genomsök web of trust", pause: "Pausa", reset: "Återställ", progress: "{crawlProcessed} / {crawlTotal}", }, npub_cash: { use_npubx: "Använd npubx.cash", copy_lightning_address: "Kopiera Lightning-adress", v2_mint: "npub.cash v2 mint", }, multinut: { use_multinut: "Använd Multinut", }, language: { title: "Språk", description: "Välj önskat språk från listan nedan.", }, sections: { backup_restore: "SÄKERHETSKOPIERING & ÅTERSTÄLLNING", lightning_address: "LIGHTNING ADRESS", nostr_keys: "NOSTR NYCKLAR", nostr: { title: "NOSTR", relays: { expand_label: "Klicka för att redigera reläer", add: { title: "Lägg till relä", description: "Din plånbok använder dessa reläer för nostr-operationer som betalningsförfrågningar, nostr wallet connect och säkerhetskopior.", }, list: { title: "Reläer", description: "Din plånbok kommer att ansluta till dessa reläer.", copy_tooltip: "Kopiera relä", remove_tooltip: "Ta bort relä", }, }, }, payment_requests: "BETALNINGSFÖRFRÅGNINGAR", nostr_wallet_connect: "NOSTR PLÅNBOKSANSLUTNING", hardware_features: "MASKINVARA FUNKTIONER", p2pk_features: "P2PK FUNKTIONER", privacy: "INTEGRITET", experimental: "EXPERIMENTELLA", appearance: "UTSEENDE", }, backup_restore: { backup_seed: { title: "Säkerhetskopiera återställningsfras", description: "Din återställningsfras kan återställa din plånbok. Håll den säker och privat.", seed_phrase_label: "Återställningsfras", }, restore_ecash: { title: "Återställ ecash", description: "Återställningsguiden låter dig återställa förlorad ecash från en mnemonisk återställningsfras. Din nuvarande plånboks återställningsfras kommer inte att påverkas, guiden tillåter dig endast att återställa ecash från en annan återställningsfras.", button: "Återställ", }, }, lightning_address: { title: "Lightning-adress", description: "Ta emot betalningar till din Lightning-adress.", enable: { toggle: "Aktivera", description: "Lightning-adress med npub.cash", }, address: { copy_tooltip: "Kopiera Lightning-adress", }, automatic_claim: { toggle: "Hämta automatiskt", description: "Ta emot inkommande betalningar automatiskt.", }, npc_v2: { choose_mint_title: "Välj mint för npub.cash v2", choose_mint_placeholder: "Välj en mint...", }, }, nostr_keys: { title: "Dina nostr-nycklar", description: "Ställ in nostr-nycklarna för din Lightning-adress.", wallet_seed: { title: "Plånbokens återställningsfras", description: "Generera nostr nyckelpar från plånbokens återställningsfras", copy_nsec: "Kopiera nsec", }, nsec_bunker: { title: "Nsec Bunker", description: "Använd en NIP-46 bunker", delete_tooltip: "Radera anslutning", }, use_nsec: { title: "Använd din nsec", description: "Denna metod är farlig och rekommenderas inte", delete_tooltip: "Radera nsec", }, signing_extension: { title: "Signeringsutökning", description: "Använd en NIP-07 signeringsutökning", not_found: "Ingen NIP-07 signeringsutökning hittades", }, }, payment_requests: { title: "Betalningsförfrågningar", description: "Betalningsförfrågningar gör det möjligt att ta emot betalningar via nostr. Om du aktiverar detta prenumererar din plånbok på dina nostr-reläer.", enable_toggle: "Aktivera betalningsförfrågningar", claim_automatically: { toggle: "Hämta automatiskt", description: "Ta emot inkommande betalningar automatiskt.", }, }, nostr_wallet_connect: { title: "Nostr Plånboksanslutning (NWC)", description: "Använd NWC för att styra din plånbok från valfri annan applikation.", enable_toggle: "Aktivera NWC", payments_note: "Du kan endast använda NWC för betalningar från ditt Bitcoin-saldo. Betalningar kommer att göras från din aktiva mint.", connection: { copy_tooltip: "Kopiera anslutningssträng", qr_tooltip: "Visa QR-kod", allowance_label: "Tillåtelse kvar (sat)", }, }, hardware_features: { webnfc: { title: "WebNFC", description: "Välj kodning för att skriva till NFC-kort", text: { title: "Text", description: "Spara token i klartext", }, weburl: { title: "URL", description: "Spara URL till denna plånbok med token", }, binary: { title: "Binär", description: "Lagra tokens som binärdata", }, quick_access: { toggle: "Snabb åtkomst till NFC", description: "Skanna snabbt NFC-kort i Ta emot Ecash-menyn. Detta alternativ lägger till en NFC-knapp i Ta emot Ecash-menyn.", }, }, }, p2pk_features: { title: "P2PK", description: "Generera ett nyckelpar för att ta emot P2PK-låst ecash. Varning: Denna funktion är experimentell. Använd endast med små belopp. Om du förlorar dina privata nycklar kommer ingen att kunna låsa upp ecash som är låst till den längre.", generate_button: "Generera nyckel", import_button: "Importera nsec", quick_access: { toggle: "Snabb åtkomst till lås", description: "Använd detta för att snabbt visa din P2PK låsningsnyckel i menyn för att ta emot ecash.", }, keys_expansion: { label: "Klicka för att bläddra bland {count} nycklar", used_badge: "använd", }, }, privacy: { title: "Integritet", description: "Dessa inställningar påverkar din integritet.", check_incoming: { toggle: "Kontrollera inkommande faktura", description: "Om aktiverat kommer plånboken att kontrollera den senaste fakturan i bakgrunden. Detta ökar plånbokens responsivitet vilket gör fingeravtryckning enklare. Du kan manuellt kontrollera obetalda fakturor under fliken Fakturor.", }, check_startup: { toggle: "Kontrollera väntande fakturor vid start", description: "Om aktiverat kommer plånboken att kontrollera väntande fakturor från de senaste 24 timmarna vid start.", }, check_all: { toggle: "Kontrollera alla fakturor", description: "Om aktiverat kommer plånboken periodvis att kontrollera obetalda fakturor i bakgrunden i upp till två veckor. Detta ökar plånbokens online-aktivitet vilket gör fingeravtryckning enklare. Du kan manuellt kontrollera obetalda fakturor under fliken Fakturor.", }, check_sent: { toggle: "Kontrollera skickad ecash", description: "Om aktiverat kommer plånboken att använda periodiska bakgrundskontroller för att avgöra om skickade tokens har lösts in. Detta ökar plånbokens online-aktivitet vilket gör fingeravtryckning enklare.", }, websockets: { toggle: "Använd WebSockets", description: "Om aktiverat kommer plånboken att använda långlivade WebSocket-anslutningar för att ta emot uppdateringar om betalda fakturor och spenderade tokens från mints. Detta ökar plånbokens responsivitet men gör också fingeravtryckning enklare.", }, bitcoin_price: { toggle: "Hämta växelkurs från Coinbase", description: "Om aktiverat kommer aktuell Bitcoin-växelkurs att hämtas från coinbase.com och ditt konverterade saldo kommer att visas.", currency: { title: "Fiat-valuta", description: "Välj fiat-valuta för Bitcoin-prisvisning.", }, }, }, experimental: { title: "Experimentella", description: "Dessa funktioner är experimentella.", receive_swaps: { toggle: "Ta emot byten", badge: "Beta", description: "Möjlighet att byta mottagen Ecash till din aktiva mint i dialogrutan Ta emot Ecash.", }, auto_paste: { toggle: "Klistra in Ecash automatiskt", description: "Klistra in ecash från ditt urklipp automatiskt när du trycker på Ta emot, sedan Ecash, sedan Klistra in. Automatisk inklistring kan orsaka UI-problem i iOS, stäng av det om du upplever problem.", }, auditor: { toggle: "Aktivera revisor", badge: "Beta", description: "Om aktiverat kommer plånboken att visa revisorsinformation i dialogrutan för mintdetaljer. Revisorn är en tredjepartstjänst som övervakar mints pålitlighet.", url_label: "Revisor URL", api_url_label: "Revisor API URL", }, multinut: { toggle: "Aktivera Multinut", description: "Om aktiverat kommer plånboken att använda Multinut för att betala fakturor från flera mints samtidigt.", }, nostr_mint_backup: { toggle: "Säkerhetskopiera mintlista på Nostr", description: "Om aktiverat kommer din mintlista automatiskt att säkerhetskopieras till Nostr-reläer med dina konfigurerade Nostr-nycklar. Detta gör att du kan återställa din mintlista över enheter.", notifications: { enabled: "Nostr mint-säkerhetskopiering aktiverad", disabled: "Nostr mint-säkerhetskopiering inaktiverad", failed: "Misslyckades att aktivera Nostr mint-säkerhetskopiering", }, }, }, appearance: { keyboard: { title: "Skärmtangentbord", description: "Använd det numeriska tangentbordet för att ange belopp.", toggle: "Använd numeriskt tangentbord", toggle_description: "Om aktiverat kommer det numeriska tangentbordet att användas för att ange belopp.", }, theme: { title: "Utseende", description: "Ändra hur din plånbok ser ut.", tooltips: { mono: "mono", cyber: "cyber", freedom: "frihet", nostr: "nostr", bitcoin: "bitcoin", mint: "mint", nut: "nöt", blu: "blå", flamingo: "flamingo", }, }, bip177: { title: "Bitcoin-symbol", description: "Använd ₿-symbolen istället för sats.", toggle: "Använd ₿-symbolen", }, }, advanced: { title: "Avancerade", developer: { title: "Utvecklarinställningar", description: "Följande inställningar är för utveckling och felsökning.", new_seed: { button: "Generera ny återställningsfras", description: "Detta kommer att generera en ny återställningsfras. Du måste skicka hela ditt saldo till dig själv för att kunna återställa det med en ny återställningsfras.", confirm_question: "Är du säker på att du vill generera en ny återställningsfras?", cancel: "Avbryt", confirm: "Bekräfta", }, remove_spent: { button: "Ta bort spenderade proofs", description: "Kontrollera om ecash-tokens från dina aktiva mints är spenderade och ta bort de spenderade från din plånbok. Använd detta endast om din plånbok har fastnat.", }, debug_console: { button: "Visa/dölj debugkonsol", description: "Öppna Javascript debugterminalen. Klistra aldrig in något i den här terminalen som du inte förstår. En tjuv kan försöka lura dig att klistra in skadlig kod här.", }, export_proofs: { button: "Exportera aktiva proofs", description: "Kopiera hela ditt saldo från den aktiva minten som en Cashu-token till ditt urklipp. Detta exporterar endast tokens från den valda minten och enheten. För en fullständig export, välj en annan mint och enhet och exportera igen.", }, keyset_counters: { title: "Öka keyset-räknare", description: 'Klicka på keyset-ID för att öka härledningsvägsräknarna för keysets i din plånbok. Detta är användbart om du ser felet "outputs have already been signed".', counter: "räknare: {count}", }, unset_reserved: { button: "Avboka alla reserverade tokens", description: 'Denna plånbok markerar väntande utgående ecash som reserverad (och drar av den från ditt saldo) för att förhindra försök till dubbelspending. Den här knappen kommer att avboka alla reserverade tokens så att de kan användas igen. Om du gör detta kan din plånbok inkludera spenderade proofs. Tryck på knappen "Ta bort spenderade proofs" för att bli av med dem.', }, show_onboarding: { button: "Visa introduktion", description: "Visa introduktionsskärmen igen.", }, reset_wallet: { button: "Återställ plånboksdata", description: "Återställ dina plånboksdata. Varning: Detta kommer att radera allt! Se till att du skapar en säkerhetskopia först.", confirm_question: "Är du säker på att du vill radera din plånboksdata?", cancel: "Avbryt", confirm: "Radera plånbok", }, export_wallet: { button: "Exportera plånboksdata", description: "Ladda ner en dump av din plånbok. Du kan återställa din plånbok från den här filen i välkomstskärmen på en ny plånbok. Den här filen kommer att vara osynkroniserad om du fortsätter att använda din plånbok efter att ha exporterat den.", }, }, }, }, NoMintWarnBanner: { title: "Gå med i en mint", subtitle: "Du har inte gått med i någon Cashu mint ännu. Lägg till en mint URL i inställningarna eller ta emot ecash från en ny mint för att komma igång.", actions: { add_mint: { label: "@:global.actions.add_mint.label", }, receive: { label: "Ta emot Ecash", }, }, }, WalletPage: { actions: { send: { label: "@:global.actions.send.label", }, receive: { label: "@:global.actions.receive.label", }, }, tabs: { history: { label: "Historik", }, invoices: { label: "Fakturor", }, mints: { label: "Mints", }, }, install: { text: "Installera", tooltip: "Installera Cashu", }, }, AlreadyRunning: { title: "Nej.", text: "En annan flik körs redan. Stäng den här fliken och försök igen.", actions: { retry: { label: "Försök igen", }, }, }, ErrorNotFound: { title: "404", text: "Oops. Ingenting här…", actions: { home: { label: "Gå tillbaka hem", }, }, }, BalanceView: { mintUrl: { label: "Mint", }, mintBalance: { label: "Saldo", }, mintError: { label: "Mint fel", }, pending: { label: "Väntande", tooltip: "Kontrollera alla väntande tokens", }, }, WelcomePage: { actions: { previous: { label: "Föregående", }, next: { label: "Nästa", }, }, }, WelcomeSlide1: { title: "Välkommen till Cashu", text: "Cashu.me är en gratis och öppen källkods Bitcoin-plånbok som använder ecash för att hålla dina pengar säkra och privata.", actions: { more: { label: "Klicka för att lära dig mer", }, }, p1: { text: "Cashu är ett gratis och öppet källkods ecash-protokoll för Bitcoin. Du kan lära dig mer om det på { link }.", link: { text: "cashu.space", }, }, p2: { text: "Denna plånbok är inte ansluten till någon mint. För att använda denna plånbok måste du ansluta till en eller flera Cashu mints som du litar på.", }, p3: { text: "Denna plånbok lagrar ecash som endast du har åtkomst till. Om du raderar dina webbläsardata utan en säkerhetskopia av återställningsfrasen kommer du att förlora dina tokens.", }, p4: { text: "Denna plånbok är i beta. Vi tar inget ansvar för att personer förlorar åtkomst till medel. Använd på egen risk! Denna kod är öppen källkod och licensierad under MIT-licensen.", }, }, WelcomeSlide2: { title: "Installera PWA", alt: { pwa_example: "Exempel på PWA-installation" }, installing: "Installerar…", instruction: { intro: { text: "För bästa upplevelsen, använd denna plånbok med din enhets webbläsare för att installera den som en Progressive Web App. Gör detta nu.", }, android: { title: "Android (Chrome)", step1: { item: "1. { icon } { text }", text: "Tryck på menyn (uppe till höger)", }, step2: { item: "2. { icon } { text }", text: "Tryck på { buttonText }", buttonText: "@:AndroidPWAPrompt.buttonText", }, }, ios: { title: "iOS (Safari)", step1: { item: "1. { icon } { text }", text: "Tryck på dela (nere)", }, step2: { item: "2. { icon } { text }", text: "Tryck på { buttonText }", buttonText: "@:iOSPWAPrompt.buttonText", }, }, outro: { text: "När du har installerat denna app på din enhet, stäng detta webbläsarfönster och använd appen från din startskärm.", }, }, pwa: { success: { title: "Klart!", text: "Du använder Cashu som en PWA. Stäng alla andra öppna webbläsarfönster och använd appen från din startskärm.", nextSteps: "Du kan nu stänga denna flik och öppna appen från hemskärmen.", }, }, }, iOSPWAPrompt: { text: "Tryck på { icon } och { buttonText }", buttonText: "Lägg till på hemskärmen", }, AndroidPWAPrompt: { text: "Tryck på { icon } och { buttonText }", buttonText: "Lägg till på hemskärmen", }, WelcomeSlide3: { title: "Din återställningsfras", text: "Spara din återställningsfras i en lösenordshanterare eller på papper. Din återställningsfras är det enda sättet att återställa dina pengar om du förlorar åtkomst till denna enhet.", inputs: { seed_phrase: { label: "Återställningsfras", caption: "Du kan se din återställningsfras i inställningarna.", }, checkbox: { label: "Jag har skrivit ner den", }, }, }, WelcomeSlide4: { title: "Villkor", actions: { more: { label: "Läs användarvillkoren", }, }, inputs: { checkbox: { label: "Jag har läst och accepterar dessa villkor", }, }, }, WelcomeSlideChoice: { title: "Ställ in din plånbok", text: "Vill du återställa från en återställningsfras eller skapa en ny plånbok?", options: { new: { title: "Skapa ny plånbok", subtitle: "Generera en ny fras och lägg till mints.", }, recover: { title: "Återställ plånbok", subtitle: "Ange din återställningsfras, återställ mints och ecash.", }, }, }, WelcomeMintSetup: { title: "Lägg till mints", text: "Mints är servrar som hjälper dig skicka och ta emot ecash. Välj en upptäckt mint eller lägg till en manuellt. Du kan hoppa över och lägga till senare.", sections: { your_mints: "Dina mints" }, restoring: "Återställer mints…", placeholder: { mint_url: "https://" }, }, WelcomeRecoverSeed: { title: "Ange din återställningsfras", text: "Klistra in eller skriv din 12 ord långa fras för att återställa.", inputs: { word: "Ord { index }" }, actions: { paste_all: "Klistra in alla" }, disclaimer: "Din fras används endast lokalt för att härleda dina plånboksnycklar.", }, WelcomeRestoreEcash: { title: "Återställ ditt ecash", text: "Sök efter ospenderade proofs på dina konfigurerade mints och lägg till dem i plånboken.", }, MintRatings: { title: "Mint-recensioner", reviews: "recensioner", ratings: "Betyg", no_reviews: "Inga recensioner hittades", your_review: "Din recension", no_reviews_to_display: "Inga recensioner att visa.", no_rating: "Ingen betygsättning", out_of: "av", rows: "Reviews", sort: "Sortera", sort_options: { newest: "Nyaste", oldest: "Äldsta", highest: "Högsta", lowest: "Lägsta", }, actions: { write_review: "Skriv en recension" }, empty_state_subtitle: "Hjälp genom att lämna en recension. Dela din upplevelse med denna mint och hjälp andra genom att lämna en recension.", }, CreateMintReview: { title: "Recensera mint", publishing_as: "Publicerar som", inputs: { rating: { label: "Betyg" }, review: { label: "Recension (valfritt)" }, }, actions: { publish: { label: "Publicera", in_progress: "Publicerar…" }, }, }, RestoreView: { seed_phrase: { label: "Återställ från återställningsfras", caption: "Ange din återställningsfras för att återställa din plånbok. Innan du återställer, se till att du har lagt till alla mints som du har använt tidigare.", inputs: { seed_phrase: { label: "Återställningsfras", caption: "Du kan se din återställningsfras i inställningarna.", }, }, }, information: { label: "Information", caption: "Guiden kommer endast att återställa ecash från en annan återställningsfras, du kommer inte att kunna använda denna återställningsfras eller ändra återställningsfrasen för plånboken du för närvarande använder. Detta innebär att återställd ecash inte kommer att skyddas av din nuvarande återställningsfras så länge du inte skickar ecash till dig själv en gång.", }, restore_mints: { label: "Återställ Mints", caption: 'Välj mint att återställa. Du kan lägga till fler mints på huvudskärmen under "Mints" och återställa dem här.', }, actions: { paste: { error: "Kunde inte läsa urklippsinnehåll.", }, validate: { error: "Mnemoniska frasen bör vara minst 12 ord.", }, select_all: { label: "Välj alla", }, deselect_all: { label: "Avmarkera alla", }, restore: { label: "Återställ", in_progress: "Återställer mint…", error: "Fel vid återställning av mint: { error }", }, restore_all_mints: { label: "Återställ Alla Mints", in_progress: "Återställer mint { index } av { length } …", success: "Återställning slutfördes framgångsrikt", error: "Fel vid återställning av mints: { error }", }, restore_selected_mints: { label: "Återställ valda mints ({count})", in_progress: "Återställer mint {index} av {length} ...", success: "Lyckades återställa {count} mint(s)", error: "Fel vid återställning av valda mints: {error}", }, }, nostr_mints: { label: "Återställ Mints från Nostr", caption: "Sök efter mint-säkerhetskopior lagrade på Nostr-reläer med din återställningsfras. Detta hjälper dig att upptäcka mints du tidigare använt.", search_button: "Sök efter Mint-säkerhetskopior", select_all: "Välj alla", deselect_all: "Avmarkera alla", backed_up: "Säkerhetskopierad", already_added: "Redan tillagd", add_selected: "Lägg till valda ({count})", no_backups_found: "Inga mint-säkerhetskopior hittades", no_backups_hint: "Se till att Nostr mint-säkerhetskopiering är aktiverat i inställningarna för att automatiskt säkerhetskopiera din mintlista.", invalid_mnemonic: "Ange en giltig återställningsfras innan du söker.", search_error: "Misslyckades att söka efter mint-säkerhetskopior.", add_error: "Misslyckades att lägga till valda mints.", }, }, MintSettings: { add: { title: "Lägg till mint", description: "Ange URL:en för en Cashu mint för att ansluta till den. Denna plånbok är inte ansluten till någon mint.", inputs: { nickname: { placeholder: "Smeknamn (t.ex. Testnet)", }, }, actions: { add_mint: { label: "@:global.actions.add_mint.label", error_invalid_url: "Ogiltig URL", }, scan: { label: "Skanna QR-kod", }, }, }, discover: { title: "Upptäck mints", overline: "Upptäck", caption: "Upptäck mints som andra användare har rekommenderat på nostr.", actions: { discover: { label: "Upptäck mints", in_progress: "Laddar…", error_no_mints: "Inga mints hittades", success: "{ length } mints hittades", }, }, recommendations: { overline: "{ length } mints hittades", caption: "Dessa mints rekommenderades av andra Nostr-användare. Var försiktig och gör din egen research innan du använder en mint.", actions: { browse: { label: "Klicka för att bläddra bland mints", }, }, }, }, swap: { title: "Byt", overline: "Multimint-byten", caption: "Byt medel mellan mints via Lightning. Obs: Lämna utrymme för eventuella Lightning-avgifter. Om den inkommande betalningen inte lyckas, kontrollera fakturan manuellt.", inputs: { from: { label: "Från", }, to: { label: "Till", }, amount: { label: "Belopp ({ ticker })", }, }, actions: { swap: { label: "@:global.actions.swap.label", in_progress: "@:MintSettings.swap.actions.swap.label", }, }, }, error_badge: "Fel", reviews_text: "recensioner", no_reviews_yet: "Inga recensioner ännu", discover_mints_button: "Upptäck mints", }, QrcodeReader: { progress: { text: "{ percentage }{ addon }", percentage: "{ percentage }%", keep_scanning_text: " - Fortsätt skanna", }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, }, }, InvoiceDetailDialog: { title: "Ta emot Lightning", create_invoice_title: "Skapa faktura", inputs: { amount: { label: "Belopp ({ ticker }) *", }, }, actions: { close: { label: "@:global.actions.close.label", }, create: { label: "Skapa faktura", label_blocked: "Skapar faktura…", in_progress: "Skapar", }, }, invoice: { caption: "Lightning-faktura", status_paid_text: "Betald!", actions: { close: { label: "@:global.actions.close.label", }, copy: { label: "@:global.actions.copy.label", }, }, }, }, SendDialog: { title: "Skicka", actions: { ecash: { label: "Ecash", error_no_mints: "Inga mints tillgängliga", }, lightning: { label: "Lightning", error_no_mints: "Inga mints tillgängliga", }, }, }, SendTokenDialog: { title: "Skicka Ecash", title_ecash_text: "Ecash", badge_offline_text: "Offline", inputs: { amount: { label: "Belopp ({ ticker }) *", invalid_too_much_error_text: "För mycket", }, p2pk_pubkey: { label: "Mottagarens publika nyckel", label_invalid: "Mottagarens publika nyckel", }, }, actions: { close: { label: "@:global.actions.close.label", }, close_card_scanner: { label: "@:global.actions.close.label", }, copy_emoji: { label: "🥜", tooltip_text: "Kopiera Emoji", }, copy_tokens: { label: "@:global.actions.copy.label", }, copy_link: { tooltip_text: "Kopiera länk", }, share: { tooltip_text: "Dela ecash", }, lock: { label: "@:global.actions.lock.label", }, paste_p2pk_pubkey: { tooltip_text: "@:global.actions.paste.label", }, send: { label: "@:global.actions.send.label", }, delete: { tooltip_text: "Ta bort från historik", }, write_tokens_to_card: { tooltips: { ndef_supported_text: "Flash till NFC-kort", ndef_unsupported_text: "NDEF stöds inte", }, }, }, }, ReceiveDialog: { title: "Ta emot", actions: { ecash: { label: "Ecash", error_no_mints: "Inga mints tillgängliga", }, lightning: { label: "Lightning", error_no_mints: "Du måste ansluta till en mint för att ta emot via Lightning", }, }, }, ReceiveEcashDrawer: { title: "Ta emot Ecash", actions: { paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, request: { label: "Begär", }, lock: { label: "@:global.actions.lock.label", }, nfc: { label: "NFC", scanning_text: "Skannar…", }, }, }, ReceiveTokenDialog: { title: "Ta emot Ecash", title_ecash_text: "Ecash", inputs: { tokens_base64: { label: "Klistra in Cashu token", }, }, errors: { invalid_token: { label: "Ogiltig token", }, p2pk_lock_mismatch: { label: "Kan inte ta emot. Denna tokens P2PK-lås matchar inte din publika nyckel.", }, }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, scan: { label: "@:global.actions.scan.label", }, receive: { label: "@:global.actions.receive.label", label_known_mint: "@:ReceiveTokenDialog.actions.receive.label", label_adding_mint: "Lägger till mint…", }, swap: { label: "@:global.actions.swap.label", tooltip_text: "Byt till en betrodd mint", caption: "Byt { value }", }, cancel_swap: { label: "@:global.actions.cancel.label", tooltip_text: "Avbryt byte", }, confirm_swap: { label: "@:ReceiveTokenDialog.actions.swap.label", tooltip_text: "@:ReceiveTokenDialog.actions.swap.tooltip_text", in_progress: "@:ReceiveTokenDialog.actions.confirm_swap.label", }, later: { label: "Ta emot senare", tooltip_text: "Lägg till i historik för att ta emot senare", already_in_history_success_text: "Ecash redan i historik", added_to_history_success_text: "Ecash lades till i historik", }, nfc: { label: "NFC", tooltips: { ndef_supported_text: "Läs från NFC-kort", ndef_unsupported_text: "NDEF stöds inte", }, }, }, }, P2PKDialog: { p2pk: { caption: "P2PK Nyckel", description: "Ta emot ecash låst till denna nyckel", used_warning_text: "Varning: Denna nyckel användes tidigare. Använd en ny nyckel för bättre integritet.", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_key: { label: "Generera ny nyckel", }, }, }, PaymentRequestDialog: { payment_request: { caption: "Betalningsförfrågan", description: "Ta emot betalningar via Nostr", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_request: { label: "Ny förfrågan", }, add_amount: { label: "Lägg till belopp", }, use_active_mint: { label: "Valfri mint", }, }, inputs: { amount: { placeholder: "Ange belopp", }, }, }, NumericKeyboard: { actions: { close: { label: "@:global.actions.close.label", closed_info_text: "Tangentbordet inaktiverat. Du kan återaktivera tangentbordet i inställningarna.", }, enter: { label: "@:global.actions.enter.label", }, }, }, NWCDialog: { nwc: { caption: "Nostr Plånboksanslutning", description: "Styr din plånbok på distans med NWC. Tryck på QR-koden för att länka din plånbok med en kompatibel app.", warning_text: "Varning: den som har åtkomst till denna anslutningssträng kan initiera betalningar från din plånbok. Dela inte!", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, }, }, MintMotdMessage: { title: "Mintmeddelande", }, MintDetailsDialog: { contact: { title: "Kontakt", }, details: { title: "Mintdetaljer", url: { label: "URL", }, nuts: { label: "Nuts", actions: { show: { label: "Visa alla", }, hide: { label: "Dölj", }, }, }, currency: { label: "Valuta", }, currencies: { label: "@:MintDetailsDialog.details.currency.label", }, version: { label: "Version", }, }, actions: { title: "Åtgärder", copy_mint_url: { label: "Kopiera mint URL", }, delete: { label: "Radera mint", }, edit: { label: "Redigera mint", }, }, }, ChooseMint: { title: "Välj en mint", badge_mint_error_text: "Fel", badge_option_mint_error_text: "@:ChooseMint.badge_mint_error_text", }, HistoryTable: { empty_text: "Ingen historik än", row: { type_label: "Ecash", date_label: "{ value } sedan", }, actions: { check_status: { tooltip_text: "Kontrollera status", }, receive: { tooltip_text: "Ta emot", }, filter_pending: { label: "Filtrera väntande", }, show_all: { label: "Visa alla", }, }, old_token_not_found_error_text: "Gammal token hittades inte", }, InvoiceTable: { empty_text: "Inga fakturor än", row: { type_label: "Lightning", type_tooltip_text: "Klicka för att kopiera", date_label: "{ value } sedan", }, actions: { check_status: { tooltip_text: "Kontrollera status", }, filter_pending: { label: "Filtrera väntande", }, show_all: { label: "Visa alla", }, }, }, RemoveMintDialog: { title: "Är du säker på att du vill radera denna mint?", nickname: { label: "Smeknamn", }, balances: { label: "Saldon", }, warning_text: "Obs: Eftersom denna plånbok är paranoid, kommer din ecash från denna mint inte att raderas helt utan förbli lagrad på din enhet. Du kommer att se den återkomma om du lägger till denna mint igen senare.", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { confirm: { label: "Ta bort mint", }, cancel: { label: "@:global.actions.cancel.label", }, }, }, ParseInputComponent: { placeholder: { default: "Cashu token eller Lightning-adress", receive: "Cashu token", pay: "Lightning-adress eller faktura", }, qr_scanner: { title: "Skanna QR-kod", description: "Tryck för att skanna en adress", }, paste_button: { label: "@:global.actions.paste.label", }, }, PayInvoiceDialog: { input_data: { title: "Betala Lightning", inputs: { invoice_data: { label: "Lightning-faktura eller adress", }, }, actions: { close: { label: "@:global.actions.close.label", }, enter: { label: "@:global.actions.enter.label", }, paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, }, }, lnurlpay: { amount_exact_label: "{ payee } begär { value } { ticker }", amount_range_label: "{ payee } begär{br}mellan { min } och { max } { ticker }", sending_to_lightning_address: "Skickar till { address }", inputs: { amount: { label: "Belopp ({ ticker }) *", }, comment: { label: "Kommentar (valfritt)", }, }, actions: { close: { label: "@:global.actions.close.label", }, send: { label: "@:global.actions.send.label", }, }, }, invoice: { title: "Betala { value }", paying: "Betalar", paid: "Betald", fee: "Avgift", memo: { label: "Memo", }, processing_info_text: "Bearbetar…", balance_too_low_warning_text: "Saldot för lågt", actions: { close: { label: "@:global.actions.close.label", }, pay: { label: "Betala", in_progress: "@:PayInvoiceDialog.invoice.processing_info_text", error: "Fel", }, }, }, }, EditMintDialog: { title: "Redigera mint", inputs: { nickname: { label: "Smeknamn", }, mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, update: { label: "@:global.actions.update.label", }, }, }, AddMintDialog: { title: "Litar du på denna mint?", description: "Innan du använder denna mint, se till att du litar på den. Mints kan bli skadliga eller upphöra med sin verksamhet när som helst.", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, add_mint: { label: "@:global.actions.add_mint.label", in_progress: "Lägger till mint", }, }, }, restore: { mnemonic_error_text: "Ange en mnemonisk fras", restore_mint_error_text: "Fel vid återställning av mint: { error }", prepare_info_text: "Förbereder återställningsprocessen…", restored_proofs_for_keyset_info_text: "Återställde { restoreCounter } proofs för keyset { keysetId }", checking_proofs_for_keyset_info_text: "Kontrollerar proofs { startIndex } till { endIndex } för keyset { keysetId }", no_proofs_info_text: "Inga proofs hittades att återställa", restored_amount_success_text: "Återställde { amount }", }, swap: { in_progress_warning_text: "Byte pågår", invalid_swap_data_error_text: "Ogiltig bytesdata", swap_error_text: "Fel vid byte", }, TokenInformation: { fee: "Avgift", unit: "Enhet", fiat: "Fiat", p2pk: "P2PK", locked: "Låst", locked_to_you: "Låst till dig", mint: "Myntverk", memo: "Memo", payment_request: "Betalningsförfrågan", nostr: "Nostr", token_copied: "Token kopierad till urklipp", }, }; ================================================ FILE: src/i18n/th-TH/index.ts ================================================ export default { MultinutPicker: { payment: "การชำระเงิน Multinut", selectMints: "เลือกหนึ่งหรือหลาย mint เพื่อทำการชำระเงิน", totalSelectedBalance: "ยอดคงเหลือที่เลือกทั้งหมด", multiMintPay: "จ่ายแบบหลาย Mint", balanceNotEnough: "ยอดหลาย mint ไม่เพียงพอสำหรับใบแจ้งหนี้นี้", failed: "ไม่สามารถประมวลผล: {error}", paid: "จ่าย {amount} ผ่าน Lightning", }, global: { copy_to_clipboard: { success: "คัดลอกไปยังคลิปบอร์ดแล้ว!", }, actions: { add_mint: { label: "เพิ่ม Mint", }, cancel: { label: "ยกเลิก", }, copy: { label: "คัดลอก", }, close: { label: "ปิด", }, enter: { label: "ป้อน", }, lock: { label: "ล็อก", }, paste: { label: "วาง", }, receive: { label: "รับ", }, scan: { label: "สแกน", }, send: { label: "ส่ง", }, swap: { label: "แลกเปลี่ยน", }, update: { label: "อัปเดต", }, }, inputs: { mint_url: { label: "Mint URL", }, }, }, wallet: { notifications: { balance_too_low: "ยอดคงเหลือน้อยเกินไป", received: "ได้รับ {amount}", fee: " (ค่าธรรมเนียม: {fee})", could_not_request_mint: "ไม่สามารถขอ mint ได้", invoice_still_pending: "ใบแจ้งหนี้ยังอยู่ระหว่างดำเนินการ", paid_lightning: "จ่าย {amount} ผ่าน Lightning", payment_pending_refresh: "การชำระเงินอยู่ระหว่างดำเนินการ รีเฟรชใบแจ้งหนี้ด้วยตนเอง", sent: "ส่ง {amount}", token_still_pending: "โทเค็นยังอยู่ระหว่างดำเนินการ", received_lightning: "ได้รับ {amount} ผ่าน Lightning", lightning_payment_failed: "การชำระเงิน Lightning ล้มเหลว", failed_to_decode_invoice: "ไม่สามารถถอดรหัสใบแจ้งหนี้", invalid_lnurl: "LNURL ไม่ถูกต้อง", lnurl_error: "ข้อผิดพลาด LNURL", no_amount: "ไม่มียอดเงิน", no_lnurl_data: "ไม่มีข้อมูล LNURL", no_price_data: "ไม่มีข้อมูลราคา", please_try_again: "โปรดลองอีกครั้ง", }, mint: { notifications: { already_added: "เพิ่ม Mint แล้ว", added: "เพิ่ม Mint แล้ว", not_found: "ไม่พบ Mint", activation_failed: "การเปิดใช้งาน Mint ล้มเหลว", no_active_mint: "ไม่มี Mint ที่ใช้งานอยู่", unit_activation_failed: "การเปิดใช้งานหน่วยล้มเหลว", unit_not_supported: "หน่วยไม่รองรับโดย Mint", activated: "เปิดใช้งาน Mint แล้ว", could_not_connect: "ไม่สามารถเชื่อมต่อกับ Mint ได้", could_not_get_info: "ไม่สามารถดึงข้อมูล Mint ได้", could_not_get_keys: "ไม่สามารถดึงคีย์ Mint ได้", could_not_get_keysets: "ไม่สามารถดึงชุดคีย์ Mint ได้", mint_validation_error: "ข้อผิดพลาดในการตรวจสอบ mint", removed: "ลบ Mint แล้ว", error: "ข้อผิดพลาด Mint", }, }, }, MainHeader: { menu: { settings: { title: "การตั้งค่า", settings: { title: "การตั้งค่า", caption: "การกำหนดค่า Wallet", }, }, terms: { title: "เงื่อนไข", terms: { title: "เงื่อนไข", caption: "ข้อกำหนดในการให้บริการ", }, }, links: { title: "ลิงก์", cashuSpace: { title: "Cashu.space", caption: "cashu.space", }, github: { title: "Github", caption: "github.com/cashubtc", }, telegram: { title: "Telegram", caption: "t.me/CashuMe", }, twitter: { title: "Twitter", caption: "{'@'}CashuBTC", }, donate: { title: "บริจาค", caption: "สนับสนุน Cashu", }, }, }, offline: { warning: { text: "ออฟไลน์", }, }, reload: { warning: { text: "โหลดใหม่ใน { countdown }", }, }, staging: { warning: { text: "กำลังทดสอบ – ห้ามใช้กับเงินจริง!", }, }, }, FullscreenHeader: { actions: { back: { label: "Wallet", }, }, }, Settings: { web_of_trust: { title: "เครือข่ายที่เชื่อถือได้", known_pubkeys: "Pubkey ที่รู้จัก: {wotCount}", continue_crawl: "ดำเนินการสำรวจต่อ", crawl_odell: "สำรวจ ODELL'S WEB OF TRUST", crawl_wot: "สำรวจ web of trust", pause: "หยุดชั่วคราว", reset: "รีเซ็ต", progress: "{crawlProcessed} / {crawlTotal}", }, npub_cash: { use_npubx: "ใช้ npubx.cash", copy_lightning_address: "คัดลอกที่อยู่ Lightning", v2_mint: "npub.cash v2 mint", }, multinut: { use_multinut: "ใช้ Multinut", }, language: { title: "ภาษา", description: "โปรดเลือกภาษาที่คุณต้องการจากรายการด้านล่าง", }, sections: { backup_restore: "สำรองข้อมูล & กู้คืน", lightning_address: "ที่อยู่ LIGHTNING", nostr_keys: "คีย์ NOSTR", nostr: { title: "NOSTR", relays: { expand_label: "คลิกเพื่อแก้ไขรีเลย์", add: { title: "เพิ่มรีเลย์", description: "กระเป๋าเงินของคุณใช้รีเลย์เหล่านี้สำหรับการดำเนินงาน nostr เช่น คำขอชำระเงิน nostr wallet connect และการสำรองข้อมูล", }, list: { title: "รีเลย์", description: "กระเป๋าเงินของคุณจะเชื่อมต่อกับรีเลย์เหล่านี้", copy_tooltip: "คัดลอกรีเลย์", remove_tooltip: "ลบรีเลย์", }, }, }, payment_requests: "คำขอชำระเงิน", nostr_wallet_connect: "NOSTR WALLET CONNECT", hardware_features: "คุณสมบัติฮาร์ดแวร์", p2pk_features: "คุณสมบัติ P2PK", privacy: "ความเป็นส่วนตัว", experimental: "ทดลอง", appearance: "รูปลักษณ์", }, backup_restore: { backup_seed: { title: "สำรองวลีกู้คืน", description: "วลีกู้คืนของคุณสามารถกู้คืน Wallet ของคุณได้ เก็บไว้ให้ปลอดภัยและเป็นส่วนตัว", seed_phrase_label: "วลีกู้คืน", }, restore_ecash: { title: "กู้คืน ecash", description: "วิซาร์ดการกู้คืนช่วยให้คุณกู้คืน ecash ที่สูญหายจากวลีกู้คืนแบบ Mnemonic ได้ วลีกู้คืนของ Wallet ปัจจุบันของคุณจะไม่ได้รับผลกระทบ วิซาร์ดจะอนุญาตให้คุณ กู้คืน ecash จากวลีกู้คืนอื่นเท่านั้น", button: "กู้คืน", }, }, lightning_address: { title: "ที่อยู่ Lightning", description: "รับการชำระเงินไปยังที่อยู่ Lightning ของคุณ", enable: { toggle: "เปิดใช้งาน", description: "ที่อยู่ Lightning กับ npub.cash", }, address: { copy_tooltip: "คัดลอกที่อยู่ Lightning", }, automatic_claim: { toggle: "รับอัตโนมัติ", description: "รับการชำระเงินขาเข้าโดยอัตโนมัติ", }, npc_v2: { choose_mint_title: "เลือก mint สำหรับ npub.cash v2", choose_mint_placeholder: "เลือก mint...", }, }, nostr_keys: { title: "คีย์ Nostr ของคุณ", description: "ตั้งค่าคีย์ nostr สำหรับที่อยู่ Lightning ของคุณ", wallet_seed: { title: "วลีสำหรับกู้คืน Wallet", description: "สร้างคู่คีย์ nostr จากวลีสำหรับกู้คืน Wallet", copy_nsec: "คัดลอก nsec", }, nsec_bunker: { title: "Nsec Bunker", description: "ใช้ NIP-46 bunker", delete_tooltip: "ลบการเชื่อมต่อ", }, use_nsec: { title: "ใช้ nsec ของคุณ", description: "วิธีนี้อันตรายและไม่แนะนำ", delete_tooltip: "ลบ nsec", }, signing_extension: { title: "ส่วนขยายการลงนาม", description: "ใช้ส่วนขยายการลงนาม NIP-07", not_found: "ไม่พบส่วนขยายการลงนาม NIP-07", }, }, payment_requests: { title: "คำขอชำระเงิน", description: "คำขอชำระเงินช่วยให้คุณรับการชำระเงินผ่าน nostr ได้ หากเปิดใช้งาน Wallet ของคุณจะสมัครสมาชิก Nostr relays ของคุณ", enable_toggle: "เปิดใช้งานคำขอชำระเงิน", claim_automatically: { toggle: "รับอัตโนมัติ", description: "รับการชำระเงินขาเข้าโดยอัตโนมัติ", }, }, nostr_wallet_connect: { title: "Nostr Wallet Connect (NWC)", description: "ใช้ NWC เพื่อควบคุม Wallet ของคุณจากแอปพลิเคชันอื่นใด", enable_toggle: "เปิดใช้งาน NWC", payments_note: "คุณสามารถใช้ NWC สำหรับการชำระเงินจากยอดคงเหลือ Bitcoin ของคุณเท่านั้น การชำระเงินจะทำจาก Mint ที่เปิดใช้งานของคุณ", connection: { copy_tooltip: "คัดลอกสตริงการเชื่อมต่อ", qr_tooltip: "แสดงรหัส QR", allowance_label: "ยอดคงเหลือที่เหลือ (sat)", }, }, hardware_features: { webnfc: { title: "WebNFC", description: "เลือกการเข้ารหัสสำหรับการเขียนลงในการ์ด NFC", text: { title: "ข้อความ", description: "เก็บ token ในรูปแบบข้อความธรรมดา", }, weburl: { title: "URL", description: "เก็บ URL ไปยัง Wallet นี้พร้อม token", }, binary: { title: "ไบนารี", description: "จัดเก็บโทเค็นเป็นข้อมูลไบนารี", }, quick_access: { toggle: "เข้าถึง NFC ด่วน", description: "สแกนการ์ด NFC ได้อย่างรวดเร็วในเมนู รับ Ecash ตัวเลือกนี้จะเพิ่มปุ่ม NFC ในเมนู รับ Ecash", }, }, }, p2pk_features: { title: "P2PK", description: "สร้างคู่คีย์เพื่อรับ ecash ที่ล็อกด้วย P2PK คำเตือน: คุณสมบัตินี้เป็นการทดลอง ใช้เฉพาะกับจำนวนเล็กน้อยเท่านั้น หากคุณทำคีย์ส่วนตัวของคุณหาย จะไม่มีใครสามารถปลดล็อก ecash ที่ล็อกด้วยคีย์นั้นได้อีกต่อไป", generate_button: "สร้างคีย์", import_button: "นำเข้า nsec", quick_access: { toggle: "เข้าถึงล็อกด่วน", description: "ใช้สิ่งนี้เพื่อแสดงคีย์ล็อก P2PK ของคุณอย่างรวดเร็วในเมนูรับ ecash", }, keys_expansion: { label: "คลิกเพื่อเรียกดู {count} คีย์", used_badge: "ใช้แล้ว", }, }, privacy: { title: "ความเป็นส่วนตัว", description: "การตั้งค่าเหล่านี้ส่งผลต่อความเป็นส่วนตัวของคุณ", check_incoming: { toggle: "ตรวจสอบใบแจ้งหนี้ขาเข้า", description: "หากเปิดใช้งาน Wallet จะตรวจสอบใบแจ้งหนี้ล่าสุดในเบื้องหลัง ซึ่งช่วยเพิ่มความสามารถในการตอบสนองของ Wallet ทำให้การสร้างรอยนิ้วมือทำได้ง่ายขึ้น คุณสามารถตรวจสอบใบแจ้งหนี้ที่ยังไม่ได้ชำระด้วยตนเองได้ในแท็บใบแจ้งหนี้", }, check_startup: { toggle: "ตรวจสอบใบแจ้งหนี้ที่รอดำเนินการเมื่อเริ่มต้น", description: "หากเปิดใช้งาน Wallet จะตรวจสอบใบแจ้งหนี้ที่รอดำเนินการในช่วง 24 ชั่วโมงที่ผ่านมาเมื่อเริ่มต้น", }, check_all: { toggle: "ตรวจสอบใบแจ้งหนี้ทั้งหมด", description: "หากเปิดใช้งาน Wallet จะตรวจสอบใบแจ้งหนี้ที่ยังไม่ได้ชำระเป็นระยะๆ ในเบื้องหลังเป็นเวลาสูงสุดสองสัปดาห์ ซึ่งช่วยเพิ่มกิจกรรมออนไลน์ของ Wallet ทำให้การสร้างรอยนิ้วมือทำได้ง่ายขึ้น คุณสามารถตรวจสอบใบแจ้งหนี้ที่ยังไม่ได้ชำระด้วยตนเองได้ในแท็บใบแจ้งหนี้", }, check_sent: { toggle: "ตรวจสอบ ecash ที่ส่ง", description: "หากเปิดใช้งาน Wallet จะใช้การตรวจสอบเบื้องหลังเป็นระยะๆ เพื่อพิจารณาว่าโทเค็นที่ส่งถูกแลกแล้วหรือไม่ ซึ่งเพิ่มกิจกรรมออนไลน์ของ Wallet ทำให้การสร้างรอยนิ้วมือทำได้ง่ายขึ้น", }, websockets: { toggle: "ใช้ WebSockets", description: "หากเปิดใช้งาน Wallet จะใช้การเชื่อมต่อ WebSocket ที่มีอายุยืนยาวเพื่อรับการอัปเดตเกี่ยวกับใบแจ้งหนี้ที่ชำระแล้วและโทเค็นที่ใช้จ่ายจาก Mints ซึ่งเพิ่มความสามารถในการตอบสนองของ Wallet แต่ก็ทำให้การสร้างรอยนิ้วมือทำได้ง่ายขึ้นเช่นกัน", }, bitcoin_price: { toggle: "รับอัตราแลกเปลี่ยนจาก Coinbase", description: "หากเปิดใช้งาน จะดึงอัตราแลกเปลี่ยน Bitcoin ปัจจุบันจาก coinbase.com และแสดงยอดคงเหลือที่แปลงแล้วของคุณ", currency: { title: "สกุลเงินเฟียต", description: "เลือกสกุลเงินเฟียตสำหรับการแสดงราคา Bitcoin", }, }, }, experimental: { title: "ทดลอง", description: "คุณสมบัติเหล่านี้เป็นคุณสมบัติทดลอง", receive_swaps: { toggle: "รับ swaps", badge: "เบต้า", description: "ตัวเลือกในการแลกเปลี่ยน Ecash ที่ได้รับไปยัง Mint ที่เปิดใช้งานของคุณในกล่องโต้ตอบ รับ Ecash", }, auto_paste: { toggle: "วาง Ecash โดยอัตโนมัติ", description: "วาง ecash ในคลิปบอร์ดของคุณโดยอัตโนมัติเมื่อคุณกด รับ, จากนั้น Ecash, จากนั้น วาง การวางอัตโนมัติอาจทำให้เกิดความผิดปกติของ UI ใน iOS ให้ปิดหากคุณประสบปัญหา", }, auditor: { toggle: "เปิดใช้งานผู้ตรวจสอบ", badge: "เบต้า", description: "หากเปิดใช้งาน Wallet จะแสดงข้อมูลผู้ตรวจสอบในกล่องโต้ตอบรายละเอียด Mint ผู้ตรวจสอบคือบริการบุคคลที่สามที่ตรวจสอบความน่าเชื่อถือของ Mints", url_label: "URL ผู้ตรวจสอบ", api_url_label: "URL API ผู้ตรวจสอบ", }, multinut: { toggle: "เปิดใช้งาน Multinut", description: "หากเปิดใช้งาน Wallet จะใช้ Multinut เพื่อชำระค่าใบแจ้งหนี้จากหลาย mints พร้อมกัน", }, nostr_mint_backup: { toggle: "สำรองข้อมูล Mint list บน Nostr", description: "หากเปิดใช้งาน รายการ Mint ของคุณจะถูกสำรองข้อมูลไปยัง Nostr relays โดยอัตโนมัติโดยใช้คีย์ Nostr ที่กำหนดค่าไว้ สิ่งนี้ช่วยให้คุณสามารถกู้คืนรายการ Mint ของคุณในอุปกรณ์ต่างๆ ได้", notifications: { enabled: "เปิดใช้งานการสำรองข้อมูล Nostr mint แล้ว", disabled: "ปิดใช้งานการสำรองข้อมูล Nostr mint แล้ว", failed: "ไม่สามารถเปิดใช้งานการสำรองข้อมูล Nostr mint ได้", }, }, }, appearance: { keyboard: { title: "แป้นพิมพ์บนหน้าจอ", description: "ใช้แป้นพิมพ์ตัวเลขสำหรับการป้อนจำนวนเงิน", toggle: "ใช้แป้นพิมพ์ตัวเลข", toggle_description: "หากเปิดใช้งาน จะใช้แป้นพิมพ์ตัวเลขสำหรับการป้อนจำนวนเงิน", }, theme: { title: "รูปลักษณ์", description: "เปลี่ยนรูปลักษณ์ของ Wallet ของคุณ", tooltips: { mono: "mono", cyber: "cyber", freedom: "freedom", nostr: "nostr", bitcoin: "bitcoin", mint: "mint", nut: "nut", blu: "blu", flamingo: "flamingo", }, }, bip177: { title: "สัญลักษณ์ Bitcoin", description: "ใช้สัญลักษณ์ ₿ แทน sats", toggle: "ใช้สัญลักษณ์ ₿", }, }, advanced: { title: "ขั้นสูง", developer: { title: "การตั้งค่าสำหรับนักพัฒนา", description: "การตั้งค่าต่อไปนี้มีไว้สำหรับการพัฒนาและการดีบั๊ก", new_seed: { button: "สร้างวลีสำหรับกู้คืนใหม่", description: "สิ่งนี้จะสร้างวลีสำหรับกู้คืนใหม่ คุณต้องส่งยอดเงินทั้งหมดของคุณไปให้ตัวเองเพื่อที่จะกู้คืนด้วยวลีสำหรับกู้คืนใหม่ได้", confirm_question: "คุณแน่ใจหรือไม่ว่าต้องการสร้างวลีสำหรับกู้คืนใหม่?", cancel: "ยกเลิก", confirm: "ยืนยัน", }, remove_spent: { button: "ลบหลักฐานที่ใช้แล้ว", description: "ตรวจสอบว่าโทเค็น ecash จาก mints ที่เปิดใช้งานของคุณถูกใช้ไปแล้วหรือไม่ และลบโทเค็นที่ใช้แล้วออกจาก Wallet ของคุณ ใช้สิ่งนี้เฉพาะเมื่อ Wallet ของคุณติดค้าง", }, debug_console: { button: "สลับคอนโซลดีบั๊ก", description: "เปิดเทอร์มินัลดีบั๊ก Javascript ห้ามวางสิ่งใดๆ ลงในเทอร์มินัลนี้ที่คุณไม่เข้าใจ ขโมยอาจพยายามหลอกให้คุณวางโค้ดที่เป็นอันตรายที่นี่", }, export_proofs: { button: "ส่งออกหลักฐานที่ใช้งานอยู่", description: "คัดลอกยอดคงเหลือทั้งหมดของคุณจาก mint ที่เปิดใช้งานเป็นโทเค็น Cashu ไปยังคลิปบอร์ดของคุณ นี่จะส่งออกเฉพาะโทเค็นจาก mint และหน่วยที่เลือก สำหรับการส่งออกทั้งหมด ให้เลือก mint และหน่วยอื่นแล้วส่งออกอีกครั้ง", }, keyset_counters: { title: "เพิ่มเคาน์เตอร์ keyset", description: 'คลิกที่ Keyset ID เพื่อเพิ่มเคาน์เตอร์ derivation path สำหรับ keysets ใน Wallet ของคุณ สิ่งนี้มีประโยชน์หากคุณเห็นข้อผิดพลาด "outputs have already been signed"', counter: "ตัวนับ: {count}", }, unset_reserved: { button: "ยกเลิกการสำรองโทเค็นทั้งหมด", description: 'Wallet นี้จะทำเครื่องหมาย ecash ขาออกที่รอดำเนินการว่าถูกสำรอง (และหักออกจากยอดคงเหลือของคุณ) เพื่อป้องกันความพยายามในการใช้จ่ายซ้ำ ปุ่มนี้จะยกเลิกการสำรองโทเค็นทั้งหมดเพื่อให้สามารถใช้ได้อีกครั้ง หากคุณทำเช่นนี้ Wallet ของคุณอาจมีหลักฐานที่ใช้แล้ว กดปุ่ม "ลบหลักฐานที่ใช้แล้ว" เพื่อกำจัดออกไป', }, show_onboarding: { button: "แสดงหน้าแนะนำ", description: "แสดงหน้าจอแนะนำอีกครั้ง", }, reset_wallet: { button: "รีเซ็ตข้อมูล Wallet", description: "รีเซ็ตข้อมูล Wallet ของคุณ คำเตือน: สิ่งนี้จะลบทุกอย่าง! ตรวจสอบให้แน่ใจว่าคุณสร้างการสำรองข้อมูลก่อน", confirm_question: "คุณแน่ใจหรือไม่ว่าต้องการลบข้อมูล Wallet ของคุณ?", cancel: "ยกเลิก", confirm: "ลบ Wallet", }, export_wallet: { button: "ส่งออกข้อมูล Wallet", description: "ดาวน์โหลดข้อมูล Wallet ของคุณ คุณสามารถกู้คืน Wallet ของคุณจากไฟล์นี้บนหน้าจอต้อนรับของ Wallet ใหม่ ไฟล์นี้จะไม่ตรงกันหากคุณยังคงใช้ Wallet ของคุณหลังจากส่งออก", }, }, }, }, NoMintWarnBanner: { title: "เข้าร่วม Mint", subtitle: "คุณยังไม่ได้เข้าร่วม Cashu mint ใด ๆ เพิ่ม URL ของ mint ในการตั้งค่าหรือรับ ecash จาก mint ใหม่เพื่อเริ่มต้น", actions: { add_mint: { label: "@:global.actions.add_mint.label", }, receive: { label: "รับ Ecash", }, }, }, WalletPage: { actions: { send: { label: "@:global.actions.send.label", }, receive: { label: "@:global.actions.receive.label", }, }, tabs: { history: { label: "ประวัติ", }, invoices: { label: "ใบแจ้งหนี้", }, mints: { label: "Mints", }, }, install: { text: "ติดตั้ง", tooltip: "ติดตั้ง Cashu", }, }, AlreadyRunning: { title: "ไม่.", text: "มีแท็บอื่นกำลังทำงานอยู่แล้ว ปิดแท็บนี้แล้วลองอีกครั้ง", actions: { retry: { label: "ลองใหม่", }, }, }, ErrorNotFound: { title: "404", text: "อุ๊บส์. ไม่มีอะไรที่นี่…", actions: { home: { label: "กลับหน้าหลัก", }, }, }, BalanceView: { mintUrl: { label: "Mint", }, mintBalance: { label: "ยอดเงินคงเหลือ", }, mintError: { label: "ข้อผิดพลาด Mint", }, pending: { label: "รอดำเนินการ", tooltip: "ตรวจสอบโทเค็นที่รอดำเนินการทั้งหมด", }, }, WelcomePage: { actions: { previous: { label: "ก่อนหน้า", }, next: { label: "ถัดไป", }, }, }, WelcomeSlide1: { title: "ยินดีต้อนรับสู่ Cashu", text: "Cashu.me คือ Wallet Bitcoin ที่ฟรีและเป็นโอเพนซอร์ส ซึ่งใช้ ecash เพื่อรักษาความปลอดภัยและความเป็นส่วนตัวของเงินของคุณ", actions: { more: { label: "คลิกเพื่อเรียนรู้เพิ่มเติม", }, }, p1: { text: "Cashu เป็นโปรโตคอล ecash ที่ฟรีและเป็นโอเพนซอร์สสำหรับ Bitcoin คุณสามารถเรียนรู้เพิ่มเติมได้ที่ { link }", link: { text: "cashu.space", }, }, p2: { text: "Wallet นี้ไม่มีส่วนเกี่ยวข้องกับ Mint ใด ๆ ในการใช้ Wallet นี้ คุณต้องเชื่อมต่อกับ Mint Cashu ที่คุณเชื่อถืออย่างน้อยหนึ่งแห่ง", }, p3: { text: "Wallet นี้จัดเก็บ ecash ที่มีเพียงคุณเท่านั้นที่เข้าถึงได้ หากคุณลบข้อมูลเบราว์เซอร์ของคุณโดยไม่มีการสำรองวลีสำหรับกู้คืน คุณจะสูญเสียโทเค็นของคุณ", }, p4: { text: "Wallet นี้อยู่ในช่วงเบต้า เราไม่รับผิดชอบต่อบุคคลที่สูญเสียการเข้าถึงเงินทุน ใช้งานด้วยความเสี่ยงของคุณเอง! รหัสนี้เป็นโอเพนซอร์สและได้รับอนุญาตภายใต้ใบอนุญาต MIT", }, }, WelcomeSlide2: { title: "ติดตั้ง PWA", alt: { pwa_example: "ตัวอย่างการติดตั้ง PWA" }, installing: "กำลังติดตั้ง…", instruction: { intro: { text: "เพื่อประสบการณ์ที่ดีที่สุด ใช้ Wallet นี้กับเว็บเบราว์เซอร์พื้นฐานของอุปกรณ์ของคุณเพื่อติดตั้งเป็น Progressive Web App ทำสิ่งนี้ทันที", }, android: { title: "Android (Chrome)", step1: { item: "1. { icon } { text }", text: "แตะเมนู (มุมขวาบน)", }, step2: { item: "2. { icon } { text }", text: "กด { buttonText }", buttonText: "@:AndroidPWAPrompt.buttonText", }, }, ios: { title: "iOS (Safari)", step1: { item: "1. { icon } { text }", text: "แตะ แชร์ (ด้านล่าง)", }, step2: { item: "2. { icon } { text }", text: "กด { buttonText }", buttonText: "@:iOSPWAPrompt.buttonText", }, }, outro: { text: "เมื่อคุณติดตั้งแอปนี้บนอุปกรณ์ของคุณแล้ว ให้ปิดหน้าต่างเบราว์เซอร์นี้และใช้แอปจากหน้าจอหลักของคุณ", }, }, pwa: { success: { title: "สำเร็จ!", text: "คุณกำลังใช้ Cashu เป็น PWA ปิดหน้าต่างเบราว์เซอร์อื่นที่เปิดอยู่และใช้แอปจากหน้าจอหลักของคุณ", nextSteps: "ตอนนี้คุณสามารถปิดแท็บนี้และเปิดแอปจากหน้าจอหลักได้", }, }, }, iOSPWAPrompt: { text: "แตะ { icon } และ { buttonText }", buttonText: "เพิ่มไปยังหน้าจอหลัก", }, AndroidPWAPrompt: { text: "แตะ { icon } และ { buttonText }", buttonText: "เพิ่มไปยังหน้าจอหลัก", }, WelcomeSlide3: { title: "วลีสำหรับกู้คืนของคุณ", text: "จัดเก็บวลีสำหรับกู้คืนของคุณไว้ในตัวจัดการรหัสผ่านหรือบนกระดาษ วลีสำหรับกู้คืนของคุณเป็นวิธีเดียวที่จะกู้คืนเงินทุนของคุณได้หากคุณสูญเสียการเข้าถึงอุปกรณ์นี้", inputs: { seed_phrase: { label: "วลีสำหรับกู้คืน", caption: "คุณสามารถดูวลีสำหรับกู้คืนของคุณได้ในการตั้งค่า", }, checkbox: { label: "ฉันได้จดไว้แล้ว", }, }, }, WelcomeSlide4: { title: "เงื่อนไข", actions: { more: { label: "อ่านข้อกำหนดในการให้บริการ", }, }, inputs: { checkbox: { label: "ฉันได้อ่านและยอมรับข้อกำหนดและเงื่อนไขเหล่านี้", }, }, }, WelcomeSlideChoice: { title: "ตั้งค่ากระเป๋าเงินของคุณ", text: "คุณต้องการกู้คืนจากวลีสำหรับกู้คืนหรือสร้างกระเป๋าเงินใหม่?", options: { new: { title: "สร้างกระเป๋าเงินใหม่", subtitle: "สร้างวลีสำหรับกู้คืนใหม่และเพิ่ม Mint", }, recover: { title: "กู้คืนกระเป๋าเงิน", subtitle: "ป้อนวลีสำหรับกู้คืนของคุณ กู้คืน Mint และ ecash", }, }, }, WelcomeMintSetup: { title: "เพิ่ม Mint", text: "Mint คือเซิร์ฟเวอร์ที่ช่วยให้คุณส่งและรับ ecash เลือก Mint ที่ค้นพบหรือเพิ่มด้วยตนเอง คุณสามารถข้ามและเพิ่มในภายหลังได้", sections: { your_mints: "Mint ของคุณ" }, restoring: "กำลังกู้คืน Mint…", placeholder: { mint_url: "https://" }, }, WelcomeRecoverSeed: { title: "ป้อนวลีสำหรับกู้คืนของคุณ", text: "วางหรือพิมพ์วลี 12 คำเพื่อกู้คืน", inputs: { word: "คำ { index }" }, actions: { paste_all: "วางทั้งหมด" }, disclaimer: "วลีสำหรับกู้คืนใช้เฉพาะในเครื่องเพื่อสร้างคีย์กระเป๋าเงินของคุณ", }, WelcomeRestoreEcash: { title: "กู้คืน ecash ของคุณ", text: "สแกนหา proof ที่ยังไม่ถูกใช้บน Mint ที่กำหนดค่าไว้และเพิ่มลงในกระเป๋าเงินของคุณ", }, MintRatings: { title: "รีวิว Mint", reviews: "รีวิว", ratings: "คะแนน", no_reviews: "ไม่พบบทรีวิว", your_review: "รีวิวของคุณ", no_reviews_to_display: "ไม่มีรีวิวที่จะแสดง", no_rating: "ไม่มีคะแนน", out_of: "จาก", rows: "Reviews", sort: "เรียงลำดับ", sort_options: { newest: "ใหม่ที่สุด", oldest: "เก่าที่สุด", highest: "สูงที่สุด", lowest: "ต่ำที่สุด", }, actions: { write_review: "เขียนรีวิว" }, empty_state_subtitle: "ช่วยเหลือโดยการเขียนรีวิว แบ่งปันประสบการณ์ของคุณกับ Mint นี้และช่วยเหลือผู้อื่นโดยการเขียนรีวิว", }, CreateMintReview: { title: "รีวิว Mint", publishing_as: "เผยแพร่เป็น", inputs: { rating: { label: "คะแนน" }, review: { label: "รีวิว (ไม่บังคับ)" }, }, actions: { publish: { label: "เผยแพร่", in_progress: "กำลังเผยแพร่…" }, }, }, RestoreView: { seed_phrase: { label: "กู้คืนจากวลีสำหรับกู้คืน", caption: "ป้อนวลีสำหรับกู้คืนของคุณเพื่อกู้คืน Wallet ของคุณ ก่อนที่จะกู้คืน ตรวจสอบให้แน่ใจว่าคุณได้เพิ่ม Mint ทั้งหมดที่คุณเคยใช้มาก่อน", inputs: { seed_phrase: { label: "วลีสำหรับกู้คืน", caption: "คุณสามารถดูวลีสำหรับกู้คืนของคุณได้ในการตั้งค่า", }, }, }, information: { label: "ข้อมูล", caption: "วิซาร์ดจะกู้คืน ecash จากวลีสำหรับกู้คืนอื่นเท่านั้น คุณจะไม่สามารถใช้วลีสำหรับกู้คืนนี้หรือเปลี่ยนวลีสำหรับกู้คืนของ Wallet ที่คุณกำลังใช้อยู่ได้ ซึ่งหมายความว่า ecash ที่กู้คืนจะไม่ได้รับการป้องกันโดยวลีสำหรับกู้คืนปัจจุบันของคุณ ตราบใดที่คุณยังไม่ได้ส่ง ecash ให้ตัวเองหนึ่งครั้ง", }, restore_mints: { label: "กู้คืน Mints", caption: 'เลือก Mint ที่จะกู้คืน คุณสามารถเพิ่ม Mint เพิ่มเติมในหน้าจอหลักภายใต้ "Mints" และกู้คืนได้ที่นี่', }, actions: { paste: { error: "อ่านเนื้อหาในคลิปบอร์ดไม่สำเร็จ", }, validate: { error: "Mnemonic ควรมีอย่างน้อย 12 คำ", }, select_all: { label: "เลือกทั้งหมด", }, deselect_all: { label: "ไม่เลือกทั้งหมด", }, restore: { label: "กู้คืน", in_progress: "กำลังกู้คืน Mint…", error: "ข้อผิดพลาดในการกู้คืน Mint: { error }", }, restore_all_mints: { label: "กู้คืน Mints ทั้งหมด", in_progress: "กำลังกู้คืน Mint { index } จาก { length }…", success: "กู้คืนสำเร็จ", error: "ข้อผิดพลาดในการกู้คืน Mints: { error }", }, restore_selected_mints: { label: "กู้คืน Mint ที่เลือก ({count})", in_progress: "กำลังกู้คืน Mint {index} จาก {length} ...", success: "กู้คืน {count} Mint(s) สำเร็จ", error: "ข้อผิดพลาดในการกู้คืน Mint ที่เลือก: {error}", }, }, nostr_mints: { label: "กู้คืน Mint จาก Nostr", caption: "ค้นหาการสำรองข้อมูล Mint ที่เก็บไว้ใน Nostr relays โดยใช้วลีสำหรับกู้คืนของคุณ สิ่งนี้จะช่วยให้คุณค้นพบ Mint ที่คุณเคยใช้มาก่อน", search_button: "ค้นหาการสำรองข้อมูล Mint", select_all: "เลือกทั้งหมด", deselect_all: "ไม่เลือกทั้งหมด", backed_up: "สำรองแล้ว", already_added: "เพิ่มแล้ว", add_selected: "เพิ่มที่เลือก ({count})", no_backups_found: "ไม่พบการสำรองข้อมูล Mint", no_backups_hint: "ตรวจสอบให้แน่ใจว่าได้เปิดใช้งานการสำรองข้อมูล Nostr mint ในการตั้งค่าเพื่อสำรองข้อมูลรายการ Mint ของคุณโดยอัตโนมัติ", invalid_mnemonic: "โปรดป้อนวลีสำหรับกู้คืนที่ถูกต้องก่อนค้นหา", search_error: "ไม่สามารถค้นหาการสำรองข้อมูล Mint ได้", add_error: "ไม่สามารถเพิ่ม Mint ที่เลือกได้", }, }, MintSettings: { add: { title: "เพิ่ม Mint", description: "ป้อน URL ของ Cashu mint เพื่อเชื่อมต่อ Wallet นี้ไม่มีส่วนเกี่ยวข้องกับ Mint ใดๆ", inputs: { nickname: { placeholder: "ชื่อเล่น (เช่น Testnet)", }, }, actions: { add_mint: { label: "@:global.actions.add_mint.label", error_invalid_url: "URL ไม่ถูกต้อง", }, scan: { label: "สแกนรหัส QR", }, }, }, discover: { title: "สำรวจ Mints", overline: "สำรวจ", caption: "สำรวจ Mints ที่ผู้ใช้คนอื่นแนะนำบน nostr", actions: { discover: { label: "สำรวจ Mints", in_progress: "กำลังโหลด…", error_no_mints: "ไม่พบ Mints", success: "พบ { length } Mints", }, }, recommendations: { overline: "พบ { length } Mints", caption: "Mints เหล่านี้ถูกแนะนำโดยผู้ใช้ Nostr คนอื่น ๆ โปรดใช้ความระมัดระวังและทำการวิจัยของคุณเองก่อนใช้ Mint", actions: { browse: { label: "คลิกเพื่อเรียกดู Mints", }, }, }, }, swap: { title: "แลกเปลี่ยน", overline: "การแลกเปลี่ยนระหว่าง Mints", caption: "แลกเปลี่ยนเงินระหว่าง Mints ผ่าน Lightning หมายเหตุ: เผื่อค่าธรรมเนียม Lightning ที่อาจเกิดขึ้น หากการชำระเงินขาเข้าไม่สำเร็จ ให้ตรวจสอบใบแจ้งหนี้ด้วยตนเอง", inputs: { from: { label: "จาก", }, to: { label: "ถึง", }, amount: { label: "จำนวน ({ ticker }) )", }, }, actions: { swap: { label: "@:global.actions.swap.label", in_progress: "@:MintSettings.swap.actions.swap.label", }, }, }, error_badge: "ข้อผิดพลาด", reviews_text: "รีวิว", no_reviews_yet: "ยังไม่มีรีวิว", discover_mints_button: "สำรวจ Mints", }, QrcodeReader: { progress: { text: "{ percentage }{ addon }", percentage: "{ percentage }%", keep_scanning_text: " - สแกนต่อไป", }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, }, }, InvoiceDetailDialog: { title: "รับ Lightning", create_invoice_title: "สร้างใบแจ้งหนี้", inputs: { amount: { label: "จำนวน ({ ticker }) *", }, }, actions: { close: { label: "@:global.actions.close.label", }, create: { label: "สร้างใบแจ้งหนี้", label_blocked: "กำลังสร้างใบแจ้งหนี้…", in_progress: "กำลังสร้าง", }, }, invoice: { caption: "ใบแจ้งหนี้ Lightning", status_paid_text: "ชำระแล้ว!", actions: { close: { label: "@:global.actions.close.label", }, copy: { label: "@:global.actions.copy.label", }, }, }, }, SendDialog: { title: "ส่ง", actions: { ecash: { label: "Ecash", error_no_mints: "ไม่มี Mints ให้เลือก", }, lightning: { label: "Lightning", error_no_mints: "ไม่มี Mints ให้เลือก", }, }, }, SendTokenDialog: { title: "ส่ง Ecash", title_ecash_text: "Ecash", badge_offline_text: "ออฟไลน์", inputs: { amount: { label: "จำนวน ({ ticker }) *", invalid_too_much_error_text: "มากเกินไป", }, p2pk_pubkey: { label: "คีย์สาธารณะของผู้รับ", label_invalid: "คีย์สาธารณะของผู้รับ", }, }, actions: { close: { label: "@:global.actions.close.label", }, close_card_scanner: { label: "@:global.actions.close.label", }, copy_emoji: { label: "🥜", tooltip_text: "คัดลอก Emoji", }, copy_tokens: { label: "@:global.actions.copy.label", }, copy_link: { tooltip_text: "คัดลอกลิงก์", }, share: { tooltip_text: "แชร์ ecash", }, lock: { label: "@:global.actions.lock.label", }, paste_p2pk_pubkey: { tooltip_text: "@:global.actions.paste.label", }, send: { label: "@:global.actions.send.label", }, delete: { tooltip_text: "ลบออกจากประวัติ", }, write_tokens_to_card: { tooltips: { ndef_supported_text: "แฟลชไปยังการ์ด NFC", ndef_unsupported_text: "ไม่รองรับ NDEF", }, }, }, }, ReceiveDialog: { title: "รับ", actions: { ecash: { label: "Ecash", error_no_mints: "ไม่มี Mints ให้เลือก", }, lightning: { label: "Lightning", error_no_mints: "คุณต้องเชื่อมต่อกับ Mint เพื่อรับผ่าน Lightning", }, }, }, ReceiveEcashDrawer: { title: "รับ Ecash", actions: { paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, request: { label: "ขอ", }, lock: { label: "@:global.actions.lock.label", }, nfc: { label: "NFC", scanning_text: "กำลังสแกน…", }, }, }, ReceiveTokenDialog: { title: "รับ Ecash", title_ecash_text: "Ecash", inputs: { tokens_base64: { label: "วางโทเค็น Cashu", }, }, errors: { invalid_token: { label: "โทเค็นไม่ถูกต้อง", }, p2pk_lock_mismatch: { label: "ไม่สามารถรับได้ คีย์ล็อก P2PK ของโทเค็นนี้ไม่ตรงกับคีย์สาธารณะของคุณ", }, }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, scan: { label: "@:global.actions.scan.label", }, receive: { label: "@:global.actions.receive.label", label_known_mint: "@:ReceiveTokenDialog.actions.receive.label", label_adding_mint: "กำลังเพิ่ม Mint…", }, swap: { label: "@:global.actions.swap.label", tooltip_text: "แลกเปลี่ยนไปยัง Mint ที่เชื่อถือได้", caption: "แลกเปลี่ยน { value }", }, cancel_swap: { label: "@:global.actions.cancel.label", tooltip_text: "ยกเลิกการแลกเปลี่ยน", }, confirm_swap: { label: "@:ReceiveTokenDialog.actions.swap.label", tooltip_text: "@:ReceiveTokenDialog.actions.swap.tooltip_text", in_progress: "@:ReceiveTokenDialog.actions.confirm_swap.label", }, later: { label: "รับภายหลัง", tooltip_text: "เพิ่มไปยังประวัติเพื่อรับภายหลัง", already_in_history_success_text: "Ecash อยู่ในประวัติแล้ว", added_to_history_success_text: "เพิ่ม Ecash ในประวัติแล้ว", }, nfc: { label: "NFC", tooltips: { ndef_supported_text: "อ่านจากการ์ด NFC", ndef_unsupported_text: "ไม่รองรับ NDEF", }, }, }, }, P2PKDialog: { p2pk: { caption: "คีย์ P2PK", description: "รับ ecash ที่ล็อกด้วยคีย์นี้", used_warning_text: "คำเตือน: คีย์นี้เคยถูกใช้มาก่อน ใช้คีย์ใหม่เพื่อความเป็นส่วนตัวที่ดีขึ้น", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_key: { label: "สร้างคีย์ใหม่", }, }, }, PaymentRequestDialog: { payment_request: { caption: "คำขอชำระเงิน", description: "รับการชำระเงินผ่าน Nostr", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_request: { label: "คำขอใหม่", }, add_amount: { label: "เพิ่มจำนวนเงิน", }, use_active_mint: { label: "Mint ใดก็ได้", }, }, inputs: { amount: { placeholder: "ป้อนจำนวนเงิน", }, }, }, NumericKeyboard: { actions: { close: { label: "@:global.actions.close.label", closed_info_text: "ปิดใช้งานแป้นพิมพ์แล้ว คุณสามารถเปิดใช้งานแป้นพิมพ์ได้อีกครั้งในการตั้งค่า", }, enter: { label: "@:global.actions.enter.label", }, }, }, NWCDialog: { nwc: { caption: "Nostr Wallet Connect", description: "ควบคุม Wallet ของคุณจากระยะไกลด้วย NWC กดที่รหัส QR เพื่อเชื่อมโยง Wallet ของคุณกับแอปพลิเคชันที่เข้ากันได้", warning_text: "คำเตือน: ใครก็ตามที่เข้าถึงสตริงการเชื่อมต่อนี้สามารถเริ่มการชำระเงินจาก Wallet ของคุณได้ ห้ามแบ่งปัน!", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, }, }, MintMotdMessage: { title: "ข้อความจาก Mint", }, MintDetailsDialog: { contact: { title: "ติดต่อ", }, details: { title: "รายละเอียด Mint", url: { label: "URL", }, nuts: { label: "Nuts", actions: { show: { label: "แสดงทั้งหมด", }, hide: { label: "ซ่อน", }, }, }, currency: { label: "สกุลเงิน", }, currencies: { label: "@:MintDetailsDialog.details.currency.label", }, version: { label: "เวอร์ชัน", }, }, actions: { title: "การดำเนินการ", copy_mint_url: { label: "คัดลอก Mint URL", }, delete: { label: "ลบ Mint", }, edit: { label: "แก้ไข Mint", }, }, }, ChooseMint: { title: "เลือก Mint", badge_mint_error_text: "ข้อผิดพลาด", badge_option_mint_error_text: "@:ChooseMint.badge_mint_error_text", }, HistoryTable: { empty_text: "ยังไม่มีประวัติ", row: { type_label: "Ecash", date_label: "{ value } ที่ผ่านมา", }, actions: { check_status: { tooltip_text: "ตรวจสอบสถานะ", }, receive: { tooltip_text: "รับ", }, filter_pending: { label: "กรองที่รอดำเนินการ", }, show_all: { label: "แสดงทั้งหมด", }, }, old_token_not_found_error_text: "ไม่พบโทเค็นเก่า", }, InvoiceTable: { empty_text: "ยังไม่มีใบแจ้งหนี้", row: { type_label: "Lightning", type_tooltip_text: "คลิกเพื่อคัดลอก", date_label: "{ value } ที่ผ่านมา", }, actions: { check_status: { tooltip_text: "ตรวจสอบสถานะ", }, filter_pending: { label: "กรองที่รอดำเนินการ", }, show_all: { label: "แสดงทั้งหมด", }, }, }, RemoveMintDialog: { title: "คุณแน่ใจหรือไม่ว่าต้องการลบ Mint นี้?", nickname: { label: "ชื่อเล่น", }, balances: { label: "ยอดเงินคงเหลือ", }, warning_text: "หมายเหตุ: เนื่องจาก Wallet นี้มีความระมัดระวังสูง ecash ของคุณจาก Mint นี้จะไม่ถูกลบจริง แต่จะยังคงเก็บไว้ในอุปกรณ์ของคุณ คุณจะเห็นมันปรากฏขึ้นอีกครั้งหากคุณเพิ่ม Mint นี้อีกครั้งในภายหลัง", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { confirm: { label: "ลบ Mint", }, cancel: { label: "@:global.actions.cancel.label", }, }, }, ParseInputComponent: { placeholder: { default: "Cashu token หรือที่อยู่ Lightning", receive: "Cashu token", pay: "ที่อยู่ Lightning หรือใบแจ้งหนี้", }, qr_scanner: { title: "สแกนรหัส QR", description: "แตะเพื่อสแกนที่อยู่", }, paste_button: { label: "@:global.actions.paste.label", }, }, PayInvoiceDialog: { input_data: { title: "ชำระเงิน Lightning", inputs: { invoice_data: { label: "ใบแจ้งหนี้หรือที่อยู่ Lightning", }, }, actions: { close: { label: "@:global.actions.close.label", }, enter: { label: "@:global.actions.enter.label", }, paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, }, }, lnurlpay: { amount_exact_label: "{ payee } กำลังขอ { value } { ticker }", amount_range_label: "{ payee } กำลังขอ{br}ระหว่าง { min } และ { max } { ticker }", sending_to_lightning_address: "กำลังส่งไปยัง { address }", inputs: { amount: { label: "จำนวน ({ ticker }) *", }, comment: { label: "ความคิดเห็น (ไม่บังคับ)", }, }, actions: { close: { label: "@:global.actions.close.label", }, send: { label: "@:global.actions.send.label", }, }, }, invoice: { title: "ชำระเงิน { value }", paying: "กำลังชำระเงิน", paid: "ชำระแล้ว", fee: "ค่าธรรมเนียม", memo: { label: "บันทึก", }, processing_info_text: "กำลังประมวลผล…", balance_too_low_warning_text: "ยอดเงินคงเหลือต่ำเกินไป", actions: { close: { label: "@:global.actions.close.label", }, pay: { label: "ชำระเงิน", in_progress: "@:PayInvoiceDialog.invoice.processing_info_text", error: "ข้อผิดพลาด", }, }, }, }, EditMintDialog: { title: "แก้ไข Mint", inputs: { nickname: { label: "ชื่อเล่น", }, mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, update: { label: "@:global.actions.update.label", }, }, }, AddMintDialog: { title: "คุณเชื่อถือ Mint นี้หรือไม่?", description: "ก่อนที่จะใช้ Mint นี้ ตรวจสอบให้แน่ใจว่าคุณเชื่อถือได้ Mints อาจกลายเป็นอันตรายหรือหยุดดำเนินการได้ทุกเมื่อ", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, add_mint: { label: "@:global.actions.add_mint.label", in_progress: "กำลังเพิ่ม Mint", }, }, }, restore: { mnemonic_error_text: "โปรดป้อน mnemonic", restore_mint_error_text: "ข้อผิดพลาดในการกู้คืน Mint: { error }", prepare_info_text: "กำลังเตรียมกระบวนการกู้คืน…", restored_proofs_for_keyset_info_text: "กู้คืน { restoreCounter } proofs สำหรับ keyset { keysetId }", checking_proofs_for_keyset_info_text: "กำลังตรวจสอบ proofs { startIndex } ถึง { endIndex } สำหรับ keyset { keysetId }", no_proofs_info_text: "ไม่พบ proofs ที่จะกู้คืน", restored_amount_success_text: "กู้คืน { amount }", }, swap: { in_progress_warning_text: "กำลังดำเนินการแลกเปลี่ยน", invalid_swap_data_error_text: "ข้อมูลการแลกเปลี่ยนไม่ถูกต้อง", swap_error_text: "ข้อผิดพลาดในการแลกเปลี่ยน", }, TokenInformation: { fee: "ค่าธรรมเนียม", unit: "หน่วย", fiat: "สกุลเงิน", p2pk: "P2PK", locked: "ล็อค", locked_to_you: "ล็อคให้คุณ", mint: "โรงกษาปณ์", memo: "บันทึก", payment_request: "คำขอชำระเงิน", nostr: "Nostr", token_copied: "คัดลอกโทเค็นไปยังคลิปบอร์ดแล้ว", }, }; ================================================ FILE: src/i18n/tr-TR/index.ts ================================================ export default { MultinutPicker: { payment: "Multinut ödeme", selectMints: "Ödeme yapmak için bir veya birden fazla mint seçin.", totalSelectedBalance: "Seçilen Toplam Bakiye", multiMintPay: "Çoklu-Mint Ödeme", balanceNotEnough: "Çoklu mint bakiyesi bu faturayı karşılamaya yetmiyor", failed: "İşlenemedi: {error}", paid: "Lightning ile {amount} ödendi", }, // merged into single Settings block above advanced: { developer: {}, }, global: { copy_to_clipboard: { success: "Panoya kopyalandı!", }, actions: { add_mint: { label: "Nane ekle", }, cancel: { label: "İptal", }, copy: { label: "Kopyala", }, close: { label: "Kapat", }, enter: { label: "Gir", }, lock: { label: "Kilitle", }, paste: { label: "Yapıştır", }, receive: { label: "Al", }, scan: { label: "Tara", }, send: { label: "Gönder", }, swap: { label: "Değiştir", }, update: { label: "Güncelle", }, }, inputs: { mint_url: { label: "Nane URL'si", }, }, }, wallet: { notifications: { balance_too_low: "Bakiye çok düşük", received: "{amount} alındı", fee: " (ücret: {fee})", could_not_request_mint: "Nane isteği yapılamadı", invoice_still_pending: "Fatura hala beklemede", paid_lightning: "Lightning üzerinden {amount} ödendi", payment_pending_refresh: "Ödeme beklemede. Faturayı manuel olarak yenileyin.", sent: "{amount} gönderildi", token_still_pending: "Token hala beklemede", received_lightning: "Lightning üzerinden {amount} alındı", lightning_payment_failed: "Lightning ödemesi başarısız oldu", failed_to_decode_invoice: "Fatura çözülemedi", invalid_lnurl: "Geçersiz LNURL", lnurl_error: "LNURL hatası", no_amount: "Tutar yok", no_lnurl_data: "LNURL verisi yok", no_price_data: "Fiyat verisi yok.", please_try_again: "Lütfen tekrar deneyin.", }, mint: { notifications: { already_added: "Nane zaten eklenmiş", added: "Nane eklendi", not_found: "Nane bulunamadı", activation_failed: "Nane etkinleştirmesi başarısız oldu", no_active_mint: "Aktif nane yok", unit_activation_failed: "Birim etkinleştirmesi başarısız oldu", unit_not_supported: "Birim nane tarafından desteklenmiyor", activated: "Nane etkinleştirildi", could_not_connect: "Naneye bağlanılamadı", could_not_get_info: "Nane bilgisi alınamadı", could_not_get_keys: "Nane anahtarları alınamadı", could_not_get_keysets: "Nane anahtar setleri alınamadı", removed: "Nane kaldırıldı", error: "Nane hatası", mint_validation_error: "Mint doğrulama hatası", }, }, }, MainHeader: { menu: { settings: { title: "Ayarlar", settings: { title: "Ayarlar", caption: "Cüzdan yapılandırması", }, }, terms: { title: "Şartlar", terms: { title: "Şartlar", caption: "Hizmet Şartları", }, }, links: { title: "Bağlantılar", cashuSpace: { title: "Cashu.space", caption: "cashu.space", }, github: { title: "Github", caption: "github.com/cashubtc", }, telegram: { title: "Telegram", caption: "t.me/CashuMe", }, twitter: { title: "Twitter", caption: "{'@'}CashuBTC", }, donate: { title: "Bağış yap", caption: "Cashu'yu Destekle", }, }, }, offline: { warning: { text: "Çevrimdışı", }, }, reload: { warning: { text: "{ countdown } içinde yeniden yükle", }, }, staging: { warning: { text: "Hazırlık aşaması – gerçek fonlarla kullanmayın!", }, }, }, FullscreenHeader: { actions: { back: { label: "Cüzdan", }, }, }, Settings: { language: { title: "Dil", description: "Lütfen aşağıdaki listeden tercih ettiğiniz dili seçin.", }, sections: { backup_restore: "YEDEKLE & GERİ YÜKLE", lightning_address: "LIGHTNING ADRESİ", nostr_keys: "NOSTR ANAHTARLARI", nostr: { title: "NOSTR", relays: { expand_label: "Röleleri düzenlemek için tıklayın", add: { title: "Röle ekle", description: "Cüzdanınız ödeme istekleri, nostr cüzdan bağlantısı ve yedeklemeler gibi nostr işlemleri için bu röleleri kullanır.", }, list: { title: "Röleler", description: "Cüzdanınız bu rölelere bağlanacak.", copy_tooltip: "Röleyi kopyala", remove_tooltip: "Röleyi kaldır", }, }, }, payment_requests: "ÖDEME TALEPLERİ", nostr_wallet_connect: "NOSTR CÜZDAN BAĞLANTISI", hardware_features: "DONANIM ÖZELLİKLERİ", p2pk_features: "P2PK ÖZELLİKLERİ", privacy: "GİZLİLİK", experimental: "DENEYSEL", appearance: "GÖRÜNÜM", }, backup_restore: { backup_seed: { title: "Kurtarma kelimelerini yedekle", description: "Kurtarma kelimeleriniz cüzdanınızı geri yükleyebilir. Güvenli ve gizli tutun.", seed_phrase_label: "Kurtarma kelimeleri", }, restore_ecash: { title: "Ecash'i geri yükle", description: "Geri yükleme sihirbazı, kayıp ecash'inizi anımsatıcı kurtarma kelimelerinden kurtarmanıza olanak tanır. Mevcut cüzdanınızın kurtarma kelimeleri etkilenmeyecektir, sihirbaz yalnızca başka bir kurtarma kelimesinden ecash'i geri yüklemenizi sağlayacaktır.", button: "Geri Yükle", }, }, lightning_address: { title: "Lightning adresi", description: "Lightning adresinize ödeme alın.", enable: { toggle: "Etkinleştir", description: "npub.cash ile Lightning adresi", }, address: { copy_tooltip: "Lightning adresini kopyala", }, automatic_claim: { toggle: "Otomatik olarak talep et", description: "Gelen ödemeleri otomatik olarak alın.", }, npc_v2: { choose_mint_title: "npub.cash v2 için mint seçin", choose_mint_placeholder: "Bir mint seçin…", }, }, nostr_keys: { title: "Nostr anahtarlarınız", description: "Lightning adresiniz için nostr anahtarlarını ayarlayın.", wallet_seed: { title: "Cüzdan kurtarma kelimeleri", description: "Cüzdan kurtarma kelimelerinden nostr anahtar çifti oluştur", copy_nsec: "nsec'i kopyala", }, nsec_bunker: { title: "Nsec Bunker", description: "Bir NIP-46 bunker kullanın", delete_tooltip: "Bağlantıyı sil", }, use_nsec: { title: "nsec'inizi kullanın", description: "Bu yöntem tehlikelidir ve önerilmez", delete_tooltip: "nsec'i sil", }, signing_extension: { title: "İmzalama uzantısı", description: "Bir NIP-07 imzalama uzantısı kullanın", not_found: "NIP-07 imzalama uzantısı bulunamadı", }, }, payment_requests: { title: "Ödeme talepleri", description: "Ödeme talepleri, nostr aracılığıyla ödeme almanıza olanak tanır. Bunu etkinleştirirseniz, cüzdanınız nostr rölelerinize abone olacaktır.", enable_toggle: "Ödeme Taleplerini Etkinleştir", claim_automatically: { toggle: "Otomatik olarak talep et", description: "Gelen ödemeleri otomatik olarak alın.", }, }, nostr_wallet_connect: { title: "Nostr Cüzdan Bağlantısı (NWC)", description: "NWC'yi kullanarak cüzdanınızı başka herhangi bir uygulamadan kontrol edin.", enable_toggle: "NWC'yi Etkinleştir", payments_note: "NWC'yi yalnızca Bitcoin bakiyenizden ödemeler için kullanabilirsiniz. Ödemeler etkin nanenizden yapılacaktır.", connection: { copy_tooltip: "Bağlantı dizesini kopyala", qr_tooltip: "QR kodunu göster", allowance_label: "Kalan ödenek (sat)", }, }, hardware_features: { webnfc: { title: "WebNFC", description: "NFC kartlarına yazmak için kodlamayı seçin", text: { title: "Metin", description: "Token'ı düz metin olarak sakla", }, weburl: { title: "URL", description: "Bu cüzdanın URL'sini token ile sakla", }, binary: { title: "İkilik", description: "Token'ları ikili veri olarak sakla", }, quick_access: { toggle: "NFC'ye hızlı erişim", description: "Ecash Al menüsünde NFC kartlarını hızlıca tarayın. Bu seçenek Ecash Al menüsüne bir NFC düğmesi ekler.", }, }, }, p2pk_features: { title: "P2PK", description: "P2PK kilitli ecash almak için bir anahtar çifti oluşturun. Uyarı: Bu özellik deneyseldir. Yalnızca küçük miktarlarla kullanın. Özel anahtarlarınızı kaybederseniz, artık kimse ona kilitlenmiş ecash'in kilidini açamaz.", generate_button: "Anahtar oluştur", import_button: "nsec'i içe aktar", quick_access: { toggle: "Kilitlemeye hızlı erişim", description: "Bunu, P2PK kilitleme anahtarınızı ecash alma menüsünde hızlıca göstermek için kullanın.", }, keys_expansion: { label: "{count} anahtara göz atmak için tıklayın", used_badge: "kullanıldı", }, }, privacy: { title: "Gizlilik", description: "Bu ayarlar gizliliğinizi etkiler.", check_incoming: { toggle: "Gelen faturayı kontrol et", description: "Etkinleştirilirse, cüzdan arka planda en son faturayı kontrol edecektir. Bu, parmak izi almayı kolaylaştıran cüzdanın tepkiselliğini artırır. Ödenmemiş faturaları Faturalar sekmesinde manuel olarak kontrol edebilirsiniz.", }, check_startup: { toggle: "Başlangıçta bekleyen faturaları kontrol et", description: "Etkinleştirilirse, cüzdan başlangıçta son 24 saat içindeki bekleyen faturaları kontrol edecektir.", }, check_all: { toggle: "Tüm faturaları kontrol et", description: "Etkinleştirilirse, cüzdan iki haftaya kadar ödenmemiş faturaları arka planda periyodik olarak kontrol edecektir. Bu, parmak izi almayı kolaylaştıran cüzdanın çevrimiçi aktivitesini artırır. Ödenmemiş faturaları Faturalar sekmesinde manuel olarak kontrol edebilirsiniz.", }, check_sent: { toggle: "Gönderilen ecash'i kontrol et", description: "Etkinleştirilirse, cüzdan gönderilen token'ların kullanılıp kullanılmadığını belirlemek için periyodik arka plan kontrollerini kullanacaktır. Bu, parmak izi almayı kolaylaştıran cüzdanın çevrimiçi aktivitesini artırır.", }, websockets: { toggle: "WebSockets kullan", description: "Etkinleştirilirse, cüzdan ödenen faturalar ve nane'lerden harcanan token'larla ilgili güncellemeleri almak için uzun ömürlü WebSocket bağlantıları kullanacaktır. Bu, cüzdanın tepkiselliğini artırır ancak parmak izi almayı da kolaylaştırır.", }, bitcoin_price: { toggle: "Döviz kurunu Coinbase'den al", description: "Etkinleştirilirse, güncel Bitcoin döviz kuru coinbase.com'dan alınacak ve dönüştürülmüş bakiyeniz görüntülenecektir.", currency: { title: "Fiat Para Birimi", description: "Bitcoin fiyat gösterimi için fiat para birimini seçin.", }, }, }, experimental: { title: "Deneysel", description: "Bu özellikler deneyseldir.", receive_swaps: { toggle: "Takasları al", badge: "Beta", description: "Ecash Al iletişim kutusunda alınan Ecash'i etkin nanenizle takas etme seçeneği.", }, auto_paste: { toggle: "Ecash'i otomatik yapıştır", description: "Al, sonra Ecash, sonra Yapıştır düğmesine bastığınızda panonuzdaki ecash'i otomatik olarak yapıştırın. Otomatik yapıştırma iOS'ta UI hatalarına neden olabilir, sorun yaşıyorsanız kapatın.", }, auditor: { toggle: "Denetleyiciyi etkinleştir", badge: "Beta", description: "Etkinleştirilirse, cüzdan nane detayları iletişim kutusunda denetleyici bilgilerini görüntüler. Denetleyici, nane'lerin güvenilirliğini izleyen üçüncü taraf bir hizmettir.", url_label: "Denetleyici URL'si", api_url_label: "Denetleyici API URL'si", }, multinut: { toggle: "Multinut'u Etkinleştir", description: "Etkinleştirilirse, cüzdan faturaları aynı anda birden fazla nane'den ödemek için Multinut'u kullanacaktır.", }, nostr_mint_backup: { toggle: "Nostr'da nane listesini yedekle", description: "Etkinleştirilirse, nane listeniz yapılandırılmış Nostr anahtarlarınız kullanılarak otomatik olarak Nostr rölelerine yedeklenecektir. Bu, nane listenizi cihazlar arasında geri yüklemenizi sağlar.", notifications: { enabled: "Nostr nane yedeği etkinleştirildi", disabled: "Nostr nane yedeği devre dışı bırakıldı", failed: "Nostr nane yedeği etkinleştirilemedi", }, }, }, appearance: { bip177: { title: "Bitcoin sembolü", description: "sats yerine ₿ sembolünü kullan.", toggle: "₿ sembolünü kullan", }, keyboard: { title: "Ekran klavyesi", description: "Miktarları girmek için sayısal klavyeyi kullanın.", toggle: "Sayısal klavye kullan", toggle_description: "Etkinleştirilirse, miktarları girmek için sayısal klavye kullanılacaktır.", }, theme: { title: "Görünüm", description: "Cüzdanınızın görünümünü değiştirin.", tooltips: { mono: "mono", cyber: "siber", freedom: "özgürlük", nostr: "nostr", bitcoin: "bitcoin", mint: "nane", nut: "ceviz", blu: "mavi", flamingo: "flamingo", }, }, }, advanced: { title: "Gelişmiş", developer: { title: "Geliştirici ayarları", description: "Aşağıdaki ayarlar geliştirme ve hata ayıklama içindir.", new_seed: { button: "Yeni kurtarma kelimeleri oluştur", description: "Bu, yeni bir kurtarma kelimesi oluşturacaktır. Yeni bir kurtarma kelimesiyle geri yükleyebilmek için tüm bakiyenizi kendinize göndermelisiniz.", confirm_question: "Yeni bir kurtarma kelimesi oluşturmak istediğinizden emin misiniz?", cancel: "İptal", confirm: "Onayla", }, remove_spent: { button: "Harcanmış kanıtları kaldır", description: "Etkin nane'lerinizden ecash token'larının harcanıp harcanmadığını kontrol edin ve harcananları cüzdanınızdan kaldırın. Bunu yalnızca cüzdanınız takılı kalırsa kullanın.", }, debug_console: { button: "Hata Ayıklama Konsolunu Aç/Kapat", description: "Javascript hata ayıklama terminalini açın. Anlamadığınız hiçbir şeyi bu terminale yapıştırmayın. Bir hırsız sizi buraya kötü amaçlı kod yapıştırmaya kandırmaya çalışabilir.", }, export_proofs: { button: "Aktif kanıtları dışa aktar", description: "Aktif nane'den tüm bakiyenizi bir Cashu token'ı olarak panonuza kopyalayın. Bu yalnızca seçilen nane ve birimin token'larını dışa aktaracaktır. Tam bir dışa aktarma için farklı bir nane ve birim seçin ve tekrar dışa aktarın.", }, keyset_counters: { title: "Anahtar kümesi sayaçlarını artır", description: 'Cüzdanınızdaki anahtar kümeleri için türetme yolu sayaçlarını artırmak için anahtar kümesi kimliğine tıklayın. Bu, "çıktılar zaten imzalandı" hatasını görüyorsanız yararlıdır.', counter: "sayaç: {count}", }, unset_reserved: { button: "Tüm ayrılmış token'ları kaldır", description: "Bu cüzdan, çifte harcama girişimlerini önlemek için bekleyen giden ecash'i ayrılmış olarak işaretler (ve bakiyenizden düşer). Bu düğme tüm ayrılmış token'ları kaldıracaktır, böylece tekrar kullanılabilirler. Bunu yaparsanız, cüzdanınız harcanmış kanıtlar içerebilir. Onlardan kurtulmak için Harcanmış kanıtları kaldır düğmesine basın.", }, show_onboarding: { button: "Başlangıç ekranını göster", description: "Başlangıç ekranını tekrar gösterin.", }, reset_wallet: { button: "Cüzdan verilerini sıfırla", description: "Cüzdan verilerinizi sıfırlayın. Uyarı: Bu her şeyi siler! Önce bir yedek oluşturduğunuzdan emin olun.", confirm_question: "Cüzdan verilerinizi silmek istediğinizden emin misiniz?", cancel: "İptal", confirm: "Cüzdanı sil", }, export_wallet: { button: "Cüzdan verilerini dışa aktar", description: "Cüzdanınızın bir dökümünü indirin. Yeni bir cüzdanın karşılama ekranından bu dosyadan cüzdanınızı geri yükleyebilirsiniz. Bu dosya, dışa aktardıktan sonra cüzdanınızı kullanmaya devam ederseniz senkronize olmayacaktır.", }, }, }, web_of_trust: { title: "Güven ağı", known_pubkeys: "Bilinen pubkeyler: {wotCount}", continue_crawl: "Taramaya devam et", crawl_odell: "ODELL'İN WEB OF TRUST'unu tara", crawl_wot: "Web of trust tara", pause: "Duraklat", reset: "Sıfırla", progress: "{crawlProcessed} / {crawlTotal}", }, npub_cash: { use_npubx: "npubx.cash kullan", copy_lightning_address: "Lightning adresini kopyala", v2_mint: "npub.cash v2 mint", }, multinut: { use_multinut: "Multinut kullan", }, }, NoMintWarnBanner: { title: "Bir nane'ye katılın", subtitle: "Henüz bir Cashu nane'sine katılmadınız. Başlamak için ayarlardan bir nane URL'si ekleyin veya yeni bir nane'den ecash alın.", actions: { add_mint: { label: "@:global.actions.add_mint.label", }, receive: { label: "Ecash Al", }, }, }, WalletPage: { actions: { send: { label: "@:global.actions.send.label", }, receive: { label: "@:global.actions.receive.label", }, }, tabs: { history: { label: "Geçmiş", }, invoices: { label: "Faturalar", }, mints: { label: "Naneler", }, }, install: { text: "Yükle", tooltip: "Cashu'yu Yükle", }, }, AlreadyRunning: { title: "Hayır.", text: "Başka bir sekme zaten çalışıyor. Bu sekmeyi kapatın ve tekrar deneyin.", actions: { retry: { label: "Tekrar dene", }, }, }, ErrorNotFound: { title: "404", text: "Oops. Burada bir şey yok…", actions: { home: { label: "Ana sayfaya geri dön", }, }, }, BalanceView: { mintUrl: { label: "Nane", }, mintBalance: { label: "Bakiye", }, mintError: { label: "Nane hatası", }, pending: { label: "Beklemede", tooltip: "Tüm bekleyen token'ları kontrol et", }, }, WelcomePage: { actions: { previous: { label: "Önceki", }, next: { label: "Sonraki", }, }, }, WelcomeSlide1: { title: "Cashu'ya hoş geldiniz", text: "Cashu.me, fonlarınızı güvenli ve gizli tutmak için ecash kullanan ücretsiz ve açık kaynaklı bir Bitcoin cüzdanıdır.", actions: { more: { label: "Daha fazla bilgi edinmek için tıklayın", }, }, p1: { text: "Cashu, Bitcoin için ücretsiz ve açık kaynaklı bir ecash protokolüdür. { link } adresinden daha fazla bilgi edinebilirsiniz.", link: { text: "cashu.space", }, }, p2: { text: "Bu cüzdan herhangi bir nane'ye bağlı değildir. Bu cüzdanı kullanmak için güvendiğiniz bir veya daha fazla Cashu nane'sine bağlanmanız gerekir.", }, p3: { text: "Bu cüzdan, yalnızca sizin erişiminiz olan ecash'i saklar. Kurtarma kelimeleri yedeklemesi olmadan tarayıcı verilerinizi silerseniz, token'larınızı kaybedersiniz.", }, p4: { text: "Bu cüzdan beta aşamasındadır. Fonlara erişimini kaybeden kişilerden sorumlu değiliz. Kendi sorumluluğunuzda kullanın! Bu kod açık kaynaklıdır ve MIT lisansı altında lisanslanmıştır.", }, }, WelcomeSlide2: { title: "PWA Yükle", alt: { pwa_example: "PWA kurulum örneği" }, installing: "Yükleniyor…", instruction: { intro: { text: "En iyi deneyim için, cihazınızın yerel web tarayıcısını kullanarak bu cüzdanı Aşamalı Web Uygulaması olarak yükleyin. Bunu hemen yapın.", }, android: { title: "Android (Chrome)", step1: { item: "1. { icon } { text }", text: "Menüye dokunun (sağ üst)", }, step2: { item: "2. { icon } { text }", text: "{ buttonText }'e basın", buttonText: "@:AndroidPWAPrompt.buttonText", }, }, ios: { title: "iOS (Safari)", step1: { item: "1. { icon } { text }", text: "Paylaş'a dokunun (alt)", }, step2: { item: "2. { icon } { text }", text: "{ buttonText }'e basın", buttonText: "@:iOSPWAPrompt.buttonText", }, }, outro: { text: "Bu uygulamayı cihazınıza yükledikten sonra bu tarayıcı penceresini kapatın ve uygulamayı ana ekranınızdan kullanın.", }, }, pwa: { success: { title: "Başarılı!", text: "Cashu'yu PWA olarak kullanıyorsunuz. Diğer açık tarayıcı pencerelerini kapatın ve uygulamayı ana ekranınızdan kullanın.", nextSteps: "Artık bu sekmeyi kapatıp uygulamayı ana ekranınızdan açabilirsiniz.", }, }, }, iOSPWAPrompt: { text: "{ icon } ve { buttonText }'e dokunun", buttonText: "Ana Ekrana Ekle", }, AndroidPWAPrompt: { text: "{ icon } ve { buttonText }'e dokunun", buttonText: "Ana Ekrana Ekle", }, WelcomeSlide3: { title: "Kurtarma Kelimeleriniz", text: "Kurtarma kelimelerinizi bir parola yöneticisinde veya kağıt üzerinde saklayın. Cihaza erişiminizi kaybederseniz fonlarınızı kurtarmanın tek yolu kurtarma kelimelerinizdir.", inputs: { seed_phrase: { label: "Kurtarma Kelimeleri", caption: "Kurtarma kelimelerinizi ayarlarda görebilirsiniz.", }, checkbox: { label: "Yazdım", }, }, }, WelcomeSlide4: { title: "Şartlar", actions: { more: { label: "Hizmet Şartlarını Oku", }, }, inputs: { checkbox: { label: "Bu şartları ve koşulları okudum ve kabul ediyorum", }, }, }, WelcomeSlideChoice: { title: "Cüzdanınızı ayarlayın", text: "Bir seed ifadesinden mi kurtarmak istersiniz yoksa yeni bir cüzdan mı oluşturmak istersiniz?", options: { new: { title: "Yeni cüzdan oluştur", subtitle: "Yeni bir seed oluşturun ve mint ekleyin.", }, recover: { title: "Cüzdanı kurtar", subtitle: "Seed ifadenizi girin, mintleri ve ecash'i geri yükleyin.", }, }, }, WelcomeMintSetup: { title: "Mint ekle", text: "Mintler ecash göndermenize ve almanıza yardımcı olan sunuculardır. Keşfedilen bir minti seçin veya manuel ekleyin. Daha sonra da ekleyebilirsiniz.", sections: { your_mints: "Mintleriniz" }, restoring: "Mintler geri yükleniyor…", placeholder: { mint_url: "https://" }, }, WelcomeRecoverSeed: { title: "Seed ifadenizi girin", text: "Kurtarmak için 12 kelimelik seed ifadenizi yapıştırın veya yazın.", inputs: { word: "Kelime { index }" }, actions: { paste_all: "Tümünü yapıştır" }, disclaimer: "Seed ifadeniz yalnızca yerelde cüzdan anahtarlarını türetmek için kullanılır.", }, WelcomeRestoreEcash: { title: "Ecash'inizi geri yükleyin", text: "Yapılandırılmış mintlerinizde harcanmamış kanıtları tarayın ve cüzdanınıza ekleyin.", }, MintRatings: { title: "Mint yorumları", reviews: "yorum", ratings: "Değerlendirmeler", no_reviews: "Hiç yorum bulunamadı", your_review: "Yorumunuz", no_reviews_to_display: "Gösterilecek yorum yok.", no_rating: "Puan yok", out_of: "üzerinden", rows: "Reviews", sort: "Sırala", sort_options: { newest: "En yeni", oldest: "En eski", highest: "En yüksek", lowest: "En düşük", }, actions: { write_review: "Yorum yaz" }, empty_state_subtitle: "Bir yorum bırakarak yardımcı olun. Bu mint ile ilgili deneyiminizi paylaşın ve bir yorum bırakarak başkalarına yardımcı olun.", }, CreateMintReview: { title: "Mint yorumu", publishing_as: "Şu kişi olarak yayımlanıyor", inputs: { rating: { label: "Puan" }, review: { label: "Yorum (isteğe bağlı)" }, }, actions: { publish: { label: "Yayımla", in_progress: "Yayımlanıyor…" }, }, }, RestoreView: { seed_phrase: { label: "Kurtarma Kelimelerinden Geri Yükle", caption: "Cüzdanınızı geri yüklemek için kurtarma kelimelerinizi girin. Geri yüklemeden önce, daha önce kullandığınız tüm nane'leri eklediğinizden emin olun.", inputs: { seed_phrase: { label: "Kurtarma kelimeleri", caption: "Kurtarma kelimelerinizi ayarlarda görebilirsiniz.", }, }, }, information: { label: "Bilgi", caption: "Sihirbaz yalnızca başka bir kurtarma kelimesinden ecash'i geri yükleyecektir, şu anda kullandığınız cüzdanın kurtarma kelimesini kullanamayacak veya değiştiremeyeceksiniz. Bu, geri yüklenen ecash'in bir kez kendinize göndermediğiniz sürece mevcut kurtarma kelimeniz tarafından korunmayacağı anlamına gelir.", }, restore_mints: { label: "Nane'leri Geri Yükle", caption: "Geri yüklenecek nane'yi seçin. Ana ekranda 'Naneler' altında daha fazla nane ekleyebilir ve buradan geri yükleyebilirsiniz.", }, nostr_mints: { label: "Nostr'dan Naneleri Geri Yükle", caption: "Seed ifadenizi kullanarak Nostr rölelerinde depolanan nane yedeklerini arayın. Bu, daha önce kullandığınız naneleri keşfetmenize yardımcı olacaktır.", search_button: "Nane Yedeklerini Ara", select_all: "Tümünü Seç", deselect_all: "Tüm Seçimi Kaldır", backed_up: "Yedeklendi", already_added: "Zaten Eklendi", add_selected: "Seçileni Ekle ({count})", no_backups_found: "Nane yedeği bulunamadı", no_backups_hint: "Nane listenizi otomatik olarak yedeklemek için ayarlarda Nostr nane yedeğinin etkinleştirildiğinden emin olun.", invalid_mnemonic: "Lütfen aramadan önce geçerli bir seed ifadesi girin.", search_error: "Nane yedekleri aranırken hata oluştu.", add_error: "Seçilen naneler eklenirken hata oluştu.", }, actions: { paste: { error: "Pano içeriği okunamadı.", }, validate: { error: "Anımsatıcı en az 12 kelime olmalıdır.", }, select_all: { label: "Tümünü Seç", }, deselect_all: { label: "Tüm Seçimi Kaldır", }, restore: { label: "Geri Yükle", in_progress: "Nane geri yükleniyor…", error: "Nane geri yükleme hatası: { error }", }, restore_all_mints: { label: "Tüm Nane'leri Geri Yükle", in_progress: "{ length } nane'den { index } geri yükleniyor…", success: "Geri yükleme başarıyla tamamlandı", error: "Nane'leri geri yükleme hatası: { error }", }, restore_selected_mints: { label: "Seçili Naneleri Geri Yükle ({count})", in_progress: "{ length } nane'den { index } geri yükleniyor…", success: "{count} nane başarıyla geri yüklendi", error: "Seçili naneleri geri yükleme hatası: { error }", }, }, }, MintSettings: { add: { title: "Nane ekle", description: "Bağlanmak için bir Cashu nane'sinin URL'sini girin. Bu cüzdan herhangi bir nane'ye bağlı değildir.", inputs: { nickname: { placeholder: "Takma ad (örneğin Testnet)", }, }, actions: { add_mint: { label: "@:global.actions.add_mint.label", error_invalid_url: "Geçersiz URL", }, scan: { label: "QR Kodu Tara", }, }, }, discover: { title: "Nane'leri keşfet", overline: "Keşfet", caption: "Diğer kullanıcıların nostr'da önerdiği nane'leri keşfet.", actions: { discover: { label: "Nane'leri keşfet", in_progress: "Yükleniyor…", error_no_mints: "Nane bulunamadı", success: "{ length } nane bulundu", }, }, recommendations: { overline: "{ length } nane bulundu", caption: "Bu nane'ler diğer Nostr kullanıcıları tarafından önerildi. Dikkatli olun ve bir nane kullanmadan önce kendi araştırmanızı yapın.", actions: { browse: { label: "Nane'lere göz atmak için tıklayın", }, }, }, }, swap: { title: "Değiştir", overline: "Çoklu Nane Takasları", caption: "Fonları Lightning aracılığıyla nane'ler arasında değiştirin. Not: Potansiyel Lightning ücretleri için yer bırakın. Gelen ödeme başarılı olmazsa, faturayı manuel olarak kontrol edin.", inputs: { from: { label: "Kimden", }, to: { label: "Kime", }, amount: { label: "Miktar ({ ticker })", }, }, actions: { swap: { label: "@:global.actions.swap.label", in_progress: "@:MintSettings.swap.actions.swap.label", }, }, }, error_badge: "Hata", reviews_text: "yorumlar", no_reviews_yet: "Henüz yorum yok", discover_mints_button: "Naneleri keşfet", }, QrcodeReader: { progress: { text: "{ percentage }{ addon }", percentage: "%{ percentage }", keep_scanning_text: " - Taramaya devam et", }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, }, }, InvoiceDetailDialog: { title: "Lightning Al", create_invoice_title: "Fatura Oluştur", inputs: { amount: { label: "Miktar ({ ticker }) *", }, }, actions: { close: { label: "@:global.actions.close.label", }, create: { label: "Fatura Oluştur", label_blocked: "Fatura oluşturuluyor…", in_progress: "Oluşturuluyor", }, }, invoice: { caption: "Lightning faturası", status_paid_text: "Ödendi!", actions: { close: { label: "@:global.actions.close.label", }, copy: { label: "@:global.actions.copy.label", }, }, }, }, SendDialog: { title: "Gönder", actions: { ecash: { label: "Ecash", error_no_mints: "Nane yok", }, lightning: { label: "Lightning", error_no_mints: "Nane yok", }, }, }, SendTokenDialog: { title: "Ecash Gönder", title_ecash_text: "Ecash", badge_offline_text: "Çevrimdışı", inputs: { amount: { label: "Miktar ({ ticker }) *", invalid_too_much_error_text: "Çok fazla", }, p2pk_pubkey: { label: "Alıcının genel anahtarı", label_invalid: "Alıcının genel anahtarı", }, }, actions: { close: { label: "@:global.actions.close.label", }, close_card_scanner: { label: "@:global.actions.close.label", }, copy_emoji: { label: "🥜", tooltip_text: "Emoji kopyala", }, copy_tokens: { label: "@:global.actions.copy.label", }, copy_link: { tooltip_text: "Bağlantıyı kopyala", }, share: { tooltip_text: "Ecash'ını paylaş", }, lock: { label: "@:global.actions.lock.label", }, paste_p2pk_pubkey: { tooltip_text: "@:global.actions.paste.label", }, send: { label: "@:global.actions.send.label", }, delete: { tooltip_text: "Geçmişten sil", }, write_tokens_to_card: { tooltips: { ndef_supported_text: "NFC kartına flaşla", ndef_unsupported_text: "NDEF desteklenmiyor", }, }, }, }, ReceiveDialog: { title: "Al", actions: { ecash: { label: "Ecash", error_no_mints: "Nane yok", }, lightning: { label: "Lightning", error_no_mints: "Lightning aracılığıyla almak için bir nane'ye bağlanmanız gerekir", }, }, }, ReceiveEcashDrawer: { title: "Ecash Al", actions: { paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, request: { label: "Talep et", }, lock: { label: "@:global.actions.lock.label", }, nfc: { label: "NFC", scanning_text: "Taranıyor…", }, }, }, ReceiveTokenDialog: { title: "Ecash Al", title_ecash_text: "Ecash", inputs: { tokens_base64: { label: "Cashu token'ını yapıştır", }, }, errors: { invalid_token: { label: "Geçersiz token", }, p2pk_lock_mismatch: { label: "Alınamıyor. Bu token'ın P2PK kilidi genel anahtarınızla eşleşmiyor.", }, }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, scan: { label: "@:global.actions.scan.label", }, receive: { label: "@:global.actions.receive.label", label_known_mint: "@:ReceiveTokenDialog.actions.receive.label", label_adding_mint: "Nane ekleniyor…", }, swap: { label: "@:global.actions.swap.label", tooltip_text: "Güvenilen bir nane'ye takas et", caption: "{ value } takas et", }, cancel_swap: { label: "@:global.actions.cancel.label", tooltip_text: "Takası iptal et", }, confirm_swap: { label: "@:ReceiveTokenDialog.actions.swap.label", tooltip_text: "@:ReceiveTokenDialog.actions.swap.tooltip_text", in_progress: "@:ReceiveTokenDialog.actions.confirm_swap.label", }, later: { label: "Daha sonra al", tooltip_text: "Daha sonra almak için geçmişe ekle", already_in_history_success_text: "Ecash zaten Geçmişte", added_to_history_success_text: "Ecash Geçmişe eklendi", }, nfc: { label: "NFC", tooltips: { ndef_supported_text: "NFC kartından oku", ndef_unsupported_text: "NDEF desteklenmiyor", }, }, }, }, P2PKDialog: { p2pk: { caption: "P2PK Anahtarı", description: "Bu anahtara kilitlenmiş ecash al", used_warning_text: "Uyarı: Bu anahtar daha önce kullanıldı. Daha iyi gizlilik için yeni bir anahtar kullanın.", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_key: { label: "Yeni anahtar oluştur", }, }, }, PaymentRequestDialog: { payment_request: { caption: "Ödeme Talebi", description: "Nostr aracılığıyla ödeme al", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_request: { label: "Yeni talep", }, add_amount: { label: "Miktar ekle", }, use_active_mint: { label: "Herhangi bir nane", }, }, inputs: { amount: { placeholder: "Miktar girin", }, }, }, NumericKeyboard: { actions: { close: { label: "@:global.actions.close.label", closed_info_text: "Klavye devre dışı bırakıldı. Klavyeyi ayarlardan yeniden etkinleştirebilirsiniz.", }, enter: { label: "@:global.actions.enter.label", }, }, }, NWCDialog: { nwc: { caption: "Nostr Cüzdan Bağlantısı", description: "NWC ile cüzdanınızı uzaktan kontrol edin. Cüzdanınızı uyumlu bir uygulamayla bağlamak için QR koduna basın.", warning_text: "Uyarı: Bu bağlantı dizesine erişimi olan herkes cüzdanınızdan ödeme başlatabilir. Paylaşmayın!", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, }, }, MintMotdMessage: { title: "Nane Mesajı", }, MintDetailsDialog: { contact: { title: "İletişim", }, details: { title: "Nane detayları", url: { label: "URL", }, nuts: { label: "Kurallar", actions: { show: { label: "Tümünü görüntüle", }, hide: { label: "Gizle", }, }, }, currency: { label: "Para Birimi", }, currencies: { label: "@:MintDetailsDialog.details.currency.label", }, version: { label: "Sürüm", }, }, actions: { title: "Eylemler", copy_mint_url: { label: "Nane URL'sini kopyala", }, delete: { label: "Nane'yi sil", }, edit: { label: "Nane'yi düzenle", }, }, }, ChooseMint: { title: "Bir nane seçin", badge_mint_error_text: "Hata", badge_option_mint_error_text: "@:ChooseMint.badge_mint_error_text", }, HistoryTable: { empty_text: "Henüz geçmiş yok", row: { type_label: "Ecash", date_label: "{ value } önce", }, actions: { check_status: { tooltip_text: "Durumu kontrol et", }, receive: { tooltip_text: "Al", }, filter_pending: { label: "Bekleyenleri filtrele", }, show_all: { label: "Tümünü göster", }, }, old_token_not_found_error_text: "Eski token bulunamadı", }, InvoiceTable: { empty_text: "Henüz fatura yok", row: { type_label: "Lightning", type_tooltip_text: "Kopyalamak için tıklayın", date_label: "{ value } önce", }, actions: { check_status: { tooltip_text: "Durumu kontrol et", }, filter_pending: { label: "Bekleyenleri filtrele", }, show_all: { label: "Tümünü göster", }, }, }, RemoveMintDialog: { title: "Bu nane'yi silmek istediğinizden emin misiniz?", nickname: { label: "Takma ad", }, balances: { label: "Bakiyeler", }, warning_text: "Not: Bu cüzdan paranoyak olduğu için, bu nane'den ecash'iniz aslında silinmeyecek, ancak cihazınızda saklanmaya devam edecektir. Bu nane'yi daha sonra tekrar eklerseniz yeniden göründüğünü göreceksiniz.", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { confirm: { label: "Nane'yi kaldır", }, cancel: { label: "@:global.actions.cancel.label", }, }, }, ParseInputComponent: { placeholder: { default: "Cashu token veya Lightning adresi", receive: "Cashu token", pay: "Lightning adresi veya faturası", }, qr_scanner: { title: "QR Kodu Tara", description: "Bir adresi taramak için dokunun", }, paste_button: { label: "@:global.actions.paste.label", }, }, PayInvoiceDialog: { input_data: { title: "Lightning öde", inputs: { invoice_data: { label: "Lightning faturası veya adresi", }, }, actions: { close: { label: "@:global.actions.close.label", }, enter: { label: "@:global.actions.enter.label", }, paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, }, }, lnurlpay: { amount_exact_label: "{ payee }, { value } { ticker } talep ediyor", amount_range_label: "{ payee }{br} { min } ile { max } { ticker } arasında talep ediyor", sending_to_lightning_address: "{ address } adresine gönderiliyor", inputs: { amount: { label: "Miktar ({ ticker }) *", }, comment: { label: "Yorum (isteğe bağlı)", }, }, actions: { close: { label: "@:global.actions.close.label", }, send: { label: "@:global.actions.send.label", }, }, }, invoice: { title: "{ value } öde", paying: "Ödeniyor", paid: "Ödendi", fee: "Ücret", memo: { label: "Memo", }, processing_info_text: "İşleniyor…", balance_too_low_warning_text: "Bakiye yetersiz", actions: { close: { label: "@:global.actions.close.label", }, pay: { label: "Öde", in_progress: "@:PayInvoiceDialog.invoice.processing_info_text", error: "Hata", }, }, }, }, EditMintDialog: { title: "Nane'yi düzenle", inputs: { nickname: { label: "Takma ad", }, mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, update: { label: "@:global.actions.update.label", }, }, }, AddMintDialog: { title: "Bu nane'ye güveniyor musunuz?", description: "Bu nane'yi kullanmadan önce güvendiğinizden emin olun. Nane'ler herhangi bir zamanda kötü niyetli hale gelebilir veya faaliyetlerini durdurabilir.", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, add_mint: { label: "@:global.actions.add_mint.label", in_progress: "Nane ekleniyor", }, }, }, restore: { mnemonic_error_text: "Lütfen bir anımsatıcı girin", restore_mint_error_text: "Nane geri yükleme hatası: { error }", prepare_info_text: "Geri yükleme süreci hazırlanıyor…", restored_proofs_for_keyset_info_text: "{ keysetId } anahtar kümesi için { restoreCounter } kanıt geri yüklendi", checking_proofs_for_keyset_info_text: "{ keysetId } anahtar kümesi için { startIndex } ila { endIndex } kanıtları kontrol ediliyor", no_proofs_info_text: "Geri yüklenecek kanıt bulunamadı", restored_amount_success_text: "{ amount } geri yüklendi", }, swap: { in_progress_warning_text: "Takas devam ediyor", invalid_swap_data_error_text: "Geçersiz takas verisi", swap_error_text: "Takas hatası", }, TokenInformation: { fee: "Ücret", unit: "Birim", fiat: "Fiat", p2pk: "P2PK", locked: "Kilitli", locked_to_you: "Size kilitli", mint: "Darphane", memo: "Not", payment_request: "Ödeme talebi", nostr: "Nostr", token_copied: "Token panoya kopyalandı", }, }; ================================================ FILE: src/i18n/zh-CN/index.ts ================================================ export default { MultinutPicker: { payment: "多坚果支付", selectMints: "选择一个或多个铸币厂来发起支付。", totalSelectedBalance: "所选总余额", multiMintPay: "多铸币支付", balanceNotEnough: "多铸币余额不足以支付此发票", failed: "处理失败:{error}", paid: "通过闪电网络支付了 {amount}", }, global: { copy_to_clipboard: { success: "已复制到剪贴板!", }, actions: { add_mint: { label: "添加 Mint", }, cancel: { label: "取消", }, copy: { label: "复制", }, close: { label: "关闭", }, enter: { label: "输入", }, lock: { label: "锁定", }, paste: { label: "粘贴", }, receive: { label: "接收", }, scan: { label: "扫描", }, send: { label: "发送", }, swap: { label: "兑换", }, update: { label: "更新", }, }, inputs: { mint_url: { label: "Mint URL", }, }, }, wallet: { notifications: { balance_too_low: "余额不足", received: "已收到 {amount}", fee: " (手续费: {fee})", could_not_request_mint: "无法请求铸造", invoice_still_pending: "发票仍在处理中", paid_lightning: "通过闪电网络支付了 {amount}", payment_pending_refresh: "付款正在处理。请手动刷新发票。", sent: "已发送 {amount}", token_still_pending: "代币仍在处理中", received_lightning: "通过闪电网络收到 {amount}", lightning_payment_failed: "闪电网络支付失败", failed_to_decode_invoice: "无法解码发票", invalid_lnurl: "无效的LNURL", lnurl_error: "LNURL错误", no_amount: "没有金额", no_lnurl_data: "没有LNURL数据", no_price_data: "没有价格数据。", please_try_again: "请重试。", }, mint: { notifications: { already_added: "Mint 已添加", added: "Mint 已添加", not_found: "未找到 Mint", activation_failed: "Mint 激活失败", no_active_mint: "没有激活的 Mint", unit_activation_failed: "单位激活失败", unit_not_supported: "Mint 不支持该单位", activated: "Mint 已激活", could_not_connect: "无法连接到 Mint", could_not_get_info: "无法获取 Mint 信息", could_not_get_keys: "无法获取 Mint 密钥", could_not_get_keysets: "无法获取 Mint 密钥集", removed: "Mint 已移除", error: "Mint 错误", mint_validation_error: "铸币厂验证错误", }, }, }, MainHeader: { menu: { settings: { title: "设置", settings: { title: "设置", caption: "钱包配置", }, }, terms: { title: "条款", terms: { title: "条款", caption: "服务条款", }, }, links: { title: "链接", cashuSpace: { title: "Cashu.space", caption: "cashu.space", }, github: { title: "Github", caption: "github.com/cashubtc", }, telegram: { title: "Telegram", caption: "t.me/CashuMe", }, twitter: { title: "Twitter", caption: "{'@'}CashuBTC", }, donate: { title: "捐赠", caption: "支持 Cashu", }, }, }, offline: { warning: { text: "离线", }, }, reload: { warning: { text: "在 { countdown } 后重新加载", }, }, staging: { warning: { text: "测试环境 – 请勿使用真实资金!", }, }, }, FullscreenHeader: { actions: { back: { label: "钱包", }, }, }, Settings: { web_of_trust: { title: "信任网络", known_pubkeys: "已知公钥:{wotCount}", continue_crawl: "继续抓取", crawl_odell: "抓取 ODELL 的信任网络", crawl_wot: "抓取信任网络", pause: "暂停", reset: "重置", progress: "{crawlProcessed} / {crawlTotal}", }, npub_cash: { use_npubx: "使用 npubx.cash", copy_lightning_address: "复制闪电地址", v2_mint: "npub.cash v2 铸币厂", }, multinut: { use_multinut: "使用 Multinut", }, language: { title: "语言", description: "请从下方列表中选择您的首选语言。", }, sections: { backup_restore: "备份与恢复", lightning_address: "LIGHTNING 地址", nostr_keys: "NOSTR 密钥", nostr: { title: "NOSTR", relays: { expand_label: "点击编辑中继", add: { title: "添加中继", description: "您的钱包使用这些中继进行nostr操作,例如付款请求、nostr钱包连接和备份。", }, list: { title: "中继", description: "您的钱包将连接到这些中继。", copy_tooltip: "复制中继", remove_tooltip: "删除中继", }, }, }, payment_requests: "支付请求", nostr_wallet_connect: "NOSTR 钱包连接", hardware_features: "硬件功能", p2pk_features: "P2PK 功能", privacy: "隐私", experimental: "实验性", appearance: "外观", }, backup_restore: { backup_seed: { title: "备份种子短语", description: "您的种子短语可以恢复您的钱包。请务必妥善保管并保密。", seed_phrase_label: "种子短语", }, restore_ecash: { title: "恢复 ecash", description: "恢复向导允许您从助记符种子短语中恢复丢失的 ecash。您当前钱包的种子短语不会受到影响,该向导仅允许您从另一个种子短语中 恢复 ecash。", button: "恢复", }, }, lightning_address: { title: "Lightning 地址", description: "接收支付到您的 Lightning 地址。", enable: { toggle: "启用", description: "带有 npub.cash 的 Lightning 地址", }, address: { copy_tooltip: "复制 Lightning 地址", }, automatic_claim: { toggle: "自动认领", description: "自动接收收到的支付。", }, npc_v2: { choose_mint_title: "为 npub.cash v2 选择铸币厂", choose_mint_placeholder: "选择一个铸币厂…", }, }, nostr_keys: { title: "您的 Nostr 密钥", description: "为您的 Lightning 地址设置 Nostr 密钥。", wallet_seed: { title: "钱包种子短语", description: "从钱包种子生成 Nostr 密钥对", copy_nsec: "复制 nsec", }, nsec_bunker: { title: "Nsec Bunker", description: "使用 NIP-46 bunker", delete_tooltip: "删除连接", }, use_nsec: { title: "使用您的 nsec", description: "这种方法很危险,不建议使用", delete_tooltip: "删除 nsec", }, signing_extension: { title: "签名扩展", description: "使用 NIP-07 签名扩展", not_found: "未找到 NIP-07 签名扩展", }, }, payment_requests: { title: "支付请求", description: "支付请求允许您通过 Nostr 接收支付。如果您启用此功能,您的钱包将订阅您的 Nostr 中继。", enable_toggle: "启用支付请求", claim_automatically: { toggle: "自动认领", description: "自动接收收到的支付。", }, }, nostr_wallet_connect: { title: "Nostr 钱包连接 (NWC)", description: "使用 NWC 从任何其他应用程序控制您的钱包。", enable_toggle: "启用 NWC", payments_note: "您只能使用 NWC 从您的比特币余额支付。支付将从您激活的 Mint 进行。", connection: { copy_tooltip: "复制连接字符串", qr_tooltip: "显示二维码", allowance_label: "剩余额度 (sat)", }, }, hardware_features: { webnfc: { title: "WebNFC", description: "选择写入 NFC 卡的编码", text: { title: "文本", description: "以纯文本格式存储 token", }, weburl: { title: "URL", description: "存储此钱包的 URL 和 token", }, binary: { title: "二进制", description: "将令牌存储为二进制数据", }, quick_access: { toggle: "NFC 快速访问", description: "在 '接收 Ecash' 菜单中快速扫描 NFC 卡。此选项会在 '接收 Ecash' 菜单中添加一个 NFC 按钮。", }, }, }, p2pk_features: { title: "P2PK", description: "生成密钥对以接收 P2PK 锁定的 ecash。警告:此功能是实验性的。仅用于小额。如果您丢失了您的私钥,将没有人能够再解锁锁定到它的 ecash。", generate_button: "生成密钥", import_button: "导入 nsec", quick_access: { toggle: "快速访问锁定", description: "使用此功能在 '接收 Ecash' 菜单中快速显示您的 P2PK 锁定密钥。", }, keys_expansion: { label: "点击浏览 {count} 个密钥", used_badge: "已使用", }, }, privacy: { title: "隐私", description: "这些设置会影响您的隐私。", check_incoming: { toggle: "检查收到的发票", description: "如果启用,钱包会在后台检查最新的发票。这增加了钱包的响应速度,但也使指纹识别更容易。您可以在发票标签中手动检查未付款的发票。", }, check_startup: { toggle: "启动时检查待处理发票", description: "如果启用,钱包会在启动时检查过去 24 小时内待处理的发票。", }, check_all: { toggle: "检查所有发票", description: "如果启用,钱包会在后台定期检查未付款的发票,最长可达两周。这增加了钱包的在线活动,从而使指纹识别更容易。您可以在发票标签中手动检查未付款的发票。", }, check_sent: { toggle: "检查已发送的 ecash", description: "如果启用,钱包会使用周期性后台检查来确定已发送的 token 是否已被兑换。这增加了钱包的在线活动,从而使指纹识别更容易。", }, websockets: { toggle: "使用 WebSockets", description: "如果启用,钱包将使用长连接的 WebSocket 来接收有关已付款发票和已花费 token 的更新信息。这增加了钱包的响应速度,但也使指纹识别更容易。", }, bitcoin_price: { toggle: "从 Coinbase 获取汇率", description: "如果启用,将从 coinbase.com 获取当前比特币汇率并显示您的转换后余额。", currency: { title: "法定货币", description: "选择用于比特币价格显示的法定货币。", }, }, }, experimental: { title: "实验性", description: "这些功能是实验性的。", receive_swaps: { toggle: "接收 swaps", badge: "测试版", description: "在接收 Ecash 对话框中,选择将接收到的 Ecash 兑换为您的激活 Mint。", }, auto_paste: { toggle: "自动粘贴 Ecash", description: "当您按接收,然后Ecash,然后粘贴时,自动粘贴剪贴板中的 ecash。自动粘贴可能会导致 iOS 中的 UI 故障,如果您遇到问题,请关闭此功能。", }, auditor: { toggle: "启用审计器", badge: "测试版", description: "如果启用,钱包将在 Mint 详细信息对话框中显示审计器信息。审计器是第三方服务,用于监控 Mint 的可靠性。", url_label: "审计器 URL", api_url_label: "审计器 API URL", }, multinut: { toggle: "启用 Multinut", description: "如果启用,钱包将使用 Multinut 同时从多个 Mint 支付发票。", }, nostr_mint_backup: { toggle: "在 Nostr 上备份 Mint 列表", description: "如果启用,您的 Mint 列表将使用您配置的 Nostr 密钥自动备份到 Nostr 中继。这允许您在不同设备上恢复您的 Mint 列表。", notifications: { enabled: "Nostr Mint 备份已启用", disabled: "Nostr Mint 备份已禁用", failed: "无法启用 Nostr Mint 备份", }, }, }, appearance: { keyboard: { title: "屏幕键盘", description: "使用数字键盘输入金额。", toggle: "使用数字键盘", toggle_description: "如果启用,将使用数字键盘输入金额。", }, theme: { title: "外观", description: "更改您钱包的外观。", tooltips: { mono: "单色", cyber: "赛博", freedom: "自由", nostr: "nostr", bitcoin: "比特币", mint: "薄荷", nut: "坚果", blu: "蓝色", flamingo: "火烈鸟", }, }, bip177: { title: "比特币符号", description: "使用 ₿ 符号代替 sats。", toggle: "使用 ₿ 符号", }, }, advanced: { title: "高级", developer: { title: "开发者设置", description: "以下设置为开发和调试用途。", new_seed: { button: "生成新的种子短语", description: "这将生成一个新的种子短语。您必须将您的全部余额发送给自己,以便能够使用新的种子恢复。", confirm_question: "您确定要生成新的种子短语吗?", cancel: "取消", confirm: "确认", }, remove_spent: { button: "删除已花费的证明", description: "检查您的活动 Mint 中的 ecash token 是否已花费,并从您的钱包中删除已花费的 token。仅当您的钱包卡住时使用此功能。", }, debug_console: { button: "切换调试控制台", description: "打开 Javascript 调试终端。切勿向此终端粘贴您不理解的任何内容。小偷可能会试图欺骗您在此处粘贴恶意代码。", }, export_proofs: { button: "导出活动证明", description: "将活动 Mint 中的全部余额作为 Cashu token 复制到剪贴板。这只会导出所选 Mint 和单位的 token。要进行完全导出,请选择不同的 Mint 和单位并再次导出。", }, keyset_counters: { title: "增加 keyset 计数器", description: "点击 keyset ID 以增加您钱包中 keysets 的 derivation path 计数器。如果您看到输出已被签名错误,这将很有用。", counter: "计数器: {count}", }, unset_reserved: { button: "取消所有保留的 token", description: "此钱包会将待处理的传出 ecash 标记为已保留(并从您的余额中扣除),以防止双重支付尝试。此按钮将取消所有保留的 token,以便可以再次使用它们。如果您执行此操作,您的钱包可能会包含已花费的证明。按删除已花费的证明按钮以清除它们。", }, show_onboarding: { button: "显示入门指南", description: "再次显示入门指南屏幕。", }, reset_wallet: { button: "重置钱包数据", description: "重置您的钱包数据。警告:这将删除所有内容!请务必先创建备份。", confirm_question: "您确定要删除您的钱包数据吗?", cancel: "取消", confirm: "删除钱包", }, export_wallet: { button: "导出钱包数据", description: "下载您的钱包数据。您可以在新钱包的欢迎屏幕上从该文件中恢复您的钱包。如果您在导出后继续使用您的钱包,该文件将不同步。", }, }, }, }, NoMintWarnBanner: { title: "加入 Mint", subtitle: "您还没有加入任何 Cashu Mint。请在设置中添加 Mint URL 或接收来自新 Mint 的 ecash 以开始。", actions: { add_mint: { label: "@:global.actions.add_mint.label", }, receive: { label: "接收 Ecash", }, }, }, WalletPage: { actions: { send: { label: "@:global.actions.send.label", }, receive: { label: "@:global.actions.receive.label", }, }, tabs: { history: { label: "历史记录", }, invoices: { label: "发票", }, mints: { label: "Mints", }, }, install: { text: "安装", tooltip: "安装 Cashu", }, }, AlreadyRunning: { title: "不行。", text: "另一个标签页正在运行。请关闭此标签页并重试。", actions: { retry: { label: "重试", }, }, }, ErrorNotFound: { title: "404", text: "哎呀。这里什么都没有…", actions: { home: { label: "返回主页", }, }, }, BalanceView: { mintUrl: { label: "Mint", }, mintBalance: { label: "余额", }, mintError: { label: "Mint 错误", }, pending: { label: "待处理", tooltip: "检查所有待处理的 token", }, }, WelcomePage: { actions: { previous: { label: "上一步", }, next: { label: "下一步", }, }, }, WelcomeSlide1: { title: "欢迎使用 Cashu", text: "Cashu.me 是一款免费且开源的比特币钱包,使用 ecash 确保您的资金安全和隐私。", actions: { more: { label: "点击了解更多", }, }, p1: { text: "Cashu 是一个免费且开源的比特币 ecash 协议。您可以在 { link } 了解更多。", link: { text: "cashu.space", }, }, p2: { text: "此钱包不隶属于任何 Mint。要使用此钱包,您需要连接到一个或多个您信任的 Cashu Mint。", }, p3: { text: "此钱包存储只有您才能访问的 ecash。如果您在没有种子短语备份的情况下删除您的浏览器数据,您将丢失您的 token。", }, p4: { text: "此钱包处于测试阶段。我们对用户丢失资金概不负责。使用风险自负!此代码是开源的,并在 MIT 许可证下获得许可。", }, }, WelcomeSlide2: { title: "安装 PWA", alt: { pwa_example: "PWA 安装示例" }, installing: "正在安装…", instruction: { intro: { text: "为了获得最佳体验,请使用您设备的本地网络浏览器将此钱包安装为渐进式 Web 应用程序。请立即执行此操作。", }, android: { title: "Android (Chrome)", step1: { item: "1. { icon } { text }", text: "点击菜单(右上角)", }, step2: { item: "2. { icon } { text }", text: "按 { buttonText }", buttonText: "@:AndroidPWAPrompt.buttonText", }, }, ios: { title: "iOS (Safari)", step1: { item: "1. { icon } { text }", text: "点击分享(底部)", }, step2: { item: "2. { icon } { text }", text: "按 { buttonText }", buttonText: "@:iOSPWAPrompt.buttonText", }, }, outro: { text: "在您的设备上安装此应用后,关闭此浏览器窗口并从主屏幕使用该应用。", }, }, pwa: { success: { title: "成功!", text: "您正在使用 Cashu 作为 PWA。关闭所有其他打开的浏览器窗口,并从主屏幕使用该应用。", nextSteps: "您现在可以关闭此标签页,并从主屏幕打开应用。", }, }, }, iOSPWAPrompt: { text: "点击 { icon } 和 { buttonText }", buttonText: "添加到主屏幕", }, AndroidPWAPrompt: { text: "点击 { icon } 和 { buttonText }", buttonText: "添加到主屏幕", }, WelcomeSlide3: { title: "您的种子短语", text: "将您的种子短语存储在密码管理器或纸上。如果您的设备丢失,您的种子短语是恢复资金的唯一途径。", inputs: { seed_phrase: { label: "种子短语", caption: "您可以在设置中查看您的种子短语。", }, checkbox: { label: "我已经写下来了", }, }, }, WelcomeSlide4: { title: "条款", actions: { more: { label: "阅读服务条款", }, }, inputs: { checkbox: { label: "我已阅读并接受这些条款和条件", }, }, }, WelcomeSlideChoice: { title: "设置您的钱包", text: "您想从种子短语恢复,还是创建一个新钱包?", options: { new: { title: "创建新钱包", subtitle: "生成新的种子并添加 Mint。", }, recover: { title: "恢复钱包", subtitle: "输入您的种子短语,恢复 Mints 和 ecash。", }, }, }, WelcomeMintSetup: { title: "添加 Mints", text: "Mint 是帮助您发送和接收 ecash 的服务器。选择一个已发现的 Mint 或手动添加。您也可以稍后添加。", sections: { your_mints: "您的 Mints" }, restoring: "正在恢复 Mints…", placeholder: { mint_url: "https://" }, }, WelcomeRecoverSeed: { title: "输入您的种子短语", text: "粘贴或输入您的 12 个词种子短语以进行恢复。", inputs: { word: "第 { index } 个词" }, actions: { paste_all: "全部粘贴" }, disclaimer: "您的种子短语仅在本地使用,用于派生您的钱包密钥。", }, WelcomeRestoreEcash: { title: "恢复您的 ecash", text: "扫描您配置的 Mints 上未花费的证明,并将其添加到您的钱包。", }, MintRatings: { title: "Mint 评价", reviews: "条评价", ratings: "评分", no_reviews: "未找到评价", your_review: "您的评价", no_reviews_to_display: "暂无可显示的评价。", no_rating: "暂无评分", out_of: "共", rows: "Reviews", sort: "排序", sort_options: { newest: "最新", oldest: "最旧", highest: "最高", lowest: "最低", }, actions: { write_review: "撰写评价" }, empty_state_subtitle: "通过留下评价来帮助他人。分享您对此 Mint 的体验,通过留下评价来帮助他人。", }, CreateMintReview: { title: "评价 Mint", publishing_as: "发布身份", inputs: { rating: { label: "评分" }, review: { label: "评价(可选)" }, }, actions: { publish: { label: "发布", in_progress: "发布中…" }, }, }, RestoreView: { seed_phrase: { label: "从种子短语恢复", caption: "输入您的种子短语以恢复您的钱包。在恢复之前,请确保您已添加所有您之前使用过的 Mint。", inputs: { seed_phrase: { label: "种子短语", caption: "您可以在设置中查看您的种子短语。", }, }, }, information: { label: "信息", caption: "该向导仅从另一个种子短语恢复 ecash,您将无法使用此种子短语或更改您当前使用的钱包的种子短语。这意味着,除非您将 ecash 发送给自己一次,否则恢复的 ecash 将不会受到您当前种子短语的保护。", }, restore_mints: { label: "恢复 Mints", caption: "选择要恢复的 Mint。您可以在主屏幕的Mints下添加更多 Mint 并在此处恢复它们。", }, nostr_mints: { label: "从 Nostr 恢复 Mints", caption: "使用您的种子短语在 Nostr 中继上搜索存储的 Mint 备份。这将帮助您发现以前使用过的 Mint。", search_button: "搜索 Mint 备份", select_all: "全选", deselect_all: "取消全选", backed_up: "已备份", already_added: "已添加", add_selected: "添加所选 ({count})", no_backups_found: "未找到 Mint 备份", no_backups_hint: "请确保在设置中启用 Nostr Mint 备份以自动备份您的 Mint 列表。", invalid_mnemonic: "请在搜索前输入有效的种子短语。", search_error: "搜索 Mint 备份失败。", add_error: "添加所选 Mint 失败。", }, actions: { paste: { error: "读取剪贴板内容失败。", }, validate: { error: "助记符应至少包含 12 个词。", }, select_all: { label: "全选", }, deselect_all: { label: "取消全选", }, restore: { label: "恢复", in_progress: "正在恢复 Mint…", error: "恢复 Mint 错误: { error }", }, restore_all_mints: { label: "恢复所有 Mints", in_progress: "正在恢复第 { index } 个 Mint,共 { length } 个…", success: "恢复成功", error: "恢复 Mints 错误: { error }", }, restore_selected_mints: { label: "恢复所选 Mints ({count})", in_progress: "正在恢复第 { index } 个 Mint,共 { length } 个…", success: "成功恢复 {count} 个 Mint", error: "恢复所选 Mints 错误: { error }", }, }, }, MintSettings: { add: { title: "添加 Mint", description: "输入 Cashu Mint 的 URL 以连接。此钱包不隶属于任何 Mint。", inputs: { nickname: { placeholder: "昵称 (例如 Testnet)", }, }, actions: { add_mint: { label: "@:global.actions.add_mint.label", error_invalid_url: "无效的 URL", }, scan: { label: "扫描二维码", }, }, }, discover: { title: "发现 Mints", overline: "发现", caption: "发现其他用户在 Nostr 上推荐的 Mints。", actions: { discover: { label: "发现 Mints", in_progress: "正在加载…", error_no_mints: "未找到 Mints", success: "找到 { length } 个 Mints", }, }, recommendations: { overline: "找到 { length } 个 Mints", caption: "这些 Mints 是由其他 Nostr 用户推荐的。请小心谨慎,并在使用 Mint 之前自行研究。", actions: { browse: { label: "点击浏览 Mints", }, }, }, }, swap: { title: "兑换", overline: "多 Mint 兑换", caption: "通过 Lightning 在 Mints 之间兑换资金。注意:留出潜在的 Lightning 费用。如果收到的支付不成功,请手动检查发票。", inputs: { from: { label: "从", }, to: { label: "到", }, amount: { label: "金额 ({ ticker })", }, }, actions: { swap: { label: "@:global.actions.swap.label", in_progress: "@:MintSettings.swap.actions.swap.label", }, }, }, error_badge: "错误", reviews_text: "评论", no_reviews_yet: "暂无评论", discover_mints_button: "发现 Mints", }, QrcodeReader: { progress: { text: "{ percentage }{ addon }", percentage: "{ percentage }%", keep_scanning_text: " - 继续扫描", }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, }, }, InvoiceDetailDialog: { title: "接收 Lightning", create_invoice_title: "创建发票", inputs: { amount: { label: "金额 ({ ticker }) *", }, }, actions: { close: { label: "@:global.actions.close.label", }, create: { label: "创建发票", label_blocked: "正在创建发票…", in_progress: "正在创建", }, }, invoice: { caption: "Lightning 发票", status_paid_text: "已付款!", actions: { close: { label: "@:global.actions.close.label", }, copy: { label: "@:global.actions.copy.label", }, }, }, }, SendDialog: { title: "发送", actions: { ecash: { label: "Ecash", error_no_mints: "没有可用的 Mints", }, lightning: { label: "Lightning", error_no_mints: "没有可用的 Mints", }, }, }, SendTokenDialog: { title: "发送 Ecash", title_ecash_text: "Ecash", badge_offline_text: "离线", inputs: { amount: { label: "金额 ({ ticker }) *", invalid_too_much_error_text: "太多了", }, p2pk_pubkey: { label: "接收者公钥", label_invalid: "接收者公钥", }, }, actions: { close: { label: "@:global.actions.close.label", }, close_card_scanner: { label: "@:global.actions.close.label", }, copy_emoji: { label: "🥜", tooltip_text: "复制 Emoji", }, copy_tokens: { label: "@:global.actions.copy.label", }, copy_link: { tooltip_text: "复制链接", }, share: { tooltip_text: "分享ecash代币", }, lock: { label: "@:global.actions.lock.label", }, paste_p2pk_pubkey: { tooltip_text: "@:global.actions.paste.label", }, send: { label: "@:global.actions.send.label", }, delete: { tooltip_text: "从历史记录中删除", }, write_tokens_to_card: { tooltips: { ndef_supported_text: "写入 NFC 卡", ndef_unsupported_text: "不支持 NDEF", }, }, }, }, ReceiveDialog: { title: "接收", actions: { ecash: { label: "Ecash", error_no_mints: "没有可用的 Mints", }, lightning: { label: "Lightning", error_no_mints: "您需要连接到 Mint 才能通过 Lightning 接收", }, }, }, ReceiveEcashDrawer: { title: "接收 Ecash", actions: { paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, request: { label: "请求", }, lock: { label: "@:global.actions.lock.label", }, nfc: { label: "NFC", scanning_text: "正在扫描…", }, }, }, ReceiveTokenDialog: { title: "接收 Ecash", title_ecash_text: "Ecash", inputs: { tokens_base64: { label: "粘贴 Cashu token", }, }, errors: { invalid_token: { label: "无效的 token", }, p2pk_lock_mismatch: { label: "无法接收。此令牌的P2PK锁定与您的公钥不匹配。", }, }, actions: { paste: { label: "@:global.actions.paste.label", }, close: { label: "@:global.actions.close.label", }, scan: { label: "@:global.actions.scan.label", }, receive: { label: "@:global.actions.receive.label", label_known_mint: "@:ReceiveTokenDialog.actions.receive.label", label_adding_mint: "正在添加 Mint…", }, swap: { label: "@:global.actions.swap.label", tooltip_text: "兑换到信任的 Mint", caption: "兑换 { value }", }, cancel_swap: { label: "@:global.actions.cancel.label", tooltip_text: "取消兑换", }, confirm_swap: { label: "@:ReceiveTokenDialog.actions.swap.label", tooltip_text: "@:ReceiveTokenDialog.actions.swap.tooltip_text", in_progress: "@:ReceiveTokenDialog.actions.confirm_swap.label", }, later: { label: "稍后接收", tooltip_text: "添加到历史记录,稍后接收", already_in_history_success_text: "Ecash 已在历史记录中", added_to_history_success_text: "Ecash 已添加到历史记录", }, nfc: { label: "NFC", tooltips: { ndef_supported_text: "从 NFC 卡读取", ndef_unsupported_text: "不支持 NDEF", }, }, }, }, P2PKDialog: { p2pk: { caption: "P2PK 密钥", description: "接收此密钥锁定的 ecash", used_warning_text: "警告:此密钥已被使用过。请使用新密钥以获得更好的隐私。", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_key: { label: "生成新密钥", }, }, }, PaymentRequestDialog: { payment_request: { caption: "支付请求", description: "通过 Nostr 接收支付", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, new_request: { label: "新请求", }, add_amount: { label: "添加金额", }, use_active_mint: { label: "任何 Mint", }, }, inputs: { amount: { placeholder: "输入金额", }, }, }, NumericKeyboard: { actions: { close: { label: "@:global.actions.close.label", closed_info_text: "键盘已禁用。您可以在设置中重新启用键盘。", }, enter: { label: "@:global.actions.enter.label", }, }, }, NWCDialog: { nwc: { caption: "Nostr 钱包连接", description: "使用 NWC 远程控制您的钱包。按下二维码将您的钱包与兼容的应用程序链接。", warning_text: "警告:任何有权访问此连接字符串的人都可以从您的钱包发起支付。请勿分享!", }, actions: { copy: { label: "@:global.actions.copy.label", }, close: { label: "@:global.actions.close.label", }, }, }, MintMotdMessage: { title: "Mint 消息", }, MintDetailsDialog: { contact: { title: "联系", }, details: { title: "Mint 详情", url: { label: "URL", }, nuts: { label: "Nuts", actions: { show: { label: "显示全部", }, hide: { label: "隐藏", }, }, }, currency: { label: "货币", }, currencies: { label: "@:MintDetailsDialog.details.currency.label", }, version: { label: "版本", }, }, actions: { title: "操作", copy_mint_url: { label: "复制 Mint URL", }, delete: { label: "删除 Mint", }, edit: { label: "编辑 Mint", }, }, }, ChooseMint: { title: "选择 Mint", badge_mint_error_text: "错误", badge_option_mint_error_text: "@:ChooseMint.badge_mint_error_text", }, HistoryTable: { empty_text: "暂无历史记录", row: { type_label: "Ecash", date_label: "{ value } 前", }, actions: { check_status: { tooltip_text: "检查状态", }, receive: { tooltip_text: "接收", }, filter_pending: { label: "过滤待处理", }, show_all: { label: "显示全部", }, }, old_token_not_found_error_text: "未找到旧 token", }, InvoiceTable: { empty_text: "暂无发票", row: { type_label: "Lightning", type_tooltip_text: "点击复制", date_label: "{ value } 前", }, actions: { check_status: { tooltip_text: "检查状态", }, filter_pending: { label: "过滤待处理", }, show_all: { label: "显示全部", }, }, }, RemoveMintDialog: { title: "您确定要删除此 Mint 吗?", nickname: { label: "昵称", }, balances: { label: "余额", }, warning_text: "注意:由于此钱包是偏执的,您的此 Mint 中的 ecash 不会真正删除,但会保留在您的设备上。如果您稍后再次添加此 Mint,您会再次看到它。", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { confirm: { label: "删除 Mint", }, cancel: { label: "@:global.actions.cancel.label", }, }, }, ParseInputComponent: { placeholder: { default: "Cashu token 或闪电地址", receive: "Cashu token", pay: "闪电地址或发票", }, qr_scanner: { title: "扫描二维码", description: "点击扫描地址", }, paste_button: { label: "@:global.actions.paste.label", }, }, PayInvoiceDialog: { input_data: { title: "支付 Lightning", inputs: { invoice_data: { label: "Lightning 发票或地址", }, }, actions: { close: { label: "@:global.actions.close.label", }, enter: { label: "@:global.actions.enter.label", }, paste: { label: "@:global.actions.paste.label", }, scan: { label: "@:global.actions.scan.label", }, }, }, lnurlpay: { amount_exact_label: "{ payee } 请求 { value } { ticker }", amount_range_label: "{ payee } 请求{br}介于 { min } 和 { max } { ticker } 之间", sending_to_lightning_address: "发送至 { address }", inputs: { amount: { label: "金额 ({ ticker }) *", }, comment: { label: "评论 (可选)", }, }, actions: { close: { label: "@:global.actions.close.label", }, send: { label: "@:global.actions.send.label", }, }, }, invoice: { title: "支付 { value }", paying: "支付中", paid: "已支付", fee: "费用", memo: { label: "备忘录", }, processing_info_text: "正在处理…", balance_too_low_warning_text: "余额不足", actions: { close: { label: "@:global.actions.close.label", }, pay: { label: "支付", in_progress: "@:PayInvoiceDialog.invoice.processing_info_text", error: "错误", }, }, }, }, EditMintDialog: { title: "编辑 Mint", inputs: { nickname: { label: "昵称", }, mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, update: { label: "@:global.actions.update.label", }, }, }, AddMintDialog: { title: "您信任此 Mint 吗?", description: "在使用此 Mint 之前,请确保您信任它。Mints 随时可能变得恶意或停止运营。", inputs: { mint_url: { label: "@:global.inputs.mint_url.label", }, }, actions: { cancel: { label: "@:global.actions.cancel.label", }, add_mint: { label: "@:global.actions.add_mint.label", in_progress: "正在添加 Mint", }, }, }, restore: { mnemonic_error_text: "请输入助记符", restore_mint_error_text: "恢复 Mint 错误: { error }", prepare_info_text: "正在准备恢复流程…", restored_proofs_for_keyset_info_text: "已为 keyset { keysetId } 恢复 { restoreCounter } 个证明", checking_proofs_for_keyset_info_text: "正在检查 keyset { keysetId } 的证明 { startIndex } 到 { endIndex }", no_proofs_info_text: "未找到要恢复的证明", restored_amount_success_text: "已恢复 { amount }", }, swap: { in_progress_warning_text: "兑换进行中", invalid_swap_data_error_text: "无效的兑换数据", swap_error_text: "兑换错误", }, TokenInformation: { fee: "费用", unit: "单位", fiat: "法币", p2pk: "P2PK", locked: "已锁定", locked_to_you: "已锁定给您", mint: "铸币厂", memo: "备注", payment_request: "支付请求", nostr: "Nostr", token_copied: "令牌已复制到剪贴板", }, }; ================================================ FILE: src/icons.js ================================================ import { createApp } from "vue"; import { LucideX, LucideQrCode, LucideWallet, LucideZap, // Add other icons you need here } from "lucide-vue-next"; export default function (app) { app.component("IconClose", LucideX); app.component("IconQrCode", LucideQrCode); app.component("IconWallet", LucideWallet); app.component("IconZap", LucideZap); // Register other icons here } ================================================ FILE: src/js/__tests__/legacy-qr.test.js ================================================ import { describe, expect, it } from "vitest"; import { isLegacyRetailQR, translateLegacyQRToLightningAddress, } from "../legacy-qr"; describe("legacy-qr", () => { describe("isLegacyRetailQR", () => { it("should return false for non-string values", () => { expect(isLegacyRetailQR(null)).toBe(false); expect(isLegacyRetailQR(undefined)).toBe(false); expect(isLegacyRetailQR(123)).toBe(false); expect(isLegacyRetailQR({})).toBe(false); expect(isLegacyRetailQR([])).toBe(false); }); it("should return true for EMV QR codes (starting with 000201)", () => { expect(isLegacyRetailQR("000201")).toBe(true); expect( isLegacyRetailQR( "00020126260008za.co.mp0110248723666427530023za.co.electrum.picknpay0122ydgKJviKSomaVw0297RaZw5303710540571.406304CE9C" ) ).toBe(true); }); it("should return false for non-EMV codes", () => { expect(isLegacyRetailQR("00020")).toBe(false); expect(isLegacyRetailQR("lnbc1234567890")).toBe(false); expect(isLegacyRetailQR("cashuA123456")).toBe(false); }); it("should handle whitespace", () => { expect(isLegacyRetailQR(" 000201 ")).toBe(true); }); }); describe("translateLegacyQRToLightningAddress", () => { it("should convert PicknPay EMV QR code", () => { const qr = "00020126260008za.co.mp0110248723666427530023za.co.electrum.picknpay0122ydgKJviKSomaVw0297RaZw5303710540571.406304CE9C"; expect(translateLegacyQRToLightningAddress(qr)).toBe( `${qr}@cryptoqr.net` ); }); it("should convert Ecentric EMV QR code", () => { const qr = "00020129530019za.co.ecentric.payment0122RD2HAK3KTI53EC/confirm520458125303710540115802ZA5916cryptoqrtestscan6002CT63049BE2"; // slash is URL-encoded as %2F expect(translateLegacyQRToLightningAddress(qr)).toBe( "00020129530019za.co.ecentric.payment0122RD2HAK3KTI53EC%2Fconfirm520458125303710540115802ZA5916cryptoqrtestscan6002CT63049BE2@cryptoqr.net" ); }); it("should return null for non-EMV codes", () => { expect(translateLegacyQRToLightningAddress("lnbc123")).toBeNull(); }); it("should return null for unsupported merchants", () => { expect( translateLegacyQRToLightningAddress( "00020129530023other.merchant.code0122test" ) ).toBeNull(); }); }); }); ================================================ FILE: src/js/__tests__/token.test.js ================================================ import token from "../token"; import { describe, expect, it } from "vitest"; const VALID_V4_TOKEN = "cashuBo2FteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luYXVjc2F0YXSBomFpSABQBVDwSUFGYXCBpGFhAWFzeEBiMjBjYjNkZmZjMzY0ZWQ2ZDhiNWIzZjAzNDEzODQ4ZDU2MTZhMmZiMGMxZGEyMDNlN2ExYTNlNzM4NzBhNWJjYWNYIQIBBWMjM-FXqkXqHeiQMsK24hFzqeittTtTRBv9o6-LO2Fko2FlWCC49Cmyit61XiLZeotP_058iw6Av1Du2r3HY4oNoU1ws2FzWCDb9bMQubjVh9BzuQPP2JyEE0pExaEk6Vk8-h_c3UofuGFyWCCwhsZx88PRY9DMO5h6uiT140WF9zgBKNUrvcY3ssEHmA"; const VALID_V3_TOKEN = "cashuAeyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJpZCI6IkkyeU4raVJZZmt6VCIsImFtb3VudCI6MSwiQyI6IjAyZTRkYmJmMGZmNDI4YTU4ZDZjNjZjMTljNjI0YWRlY2MxNzg0YzdlNTU5ODZhNGVmNDQ4NDM5MzZhM2M4ZjM1OSIsInNlY3JldCI6ImZHWVpzSlVjME1mU1orVlhGandEZXNsNkJScW5wNmRSblZpUGQ2L00yQ0k9In1dLCJtaW50IjoiaHR0cHM6Ly84MzMzLnNwYWNlOjMzMzgifV19"; const VALID_V2_TOKEN = "eyJwcm9vZnMiOlt7ImlkIjoiSTJ5TitpUllma3pUIiwiYW1vdW50IjoxLCJDIjoiMDNjMzAwYzMzMzAzNTMzNDA3MjYwMzU3MzA3NzViNGM2YjRlMDRlYmVjOGY2OGVmYzVmYjY2ZDE3OTI0ZDRkMmQyIiwic2VjcmV0IjoicjE5S3I1anlwQXNaWm1tOUg3cUtFQWJsc1c1ZmsxaWsycFQwUWs2TFUxWT0ifV0sIm1pbnRzIjpbeyJ1cmwiOiJodHRwczovLzgzMzMuc3BhY2U6MzMzOCIsImlkcyI6WyJMM3p4eFJCL0k4dUUiLCJJMnlOK2lSWWZrelQiXX1dfQ=="; describe("token", () => { describe("decode", () => { it("should properly decode a V4 token", () => { const decoded = token.decode(VALID_V4_TOKEN); expect(decoded.proofs.length).toEqual(1); expect(decoded.mint).toEqual("https://mint.minibits.cash/Bitcoin"); expect(decoded.proofs.length).toEqual(1); }); it("should properly decode a V3 token", () => { const decoded = token.decode(VALID_V3_TOKEN); expect(decoded.proofs.length).toEqual(1); expect(decoded.mint).toEqual("https://8333.space:3338"); expect(decoded.proofs.length).toEqual(1); }); it("should throw unsupported token error for a V2 token", () => { expect(() => token.decode(VALID_V2_TOKEN)).toThrow( "Token version is not supported" ); }); }); it("should throw if the token is invalid or V2", () => { expect(() => token.decode("invalid")).toThrow(); }); }); ================================================ FILE: src/js/base64.js ================================================ function unescapeBase64Url(str) { return (str + "===".slice((str.length + 3) % 4)) .replace(/-/g, "+") .replace(/_/g, "/"); } function escapeBase64Url(str) { return str.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); } const uint8ToBase64 = (function (exports) { "use strict"; const encode = function encode(uint8array) { const output = []; for (let i = 0; i < uint8array.length; i++) { output.push(String.fromCharCode(uint8array[i])); } return btoa(output.join("")); }; const asCharCode = function asCharCode(c) { return c.charCodeAt(0); }; const decode = function decode(chars) { return Uint8Array.from(atob(chars), asCharCode); }; exports.decode = decode; exports.encode = encode; return exports; })({}); export { uint8ToBase64 }; ================================================ FILE: src/js/dhke.js ================================================ import { bytesToNumber } from "./utils"; import * as nobleSecp256k1 from "@noble/secp256k1"; async function hashToCurve(secretMessage) { let point; while (!point) { const hash = await nobleSecp256k1.utils.sha256(secretMessage); const hashHex = nobleSecp256k1.utils.bytesToHex(hash); const pointX = "02" + hashHex; try { point = nobleSecp256k1.Point.fromHex(pointX); } catch (error) { secretMessage = await nobleSecp256k1.utils.sha256(secretMessage); } } return point; } async function step1Alice(secretMessage) { secretMessage = nobleSecp256k1.utils.bytesToHex(secretMessage); secretMessage = new TextEncoder().encode(secretMessage); const Y = await hashToCurve(secretMessage); const r_bytes = nobleSecp256k1.utils.randomPrivateKey(); const r = bytesToNumber(r_bytes); const P = nobleSecp256k1.Point.fromPrivateKey(r); const B_ = Y.add(P); return { B_: B_.toHex(true), r: nobleSecp256k1.utils.bytesToHex(r_bytes) }; } function step3Alice(C_, r, A) { const rInt = bytesToNumber(r); const C = C_.subtract(A.multiply(rInt)); return C; } export { step1Alice, step3Alice, hashToCurve }; ================================================ FILE: src/js/eventBus.js ================================================ import { reactive } from "vue"; export const EventBus = reactive({ events: {}, on(event, callback) { if (!this.events[event]) { this.events[event] = []; } this.events[event].push(callback); }, off(event, callback) { if (!this.events[event]) return; this.events[event] = this.events[event].filter((cb) => cb !== callback); }, emit(event, payload) { console.log("eventBus emit", event, payload); if (!this.events[event]) return; this.events[event].forEach((callback) => callback(payload)); }, }); ================================================ FILE: src/js/legacy-qr.js ================================================ // Converts South African retail EMV QR codes to Lightning Address format via cryptoqr.net const MERCHANT_PATTERNS = [ /(?.*za\.co\.electrum\.picknpay.*)/iu, /(?.*za\.co\.ecentric.*)/iu, ]; export function isLegacyRetailQR(code) { return typeof code === "string" && code.trim().startsWith("000201"); } export function translateLegacyQRToLightningAddress(qrCode) { if (!isLegacyRetailQR(qrCode)) return null; const trimmed = qrCode.trim(); for (const pattern of MERCHANT_PATTERNS) { const match = trimmed.match(pattern); if (match?.groups?.identifier) { return `${encodeURIComponent(match.groups.identifier)}@cryptoqr.net`; } } return null; } ================================================ FILE: src/js/notify.ts ================================================ import { Notify, QNotifyCreateOptions } from "quasar"; type StatusMap = { [x: number]: "warning" | "negative" }; const errorTypes = { 400: "warning", 401: "warning", 500: "negative", } as StatusMap; async function notifyApiError( error: Error, caption: string = "", position = "top" as QNotifyCreateOptions["position"] ) { try { Notify.create({ timeout: 5000, type: "warning", position, message: error.message, caption: caption ?? null, progress: true, actions: [ { icon: "close", color: "white", handler: () => {}, }, ], }); } catch (e) { // skip } } async function notifySuccess( message: string, position = "top" as QNotifyCreateOptions["position"] ) { Notify.create({ timeout: 5000, type: "positive", message: message, position, progress: true, actions: [ { icon: "close", color: "white", handler: () => {}, }, ], }); } async function notifyError(message: string, caption?: string) { Notify.create({ color: "red", message: message, caption, position: "top", progress: true, actions: [ { icon: "close", color: "white", handler: () => {}, }, ], }); } async function notifyWarning( message: string, caption?: string, timeout = 5000 ) { Notify.create({ timeout: timeout, type: "warning", message: message, caption: caption, position: "top", progress: true, actions: [ { icon: "close", color: "black", handler: () => {}, }, ], }); } async function notify( message: string, position = "top" as QNotifyCreateOptions["position"] ) { // failure Notify.create({ timeout: 5000, type: "null", color: "grey", message: message, position: position, actions: [ { icon: "close", color: "white", handler: () => {}, }, ], }); } export { notifyApiError, notifySuccess, notifyError, notifyWarning, notify }; ================================================ FILE: src/js/string-utils.js ================================================ function shortenString(s, length = 20, lastchars = 5) { if (s.length > length + lastchars) { return ( s.substring(0, length) + "..." + s.substring(s.length - lastchars, s.length) ); } } export { shortenString }; ================================================ FILE: src/js/token.ts ================================================ import { type Token, getDecodedToken, getTokenMetadata, Mint, TokenMetadata, } from "@cashu/cashu-ts"; import { useMintsStore, WalletProof } from "src/stores/mints"; import { useProofsStore } from "src/stores/proofs"; export default { decode, decodeFull, getProofs, getMint, getUnit, getMemo }; /** * Decodes an encoded cashu token metadata */ function decode(encoded_token: string): TokenMetadata { if (!encoded_token || encoded_token === "") return; const metadata = getTokenMetadata(encoded_token); metadata.proofs = metadata.incompleteProofs; return metadata; } /** * Decodes an encoded cashu token with full proofs */ async function decodeFull(encoded_token: string): Promise { if (!encoded_token || encoded_token === "") return; try { return getDecodedToken(encoded_token, useMintsStore().allMintKeysets); } catch (error) { const tokenMint = getTokenMetadata(encoded_token).mint; const fetchKeysets = await new Mint(tokenMint).getKeySets(); return getDecodedToken(encoded_token, fetchKeysets.keysets); } } /** * Returns a list of proofs from a decoded token */ function getProofs(decoded_token: Token): WalletProof[] { if (!(decoded_token.proofs.length > 0)) { throw new Error("Token format wrong"); } const proofs = decoded_token.proofs.flat(); return useProofsStore().proofsToWalletProofs(proofs); } function getMint(decoded_token: Token) { /* Returns first mint of a token (very rough way). */ if (decoded_token.proofs.length > 0) { return decoded_token.mint; } else { return ""; } } function getUnit(decoded_token: Token) { if (decoded_token.unit != null) { return decoded_token.unit; } else { // search for unit in mints[...].keysets[...].unit const mintStore = useMintsStore(); const mint = getMint(decoded_token); const keysets = mintStore.mints .filter((m) => m.url === mint) .flatMap((m) => m.keysets); if (keysets.length > 0) { return keysets[0].unit; } return ""; } } function getMemo(decoded_token: Token) { if (decoded_token.memo != null) { return decoded_token.memo; } else { return ""; } } ================================================ FILE: src/js/utils.js ================================================ import { date } from "quasar"; import * as nobleSecp256k1 from "@noble/secp256k1"; function splitAmount(value) { const chunks = []; for (let i = 0; i < 32; i++) { const mask = 1 << i; if ((value & mask) !== 0) chunks.push(Math.pow(2, i)); } return chunks; } function bytesToNumber(bytes) { return hexToNumber(nobleSecp256k1.etc.bytesToHex(bytes)); } function bigIntStringify(key, value) { return typeof value === "bigint" ? value.toString() : value; } function hexToNumber(hex) { if (typeof hex !== "string") { throw new TypeError("hexToNumber: expected string, got " + typeof hex); } return BigInt(`0x${hex}`); } function currentDateStr() { return date.formatDate(new Date(), "YYYY-MM-DD HH:mm:ss"); } export { splitAmount, bytesToNumber, bigIntStringify, currentDateStr }; ================================================ FILE: src/js/wallet-helpers.js ================================================ function getShortUrl(url) { url = url.replace("https://", ""); url = url.replace("http://", ""); const cut_param = 26; if (url.length > cut_param && url.indexOf("/") != -1) { url = url.substring(0, url.indexOf("/") + 1) + "..." + url.substring(url.length - cut_param / 2, url.length); } // cut the url if it is too long, keep the first cut_param/2 characters and the last cut_param/2 characters if (url.length > cut_param) { url = url.substring(0, cut_param / 2) + "..." + url.substring(url.length - cut_param / 2, url.length); } return url; } export { getShortUrl }; ================================================ FILE: src/layouts/BlankLayout.vue ================================================ ================================================ FILE: src/layouts/FullscreenLayout.vue ================================================ ================================================ FILE: src/layouts/MainLayout.vue ================================================ ================================================ FILE: src/main.js ================================================ import { createApp } from "vue"; import App from "./App.vue"; import registerIcons from "./icons"; const app = createApp(App); registerIcons(app); ================================================ FILE: src/pages/AlreadyRunning.vue ================================================ ================================================ FILE: src/pages/CreateMintReviewPage.vue ================================================ ================================================ FILE: src/pages/ErrorNotFound.vue ================================================ ================================================ FILE: src/pages/MintDetailsPage.vue ================================================ ================================================ FILE: src/pages/MintDiscoveryPage.vue ================================================ ================================================ FILE: src/pages/MintRatingsPage.vue ================================================ ================================================ FILE: src/pages/Restore.vue ================================================ ================================================ FILE: src/pages/Settings.vue ================================================ ================================================ FILE: src/pages/TermsPage.vue ================================================ ================================================ FILE: src/pages/WalletPage.vue ================================================ ================================================ FILE: src/pages/WelcomePage.vue ================================================ ================================================ FILE: src/pages/welcome/WelcomeMintSetup.vue ================================================ ================================================ FILE: src/pages/welcome/WelcomeRecoverSeed.vue ================================================ ================================================ FILE: src/pages/welcome/WelcomeRestoreEcash.vue ================================================ ================================================ FILE: src/pages/welcome/WelcomeSlide1.vue ================================================ ================================================ FILE: src/pages/welcome/WelcomeSlide2.vue ================================================ ================================================ FILE: src/pages/welcome/WelcomeSlide3.vue ================================================ ================================================ FILE: src/pages/welcome/WelcomeSlide4.vue ================================================ ================================================ FILE: src/pages/welcome/WelcomeSlideChoice.vue ================================================ ================================================ FILE: src/router/index.js ================================================ import { route } from "quasar/wrappers"; import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory, } from "vue-router"; import routes from "./routes"; /* * If not building with SSR mode, you can * directly export the Router instantiation; * * The function below can be async too; either use * async/await or return a Promise which resolves * with the Router instance. */ export default route(function (/* { store, ssrContext } */) { const createHistory = process.env.SERVER ? createMemoryHistory : process.env.VUE_ROUTER_MODE === "history" ? createWebHistory : createWebHashHistory; const Router = createRouter({ scrollBehavior: () => ({ left: 0, top: 0 }), routes, // Leave this as is and make changes in quasar.conf.js instead! // quasar.conf.js -> build -> vueRouterMode // quasar.conf.js -> build -> publicPath history: createHistory(process.env.VUE_ROUTER_BASE), }); return Router; }); ================================================ FILE: src/router/routes.js ================================================ const routes = [ { path: "/", component: () => import("layouts/MainLayout.vue"), children: [ { path: "", component: () => import("src/pages/WalletPage.vue") }, ], }, { path: "/settings", component: () => import("layouts/FullscreenLayout.vue"), children: [{ path: "", component: () => import("src/pages/Settings.vue") }], }, { path: "/mintdetails", component: () => import("layouts/FullscreenLayout.vue"), children: [ { path: "", component: () => import("src/pages/MintDetailsPage.vue") }, ], }, { path: "/discoverMints", component: () => import("layouts/FullscreenLayout.vue"), children: [ { path: "", component: () => import("src/pages/MintDiscoveryPage.vue") }, ], }, { path: "/mintratings", component: () => import("layouts/FullscreenLayout.vue"), children: [ { path: "", component: () => import("src/pages/MintRatingsPage.vue") }, ], }, { path: "/createreview", component: () => import("layouts/FullscreenLayout.vue"), children: [ { path: "", component: () => import("src/pages/CreateMintReviewPage.vue"), }, ], }, { path: "/restore", component: () => import("layouts/FullscreenLayout.vue"), children: [{ path: "", component: () => import("src/pages/Restore.vue") }], }, { path: "/already-running", component: () => import("layouts/BlankLayout.vue"), children: [ { path: "", component: () => import("src/pages/AlreadyRunning.vue") }, ], }, { path: "/welcome", component: () => import("layouts/BlankLayout.vue"), children: [ { path: "", component: () => import("src/pages/WelcomePage.vue") }, ], }, { path: "/terms", component: () => import("layouts/FullscreenLayout.vue"), children: [ { path: "", component: () => import("src/pages/TermsPage.vue") }, ], }, // Always leave this as last one, // but you can also remove it { path: "/:pathMatch(.*)*", component: () => import("src/pages/ErrorNotFound.vue"), }, ]; export default routes; ================================================ FILE: src/stores/__tests__/wallet.test.js ================================================ import { beforeEach, describe, expect, it, vi } from "vitest"; const h = vi.hoisted(() => { const notify = vi.fn(); const notifyApiError = vi.fn(); const notifyError = vi.fn(); const notifySuccess = vi.fn(); const notifyWarning = vi.fn(); const receiveTokensStore = { showReceiveTokens: false, receiveData: { tokensBase64: "", p2pkPrivateKey: "" }, }; const sendTokensStore = { showSendTokens: false, sendData: { p2pkPubkey: "" }, }; const tokensStore = { historyTokens: [], addPaidToken: vi.fn(), setTokenPaid: vi.fn(), }; const proofsStore = { proofs: [], getUnreservedProofs: vi.fn((proofs) => proofs.filter((p) => !p.reserved)), sumProofs: vi.fn((proofs) => proofs.reduce((sum, p) => sum + p.amount, 0)), removeProofs: vi.fn(), addProofs: vi.fn(), setReserved: vi.fn(), }; const uiStore = { lockMutex: vi.fn(async () => {}), unlockMutex: vi.fn(), closeDialogs: vi.fn(), formatCurrency: vi.fn((amount, unit) => `${amount} ${unit}`), vibrate: vi.fn(), }; const p2pkStore = { isValidPubkey: vi.fn(() => false), setPrivateKeyUsed: vi.fn(), }; const prStore = { decodePaymentRequest: vi.fn(async () => {}), }; const mintsStore = { activeMintUrl: "https://mint-a.example", activeUnit: "sat", addMintData: { url: "", nickname: "" }, mints: [], mintUnitKeysets: vi.fn((mint, unit) => mint.keysets.filter((k) => k.unit === unit) ), mintUnitProofs: vi.fn(() => []), updateMintInfoAndKeys: vi.fn(async () => {}), }; const walletLoadMintFromCache = vi.fn(); const walletGetFeesForProofs = vi.fn(() => 7); const keychainMintToCacheDTO = vi.fn(() => ({ cache: "dto" })); class WalletMock { constructor(url, options) { this.mint = { mintUrl: url }; this.unit = options.unit; this.options = options; this.loadMintFromCache = walletLoadMintFromCache; this.getFeesForProofs = walletGetFeesForProofs; } selectProofsToSend(proofs, amount) { let running = 0; const send = []; for (const proof of proofs) { if (running >= amount) break; send.push(proof); running += proof.amount; } const sendSecrets = new Set(send.map((p) => p.secret)); return { send, keep: proofs.filter((p) => !sendSecrets.has(p.secret)), }; } } return { notify, notifyApiError, notifyError, notifySuccess, notifyWarning, receiveTokensStore, sendTokensStore, tokensStore, proofsStore, uiStore, p2pkStore, prStore, mintsStore, walletLoadMintFromCache, walletGetFeesForProofs, keychainMintToCacheDTO, WalletMock, }; }); vi.mock("src/js/utils", () => ({ currentDateStr: () => "2026-03-10T12:00:00.000Z", })); vi.mock("src/js/notify", () => ({ notify: (...args) => h.notify(...args), notifyApiError: (...args) => h.notifyApiError(...args), notifyError: (...args) => h.notifyError(...args), notifySuccess: (...args) => h.notifySuccess(...args), notifyWarning: (...args) => h.notifyWarning(...args), })); vi.mock("@vueuse/core", () => ({ useLocalStorage: (_key, value) => value, })); vi.mock("vue-i18n", () => ({ useI18n: () => ({ t: (key) => key }), createI18n: () => ({ global: { t: (key) => key, }, }), })); vi.mock("light-bolt11-decoder", () => ({ decode: vi.fn(() => ({ paymentRequest: "lnbc123", sections: [] })), })); vi.mock("@cashu/cashu-ts", () => ({ Wallet: h.WalletMock, KeyChain: { mintToCacheDTO: (...args) => h.keychainMintToCacheDTO(...args), }, CheckStateEnum: { SPENT: "SPENT" }, MeltQuoteState: { PAID: "PAID", PENDING: "PENDING" }, MintQuoteState: { PAID: "PAID", ISSUED: "ISSUED", PENDING: "PENDING" }, })); vi.mock("src/stores/receiveTokensStore", () => ({ useReceiveTokensStore: () => h.receiveTokensStore, })); vi.mock("src/stores/sendTokensStore", () => ({ useSendTokensStore: () => h.sendTokensStore, })); vi.mock("src/stores/p2pk", () => ({ useP2PKStore: () => h.p2pkStore, })); vi.mock("src/stores/payment-request", () => ({ usePRStore: () => h.prStore, })); vi.mock("src/stores/workers", () => ({ useWorkersStore: () => ({ checkTokenSpendableWorker: vi.fn() }), })); vi.mock("src/stores/invoicesWorker", () => ({ useInvoicesWorkerStore: () => ({ addInvoice: vi.fn(), removeInvoice: vi.fn(), }), })); vi.mock("src/stores/ui", () => ({ useUiStore: () => h.uiStore, })); vi.mock("src/stores/proofs", () => ({ useProofsStore: () => h.proofsStore, })); vi.mock("src/stores/tokens", () => ({ useTokensStore: () => h.tokensStore, })); vi.mock("src/stores/mints", async () => { const actual = await vi.importActual("src/stores/mints"); return { ...actual, useMintsStore: () => h.mintsStore, }; }); import { useWalletStore } from "src/stores/wallet"; describe("wallet store", () => { beforeEach(() => { vi.clearAllMocks(); h.receiveTokensStore.showReceiveTokens = false; h.receiveTokensStore.receiveData.tokensBase64 = ""; h.sendTokensStore.sendData.p2pkPubkey = ""; h.sendTokensStore.showSendTokens = false; h.mintsStore.activeMintUrl = "https://mint-a.example"; h.mintsStore.activeUnit = "sat"; h.mintsStore.addMintData = { url: "", nickname: "" }; h.mintsStore.mints = [ { url: "https://mint-a.example", keys: [{ id: "00aa" }], keysets: [ { id: "base64-a", unit: "sat", active: true }, { id: "00aa", unit: "sat", active: true }, ], info: { name: "mint-a" }, }, ]; h.proofsStore.getUnreservedProofs.mockImplementation((proofs) => proofs.filter((p) => !p.reserved) ); h.proofsStore.sumProofs.mockImplementation((proofs) => proofs.reduce((sum, p) => sum + p.amount, 0) ); h.p2pkStore.isValidPubkey.mockReturnValue(false); }); it("manages keyset counters", () => { const wallet = useWalletStore(); expect(wallet.keysetCounter("k1")).toBe(1); wallet.increaseKeysetCounter("k1", 4); expect(wallet.keysetCounter("k1")).toBe(5); wallet.increaseKeysetCounter("k2", 3); expect(wallet.keysetCounter("k2")).toBe(3); }); it("creates a new mnemonic and archives previous counters", () => { const wallet = useWalletStore(); wallet.mnemonic = "old mnemonic"; wallet.keysetCounters = [{ id: "00aa", counter: 11 }]; wallet.newMnemonic(); expect(wallet.oldMnemonicCounters[0]).toEqual({ mnemonic: "old mnemonic", keysetCounters: [{ id: "00aa", counter: 11 }], }); expect(wallet.keysetCounters).toEqual([]); expect(wallet.mnemonic).not.toBe("old mnemonic"); }); it("splits amounts into binary chunks", () => { const wallet = useWalletStore(); expect(wallet.splitAmount(0)).toEqual([]); expect(wallet.splitAmount(13)).toEqual([1, 4, 8]); }); it("selects base64 proofs by descending amount", () => { const wallet = useWalletStore(); const proofs = [ { id: "base64-1", amount: 2, reserved: false }, { id: "00aa", amount: 32, reserved: false }, { id: "base64-2", amount: 8, reserved: false }, { id: "base64-3", amount: 4, reserved: false }, ]; expect(wallet.coinSelectSpendBase64(proofs, 10)).toEqual([ { id: "base64-2", amount: 8, reserved: false }, { id: "base64-3", amount: 4, reserved: false }, ]); expect(wallet.coinSelectSpendBase64(proofs, 20)).toEqual([]); }); it("returns spendable proofs and throws on insufficient amount", () => { const wallet = useWalletStore(); const proofs = [ { id: "00aa", amount: 5, reserved: false }, { id: "00aa", amount: 4, reserved: true }, { id: "00aa", amount: 6, reserved: false }, ]; expect(wallet.spendableProofs(proofs, 10)).toHaveLength(2); expect(() => wallet.spendableProofs(proofs, 20)).toThrow( "wallet.notifications.balance_too_low" ); }); it("chooses active hex keyset first", () => { const wallet = useWalletStore(); expect(wallet.getKeyset("https://mint-a.example", "sat")).toBe("00aa"); }); it("updates invoice status to paid", () => { const wallet = useWalletStore(); wallet.invoiceHistory = [ { quote: "q-1", amount: 1, bolt11: "lnbc", memo: "memo", date: "old", status: "pending", mint: "https://mint-a.example", unit: "sat", }, ]; wallet.setInvoicePaid("q-1"); expect(wallet.invoiceHistory[0].status).toBe("paid"); expect(wallet.invoiceHistory[0].paidDate).toBe("2026-03-10T12:00:00.000Z"); }); it("adds, updates and removes outgoing invoices", async () => { const wallet = useWalletStore(); wallet.payInvoiceData.input.request = "lnbc123"; const quote = { quote: "melt-q-1", amount: 101, fee_reserve: 4 }; await wallet.addOutgoingPendingInvoiceToHistory( quote, "https://mint-a.example", "sat" ); wallet.updateOutgoingInvoiceInHistory(quote, { status: "paid", amount: -99, }); wallet.removeOutgoingInvoiceFromHistory("melt-q-1"); expect(wallet.invoiceHistory).toEqual([]); }); it("creates active wallet and loads cache", async () => { const wallet = useWalletStore(); wallet.mnemonic = ""; wallet.keysetCounters = [{ id: "00aa", counter: 13 }]; const activeWallet = await wallet.activeWallet(); expect(activeWallet.mint.mintUrl).toBe("https://mint-a.example"); expect(h.keychainMintToCacheDTO).toHaveBeenCalled(); expect(h.walletLoadMintFromCache).toHaveBeenCalledWith( { name: "mint-a" }, { cache: "dto" } ); }); it("refreshes stale keysets when requested", async () => { const wallet = useWalletStore(); h.mintsStore.mints = [ { url: "https://mint-a.example", keys: [{ id: "00aa" }], keysets: [{ id: "00aa", unit: "sat", active: true }], info: { name: "mint-a" }, lastKeysetsUpdated: "1970-01-01T00:00:00.000Z", }, ]; await wallet.mintWallet("https://mint-a.example", "sat", true); expect(h.mintsStore.updateMintInfoAndKeys).toHaveBeenCalledTimes(1); }); it("accounts for signed-output errors", () => { const wallet = useWalletStore(); wallet.keysetCounters = [{ id: "00aa", counter: 1 }]; const handled = wallet.handleOutputsHaveAlreadyBeenSignedError("00aa", { message: "outputs have already been signed", }); expect(handled).toBe(true); expect(wallet.keysetCounter("00aa")).toBe(11); expect(h.notify).toHaveBeenCalledWith( "wallet.notifications.please_try_again" ); }); it("routes decodeRequest branches", async () => { const wallet = useWalletStore(); vi.spyOn(wallet, "handleBolt11Invoice").mockResolvedValue(undefined); vi.spyOn(wallet, "lnurlPayFirst").mockResolvedValue(undefined); vi.spyOn(wallet, "handlePaymentRequest").mockResolvedValue(undefined); await wallet.decodeRequest(" lightning:lnbcabc "); await wallet.decodeRequest( "bitcoin:bc1qxyz?lightning=lnbcfrombitcoin&amount=1" ); await wallet.decodeRequest( "bitcoin:bc1qxyz?creq=creqb1cashurequest&lightning=lnbcfrombitcoin&amount=1" ); await wallet.decodeRequest("lnurl:lnurl1example"); await wallet.decodeRequest("cashuAabcdef"); h.p2pkStore.isValidPubkey.mockReturnValueOnce(true); await wallet.decodeRequest("02abcdef"); await wallet.decodeRequest("https://mint-b.example"); await wallet.decodeRequest("creqA123"); await wallet.decodeRequest("creqb1xyz"); expect(h.receiveTokensStore.receiveData.tokensBase64).toBe("cashuAabcdef"); expect(h.sendTokensStore.sendData.p2pkPubkey).toBe("02abcdef"); expect(h.mintsStore.addMintData.url).toBe("https://mint-b.example"); expect(wallet.handlePaymentRequest).toHaveBeenCalledWith("creqA123"); expect(wallet.handlePaymentRequest).toHaveBeenCalledWith("creqb1xyz"); expect(wallet.handlePaymentRequest).toHaveBeenCalledWith( "creqb1cashurequest" ); expect(h.uiStore.closeDialogs).toHaveBeenCalled(); }); }); ================================================ FILE: src/stores/camera.ts ================================================ import { defineStore } from "pinia"; export const useCameraStore = defineStore("camera", { state: () => ({ camera: { data: null, show: false, camera: "auto", }, hasCamera: function () { navigator.permissions.query({ name: "camera" }).then((res) => { return res.state == "granted"; }); }, }), actions: { closeCamera: function () { this.camera.show = false; }, showCamera: function () { this.camera.show = true; }, }, }); ================================================ FILE: src/stores/dexie.ts ================================================ import { defineStore } from "pinia"; import Dexie, { Table } from "dexie"; import { useLocalStorage } from "@vueuse/core"; import { WalletProof } from "./mints"; import { useStorageStore } from "./storage"; import { useProofsStore } from "./proofs"; import { notifyError, notifySuccess } from "../js/notify"; // export interface Proof { // id: string // C: string // amount: number // reserved: boolean // secret: string // quote?: string // } export class CashuDexie extends Dexie { proofs!: Table; constructor() { super("db"); this.version(1).stores({ proofs: "secret, id, C, amount, reserved, quote", }); } } export const cashuDb = new CashuDexie(); export const useDexieStore = defineStore("dexie", { state: () => ({ migratedToDexie: useLocalStorage("cashu.dexie.migrated", false), }), getters: {}, actions: { migrateToDexie: async function () { const proofsStore = useProofsStore(); if (this.migratedToDexie) { return; } console.log("Migrating to Dexie"); const proofs = localStorage.getItem("cashu.proofs"); let parsedProofs: WalletProof[] = []; if (!proofs) { console.log("No cashu.proofs in localStorage to migrate"); this.migratedToDexie = true; return; } parsedProofs = JSON.parse(proofs) as WalletProof[]; if (!parsedProofs.length) { console.log("No proofs to migrate"); this.migratedToDexie = true; return; } // start migration await useStorageStore().exportWalletState(); parsedProofs.forEach((proof) => { cashuDb.proofs.add(proof); }); console.log( `Migrated ${cashuDb.proofs.count()} proofs. Before: ${ parsedProofs.length } proofs, After: ${(await proofsStore.getProofs()).length} proofs` ); console.log( `Proofs sum before: ${proofsStore.sumProofs( parsedProofs )}, after: ${proofsStore.sumProofs(await proofsStore.getProofs())}` ); this.migratedToDexie = true; // remove proofs from localstorage localStorage.removeItem("cashu.proofs"); }, deleteAllTables: function () { cashuDb.proofs.clear(); }, }, }); ================================================ FILE: src/stores/index.js ================================================ import { store } from "quasar/wrappers"; import { createPinia } from "pinia"; /* * If not building with SSR mode, you can * directly export the Store instantiation; * * The function below can be async too; either use * async/await or return a Promise which resolves * with the Store instance. */ export default store((/* { ssrContext } */) => { const pinia = createPinia(); // You can add Pinia plugins here // pinia.use(SomePiniaPlugin) return pinia; }); ================================================ FILE: src/stores/invoicesWorker.ts ================================================ import { defineStore } from "pinia"; import { useWalletStore } from "./wallet"; import { useLocalStorage } from "@vueuse/core"; import { useSettingsStore } from "./settings"; interface InvoiceQuote { quote: string; addedAt: number; lastChecked: number; checkCount: number; } export const useInvoicesWorkerStore = defineStore("invoicesWorker", { state: () => { return { checkInterval: 5000, maxLength: 50, // Two weeks maxAge: 1000 * 60 * 60 * 24 * 14, oneDay: 1000 * 60 * 60 * 24, oneHour: 1000 * 60 * 60, // Once per day maxInterval: 1000 * 60 * 60 * 24, keepIntervalConstantForNChecks: 5, invoiceCheckListener: null as NodeJS.Timeout | null, invoiceWorkerRunning: false, quotes: useLocalStorage( "cashu.worker.invoices.quotesQueue", [] ), lastInvoiceCheckTime: 0, maxQuotesToCheckOnStartup: 10, lastPendingInvoiceCheck: useLocalStorage( "cashu.worker.invoices.lastPendingInvoiceCheck", 0 ), checkPendingInvoicesInterval: 1000 * 10, // delay between bulk invoice checks }; }, actions: { startInvoiceCheckerWorker() { if (!useSettingsStore().periodicallyCheckIncomingInvoices) return; if (this.invoiceCheckListener) return; this.invoiceWorkerRunning = true; this.invoiceCheckListener = setInterval(() => { this.processQuotes(); }, this.checkInterval); }, stopInvoiceCheckerWorker() { if (this.invoiceCheckListener) { clearInterval(this.invoiceCheckListener); this.invoiceCheckListener = null; this.invoiceWorkerRunning = false; } }, addInvoiceToChecker(quote: string) { const existingIndex = this.quotes.findIndex((q) => q.quote === quote); if (existingIndex !== -1) { this.quotes.splice(existingIndex, 1); } if (this.quotes.length >= this.maxLength) { this.quotes.shift(); } this.quotes.push({ quote, addedAt: Date.now(), lastChecked: 0, checkCount: 0, }); this.startInvoiceCheckerWorker(); }, removeInvoiceFromChecker(quote: string) { const index = this.quotes.findIndex((q) => q.quote === quote); if (index !== -1) { this.quotes.splice(index, 1); } }, dueTime(q: InvoiceQuote) { if (q.checkCount > this.keepIntervalConstantForNChecks) { return ( q.lastChecked + Math.min( this.checkInterval * Math.pow(2, q.checkCount - this.keepIntervalConstantForNChecks), this.maxInterval ) ); } else { return q.lastChecked + this.checkInterval; } }, async processQuotes() { const now = Date.now(); this.quotes = this.quotes.filter((q) => now - q.addedAt < this.maxAge); if (this.quotes.length === 0) return; // Global rate limit if (now - this.lastInvoiceCheckTime < this.checkInterval) { return; } for (let i = this.quotes.length - 1; i >= 0; i--) { const q = this.quotes[i]; const dueTime = this.dueTime(q); if (now > dueTime) { const walletStore = useWalletStore(); try { await walletStore.checkInvoice(q.quote, false); this.quotes.splice(i, 1); } catch (error) { q.lastChecked = now; q.checkCount += 1; } this.lastInvoiceCheckTime = now; break; } } }, async checkPendingInvoices() { if (!useSettingsStore().checkInvoicesOnStartup) return; if ( Date.now() < this.lastPendingInvoiceCheck + this.checkPendingInvoicesInterval ) return; const walletStore = useWalletStore(); const quotesToCheck = walletStore.invoiceHistory.filter( (q) => q.status === "pending" && q.amount > 0 && Date.now() - Date.parse(q.date) < this.oneDay ); if (quotesToCheck.length > this.maxQuotesToCheckOnStartup) { quotesToCheck.splice(this.maxQuotesToCheckOnStartup); } this.lastPendingInvoiceCheck = Date.now(); console.log(`Checking ${quotesToCheck.length} quotes`); for (const q of quotesToCheck) { try { console.log(`Checking quote ${q.quote}`); walletStore.mintOnPaid(q.quote, false, false); } catch (error) { console.error(error); } } }, }, }); ================================================ FILE: src/stores/migrations.ts ================================================ import { defineStore } from "pinia"; import { useLocalStorage } from "@vueuse/core"; import { useMintsStore } from "./mints"; import { notifySuccess } from "../js/notify"; import { useUiStore } from "./ui"; import { useSettingsStore } from "./settings"; import { useNostrMintBackupStore } from "./nostrMintBackup"; // Define the migration version type export type Migration = { version: number; name: string; description: string; execute: () => Promise; }; export const useMigrationsStore = defineStore("migrations", { state: () => ({ currentVersion: useLocalStorage("cashu.migrations.version", 0), migrations: [] as Migration[], }), actions: { registerMigration(migration: Migration) { // Add migration if it doesn't already exist if (!this.migrations.some((m) => m.version === migration.version)) { this.migrations.push(migration); // Sort migrations by version this.migrations.sort((a, b) => a.version - b.version); } }, async runMigrations() { // Get migrations that need to be run (newer than current version) const pendingMigrations = this.migrations.filter( (m) => m.version > this.currentVersion ); if (pendingMigrations.length === 0) { console.log("No migrations to run"); return; } console.log(`Running ${pendingMigrations.length} migrations...`); // Run each migration in order const uIStore = useUiStore(); await uIStore.lockMutex(); try { for (const migration of pendingMigrations) { console.log( `Running migration ${migration.version}: ${migration.name}` ); try { await migration.execute(); // Update the current version after successful migration this.currentVersion = migration.version; console.log( `Migration ${migration.version} completed successfully` ); } catch (error) { console.error(`Migration ${migration.version} failed:`, error); // Stop running migrations if one fails break; } } } finally { await uIStore.unlockMutex(); } }, // First migration: Update mint URL from stablenuts.cash to umint.cash async migrateStablenutsToCash() { const mintStore = useMintsStore(); let updated = false; for (let i = 0; i < mintStore.mints.length; i++) { if (mintStore.mints[i].url === "https://stablenut.umint.cash") { console.log("Updating mint URL from stablenuts.cash to umint.cash"); mintStore.mints[i].url = "https://stablenut.cashu.network"; // If this was the active mint, update the active mint URL as well if (mintStore.activeMintUrl === "https://stablenut.umint.cash") { mintStore.activeMintUrl = "https://stablenut.cashu.network"; } updated = true; } } if (updated) { console.log("Successfully updated mint URL"); } else { console.log("No stablenuts.cash mint found to update"); } }, // Migration v2: add "wss://relay.primal.net " relay, enable nostrMintBackup, clear mint recs cache async migrateAddPrimalRelayAndEnableBackupAndClearMintRecs() { const settings = useSettingsStore(); // 1) Add relay string with leading '@' and trailing space if not present const relayToAdd = "wss://relay.primal.net "; try { const relays = Array.isArray(settings.defaultNostrRelays) ? settings.defaultNostrRelays : []; if (!relays.includes(relayToAdd)) { relays.push(relayToAdd); settings.defaultNostrRelays = relays; console.log(`Added relay to defaultNostrRelays: ${relayToAdd}`); } else { console.log( `Relay already present in defaultNostrRelays: ${relayToAdd}` ); } } catch (e) { console.error( "Failed to update defaultNostrRelays during migration v2", e ); } // 2) Ensure nostrMintBackupEnabled is true try { if (!settings.nostrMintBackupEnabled) { settings.nostrMintBackupEnabled = true; console.log("Enabled nostrMintBackupEnabled setting"); // kick off a backup useNostrMintBackupStore().forceBackup(); } else { console.log("nostrMintBackupEnabled already true"); } } catch (e) { console.error( "Failed to enable nostrMintBackupEnabled during migration v2", e ); } // 3) Clear cached mint recommendations try { localStorage.removeItem("cashu.ndk.mintRecommendations"); console.log("Cleared localStorage key: cashu.ndk.mintRecommendations"); } catch (e) { console.error( "Failed to clear cashu.ndk.mintRecommendations during migration v2", e ); } }, // Initialize migrations initMigrations() { // Register the first migration this.registerMigration({ version: 1, name: "Migrate stablenuts.cash to umint.cash", description: "Updates mint URL from https://stablenut.umint.cash to https://stablenut.cashu.network", execute: async () => await this.migrateStablenutsToCash(), }); // Register migration v2 this.registerMigration({ version: 2, name: "Add wss://relay.primal.net relay; enable mint backup; clear recs", description: "Adds 'wss://relay.primal.net ' to defaultNostrRelays, enables nostrMintBackupEnabled, clears cashu.ndk.mintRecommendations", execute: async () => await this.migrateAddPrimalRelayAndEnableBackupAndClearMintRecs(), }); // Add more migrations here in the future }, }, }); ================================================ FILE: src/stores/mintRecommendations.ts ================================================ import { defineStore } from "pinia"; import NDK, { NDKEvent, NDKFilter, NDKKind } from "@nostr-dev-kit/ndk"; import { useSettingsStore } from "./settings"; import { useNostrStore } from "./nostr"; import { useLocalStorage } from "@vueuse/core"; import Dexie from "dexie"; export type MintReview = { eventId: string; pubkey: string; created_at: number; rating: number | null; comment: string; raw: any; }; export type MintRecommendation = { url: string; reviewsCount: number; averageRating: number | null; info?: any; error?: boolean; lastHttpInfoFetchAt?: number; // unix seconds }; function parseRatingAndComment(content: string): { rating: number | null; comment: string; } { const m = content.match(/\s*\[(\d)\s*\/\s*5\]\s*(.*)$/s); if (!m) return { rating: null, comment: content || "" }; const rating = parseInt(m[1], 10); const comment = (m[2] || "").trim(); if (isNaN(rating) || rating < 1 || rating > 5) return { rating: null, comment }; return { rating, comment }; } export const useMintRecommendationsStore = defineStore("mintRecommendations", { state: () => ({ ndk: {} as NDK, connected: false, // Dexie DB handle dbInitialized: false, dbHydrated: false, db: null as MintReviewsDB | null, // Minimal in-memory caches for UI urlReviews: new Map() as Map, httpInfoByUrl: new Map() as Map, infoTimers: new Map() as Map, inflightInfo: new Set() as Set, infoTimeoutMs: 10000, httpInfoFetchIntervalSeconds: 60 * 60, // 1 hour // Aggregated list by URL (persisted) recommendations: useLocalStorage( "cashu.ndk.mintRecommendations", [] ), subsActive: false, }), actions: { initDb: async function () { if (this.dbInitialized && this.db) return; this.db = new MintReviewsDB(); await this.db.open(); this.dbInitialized = true; }, ensureDbInitialized: async function () { if (!this.dbInitialized || !this.db) await this.initDb(); }, init: function () { if (this.connected) return; const settings = useSettingsStore(); const nostr = useNostrStore(); if (!nostr.ndk || !(nostr.ndk as any).pool) nostr.initNdkReadOnly(); this.ndk = nostr.ndk || new NDK({ explicitRelayUrls: settings.defaultNostrRelays }); this.ndk.connect(); this.connected = true; this.ensureDbInitialized(); this.hydrateFromDb(); }, // Load all reviews from DB into a lightweight cache for instant UI and build aggregates hydrateFromDb: async function () { try { if (this.dbHydrated) return; await this.ensureDbInitialized(); const rows = await (this.db as MintReviewsDB).reviews.toArray(); const map = new Map(); for (const r of rows) { if (!r || !r.url || !r.eventId) continue; const list = map.get(r.url) || []; list.push({ eventId: r.eventId, pubkey: r.pubkey, created_at: r.created_at, rating: r.rating, comment: r.comment, raw: r.raw, }); map.set(r.url, list); } for (const [url, list] of map.entries()) { list.sort((a, b) => (a.created_at || 0) - (b.created_at || 0)); this.urlReviews.set(url, list); } this.dbHydrated = true; await this.rebuildAggregates(); // After hydration, opportunistically refetch stale HTTP info within interval //void this.refetchStaleHttpInfoForKnownMints(); } catch {} }, fetchMintInfos: async function () { this.init(); await this.ensureDbInitialized(); const filter: NDKFilter = { kinds: [38172 as NDKKind], limit: 5000 }; const events = await this.ndk.fetchEvents(filter); for (const ev of events) await this.handleMintInfoEvent(ev); await this.rebuildAggregates(); }, fetchReviews: async function () { this.init(); await this.ensureDbInitialized(); const filter: NDKFilter = { kinds: [38000 as NDKKind], ["#k"]: ["38172"], limit: 5000, } as any; const events = await this.ndk.fetchEvents(filter); for (const ev of events) await this.handleReviewEvent(ev); await this.rebuildAggregates(); }, fetchReviewsForUrl: async function (url: string) { try { this.init(); if (!url || typeof url !== "string" || !url.startsWith("http")) return; const filter: NDKFilter = { kinds: [38000 as NDKKind], ["#k"]: ["38172"], ["#u"]: [url], limit: 5000, } as any; const events = await this.ndk.fetchEvents(filter); for (const ev of events) await this.handleReviewEvent(ev); await this.rebuildAggregates(); } catch {} }, fetchMintInfoForUrl: async function (url: string) { try { this.init(); if (!url || typeof url !== "string" || !url.startsWith("http")) return; const filter: NDKFilter = { kinds: [38172 as NDKKind], ["#u"]: [url], limit: 1000, } as any; const events = await this.ndk.fetchEvents(filter); for (const ev of events) await this.handleMintInfoEvent(ev); await this.rebuildAggregates(); } catch {} }, clearRecommendations: function () { this.recommendations.splice(0, this.recommendations.length); }, clearDiscoveryCaches: async function () { try { await this.ensureDbInitialized(); // Do NOT clear HTTP info; preserve last known info across reloads } catch {} this.inflightInfo.clear(); this.infoTimers.forEach((t) => clearTimeout(t)); this.infoTimers.clear(); await this.rebuildAggregates(); }, setInfoTimeoutMs: function (ms: number) { this.infoTimeoutMs = ms; }, discover: async function (): Promise { await this.fetchMintInfos(); await this.fetchReviews(); return this.recommendations; }, startSubscriptions: function () { if (this.subsActive) return; this.init(); this.hydrateFromDb(); const subInfos = this.ndk.subscribe( { kinds: [38172 as NDKKind] } as NDKFilter, { closeOnEose: false, groupable: false } ); subInfos.on("event", async (ev: NDKEvent) => { await this.handleMintInfoEvent(ev); try { const u = ev.tags.find( (t) => t[0] === "u" && (t[2] === "cashu" || t.length >= 2) )?.[1]; if (typeof u === "string" && u.startsWith("http")) { // Kick off HTTP info fetch (concurrency-limited via scheduler) void this.scheduleHttpInfoFetches([u], 20, 100, this.infoTimeoutMs); } } catch {} void this.rebuildAggregates(); }); const subReviews = this.ndk.subscribe( { kinds: [38000 as NDKKind] } as NDKFilter, { closeOnEose: false, groupable: false } ); subReviews.on("event", async (ev: NDKEvent) => { await this.handleReviewEvent(ev); void this.rebuildAggregates(); }); this.subsActive = true; }, requestMintHttpInfo: async function (url: string, timeoutMs?: number) { try { await this.ensureDbInitialized(); const existing = await (this.db as MintReviewsDB).httpInfo.get(url); const nowSec = Math.floor(Date.now() / 1000); const interval = this.httpInfoFetchIntervalSeconds || 0; const isFresh = !!existing && !!existing.info && !!existing.fetchedAt && nowSec - existing.fetchedAt < interval; if (isFresh) { await this.rebuildAggregates(); return; } if (this.inflightInfo.has(url)) return; this.inflightInfo.add(url); const ms = timeoutMs ?? this.infoTimeoutMs; const tempMint = { url, keys: [], keysets: [] } as any; const mod = await import("src/stores/mints"); console.log("Fetching HTTP info for mint", url); // Start timeout timer only when we actually fire the HTTP request if (!this.infoTimers.has(url)) { const id = setTimeout(async () => { try { const existing = await (this.db as MintReviewsDB).httpInfo.get( url ); const row: HttpInfoRow = { url, info: existing?.info ?? null, fetchedAt: Math.floor(Date.now() / 1000) ?? 0, error: false, }; await (this.db as MintReviewsDB).httpInfo.put(row); void this.rebuildAggregates(); } catch {} this.infoTimers.delete(url); }, ms); this.infoTimers.set(url, id); } const info = await new (mod as any).MintClass(tempMint).api.getInfo(); console.log("HTTP info for mint", url, info.name); const row: HttpInfoRow = { url, info, fetchedAt: Math.floor(Date.now() / 1000), error: false, }; // unset error in localstore too: await (this.db as MintReviewsDB).httpInfo.put(row); // Update in-memory cache only; do not persist info to localStorage this.httpInfoByUrl.set(url, info); // Rebuild aggregates to reflect fresh fetchedAt and clear error await this.rebuildAggregates(); const t = this.infoTimers.get(url); if (t) clearTimeout(t); this.infoTimers.delete(url); // done } catch { console.log("Error fetching HTTP info for mint", url); // Immediately persist error state (do not wait for timer) try { const existing = await (this.db as MintReviewsDB).httpInfo.get(url); const nowSec = Math.floor(Date.now() / 1000); // Failure: keep existing info/fetchedAt if present, only set error const row: HttpInfoRow = { url, info: existing?.info ?? null, fetchedAt: existing?.fetchedAt ?? nowSec, error: true, }; await (this.db as MintReviewsDB).httpInfo.put(row); } catch {} const t = this.infoTimers.get(url); if (t) clearTimeout(t); this.infoTimers.delete(url); await this.rebuildAggregates(); } finally { this.inflightInfo.delete(url); } }, scheduleHttpInfoFetches: async function ( urls: string[], concurrency: number = 20, delayMs: number = 100, timeoutMs?: number ) { try { await this.ensureDbInitialized(); const nowSec = Math.floor(Date.now() / 1000); const interval = this.httpInfoFetchIntervalSeconds || 0; const seen = new Set(); const toFetch: string[] = []; for (const u of urls) { if (typeof u !== "string" || !u.startsWith("http")) continue; if (seen.has(u)) continue; seen.add(u); // Skip if a fetch is already in-flight for this URL if (this.inflightInfo.has(u)) continue; const existing = await (this.db as MintReviewsDB).httpInfo.get(u); const fresh = !!existing && !!existing.fetchedAt && nowSec - existing.fetchedAt < interval; if (!fresh) toFetch.push(u); } if (!toFetch.length) return; let idx = 0; const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); const worker = async () => { while (true) { const i = idx++; if (i >= toFetch.length) break; const u = toFetch[i]; try { await this.requestMintHttpInfo(u, timeoutMs); } catch {} await delay(delayMs); } }; const workers = Array.from( { length: Math.min(concurrency, toFetch.length) }, () => worker() ); await Promise.all(workers); } catch {} }, refetchStaleHttpInfoForKnownMints: async function () { try { await this.ensureDbInitialized(); const urls = this.recommendations.map((r) => r.url); await this.scheduleHttpInfoFetches(urls, 10, 100); } catch {} }, // Expose getters for HTTP info from in-memory cache getHttpInfoForUrl: function (url: string): any | undefined { return this.httpInfoByUrl.get(url); }, hasHttpInfo: function (url: string): boolean { return this.httpInfoByUrl.has(url); }, handleMintInfoEvent: async function (ev: NDKEvent) { try { if (ev.kind !== 38172) return; const u = ev.tags.find( (t) => t[0] === "u" && (t[2] === "cashu" || t.length >= 2) )?.[1]; if (!u || typeof u !== "string" || !u.startsWith("http")) return; let content: any = undefined; try { content = ev.content ? JSON.parse(ev.content) : undefined; } catch {} const row: InfoRow = { url: u, pubkey: ev.pubkey, d: ev.tagValue("d") || "", content, created_at: ev.created_at || 0, }; await (this.db as MintReviewsDB).infos.add(row); } catch {} }, upsertReviewForUrl: async function (url: string, review: MintReview) { if (!url || !url.startsWith("http")) return; const list = this.urlReviews.get(url) || []; if (list.some((r) => r.eventId === review.eventId)) return; const withoutSameAuthor = list.filter((r) => r.pubkey !== review.pubkey); withoutSameAuthor.push(review); withoutSameAuthor.sort( (a, b) => (a.created_at || 0) - (b.created_at || 0) ); this.urlReviews.set(url, withoutSameAuthor); await this.persistReviewRow(url, review); }, handleReviewEvent: async function (ev: NDKEvent) { try { if (ev.kind !== 38000) return; const kTag = ev.tags.find((t) => t[0] === "k"); if (!kTag || kTag[1] !== "38172") return; const uTags = ev.tags.filter( (t) => t[0] === "u" && (t[2] === "cashu" || t.length >= 2) ); if (!uTags.length) return; const { rating, comment } = parseRatingAndComment(ev.content || ""); const review: MintReview = { eventId: ev.id, pubkey: ev.pubkey, created_at: ev.created_at || 0, rating, comment, raw: ev.rawEvent(), }; for (const u of uTags) { const url = u[1]; if (typeof url === "string" && url.startsWith("http")) { await this.upsertReviewForUrl(url, review); } } } catch {} }, rebuildAggregates: async function () { try { await this.ensureDbInitialized(); const [reviews, infos, httpRows] = await Promise.all([ (this.db as MintReviewsDB).reviews.toArray(), (this.db as MintReviewsDB).infos.toArray(), (this.db as MintReviewsDB).httpInfo.toArray(), ]); const urlSet = new Set(); reviews.forEach((r) => r.url && urlSet.add(r.url)); infos.forEach((i) => i.url && urlSet.add(i.url)); httpRows.forEach((h) => h.url && urlSet.add(h.url)); // Group reviews by URL const grouped = new Map(); for (const r of reviews) { if (!r.url) continue; const list = grouped.get(r.url) || []; list.push(r); grouped.set(r.url, list); } const httpByUrl = new Map(); // Refresh in-memory cache from Dexie and build quick lookups this.httpInfoByUrl.clear(); for (const h of httpRows) { httpByUrl.set(h.url, h); if (h.info) this.httpInfoByUrl.set(h.url, h.info); } const recs: MintRecommendation[] = []; for (const url of urlSet) { const list = grouped.get(url) || []; const ratings = list .map((r) => r.rating) .filter((n): n is number => typeof n === "number"); const avg = ratings.length ? ratings.reduce((a, b) => a + b, 0) / ratings.length : null; const http = httpByUrl.get(url); recs.push({ url, reviewsCount: list.length, averageRating: avg, reviews: [], info: http?.info ?? undefined, error: !!http?.error || false, lastHttpInfoFetchAt: http?.fetchedAt ?? undefined, }); } recs.sort( (a, b) => b.reviewsCount - a.reviewsCount || (b.averageRating || 0) - (a.averageRating || 0) ); this.recommendations = recs; } catch {} }, persistReviewRow: async function (url: string, review: MintReview) { try { await this.ensureDbInitialized(); const row: ReviewRow = { eventId: review.eventId, url, pubkey: review.pubkey, created_at: review.created_at, rating: review.rating, comment: review.comment, raw: review.raw, }; await (this.db as MintReviewsDB).reviews.put(row); } catch {} }, getReviewsForUrl: async function (url: string): Promise { try { await this.ensureDbInitialized(); if (!url) return []; const rows = await (this.db as MintReviewsDB).reviews .where("url") .equals(url) .sortBy("created_at"); const list = rows.map( (r) => ({ eventId: r.eventId, pubkey: r.pubkey, created_at: r.created_at, rating: r.rating, comment: r.comment, raw: r.raw, } as MintReview) ); list.sort((a, b) => (a.created_at || 0) - (b.created_at || 0)); this.urlReviews.set(url, list); await this.rebuildAggregates(); return list; } catch { return []; } }, getAverageForUrl: function (url: string): number | null { const rec = this.recommendations.find((r) => r.url === url); return rec ? rec.averageRating : null; }, getCountForUrl: function (url: string): number { const rec = this.recommendations.find((r) => r.url === url); return rec ? rec.reviewsCount : 0; }, clearAllDatabases: async function () { try { await this.ensureDbInitialized(); await Promise.all([ (this.db as MintReviewsDB).reviews.clear(), (this.db as MintReviewsDB).infos.clear(), (this.db as MintReviewsDB).httpInfo.clear(), ]); } catch {} this.urlReviews.clear(); this.dbHydrated = false; await this.rebuildAggregates(); }, }, }); // Dexie DB class MintReviewsDB extends Dexie { reviews!: Dexie.Table; infos!: Dexie.Table; httpInfo!: Dexie.Table; constructor() { super("mintReviews"); this.version(1).stores({ reviews: "eventId, url, created_at", }); this.version(2).stores({ infos: "++id, url, created_at", httpInfo: "url", }); } } type ReviewRow = { eventId: string; url: string; pubkey: string; created_at: number; rating: number | null; comment: string; raw: any; }; type InfoRow = { url: string; pubkey: string; d: string; content?: any; created_at: number; }; type HttpInfoRow = { url: string; info: any | null; fetchedAt: number; // unix seconds error?: boolean; }; ================================================ FILE: src/stores/mints.ts ================================================ import { defineStore } from "pinia"; import { useLocalStorage } from "@vueuse/core"; import { useWorkersStore } from "./workers"; import { notifyError, notifySuccess } from "src/js/notify"; import { Mint, MintKeys, Proof, SerializedBlindedSignature, MintKeyset, GetInfoResponse, } from "@cashu/cashu-ts"; import { useUiStore } from "./ui"; import { ref, watch } from "vue"; import { useProofsStore } from "./proofs"; import { i18n } from "src/boot/i18n"; import { useSettingsStore } from "./settings"; import { useNostrMintBackupStore } from "./nostrMintBackup"; import { bytesToHex } from "@noble/hashes/utils"; // already an installed dependency export type StoredMint = { url: string; keys: MintKeys[]; keysets: MintKeyset[]; nickname?: string; info?: GetInfoResponse; errored?: boolean; motdDismissed?: boolean; multinutSelected?: boolean; lastInfoUpdated?: string; lastKeysetsUpdated?: string; // initialize api: new Mint(url) on activation }; export class MintClass { mint: StoredMint; constructor(mint: StoredMint) { this.mint = mint; } get api() { return new Mint(this.mint.url); } get proofs() { const proofsStore = useProofsStore(); return proofsStore.proofs.filter((p) => this.mint.keysets.map((k) => k.id).includes(p.id) ); } get allBalances() { // return an object with all balances for each unit const balances: Record = {}; this.units.forEach((unit) => { balances[unit] = this.unitBalance(unit); }); return balances; } get keysets() { return this.mint.keysets.filter((k) => k.active); } get units() { return this.mint.keysets .map((k) => k.unit) .filter((value, index, self) => self.indexOf(value) === index); } unitKeysets(unit: string): MintKeyset[] { return this.mint.keysets.filter((k) => k.unit === unit); } unitProofs(unit: string): WalletProof[] { const proofsStore = useProofsStore(); const unitKeysets = this.unitKeysets(unit); return proofsStore.proofs.filter( (p) => unitKeysets.map((k) => k.id).includes(p.id) && !p.reserved ); } unitBalance(unit: string) { const proofs = this.unitProofs(unit); return proofs.reduce((sum, p) => sum + p.amount, 0); } } // type that extends type Proof with reserved boolean export type WalletProof = Proof & { reserved: boolean; quote?: string }; export type Balances = { [unit: string]: number; }; type BlindSignatureAudit = { signature: SerializedBlindedSignature; amount: number; secret: Uint8Array; id: string; r: string; }; export const useMintsStore = defineStore("mints", { state: () => { const t = i18n.global.t; const activeProofs = ref([]); const activeUnit = useLocalStorage("cashu.activeUnit", "sat"); const activeMintUrl = useLocalStorage("cashu.activeMintUrl", ""); const addMintData = ref({ url: "", nickname: "", }); const mints = useLocalStorage("cashu.mints", [] as StoredMint[]); const showAddMintDialog = ref(false); const addMintBlocking = ref(false); const showRemoveMintDialog = ref(false); const showMintInfoDialog = ref(false); const showEditMintDialog = ref(false); const uiStoreGlobal: any = useUiStore(); const settingsStoreGlobal: any = useSettingsStore(); // Watch for changes in activeMintUrl and activeUnit watch([activeMintUrl, activeUnit], async () => { const proofsStore = useProofsStore(); console.log( `watcher: activeMintUrl: ${activeMintUrl.value}, activeUnit: ${activeUnit.value}` ); await proofsStore.updateActiveProofs(); }); return { t, activeProofs, activeUnit, activeMintUrl, addMintData, mints, showAddMintDialog, addMintBlocking, showRemoveMintDialog, showMintInfoDialog, showEditMintDialog, uiStoreGlobal, settingsStoreGlobal, }; }, getters: { multiMints({ activeUnit }) { return this.mints.filter((m) => { try { const version = m.info?.version; if (!version) return false; const regex = /^(Nutshell)\/(\d+)\.(\d+)\.(\d+)/; // Regex to match "Nutshell/version" const match = version.match(regex); if (!match || match[1] !== "Nutshell") return false; if (parseInt(match[2]) === 0 && parseInt(match[3]) < 17) return false; // If < 0.17.* then not viable const nut15 = m.info?.nuts[15]; const viableMint = nut15?.methods.find( (m) => m.method === "bolt11" && m.unit === activeUnit ); const balance = new MintClass(m).unitBalance(activeUnit); if (nut15 && viableMint && balance > 0) return true; else return false; } catch (e) { console.error(`${e}`); return false; } }); }, totalUnitBalance({ activeUnit }): number { const proofsStore = useProofsStore(); const allUnitKeysets = this.mints .map((m) => m.keysets) .flat() .filter((k) => k.unit === activeUnit); const balance = proofsStore.proofs .filter((p) => allUnitKeysets.map((k) => k.id).includes(p.id)) .filter((p) => !p.reserved) .reduce((sum, p) => sum + p.amount, 0); this.uiStoreGlobal.lastBalanceCached = balance; return balance; }, activeBalance(): number { return this.activeProofs .flat() .reduce((sum, el) => (sum += el.amount), 0); }, activeKeysets({ activeMintUrl, activeUnit }): MintKeyset[] { const unitKeysets = this.mints .find((m) => m.url === activeMintUrl) ?.keysets?.filter((k) => k.unit === activeUnit); if (!unitKeysets) { return []; } return unitKeysets; }, activeKeys({ activeMintUrl, activeUnit }): MintKeys[] { const unitKeys = this.mints .find((m) => m.url === activeMintUrl) ?.keys?.filter((k) => k.unit === activeUnit); if (!unitKeys) { return []; } return unitKeys; }, activeInfo({ activeMintUrl }): GetInfoResponse { return ( this.mints.find((m) => m.url === activeMintUrl)?.info || ({} as GetInfoResponse) ); }, activeUnitLabel({ activeUnit }): string { if (activeUnit == "sat") { if (this.settingsStoreGlobal.bip177BitcoinSymbol) { return "₿"; } else { return "SAT"; } } else if (activeUnit == "usd") { return "USD"; } else if (activeUnit == "eur") { return "EUR"; } else if (activeUnit == "msat") { return "mSAT"; } else { return activeUnit; } }, activeUnitCurrencyMultiplyer({ activeUnit }): number { if (activeUnit == "usd") { return 100; } else if (activeUnit == "eur") { return 100; } else { return 1; } }, allMintKeysets: function () { return [].concat(...this.mints.map((m) => m.keysets)); }, }, actions: { activeMint() { const mint = this.mints.find((m) => m.url === this.activeMintUrl); if (mint) { return new MintClass(mint); } else { if (this.mints.length) { console.error( "No active mint. This should not happen. switching to first one." ); this.activateMintUrl(this.mints[0].url, false, true); return new MintClass(this.mints[0]); } throw new Error("No active mint"); } }, mintUnitProofs(mint: StoredMint, unit: string): WalletProof[] { const proofsStore = useProofsStore(); const unitKeysets = mint.keysets.filter((k) => k.unit === unit); return proofsStore.proofs.filter( (p) => unitKeysets.map((k) => k.id).includes(p.id) && !p.reserved ); }, mintUnitKeysets(mint: StoredMint, unit: string): MintKeyset[] { return mint.keysets.filter((k) => k.unit === unit); }, toggleUnit: function () { const units = this.activeMint().units; this.activeUnit = units[(units.indexOf(this.activeUnit) + 1) % units.length]; return this.activeUnit; }, toggleActiveUnitForMint(mint: StoredMint) { // method to set the active unit to one that is supported by `mint` const mintClass = new MintClass(mint); if ( !this.activeUnit || mintClass.allBalances[this.activeUnit] == undefined ) { this.activeUnit = mintClass.units[0]; } }, updateMint(oldMint: StoredMint, newMint: StoredMint) { const index = this.mints.findIndex((m) => m.url === oldMint.url); this.mints[index] = newMint; }, updateMintMultinutSelection(mintUrl: string, selected: boolean) { const mint = this.mints.find((m) => m.url === mintUrl); if (mint) { mint.multinutSelected = selected; } }, getKeysForKeyset: async function (keyset_id: string): Promise { const mint = this.mints.find((m) => m.url === this.activeMintUrl); if (mint) { const keys = mint.keys?.find((k) => k.id === keyset_id); if (keys) { return keys; } else { throw new Error("Keys not found"); } } else { throw new Error("Mint not found"); } }, addMint: async function ( addMintData: { url: string; nickname?: string }, verbose = false ): Promise { let url = addMintData.url; this.addMintBlocking = true; try { // sanitize url const sanitizeUrl = (url: string): string => { let cleanedUrl = url.trim().replace(/\/+$/, ""); if (!/^[a-z]+:\/\//.test(cleanedUrl)) { // Check for any protocol followed by "://" cleanedUrl = "https://" + cleanedUrl; } return cleanedUrl; }; url = sanitizeUrl(url); const mintToAdd: StoredMint = { url: url, keys: [], keysets: [], nickname: addMintData.nickname, }; // we have no mints at all if (this.mints.length === 0) { this.mints = [mintToAdd]; } else if (this.mints.filter((m) => m.url === url).length === 0) { // we don't have this mint yet // add mint to this.mints so it can be activated in this.mints.push(mintToAdd); } else { // we already have this mint if (verbose) { notifySuccess(this.t("wallet.mint.notifications.already_added")); } return mintToAdd; } await this.activateMint(mintToAdd, false, true); if (verbose) { await notifySuccess(this.t("wallet.mint.notifications.added")); } // Trigger Nostr backup if enabled this.triggerNostrBackup(); return mintToAdd; } catch (error) { // activation failed, we remove the mint again from local storage this.mints = this.mints.filter((m) => m.url !== url); throw error; } finally { this.showAddMintDialog = false; this.addMintBlocking = false; } }, activateMintUrl: async function ( url: string, verbose = false, force = false, unit: string | undefined = undefined ) { const mint = this.mints.filter((m) => m.url === url)[0]; if (mint) { await this.activateMint(mint, verbose, force); if (unit) { await this.activateUnit(unit, verbose); } } else { notifyError( this.t("wallet.mint.notifications.not_found"), this.t("wallet.mint.notifications.activation_failed") ); } }, activateUnit: async function (unit: string, verbose = false) { if (unit === this.activeUnit) { return; } const uIStore = useUiStore(); await uIStore.lockMutex(); const mint = this.mints.find((m) => m.url === this.activeMintUrl); if (!mint) { notifyError( this.t("wallet.mint.notifications.no_active_mint"), this.t("wallet.mint.notifications.unit_activation_failed") ); return; } const mintClass = new MintClass(mint); if (mintClass.units.includes(unit)) { this.activeUnit = unit; } else { notifyError( this.t("wallet.mint.notifications.unit_not_supported"), this.t("wallet.mint.notifications.unit_activation_failed") ); } await uIStore.unlockMutex(); const worker = useWorkersStore(); worker.clearAllWorkers(); }, updateMintInfoAndKeys: async function (mint: StoredMint) { const newMintInfo = await this.fetchMintInfo(mint); this.triggerMintInfoMotdChanged(newMintInfo, mint); mint = await this.fetchMintKeys(mint); const mintToUpdate = this.mints.filter((m) => m.url === mint.url)[0]; mintToUpdate.errored = false; return mint; }, activateMint: async function ( mint: StoredMint, verbose = false, force = false ) { if (mint.url === this.activeMintUrl && !force) { return; } const workers = useWorkersStore(); const uIStore = useUiStore(); // we need to stop workers because they will reset the activeMint again workers.clearAllWorkers(); // create new mint.api instance because we can't store it in local storage const previousUrl = this.activeMintUrl; await uIStore.lockMutex(); try { mint = await this.updateMintInfoAndKeys(mint); this.toggleActiveUnitForMint(mint); if (verbose) { await notifySuccess(this.t("wallet.mint.notifications.activated")); } this.activeMintUrl = mint.url; console.log("### activateMint: Mint activated: ", this.activeMintUrl); } catch (error: any) { // restore previous values because the activation errored // this.activeMintUrl = previousUrl; let err_msg = this.t("wallet.mint.notifications.could_not_connect"); if (error.message.length) { err_msg = err_msg + ` ${error.message}.`; } await notifyError( err_msg, this.t("wallet.mint.notifications.activation_failed") ); this.mints.filter((m) => m.url === mint.url)[0].errored = true; throw error; } finally { await uIStore.unlockMutex(); } }, checkMintInfoMotdChanged(newMintInfo: GetInfoResponse, mint: StoredMint) { // if mint doesn't have info yet, we don't need to trigger the motd change if (!this.mints.find((m) => m.url === mint.url)?.info) { return false; } const motd = newMintInfo.motd; if (motd !== this.mints.filter((m) => m.url === mint.url)[0].info?.motd) { return true; } return false; }, triggerMintInfoMotdChanged( newMintInfo: GetInfoResponse, mint: StoredMint, navigate = true ) { if (!this.checkMintInfoMotdChanged(newMintInfo, mint)) { return; } // set motd_viewed to false this.mints.filter((m) => m.url === mint.url)[0].motdDismissed = false; // Navigate to mint details page with mint URL as query parameter if (navigate) { window.location.href = `/mintdetails?mintUrl=${encodeURIComponent( mint.url )}`; } }, fetchMintInfo: async function (mint: StoredMint) { try { const mintClass = new MintClass(mint); const data = await mintClass.api.getInfo(); // if we have this mint in localstorage, update it const storedMint = this.mints.find((m) => m.url === mint.url); if (storedMint) { storedMint.info = data; storedMint.lastInfoUpdated = new Date().toISOString(); } return data; } catch (error: any) { console.error(error); try { // notifyApiError(error, this.t("wallet.mint.notifications.could_not_get_info")); } catch {} throw error; } }, checkForMintKeysetIdCollisions: async function ( mintToAdd: StoredMint, keysets: MintKeyset[] ) { // check if there are any keysets with the same id in another mint const allKeysets = this.mints .filter((m) => m.url !== mintToAdd.url) // exclude the mint we are adding .map((m) => m.keysets) .flat(); const collisions = keysets.filter((k) => allKeysets.map((k) => k.id).includes(k.id) ); // perform the same check for the integer representation of the keyset id function keysetIdToBigInt(id: string): bigint { if (/^[0-9a-fA-F]+$/.test(id)) { return BigInt(`0x${id}`) % BigInt(2 ** 31 - 1); } else { const bin = atob(id); const hex = bytesToHex(Uint8Array.from(bin, (c) => c.charCodeAt(0))); return BigInt(`0x${hex}`) % BigInt(2 ** 31 - 1); } } const allKeysetsIdsBigInt = allKeysets.map((k) => keysetIdToBigInt(k.id)); const hasCollisions = keysets.some((k) => allKeysetsIdsBigInt.includes(keysetIdToBigInt(k.id)) ); if (hasCollisions) { const errorMessage = this.t( "wallet.mint.notifications.mint_validation_error" ); throw new Error(errorMessage); } return true; }, fetchMintKeys: async function (mint: StoredMint): Promise { try { const mintClass = new MintClass(mint); const keysets = await this.fetchMintKeysets(mint); // if we do not have any keys yet, fetch them if (mint.keys.length === 0 || mint.keys.length == undefined) { const keys = await mintClass.api.getKeys(); // store keys in mint and update local storage this.mints.filter((m) => m.url === mint.url)[0].keys = keys.keysets; } // reload mint from local storage mint = this.mints.filter((m) => m.url === mint.url)[0]; // for each keyset we do not have keys for, fetch keys for (const keyset of keysets) { if (!mint.keys.find((k) => k.id === keyset.id)) { const keys = await mintClass.api.getKeys(keyset.id); // store keys in mint and update local storage this.mints .filter((m) => m.url === mint.url)[0] .keys.push(keys.keysets[0]); } } this.mints.filter((m) => m.url === mint.url)[0].lastKeysetsUpdated = new Date().toISOString(); // return the mint with keys set return this.mints.filter((m) => m.url === mint.url)[0]; } catch (error: any) { console.error(error); try { // notifyApiError(error, this.t("wallet.mint.notifications.could_not_get_keys")); } catch {} throw error; } }, fetchMintKeysets: async function (mint: StoredMint) { // fetches and stores keysets for a mint try { const mintClass = new MintClass(mint); const data = await mintClass.api.getKeySets(); const keysets = data.keysets; if (keysets.length > 0) { // check for keyset id collisions with other mints await this.checkForMintKeysetIdCollisions(mint, keysets); // store keysets in mint and update local storage // merge new keysets with existing ones instead of overwriting const storedMint = this.mints.find((m) => m.url === mint.url); if (storedMint) { const existingKeysets = storedMint.keysets || []; const mergedKeysets = [...existingKeysets]; // Add or update keysets for (const newKeyset of keysets) { const existingIndex = mergedKeysets.findIndex( (k) => k.id === newKeyset.id ); if (existingIndex !== -1) { // Update existing keyset mergedKeysets[existingIndex] = newKeyset; } else { // Add new keyset mergedKeysets.push(newKeyset); } } storedMint.keysets = mergedKeysets; } } return keysets; } catch (error: any) { console.error(error); throw error; } }, removeMint: async function (url: string) { this.mints = this.mints.filter((m) => m.url !== url); if (url === this.activeMintUrl) { this.activeMintUrl = ""; } // todo: we always reset to the first mint, improve this if (this.mints.length > 0) { await this.activateMint(this.mints[0], false); } notifySuccess(this.t("wallet.mint.notifications.removed")); // Trigger Nostr backup if enabled this.triggerNostrBackup(); }, assertMintError: function (response: { error?: any }, verbose = true) { if (response.error != null) { if (verbose) { notifyError( response.error, this.t("wallet.mint.notifications.error") ); } throw new Error(`Mint error: ${response.error}`); } }, // Trigger Nostr backup when mints change triggerNostrBackup: async function () { try { const nostrMintBackupStore = useNostrMintBackupStore(); if (nostrMintBackupStore.enabled && nostrMintBackupStore.needsBackup) { setTimeout(async () => { try { await nostrMintBackupStore.backupMintsToNostr(); } catch (error) { console.error("Failed to backup mints to Nostr:", error); } }, 1000); } } catch (error) { console.error("Failed to trigger Nostr backup:", error); } }, }, }); ================================================ FILE: src/stores/nostr.ts ================================================ import { defineStore } from "pinia"; import NDK, { NDKEvent, NDKSigner, NDKNip07Signer, NDKNip46Signer, NDKFilter, NDKPrivateKeySigner, NostrEvent, NDKKind, NDKRelaySet, NDKRelay, NDKTag, ProfilePointer, } from "@nostr-dev-kit/ndk"; import { nip04, nip19, nip44 } from "nostr-tools"; import { bytesToHex, hexToBytes } from "@noble/hashes/utils"; // already an installed dependency import { useWalletStore } from "./wallet"; import { generateSecretKey, getPublicKey } from "nostr-tools"; import { useLocalStorage } from "@vueuse/core"; import { useSettingsStore } from "./settings"; import { useReceiveTokensStore } from "./receiveTokensStore"; import { getEncodedTokenV4, PaymentRequestPayload, Token, } from "@cashu/cashu-ts"; import { useTokensStore } from "./tokens"; import { notifyApiError, notifyError, notifySuccess, notifyWarning, notify, } from "../js/notify"; import { useSendTokensStore } from "./sendTokensStore"; import { usePRStore } from "./payment-request"; import token from "../js/token"; import { HistoryToken } from "./tokens"; type NostrEventLog = { id: string; created_at: number; }; export enum SignerType { NIP07 = "NIP07", NIP46 = "NIP46", PRIVATEKEY = "PRIVATEKEY", SEED = "SEED", } export const useNostrStore = defineStore("nostr", { state: () => ({ connected: false, pubkey: useLocalStorage("cashu.ndk.pubkey", ""), relays: useLocalStorage( "cashu.nostr.relays", useSettingsStore().defaultNostrRelays ), ndk: {} as NDK, signerType: useLocalStorage( "cashu.ndk.signerType", SignerType.SEED ), nip07signer: {} as NDKNip07Signer, nip46Token: useLocalStorage("cashu.ndk.nip46Token", ""), nip46signer: {} as NDKNip46Signer, privateKeySignerPrivateKey: useLocalStorage( "cashu.ndk.privateKeySignerPrivateKey", "" ), seedSignerPrivateKey: useLocalStorage( "cashu.ndk.seedSignerPrivateKey", "" ), seedSignerPublicKey: useLocalStorage( "cashu.ndk.seedSignerPublicKey", "" ), seedSigner: {} as NDKPrivateKeySigner, seedSignerPrivateKeyNsec: "", privateKeySigner: {} as NDKPrivateKeySigner, signer: {} as NDKSigner, initialized: false, lastEventTimestamp: useLocalStorage( "cashu.ndk.lastEventTimestamp", 0 ), nip17EventIdsWeHaveSeen: useLocalStorage( "cashu.ndk.nip17EventIdsWeHaveSeen", [] ), }), getters: { seedSignerPrivateKeyNsec: (state) => { const sk = hexToBytes(state.seedSignerPrivateKey); return nip19.nsecEncode(sk); }, nprofile: (state) => { const profile: ProfilePointer = { pubkey: state.pubkey, relays: state.relays, }; return nip19.nprofileEncode(profile); }, seedSignerNprofile: (state) => { const profile: ProfilePointer = { pubkey: state.seedSignerPublicKey, relays: state.relays, }; return nip19.nprofileEncode(profile); }, }, actions: { initNdkReadOnly: function () { this.ndk = new NDK({ explicitRelayUrls: this.relays }); this.ndk.connect(); this.connected = true; }, initSignerIfNotSet: async function () { if (!this.initialized) { await this.initSigner(); } }, initSigner: async function () { if (this.signerType === SignerType.NIP07) { await this.initNip07Signer(); } else if (this.signerType === SignerType.NIP46) { await this.initNip46Signer(); } else if (this.signerType === SignerType.PRIVATEKEY) { await this.initPrivateKeySigner(); } else { await this.initWalletSeedPrivateKeySigner(); } this.initialized = true; }, setSigner: function (signer: NDKSigner) { this.signer = signer; this.ndk = new NDK({ signer: signer, explicitRelayUrls: this.relays }); }, signDummyEvent: async function (): Promise { const ndkEvent = new NDKEvent(); ndkEvent.kind = 1; ndkEvent.content = "Hello, world!"; const sig = await ndkEvent.sign(this.signer); console.log(`nostr signature: ${sig})`); const eventString = JSON.stringify(ndkEvent.rawEvent()); console.log(`nostr event: ${eventString}`); return ndkEvent; }, setPubkey: function (pubkey: string) { console.log("Setting pubkey to", pubkey); this.pubkey = pubkey; }, checkNip07Signer: async function (): Promise { const signer = new NDKNip07Signer(); try { await signer.user(); return true; } catch (e) { return false; } }, initNip07Signer: async function () { const signer = new NDKNip07Signer(); const user = await signer.blockUntilReady(); this.signerType = SignerType.NIP07; this.setSigner(signer); this.setPubkey(user.pubkey); }, initNip46Signer: async function (nip46Token?: string) { const ndk = new NDK({ explicitRelayUrls: this.relays }); if (!nip46Token && !this.nip46Token.length) { nip46Token = (await prompt( "Enter your NIP-46 connection string" )) as string; if (!nip46Token) { return; } this.nip46Token = nip46Token; } else { if (nip46Token) { this.nip46Token = nip46Token; } } const signer = new NDKNip46Signer(ndk, this.nip46Token); this.signerType = SignerType.NIP46; this.setSigner(signer); // If the backend sends an auth_url event, open that URL as a popup so the user can authorize the app signer.on("authUrl", (url) => { window.open(url, "auth", "width=600,height=600"); }); // wait until the signer is ready const loggedinUser = await signer.blockUntilReady(); alert("You are now logged in as " + loggedinUser.npub); this.setPubkey(loggedinUser.pubkey); }, resetNip46Signer: async function () { this.nip46Token = ""; await this.initWalletSeedPrivateKeySigner(); }, initPrivateKeySigner: async function (nsec?: string) { let privateKeyBytes: Uint8Array; if (!nsec && !this.privateKeySignerPrivateKey.length) { nsec = (await prompt("Enter your nsec")) as string; if (!nsec) { return; } privateKeyBytes = nip19.decode(nsec).data as Uint8Array; } else { if (nsec) { privateKeyBytes = nip19.decode(nsec).data as Uint8Array; } else { privateKeyBytes = hexToBytes(this.privateKeySignerPrivateKey); } } this.privateKeySigner = new NDKPrivateKeySigner( this.privateKeySignerPrivateKey ); this.privateKeySignerPrivateKey = bytesToHex(privateKeyBytes); this.signerType = SignerType.PRIVATEKEY; this.setSigner(this.privateKeySigner); const publicKeyHex = getPublicKey(privateKeyBytes); this.setPubkey(publicKeyHex); }, resetPrivateKeySigner: async function () { this.privateKeySignerPrivateKey = ""; await this.initWalletSeedPrivateKeySigner(); }, walletSeedGenerateKeyPair: async function () { const walletStore = useWalletStore(); const sk = walletStore.seed.slice(0, 32); const walletPublicKeyHex = getPublicKey(sk); // `pk` is a hex string const walletPrivateKeyHex = bytesToHex(sk); this.seedSignerPrivateKey = walletPrivateKeyHex; this.seedSignerPublicKey = walletPublicKeyHex; this.seedSigner = new NDKPrivateKeySigner(this.seedSignerPrivateKey); }, initWalletSeedPrivateKeySigner: async function () { await this.walletSeedGenerateKeyPair(); // TODO: remove duplicate privateKeysigner this.privateKeySigner = this.seedSigner; this.signerType = SignerType.SEED; this.setSigner(this.privateKeySigner); this.setPubkey(this.seedSignerPublicKey); }, fetchEventsFromUser: async function () { const filter: NDKFilter = { kinds: [1], authors: [this.pubkey] }; return await this.ndk.fetchEvents(filter); }, sendNip04DirectMessage: async function ( recipient: string, message: string ) { const randomPrivateKey = generateSecretKey(); const randomPublicKey = getPublicKey(randomPrivateKey); // const randomPrivateKey = hexToBytes(this.seedSignerPrivateKey); // const randomPublicKey = this.pubkey; const ndk = new NDK({ explicitRelayUrls: this.relays, signer: new NDKPrivateKeySigner(bytesToHex(randomPrivateKey)), }); const event = new NDKEvent(ndk); ndk.connect(); event.kind = NDKKind.EncryptedDirectMessage; event.content = await nip04.encrypt(randomPrivateKey, recipient, message); event.tags = [["p", recipient]]; event.sign(); try { await event.publish(); notifySuccess("NIP-04 event published"); } catch (e) { console.error(e); notifyError("Could not publish NIP-04 event"); } }, subscribeToNip04DirectMessages: async function () { await this.walletSeedGenerateKeyPair(); await this.initNdkReadOnly(); let nip04DirectMessageEvents: Set = new Set(); const fetchEventsPromise = new Promise>((resolve) => { if (!this.lastEventTimestamp) { this.lastEventTimestamp = Math.floor(Date.now() / 1000); } console.log( `### Subscribing to NIP-04 direct messages to ${this.seedSignerPublicKey} since ${this.lastEventTimestamp}` ); this.ndk.connect(); const sub = this.ndk.subscribe( { kinds: [NDKKind.EncryptedDirectMessage], "#p": [this.seedSignerPublicKey], since: this.lastEventTimestamp, } as NDKFilter, { closeOnEose: false, groupable: false } ); sub.on("event", (event: NDKEvent) => { console.log("event"); nip04 .decrypt( hexToBytes(this.seedSignerPrivateKey), event.pubkey, event.content ) .then((content) => { console.log("NIP-04 DM from", event.pubkey); console.log("Content:", content); nip04DirectMessageEvents.add(event); this.lastEventTimestamp = Math.floor(Date.now() / 1000); this.parseMessageForEcash(content); }); }); }); try { nip04DirectMessageEvents = await fetchEventsPromise; } catch (error) { console.error("Error fetching contact events:", error); } }, sendNip17DirectMessageToNprofile: async function ( nprofile: string, message: string ) { const result = nip19.decode(nprofile); const pubkey: string = (result.data as ProfilePointer).pubkey; const relays: string[] | undefined = (result.data as ProfilePointer) .relays; this.sendNip17DirectMessage(pubkey, message, relays); }, randomTimeUpTo2DaysInThePast: function () { return Math.floor(Date.now() / 1000) - Math.floor(Math.random() * 172800); }, sendNip17DirectMessage: async function ( recipient: string, message: string, relays?: string[] ) { await this.walletSeedGenerateKeyPair(); const randomPrivateKey = generateSecretKey(); const randomPublicKey = getPublicKey(randomPrivateKey); const dmEvent = new NDKEvent(); dmEvent.kind = 14; dmEvent.content = message; dmEvent.tags = [["p", recipient]]; dmEvent.created_at = Math.floor(Date.now() / 1000); dmEvent.pubkey = this.seedSignerPublicKey; dmEvent.id = dmEvent.getEventHash(); const dmEventString = JSON.stringify(await dmEvent.toNostrEvent()); const seedNdk = new NDK({ signer: this.seedSigner, explicitRelayUrls: this.relays, }); const sealEvent = new NDKEvent(seedNdk); sealEvent.kind = 13; sealEvent.content = nip44.v2.encrypt( dmEventString, nip44.v2.utils.getConversationKey(this.seedSignerPrivateKey, recipient) ); sealEvent.created_at = this.randomTimeUpTo2DaysInThePast(); sealEvent.pubkey = this.seedSignerPublicKey; sealEvent.id = sealEvent.getEventHash(); sealEvent.sig = await sealEvent.sign(); const sealEventString = JSON.stringify(await sealEvent.toNostrEvent()); const randomNdk = new NDK({ explicitRelayUrls: relays ?? this.relays, signer: new NDKPrivateKeySigner(bytesToHex(randomPrivateKey)), }); const wrapEvent = new NDKEvent(randomNdk); wrapEvent.kind = 1059; wrapEvent.tags = [["p", recipient]]; wrapEvent.content = nip44.v2.encrypt( sealEventString, nip44.v2.utils.getConversationKey( bytesToHex(randomPrivateKey), recipient ) ); wrapEvent.created_at = this.randomTimeUpTo2DaysInThePast(); wrapEvent.pubkey = randomPublicKey; wrapEvent.id = wrapEvent.getEventHash(); wrapEvent.sig = await wrapEvent.sign(); try { randomNdk.connect(); await wrapEvent.publish(); } catch (e) { console.error(e); notifyError("Could not publish NIP-17 event"); } }, subscribeToNip17DirectMessages: async function () { await this.walletSeedGenerateKeyPair(); await this.initNdkReadOnly(); let nip17DirectMessageEvents: Set = new Set(); const fetchEventsPromise = new Promise>((resolve) => { if (!this.lastEventTimestamp) { this.lastEventTimestamp = Math.floor(Date.now() / 1000); } const since = this.lastEventTimestamp - 172800; // last 2 days console.log( `### Subscribing to NIP-17 direct messages to ${this.seedSignerPublicKey} since ${since}` ); this.ndk.connect(); const sub = this.ndk.subscribe( { kinds: [1059 as NDKKind], "#p": [this.seedSignerPublicKey], since: since, } as NDKFilter, { closeOnEose: false, groupable: false } ); sub.on("event", (wrapEvent: NDKEvent) => { const eventLog = { id: wrapEvent.id, created_at: wrapEvent.created_at, } as NostrEventLog; if (this.nip17EventIdsWeHaveSeen.find((e) => e.id === wrapEvent.id)) { // console.log(`### Already seen NIP-17 event ${wrapEvent.id} (time: ${wrapEvent.created_at})`); return; } else { console.log(`### New event ${wrapEvent.id}`); this.nip17EventIdsWeHaveSeen.push(eventLog); // remove all events older than 10 days to keep the list small const fourDaysAgo = Math.floor(Date.now() / 1000) - 10 * 24 * 60 * 60; this.nip17EventIdsWeHaveSeen = this.nip17EventIdsWeHaveSeen.filter( (e) => e.created_at > fourDaysAgo ); } let dmEvent: NDKEvent; let content: string; try { const wappedContent = nip44.v2.decrypt( wrapEvent.content, nip44.v2.utils.getConversationKey( this.seedSignerPrivateKey, wrapEvent.pubkey ) ); const sealEvent = JSON.parse(wappedContent) as NostrEvent; const dmEventString = nip44.v2.decrypt( sealEvent.content, nip44.v2.utils.getConversationKey( this.seedSignerPrivateKey, sealEvent.pubkey ) ); dmEvent = JSON.parse(dmEventString) as NDKEvent; content = dmEvent.content; console.log("### NIP-17 DM from", dmEvent.pubkey); console.log("Content:", content); } catch (e) { console.error(e); return; } nip17DirectMessageEvents.add(dmEvent); this.lastEventTimestamp = Math.floor(Date.now() / 1000); this.parseMessageForEcash(content); }); }); try { nip17DirectMessageEvents = await fetchEventsPromise; } catch (error) { console.error("Error fetching contact events:", error); } }, parseMessageForEcash: async function (message: string) { // first check if the message can be converted to a json and then to a PaymentRequestPayload try { const payload = JSON.parse(message) as PaymentRequestPayload; if (payload) { const receiveStore = useReceiveTokensStore(); const prStore = usePRStore(); const sendTokensStore = useSendTokensStore(); const tokensStore = useTokensStore(); const proofs = payload.proofs; const mint = payload.mint; const unit = payload.unit; const token = { proofs: proofs, mint: mint, unit: unit, } as Token; const tokenStr = getEncodedTokenV4(token); const tokenInHistory = tokensStore.tokenAlreadyInHistory(tokenStr); if (tokenInHistory && tokenInHistory.amount > 0) { console.log("### incoming token already in history"); return; } const historyId = await this.addPendingTokenToHistory( tokenStr, false, payload.id ); try { if (historyId) { prStore.registerIncomingPaymentForRequest( payload.id ?? "", historyId ); } } catch (e) { console.error("Failed to register incoming payment to PR:", e); } receiveStore.receiveData.tokensBase64 = tokenStr; sendTokensStore.showSendTokens = false; const knowThisMint = receiveStore.knowThisMintOfTokenJson(token); if (prStore.receivePaymentRequestsAutomatically && knowThisMint) { const success = await receiveStore.receiveIfDecodes(); if (success) { prStore.showPRDialog = false; } else { notifyWarning("Could not receive incoming payment"); } } else { prStore.showPRDialog = false; receiveStore.showReceiveTokens = true; } return; } } catch (e) { // console.log("### parsing message for ecash failed"); return; } console.log("### parsing message for ecash", message); const receiveStore = useReceiveTokensStore(); const words = message.split(" "); const tokens = words.filter((word) => { return word.startsWith("cashuA") || word.startsWith("cashuB"); }); for (const tokenStr of tokens) { receiveStore.receiveData.tokensBase64 = tokenStr; receiveStore.showReceiveTokens = true; await this.addPendingTokenToHistory(tokenStr); } }, addPendingTokenToHistory: function ( tokenStr: string, verbose = true, paymentRequestId?: string ): string | undefined { const receiveStore = useReceiveTokensStore(); const tokensStore = useTokensStore(); if (tokensStore.tokenAlreadyInHistory(tokenStr)) { notifySuccess("Ecash already in history"); receiveStore.showReceiveTokens = false; return undefined; } const decodedToken = token.decode(tokenStr); if (decodedToken == undefined) { throw Error("could not decode token"); } // get amount from decodedToken.token.proofs[..].amount const amount = token .getProofs(decodedToken) .reduce((sum, el) => (sum += el.amount), 0); const id = tokensStore.addPendingToken({ amount: amount, token: tokenStr, mint: token.getMint(decodedToken), unit: token.getUnit(decodedToken), paymentRequestId, }); receiveStore.showReceiveTokens = false; // show success notification if (verbose) { notifySuccess("Ecash added to history."); } return id; }, }, }); ================================================ FILE: src/stores/nostrMintBackup.ts ================================================ import { defineStore } from "pinia"; import { useLocalStorage } from "@vueuse/core"; import { bytesToHex } from "@noble/hashes/utils"; import { sha256 } from "@noble/hashes/sha256"; import { getPublicKey } from "nostr-tools"; import { mnemonicToSeedSync } from "@scure/bip39"; import NDK, { NDKEvent, NDKFilter, NDKPrivateKeySigner, } from "@nostr-dev-kit/ndk"; import { nip44 } from "nostr-tools"; import { useWalletStore } from "./wallet"; import { useMintsStore } from "./mints"; import { useNostrStore } from "./nostr"; import { notify, notifyError, notifySuccess } from "../js/notify"; import { useSettingsStore } from "./settings"; // NIP kind for mint backup events const MINT_BACKUP_KIND = 30078; type MintBackupData = { mints: string[]; timestamp: number; }; type DiscoveredMint = { url: string; timestamp: number; selected: boolean; }; export const useNostrMintBackupStore = defineStore("nostrMintBackup", { state: () => ({ // Last backup timestamp lastBackupTimestamp: useLocalStorage( "cashu.nostrMintBackup.lastBackupTimestamp", 0 ), // Discovered mints from nostr discoveredMints: [] as DiscoveredMint[], // Loading states backupInProgress: false, searchInProgress: false, // Derived private key for mint backups mintBackupPrivateKey: "", mintBackupPublicKey: "", }), getters: { // Get enabled state from settings store enabled: (): boolean => { const settingsStore = useSettingsStore(); return settingsStore.nostrMintBackupEnabled; }, // Get the current mint URLs currentMintUrls: (): string[] => { const mintsStore = useMintsStore(); return mintsStore.mints.map((mint) => mint.url); }, // Check if backup is needed (mints have changed since last backup) needsBackup: (state): boolean => { const settingsStore = useSettingsStore(); if (!settingsStore.nostrMintBackupEnabled) return false; const mintsStore = useMintsStore(); const currentMints = mintsStore.mints.map((mint) => mint.url).sort(); // If no mints, no backup needed if (currentMints.length === 0) return false; // If never backed up, need backup if (state.lastBackupTimestamp === 0) return true; // Check if mints have changed (this is a simple check, you might want to store the last backed up mints) return true; // For now, always allow backup }, // Get conversation key for encryption conversationKey: (state): Uint8Array | null => { if (!state.mintBackupPrivateKey || !state.mintBackupPublicKey) return null; return nip44.v2.utils.getConversationKey( state.mintBackupPrivateKey, state.mintBackupPublicKey ); }, }, actions: { // Initialize backup keys from wallet seed async initializeBackupKeys(): Promise { const walletStore = useWalletStore(); // Derive a deterministic private key from wallet seed for mint backup const { privateKeyHex, publicKeyHex } = await this.initializeBackupKeysFromMnemonic(walletStore.mnemonic); this.mintBackupPrivateKey = privateKeyHex; this.mintBackupPublicKey = publicKeyHex; }, // Initialize backup keys from custom mnemonic (for restore) async initializeBackupKeysFromMnemonic( mnemonic: string ): Promise<{ privateKeyHex: string; publicKeyHex: string }> { // Derive seed from mnemonic const seed: Uint8Array = mnemonicToSeedSync(mnemonic); const domainSeparator = new TextEncoder().encode("cashu-mint-backup"); const combinedData = new Uint8Array(seed.length + domainSeparator.length); combinedData.set(seed); combinedData.set(domainSeparator, seed.length); // Use sha256 of combined data as private key const privateKeyBytes = sha256(combinedData); const privateKeyHex = bytesToHex(privateKeyBytes); const publicKeyHex = getPublicKey(privateKeyBytes); return { privateKeyHex, publicKeyHex }; }, // Create and publish mint backup event async backupMintsToNostr(verbose: boolean = false): Promise { const settingsStore = useSettingsStore(); if (!settingsStore.nostrMintBackupEnabled) { console.log("Nostr mint backup is disabled"); return; } if (this.backupInProgress) { console.log("Backup already in progress"); return; } this.backupInProgress = true; try { // Initialize keys if not already done if (!this.mintBackupPrivateKey) { await this.initializeBackupKeys(); } const settingsStore = useSettingsStore(); const mintsStore = useMintsStore(); // Check if relays are configured if ( !settingsStore.defaultNostrRelays || settingsStore.defaultNostrRelays.length === 0 ) { const errorMsg = "No Nostr relays configured"; console.error(errorMsg); if (verbose) { notifyError(`Failed to backup mint list to Nostr: ${errorMsg}`); } throw new Error(errorMsg); } const currentMints = mintsStore.mints.map((mint) => mint.url); if (currentMints.length === 0) { console.log("No mints to backup"); return; } // Create backup data const backupData: MintBackupData = { mints: currentMints, timestamp: Math.floor(Date.now() / 1000), }; // Encrypt the backup data const conversationKey = nip44.v2.utils.getConversationKey( this.mintBackupPrivateKey, this.mintBackupPublicKey ); const encryptedContent = nip44.v2.encrypt( JSON.stringify(backupData), conversationKey ); // Create NDK instance const ndk = new NDK({ explicitRelayUrls: settingsStore.defaultNostrRelays, signer: new NDKPrivateKeySigner(this.mintBackupPrivateKey), }); await ndk.connect(); // Create the event const event = new NDKEvent(ndk); event.kind = MINT_BACKUP_KIND; event.content = encryptedContent; event.tags = [ ["d", "mint-list"], // replaceable event identifier ["client", "cashu.me"], ]; event.created_at = backupData.timestamp; event.pubkey = this.mintBackupPublicKey; // Sign and publish await event.sign(); await event.publish(); this.lastBackupTimestamp = backupData.timestamp; if (verbose) { notifySuccess("Mint list backed up to Nostr successfully"); } console.log("Mint backup published to Nostr:", event.id); } catch (error) { console.error("Failed to backup mints to Nostr:", error); // Only show error notification if verbose is true // This prevents showing errors for automatic backups that fail silently if (verbose) { notifyError( "Failed to backup mint list to Nostr: " + (error as Error).message ); } throw error; } finally { this.backupInProgress = false; } }, // Search for mint backups on Nostr async searchMintsOnNostr(mnemonic: string): Promise { this.searchInProgress = true; this.discoveredMints = []; try { const settingsStore = useSettingsStore(); // Derive keys from provided mnemonic const { publicKeyHex } = await this.initializeBackupKeysFromMnemonic( mnemonic ); // Create read-only NDK instance const ndk = new NDK({ explicitRelayUrls: settingsStore.defaultNostrRelays, }); await ndk.connect(); // Search for mint backup events from this pubkey const filter: NDKFilter = { kinds: [MINT_BACKUP_KIND], authors: [publicKeyHex], "#d": ["mint-list"], limit: 10, }; const events = await ndk.fetchEvents(filter); console.log(`Found ${events.size} mint backup events`); const allDiscoveredMints: DiscoveredMint[] = []; for (const event of events) { try { // Decrypt event content const { privateKeyHex } = await this.initializeBackupKeysFromMnemonic(mnemonic); const conversationKey = nip44.v2.utils.getConversationKey( privateKeyHex, publicKeyHex ); const decryptedContent = nip44.v2.decrypt( event.content, conversationKey ); const backupData: MintBackupData = JSON.parse(decryptedContent); // Add discovered mints for (const mintUrl of backupData.mints) { const existingMint = allDiscoveredMints.find( (m) => m.url === mintUrl ); if (!existingMint) { allDiscoveredMints.push({ url: mintUrl, timestamp: backupData.timestamp, selected: false, }); } else if (backupData.timestamp > existingMint.timestamp) { existingMint.timestamp = backupData.timestamp; } } } catch (decryptError) { console.error("Failed to decrypt backup event:", decryptError); } } // Sort by timestamp (newest first) allDiscoveredMints.sort((a, b) => b.timestamp - a.timestamp); this.discoveredMints = allDiscoveredMints; if (allDiscoveredMints.length > 0) { notify(`Found ${allDiscoveredMints.length} mint(s) in Nostr backups`); } else { notify("No mint backups found on Nostr for this seed phrase"); } return allDiscoveredMints; } catch (error) { console.error("Failed to search mints on Nostr:", error); notifyError( "Failed to search for mint backups: " + (error as Error).message ); throw error; } finally { this.searchInProgress = false; } }, // Add selected mints to the wallet async addSelectedMintsToWallet( selectedMints: DiscoveredMint[] ): Promise { const mintsStore = useMintsStore(); try { let addedCount = 0; for (const mint of selectedMints) { if (!mint.selected) continue; try { // Check if mint already exists const existing = mintsStore.mints.find((m) => m.url === mint.url); if (existing) { console.log(`Mint ${mint.url} already exists, skipping`); continue; } // Add the mint await mintsStore.addMint({ url: mint.url }, false); addedCount++; } catch (error) { console.error(`Failed to add mint ${mint.url}:`, error); notifyError( `Failed to add mint ${mint.url}: ${(error as Error).message}` ); } } if (addedCount > 0) { notifySuccess(`Added ${addedCount} mint(s) to your wallet`); } else { notify("No new mints were added"); } } catch (error) { console.error("Failed to add selected mints:", error); notifyError( "Failed to add selected mints: " + (error as Error).message ); throw error; } }, // Toggle selection of a discovered mint toggleMintSelection(mintUrl: string): void { const mint = this.discoveredMints.find((m) => m.url === mintUrl); if (mint) { mint.selected = !mint.selected; } }, // Select all discovered mints selectAllMints(): void { this.discoveredMints.forEach((mint) => { mint.selected = true; }); }, // Deselect all discovered mints deselectAllMints(): void { this.discoveredMints.forEach((mint) => { mint.selected = false; }); }, // Enable the backup feature async enableBackup(): Promise { const settingsStore = useSettingsStore(); settingsStore.nostrMintBackupEnabled = true; // Initialize backup keys await this.initializeBackupKeys(); // Perform initial backup if needed if (this.needsBackup) { await this.backupMintsToNostr(true); } }, // Disable the backup feature disableBackup(): void { const settingsStore = useSettingsStore(); settingsStore.nostrMintBackupEnabled = false; }, // Force backup (regardless of whether it's needed) async forceBackup(): Promise { const settingsStore = useSettingsStore(); const wasEnabled = settingsStore.nostrMintBackupEnabled; settingsStore.nostrMintBackupEnabled = true; try { await this.backupMintsToNostr(true); } finally { settingsStore.nostrMintBackupEnabled = wasEnabled; } }, // Clear discovered mints clearDiscoveredMints(): void { this.discoveredMints = []; }, }, }); ================================================ FILE: src/stores/nostrUser.ts ================================================ import { defineStore } from "pinia"; import NDK, { NDKEvent, NDKFilter, NDKKind } from "@nostr-dev-kit/ndk"; import { useLocalStorage } from "@vueuse/core"; import { useNostrStore } from "./nostr"; import Dexie from "dexie"; type NostrProfile = { name?: string; display_name?: string; picture?: string; image?: string; // sometimes used instead of picture about?: string; nip05?: string; lud16?: string; }; export const useNostrUserStore = defineStore("nostrUser", { state: () => ({ pubkey: useLocalStorage("cashu.nostrUser.pubkey", ""), profile: null as NostrProfile | null, follows: [] as string[], wotHopsByPubkey: {} as Record, lastUpdatedAt: 0, ndkConnected: false, wotLoading: false, wotMaxHops: 2, profileRefreshIntervalSeconds: 60, // 1 minute dbInitialized: false, crawlProcessed: 0, crawlTotal: 0, crawlCheckpointNextIndex: 0, crawlCheckpointTotal: 0, wotCancelRequested: false, defaultWoTSeedPubkey: "04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9", defaultShallowWoTPubkeys: [ "04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9", // ODELL "50d94fc2d8580c682b071a542f8b1e31a200b0508bab95a33bef0855df281d63", // Calle "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2", // jack "b33bf9e97b78f35694a02e6bbef8e77059373e42b0a85a63f25a50ebfdadf50d", // Minibits "1afe0c74e3d7784eba93a5e3fa554a6eeb01928d12739ae8ba4832786808e36d", // AmericanHodl "c48e29f04b482cc01ca1f9ef8c86ef8318c059e0e9353235162f080f26e14c11", // Walker "c43bbb58e2e6bc2f9455758257f6ba5329107bd4e8274068c2936c69d9980b7d", // roya ], }), getters: { displayName(state): string { const p = state.profile || {}; const name = (p.display_name || p.name || "").trim(); return ( name || (state.pubkey ? `${state.pubkey.slice(0, 8)}…${state.pubkey.slice(-4)}` : "") ); }, wotCount(state): number { return Object.keys(state.wotHopsByPubkey || {}).length; }, isInWebOfTrust: (state) => (pk: string): boolean => { return pk in state.wotHopsByPubkey; }, getHop: (state) => (pk: string): number | null => { return state.wotHopsByPubkey[pk] ?? null; }, hasCrawlCheckpoint(state): boolean { return ( typeof state.crawlCheckpointNextIndex === "number" && typeof state.crawlCheckpointTotal === "number" && state.crawlCheckpointTotal > 0 && state.crawlCheckpointNextIndex > 0 && state.crawlCheckpointNextIndex < state.crawlCheckpointTotal ); }, }, actions: { initDb: async function () { if (db.isOpen()) return; await db.open(); }, ensureDbInitialized: async function () { if (this.dbInitialized) return; await this.initDb(); // Load persisted data const [follows, wot, last, nextIdx, hop1Saved] = await Promise.all([ db.follows.toArray(), db.wot.toArray(), db.meta.get("lastUpdatedAt"), db.meta.get("wot.crawl.nextIndex"), db.meta.get("wot.crawl.hop1"), ]); this.follows = follows.map((f) => f.pubkey); this.wotHopsByPubkey = Object.fromEntries( wot.map((e) => [e.pubkey, e.hop]) ); this.lastUpdatedAt = (last?.value as number) || 0; this.crawlCheckpointNextIndex = (nextIdx?.value as number) || 0; this.crawlCheckpointTotal = Array.isArray(hop1Saved?.value) ? (hop1Saved?.value as string[]).length : 0; this.dbInitialized = true; }, ensureNdk: function (): NDK { // Use the global NDK instance managed by the nostr store to avoid stuck fetches const nostr = useNostrStore(); if (!nostr.connected || !nostr.ndk) nostr.initNdkReadOnly(); return nostr.ndk as unknown as NDK; }, sleep: async function (ms: number) { return await new Promise((resolve) => setTimeout(resolve, ms)); }, setPubkey: function (pubkey: string) { this.pubkey = pubkey || ""; }, updateUserProfile: async function (force = false) { await this.ensureDbInitialized(); if (!this.pubkey) return; const now = Math.floor(Date.now() / 1000); if ( !force && now - this.lastUpdatedAt < this.profileRefreshIntervalSeconds ) return; // throttle to profileRefreshIntervalSeconds seconds console.log( `[nostrUser] Updating user profile for ${this.pubkey} (force=${force})` ); await this.fetchProfile(); await this.fetchFollows(); this.lastUpdatedAt = now; await db.meta.put({ key: "lastUpdatedAt", value: now }); }, fetchProfile: async function () { if (!this.pubkey) return; const ndk = this.ensureNdk(); const filter: NDKFilter = { kinds: [NDKKind.Metadata], authors: [this.pubkey], limit: 1, }; try { console.log(`[nostrUser] Fetching kind:0 profile for ${this.pubkey}`); const events = await ndk.fetchEvents(filter); let latest: NDKEvent | undefined; events.forEach((e) => { if (!latest || (e.created_at || 0) > (latest.created_at || 0)) latest = e; }); if (latest) { try { const content = JSON.parse(latest.content || "{}"); this.profile = content as NostrProfile; } catch {} } } catch {} }, fetchFollowsOf: async function (pk: string): Promise { const ndk = this.ensureNdk(); const filter: NDKFilter = { kinds: [NDKKind.Contacts], authors: [pk], limit: 1, }; try { console.log(`[nostrUser] Fetching follows (kind:3) for ${pk}`); const events = await ndk.fetchEvents(filter); let latest: NDKEvent | undefined; events.forEach((e) => { if (!latest || (e.created_at || 0) > (latest.created_at || 0)) latest = e; }); if (!latest) return []; const follows = (latest.tags || []) .filter((t) => t[0] === "p" && typeof t[1] === "string") .map((t) => t[1]); console.log(`[nostrUser] Fetched ${follows.length} follows for ${pk}`); return Array.from(new Set(follows)); } catch { return []; } }, fetchFollows: async function () { await this.ensureDbInitialized(); if (!this.pubkey) return; try { console.log(`[nostrUser] Fetching follows (kind:3) for ${this.pubkey}`); const follows = await this.fetchFollowsOf(this.pubkey); this.follows = follows; console.log( `[nostrUser] Fetched ${follows.length} follows for ${this.pubkey}` ); await db.follows.clear(); if (follows.length) { await db.follows.bulkPut(follows.map((pk) => ({ pubkey: pk }))); } // Immediately reflect 1-hop WOT with follows const next: Record = { ...this.wotHopsByPubkey }; for (const pk of follows) { if (pk && pk !== this.pubkey) next[pk] = 1; } this.wotHopsByPubkey = next; if (Object.keys(next).length) { // Upsert WOT entries with hop=1 const wotRows = Object.entries(next).map(([pubkey]) => ({ pubkey, hop: next[pubkey], })); await db.wot.bulkPut(wotRows); } } catch {} }, shallowCrawlWebOfTrust: async function () { // call 1-hop crawlWebOfTrust for each of the defaultShallowWoTPubkeys for (const pubkey of this.defaultShallowWoTPubkeys) { await this.crawlWebOfTrust(1, pubkey); } }, crawlWebOfTrust: async function ( maxHops: number | undefined = undefined, sourcePubKey: string | undefined = undefined ) { await this.ensureDbInitialized(); maxHops = maxHops || this.wotMaxHops; const nostr = useNostrStore(); const source = sourcePubKey || (nostr.signerType === "SEED" ? this.defaultWoTSeedPubkey : this.pubkey); if (!source) return; if (this.wotLoading) return; this.wotLoading = true; this.wotCancelRequested = false; try { console.log( `[nostrUser] Crawling web of trust from ${source} up to ${maxHops} hops…` ); // Determine resume vs fresh crawl const hop1Saved = (await db.meta.get("wot.crawl.hop1"))?.value as | string[] | undefined; const nextIndexSaved = (await db.meta.get("wot.crawl.nextIndex")) ?.value as number | undefined; let hop1: string[] = []; let startIndex = 0; if ( Array.isArray(hop1Saved) && typeof nextIndexSaved === "number" && nextIndexSaved >= 0 && nextIndexSaved < hop1Saved.length ) { // Resume hop1 = hop1Saved; startIndex = nextIndexSaved; } else { // Fresh start from source's follows const baseFollows = source === this.pubkey ? this.follows.length ? this.follows : await this.fetchFollowsOf(source) : await this.fetchFollowsOf(source); hop1 = Array.from(new Set(baseFollows)); startIndex = 0; await db.meta.put({ key: "wot.crawl.hop1", value: hop1 }); await db.meta.put({ key: "wot.crawl.nextIndex", value: 0 }); } const wot: Record = {}; // Ensure 1 hop are reflected immediately for (const pk of hop1) { if (pk && pk !== source) wot[pk] = 1; } // Update progress counters for UI this.crawlTotal = hop1.length; this.crawlProcessed = startIndex; this.crawlCheckpointTotal = hop1.length; this.crawlCheckpointNextIndex = startIndex; // Commit 1-hop immediately, keeping shortest hop in both state and DB const mergedInitial: Record = { ...this.wotHopsByPubkey, }; for (const [k, v] of Object.entries(wot)) { mergedInitial[k] = Math.min(mergedInitial[k] ?? v, v); } this.wotHopsByPubkey = mergedInitial; if (Object.keys(wot).length) { await db.wot.bulkPut( Object.keys(wot).map((pubkey) => ({ pubkey, hop: this.wotHopsByPubkey[pubkey], })) ); } if (maxHops >= 2 && hop1.length) { // Sequentially fetch to avoid blocking the UI; short delay between requests const stepDelayMs = 20; // ~1 frame for (let i = startIndex; i < hop1.length; i++) { if (this.wotCancelRequested) break; const pk1 = hop1[i]; const followsOfFollow = await this.fetchFollowsOf(pk1); for (const pk2 of followsOfFollow) { if (!pk2 || pk2 === source) continue; if (!(pk2 in wot)) wot[pk2] = 2; } this.crawlProcessed = i + 1; // Persist checkpoint so we can resume later await db.meta.put({ key: "wot.crawl.nextIndex", value: i + 1 }); this.crawlCheckpointNextIndex = i + 1; if (this.crawlProcessed % 3 === 0) { // Periodically update state so UI reflects progress const merged: Record = { ...this.wotHopsByPubkey, }; for (const [k, v] of Object.entries(wot)) { merged[k] = Math.min(merged[k] ?? v, v); } this.wotHopsByPubkey = merged; await db.wot.bulkPut( Object.keys(wot).map((pubkey) => ({ pubkey, hop: this.wotHopsByPubkey[pubkey], })) ); } await this.sleep(stepDelayMs); } } // Merge with existing to retain shorter hops and previous entries const merged: Record = { ...this.wotHopsByPubkey }; for (const [k, v] of Object.entries(wot)) { merged[k] = Math.min(merged[k] ?? v, v); } this.wotHopsByPubkey = merged; if (Object.keys(wot).length) { await db.wot.bulkPut( Object.keys(wot).map((pubkey) => ({ pubkey, hop: this.wotHopsByPubkey[pubkey], })) ); } if (!this.wotCancelRequested) { console.log( `[nostrUser] Crawl complete. Known pubkeys: ${ Object.keys(this.wotHopsByPubkey).length }` ); // Clear checkpoint upon completion await db.meta.delete("wot.crawl.hop1"); await db.meta.delete("wot.crawl.nextIndex"); this.crawlCheckpointNextIndex = 0; this.crawlCheckpointTotal = 0; } else { console.log( `[nostrUser] Crawl cancelled at ${this.crawlProcessed}/${this.crawlTotal}` ); } } finally { this.wotLoading = false; // Reset progress counters when done if (!this.wotCancelRequested) { this.crawlTotal = 0; this.crawlProcessed = 0; } this.wotCancelRequested = false; } }, cancelCrawl: function () { if (!this.wotLoading) return; this.wotCancelRequested = true; }, resetWebOfTrust: async function () { await this.ensureDbInitialized(); if (this.wotLoading) return; this.wotHopsByPubkey = {}; await db.wot.clear(); await db.meta.delete("wot.crawl.hop1"); await db.meta.delete("wot.crawl.nextIndex"); this.crawlCheckpointNextIndex = 0; this.crawlCheckpointTotal = 0; }, clearAllDatabases: function () { db.wot.clear(); db.follows.clear(); db.meta.clear(); }, }, }); // Dexie DB setup for web-of-trust persistence class NostrUserDB extends Dexie { wot!: Dexie.Table<{ pubkey: string; hop: number }, string>; follows!: Dexie.Table<{ pubkey: string }, string>; meta!: Dexie.Table<{ key: string; value: any }, string>; constructor() { super("nostrUserDB"); this.version(1).stores({ wot: "pubkey", follows: "pubkey", meta: "key", }); } } const db = new NostrUserDB(); ================================================ FILE: src/stores/npcv2.ts ================================================ import { defineStore } from "pinia"; import NDK, { NDKEvent } from "@nostr-dev-kit/ndk"; import { useLocalStorage } from "@vueuse/core"; import { nip19 } from "nostr-tools"; import { useWalletStore } from "./wallet"; import { notifyApiError, notifyError, notifySuccess } from "../js/notify"; import { MintQuoteState } from "@cashu/cashu-ts"; import { useNostrStore } from "../stores/nostr"; import { date } from "quasar"; import { useMintsStore } from "./mints"; type NPCUser = { lockQuote: boolean; mintUrl: string; name?: string; pubkey: string; }; type NPCV2InfoReponse = | { error: true; message: string; } | { error: false; data: { user: NPCUser; }; }; type NPCV2UsernameReponse = | { error: true; message: string } | { error: false; data: { user: NPCUser } }; type NPCQuote = { createdAt: number; paidAt: number; expiresAt: number; mintUrl: string; quoteId: string; request: string; amount: number; state: string; locked: boolean; }; type NPCQuoteResponse = | { error: true; message: string; } | { error: false; data: { quotes: NPCQuote[]; }; metadata: { limit: number; total: number; since?: number }; }; type UsernameQuote = { username: string; creq: string }; const NIP98Kind = 27235; export const useNPCV2Store = defineStore("npcV2", { state: () => ({ npcV2Enabled: useLocalStorage("cashu.npc.v2.enabled", false), npcV2ClaimAutomatically: useLocalStorage( "cashu.npc.v2.claimAutomatically", true ), npcV2LastCheck: useLocalStorage("cashu.npc.v2.lastCheck", null), npcV2Address: useLocalStorage("cashu.npc.v2.address", ""), npcV2Mint: useLocalStorage("cashu.npc.v2.mint", null), npcV2Domain: "", npcV2BaseURL: useLocalStorage( "cashu.npc.v2.baseURL", "https://npubx.cash" ), npcV2Loading: false, // ndk: new NDK(), // signer: {} as NDKPrivateKeySigner, }), getters: {}, actions: { generateNPCV2Connection: async function () { if (!this.npcV2Enabled) { return; } const nostrStore = useNostrStore(); const mintsStore = useMintsStore(); if (!nostrStore.pubkey) { return; } const walletPublicKeyHex = nostrStore.pubkey; this.npcV2Domain = new URL(this.npcV2BaseURL).hostname; this.npcV2Address = nip19.npubEncode(walletPublicKeyHex) + "@" + this.npcV2Domain; this.npcV2Loading = true; try { const previousAddress = this.npcV2Address; const info = await this.getV2Info(); if (info.name) { const usernameAddress = info.name + "@" + this.npcV2Domain; if (previousAddress !== usernameAddress) { notifySuccess(`Logged in as ${info.name}`); } this.npcV2Address = usernameAddress; } if (mintsStore.mints.map((m) => m.url).includes(info.mintUrl)) { this.npcV2Mint = info.mintUrl; } else if (mintsStore.activeMintUrl) { await this.changeMintUrl(mintsStore.activeMintUrl); } else { await mintsStore.addMint({ url: info.mintUrl }); this.npcV2Mint = info.mintUrl; } } catch (e) { if (e instanceof Error) { notifyApiError(e); } console.log(e); } finally { this.npcV2Loading = false; } }, getV2Info: async function (): Promise<{ name?: string; mintUrl: string; lockQuote: boolean; pubkey: string; }> { try { const response = await this.sendAuthedRequest( `${this.npcV2BaseURL}/api/v2/user/info` ); const info: NPCV2InfoReponse = await response.json(); if (info.error) { notifyError(info.message); throw new Error(info.message); } return info.data.user; } catch (e) { console.error(e); return { mintUrl: "", name: "", pubkey: "", lockQuote: false, }; } }, changeMintUrl: async function (mintUrl: string) { const mintstore = useMintsStore(); if (!mintstore.mints.find((m) => m.url === mintUrl)) { notifyError( `Please make sure ${mintUrl} is added to your wallet first!`, "Could not update npubx.cash mint" ); return; } try { const res = await this.sendAuthedRequest( `${this.npcV2BaseURL}/api/v2/user/mint`, { headers: { "Content-Type": "application/json", }, method: "PATCH", body: JSON.stringify({ mint_url: mintUrl }), } ); const data = await res.json(); if (data.error) { throw new Error(data.message); } this.npcV2Mint = data.data.user.mintUrl; } catch (e) { console.log(e); if (e instanceof Error) { notifyError(e.message); } else { notifyError("Something went wrong!"); } } }, getLatestQuotes: async function () { if (!this.npcV2Enabled) { return; } const walletStore = useWalletStore(); const since = this.npcV2LastCheck ? `?since=${this.npcV2LastCheck}` : ""; const quoteUrl = `${this.npcV2BaseURL}/api/v2/wallet/quotes`; try { const response = await this.sendAuthedRequest( quoteUrl + since, undefined, quoteUrl ); const resData: NPCQuoteResponse = await response.json(); if (resData.error) { return; } let latestQuoteTime: number | undefined = undefined; resData.data.quotes.forEach(async (quote) => { if ( walletStore.invoiceHistory.find((i) => i.quote === quote.quoteId) ) { return; } if (!latestQuoteTime || latestQuoteTime < quote.createdAt) { latestQuoteTime = quote.createdAt; } walletStore.invoiceHistory.push({ label: "Zap", mint: quote.mintUrl, memo: "", bolt11: quote.request, amount: quote.amount, quote: quote.quoteId, date: date.formatDate( new Date(quote.createdAt * 1000), "YYYY-MM-DD HH:mm:ss" ), status: "pending", unit: "sat", mintQuote: { request: quote.request, quote: quote.quoteId, state: MintQuoteState.PAID, expiry: quote.expiresAt, amount: quote.amount, unit: "sat", }, }); if (this.npcV2ClaimAutomatically) { await walletStore.mintOnPaid(quote.quoteId); } }); if (latestQuoteTime) { this.npcV2LastCheck = latestQuoteTime; } } catch (e) { console.error(e); return; } }, getUsernameQuote: async function ( username: string ): Promise { const res = await this.sendAuthedRequest( `${this.npcV2BaseURL}/api/v2/user/username`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username }), } ); const data = (await res.json()) as NPCV2UsernameReponse; if (data.error) { if (res.status === 402) { const paymentHeader = res.headers.get("X-Cashu"); if (!paymentHeader) { throw new Error("Unexpected reply without payment request"); } return { username, creq: paymentHeader }; } throw new Error(data.message); } throw new Error("Unexpected reply without payment request"); }, setUsername: async function (username: string, token: string) { try { const res = await this.sendAuthedRequest( `${this.npcV2BaseURL}/api/v2/user/username`, { method: "POST", headers: { "Content-Type": "application/json", "X-Cashu": token }, body: JSON.stringify({ username }), } ); const data = (await res.json()) as NPCV2UsernameReponse; if (data.error) { throw new Error(data.message); } this.npcV2Address = `${data.data.user.name}@${this.npcV2Domain}`; } catch (e) { console.log(e); if (e instanceof Error) { notifyError(e.message); } } }, sendAuthedRequest: async function ( url: string, opts?: RequestInit, authUrl?: string ) { const authHeader = await this.generateNip98Event( authUrl || url, opts?.method || "GET" ); return fetch(url, { ...opts, headers: { ...opts?.headers, authorization: `Nostr ${authHeader}` }, }); }, generateNip98Event: async function ( url: string, method: string ): Promise { const nostrStore = useNostrStore(); await nostrStore.initSignerIfNotSet(); const nip98Event = new NDKEvent(new NDK()); nip98Event.kind = NIP98Kind; nip98Event.content = ""; nip98Event.tags = [ ["u", url], ["method", method], ]; await nip98Event.sign(nostrStore.signer); const eventString = JSON.stringify(nip98Event.rawEvent()); return btoa(eventString); }, }, }); ================================================ FILE: src/stores/npubcash.ts ================================================ import { defineStore } from "pinia"; import NDK, { NDKEvent } from "@nostr-dev-kit/ndk"; import { useLocalStorage } from "@vueuse/core"; import { nip19 } from "nostr-tools"; import { useWalletStore } from "./wallet"; import { useReceiveTokensStore } from "./receiveTokensStore"; import { notifyApiError, notifyError, notifySuccess } from "../js/notify"; import token from "../js/token"; import { useTokensStore } from "../stores/tokens"; import { useNostrStore } from "../stores/nostr"; // type NPCConnection = { // walletPublicKey: string, // walletPrivateKey: string, // } type NPCInfo = { mintUrl: string; npub: string; username: string; error?: string; }; type NPCBalance = { error: string; data: number; }; type NPCClaim = { error: string; data: { token: string; }; }; type NPCWithdrawl = { id: number; claim_ids: number[]; created_at: number; pubkey: string; amount: number; }; type NPCWithdrawals = { error: string; data: { count: number; withdrawals: Array; }; }; const NIP98Kind = 27235; export const useNPCStore = defineStore("npc", { state: () => ({ npcEnabled: useLocalStorage("cashu.npc.enabled", false), npcLastCheck: useLocalStorage("cashu.npc.lastCheck", null), automaticClaim: useLocalStorage("cashu.npc.automaticClaim", true), // npcConnections: useLocalStorage("cashu.npc.connections", []), npcAddress: useLocalStorage("cashu.npc.address", ""), npcDomain: useLocalStorage("cashu.npc.domain", "npub.cash"), baseURL: useLocalStorage("cashu.npc.baseURL", "https://npub.cash"), npcLoading: false, // ndk: new NDK(), // signer: {} as NDKPrivateKeySigner, }), getters: {}, actions: { generateNPCConnection: async function () { const nostrStore = useNostrStore(); if (!nostrStore.pubkey) { return; } const walletPublicKeyHex = nostrStore.pubkey; console.log( "Lightning address for wallet:", nip19.npubEncode(walletPublicKeyHex) + "@" + this.npcDomain ); console.log("npub:", nip19.npubEncode(walletPublicKeyHex)); this.baseURL = `https://${this.npcDomain}`; const previousAddress = this.npcAddress; this.npcAddress = nip19.npubEncode(walletPublicKeyHex) + "@" + this.npcDomain; if (!this.npcEnabled) { return; } // get info this.npcLoading = true; try { const info = await this.getInfo(); if (info.error) { notifyError(info.error); return; } // log info console.log(info); if (info.username) { const usernameAddress = info.username + "@" + this.npcDomain; if (previousAddress !== usernameAddress) { notifySuccess(`Logged in as ${info.username}`); } this.npcAddress = usernameAddress; } } catch (e: any) { notifyApiError(e); } finally { this.npcLoading = false; } }, generateNip98Event: async function ( url: string, method: string, body?: string ): Promise { const nostrStore = useNostrStore(); await nostrStore.initSignerIfNotSet(); const nip98Event = new NDKEvent(new NDK()); nip98Event.kind = NIP98Kind; nip98Event.content = ""; nip98Event.tags = [ ["u", url], ["method", method], ]; // TODO: if body is set, add 'payload' tag with sha256 hash of body const sig = await nip98Event.sign(nostrStore.signer); const eventString = JSON.stringify(nip98Event.rawEvent()); // encode the eventString to base64 return btoa(eventString); }, getInfo: async function (): Promise { const authHeader = await this.generateNip98Event( `${this.baseURL}/api/v1/info`, "GET" ); try { const response = await fetch(`${this.baseURL}/api/v1/info`, { method: "GET", headers: { Authorization: `Nostr ${authHeader}`, }, }); const info: NPCInfo = await response.json(); return info; } catch (e) { console.error(e); return { mintUrl: "", npub: "", username: "", }; } }, claimAllTokens: async function () { if (!this.npcEnabled) { return; } const receiveStore = useReceiveTokensStore(); const npubCashBalance = await this.getBalance(); console.log("npub.cash balance: " + npubCashBalance); if (npubCashBalance > 0) { notifySuccess(`You have ${npubCashBalance} sats on npub.cash`); const token = await this.getClaim(); if (token) { // add token to history first this.addPendingTokenToHistory(token); receiveStore.receiveData.tokensBase64 = token; if (this.automaticClaim) { try { // redeem token automatically const walletStore = useWalletStore(); await walletStore.redeem(); } catch { // if it doesn't work, show the receive window receiveStore.showReceiveTokens = true; } } else { receiveStore.showReceiveTokens = true; } } } }, tokenAlreadyInHistory: function (tokenStr: string) { const tokensStore = useTokensStore(); return ( tokensStore.historyTokens.find((t) => t.token === tokenStr) !== undefined ); }, addPendingTokenToHistory: function (tokenStr: string) { const receiveStore = useReceiveTokensStore(); if (this.tokenAlreadyInHistory(tokenStr)) { notifySuccess("Ecash already in history"); receiveStore.showReceiveTokens = false; return; } const tokensStore = useTokensStore(); const decodedToken = token.decode(tokenStr); if (decodedToken == undefined) { throw Error("could not decode token"); } // get amount from decodedToken.token.proofs[..].amount const amount = token .getProofs(decodedToken) .reduce((sum, el) => (sum += el.amount), 0); const mintUrl = token.getMint(decodedToken); const unit = token.getUnit(decodedToken); tokensStore.addPendingToken({ label: "Zaps", amount: amount, token: tokenStr, mint: mintUrl, unit: unit, }); receiveStore.showReceiveTokens = false; }, getBalance: async function (): Promise { const authHeader = await this.generateNip98Event( `${this.baseURL}/api/v1/balance`, "GET" ); try { const response = await fetch(`${this.baseURL}/api/v1/balance`, { method: "GET", headers: { Authorization: `Nostr ${authHeader}`, }, }); // deserialize the response to NPCBalance const balance: NPCBalance = await response.json(); if (balance.error) { return 0; } return balance.data; } catch (e) { console.error(e); return 0; } }, getClaim: async function (): Promise { const authHeader = await this.generateNip98Event( `${this.baseURL}/api/v1/claim`, "GET" ); try { const response = await fetch(`${this.baseURL}/api/v1/claim`, { method: "GET", headers: { Authorization: `Nostr ${authHeader}`, }, }); // deserialize the response to NPCClaim const claim: NPCClaim = await response.json(); if (claim.error) { return ""; } return claim.data.token; } catch (e) { console.error(e); return ""; } }, }, }); ================================================ FILE: src/stores/nwc.ts ================================================ import { defineStore } from "pinia"; import NDK, { NDKEvent, NDKFilter, NDKPrivateKeySigner, NDKKind, NDKSubscription, } from "@nostr-dev-kit/ndk"; import { useLocalStorage } from "@vueuse/core"; import { bytesToHex } from "@noble/hashes/utils"; // already an installed dependency import { nip04, generateSecretKey, getPublicKey } from "nostr-tools"; import { useMintsStore } from "./mints"; import { useWalletStore, InvoiceHistory } from "./wallet"; import { useProofsStore } from "./proofs"; import { notifyWarning } from "../js/notify"; import { useNostrStore } from "./nostr"; import { decode as decodeBolt11 } from "light-bolt11-decoder"; type NWCConnection = { walletPublicKey: string; walletPrivateKey: string; connectionSecret: string; connectionPublicKey: string; allowanceLeft: number; }; type NWCCommand = { method: string; params: any; }; type NWCTransaction = { type: string; invoice: string; description: string | null; preimage: string | null; payment_hash: string | null; amount: number; fees_paid: number | null; created_at: number; settled_at: number | null; expires_at: number | null; }; type NWCResult = { result_type: string; result: any; }; type NWCError = { result_type: string; error: { code: string; message: string; }; }; const NWCKind = { NWCInfo: 13194, NWCRequest: 23194, NWCResponse: 23195, }; export const useNWCStore = defineStore("nwc", { state: () => ({ nwcEnabled: useLocalStorage("cashu.nwc.enabled", false), connections: useLocalStorage("cashu.nwc.connections", []), seenCommandsUntil: useLocalStorage( "cashu.nwc.seenCommandsUntil", 0 ), supportedMethods: [ "pay_invoice", "make_invoice", "get_balance", "get_info", "list_transactions", "lookup_invoice", ], blocking: false, ndk: new NDK(), subscriptions: [] as NDKSubscription[], showNWCDialog: false, showNWCData: { connection: {} as NWCConnection, connectionString: "" }, }), getters: {}, actions: { // ––––---------- NWC Command Handlers ––––---------- handleGetInfo: async function (nwcCommand: NWCCommand) { console.log("### get_info", nwcCommand.method); return { result_type: "get_info", result: { alias: "Cashu.me", color: "#FF0000", pubkey: this.connections[0].walletPublicKey, network: "mainnet", block_height: 1, block_hash: "blockchain disrespectoor", methods: this.supportedMethods, }, }; }, handleGetBalance: async function (nwcCommand: NWCCommand) { const mintsStore = useMintsStore() as any; console.log("### get_balance", nwcCommand.method); return { result_type: "get_balance", result: { balance: mintsStore.totalUnitBalance * 1000, }, }; }, handlePayInvoice: async function (nwcCommand: NWCCommand) { const invoice = nwcCommand.params.invoice; const amountMsat = nwcCommand.params.amount; console.log("### pay_invoice", nwcCommand.method); console.log("### invoice", invoice); console.log("### amountMsat", amountMsat); // pay invoice const walletStore = useWalletStore(); const proofsStore = useProofsStore(); const mintStore = useMintsStore(); try { await walletStore.decodeRequest(invoice); } catch (e) { console.log("### error decoding invoice", e); return { result_type: nwcCommand.method, error: { code: "INTERNAL", message: "Invalid invoice" }, } as NWCError; } // expect that the melt quote was requested if ( walletStore.payInvoiceData.meltQuote.response.amount == 0 || walletStore.payInvoiceData.meltQuote.error ) { notifyWarning("NWC: Error requesting melt quote"); return { result_type: nwcCommand.method, error: { code: "INTERNAL", message: "Error requesting melt quote" }, } as NWCError; } const maximumAmount = walletStore.payInvoiceData.meltQuote.response.amount + walletStore.payInvoiceData.meltQuote.response.fee_reserve; if (mintStore.activeUnit != "sat") { notifyWarning("NWC: Active unit must be sats"); return { result_type: nwcCommand.method, error: { code: "INTERNAL", message: "Your active must be sats" }, } as NWCError; } if (maximumAmount > this.connections[0].allowanceLeft) { notifyWarning("NWC: Allowance exceeded"); return { result_type: nwcCommand.method, error: { code: "QUOTA_EXCEEDED", message: "Your quota has exceeded" }, } as NWCError; } try { const meltData = await walletStore.meltInvoiceData(); const paidAmount = walletStore.payInvoiceData.meltQuote.response.amount + walletStore.payInvoiceData.meltQuote.response.fee_reserve - proofsStore.sumProofs(meltData.change ?? []); this.connections[0].allowanceLeft -= paidAmount; return { result_type: nwcCommand.method, result: { // preimage: meltData.preimage, }, }; } catch (e) { return { result_type: nwcCommand.method, error: { code: "INTERNAL", message: "Could not pay invoice" }, } as NWCError; } }, handleMakeInvoice: async function (nwcCommand: NWCCommand) { const { amount, description, expiry } = nwcCommand.params; console.log("### make_invoice"); console.log("### amount", amount); // msats console.log("### description", description); console.log("### expiry", expiry); // seconds // make invoice const walletStore = useWalletStore(); const wallet = await walletStore.activeWallet(); const quote = await walletStore.requestMint(amount / 1000, wallet); if (!quote) { // requesting mint invoice can fail if no mint was selected yet // the error will have been shown as a notification // TODO: make requestMint throw and return useful message return { result_type: nwcCommand.method, error: { code: "INTERNAL", message: "failed to request mint invoice", }, }; } walletStore.mintOnPaid(quote.quote, false, true); return { result_type: nwcCommand.method, result: { type: "incoming", invoice: quote?.request, description, amount, }, }; }, handleListTransactions: async function (nwcCommand: NWCCommand) { console.log("### list_transactions", nwcCommand.method); const walletStore = useWalletStore(); const from = nwcCommand.params.from || 0; const until = nwcCommand.params.until || Math.floor(Date.now() / 1000); const limit = nwcCommand.params.limit || 10; const offset = nwcCommand.params.offset || 0; const unpaid = nwcCommand.params.unpaid || false; const type = nwcCommand.params.type || undefined; const invoiceHistory = walletStore.invoiceHistory; const transactionsHistory = invoiceHistory .filter((invoice) => { const date = new Date(invoice.date); const created_at = Math.floor(date.getTime() / 1000); if (from && created_at < from) { return false; } if (until && created_at > until) { return false; } if (type && type == "incoming" && invoice.amount < 0) { return false; } if (type && type == "outgoing" && invoice.amount > 0) { return false; } if (unpaid && invoice.status == "paid") { return false; } return true; }) .slice(offset, offset + limit); // now create an array "transactions" out of nwcTransaction from transactionsHistory // // type = "incoming" if amount > 0 else "outgoing" // amount = abs(amount) // created_at = unix timestamp of date // settled_at = unix timestamp of date if status == "paid" else null // According to the NWC spec (NIP47): "Transactions are returned in descending order of creation time." const transactions = transactionsHistory .map(this.mapToNwcTransaction) .sort((a, b) => b.created_at - a.created_at); return { result_type: "list_transactions", result: { transactions: transactions, }, }; }, handleLookupInvoice: async function (nwcCommand: NWCCommand) { let hash = nwcCommand.params.payment_hash; if (!hash) { const bolt11 = nwcCommand.params.invoice; const decoded = bolt11 ? decodeBolt11(bolt11) : null; // @ts-ignore hash = decoded?.sections.find((s) => s.name === "payment_hash")?.value; } if (!hash) { return { result_type: nwcCommand.method, error: { code: "OTHER", message: "invoice or payment_hash required" }, }; } console.log("### lookup_invoice"); const walletStore = useWalletStore(); const invoiceHistory = walletStore.invoiceHistory; for (const inv of invoiceHistory) { const decoded = decodeBolt11(nwcCommand.params.invoice); // @ts-ignore const invHash = decoded.sections.find( (s) => s.name === "payment_hash" )?.value; if (invHash === hash) { return { result_type: nwcCommand.method, result: this.mapToNwcTransaction(inv), }; } } return { result_type: nwcCommand.method, error: { code: "NOT_FOUND", message: "invoice not found", }, }; }, mapToNwcTransaction(invoice: InvoiceHistory): NWCTransaction { const type = invoice.amount > 0 ? "incoming" : "outgoing"; const amount = Math.abs(invoice.amount) * 1000; const created_at = Math.floor(new Date(invoice.date).getTime() / 1000); const settled_at = invoice.status == "paid" ? Math.floor(new Date(invoice.date).getTime() / 1000) : null; return { type: type, invoice: invoice.bolt11, description: invoice.memo, amount: amount, fees_paid: 0, created_at: created_at, settled_at: settled_at, } as NWCTransaction; }, // ––––---------- NWC Connection ––––---------- replyNWC: async function ( result: NWCResult | NWCError, event: NDKEvent, conn: NWCConnection ) { // reply to NWC with result const replyEvent = new NDKEvent(event.ndk); replyEvent.kind = 23195; console.log("### replying with", JSON.stringify(result)); replyEvent.content = await nip04.encrypt( conn.walletPrivateKey, event.author.pubkey, JSON.stringify(result) ); replyEvent.tags = [ ["p", event.author.pubkey], ["e", event.id], ]; console.log("### replyEvent", replyEvent); console.log("### replying to", event.id); // await this.ndk.publish(replyEvent); await replyEvent.publish(); }, parseNWCCommand: async function ( command: string, event: NDKEvent, conn: NWCConnection ) { // parse command to JSON object {method: 'pay_invoice', params: {invoice: '1234'}} const nwcCommand: NWCCommand = JSON.parse(command); let result: NWCResult | NWCError; console.log("### nwcCommand", nwcCommand); // parse "get_info" without params if (nwcCommand.method == "get_info") { result = await this.handleGetInfo(nwcCommand); } else if (nwcCommand.method == "get_balance") { result = await this.handleGetBalance(nwcCommand); } else if (nwcCommand.method == "pay_invoice") { if (this.blocking) { result = { result_type: nwcCommand.method, error: { code: "INTERNAL", message: "Already processing a payment.", }, } as NWCError; } this.blocking = true; try { result = await this.handlePayInvoice(nwcCommand); } catch (e) { return; } finally { this.blocking = false; } } else if (nwcCommand.method === "make_invoice") { result = await this.handleMakeInvoice(nwcCommand); } else if (nwcCommand.method == "list_transactions") { result = await this.handleListTransactions(nwcCommand); } else if (nwcCommand.method === "lookup_invoice") { result = await this.handleLookupInvoice(nwcCommand); } else { console.log("### method not supported", nwcCommand.method); result = { result_type: nwcCommand.method, error: { code: "NOT_IMPLEMENTED", message: "Method not supported" }, } as NWCError; } await this.replyNWC(result, event, conn); }, getConnectionString: function (connection: NWCConnection) { const walletPublicKeyHex = connection.walletPublicKey; const connectionSecretHex = connection.connectionSecret; const nostrStore = useNostrStore(); return `nostr+walletconnect://${walletPublicKeyHex}?relay=${nostrStore.relays.join( "&relay=" )}&secret=${connectionSecretHex}`; }, generateNWCConnection: async function () { let conn: NWCConnection; // NOTE: we only support one connection for now if (!this.connections.length) { const sk = generateSecretKey(); // `sk` is a Uint8Array const walletPublicKeyHex = getPublicKey(sk); // `pk` is a hex string const walletPrivateKeyHex = bytesToHex(sk); const connectionSecret = generateSecretKey(); const connectionPublicKeyHex = getPublicKey(connectionSecret); const connectionSecretHex = bytesToHex(connectionSecret); conn = { walletPublicKey: walletPublicKeyHex, walletPrivateKey: walletPrivateKeyHex, connectionSecret: connectionSecretHex, connectionPublicKey: connectionPublicKeyHex, allowanceLeft: 1000, } as NWCConnection; this.connections = this.connections.concat(conn); } else { conn = this.connections[0]; } const walletSigner = new NDKPrivateKeySigner(conn.walletPrivateKey); // close and delete all old subscriptions this.unsubscribeNWC(); const nostrStore = useNostrStore(); this.ndk = new NDK({ explicitRelayUrls: nostrStore.relays, signer: walletSigner, }); this.ndk.connect(); const nip47InfoEvent = new NDKEvent(this.ndk as NDK); nip47InfoEvent.kind = NWCKind.NWCInfo; nip47InfoEvent.content = this.supportedMethods.join(" "); try { // let's fetch the info event from the relay to see if we need to republish it // use NWCKind.NWCInfo as an integer here const filterInfoEvent: NDKFilter = { kinds: [NWCKind.NWCInfo], authors: [conn.walletPublicKey], }; const eventsInfoEvent = await this.ndk.fetchEvents(filterInfoEvent); if (eventsInfoEvent.size === 0) { await nip47InfoEvent.publish(); console.log("### published nip47InfoEvent", nip47InfoEvent); } else { console.log("### nip47InfoEvent already published"); } } catch (e) { console.log("### could not publish nip47InfoEvent", nip47InfoEvent); console.log("### error", e); } }, listenToNWCCommands: async function () { // if (!this.connections.length) { // await this.generateNWCConnection() // } await this.generateNWCConnection(); // we only support one connection for now const conn = this.connections[0]; const currentUnitTime = Math.floor(Date.now() / 1000); const subscribeSince = currentUnitTime - 60; // 1 minute const filter = { kinds: [NWCKind.NWCRequest as NDKKind], since: subscribeSince, authors: [conn.connectionPublicKey], "#p": [conn.walletPublicKey], } as NDKFilter; const sub = this.ndk.subscribe(filter); const nostrStore = useNostrStore(); console.log("### subscribing to NWC on relays: ", nostrStore.relays); this.subscriptions.push(sub); sub.on("eose", () => console.log("All relays have reached the end of the event stream") ); sub.on("close", () => console.log("Subscription closed")); sub.on("event", async (event) => { // console.log("### event", event) // console.log('### event.kind', event.kind) // console.log('### event.id', event.id) // console.log('### event.author.pubkey', event.author.pubkey) // console.log("### event.tagValue('p')", event.tagValue("p")) // console.log("### event.tagValue('e')", event.tagValue("e")) // console.log("### event.content", event.content) if (event.kind != NWCKind.NWCRequest) { return; // ignore non-NWC events } if (!this.nwcEnabled) { console.log("### Received NWC command but NWC is disabled"); return; } // check if the events date is after the last seen command if (event.created_at <= this.seenCommandsUntil) { return; } this.seenCommandsUntil = event.created_at; console.log("### NWC request!"); console.log("### event", event); const decryptedContent = await nip04.decrypt( conn.connectionSecret, conn.walletPublicKey, event.content ); // console.log("### decryptedContent", decryptedContent) await this.parseNWCCommand(decryptedContent, event, conn); }); }, unsubscribeNWC: function () { console.log("### unsubscribing from NWC"); for (const sub of this.subscriptions) { sub.stop(); } this.subscriptions = []; }, }, }); ================================================ FILE: src/stores/p2pk.ts ================================================ import { defineStore } from "pinia"; import { useLocalStorage } from "@vueuse/core"; import { generateSecretKey, getPublicKey, nip19 } from "nostr-tools"; import { bytesToHex } from "@noble/hashes/utils"; // already an installed dependency import { WalletProof } from "stores/mints"; import token from "src/js/token"; type P2PKKey = { publicKey: string; privateKey: string; used: boolean; usedCount: number; }; export const useP2PKStore = defineStore("p2pk", { state: () => ({ p2pkKeys: useLocalStorage("cashu.P2PKKeys", []), showP2PkButtonInDrawer: useLocalStorage( "cashu.p2pk.showP2PkButtonInDrawer", false ), showP2PKDialog: false, showP2PKData: {} as P2PKKey, }), getters: {}, actions: { haveThisKey: function (key: string) { return this.p2pkKeys.filter((m) => m.publicKey == key).length > 0; }, maybeConvertNpub: function (key: string) { // Check and convert npub to P2PK if (key && key.startsWith("npub1")) { const { type, data } = nip19.decode(key); if (type === "npub" && data.length === 64) { key = "02" + data; } } return key; }, isValidPubkey: function (key: string) { key = this.maybeConvertNpub(key); return key && key.length == 66; }, setPrivateKeyUsed: function (key: string) { const thisKeys = this.p2pkKeys.filter((k) => k.privateKey == key); if (thisKeys.length) { thisKeys[0].used = true; thisKeys[0].usedCount += 1; } }, showKeyDetails: function (key: string) { const thisKeys = this.p2pkKeys.filter((k) => k.publicKey == key); if (thisKeys.length) { this.showP2PKData = JSON.parse(JSON.stringify(thisKeys[0])); this.showP2PKDialog = true; } }, showLastKey: function () { if (this.p2pkKeys.length) { this.showP2PKData = JSON.parse( JSON.stringify(this.p2pkKeys[this.p2pkKeys.length - 1]) ); this.showP2PKDialog = true; } }, importNsec: async function () { const nsec = (await prompt("Enter your nsec")) as string; if (!nsec || !nsec.startsWith("nsec1")) { console.log("input was not an nsec"); return; } const sk = nip19.decode(nsec).data as Uint8Array; // `sk` is a Uint8Array const pk = "02" + getPublicKey(sk); // `pk` is a hex string const skHex = bytesToHex(sk); if (this.haveThisKey(pk)) { console.log("nsec already exists in p2pk keystore"); return; } const keyPair: P2PKKey = { publicKey: pk, privateKey: skHex, used: false, usedCount: 0, }; this.p2pkKeys = this.p2pkKeys.concat(keyPair); }, generateKeypair: function () { const sk = generateSecretKey(); // `sk` is a Uint8Array const pk = "02" + getPublicKey(sk); // `pk` is a hex string const skHex = bytesToHex(sk); const keyPair: P2PKKey = { publicKey: pk, privateKey: skHex, used: false, usedCount: 0, }; this.p2pkKeys = this.p2pkKeys.concat(keyPair); }, getSecretP2PKPubkey: function (secret: string): string { try { const secretObject = JSON.parse(secret); if (secretObject[0] != "P2PK" || secretObject[1]["data"] == undefined) { console.log("not p2pk locked"); return ""; // not p2pk locked } // Get all the p2pk secret data const now = Math.floor(Date.now() / 1000); // unix TS const { data, tags } = secretObject[1]; const locktimeTag = tags && tags.find((tag) => tag[0] === "locktime"); const locktime = locktimeTag ? parseInt(locktimeTag[1], 10) : Infinity; // Permanent lock if not set const refundTag = tags && tags.find((tag) => tag[0] === "refund"); const refundKeys = refundTag && refundTag.length > 1 ? refundTag.slice(1) : []; const pubkeysTag = tags && tags.find((tag) => tag[0] === "pubkeys"); const pubkeys = pubkeysTag && pubkeysTag.length > 1 ? pubkeysTag.slice(1) : []; const n_sigsTag = tags && tags.find((tag) => tag[0] === "n_sigs"); const n_sigs = n_sigsTag ? parseInt(n_sigsTag[1], 10) : undefined; // If locktime is in the future, return first owned additional 'pubkeys' // match if multisig ('n_sigs'), otherwise return the main key ('data') if (locktime > now) { console.log("p2pk token - locktime is active"); if (n_sigs && n_sigs >= 1) { for (const pk of pubkeys) { if (this.haveThisKey(pk)) return pk; } } return data; // Main lock key (shows locked state) } // If locktime expired, return first owned 'refund' key match or // or just return the first refund key to show token is locked if (refundKeys.length > 0) { console.log("p2pk token - locked to refund keys"); for (const pk of refundKeys) { if (this.haveThisKey(pk)) return pk; } return refundKeys[0]; // First refund key (shows locked state) } console.log("p2pk token - lock has expired"); } catch {} return ""; // Token is not locked / secret is not P2PK }, isLocked: function (proofs: WalletProof[]) { const secrets = proofs.map((p) => p.secret); for (const secret of secrets) { try { if (this.getSecretP2PKPubkey(secret)) { return true; } } catch {} } return false; }, isLockedToUs: function (proofs: WalletProof[]) { const secrets = proofs.map((p) => p.secret); for (const secret of secrets) { const pubkey = this.getSecretP2PKPubkey(secret); if (pubkey) { return this.haveThisKey(pubkey); } } }, getPrivateKeyForP2PKEncodedToken: async function ( encodedToken: string ): Promise { const decodedToken = await token.decodeFull(encodedToken); if (!decodedToken) { return ""; } const proofs = token.getProofs(decodedToken); if (!this.isLocked(proofs) || !this.isLockedToUs(proofs)) { return ""; } const secrets = proofs.map((p) => p.secret); for (const secret of secrets) { const pubkey = this.getSecretP2PKPubkey(secret); if (pubkey && this.haveThisKey(pubkey)) { // NOTE: we assume all tokens are locked to the same key here! return this.p2pkKeys.filter((m) => m.publicKey == pubkey)[0] .privateKey; } } return ""; }, }, }); ================================================ FILE: src/stores/payment-request.ts ================================================ import { defineStore } from "pinia"; import { useWalletStore } from "./wallet"; import { decodePaymentRequest, PaymentRequest, PaymentRequestPayload, PaymentRequestTransport, PaymentRequestTransportType, } from "@cashu/cashu-ts"; import { useMintsStore } from "./mints"; import { useSendTokensStore } from "./sendTokensStore"; import { useNostrStore } from "./nostr"; import { useTokensStore } from "./tokens"; import type { HistoryToken } from "./tokens"; import token from "src/js/token"; import { notify, notifyError, notifySuccess, notifyWarning, } from "src/js/notify"; import { useLocalStorage } from "@vueuse/core"; import { v4 as uuidv4 } from "uuid"; export type OurPaymentRequest = { id: string; // UUID from PaymentRequest encoded: string; unit?: string; mints?: string[]; memo?: string; createdAt: string; receivedPaymentIds: string[]; // HistoryToken ids mapped to this PR }; export const usePRStore = defineStore("payment-request", { state: () => ({ showPRDialog: false, showPRKData: "" as string, enablePaymentRequest: useLocalStorage("cashu.pr.enable", true), receivePaymentRequestsAutomatically: useLocalStorage( "cashu.pr.receive", false ), ourPaymentRequests: useLocalStorage( "cashu.pr.ours", [] ), selectedPRIndex: useLocalStorage("cashu.pr.selected_index", 0), }), getters: { currentPaymentRequest(state): OurPaymentRequest | undefined { if (!state.ourPaymentRequests.length) return undefined; const idx = Math.min( Math.max(0, state.selectedPRIndex ?? 0), state.ourPaymentRequests.length - 1 ); return state.ourPaymentRequests[idx]; }, }, actions: { newPaymentRequest( amount?: number, memo?: string, mintUrl?: string, forceNew: boolean = false ) { const walletStore = useWalletStore(); // If not forcing a new request and we already have at least one, // do not auto-create a new one; just show the currently selected. if (!forceNew && this.ourPaymentRequests.length > 0) { const current = this.currentPaymentRequest || this.ourPaymentRequests[0]; this.showPRKData = current.encoded; return; } this.showPRKData = this.createPaymentRequest(amount, memo, mintUrl); }, createPaymentRequest: function ( amount?: number, memo?: string, mintUrl?: string ) { const nostrStore = useNostrStore(); const mintStore = useMintsStore(); const tags = [["n", "17"]]; const transport = [ { type: PaymentRequestTransportType.NOSTR, target: nostrStore.seedSignerNprofile, tags: tags, }, ] as PaymentRequestTransport[]; const uuid = uuidv4().split("-")[0]; const paymentRequest = new PaymentRequest( transport, uuid, amount, mintStore.activeUnit, mintUrl?.length ? mintStore.activeMintUrl ? [mintStore.activeMintUrl] : undefined : undefined, memo ); const encoded = paymentRequest.toEncodedRequest(); this.ensureStoredRequest(paymentRequest, encoded, memo); this.showPRKData = encoded; return encoded; }, ensureStoredRequest( request: PaymentRequest, encoded: string, memo?: string ) { const existIdx = this.ourPaymentRequests.findIndex( (r) => r.id === request.id ); const entry: OurPaymentRequest = { id: request.id, encoded, unit: request.unit, mints: request.mints, memo, createdAt: new Date().toISOString(), receivedPaymentIds: [], }; if (existIdx >= 0) { // Update encoded/memo/unit/mints in case changed this.ourPaymentRequests[existIdx] = { ...this.ourPaymentRequests[existIdx], ...entry, }; this.selectedPRIndex = existIdx; } else { this.ourPaymentRequests.push(entry); this.selectedPRIndex = this.ourPaymentRequests.length - 1; } }, selectPrevRequest() { if (!this.ourPaymentRequests.length) return; this.selectedPRIndex = (this.selectedPRIndex - 1 + this.ourPaymentRequests.length) % this.ourPaymentRequests.length; this.showPRKData = this.ourPaymentRequests[this.selectedPRIndex].encoded; }, selectNextRequest() { if (!this.ourPaymentRequests.length) return; this.selectedPRIndex = (this.selectedPRIndex + 1) % this.ourPaymentRequests.length; this.showPRKData = this.ourPaymentRequests[this.selectedPRIndex].encoded; }, selectRequestByIndex(index: number) { if (!this.ourPaymentRequests.length) return; const idx = Math.min( Math.max(0, index), this.ourPaymentRequests.length - 1 ); this.selectedPRIndex = idx; this.showPRKData = this.ourPaymentRequests[idx].encoded; }, registerIncomingPaymentForRequest( requestId: string, historyTokenId: string ) { const pr = this.ourPaymentRequests.find((r) => r.id === requestId); if (!pr) return; if (!pr.receivedPaymentIds.includes(historyTokenId)) { pr.receivedPaymentIds.push(historyTokenId); } }, getPaymentsForRequest(requestId: string) { const tokensStore = useTokensStore(); const pr = this.ourPaymentRequests.find((r) => r.id === requestId); if (!pr) return []; return pr.receivedPaymentIds .map((id) => tokensStore.historyTokens.find((t) => t.id === id)) .filter((t): t is HistoryToken => !!t); }, async decodePaymentRequest(pr: string) { console.log("decodePaymentRequest", pr); const request: PaymentRequest = decodePaymentRequest(pr); console.log("decodePaymentRequest", request); const mintsStore = useMintsStore() as any; // activate the mint in the payment request if (request.mints && request.mints.length > 0) { let foundMint = false; for (const mint of request.mints) { if (mintsStore.mints.find((m) => m.url == mint)) { // await mintsStore.activateMintUrl(mint, false, false, request.unit); mintsStore.activeMintUrl = mint; foundMint = true; break; } } if (!foundMint) { notifyError(`This payment requires using the mint: ${request.mints}`); throw new Error( `This payment requires using the mint: ${request.mints}` ); } } // activate the unit in the payment request if (request.unit) { // if the activeMint() supports this unit, set it if (mintsStore.activeMint().units.find((u) => u == request.unit)) { mintsStore.activeUnit = request.unit; } else { notifyWarning( `The mint does not support the unit in the payment request: ${request.unit}` ); } } const sendTokenStore = useSendTokensStore(); if (!sendTokenStore.showSendTokens) { // if the sendtokendialog is not currently open, clear all data and then show the send dialog sendTokenStore.clearSendData(); } // if the payment request has an amount, set it if (request.amount) { sendTokenStore.sendData.amount = request.amount / mintsStore.activeUnitCurrencyMultiplyer; } // Also make sure this decoded request gets stored (e.g., if user pasted an older one) try { const encoded = pr; this.ensureStoredRequest(request, encoded); this.showPRKData = encoded; } catch (e) { // noop } sendTokenStore.sendData.paymentRequest = request; if (!sendTokenStore.showSendTokens) { // show the send dialog sendTokenStore.showSendTokens = true; } }, async parseAndPayPaymentRequest( request: PaymentRequest, tokenStr: string ): Promise { const transports: PaymentRequestTransport[] = request.transport ?? []; for (const transport of transports) { if (transport.type == PaymentRequestTransportType.NOSTR) { return await this.payNostrPaymentRequest( request, transport, tokenStr ); } if (transport.type == PaymentRequestTransportType.POST) { return await this.payPostPaymentRequest(request, transport, tokenStr); } } throw new Error("Unsupported payment request transport."); }, async payNostrPaymentRequest( request: PaymentRequest, transport: PaymentRequestTransport, tokenStr: string ): Promise { console.log("payNostrPaymentRequest", request, tokenStr); console.log("transport", transport); const nostrStore = useNostrStore(); const decodedToken = await token.decodeFull(tokenStr); if (!decodedToken) { console.error("could not decode token"); throw new Error("Could not decode ecash token."); } const proofs = token.getProofs(decodedToken); const mint = token.getMint(decodedToken); const paymentPayload: PaymentRequestPayload = { id: request.id, mint: mint, unit: request.unit || "", proofs: proofs, }; const paymentPayloadString = JSON.stringify(paymentPayload); try { await nostrStore.sendNip17DirectMessageToNprofile( transport.target, paymentPayloadString ); } catch (error) { console.error("Error paying payment request:", error); throw error; } notifySuccess("Payment sent"); return true; }, async payPostPaymentRequest( request: PaymentRequest, transport: PaymentRequestTransport, tokenStr: string ): Promise { console.log("payPostPaymentRequest", request, tokenStr); // get the endpoint from the transport target and make an HTTP POST request with the paymentPayload as the body const decodedToken = await token.decodeFull(tokenStr); if (!decodedToken) { console.error("could not decode token"); throw new Error("Could not decode ecash token."); } const proofs = token.getProofs(decodedToken); const unit = token.getUnit(decodedToken); const mint = token.getMint(decodedToken); const paymentPayload: PaymentRequestPayload = { id: request.id, mint: mint, unit: unit, proofs: proofs, }; const paymentPayloadString = JSON.stringify(paymentPayload); try { const response = await fetch(transport.target, { headers: { "Content-Type": "application/json", }, method: "POST", body: paymentPayloadString, }); if (!response.ok) { console.error("Error paying payment request:", response.statusText); throw new Error(response.statusText); } notifySuccess("Payment sent"); } catch (error) { console.error("Error paying payment request:", error); throw error; } return true; }, }, }); ================================================ FILE: src/stores/price.ts ================================================ import { defineStore } from "pinia"; import { useSettingsStore } from "./settings"; import { useLocalStorage } from "@vueuse/core"; import { notifyApiError, notifyError, notifySuccess, notifyWarning, notify, } from "../js/notify"; import axios from "axios"; export const usePriceStore = defineStore("price", { state: () => ({ bitcoinPrice: useLocalStorage("cashu.price.bitcoinPrice", 0 as number), bitcoinPriceLastUpdated: useLocalStorage( "cashu.price.bitcoinPriceLastUpdated", 0 as number ), bitcoinPriceMinRefreshInterval: 60_000, bitcoinPrices: useLocalStorage( "cashu.price.bitcoinPrices", {} as Record ), }), actions: { fetchBitcoinPrice: async function () { const settingsStore = useSettingsStore(); if (!settingsStore.getBitcoinPrice) { this.bitcoinPrice = 0; this.bitcoinPriceLastUpdated = 0; this.bitcoinPrices = {}; console.log("Not fetching bitcoin price, disabled in settings"); return; } if ( Date.now() - this.bitcoinPriceLastUpdated < this.bitcoinPriceMinRefreshInterval ) { console.log( `Not fetching bitcoin price, last updated ${ Date.now() - this.bitcoinPriceLastUpdated }ms ago: ${this.bitcoinPrice}` ); return; } try { const { data } = await axios.get( "https://api.coinbase.com/v2/exchange-rates?currency=BTC" ); this.bitcoinPrices = data.data.rates; // Update the main bitcoinPrice to current selected currency for backward compatibility this.bitcoinPrice = data.data.rates[settingsStore.bitcoinPriceCurrency] || data.data.rates.USD; this.bitcoinPriceLastUpdated = Date.now(); } catch (error) { console.error("Failed to fetch bitcoin price:", error); notifyError("Failed to fetch bitcoin price"); } }, updateBitcoinPriceForCurrentCurrency: function () { const settingsStore = useSettingsStore(); // Update the main bitcoinPrice to reflect the current selected currency this.bitcoinPrice = this.bitcoinPrices[settingsStore.bitcoinPriceCurrency] || this.bitcoinPrice; }, }, getters: { currentCurrencyPrice(): number { const settingsStore = useSettingsStore(); return ( this.bitcoinPrices[settingsStore.bitcoinPriceCurrency] || this.bitcoinPrice ); }, }, }); ================================================ FILE: src/stores/proofs.ts ================================================ import { ref } from "vue"; import { defineStore } from "pinia"; import { useMintsStore, WalletProof } from "./mints"; import { cashuDb, CashuDexie, useDexieStore } from "./dexie"; import { Proof, getEncodedToken, getEncodedTokenV4, Token, } from "@cashu/cashu-ts"; import { liveQuery } from "dexie"; export const useProofsStore = defineStore("proofs", { state: () => { const proofs = ref([]); liveQuery(() => cashuDb.proofs.toArray()).subscribe({ next: (newProofs) => { proofs.value = newProofs; updateActiveProofs(); }, error: (err) => { console.error(err); }, }); // Function to update activeProofs const updateActiveProofs = async () => { const mintStore = useMintsStore(); const currentMint = mintStore.mints.find( (m) => m.url === mintStore.activeMintUrl ); if (!currentMint) { mintStore.activeProofs = []; return; } const unitKeysets = currentMint?.keysets?.filter( (k) => k.unit === mintStore.activeUnit ); if (!unitKeysets || unitKeysets.length === 0) { mintStore.activeProofs = []; return; } const keysetIds = unitKeysets.map((k) => k.id); const activeProofs = await cashuDb.proofs .where("id") .anyOf(keysetIds) .toArray() .then((proofs) => { return proofs.filter((p) => !p.reserved); }); mintStore.activeProofs = activeProofs; }; return { proofs, updateActiveProofs, }; }, actions: { sumProofs: function (proofs: Proof[]) { return proofs.reduce((s, t) => (s += t.amount), 0); }, getProofs: async function (): Promise { return await cashuDb.proofs.toArray(); }, setReserved: async function ( proofs: Proof[], reserved: boolean = true, quote?: string ) { const setQuote: string | undefined = reserved ? quote : undefined; await cashuDb.transaction("rw", cashuDb.proofs, async () => { for (const p of proofs) { await cashuDb.proofs .where("secret") .equals(p.secret) .modify((pr) => { pr.reserved = reserved; pr.quote = setQuote; }); } }); }, proofsToWalletProofs(proofs: Proof[], quote?: string): WalletProof[] { return proofs.map((p) => { return { ...p, reserved: false, quote: quote, } as WalletProof; }); }, async addProofs(proofs: Proof[], quote?: string) { const walletProofs = this.proofsToWalletProofs(proofs); await cashuDb.transaction("rw", cashuDb.proofs, async () => { walletProofs.forEach(async (p) => { await cashuDb.proofs.add(p); }); }); }, async removeProofs(proofs: Proof[]) { const walletProofs = this.proofsToWalletProofs(proofs); await cashuDb.transaction("rw", cashuDb.proofs, async () => { walletProofs.forEach(async (p) => { await cashuDb.proofs.delete(p.secret); }); }); }, async getProofsForQuote(quote: string): Promise { return await cashuDb.proofs.where("quote").equals(quote).toArray(); }, getUnreservedProofs: function (proofs: WalletProof[]) { return proofs.filter((p) => !p.reserved); }, serializeProofs: function (proofs: Proof[]): string { const mintStore = useMintsStore(); // unique keyset IDs of proofs const uniqueIds = [...new Set(proofs.map((p) => p.id))]; // keysets with these uniqueIds const keysets = mintStore.mints.flatMap((m) => m.keysets.filter((k) => uniqueIds.includes(k.id)) ); if (keysets.length === 0) { throw new Error("No keysets found for proofs"); } // mints that have any of the keyset.id const mints = mintStore.mints.filter((m) => m.keysets.some((k) => uniqueIds.includes(k.id)) ); if (mints.length === 0) { throw new Error("No mints found for proofs"); } // unit of keysets const unit = keysets[0].unit; const token = { mint: mints[0].url, proofs: proofs, unit: unit, } as Token; try { return getEncodedTokenV4(token); } catch (e) { console.log("Could not encode TokenV4, defaulting to TokenV3", e); return getEncodedToken(token); } // // what we put into the JSON // let mintsJson = mints.map((m) => [{ url: m.url, ids: m.keysets }][0]); // let tokenV3 = { // token: [{ proofs: proofs, mint: mintsJson[0].url }], // unit: unit, // }; // return "cashuA" + btoa(JSON.stringify(tokenV3)); }, getProofsMint: function (proofs: WalletProof[]) { const mintStore = useMintsStore(); // unique keyset IDs of proofs const uniqueIds = [...new Set(proofs.map((p) => p.id))]; // mints that have any of the keyset IDs const mints_keysets = mintStore.mints.filter((m) => m.keysets.some((k) => uniqueIds.includes(k.id)) ); // what we put into the JSON const mints = mints_keysets.map( (m) => [{ url: m.url, ids: m.keysets }][0] ); return mints[0]; }, }, }); ================================================ FILE: src/stores/receiveTokensStore.ts ================================================ import { defineStore } from "pinia"; import { StoredMint, useMintsStore } from "./mints"; import { useUiStore } from "./ui"; import { useP2PKStore } from "./p2pk"; import { useWalletStore } from "./wallet"; import token from "src/js/token"; import { useTokensStore } from "./tokens"; import { notifyError, notifySuccess, notify, notifyWarning, } from "../js/notify"; import { getDecodedTokenBinary, getEncodedToken, Token } from "@cashu/cashu-ts"; import { useSwapStore } from "./swap"; export const useReceiveTokensStore = defineStore("receiveTokensStore", { state: () => ({ showReceiveTokens: false, watchClipboardPaste: false, receiveData: { tokensBase64: "", p2pkPrivateKey: "", }, scanningCard: false, }), actions: { decodeToken: function (encodedToken: string) { let decodedToken = undefined; try { decodedToken = token.decode(encodedToken); } catch (error) {} return decodedToken; }, knowThisMintOfTokenJson: function (tokenJson: Token) { const mintStore = useMintsStore(); const uniqueIds = [ ...new Set(token.getProofs(tokenJson).map((p) => p.id)), ]; return mintStore.mints .map((m) => m.url) .includes(token.getMint(tokenJson)); }, receiveToken: async function (encodedToken: string) { const mintStore = useMintsStore(); const walletStore = useWalletStore(); const receiveStore = useReceiveTokensStore(); const uiStore = useUiStore(); console.log("### receive tokens", receiveStore.receiveData.tokensBase64); if (receiveStore.receiveData.tokensBase64.length == 0) { throw new Error("no tokens provided."); } // get the private key for the token we want to receive if it is locked with P2PK receiveStore.receiveData.p2pkPrivateKey = await useP2PKStore().getPrivateKeyForP2PKEncodedToken( receiveStore.receiveData.tokensBase64 ); const tokenJson = await token.decodeFull( receiveStore.receiveData.tokensBase64 ); if (tokenJson == undefined) { throw new Error("no tokens provided."); } // check if we have all mints if (!this.knowThisMintOfTokenJson(tokenJson)) { // add the mint await mintStore.addMint({ url: token.getMint(tokenJson) }); } // redeem the token await walletStore.redeem(); receiveStore.showReceiveTokens = false; uiStore.closeDialogs(); }, receiveIfDecodes: async function () { try { const decodedToken = this.decodeToken(this.receiveData.tokensBase64); if (decodedToken) { await this.receiveToken(this.receiveData.tokensBase64); return true; } } catch (error) { console.error(error); return false; } }, meltTokenToMint: async function (encodedToken: string, mint: StoredMint) { const receiveStore = useReceiveTokensStore(); const mintStore = useMintsStore(); const uiStore = useUiStore(); const tokenJson = await token.decodeFull(encodedToken); if (tokenJson == undefined) { throw new Error("no tokens provided."); } // check if we have all mints if (!this.knowThisMintOfTokenJson(tokenJson)) { // add the mint await mintStore.addMint({ url: token.getMint(tokenJson) }); } await useSwapStore().meltProofsToMint(tokenJson, mint); receiveStore.showReceiveTokens = false; uiStore.closeDialogs(); }, pasteToParseDialog: async function (verbose = false) { const text = await useUiStore().pasteFromClipboard(); if (this.decodeToken(text)) { const tokensStore = useTokensStore(); const historyToken = tokensStore.tokenAlreadyInHistory(text); if ( historyToken && (historyToken.amount > 0 || historyToken.status === "paid") ) { if (verbose) notify("Token already in history."); return false; } this.receiveData.tokensBase64 = text; return true; } else { // notifyWarning("Invalid token"); return false; } }, toggleScanner: function () { const receiveStore = useReceiveTokensStore(); const tokenStore = useTokensStore(); const uiStore = useUiStore(); if (this.scanningCard === false) { try { this.ndef = new window.NDEFReader(); this.controller = new AbortController(); const signal = this.controller.signal; this.ndef .scan({ signal }) .then(() => { console.log("> Scan started"); this.ndef.addEventListener("readingerror", () => { console.error("Cannot read data from the NFC tag."); notifyError("Cannot read data from the NFC tag."); this.controller.abort(); this.scanningCard = false; }); this.ndef.addEventListener( "reading", ({ message, serialNumber }) => { try { const record = message.records[0]; const recordType = record.recordType; let tokenStr = ""; switch (recordType) { case "text": { const text = new TextDecoder().decode(record.data); if (!text.startsWith("cashu")) { throw new Error( "text does not contain a cashu token" ); } tokenStr = text; break; } case "url": { const url = new TextDecoder().decode(record.data); const i = url.indexOf("#token=cashu"); if (i === -1) { throw new Error("URL does not contain a cashu token"); } tokenStr = url.substring(i + 7); break; } case "mime": { if (record.mediaType !== "application/octet-stream") { throw new Error("binary data expected"); } const data = new Uint8Array(record.data.buffer); const prefix = String.fromCharCode(...data.slice(0, 4)); if (prefix !== "craw") { throw new Error( "binary data does not contain a cashu token" ); } const token = getDecodedTokenBinary(data); tokenStr = getEncodedToken(token); break; } default: throw new Error(`unsupported recordType ${recordType}`); } const historyToken = tokenStore.tokenAlreadyInHistory(tokenStr); if (!historyToken || historyToken.status === "pending") { receiveStore.receiveData.tokensBase64 = tokenStr; receiveStore.showReceiveTokens = true; uiStore.closeDialogs(); } else { notify("Token already in history."); } } catch (err) { console.error(`Something went wrong! ${err}`); notifyError(`Something went wrong! ${err}`); } this.controller.abort(); this.scanningCard = false; } ); this.scanningCard = true; }) .catch((error) => { console.error(`Scan error: ${error.message}`); notifyError(`Scan error: ${error.message}`); }); } catch (error) { console.error(`NFC error: ${error.message}`); notifyError(`NFC error: ${error.message}`); } } else { this.controller.abort(); this.scanningCard = false; } }, }, }); ================================================ FILE: src/stores/restore.ts ================================================ import { defineStore } from "pinia"; import { useLocalStorage } from "@vueuse/core"; import { generateSecretKey, getPublicKey } from "nostr-tools"; import { bytesToHex } from "@noble/hashes/utils"; // already an installed dependency import { useWalletStore } from "./wallet"; import { Mint, Wallet, CheckStateEnum, Proof } from "@cashu/cashu-ts"; import { useMintsStore } from "./mints"; import { notify, notifyError, notifySuccess } from "src/js/notify"; import { useUiStore } from "./ui"; import { useProofsStore } from "./proofs"; import { i18n } from "../boot/i18n"; const BATCH_SIZE = 200; const MAX_GAP = 2; export const useRestoreStore = defineStore("restore", { state: () => ({ showRestoreDialog: useLocalStorage( "cashu.restore.showRestoreDialog", false ), restoringState: false, restoringMint: "", mnemonicToRestore: useLocalStorage( "cashu.restore.mnemonicToRestore", "" ), restoreProgress: 0, restoreCounter: 0, restoreStatus: "", }), getters: {}, actions: { restoreMint: async function (url: string) { this.restoringState = true; this.restoringMint = url; this.restoreProgress = 0; this.restoreCounter = 0; this.restoreStatus = ""; try { await this._restoreMint(url); } catch (error) { notifyError( i18n.global.t("restore.restore_mint_error_text", { error }) ); } finally { this.restoringState = false; this.restoringMint = ""; this.restoreProgress = 0; } }, _restoreMint: async function (url: string) { if (this.mnemonicToRestore.length === 0) { notifyError(i18n.global.t("restore.mnemonic_error_text")); return; } this.restoreProgress = 0; const walletStore = useWalletStore(); const proofsStore = useProofsStore(); const mintStore = useMintsStore(); await mintStore.activateMintUrl(url); const mnemonic = this.mnemonicToRestore; this.restoreStatus = i18n.global.t("restore.prepare_info_text"); const mint = new Mint(url); const keysets = (await mint.getKeySets()).keysets; let restoredSomething = false; // Calculate total steps for progress calculation let totalSteps = keysets.length * MAX_GAP; let currentStep = 0; for (const keyset of keysets) { console.log(`Restoring keyset ${keyset.id} with unit ${keyset.unit}`); const bip39Seed = walletStore.mnemonicToSeedSync(mnemonic); const wallet = new Wallet(mint, { bip39seed: bip39Seed, unit: keyset.unit, }); await wallet.loadMint(); let start = 0; let emptyBatchCount = 0; let restoreProofs: Proof[] = []; while (emptyBatchCount < MAX_GAP) { console.log(`Restoring proofs ${start} to ${start + BATCH_SIZE}`); let proofs: Proof[] = []; try { proofs = ( await wallet.restore(start, BATCH_SIZE, { keysetId: keyset.id }) ).proofs; } catch (error) { console.error(`Error restoring proofs: ${error}`); proofs = []; } if (proofs.length === 0) { console.log(`No proofs found for keyset ${keyset.id}`); emptyBatchCount++; } else { console.log( `> Restored ${proofs.length} proofs with sum ${proofs.reduce( (s, p) => s + p.amount, 0 )}` ); restoreProofs = restoreProofs.concat(proofs); emptyBatchCount = 0; this.restoreCounter += proofs.length; totalSteps += 1; } this.restoreStatus = i18n.global.t( "restore.restored_proofs_for_keyset_info_text", { restoreCounter: this.restoreCounter, keysetId: keyset.id, } ); start += BATCH_SIZE; currentStep++; this.restoreProgress = currentStep / totalSteps; } let restoredProofs: Proof[] = []; for (let i = 0; i < restoreProofs.length; i += BATCH_SIZE) { this.restoreStatus = i18n.global.t( "restore.checking_proofs_for_keyset_info_text", { startIndex: i, endIndex: i + BATCH_SIZE, keysetId: keyset.id, } ); const checkRestoreProofs = restoreProofs.slice(i, i + BATCH_SIZE); const proofStates = await wallet.checkProofsStates( checkRestoreProofs ); const spentProofs = checkRestoreProofs.filter( (p, i) => proofStates[i].state === CheckStateEnum.SPENT ); const spentProofsSecrets = spentProofs.map((p) => p.secret); const unspentProofs = checkRestoreProofs.filter( (p) => !spentProofsSecrets.includes(p.secret) ); if (unspentProofs.length > 0) { console.log( `Found ${ unspentProofs.length } unspent proofs with sum ${unspentProofs.reduce( (s, p) => s + p.amount, 0 )}` ); } const newProofs = unspentProofs.filter( (p) => !proofsStore.proofs.some((pr) => pr.secret === p.secret) ); await useProofsStore().addProofs(newProofs); restoredProofs = restoredProofs.concat(newProofs); currentStep++; this.restoreProgress = currentStep / totalSteps; } const restoredAmount = restoredProofs.reduce((s, p) => s + p.amount, 0); const restoredAmountStr = useUiStore().formatCurrency( restoredAmount, keyset.unit ); if (restoredAmount > 0) { notifySuccess( i18n.global.t("restore.restored_amount_success_text", { amount: restoredAmountStr, }) ); restoredSomething = true; } } if (!restoredSomething) { notify(i18n.global.t("restore.no_proofs_info_text")); } }, }, }); ================================================ FILE: src/stores/sendTokensStore.ts ================================================ import { defineStore } from "pinia"; import { decodePaymentRequest, PaymentRequest } from "@cashu/cashu-ts"; import { HistoryToken } from "./tokens"; export const useSendTokensStore = defineStore("sendTokensStore", { state: () => ({ showSendTokens: false, showLockInput: false, sendData: { amount: null, historyAmount: null, memo: "", tokens: "", tokensBase64: "", p2pkPubkey: "", paymentRequest: undefined, historyToken: undefined, } as { amount: number | null; historyAmount: number | null; memo: string; tokens: string; tokensBase64: string; p2pkPubkey: string; paymentRequest?: PaymentRequest; historyToken: HistoryToken | undefined; }, }), actions: { clearSendData() { this.sendData.amount = null; this.sendData.historyAmount = null; this.sendData.memo = ""; this.sendData.tokens = ""; this.sendData.tokensBase64 = ""; this.sendData.p2pkPubkey = ""; this.sendData.paymentRequest = undefined; this.sendData.historyToken = undefined; }, }, }); ================================================ FILE: src/stores/settings.ts ================================================ import { defineStore } from "pinia"; import { useLocalStorage } from "@vueuse/core"; const defaultNostrRelays = [ "wss://relay.damus.io", "wss://relay.8333.space/", "wss://nos.lol", "wss://relay.primal.net", ]; export const useSettingsStore = defineStore("settings", { state: () => { return { getBitcoinPrice: useLocalStorage( "cashu.settings.getBitcoinPrice", true ), bitcoinPriceCurrency: useLocalStorage( "cashu.settings.bitcoinPriceCurrency", "USD" ), checkSentTokens: useLocalStorage( "cashu.settings.checkSentTokens", true ), checkIncomingInvoices: useLocalStorage( "cashu.settings.checkIncomingInvoices", true ), periodicallyCheckIncomingInvoices: useLocalStorage( "cashu.settings.periodicallyCheckIncomingInvoices", true ), checkInvoicesOnStartup: useLocalStorage( "cashu.settings.checkInvoicesOnStartup", true ), useWebsockets: useLocalStorage( "cashu.settings.useWebsockets", true ), defaultNostrRelays: useLocalStorage( "cashu.settings.defaultNostrRelays", defaultNostrRelays ), includeFeesInSendAmount: useLocalStorage( "cashu.settings.includeFeesInSendAmount", false ), nfcEncoding: useLocalStorage( "cashu.settings.nfcEncoding", "weburl" ), useNumericKeyboard: useLocalStorage( "cashu.settings.useNumericKeyboard", false ), enableReceiveSwaps: useLocalStorage( "cashu.settings.enableReceiveSwaps", true ), showNfcButtonInDrawer: useLocalStorage( "cashu.ui.showNfcButtonInDrawer", true ), autoPasteEcashReceive: useLocalStorage( "cashu.settings.autoPasteEcashReceive", true ), auditorEnabled: useLocalStorage( "cashu.settings.auditorEnabled", true ), auditorUrl: useLocalStorage( "cashu.settings.auditorUrl", "https://audit.8333.space" ), auditorApiUrl: useLocalStorage( "cashu.settings.auditorApiUrl", "https://api.audit.8333.space" ), bip177BitcoinSymbol: useLocalStorage( "cashu.settings.bip177", true ), multinutEnabled: useLocalStorage( "cashu.settings.multinutEnabled", false ), nostrMintBackupEnabled: useLocalStorage( "cashu.settings.nostrMintBackupEnabled", true ), }; }, }); ================================================ FILE: src/stores/storage.ts ================================================ import { defineStore } from "pinia"; import { useWalletStore } from "./wallet"; import { useMintsStore } from "./mints"; import { useLocalStorage } from "@vueuse/core"; import { notifyError, notifySuccess } from "../js/notify"; import { useTokensStore } from "./tokens"; import { currentDateStr } from "src/js/utils"; import { useProofsStore } from "./proofs"; export const useStorageStore = defineStore("storage", { state: () => ({ lastLocalStorageCleanUp: useLocalStorage( "cashu.lastLocalStorageCleanUp", new Date() ), }), actions: { restoreFromBackup: async function (backup: any) { const proofsStore = useProofsStore(); if (!backup) { notifyError("Unrecognized Backup Format!"); } else { const keys = Object.keys(backup); for (const key of keys) { // we treat some keys differently *magic* if (key === "cashu.dexie.db.proofs") { const proofs = JSON.parse(backup[key]); await proofsStore.addProofs(proofs); } else { localStorage.setItem(key, backup[key]); } } notifySuccess("Backup restored"); window.location.reload(); } }, exportWalletState: async function () { const jsonToSave: any = {}; for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (!k) { continue; } const v = localStorage.getItem(k); jsonToSave[k] = v; } // proofs table *magic* const proofs = await useProofsStore().getProofs(); jsonToSave["cashu.dexie.db.proofs"] = JSON.stringify(proofs); const textToSave = JSON.stringify(jsonToSave); const textToSaveAsBlob = new Blob([textToSave], { type: "text/plain", }); const textToSaveAsURL = window.URL.createObjectURL(textToSaveAsBlob); const fileName = `cashu_me_backup_${currentDateStr()}.json`; const downloadLink = document.createElement("a"); downloadLink.download = fileName; downloadLink.innerHTML = "Download File"; downloadLink.href = textToSaveAsURL; downloadLink.onclick = function () { document.body.removeChild(event.target); }; downloadLink.style.display = "none"; document.body.appendChild(downloadLink); downloadLink.click(); notifySuccess("Wallet backup exported"); }, checkLocalStorage: async function () { const needsCleanup = this.checkLocalStorageQuota(); if (needsCleanup) { this.cleanUpLocalStorage(true); } else { this.cleanUpLocalStorageScheduler(); } }, checkLocalStorageQuota: function (): boolean { // determine if the user might have exceeded the local storage quota // store 10kb of data in local storage to check if it fails const localStorageSize = JSON.stringify(localStorage).length; console.log(`Local storage size: ${localStorageSize} bytes`); const data = new Array(10240).join("x"); try { localStorage.setItem("cashu.test", data); localStorage.removeItem("cashu.test"); return false; } catch (e) { console.log("Local storage quota exceeded"); notifyError( "Local storage quota exceeded. Clean up your local storage." ); return true; } }, cleanUpLocalStorageScheduler: function () { const cleanUpInterval = 1000 * 60 * 60 * 24 * 7; // 7 day const lastCleanUp = this.lastLocalStorageCleanUp; if ( !lastCleanUp || isNaN(new Date(lastCleanUp).getTime()) || new Date().getTime() - new Date(lastCleanUp).getTime() > cleanUpInterval ) { console.log(`Last clean up: ${lastCleanUp}, cleaning up local storage`); this.cleanUpLocalStorage(); } }, cleanUpLocalStorage: function (verbose = false) { const walletStore = useWalletStore(); const tokenStore = useTokensStore(); const localStorageSizeBefore = JSON.stringify(localStorage).length; // delete cashu.spentProofs from local storage localStorage.removeItem("cashu.spentProofs"); // from all paid invoices in this.invoiceHistory, delete the oldest so that only max 100 remain const max_history = 200; const paidInvoices = walletStore.invoiceHistory.filter( (i) => i.status == "paid" ); if (paidInvoices.length > max_history) { const sortedInvoices = paidInvoices.sort((a, b) => { return new Date(a.date).getTime() - new Date(b.date).getTime(); }); const deleteInvoices = sortedInvoices.slice( 0, sortedInvoices.length - max_history ); walletStore.invoiceHistory = walletStore.invoiceHistory.filter( (i) => !deleteInvoices.includes(i) ); } // walk through the oldest paid tokenStore.historyTokens and delete the token const paidTokens = tokenStore.historyTokens.filter( (t) => t.status == "paid" ); if (paidTokens.length > max_history) { const sortedTokens = paidTokens.sort((a, b) => { return new Date(a.date).getTime() - new Date(b.date).getTime(); }); const deleteTokens = sortedTokens.slice( 0, sortedTokens.length - max_history ); for (let i = 0; i < deleteTokens.length; i++) { deleteTokens[i].token = undefined; } } const localStorageSizeAfter = JSON.stringify(localStorage).length; const localStorageSizeDiff = localStorageSizeBefore - localStorageSizeAfter; console.log(`Cleaned up ${localStorageSizeDiff} bytes of local storage`); if (localStorageSizeDiff > 0 && verbose) { notifySuccess(`Cleaned up ${localStorageSizeDiff} bytes`); } this.lastLocalStorageCleanUp = new Date(); }, }, getters: { canPasteFromClipboard() { return ( window.isSecureContext && navigator.clipboard && navigator.clipboard.readText ); }, }, }); ================================================ FILE: src/stores/store-flag.d.ts ================================================ /* eslint-disable */ // THIS FEATURE-FLAG FILE IS AUTOGENERATED, // REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING import "quasar/dist/types/feature-flag"; declare module "quasar/dist/types/feature-flag" { interface QuasarFeatureFlags { store: true; } } ================================================ FILE: src/stores/swap.ts ================================================ import { defineStore } from "pinia"; import { PaymentRequest, Token, MeltQuoteBolt11Response, } from "@cashu/cashu-ts"; import { StoredMint, useMintsStore } from "./mints"; import { useWalletStore } from "./wallet"; import { useProofsStore } from "./proofs"; import { notifyError, notifyWarning } from "../js/notify"; import token from "src/js/token"; import { i18n } from "../boot/i18n"; /** * The tokens store handles everything related to tokens and proofs */ type SwapAmountData = { fromUrl: string | undefined; toUrl: string | undefined; amount: number; }; export type HistoryToken = { status: "paid" | "pending"; amount: number; date: string; token?: string; mint: string; unit: string; paymentRequest?: PaymentRequest; fee?: number; meltQuote?: MeltQuoteBolt11Response; paidDate?: string; }; export const useSwapStore = defineStore("swap", { state: () => ({ swapAmountData: {} as SwapAmountData, swapBlocking: false, }), actions: { // mintAmountSwap: async function (swapAmountData: SwapAmountData) { const walletStore = useWalletStore(); const mintStore = useMintsStore(); if (this.swapBlocking) { notifyWarning(i18n.global.t("swap.in_progress_warning_text")); return; } if (!swapAmountData.fromUrl || !swapAmountData.toUrl) { notifyError(i18n.global.t("swap.invalid_swap_data_error_text")); return; } this.swapBlocking = true; try { // get invoice // await mintStore.activateMintUrl(swapAmountData.toUrl); const toWallet = await walletStore.mintWallet( swapAmountData.toUrl, mintStore.activeUnit, true ); const mintQuote = await walletStore.requestMint( swapAmountData.amount, toWallet ); // pay invoice const fromWallet = await walletStore.mintWallet( swapAmountData.fromUrl, mintStore.activeUnit, true ); const meltQuote = await walletStore.meltQuote( fromWallet, mintQuote.request ); const mint = mintStore.mints.find( (m) => m.url === swapAmountData.fromUrl ); if (!mint) { throw new Error("mint not found"); } const mintProofs = mintStore.mintUnitProofs(mint, fromWallet.unit); await walletStore.melt(mintProofs, meltQuote, fromWallet); // settle invoice on other side await walletStore.checkInvoice(mintQuote.quote); } catch (e) { console.error("Error swapping", e); notifyError(i18n.global.t("swap.swap_error_text")); } finally { this.swapBlocking = false; } }, meltToMintFees: async function (tokenJson: Token) { const proofsStore = useProofsStore(); const walletStore = useWalletStore(); const fromMintUrl = token.getMint(tokenJson); const unit = token.getUnit(tokenJson); const tokenAmount = proofsStore.sumProofs(token.getProofs(tokenJson)); let meltAmount = tokenAmount - Math.max(2, Math.ceil(tokenAmount * 0.02)); try { // walletStore.mintWallet(fromMintUrl, unit); will fail if we don't have fromMintUrl yet const fromWallet = await walletStore.mintWallet(fromMintUrl, unit); const proofs = token.getProofs(tokenJson); meltAmount -= fromWallet.getFeesForProofs(proofs); } catch (e) {} return tokenAmount - meltAmount; }, meltProofsToMint: async function (tokenJson: Token, mint: StoredMint) { const proofsStore = useProofsStore(); const walletStore = useWalletStore(); if (this.swapBlocking) { notifyWarning(i18n.global.t("swap.in_progress_warning_text")); return; } this.swapBlocking = true; try { const tokenAmount = proofsStore.sumProofs(token.getProofs(tokenJson)); let meltAmount = tokenAmount - Math.max(2, Math.ceil(tokenAmount * 0.02)); const unit = token.getUnit(tokenJson); const fromMintUrl = token.getMint(tokenJson); const fromWallet = await walletStore.mintWallet( fromMintUrl, unit, true ); const toWallet = await walletStore.mintWallet(mint.url, unit, true); const proofs = token.getProofs(tokenJson); meltAmount -= fromWallet.getFeesForProofs(proofs); const mintQuote = await walletStore.requestMint(meltAmount, toWallet); const meltQuote = await walletStore.meltQuote( fromWallet, mintQuote.request ); await walletStore.melt(proofs, meltQuote, fromWallet); await walletStore.checkInvoice(mintQuote.quote); } catch (e) { console.error("Error swapping", e); notifyError(i18n.global.t("swap.swap_error_text")); } finally { this.swapBlocking = false; } }, }, }); ================================================ FILE: src/stores/tokens.ts ================================================ import { useLocalStorage } from "@vueuse/core"; import { date } from "quasar"; import { defineStore } from "pinia"; import { PaymentRequest, Proof, Token, MeltQuoteBolt11Response, } from "@cashu/cashu-ts"; import token from "src/js/token"; import { v4 as uuidv4 } from "uuid"; /** * The tokens store handles everything related to tokens and proofs */ export type HistoryToken = { id: string; status: "paid" | "pending"; amount: number; date: string; token: string; mint: string; unit: string; paymentRequest?: PaymentRequest; fee?: number; label?: string; // Add label field for custom naming meltQuote?: MeltQuoteBolt11Response; paidDate?: string; paymentRequestId?: string; // If created in response to a payment request }; export const useTokensStore = defineStore("tokens", { state: () => ({ historyTokens: useLocalStorage("cashu.historyTokens", [] as HistoryToken[]), }), actions: { /** * @param {{amount: number, token: string, mint: string, unit: string}} param0 */ addPaidToken({ amount, token, mint, unit, fee, paymentRequest, label, paymentRequestId, }: { amount: number; token: string; mint: string; unit: string; fee?: number; paymentRequest?: PaymentRequest; label?: string; paymentRequestId?: string; }): string { const id = uuidv4(); this.historyTokens.push({ id, status: "paid", amount, date: currentDateStr(), token, mint, unit, fee, paymentRequest, label, paymentRequestId, } as HistoryToken); return id; }, addPendingToken({ amount, token, mint, unit, fee, paymentRequest, label, paymentRequestId, }: { amount: number; token: string; mint: string; unit: string; fee?: number; paymentRequest?: PaymentRequest; label?: string; paymentRequestId?: string; }): string { const id = uuidv4(); this.historyTokens.push({ id, status: "pending", amount, date: currentDateStr(), token: token, mint, unit, fee, paymentRequest, label, paymentRequestId, }); return id; }, editHistoryToken( tokenToEdit: string, options?: { newAmount?: number; addAmount?: number; newStatus?: "paid" | "pending"; newToken?: string; newFee?: number; } ): HistoryToken | undefined { const index = this.historyTokens.findIndex( (t) => t.token === tokenToEdit ); if (index >= 0) { if (options) { if (options.newToken) { this.historyTokens[index].token = options.newToken; } if (options.newAmount) { this.historyTokens[index].amount = options.newAmount * Math.sign(this.historyTokens[index].amount); } if (options.addAmount) { if (this.historyTokens[index].amount > 0) { this.historyTokens[index].amount += options.addAmount; } else { this.historyTokens[index].amount -= options.addAmount; } } if (options.newStatus) { this.historyTokens[index].status = options.newStatus; } if (options.newFee) { this.historyTokens[index].fee = options.newFee; } } return this.historyTokens[index]; } return undefined; }, setTokenPaid(token: string) { const index = this.historyTokens.findIndex( (t) => t.token === token && t.status == "pending" ); if (index >= 0) { this.historyTokens[index].status = "paid"; } }, deleteToken(token: string) { const index = this.historyTokens.findIndex((t) => t.token === token); if (index >= 0) { this.historyTokens.splice(index, 1); } }, tokenAlreadyInHistory(tokenStr: string): HistoryToken | undefined { return this.historyTokens.find((t) => t.token === tokenStr); }, }, }); function currentDateStr() { return date.formatDate(new Date(), "YYYY-MM-DD HH:mm:ss"); } ================================================ FILE: src/stores/ui.ts ================================================ import { defineStore } from "pinia"; import { useMintsStore } from "./mints"; import { useLocalStorage } from "@vueuse/core"; import { notifyApiError, notifyError, notifySuccess, notifyWarning, notify, } from "../js/notify"; import { Clipboard } from "@capacitor/clipboard"; import { Haptics, ImpactStyle } from "@capacitor/haptics"; import ts from "typescript"; import { useSettingsStore } from "./settings"; const unitTickerShortMap = { sat: "sats", usd: "USD", eur: "EUR", msat: "msats", }; export const useUiStore = defineStore("ui", { state: () => ({ hideBalance: useLocalStorage("cashu.ui.hideBalance", false), tickerLong: "Satoshis", showInvoiceDetails: false, showCreateInvoiceDialog: false, showSendDialog: false, showReceiveDialog: false, showReceiveEcashDrawer: false, showNumericKeyboard: false, activityOrb: false, tab: useLocalStorage("cashu.ui.tab", "history" as string), expandHistory: useLocalStorage("cashu.ui.expandHistory", true as boolean), globalMutexLock: false, showDebugConsole: useLocalStorage("cashu.ui.showDebugConsole", false), lastBalanceCached: useLocalStorage("cashu.ui.lastBalanceCached", 0), multinutExperimentalWarningDismissed: useLocalStorage( "cashu.ui.multinutExperimentalWarningDismissed", false ), }), actions: { closeDialogs() { this.showInvoiceDetails = false; this.showCreateInvoiceDialog = false; this.showSendDialog = false; this.showReceiveDialog = false; this.showReceiveEcashDrawer = false; }, async lockMutex() { const nRetries = 10; const retryInterval = 500; let retries = 0; while (this.globalMutexLock) { if (retries >= nRetries) { notify("Please try again."); throw new Error("Failed to acquire global mutex lock"); } retries++; await new Promise((resolve) => setTimeout(resolve, retryInterval)); } this.globalMutexLock = true; }, unlockMutex() { this.globalMutexLock = false; }, triggerActivityOrb() { this.activityOrb = true; }, setTab(tab: string) { this.tab = tab; }, formatSat: function (value: number) { // convert value to integer if (useSettingsStore().bip177BitcoinSymbol) { if (value >= 0) { return "₿" + new Intl.NumberFormat(navigator.language).format(value); } else { return ( "-₿" + new Intl.NumberFormat(navigator.language).format(Math.abs(value)) ); } } return new Intl.NumberFormat(navigator.language).format(value) + " sat"; }, fromMsat: function (value: number) { return new Intl.NumberFormat(navigator.language).format(value) + " msat"; }, formatCurrency: function ( value: number, currency: string, showBalance = false ) { if (currency == undefined) { currency = "sat"; } if (useUiStore().hideBalance && !showBalance) { return "****"; } if (currency == "sat") return this.formatSat(value); if (currency == "msat") return this.fromMsat(value); if (currency == "usd") value = value / 100; if (currency == "eur") value = value / 100; return new Intl.NumberFormat(navigator.language, { style: "currency", currency: currency, }).format(value); // + " " + // currency.toUpperCase() }, toggleDebugConsole() { this.showDebugConsole = !this.showDebugConsole; if (this.showDebugConsole) { this.enableDebugConsole(); } else { this.disableDebugConsole(); } }, enableDebugConsole() { if (!this.showDebugConsole) { return; } // enable debug terminal const script = document.createElement("script"); script.src = "//cdn.jsdelivr.net/npm/eruda"; document.body.appendChild(script); script.onload = function () { // @ts-ignore eruda.init(); }; }, disableDebugConsole() { // @ts-ignore document.querySelector("#eruda").remove(); }, pasteFromClipboard: async function () { let text = ""; // @ts-ignore if (window?.Capacitor) { const { value } = await Clipboard.read(); text = value; } else { text = await navigator.clipboard.readText(); } return text; }, vibrate: async function () { // @ts-ignore if (window.Capacitor) { // Haptics.impact({ style: ImpactStyle.Light }); Haptics.vibrate({ duration: 200 }); } else { navigator.vibrate(200); } }, }, getters: { tickerShort() { const unit = useMintsStore().activeUnit; if (unit == "sat" && useSettingsStore().bip177BitcoinSymbol) { return "₿"; } else { return unitTickerShortMap[unit as keyof typeof unitTickerShortMap]; } }, ndefSupported(): boolean { //console.log(`window.Capacitor.getPlatform() = ${window.Capacitor.getPlatform()}`) // @ts-ignore if (window.Capacitor.getPlatform() !== "web") { return false; } return "NDEFReader" in globalThis; }, canPasteFromClipboard() { return ( window.isSecureContext && navigator.clipboard && navigator.clipboard.readText ); }, webShareSupported(): boolean { return "share" in navigator; }, }, }); ================================================ FILE: src/stores/wallet.ts ================================================ import { defineStore } from "pinia"; import { currentDateStr } from "src/js/utils"; import { useMintsStore, WalletProof, MintClass } from "./mints"; import { useLocalStorage } from "@vueuse/core"; import { useProofsStore } from "./proofs"; import { HistoryToken, useTokensStore } from "./tokens"; import { useReceiveTokensStore } from "./receiveTokensStore"; import { useUiStore } from "src/stores/ui"; import { useP2PKStore } from "src/stores/p2pk"; import { useSendTokensStore } from "src/stores/sendTokensStore"; import { usePRStore } from "./payment-request"; import { useWorkersStore } from "./workers"; import { useInvoicesWorkerStore } from "./invoicesWorker"; import * as nobleSecp256k1 from "@noble/secp256k1"; import { bytesToHex } from "@noble/hashes/utils"; import _ from "underscore"; import token from "src/js/token"; import { notifyApiError, notifyError, notifySuccess, notifyWarning, notify, } from "src/js/notify"; import { Wallet, Proof, MintQuoteBolt11Request, MeltQuoteBolt11Request, MintQuoteBolt11Response, MeltQuoteBolt11Response, CheckStateEnum, MeltQuoteState, MintQuoteState, ProofState, KeyChain, // ConsoleLogger, } from "@cashu/cashu-ts"; // @ts-ignore import * as bolt11Decoder from "light-bolt11-decoder"; import { bech32 } from "bech32"; import axios from "axios"; import { date } from "quasar"; // bip39 requires Buffer // import { Buffer } from 'buffer'; // window.Buffer = Buffer; import { generateMnemonic, mnemonicToSeedSync } from "@scure/bip39"; import { wordlist } from "@scure/bip39/wordlists/english"; import { useSettingsStore } from "./settings"; import { usePriceStore } from "./price"; import { useI18n } from "vue-i18n"; import { isLegacyRetailQR, translateLegacyQRToLightningAddress, } from "src/js/legacy-qr"; // HACK: this is a workaround so that the catch block in the melt function does not throw an error when the user exits the app // before the payment is completed. This is necessary because the catch block in the melt function would otherwise remove all // quotes from the invoiceHistory and the user would not be able to pay the invoice again after reopening the app. let isUnloading = false; window.addEventListener("beforeunload", () => { isUnloading = true; }); type Invoice = { amount: number; bolt11: string; quote: string; memo: string; }; export type InvoiceHistory = Invoice & { date: string; status: "pending" | "paid"; mint: string; unit: string; mintQuote?: MintQuoteBolt11Response; meltQuote?: MeltQuoteBolt11Response; label?: string; // Add label field for custom naming privKey?: string; // Private key, if the quote is locked paidDate?: string; }; type KeysetCounter = { id: string; counter: number; }; const receiveStore = useReceiveTokensStore(); const tokenStore = useTokensStore(); const proofsStore = useProofsStore(); export const useWalletStore = defineStore("wallet", { state: () => { const { t } = useI18n(); return { t: t, mnemonic: useLocalStorage("cashu.mnemonic", ""), invoiceHistory: useLocalStorage( "cashu.invoiceHistory", [] as InvoiceHistory[] ), keysetCounters: useLocalStorage( "cashu.keysetCounters", [] as KeysetCounter[] ), oldMnemonicCounters: useLocalStorage( "cashu.oldMnemonicCounters", [] as { mnemonic: string; keysetCounters: KeysetCounter[] }[] ), invoiceData: {} as InvoiceHistory, activeWebsocketConnections: 0, payInvoiceData: { blocking: false, bolt11: "", show: false, fee_paid: 0, meltQuote: { payload: { unit: "", request: "", } as MeltQuoteBolt11Request, response: { quote: "", amount: 0, fee_reserve: 0, } as MeltQuoteBolt11Response, error: "", }, invoice: { sat: 0, memo: "", bolt11: "", } as { sat: number; memo: string; bolt11: string } | null, lnurlpay: { domain: "", callback: "", minSendable: 0, maxSendable: 0, metadata: {}, successAction: {}, routes: [], tag: "", lightningAddress: "", }, lnurlauth: {}, input: { request: "", amount: undefined, comment: "", quote: "", } as { request: string; amount: number | undefined; comment: string; quote: string; }, }, }; }, getters: { seed(): Uint8Array { return mnemonicToSeedSync(this.mnemonic); }, }, actions: { setMnemonicFromUser: function (mnemonic: string) { this.mnemonic = mnemonic.trim().toLowerCase(); // normalize }, /** * Returns a fully initialised Wallet for the active mint. * Calls loadMint internally, so is safe for all wallet operations. */ async activeWallet(updateKeysets: boolean = false): Promise { const mints = useMintsStore() as any; return this.mintWallet( mints.activeMintUrl, mints.activeUnit, updateKeysets ); }, async mintWallet( url: string, unit: string, updateKeysets: boolean = false ): Promise { // short-lived wallet for mint operations // note: the unit of the wallet will be activeUnit by default, // overwrite wallet.unit if needed const mints = useMintsStore() as any; let storedMint = mints.mints.find((m: any) => m.url === url); if (!storedMint) { throw new Error("mint not found"); } // if updateKeysets is true and keysetsLastFetched is older than 1 hour, fetch the keysets for the mint const ONE_HOUR = 60 * 60 * 1000; const lastUpdated = storedMint.lastKeysetsUpdated ? new Date(storedMint.lastKeysetsUpdated).getTime() : 0; const mintNeedsUpdate = updateKeysets && lastUpdated < Date.now() - ONE_HOUR; if (mintNeedsUpdate) { console.log("updating mint info and keys for mint", storedMint.url); try { await mints.updateMintInfoAndKeys(storedMint); // Re-fetch mint after update to get fresh keysets storedMint = mints.mints.find((m: any) => m.url === url); } catch (error: any) { console.error("Failed to update mint info/keys:", error); // Continue with potentially stale keysets rather than failing } } return this.createWalletInstance(storedMint, url, unit, mints); }, // Synchronous wallet creation for non-critical operations (e.g., fee calculation display) // Use mintWallet() with updateKeysets=true for critical operations mintWalletSync(url: string, unit: string): Wallet { const mints = useMintsStore() as any; const storedMint = mints.mints.find((m: any) => m.url === url); if (!storedMint) { throw new Error("mint not found"); } return this.createWalletInstance(storedMint, url, unit, mints); }, createWalletInstance( storedMint: any, url: string, unit: string, mints: any ): Wallet { if (this.mnemonic == "") { this.mnemonic = generateMnemonic(wordlist); } const bip39seed = mnemonicToSeedSync(this.mnemonic); const counterInit = Object.fromEntries( this.keysetCounters.map(({ id, counter }) => [id, counter]) ); const wallet = new Wallet(url, { unit, bip39seed, counterInit, // logger: new ConsoleLogger("debug"), }); // Load the caches const unitKeysets = mints.mintUnitKeysets(storedMint, unit); const keychainCache = KeyChain.mintToCacheDTO( unit, url, unitKeysets, storedMint.keys ); wallet.loadMintFromCache(storedMint.info, keychainCache); return wallet; }, mnemonicToSeedSync: function (mnemonic: string): Uint8Array { return mnemonicToSeedSync(mnemonic); }, newMnemonic: function () { // store old mnemonic and keysetCounters const oldMnemonicCounters = this.oldMnemonicCounters; const keysetCounters = this.keysetCounters; oldMnemonicCounters.push({ mnemonic: this.mnemonic, keysetCounters }); this.keysetCounters = []; this.mnemonic = generateMnemonic(wordlist); }, keysetCounter: function (id: string) { const keysetCounter = this.keysetCounters.find((c) => c.id === id); if (keysetCounter) { return keysetCounter.counter; } else { this.keysetCounters.push({ id, counter: 1 }); return 1; } }, increaseKeysetCounter: function (id: string, by: number) { const keysetCounter = this.keysetCounters.find((c) => c.id === id); if (keysetCounter) { keysetCounter.counter += by; } else { const newCounter = { id, counter: by } as KeysetCounter; this.keysetCounters.push(newCounter); } }, getKeyset( mintUrl: string | null = null, unit: string | null = null ): string { unit = unit || useMintsStore().activeUnit; mintUrl = mintUrl || useMintsStore().activeMintUrl; const mint = useMintsStore().mints.find((m) => m.url === mintUrl); if (!mint) { throw new Error("mint not found"); } const mintClass = new MintClass(mint); // const mintStore = useMintsStore(); const keysets = mint.keysets; if (keysets == null || keysets.length == 0) { throw new Error("no keysets found."); } const unitKeysets = mintClass.unitKeysets(unit); if (unitKeysets == null || unitKeysets.length == 0) { console.error("no keysets found for unit", unit); throw new Error("no keysets found for unit"); } // select the keyset id // const keyset_id = unitKeysets[0].id; // rules for selection: // - filter all keysets that are active=true // - order by id (whether it is hex or base64) // - order by input_fee_ppk (ascending) TODO: this is not implemented yet // - select the first one const activeKeysets = unitKeysets.filter((k) => k.active); const hexKeysets = activeKeysets.filter((k) => k.id.startsWith("00")); const base64Keysets = activeKeysets.filter((k) => !k.id.startsWith("00")); const sortedKeysets = hexKeysets.concat(base64Keysets); // const sortedKeysets = _.sortBy(activeKeysets, k => [k.id, k.input_fee_ppk]) if (sortedKeysets.length == 0) { console.error("no active keysets found for unit", unit); throw new Error("no active keysets found for unit"); } return sortedKeysets[0].id; }, /** * Sets an invoice status to paid */ setInvoicePaid(quoteId: string) { const invoice = this.invoiceHistory.find((i) => i.quote === quoteId); if (!invoice) return; invoice.status = "paid"; invoice.paidDate = currentDateStr(); }, splitAmount: function (value: number) { // returns optimal 2^n split const chunks: Array = []; for (let i = 0; i < 32; i++) { const mask: number = 1 << i; if ((value & mask) !== 0) { chunks.push(Math.pow(2, i)); } } return chunks; }, coinSelectSpendBase64: function ( proofs: WalletProof[], amount: number ): WalletProof[] { const base64Proofs = proofs.filter((p) => !p.id.startsWith("00")); if (base64Proofs.length > 0) { base64Proofs.sort((a, b) => b.amount - a.amount); let sum = 0; const selectedProofs: WalletProof[] = []; for (let i = 0; i < base64Proofs.length; i++) { const proof = base64Proofs[i]; sum += proof.amount; selectedProofs.push(proof); if (sum >= amount) { return selectedProofs; } } return []; } return []; }, coinSelect: function ( proofs: WalletProof[], wallet: Wallet, amount: number, includeFees: boolean = false ): WalletProof[] { if (proofs.reduce((s, t) => (s += t.amount), 0) < amount) { // there are not enough proofs to pay the amount return []; } const { send: selectedProofs, keep: _ } = wallet.selectProofsToSend( proofs, amount, includeFees ); const selectedWalletProofs = selectedProofs.map((p) => { return { ...p, reserved: false } as WalletProof; }); return selectedWalletProofs; }, spendableProofs: function ( proofs: WalletProof[], amount: number ): WalletProof[] { const proofsStore = useProofsStore(); const spendableProofs = proofsStore.getUnreservedProofs(proofs); if (proofsStore.sumProofs(spendableProofs) < amount) { throw Error(this.t("wallet.notifications.balance_too_low")); } return spendableProofs; }, getFeesForProofs: function (proofs: Proof[]): number { const mints = useMintsStore() as any; const wallet = this.mintWalletSync(mints.activeMintUrl, mints.activeUnit); return wallet.getFeesForProofs(proofs); }, sendToLock: async function ( proofs: WalletProof[], wallet: Wallet, amount: number, receiverPubkey: string ) { const spendableProofs = this.spendableProofs(proofs, amount); const proofsToSend = this.coinSelect( spendableProofs, wallet, amount, true ); const keysetId = this.getKeyset(wallet.mint.mintUrl, wallet.unit); const { keep: keepProofs, send: sendProofs } = await wallet.ops .send(amount, proofsToSend) .keyset(keysetId) .asP2PK({ pubkey: receiverPubkey }) .run(); const proofsStore = useProofsStore(); await proofsStore.removeProofs(proofsToSend); // note: we do not store sendProofs in the proofs store but // expect from the caller to store it in the history await proofsStore.addProofs(keepProofs); return { keepProofs, sendProofs }; }, send: async function ( proofs: WalletProof[], wallet: Wallet, amount: number, invalidate: boolean = false, includeFees: boolean = false ): Promise<{ keepProofs: Proof[]; sendProofs: Proof[] }> { /* splits proofs so the user can keep firstProofs, send scndProofs. then sets scndProofs as reserved. if invalidate, scndProofs (the one to send) are invalidated */ const proofsStore = useProofsStore(); const uIStore = useUiStore(); let proofsToSend: WalletProof[] = []; const keysetId = this.getKeyset(wallet.mint.mintUrl, wallet.unit); await uIStore.lockMutex(); try { const spendableProofs = this.spendableProofs(proofs, amount); proofsToSend = this.coinSelect( spendableProofs, wallet, amount, includeFees ); const totalAmount = proofsToSend.reduce((s, t) => (s += t.amount), 0); const fees = includeFees ? wallet.getFeesForProofs(proofsToSend) : 0; const targetAmount = amount + fees; let keepProofs: Proof[] = []; let sendProofs: Proof[] = []; if (totalAmount != targetAmount) { // we need to swap! // get a new wallet with potentially updated keysets / info const swapWallet = await this.mintWallet( wallet.mint.mintUrl, wallet.unit, true ); const counter = this.keysetCounter(keysetId); proofsToSend = this.coinSelect( spendableProofs, swapWallet, targetAmount, true ); ({ keep: keepProofs, send: sendProofs } = await swapWallet.ops .send(targetAmount, proofsToSend) .asDeterministic(counter) .keyset(keysetId) .proofsWeHave(spendableProofs) .run()); this.increaseKeysetCounter( keysetId, keepProofs.length + sendProofs.length ); await proofsStore.addProofs(keepProofs); await proofsStore.addProofs(sendProofs); // make sure we don't delete any proofs that were returned const proofsToSendNotReturned = proofsToSend .filter((p) => !sendProofs.find((s) => s.secret === p.secret)) .filter((p) => !keepProofs.find((k) => k.secret === p.secret)); await proofsStore.removeProofs(proofsToSendNotReturned); } else if (totalAmount == targetAmount) { keepProofs = []; sendProofs = proofsToSend; } else { throw new Error("could not split proofs."); } await proofsStore.setReserved(sendProofs, true); if (invalidate) { await proofsStore.removeProofs(sendProofs); } return { keepProofs, sendProofs }; } catch (error: any) { await proofsStore.setReserved(proofsToSend, false); console.error(error); notifyApiError(error); this.handleOutputsHaveAlreadyBeenSignedError(keysetId, error); throw error; } finally { uIStore.unlockMutex(); } }, redeem: async function () { /* Receives a token that is prepared in the receiveToken – it is not yet in the history */ const uIStore = useUiStore(); const mintStore = useMintsStore(); const p2pkStore = useP2PKStore(); const wasReceiveDialogVisible = receiveStore.showReceiveTokens; if (receiveStore.receiveData.tokensBase64.length == 0) { throw new Error("no tokens provided."); } const tokenJson = await token.decodeFull( receiveStore.receiveData.tokensBase64 ); if (tokenJson == undefined) { throw new Error("no tokens provided."); } const proofs = token.getProofs(tokenJson); if (proofs.length == 0) { throw new Error("no proofs found."); } const inputAmount = proofs.reduce((s, t) => (s += t.amount), 0); let fee = 0; const mintInToken = token.getMint(tokenJson); const unitInToken = token.getUnit(tokenJson); const historyToken = { amount: inputAmount, token: receiveStore.receiveData.tokensBase64, unit: unitInToken, mint: mintInToken, fee: fee, }; const mintWallet = await this.mintWallet( historyToken.mint, historyToken.unit, true ); const mint = mintStore.mints.find((m) => m.url === historyToken.mint); if (!mint) { throw new Error("mint not found"); } await uIStore.lockMutex(); try { // redeem const keysetId = this.getKeyset(historyToken.mint, historyToken.unit); const counter = this.keysetCounter(keysetId); const privkey = receiveStore.receiveData.p2pkPrivateKey; let proofs: Proof[]; try { proofs = await mintWallet.ops .receive(receiveStore.receiveData.tokensBase64) .asDeterministic(counter) .privkey(privkey) .proofsWeHave(mintStore.mintUnitProofs(mint, historyToken.unit)) .run(); await proofsStore.addProofs(proofs); this.increaseKeysetCounter(keysetId, proofs.length); } catch (error: any) { console.error(error); this.handleOutputsHaveAlreadyBeenSignedError(keysetId, error); throw new Error("Error receiving tokens: " + error); } p2pkStore.setPrivateKeyUsed(privkey); const outputAmount = proofs.reduce((s, t) => (s += t.amount), 0); // if token is already in history, set to paid, else add to history if ( tokenStore.historyTokens.find( (t) => t.token === receiveStore.receiveData.tokensBase64 && t.amount > 0 ) ) { tokenStore.setTokenPaid(receiveStore.receiveData.tokensBase64); } else { // if this is a self-sent token, we will find an outgoing token with the inverse amount if ( tokenStore.historyTokens.find( (t) => t.token === receiveStore.receiveData.tokensBase64 && t.amount < 0 ) ) { tokenStore.setTokenPaid(receiveStore.receiveData.tokensBase64); } fee = inputAmount - outputAmount; historyToken.fee = fee; historyToken.amount = outputAmount; tokenStore.addPaidToken(historyToken as any); } useUiStore().vibrate(); let message = this.t("wallet.notifications.received", { amount: uIStore.formatCurrency(outputAmount, historyToken.unit), }); if (fee > 0) { message += this.t("wallet.notifications.fee", { fee: uIStore.formatCurrency(fee, historyToken.unit), }); } notifySuccess(message); if (wasReceiveDialogVisible) { receiveStore.showReceiveTokens = false; uIStore.closeDialogs(); } } catch (error: any) { console.error(error); notifyApiError(error); throw error; } finally { uIStore.unlockMutex(); } // } }, // /mint /** * Ask the mint to generate an invoice for the given amount * Upon paying the request, the mint will credit the wallet with * cashu tokens */ requestMint: async function ( amount: number, mintWallet: Wallet ): Promise { try { await mintWallet.loadMint(); // defensive const { supported: nut20supported } = mintWallet .getMintInfo() .isSupported(20); const privkey = nut20supported ? bytesToHex(nobleSecp256k1.utils.randomPrivateKey()) : undefined; const pubkey = nut20supported ? bytesToHex(nobleSecp256k1.getPublicKey(privkey!!, true)) : undefined; const payload: MintQuoteBolt11Request = { amount: amount, unit: mintWallet.unit, pubkey: pubkey, }; const data = await mintWallet.mint.createMintQuoteBolt11(payload); this.invoiceData.amount = amount; this.invoiceData.bolt11 = data.request; this.invoiceData.quote = data.quote; this.invoiceData.date = currentDateStr(); this.invoiceData.status = "pending"; this.invoiceData.mint = mintWallet.mint.mintUrl; this.invoiceData.unit = mintWallet.unit; this.invoiceData.mintQuote = data as MintQuoteBolt11Response; this.invoiceData.privKey = privkey; this.invoiceHistory.push({ ...this.invoiceData, }); return data as MintQuoteBolt11Response; } catch (error: any) { console.error(error); notifyApiError( error, this.t("wallet.notifications.could_not_request_mint") ); throw error; } finally { } }, mint: async function (invoice: InvoiceHistory, verbose: boolean = true) { const proofsStore = useProofsStore(); const mintStore = useMintsStore(); const uIStore = useUiStore(); const keysetId = this.getKeyset(invoice.mint, invoice.unit); const mintWallet = await this.mintWallet( invoice.mint, invoice.unit, true ); const mint = mintStore.mints.find((m) => m.url === invoice.mint); if (!mint) { throw new Error("mint not found"); } await uIStore.lockMutex(); try { // first we check if the mint quote is paid const mintQuote = await mintWallet.checkMintQuoteBolt11(invoice.quote); invoice.mintQuote = mintQuote as MintQuoteBolt11Response; console.log("### mint(): mintQuote", mintQuote); switch (mintQuote.state) { case MintQuoteState.PAID: break; case MintQuoteState.UNPAID: if (verbose) { notify(this.t("wallet.notifications.invoice_still_pending")); } throw new Error("invoice pending."); case MintQuoteState.ISSUED: throw new Error("invoice already issued."); default: throw new Error("unknown state."); } // MintQuoteState must be PAID const counter = this.keysetCounter(keysetId); const proofs = await mintWallet.ops .mintBolt11(invoice.amount, invoice.mintQuote!!) .keyset(keysetId) .asDeterministic(counter) .proofsWeHave(mintStore.mintUnitProofs(mint, invoice.unit)) .privkey(invoice.privKey as string) .run(); this.increaseKeysetCounter(keysetId, proofs.length); await proofsStore.addProofs(proofs); // update UI this.setInvoicePaid(invoice.quote); useInvoicesWorkerStore().removeInvoiceFromChecker(invoice.quote); return proofs; } catch (error: any) { console.error(error); if (verbose) { notifyApiError(error); } this.handleOutputsHaveAlreadyBeenSignedError(keysetId, error); throw error; } finally { uIStore.unlockMutex(); } }, // get a melt quote for the current invoice data meltQuoteInvoiceData: async function () { // choose active wallet with active mint and unit const mintWallet = await this.activeWallet(); // throw an error if this.payInvoiceData.blocking is true if (this.payInvoiceData.blocking) { throw new Error("already processing an melt quote."); } this.payInvoiceData.blocking = true; this.payInvoiceData.meltQuote.error = ""; try { const mintStore = useMintsStore(); if (this.payInvoiceData.input.request == "") { throw new Error("no invoice provided."); } const payload: MeltQuoteBolt11Request = { unit: mintStore.activeUnit, request: this.payInvoiceData.input.request, }; this.payInvoiceData.meltQuote.payload = payload; const data = await this.meltQuote(mintWallet, payload.request); mintStore.assertMintError(data as any); this.payInvoiceData.meltQuote.response = data; return data; } catch (error: any) { this.payInvoiceData.meltQuote.error = error; console.error(error); notifyApiError(error); throw error; } finally { this.payInvoiceData.blocking = false; } }, meltQuote: async function ( wallet: Wallet, request: string, mpp_amount: number | undefined = undefined ): Promise { const mintStore = useMintsStore(); let data; if (mpp_amount) { data = await wallet.createMultiPathMeltQuote( request, mpp_amount * 1000 ); } else { data = await wallet.createMeltQuoteBolt11(request); } mintStore.assertMintError(data as any); return data; }, meltInvoiceData: async function (silent?: boolean) { if (this.payInvoiceData.invoice == null) { throw new Error("no invoice provided."); } const quote = this.payInvoiceData.meltQuote.response; if (quote == null) { throw new Error("no quote found."); } const request = this.payInvoiceData.invoice.bolt11; if ( this.invoiceHistory.find( (i) => i.bolt11 === request && i.amount < 0 && i.status === "paid" ) ) { notifyError("Invoice already paid."); throw new Error("invoice already paid."); } // Construct active wallet manually as we need mintStore anyway. const mintStore = useMintsStore(); const mintWallet = await this.mintWallet( mintStore.activeMintUrl, mintStore.activeUnit, true ); return await this.melt(mintStore.activeProofs, quote, mintWallet, silent); }, melt: async function ( proofs: WalletProof[], quote: MeltQuoteBolt11Response, mintWallet: Wallet, silent?: boolean, releaseMutex?: boolean ) { const uIStore = useUiStore(); const proofsStore = useProofsStore(); console.log("#### melt()"); const amount = quote.amount + quote.fee_reserve; let countChangeOutputs = 0; const keysetId = this.getKeyset(mintWallet.mint.mintUrl, mintWallet.unit); let keysetCounterIncrease = 0; // start melt let sendProofs: Proof[] = []; try { const { sendProofs: _sendProofs } = await this.send( proofs, mintWallet, amount, false, true ); sendProofs = _sendProofs; if (sendProofs.length == 0) { throw new Error("could not split proofs."); } } catch (error: any) { console.error(error); if (!silent) notifyApiError(error, "Payment failed"); throw error; } await uIStore.lockMutex(); try { await this.addOutgoingPendingInvoiceToHistory( quote, mintWallet.mint.mintUrl, mintWallet.unit ); await proofsStore.setReserved(sendProofs, true, quote.quote); // NUT-08 blank outputs for change const counter = this.keysetCounter(keysetId); // QUIRK: we increase the keyset counter by sendProofs and the maximum number of possible change outputs // this way, in case the user exits the app before meltProofs is completed, the returned change outputs won't cause a "outputs already signed" error // if the payment fails, we decrease the counter again this.increaseKeysetCounter(keysetId, sendProofs.length); if (quote.fee_reserve > 0) { countChangeOutputs = Math.ceil(Math.log2(quote.fee_reserve)) || 1; this.increaseKeysetCounter(keysetId, countChangeOutputs); keysetCounterIncrease += countChangeOutputs; } uIStore.triggerActivityOrb(); // NOTE: if the user exits the app while we're in the API call, JS will emit an error that we would catch below! // We have to handle that case in the catch block below if (releaseMutex) uIStore.unlockMutex(); // Momentarely release the mutex (needed for concurrent melts) let data; try { data = await mintWallet.ops .meltBolt11(quote, sendProofs) .keyset(keysetId) .asDeterministic(counter) .run(); // store melt quote in invoice history this.updateOutgoingInvoiceInHistory( data.quote as MeltQuoteBolt11Response ); } catch (error) { throw error; } finally { if (releaseMutex) await uIStore.lockMutex(); } if (data.quote.state != MeltQuoteState.PAID) { throw new Error("Invoice not paid."); } // NUT-08 get change if (data.change != null) { const changeProofs = data.change; console.log( "## Received change: " + proofsStore.sumProofs(changeProofs) ); await proofsStore.addProofs(changeProofs); } // delete spent tokens from db await proofsStore.removeProofs(sendProofs); const amount_paid = amount - proofsStore.sumProofs(data.change ?? []); useUiStore().vibrate(); if (!silent) { notifySuccess( this.t("wallet.notifications.paid_lightning", { amount: uIStore.formatCurrency(amount_paid, mintWallet.unit), }) ); } console.log( `#### pay lightning: ${amount_paid} ${mintWallet.unit} paid` ); this.updateOutgoingInvoiceInHistory(quote, { status: "paid", amount: -amount_paid, }); this.payInvoiceData.invoice = { sat: 0, memo: "", bolt11: "" }; this.payInvoiceData.show = false; return data; } catch (error: any) { if (isUnloading) { // NOTE: An error is thrown when the user exits the app while the payment is in progress. // do not handle the error if the user exits the app throw error; } // get quote and check state const meltQuote = await mintWallet.mint.checkMeltQuoteBolt11( quote.quote ); // store melt quote in invoice history this.updateOutgoingInvoiceInHistory( meltQuote as MeltQuoteBolt11Response ); if ( meltQuote.state == MeltQuoteState.PAID || meltQuote.state == MeltQuoteState.PENDING ) { console.log( "### melt: error, but quote is paid or pending. not rolling back." ); this.payInvoiceData.show = false; notify(this.t("wallet.notifications.payment_pending_refresh")); throw error; } // roll back proof management and keyset counter await proofsStore.setReserved(sendProofs, false); this.increaseKeysetCounter(keysetId, -keysetCounterIncrease); this.removeOutgoingInvoiceFromHistory(quote.quote); console.error(error); this.handleOutputsHaveAlreadyBeenSignedError(keysetId, error); if (!silent) notifyApiError(error, "Payment failed"); throw error; } finally { uIStore.unlockMutex(); } }, // /check checkProofsSpendable: async function ( proofs: Proof[], wallet: Wallet, update_history = false ) { /* checks with the mint whether an array of proofs is still spendable or already invalidated */ const uIStore = useUiStore(); const proofsStore = useProofsStore(); const tokenStore = useTokensStore(); if (proofs.length == 0) { return; } try { uIStore.triggerActivityOrb(); const { spent: spentProofs } = await wallet.groupProofsByState(proofs); if (spentProofs.length) { await proofsStore.removeProofs(spentProofs); // update UI const serializedProofs = proofsStore.serializeProofs(spentProofs); if (serializedProofs == null) { throw new Error("could not serialize proofs."); } if (update_history) { tokenStore.addPaidToken({ amount: -proofsStore.sumProofs(spentProofs), token: serializedProofs, unit: wallet.unit, mint: wallet.mint.mintUrl, }); } } // return spent proofs return spentProofs; } catch (error: any) { console.error(error); notifyApiError(error); throw error; } }, checkTokenSpendable: async function ( historyToken: HistoryToken, verbose: boolean = true ) { /* checks whether a base64-encoded token (from the history table) has been spent already. if it is spent, the appropraite entry in the history table is set to paid. */ const uIStore = useUiStore(); const mintStore = useMintsStore(); const tokenStore = useTokensStore(); const proofsStore = useProofsStore(); const tokenJson = await token.decodeFull(historyToken.token); if (tokenJson == undefined) { throw new Error("no tokens provided."); } const proofs = token.getProofs(tokenJson); const mintWallet = await this.mintWallet( historyToken.mint, historyToken.unit ); const mint = mintStore.mints.find((m) => m.url === historyToken.mint); if (!mint) { throw new Error("mint not found"); } const spentProofs = await this.checkProofsSpendable(proofs, mintWallet); if (spentProofs != undefined && spentProofs.length == proofs.length) { // all proofs are spent, set token to paid tokenStore.setTokenPaid(historyToken.token); } else if ( spentProofs != undefined && spentProofs.length && spentProofs.length < proofs.length ) { // not all proofs are spent, we remove the spent part of the token from the history const spentAmount = proofsStore.sumProofs(spentProofs); const serializedSpentProofs = proofsStore.serializeProofs(spentProofs); const unspentProofs = proofs.filter( (p) => !spentProofs.find((sp) => sp.secret === p.secret) ); const unspentAmount = proofsStore.sumProofs(unspentProofs); const serializedUnspentProofs = proofsStore.serializeProofs(unspentProofs); if (serializedSpentProofs && serializedUnspentProofs) { const historyToken2 = tokenStore.editHistoryToken( historyToken.token, { newAmount: spentAmount, newStatus: "paid", newToken: serializedSpentProofs, } ); // add all unspent proofs back to the history // QUICK: we use the historyToken object here because we don't know if the transaction is incoming or outgoing (we don't know the sign of the amount) if (historyToken2) { tokenStore.addPendingToken({ amount: unspentAmount * Math.sign(historyToken2.amount), token: serializedUnspentProofs, unit: historyToken2.unit, mint: historyToken2.mint, }); } } } if (spentProofs != undefined && spentProofs.length) { useUiStore().vibrate(); const proofStore = useProofsStore(); notifySuccess( this.t("wallet.notifications.sent", { amount: uIStore.formatCurrency( proofStore.sumProofs(spentProofs), historyToken.unit ), }) ); } else { console.log("### token not paid yet"); if (verbose) { notify(this.t("wallet.notifications.token_still_pending")); } return false; } return true; }, checkInvoice: async function ( quote: string, verbose = true, hideInvoiceDetailsOnMint = true ) { const uIStore = useUiStore(); uIStore.triggerActivityOrb(); const mintStore = useMintsStore(); const invoice = this.invoiceHistory.find((i) => i.quote === quote); if (!invoice) { throw new Error("invoice not found"); } const mintWallet = await this.mintWallet(invoice.mint, invoice.unit); const mint = mintStore.mints.find((m) => m.url === invoice.mint); if (!mint) { throw new Error("mint not found"); } try { // check the state first const state = (await mintWallet.checkMintQuoteBolt11(quote)).state; if (state == MintQuoteState.ISSUED) { this.setInvoicePaid(quote); return; } if (state != MintQuoteState.PAID) { console.log("### mintQuote not paid yet"); if (verbose) { notify(this.t("wallet.notifications.invoice_still_pending")); } throw new Error(`invoice state not paid: ${state}`); } const proofs = await this.mint(invoice, verbose); if (hideInvoiceDetailsOnMint) { uIStore.showInvoiceDetails = false; } useUiStore().vibrate(); notifySuccess( this.t("wallet.notifications.received_lightning", { amount: uIStore.formatCurrency(invoice.amount, invoice.unit), }) ); return proofs; } catch (error) { // if (verbose) { // notify("Invoice still pending"); // } console.log("Invoice still pending", invoice.quote); throw error; } }, checkOutgoingInvoice: async function (quote: string, verbose = true) { const uIStore = useUiStore(); const mintStore = useMintsStore(); const invoice = this.invoiceHistory.find((i) => i.quote === quote); if (!invoice) { throw new Error("invoice not found"); } const mintWallet = await this.mintWallet(invoice.mint, invoice.unit); const mint = mintStore.mints.find((m) => m.url === invoice.mint); if (!mint) { throw new Error("mint not found"); } const proofs: Proof[] = await proofsStore.getProofsForQuote(quote); try { // this is an outgoing invoice, we first do a getMintQuote to check if the invoice is paid const meltQuote = await mintWallet.mint.checkMeltQuoteBolt11(quote); this.updateOutgoingInvoiceInHistory( meltQuote as MeltQuoteBolt11Response ); if (meltQuote.state == MeltQuoteState.PENDING) { console.log("### mintQuote not paid yet"); if (verbose) { notify(this.t("wallet.notifications.invoice_still_pending")); } throw new Error("invoice not paid yet."); } else if (meltQuote.state == MeltQuoteState.UNPAID) { // we assume that the payment failed and we unset the proofs as reserved await useProofsStore().setReserved(proofs, false); this.removeOutgoingInvoiceFromHistory(quote); notifyWarning( this.t("wallet.notifications.lightning_payment_failed") ); } else if (meltQuote.state == MeltQuoteState.PAID) { // if the invoice is paid, we check if all proofs are spent and if so, we invalidate them and set the invoice state in the history to "paid" const spentProofs = await this.checkProofsSpendable( proofs, mintWallet, true ); if (spentProofs != undefined && spentProofs.length == proofs.length) { useUiStore().vibrate(); notifySuccess( this.t("wallet.notifications.sent", { amount: uIStore.formatCurrency( useProofsStore().sumProofs(proofs), invoice.unit ), }) ); } // set invoice in history to paid this.setInvoicePaid(quote); } } catch (error: any) { if (verbose) { notifyApiError(error); } console.log("Could not check quote", invoice.quote, error); throw error; } }, onTokenPaid: async function (historyToken: HistoryToken) { const sendTokensStore = useSendTokensStore(); const uIStore = useUiStore(); const tokenJson = await token.decodeFull(historyToken.token); const mintStore = useMintsStore(); const settingsStore = useSettingsStore(); if (!settingsStore.checkSentTokens) { console.log( "settingsStore.checkSentTokens is disabled, skipping token check" ); return; } const mint = mintStore.mints.find((m) => m.url === historyToken.mint); if (!mint) { throw new Error("mint not found"); } if ( !settingsStore.useWebsockets || !mint.info?.nuts[17]?.supported || !mint.info?.nuts[17]?.supported.find( (s) => s.method == "bolt11" && s.unit == historyToken.unit && s.commands.indexOf("proof_state") != -1 ) ) { console.log( "Websockets not supported, kicking off token check worker." ); useWorkersStore().checkTokenSpendableWorker(historyToken); return; } try { console.log("onTokenPaid kicking off websocket"); if (tokenJson == undefined) { throw new Error("no tokens provided."); } const proofs = token.getProofs(tokenJson); const oneProof = [proofs[0]]; this.activeWebsocketConnections++; uIStore.triggerActivityOrb(); const wallet = await this.activeWallet(); const unsub = await wallet.on.proofStateUpdates( oneProof, async (proofState: ProofState) => { console.log(`Websocket: proof state updated: ${proofState.state}`); if (proofState.state == CheckStateEnum.SPENT) { const tokenSpent = await this.checkTokenSpendable(historyToken); if (tokenSpent) { sendTokensStore.showSendTokens = false; unsub(); } } }, async (error: any) => { console.error(error); notifyApiError(error); throw error; } ); } catch (error) { console.error( "Error in websocket subscription. Starting invoices worker.", error ); useWorkersStore().checkTokenSpendableWorker(historyToken); } finally { this.activeWebsocketConnections--; } }, mintOnPaid: async function ( quote: string, verbose = true, kickOffInvoiceChecker = true, hideInvoiceDetailsOnMint = true ) { const mintStore = useMintsStore(); const settingsStore = useSettingsStore(); if (!settingsStore.checkIncomingInvoices) { console.log( "settingsStore.checkIncomingInvoices is disabled, skipping invoice check" ); return; } const invoice = this.invoiceHistory.find((i) => i.quote === quote); if (!invoice) { throw new Error("invoice not found"); } const mintWallet = await this.mintWallet(invoice.mint, invoice.unit); const mint = mintStore.mints.find((m) => m.url === invoice.mint); if (!mint) { throw new Error("mint not found"); } // add to checker before we try a websocket if (kickOffInvoiceChecker) { if (useSettingsStore().periodicallyCheckIncomingInvoices) { console.log(`Adding quote ${quote} to long-polling checker.`); useInvoicesWorkerStore().addInvoiceToChecker(quote); } else if (useSettingsStore().checkIncomingInvoices) { console.log(`Adding quote ${quote} to old worker checker.`); useWorkersStore().invoiceCheckWorker(quote); } } if ( !settingsStore.useWebsockets || !mint.info?.nuts[17]?.supported || !mint.info?.nuts[17]?.supported.find( (s) => s.method == "bolt11" && s.unit == invoice.unit && s.commands.indexOf("bolt11_mint_quote") != -1 ) ) { console.log("Websockets not supported."); return; } const uIStore = useUiStore(); try { this.activeWebsocketConnections++; uIStore.triggerActivityOrb(); const unsub = await mintWallet.on.mintQuotePaid( quote, async (_mintQuoteResponse: MintQuoteBolt11Response) => { console.log("Websocket: mint quote paid."); let proofs; try { proofs = await this.mint(invoice, false); } catch (error: any) { console.error(error); // notifyApiError(error); throw error; } if (hideInvoiceDetailsOnMint) { uIStore.showInvoiceDetails = false; } useUiStore().vibrate(); notifySuccess( this.t("wallet.notifications.received_lightning", { amount: uIStore.formatCurrency(invoice.amount, invoice.unit), }) ); unsub(); return proofs; }, async (error: any) => { if (verbose) { notifyApiError(error); } console.log("Invoice still pending", invoice.quote); throw error; } ); } catch (error) { console.log("Error in websocket subscription", error); } finally { this.activeWebsocketConnections--; } }, ////////////// UI HELPERS ////////////// addOutgoingPendingInvoiceToHistory: async function ( quote: MeltQuoteBolt11Response, mint: string, unit: string ) { this.invoiceHistory.push({ amount: -(quote.amount + quote.fee_reserve), bolt11: this.payInvoiceData.input.request, quote: quote.quote, memo: "Outgoing invoice", date: currentDateStr(), status: "pending", mint: mint, unit: unit, meltQuote: quote, }); }, removeOutgoingInvoiceFromHistory: function (quote: string) { const index = this.invoiceHistory.findIndex((i) => i.quote === quote); if (index >= 0) { this.invoiceHistory.splice(index, 1); } }, updateOutgoingInvoiceInHistory: function ( quote: MeltQuoteBolt11Response, options?: { status?: "pending" | "paid"; amount?: number } ) { this.invoiceHistory .filter((i) => i.quote === quote.quote) .forEach((i) => { if (options) { if (options.status) { i.status = options.status; if (options.status === "paid") { i.paidDate = currentDateStr(); } } if (options.amount) { i.amount = options.amount; } i.meltQuote = quote; } }); }, checkPendingTokens: async function (verbose: boolean = true) { const tokenStore = useTokensStore(); const last_n = 5; let i = 0; // invert for loop for (const t of tokenStore.historyTokens.slice().reverse()) { if (i >= last_n) { break; } if (t.status === "pending" && t.amount < 0 && t.token) { console.log("### checkPendingTokens", t.token); this.checkTokenSpendable(t, verbose); i += 1; } } }, handleBolt11Invoice: async function () { this.payInvoiceData.show = true; let invoice; try { invoice = bolt11Decoder.decode(this.payInvoiceData.input.request); } catch (error) { notifyWarning( this.t("wallet.notifications.failed_to_decode_invoice"), undefined, 3000 ); this.payInvoiceData.show = false; throw error; } const cleanInvoice = { bolt11: invoice.paymentRequest, memo: "", msat: 0, sat: 0, fsat: 0, hash: "", description: "", timestamp: 0, expireDate: "", expired: false, }; _.each(invoice.sections, (tag) => { if (_.isObject(tag) && _.has(tag, "name")) { if (tag.name === "amount") { cleanInvoice.msat = parseInt(tag.value, 10); cleanInvoice.sat = parseInt(tag.value, 10) / 1000; cleanInvoice.fsat = cleanInvoice.sat; } else if (tag.name === "payment_hash") { cleanInvoice.hash = tag.value; } else if (tag.name === "description") { cleanInvoice.description = tag.value; } else if (tag.name === "timestamp") { cleanInvoice.timestamp = tag.value; } else if (tag.name === "expiry") { const expireDate = new Date( (cleanInvoice.timestamp + tag.value) * 1000 ); cleanInvoice.expireDate = date.formatDate( expireDate, "YYYY-MM-DDTHH:mm:ss.SSSZ" ); cleanInvoice.expired = false; // TODO } } }); this.payInvoiceData.invoice = Object.freeze(cleanInvoice); // get quote for this request await this.meltQuoteInvoiceData(); }, handleCashuToken: function () { this.payInvoiceData.show = false; receiveStore.showReceiveTokens = true; }, handleP2PK: function (req: string) { const sendTokenStore = useSendTokensStore(); sendTokenStore.sendData.p2pkPubkey = req; sendTokenStore.showSendTokens = true; }, handlePaymentRequest: async function (req: string) { const prStore = usePRStore(); await prStore.decodePaymentRequest(req); }, decodeRequest: async function (req: string) { const p2pkStore = useP2PKStore(); req = req.trim(); this.payInvoiceData.input.request = req; if (req.toLowerCase().startsWith("lnbc")) { this.payInvoiceData.input.request = req; await this.handleBolt11Invoice(); } else if (req.toLowerCase().startsWith("lightning:")) { this.payInvoiceData.input.request = req.slice(10); await this.handleBolt11Invoice(); } else if (req.toLowerCase().startsWith("bitcoin:")) { try { const url = new URL( req.replace(/^bitcoin:/i, "bitcoin://placeholder/") ); const creq = url.searchParams.get("creq"); const lightning = url.searchParams.get("lightning"); if (creq) { this.payInvoiceData.input.request = creq; await this.handlePaymentRequest(creq); } else if (lightning) { this.payInvoiceData.input.request = lightning; if (lightning.toLowerCase().startsWith("lnurl1")) { await this.lnurlPayFirst(lightning); } else { await this.handleBolt11Invoice(); } } } catch { const creqMatch = req.match(/[?&]creq=([^&]+)/i); const lightningMatch = req.match(/[?&]lightning=([^&]+)/i); if (creqMatch) { this.payInvoiceData.input.request = creqMatch[1]; await this.handlePaymentRequest(creqMatch[1]); } else if (lightningMatch) { this.payInvoiceData.input.request = lightningMatch[1]; await this.handleBolt11Invoice(); } } } else if (req.toLowerCase().startsWith("lnurl:")) { this.payInvoiceData.input.request = req.slice(6); await this.lnurlPayFirst(this.payInvoiceData.input.request); } else if (req.indexOf("lightning=lnurl1") !== -1) { this.payInvoiceData.input.request = req .split("lightning=")[1] .split("&")[0]; await this.lnurlPayFirst(this.payInvoiceData.input.request); } else if ( req.toLowerCase().startsWith("lnurl1") || req.match(/[\w.+-~_]+@[\w.+-~_]/) ) { this.payInvoiceData.input.request = req; await this.lnurlPayFirst(this.payInvoiceData.input.request); } else if (req.startsWith("cashuA") || req.startsWith("cashuB")) { // parse cashu tokens from a pasted token receiveStore.receiveData.tokensBase64 = req; this.handleCashuToken(); } else if (req.indexOf("token=cashu") !== -1) { // parse cashu tokens from a URL like https://example.com#token=cashu... const token = req.slice(req.indexOf("token=cashu") + 6); receiveStore.receiveData.tokensBase64 = token; this.handleCashuToken(); } else if (p2pkStore.isValidPubkey(req)) { this.handleP2PK(req); } else if (req.startsWith("http")) { const mintStore = useMintsStore(); mintStore.addMintData = { url: req, nickname: "" }; } else if ( req.toLowerCase().startsWith("creqa") || req.toLowerCase().startsWith("creqb") ) { await this.handlePaymentRequest(req); } else if (isLegacyRetailQR(req)) { // Try to convert legacy retail QR code (EMV format) to Lightning Address const lightningAddress = translateLegacyQRToLightningAddress(req); if (lightningAddress) { // Process as Lightning Address (LNURL) this.payInvoiceData.input.request = lightningAddress; await this.lnurlPayFirst(lightningAddress); } else { // Not a supported merchant QR code notifyWarning( this.t("wallet.notifications.unsupported_legacy_qr"), this.t("wallet.notifications.legacy_qr_not_supported") ); } } const uiStore = useUiStore(); uiStore.closeDialogs(); }, lnurlPayFirst: async function (address: string) { let host; let data; if (address.split("@").length == 2) { const [user, lnaddresshost] = address.split("@"); host = `https://${lnaddresshost}/.well-known/lnurlp/${user}`; const resp = await axios.get(host); // Moved it here: we don't want 2 potential calls data = resp.data; } else if (address.toLowerCase().slice(0, 6) === "lnurl1") { const decoded = bech32.decode(address, 20000); const words = bech32.fromWords(decoded.words); const uint8Array = new Uint8Array(words); host = new TextDecoder().decode(uint8Array); const resp = await axios.get(host); data = resp.data; } if (host == undefined) { notifyError( this.t("wallet.notifications.invalid_lnurl"), this.t("wallet.notifications.lnurl_error") ); return; } if (data.tag == "payRequest") { this.payInvoiceData.lnurlpay = data; this.payInvoiceData.lnurlpay.domain = host .split("https://")[1] .split("/")[0]; // Store lightning address if it was a lightning address (not a LNURL) if (address.split("@").length == 2) { this.payInvoiceData.lnurlpay.lightningAddress = address; } else { this.payInvoiceData.lnurlpay.lightningAddress = ""; } if ( this.payInvoiceData.lnurlpay.maxSendable == this.payInvoiceData.lnurlpay.minSendable ) { this.payInvoiceData.input.amount = this.payInvoiceData.lnurlpay.maxSendable / 1000; } this.payInvoiceData.invoice = null; this.payInvoiceData.input = { request: "", amount: undefined, comment: "", quote: "", }; this.payInvoiceData.show = true; } }, lnurlPaySecond: async function () { const mintStore = useMintsStore(); let amount = this.payInvoiceData.input.amount; if (amount == null) { notifyError( this.t("wallet.notifications.no_amount"), this.t("wallet.notifications.lnurl_error") ); return; } if (this.payInvoiceData.lnurlpay == null) { notifyError( this.t("wallet.notifications.no_lnurl_data"), this.t("wallet.notifications.lnurl_error") ); return; } if ( this.payInvoiceData.lnurlpay.tag == "payRequest" && this.payInvoiceData.lnurlpay.minSendable <= amount * 1000 && this.payInvoiceData.lnurlpay.maxSendable >= amount * 1000 ) { if (mintStore.activeUnit == "usd") { const priceUsd = usePriceStore().bitcoinPrice; if (priceUsd == 0) { notifyError( this.t("wallet.notifications.no_price_data"), this.t("wallet.notifications.lnurl_error") ); return; } const satPrice = 1 / (priceUsd / 1e8); const usdAmount = amount; amount = Math.floor(usdAmount * satPrice); } const callback = this.payInvoiceData.lnurlpay.callback; const separator = callback.includes("?") ? "&" : "?"; const { data } = await axios.get( `${callback}${separator}amount=${amount * 1000}` ); // check http error if (data.status == "ERROR") { notifyError(data.reason, this.t("wallet.notifications.lnurl_error")); return; } await this.decodeRequest(data.pr); } }, initializeMnemonic: function () { if (this.mnemonic == "") { this.mnemonic = generateMnemonic(wordlist); } return this.mnemonic; }, handleOutputsHaveAlreadyBeenSignedError: function ( keysetId: string, error: any ) { if (error.message.includes("outputs have already been signed")) { this.increaseKeysetCounter(keysetId, 10); notify(this.t("wallet.notifications.please_try_again")); return true; } return false; }, }, }); ================================================ FILE: src/stores/welcome.ts ================================================ // src/stores/welcome.ts import { defineStore } from "pinia"; import { useLocalStorage } from "@vueuse/core"; import { computed } from "vue"; export type WelcomeState = { showWelcome: boolean; currentSlide: number; seedPhraseValidated: boolean; termsAccepted: boolean; onboardingPath: string; // 'new' | 'recover' | '' seedEnteredValid: boolean; mintSetupCompleted: boolean; ecashRestoreCompleted: boolean; }; // Define the Pinia store export const useWelcomeStore = defineStore("welcome", { state: (): WelcomeState => ({ showWelcome: useLocalStorage("cashu.welcome.showWelcome", true), currentSlide: useLocalStorage("cashu.welcome.currentSlide", 0), seedPhraseValidated: useLocalStorage( "cashu.welcome.seedPhraseValidated", false ), termsAccepted: useLocalStorage( "cashu.welcome.termsAccepted", false ), onboardingPath: useLocalStorage("cashu.welcome.path", ""), seedEnteredValid: useLocalStorage( "cashu.welcome.seedEnteredValid", false ), mintSetupCompleted: useLocalStorage( "cashu.welcome.mintSetupCompleted", false ), ecashRestoreCompleted: useLocalStorage( "cashu.welcome.ecashRestoreCompleted", false ), }), getters: { // Determines if the current slide is the last one isLastSlide: (state) => { // Slides: // 0 Intro, 1 PWA, 2 Choice, // New: 3 Seed, 4 Mints (no more terms screen) // Recover: 3 SeedIn, 4 Mints, 5 Restore (no more terms screen) if (state.onboardingPath === "recover") return state.currentSlide === 5; if (state.onboardingPath === "new") return state.currentSlide === 4; // before choosing a path return false; }, // Determines if the user can proceed to the next slide canProceed: (state) => { // 0 Intro if (state.currentSlide === 0) return true; // 1 PWA if (state.currentSlide === 1) return true; // 2 Choice if (state.currentSlide === 2) return state.onboardingPath !== ""; // 3 (seed phrase for both paths) if (state.currentSlide === 3) { if (state.onboardingPath === "new") return state.seedPhraseValidated; if (state.onboardingPath === "recover") return state.seedEnteredValid; } // 4 (mints setup for both paths - last step for "new" path) if (state.currentSlide === 4) return state.mintSetupCompleted || true; // 5 (restore step for recover path - last step for "recover" path) if (state.currentSlide === 5) { if (state.onboardingPath === "recover") return state.ecashRestoreCompleted || true; } return false; }, // Determines if the user can navigate to the previous slide canGoPrev: (state) => state.currentSlide > 0, }, actions: { /** * Initializes the welcome dialog based on local storage. * Should be called when the store is initialized. */ initializeWelcome() { if (!this.showWelcome) { window.location.href = "/"; } }, /** * Closes the welcome dialog and marks it as seen. */ closeWelcome() { this.showWelcome = false; // Reset the slide to the beginning for next time (if welcome is ever shown again) this.currentSlide = 0; // Redirect to home or desired route window.location.href = "/" + window.location.search + window.location.hash; }, setPath(path: "new" | "recover") { this.onboardingPath = path; }, /** * Sets the current slide index. * @param index - The index of the slide to navigate to. */ setCurrentSlide(index: number) { this.currentSlide = index; }, /** * Marks the terms as accepted. */ acceptTerms() { this.termsAccepted = true; }, /** * Validates the seed phrase. */ validateSeedPhrase() { this.seedPhraseValidated = true; }, /** * Resets the welcome dialog state (useful for testing or resetting). */ resetWelcome() { this.showWelcome = true; this.currentSlide = 0; this.termsAccepted = false; this.seedPhraseValidated = false; this.onboardingPath = ""; this.seedEnteredValid = false; this.mintSetupCompleted = false; this.ecashRestoreCompleted = false; }, /** * Navigates to the previous slide if possible. */ goToPrevSlide() { if (this.canGoPrev) { this.currentSlide -= 1; } // Optionally, handle edge cases or emit events }, /** * Navigates to the next slide if possible. * If on the last slide, it can close the welcome dialog. */ goToNextSlide() { if (this.canProceed) { if (this.isLastSlide) { this.closeWelcome(); } else { this.currentSlide += 1; } } // Optionally, handle edge cases or emit events console.log(`href: ${window.location.href}`); }, }, }); ================================================ FILE: src/stores/workers.ts ================================================ import { defineStore } from "pinia"; import { useWalletStore } from "src/stores/wallet"; // invoiceData, import { useUiStore } from "src/stores/ui"; // showInvoiceDetails import { useSendTokensStore } from "src/stores/sendTokensStore"; // showSendTokens and sendData import { useSettingsStore } from "./settings"; import { HistoryToken, useTokensStore } from "./tokens"; export const useWorkersStore = defineStore("workers", { state: () => { return { invoiceCheckListener: null as NodeJS.Timeout | null, tokensCheckSpendableListener: null as NodeJS.Timeout | null, invoiceWorkerRunning: false, tokenWorkerRunning: false, checkInterval: 5000, }; }, getters: {}, actions: { clearAllWorkers: function () { if (this.invoiceCheckListener) { clearInterval(this.invoiceCheckListener); this.invoiceWorkerRunning = false; } if (this.tokensCheckSpendableListener) { clearInterval(this.tokensCheckSpendableListener); this.tokenWorkerRunning = false; } }, invoiceCheckWorker: async function (quote: string) { const walletStore = useWalletStore(); let nInterval = 0; this.clearAllWorkers(); this.invoiceCheckListener = setInterval(async () => { try { this.invoiceWorkerRunning = true; nInterval += 1; // exit loop after 1m if (nInterval > 12) { console.log("### stopping invoice check worker"); this.clearAllWorkers(); } console.log("### invoiceCheckWorker setInterval", nInterval); // this will throw an error if the invoice is pending await walletStore.checkInvoice(quote, false); // only without error (invoice paid) will we reach here console.log("### stopping invoice check worker"); this.clearAllWorkers(); } catch (error) { console.log("invoiceCheckWorker: not paid yet"); } }, this.checkInterval); }, checkTokenSpendableWorker: async function (historyToken: HistoryToken) { const settingsStore = useSettingsStore(); if (!settingsStore.checkSentTokens) { console.log( "settingsStore.checkSentTokens is disabled, not kicking off checkTokenSpendableWorker" ); return; } console.log("### kicking off checkTokenSpendableWorker"); this.tokenWorkerRunning = true; const walletStore = useWalletStore(); const sendTokensStore = useSendTokensStore(); let nInterval = 0; this.clearAllWorkers(); this.tokensCheckSpendableListener = setInterval(async () => { try { nInterval += 1; // exit loop after 30s if (nInterval > 10) { console.log("### stopping token check worker"); this.clearAllWorkers(); } console.log("### checkTokenSpendableWorker setInterval", nInterval); const paid = await walletStore.checkTokenSpendable( historyToken, false ); if (paid) { console.log("### stopping token check worker"); this.clearAllWorkers(); sendTokensStore.showSendTokens = false; } } catch (error) { console.log("checkTokenSpendableWorker: some error", error); this.clearAllWorkers(); } }, this.checkInterval); }, }, }); ================================================ FILE: src-electron/electron-env.d.ts ================================================ /* eslint-disable */ declare namespace NodeJS { interface ProcessEnv { QUASAR_PUBLIC_FOLDER: string; QUASAR_ELECTRON_PRELOAD: string; APP_URL: string; } } ================================================ FILE: src-electron/electron-flag.d.ts ================================================ /* eslint-disable */ // THIS FEATURE-FLAG FILE IS AUTOGENERATED, // REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING import "quasar/dist/types/feature-flag"; declare module "quasar/dist/types/feature-flag" { interface QuasarFeatureFlags { electron: true; } } ================================================ FILE: src-electron/electron-main.ts ================================================ import { app, BrowserWindow } from "electron"; import path from "path"; import os from "os"; // needed in case process is undefined under Linux const platform = process.platform || os.platform(); let mainWindow: BrowserWindow | undefined; function createWindow() { /** * Initial window options */ mainWindow = new BrowserWindow({ icon: path.resolve(__dirname, "icons/icon.png"), // tray icon width: 1000, height: 600, useContentSize: true, webPreferences: { contextIsolation: true, // More info: https://v2.quasar.dev/quasar-cli-vite/developing-electron-apps/electron-preload-script preload: path.resolve(__dirname, process.env.QUASAR_ELECTRON_PRELOAD), }, }); mainWindow.loadURL(process.env.APP_URL); if (process.env.DEBUGGING) { // if on DEV or Production with debug enabled mainWindow.webContents.openDevTools(); } else { // we're on production; no access to devtools pls mainWindow.webContents.on("devtools-opened", () => { mainWindow?.webContents.closeDevTools(); }); } mainWindow.on("closed", () => { mainWindow = undefined; }); } app.whenReady().then(createWindow); app.on("window-all-closed", () => { if (platform !== "darwin") { app.quit(); } }); app.on("activate", () => { if (mainWindow === undefined) { createWindow(); } }); ================================================ FILE: src-electron/electron-preload.ts ================================================ /** * This file is used specifically for security reasons. * Here you can access Nodejs stuff and inject functionality into * the renderer thread (accessible there through the "window" object) * * WARNING! * If you import anything from node_modules, then make sure that the package is specified * in package.json > dependencies and NOT in devDependencies * * Example (injects window.myAPI.doAThing() into renderer thread): * * import { contextBridge } from 'electron' * * contextBridge.exposeInMainWorld('myAPI', { * doAThing: () => {} * }) * * WARNING! * If accessing Node functionality (like importing @electron/remote) then in your * electron-main.ts you will need to set the following when you instantiate BrowserWindow: * * mainWindow = new BrowserWindow({ * // ... * webPreferences: { * // ... * sandbox: false // <-- to be able to import @electron/remote in preload script * } * } */ ================================================ FILE: src-pwa/custom-service-worker.js ================================================ /* eslint-env serviceworker */ /* * This file (which will be your service worker) * is picked up by the build system ONLY if * quasar.config.js > pwa > workboxMode is set to "injectManifest" */ import { clientsClaim } from "workbox-core"; import { precacheAndRoute, cleanupOutdatedCaches, createHandlerBoundToURL, } from "workbox-precaching"; import { registerRoute, NavigationRoute } from "workbox-routing"; self.skipWaiting(); clientsClaim(); // Use with precache injection precacheAndRoute(self.__WB_MANIFEST); cleanupOutdatedCaches(); // Non-SSR fallback to index.html // Production SSR fallback to offline.html (except for dev) if (process.env.MODE !== "ssr" || process.env.PROD) { registerRoute( new NavigationRoute( createHandlerBoundToURL(process.env.PWA_FALLBACK_HTML), { denylist: [/sw\.js$/, /workbox-(.)*\.js$/] } ) ); } ================================================ FILE: src-pwa/manifest.json ================================================ { "name": "Cashu.me", "short_name": "Cashu.me", "description": "A Cashu Ecash wallet for the web.", "display": "standalone", "orientation": "portrait", "background_color": "#000000", "theme_color": "#000000", "protocol_handlers": [ { "protocol": "web+cashu", "url": "/?token=%s" }, { "protocol": "web+lightning", "url": "/?lightning=%s" } ], "icons": [ { "src": "icons/icon-128x128.png", "sizes": "128x128", "type": "image/png", "purpose": "maskable" }, { "src": "icons/icon-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/icon-256x256.png", "sizes": "256x256", "type": "image/png", "purpose": "maskable" }, { "src": "icons/icon-384x384.png", "sizes": "384x384", "type": "image/png", "purpose": "maskable" }, { "src": "icons/icon-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }, { "src": "icons/icon-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" } ], "screenshots": [ { "src": "/screenshots/narrow-1.png", "form_factor": "narrow", "sizes": "542x942", "type": "image/png", "label": "fullscreen view" }, { "src": "/screenshots/narrow-2.png", "form_factor": "narrow", "sizes": "542x942", "type": "image/png", "label": "fullscreen view" }, { "src": "/screenshots/wide-1.png", "form_factor": "wide", "sizes": "1910x932", "type": "image/png", "label": "fullscreen view" }, { "src": "/screenshots/wide-2.png", "form_factor": "wide", "sizes": "1910x932", "type": "image/png", "label": "fullscreen view" } ] } ================================================ FILE: src-pwa/pwa-flag.d.ts ================================================ /* eslint-disable */ // THIS FEATURE-FLAG FILE IS AUTOGENERATED, // REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING import "quasar/dist/types/feature-flag"; declare module "quasar/dist/types/feature-flag" { interface QuasarFeatureFlags { pwa: true; } } ================================================ FILE: src-pwa/register-service-worker.js ================================================ import { register } from "register-service-worker"; // The ready(), registered(), cached(), updatefound() and updated() // events passes a ServiceWorkerRegistration instance in their arguments. // ServiceWorkerRegistration: https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration register(process.env.SERVICE_WORKER_FILE, { // The registrationOptions object will be passed as the second argument // to ServiceWorkerContainer.register() // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register#Parameter // registrationOptions: { scope: './' }, ready(/* registration */) { // console.log('Service worker is active.') }, registered(/* registration */) { // console.log('Service worker has been registered.') }, cached(/* registration */) { // console.log('Content has been cached for offline use.') }, updatefound(/* registration */) { // console.log('New content is downloading.') }, updated(/* registration */) { // console.log('New content is available; please refresh.') }, offline() { // console.log('No internet connection found. App is running in offline mode.') }, error(/* err */) { // console.error('Error during service worker registration:', err) }, }); ================================================ FILE: test/vitest/__tests__/bip39seed.test.ts ================================================ import { test, describe, expect } from "vitest"; import { mnemonicToSeedSync, validateMnemonic, mnemonicToSeed, } from "@scure/bip39"; import { wordlist } from "@scure/bip39/wordlists/english"; describe("mnemonicToSeedSync", () => { test("converts same mnemonic consistently", () => { const mnem = "legal winner thank year wave sausage worth useful legal winner thank yellow"; expect(validateMnemonic(mnem, wordlist)).toBeTruthy(); const seed1 = mnemonicToSeedSync(mnem); const seed2 = mnemonicToSeedSync(mnem); expect(seed1).toEqual(seed2); }); test("converts same mnemonic consistently sync/async", async () => { const mnem = "legal winner thank year wave sausage worth useful legal winner thank yellow"; expect(validateMnemonic(mnem, wordlist)).toBeTruthy(); const seed1 = await mnemonicToSeed(mnem); const seed2 = mnemonicToSeedSync(mnem); expect(seed1).toEqual(seed2); }); test("varies with capitalization", () => { const mnem = "legal winner thank year wave sausage worth useful legal winner thank yellow"; // [w]inner const mNem = "legal Winner thank year wave sausage worth useful legal winner thank yellow"; // [W]inner expect(validateMnemonic(mnem, wordlist)).toBeTruthy(); expect(validateMnemonic(mNem, wordlist)).toBeFalsy(); const lowerSeed = mnemonicToSeedSync(mnem); const mixedSeed = mnemonicToSeedSync(mNem); expect(lowerSeed).not.toEqual(mixedSeed); }); test("fails with extra/missing spacing", () => { const mnem1 = "legal winner thank year wave sausage worth useful legal winner thank yellow"; // 2 spaces const mnem2 = "legalwinner thank year wave sausage worth useful legal winner thank yellow"; // missing space const mnem3 = " legalwinner thank year wave sausage worth useful legal winner thank yellow "; // untrimmed expect(validateMnemonic(mnem1, wordlist)).toBeFalsy(); expect(validateMnemonic(mnem2, wordlist)).toBeFalsy(); expect(validateMnemonic(mnem3, wordlist)).toBeFalsy(); expect(() => mnemonicToSeedSync(mnem1)).toThrow(); expect(() => mnemonicToSeedSync(mnem2)).toThrow(); expect(() => mnemonicToSeedSync(mnem3)).toThrow(); }); test("converts any words/order does not matter", () => { const mnem1 = "legal thank winner year wave sausage worth useful legal winner yellow thank"; // invalid checksum expect(validateMnemonic(mnem1, wordlist)).toBeFalsy(); expect(() => mnemonicToSeedSync(mnem1)).not.toThrow(); const mnem2 = "a b c d e f g h i j k l"; // 12 "words" expect(validateMnemonic(mnem2, wordlist)).toBeFalsy(); expect(() => mnemonicToSeedSync(mnem2)).not.toThrow(); const mnem3 = "lega winn than year wave saus wort usef lega winn than yell"; // first 4 expect(validateMnemonic(mnem3, wordlist)).toBeFalsy(); expect(() => mnemonicToSeedSync(mnem3)).not.toThrow(); expect(mnemonicToSeedSync(mnem1)).not.toEqual(mnemonicToSeedSync(mnem3)); }); }); ================================================ FILE: test/vitest/setup-file.js ================================================ // This file will be run before each test file import { createPinia, setActivePinia } from "pinia"; // Initialize Pinia globally at module load time console.log("Vitest setup file is running"); setActivePinia(createPinia()); // Runs immediately, before imports // Still keep beforeEach to reset Pinia state between tests import { beforeEach } from "vitest"; beforeEach(() => { console.log("Setting up Pinia"); setActivePinia(createPinia()); // Fresh instance for each test }); // Mock localStorage const localStorageMock = (function () { let store = {}; return { getItem: function (key) { return store[key] || null; }, setItem: function (key, value) { store[key] = value.toString(); }, removeItem: function (key) { delete store[key]; }, clear: function () { store = {}; }, }; })(); Object.defineProperty(window, "localStorage", { value: localStorageMock, writable: true, }); ================================================ FILE: tsconfig.json ================================================ { "extends": "@quasar/app-vite/tsconfig-preset", "compilerOptions": { "baseUrl": ".", "target": "ES2020", "paths": { "src/*": ["src/*"], "app/*": ["*"], "components/*": ["src/components/*"], "layouts/*": ["src/layouts/*"], "pages/*": ["src/pages/*"], "assets/*": ["src/assets/*"], "boot/*": ["src/boot/*"], "stores/*": ["src/stores/*"], "vue$": ["node_modules/vue/dist/vue.runtime.esm-bundler.js"] } } } ================================================ FILE: types/light-bolt11-decoder/index.d.ts ================================================ declare module "light-bolt11-decoder" { export interface DecodedSection { name?: string; value?: any; [key: string]: any; } export interface DecodedBolt11 { paymentRequest: string; sections: DecodedSection[]; } export function decode(paymentRequest: string): DecodedBolt11; } ================================================ FILE: vitest.config.js ================================================ import { defineConfig } from "vitest/config"; import vue from "@vitejs/plugin-vue"; import { quasar, transformAssetUrls } from "@quasar/vite-plugin"; import jsconfigPaths from "vite-jsconfig-paths"; // https://vitejs.dev/config/ export default defineConfig({ build: { target: "esnext", }, optimizeDeps: { esbuildOptions: { target: "esnext", }, }, test: { environment: "happy-dom", setupFiles: "test/vitest/setup-file.js", include: [ // Matches vitest tests in any subfolder of 'src' or into 'test/vitest/__tests__' // Matches all files with extension 'js', 'jsx', 'ts' and 'tsx' "src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}", "test/vitest/__tests__/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}", ], }, plugins: [ vue({ template: { transformAssetUrls }, }), quasar({ sassVariables: "src/quasar-variables.scss", }), jsconfigPaths(), ], });