diff --git a/.env.example b/.env.example index b39d33b4..2e414da1 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,4 @@ VITE_NETWORK= VITE_QLI_API_URL= VITE_QUBIC_RPC_URL= VITE_STATIC_API_URL= +VITE_EVENTS_API_URL= diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c3bbf531..4fd576da 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -57,6 +57,7 @@ jobs: github.ref == 'refs/heads/staging' && secrets.STATIC_API_URL_STAGING || secrets.STATIC_API_URL_DEV }} + VITE_EVENTS_API_URL: ${{ secrets.EVENTS_API_URL }} run: pnpm run build - name: 📤 Deploy to the corresponding environment diff --git a/dev-proxy.config.ts b/dev-proxy.config.ts index cf7f3718..29ff0faf 100644 --- a/dev-proxy.config.ts +++ b/dev-proxy.config.ts @@ -62,3 +62,9 @@ export const staticApiProxy = createProxyConfig( '/dev-proxy-static-api', 'STATIC-API-DEV-PROXY' ) + +export const eventsApiProxy = createProxyConfig( + 'https://dev01.qubic.org', + '/dev-proxy-events-api', + 'EVENTS-API-DEV-PROXY' +) diff --git a/package.json b/package.json index 410ecf8f..ba1ae785 100644 --- a/package.json +++ b/package.json @@ -25,9 +25,7 @@ "@qubic.ts/core": "^1.1.0", "@react-spring/web": "^9.7.5", "@reduxjs/toolkit": "^2.2.6", - "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/forms": "^0.5.7", - "@tailwindcss/typography": "^0.5.13", "axios": "^1.8.2", "clsx": "^2.1.1", "i18next": "^23.12.1", @@ -42,7 +40,6 @@ "react-router-dom": "^6.24.1", "react-syntax-highlighter": "^15.6.1", "react-tooltip": "^5.28.0", - "redux-logger": "^3.0.6", "tailwind-merge": "^2.4.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 63705b81..38596885 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,15 +26,9 @@ importers: '@reduxjs/toolkit': specifier: ^2.2.6 version: 2.2.6(react-redux@9.1.2(@types/react@18.3.3)(react@18.3.1)(redux@5.0.1))(react@18.3.1) - '@tailwindcss/aspect-ratio': - specifier: ^0.4.2 - version: 0.4.2(tailwindcss@3.4.4) '@tailwindcss/forms': specifier: ^0.5.7 version: 0.5.7(tailwindcss@3.4.4) - '@tailwindcss/typography': - specifier: ^0.5.13 - version: 0.5.13(tailwindcss@3.4.4) axios: specifier: ^1.8.2 version: 1.13.2 @@ -77,9 +71,6 @@ importers: react-tooltip: specifier: ^5.28.0 version: 5.28.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - redux-logger: - specifier: ^3.0.6 - version: 3.0.6 tailwind-merge: specifier: ^2.4.0 version: 2.4.0 @@ -998,21 +989,11 @@ packages: '@swc/helpers@0.5.12': resolution: {integrity: sha512-KMZNXiGibsW9kvZAO1Pam2JPTDBm+KSHMMHWdsyI/1DbIZjT2A6Gy3hblVXUMEDvUAKq+e0vL0X0o54owWji7g==} - '@tailwindcss/aspect-ratio@0.4.2': - resolution: {integrity: sha512-8QPrypskfBa7QIMuKHg2TA7BqES6vhBrDLOv8Unb6FcFyd3TjKbc6lcmb9UPQHxfl24sXoJ41ux/H7qQQvfaSQ==} - peerDependencies: - tailwindcss: '>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1' - '@tailwindcss/forms@0.5.7': resolution: {integrity: sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==} peerDependencies: tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1' - '@tailwindcss/typography@0.5.13': - resolution: {integrity: sha512-ADGcJ8dX21dVVHIwTRgzrcunY6YY9uSlAHHGVKvkA+vLc5qLwEszvKts40lx7z0qc4clpjclwLeK5rVCV2P/uw==} - peerDependencies: - tailwindcss: '>=3.0.0 || insiders' - '@tanstack/react-virtual@3.8.3': resolution: {integrity: sha512-9ICwbDUUzN99CJIGc373i8NLoj6zFTKI2Hlcmo0+lCSAhPQ5mxq4dGOMKmLYoEFyHcGQ64Bd6ZVbnPpM6lNK5w==} peerDependencies: @@ -1587,9 +1568,6 @@ packages: supports-color: optional: true - deep-diff@0.3.8: - resolution: {integrity: sha512-yVn6RZmHiGnxRKR9sJb3iVV2XTF1Ghh2DiWRZ3dMnGc43yUdWWF/kX6lQyk3+P84iprfWKU/8zFTrlkvtFm1ug==} - deep-equal@2.2.3: resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} engines: {node: '>= 0.4'} @@ -2641,9 +2619,6 @@ packages: lodash.capitalize@4.2.1: resolution: {integrity: sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==} - lodash.castarray@4.4.0: - resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} - lodash.escaperegexp@4.1.2: resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} @@ -3196,10 +3171,6 @@ packages: peerDependencies: postcss: ^8.2.14 - postcss-selector-parser@6.0.10: - resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} - engines: {node: '>=4'} - postcss-selector-parser@6.1.0: resolution: {integrity: sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==} engines: {node: '>=4'} @@ -3410,9 +3381,6 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} - redux-logger@3.0.6: - resolution: {integrity: sha512-JoCIok7bg/XpqA1JqCqXFypuqBbQzGQySrhFzewB7ThcnysTO30l4VCst86AuB9T9tuT03MAA56Jw2PNhRSNCg==} - redux-thunk@3.1.0: resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} peerDependencies: @@ -4928,23 +4896,11 @@ snapshots: dependencies: tslib: 2.6.3 - '@tailwindcss/aspect-ratio@0.4.2(tailwindcss@3.4.4)': - dependencies: - tailwindcss: 3.4.4 - '@tailwindcss/forms@0.5.7(tailwindcss@3.4.4)': dependencies: mini-svg-data-uri: 1.4.4 tailwindcss: 3.4.4 - '@tailwindcss/typography@0.5.13(tailwindcss@3.4.4)': - dependencies: - lodash.castarray: 4.4.0 - lodash.isplainobject: 4.0.6 - lodash.merge: 4.6.2 - postcss-selector-parser: 6.0.10 - tailwindcss: 3.4.4 - '@tanstack/react-virtual@3.8.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/virtual-core': 3.8.3 @@ -5586,8 +5542,6 @@ snapshots: dependencies: ms: 2.1.3 - deep-diff@0.3.8: {} - deep-equal@2.2.3: dependencies: array-buffer-byte-length: 1.0.1 @@ -6824,8 +6778,6 @@ snapshots: lodash.capitalize@4.2.1: {} - lodash.castarray@4.4.0: {} - lodash.escaperegexp@4.1.2: {} lodash.isplainobject@4.0.6: {} @@ -7243,11 +7195,6 @@ snapshots: postcss: 8.4.39 postcss-selector-parser: 6.1.0 - postcss-selector-parser@6.0.10: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - postcss-selector-parser@6.1.0: dependencies: cssesc: 3.0.0 @@ -7414,10 +7361,6 @@ snapshots: dependencies: picomatch: 2.3.1 - redux-logger@3.0.6: - dependencies: - deep-diff: 0.3.8 - redux-thunk@3.1.0(redux@5.0.1): dependencies: redux: 5.0.1 diff --git a/public/locales/ar/global.json b/public/locales/ar/global.json index ad8668b4..eaefb96c 100644 --- a/public/locales/ar/global.json +++ b/public/locales/ar/global.json @@ -1,9 +1,12 @@ { "address": "عنوان", "balance": "الرصيد", + "navigationMenu": "قائمة التنقل", "noResultsFound": "لم يتم العثور على نتائج", "noResultsFoundFor": "لم يتم العثور على نتائج لـ \"{{keyword}}\"", "searchPlaceholder": "البحث عن TXs، ticks، IDs، tokens، contracts، exchanges...", + "selectLanguage": "اختر اللغة", + "selectNetwork": "اختر الشبكة", "tick": "تيك", "transaction": "معاملة", "transactions": "معاملات" diff --git a/public/locales/ar/network-page.json b/public/locales/ar/network-page.json index f76f03e7..9a4f591b 100644 --- a/public/locales/ar/network-page.json +++ b/public/locales/ar/network-page.json @@ -1,192 +1,223 @@ { "activeAddresses": "العناوين النشطة", + "addAddress": "إضافة عنوان", "address": "العنوان", + "addressID": "معرّف العنوان", + "addressNotFoundError": "خطأ: لم نتمكن من العثور على العنوان الذي تبحث عنه. يرجى التحقق من العنوان والمحاولة مرة أخرى.", + "addressPlaceholder": "60 حرف عنوان", + "allProcedures": "All procedures", + "allTransactionsLoaded": "لقد شاهدت جميع المعاملات", "amount": "المبلغ", - "circulatingSupply": "العرض المتداول", + "amountAssetAny": "الكل", + "amountAssetOther": "أصول أخرى", + "amountAssetQubic": "QUBIC", + "amountAssetType": "الأصل", + "amount100Mto1B": "100 مليون - 1 مليار", + "amount1Bto10B": "1 مليار - 10 مليار", + "amount1Mto100M": "1 مليون - 100 مليون", + "amount1to1M": "1 - 1 مليون", + "amountFilterHint": "مبلغ المعاملة بـ QUBIC.", + "amountOver0": "> 0", + "amountOver10B": "> 10 مليار", + "applyFilters": "تطبيق الفلاتر", + "assetIssuer": "جهة إصدار الأصل", + "assets": "الأصول", + "assetsRichList": "قائمة الأغنياء بالأصول", + "assetsRichListDesc": "تعرض قائمة الأغنياء بالأصول أعلى الحائزين على كل أصل.", + "assetsRichListInvalidAssetOrIssuer": "خطأ: عنوان المُصدِر و/أو اسم الأصل غير صالح. يرجى التحقق من معلمات الرابط.", + "assetTransferWarning": "هذه المعاملة مرئية فقط في قائمة المعاملات لعنوان المصدر. عرضها في قائمة معاملات عنوان الوجهة غير مدعوم بعد في الإصدار الحالي.", + "billionShort": "مليار", + "blockchain": "Blockchain", "burnedSupply": "العرض المحترق", + "checkContractProposal": "عرض مقترح العقد", + "checkContractShareholders": "التحقق من مساهمي العقد", + "checkSourceCode": "عرض الشيفرة المصدرية", + "circulatingSupply": "العرض المتداول", + "clearAllFiltersTooltip": "انقر لإزالة جميع الفلاتر", + "collapseAll": "طي الكل", "complete": "مكتمل", + "contract": "عقد", + "contractIndex": "الفهرس", + "contractMessageType": "نوع رسالة العقد", + "contractName": "اسم العقد", + "copied": "تم النسخ", + "copyAddress": "نسخ العنوان", + "copyToClipboard": "نسخ إلى الحافظة", + "copyTransactionId": "نسخ معرّف المعاملة", "currentTick": "التيك الحالي", + "customRange": "نطاق مخصص", + "data": "البيانات", "dataStatus": "حالة البيانات", "date": "التاريخ", + "dateLast24Hours": "آخر 24 ساعة", + "dateLastHour": "الساعة الأخيرة", + "dateLastNDays": "آخر {{count}} يومًا", + "deductedAmount": "المبلغ المخصوم", + "defaultView": "العرض الافتراضي", "destination": "المستلم", + "destinationFilterHint": "بالنسبة لمعاملات العقود الذكية، هذا هو عنوان العقد وليس متلقي التوكن النهائي.", + "direction": "الاتجاه", + "directionAll": "الكل", + "directionIncoming": "واردة", + "directionIncomingTooltip": "المعاملات الواردة", + "directionOutgoing": "صادرة", + "directionOutgoingTooltip": "المعاملات الصادرة", + "duplicateAddress": "عنوان مكرر", "empty": "فارغ", + "endDate": "إلى تاريخ", + "endTick": "نهاية التيك", + "endTickTooLarge": "قيمة تيك النهاية كبيرة جداً.", "entityReportsFromRandomPeers": "تقارير من أقران عشوائيين", "epoch": "فترة", "epochTicks": "علامات الحقبة", + "errorLoadingPrice": "خطأ: فشل في تحميل السعر. يرجى تحديث الصفحة أو المحاولة لاحقًا.", "errorMessage": "بيتا - قد تكون البيانات غير مكتملة أو بها أخطاء", + "estimatedWait": "الوقت المقدر للانتظار", + "event": "حدث", + "eventDetails": "تفاصيل الحدث", + "eventNotFound": "الحدث غير موجود", + "events": "الأحداث", + "eventsBetaDisclaimer": "هذه الميزة قيد التطوير. يتم تضمين البيانات الحديثة فقط وقد يتم حذفها حتى يتوفر إصدار جاهز للإنتاج. نقاط نهاية API المستخدمة عرضة للتغيير في الإصدارات القادمة.", + "eventsFound": "{{count}} أحداث", + "eventsLoadFailed": "خطأ: فشل في تحميل الأحداث. يرجى المحاولة مرة أخرى.", + "eventType": "نوع الحدث", + "exchange": "تبادل", + "exchanges": "التبادلات", + "exchangesLoadFailed": "خطأ: فشل في تحميل التبادلات. يرجى تحديث الصفحة أو المحاولة لاحقًا.", + "exclude": "استبعاد", "executed": "تم التنفيذ", - "txStatusExecuted": "تم تنفيذ المعاملة", - "txStatusFailed": "فشل التحويل", + "expandAll": "توسيع الكل", + "exportJson": "تصدير JSON", + "fee": "رسوم", + "filterAll": "الكل", + "filterAllTransactions": "جميع المعاملات", + "filterApproved": "معتمد", + "filterApprovedTransactions": "المعاملات المعتمدة", + "filterButton": "تصفية", + "filterByCategory": "تصفية حسب الفئة", + "filters": "الفلاتر", + "filterTransfer": "التحويلات", + "filterTransferTransactions": "معاملات التحويل", + "hide": "إخفاء", "id": "معرّف", - "incomingTransfers": "التحويلات الواردة", + "include": "تضمين", "incomingAmount": "المبلغ الوارد", + "incomingTransfers": "التحويلات الواردة", "incomplete": "غير مكتمل", + "inputType": "نوع الإدخال", + "invalidAddressError": "تنسيق العنوان غير صحيح. يرجى التحقق من العنوان والمحاولة مرة أخرى.", + "invalidAddressFormat": "يجب أن يتكون العنوان من 60 حرفًا كبيرًا", + "invalidDateRange": "يجب أن يكون تاريخ البداية أقل من تاريخ النهاية", + "invalidDestinationIdentity": "عنوان الوجهة غير صالح: {{address}}", + "invalidRangeAmount": "نطاق غير صالح - يجب أن تكون قيمة البداية أقل من أو تساوي قيمة النهاية", + "invalidRangeInputType": "يجب أن تكون القيمة الدنيا أقل من أو تساوي القيمة القصوى", + "invalidSourceIdentity": "عنوان المصدر غير صالح: {{address}}", + "invalidTickRange": "لا يمكن أن يكون تيك البداية أكبر من تيك النهاية", + "invalidTransactionId": "معرّف المعاملة غير صالح. يرجى التحقق من المعرّف والمحاولة مرة أخرى.", + "issuer": "الجهة المُصدرة", "lastNTickQuality": "جودة النقطة في آخر 10K", "lastNTickQualityTooltip": "نسبة النقاط غير الفارغة في آخر 10K، بغض النظر عن الحقبة", "latest": "الأحدث", + "latestStatsLoadFailed": "خطأ: فشل في تحميل أحدث الإحصائيات. يرجى تحديث الصفحة أو المحاولة لاحقًا.", "latestTransfers": "أحدث التحويلات", + "loading": "جار التحميل", + "loadingTransactionsError": "خطأ: حدث خطأ أثناء تحميل المعاملات. يرجى المحاولة مرة أخرى لاحقًا.", + "loadMore": "تحميل المزيد", + "logDigest": "ملخص السجل", + "managedBy": "يُدار بواسطة {{contract}}", + "managingContractIndex": "فهرس عقد الإدارة", "marketCap": "القيمة السوقية", + "matchingEntities": "النتائج المطابقة", + "maxAddressesReached": "تم الوصول للحد الأقصى 5 عناوين", + "maxAmount": "الحد الأقصى للمبلغ", + "maxAmountTooLarge": "الحد الأقصى لقيمة المبلغ كبير جداً.", + "maxInputType": "الحد الأقصى", + "maxInputTypeTooLarge": "الحد الأقصى لقيمة نوع الإدخال كبير جداً.", + "maxResultsHint": "النتائج محدودة بـ {{count}}. استخدم الفلاتر لتضييق نطاق القائمة.", + "millionShort": "مليون", + "minAmount": "الحد الأدنى للمبلغ", + "minAmountTooLarge": "الحد الأدنى لقيمة المبلغ كبير جداً.", + "minInputType": "الحد الأدنى", + "minInputTypeTooLarge": "الحد الأدنى لقيمة نوع الإدخال كبير جداً.", + "name": "الاسم", + "noEntries": "لا توجد بيانات للعرض", + "noEvents": "لا توجد أحداث", "nonEmpty": "غير فارغ", + "noTransactions": "لا توجد معاملات", + "numberOfDecimalPlaces": "المنازل العشرية", "numberOfTransactions": "عدد المعاملات", - "outgoingTransfers": "التحويلات الصادرة", "outgoingAmount": "المبلغ الصادر", + "outgoingTransfers": "التحويلات الصادرة", "price": "السعر", + "qrcodeModalTitle": "رمز استجابة سريعة للعنوان", + "rank": "الترتيب", + "remainingAmount": "المبلغ المتبقي", + "removeAddress": "إزالة العنوان", + "resetFilters": "إعادة تعيين الفلاتر", + "richList": "قائمة الأثرياء", + "richListLoadFailed": "خطأ: فشل تحميل قائمة الأثرياء. يرجى تحديث الصفحة أو المحاولة مرة أخرى لاحقًا.", + "richListWarning": "يتم تحديث بيانات قائمة الأثرياء في بداية كل فترة", + "scShares": "أسهم العقود الذكية", "search": "بحث", + "selectAsset": "اختر الأصل", + "selectContract": "Select contract", + "selectUpTo": "اختر حتى {{count}}", + "shareholders": "المساهمون", + "sharesIssuer": "مُصدر الأسهم", + "show": "عرض", + "showingMaxTransactions": "عرض آخر {{count}} معاملة", + "showingMaxEvents": "عرض آخر {{count}} حدث", + "showLess": "عرض أقل", + "showMore": "عرض المزيد", + "showPerPagePrefix": "عرض", + "showPerPageSuffix": "لكل صفحة", + "showQRCode": "عرض رمز الاستجابة السريعة", "signature": "توقيع", + "smartContract": "عقد ذكي", + "smartContractCodeLoadError": "خطأ: فشل تحميل شيفرة العقد الذكي. يرجى تحديث الصفحة أو المحاولة لاحقًا.", + "smartContracts": "العقود الذكية", + "smartContractsLoadFailed": "خطأ: فشل في تحميل العقود الذكية. يرجى تحديث الصفحة أو المحاولة لاحقًا.", "source": "مصدر", "standard": "قياسي", - "txStatusSuccess": "تحويل ناجح", + "startDate": "من تاريخ", + "startTick": "بداية التيك", + "startTickTooLarge": "قيمة تيك البداية كبيرة جداً.", + "status": "Status", + "targetTick": "التيك المستهدف", + "thousandShort": "ألف", "tick": "تيك", + "tickCheckFailed": "تعذر التحقق من حالة التك. يرجى المحاولة مرة أخرى لاحقًا.", "tickLeader": "زعيم التيك", "tickQuality": "جودة النقطة في الحقبة", "tickQualityTooltip": "نسبة التكات غير الفارغة في هذه الحقبة", "ticks": "تيكات", + "ticksLoadFailed": "خطأ: فشل في تحميل التيكات. يرجى تحديث الصفحة أو المحاولة مرة أخرى لاحقًا. إذا كان اليوم هو الأربعاء، يرجى الانتظار حتى يبدأ العصر التالي.", "tickStatus": "حالة التيك", "ticksThisEpoch": "تيكات في الفترة الحالية (فارغة)", "ticksThisEpochTooltip": "إجمالي عدد التكات المُعالجة في هذه الحقبة وعدد التكات الفارغة بين قوسين", + "timestamp": "الطابع الزمني", + "token": "رمز", + "tokenIssuer": "{{name}} المُصدر", + "tokenLoadFailed": "خطأ: فشل في تحميل الرموز. يرجى تحديث الصفحة أو المحاولة لاحقًا.", + "tokens": "رموز", + "totalValueLocked": "إجمالي القيمة المقفلة (TVL)", + "transactionNotFound": "المعاملة غير موجودة", "transactionPreview": "معاينة المعاملات", "transactions": "معاملات", - "data": "البيانات", - "defaultView": "العرض الافتراضي", "unableToDecodeContractInput": "تعذّر فك ترميز مدخلات هذا العقد.", - "exportJson": "تصدير JSON", - "showMore": "عرض المزيد", - "showLess": "عرض أقل", - "type": "النوع", "unexecuted": "غير منفذ", "value": "القيمة", - "loadMore": "تحميل المزيد", - "noTransactions": "لا توجد معاملات", - "allTransactionsLoaded": "لقد شاهدت جميع المعاملات", - "loading": "جار التحميل", - "hide": "إخفاء", - "show": "عرض", - "transactionNotFound": "المعاملة غير موجودة", - "invalidTransactionId": "معرّف المعاملة غير صالح. يرجى التحقق من المعرّف والمحاولة مرة أخرى.", - "invalidAddressError": "تنسيق العنوان غير صحيح. يرجى التحقق من العنوان والمحاولة مرة أخرى.", - "addressNotFoundError": "خطأ: لم نتمكن من العثور على العنوان الذي تبحث عنه. يرجى التحقق من العنوان والمحاولة مرة أخرى.", - "loadingTransactionsError": "خطأ: حدث خطأ أثناء تحميل المعاملات. يرجى المحاولة مرة أخرى لاحقًا.", - "invalidSourceIdentity": "عنوان المصدر غير صالح: {{address}}", - "invalidDestinationIdentity": "عنوان الوجهة غير صالح: {{address}}", - "minInputTypeTooLarge": "الحد الأدنى لقيمة نوع الإدخال كبير جداً.", - "maxInputTypeTooLarge": "الحد الأقصى لقيمة نوع الإدخال كبير جداً.", - "minAmountTooLarge": "الحد الأدنى لقيمة المبلغ كبير جداً.", - "maxAmountTooLarge": "الحد الأقصى لقيمة المبلغ كبير جداً.", - "startTickTooLarge": "قيمة تيك البداية كبيرة جداً.", - "endTickTooLarge": "قيمة تيك النهاية كبيرة جداً.", - "richList": "قائمة الأثرياء", - "rank": "الترتيب", - "addressID": "معرّف العنوان", - "richListLoadFailed": "خطأ: فشل تحميل قائمة الأثرياء. يرجى تحديث الصفحة أو المحاولة مرة أخرى لاحقًا.", - "assetsRichListInvalidAssetOrIssuer": "خطأ: عنوان المُصدِر و/أو اسم الأصل غير صالح. يرجى التحقق من معلمات الرابط.", - "richListWarning": "يتم تحديث بيانات قائمة الأثرياء في بداية كل فترة", - "timestamp": "الطابع الزمني", - "fee": "رسوم", - "assetTransferWarning": "هذه المعاملة مرئية فقط في قائمة المعاملات لعنوان المصدر. عرضها في قائمة معاملات عنوان الوجهة غير مدعوم بعد في الإصدار الحالي.", - "ticksLoadFailed": "خطأ: فشل في تحميل التيكات. يرجى تحديث الصفحة أو المحاولة مرة أخرى لاحقًا. إذا كان اليوم هو الأربعاء، يرجى الانتظار حتى يبدأ العصر التالي.", - "showItemsPerPage": "عرض {{count}} عنصر", - "itemsPerPage": "العناصر لكل صفحة", - "latestStatsLoadFailed": "خطأ: فشل في تحميل أحدث الإحصائيات. يرجى تحديث الصفحة أو المحاولة لاحقًا.", - "errorLoadingPrice": "خطأ: فشل في تحميل السعر. يرجى تحديث الصفحة أو المحاولة لاحقًا.", - "exchange": "تبادل", - "exchanges": "التبادلات", - "exchangesLoadFailed": "خطأ: فشل في تحميل التبادلات. يرجى تحديث الصفحة أو المحاولة لاحقًا.", - "token": "رمز", - "tokens": "رموز", - "smartContract": "عقد ذكي", - "smartContracts": "العقود الذكية", - "tokenLoadFailed": "خطأ: فشل في تحميل الرموز. يرجى تحديث الصفحة أو المحاولة لاحقًا.", - "smartContractsLoadFailed": "خطأ: فشل في تحميل العقود الذكية. يرجى تحديث الصفحة أو المحاولة لاحقًا.", - "issuer": "الجهة المُصدرة", - "name": "الاسم", "wallets": "المحافظ", - "assets": "الأصول", - "totalValueLocked": "إجمالي القيمة المقفلة (TVL)", - "noEntries": "لا توجد بيانات للعرض", - "assetsRichList": "قائمة الأغنياء بالأصول", - "assetsRichListDesc": "تعرض قائمة الأغنياء بالأصول أعلى الحائزين على كل أصل.", - "contractName": "اسم العقد", - "shareholders": "المساهمون", - "checkContractShareholders": "التحقق من مساهمي العقد", - "checkSourceCode": "عرض الشيفرة المصدرية", - "checkContractProposal": "عرض مقترح العقد", - "smartContractCodeLoadError": "خطأ: فشل تحميل شيفرة العقد الذكي. يرجى تحديث الصفحة أو المحاولة لاحقًا.", - "contract": "عقد", - "copied": "تم النسخ", - "copyAddress": "نسخ العنوان", - "copyTransactionId": "نسخ معرّف المعاملة", - "copyToClipboard": "نسخ إلى الحافظة", - "showQRCode": "عرض رمز الاستجابة السريعة", - "qrcodeModalTitle": "رمز استجابة سريعة للعنوان", - "filterAllTransactions": "جميع المعاملات", - "filterTransferTransactions": "معاملات التحويل", - "filterApprovedTransactions": "المعاملات المعتمدة", - "filterAll": "الكل", - "filterTransfer": "التحويلات", - "filterApproved": "معتمد", - "filterByCategory": "تصفية حسب الفئة", - "selectAsset": "اختر الأصل", - "scShares": "أسهم العقود الذكية", - "sharesIssuer": "مُصدر الأسهم", - "contractIndex": "الفهرس", - "expandAll": "توسيع الكل", - "collapseAll": "طي الكل", - "managedBy": "يُدار بواسطة {{contract}}", "unknownContract": "عقد غير معروف", - "resetFilters": "إعادة تعيين الفلاتر", - "startTick": "بداية التيك", - "endTick": "نهاية التيك", - "startDate": "من تاريخ", - "endDate": "إلى تاريخ", - "invalidRangeAmount": "نطاق غير صالح - يجب أن تكون قيمة البداية أقل من أو تساوي قيمة النهاية", - "invalidRangeInputType": "يجب أن تكون القيمة الدنيا أقل من أو تساوي القيمة القصوى", - "invalidTickRange": "لا يمكن أن يكون تيك البداية أكبر من تيك النهاية", - "invalidDateRange": "يجب أن يكون تاريخ البداية أقل من تاريخ النهاية", - "invalidAddressFormat": "يجب أن يتكون العنوان من 60 حرفًا كبيرًا", - "destinationFilterHint": "بالنسبة لمعاملات العقود الذكية، هذا هو عنوان العقد وليس متلقي التوكن النهائي.", - "amountFilterHint": "مبلغ المعاملة بـ QUBIC.", - "customRange": "نطاق مخصص", - "minAmount": "الحد الأدنى للمبلغ", - "maxAmount": "الحد الأقصى للمبلغ", - "amountOver0": "> 0", - "amount1to1M": "1 - 1 مليون", - "amount1Mto100M": "1 مليون - 100 مليون", - "amount100Mto1B": "100 مليون - 1 مليار", - "amount1Bto10B": "1 مليار - 10 مليار", - "amountOver10B": "> 10 مليار", - "thousandShort": "ألف", - "millionShort": "مليون", - "billionShort": "مليار", - "dateLastHour": "الساعة الأخيرة", - "dateLast24Hours": "آخر 24 ساعة", - "dateLastNDays": "آخر {{count}} يومًا", - "addressPlaceholder": "60 حرف عنوان", - "clearAllFiltersTooltip": "انقر لإزالة جميع الفلاتر", - "filterButton": "تصفية", - "filters": "الفلاتر", - "applyFilters": "تطبيق الفلاتر", - "directionAll": "الكل", - "directionIncoming": "واردة", - "directionOutgoing": "صادرة", - "directionIncomingTooltip": "المعاملات الواردة", - "directionOutgoingTooltip": "المعاملات الصادرة", - "direction": "الاتجاه", - "inputType": "نوع الإدخال", - "minInputType": "الحد الأدنى", - "maxInputType": "الحد الأقصى", - "include": "تضمين", - "exclude": "استبعاد", - "addAddress": "إضافة عنوان", - "removeAddress": "إزالة العنوان", - "maxAddressesReached": "تم الوصول للحد الأقصى 5 عناوين", - "duplicateAddress": "عنوان مكرر", - "matchingEntities": "النتائج المطابقة", "transactionsFound": "{{count}} معاملة", - "showingMaxTransactions": "عرض آخر {{count}} معاملة", - "maxResultsHint": "النتائج محدودة بـ {{count}}. استخدم الفلاتر لتضييق نطاق القائمة.", - "waitingForTickTitle": "في انتظار معالجة التيك", + "txID": "TX ID", + "txStatusExecuted": "تم تنفيذ المعاملة", + "txStatusFailed": "فشل التحويل", + "txStatusSuccess": "تحويل ناجح", + "txType": "TX Type", + "unitOfMeasurement": "وحدة القياس", + "virtualTransactionBanner": "هذه معاملة افتراضية تم إنشاؤها بواسطة بروتوكول Qubic لأحداث دورة حياة العقود الذكية.", + "virtualTransactionNotFound": "المعاملة الافتراضية غير موجودة", "waitingForTickDesc": "هذه المعاملة مجدولة للتيك {{targetTick}}. ستكون متاحة بمجرد وصول الشبكة إلى ذلك التيك.", - "targetTick": "التيك المستهدف", - "estimatedWait": "الوقت المقدر للانتظار", - "tickCheckFailed": "تعذر التحقق من حالة التك. يرجى المحاولة مرة أخرى لاحقًا." + "waitingForTickTitle": "في انتظار معالجة التيك" } diff --git a/public/locales/de/global.json b/public/locales/de/global.json index 254bccc1..d041b27c 100644 --- a/public/locales/de/global.json +++ b/public/locales/de/global.json @@ -1,9 +1,12 @@ { "address": "Adresse", "balance": "Kontostand", + "navigationMenu": "Navigationsmenü", "noResultsFound": "Keine Ergebnisse Gefunden", "noResultsFoundFor": "Keine Ergebnisse gefunden für \"{{keyword}}\"", "searchPlaceholder": "TXs, Ticks, IDs, Tokens, Verträge, Börsen suchen...", + "selectLanguage": "Sprache auswählen", + "selectNetwork": "Netzwerk auswählen", "tick": "Tick", "transaction": "Transaktion", "transactions": "Transaktionen" diff --git a/public/locales/de/network-page.json b/public/locales/de/network-page.json index 6aa7d7d0..f485fe7b 100644 --- a/public/locales/de/network-page.json +++ b/public/locales/de/network-page.json @@ -1,192 +1,223 @@ { "activeAddresses": "Aktive Adressen", + "addAddress": "Adresse hinzufügen", "address": "Adresse", + "addressID": "Adress-ID", + "addressNotFoundError": "Fehler: Wir konnten die gesuchte Adresse nicht finden. Bitte überprüfen Sie die Adresse und versuchen Sie es erneut.", + "addressPlaceholder": "60 Zeichen Adresse", + "allProcedures": "All procedures", + "allTransactionsLoaded": "Sie haben alle Transaktionen gesehen", "amount": "Betrag", - "circulatingSupply": "Umlaufangebot", + "amountAssetAny": "Alle", + "amountAssetOther": "Andere Assets", + "amountAssetQubic": "QUBIC", + "amountAssetType": "Asset", + "amount100Mto1B": "100Mio - 1Mrd", + "amount1Bto10B": "1Mrd - 10Mrd", + "amount1Mto100M": "1Mio - 100Mio", + "amount1to1M": "1 - 1Mio", + "amountFilterHint": "Transaktionsbetrag in QUBIC.", + "amountOver0": "> 0", + "amountOver10B": "> 10Mrd", + "applyFilters": "Filter anwenden", + "assetIssuer": "Asset-Emittent", + "assets": "Vermögenswerte", + "assetsRichList": "Vermögens-Ranglisten", + "assetsRichListDesc": "Die Vermögens-Rangliste zeigt die größten Inhaber jedes Vermögenswerts.", + "assetsRichListInvalidAssetOrIssuer": "Fehler: Ungültige Emittentenadresse und/oder Asset-Name. Bitte überprüfen Sie die URL-Parameter.", + "assetTransferWarning": "Diese Transaktion ist nur in der Transaktionsliste der Quelladresse sichtbar. Die Anzeige in der Transaktionsliste der Zieladresse wird in der aktuellen Version noch nicht unterstützt.", + "billionShort": "Mrd", + "blockchain": "Blockchain", "burnedSupply": "Verbrannte Versorgung", + "checkContractProposal": "Vertragsvorschlag anzeigen", + "checkContractShareholders": "Vertragsaktionäre prüfen", + "checkSourceCode": "Quellcode anzeigen", + "circulatingSupply": "Umlaufangebot", + "clearAllFiltersTooltip": "Klicken, um alle Filter zu entfernen", + "collapseAll": "Alle einklappen", "complete": "Vollständig", + "contract": "Vertrag", + "contractIndex": "Index", + "contractMessageType": "Vertragsnachrichtentyp", + "contractName": "Vertragsname", + "copied": "Kopiert", + "copyAddress": "Adresse kopieren", + "copyToClipboard": "In die Zwischenablage kopieren", + "copyTransactionId": "TX-ID kopieren", "currentTick": "Aktueller Tick", + "customRange": "Benutzerdefinierter Bereich", + "data": "Daten", "dataStatus": "Datenstatus", "date": "Datum", + "dateLast24Hours": "Letzte 24 Stunden", + "dateLastHour": "Letzte Stunde", + "dateLastNDays": "Letzte {{count}} Tage", + "deductedAmount": "Abgezogener Betrag", + "defaultView": "Standardansicht", "destination": "Ziel", + "destinationFilterHint": "Bei Smart-Contract-Transaktionen ist dies die Vertragsadresse, nicht der endgültige Token-Empfänger.", + "direction": "Richtung", + "directionAll": "Alle", + "directionIncoming": "Eingehend", + "directionIncomingTooltip": "Eingehende Transaktionen", + "directionOutgoing": "Ausgehend", + "directionOutgoingTooltip": "Ausgehende Transaktionen", + "duplicateAddress": "Doppelte Adresse", "empty": "Leer", + "endDate": "Bis Datum", + "endTick": "End-Tick", + "endTickTooLarge": "Der End-Tick-Wert ist zu groß.", "entityReportsFromRandomPeers": "Berichte von zufälligen Peers", "epoch": "Epoche", "epochTicks": "Epoche-Ticks", + "errorLoadingPrice": "Fehler: Laden des Preises fehlgeschlagen. Bitte aktualisieren Sie die Seite oder versuchen Sie es später erneut.", "errorMessage": "Beta - Daten könnten unvollständig oder fehlerhaft sein", + "estimatedWait": "Geschätzte Wartezeit", + "event": "Ereignis", + "eventDetails": "Ereignisdetails", + "eventNotFound": "Ereignis nicht gefunden", + "events": "Ereignisse", + "eventsBetaDisclaimer": "Diese Funktion befindet sich in der Entwicklung. Es sind nur aktuelle Daten enthalten, die bis zur Veröffentlichung einer produktionsreifen Version gelöscht werden können. Die verwendeten API-Endpunkte können sich in kommenden Versionen ändern.", + "eventsFound": "{{count}} Ereignisse", + "eventsLoadFailed": "Fehler: Ereignisse konnten nicht geladen werden. Bitte versuchen Sie es erneut.", + "eventType": "Ereignistyp", + "exchange": "Börse", + "exchanges": "Börsen", + "exchangesLoadFailed": "Fehler: Laden der Börsen fehlgeschlagen. Bitte aktualisieren Sie die Seite oder versuchen Sie es später erneut.", + "exclude": "Ausschließen", "executed": "Ausgeführt", - "txStatusExecuted": "Transaktion ausgeführt", - "txStatusFailed": "Überweisung fehlgeschlagen", + "expandAll": "Alle erweitern", + "exportJson": "JSON exportieren", + "fee": "Gebühr", + "filterAll": "Alle", + "filterAllTransactions": "Alle Transaktionen", + "filterApproved": "Genehmigt", + "filterApprovedTransactions": "Genehmigte Transaktionen", + "filterButton": "Filtern", + "filterByCategory": "Nach Kategorie filtern", + "filters": "Filter", + "filterTransfer": "Überweisungen", + "filterTransferTransactions": "Überweisungen", + "hide": "Ausblenden", "id": "ID", - "incomingTransfers": "Eingehende Überweisungen", + "include": "Einschließen", "incomingAmount": "Eingehender Betrag", + "incomingTransfers": "Eingehende Überweisungen", "incomplete": "Unvollständig", + "inputType": "Eingabetyp", + "invalidAddressError": "Das Adressformat ist ungültig. Bitte überprüfen Sie die Adresse und versuchen Sie es erneut.", + "invalidAddressFormat": "Adresse muss aus 60 Großbuchstaben bestehen", + "invalidDateRange": "Startdatum muss vor dem Enddatum liegen", + "invalidDestinationIdentity": "Ungültige Zieladresse: {{address}}", + "invalidRangeAmount": "Ungültiger Bereich - Startwert muss kleiner oder gleich Endwert sein", + "invalidRangeInputType": "Mindestwert muss kleiner oder gleich dem Höchstwert sein", + "invalidSourceIdentity": "Ungültige Quelladresse: {{address}}", + "invalidTickRange": "Start-Tick darf nicht größer als End-Tick sein", + "invalidTransactionId": "Ungültige Transaktions-ID. Bitte überprüfen Sie die ID und versuchen Sie es erneut.", + "issuer": "Emittent", "lastNTickQuality": "Tick-Qualität der letzten 10K", "lastNTickQualityTooltip": "Prozentsatz der nicht-leeren Ticks in den letzten 10K Ticks, unabhängig von der Epoche", "latest": "Neueste", + "latestStatsLoadFailed": "Fehler: Laden der neuesten Statistiken fehlgeschlagen. Bitte aktualisieren Sie die Seite oder versuchen Sie es später erneut.", "latestTransfers": "Neueste Überweisungen", + "loading": "Laden", + "loadingTransactionsError": "Fehler: Beim Laden der Transaktionen ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.", + "loadMore": "Mehr laden", + "logDigest": "Log-Zusammenfassung", + "managedBy": "Verwaltet von {{contract}}", + "managingContractIndex": "Verwaltungsvertragsindex", "marketCap": "Marktkapitalisierung", + "matchingEntities": "Übereinstimmende Ergebnisse", + "maxAddressesReached": "Maximal 5 Adressen erreicht", + "maxAmount": "Höchstbetrag", + "maxAmountTooLarge": "Der maximale Betragswert ist zu groß.", + "maxInputType": "Max. Wert", + "maxInputTypeTooLarge": "Der maximale Input-Typ-Wert ist zu groß.", + "maxResultsHint": "Ergebnisse sind auf {{count}} begrenzt. Verwenden Sie Filter, um die Liste einzugrenzen.", + "millionShort": "Mio", + "minAmount": "Mindestbetrag", + "minAmountTooLarge": "Der minimale Betragswert ist zu groß.", + "minInputType": "Min. Wert", + "minInputTypeTooLarge": "Der minimale Input-Typ-Wert ist zu groß.", + "name": "Name", + "noEntries": "Keine Einträge zum Anzeigen", + "noEvents": "Keine Ereignisse", "nonEmpty": "Nicht leer", + "noTransactions": "Keine Transaktionen", + "numberOfDecimalPlaces": "Dezimalstellen", "numberOfTransactions": "Anzahl der Transaktionen", - "outgoingTransfers": "Ausgehende Überweisungen", "outgoingAmount": "Ausgehender Betrag", + "outgoingTransfers": "Ausgehende Überweisungen", "price": "Preis", + "qrcodeModalTitle": "Adress-QR-Code", + "rank": "Rang", + "remainingAmount": "Verbleibender Betrag", + "removeAddress": "Adresse entfernen", + "resetFilters": "Filter zurücksetzen", + "richList": "Reichenliste", + "richListLoadFailed": "Fehler: Reichenliste konnte nicht geladen werden. Bitte aktualisieren Sie die Seite oder versuchen Sie es später erneut.", + "richListWarning": "Die Daten der Reichenliste werden zu Beginn jeder Epoche aktualisiert", + "scShares": "Smart Contract Anteile", "search": "Suchen", + "selectAsset": "Asset auswählen", + "selectContract": "Select contract", + "selectUpTo": "Wähle bis zu {{count}}", + "shareholders": "Aktionäre", + "sharesIssuer": "Aktien-Emittent", + "show": "Anzeigen", + "showingMaxTransactions": "Zeige die letzten {{count}} Transaktionen", + "showingMaxEvents": "Zeige die letzten {{count}} Ereignisse", + "showLess": "Weniger anzeigen", + "showMore": "Mehr anzeigen", + "showPerPagePrefix": "Zeige", + "showPerPageSuffix": "pro Seite", + "showQRCode": "QR-Code anzeigen", "signature": "Signatur", + "smartContract": "Smart Contract", + "smartContractCodeLoadError": "Fehler: Der Smart-Contract-Code konnte nicht geladen werden. Bitte lade die Seite neu oder versuche es später erneut.", + "smartContracts": "Smart Contracts", + "smartContractsLoadFailed": "Fehler: Laden der Smart Contracts fehlgeschlagen. Bitte aktualisieren Sie die Seite oder versuchen Sie es später erneut.", "source": "Quelle", "standard": "Standard", - "txStatusSuccess": "Erfolgreiche Überweisung", + "startDate": "Von Datum", + "startTick": "Start-Tick", + "startTickTooLarge": "Der Start-Tick-Wert ist zu groß.", + "status": "Status", + "targetTick": "Ziel-Tick", + "thousandShort": "Tsd", "tick": "Tick", + "tickCheckFailed": "Tick-Status konnte nicht überprüft werden. Bitte versuchen Sie es später erneut.", "tickLeader": "Tick-Leader", "tickQuality": "Tick-Qualität der Epoche", "tickQualityTooltip": "Prozentsatz der nicht-leeren Ticks in dieser Epoche", "ticks": "Ticks", + "ticksLoadFailed": "Fehler: Konnte Ticks nicht laden. Bitte aktualisieren Sie die Seite oder versuchen Sie es später erneut. Wenn heute Mittwoch ist, warten Sie bitte, bis die nächste Epoche beginnt.", "tickStatus": "Tick-Status", "ticksThisEpoch": "Ticks in dieser Epoche (Leer)", "ticksThisEpochTooltip": "Gesamtzahl der in dieser Epoche verarbeiteten Ticks und die Anzahl leerer Ticks in Klammern", + "timestamp": "Zeitstempel", + "token": "Token", + "tokenIssuer": "{{name}} Emittent", + "tokenLoadFailed": "Fehler: Laden der Token fehlgeschlagen. Bitte aktualisieren Sie die Seite oder versuchen Sie es später erneut.", + "tokens": "Token", + "totalValueLocked": "Gesamter gesperrter Wert (TVL)", + "transactionNotFound": "Transaktion nicht gefunden", "transactionPreview": "Transaktionsvorschau", "transactions": "Transaktionen", - "data": "Daten", - "defaultView": "Standardansicht", "unableToDecodeContractInput": "Die Eingabedaten dieses Vertrags konnten nicht dekodiert werden.", - "exportJson": "JSON exportieren", - "showMore": "Mehr anzeigen", - "showLess": "Weniger anzeigen", - "type": "Typ", "unexecuted": "Nicht ausgeführt", "value": "Wert", - "loadMore": "Mehr laden", - "noTransactions": "Keine Transaktionen", - "allTransactionsLoaded": "Sie haben alle Transaktionen gesehen", - "loading": "Laden", - "hide": "Ausblenden", - "show": "Anzeigen", - "transactionNotFound": "Transaktion nicht gefunden", - "invalidTransactionId": "Ungültige Transaktions-ID. Bitte überprüfen Sie die ID und versuchen Sie es erneut.", - "invalidAddressError": "Das Adressformat ist ungültig. Bitte überprüfen Sie die Adresse und versuchen Sie es erneut.", - "addressNotFoundError": "Fehler: Wir konnten die gesuchte Adresse nicht finden. Bitte überprüfen Sie die Adresse und versuchen Sie es erneut.", - "loadingTransactionsError": "Fehler: Beim Laden der Transaktionen ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.", - "invalidSourceIdentity": "Ungültige Quelladresse: {{address}}", - "invalidDestinationIdentity": "Ungültige Zieladresse: {{address}}", - "minInputTypeTooLarge": "Der minimale Input-Typ-Wert ist zu groß.", - "maxInputTypeTooLarge": "Der maximale Input-Typ-Wert ist zu groß.", - "minAmountTooLarge": "Der minimale Betragswert ist zu groß.", - "maxAmountTooLarge": "Der maximale Betragswert ist zu groß.", - "startTickTooLarge": "Der Start-Tick-Wert ist zu groß.", - "endTickTooLarge": "Der End-Tick-Wert ist zu groß.", - "richList": "Reichenliste", - "rank": "Rang", - "addressID": "Adress-ID", - "richListLoadFailed": "Fehler: Reichenliste konnte nicht geladen werden. Bitte aktualisieren Sie die Seite oder versuchen Sie es später erneut.", - "assetsRichListInvalidAssetOrIssuer": "Fehler: Ungültige Emittentenadresse und/oder Asset-Name. Bitte überprüfen Sie die URL-Parameter.", - "richListWarning": "Die Daten der Reichenliste werden zu Beginn jeder Epoche aktualisiert", - "timestamp": "Zeitstempel", - "fee": "Gebühr", - "assetTransferWarning": "Diese Transaktion ist nur in der Transaktionsliste der Quelladresse sichtbar. Die Anzeige in der Transaktionsliste der Zieladresse wird in der aktuellen Version noch nicht unterstützt.", - "ticksLoadFailed": "Fehler: Konnte Ticks nicht laden. Bitte aktualisieren Sie die Seite oder versuchen Sie es später erneut. Wenn heute Mittwoch ist, warten Sie bitte, bis die nächste Epoche beginnt.", - "showItemsPerPage": "{{count}} Elemente anzeigen", - "itemsPerPage": "Elemente pro Seite", - "latestStatsLoadFailed": "Fehler: Laden der neuesten Statistiken fehlgeschlagen. Bitte aktualisieren Sie die Seite oder versuchen Sie es später erneut.", - "errorLoadingPrice": "Fehler: Laden des Preises fehlgeschlagen. Bitte aktualisieren Sie die Seite oder versuchen Sie es später erneut.", - "exchange": "Börse", - "exchanges": "Börsen", - "exchangesLoadFailed": "Fehler: Laden der Börsen fehlgeschlagen. Bitte aktualisieren Sie die Seite oder versuchen Sie es später erneut.", - "token": "Token", - "tokens": "Token", - "smartContract": "Smart Contract", - "smartContracts": "Smart Contracts", - "tokenLoadFailed": "Fehler: Laden der Token fehlgeschlagen. Bitte aktualisieren Sie die Seite oder versuchen Sie es später erneut.", - "smartContractsLoadFailed": "Fehler: Laden der Smart Contracts fehlgeschlagen. Bitte aktualisieren Sie die Seite oder versuchen Sie es später erneut.", - "issuer": "Emittent", - "name": "Name", "wallets": "Wallets", - "assets": "Vermögenswerte", - "totalValueLocked": "Gesamter gesperrter Wert (TVL)", - "noEntries": "Keine Einträge zum Anzeigen", - "assetsRichList": "Vermögens-Ranglisten", - "assetsRichListDesc": "Die Vermögens-Rangliste zeigt die größten Inhaber jedes Vermögenswerts.", - "contractName": "Vertragsname", - "shareholders": "Aktionäre", - "checkContractShareholders": "Vertragsaktionäre prüfen", - "checkSourceCode": "Quellcode anzeigen", - "checkContractProposal": "Vertragsvorschlag anzeigen", - "smartContractCodeLoadError": "Fehler: Der Smart-Contract-Code konnte nicht geladen werden. Bitte lade die Seite neu oder versuche es später erneut.", - "contract": "Vertrag", - "copied": "Kopiert", - "copyAddress": "Adresse kopieren", - "copyTransactionId": "TX-ID kopieren", - "copyToClipboard": "In die Zwischenablage kopieren", - "showQRCode": "QR-Code anzeigen", - "qrcodeModalTitle": "Adress-QR-Code", - "filterAllTransactions": "Alle Transaktionen", - "filterTransferTransactions": "Überweisungen", - "filterApprovedTransactions": "Genehmigte Transaktionen", - "filterAll": "Alle", - "filterTransfer": "Überweisungen", - "filterApproved": "Genehmigt", - "filterByCategory": "Nach Kategorie filtern", - "selectAsset": "Asset auswählen", - "scShares": "Smart Contract Anteile", - "sharesIssuer": "Aktien-Emittent", - "contractIndex": "Index", - "expandAll": "Alle erweitern", - "collapseAll": "Alle einklappen", - "managedBy": "Verwaltet von {{contract}}", "unknownContract": "Unbekannter Vertrag", - "resetFilters": "Filter zurücksetzen", - "startTick": "Start-Tick", - "endTick": "End-Tick", - "startDate": "Von Datum", - "endDate": "Bis Datum", - "invalidRangeAmount": "Ungültiger Bereich - Startwert muss kleiner oder gleich Endwert sein", - "invalidRangeInputType": "Mindestwert muss kleiner oder gleich dem Höchstwert sein", - "invalidTickRange": "Start-Tick darf nicht größer als End-Tick sein", - "invalidDateRange": "Startdatum muss vor dem Enddatum liegen", - "invalidAddressFormat": "Adresse muss aus 60 Großbuchstaben bestehen", - "destinationFilterHint": "Bei Smart-Contract-Transaktionen ist dies die Vertragsadresse, nicht der endgültige Token-Empfänger.", - "amountFilterHint": "Transaktionsbetrag in QUBIC.", - "customRange": "Benutzerdefinierter Bereich", - "minAmount": "Mindestbetrag", - "maxAmount": "Höchstbetrag", - "amountOver0": "> 0", - "amount1to1M": "1 - 1Mio", - "amount1Mto100M": "1Mio - 100Mio", - "amount100Mto1B": "100Mio - 1Mrd", - "amount1Bto10B": "1Mrd - 10Mrd", - "amountOver10B": "> 10Mrd", - "thousandShort": "Tsd", - "millionShort": "Mio", - "billionShort": "Mrd", - "dateLastHour": "Letzte Stunde", - "dateLast24Hours": "Letzte 24 Stunden", - "dateLastNDays": "Letzte {{count}} Tage", - "addressPlaceholder": "60 Zeichen Adresse", - "clearAllFiltersTooltip": "Klicken, um alle Filter zu entfernen", - "filterButton": "Filtern", - "filters": "Filter", - "applyFilters": "Filter anwenden", - "directionAll": "Alle", - "directionIncoming": "Eingehend", - "directionOutgoing": "Ausgehend", - "directionIncomingTooltip": "Eingehende Transaktionen", - "directionOutgoingTooltip": "Ausgehende Transaktionen", - "direction": "Richtung", - "inputType": "Eingabetyp", - "minInputType": "Min. Wert", - "maxInputType": "Max. Wert", - "include": "Einschließen", - "exclude": "Ausschließen", - "addAddress": "Adresse hinzufügen", - "removeAddress": "Adresse entfernen", - "maxAddressesReached": "Maximal 5 Adressen erreicht", - "duplicateAddress": "Doppelte Adresse", - "matchingEntities": "Übereinstimmende Ergebnisse", "transactionsFound": "{{count}} Transaktionen", - "showingMaxTransactions": "Zeige die letzten {{count}} Transaktionen", - "maxResultsHint": "Ergebnisse sind auf {{count}} begrenzt. Verwenden Sie Filter, um die Liste einzugrenzen.", - "waitingForTickTitle": "Warten auf Verarbeitung des Ticks", + "txID": "TX ID", + "txStatusExecuted": "Transaktion ausgeführt", + "txStatusFailed": "Überweisung fehlgeschlagen", + "txStatusSuccess": "Erfolgreiche Überweisung", + "txType": "TX Type", + "unitOfMeasurement": "Maßeinheit", + "virtualTransactionBanner": "Dies ist eine virtuelle Transaktion, die vom Qubic-Protokoll für Lebenszyklus-Ereignisse von Smart Contracts erzeugt wurde.", + "virtualTransactionNotFound": "Virtuelle Transaktion nicht gefunden", "waitingForTickDesc": "Diese Transaktion ist für Tick {{targetTick}} geplant. Sie wird verfügbar sein, sobald das Netzwerk diesen Tick erreicht.", - "targetTick": "Ziel-Tick", - "estimatedWait": "Geschätzte Wartezeit", - "tickCheckFailed": "Tick-Status konnte nicht überprüft werden. Bitte versuchen Sie es später erneut." + "waitingForTickTitle": "Warten auf Verarbeitung des Ticks" } diff --git a/public/locales/en/global.json b/public/locales/en/global.json index 506a5ff5..ebac64d2 100644 --- a/public/locales/en/global.json +++ b/public/locales/en/global.json @@ -1,9 +1,12 @@ { "address": "Address", "balance": "Balance", + "navigationMenu": "Navigation menu", "noResultsFound": "No Results Found", "noResultsFoundFor": "No results found for \"{{keyword}}\"", "searchPlaceholder": "Search TXs, ticks, IDs, tokens, contracts, exchanges...", + "selectLanguage": "Select language", + "selectNetwork": "Select network", "tick": "Tick", "transaction": "Transaction", "transactions": "Transactions" diff --git a/public/locales/en/network-page.json b/public/locales/en/network-page.json index 6a5ff37d..0b0884e6 100644 --- a/public/locales/en/network-page.json +++ b/public/locales/en/network-page.json @@ -1,192 +1,223 @@ { "activeAddresses": "Active Addresses", + "addAddress": "Add Address", "address": "Address", + "addressID": "Address ID", + "addressNotFoundError": "Error: We could not find the address you are looking for. Please check the address and try again.", + "addressPlaceholder": "60 characters address", + "allProcedures": "All procedures", + "allTransactionsLoaded": "You have seen all transactions", "amount": "Amount", - "circulatingSupply": "Circulating Supply", + "amountAssetAny": "Any", + "amountAssetOther": "Other Assets", + "amountAssetQubic": "QUBIC", + "amountAssetType": "Asset", + "amount100Mto1B": "100M - 1B", + "amount1Bto10B": "1B - 10B", + "amount1Mto100M": "1M - 100M", + "amount1to1M": "1 - 1M", + "amountFilterHint": "Transaction amount in QUBIC.", + "amountOver0": "> 0", + "amountOver10B": "> 10B", + "applyFilters": "Apply Filters", + "assetIssuer": "Asset Issuer", + "assets": "Assets", + "assetsRichList": "Assets Rich Lists", + "assetsRichListDesc": "The assets rich list shows the top holders of each asset.", + "assetsRichListInvalidAssetOrIssuer": "Error: Invalid issuer address and/or asset name. Please check the URL parameters.", + "assetTransferWarning": "This transaction is visible only in the list of transactions for the source address. Displaying it in the destination address's list of transactions is not supported yet in the current version.", + "billionShort": "B", + "blockchain": "Blockchain", "burnedSupply": "Burned Supply", + "checkContractProposal": "Check Contract Proposal", + "checkContractShareholders": "Check Contract Shareholders", + "checkSourceCode": "Check Source Code", + "circulatingSupply": "Circulating Supply", + "clearAllFiltersTooltip": "Click to remove all filters", + "collapseAll": "Collapse All", "complete": "Complete", + "contract": "Contract", + "contractIndex": "Index", + "contractMessageType": "Contract Message Type", + "contractName": "Contract Name", + "copied": "Copied", + "copyAddress": "Copy Address", + "copyToClipboard": "Copy to clipboard", + "copyTransactionId": "Copy TX ID", "currentTick": "Current Tick", + "customRange": "Custom range", + "data": "Data", "dataStatus": "Data Status", "date": "Date", + "dateLast24Hours": "Last 24 hours", + "dateLastHour": "Last hour", + "dateLastNDays": "Last {{count}} days", + "deductedAmount": "Deducted Amount", + "defaultView": "Default View", "destination": "Destination", + "destinationFilterHint": "For smart contract transactions, this is the contract address, not the final token recipient.", + "direction": "Direction", + "directionAll": "All", + "directionIncoming": "Incoming", + "directionIncomingTooltip": "Incoming transactions", + "directionOutgoing": "Outgoing", + "directionOutgoingTooltip": "Outgoing transactions", + "duplicateAddress": "Duplicate address", "empty": "Empty", + "endDate": "To Date", + "endTick": "End Tick", + "endTickTooLarge": "End tick value is too large.", "entityReportsFromRandomPeers": "Reports from random peers", "epoch": "Epoch", "epochTicks": "Epoch Ticks", + "errorLoadingPrice": "Error: Failed to load price. Please refresh the page or try again later.", "errorMessage": "Beta - Data may be incomplete or erroneous", + "estimatedWait": "Estimated wait", + "event": "Event", + "eventDetails": "Event Details", + "eventNotFound": "Event not found", + "events": "Events", + "eventsBetaDisclaimer": "This feature is in development. Only recent data is included and may be purged until a production-ready version is available. The API endpoints used are subject to change in upcoming releases.", + "eventsFound": "{{count}} events", + "eventsLoadFailed": "Failed to load events", + "eventType": "Event Type", + "exchange": "Exchange", + "exchanges": "Exchanges", + "exchangesLoadFailed": "Error: Failed to load exchanges. Please refresh the page or try again later.", + "exclude": "Exclude", "executed": "Executed", - "txStatusExecuted": "Transaction executed", - "txStatusFailed": "Transfer failed", + "expandAll": "Expand All", + "exportJson": "Export JSON", + "fee": "Fee", + "filterAll": "All", + "filterAllTransactions": "All transactions", + "filterApproved": "Approved", + "filterApprovedTransactions": "Approved transactions", + "filterButton": "Filter", + "filterByCategory": "Filter by category", + "filters": "Filters", + "filterTransfer": "Transfers", + "filterTransferTransactions": "Transfer transactions", + "hide": "Hide", "id": "ID", - "incomingTransfers": "Incoming Transfers", + "include": "Include", "incomingAmount": "Incoming Amount", + "incomingTransfers": "Incoming Transfers", "incomplete": "Incomplete", + "inputType": "Input Type", + "invalidAddressError": "The address format is invalid. Please check the address and try again.", + "invalidAddressFormat": "Address must be 60 uppercase letters", + "invalidDateRange": "Start date must be less than end date", + "invalidDestinationIdentity": "Invalid destination address: {{address}}", + "invalidRangeAmount": "Min amount must be less than or equal to max amount", + "invalidRangeInputType": "Min value must be less than or equal to max value", + "invalidSourceIdentity": "Invalid source address: {{address}}", + "invalidTickRange": "Start tick cannot be greater than end tick", + "invalidTransactionId": "Invalid ID format. Please check the transaction ID and try again.", + "issuer": "Issuer", "lastNTickQuality": "Last 10K Tick Quality", "lastNTickQualityTooltip": "Percentage of non-empty ticks in the last 10K ticks, regardless of epoch", "latest": "Latest", + "latestStatsLoadFailed": "Error: Failed to load latest stats. Please refresh the page or try again later.", "latestTransfers": "Latest Transfers", + "loading": "Loading", + "loadingTransactionsError": "Error: Something went wrong while loading transactions. Please try again later.", + "loadMore": "Load More", + "logDigest": "Log Digest", + "managedBy": "Managed by {{contract}}", + "managingContractIndex": "Managing Contract Index", "marketCap": "Market Cap", + "matchingEntities": "Matching Results", + "maxAddressesReached": "Maximum 5 addresses reached", + "maxAmount": "Max Amount", + "maxAmountTooLarge": "Max amount value is too large.", + "maxInputType": "Max Value", + "maxInputTypeTooLarge": "Max input type value is too large.", + "maxResultsHint": "Results are limited to {{count}}. Use filters to narrow down the list.", + "millionShort": "M", + "minAmount": "Min Amount", + "minAmountTooLarge": "Min amount value is too large.", + "minInputType": "Min Value", + "minInputTypeTooLarge": "Min input type value is too large.", + "name": "Name", + "noEntries": "No entries to show", + "noEvents": "No Events", "nonEmpty": "Non-empty", + "noTransactions": "No Transactions", + "numberOfDecimalPlaces": "Decimal Places", "numberOfTransactions": "Number of Transactions", - "outgoingTransfers": "Outgoing Transfers", "outgoingAmount": "Outgoing Amount", + "outgoingTransfers": "Outgoing Transfers", "price": "Price", + "qrcodeModalTitle": "Address QR Code", + "rank": "Rank", + "remainingAmount": "Remaining Amount", + "removeAddress": "Remove address", + "resetFilters": "Reset Filters", + "richList": "Rich List", + "richListLoadFailed": "Error: Failed to load rich list. Please refresh the page or try again later.", + "richListWarning": "Rich list data is updated at the beginning of each epoch", + "scShares": "Smart Contract Shares", "search": "Search", + "selectAsset": "Select asset", + "selectContract": "Select contract", + "selectUpTo": "Select up to {{count}}", + "shareholders": "Shareholders", + "sharesIssuer": "Shares Issuer", + "show": "Show", + "showingMaxTransactions": "Showing the last {{count}} transactions", + "showingMaxEvents": "Showing the last {{count}} events", + "showLess": "Show less", + "showMore": "Show more", + "showPerPagePrefix": "Show", + "showPerPageSuffix": "per page", + "showQRCode": "Show QR Code", "signature": "Signature", + "smartContract": "Smart Contract", + "smartContractCodeLoadError": "Error: Failed to load smart contract code. Please refresh the page or try again later.", + "smartContracts": "Smart Contracts", + "smartContractsLoadFailed": "Error: Failed to load smart contracts. Please refresh the page or try again later.", "source": "Source", "standard": "Standard", - "txStatusSuccess": "Successful transfer", + "startDate": "From Date", + "startTick": "Start Tick", + "startTickTooLarge": "Start tick value is too large.", + "status": "Status", + "targetTick": "Target Tick", + "thousandShort": "K", "tick": "Tick", + "tickCheckFailed": "Unable to check tick status. Please try again later.", "tickLeader": "Tick Leader", "tickQuality": "Epoch Tick Quality", "tickQualityTooltip": "Percentage of non-empty ticks this epoch", "ticks": "Ticks", + "ticksLoadFailed": "Error: Failed to load ticks. Please refresh the page or try again later. If it's Wednesday, please wait until the next epoch begins.", "tickStatus": "Tick Status", "ticksThisEpoch": "Ticks This Epoch (Empty)", "ticksThisEpochTooltip": "Total number of ticks processed this epoch and empty ticks count in parentheses", + "timestamp": "Timestamp", + "token": "Token", + "tokenIssuer": "{{name}} Issuer", + "tokenLoadFailed": "Error: Failed to load tokens. Please refresh the page or try again later.", + "tokens": "Tokens", + "totalValueLocked": "Total Value Locked (TVL)", + "transactionNotFound": "Transaction not found", "transactionPreview": "Transaction Preview", "transactions": "Transactions", - "data": "Data", - "defaultView": "Default View", "unableToDecodeContractInput": "Unable to decode this contract input.", - "exportJson": "Export JSON", - "showMore": "Show more", - "showLess": "Show less", - "type": "Type", "unexecuted": "Unexecuted", "value": "Value", - "loadMore": "Load More", - "noTransactions": "No Transactions", - "allTransactionsLoaded": "You have seen all transactions", - "loading": "Loading", - "hide": "Hide", - "show": "Show", - "transactionNotFound": "Transaction not found", - "invalidTransactionId": "Invalid ID format. Please check the transaction ID and try again.", - "invalidAddressError": "The address format is invalid. Please check the address and try again.", - "addressNotFoundError": "Error: We could not find the address you are looking for. Please check the address and try again.", - "loadingTransactionsError": "Error: Something went wrong while loading transactions. Please try again later.", - "invalidSourceIdentity": "Invalid source address: {{address}}", - "invalidDestinationIdentity": "Invalid destination address: {{address}}", - "minInputTypeTooLarge": "Min input type value is too large.", - "maxInputTypeTooLarge": "Max input type value is too large.", - "minAmountTooLarge": "Min amount value is too large.", - "maxAmountTooLarge": "Max amount value is too large.", - "startTickTooLarge": "Start tick value is too large.", - "endTickTooLarge": "End tick value is too large.", - "richList": "Rich List", - "rank": "Rank", - "addressID": "Address ID", - "richListLoadFailed": "Error: Failed to load rich list. Please refresh the page or try again later.", - "assetsRichListInvalidAssetOrIssuer": "Error: Invalid issuer address and/or asset name. Please check the URL parameters.", - "richListWarning": "Rich list data is updated at the beginning of each epoch", - "timestamp": "Timestamp", - "fee": "Fee", - "assetTransferWarning": "This transaction is visible only in the list of transactions for the source address. Displaying it in the destination address's list of transactions is not supported yet in the current version.", - "ticksLoadFailed": "Error: Failed to load ticks. Please refresh the page or try again later. If it's Wednesday, please wait until the next epoch begins.", - "showItemsPerPage": "Show {{count}} items", - "itemsPerPage": "Items per page", - "latestStatsLoadFailed": "Error: Failed to load latest stats. Please refresh the page or try again later.", - "errorLoadingPrice": "Error: Failed to load price. Please refresh the page or try again later.", - "exchange": "Exchange", - "exchanges": "Exchanges", - "exchangesLoadFailed": "Error: Failed to load exchanges. Please refresh the page or try again later.", - "token": "Token", - "tokens": "Tokens", - "smartContract": "Smart Contract", - "smartContracts": "Smart Contracts", - "tokenLoadFailed": "Error: Failed to load tokens. Please refresh the page or try again later.", - "smartContractsLoadFailed": "Error: Failed to load smart contracts. Please refresh the page or try again later.", - "issuer": "Issuer", - "name": "Name", "wallets": "Wallets", - "assets": "Assets", - "totalValueLocked": "Total Value Locked (TVL)", - "noEntries": "No entries to show", - "assetsRichList": "Assets Rich Lists", - "assetsRichListDesc": "The assets rich list shows the top holders of each asset.", - "contractName": "Contract Name", - "shareholders": "Shareholders", - "checkContractShareholders": "Check Contract Shareholders", - "checkSourceCode": "Check Source Code", - "checkContractProposal": "Check Contract Proposal", - "smartContractCodeLoadError": "Error: Failed to load smart contract code. Please refresh the page or try again later.", - "contract": "Contract", - "copied": "Copied", - "copyAddress": "Copy Address", - "copyTransactionId": "Copy TX ID", - "copyToClipboard": "Copy to clipboard", - "showQRCode": "Show QR Code", - "qrcodeModalTitle": "Address QR Code", - "filterAllTransactions": "All transactions", - "filterTransferTransactions": "Transfer transactions", - "filterApprovedTransactions": "Approved transactions", - "filterAll": "All", - "filterTransfer": "Transfers", - "filterApproved": "Approved", - "filterByCategory": "Filter by category", - "selectAsset": "Select asset", - "scShares": "Smart Contract Shares", - "sharesIssuer": "Shares Issuer", - "contractIndex": "Index", - "expandAll": "Expand All", - "collapseAll": "Collapse All", - "managedBy": "Managed by {{contract}}", "unknownContract": "Unknown Contract", - "resetFilters": "Reset Filters", - "startTick": "Start Tick", - "endTick": "End Tick", - "startDate": "From Date", - "endDate": "To Date", - "invalidRangeAmount": "Min amount must be less than or equal to max amount", - "invalidRangeInputType": "Min value must be less than or equal to max value", - "invalidTickRange": "Start tick cannot be greater than end tick", - "invalidDateRange": "Start date must be less than end date", - "invalidAddressFormat": "Address must be 60 uppercase letters", - "destinationFilterHint": "For smart contract transactions, this is the contract address, not the final token recipient.", - "amountFilterHint": "Transaction amount in QUBIC.", - "customRange": "Custom range", - "minAmount": "Min Amount", - "maxAmount": "Max Amount", - "minInputType": "Min Value", - "maxInputType": "Max Value", - "amountOver0": "> 0", - "amount1to1M": "1 - 1M", - "amount1Mto100M": "1M - 100M", - "amount100Mto1B": "100M - 1B", - "amount1Bto10B": "1B - 10B", - "amountOver10B": "> 10B", - "thousandShort": "K", - "millionShort": "M", - "billionShort": "B", - "dateLastHour": "Last hour", - "dateLast24Hours": "Last 24 hours", - "dateLastNDays": "Last {{count}} days", - "addressPlaceholder": "60 characters address", - "clearAllFiltersTooltip": "Click to remove all filters", - "filterButton": "Filter", - "filters": "Filters", - "applyFilters": "Apply Filters", - "directionAll": "All", - "directionIncoming": "Incoming", - "directionOutgoing": "Outgoing", - "directionIncomingTooltip": "Incoming transactions", - "directionOutgoingTooltip": "Outgoing transactions", - "direction": "Direction", - "inputType": "Input Type", - "include": "Include", - "exclude": "Exclude", - "addAddress": "Add Address", - "removeAddress": "Remove address", - "maxAddressesReached": "Maximum 5 addresses reached", - "duplicateAddress": "Duplicate address", - "matchingEntities": "Matching Results", "transactionsFound": "{{count}} transactions", - "showingMaxTransactions": "Showing the last {{count}} transactions", - "maxResultsHint": "Results are limited to {{count}}. Use filters to narrow down the list.", - "waitingForTickTitle": "Waiting for tick to be processed", + "txID": "TX ID", + "txStatusExecuted": "Transaction executed", + "txStatusFailed": "Transfer failed", + "txStatusSuccess": "Successful transfer", + "txType": "TX Type", + "unitOfMeasurement": "Unit of Measurement", + "virtualTransactionBanner": "This is a virtual transaction generated by the Qubic protocol for smart contract lifecycle events.", + "virtualTransactionNotFound": "Virtual transaction not found", "waitingForTickDesc": "This transaction is scheduled for tick {{targetTick}}. It will be available once the network reaches that tick.", - "targetTick": "Target Tick", - "estimatedWait": "Estimated wait", - "tickCheckFailed": "Unable to check tick status. Please try again later." + "waitingForTickTitle": "Waiting for tick to be processed" } diff --git a/public/locales/es/global.json b/public/locales/es/global.json index eedeed85..d5c10b83 100644 --- a/public/locales/es/global.json +++ b/public/locales/es/global.json @@ -1,9 +1,12 @@ { "address": "Dirección", "balance": "Saldo", + "navigationMenu": "Menú de navegación", "noResultsFound": "No Se Encontraron Resultados", "noResultsFoundFor": "No se encontraron resultados para \"{{keyword}}\"", "searchPlaceholder": "Buscar TXs, ticks, IDs, tokens, contratos, exchanges...", + "selectLanguage": "Seleccionar idioma", + "selectNetwork": "Seleccionar red", "tick": "Tick", "transaction": "Transacción", "transactions": "Transacciones" diff --git a/public/locales/es/network-page.json b/public/locales/es/network-page.json index d16fb4ff..df9863a6 100644 --- a/public/locales/es/network-page.json +++ b/public/locales/es/network-page.json @@ -1,192 +1,223 @@ { "activeAddresses": "Direcciones activas", + "addAddress": "Agregar dirección", "address": "Dirección", + "addressID": "ID de Dirección", + "addressNotFoundError": "Error: No pudimos encontrar la dirección que estás buscando. Verifica la dirección y vuelve a intentarlo.", + "addressPlaceholder": "60 caracteres dirección", + "allProcedures": "All procedures", + "allTransactionsLoaded": "Has visto todas las transacciones", "amount": "Cantidad", - "circulatingSupply": "Acciones en circulación", + "amountAssetAny": "Todos", + "amountAssetOther": "Otros Activos", + "amountAssetQubic": "QUBIC", + "amountAssetType": "Activo", + "amount100Mto1B": "100M - 1MM", + "amount1Bto10B": "1MM - 10MM", + "amount1Mto100M": "1M - 100M", + "amount1to1M": "1 - 1M", + "amountFilterHint": "Monto de la transacción en QUBIC.", + "amountOver0": "> 0", + "amountOver10B": "> 10MM", + "applyFilters": "Aplicar filtros", + "assetIssuer": "Emisor del Activo", + "assets": "Activos", + "assetsRichList": "Listas de Riqueza de Activos", + "assetsRichListDesc": "La lista de riqueza muestra los principales poseedores de cada activo.", + "assetsRichListInvalidAssetOrIssuer": "Error: Dirección del emisor y/o nombre del activo inválidos. Por favor, verifique los parámetros de la URL.", + "assetTransferWarning": "Esta transacción es visible solo en la lista de transacciones de la dirección de origen. Mostrarla en la lista de transacciones de la dirección de destino aún no está soportado en la versión actual.", + "billionShort": "MM", + "blockchain": "Blockchain", "burnedSupply": "Suministro Quemado", + "checkContractProposal": "Ver propuesta del contrato", + "checkContractShareholders": "Ver accionistas del contrato", + "checkSourceCode": "Ver código fuente", + "circulatingSupply": "Acciones en circulación", + "clearAllFiltersTooltip": "Haga clic para eliminar todos los filtros", + "collapseAll": "Contraer todo", "complete": "Completo", + "contract": "Contrato", + "contractIndex": "Índice", + "contractMessageType": "Tipo de mensaje del contrato", + "contractName": "Nombre del contrato", + "copied": "Copiado", + "copyAddress": "Copiar dirección", + "copyToClipboard": "Copiar al portapapeles", + "copyTransactionId": "Copiar ID de TX", "currentTick": "Tick actual", + "customRange": "Rango personalizado", + "data": "Datos", "dataStatus": "Estado de los datos", "date": "Fecha", + "dateLast24Hours": "Últimas 24 horas", + "dateLastHour": "Última hora", + "dateLastNDays": "Últimos {{count}} días", + "deductedAmount": "Monto Deducido", + "defaultView": "Vista predeterminada", "destination": "Destinatario", + "destinationFilterHint": "Para transacciones de contratos inteligentes, esta es la dirección del contrato, no el destinatario final del token.", + "direction": "Dirección", + "directionAll": "Todas", + "directionIncoming": "Entrante", + "directionIncomingTooltip": "Transacciones entrantes", + "directionOutgoing": "Saliente", + "directionOutgoingTooltip": "Transacciones salientes", + "duplicateAddress": "Dirección duplicada", "empty": "Vacíos", + "endDate": "Hasta fecha", + "endTick": "Tick final", + "endTickTooLarge": "El valor del tick final es demasiado grande.", "entityReportsFromRandomPeers": "Reportes de pares aleatorios", "epoch": "Época", "epochTicks": "Ticks de época", + "errorLoadingPrice": "Error: No se pudo cargar el precio. Por favor, actualiza la página o inténtalo de nuevo más tarde.", "errorMessage": "Beta - Los datos podrían estar incompletos o tener errores", + "estimatedWait": "Tiempo estimado de espera", + "event": "Evento", + "eventDetails": "Detalles del Evento", + "eventNotFound": "Evento no encontrado", + "events": "Eventos", + "eventsBetaDisclaimer": "Esta función está en desarrollo. Solo se incluyen datos recientes y pueden ser eliminados hasta que esté disponible una versión lista para producción. Los endpoints de API utilizados están sujetos a cambios en próximas versiones.", + "eventsFound": "{{count}} eventos", + "eventsLoadFailed": "Error: No se pudieron cargar los eventos. Por favor, inténtelo de nuevo.", + "eventType": "Tipo de evento", + "exchange": "Intercambio", + "exchanges": "Intercambios", + "exchangesLoadFailed": "Error: No se pudieron cargar los intercambios. Por favor, actualiza la página o inténtalo de nuevo más tarde.", + "exclude": "Excluir", "executed": "Ejecutado", - "txStatusExecuted": "Transacción ejecutada", - "txStatusFailed": "Transferencia fallida", + "expandAll": "Expandir todo", + "exportJson": "Exportar JSON", + "fee": "Tarifa", + "filterAll": "Todas", + "filterAllTransactions": "Todas las transacciones", + "filterApproved": "Aprobadas", + "filterApprovedTransactions": "Transacciones aprobadas", + "filterButton": "Filtrar", + "filterByCategory": "Filtrar por categoría", + "filters": "Filtros", + "filterTransfer": "Transferencias", + "filterTransferTransactions": "Transferencias", + "hide": "Ocultar", "id": "ID", - "incomingTransfers": "Transferencias entrantes", + "include": "Incluir", "incomingAmount": "Cantidad entrante", + "incomingTransfers": "Transferencias entrantes", "incomplete": "Incompleto", + "inputType": "Tipo de entrada", + "invalidAddressError": "El formato de la dirección no es válido. Verifica la dirección y vuelve a intentarlo.", + "invalidAddressFormat": "La dirección debe contener 60 letras mayúsculas", + "invalidDateRange": "La fecha inicial debe ser anterior a la fecha final", + "invalidDestinationIdentity": "Dirección de destino inválida: {{address}}", + "invalidRangeAmount": "Rango inválido - el valor inicial debe ser menor o igual al valor final", + "invalidRangeInputType": "El valor mínimo debe ser menor o igual al valor máximo", + "invalidSourceIdentity": "Dirección de origen inválida: {{address}}", + "invalidTickRange": "El tick inicial no puede ser mayor que el tick final", + "invalidTransactionId": "ID de transacción inválido. Verifica la ID y vuelve a intentarlo.", + "issuer": "Emisor", "lastNTickQuality": "Calidad del Tick de los Últimos 10K", "lastNTickQualityTooltip": "Porcentaje de ticks no vacíos en los últimos 10K ticks, independientemente de la época", "latest": "Último", + "latestStatsLoadFailed": "Error: No se pudieron cargar las estadísticas más recientes. Por favor, actualiza la página o inténtalo de nuevo más tarde.", "latestTransfers": "Últimas transferencias", + "loading": "Cargando", + "loadingTransactionsError": "Error: Algo ha ido mal al cargar las transacciones. Por favor, inténtalo de nuevo más tarde.", + "loadMore": "Cargar más", + "logDigest": "Resumen del Registro", + "managedBy": "Gestionado por {{contract}}", + "managingContractIndex": "Índice de Contrato de Gestión", "marketCap": "Capitalización de mercado", + "matchingEntities": "Resultados coincidentes", + "maxAddressesReached": "Máximo 5 direcciones alcanzado", + "maxAmount": "Monto máximo", + "maxAmountTooLarge": "El valor máximo del monto es demasiado grande.", + "maxInputType": "Valor máx.", + "maxInputTypeTooLarge": "El valor máximo del tipo de entrada es demasiado grande.", + "maxResultsHint": "Los resultados están limitados a {{count}}. Use filtros para refinar la lista.", + "millionShort": "M", + "minAmount": "Monto mínimo", + "minAmountTooLarge": "El valor mínimo del monto es demasiado grande.", + "minInputType": "Valor mín.", + "minInputTypeTooLarge": "El valor mínimo del tipo de entrada es demasiado grande.", + "name": "Nombre", + "noEntries": "No hay entradas para mostrar", + "noEvents": "No hay eventos", "nonEmpty": "No vacío", + "noTransactions": "No hay transacciones", + "numberOfDecimalPlaces": "Decimales", "numberOfTransactions": "Número de transacciones", - "outgoingTransfers": "Transferencias salientes", "outgoingAmount": "Cantidad saliente", + "outgoingTransfers": "Transferencias salientes", "price": "Precio", + "qrcodeModalTitle": "Código QR de la dirección", + "rank": "Rango", + "remainingAmount": "Monto Restante", + "removeAddress": "Eliminar dirección", + "resetFilters": "Restablecer filtros", + "richList": "Lista de Ricos", + "richListLoadFailed": "Error: No se pudo cargar la lista de ricos. Por favor, actualice la página o intente nuevamente más tarde.", + "richListWarning": "Los datos de la lista de ricos se actualizan al comienzo de cada época", + "scShares": "Acciones de Contratos Inteligentes", "search": "Buscar", + "selectAsset": "Seleccionar activo", + "selectContract": "Select contract", + "selectUpTo": "Selecciona hasta {{count}}", + "shareholders": "Accionistas", + "sharesIssuer": "Emisor de Acciones", + "show": "Mostrar", + "showingMaxTransactions": "Mostrando las últimas {{count}} transacciones", + "showingMaxEvents": "Mostrando los últimos {{count}} eventos", + "showLess": "Mostrar menos", + "showMore": "Mostrar más", + "showPerPagePrefix": "Mostrar", + "showPerPageSuffix": "por página", + "showQRCode": "Mostrar código QR", "signature": "Firma", + "smartContract": "Contrato inteligente", + "smartContractCodeLoadError": "Error: No se pudo cargar el código del contrato inteligente. Por favor, actualiza la página o inténtalo más tarde.", + "smartContracts": "Contratos inteligentes", + "smartContractsLoadFailed": "Error: No se pudieron cargar los contratos inteligentes. Por favor, actualiza la página o inténtalo de nuevo más tarde.", "source": "Fuente", "standard": "Estándar", - "txStatusSuccess": "Transferencia exitosa", + "startDate": "Desde fecha", + "startTick": "Tick inicial", + "startTickTooLarge": "El valor del tick inicial es demasiado grande.", + "status": "Status", + "targetTick": "Tick objetivo", + "thousandShort": "mil", "tick": "Tick", + "tickCheckFailed": "No se pudo verificar el estado del tick. Por favor, inténtelo de nuevo más tarde.", "tickLeader": "Tick líder", "tickQuality": "Calidad del Tick de la Época", "tickQualityTooltip": "Porcentaje de ticks no vacíos en esta época", "ticks": "Ticks", + "ticksLoadFailed": "Error: No se pudieron cargar los ticks. Por favor, actualiza la página o inténtalo de nuevo más tarde. Si es miércoles, espera hasta que comience la próxima época.", "tickStatus": "Estado del tick", "ticksThisEpoch": "Ticks en la época actual (Vacíos)", "ticksThisEpochTooltip": "Número total de ticks procesados en este epoch y la cantidad de ticks vacíos entre paréntesis", + "timestamp": "Fecha", + "token": "Token", + "tokenIssuer": "{{name}} Emisor", + "tokenLoadFailed": "Error: No se pudieron cargar los tokens. Por favor, actualiza la página o inténtalo de nuevo más tarde.", + "tokens": "Tokens", + "totalValueLocked": "Valor Total Bloqueado (TVL)", + "transactionNotFound": "Transacción no encontrada", "transactionPreview": "Vista previa de las transacciones", "transactions": "Transacciones", - "data": "Datos", - "defaultView": "Vista predeterminada", "unableToDecodeContractInput": "No se pudo decodificar la entrada de este contrato.", - "exportJson": "Exportar JSON", - "showMore": "Mostrar más", - "showLess": "Mostrar menos", - "type": "Tipo", "unexecuted": "No ejecutado", "value": "Valor", - "loadMore": "Cargar más", - "noTransactions": "No hay transacciones", - "allTransactionsLoaded": "Has visto todas las transacciones", - "loading": "Cargando", - "hide": "Ocultar", - "show": "Mostrar", - "transactionNotFound": "Transacción no encontrada", - "invalidTransactionId": "ID de transacción inválido. Verifica la ID y vuelve a intentarlo.", - "invalidAddressError": "El formato de la dirección no es válido. Verifica la dirección y vuelve a intentarlo.", - "addressNotFoundError": "Error: No pudimos encontrar la dirección que estás buscando. Verifica la dirección y vuelve a intentarlo.", - "loadingTransactionsError": "Error: Algo ha ido mal al cargar las transacciones. Por favor, inténtalo de nuevo más tarde.", - "invalidSourceIdentity": "Dirección de origen inválida: {{address}}", - "invalidDestinationIdentity": "Dirección de destino inválida: {{address}}", - "minInputTypeTooLarge": "El valor mínimo del tipo de entrada es demasiado grande.", - "maxInputTypeTooLarge": "El valor máximo del tipo de entrada es demasiado grande.", - "minAmountTooLarge": "El valor mínimo del monto es demasiado grande.", - "maxAmountTooLarge": "El valor máximo del monto es demasiado grande.", - "startTickTooLarge": "El valor del tick inicial es demasiado grande.", - "endTickTooLarge": "El valor del tick final es demasiado grande.", - "richList": "Lista de Ricos", - "rank": "Rango", - "addressID": "ID de Dirección", - "richListLoadFailed": "Error: No se pudo cargar la lista de ricos. Por favor, actualice la página o intente nuevamente más tarde.", - "assetsRichListInvalidAssetOrIssuer": "Error: Dirección del emisor y/o nombre del activo inválidos. Por favor, verifique los parámetros de la URL.", - "richListWarning": "Los datos de la lista de ricos se actualizan al comienzo de cada época", - "timestamp": "Fecha", - "fee": "Tarifa", - "assetTransferWarning": "Esta transacción es visible solo en la lista de transacciones de la dirección de origen. Mostrarla en la lista de transacciones de la dirección de destino aún no está soportado en la versión actual.", - "ticksLoadFailed": "Error: No se pudieron cargar los ticks. Por favor, actualiza la página o inténtalo de nuevo más tarde. Si es miércoles, espera hasta que comience la próxima época.", - "showItemsPerPage": "Mostrar {{count}} elementos", - "itemsPerPage": "Elementos por página", - "latestStatsLoadFailed": "Error: No se pudieron cargar las estadísticas más recientes. Por favor, actualiza la página o inténtalo de nuevo más tarde.", - "errorLoadingPrice": "Error: No se pudo cargar el precio. Por favor, actualiza la página o inténtalo de nuevo más tarde.", - "exchange": "Intercambio", - "exchanges": "Intercambios", - "exchangesLoadFailed": "Error: No se pudieron cargar los intercambios. Por favor, actualiza la página o inténtalo de nuevo más tarde.", - "token": "Token", - "tokens": "Tokens", - "smartContract": "Contrato inteligente", - "smartContracts": "Contratos inteligentes", - "tokenLoadFailed": "Error: No se pudieron cargar los tokens. Por favor, actualiza la página o inténtalo de nuevo más tarde.", - "smartContractsLoadFailed": "Error: No se pudieron cargar los contratos inteligentes. Por favor, actualiza la página o inténtalo de nuevo más tarde.", - "issuer": "Emisor", - "name": "Nombre", "wallets": "Carteras", - "assets": "Activos", - "totalValueLocked": "Valor Total Bloqueado (TVL)", - "noEntries": "No hay entradas para mostrar", - "assetsRichList": "Listas de Riqueza de Activos", - "assetsRichListDesc": "La lista de riqueza muestra los principales poseedores de cada activo.", - "contractName": "Nombre del contrato", - "shareholders": "Accionistas", - "checkContractShareholders": "Ver accionistas del contrato", - "checkSourceCode": "Ver código fuente", - "checkContractProposal": "Ver propuesta del contrato", - "smartContractCodeLoadError": "Error: No se pudo cargar el código del contrato inteligente. Por favor, actualiza la página o inténtalo más tarde.", - "contract": "Contrato", - "copied": "Copiado", - "copyAddress": "Copiar dirección", - "copyTransactionId": "Copiar ID de TX", - "copyToClipboard": "Copiar al portapapeles", - "showQRCode": "Mostrar código QR", - "qrcodeModalTitle": "Código QR de la dirección", - "filterAllTransactions": "Todas las transacciones", - "filterTransferTransactions": "Transferencias", - "filterApprovedTransactions": "Transacciones aprobadas", - "filterAll": "Todas", - "filterTransfer": "Transferencias", - "filterApproved": "Aprobadas", - "filterByCategory": "Filtrar por categoría", - "selectAsset": "Seleccionar activo", - "scShares": "Acciones de Contratos Inteligentes", - "sharesIssuer": "Emisor de Acciones", - "contractIndex": "Índice", - "expandAll": "Expandir todo", - "collapseAll": "Contraer todo", - "managedBy": "Gestionado por {{contract}}", "unknownContract": "Contrato desconocido", - "resetFilters": "Restablecer filtros", - "startTick": "Tick inicial", - "endTick": "Tick final", - "startDate": "Desde fecha", - "endDate": "Hasta fecha", - "invalidRangeAmount": "Rango inválido - el valor inicial debe ser menor o igual al valor final", - "invalidRangeInputType": "El valor mínimo debe ser menor o igual al valor máximo", - "invalidTickRange": "El tick inicial no puede ser mayor que el tick final", - "invalidDateRange": "La fecha inicial debe ser anterior a la fecha final", - "invalidAddressFormat": "La dirección debe contener 60 letras mayúsculas", - "destinationFilterHint": "Para transacciones de contratos inteligentes, esta es la dirección del contrato, no el destinatario final del token.", - "amountFilterHint": "Monto de la transacción en QUBIC.", - "customRange": "Rango personalizado", - "minAmount": "Monto mínimo", - "maxAmount": "Monto máximo", - "amountOver0": "> 0", - "amount1to1M": "1 - 1M", - "amount1Mto100M": "1M - 100M", - "amount100Mto1B": "100M - 1MM", - "amount1Bto10B": "1MM - 10MM", - "amountOver10B": "> 10MM", - "thousandShort": "mil", - "millionShort": "M", - "billionShort": "MM", - "dateLastHour": "Última hora", - "dateLast24Hours": "Últimas 24 horas", - "dateLastNDays": "Últimos {{count}} días", - "addressPlaceholder": "60 caracteres dirección", - "clearAllFiltersTooltip": "Haga clic para eliminar todos los filtros", - "filterButton": "Filtrar", - "filters": "Filtros", - "applyFilters": "Aplicar filtros", - "directionAll": "Todas", - "directionIncoming": "Entrante", - "directionOutgoing": "Saliente", - "directionIncomingTooltip": "Transacciones entrantes", - "directionOutgoingTooltip": "Transacciones salientes", - "direction": "Dirección", - "inputType": "Tipo de entrada", - "minInputType": "Valor mín.", - "maxInputType": "Valor máx.", - "include": "Incluir", - "exclude": "Excluir", - "addAddress": "Agregar dirección", - "removeAddress": "Eliminar dirección", - "maxAddressesReached": "Máximo 5 direcciones alcanzado", - "duplicateAddress": "Dirección duplicada", - "matchingEntities": "Resultados coincidentes", "transactionsFound": "{{count}} transacciones", - "showingMaxTransactions": "Mostrando las últimas {{count}} transacciones", - "maxResultsHint": "Los resultados están limitados a {{count}}. Use filtros para refinar la lista.", - "waitingForTickTitle": "Esperando a que el tick sea procesado", + "txID": "TX ID", + "txStatusExecuted": "Transacción ejecutada", + "txStatusFailed": "Transferencia fallida", + "txStatusSuccess": "Transferencia exitosa", + "txType": "TX Type", + "unitOfMeasurement": "Unidad de Medida", + "virtualTransactionBanner": "Esta es una transacción virtual generada por el protocolo Qubic para eventos del ciclo de vida de contratos inteligentes.", + "virtualTransactionNotFound": "Transacción virtual no encontrada", "waitingForTickDesc": "Esta transacción está programada para el tick {{targetTick}}. Estará disponible una vez que la red alcance ese tick.", - "targetTick": "Tick objetivo", - "estimatedWait": "Tiempo estimado de espera", - "tickCheckFailed": "No se pudo verificar el estado del tick. Por favor, inténtelo de nuevo más tarde." + "waitingForTickTitle": "Esperando a que el tick sea procesado" } diff --git a/public/locales/fr/global.json b/public/locales/fr/global.json index 349143c6..3732c106 100644 --- a/public/locales/fr/global.json +++ b/public/locales/fr/global.json @@ -1,9 +1,12 @@ { "address": "Adresse", "balance": "Solde", + "navigationMenu": "Menu de navigation", "noResultsFound": "Aucun Résultat Trouvé", "noResultsFoundFor": "Aucun résultat trouvé pour \"{{keyword}}\"", "searchPlaceholder": "Rechercher TXs, ticks, IDs, tokens, contrats, exchanges...", + "selectLanguage": "Sélectionner la langue", + "selectNetwork": "Sélectionner le réseau", "tick": "Tick", "transaction": "Transaction", "transactions": "Transactions" diff --git a/public/locales/fr/network-page.json b/public/locales/fr/network-page.json index 8a555624..2d85c6ab 100644 --- a/public/locales/fr/network-page.json +++ b/public/locales/fr/network-page.json @@ -1,192 +1,223 @@ { "activeAddresses": "Adresses actives", + "addAddress": "Ajouter une adresse", "address": "Adresse", + "addressID": "ID d'adresse", + "addressNotFoundError": "Erreur : Nous n'avons pas pu trouver l'adresse que vous recherchez. Veuillez vérifier l'adresse et réessayer.", + "addressPlaceholder": "60 car. adresse", + "allProcedures": "All procedures", + "allTransactionsLoaded": "Vous avez vu toutes les transactions", "amount": "Montant", - "circulatingSupply": "Offre en circulation", + "amountAssetAny": "Tous", + "amountAssetOther": "Autres actifs", + "amountAssetQubic": "QUBIC", + "amountAssetType": "Actif", + "amount100Mto1B": "100M - 1Md", + "amount1Bto10B": "1Md - 10Md", + "amount1Mto100M": "1M - 100M", + "amount1to1M": "1 - 1M", + "amountFilterHint": "Montant de la transaction en QUBIC.", + "amountOver0": "> 0", + "amountOver10B": "> 10Md", + "applyFilters": "Appliquer les filtres", + "assetIssuer": "Émetteur de l'Actif", + "assets": "Actifs", + "assetsRichList": "Liste des détenteurs d’actifs", + "assetsRichListDesc": "La liste des détenteurs montre les plus grands détenteurs de chaque actif.", + "assetsRichListInvalidAssetOrIssuer": "Erreur : Adresse de l'émetteur et/ou nom de l'actif invalides. Veuillez vérifier les paramètres de l'URL.", + "assetTransferWarning": "Cette transaction est visible uniquement dans la liste des transactions de l'adresse source. L'afficher dans la liste des transactions de l'adresse de destination n'est pas encore pris en charge dans la version actuelle.", + "billionShort": "Md", + "blockchain": "Blockchain", "burnedSupply": "Offre Brûlée", + "checkContractProposal": "Voir la proposition de contrat", + "checkContractShareholders": "Voir les actionnaires du contrat", + "checkSourceCode": "Voir le code source", + "circulatingSupply": "Offre en circulation", + "clearAllFiltersTooltip": "Cliquez pour supprimer tous les filtres", + "collapseAll": "Tout réduire", "complete": "Complet", + "contract": "Contrat", + "contractIndex": "Index", + "contractMessageType": "Type de message du contrat", + "contractName": "Nom du contrat", + "copied": "Copié", + "copyAddress": "Copier l'adresse", + "copyToClipboard": "Copier dans le presse-papiers", + "copyTransactionId": "Copier l'ID de TX", "currentTick": "Tick actuel", + "customRange": "Plage personnalisée", + "data": "Données", "dataStatus": "État des données", "date": "Date", + "dateLast24Hours": "Dernières 24 heures", + "dateLastHour": "Dernière heure", + "dateLastNDays": "{{count}} derniers jours", + "deductedAmount": "Montant déduit", + "defaultView": "Vue par défaut", "destination": "Destinataire", + "destinationFilterHint": "Pour les transactions de contrats intelligents, il s'agit de l'adresse du contrat, pas du destinataire final du jeton.", + "direction": "Direction", + "directionAll": "Tous", + "directionIncoming": "Entrant", + "directionIncomingTooltip": "Transactions entrantes", + "directionOutgoing": "Sortant", + "directionOutgoingTooltip": "Transactions sortantes", + "duplicateAddress": "Adresse en double", "empty": "Vide", + "endDate": "Date de fin", + "endTick": "Tick de fin", + "endTickTooLarge": "La valeur du tick de fin est trop grande.", "entityReportsFromRandomPeers": "Rapports de pairs aléatoires", "epoch": "Époque", "epochTicks": "Ticks d'époque", + "errorLoadingPrice": "Erreur : Échec du chargement du prix. Veuillez actualiser la page ou réessayer plus tard.", "errorMessage": "Bêta - Les données peuvent être incomplètes ou erronées", + "estimatedWait": "Temps d'attente estimé", + "event": "Événement", + "eventDetails": "Détails de l'événement", + "eventNotFound": "Événement non trouvé", + "events": "Événements", + "eventsBetaDisclaimer": "Cette fonctionnalité est en cours de développement. Seules les données récentes sont incluses et peuvent être supprimées jusqu'à ce qu'une version prête pour la production soit disponible. Les endpoints API utilisés sont susceptibles de changer dans les prochaines versions.", + "eventsFound": "{{count}} événements", + "eventsLoadFailed": "Erreur : Échec du chargement des événements. Veuillez réessayer.", + "eventType": "Type d'événement", + "exchange": "Échange", + "exchanges": "Échanges", + "exchangesLoadFailed": "Erreur : Échec du chargement des échanges. Veuillez actualiser la page ou réessayer plus tard.", + "exclude": "Exclure", "executed": "Exécuté", - "txStatusExecuted": "Transaction exécutée", - "txStatusFailed": "Transfert échoué", + "expandAll": "Tout développer", + "exportJson": "Exporter JSON", + "fee": "Frais", + "filterAll": "Toutes", + "filterAllTransactions": "Toutes les transactions", + "filterApproved": "Approuvées", + "filterApprovedTransactions": "Transactions approuvées", + "filterButton": "Filtrer", + "filterByCategory": "Filtrer par catégorie", + "filters": "Filtres", + "filterTransfer": "Transferts", + "filterTransferTransactions": "Transactions de transfert", + "hide": "Masquer", "id": "ID", - "incomingTransfers": "Transferts entrants", + "include": "Inclure", "incomingAmount": "Montant entrant", + "incomingTransfers": "Transferts entrants", "incomplete": "Incomplet", + "inputType": "Type d'entrée", + "invalidAddressError": "Le format de l'adresse n'est pas valide. Veuillez vérifier l'adresse et réessayer.", + "invalidAddressFormat": "L'adresse doit contenir 60 lettres majuscules", + "invalidDateRange": "La date de début doit être antérieure à la date de fin", + "invalidDestinationIdentity": "Adresse de destination invalide: {{address}}", + "invalidRangeAmount": "Plage invalide - la valeur de départ doit être inférieure ou égale à la valeur de fin", + "invalidRangeInputType": "La valeur minimale doit être inférieure ou égale à la valeur maximale", + "invalidSourceIdentity": "Adresse source invalide: {{address}}", + "invalidTickRange": "Le tick de début ne peut pas être supérieur au tick de fin", + "invalidTransactionId": "ID de transaction invalide. Veuillez vérifier l'ID et réessayer.", + "issuer": "Émetteur", "lastNTickQuality": "Qualité des ticks des 10K derniers", "lastNTickQualityTooltip": "Pourcentage de ticks non vides dans les 10K derniers ticks, quel que soit l'epoch", "latest": "Dernier", + "latestStatsLoadFailed": "Erreur : Échec du chargement des statistiques récentes. Veuillez actualiser la page ou réessayer plus tard.", "latestTransfers": "Derniers transferts", + "loading": "Chargement", + "loadingTransactionsError": "Erreur : Quelque chose a mal tourné lors du chargement des transactions. Veuillez réessayer plus tard.", + "loadMore": "Charger plus", + "logDigest": "Résumé du journal", + "managedBy": "Géré par {{contract}}", + "managingContractIndex": "Indice du Contrat de Gestion", "marketCap": "Capitalisation boursière", + "matchingEntities": "Résultats correspondants", + "maxAddressesReached": "Maximum 5 adresses atteint", + "maxAmount": "Montant maximum", + "maxAmountTooLarge": "La valeur maximale du montant est trop grande.", + "maxInputType": "Valeur max.", + "maxInputTypeTooLarge": "La valeur maximale du type d'entrée est trop grande.", + "maxResultsHint": "Les résultats sont limités à {{count}}. Utilisez des filtres pour affiner la liste.", + "millionShort": "M", + "minAmount": "Montant minimum", + "minAmountTooLarge": "La valeur minimale du montant est trop grande.", + "minInputType": "Valeur min.", + "minInputTypeTooLarge": "La valeur minimale du type d'entrée est trop grande.", + "name": "Nom", + "noEntries": "Aucune entrée à afficher", + "noEvents": "Aucun événement", "nonEmpty": "Non vide", + "noTransactions": "Aucune transaction", + "numberOfDecimalPlaces": "Décimales", "numberOfTransactions": "Nombre de transactions", - "outgoingTransfers": "Transferts sortants", "outgoingAmount": "Montant sortant", + "outgoingTransfers": "Transferts sortants", "price": "Prix", + "qrcodeModalTitle": "Code QR de l'adresse", + "rank": "Rang", + "remainingAmount": "Montant restant", + "removeAddress": "Supprimer l'adresse", + "resetFilters": "Réinitialiser les filtres", + "richList": "Liste des riches", + "richListLoadFailed": "Erreur : Impossible de charger la liste des riches. Veuillez actualiser la page ou réessayer plus tard.", + "richListWarning": "Les données de la liste des riches sont mises à jour au début de chaque époque", + "scShares": "Parts de Contrats Intelligents", "search": "Rechercher", + "selectAsset": "Sélectionner un actif", + "selectContract": "Select contract", + "selectUpTo": "Sélectionnez jusqu'à {{count}}", + "shareholders": "Actionnaires", + "sharesIssuer": "Émetteur d'Actions", + "show": "Afficher", + "showingMaxTransactions": "Affichage des {{count}} dernières transactions", + "showingMaxEvents": "Affichage des {{count}} derniers événements", + "showLess": "Afficher moins", + "showMore": "Afficher plus", + "showPerPagePrefix": "Afficher", + "showPerPageSuffix": "par page", + "showQRCode": "Afficher le code QR", "signature": "Signature", + "smartContract": "Contrat intelligent", + "smartContractCodeLoadError": "Erreur : Échec du chargement du code du contrat intelligent. Veuillez actualiser la page ou réessayer plus tard.", + "smartContracts": "Contrats intelligents", + "smartContractsLoadFailed": "Erreur : Échec du chargement des contrats intelligents. Veuillez actualiser la page ou réessayer plus tard.", "source": "Source", "standard": "Standard", - "txStatusSuccess": "Transfert réussi", + "startDate": "Date de début", + "startTick": "Tick de début", + "startTickTooLarge": "La valeur du tick de début est trop grande.", + "status": "Status", + "targetTick": "Tick cible", + "thousandShort": "K", "tick": "Tick", + "tickCheckFailed": "Impossible de vérifier le statut du tick. Veuillez réessayer plus tard.", "tickLeader": "Chef de Tick", "tickQuality": "Qualité des ticks de cette époque", "tickQualityTooltip": "Pourcentage de ticks non vides pendant cet epoch", "ticks": "Ticks", + "ticksLoadFailed": "Erreur : Échec du chargement des ticks. Veuillez actualiser la page ou réessayer plus tard. Si nous sommes mercredi, veuillez attendre le début de la prochaine époque.", "tickStatus": "Statut du tick", "ticksThisEpoch": "Ticks cette époque (Vides)", "ticksThisEpochTooltip": "Nombre total de ticks traités pendant cet epoch et le nombre de ticks vides entre parenthèses", + "timestamp": "Horodatage", + "token": "Jeton", + "tokenIssuer": "{{name}} Émetteur", + "tokenLoadFailed": "Erreur : Échec du chargement des jetons. Veuillez actualiser la page ou réessayer plus tard.", + "tokens": "Jetons", + "totalValueLocked": "Valeur Totale Verrouillée (TVL)", + "transactionNotFound": "Transaction non trouvée", "transactionPreview": "Aperçu des transactions", "transactions": "Transactions", - "data": "Données", - "defaultView": "Vue par défaut", "unableToDecodeContractInput": "Impossible de décoder l'entrée de ce contrat.", - "exportJson": "Exporter JSON", - "showMore": "Afficher plus", - "showLess": "Afficher moins", - "type": "Type", "unexecuted": "Non exécuté", "value": "Valeur", - "loadMore": "Charger plus", - "noTransactions": "Aucune transaction", - "allTransactionsLoaded": "Vous avez vu toutes les transactions", - "loading": "Chargement", - "hide": "Masquer", - "show": "Afficher", - "transactionNotFound": "Transaction non trouvée", - "invalidTransactionId": "ID de transaction invalide. Veuillez vérifier l'ID et réessayer.", - "invalidAddressError": "Le format de l'adresse n'est pas valide. Veuillez vérifier l'adresse et réessayer.", - "addressNotFoundError": "Erreur : Nous n'avons pas pu trouver l'adresse que vous recherchez. Veuillez vérifier l'adresse et réessayer.", - "loadingTransactionsError": "Erreur : Quelque chose a mal tourné lors du chargement des transactions. Veuillez réessayer plus tard.", - "invalidSourceIdentity": "Adresse source invalide: {{address}}", - "invalidDestinationIdentity": "Adresse de destination invalide: {{address}}", - "minInputTypeTooLarge": "La valeur minimale du type d'entrée est trop grande.", - "maxInputTypeTooLarge": "La valeur maximale du type d'entrée est trop grande.", - "minAmountTooLarge": "La valeur minimale du montant est trop grande.", - "maxAmountTooLarge": "La valeur maximale du montant est trop grande.", - "startTickTooLarge": "La valeur du tick de début est trop grande.", - "endTickTooLarge": "La valeur du tick de fin est trop grande.", - "richList": "Liste des riches", - "rank": "Rang", - "addressID": "ID d'adresse", - "richListLoadFailed": "Erreur : Impossible de charger la liste des riches. Veuillez actualiser la page ou réessayer plus tard.", - "assetsRichListInvalidAssetOrIssuer": "Erreur : Adresse de l'émetteur et/ou nom de l'actif invalides. Veuillez vérifier les paramètres de l'URL.", - "richListWarning": "Les données de la liste des riches sont mises à jour au début de chaque époque", - "timestamp": "Horodatage", - "fee": "Frais", - "assetTransferWarning": "Cette transaction est visible uniquement dans la liste des transactions de l'adresse source. L'afficher dans la liste des transactions de l'adresse de destination n'est pas encore pris en charge dans la version actuelle.", - "ticksLoadFailed": "Erreur : Échec du chargement des ticks. Veuillez actualiser la page ou réessayer plus tard. Si nous sommes mercredi, veuillez attendre le début de la prochaine époque.", - "showItemsPerPage": "Afficher {{count}} éléments", - "itemsPerPage": "Éléments par page", - "latestStatsLoadFailed": "Erreur : Échec du chargement des statistiques récentes. Veuillez actualiser la page ou réessayer plus tard.", - "errorLoadingPrice": "Erreur : Échec du chargement du prix. Veuillez actualiser la page ou réessayer plus tard.", - "exchange": "Échange", - "exchanges": "Échanges", - "exchangesLoadFailed": "Erreur : Échec du chargement des échanges. Veuillez actualiser la page ou réessayer plus tard.", - "token": "Jeton", - "tokens": "Jetons", - "smartContract": "Contrat intelligent", - "smartContracts": "Contrats intelligents", - "tokenLoadFailed": "Erreur : Échec du chargement des jetons. Veuillez actualiser la page ou réessayer plus tard.", - "smartContractsLoadFailed": "Erreur : Échec du chargement des contrats intelligents. Veuillez actualiser la page ou réessayer plus tard.", - "issuer": "Émetteur", - "name": "Nom", "wallets": "Portefeuilles", - "assets": "Actifs", - "totalValueLocked": "Valeur Totale Verrouillée (TVL)", - "noEntries": "Aucune entrée à afficher", - "assetsRichList": "Liste des détenteurs d’actifs", - "assetsRichListDesc": "La liste des détenteurs montre les plus grands détenteurs de chaque actif.", - "contractName": "Nom du contrat", - "shareholders": "Actionnaires", - "checkContractShareholders": "Voir les actionnaires du contrat", - "checkSourceCode": "Voir le code source", - "checkContractProposal": "Voir la proposition de contrat", - "smartContractCodeLoadError": "Erreur : Échec du chargement du code du contrat intelligent. Veuillez actualiser la page ou réessayer plus tard.", - "contract": "Contrat", - "copied": "Copié", - "copyAddress": "Copier l'adresse", - "copyTransactionId": "Copier l'ID de TX", - "copyToClipboard": "Copier dans le presse-papiers", - "showQRCode": "Afficher le code QR", - "qrcodeModalTitle": "Code QR de l'adresse", - "filterAllTransactions": "Toutes les transactions", - "filterTransferTransactions": "Transactions de transfert", - "filterApprovedTransactions": "Transactions approuvées", - "filterAll": "Toutes", - "filterTransfer": "Transferts", - "filterApproved": "Approuvées", - "filterByCategory": "Filtrer par catégorie", - "selectAsset": "Sélectionner un actif", - "scShares": "Parts de Contrats Intelligents", - "sharesIssuer": "Émetteur d'Actions", - "contractIndex": "Index", - "expandAll": "Tout développer", - "collapseAll": "Tout réduire", - "managedBy": "Géré par {{contract}}", "unknownContract": "Contrat inconnu", - "resetFilters": "Réinitialiser les filtres", - "startTick": "Tick de début", - "endTick": "Tick de fin", - "startDate": "Date de début", - "endDate": "Date de fin", - "invalidRangeAmount": "Plage invalide - la valeur de départ doit être inférieure ou égale à la valeur de fin", - "invalidRangeInputType": "La valeur minimale doit être inférieure ou égale à la valeur maximale", - "invalidTickRange": "Le tick de début ne peut pas être supérieur au tick de fin", - "invalidDateRange": "La date de début doit être antérieure à la date de fin", - "invalidAddressFormat": "L'adresse doit contenir 60 lettres majuscules", - "destinationFilterHint": "Pour les transactions de contrats intelligents, il s'agit de l'adresse du contrat, pas du destinataire final du jeton.", - "amountFilterHint": "Montant de la transaction en QUBIC.", - "customRange": "Plage personnalisée", - "minAmount": "Montant minimum", - "maxAmount": "Montant maximum", - "amountOver0": "> 0", - "amount1to1M": "1 - 1M", - "amount1Mto100M": "1M - 100M", - "amount100Mto1B": "100M - 1Md", - "amount1Bto10B": "1Md - 10Md", - "amountOver10B": "> 10Md", - "thousandShort": "K", - "millionShort": "M", - "billionShort": "Md", - "dateLastHour": "Dernière heure", - "dateLast24Hours": "Dernières 24 heures", - "dateLastNDays": "{{count}} derniers jours", - "addressPlaceholder": "60 car. adresse", - "clearAllFiltersTooltip": "Cliquez pour supprimer tous les filtres", - "filterButton": "Filtrer", - "filters": "Filtres", - "applyFilters": "Appliquer les filtres", - "directionAll": "Tous", - "directionIncoming": "Entrant", - "directionOutgoing": "Sortant", - "directionIncomingTooltip": "Transactions entrantes", - "directionOutgoingTooltip": "Transactions sortantes", - "direction": "Direction", - "inputType": "Type d'entrée", - "minInputType": "Valeur min.", - "maxInputType": "Valeur max.", - "include": "Inclure", - "exclude": "Exclure", - "addAddress": "Ajouter une adresse", - "removeAddress": "Supprimer l'adresse", - "maxAddressesReached": "Maximum 5 adresses atteint", - "duplicateAddress": "Adresse en double", - "matchingEntities": "Résultats correspondants", "transactionsFound": "{{count}} transactions", - "showingMaxTransactions": "Affichage des {{count}} dernières transactions", - "maxResultsHint": "Les résultats sont limités à {{count}}. Utilisez des filtres pour affiner la liste.", - "waitingForTickTitle": "En attente du traitement du tick", + "txID": "TX ID", + "txStatusExecuted": "Transaction exécutée", + "txStatusFailed": "Transfert échoué", + "txStatusSuccess": "Transfert réussi", + "txType": "TX Type", + "unitOfMeasurement": "Unité de mesure", + "virtualTransactionBanner": "Ceci est une transaction virtuelle générée par le protocole Qubic pour les événements du cycle de vie des contrats intelligents.", + "virtualTransactionNotFound": "Transaction virtuelle non trouvée", "waitingForTickDesc": "Cette transaction est prévue pour le tick {{targetTick}}. Elle sera disponible une fois que le réseau aura atteint ce tick.", - "targetTick": "Tick cible", - "estimatedWait": "Temps d'attente estimé", - "tickCheckFailed": "Impossible de vérifier le statut du tick. Veuillez réessayer plus tard." + "waitingForTickTitle": "En attente du traitement du tick" } diff --git a/public/locales/ja/global.json b/public/locales/ja/global.json index dad06247..c1291c76 100644 --- a/public/locales/ja/global.json +++ b/public/locales/ja/global.json @@ -1,9 +1,12 @@ { "address": "住所", "balance": "残高", + "navigationMenu": "ナビゲーションメニュー", "noResultsFound": "結果が見つかりません", "noResultsFoundFor": "「{{keyword}}」の結果が見つかりません", "searchPlaceholder": "TX、ティック、ID、トークン、コントラクト、取引所を検索...", + "selectLanguage": "言語を選択", + "selectNetwork": "ネットワークを選択", "tick": "ティック", "transaction": "取引", "transactions": "取引" diff --git a/public/locales/ja/network-page.json b/public/locales/ja/network-page.json index 5aa9d822..bc78373b 100644 --- a/public/locales/ja/network-page.json +++ b/public/locales/ja/network-page.json @@ -1,192 +1,223 @@ { "activeAddresses": "アクティブアドレス", + "addAddress": "アドレスを追加", "address": "アドレス", + "addressID": "アドレスID", + "addressNotFoundError": "エラー:探しているアドレスが見つかりませんでした。アドレスを確認して、再度お試しください。", + "addressPlaceholder": "60文字 アドレス", + "allProcedures": "All procedures", + "allTransactionsLoaded": "すべてのトランザクションを確認しました", "amount": "金額", - "circulatingSupply": "流通供給量", + "amountAssetAny": "すべて", + "amountAssetOther": "その他のアセット", + "amountAssetQubic": "QUBIC", + "amountAssetType": "アセット", + "amount100Mto1B": "1億 - 10億", + "amount1Bto10B": "10億 - 100億", + "amount1Mto100M": "1百万 - 1億", + "amount1to1M": "1 - 1百万", + "amountFilterHint": "QUBICでの取引金額。", + "amountOver0": "> 0", + "amountOver10B": "> 100億", + "applyFilters": "フィルターを適用", + "assetIssuer": "アセット発行者", + "assets": "資産", + "assetsRichList": "アセット保有ランキング", + "assetsRichListDesc": "アセット保有ランキングでは各アセットの上位保有者を表示します。", + "assetsRichListInvalidAssetOrIssuer": "エラー:発行者アドレスおよび/またはアセット名が無効です。URLパラメータを確認してください。", + "assetTransferWarning": "この取引は送信元アドレスの取引リストにのみ表示されます。宛先アドレスの取引リストに表示する機能は、現在のバージョンではまだサポートされていません。", + "billionShort": "十億", + "blockchain": "Blockchain", "burnedSupply": "燃やされた供給", + "checkContractProposal": "コントラクトの提案を確認", + "checkContractShareholders": "コントラクトの株主を確認", + "checkSourceCode": "ソースコードを確認", + "circulatingSupply": "流通供給量", + "clearAllFiltersTooltip": "クリックしてすべてのフィルターを削除", + "collapseAll": "すべて折りたたむ", "complete": "完了", + "contract": "契約", + "contractIndex": "インデックス", + "contractMessageType": "コントラクトメッセージタイプ", + "contractName": "コントラクト名", + "copied": "コピー済み", + "copyAddress": "アドレスをコピー", + "copyToClipboard": "クリップボードにコピー", + "copyTransactionId": "TX IDをコピー", "currentTick": "現在のティック", + "customRange": "カスタム範囲", + "data": "データ", "dataStatus": "データステータス", "date": "日付", + "dateLast24Hours": "過去24時間", + "dateLastHour": "過去1時間", + "dateLastNDays": "過去{{count}}日間", + "deductedAmount": "控除額", + "defaultView": "デフォルト表示", "destination": "宛先", + "destinationFilterHint": "スマートコントラクトトランザクションの場合、これはコントラクトアドレスであり、最終的なトークン受取人ではありません。", + "direction": "方向", + "directionAll": "すべて", + "directionIncoming": "受信", + "directionIncomingTooltip": "受信トランザクション", + "directionOutgoing": "送信", + "directionOutgoingTooltip": "送信トランザクション", + "duplicateAddress": "重複アドレス", "empty": "空", + "endDate": "終了日", + "endTick": "終了ティック", + "endTickTooLarge": "終了ティックの値が大きすぎます。", "entityReportsFromRandomPeers": "ランダムなピアからのレポート", "epoch": "エポック", "epochTicks": "エポックティック", + "errorLoadingPrice": "エラー: 価格の読み込みに失敗しました。ページを更新するか、後でもう一度お試しください。", "errorMessage": "ベータ版 - データが不完全であるか、誤りがある可能性があります", + "estimatedWait": "推定待ち時間", + "event": "イベント", + "eventDetails": "イベント詳細", + "eventNotFound": "イベントが見つかりません", + "events": "イベント", + "eventsBetaDisclaimer": "この機能は開発中です。最近のデータのみが含まれており、本番対応版が利用可能になるまで削除される場合があります。使用されているAPIエンドポイントは今後のリリースで変更される可能性があります。", + "eventsFound": "{{count}} イベント", + "eventsLoadFailed": "エラー: イベントの読み込みに失敗しました。もう一度お試しください。", + "eventType": "イベントタイプ", + "exchange": "取引所", + "exchanges": "取引所", + "exchangesLoadFailed": "エラー: 取引所の読み込みに失敗しました。ページを更新するか、後でもう一度お試しください。", + "exclude": "除外", "executed": "実行済み", - "txStatusExecuted": "トランザクション実行済み", - "txStatusFailed": "送金失敗", + "expandAll": "すべて展開", + "exportJson": "JSONをエクスポート", + "fee": "手数料", + "filterAll": "すべて", + "filterAllTransactions": "すべてのトランザクション", + "filterApproved": "承認済み", + "filterApprovedTransactions": "承認されたトランザクション", + "filterButton": "フィルター", + "filterByCategory": "カテゴリで絞り込む", + "filters": "フィルター", + "filterTransfer": "転送", + "filterTransferTransactions": "転送トランザクション", + "hide": "隠す", "id": "ID", - "incomingTransfers": "受信転送", + "include": "含める", "incomingAmount": "受信金額", + "incomingTransfers": "受信転送", "incomplete": "未完了", + "inputType": "入力タイプ", + "invalidAddressError": "アドレス形式が無効です。アドレスを確認して、再度お試しください。", + "invalidAddressFormat": "アドレスは60文字の大文字である必要があります", + "invalidDateRange": "開始日は終了日より前でなければなりません", + "invalidDestinationIdentity": "無効な送信先アドレス: {{address}}", + "invalidRangeAmount": "無効な範囲 - 開始値は終了値以下でなければなりません", + "invalidRangeInputType": "最小値は最大値以下でなければなりません", + "invalidSourceIdentity": "無効な送信元アドレス: {{address}}", + "invalidTickRange": "開始ティックは終了ティックより大きくできません", + "invalidTransactionId": "トランザクションIDが無効です。IDを確認して、再度お試しください。", + "issuer": "発行者", "lastNTickQuality": "最後の10Kティック品質", "lastNTickQualityTooltip": "直近10K刻み中の非空刻みの割合(エポックに関係なく)", "latest": "最新", + "latestStatsLoadFailed": "エラー: 最新の統計情報の読み込みに失敗しました。ページを更新するか、後でもう一度お試しください。", "latestTransfers": "最新の転送", + "loading": "読み込み中", + "loadingTransactionsError": "エラー:トランザクションの読み込み中に問題が発生しました。後でもう一度試してください。", + "loadMore": "もっと読み込む", + "logDigest": "ログダイジェスト", + "managedBy": "{{contract}}によって管理", + "managingContractIndex": "管理コントラクトインデックス", "marketCap": "時価総額", + "matchingEntities": "一致する結果", + "maxAddressesReached": "最大5アドレスに達しました", + "maxAmount": "最大金額", + "maxAmountTooLarge": "金額の最大値が大きすぎます。", + "maxInputType": "最大値", + "maxInputTypeTooLarge": "入力タイプの最大値が大きすぎます。", + "maxResultsHint": "結果は{{count}}件に制限されています。フィルターを使用してリストを絞り込んでください。", + "millionShort": "百万", + "minAmount": "最小金額", + "minAmountTooLarge": "金額の最小値が大きすぎます。", + "minInputType": "最小値", + "minInputTypeTooLarge": "入力タイプの最小値が大きすぎます。", + "name": "名前", + "noEntries": "表示するエントリがありません", + "noEvents": "イベントがありません", "nonEmpty": "空ではない", + "noTransactions": "トランザクションがありません", + "numberOfDecimalPlaces": "小数点以下の桁数", "numberOfTransactions": "取引数", - "outgoingTransfers": "送信転送", "outgoingAmount": "送信金額", + "outgoingTransfers": "送信転送", "price": "価格", + "qrcodeModalTitle": "アドレスQRコード", + "rank": "ランク", + "remainingAmount": "残額", + "removeAddress": "アドレスを削除", + "resetFilters": "フィルターをリセット", + "richList": "リッチリスト", + "richListLoadFailed": "エラー:リッチリストの読み込みに失敗しました。ページを更新するか、後でもう一度お試しください。", + "richListWarning": "リッチリストのデータは各エポックの開始時に更新されます", + "scShares": "スマートコントラクトシェア", "search": "検索", + "selectAsset": "アセットを選択", + "selectContract": "Select contract", + "selectUpTo": "最大{{count}}個まで選択", + "shareholders": "株主", + "sharesIssuer": "株式発行者", + "show": "表示する", + "showingMaxTransactions": "最新{{count}}件のトランザクションを表示", + "showingMaxEvents": "最新の {{count}} イベントを表示", + "showLess": "折りたたむ", + "showMore": "もっと見る", + "showPerPagePrefix": "ページあたり", + "showPerPageSuffix": "件表示", + "showQRCode": "QRコードを表示", "signature": "署名", + "smartContract": "スマートコントラクト", + "smartContractCodeLoadError": "エラー:スマートコントラクトのコードを読み込めませんでした。ページを更新するか、後でもう一度お試しください。", + "smartContracts": "スマートコントラクト", + "smartContractsLoadFailed": "エラー: スマートコントラクトの読み込みに失敗しました。ページを更新するか、後でもう一度お試しください。", "source": "ソース", "standard": "標準", - "txStatusSuccess": "送金成功", + "startDate": "開始日", + "startTick": "開始ティック", + "startTickTooLarge": "開始ティックの値が大きすぎます。", + "status": "Status", + "targetTick": "目標ティック", + "thousandShort": "千", "tick": "ティック", + "tickCheckFailed": "ティックの状態を確認できませんでした。後でもう一度お試しください。", "tickLeader": "ティックリーダー", "tickQuality": "エポックティックの品質", "tickQualityTooltip": "このエポックにおける非空刻みの割合", "ticks": "ティック", + "ticksLoadFailed": "エラー: ティックの読み込みに失敗しました。ページを更新するか、後でもう一度お試しください。水曜日の場合は、次のエポックが始まるまでお待ちください。", "tickStatus": "ティックステータス", "ticksThisEpoch": "このエポックのティック(空)", "ticksThisEpochTooltip": "このエポックで処理された刻み数と空刻みの数(カッコ内に表示)", + "timestamp": "タイムスタンプ", + "token": "トークン", + "tokenIssuer": "{{name}} 発行者", + "tokenLoadFailed": "エラー: トークンの読み込みに失敗しました。ページを更新するか、後でもう一度お試しください。", + "tokens": "トークン", + "totalValueLocked": "総ロック価値 (TVL)", + "transactionNotFound": "トランザクションが見つかりません", "transactionPreview": "トランザクションのプレビュー", "transactions": "トランザクション", - "data": "データ", - "defaultView": "デフォルト表示", "unableToDecodeContractInput": "このコントラクトの入力をデコードできませんでした。", - "exportJson": "JSONをエクスポート", - "showMore": "もっと見る", - "showLess": "折りたたむ", - "type": "タイプ", "unexecuted": "未実行", "value": "値", - "loadMore": "もっと読み込む", - "noTransactions": "トランザクションがありません", - "allTransactionsLoaded": "すべてのトランザクションを確認しました", - "loading": "読み込み中", - "hide": "隠す", - "show": "表示する", - "transactionNotFound": "トランザクションが見つかりません", - "invalidTransactionId": "トランザクションIDが無効です。IDを確認して、再度お試しください。", - "invalidAddressError": "アドレス形式が無効です。アドレスを確認して、再度お試しください。", - "addressNotFoundError": "エラー:探しているアドレスが見つかりませんでした。アドレスを確認して、再度お試しください。", - "loadingTransactionsError": "エラー:トランザクションの読み込み中に問題が発生しました。後でもう一度試してください。", - "invalidSourceIdentity": "無効な送信元アドレス: {{address}}", - "invalidDestinationIdentity": "無効な送信先アドレス: {{address}}", - "minInputTypeTooLarge": "入力タイプの最小値が大きすぎます。", - "maxInputTypeTooLarge": "入力タイプの最大値が大きすぎます。", - "minAmountTooLarge": "金額の最小値が大きすぎます。", - "maxAmountTooLarge": "金額の最大値が大きすぎます。", - "startTickTooLarge": "開始ティックの値が大きすぎます。", - "endTickTooLarge": "終了ティックの値が大きすぎます。", - "richList": "リッチリスト", - "rank": "ランク", - "addressID": "アドレスID", - "richListLoadFailed": "エラー:リッチリストの読み込みに失敗しました。ページを更新するか、後でもう一度お試しください。", - "assetsRichListInvalidAssetOrIssuer": "エラー:発行者アドレスおよび/またはアセット名が無効です。URLパラメータを確認してください。", - "richListWarning": "リッチリストのデータは各エポックの開始時に更新されます", - "timestamp": "タイムスタンプ", - "fee": "手数料", - "assetTransferWarning": "この取引は送信元アドレスの取引リストにのみ表示されます。宛先アドレスの取引リストに表示する機能は、現在のバージョンではまだサポートされていません。", - "ticksLoadFailed": "エラー: ティックの読み込みに失敗しました。ページを更新するか、後でもう一度お試しください。水曜日の場合は、次のエポックが始まるまでお待ちください。", - "showItemsPerPage": "{{count}} 件を表示", - "itemsPerPage": "ページあたりの件数", - "latestStatsLoadFailed": "エラー: 最新の統計情報の読み込みに失敗しました。ページを更新するか、後でもう一度お試しください。", - "errorLoadingPrice": "エラー: 価格の読み込みに失敗しました。ページを更新するか、後でもう一度お試しください。", - "exchange": "取引所", - "exchanges": "取引所", - "exchangesLoadFailed": "エラー: 取引所の読み込みに失敗しました。ページを更新するか、後でもう一度お試しください。", - "token": "トークン", - "tokens": "トークン", - "smartContract": "スマートコントラクト", - "smartContracts": "スマートコントラクト", - "tokenLoadFailed": "エラー: トークンの読み込みに失敗しました。ページを更新するか、後でもう一度お試しください。", - "smartContractsLoadFailed": "エラー: スマートコントラクトの読み込みに失敗しました。ページを更新するか、後でもう一度お試しください。", - "issuer": "発行者", - "name": "名前", "wallets": "ウォレット", - "assets": "資産", - "totalValueLocked": "総ロック価値 (TVL)", - "noEntries": "表示するエントリがありません", - "assetsRichList": "アセット保有ランキング", - "assetsRichListDesc": "アセット保有ランキングでは各アセットの上位保有者を表示します。", - "contractName": "コントラクト名", - "shareholders": "株主", - "checkContractShareholders": "コントラクトの株主を確認", - "checkSourceCode": "ソースコードを確認", - "checkContractProposal": "コントラクトの提案を確認", - "smartContractCodeLoadError": "エラー:スマートコントラクトのコードを読み込めませんでした。ページを更新するか、後でもう一度お試しください。", - "contract": "契約", - "copied": "コピー済み", - "copyAddress": "アドレスをコピー", - "copyTransactionId": "TX IDをコピー", - "copyToClipboard": "クリップボードにコピー", - "showQRCode": "QRコードを表示", - "qrcodeModalTitle": "アドレスQRコード", - "filterAllTransactions": "すべてのトランザクション", - "filterTransferTransactions": "転送トランザクション", - "filterApprovedTransactions": "承認されたトランザクション", - "filterAll": "すべて", - "filterTransfer": "転送", - "filterApproved": "承認済み", - "filterByCategory": "カテゴリで絞り込む", - "selectAsset": "アセットを選択", - "scShares": "スマートコントラクトシェア", - "sharesIssuer": "株式発行者", - "contractIndex": "インデックス", - "expandAll": "すべて展開", - "collapseAll": "すべて折りたたむ", - "managedBy": "{{contract}}によって管理", "unknownContract": "不明なコントラクト", - "resetFilters": "フィルターをリセット", - "startTick": "開始ティック", - "endTick": "終了ティック", - "startDate": "開始日", - "endDate": "終了日", - "invalidRangeAmount": "無効な範囲 - 開始値は終了値以下でなければなりません", - "invalidRangeInputType": "最小値は最大値以下でなければなりません", - "invalidTickRange": "開始ティックは終了ティックより大きくできません", - "invalidDateRange": "開始日は終了日より前でなければなりません", - "invalidAddressFormat": "アドレスは60文字の大文字である必要があります", - "destinationFilterHint": "スマートコントラクトトランザクションの場合、これはコントラクトアドレスであり、最終的なトークン受取人ではありません。", - "amountFilterHint": "QUBICでの取引金額。", - "customRange": "カスタム範囲", - "minAmount": "最小金額", - "maxAmount": "最大金額", - "amountOver0": "> 0", - "amount1to1M": "1 - 1百万", - "amount1Mto100M": "1百万 - 1億", - "amount100Mto1B": "1億 - 10億", - "amount1Bto10B": "10億 - 100億", - "amountOver10B": "> 100億", - "thousandShort": "千", - "millionShort": "百万", - "billionShort": "十億", - "dateLastHour": "過去1時間", - "dateLast24Hours": "過去24時間", - "dateLastNDays": "過去{{count}}日間", - "addressPlaceholder": "60文字 アドレス", - "clearAllFiltersTooltip": "クリックしてすべてのフィルターを削除", - "filterButton": "フィルター", - "filters": "フィルター", - "applyFilters": "フィルターを適用", - "directionAll": "すべて", - "directionIncoming": "受信", - "directionOutgoing": "送信", - "directionIncomingTooltip": "受信トランザクション", - "directionOutgoingTooltip": "送信トランザクション", - "direction": "方向", - "inputType": "入力タイプ", - "minInputType": "最小値", - "maxInputType": "最大値", - "include": "含める", - "exclude": "除外", - "addAddress": "アドレスを追加", - "removeAddress": "アドレスを削除", - "maxAddressesReached": "最大5アドレスに達しました", - "duplicateAddress": "重複アドレス", - "matchingEntities": "一致する結果", "transactionsFound": "{{count}}件のトランザクション", - "showingMaxTransactions": "最新{{count}}件のトランザクションを表示", - "maxResultsHint": "結果は{{count}}件に制限されています。フィルターを使用してリストを絞り込んでください。", - "waitingForTickTitle": "ティックの処理を待機中", + "txID": "TX ID", + "txStatusExecuted": "トランザクション実行済み", + "txStatusFailed": "送金失敗", + "txStatusSuccess": "送金成功", + "txType": "TX Type", + "unitOfMeasurement": "単位", + "virtualTransactionBanner": "これはQubicプロトコルによってスマートコントラクトのライフサイクルイベント用に生成された仮想トランザクションです。", + "virtualTransactionNotFound": "仮想トランザクションが見つかりません", "waitingForTickDesc": "このトランザクションはティック{{targetTick}}に予定されています。ネットワークがそのティックに到達すると利用可能になります。", - "targetTick": "目標ティック", - "estimatedWait": "推定待ち時間", - "tickCheckFailed": "ティックの状態を確認できませんでした。後でもう一度お試しください。" + "waitingForTickTitle": "ティックの処理を待機中" } diff --git a/public/locales/nl/global.json b/public/locales/nl/global.json index 5e28e10c..c79d4d30 100644 --- a/public/locales/nl/global.json +++ b/public/locales/nl/global.json @@ -1,9 +1,12 @@ { "address": "Adres", "balance": "Balans", + "navigationMenu": "Navigatiemenu", "noResultsFound": "Geen Resultaten Gevonden", "noResultsFoundFor": "Geen resultaten gevonden voor \"{{keyword}}\"", "searchPlaceholder": "Zoek TXs, ticks, IDs, tokens, contracten, exchanges...", + "selectLanguage": "Taal selecteren", + "selectNetwork": "Netwerk selecteren", "tick": "Tick", "transaction": "Transactie", "transactions": "Transacties" diff --git a/public/locales/nl/network-page.json b/public/locales/nl/network-page.json index ff524c50..a1e99383 100644 --- a/public/locales/nl/network-page.json +++ b/public/locales/nl/network-page.json @@ -1,192 +1,223 @@ { "activeAddresses": "Actieve adressen", + "addAddress": "Adres toevoegen", "address": "Adres", + "addressID": "Adres-ID", + "addressNotFoundError": "Fout: We konden het adres dat je zoekt niet vinden. Controleer het adres en probeer het opnieuw.", + "addressPlaceholder": "60 tekens adres", + "allProcedures": "All procedures", + "allTransactionsLoaded": "Je hebt alle transacties gezien", "amount": "Bedrag", - "circulatingSupply": "Circulerende voorraad", + "amountAssetAny": "Alle", + "amountAssetOther": "Andere activa", + "amountAssetQubic": "QUBIC", + "amountAssetType": "Activa", + "amount100Mto1B": "100M - 1Mrd", + "amount1Bto10B": "1Mrd - 10Mrd", + "amount1Mto100M": "1M - 100M", + "amount1to1M": "1 - 1M", + "amountFilterHint": "Transactiebedrag in QUBIC.", + "amountOver0": "> 0", + "amountOver10B": "> 10Mrd", + "applyFilters": "Filters toepassen", + "assetIssuer": "Asset Uitgever", + "assets": "Activa", + "assetsRichList": "Ranglijsten voor Activa", + "assetsRichListDesc": "De ranglijst toont de grootste bezitters van elk activum.", + "assetsRichListInvalidAssetOrIssuer": "Fout: Ongeldig uitgeveradres en/of assetnaam. Controleer de URL-parameters.", + "assetTransferWarning": "Deze transactie is alleen zichtbaar in de transactielijst van het bronadres. Weergave in de transactielijst van het bestemmingsadres wordt in de huidige versie nog niet ondersteund.", + "billionShort": "Mrd", + "blockchain": "Blockchain", "burnedSupply": "Verbrand Voorraad", + "checkContractProposal": "Bekijk contractvoorstel", + "checkContractShareholders": "Bekijk contractaandeelhouders", + "checkSourceCode": "Bekijk broncode", + "circulatingSupply": "Circulerende voorraad", + "clearAllFiltersTooltip": "Klik om alle filters te verwijderen", + "collapseAll": "Alles invouwen", "complete": "Voltooid", + "contract": "Contract", + "contractIndex": "Index", + "contractMessageType": "Contractberichttype", + "contractName": "Contractnaam", + "copied": "Gekopieerd", + "copyAddress": "Adres kopiëren", + "copyToClipboard": "Kopiëren naar klembord", + "copyTransactionId": "TX-ID kopiëren", "currentTick": "Huidige Tick", + "customRange": "Aangepast bereik", + "data": "Data", "dataStatus": "Datastatus", "date": "Datum", + "dateLast24Hours": "Laatste 24 uur", + "dateLastHour": "Laatste uur", + "dateLastNDays": "Laatste {{count}} dagen", + "deductedAmount": "Afgetrokken Bedrag", + "defaultView": "Standaardweergave", "destination": "Bestemming", + "destinationFilterHint": "Voor smart contract transacties is dit het contractadres, niet de uiteindelijke token ontvanger.", + "direction": "Richting", + "directionAll": "Alle", + "directionIncoming": "Inkomend", + "directionIncomingTooltip": "Inkomende transacties", + "directionOutgoing": "Uitgaand", + "directionOutgoingTooltip": "Uitgaande transacties", + "duplicateAddress": "Dubbel adres", "empty": "Leeg", + "endDate": "Tot datum", + "endTick": "Eind tick", + "endTickTooLarge": "De waarde van de eindtick is te groot.", "entityReportsFromRandomPeers": "Rapporten van willekeurige peers", "epoch": "Epoche", "epochTicks": "Epoche-tikken", + "errorLoadingPrice": "Fout: Het laden van de prijs is mislukt. Vernieuw de pagina of probeer het later opnieuw.", "errorMessage": "Beta - Gegevens kunnen onvolledig of onjuist zijn", + "estimatedWait": "Geschatte wachttijd", + "event": "Gebeurtenis", + "eventDetails": "Gebeurtenisdetails", + "eventNotFound": "Gebeurtenis niet gevonden", + "events": "Evenementen", + "eventsBetaDisclaimer": "Deze functie is in ontwikkeling. Alleen recente gegevens zijn opgenomen en kunnen worden verwijderd totdat een productieversie beschikbaar is. De gebruikte API-endpoints kunnen in toekomstige versies wijzigen.", + "eventsFound": "{{count}} evenementen", + "eventsLoadFailed": "Fout: Het laden van evenementen is mislukt. Probeer het opnieuw.", + "eventType": "Evenementtype", + "exchange": "Beurs", + "exchanges": "Beurzen", + "exchangesLoadFailed": "Fout: Het laden van beurzen is mislukt. Vernieuw de pagina of probeer het later opnieuw.", + "exclude": "Uitsluiten", "executed": "Uitgevoerd", - "txStatusExecuted": "Transactie uitgevoerd", - "txStatusFailed": "Overdracht mislukt", + "expandAll": "Alles uitvouwen", + "exportJson": "JSON exporteren", + "fee": "Kosten", + "filterAll": "Alle", + "filterAllTransactions": "Alle transacties", + "filterApproved": "Goedgekeurd", + "filterApprovedTransactions": "Goedgekeurde transacties", + "filterButton": "Filteren", + "filterByCategory": "Filteren op categorie", + "filters": "Filters", + "filterTransfer": "Overboekingen", + "filterTransferTransactions": "Overboekingen", + "hide": "Verbergen", "id": "ID", - "incomingTransfers": "Binnenkomende overboekingen", + "include": "Opnemen", "incomingAmount": "Binnenkomend bedrag", + "incomingTransfers": "Binnenkomende overboekingen", "incomplete": "Onvolledig", + "inputType": "Invoertype", + "invalidAddressError": "Het adresformaat is ongeldig. Controleer het adres en probeer het opnieuw.", + "invalidAddressFormat": "Adres moet 60 hoofdletters bevatten", + "invalidDateRange": "Startdatum moet voor einddatum liggen", + "invalidDestinationIdentity": "Ongeldig bestemmingsadres: {{address}}", + "invalidRangeAmount": "Ongeldig bereik - startwaarde moet kleiner of gelijk zijn aan eindwaarde", + "invalidRangeInputType": "Minimumwaarde moet kleiner of gelijk zijn aan maximumwaarde", + "invalidSourceIdentity": "Ongeldig bronadres: {{address}}", + "invalidTickRange": "Starttick mag niet groter zijn dan eindtick", + "invalidTransactionId": "Ongeldig transactie-ID. Controleer de ID en probeer het opnieuw.", + "issuer": "Uitgever", "lastNTickQuality": "Laatste 10K Tick Kwaliteit", "lastNTickQualityTooltip": "Percentage van niet-lege ticks in de laatste 10K ticks, ongeacht de epoch", "latest": "Laatste", + "latestStatsLoadFailed": "Fout: Het laden van de nieuwste statistieken is mislukt. Vernieuw de pagina of probeer het later opnieuw.", "latestTransfers": "Laatste overboekingen", + "loading": "Laden", + "loadingTransactionsError": "Fout: Er is iets misgegaan bij het laden van de transacties. Probeer het later opnieuw.", + "loadMore": "Meer laden", + "logDigest": "Logsamenvatting", + "managedBy": "Beheerd door {{contract}}", + "managingContractIndex": "Beheercontractindex", "marketCap": "Marktkapitalisatie", + "matchingEntities": "Overeenkomende resultaten", + "maxAddressesReached": "Maximum 5 adressen bereikt", + "maxAmount": "Maximum bedrag", + "maxAmountTooLarge": "De maximale waarde van het bedrag is te groot.", + "maxInputType": "Max. waarde", + "maxInputTypeTooLarge": "De maximale waarde van het inputtype is te groot.", + "maxResultsHint": "Resultaten zijn beperkt tot {{count}}. Gebruik filters om de lijst te verfijnen.", + "millionShort": "M", + "minAmount": "Minimum bedrag", + "minAmountTooLarge": "De minimale waarde van het bedrag is te groot.", + "minInputType": "Min. waarde", + "minInputTypeTooLarge": "De minimale waarde van het inputtype is te groot.", + "name": "Naam", + "noEntries": "Geen items om weer te geven", + "noEvents": "Geen evenementen", "nonEmpty": "Niet leeg", + "noTransactions": "Geen transacties", + "numberOfDecimalPlaces": "Decimalen", "numberOfTransactions": "Aantal transacties", - "outgoingTransfers": "Uitgaande overboekingen", "outgoingAmount": "Uitgaand bedrag", + "outgoingTransfers": "Uitgaande overboekingen", "price": "Prijs", + "qrcodeModalTitle": "Adres QR-code", + "rank": "Rang", + "remainingAmount": "Resterend Bedrag", + "removeAddress": "Adres verwijderen", + "resetFilters": "Filters resetten", + "richList": "Rijkelijst", + "richListLoadFailed": "Fout: Het laden van de rijkelijst is mislukt. Ververs de pagina of probeer het later opnieuw.", + "richListWarning": "Rijkelijstgegevens worden aan het begin van elke epoche bijgewerkt", + "scShares": "Smart Contract Aandelen", "search": "Zoeken", + "selectAsset": "Selecteer asset", + "selectContract": "Select contract", + "selectUpTo": "Selecteer maximaal {{count}}", + "shareholders": "Aandeelhouders", + "sharesIssuer": "Aandelen Uitgever", + "show": "Tonen", + "showingMaxTransactions": "Toont de laatste {{count}} transacties", + "showingMaxEvents": "Toont de laatste {{count}} evenementen", + "showLess": "Minder tonen", + "showMore": "Meer tonen", + "showPerPagePrefix": "Toon", + "showPerPageSuffix": "per pagina", + "showQRCode": "QR-code weergeven", "signature": "Handtekening", + "smartContract": "Slim contract", + "smartContractCodeLoadError": "Fout: Het laden van de smartcontractcode is mislukt. Vernieuw de pagina of probeer het later opnieuw.", + "smartContracts": "Slimme Contracten", + "smartContractsLoadFailed": "Fout: Het laden van slimme contracten is mislukt. Vernieuw de pagina of probeer het later opnieuw.", "source": "Bron", "standard": "Standaard", - "txStatusSuccess": "Succesvolle overdracht", + "startDate": "Van datum", + "startTick": "Start tick", + "startTickTooLarge": "De waarde van de starttick is te groot.", + "status": "Status", + "targetTick": "Doeltick", + "thousandShort": "K", "tick": "Tick", + "tickCheckFailed": "Kan tickstatus niet controleren. Probeer het later opnieuw.", "tickLeader": "Tick-leider", "tickQuality": "Epoch Tick Kwaliteit", "tickQualityTooltip": "Percentage van niet-lege ticks in deze epoch", "ticks": "Ticks", + "ticksLoadFailed": "Fout: Het laden van ticks is mislukt. Vernieuw de pagina of probeer het later opnieuw. Als het woensdag is, wacht dan tot de volgende tijdperk begint.", "tickStatus": "Tick-status", "ticksThisEpoch": "Ticks in deze epoche (Leeg)", "ticksThisEpochTooltip": "Totaal aantal verwerkte ticks in deze epoch en het aantal lege ticks tussen haakjes", + "timestamp": "Tijdstempel", + "token": "Token", + "tokenIssuer": "{{name}} Uitgever", + "tokenLoadFailed": "Fout: Het laden van tokens is mislukt. Vernieuw de pagina of probeer het later opnieuw.", + "tokens": "Tokens", + "totalValueLocked": "Totale Vergrendelde Waarde (TVL)", + "transactionNotFound": "Transactie niet gevonden", "transactionPreview": "Transactievoorvertoning", "transactions": "Transacties", - "data": "Data", - "defaultView": "Standaardweergave", "unableToDecodeContractInput": "Kan de invoer van dit contract niet decoderen.", - "exportJson": "JSON exporteren", - "showMore": "Meer tonen", - "showLess": "Minder tonen", - "type": "Type", "unexecuted": "Niet uitgevoerd", "value": "Waarde", - "loadMore": "Meer laden", - "noTransactions": "Geen transacties", - "allTransactionsLoaded": "Je hebt alle transacties gezien", - "loading": "Laden", - "hide": "Verbergen", - "show": "Tonen", - "transactionNotFound": "Transactie niet gevonden", - "invalidTransactionId": "Ongeldig transactie-ID. Controleer de ID en probeer het opnieuw.", - "invalidAddressError": "Het adresformaat is ongeldig. Controleer het adres en probeer het opnieuw.", - "addressNotFoundError": "Fout: We konden het adres dat je zoekt niet vinden. Controleer het adres en probeer het opnieuw.", - "loadingTransactionsError": "Fout: Er is iets misgegaan bij het laden van de transacties. Probeer het later opnieuw.", - "invalidSourceIdentity": "Ongeldig bronadres: {{address}}", - "invalidDestinationIdentity": "Ongeldig bestemmingsadres: {{address}}", - "minInputTypeTooLarge": "De minimale waarde van het inputtype is te groot.", - "maxInputTypeTooLarge": "De maximale waarde van het inputtype is te groot.", - "minAmountTooLarge": "De minimale waarde van het bedrag is te groot.", - "maxAmountTooLarge": "De maximale waarde van het bedrag is te groot.", - "startTickTooLarge": "De waarde van de starttick is te groot.", - "endTickTooLarge": "De waarde van de eindtick is te groot.", - "richList": "Rijkelijst", - "rank": "Rang", - "addressID": "Adres-ID", - "richListLoadFailed": "Fout: Het laden van de rijkelijst is mislukt. Ververs de pagina of probeer het later opnieuw.", - "assetsRichListInvalidAssetOrIssuer": "Fout: Ongeldig uitgeveradres en/of assetnaam. Controleer de URL-parameters.", - "richListWarning": "Rijkelijstgegevens worden aan het begin van elke epoche bijgewerkt", - "timestamp": "Tijdstempel", - "fee": "Kosten", - "assetTransferWarning": "Deze transactie is alleen zichtbaar in de transactielijst van het bronadres. Weergave in de transactielijst van het bestemmingsadres wordt in de huidige versie nog niet ondersteund.", - "ticksLoadFailed": "Fout: Het laden van ticks is mislukt. Vernieuw de pagina of probeer het later opnieuw. Als het woensdag is, wacht dan tot de volgende tijdperk begint.", - "showItemsPerPage": "{{count}} items weergeven", - "itemsPerPage": "Items per pagina", - "latestStatsLoadFailed": "Fout: Het laden van de nieuwste statistieken is mislukt. Vernieuw de pagina of probeer het later opnieuw.", - "errorLoadingPrice": "Fout: Het laden van de prijs is mislukt. Vernieuw de pagina of probeer het later opnieuw.", - "exchange": "Beurs", - "exchanges": "Beurzen", - "exchangesLoadFailed": "Fout: Het laden van beurzen is mislukt. Vernieuw de pagina of probeer het later opnieuw.", - "token": "Token", - "tokens": "Tokens", - "smartContract": "Slim contract", - "smartContracts": "Slimme Contracten", - "tokenLoadFailed": "Fout: Het laden van tokens is mislukt. Vernieuw de pagina of probeer het later opnieuw.", - "smartContractsLoadFailed": "Fout: Het laden van slimme contracten is mislukt. Vernieuw de pagina of probeer het later opnieuw.", - "issuer": "Uitgever", - "name": "Naam", "wallets": "Portefeuilles", - "assets": "Activa", - "totalValueLocked": "Totale Vergrendelde Waarde (TVL)", - "noEntries": "Geen items om weer te geven", - "assetsRichList": "Ranglijsten voor Activa", - "assetsRichListDesc": "De ranglijst toont de grootste bezitters van elk activum.", - "contractName": "Contractnaam", - "shareholders": "Aandeelhouders", - "checkContractShareholders": "Bekijk contractaandeelhouders", - "checkSourceCode": "Bekijk broncode", - "checkContractProposal": "Bekijk contractvoorstel", - "smartContractCodeLoadError": "Fout: Het laden van de smartcontractcode is mislukt. Vernieuw de pagina of probeer het later opnieuw.", - "contract": "Contract", - "copied": "Gekopieerd", - "copyAddress": "Adres kopiëren", - "copyTransactionId": "TX-ID kopiëren", - "copyToClipboard": "Kopiëren naar klembord", - "showQRCode": "QR-code weergeven", - "qrcodeModalTitle": "Adres QR-code", - "filterAllTransactions": "Alle transacties", - "filterTransferTransactions": "Overboekingen", - "filterApprovedTransactions": "Goedgekeurde transacties", - "filterAll": "Alle", - "filterTransfer": "Overboekingen", - "filterApproved": "Goedgekeurd", - "filterByCategory": "Filteren op categorie", - "selectAsset": "Selecteer asset", - "scShares": "Smart Contract Aandelen", - "sharesIssuer": "Aandelen Uitgever", - "contractIndex": "Index", - "expandAll": "Alles uitvouwen", - "collapseAll": "Alles invouwen", - "managedBy": "Beheerd door {{contract}}", "unknownContract": "Onbekend contract", - "resetFilters": "Filters resetten", - "startTick": "Start tick", - "endTick": "Eind tick", - "startDate": "Van datum", - "endDate": "Tot datum", - "invalidRangeAmount": "Ongeldig bereik - startwaarde moet kleiner of gelijk zijn aan eindwaarde", - "invalidRangeInputType": "Minimumwaarde moet kleiner of gelijk zijn aan maximumwaarde", - "invalidTickRange": "Starttick mag niet groter zijn dan eindtick", - "invalidDateRange": "Startdatum moet voor einddatum liggen", - "invalidAddressFormat": "Adres moet 60 hoofdletters bevatten", - "destinationFilterHint": "Voor smart contract transacties is dit het contractadres, niet de uiteindelijke token ontvanger.", - "amountFilterHint": "Transactiebedrag in QUBIC.", - "customRange": "Aangepast bereik", - "minAmount": "Minimum bedrag", - "maxAmount": "Maximum bedrag", - "amountOver0": "> 0", - "amount1to1M": "1 - 1M", - "amount1Mto100M": "1M - 100M", - "amount100Mto1B": "100M - 1Mrd", - "amount1Bto10B": "1Mrd - 10Mrd", - "amountOver10B": "> 10Mrd", - "thousandShort": "K", - "millionShort": "M", - "billionShort": "Mrd", - "dateLastHour": "Laatste uur", - "dateLast24Hours": "Laatste 24 uur", - "dateLastNDays": "Laatste {{count}} dagen", - "addressPlaceholder": "60 tekens adres", - "clearAllFiltersTooltip": "Klik om alle filters te verwijderen", - "filterButton": "Filteren", - "filters": "Filters", - "applyFilters": "Filters toepassen", - "directionAll": "Alle", - "directionIncoming": "Inkomend", - "directionOutgoing": "Uitgaand", - "directionIncomingTooltip": "Inkomende transacties", - "directionOutgoingTooltip": "Uitgaande transacties", - "direction": "Richting", - "inputType": "Invoertype", - "minInputType": "Min. waarde", - "maxInputType": "Max. waarde", - "include": "Opnemen", - "exclude": "Uitsluiten", - "addAddress": "Adres toevoegen", - "removeAddress": "Adres verwijderen", - "maxAddressesReached": "Maximum 5 adressen bereikt", - "duplicateAddress": "Dubbel adres", - "matchingEntities": "Overeenkomende resultaten", "transactionsFound": "{{count}} transacties", - "showingMaxTransactions": "Toont de laatste {{count}} transacties", - "maxResultsHint": "Resultaten zijn beperkt tot {{count}}. Gebruik filters om de lijst te verfijnen.", - "waitingForTickTitle": "Wachten op verwerking van de tick", + "txID": "TX ID", + "txStatusExecuted": "Transactie uitgevoerd", + "txStatusFailed": "Overdracht mislukt", + "txStatusSuccess": "Succesvolle overdracht", + "txType": "TX Type", + "unitOfMeasurement": "Meeteenheid", + "virtualTransactionBanner": "Dit is een virtuele transactie gegenereerd door het Qubic-protocol voor levenscyclusgebeurtenissen van slimme contracten.", + "virtualTransactionNotFound": "Virtuele transactie niet gevonden", "waitingForTickDesc": "Deze transactie is gepland voor tick {{targetTick}}. Deze wordt beschikbaar zodra het netwerk die tick bereikt.", - "targetTick": "Doeltick", - "estimatedWait": "Geschatte wachttijd", - "tickCheckFailed": "Kan tickstatus niet controleren. Probeer het later opnieuw." + "waitingForTickTitle": "Wachten op verwerking van de tick" } diff --git a/public/locales/pt/global.json b/public/locales/pt/global.json index 30427659..201bbc5b 100644 --- a/public/locales/pt/global.json +++ b/public/locales/pt/global.json @@ -1,9 +1,12 @@ { "address": "Endereço", "balance": "Saldo", + "navigationMenu": "Menu de navegação", "noResultsFound": "Nenhum Resultado Encontrado", "noResultsFoundFor": "Nenhum resultado encontrado para \"{{keyword}}\"", "searchPlaceholder": "Pesquisar TXs, ticks, IDs, tokens, contratos, exchanges...", + "selectLanguage": "Selecionar idioma", + "selectNetwork": "Selecionar rede", "tick": "Tick", "transaction": "Transação", "transactions": "Transações" diff --git a/public/locales/pt/network-page.json b/public/locales/pt/network-page.json index 7b45be1e..7b733783 100644 --- a/public/locales/pt/network-page.json +++ b/public/locales/pt/network-page.json @@ -1,192 +1,223 @@ { "activeAddresses": "Endereços ativos", + "addAddress": "Adicionar endereço", "address": "Endereço", + "addressID": "ID do Endereço", + "addressNotFoundError": "Erro: Não conseguimos encontrar o endereço que você está procurando. Verifique o endereço e tente novamente.", + "addressPlaceholder": "60 caracteres endereço", + "allProcedures": "All procedures", + "allTransactionsLoaded": "Você viu todas as transações", "amount": "Quantidade", - "circulatingSupply": "Oferta circulante", + "amountAssetAny": "Todos", + "amountAssetOther": "Outros Ativos", + "amountAssetQubic": "QUBIC", + "amountAssetType": "Ativo", + "amount100Mto1B": "100mi - 1bi", + "amount1Bto10B": "1bi - 10bi", + "amount1Mto100M": "1mi - 100mi", + "amount1to1M": "1 - 1mi", + "amountFilterHint": "Valor da transação em QUBIC.", + "amountOver0": "> 0", + "amountOver10B": "> 10bi", + "applyFilters": "Aplicar filtros", + "assetIssuer": "Emissor do Ativo", + "assets": "Ativos", + "assetsRichList": "Lista de Ricos em Ativos", + "assetsRichListDesc": "A lista mostra os principais detentores de cada ativo.", + "assetsRichListInvalidAssetOrIssuer": "Erro: Endereço do emissor e/ou nome do ativo inválidos. Por favor, verifique os parâmetros da URL.", + "assetTransferWarning": "Esta transação está visível apenas na lista de transações do endereço de origem. Exibi-la na lista de transações do endereço de destino ainda não é suportado na versão atual.", + "billionShort": "bi", + "blockchain": "Blockchain", "burnedSupply": "Suprimento Queimado", + "checkContractProposal": "Ver proposta do contrato", + "checkContractShareholders": "Ver acionistas do contrato", + "checkSourceCode": "Ver código-fonte", + "circulatingSupply": "Oferta circulante", + "clearAllFiltersTooltip": "Clique para remover todos os filtros", + "collapseAll": "Recolher tudo", "complete": "Completo", + "contract": "Contrato", + "contractIndex": "Índice", + "contractMessageType": "Tipo de mensagem do contrato", + "contractName": "Nome do contrato", + "copied": "Copiado", + "copyAddress": "Copiar endereço", + "copyToClipboard": "Copiar para a área de transferência", + "copyTransactionId": "Copiar ID de TX", "currentTick": "Tick atual", + "customRange": "Intervalo personalizado", + "data": "Dados", "dataStatus": "Status dos dados", "date": "Data", + "dateLast24Hours": "Últimas 24 horas", + "dateLastHour": "Última hora", + "dateLastNDays": "Últimos {{count}} dias", + "deductedAmount": "Valor Deduzido", + "defaultView": "Visualização padrão", "destination": "Destino", + "destinationFilterHint": "Para transações de contratos inteligentes, este é o endereço do contrato, não o destinatário final do token.", + "direction": "Direção", + "directionAll": "Todas", + "directionIncoming": "Entrada", + "directionIncomingTooltip": "Transações de entrada", + "directionOutgoing": "Saída", + "directionOutgoingTooltip": "Transações de saída", + "duplicateAddress": "Endereço duplicado", "empty": "Vazio", + "endDate": "Data final", + "endTick": "Tick final", + "endTickTooLarge": "O valor do tick final é muito grande.", "entityReportsFromRandomPeers": "Relatórios de pares aleatórios", "epoch": "Época", "epochTicks": "Ticks da época", + "errorLoadingPrice": "Erro: Falha ao carregar o preço. Por favor, atualize a página ou tente novamente mais tarde.", "errorMessage": "Beta - Os dados podem estar incompletos ou incorretos", + "estimatedWait": "Tempo estimado de espera", + "event": "Evento", + "eventDetails": "Detalhes do Evento", + "eventNotFound": "Evento não encontrado", + "events": "Eventos", + "eventsBetaDisclaimer": "Esta funcionalidade está em desenvolvimento. Apenas dados recentes são incluídos e podem ser removidos até que uma versão pronta para produção esteja disponível. Os endpoints de API utilizados estão sujeitos a alterações em versões futuras.", + "eventsFound": "{{count}} eventos", + "eventsLoadFailed": "Erro: Falha ao carregar os eventos. Por favor, tente novamente.", + "eventType": "Tipo de evento", + "exchange": "Câmbio", + "exchanges": "Câmbios", + "exchangesLoadFailed": "Erro: Falha ao carregar as câmbios. Por favor, atualize a página ou tente novamente mais tarde.", + "exclude": "Excluir", "executed": "Executado", - "txStatusExecuted": "Transação executada", - "txStatusFailed": "Transferência falhou", + "expandAll": "Expandir tudo", + "exportJson": "Exportar JSON", + "fee": "Taxa", + "filterAll": "Todas", + "filterAllTransactions": "Todas as transações", + "filterApproved": "Aprovadas", + "filterApprovedTransactions": "Transações aprovadas", + "filterButton": "Filtrar", + "filterByCategory": "Filtrar por categoria", + "filters": "Filtros", + "filterTransfer": "Transferências", + "filterTransferTransactions": "Transações de transferência", + "hide": "Esconder", "id": "ID", - "incomingTransfers": "Transferências de entrada", + "include": "Incluir", "incomingAmount": "Quantia de entrada", + "incomingTransfers": "Transferências de entrada", "incomplete": "Incompleto", + "inputType": "Tipo de entrada", + "invalidAddressError": "O formato do endereço é inválido. Verifique o endereço e tente novamente.", + "invalidAddressFormat": "O endereço deve conter 60 letras maiúsculas", + "invalidDateRange": "A data inicial deve ser anterior à data final", + "invalidDestinationIdentity": "Endereço de destino inválido: {{address}}", + "invalidRangeAmount": "Intervalo inválido - o valor inicial deve ser menor ou igual ao valor final", + "invalidRangeInputType": "O valor mínimo deve ser menor ou igual ao valor máximo", + "invalidSourceIdentity": "Endereço de origem inválido: {{address}}", + "invalidTickRange": "O tick inicial não pode ser maior que o tick final", + "invalidTransactionId": "ID de transação inválido. Verifique o ID e tente novamente.", + "issuer": "Emissor", "lastNTickQuality": "Qualidade do Tick dos Últimos 10K", "lastNTickQualityTooltip": "Percentagem de ticks não vazios nos últimos 10K ticks, independentemente do epoch", "latest": "Mais recente", + "latestStatsLoadFailed": "Erro: Falha ao carregar as estatísticas mais recentes. Por favor, atualize a página ou tente novamente mais tarde.", "latestTransfers": "Transferências mais recentes", + "loading": "Carregando", + "loadingTransactionsError": "Erro: Algo deu errado ao carregar as transações. Por favor, tente novamente mais tarde.", + "loadMore": "Carregar mais", + "logDigest": "Resumo do Log", + "managedBy": "Gerenciado por {{contract}}", + "managingContractIndex": "Índice do Contrato de Gestão", "marketCap": "Capitalização de mercado", + "matchingEntities": "Resultados correspondentes", + "maxAddressesReached": "Máximo de 5 endereços atingido", + "maxAmount": "Valor máximo", + "maxAmountTooLarge": "O valor máximo do montante é muito grande.", + "maxInputType": "Valor máx.", + "maxInputTypeTooLarge": "O valor máximo do tipo de entrada é muito grande.", + "maxResultsHint": "Os resultados estão limitados a {{count}}. Use filtros para refinar a lista.", + "millionShort": "mi", + "minAmount": "Valor mínimo", + "minAmountTooLarge": "O valor mínimo do montante é muito grande.", + "minInputType": "Valor mín.", + "minInputTypeTooLarge": "O valor mínimo do tipo de entrada é muito grande.", + "name": "Nome", + "noEntries": "Nenhuma entrada para mostrar", + "noEvents": "Não há eventos", "nonEmpty": "Não vazio", + "noTransactions": "Não há transações", + "numberOfDecimalPlaces": "Casas Decimais", "numberOfTransactions": "Número de transações", - "outgoingTransfers": "Transferências de saída", "outgoingAmount": "Quantia de saída", + "outgoingTransfers": "Transferências de saída", "price": "Preço", + "qrcodeModalTitle": "Código QR de endereço", + "rank": "Classificação", + "remainingAmount": "Valor Restante", + "removeAddress": "Remover endereço", + "resetFilters": "Redefinir filtros", + "richList": "Lista de Ricos", + "richListLoadFailed": "Erro: Falha ao carregar a lista de ricos. Por favor, atualize a página ou tente novamente mais tarde.", + "richListWarning": "Os dados da lista de ricos são atualizados no início de cada época", + "scShares": "Ações de Contratos Inteligentes", "search": "Pesquisar", + "selectAsset": "Selecionar ativo", + "selectContract": "Select contract", + "selectUpTo": "Selecione até {{count}}", + "shareholders": "Acionistas", + "sharesIssuer": "Emissor de Ações", + "show": "Mostrar", + "showingMaxTransactions": "Mostrando as últimas {{count}} transações", + "showingMaxEvents": "Mostrando os últimos {{count}} eventos", + "showLess": "Mostrar menos", + "showMore": "Mostrar mais", + "showPerPagePrefix": "Mostrar", + "showPerPageSuffix": "por página", + "showQRCode": "Mostrar código QR", "signature": "Assinatura", + "smartContract": "Contrato Inteligente", + "smartContractCodeLoadError": "Erro: Falha ao carregar o código do contrato inteligente. Por favor, atualize a página ou tente novamente mais tarde.", + "smartContracts": "Contratos Inteligentes", + "smartContractsLoadFailed": "Erro: Falha ao carregar os contratos inteligentes. Por favor, atualize a página ou tente novamente mais tarde.", "source": "Fonte", "standard": "Padrão", - "txStatusSuccess": "Transferência bem-sucedida", + "startDate": "Data inicial", + "startTick": "Tick inicial", + "startTickTooLarge": "O valor do tick inicial é muito grande.", + "status": "Status", + "targetTick": "Tick alvo", + "thousandShort": "mil", "tick": "Tick", + "tickCheckFailed": "Não foi possível verificar o estado do tick. Por favor, tente novamente mais tarde.", "tickLeader": "Líder de Tick", "tickQuality": "Qualidade do Tick da Época", "tickQualityTooltip": "Percentagem de ticks não vazios neste epoch", "ticks": "Ticks", + "ticksLoadFailed": "Erro: Falha ao carregar ticks. Por favor, atualize a página ou tente novamente mais tarde. Se for quarta-feira, aguarde até o início da próxima época.", "tickStatus": "Status do tick", "ticksThisEpoch": "Ticks nesta época (Vazios)", "ticksThisEpochTooltip": "Número total de ticks processados neste epoch e a quantidade de ticks vazios entre parênteses", + "timestamp": "Carimbo de tempo", + "token": "Token", + "tokenIssuer": "{{name}} Emissor", + "tokenLoadFailed": "Erro: Falha ao carregar os tokens. Por favor, atualize a página ou tente novamente mais tarde.", + "tokens": "Tokens", + "totalValueLocked": "Valor Total Bloqueado (TVL)", + "transactionNotFound": "Transação não encontrada", "transactionPreview": "Visualização da transação", "transactions": "Transações", - "data": "Dados", - "defaultView": "Visualização padrão", "unableToDecodeContractInput": "Não foi possível decodificar a entrada deste contrato.", - "exportJson": "Exportar JSON", - "showMore": "Mostrar mais", - "showLess": "Mostrar menos", - "type": "Tipo", "unexecuted": "Não executado", "value": "Valor", - "loadMore": "Carregar mais", - "noTransactions": "Não há transações", - "allTransactionsLoaded": "Você viu todas as transações", - "loading": "Carregando", - "hide": "Esconder", - "show": "Mostrar", - "transactionNotFound": "Transação não encontrada", - "invalidTransactionId": "ID de transação inválido. Verifique o ID e tente novamente.", - "invalidAddressError": "O formato do endereço é inválido. Verifique o endereço e tente novamente.", - "addressNotFoundError": "Erro: Não conseguimos encontrar o endereço que você está procurando. Verifique o endereço e tente novamente.", - "loadingTransactionsError": "Erro: Algo deu errado ao carregar as transações. Por favor, tente novamente mais tarde.", - "invalidSourceIdentity": "Endereço de origem inválido: {{address}}", - "invalidDestinationIdentity": "Endereço de destino inválido: {{address}}", - "minInputTypeTooLarge": "O valor mínimo do tipo de entrada é muito grande.", - "maxInputTypeTooLarge": "O valor máximo do tipo de entrada é muito grande.", - "minAmountTooLarge": "O valor mínimo do montante é muito grande.", - "maxAmountTooLarge": "O valor máximo do montante é muito grande.", - "startTickTooLarge": "O valor do tick inicial é muito grande.", - "endTickTooLarge": "O valor do tick final é muito grande.", - "richList": "Lista de Ricos", - "rank": "Classificação", - "addressID": "ID do Endereço", - "richListLoadFailed": "Erro: Falha ao carregar a lista de ricos. Por favor, atualize a página ou tente novamente mais tarde.", - "assetsRichListInvalidAssetOrIssuer": "Erro: Endereço do emissor e/ou nome do ativo inválidos. Por favor, verifique os parâmetros da URL.", - "richListWarning": "Os dados da lista de ricos são atualizados no início de cada época", - "timestamp": "Carimbo de tempo", - "fee": "Taxa", - "assetTransferWarning": "Esta transação está visível apenas na lista de transações do endereço de origem. Exibi-la na lista de transações do endereço de destino ainda não é suportado na versão atual.", - "ticksLoadFailed": "Erro: Falha ao carregar ticks. Por favor, atualize a página ou tente novamente mais tarde. Se for quarta-feira, aguarde até o início da próxima época.", - "showItemsPerPage": "Mostrar {{count}} itens", - "itemsPerPage": "Itens por página", - "latestStatsLoadFailed": "Erro: Falha ao carregar as estatísticas mais recentes. Por favor, atualize a página ou tente novamente mais tarde.", - "errorLoadingPrice": "Erro: Falha ao carregar o preço. Por favor, atualize a página ou tente novamente mais tarde.", - "exchange": "Câmbio", - "exchanges": "Câmbios", - "exchangesLoadFailed": "Erro: Falha ao carregar as câmbios. Por favor, atualize a página ou tente novamente mais tarde.", - "token": "Token", - "tokens": "Tokens", - "smartContract": "Contrato Inteligente", - "smartContracts": "Contratos Inteligentes", - "tokenLoadFailed": "Erro: Falha ao carregar os tokens. Por favor, atualize a página ou tente novamente mais tarde.", - "smartContractsLoadFailed": "Erro: Falha ao carregar os contratos inteligentes. Por favor, atualize a página ou tente novamente mais tarde.", - "issuer": "Emissor", - "name": "Nome", "wallets": "Carteiras", - "assets": "Ativos", - "totalValueLocked": "Valor Total Bloqueado (TVL)", - "noEntries": "Nenhuma entrada para mostrar", - "assetsRichList": "Lista de Ricos em Ativos", - "assetsRichListDesc": "A lista mostra os principais detentores de cada ativo.", - "contractName": "Nome do contrato", - "shareholders": "Acionistas", - "checkContractShareholders": "Ver acionistas do contrato", - "checkSourceCode": "Ver código-fonte", - "checkContractProposal": "Ver proposta do contrato", - "smartContractCodeLoadError": "Erro: Falha ao carregar o código do contrato inteligente. Por favor, atualize a página ou tente novamente mais tarde.", - "contract": "Contrato", - "copied": "Copiado", - "copyAddress": "Copiar endereço", - "copyTransactionId": "Copiar ID de TX", - "copyToClipboard": "Copiar para a área de transferência", - "showQRCode": "Mostrar código QR", - "qrcodeModalTitle": "Código QR de endereço", - "filterAllTransactions": "Todas as transações", - "filterTransferTransactions": "Transações de transferência", - "filterApprovedTransactions": "Transações aprovadas", - "filterAll": "Todas", - "filterTransfer": "Transferências", - "filterApproved": "Aprovadas", - "filterByCategory": "Filtrar por categoria", - "selectAsset": "Selecionar ativo", - "scShares": "Ações de Contratos Inteligentes", - "sharesIssuer": "Emissor de Ações", - "contractIndex": "Índice", - "expandAll": "Expandir tudo", - "collapseAll": "Recolher tudo", - "managedBy": "Gerenciado por {{contract}}", "unknownContract": "Contrato desconhecido", - "resetFilters": "Redefinir filtros", - "startTick": "Tick inicial", - "endTick": "Tick final", - "startDate": "Data inicial", - "endDate": "Data final", - "invalidRangeAmount": "Intervalo inválido - o valor inicial deve ser menor ou igual ao valor final", - "invalidRangeInputType": "O valor mínimo deve ser menor ou igual ao valor máximo", - "invalidTickRange": "O tick inicial não pode ser maior que o tick final", - "invalidDateRange": "A data inicial deve ser anterior à data final", - "invalidAddressFormat": "O endereço deve conter 60 letras maiúsculas", - "destinationFilterHint": "Para transações de contratos inteligentes, este é o endereço do contrato, não o destinatário final do token.", - "amountFilterHint": "Valor da transação em QUBIC.", - "customRange": "Intervalo personalizado", - "minAmount": "Valor mínimo", - "maxAmount": "Valor máximo", - "amountOver0": "> 0", - "amount1to1M": "1 - 1mi", - "amount1Mto100M": "1mi - 100mi", - "amount100Mto1B": "100mi - 1bi", - "amount1Bto10B": "1bi - 10bi", - "amountOver10B": "> 10bi", - "thousandShort": "mil", - "millionShort": "mi", - "billionShort": "bi", - "dateLastHour": "Última hora", - "dateLast24Hours": "Últimas 24 horas", - "dateLastNDays": "Últimos {{count}} dias", - "addressPlaceholder": "60 caracteres endereço", - "clearAllFiltersTooltip": "Clique para remover todos os filtros", - "filterButton": "Filtrar", - "filters": "Filtros", - "applyFilters": "Aplicar filtros", - "directionAll": "Todas", - "directionIncoming": "Entrada", - "directionOutgoing": "Saída", - "directionIncomingTooltip": "Transações de entrada", - "directionOutgoingTooltip": "Transações de saída", - "direction": "Direção", - "inputType": "Tipo de entrada", - "minInputType": "Valor mín.", - "maxInputType": "Valor máx.", - "include": "Incluir", - "exclude": "Excluir", - "addAddress": "Adicionar endereço", - "removeAddress": "Remover endereço", - "maxAddressesReached": "Máximo de 5 endereços atingido", - "duplicateAddress": "Endereço duplicado", - "matchingEntities": "Resultados correspondentes", "transactionsFound": "{{count}} transações", - "showingMaxTransactions": "Mostrando as últimas {{count}} transações", - "maxResultsHint": "Os resultados estão limitados a {{count}}. Use filtros para refinar a lista.", - "waitingForTickTitle": "Aguardando o processamento do tick", + "txID": "TX ID", + "txStatusExecuted": "Transação executada", + "txStatusFailed": "Transferência falhou", + "txStatusSuccess": "Transferência bem-sucedida", + "txType": "TX Type", + "unitOfMeasurement": "Unidade de Medida", + "virtualTransactionBanner": "Esta é uma transação virtual gerada pelo protocolo Qubic para eventos do ciclo de vida de contratos inteligentes.", + "virtualTransactionNotFound": "Transação virtual não encontrada", "waitingForTickDesc": "Esta transação está agendada para o tick {{targetTick}}. Ficará disponível assim que a rede atingir esse tick.", - "targetTick": "Tick alvo", - "estimatedWait": "Tempo estimado de espera", - "tickCheckFailed": "Não foi possível verificar o estado do tick. Por favor, tente novamente mais tarde." + "waitingForTickTitle": "Aguardando o processamento do tick" } diff --git a/public/locales/ru/global.json b/public/locales/ru/global.json index 21a608e4..672930bf 100644 --- a/public/locales/ru/global.json +++ b/public/locales/ru/global.json @@ -1,9 +1,12 @@ { "address": "Адрес", "balance": "Баланс", + "navigationMenu": "Меню навигации", "noResultsFound": "Результаты Не Найдены", "noResultsFoundFor": "Результаты не найдены для \"{{keyword}}\"", "searchPlaceholder": "Поиск TX, тиков, ID, токенов, контрактов, бирж...", + "selectLanguage": "Выбрать язык", + "selectNetwork": "Выбрать сеть", "tick": "Тик", "transaction": "Транзакция", "transactions": "Транзакции" diff --git a/public/locales/ru/network-page.json b/public/locales/ru/network-page.json index 6d4e34b5..8189072e 100644 --- a/public/locales/ru/network-page.json +++ b/public/locales/ru/network-page.json @@ -1,192 +1,223 @@ { "activeAddresses": "Активные адреса", + "addAddress": "Добавить адрес", "address": "Адрес", + "addressID": "ID адреса", + "addressNotFoundError": "Ошибка: Мы не смогли найти адрес, который вы ищете. Пожалуйста, проверьте адрес и попробуйте снова.", + "addressPlaceholder": "60 символов адрес", + "allProcedures": "All procedures", + "allTransactionsLoaded": "Вы просмотрели все транзакции", "amount": "Сумма", - "circulatingSupply": "Объем в обращении", + "amountAssetAny": "Все", + "amountAssetOther": "Другие активы", + "amountAssetQubic": "QUBIC", + "amountAssetType": "Актив", + "amount100Mto1B": "100млн - 1млрд", + "amount1Bto10B": "1млрд - 10млрд", + "amount1Mto100M": "1млн - 100млн", + "amount1to1M": "1 - 1млн", + "amountFilterHint": "Сумма транзакции в QUBIC.", + "amountOver0": "> 0", + "amountOver10B": "> 10млрд", + "applyFilters": "Применить фильтры", + "assetIssuer": "Эмитент актива", + "assets": "Активы", + "assetsRichList": "Рейтинг держателей активов", + "assetsRichListDesc": "Рейтинг показывает крупнейших держателей каждого актива.", + "assetsRichListInvalidAssetOrIssuer": "Ошибка: Неверный адрес эмитента и/или название актива. Пожалуйста, проверьте параметры URL.", + "assetTransferWarning": "Эта транзакция видна только в списке транзакций исходного адреса. Отображение в списке транзакций адреса назначения пока не поддерживается в текущей версии.", + "billionShort": "млрд", + "blockchain": "Blockchain", "burnedSupply": "Сожжённое Предложение", + "checkContractProposal": "Посмотреть предложение контракта", + "checkContractShareholders": "Посмотреть акционеров контракта", + "checkSourceCode": "Посмотреть исходный код", + "circulatingSupply": "Объем в обращении", + "clearAllFiltersTooltip": "Нажмите, чтобы удалить все фильтры", + "collapseAll": "Свернуть все", "complete": "Завершено", + "contract": "Контракт", + "contractIndex": "Индекс", + "contractMessageType": "Тип сообщения контракта", + "contractName": "Название контракта", + "copied": "Скопировано", + "copyAddress": "Копировать адрес", + "copyToClipboard": "Скопировать в буфер обмена", + "copyTransactionId": "Копировать ID TX", "currentTick": "Текущий тик", + "customRange": "Произвольный диапазон", + "data": "Данные", "dataStatus": "Состояние данных", "date": "Дата", + "dateLast24Hours": "Последние 24 часа", + "dateLastHour": "Последний час", + "dateLastNDays": "Последние {{count}} дней", + "deductedAmount": "Вычтенная сумма", + "defaultView": "Вид по умолчанию", "destination": "Получатель", + "destinationFilterHint": "Для транзакций смарт-контрактов это адрес контракта, а не конечный получатель токена.", + "direction": "Направление", + "directionAll": "Все", + "directionIncoming": "Входящие", + "directionIncomingTooltip": "Входящие транзакции", + "directionOutgoing": "Исходящие", + "directionOutgoingTooltip": "Исходящие транзакции", + "duplicateAddress": "Повторяющийся адрес", "empty": "Пусто", + "endDate": "По дату", + "endTick": "Конечный тик", + "endTickTooLarge": "Значение конечного тика слишком велико.", "entityReportsFromRandomPeers": "Отчеты от случайных пиров", "epoch": "Эпоха", "epochTicks": "Тики эпохи", + "errorLoadingPrice": "Ошибка: Не удалось загрузить цену. Обновите страницу или попробуйте позже.", "errorMessage": "Бета - данные могут быть неполными или содержать ошибки", + "estimatedWait": "Ожидаемое время ожидания", + "event": "Событие", + "eventDetails": "Детали события", + "eventNotFound": "Событие не найдено", + "events": "События", + "eventsBetaDisclaimer": "Эта функция находится в разработке. Включены только недавние данные, которые могут быть удалены до выхода стабильной версии. Используемые API-эндпоинты могут измениться в будущих выпусках.", + "eventsFound": "{{count}} событий", + "eventsLoadFailed": "Ошибка: Не удалось загрузить события. Пожалуйста, попробуйте снова.", + "eventType": "Тип события", + "exchange": "Биржа", + "exchanges": "Биржи", + "exchangesLoadFailed": "Ошибка: Не удалось загрузить биржи. Обновите страницу или попробуйте позже.", + "exclude": "Исключить", "executed": "Выполнено", - "txStatusExecuted": "Транзакция выполнена", - "txStatusFailed": "Перевод не удался", + "expandAll": "Развернуть все", + "exportJson": "Экспортировать JSON", + "fee": "Комиссия", + "filterAll": "Все", + "filterAllTransactions": "Все транзакции", + "filterApproved": "Одобрено", + "filterApprovedTransactions": "Одобренные транзакции", + "filterButton": "Фильтр", + "filterByCategory": "Фильтр по категории", + "filters": "Фильтры", + "filterTransfer": "Переводы", + "filterTransferTransactions": "Транзакции перевода", + "hide": "Скрыть", "id": "ID", - "incomingTransfers": "Входящие переводы", + "include": "Включить", "incomingAmount": "Входящая сумма", + "incomingTransfers": "Входящие переводы", "incomplete": "Незавершённый", + "inputType": "Тип ввода", + "invalidAddressError": "Недопустимый формат адреса. Пожалуйста, проверьте адрес и попробуйте снова.", + "invalidAddressFormat": "Адрес должен содержать 60 заглавных букв", + "invalidDateRange": "Начальная дата должна быть раньше конечной даты", + "invalidDestinationIdentity": "Недействительный адрес получателя: {{address}}", + "invalidRangeAmount": "Недопустимый диапазон - начальное значение должно быть меньше или равно конечному", + "invalidRangeInputType": "Минимальное значение должно быть меньше или равно максимальному", + "invalidSourceIdentity": "Недействительный адрес отправителя: {{address}}", + "invalidTickRange": "Начальный тик не может быть больше конечного тика", + "invalidTransactionId": "Недействительный ID транзакции. Пожалуйста, проверьте ID и попробуйте снова.", + "issuer": "Эмитент", "lastNTickQuality": "Качество Тика за последние 10K", "lastNTickQualityTooltip": "Процент непустых тиков в последних 10K тиках, независимо от эпохи", "latest": "Последний", + "latestStatsLoadFailed": "Ошибка: Не удалось загрузить последние статистические данные. Обновите страницу или попробуйте позже.", "latestTransfers": "Последние переводы", + "loading": "Загрузка", + "loadingTransactionsError": "Ошибка: Произошла ошибка при загрузке транзакций. Пожалуйста, попробуйте позже.", + "loadMore": "Загрузить еще", + "logDigest": "Дайджест лога", + "managedBy": "Управляется {{contract}}", + "managingContractIndex": "Индекс управляющего контракта", "marketCap": "Рыночная капитализация", + "matchingEntities": "Совпадающие результаты", + "maxAddressesReached": "Достигнуто максимум 5 адресов", + "maxAmount": "Максимальная сумма", + "maxAmountTooLarge": "Максимальное значение суммы слишком велико.", + "maxInputType": "Макс. значение", + "maxInputTypeTooLarge": "Максимальное значение типа ввода слишком велико.", + "maxResultsHint": "Результаты ограничены {{count}}. Используйте фильтры для уточнения списка.", + "millionShort": "млн", + "minAmount": "Минимальная сумма", + "minAmountTooLarge": "Минимальное значение суммы слишком велико.", + "minInputType": "Мин. значение", + "minInputTypeTooLarge": "Минимальное значение типа ввода слишком велико.", + "name": "Имя", + "noEntries": "Нет записей для отображения", + "noEvents": "Нет событий", "nonEmpty": "Не пустой", + "noTransactions": "Нет транзакций", + "numberOfDecimalPlaces": "Десятичные знаки", "numberOfTransactions": "Количество транзакций", - "outgoingTransfers": "Исходящие переводы", "outgoingAmount": "Исходящая сумма", + "outgoingTransfers": "Исходящие переводы", "price": "Цена", + "qrcodeModalTitle": "QR-код адреса", + "rank": "Ранг", + "remainingAmount": "Оставшаяся сумма", + "removeAddress": "Удалить адрес", + "resetFilters": "Сбросить фильтры", + "richList": "Список богатых", + "richListLoadFailed": "Ошибка: Не удалось загрузить список богатых. Пожалуйста, обновите страницу или попробуйте позже.", + "richListWarning": "Данные списка богатых обновляются в начале каждой эпохи", + "scShares": "Доли смарт-контрактов", "search": "Поиск", + "selectAsset": "Выберите актив", + "selectContract": "Select contract", + "selectUpTo": "Выберите до {{count}}", + "shareholders": "Акционеры", + "sharesIssuer": "Эмитент акций", + "show": "Показать", + "showingMaxTransactions": "Показаны последние {{count}} транзакций", + "showingMaxEvents": "Показаны последние {{count}} событий", + "showLess": "Показать меньше", + "showMore": "Показать больше", + "showPerPagePrefix": "Показать", + "showPerPageSuffix": "на странице", + "showQRCode": "Показать QR-код", "signature": "Подпись", + "smartContract": "Смарт-контракт", + "smartContractCodeLoadError": "Ошибка: Не удалось загрузить код смарт-контракта. Обновите страницу или попробуйте позже.", + "smartContracts": "Смарт-контракты", + "smartContractsLoadFailed": "Ошибка: Не удалось загрузить смарт-контракты. Обновите страницу или попробуйте позже.", "source": "Источник", "standard": "Стандарт", - "txStatusSuccess": "Успешный перевод", + "startDate": "С даты", + "startTick": "Начальный тик", + "startTickTooLarge": "Значение начального тика слишком велико.", + "status": "Status", + "targetTick": "Целевой тик", + "thousandShort": "тыс", "tick": "Тик", + "tickCheckFailed": "Не удалось проверить статус тика. Пожалуйста, попробуйте позже.", "tickLeader": "Лидер тика", "tickQuality": "Качество Тика Эпохи", "tickQualityTooltip": "Процент непустых тиков в этой эпохе", "ticks": "Тики", + "ticksLoadFailed": "Ошибка: Не удалось загрузить тики. Обновите страницу или попробуйте позже. Если сегодня среда, подождите до начала следующей эпохи.", "tickStatus": "Статус тика", "ticksThisEpoch": "Тики в этой эпохе (Пустые)", "ticksThisEpochTooltip": "Общее количество тиков в этом эпохе и количество пустых тиков в скобках", + "timestamp": "Метка времени", + "token": "Токен", + "tokenIssuer": "{{name}} Эмитент", + "tokenLoadFailed": "Ошибка: Не удалось загрузить токены. Обновите страницу или попробуйте позже.", + "tokens": "Токены", + "totalValueLocked": "Общая заблокированная стоимость (TVL)", + "transactionNotFound": "Транзакция не найдена", "transactionPreview": "Предварительный просмотр транзакции", "transactions": "Транзакции", - "data": "Данные", - "defaultView": "Вид по умолчанию", "unableToDecodeContractInput": "Не удалось декодировать входные данные этого контракта.", - "exportJson": "Экспортировать JSON", - "showMore": "Показать больше", - "showLess": "Показать меньше", - "type": "Тип", "unexecuted": "Не выполнено", "value": "Значение", - "loadMore": "Загрузить еще", - "noTransactions": "Нет транзакций", - "allTransactionsLoaded": "Вы просмотрели все транзакции", - "loading": "Загрузка", - "hide": "Скрыть", - "show": "Показать", - "transactionNotFound": "Транзакция не найдена", - "invalidTransactionId": "Недействительный ID транзакции. Пожалуйста, проверьте ID и попробуйте снова.", - "invalidAddressError": "Недопустимый формат адреса. Пожалуйста, проверьте адрес и попробуйте снова.", - "addressNotFoundError": "Ошибка: Мы не смогли найти адрес, который вы ищете. Пожалуйста, проверьте адрес и попробуйте снова.", - "loadingTransactionsError": "Ошибка: Произошла ошибка при загрузке транзакций. Пожалуйста, попробуйте позже.", - "invalidSourceIdentity": "Недействительный адрес отправителя: {{address}}", - "invalidDestinationIdentity": "Недействительный адрес получателя: {{address}}", - "minInputTypeTooLarge": "Минимальное значение типа ввода слишком велико.", - "maxInputTypeTooLarge": "Максимальное значение типа ввода слишком велико.", - "minAmountTooLarge": "Минимальное значение суммы слишком велико.", - "maxAmountTooLarge": "Максимальное значение суммы слишком велико.", - "startTickTooLarge": "Значение начального тика слишком велико.", - "endTickTooLarge": "Значение конечного тика слишком велико.", - "richList": "Список богатых", - "rank": "Ранг", - "addressID": "ID адреса", - "richListLoadFailed": "Ошибка: Не удалось загрузить список богатых. Пожалуйста, обновите страницу или попробуйте позже.", - "assetsRichListInvalidAssetOrIssuer": "Ошибка: Неверный адрес эмитента и/или название актива. Пожалуйста, проверьте параметры URL.", - "richListWarning": "Данные списка богатых обновляются в начале каждой эпохи", - "timestamp": "Метка времени", - "fee": "Комиссия", - "assetTransferWarning": "Эта транзакция видна только в списке транзакций исходного адреса. Отображение в списке транзакций адреса назначения пока не поддерживается в текущей версии.", - "ticksLoadFailed": "Ошибка: Не удалось загрузить тики. Обновите страницу или попробуйте позже. Если сегодня среда, подождите до начала следующей эпохи.", - "showItemsPerPage": "Показать {{count}} элементов", - "itemsPerPage": "Элементов на страницу", - "latestStatsLoadFailed": "Ошибка: Не удалось загрузить последние статистические данные. Обновите страницу или попробуйте позже.", - "errorLoadingPrice": "Ошибка: Не удалось загрузить цену. Обновите страницу или попробуйте позже.", - "exchange": "Биржа", - "exchanges": "Биржи", - "exchangesLoadFailed": "Ошибка: Не удалось загрузить биржи. Обновите страницу или попробуйте позже.", - "token": "Токен", - "tokens": "Токены", - "smartContract": "Смарт-контракт", - "smartContracts": "Смарт-контракты", - "tokenLoadFailed": "Ошибка: Не удалось загрузить токены. Обновите страницу или попробуйте позже.", - "smartContractsLoadFailed": "Ошибка: Не удалось загрузить смарт-контракты. Обновите страницу или попробуйте позже.", - "issuer": "Эмитент", - "name": "Имя", "wallets": "Кошельки", - "assets": "Активы", - "totalValueLocked": "Общая заблокированная стоимость (TVL)", - "noEntries": "Нет записей для отображения", - "assetsRichList": "Рейтинг держателей активов", - "assetsRichListDesc": "Рейтинг показывает крупнейших держателей каждого актива.", - "contractName": "Название контракта", - "shareholders": "Акционеры", - "checkContractShareholders": "Посмотреть акционеров контракта", - "checkSourceCode": "Посмотреть исходный код", - "checkContractProposal": "Посмотреть предложение контракта", - "smartContractCodeLoadError": "Ошибка: Не удалось загрузить код смарт-контракта. Обновите страницу или попробуйте позже.", - "contract": "Контракт", - "copied": "Скопировано", - "copyAddress": "Копировать адрес", - "copyTransactionId": "Копировать ID TX", - "copyToClipboard": "Скопировать в буфер обмена", - "showQRCode": "Показать QR-код", - "qrcodeModalTitle": "QR-код адреса", - "filterAllTransactions": "Все транзакции", - "filterTransferTransactions": "Транзакции перевода", - "filterApprovedTransactions": "Одобренные транзакции", - "filterAll": "Все", - "filterTransfer": "Переводы", - "filterApproved": "Одобрено", - "filterByCategory": "Фильтр по категории", - "selectAsset": "Выберите актив", - "scShares": "Доли смарт-контрактов", - "sharesIssuer": "Эмитент акций", - "contractIndex": "Индекс", - "expandAll": "Развернуть все", - "collapseAll": "Свернуть все", - "managedBy": "Управляется {{contract}}", "unknownContract": "Неизвестный контракт", - "resetFilters": "Сбросить фильтры", - "startTick": "Начальный тик", - "endTick": "Конечный тик", - "startDate": "С даты", - "endDate": "По дату", - "invalidRangeAmount": "Недопустимый диапазон - начальное значение должно быть меньше или равно конечному", - "invalidRangeInputType": "Минимальное значение должно быть меньше или равно максимальному", - "invalidTickRange": "Начальный тик не может быть больше конечного тика", - "invalidDateRange": "Начальная дата должна быть раньше конечной даты", - "invalidAddressFormat": "Адрес должен содержать 60 заглавных букв", - "destinationFilterHint": "Для транзакций смарт-контрактов это адрес контракта, а не конечный получатель токена.", - "amountFilterHint": "Сумма транзакции в QUBIC.", - "customRange": "Произвольный диапазон", - "minAmount": "Минимальная сумма", - "maxAmount": "Максимальная сумма", - "amountOver0": "> 0", - "amount1to1M": "1 - 1млн", - "amount1Mto100M": "1млн - 100млн", - "amount100Mto1B": "100млн - 1млрд", - "amount1Bto10B": "1млрд - 10млрд", - "amountOver10B": "> 10млрд", - "thousandShort": "тыс", - "millionShort": "млн", - "billionShort": "млрд", - "dateLastHour": "Последний час", - "dateLast24Hours": "Последние 24 часа", - "dateLastNDays": "Последние {{count}} дней", - "addressPlaceholder": "60 символов адрес", - "clearAllFiltersTooltip": "Нажмите, чтобы удалить все фильтры", - "filterButton": "Фильтр", - "filters": "Фильтры", - "applyFilters": "Применить фильтры", - "directionAll": "Все", - "directionIncoming": "Входящие", - "directionOutgoing": "Исходящие", - "directionIncomingTooltip": "Входящие транзакции", - "directionOutgoingTooltip": "Исходящие транзакции", - "direction": "Направление", - "inputType": "Тип ввода", - "minInputType": "Мин. значение", - "maxInputType": "Макс. значение", - "include": "Включить", - "exclude": "Исключить", - "addAddress": "Добавить адрес", - "removeAddress": "Удалить адрес", - "maxAddressesReached": "Достигнуто максимум 5 адресов", - "duplicateAddress": "Повторяющийся адрес", - "matchingEntities": "Совпадающие результаты", "transactionsFound": "{{count}} транзакций", - "showingMaxTransactions": "Показаны последние {{count}} транзакций", - "maxResultsHint": "Результаты ограничены {{count}}. Используйте фильтры для уточнения списка.", - "waitingForTickTitle": "Ожидание обработки тика", + "txID": "TX ID", + "txStatusExecuted": "Транзакция выполнена", + "txStatusFailed": "Перевод не удался", + "txStatusSuccess": "Успешный перевод", + "txType": "TX Type", + "unitOfMeasurement": "Единица измерения", + "virtualTransactionBanner": "Это виртуальная транзакция, сгенерированная протоколом Qubic для событий жизненного цикла смарт-контрактов.", + "virtualTransactionNotFound": "Виртуальная транзакция не найдена", "waitingForTickDesc": "Эта транзакция запланирована на тик {{targetTick}}. Она станет доступна, когда сеть достигнет этого тика.", - "targetTick": "Целевой тик", - "estimatedWait": "Ожидаемое время ожидания", - "tickCheckFailed": "Не удалось проверить статус тика. Пожалуйста, попробуйте позже." + "waitingForTickTitle": "Ожидание обработки тика" } diff --git a/public/locales/tr/global.json b/public/locales/tr/global.json index 3bbd8838..64d05375 100644 --- a/public/locales/tr/global.json +++ b/public/locales/tr/global.json @@ -1,9 +1,12 @@ { "address": "Adres", "balance": "Bakiye", + "navigationMenu": "Gezinme menüsü", "noResultsFound": "Sonuç Bulunamadı", "noResultsFoundFor": "\"{{keyword}}\" için sonuç bulunamadı", "searchPlaceholder": "TX, tick, ID, token, sözleşme, borsa ara...", + "selectLanguage": "Dil seçin", + "selectNetwork": "Ağ seçin", "tick": "Tik", "transaction": "İşlem", "transactions": "İşlemler" diff --git a/public/locales/tr/network-page.json b/public/locales/tr/network-page.json index 59cfd40c..a5e867f5 100644 --- a/public/locales/tr/network-page.json +++ b/public/locales/tr/network-page.json @@ -1,192 +1,223 @@ { "activeAddresses": "Aktif adresler", + "addAddress": "Adres ekle", "address": "Adres", + "addressID": "Adres Kimliği", + "addressNotFoundError": "Hata: Aradığınız adresi bulamadık. Lütfen adresi kontrol edin ve tekrar deneyin.", + "addressPlaceholder": "60 karakter adresi", + "allProcedures": "All procedures", + "allTransactionsLoaded": "Tüm işlemleri gördünüz", "amount": "Miktar", - "circulatingSupply": "Dolaşımdaki arz", + "amountAssetAny": "Tümü", + "amountAssetOther": "Diğer Varlıklar", + "amountAssetQubic": "QUBIC", + "amountAssetType": "Varlık", + "amount100Mto1B": "100M - 1Mr", + "amount1Bto10B": "1Mr - 10Mr", + "amount1Mto100M": "1M - 100M", + "amount1to1M": "1 - 1M", + "amountFilterHint": "QUBIC cinsinden işlem tutarı.", + "amountOver0": "> 0", + "amountOver10B": "> 10Mr", + "applyFilters": "Filtreleri uygula", + "assetIssuer": "Varlık Yayımlayıcısı", + "assets": "Varlıklar", + "assetsRichList": "Varlık Zenginler Listesi", + "assetsRichListDesc": "Bu liste, her varlığın en büyük sahiplerini gösterir.", + "assetsRichListInvalidAssetOrIssuer": "Hata: Geçersiz ihraççı adresi ve/veya varlık adı. Lütfen URL parametrelerini kontrol edin.", + "assetTransferWarning": "Bu işlem yalnızca kaynak adresin işlem listesinde görünmektedir. Hedef adresin işlem listesinde gösterilmesi mevcut sürümde henüz desteklenmemektedir.", + "billionShort": "Mr", + "blockchain": "Blockchain", "burnedSupply": "Yakılmış Tedarik", + "checkContractProposal": "Sözleşme Önerisini Görüntüle", + "checkContractShareholders": "Sözleşme Hissedarlarını Görüntüle", + "checkSourceCode": "Kaynak Kodunu Görüntüle", + "circulatingSupply": "Dolaşımdaki arz", + "clearAllFiltersTooltip": "Tüm filtreleri kaldırmak için tıklayın", + "collapseAll": "Tümünü daralt", "complete": "Tamamlanmış", + "contract": "Sözleşme", + "contractIndex": "İndeks", + "contractMessageType": "Sözleşme mesaj türü", + "contractName": "Sözleşme Adı", + "copied": "Kopyalandı", + "copyAddress": "Adresi kopyala", + "copyToClipboard": "Panoya kopyala", + "copyTransactionId": "TX ID'sini kopyala", "currentTick": "Mevcut tik", + "customRange": "Özel aralık", + "data": "Veri", "dataStatus": "Veri durumu", "date": "Tarih", + "dateLast24Hours": "Son 24 saat", + "dateLastHour": "Son saat", + "dateLastNDays": "Son {{count}} gün", + "deductedAmount": "Düşülen Tutar", + "defaultView": "Varsayılan Görünüm", "destination": "Hedef", + "destinationFilterHint": "Akıllı sözleşme işlemleri için bu sözleşme adresidir, son token alıcısı değil.", + "direction": "Yön", + "directionAll": "Tümü", + "directionIncoming": "Gelen", + "directionIncomingTooltip": "Gelen işlemler", + "directionOutgoing": "Giden", + "directionOutgoingTooltip": "Giden işlemler", + "duplicateAddress": "Yinelenen adres", "empty": "Boş", + "endDate": "Bitiş tarihi", + "endTick": "Bitiş tick", + "endTickTooLarge": "Bitiş tick değeri çok büyük.", "entityReportsFromRandomPeers": "Rastgele eşlerden gelen raporlar", "epoch": "Çağ", "epochTicks": "Çağ Tikleri", + "errorLoadingPrice": "Hata: Fiyat yüklenemedi. Lütfen sayfayı yenileyin veya daha sonra tekrar deneyin.", "errorMessage": "Beta - Veriler eksik veya hatalı olabilir", + "estimatedWait": "Tahmini bekleme süresi", + "event": "Olay", + "eventDetails": "Olay Detayları", + "eventNotFound": "Olay bulunamadı", + "events": "Olaylar", + "eventsBetaDisclaimer": "Bu özellik geliştirme aşamasındadır. Yalnızca güncel veriler dahil edilmiştir ve üretime hazır bir sürüm yayınlanana kadar silinebilir. Kullanılan API uç noktaları gelecek sürümlerde değişebilir.", + "eventsFound": "{{count}} olay", + "eventsLoadFailed": "Hata: Olaylar yüklenemedi. Lütfen tekrar deneyin.", + "eventType": "Olay türü", + "exchange": "Borsa", + "exchanges": "Borsalar", + "exchangesLoadFailed": "Hata: Borsalar yüklenemedi. Lütfen sayfayı yenileyin veya daha sonra tekrar deneyin.", + "exclude": "Hariç tut", "executed": "Gerçekleştirildi", - "txStatusExecuted": "İşlem gerçekleştirildi", - "txStatusFailed": "Transfer başarısız", + "expandAll": "Tümünü genişlet", + "exportJson": "JSON dışa aktar", + "fee": "Ücret", + "filterAll": "Tümü", + "filterAllTransactions": "Tüm işlemler", + "filterApproved": "Onaylı", + "filterApprovedTransactions": "Onaylanmış işlemler", + "filterButton": "Filtrele", + "filterByCategory": "Kategoriye göre filtrele", + "filters": "Filtreler", + "filterTransfer": "Transferler", + "filterTransferTransactions": "Transfer işlemleri", + "hide": "Gizle", "id": "Kimlik", - "incomingTransfers": "Gelen Transferler", + "include": "Dahil et", "incomingAmount": "Gelen Miktar", + "incomingTransfers": "Gelen Transferler", "incomplete": "Eksik", + "inputType": "Giriş Türü", + "invalidAddressError": "Adres biçimi geçersiz. Lütfen adresi kontrol edin ve tekrar deneyin.", + "invalidAddressFormat": "Adres 60 büyük harf içermelidir", + "invalidDateRange": "Başlangıç tarihi bitiş tarihinden önce olmalıdır", + "invalidDestinationIdentity": "Geçersiz hedef adresi: {{address}}", + "invalidRangeAmount": "Geçersiz aralık - başlangıç değeri bitiş değerine eşit veya küçük olmalıdır", + "invalidRangeInputType": "Minimum değer maksimum değere eşit veya küçük olmalıdır", + "invalidSourceIdentity": "Geçersiz kaynak adresi: {{address}}", + "invalidTickRange": "Başlangıç tick'i bitiş tick'inden büyük olamaz", + "invalidTransactionId": "İşlem ID'si geçersiz. Lütfen ID'yi kontrol edin ve tekrar deneyin.", + "issuer": "Yayımlayan", "lastNTickQuality": "Son 10K Tick Kalitesi", "lastNTickQualityTooltip": "Son 10K tick'teki boş olmayan tick yüzdesi, epoch'tan bağımsız", "latest": "En Son", + "latestStatsLoadFailed": "Hata: En son istatistikler yüklenemedi. Lütfen sayfayı yenileyin veya daha sonra tekrar deneyin.", "latestTransfers": "En Son Transferler", + "loading": "Yükleniyor", + "loadingTransactionsError": "Hata: İşlemleri yüklerken bir sorun oluştu. Lütfen daha sonra tekrar deneyin.", + "loadMore": "Daha fazla yükle", + "logDigest": "Log Özeti", + "managedBy": "{{contract}} tarafından yönetiliyor", + "managingContractIndex": "Yönetim Sözleşme İndeksi", "marketCap": "Piyasa Değeri", + "matchingEntities": "Eşleşen Sonuçlar", + "maxAddressesReached": "Maksimum 5 adrese ulaşıldı", + "maxAmount": "Maksimum tutar", + "maxAmountTooLarge": "Maksimum miktar değeri çok büyük.", + "maxInputType": "Maks. Değer", + "maxInputTypeTooLarge": "Maksimum input türü değeri çok büyük.", + "maxResultsHint": "Sonuçlar {{count}} ile sınırlıdır. Listeyi daraltmak için filtreler kullanın.", + "millionShort": "M", + "minAmount": "Minimum tutar", + "minAmountTooLarge": "Minimum miktar değeri çok büyük.", + "minInputType": "Min. Değer", + "minInputTypeTooLarge": "Minimum input türü değeri çok büyük.", + "name": "Ad", + "noEntries": "Gösterilecek giriş yok", + "noEvents": "Olay yok", "nonEmpty": "Boş Değil", + "noTransactions": "İşlem yok", + "numberOfDecimalPlaces": "Ondalık Basamaklar", "numberOfTransactions": "İşlem Sayısı", - "outgoingTransfers": "Giden Transferler", "outgoingAmount": "Giden Miktar", + "outgoingTransfers": "Giden Transferler", "price": "Fiyat", + "qrcodeModalTitle": "Adres QR Kodu", + "rank": "Rütbe", + "remainingAmount": "Kalan Tutar", + "removeAddress": "Adresi kaldır", + "resetFilters": "Filtreleri sıfırla", + "richList": "Zenginler listesi", + "richListLoadFailed": "Hata: Zenginler listesi yüklenemedi. Lütfen sayfayı yenileyin veya daha sonra tekrar deneyin.", + "richListWarning": "Zenginler listesi verileri her dönemin başında güncellenir", + "scShares": "Akıllı Sözleşme Payları", "search": "Ara", + "selectAsset": "Varlık seç", + "selectContract": "Select contract", + "selectUpTo": "En fazla {{count}} seçin", + "shareholders": "Hissedarlar", + "sharesIssuer": "Hisse Senedi İhraççısı", + "show": "Göster", + "showingMaxTransactions": "Son {{count}} işlem gösteriliyor", + "showingMaxEvents": "Son {{count}} olay gösteriliyor", + "showLess": "Daha az göster", + "showMore": "Daha fazla göster", + "showPerPagePrefix": "Sayfa başına", + "showPerPageSuffix": "göster", + "showQRCode": "QR Kodunu Göster", "signature": "İmza", + "smartContract": "Akıllı Sözleşme", + "smartContractCodeLoadError": "Hata: Akıllı sözleşme kodu yüklenemedi. Lütfen sayfayı yenileyin veya daha sonra tekrar deneyin.", + "smartContracts": "Akıllı Sözleşmeler", + "smartContractsLoadFailed": "Hata: Akıllı sözleşmeler yüklenemedi. Lütfen sayfayı yenileyin veya daha sonra tekrar deneyin.", "source": "Kaynak", "standard": "Standart", - "txStatusSuccess": "Başarılı transfer", + "startDate": "Başlangıç tarihi", + "startTick": "Başlangıç tick", + "startTickTooLarge": "Başlangıç tick değeri çok büyük.", + "status": "Durum", + "targetTick": "Hedef Tick", + "thousandShort": "B", "tick": "Tik", + "tickCheckFailed": "Tick durumu kontrol edilemedi. Lütfen daha sonra tekrar deneyin.", "tickLeader": "Tik lideri", "tickQuality": "Epoch Tick Kalitesi", "tickQualityTooltip": "Bu epoch içindeki boş olmayan tick yüzdesi", "ticks": "Tikler", + "ticksLoadFailed": "Hata: Tikler yüklenemedi. Lütfen sayfayı yenileyin veya daha sonra tekrar deneyin. Eğer bugün çarşamba ise, lütfen bir sonraki dönemin başlamasını bekleyin.", "tickStatus": "Tik durumu", "ticksThisEpoch": "Bu çağdaki tikler (Boş)", "ticksThisEpochTooltip": "Bu epoch içinde işlenen toplam tick sayısı ve boş tick sayısı parantez içinde", + "timestamp": "Zaman damgası", + "token": "Jeton", + "tokenIssuer": "{{name}} Yayımlayan", + "tokenLoadFailed": "Hata: Jetonlar yüklenemedi. Lütfen sayfayı yenileyin veya daha sonra tekrar deneyin.", + "tokens": "Jetonlar", + "totalValueLocked": "Toplam Kilitli Değer (TVL)", + "transactionNotFound": "İşlem bulunamadı", "transactionPreview": "İşlem önizlemesi", "transactions": "İşlemler", - "data": "Veri", - "defaultView": "Varsayılan Görünüm", "unableToDecodeContractInput": "Bu sözleşme girişi çözümlenemedi.", - "exportJson": "JSON dışa aktar", - "showMore": "Daha fazla göster", - "showLess": "Daha az göster", - "type": "Tür", "unexecuted": "Gerçekleştirilmemiş", "value": "Değer", - "loadMore": "Daha fazla yükle", - "noTransactions": "İşlem yok", - "allTransactionsLoaded": "Tüm işlemleri gördünüz", - "loading": "Yükleniyor", - "hide": "Gizle", - "show": "Göster", - "transactionNotFound": "İşlem bulunamadı", - "invalidTransactionId": "İşlem ID'si geçersiz. Lütfen ID'yi kontrol edin ve tekrar deneyin.", - "invalidAddressError": "Adres biçimi geçersiz. Lütfen adresi kontrol edin ve tekrar deneyin.", - "addressNotFoundError": "Hata: Aradığınız adresi bulamadık. Lütfen adresi kontrol edin ve tekrar deneyin.", - "loadingTransactionsError": "Hata: İşlemleri yüklerken bir sorun oluştu. Lütfen daha sonra tekrar deneyin.", - "invalidSourceIdentity": "Geçersiz kaynak adresi: {{address}}", - "invalidDestinationIdentity": "Geçersiz hedef adresi: {{address}}", - "minInputTypeTooLarge": "Minimum input türü değeri çok büyük.", - "maxInputTypeTooLarge": "Maksimum input türü değeri çok büyük.", - "minAmountTooLarge": "Minimum miktar değeri çok büyük.", - "maxAmountTooLarge": "Maksimum miktar değeri çok büyük.", - "startTickTooLarge": "Başlangıç tick değeri çok büyük.", - "endTickTooLarge": "Bitiş tick değeri çok büyük.", - "richList": "Zenginler listesi", - "rank": "Rütbe", - "addressID": "Adres Kimliği", - "richListLoadFailed": "Hata: Zenginler listesi yüklenemedi. Lütfen sayfayı yenileyin veya daha sonra tekrar deneyin.", - "assetsRichListInvalidAssetOrIssuer": "Hata: Geçersiz ihraççı adresi ve/veya varlık adı. Lütfen URL parametrelerini kontrol edin.", - "richListWarning": "Zenginler listesi verileri her dönemin başında güncellenir", - "timestamp": "Zaman damgası", - "fee": "Ücret", - "assetTransferWarning": "Bu işlem yalnızca kaynak adresin işlem listesinde görünmektedir. Hedef adresin işlem listesinde gösterilmesi mevcut sürümde henüz desteklenmemektedir.", - "ticksLoadFailed": "Hata: Tikler yüklenemedi. Lütfen sayfayı yenileyin veya daha sonra tekrar deneyin. Eğer bugün çarşamba ise, lütfen bir sonraki dönemin başlamasını bekleyin.", - "showItemsPerPage": "{{count}} öğeyi göster", - "itemsPerPage": "Sayfa başına öğe", - "latestStatsLoadFailed": "Hata: En son istatistikler yüklenemedi. Lütfen sayfayı yenileyin veya daha sonra tekrar deneyin.", - "errorLoadingPrice": "Hata: Fiyat yüklenemedi. Lütfen sayfayı yenileyin veya daha sonra tekrar deneyin.", - "exchange": "Borsa", - "exchanges": "Borsalar", - "exchangesLoadFailed": "Hata: Borsalar yüklenemedi. Lütfen sayfayı yenileyin veya daha sonra tekrar deneyin.", - "token": "Jeton", - "tokens": "Jetonlar", - "smartContract": "Akıllı Sözleşme", - "smartContracts": "Akıllı Sözleşmeler", - "tokenLoadFailed": "Hata: Jetonlar yüklenemedi. Lütfen sayfayı yenileyin veya daha sonra tekrar deneyin.", - "smartContractsLoadFailed": "Hata: Akıllı sözleşmeler yüklenemedi. Lütfen sayfayı yenileyin veya daha sonra tekrar deneyin.", - "issuer": "Yayımlayan", - "name": "Ad", "wallets": "Cüzdanlar", - "assets": "Varlıklar", - "totalValueLocked": "Toplam Kilitli Değer (TVL)", - "noEntries": "Gösterilecek giriş yok", - "assetsRichList": "Varlık Zenginler Listesi", - "assetsRichListDesc": "Bu liste, her varlığın en büyük sahiplerini gösterir.", - "contractName": "Sözleşme Adı", - "shareholders": "Hissedarlar", - "checkContractShareholders": "Sözleşme Hissedarlarını Görüntüle", - "checkSourceCode": "Kaynak Kodunu Görüntüle", - "checkContractProposal": "Sözleşme Önerisini Görüntüle", - "smartContractCodeLoadError": "Hata: Akıllı sözleşme kodu yüklenemedi. Lütfen sayfayı yenileyin veya daha sonra tekrar deneyin.", - "contract": "Sözleşme", - "copied": "Kopyalandı", - "copyAddress": "Adresi kopyala", - "copyTransactionId": "TX ID'sini kopyala", - "copyToClipboard": "Panoya kopyala", - "showQRCode": "QR Kodunu Göster", - "qrcodeModalTitle": "Adres QR Kodu", - "filterAllTransactions": "Tüm işlemler", - "filterTransferTransactions": "Transfer işlemleri", - "filterApprovedTransactions": "Onaylanmış işlemler", - "filterAll": "Tümü", - "filterTransfer": "Transferler", - "filterApproved": "Onaylı", - "filterByCategory": "Kategoriye göre filtrele", - "selectAsset": "Varlık seç", - "scShares": "Akıllı Sözleşme Payları", - "sharesIssuer": "Hisse Senedi İhraççısı", - "contractIndex": "İndeks", - "expandAll": "Tümünü genişlet", - "collapseAll": "Tümünü daralt", - "managedBy": "{{contract}} tarafından yönetiliyor", "unknownContract": "Bilinmeyen sözleşme", - "resetFilters": "Filtreleri sıfırla", - "startTick": "Başlangıç tick", - "endTick": "Bitiş tick", - "startDate": "Başlangıç tarihi", - "endDate": "Bitiş tarihi", - "invalidRangeAmount": "Geçersiz aralık - başlangıç değeri bitiş değerine eşit veya küçük olmalıdır", - "invalidRangeInputType": "Minimum değer maksimum değere eşit veya küçük olmalıdır", - "invalidTickRange": "Başlangıç tick'i bitiş tick'inden büyük olamaz", - "invalidDateRange": "Başlangıç tarihi bitiş tarihinden önce olmalıdır", - "invalidAddressFormat": "Adres 60 büyük harf içermelidir", - "destinationFilterHint": "Akıllı sözleşme işlemleri için bu sözleşme adresidir, son token alıcısı değil.", - "amountFilterHint": "QUBIC cinsinden işlem tutarı.", - "customRange": "Özel aralık", - "minAmount": "Minimum tutar", - "maxAmount": "Maksimum tutar", - "amountOver0": "> 0", - "amount1to1M": "1 - 1M", - "amount1Mto100M": "1M - 100M", - "amount100Mto1B": "100M - 1Mr", - "amount1Bto10B": "1Mr - 10Mr", - "amountOver10B": "> 10Mr", - "thousandShort": "B", - "millionShort": "M", - "billionShort": "Mr", - "dateLastHour": "Son saat", - "dateLast24Hours": "Son 24 saat", - "dateLastNDays": "Son {{count}} gün", - "addressPlaceholder": "60 karakter adresi", - "clearAllFiltersTooltip": "Tüm filtreleri kaldırmak için tıklayın", - "filterButton": "Filtrele", - "filters": "Filtreler", - "applyFilters": "Filtreleri uygula", - "directionAll": "Tümü", - "directionIncoming": "Gelen", - "directionOutgoing": "Giden", - "directionIncomingTooltip": "Gelen işlemler", - "directionOutgoingTooltip": "Giden işlemler", - "direction": "Yön", - "inputType": "Giriş Türü", - "minInputType": "Min. Değer", - "maxInputType": "Maks. Değer", - "include": "Dahil et", - "exclude": "Hariç tut", - "addAddress": "Adres ekle", - "removeAddress": "Adresi kaldır", - "maxAddressesReached": "Maksimum 5 adrese ulaşıldı", - "duplicateAddress": "Yinelenen adres", - "matchingEntities": "Eşleşen Sonuçlar", "transactionsFound": "{{count}} işlem", - "showingMaxTransactions": "Son {{count}} işlem gösteriliyor", - "maxResultsHint": "Sonuçlar {{count}} ile sınırlıdır. Listeyi daraltmak için filtreler kullanın.", - "waitingForTickTitle": "Tick'in işlenmesi bekleniyor", + "txID": "TX ID", + "txStatusExecuted": "İşlem gerçekleştirildi", + "txStatusFailed": "Transfer başarısız", + "txStatusSuccess": "Başarılı transfer", + "txType": "TX Type", + "unitOfMeasurement": "Ölçü Birimi", + "virtualTransactionBanner": "Bu, Qubic protokolü tarafından akıllı sözleşme yaşam döngüsü olayları için oluşturulmuş sanal bir işlemdir.", + "virtualTransactionNotFound": "Sanal işlem bulunamadı", "waitingForTickDesc": "Bu işlem {{targetTick}} tick'i için planlanmıştır. Ağ o tick'e ulaştığında kullanılabilir olacaktır.", - "targetTick": "Hedef Tick", - "estimatedWait": "Tahmini bekleme süresi", - "tickCheckFailed": "Tick durumu kontrol edilemedi. Lütfen daha sonra tekrar deneyin." + "waitingForTickTitle": "Tick'in işlenmesi bekleniyor" } diff --git a/public/locales/vi/global.json b/public/locales/vi/global.json index c7bd9bf3..d37893cd 100644 --- a/public/locales/vi/global.json +++ b/public/locales/vi/global.json @@ -1,9 +1,12 @@ { "address": "Địa chỉ", "balance": "Số dư", + "navigationMenu": "Menu điều hướng", "noResultsFound": "Không Tìm Thấy Kết Quả", "noResultsFoundFor": "Không tìm thấy kết quả cho \"{{keyword}}\"", "searchPlaceholder": "Tìm TX, tick, ID, token, hợp đồng, sàn giao dịch...", + "selectLanguage": "Chọn ngôn ngữ", + "selectNetwork": "Chọn mạng", "tick": "Tick", "transaction": "Giao dịch", "transactions": "Các giao dịch" diff --git a/public/locales/vi/network-page.json b/public/locales/vi/network-page.json index cb124717..d60b3689 100644 --- a/public/locales/vi/network-page.json +++ b/public/locales/vi/network-page.json @@ -1,192 +1,223 @@ { "activeAddresses": "Địa chỉ hoạt động", + "addAddress": "Thêm địa chỉ", "address": "Địa chỉ", + "addressID": "ID địa chỉ", + "addressNotFoundError": "Lỗi: Không tìm thấy địa chỉ bạn đang tìm. Vui lòng kiểm tra và thử lại.", + "addressPlaceholder": "60 ký tự địa chỉ", + "allProcedures": "All procedures", + "allTransactionsLoaded": "Bạn đã xem tất cả giao dịch", "amount": "Số lượng", - "circulatingSupply": "Nguồn cung lưu hành", + "amountAssetAny": "Tất cả", + "amountAssetOther": "Tài sản khác", + "amountAssetQubic": "QUBIC", + "amountAssetType": "Tài sản", + "amount100Mto1B": "100Tr - 1T", + "amount1Bto10B": "1T - 10T", + "amount1Mto100M": "1Tr - 100Tr", + "amount1to1M": "1 - 1Tr", + "amountFilterHint": "Số tiền giao dịch tính bằng QUBIC.", + "amountOver0": "> 0", + "amountOver10B": "> 10T", + "applyFilters": "Áp dụng bộ lọc", + "assetIssuer": "Người Phát Hành Tài Sản", + "assets": "Tài sản", + "assetsRichList": "Danh sách giàu tài sản", + "assetsRichListDesc": "Danh sách giàu tài sản hiển thị những người nắm giữ nhiều nhất của từng tài sản.", + "assetsRichListInvalidAssetOrIssuer": "Lỗi: Địa chỉ nhà phát hành và/hoặc tên tài sản không hợp lệ. Vui lòng kiểm tra các tham số URL.", + "assetTransferWarning": "Giao dịch này chỉ hiển thị trong danh sách giao dịch của địa chỉ nguồn. Việc hiển thị nó trong danh sách giao dịch của địa chỉ đích chưa được hỗ trợ trong phiên bản hiện tại.", + "billionShort": "T", + "blockchain": "Blockchain", "burnedSupply": "Nguồn cung đã đốt", + "checkContractProposal": "Xem đề xuất hợp đồng", + "checkContractShareholders": "Xem cổ đông hợp đồng", + "checkSourceCode": "Xem mã nguồn", + "circulatingSupply": "Nguồn cung lưu hành", + "clearAllFiltersTooltip": "Nhấp để xóa tất cả bộ lọc", + "collapseAll": "Thu gọn tất cả", "complete": "Hoàn tất", + "contract": "Hợp đồng", + "contractIndex": "Chỉ Số", + "contractMessageType": "Loại tin nhắn hợp đồng", + "contractName": "Tên hợp đồng", + "copied": "Đã sao chép", + "copyAddress": "Sao chép địa chỉ", + "copyToClipboard": "Sao chép vào clipboard", + "copyTransactionId": "Sao chép ID TX", "currentTick": "Tick hiện tại", + "customRange": "Phạm vi tùy chỉnh", + "data": "Dữ liệu", "dataStatus": "Trạng thái dữ liệu", "date": "Ngày", + "dateLast24Hours": "24 giờ trước", + "dateLastHour": "Giờ trước", + "dateLastNDays": "{{count}} ngày trước", + "deductedAmount": "Số tiền khấu trừ", + "defaultView": "Chế độ xem mặc định", "destination": "Đích", + "destinationFilterHint": "Đối với giao dịch hợp đồng thông minh, đây là địa chỉ hợp đồng, không phải người nhận token cuối cùng.", + "direction": "Hướng", + "directionAll": "Tất cả", + "directionIncoming": "Đến", + "directionIncomingTooltip": "Giao dịch đến", + "directionOutgoing": "Đi", + "directionOutgoingTooltip": "Giao dịch đi", + "duplicateAddress": "Địa chỉ trùng lặp", "empty": "Trống", + "endDate": "Đến ngày", + "endTick": "Tick kết thúc", + "endTickTooLarge": "Giá trị tick kết thúc quá lớn.", "entityReportsFromRandomPeers": "Báo cáo từ các nút ngẫu nhiên", "epoch": "Epoch", "epochTicks": "Epoch Ticks", + "errorLoadingPrice": "Lỗi: Không tải được giá. Vui lòng tải lại trang hoặc thử lại sau.", "errorMessage": "Beta - Dữ liệu có thể chưa đầy đủ hoặc có lỗi", + "estimatedWait": "Thời gian chờ ước tính", + "event": "Sự kiện", + "eventDetails": "Chi tiết sự kiện", + "eventNotFound": "Không tìm thấy sự kiện", + "events": "Sự kiện", + "eventsBetaDisclaimer": "Tính năng này đang được phát triển. Chỉ có dữ liệu gần đây được bao gồm và có thể bị xóa cho đến khi phiên bản chính thức sẵn sàng. Các endpoint API được sử dụng có thể thay đổi trong các bản phát hành sắp tới.", + "eventsFound": "{{count}} sư kiện", + "eventsLoadFailed": "Lỗi: Không tải được sự kiện. Vui lòng thử lại.", + "eventType": "Loại sự kiện", + "exchange": "Sàn giao dịch", + "exchanges": "Các sàn giao dịch", + "exchangesLoadFailed": "Lỗi: Không tải được danh sách sàn giao dịch. Vui lòng tải lại trang hoặc thử lại sau.", + "exclude": "Loại trừ", "executed": "Đã thực hiện", - "txStatusExecuted": "Giao dịch đã thực thi", - "txStatusFailed": "Chuyển tiền thất bại", + "expandAll": "Mở rộng tất cả", + "exportJson": "Xuất JSON", + "fee": "Phí", + "filterAll": "Tất cả", + "filterAllTransactions": "Tất cả giao dịch", + "filterApproved": "Đã duyệt", + "filterApprovedTransactions": "Giao dịch đã duyệt", + "filterButton": "Lọc", + "filterByCategory": "Lọc theo danh mục", + "filters": "Bộ lọc", + "filterTransfer": "Các chuyển khoản", + "filterTransferTransactions": "Giao dịch chuyển khoản", + "hide": "Ẩn", "id": "ID", - "incomingTransfers": "Chuyển đến", + "include": "Bao gồm", "incomingAmount": "Số lượng nhận", + "incomingTransfers": "Chuyển đến", "incomplete": "Chưa hoàn tất", + "inputType": "Loại đầu vào", + "invalidAddressError": "Định dạng địa chỉ không hợp lệ. Vui lòng kiểm tra và thử lại.", + "invalidAddressFormat": "Địa chỉ phải gồm 60 chữ cái viết hoa", + "invalidDateRange": "Ngày bắt đầu phải trước ngày kết thúc", + "invalidDestinationIdentity": "Địa chỉ đích không hợp lệ: {{address}}", + "invalidRangeAmount": "Phạm vi không hợp lệ - giá trị bắt đầu phải nhỏ hơn hoặc bằng giá trị kết thúc", + "invalidRangeInputType": "Giá trị tối thiểu phải nhỏ hơn hoặc bằng giá trị tối đa", + "invalidSourceIdentity": "Địa chỉ nguồn không hợp lệ: {{address}}", + "invalidTickRange": "Tick bắt đầu không thể lớn hơn tick kết thúc", + "invalidTransactionId": "ID giao dịch không hợp lệ. Vui lòng kiểm tra ID và thử lại.", + "issuer": "Người phát hành", "lastNTickQuality": "Chất lượng 10K Tick gần nhất", "lastNTickQualityTooltip": "Tỷ lệ phần trăm tick không trống trong 10K tick gần nhất, bất kể epoch", "latest": "Mới nhất", + "latestStatsLoadFailed": "Lỗi: Không tải được thống kê mới nhất. Vui lòng tải lại trang hoặc thử lại sau.", "latestTransfers": "Chuyển mới nhất", + "loading": "Đang tải", + "loadingTransactionsError": "Lỗi: Đã xảy ra sự cố khi tải giao dịch. Vui lòng thử lại sau.", + "loadMore": "Tải thêm", + "logDigest": "Tóm tắt nhật ký", + "managedBy": "Được quản lý bởi {{contract}}", + "managingContractIndex": "Chỉ mục hợp đồng quản lý", "marketCap": "Vốn hóa thị trường", + "matchingEntities": "Kết quả phù hợp", + "maxAddressesReached": "Đã đạt tối đa 5 địa chỉ", + "maxAmount": "Số tiền tối đa", + "maxAmountTooLarge": "Giá trị tối đa số tiền quá lớn.", + "maxInputType": "Giá trị tối đa", + "maxInputTypeTooLarge": "Giá trị tối đa loại đầu vào quá lớn.", + "maxResultsHint": "Kết quả bị giới hạn ở {{count}}. Sử dụng bộ lọc để thu hẹp danh sách.", + "millionShort": "Tr", + "minAmount": "Số tiền tối thiểu", + "minAmountTooLarge": "Giá trị tối thiểu số tiền quá lớn.", + "minInputType": "Giá trị tối thiểu", + "minInputTypeTooLarge": "Giá trị tối thiểu loại đầu vào quá lớn.", + "name": "Tên", + "noEntries": "Không có mục nào để hiển thị", + "noEvents": "Không có sư kiện", "nonEmpty": "Không trống", + "noTransactions": "Không có giao dịch", + "numberOfDecimalPlaces": "Số thập phân", "numberOfTransactions": "Số lượng giao dịch", - "outgoingTransfers": "Chuyển đi", "outgoingAmount": "Số lượng gửi", + "outgoingTransfers": "Chuyển đi", "price": "Giá", + "qrcodeModalTitle": "Mã QR địa chỉ", + "rank": "Hạng", + "remainingAmount": "Số tiền còn lại", + "removeAddress": "Xóa địa chỉ", + "resetFilters": "Đặt lại bộ lọc", + "richList": "Danh sách giàu", + "richListLoadFailed": "Lỗi: Không tải được danh sách giàu. Vui lòng tải lại trang hoặc thử lại sau.", + "richListWarning": "Danh sách giàu được cập nhật vào đầu mỗi epoch", + "scShares": "Cổ phần hợp đồng thông minh", "search": "Tìm kiếm", + "selectAsset": "Chọn tài sản", + "selectContract": "Select contract", + "selectUpTo": "Chọn tối đa {{count}}", + "shareholders": "Cổ đông", + "sharesIssuer": "Nhà Phát Hành Cổ Phiếu", + "show": "Hiện", + "showingMaxTransactions": "Hiển thị {{count}} giao dịch gần nhất", + "showingMaxEvents": "Hiển thị {{count}} sự kiện gần nhất", + "showLess": "Thu gọn", + "showMore": "Xem thêm", + "showPerPagePrefix": "Hiển thị", + "showPerPageSuffix": "mỗi trang", + "showQRCode": "Hiển thị mã QR", "signature": "Chữ ký", + "smartContract": "Hợp đồng thông minh", + "smartContractCodeLoadError": "Lỗi: Không tải được mã hợp đồng thông minh. Vui lòng tải lại trang hoặc thử lại sau.", + "smartContracts": "Các hợp đồng thông minh", + "smartContractsLoadFailed": "Lỗi: Không tải được hợp đồng thông minh. Vui lòng tải lại trang hoặc thử lại sau.", "source": "Nguồn", "standard": "Tiêu chuẩn", - "txStatusSuccess": "Chuyển tiền thành công", + "startDate": "Từ ngày", + "startTick": "Tick bắt đầu", + "startTickTooLarge": "Giá trị tick bắt đầu quá lớn.", + "status": "Status", + "targetTick": "Tick mục tiêu", + "thousandShort": "K", "tick": "Tick", + "tickCheckFailed": "Không thể kiểm tra trạng thái tick. Vui lòng thử lại sau.", "tickLeader": "Người dẫn tick", "tickQuality": "Chất lượng tick trong epoch", "tickQualityTooltip": "Tỷ lệ phần trăm tick không trống trong epoch này", "ticks": "Các tick", + "ticksLoadFailed": "Lỗi: Không tải được các tick. Vui lòng tải lại trang hoặc thử lại sau. Nếu hôm nay là thứ Tư, vui lòng chờ đến khi epoch mới bắt đầu.", "tickStatus": "Trạng thái tick", "ticksThisEpoch": "Các tick trong epoch này (Trống)", "ticksThisEpochTooltip": "Tổng số tick đã xử lý trong epoch này và số tick trống trong ngoặc", + "timestamp": "Dấu thời gian", + "token": "Token", + "tokenIssuer": "{{name}} Người phát hành", + "tokenLoadFailed": "Lỗi: Không tải được token. Vui lòng tải lại trang hoặc thử lại sau.", + "tokens": "Các token", + "totalValueLocked": "Tổng giá trị khóa (TVL)", + "transactionNotFound": "Không tìm thấy giao dịch", "transactionPreview": "Xem trước giao dịch", "transactions": "Các giao dịch", - "data": "Dữ liệu", - "defaultView": "Chế độ xem mặc định", "unableToDecodeContractInput": "Không thể giải mã dữ liệu đầu vào của hợp đồng này.", - "exportJson": "Xuất JSON", - "showMore": "Xem thêm", - "showLess": "Thu gọn", - "type": "Loại", "unexecuted": "Chưa thực hiện", "value": "Giá trị", - "loadMore": "Tải thêm", - "noTransactions": "Không có giao dịch", - "allTransactionsLoaded": "Bạn đã xem tất cả giao dịch", - "loading": "Đang tải", - "hide": "Ẩn", - "show": "Hiện", - "transactionNotFound": "Không tìm thấy giao dịch", - "invalidTransactionId": "ID giao dịch không hợp lệ. Vui lòng kiểm tra ID và thử lại.", - "invalidAddressError": "Định dạng địa chỉ không hợp lệ. Vui lòng kiểm tra và thử lại.", - "addressNotFoundError": "Lỗi: Không tìm thấy địa chỉ bạn đang tìm. Vui lòng kiểm tra và thử lại.", - "loadingTransactionsError": "Lỗi: Đã xảy ra sự cố khi tải giao dịch. Vui lòng thử lại sau.", - "invalidSourceIdentity": "Địa chỉ nguồn không hợp lệ: {{address}}", - "invalidDestinationIdentity": "Địa chỉ đích không hợp lệ: {{address}}", - "minInputTypeTooLarge": "Giá trị tối thiểu loại đầu vào quá lớn.", - "maxInputTypeTooLarge": "Giá trị tối đa loại đầu vào quá lớn.", - "minAmountTooLarge": "Giá trị tối thiểu số tiền quá lớn.", - "maxAmountTooLarge": "Giá trị tối đa số tiền quá lớn.", - "startTickTooLarge": "Giá trị tick bắt đầu quá lớn.", - "endTickTooLarge": "Giá trị tick kết thúc quá lớn.", - "richList": "Danh sách giàu", - "rank": "Hạng", - "addressID": "ID địa chỉ", - "richListLoadFailed": "Lỗi: Không tải được danh sách giàu. Vui lòng tải lại trang hoặc thử lại sau.", - "assetsRichListInvalidAssetOrIssuer": "Lỗi: Địa chỉ nhà phát hành và/hoặc tên tài sản không hợp lệ. Vui lòng kiểm tra các tham số URL.", - "richListWarning": "Danh sách giàu được cập nhật vào đầu mỗi epoch", - "timestamp": "Dấu thời gian", - "fee": "Phí", - "assetTransferWarning": "Giao dịch này chỉ hiển thị trong danh sách giao dịch của địa chỉ nguồn. Việc hiển thị nó trong danh sách giao dịch của địa chỉ đích chưa được hỗ trợ trong phiên bản hiện tại.", - "ticksLoadFailed": "Lỗi: Không tải được các tick. Vui lòng tải lại trang hoặc thử lại sau. Nếu hôm nay là thứ Tư, vui lòng chờ đến khi epoch mới bắt đầu.", - "showItemsPerPage": "Hiển thị {{count}} mục", - "itemsPerPage": "Số mục mỗi trang", - "latestStatsLoadFailed": "Lỗi: Không tải được thống kê mới nhất. Vui lòng tải lại trang hoặc thử lại sau.", - "errorLoadingPrice": "Lỗi: Không tải được giá. Vui lòng tải lại trang hoặc thử lại sau.", - "exchange": "Sàn giao dịch", - "exchanges": "Các sàn giao dịch", - "exchangesLoadFailed": "Lỗi: Không tải được danh sách sàn giao dịch. Vui lòng tải lại trang hoặc thử lại sau.", - "token": "Token", - "tokens": "Các token", - "smartContract": "Hợp đồng thông minh", - "smartContracts": "Các hợp đồng thông minh", - "tokenLoadFailed": "Lỗi: Không tải được token. Vui lòng tải lại trang hoặc thử lại sau.", - "smartContractsLoadFailed": "Lỗi: Không tải được hợp đồng thông minh. Vui lòng tải lại trang hoặc thử lại sau.", - "issuer": "Người phát hành", - "name": "Tên", "wallets": "Ví", - "assets": "Tài sản", - "totalValueLocked": "Tổng giá trị khóa (TVL)", - "noEntries": "Không có mục nào để hiển thị", - "assetsRichList": "Danh sách giàu tài sản", - "assetsRichListDesc": "Danh sách giàu tài sản hiển thị những người nắm giữ nhiều nhất của từng tài sản.", - "contractName": "Tên hợp đồng", - "shareholders": "Cổ đông", - "checkContractShareholders": "Xem cổ đông hợp đồng", - "checkSourceCode": "Xem mã nguồn", - "checkContractProposal": "Xem đề xuất hợp đồng", - "smartContractCodeLoadError": "Lỗi: Không tải được mã hợp đồng thông minh. Vui lòng tải lại trang hoặc thử lại sau.", - "contract": "Hợp đồng", - "copied": "Đã sao chép", - "copyAddress": "Sao chép địa chỉ", - "copyTransactionId": "Sao chép ID TX", - "copyToClipboard": "Sao chép vào clipboard", - "showQRCode": "Hiển thị mã QR", - "qrcodeModalTitle": "Mã QR địa chỉ", - "filterAllTransactions": "Tất cả giao dịch", - "filterTransferTransactions": "Giao dịch chuyển khoản", - "filterApprovedTransactions": "Giao dịch đã duyệt", - "filterAll": "Tất cả", - "filterTransfer": "Các chuyển khoản", - "filterApproved": "Đã duyệt", - "filterByCategory": "Lọc theo danh mục", - "selectAsset": "Chọn tài sản", - "scShares": "Cổ phần hợp đồng thông minh", - "sharesIssuer": "Nhà Phát Hành Cổ Phiếu", - "contractIndex": "Chỉ Số", - "expandAll": "Mở rộng tất cả", - "collapseAll": "Thu gọn tất cả", - "managedBy": "Được quản lý bởi {{contract}}", "unknownContract": "Hợp đồng không xác định", - "resetFilters": "Đặt lại bộ lọc", - "startTick": "Tick bắt đầu", - "endTick": "Tick kết thúc", - "startDate": "Từ ngày", - "endDate": "Đến ngày", - "invalidRangeAmount": "Phạm vi không hợp lệ - giá trị bắt đầu phải nhỏ hơn hoặc bằng giá trị kết thúc", - "invalidRangeInputType": "Giá trị tối thiểu phải nhỏ hơn hoặc bằng giá trị tối đa", - "invalidTickRange": "Tick bắt đầu không thể lớn hơn tick kết thúc", - "invalidDateRange": "Ngày bắt đầu phải trước ngày kết thúc", - "invalidAddressFormat": "Địa chỉ phải gồm 60 chữ cái viết hoa", - "destinationFilterHint": "Đối với giao dịch hợp đồng thông minh, đây là địa chỉ hợp đồng, không phải người nhận token cuối cùng.", - "amountFilterHint": "Số tiền giao dịch tính bằng QUBIC.", - "customRange": "Phạm vi tùy chỉnh", - "minAmount": "Số tiền tối thiểu", - "maxAmount": "Số tiền tối đa", - "amountOver0": "> 0", - "amount1to1M": "1 - 1Tr", - "amount1Mto100M": "1Tr - 100Tr", - "amount100Mto1B": "100Tr - 1T", - "amount1Bto10B": "1T - 10T", - "amountOver10B": "> 10T", - "thousandShort": "K", - "millionShort": "Tr", - "billionShort": "T", - "dateLastHour": "Giờ trước", - "dateLast24Hours": "24 giờ trước", - "dateLastNDays": "{{count}} ngày trước", - "addressPlaceholder": "60 ký tự địa chỉ", - "clearAllFiltersTooltip": "Nhấp để xóa tất cả bộ lọc", - "filterButton": "Lọc", - "filters": "Bộ lọc", - "applyFilters": "Áp dụng bộ lọc", - "directionAll": "Tất cả", - "directionIncoming": "Đến", - "directionOutgoing": "Đi", - "directionIncomingTooltip": "Giao dịch đến", - "directionOutgoingTooltip": "Giao dịch đi", - "direction": "Hướng", - "inputType": "Loại đầu vào", - "minInputType": "Giá trị tối thiểu", - "maxInputType": "Giá trị tối đa", - "include": "Bao gồm", - "exclude": "Loại trừ", - "addAddress": "Thêm địa chỉ", - "removeAddress": "Xóa địa chỉ", - "maxAddressesReached": "Đã đạt tối đa 5 địa chỉ", - "duplicateAddress": "Địa chỉ trùng lặp", - "matchingEntities": "Kết quả phù hợp", "transactionsFound": "{{count}} giao dịch", - "showingMaxTransactions": "Hiển thị {{count}} giao dịch gần nhất", - "maxResultsHint": "Kết quả bị giới hạn ở {{count}}. Sử dụng bộ lọc để thu hẹp danh sách.", - "waitingForTickTitle": "Đang chờ tick được xử lý", + "txID": "TX ID", + "txStatusExecuted": "Giao dịch đã thực thi", + "txStatusFailed": "Chuyển tiền thất bại", + "txStatusSuccess": "Chuyển tiền thành công", + "txType": "TX Type", + "unitOfMeasurement": "Đơn vị đo lường", + "virtualTransactionBanner": "Đây là một giao dịch ảo được tạo bởi giao thức Qubic cho các sự kiện vòng đời hợp đồng thông minh.", + "virtualTransactionNotFound": "Không tìm thấy giao dịch ảo", "waitingForTickDesc": "Giao dịch này được lên lịch cho tick {{targetTick}}. Nó sẽ khả dụng khi mạng đạt đến tick đó.", - "targetTick": "Tick mục tiêu", - "estimatedWait": "Thời gian chờ ước tính", - "tickCheckFailed": "Không thể kiểm tra trạng thái tick. Vui lòng thử lại sau." + "waitingForTickTitle": "Đang chờ tick được xử lý" } diff --git a/public/locales/zh/global.json b/public/locales/zh/global.json index 1d88e971..f54a4bc2 100644 --- a/public/locales/zh/global.json +++ b/public/locales/zh/global.json @@ -1,9 +1,12 @@ { "address": "地址", "balance": "余额", + "navigationMenu": "导航菜单", "noResultsFound": "未找到结果", "noResultsFoundFor": "未找到 \"{{keyword}}\" 的结果", "searchPlaceholder": "搜索 TX、刻度、ID、代币、合约、交易所...", + "selectLanguage": "选择语言", + "selectNetwork": "选择网络", "tick": "刻度", "transaction": "交易", "transactions": "交易" diff --git a/public/locales/zh/network-page.json b/public/locales/zh/network-page.json index 66707a38..ff688d60 100644 --- a/public/locales/zh/network-page.json +++ b/public/locales/zh/network-page.json @@ -1,192 +1,223 @@ { "activeAddresses": "活跃地址", + "addAddress": "添加地址", "address": "地址", + "addressID": "地址 ID", + "addressNotFoundError": "错误:我们找不到您要查找的地址。请检查地址并重试。", + "addressPlaceholder": "60字符地址", + "allProcedures": "All procedures", + "allTransactionsLoaded": "您已经看过所有的交易", "amount": "金额", - "circulatingSupply": "流通供应量", + "amountAssetAny": "全部", + "amountAssetOther": "其他资产", + "amountAssetQubic": "QUBIC", + "amountAssetType": "资产", + "amount100Mto1B": "1亿 - 10亿", + "amount1Bto10B": "10亿 - 100亿", + "amount1Mto100M": "1百万 - 1亿", + "amount1to1M": "1 - 1百万", + "amountFilterHint": "QUBIC 交易金额。", + "amountOver0": "> 0", + "amountOver10B": "> 100亿", + "applyFilters": "应用筛选", + "assetIssuer": "资产发行人", + "assets": "资产", + "assetsRichList": "资产富豪榜", + "assetsRichListDesc": "资产富豪榜显示每种资产的前持有者。", + "assetsRichListInvalidAssetOrIssuer": "错误:发行者地址和/或资产名称无效。请检查URL参数。", + "assetTransferWarning": "此交易仅在源地址的交易列表中可见。在当前版本中尚不支持在目标地址的交易列表中显示它。", + "billionShort": "十亿", + "blockchain": "Blockchain", "burnedSupply": "燃烧供应量", + "checkContractProposal": "查看合约提案", + "checkContractShareholders": "查看合约股东", + "checkSourceCode": "查看源代码", + "circulatingSupply": "流通供应量", + "clearAllFiltersTooltip": "点击移除所有筛选", + "collapseAll": "全部摺疊", "complete": "完成", + "contract": "合约", + "contractIndex": "索引", + "contractMessageType": "合约消息类型", + "contractName": "合约名称", + "copied": "已复制", + "copyAddress": "复制地址", + "copyToClipboard": "复制到剪贴板", + "copyTransactionId": "复制TX ID", "currentTick": "当前刻度", + "customRange": "自定义范围", + "data": "数据", "dataStatus": "数据状态", "date": "日期", + "dateLast24Hours": "最近24小时", + "dateLastHour": "最近一小时", + "dateLastNDays": "最近{{count}}天", + "deductedAmount": "扣除金额", + "defaultView": "默认视图", "destination": "接收方", + "destinationFilterHint": "对于智能合约交易,这是合约地址,而不是最终的代币接收者。", + "direction": "方向", + "directionAll": "全部", + "directionIncoming": "收入", + "directionIncomingTooltip": "收入交易", + "directionOutgoing": "支出", + "directionOutgoingTooltip": "支出交易", + "duplicateAddress": "重复地址", "empty": "空的", + "endDate": "结束日期", + "endTick": "结束Tick", + "endTickTooLarge": "结束Tick值过大。", "entityReportsFromRandomPeers": "来自随机对等节点的报告", "epoch": "纪元", "epochTicks": "纪元刻度", + "errorLoadingPrice": "错误:无法加载价格。请刷新页面或稍后再试。", "errorMessage": "测试版 - 数据可能不完整或有误", + "estimatedWait": "预计等待时间", + "event": "事件", + "eventDetails": "事件详情", + "eventNotFound": "未找到事件", + "events": "事件", + "eventsBetaDisclaimer": "此功能正在开发中。仅包含近期数据,在正式版本发布前可能会被清除。所使用的API端点可能会在后续版本中发生变化。", + "eventsFound": "{{count}} 事件", + "eventsLoadFailed": "错误:无法加载事件。请重试。", + "eventType": "事件类型", + "exchange": "交易所", + "exchanges": "交易所", + "exchangesLoadFailed": "错误:无法加载交易所。请刷新页面或稍后再试。", + "exclude": "排除", "executed": "已执行", - "txStatusExecuted": "交易已执行", - "txStatusFailed": "转账失败", + "expandAll": "全部展開", + "exportJson": "导出 JSON", + "fee": "费用", + "filterAll": "全部", + "filterAllTransactions": "所有交易", + "filterApproved": "已核准", + "filterApprovedTransactions": "已核准交易", + "filterButton": "筛选", + "filterByCategory": "按类别筛选", + "filters": "筛选条件", + "filterTransfer": "轉帳交易", + "filterTransferTransactions": "轉帳交易", + "hide": "隐藏", "id": "ID", - "incomingTransfers": "收入转账", + "include": "包含", "incomingAmount": "收入金额", + "incomingTransfers": "收入转账", "incomplete": "未完成", + "inputType": "输入类型", + "invalidAddressError": "地址格式无效。请检查地址并重试。", + "invalidAddressFormat": "地址必须由60个大写字母组成", + "invalidDateRange": "起始日期必须早于结束日期", + "invalidDestinationIdentity": "无效的目标地址: {{address}}", + "invalidRangeAmount": "无效范围 - 起始值必须小于或等于结束值", + "invalidRangeInputType": "最小值必须小于或等于最大值", + "invalidSourceIdentity": "无效的来源地址: {{address}}", + "invalidTickRange": "起始Tick不能大于结束Tick", + "invalidTransactionId": "交易ID无效。请检查ID并重试。", + "issuer": "发行人", "lastNTickQuality": "最后10K刻度质量", "lastNTickQualityTooltip": "最后10K刻度中非空刻度的百分比,與纪元無關", "latest": "最新", + "latestStatsLoadFailed": "错误:无法加载最新统计数据。请刷新页面或稍后再试。", "latestTransfers": "最新转账", + "loading": "加载中", + "loadingTransactionsError": "错误:加载交易时出现问题。请稍后再试。", + "loadMore": "加载更多", + "logDigest": "日志摘要", + "managedBy": "由{{contract}}管理", + "managingContractIndex": "管理合约索引", "marketCap": "市值", + "matchingEntities": "匹配结果", + "maxAddressesReached": "已达到最多5个地址", + "maxAmount": "最大金额", + "maxAmountTooLarge": "最大金额值过大。", + "maxInputType": "最大值", + "maxInputTypeTooLarge": "最大输入类型值过大。", + "maxResultsHint": "结果限制为 {{count}} 条。请使用筛选器缩小列表范围。", + "millionShort": "百万", + "minAmount": "最小金额", + "minAmountTooLarge": "最小金额值过大。", + "minInputType": "最小值", + "minInputTypeTooLarge": "最小输入类型值过大。", + "name": "名称", + "noEntries": "没有可显示的条目", + "noEvents": "没有事件", "nonEmpty": "非空", + "noTransactions": "没有交易", + "numberOfDecimalPlaces": "小数位数", "numberOfTransactions": "交易数量", - "outgoingTransfers": "支出转账", "outgoingAmount": "支出金额", + "outgoingTransfers": "支出转账", "price": "价格", + "qrcodeModalTitle": "地址二维码", + "rank": "排名", + "remainingAmount": "剩余金额", + "removeAddress": "删除地址", + "resetFilters": "重置筛选", + "richList": "富豪榜", + "richListLoadFailed": "错误:无法加载富豪榜。请刷新页面或稍后再试。", + "richListWarning": "富豪榜数据会在每个纪元开始时更新", + "scShares": "智能合约份额", "search": "搜索", + "selectAsset": "选择资产", + "selectContract": "Select contract", + "selectUpTo": "最多选择{{count}}个", + "shareholders": "股东", + "sharesIssuer": "股份發行人", + "show": "显示", + "showingMaxTransactions": "显示最近 {{count}} 笔交易", + "showingMaxEvents": "显示最近 {{count}} 个事件", + "showLess": "显示更少", + "showMore": "显示更多", + "showPerPagePrefix": "每页显示", + "showPerPageSuffix": "条", + "showQRCode": "显示二维码", "signature": "签名", + "smartContract": "智能合约", + "smartContractCodeLoadError": "错误:加载智能合约代码失败。请刷新页面或稍后再试。", + "smartContracts": "智能合约", + "smartContractsLoadFailed": "错误:无法加载智能合约。请刷新页面或稍后再试。", "source": "发送方", "standard": "标准", - "txStatusSuccess": "转账成功", + "startDate": "开始日期", + "startTick": "起始Tick", + "startTickTooLarge": "起始Tick值过大。", + "status": "Status", + "targetTick": "目标刻度", + "thousandShort": "千", "tick": "刻度", + "tickCheckFailed": "无法检查tick状态,请稍后再试。", "tickLeader": "刻度验证者", "tickQuality": "纪元刻度质量", "tickQualityTooltip": "此纪元非空刻度的百分比", "ticks": "刻度", + "ticksLoadFailed": "错误:无法加载滴答数据。请刷新页面或稍后再试。如果今天是星期三,请等到下一个纪元开始。", "tickStatus": "刻度状态", "ticksThisEpoch": "当前纪元刻度(空)", "ticksThisEpochTooltip": "此紀元中處理的刻度總數和空刻度數量(括號中顯示)", + "timestamp": "时间戳", + "token": "代币", + "tokenIssuer": "{{name}} 发行人", + "tokenLoadFailed": "错误:无法加载代币。请刷新页面或稍后再试。", + "tokens": "代币", + "totalValueLocked": "总锁定价值 (TVL)", + "transactionNotFound": "未找到交易", "transactionPreview": "交易预览", "transactions": "交易", - "data": "数据", - "defaultView": "默认视图", "unableToDecodeContractInput": "无法解码此合约的输入数据。", - "exportJson": "导出 JSON", - "showMore": "显示更多", - "showLess": "显示更少", - "type": "类型", "unexecuted": "未执行", "value": "值", - "loadMore": "加载更多", - "noTransactions": "没有交易", - "allTransactionsLoaded": "您已经看过所有的交易", - "loading": "加载中", - "hide": "隐藏", - "show": "显示", - "transactionNotFound": "未找到交易", - "invalidTransactionId": "交易ID无效。请检查ID并重试。", - "invalidAddressError": "地址格式无效。请检查地址并重试。", - "addressNotFoundError": "错误:我们找不到您要查找的地址。请检查地址并重试。", - "loadingTransactionsError": "错误:加载交易时出现问题。请稍后再试。", - "invalidSourceIdentity": "无效的来源地址: {{address}}", - "invalidDestinationIdentity": "无效的目标地址: {{address}}", - "minInputTypeTooLarge": "最小输入类型值过大。", - "maxInputTypeTooLarge": "最大输入类型值过大。", - "minAmountTooLarge": "最小金额值过大。", - "maxAmountTooLarge": "最大金额值过大。", - "startTickTooLarge": "起始Tick值过大。", - "endTickTooLarge": "结束Tick值过大。", - "richList": "富豪榜", - "rank": "排名", - "addressID": "地址 ID", - "richListLoadFailed": "错误:无法加载富豪榜。请刷新页面或稍后再试。", - "assetsRichListInvalidAssetOrIssuer": "错误:发行者地址和/或资产名称无效。请检查URL参数。", - "richListWarning": "富豪榜数据会在每个纪元开始时更新", - "timestamp": "时间戳", - "fee": "费用", - "assetTransferWarning": "此交易仅在源地址的交易列表中可见。在当前版本中尚不支持在目标地址的交易列表中显示它。", - "ticksLoadFailed": "错误:无法加载滴答数据。请刷新页面或稍后再试。如果今天是星期三,请等到下一个纪元开始。", - "showItemsPerPage": "显示 {{count}} 项", - "itemsPerPage": "每页项数", - "latestStatsLoadFailed": "错误:无法加载最新统计数据。请刷新页面或稍后再试。", - "errorLoadingPrice": "错误:无法加载价格。请刷新页面或稍后再试。", - "exchange": "交易所", - "exchanges": "交易所", - "exchangesLoadFailed": "错误:无法加载交易所。请刷新页面或稍后再试。", - "token": "代币", - "tokens": "代币", - "smartContract": "智能合约", - "smartContracts": "智能合约", - "tokenLoadFailed": "错误:无法加载代币。请刷新页面或稍后再试。", - "smartContractsLoadFailed": "错误:无法加载智能合约。请刷新页面或稍后再试。", - "issuer": "发行人", - "name": "名称", "wallets": "钱包", - "assets": "资产", - "totalValueLocked": "总锁定价值 (TVL)", - "noEntries": "没有可显示的条目", - "assetsRichList": "资产富豪榜", - "assetsRichListDesc": "资产富豪榜显示每种资产的前持有者。", - "contractName": "合约名称", - "shareholders": "股东", - "checkContractShareholders": "查看合约股东", - "checkSourceCode": "查看源代码", - "checkContractProposal": "查看合约提案", - "smartContractCodeLoadError": "错误:加载智能合约代码失败。请刷新页面或稍后再试。", - "contract": "合约", - "copied": "已复制", - "copyAddress": "复制地址", - "copyTransactionId": "复制TX ID", - "copyToClipboard": "复制到剪贴板", - "showQRCode": "显示二维码", - "qrcodeModalTitle": "地址二维码", - "filterAllTransactions": "所有交易", - "filterTransferTransactions": "轉帳交易", - "filterApprovedTransactions": "已核准交易", - "filterAll": "全部", - "filterTransfer": "轉帳交易", - "filterApproved": "已核准", - "filterByCategory": "按类别筛选", - "selectAsset": "选择资产", - "scShares": "智能合约份额", - "sharesIssuer": "股份發行人", - "contractIndex": "索引", - "expandAll": "全部展開", - "collapseAll": "全部摺疊", - "managedBy": "由{{contract}}管理", "unknownContract": "未知合约", - "resetFilters": "重置筛选", - "startTick": "起始Tick", - "endTick": "结束Tick", - "startDate": "开始日期", - "endDate": "结束日期", - "invalidRangeAmount": "无效范围 - 起始值必须小于或等于结束值", - "invalidRangeInputType": "最小值必须小于或等于最大值", - "invalidTickRange": "起始Tick不能大于结束Tick", - "invalidDateRange": "起始日期必须早于结束日期", - "invalidAddressFormat": "地址必须由60个大写字母组成", - "destinationFilterHint": "对于智能合约交易,这是合约地址,而不是最终的代币接收者。", - "amountFilterHint": "QUBIC 交易金额。", - "customRange": "自定义范围", - "minAmount": "最小金额", - "maxAmount": "最大金额", - "amountOver0": "> 0", - "amount1to1M": "1 - 1百万", - "amount1Mto100M": "1百万 - 1亿", - "amount100Mto1B": "1亿 - 10亿", - "amount1Bto10B": "10亿 - 100亿", - "amountOver10B": "> 100亿", - "thousandShort": "千", - "millionShort": "百万", - "billionShort": "十亿", - "dateLastHour": "最近一小时", - "dateLast24Hours": "最近24小时", - "dateLastNDays": "最近{{count}}天", - "addressPlaceholder": "60字符地址", - "clearAllFiltersTooltip": "点击移除所有筛选", - "filterButton": "筛选", - "filters": "筛选条件", - "applyFilters": "应用筛选", - "directionAll": "全部", - "directionIncoming": "收入", - "directionOutgoing": "支出", - "directionIncomingTooltip": "收入交易", - "directionOutgoingTooltip": "支出交易", - "direction": "方向", - "inputType": "输入类型", - "minInputType": "最小值", - "maxInputType": "最大值", - "include": "包含", - "exclude": "排除", - "addAddress": "添加地址", - "removeAddress": "删除地址", - "maxAddressesReached": "已达到最多5个地址", - "duplicateAddress": "重复地址", - "matchingEntities": "匹配结果", "transactionsFound": "{{count}} 笔交易", - "showingMaxTransactions": "显示最近 {{count}} 笔交易", - "maxResultsHint": "结果限制为 {{count}} 条。请使用筛选器缩小列表范围。", - "waitingForTickTitle": "等待刻度处理完成", + "txID": "TX ID", + "txStatusExecuted": "交易已执行", + "txStatusFailed": "转账失败", + "txStatusSuccess": "转账成功", + "txType": "TX Type", + "unitOfMeasurement": "计量单位", + "virtualTransactionBanner": "这是由Qubic协议为智能合约生命周期事件生成的虚拟交易。", + "virtualTransactionNotFound": "未找到虚拟交易", "waitingForTickDesc": "此交易已安排在刻度 {{targetTick}}。当网络到达该刻度时将可用。", - "targetTick": "目标刻度", - "estimatedWait": "预计等待时间", - "tickCheckFailed": "无法检查tick状态,请稍后再试。" + "waitingForTickTitle": "等待刻度处理完成" } diff --git a/src/components/ui/Breadcrumbs.tsx b/src/components/ui/Breadcrumbs.tsx index 970760d1..5c28a34f 100644 --- a/src/components/ui/Breadcrumbs.tsx +++ b/src/components/ui/Breadcrumbs.tsx @@ -12,7 +12,7 @@ function Breadcrumbs({ children }: Props) {
  • {child}
  • {index < Children.count(children) - 1 && ( -
  • +
  • )} diff --git a/src/components/ui/DropdownMenu.tsx b/src/components/ui/DropdownMenu.tsx index 9f2075d5..55ef43af 100644 --- a/src/components/ui/DropdownMenu.tsx +++ b/src/components/ui/DropdownMenu.tsx @@ -13,6 +13,7 @@ interface DropdownMenuProps { interface DropdownMenuTriggerPropsBase { onToggle?: () => void className?: string + 'aria-label'?: string } interface DropdownMenuTriggerWithAs extends DropdownMenuTriggerPropsBase { @@ -100,7 +101,10 @@ const ClonedComponent = ({ }) DropdownMenu.Trigger = forwardRef( - function DropdownMenuTrigger({ children, onToggle, className, as }, ref) { + function DropdownMenuTrigger( + { children, onToggle, className, as, 'aria-label': ariaLabel }, + ref + ) { if (as) { return (
    }> @@ -115,6 +119,7 @@ DropdownMenu.Trigger = forwardRef( onClick={onToggle} className={className} ref={ref as React.Ref} + aria-label={ariaLabel} > {children} diff --git a/src/components/ui/PageSizeSelect.tsx b/src/components/ui/PageSizeSelect.tsx new file mode 100644 index 00000000..98b04a31 --- /dev/null +++ b/src/components/ui/PageSizeSelect.tsx @@ -0,0 +1,38 @@ +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' + +import type { Option } from '@app/components/ui/Select' +import Select from '@app/components/ui/Select' +import { PAGE_SIZE_OPTIONS } from '@app/constants' +import { clsxTwMerge } from '@app/utils' + +type Props = Readonly<{ + pageSize: number + onSelect: (option: Option) => void + className?: string +}> + +export default function PageSizeSelect({ pageSize, onSelect, className }: Props) { + const { t } = useTranslation('network-page') + + const defaultValue = useMemo( + () => PAGE_SIZE_OPTIONS.find((option) => option.value === String(pageSize)), + [pageSize] + ) + + return ( +
    + {t('showPerPagePrefix')} + )} - {shouldShowSearchResult && searchResult && 'tickData' in searchResult && ( - - )} + {shouldShowSearchResult && + searchResult && + 'tickData' in searchResult && + searchResult.tickData && ( + + )} {/* Entity search results (exchanges, smart contracts, tokens) */} {!debouncedSearchType && (entityExactMatch || entityPartialMatches.length > 0) && ( diff --git a/src/components/ui/layouts/Header/constants.ts b/src/components/ui/layouts/Header/constants.ts index d7e89271..eecd95a8 100644 --- a/src/components/ui/layouts/Header/constants.ts +++ b/src/components/ui/layouts/Header/constants.ts @@ -2,6 +2,15 @@ import { Routes } from '@app/router/routes' import type { NavigationMenuItem } from './types' export const NAVIGATION_MENU_ITEMS: NavigationMenuItem[] = [ + { + i18nKey: 'blockchain', + items: [ + { + i18nKey: 'events', + href: Routes.NETWORK.BLOCKCHAIN.EVENTS + } + ] + }, { i18nKey: 'wallets', items: [ diff --git a/src/configs/envConfig.ts b/src/configs/envConfig.ts index 34176d71..dad8c7dc 100644 --- a/src/configs/envConfig.ts +++ b/src/configs/envConfig.ts @@ -3,13 +3,15 @@ type EnvConfig = { QLI_API_URL: string QUBIC_RPC_URL: string STATIC_API_URL: string + EVENTS_API_URL: string } export const envConfig: EnvConfig = { NETWORK: import.meta.env.VITE_NETWORK, QLI_API_URL: import.meta.env.VITE_QLI_API_URL, QUBIC_RPC_URL: import.meta.env.VITE_QUBIC_RPC_URL, - STATIC_API_URL: import.meta.env.VITE_STATIC_API_URL + STATIC_API_URL: import.meta.env.VITE_STATIC_API_URL, + EVENTS_API_URL: import.meta.env.VITE_EVENTS_API_URL } export default envConfig diff --git a/src/constants/index.ts b/src/constants/index.ts index da6a58be..9268ad3a 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -3,6 +3,34 @@ import { TransactionOptionEnum } from '@app/types' export const OVERVIEW_DATA_POLLING_INTERVAL_MS = 60_000 +// Cache time for transaction and event queries (in seconds) +export const QUERY_CACHE_TIME = 30 + +export const DEFAULT_PAGE_SIZE = 25 + +export const PAGE_SIZE_OPTIONS: Option[] = [ + { label: '10', value: '10' }, + { label: '25', value: '25' }, + { label: '50', value: '50' }, + { label: '100', value: '100' } +] + +export const VALID_PAGE_SIZES = PAGE_SIZE_OPTIONS.map((o) => Number(o.value)) + +export function validatePageSize(raw: string | null): number { + const parsed = parseInt(raw ?? String(DEFAULT_PAGE_SIZE), 10) + return VALID_PAGE_SIZES.includes(parsed) ? parsed : DEFAULT_PAGE_SIZE +} + +export const MAX_PAGE = 10_000 + +export function validatePage(raw: string | null): number { + const parsed = parseInt(raw || '1', 10) || 1 + return Math.max(1, Math.min(parsed, MAX_PAGE)) +} + +export const RICH_LIST_DEFAULT_PAGE_SIZE = 10 + type KeyedOption = Omit, 'label'> & { labelKey: string } export const TRANSACTION_OPTIONS: readonly KeyedOption[] = [ @@ -17,4 +45,11 @@ export const TRANSACTION_OPTIONS_MOBILE: readonly KeyedOption[] = [ { labelKey: 'filterApproved', value: TransactionOptionEnum.APPROVED } ] as const -export default { TRANSACTION_OPTIONS, TRANSACTION_OPTIONS_MOBILE } +export default { + TRANSACTION_OPTIONS, + TRANSACTION_OPTIONS_MOBILE, + DEFAULT_PAGE_SIZE, + PAGE_SIZE_OPTIONS, + VALID_PAGE_SIZES, + RICH_LIST_DEFAULT_PAGE_SIZE +} diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 260a4e56..026fbd47 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,6 +1,12 @@ export * from './redux' export { default as useBodyScrollLock } from './useBodyScrollLock' export { default as useTailwindBreakpoint } from './useTailwindBreakpoint' -export { useTransactionExpandCollapse } from './useTransactionExpandCollapse' export { useGetAddressName } from './useGetAddressName' export type { GetAddressNameResult } from './useGetAddressName' +export { useGetEpochForTick } from './useGetEpochForTick' +export { useGetSmartContractByIndex } from './useGetSmartContractByIndex' +export { useSanitizedEventTypes } from './useSanitizedEventType' +export { usePageAutoCorrect } from './usePageAutoCorrect' +export { usePaginationSearchParams } from './usePaginationSearchParams' +export { useValidatedPage } from './useValidatedPage' +export { useValidatedPageSize } from './useValidatedPageSize' diff --git a/src/hooks/useGetAddressName.ts b/src/hooks/useGetAddressName.ts index 220e7d3e..b96f6a2e 100644 --- a/src/hooks/useGetAddressName.ts +++ b/src/hooks/useGetAddressName.ts @@ -1,4 +1,5 @@ import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' import { useGetAddressLabelsQuery, useGetExchangesQuery, @@ -21,6 +22,7 @@ export type GetAddressNameResult = { * Note: Fetches all assets once and filters in memory for efficiency */ export function useGetAddressName(address: string): GetAddressNameResult | undefined { + const { t } = useTranslation('network-page') const { data: smartContracts } = useGetSmartContractsQuery() const { data: exchanges } = useGetExchangesQuery() const { data: addressLabels } = useGetAddressLabelsQuery() @@ -29,6 +31,8 @@ export function useGetAddressName(address: string): GetAddressNameResult | undef const { data: allAssetsIssuances } = useGetAssetsIssuancesQuery() return useMemo(() => { + if (address === '') return undefined + // Check smart contracts first (highest priority) const smartContract = smartContracts?.find((sc) => sc.address === address) if (smartContract) { @@ -56,10 +60,12 @@ export function useGetAddressName(address: string): GetAddressNameResult | undef ) if (tokenIssuance) { // Try to find matching token data from static API to get website - const tokenData = tokens?.find((t) => t.name === tokenIssuance.data.name) + const tokenData = tokens?.find( + (tk) => tk.name === tokenIssuance.data.name && tk.issuer != null && tk.issuer === address + ) return { - name: tokenIssuance.data.name, - i18nKey: 'token', + name: t('tokenIssuer', { name: tokenIssuance.data.name }), + i18nKey: 'tokenIssuer', website: tokenData?.website } } @@ -75,5 +81,5 @@ export function useGetAddressName(address: string): GetAddressNameResult | undef } return undefined - }, [address, smartContracts, exchanges, addressLabels, tokens, allAssetsIssuances]) + }, [address, smartContracts, exchanges, addressLabels, tokens, allAssetsIssuances, t]) } diff --git a/src/hooks/useGetEpochForTick.ts b/src/hooks/useGetEpochForTick.ts new file mode 100644 index 00000000..320f2d55 --- /dev/null +++ b/src/hooks/useGetEpochForTick.ts @@ -0,0 +1,20 @@ +import { useMemo } from 'react' + +import { useGetProcessedTickIntervalsQuery } from '@app/store/apis/query-service' +import { getEpochForTick } from '@app/utils' + +export function useGetEpochForTick(tickNumber: number): { + epoch: number | undefined + isLoading: boolean +} { + const { data: intervals, isFetching } = useGetProcessedTickIntervalsQuery(undefined, { + skip: !tickNumber + }) + + const epoch = useMemo( + () => (intervals ? getEpochForTick(intervals, tickNumber) : undefined), + [intervals, tickNumber] + ) + + return { epoch, isLoading: isFetching } +} diff --git a/src/hooks/useGetSmartContractByIndex.ts b/src/hooks/useGetSmartContractByIndex.ts new file mode 100644 index 00000000..8f340788 --- /dev/null +++ b/src/hooks/useGetSmartContractByIndex.ts @@ -0,0 +1,19 @@ +import { useMemo } from 'react' +import { useGetSmartContractsQuery } from '@app/store/apis/qubic-static' +import type { SmartContract } from '@app/store/apis/qubic-static' + +export function useGetSmartContractByIndex( + contractIndex: number | undefined +): SmartContract | undefined { + const { data: smartContracts } = useGetSmartContractsQuery(undefined, { + skip: contractIndex === undefined + }) + + return useMemo( + () => + contractIndex !== undefined + ? smartContracts?.find((sc) => sc.contractIndex === contractIndex) + : undefined, + [smartContracts, contractIndex] + ) +} diff --git a/src/hooks/usePageAutoCorrect.ts b/src/hooks/usePageAutoCorrect.ts new file mode 100644 index 00000000..824bafd7 --- /dev/null +++ b/src/hooks/usePageAutoCorrect.ts @@ -0,0 +1,26 @@ +import { useEffect } from 'react' +import { useSearchParams } from 'react-router-dom' + +/** + * Auto-corrects the URL page param when it exceeds the available pages. + * Handles edge cases like direct URL entry with stale page params. + */ +export function usePageAutoCorrect(hasData: boolean, total: number, pageSize: number): void { + const [searchParams, setSearchParams] = useSearchParams() + + const rawPage = searchParams.get('page') + const page = rawPage ? parseInt(rawPage, 10) || 1 : 1 + const maxPage = Math.max(1, Math.ceil(total / pageSize)) + + useEffect(() => { + if (hasData && page > maxPage) { + setSearchParams( + (prev) => { + prev.set('page', '1') + return prev + }, + { replace: true } + ) + } + }, [hasData, page, maxPage, setSearchParams]) +} diff --git a/src/hooks/usePaginationSearchParams.ts b/src/hooks/usePaginationSearchParams.ts new file mode 100644 index 00000000..05da8028 --- /dev/null +++ b/src/hooks/usePaginationSearchParams.ts @@ -0,0 +1,41 @@ +import { useCallback } from 'react' +import { useSearchParams } from 'react-router-dom' + +/** + * Provides pagination handlers that sync with URL search params. + * Replaces the copy-pasted handlePageChange / handlePageSizeChange callbacks + * used across multiple pages. + */ +export function usePaginationSearchParams() { + const [, setSearchParams] = useSearchParams() + + const handlePageChange = useCallback( + (value: number) => { + setSearchParams((prev) => ({ + ...Object.fromEntries(prev.entries()), + page: value.toString() + })) + }, + [setSearchParams] + ) + + const handlePageSizeChange = useCallback( + (option: { value: string }) => { + setSearchParams((prev) => ({ + ...Object.fromEntries(prev.entries()), + pageSize: option.value, + page: '1' + })) + }, + [setSearchParams] + ) + + const resetPage = useCallback(() => { + setSearchParams((prev) => ({ + ...Object.fromEntries(prev.entries()), + page: '1' + })) + }, [setSearchParams]) + + return { handlePageChange, handlePageSizeChange, resetPage } +} diff --git a/src/hooks/useSanitizedEventType.ts b/src/hooks/useSanitizedEventType.ts new file mode 100644 index 00000000..b1dcf104 --- /dev/null +++ b/src/hooks/useSanitizedEventType.ts @@ -0,0 +1,40 @@ +import { useEffect, useMemo } from 'react' +import { useSearchParams } from 'react-router-dom' + +import { parseEventTypesParam } from '@app/store/apis/events' + +export function useSanitizedEventTypes(): number[] { + const [searchParams, setSearchParams] = useSearchParams() + + const rawEventType = searchParams.get('eventType') + const eventTypes = useMemo(() => parseEventTypesParam(rawEventType), [rawEventType]) + + useEffect(() => { + if (rawEventType === null) return + + const sanitized = eventTypes.join(',') + if (eventTypes.length === 0) { + // All values were invalid — remove param + setSearchParams( + (prev) => { + const next = new URLSearchParams(prev) + next.delete('eventType') + return next + }, + { replace: true } + ) + } else if (sanitized !== rawEventType) { + // Some values were invalid or duplicated — fix param + setSearchParams( + (prev) => { + const next = new URLSearchParams(prev) + next.set('eventType', sanitized) + return next + }, + { replace: true } + ) + } + }, [rawEventType, eventTypes, setSearchParams]) + + return eventTypes +} diff --git a/src/hooks/useValidatedPage.ts b/src/hooks/useValidatedPage.ts new file mode 100644 index 00000000..54941bc9 --- /dev/null +++ b/src/hooks/useValidatedPage.ts @@ -0,0 +1,26 @@ +import { useEffect } from 'react' +import { useSearchParams } from 'react-router-dom' + +import { validatePage } from '@app/constants' + +export function useValidatedPage(enabled = true): number { + const [searchParams, setSearchParams] = useSearchParams() + + const rawPage = searchParams.get('page') + const page = enabled ? validatePage(rawPage) : 1 + + useEffect(() => { + if (enabled && rawPage !== null && String(page) !== rawPage) { + setSearchParams( + (prev) => { + const next = new URLSearchParams(prev) + next.set('page', String(page)) + return next + }, + { replace: true } + ) + } + }, [enabled, rawPage, page, setSearchParams]) + + return page +} diff --git a/src/hooks/useValidatedPageSize.ts b/src/hooks/useValidatedPageSize.ts new file mode 100644 index 00000000..278d435b --- /dev/null +++ b/src/hooks/useValidatedPageSize.ts @@ -0,0 +1,26 @@ +import { useEffect } from 'react' +import { useSearchParams } from 'react-router-dom' + +import { validatePageSize } from '@app/constants' + +export function useValidatedPageSize(enabled = true): number { + const [searchParams, setSearchParams] = useSearchParams() + + const rawPageSize = searchParams.get('pageSize') + const pageSize = enabled ? validatePageSize(rawPageSize) : validatePageSize(null) + + useEffect(() => { + if (enabled && rawPageSize !== null && String(pageSize) !== rawPageSize) { + setSearchParams( + (prev) => { + const next = new URLSearchParams(prev) + next.set('pageSize', String(pageSize)) + return next + }, + { replace: true } + ) + } + }, [enabled, rawPageSize, pageSize, setSearchParams]) + + return pageSize +} diff --git a/src/pages/network/TxPage.tsx b/src/pages/network/TxPage.tsx index 4cea2f15..aceb6872 100644 --- a/src/pages/network/TxPage.tsx +++ b/src/pages/network/TxPage.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react' +import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useParams } from 'react-router-dom' @@ -7,14 +7,126 @@ import { Breadcrumbs } from '@app/components/ui' import { ErrorFallback } from '@app/components/ui/error-boundaries' import { PageLayout } from '@app/components/ui/layouts' import { LinearProgress } from '@app/components/ui/loaders' +import { usePageAutoCorrect, useValidatedPage, useValidatedPageSize } from '@app/hooks' +import { useGetEventsQuery, type ParsedVirtualTxId, parseVirtualTxId } from '@app/store/apis/events' import { useGetTransactionByHashQuery } from '@app/store/apis/query-service' -import { formatEllipsis } from '@app/utils' -import { HomeLink, TickLink, TxItem, WaitingForTick } from './components' -import { useTickWatcher } from './hooks' +import { formatDate, formatEllipsis } from '@app/utils' +import { CardItem, HomeLink, SubCardItem, TickLink, TxItem, WaitingForTick } from './components' +import TransactionEvents from './components/TxItem/TransactionEvents' +import { useTickWatcher, useTransactionEvents } from './hooks' -function TxPage() { +const VIRTUAL_TX_TYPE_LABELS: Record = { + 2: 'Smart Contract Begin Epoch', + 3: 'Smart Contract Begin Tick', + 4: 'Smart Contract End Tick', + 5: 'Smart Contract End Epoch' +} + +function VirtualTxContent({ txId, virtualTx }: { txId: string; virtualTx: ParsedVirtualTxId }) { const { t } = useTranslation('network-page') - const { txId = '' } = useParams() + + const page = useValidatedPage() + const pageSize = useValidatedPageSize() + const offset = (page - 1) * pageSize + + const { data, isFetching, isError } = useGetEventsQuery({ + tickNumber: virtualTx.tickNumber, + category: virtualTx.category, + offset, + size: pageSize + }) + + const total = data?.total ?? 0 + usePageAutoCorrect(!!data, total, pageSize) + + const events = data?.events ?? [] + const txTypeLabel = VIRTUAL_TX_TYPE_LABELS[virtualTx.category] ?? `Category ${virtualTx.category}` + + const firstEvent = data?.events?.[0] + const firstEventTimestamp = firstEvent?.timestamp + const epoch = firstEvent?.epoch + const { date, time } = useMemo( + () => + formatDate(firstEventTimestamp !== undefined ? String(firstEventTimestamp) : undefined, { + split: true + }), + [firstEventTimestamp] + ) + + if (isFetching && events.length === 0) return + if (isError) return + + return ( + + + +

    + {t('tick')} +

    +

    {txId}

    +
    + +

    {t('transactionPreview')}

    + +
    +

    {t('virtualTransactionBanner')}

    +
    + + + {txId}

    } + hideTopBorder + /> + {txTypeLabel}

    } + /> + } + /> + {epoch != null && ( + {epoch}

    } + /> + )} + {date && ( + + {date}{' '} + {time} +

    + } + /> + )} +
    + +
    + +
    +
    + ) +} + +function RegularTxContent({ txId }: { txId: string }) { + const { t } = useTranslation('network-page') + const { events, total, isLoading: isEventsLoading } = useTransactionEvents(txId) const { data: tx, @@ -87,17 +199,38 @@ function TxPage() {

    {formatEllipsis(tx.hash)}

    -

    {t('transactionPreview')}

    +

    {t('transactionPreview')}

    +
    + +
    ) } +function TxPage() { + const { txId = '' } = useParams() + const virtualTx = useMemo(() => parseVirtualTxId(txId), [txId]) + + if (virtualTx) { + return + } + + return +} + const TxPageWithHelmet = withHelmet(TxPage, { title: 'Transaction | Qubic Explorer' }) diff --git a/src/pages/network/address/AddressPage.tsx b/src/pages/network/address/AddressPage.tsx index 6ba5ab88..47cc6829 100644 --- a/src/pages/network/address/AddressPage.tsx +++ b/src/pages/network/address/AddressPage.tsx @@ -20,7 +20,13 @@ import { useGetSmartContractsQuery } from '@app/store/apis/qubic-static' import { clsxTwMerge, formatEllipsis, formatString, isValidQubicAddress } from '@app/utils' import { useGetAddressName } from '@app/hooks' import { HomeLink } from '../components' -import { AddressDetails, ContractOverview, OwnedAssets, TransactionsOverview } from './components' +import { + AddressDetails, + AddressEvents, + ContractOverview, + OwnedAssets, + TransactionsOverview +} from './components' function AddressPage() { const { t } = useTranslation('network-page') @@ -78,11 +84,17 @@ function AddressPage() { const [searchParams, setSearchParams] = useSearchParams() const tabParam = searchParams.get('tab') - const selectedTabIndex = tabParam === 'contract' && isSmartContract ? 1 : 0 + + const selectedTabIndex = useMemo(() => { + if (tabParam === 'events') return 1 + if (tabParam === 'contract' && isSmartContract) return 2 + return 0 + }, [tabParam, isSmartContract]) // Normalize invalid tab params so URL always reflects the visible tab useEffect(() => { - if (tabParam && !(tabParam === 'contract' && isSmartContract)) { + const isValidTab = tabParam === 'events' || (tabParam === 'contract' && isSmartContract) + if (tabParam && !isValidTab) { setSearchParams( (prev) => { prev.delete('tab') @@ -98,6 +110,8 @@ function AddressPage() { setSearchParams( (prev) => { if (index === 1) { + prev.set('tab', 'events') + } else if (index === 2) { prev.set('tab', 'contract') } else { prev.delete('tab') @@ -150,7 +164,7 @@ function AddressPage() { {addressName && (
    {/* Type Label (not for named addresses) */} - {addressName.i18nKey !== 'named-address' && ( + {addressName.i18nKey !== 'named-address' && addressName.i18nKey !== 'tokenIssuer' && ( {t(addressName.i18nKey)} @@ -214,12 +228,16 @@ function AddressPage() { > {t('transactions')} + {t('events')} {isSmartContract && {t('contract')}} + + + {isSmartContract && smartContractDetails && ( + +export default function AddressEvents({ addressId }: Props) { + const { t } = useTranslation('network-page') + const { + events, + total, + eventTypes, + direction, + tickStart, + tickEnd, + dateRange, + sourceFilter, + destinationFilter, + amountFilter, + isLoading, + hasError + } = useAddressEvents(addressId) + + const filters = useEventFilters({ + tickStart, + tickEnd, + eventTypes, + direction, + dateRange, + sourceFilter, + destinationFilter, + amountFilter, + addressId + }) + + return ( +
    + + + + + {hasError ? ( + {t('eventsLoadFailed')} + ) : ( + + )} +
    + ) +} diff --git a/src/pages/network/address/components/TransactionsOverview/LatestTransactions.tsx b/src/pages/network/address/components/TransactionsOverview/LatestTransactions.tsx index 39b35066..fd7026b9 100644 --- a/src/pages/network/address/components/TransactionsOverview/LatestTransactions.tsx +++ b/src/pages/network/address/components/TransactionsOverview/LatestTransactions.tsx @@ -1,70 +1,39 @@ -import { useCallback, useMemo } from 'react' +import { memo, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { ChevronDownIcon, Infocon } from '@app/assets/icons' -import { InfiniteScroll, Tooltip } from '@app/components/ui' -import { Button } from '@app/components/ui/buttons' -import { DotsLoader } from '@app/components/ui/loaders' -import { useTransactionExpandCollapse } from '@app/hooks' -import type { QueryServiceTransaction } from '@app/store/apis/query-service' -import { TxItem } from '../../../components' -import { MAX_TRANSACTION_RESULTS, type TransactionFilters } from '../../hooks/useLatestTransactions' +import { Infocon } from '@app/assets/icons' +import { PageSizeSelect, PaginationBar, Tooltip } from '@app/components/ui' +import type { Option } from '@app/components/ui/Select' +import { DEFAULT_PAGE_SIZE } from '@app/constants' +import useLatestTransactions, { MAX_TRANSACTION_RESULTS } from '../../hooks/useLatestTransactions' +import { TransactionRow, TransactionSkeletonRow } from '../../../components' import { parseFilterApiError } from './filterUtils' import TransactionFiltersBar from './TransactionFiltersBar' +const COLUMN_COUNT = 8 + +const SkeletonRows = memo(({ count }: { count: number }) => ( + <> + {Array.from({ length: count }).map((_, index) => ( + // eslint-disable-next-line react/no-array-index-key + + ))} + +)) + type Props = { addressId: string - transactions: QueryServiceTransaction[] - totalCount: number | null - loadMore: () => Promise - hasMore: boolean - isLoading: boolean - error: string | null - onApplyFilters: (filters: TransactionFilters) => void - onClearFilters: () => void - activeFilters: TransactionFilters } -export default function LatestTransactions({ - addressId, - transactions, - totalCount, - loadMore, - hasMore, - isLoading, - error, - onApplyFilters, - onClearFilters, - activeFilters -}: Props) { +export default function LatestTransactions({ addressId }: Props) { const { t } = useTranslation('network-page') + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE) - // Use shared expand/collapse hook with custom ID extractor for QueryServiceTransaction - const { expandAll, expandedTxIds, handleExpandAllChange, handleTxToggle } = - useTransactionExpandCollapse({ - transactions, - getTransactionId: (tx) => tx.hash, - resetDependency: addressId - }) - - const renderTxItem = useCallback( - (tx: QueryServiceTransaction) => ( - - ), - [addressId, expandedTxIds, handleTxToggle] - ) + const { transactions, totalCount, isLoading, error, applyFilters, clearFilters, activeFilters } = + useLatestTransactions(addressId, page, pageSize) // Parse API error to show localized message - // The server returns only a partial address in error messages, so we display it as-is const errorMessage = useMemo(() => { if (!error) return null const parsed = parseFilterApiError(error) @@ -74,76 +43,136 @@ export default function LatestTransactions({ return t('loadingTransactionsError') }, [error, t]) + const pageCount = totalCount !== null ? Math.ceil(totalCount / pageSize) : 0 + + const handlePageChange = useCallback((value: number) => { + setPage(value) + }, []) + + const handlePageSizeChange = useCallback((option: Option) => { + setPageSize(parseInt(option.value, 10)) + setPage(1) + }, []) + + const handleApplyFilters = useCallback( + (filters: Parameters[0]) => { + applyFilters(filters) + setPage(1) + }, + [applyFilters] + ) + + const handleClearFilters = useCallback(() => { + clearFilters() + setPage(1) + }, [clearFilters]) + + const renderTableContent = useCallback(() => { + if (isLoading) { + return + } + + if (errorMessage) { + return ( + + + {errorMessage} + + + ) + } + + if (transactions.length === 0) { + return ( + + + {t('noTransactions')} + + + ) + } + + return transactions.map((tx) => ( + + )) + }, [isLoading, errorMessage, transactions, pageSize, t, addressId]) + return (
    - {(totalCount !== null || transactions.length > 0) && ( -
    - {totalCount !== null && totalCount > 0 ? ( -
    - {totalCount >= MAX_TRANSACTION_RESULTS ? ( - <> - - {t('showingMaxTransactions', { - count: MAX_TRANSACTION_RESULTS.toLocaleString() - } as Record)} - - )} - > - - - - ) : ( +
    + {totalCount !== null && totalCount > 0 ? ( +
    + {totalCount >= MAX_TRANSACTION_RESULTS ? ( + <> - {t('transactionsFound', { count: totalCount.toLocaleString() } as Record< - string, - string - >)} + {t('showingMaxTransactions', { + count: MAX_TRANSACTION_RESULTS.toLocaleString() + } as Record)} - )} -
    - ) : null} - - {transactions.length > 0 && ( - - )} + )} + > + + + + ) : ( + + {t('transactionsFound', { count: totalCount.toLocaleString() } as Record< + string, + string + >)} + + )} +
    + ) : ( + + )} + +
    + +
    +
    + + + + + + + + + + + + + + {renderTableContent()} +
    + {t('status')} + {t('txID')}{t('txType')}{t('tick')}{t('timestamp')}{t('source')} + {t('destination')} + + {t('amount')} +
    - )} - - } - error={errorMessage} - endMessage={ -

    - {transactions.length === 0 ? t('noTransactions') : t('allTransactionsLoaded')} -

    - } - renderItem={renderTxItem} - /> + {pageCount > 1 && ( + + )} +
    ) } diff --git a/src/pages/network/address/components/TransactionsOverview/TransactionFiltersBar.tsx b/src/pages/network/address/components/TransactionsOverview/TransactionFiltersBar.tsx index 140dadac..a494fec0 100644 --- a/src/pages/network/address/components/TransactionsOverview/TransactionFiltersBar.tsx +++ b/src/pages/network/address/components/TransactionsOverview/TransactionFiltersBar.tsx @@ -1,7 +1,12 @@ -import { useCallback, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { FunnelIcon } from '@app/assets/icons' +import { + useGetAddressLabelsQuery, + useGetExchangesQuery, + useGetSmartContractsQuery +} from '@app/store/apis/qubic-static' import { ActiveFilterChip, AmountFilterContent, @@ -10,7 +15,12 @@ import { RangeFilterContent, ResetFiltersButton } from '../../../components/filters' -import { formatRangeLabel, useAmountPresetHandler, useClearFilterHandler } from '../../../hooks' +import { + formatRangeLabel, + getAmountRangeLabel, + useAmountPresetHandler, + useClearFilterHandler +} from '../../../hooks' import type { AddressFilter, TransactionDirection, @@ -50,6 +60,20 @@ export default function TransactionFiltersBar({ onClearFilters }: Props) { const { t } = useTranslation('network-page') + + // Build address → name lookup map from static data (for filter labels) + const { data: smartContracts } = useGetSmartContractsQuery() + const { data: exchanges } = useGetExchangesQuery() + const { data: addressLabels } = useGetAddressLabelsQuery() + const addressNameMap = useMemo(() => { + const map = new Map() + // Build in reverse priority order (last wins): labels → exchanges → SCs + addressLabels?.forEach((label) => map.set(label.address, label.label)) + exchanges?.forEach((ex) => map.set(ex.address, ex.name)) + smartContracts?.forEach((sc) => map.set(sc.address, sc.label || sc.name)) + return map + }, [smartContracts, exchanges, addressLabels]) + const [openDropdown, setOpenDropdown] = useState(null) const [isMobileModalOpen, setIsMobileModalOpen] = useState(false) @@ -118,7 +142,7 @@ export default function TransactionFiltersBar({ setValidationErrors((prev) => ({ ...prev, [dropdownKey]: null })) - const newFilters = { + const newFilters: TransactionFilters = { ...activeFilters, [filterKey]: start || end ? { start, end } : undefined } @@ -228,43 +252,37 @@ export default function TransactionFiltersBar({ const isInputTypeActive = !!( activeFilters.inputTypeRange?.start || activeFilters.inputTypeRange?.end ) - // Get display labels for active filters + const formatAddress = (address: string) => + addressNameMap.get(address) || formatAddressShort(address) + const getSourceLabel = () => { if (!isSourceActive) return t('source') - const mode = activeFilters.sourceFilter?.mode ?? 'include' - const modeLabel = t(mode) + const isExclude = activeFilters.sourceFilter?.mode === 'exclude' + const prefix = isExclude ? `${t('exclude')} ` : '' if (sourceAddresses.length === 1) { - return `${t('source')}: ${modeLabel} ${formatAddressShort(sourceAddresses[0])}` + return `${t('source')}: ${prefix}${formatAddress(sourceAddresses[0])}` } - return `${t('source')}: ${modeLabel} ${sourceAddresses.length}` + return `${t('source')}: ${prefix}${sourceAddresses.length}` } const getDestinationLabel = () => { if (!isDestinationActive) return t('destination') - const mode = activeFilters.destinationFilter?.mode ?? 'include' - const modeLabel = t(mode) + const isExclude = activeFilters.destinationFilter?.mode === 'exclude' + const prefix = isExclude ? `${t('exclude')} ` : '' if (destinationAddresses.length === 1) { - return `${t('destination')}: ${modeLabel} ${formatAddressShort(destinationAddresses[0])}` + return `${t('destination')}: ${prefix}${formatAddress(destinationAddresses[0])}` } - return `${t('destination')}: ${modeLabel} ${destinationAddresses.length}` + return `${t('destination')}: ${prefix}${destinationAddresses.length}` } - const getAmountLabel = () => { - if (!isAmountActive) return t('amount') - const { start, end, presetKey } = activeFilters.amountRange || {} - - if (presetKey) { - const preset = AMOUNT_PRESETS.find((p) => p.labelKey === presetKey) - if (preset) return `${t('amount')}: ${t(preset.labelKey)}` - } - - if (start && end) - return `${t('amount')}: ${formatAmountShort(start, t)} - ${formatAmountShort(end, t)}` - if (start) return `${t('amount')}: >= ${formatAmountShort(start, t)}` - if (end) return `${t('amount')}: <= ${formatAmountShort(end, t)}` - return t('amount') - } + const amountLabel = getAmountRangeLabel( + t('amount'), + activeFilters.amountRange, + AMOUNT_PRESETS, + t, + formatAmountShort + ) const getDateLabel = () => { if (!isDateActive) return t('date') @@ -318,9 +336,7 @@ export default function TransactionFiltersBar({ {isDestinationActive && ( )} - {isAmountActive && ( - - )} + {isAmountActive && } {isDateActive && } {isTickActive && } {isInputTypeActive && ( @@ -411,7 +427,7 @@ export default function TransactionFiltersBar({ {/* Amount Filter */} handleToggle('amount')} diff --git a/src/pages/network/address/components/TransactionsOverview/TransactionsOverview.tsx b/src/pages/network/address/components/TransactionsOverview/TransactionsOverview.tsx index 61ab366b..2c68a0a9 100644 --- a/src/pages/network/address/components/TransactionsOverview/TransactionsOverview.tsx +++ b/src/pages/network/address/components/TransactionsOverview/TransactionsOverview.tsx @@ -1,6 +1,5 @@ import { memo } from 'react' -import { useLatestTransactions } from '../../hooks' import LatestTransactions from './LatestTransactions' type Props = { @@ -8,32 +7,9 @@ type Props = { } function TransactionsOverview({ addressId }: Props) { - const { - transactions, - totalCount, - loadMoreTransactions, - hasMore, - isLoading, - error, - applyFilters, - clearFilters, - activeFilters - } = useLatestTransactions(addressId) - return (
    - +
    ) } diff --git a/src/pages/network/address/components/TransactionsOverview/filterUtils.ts b/src/pages/network/address/components/TransactionsOverview/filterUtils.ts index 9d2e8155..2dfbf7c2 100644 --- a/src/pages/network/address/components/TransactionsOverview/filterUtils.ts +++ b/src/pages/network/address/components/TransactionsOverview/filterUtils.ts @@ -1,5 +1,5 @@ import { isValidAddressFormat } from '@app/utils' -import { MAX_UINT32 } from '../../../utils/filterUtils' +import { MAX_UINT32, validateNumericRange } from '../../../utils/filterUtils' // Re-export shared utilities from network utils (only what's actually used by address page) export { @@ -8,7 +8,7 @@ export { formatAddressShort, formatAmountForDisplay, formatAmountShort, - parseAmountFromDisplay, + parseNumericInput, parseFilterApiError, validateAmountRange, validateInputTypeRange @@ -153,27 +153,6 @@ export function validateAddressFilter(filter: AddressFilter | undefined): string return errors.find((e) => e !== null) ?? null } -/** - * Validates a numeric range filter (amount, inputType, tick). - * @param start - Start value - * @param end - End value - * @param strictComparison - If true, start must be < end (not <=) - * Returns an error message key or null if valid. - */ -function validateNumericRange( - start: string | undefined, - end: string | undefined, - strictComparison = false -): string | null { - if (!start || !end) return null - - const startNum = Number(start) - const endNum = Number(end) - const isInvalid = strictComparison ? startNum >= endNum : startNum > endNum - - return isInvalid ? 'invalid' : null -} - /** * Validates a tick number range filter. * Checks both range validity (start <= end) and maximum value constraint (uint32 max). @@ -271,7 +250,7 @@ export const DATE_PRESETS = [ /** * Helper to check if an address filter contains only the page address */ -function isOnlyPageAddress(filter: AddressFilter | undefined, addressId: string): boolean { +export function isOnlyPageAddress(filter: AddressFilter | undefined, addressId: string): boolean { if (!filter) return false const validAddresses = filter.addresses.filter((addr) => addr.trim() !== '') return validAddresses.length === 1 && validAddresses[0] === addressId diff --git a/src/pages/network/address/components/index.ts b/src/pages/network/address/components/index.ts index 55a1f304..ee204dfb 100644 --- a/src/pages/network/address/components/index.ts +++ b/src/pages/network/address/components/index.ts @@ -1,4 +1,5 @@ export { default as AddressDetails } from './AddressDetails' +export { default as AddressEvents } from './AddressEvents' export { default as ContractOverview } from './ContractOverview' export { default as OwnedAssets } from './OwnedAssets' export { default as LatestTransactions } from './TransactionsOverview/LatestTransactions' diff --git a/src/pages/network/address/hooks/index.ts b/src/pages/network/address/hooks/index.ts index 218ba158..d700218a 100644 --- a/src/pages/network/address/hooks/index.ts +++ b/src/pages/network/address/hooks/index.ts @@ -1 +1,2 @@ +export { default as useAddressEvents } from './useAddressEvents' export { default as useLatestTransactions } from './useLatestTransactions' diff --git a/src/pages/network/address/hooks/useAddressEvents.ts b/src/pages/network/address/hooks/useAddressEvents.ts new file mode 100644 index 00000000..72734203 --- /dev/null +++ b/src/pages/network/address/hooks/useAddressEvents.ts @@ -0,0 +1,141 @@ +import { useMemo } from 'react' +import { useSearchParams } from 'react-router-dom' + +import { + usePageAutoCorrect, + useSanitizedEventTypes, + useValidatedPage, + useValidatedPageSize +} from '@app/hooks' +import { type ShouldFilter, type TransactionEvent, useGetEventsQuery } from '@app/store/apis/events' +import type { + AddressFilter, + TransactionDirection +} from '../components/TransactionsOverview/filterUtils' +import { DIRECTION } from '../components/TransactionsOverview/filterUtils' +import { + type EventAmountFilter, + buildAmountFilter, + buildEventAddressFilter, + buildTickFilter, + buildTimestampRange, + type DateRangeValue, + parseAddressFilter, + parseAmountFilter, + parseDateRange, + parseTickRange +} from '../../utils/eventFilterUtils' + +export default function useAddressEvents(addressId: string): { + events: TransactionEvent[] + total: number + eventTypes: number[] + direction: TransactionDirection | undefined + tickStart: string | undefined + tickEnd: string | undefined + dateRange: DateRangeValue | undefined + sourceFilter: AddressFilter | undefined + destinationFilter: AddressFilter | undefined + amountFilter: EventAmountFilter | undefined + isLoading: boolean + hasError: boolean +} { + const [searchParams] = useSearchParams() + + const { start: tickStart, end: tickEnd } = parseTickRange(searchParams) + + const dateRange = parseDateRange(searchParams) + const sourceFilter = parseAddressFilter(searchParams, 'source', 'sourceMode') + const destinationFilter = parseAddressFilter(searchParams, 'destination', 'destMode') + + const page = useValidatedPage() + const pageSize = useValidatedPageSize() + const offset = (page - 1) * pageSize + + const eventTypes = useSanitizedEventTypes() + + const directionRaw = searchParams.get('direction') + const direction: TransactionDirection | undefined = + directionRaw === DIRECTION.INCOMING || directionRaw === DIRECTION.OUTGOING + ? directionRaw + : undefined + + const amountFilter = parseAmountFilter(searchParams) + + const { tickNumber, tickRange } = buildTickFilter(tickStart, tickEnd) + + const timestampRange = buildTimestampRange(dateRange) + + // When direction is set, the conflicting filter is ignored (disabled in UI). + const isIncoming = direction === DIRECTION.INCOMING + const isOutgoing = direction === DIRECTION.OUTGOING + + const effectiveSourceFilter = isOutgoing ? undefined : sourceFilter + const effectiveDestFilter = isIncoming ? undefined : destinationFilter + + const sourceResult = buildEventAddressFilter(effectiveSourceFilter) + const destResult = buildEventAddressFilter(effectiveDestFilter) + + // Use `should` (OR) to scope results to the page address when no explicit direction is set. + // When direction is set, implicit source/dest handles the scoping instead. + const useShouldFilter = !direction + + const addressShould = useMemo( + () => + useShouldFilter ? [{ terms: { source: addressId, destination: addressId } }] : undefined, + [addressId, useShouldFilter] + ) + + const { amountShould, ...amountParams } = useMemo( + () => buildAmountFilter(amountFilter), + [amountFilter] + ) + + // Merge address should (OR) with amount should (OR) when both are present + const should = useMemo(() => { + if (!addressShould && !amountShould) return undefined + return [...(addressShould ?? []), ...(amountShould ?? [])] + }, [addressShould, amountShould]) + + // When direction is set, implicit source/dest scopes to the page address. + // When direction is undefined, `should` (OR) handles scoping instead. + const implicitSource = isOutgoing ? addressId : undefined + const implicitDest = isIncoming ? addressId : undefined + + const { data, isFetching, isError } = useGetEventsQuery( + { + should, + tickNumber, + tickRange, + timestampRange, + offset, + size: pageSize, + logType: eventTypes.length > 0 ? eventTypes : undefined, + source: sourceResult.include ?? implicitSource, + excludeSource: sourceResult.exclude, + destination: destResult.include ?? implicitDest, + excludeDestination: destResult.exclude, + ...amountParams + }, + { skip: !addressId } + ) + + const total = data?.total ?? 0 + + usePageAutoCorrect(!!data, total, pageSize) + + return { + events: data?.events ?? [], + total, + eventTypes, + direction, + tickStart, + tickEnd, + dateRange, + sourceFilter, + destinationFilter, + amountFilter, + isLoading: isFetching, + hasError: isError + } +} diff --git a/src/pages/network/address/hooks/useLatestTransactions.ts b/src/pages/network/address/hooks/useLatestTransactions.ts index c57c6a1c..fae5b27f 100644 --- a/src/pages/network/address/hooks/useLatestTransactions.ts +++ b/src/pages/network/address/hooks/useLatestTransactions.ts @@ -5,9 +5,14 @@ import type { } from '@app/store/apis/query-service/query-service.types' import { useCallback, useEffect, useRef, useState } from 'react' import type { TransactionFilters } from '../components/TransactionsOverview/filterUtils' -import { extractErrorMessage } from '../components/TransactionsOverview/filterUtils' +import { + buildDestinationFilter, + buildSourceFilter, + extractErrorMessage, + getStartDateFromDays +} from '../components/TransactionsOverview/filterUtils' +import { buildRangeFilter } from '../../utils/filterUtils' -// Re-export types from filterUtils for backward compatibility export type { AddressFilter, AddressFilterMode, @@ -15,29 +20,11 @@ export type { TransactionFilters } from '../components/TransactionsOverview/filterUtils' -const PAGE_SIZE = 50 export const MAX_TRANSACTION_RESULTS = 10_000 // query service limit -// Helper function to calculate start date from preset days -// This is called at request time so the date is always fresh -// Returns full datetime format (YYYY-MM-DDTHH:mm:ss) to preserve time precision for presets like "Last hour" -const getStartDateFromPresetDays = (days: number): string => { - const now = new Date() - const start = new Date(now.getTime() - days * 24 * 60 * 60 * 1000) - const year = start.getFullYear() - const month = String(start.getMonth() + 1).padStart(2, '0') - const day = String(start.getDate()).padStart(2, '0') - const hours = String(start.getHours()).padStart(2, '0') - const minutes = String(start.getMinutes()).padStart(2, '0') - const seconds = String(start.getSeconds()).padStart(2, '0') - return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}` -} - export interface UseLatestTransactionsResult { transactions: QueryServiceTransaction[] totalCount: number | null - loadMoreTransactions: () => Promise - hasMore: boolean isLoading: boolean error: string | null applyFilters: (filters: TransactionFilters) => void @@ -45,22 +32,21 @@ export interface UseLatestTransactionsResult { activeFilters: TransactionFilters } -export default function useLatestTransactions(addressId: string): UseLatestTransactionsResult { +export default function useLatestTransactions( + addressId: string, + page: number, + pageSize: number +): UseLatestTransactionsResult { const [transactions, setTransactions] = useState([]) const [totalCount, setTotalCount] = useState(null) - const [offset, setOffset] = useState(0) const [isLoading, setIsLoading] = useState(false) const [activeFilters, setActiveFilters] = useState({}) const cancellationRef = useRef(false) - const [reachedEnd, setReachedEnd] = useState(false) - const [hasError, setHasError] = useState(false) const [getTransactionsForIdentity, { error }] = useGetTransactionsForIdentityMutation() - const hasMore = !reachedEnd && !hasError && offset < MAX_TRANSACTION_RESULTS - const fetchPage = useCallback( - async (currentOffset: number, filters: TransactionFilters = {}) => { + async (offset: number, size: number, filters: TransactionFilters = {}) => { // Clean up filters - remove empty strings and undefined values // Skip special handling fields: direction, ranges, and multi-address filters const cleanFilters = Object.entries(filters).reduce( @@ -84,33 +70,9 @@ export default function useLatestTransactions(addressId: string): UseLatestTrans {} as Record ) - // Handle multi-address source filter - if (filters.sourceFilter?.addresses && filters.sourceFilter.addresses.length > 0) { - const validAddresses = filters.sourceFilter.addresses.filter((addr) => addr.trim() !== '') - if (validAddresses.length > 0) { - const commaSeparated = validAddresses.join(',') - if (filters.sourceFilter.mode === 'exclude') { - cleanFilters['source-exclude'] = commaSeparated - } else { - cleanFilters.source = commaSeparated - } - } - } - - // Handle multi-address destination filter - if (filters.destinationFilter?.addresses && filters.destinationFilter.addresses.length > 0) { - const validAddresses = filters.destinationFilter.addresses.filter( - (addr) => addr.trim() !== '' - ) - if (validAddresses.length > 0) { - const commaSeparated = validAddresses.join(',') - if (filters.destinationFilter.mode === 'exclude') { - cleanFilters['destination-exclude'] = commaSeparated - } else { - cleanFilters.destination = commaSeparated - } - } - } + // Handle multi-address source/destination filters + Object.assign(cleanFilters, buildSourceFilter(filters.sourceFilter)) + Object.assign(cleanFilters, buildDestinationFilter(filters.destinationFilter)) // Handle direction filter - set source or destination based on direction // Only apply if the corresponding multi-address filter is not already set @@ -124,111 +86,43 @@ export default function useLatestTransactions(addressId: string): UseLatestTrans } } - // Handle amount range - if start equals end, use exact match - if ( - filters.amountRange?.start && - filters.amountRange?.end && - filters.amountRange.start.trim() === filters.amountRange.end.trim() - ) { - cleanFilters.amount = filters.amountRange.start.trim() - } - - // Handle inputType range - if start equals end, use exact match - if ( - filters.inputTypeRange?.start && - filters.inputTypeRange?.end && - filters.inputTypeRange.start.trim() === filters.inputTypeRange.end.trim() - ) { - cleanFilters.inputType = filters.inputTypeRange.start.trim() - } - - // Handle tickNumber range - if start equals end, use exact match - if ( - filters.tickNumberRange?.start && - filters.tickNumberRange?.end && - filters.tickNumberRange.start.trim() === filters.tickNumberRange.end.trim() - ) { - cleanFilters.tickNumber = filters.tickNumberRange.start.trim() - } - - // Check if amount range should be used (has values and is not an exact match) - const hasAmountRange = filters.amountRange?.start || filters.amountRange?.end - const isExactAmountMatch = - filters.amountRange?.start && - filters.amountRange?.end && - filters.amountRange.start.trim() === filters.amountRange.end.trim() - const shouldUseAmountRange = hasAmountRange && !isExactAmountMatch - - // Check if inputType range should be used (has values and is not an exact match) - const hasInputTypeRange = filters.inputTypeRange?.start || filters.inputTypeRange?.end - const isExactInputTypeMatch = - filters.inputTypeRange?.start && - filters.inputTypeRange?.end && - filters.inputTypeRange.start.trim() === filters.inputTypeRange.end.trim() - const shouldUseInputTypeRange = hasInputTypeRange && !isExactInputTypeMatch + // Build ranges for amount, inputType, and tickNumber using shared utility + const amountResult = buildRangeFilter(filters.amountRange?.start, filters.amountRange?.end) + const inputTypeResult = buildRangeFilter( + filters.inputTypeRange?.start, + filters.inputTypeRange?.end + ) + const tickNumberResult = buildRangeFilter( + filters.tickNumberRange?.start, + filters.tickNumberRange?.end + ) - // Check if tickNumber range should be used (has values and is not an exact match) - const hasTickNumberRange = filters.tickNumberRange?.start || filters.tickNumberRange?.end - const isExactTickNumberMatch = - filters.tickNumberRange?.start && - filters.tickNumberRange?.end && - filters.tickNumberRange.start.trim() === filters.tickNumberRange.end.trim() - const shouldUseTickNumberRange = hasTickNumberRange && !isExactTickNumberMatch + // Set exact match values as filters + if (amountResult.exactMatch) cleanFilters.amount = amountResult.exactMatch + if (inputTypeResult.exactMatch) cleanFilters.inputType = inputTypeResult.exactMatch + if (tickNumberResult.exactMatch) cleanFilters.tickNumber = tickNumberResult.exactMatch + + // Build timestamp range - recalculate from presetDays if set (so "Last 24 hours" is always fresh) + const timestampStart = + filters.dateRange?.presetDays !== undefined + ? getStartDateFromDays(filters.dateRange.presetDays) + : filters.dateRange?.start + const timestampRange = + timestampStart || filters.dateRange?.end + ? { + ...(timestampStart ? { gte: new Date(timestampStart).getTime().toString() } : {}), + ...(filters.dateRange?.end + ? { lte: new Date(filters.dateRange.end).getTime().toString() } + : {}) + } + : undefined + // Build ranges object const ranges = { - // Handle inputType range (when not exact match) - ...(shouldUseInputTypeRange && { - inputType: { - ...(filters.inputTypeRange?.start && filters.inputTypeRange.start.trim() !== '' - ? { gte: filters.inputTypeRange.start.trim() } - : {}), - ...(filters.inputTypeRange?.end && filters.inputTypeRange.end.trim() !== '' - ? { lte: filters.inputTypeRange.end.trim() } - : {}) - } - }), - // Handle amount range (when not exact match) - ...(shouldUseAmountRange && { - amount: { - ...(filters.amountRange?.start && filters.amountRange.start.trim() !== '' - ? { gte: filters.amountRange.start.trim() } - : {}), - ...(filters.amountRange?.end && filters.amountRange.end.trim() !== '' - ? { lte: filters.amountRange.end.trim() } - : {}) - } - }), - // Handle tickNumber range (when not exact match) - ...(shouldUseTickNumberRange && { - tickNumber: { - ...(filters.tickNumberRange?.start && filters.tickNumberRange.start.trim() !== '' - ? { gte: filters.tickNumberRange.start.trim() } - : {}), - ...(filters.tickNumberRange?.end && filters.tickNumberRange.end.trim() !== '' - ? { lte: filters.tickNumberRange.end.trim() } - : {}) - } - }), - // Handle date range - recalculate from presetDays if set (so "Last 24 hours" is always fresh) - ...(() => { - // If presetDays is set, calculate the start date now (at request time) - const startDate = - filters.dateRange?.presetDays !== undefined - ? getStartDateFromPresetDays(filters.dateRange.presetDays) - : filters.dateRange?.start - - if (startDate || filters.dateRange?.end) { - return { - timestamp: { - ...(startDate ? { gte: new Date(startDate).getTime().toString() } : {}), - ...(filters.dateRange?.end - ? { lte: new Date(filters.dateRange.end).getTime().toString() } - : {}) - } - } - } - return {} - })() + ...(amountResult.range && { amount: amountResult.range }), + ...(inputTypeResult.range && { inputType: inputTypeResult.range }), + ...(tickNumberResult.range && { tickNumber: tickNumberResult.range }), + ...(timestampRange && { timestamp: timestampRange }) } const result: QueryServiceResponse = await getTransactionsForIdentity({ @@ -236,8 +130,8 @@ export default function useLatestTransactions(addressId: string): UseLatestTrans filters: Object.keys(cleanFilters).length > 0 ? cleanFilters : undefined, ranges, pagination: { - offset: currentOffset, - size: PAGE_SIZE + offset, + size } }).unwrap() @@ -249,69 +143,26 @@ export default function useLatestTransactions(addressId: string): UseLatestTrans [getTransactionsForIdentity, addressId] ) - const loadMoreTransactions = useCallback(async () => { - if (isLoading || !hasMore) return - setIsLoading(true) - - try { - const { transactions: newTxs } = await fetchPage(offset, activeFilters) - if (!cancellationRef.current) { - if (newTxs.length > 0) { - setTransactions((prev) => [...prev, ...newTxs]) - setOffset((prev) => prev + PAGE_SIZE) - } - if (newTxs.length < PAGE_SIZE) { - setReachedEnd(true) - } - setHasError(false) - } - } catch (err) { - if (!cancellationRef.current) { - setHasError(true) - } - throw err - } finally { - if (!cancellationRef.current) { - setIsLoading(false) - } - } - }, [isLoading, hasMore, offset, fetchPage, activeFilters]) - const applyFilters = useCallback((filters: TransactionFilters) => { setActiveFilters(filters) - setTransactions([]) - setOffset(0) - setReachedEnd(false) - setHasError(false) }, []) const clearFilters = useCallback(() => { setActiveFilters({}) - setTransactions([]) - setOffset(0) - setReachedEnd(false) - setHasError(false) }, []) + // Fetch the requested page whenever page, pageSize, filters, or addressId change useEffect(() => { cancellationRef.current = false - setTransactions([]) - setTotalCount(null) - setOffset(0) - setReachedEnd(false) - setHasError(false) - const initialFetch = async () => { + const doFetch = async () => { setIsLoading(true) try { - const { transactions: firstPage, total } = await fetchPage(0, activeFilters) + const offset = (page - 1) * pageSize + const { transactions: txs, total } = await fetchPage(offset, pageSize, activeFilters) if (!cancellationRef.current) { - setTransactions(firstPage) + setTransactions(txs) setTotalCount(total) - setOffset(PAGE_SIZE) - if (firstPage.length < PAGE_SIZE) { - setReachedEnd(true) - } } } finally { if (!cancellationRef.current) { @@ -321,19 +172,17 @@ export default function useLatestTransactions(addressId: string): UseLatestTrans } if (addressId) { - initialFetch() + doFetch() } return () => { cancellationRef.current = true } - }, [addressId, fetchPage, activeFilters]) + }, [addressId, page, pageSize, fetchPage, activeFilters]) return { transactions, totalCount, - loadMoreTransactions, - hasMore, isLoading, error: extractErrorMessage(error), applyFilters, diff --git a/src/pages/network/assets/rich-list/AssetsRichListPage.tsx b/src/pages/network/assets/rich-list/AssetsRichListPage.tsx index b6e336a5..735047e4 100644 --- a/src/pages/network/assets/rich-list/AssetsRichListPage.tsx +++ b/src/pages/network/assets/rich-list/AssetsRichListPage.tsx @@ -1,45 +1,22 @@ -import type { TFunction } from 'i18next' -import { memo, useCallback, useEffect, useMemo } from 'react' +import { useCallback, useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useSearchParams } from 'react-router-dom' import { withHelmet } from '@app/components/hocs' -import { Breadcrumbs, PaginationBar, Select } from '@app/components/ui' +import { Breadcrumbs, PageSizeSelect, PaginationBar, TableErrorRow } from '@app/components/ui' import { PageLayout } from '@app/components/ui/layouts' -import type { Option } from '@app/components/ui/Select' -import { useTailwindBreakpoint } from '@app/hooks' +import { RICH_LIST_DEFAULT_PAGE_SIZE, VALID_PAGE_SIZES } from '@app/constants' +import { usePaginationSearchParams, useTailwindBreakpoint } from '@app/hooks' import { useGetAssetsIssuancesQuery } from '@app/store/apis/rpc-live' import { useGetAssetsRichListQuery } from '@app/store/apis/rpc-stats' -import { HomeLink } from '../../components' +import { HomeLink, RichListLoadingRows } from '../../components' import { AssetRichListEmptyRow, - AssetRichListErrorRow, AssetRichListInvalidAssetRow, AssetRichListRow, - AssetRichListSkeletonRow, AssetsTabs } from './components' -const DEFAULT_PAGE_SIZE = 15 -const VALID_PAGE_SIZES = [15, 30, 50, 100] as const - -const PAGE_SIZE_OPTIONS = VALID_PAGE_SIZES.map((size) => ({ - i18nKey: 'showItemsPerPage', - value: String(size) -})) - -const getSelectOptions = (t: TFunction) => - PAGE_SIZE_OPTIONS.map((option) => ({ - label: t(option.i18nKey, { count: parseInt(option.value, 10) }), - value: option.value - })) - -const RichListLoadingRows = memo(({ pageSize }: { pageSize: number }) => - Array.from({ length: pageSize }).map((_, index) => ( - - )) -) - function AssetsRichListPage() { const { t } = useTranslation('network-page') const { isMobile } = useTailwindBreakpoint() @@ -48,16 +25,13 @@ function AssetsRichListPage() { const issuerParam = searchParams.get('issuer') || '' const assetParam = searchParams.get('asset') || '' const page = parseInt(searchParams.get('page') || '1', 10) - const pageSizeParam = parseInt(searchParams.get('pageSize') ?? String(DEFAULT_PAGE_SIZE), 10) - const pageSize = VALID_PAGE_SIZES.includes(pageSizeParam as (typeof VALID_PAGE_SIZES)[number]) - ? pageSizeParam - : DEFAULT_PAGE_SIZE - - const pageSizeOptions = useMemo(() => getSelectOptions(t), [t]) - const defaultPageSizeOption = useMemo( - () => pageSizeOptions.find((option) => option.value === String(pageSize)), - [pageSizeOptions, pageSize] + const pageSizeParam = parseInt( + searchParams.get('pageSize') ?? String(RICH_LIST_DEFAULT_PAGE_SIZE), + 10 ) + const pageSize = VALID_PAGE_SIZES.includes(pageSizeParam) + ? pageSizeParam + : RICH_LIST_DEFAULT_PAGE_SIZE const { data: assetsData } = useGetAssetsIssuancesQuery() @@ -68,6 +42,8 @@ function AssetsRichListPage() { ) }, [issuerParam, assetParam, assetsData]) + const { handlePageChange, handlePageSizeChange } = usePaginationSearchParams() + const { data, isFetching, error } = useGetAssetsRichListQuery( { issuer: issuerParam, @@ -78,30 +54,6 @@ function AssetsRichListPage() { { skip: !issuerParam || !assetParam || !isValidAsset } ) - const handlePageChange = useCallback( - (value: number) => { - setSearchParams((prev) => ({ - ...Object.fromEntries(prev.entries()), - page: value.toString() - })) - }, - [setSearchParams] - ) - - const handlePageSizeChange = useCallback( - (option: Option) => { - setSearchParams( - (prev) => ({ - ...Object.fromEntries(prev.entries()), - pageSize: option.value, - page: '1' - }), - { replace: true } - ) - }, - [setSearchParams] - ) - const entitiesWithRank = useMemo( () => data?.owners.map((entity, index) => ({ @@ -117,8 +69,7 @@ function AssetsRichListPage() { const hasAsset = searchParams.has('asset') && searchParams.has('issuer') const hasPage = searchParams.has('page') const hasValidPageSize = - searchParams.has('pageSize') && - VALID_PAGE_SIZES.includes(pageSizeParam as (typeof VALID_PAGE_SIZES)[number]) + searchParams.has('pageSize') && VALID_PAGE_SIZES.includes(pageSizeParam) // Only set page/pageSize defaults if asset/issuer already exist if (hasAsset && (!hasPage || !hasValidPageSize)) { @@ -126,7 +77,7 @@ function AssetsRichListPage() { (prev) => ({ ...Object.fromEntries(prev.entries()), ...(!prev.has('page') && { page: '1' }), - ...(!prev.has('pageSize') && { pageSize: String(DEFAULT_PAGE_SIZE) }) + ...(!prev.has('pageSize') && { pageSize: String(RICH_LIST_DEFAULT_PAGE_SIZE) }) }), { replace: true } ) @@ -141,7 +92,7 @@ function AssetsRichListPage() { } if (error) { - return + return } if (entitiesWithRank?.length === 0) { @@ -151,7 +102,7 @@ function AssetsRichListPage() { return entitiesWithRank?.map((entity) => ( )) - }, [entitiesWithRank, isFetching, error, isMobile, pageSize, isValidAsset]) + }, [entitiesWithRank, isFetching, error, isMobile, pageSize, isValidAsset, t]) return ( @@ -168,20 +119,18 @@ function AssetsRichListPage() {
    -
    +
    - handleChange({ assetType: e.target.value as AmountAssetType })} + className="w-full rounded bg-primary-60 px-10 py-6 text-xs text-white focus:outline-none focus:ring-1 focus:ring-primary-30" + > + {ASSET_TYPE_OPTIONS.map((option) => ( + + ))} + +
    + +
    + + {/* Range inputs */} +
    +
    +
    + + + handleChange({ min: parseNumericInput(e.target.value) || undefined }) + } + className="w-full rounded bg-primary-60 px-10 py-6 text-right text-base text-white placeholder-gray-50 focus:outline-none focus:ring-1 focus:ring-primary-30 md:text-xs" + /> +
    +
    + + + handleChange({ max: parseNumericInput(e.target.value) || undefined }) + } + className="w-full rounded bg-primary-60 px-10 py-6 text-right text-base text-white placeholder-gray-50 focus:outline-none focus:ring-1 focus:ring-primary-30 md:text-xs" + /> +
    +
    + {error &&

    {error}

    } + {showApplyButton && ( + + )} +
    +
    + ) +} diff --git a/src/pages/network/components/filters/EventTypeChips.tsx b/src/pages/network/components/filters/EventTypeChips.tsx new file mode 100644 index 00000000..8e441e6c --- /dev/null +++ b/src/pages/network/components/filters/EventTypeChips.tsx @@ -0,0 +1,51 @@ +import { useTranslation } from 'react-i18next' + +import { + EVENT_TYPE_FILTER_OPTIONS, + MAX_EVENT_TYPE_SELECTIONS, + getEventTypeLabel +} from '@app/store/apis/events' +import { clsxTwMerge } from '@app/utils' + +type Props = { + selectedTypes: number[] + onToggle: (type: number) => void + className?: string +} + +function getChipClassName(isSelected: boolean, isDisabled: boolean): string { + if (isSelected) return 'border-primary-30 bg-primary-60 text-primary-30' + if (isDisabled) return 'cursor-not-allowed border-primary-60/50 text-gray-60' + return 'border-primary-60 text-gray-50 hover:border-primary-50 hover:text-white' +} + +export default function EventTypeChips({ selectedTypes, onToggle, className }: Props) { + const { t } = useTranslation('network-page') + const isAtMax = selectedTypes.length >= MAX_EVENT_TYPE_SELECTIONS + + return ( +
    +

    + {t('selectUpTo', { count: MAX_EVENT_TYPE_SELECTIONS })} +

    +
    + {EVENT_TYPE_FILTER_OPTIONS.map((type) => { + const isSelected = selectedTypes.includes(type) + const isDisabled = !isSelected && isAtMax + + return ( + + ) + })} +
    +
    + ) +} diff --git a/src/pages/network/components/filters/EventsFilterBar.tsx b/src/pages/network/components/filters/EventsFilterBar.tsx new file mode 100644 index 00000000..d326533d --- /dev/null +++ b/src/pages/network/components/filters/EventsFilterBar.tsx @@ -0,0 +1,286 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' + +import { FunnelIcon } from '@app/assets/icons' +import { getEventTypeLabel } from '@app/store/apis/events' +import type { + AddressFilter, + TransactionDirection +} from '../../address/components/TransactionsOverview/filterUtils' +import DirectionControl from '../../address/components/TransactionsOverview/DirectionControl' +import DateFilterContent from '../../address/components/TransactionsOverview/DateFilterContent' +import MultiAddressFilterContent from '../../address/components/TransactionsOverview/MultiAddressFilterContent' +import { formatRangeLabel } from '../../hooks' +import type { EventFiltersResult } from '../../hooks/useEventFilters' +import type { DateRangeValue, EventAmountFilter } from '../../utils/eventFilterUtils' +import { getAddressFilterLabel, getDateRangeLabel } from '../../utils/eventFilterUtils' +import { formatAmountForDisplay } from '../../utils/filterUtils' +import ActiveFilterChip from './ActiveFilterChip' +import EventAmountFilterContent from './EventAmountFilterContent' +import EventsMobileFiltersModal, { type EventsFilters } from './EventsMobileFiltersModal' +import EventTypeChips from './EventTypeChips' +import FilterDropdown from './FilterDropdown' +import MobileFiltersButton from './MobileFiltersButton' +import RangeFilterContent from './RangeFilterContent' +import ResetFiltersButton from './ResetFiltersButton' + +type Props = { + filters: EventFiltersResult + eventTypes: number[] + direction?: TransactionDirection | undefined + tickStart?: string + tickEnd?: string + dateRange?: DateRangeValue + sourceFilter?: AddressFilter + destinationFilter?: AddressFilter + amountFilter?: EventAmountFilter + idPrefix: string + showTickFilter?: boolean + showDateFilter?: boolean + showDirectionFilter?: boolean + addressId?: string +} + +function getEventTypesLabel(eventTypes: number[], fallback: string): string { + if (eventTypes.length === 0) return fallback + if (eventTypes.length === 1) return getEventTypeLabel(eventTypes[0]) + return `${getEventTypeLabel(eventTypes[0])} +${eventTypes.length - 1}` +} + +export default function EventsFilterBar({ + filters, + eventTypes, + direction, + tickStart, + tickEnd, + dateRange, + sourceFilter, + destinationFilter, + amountFilter, + idPrefix, + showTickFilter = true, + showDateFilter = true, + showDirectionFilter = false, + addressId +}: Props) { + const { t } = useTranslation('network-page') + const [openDropdown, setOpenDropdown] = useState(null) + const [isMobileModalOpen, setIsMobileModalOpen] = useState(false) + + const tickLabel = showTickFilter + ? formatRangeLabel( + t('tick'), + tickStart || tickEnd ? { start: tickStart, end: tickEnd } : undefined, + formatAmountForDisplay + ) + : '' + const eventTypeLabel = getEventTypesLabel(eventTypes, t('eventType')) + const dateLabel = showDateFilter ? getDateRangeLabel(dateRange, t) : '' + const sourceLabel = getAddressFilterLabel('source', sourceFilter, t) + const destLabel = getAddressFilterLabel('destination', destinationFilter, t) + + const handleToggleDropdown = (key: string) => { + setOpenDropdown((prev) => (prev === key ? null : key)) + } + + const amountLabel = formatRangeLabel( + t('amount'), + amountFilter ? { start: amountFilter.min, end: amountFilter.max } : undefined, + formatAmountForDisplay + ) + + const mobileActiveFilters: EventsFilters = { + eventTypes, + direction, + sourceFilter, + destinationFilter, + amountFilter: filters.localAmountFilter, + ...(showTickFilter && { + tickRange: tickStart || tickEnd ? { start: tickStart, end: tickEnd } : undefined + }), + ...(showDateFilter && { dateRange }) + } + + return ( + <> + {/* Mobile: Filters button + active filter chips */} +
    +
    + setIsMobileModalOpen(true)} /> +
    + + {filters.hasActiveFilters && ( +
    + {eventTypes.length > 0 && ( + + )} + {filters.isSourceActive && ( + + )} + {filters.isDestActive && ( + + )} + {showDateFilter && filters.isDateActive && ( + + )} + {filters.isAmountActive && ( + + )} + {showTickFilter && filters.isTickActive && ( + + )} + +
    + )} +
    + + {/* Mobile Modal */} + setIsMobileModalOpen(false)} + activeFilters={mobileActiveFilters} + onApplyFilters={filters.handleMobileApplyFilters} + idPrefix={idPrefix} + showTickFilter={showTickFilter} + showDateFilter={showDateFilter} + showDirectionFilter={showDirectionFilter} + addressId={addressId} + /> + + {/* Desktop: Dropdown filters */} +
    + + {showDirectionFilter && ( + + )} + handleToggleDropdown('eventType')} + onClear={filters.isEventTypeActive ? filters.handleClearEventType : undefined} + > + + + handleToggleDropdown('source')} + onClear={filters.isSourceActive ? filters.handleClearSource : undefined} + > + { + filters.handleSourceApply() + setOpenDropdown(null) + }} + /> + + handleToggleDropdown('destination')} + onClear={filters.isDestActive ? filters.handleClearDest : undefined} + > + { + filters.handleDestApply() + setOpenDropdown(null) + }} + /> + + {showDateFilter && ( + handleToggleDropdown('date')} + onClear={filters.isDateActive ? filters.handleClearDate : undefined} + contentClassName="min-w-[280px]" + allowFullWidth + > + { + if (filters.handleDateRangeApply()) setOpenDropdown(null) + }} + onPresetSelect={(days) => { + filters.handleDatePresetSelect(days) + setOpenDropdown(null) + }} + selectedPresetDays={dateRange?.presetDays} + error={filters.dateRangeError ? t(filters.dateRangeError) : null} + /> + + )} + {showTickFilter && ( + handleToggleDropdown('tick')} + onClear={filters.isTickActive ? filters.handleClearTick : undefined} + > + { + if (filters.handleTickRangeApply()) setOpenDropdown(null) + }} + startLabel={t('startTick')} + endLabel={t('endTick')} + error={filters.tickRangeError ? t(filters.tickRangeError) : null} + formatDisplay={false} + /> + + )} + handleToggleDropdown('amount')} + onClear={filters.isAmountActive ? filters.handleClearAmount : undefined} + contentClassName="min-w-[280px]" + allowFullWidth + > + { + if (filters.handleAmountApply()) setOpenDropdown(null) + }} + error={filters.amountFilterError ? t(filters.amountFilterError) : null} + /> + + {filters.hasActiveFilters && ( + <> +
    + + + )} +
    + + ) +} diff --git a/src/pages/network/components/filters/EventsMobileFiltersModal.tsx b/src/pages/network/components/filters/EventsMobileFiltersModal.tsx new file mode 100644 index 00000000..3d938baf --- /dev/null +++ b/src/pages/network/components/filters/EventsMobileFiltersModal.tsx @@ -0,0 +1,272 @@ +import { useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' + +import { useBodyScrollLock } from '@app/hooks' +import { MAX_EVENT_TYPE_SELECTIONS } from '@app/store/apis/events' +import DateFilterContent from '../../address/components/TransactionsOverview/DateFilterContent' +import { + validateAddressFilter, + type AddressFilter, + type TransactionDirection +} from '../../address/components/TransactionsOverview/filterUtils' +import DirectionControl from '../../address/components/TransactionsOverview/DirectionControl' +import MultiAddressFilterContent from '../../address/components/TransactionsOverview/MultiAddressFilterContent' +import { scrollToValidationError } from '../../hooks' +import type { + DateRangeValue, + EventAmountFilter, + TickRangeValue +} from '../../utils/eventFilterUtils' +import { applyEventDirectionSync } from '../../utils/eventFilterUtils' +import { validateAmountRange } from '../../utils/filterUtils' +import EventAmountFilterContent from './EventAmountFilterContent' +import EventTypeChips from './EventTypeChips' +import MobileFiltersModalWrapper from './MobileFiltersModalWrapper' +import MobileFilterSection from './MobileFilterSection' +import RangeFilterContent from './RangeFilterContent' + +export type EventsFilters = { + tickRange?: TickRangeValue + eventTypes?: number[] + dateRange?: DateRangeValue + sourceFilter?: AddressFilter + destinationFilter?: AddressFilter + direction?: TransactionDirection + amountFilter?: EventAmountFilter +} + +type Props = { + isOpen: boolean + onClose: () => void + activeFilters: EventsFilters + onApplyFilters: (filters: EventsFilters) => void + idPrefix: string + showTickFilter?: boolean + showDateFilter?: boolean + showDirectionFilter?: boolean + addressId?: string +} + +export default function EventsMobileFiltersModal({ + isOpen, + onClose, + activeFilters, + onApplyFilters, + idPrefix, + showTickFilter = true, + showDateFilter = true, + showDirectionFilter = false, + addressId +}: Props) { + const { t } = useTranslation('network-page') + + useBodyScrollLock(isOpen) + + const [localTickRange, setLocalTickRange] = useState( + activeFilters.tickRange + ) + const [localEventTypes, setLocalEventTypes] = useState(activeFilters.eventTypes ?? []) + const [localDateRange, setLocalDateRange] = useState( + activeFilters.dateRange + ) + const [localSourceFilter, setLocalSourceFilter] = useState( + activeFilters.sourceFilter + ) + const [localDestFilter, setLocalDestFilter] = useState( + activeFilters.destinationFilter + ) + const [localDirection, setLocalDirection] = useState( + activeFilters.direction + ) + const [localAmountFilter, setLocalAmountFilter] = useState( + activeFilters.amountFilter + ) + const [validationErrors, setValidationErrors] = useState>({}) + + const handleLocalDirectionChange = useCallback( + (newDirection: TransactionDirection | undefined) => { + setLocalDirection(newDirection) + if (!addressId) return + const { sourceFilter: newSrc, destinationFilter: newDest } = applyEventDirectionSync( + newDirection, + addressId, + localSourceFilter, + localDestFilter + ) + setLocalSourceFilter(newSrc) + setLocalDestFilter(newDest) + setValidationErrors({}) + }, + [addressId, localSourceFilter, localDestFilter] + ) + + const handleToggleEventType = useCallback((type: number) => { + setLocalEventTypes((prev) => { + if (prev.includes(type)) return prev.filter((v) => v !== type) + if (prev.length >= MAX_EVENT_TYPE_SELECTIONS) return prev + return [...prev, type] + }) + }, []) + + useEffect(() => { + if (isOpen) { + setLocalTickRange(activeFilters.tickRange) + setLocalEventTypes(activeFilters.eventTypes ?? []) + setLocalDateRange(activeFilters.dateRange) + setLocalSourceFilter(activeFilters.sourceFilter) + setLocalDestFilter(activeFilters.destinationFilter) + setLocalDirection(activeFilters.direction) + setLocalAmountFilter(activeFilters.amountFilter) + setValidationErrors({}) + } + }, [ + isOpen, + activeFilters.tickRange, + activeFilters.eventTypes, + activeFilters.dateRange, + activeFilters.sourceFilter, + activeFilters.destinationFilter, + activeFilters.direction, + activeFilters.amountFilter + ]) + + const handleApply = useCallback(() => { + const errors: Record = {} + let firstErrorId: string | null = null + + const sourceError = validateAddressFilter(localSourceFilter) + if (sourceError) { + errors.source = t(sourceError) + if (!firstErrorId) firstErrorId = `${idPrefix}-mobile-source-filter` + } + + const destError = validateAddressFilter(localDestFilter) + if (destError) { + errors.destination = t(destError) + if (!firstErrorId) firstErrorId = `${idPrefix}-mobile-dest-filter` + } + + const amountError = validateAmountRange(localAmountFilter?.min, localAmountFilter?.max) + if (amountError) { + errors.amount = t(amountError) + if (!firstErrorId) firstErrorId = `${idPrefix}-mobile-amount-filter` + } + + if (Object.keys(errors).length > 0) { + setValidationErrors(errors) + scrollToValidationError(firstErrorId) + return + } + + onApplyFilters({ + tickRange: localTickRange, + eventTypes: localEventTypes, + dateRange: localDateRange, + sourceFilter: localSourceFilter, + destinationFilter: localDestFilter, + direction: localDirection, + amountFilter: localAmountFilter + }) + onClose() + setValidationErrors({}) + }, [ + localTickRange, + localEventTypes, + localDateRange, + localSourceFilter, + localDestFilter, + localDirection, + localAmountFilter, + onApplyFilters, + onClose, + idPrefix, + t + ]) + + const handleClose = useCallback(() => { + onClose() + setValidationErrors({}) + }, [onClose]) + + return ( + + {showDirectionFilter && ( + + + + )} + + + + + + + {}} + showApplyButton={false} + error={validationErrors.source} + /> + + + + {}} + showApplyButton={false} + error={validationErrors.destination} + /> + + + + {}} + showApplyButton={false} + layout="horizontal" + error={validationErrors.amount} + /> + + + {showDateFilter && ( + + {}} + selectedPresetDays={localDateRange?.presetDays} + showApplyButton={false} + /> + + )} + + {showTickFilter && ( + + {}} + startLabel={t('startTick')} + endLabel={t('endTick')} + showApplyButton={false} + layout="horizontal" + formatDisplay={false} + /> + + )} + + ) +} diff --git a/src/pages/network/components/filters/FilterDropdown.tsx b/src/pages/network/components/filters/FilterDropdown.tsx index 17352fe6..95ce55c8 100644 --- a/src/pages/network/components/filters/FilterDropdown.tsx +++ b/src/pages/network/components/filters/FilterDropdown.tsx @@ -15,6 +15,7 @@ type Props = { onClear?: () => void contentClassName?: string allowFullWidth?: boolean + disabled?: boolean } export default function FilterDropdown({ @@ -25,7 +26,8 @@ export default function FilterDropdown({ onToggle, onClear, contentClassName, - allowFullWidth = false + allowFullWidth = false, + disabled = false }: Props) { const wrapperRef = useRef(null) const triggerRef = useRef(null) @@ -97,18 +99,25 @@ export default function FilterDropdown({ } } + const handleToggle = disabled ? () => {} : onToggle + return (
    - +
    @@ -89,7 +89,7 @@ export default function RangeFilterContent({ type="text" inputMode="numeric" value={displayEnd} - onChange={(e) => handleEndChange(parseAmountFromDisplay(e.target.value))} + onChange={(e) => handleEndChange(parseNumericInput(e.target.value))} className="w-full rounded bg-primary-60 px-10 py-6 text-right text-base text-white placeholder-gray-50 focus:outline-none focus:ring-1 focus:ring-primary-30 md:text-xs" />
    diff --git a/src/pages/network/components/filters/index.ts b/src/pages/network/components/filters/index.ts index ea5c1f39..f75c865f 100644 --- a/src/pages/network/components/filters/index.ts +++ b/src/pages/network/components/filters/index.ts @@ -1,4 +1,7 @@ export { default as ActiveFilterChip } from './ActiveFilterChip' +export { default as EventsFilterBar } from './EventsFilterBar' +export { default as EventsMobileFiltersModal } from './EventsMobileFiltersModal' +export { default as EventTypeChips } from './EventTypeChips' export { default as AmountFilterContent } from './AmountFilterContent' export { default as FilterDropdown } from './FilterDropdown' export { default as MobileAmountFilterSection } from './MobileAmountFilterSection' diff --git a/src/pages/network/components/index.ts b/src/pages/network/components/index.ts index f1075229..11b9afe5 100644 --- a/src/pages/network/components/index.ts +++ b/src/pages/network/components/index.ts @@ -1,10 +1,15 @@ export { default as AddressLink } from './AddressLink' export { default as CardItem } from './CardItem' +export { default as EventLink } from './EventLink' export { default as HomeLink } from './HomeLink' +export { default as RichListLoadingRows } from './RichListSkeletonRow' export { default as SubCardItem } from './SubCardItem' export { default as TickLink } from './TickLink' export { default as TickStatus } from './TickStatus' +export { default as TransactionRow } from './TransactionRow' +export { default as TransactionSkeletonRow } from './TransactionSkeletonRow' export { default as TxItem } from './TxItem/TxItem' export { default as TxLink } from './TxLink' +export { default as VirtualTxLink } from './VirtualTxLink' export { default as TxStatus } from './TxStatus' export { default as WaitingForTick } from './WaitingForTick' diff --git a/src/pages/network/event-detail/EventDetailPage.tsx b/src/pages/network/event-detail/EventDetailPage.tsx new file mode 100644 index 00000000..80732a8f --- /dev/null +++ b/src/pages/network/event-detail/EventDetailPage.tsx @@ -0,0 +1,255 @@ +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { useParams } from 'react-router-dom' + +import { withHelmet } from '@app/components/hocs' +import { Badge, Breadcrumbs } from '@app/components/ui' +import { ErrorFallback } from '@app/components/ui/error-boundaries' +import { PageLayout } from '@app/components/ui/layouts' +import { LinearProgress } from '@app/components/ui/loaders' +import { useGetAddressName, useGetSmartContractByIndex } from '@app/hooks' +import { useGetEventsQuery, getEventTypeLabel } from '@app/store/apis/events' +import { formatDate, formatString } from '@app/utils' +import { AddressLink, HomeLink, SubCardItem, TickLink, TxLink, VirtualTxLink } from '../components' + +function EventDetailPage() { + const { t } = useTranslation('network-page') + const { tickNumber: tickParam = '', logId: logIdParam = '' } = useParams() + + const tickNumber = Number(tickParam) + const logId = Number(logIdParam) + const isValidParams = + Number.isFinite(tickNumber) && tickNumber > 0 && Number.isFinite(logId) && logId >= 0 + + const { data, isFetching, isError } = useGetEventsQuery( + { tickNumber, logId, size: 1 }, + { skip: !isValidParams } + ) + + const event = data?.events?.[0] + + const sourceAddressName = useGetAddressName(event?.source ?? '') + const destinationAddressName = useGetAddressName(event?.destination ?? '') + const issuerAddressName = useGetAddressName(event?.assetIssuer ?? '') + + const contract = useGetSmartContractByIndex( + event?.contractIndex && event.contractIndex > 0 ? event.contractIndex : undefined + ) + const managingContract = useGetSmartContractByIndex(event?.managingContractIndex) + + const { date, time } = useMemo( + () => + formatDate(event?.timestamp !== undefined ? String(event.timestamp) : undefined, { + split: true + }), + [event?.timestamp] + ) + + if (isFetching) return + if (isError) return + if (!event) return + + return ( + + + +

    + {t('tick')} +

    +

    + {t('event')} {event.logId} +

    +
    + +

    {t('eventDetails')}

    + +
    + {event.logId}

    } + hideTopBorder + /> + {event.epoch}

    } + /> + } + /> + + {date}{' '} + {time} +

    + } + /> + + {getEventTypeLabel(event.type)} + + } + /> + + ) : ( + + ) + } + /> + {event.contractIndex > 0 && ( + + {event.contractIndex} + {contract && ({contract.name})} +

    + } + /> + )} + {event.contractMessageType !== undefined && ( + {event.contractMessageType}

    } + /> + )} + {event.logDigest}

    } + /> + + {event.source && ( + + } + /> + )} + {event.destination && ( + + } + /> + )} + {event.amount !== undefined && ( + + {formatString(event.amount)}{' '} + {event.assetName ?? 'QUBIC'} +

    + } + /> + )} + + {event.assetIssuer && ( + + } + /> + )} + {event.managingContractIndex !== undefined && ( + + {event.managingContractIndex} + {managingContract && ( + ({managingContract.name}) + )} +

    + } + /> + )} + {event.unitOfMeasurement && ( + {event.unitOfMeasurement}

    } + /> + )} + {event.numberOfDecimalPlaces !== undefined && ( + {event.numberOfDecimalPlaces}

    } + /> + )} + {event.value && ( + {event.value}

    } + /> + )} + {event.deductedAmount !== undefined && ( + {formatString(event.deductedAmount)} QUBIC

    + } + /> + )} + {event.remainingAmount !== undefined && ( + {formatString(event.remainingAmount)} QUBIC

    + } + /> + )} +
    +
    + ) +} + +const EventDetailPageWithHelmet = withHelmet(EventDetailPage, { + title: 'Event | Qubic Explorer' +}) + +export default EventDetailPageWithHelmet diff --git a/src/pages/network/hooks/index.ts b/src/pages/network/hooks/index.ts index 4683197a..4357290d 100644 --- a/src/pages/network/hooks/index.ts +++ b/src/pages/network/hooks/index.ts @@ -1,8 +1,12 @@ export { formatRangeLabel, + getAmountRangeLabel, scrollToValidationError, useAmountPresetHandler, useClearFilterHandler, useLocalFilterSync } from './useFilterHelpers' +export { default as useEventFilters } from './useEventFilters' +export type { EventFiltersResult } from './useEventFilters' export { default as useTickWatcher } from './useTickWatcher' +export { default as useTransactionEvents } from './useTransactionEvents' diff --git a/src/pages/network/hooks/useEventFilters.ts b/src/pages/network/hooks/useEventFilters.ts new file mode 100644 index 00000000..f6941545 --- /dev/null +++ b/src/pages/network/hooks/useEventFilters.ts @@ -0,0 +1,488 @@ +import { useCallback, useState } from 'react' +import { useSearchParams } from 'react-router-dom' + +import { EVENT_TYPE_FILTER_OPTIONS, MAX_EVENT_TYPE_SELECTIONS } from '@app/store/apis/events' +import type { + AddressFilter, + TransactionDirection +} from '../address/components/TransactionsOverview/filterUtils' +import { + DIRECTION, + isOnlyPageAddress, + validateDateRange, + validateTickRange +} from '../address/components/TransactionsOverview/filterUtils' +import type { DateRangeValue, EventAmountFilter, TickRangeValue } from '../utils/eventFilterUtils' +import { + amountFilterToParams, + applyEventDirectionSync, + toTickRangeValue +} from '../utils/eventFilterUtils' +import { updateSearchParams, validateAmountRange } from '../utils/filterUtils' + +type EventFilterOptions = { + tickStart?: string + tickEnd?: string + eventTypes: number[] + direction?: TransactionDirection | undefined + dateRange?: DateRangeValue + sourceFilter: AddressFilter | undefined + destinationFilter: AddressFilter | undefined + amountFilter?: EventAmountFilter + supportsTick?: boolean + supportsDate?: boolean + addressId?: string +} + +export type EventFiltersResult = ReturnType + +export default function useEventFilters({ + tickStart, + tickEnd, + eventTypes, + direction, + dateRange, + sourceFilter, + destinationFilter, + amountFilter, + supportsTick = true, + supportsDate = true, + addressId +}: EventFilterOptions) { + const [, setSearchParams] = useSearchParams() + + // Local state for tick range inputs + const [tickRange, setTickRange] = useState( + toTickRangeValue(tickStart, tickEnd) + ) + const [tickRangeError, setTickRangeError] = useState(null) + + // Local state for date range + const [localDateRange, setLocalDateRange] = useState(dateRange) + const [dateRangeError, setDateRangeError] = useState(null) + + // Local state for source/destination filters + const [localSourceFilter, setLocalSourceFilter] = useState( + sourceFilter + ) + const [localDestFilter, setLocalDestFilter] = useState( + destinationFilter + ) + + // Local state for amount filter + const [localAmountFilter, setLocalAmountFilter] = useState( + amountFilter + ) + const [amountFilterError, setAmountFilterError] = useState(null) + + const checkIsPageAddress = useCallback( + (filter: AddressFilter | undefined): boolean => + !!addressId && filter?.mode === 'include' && isOnlyPageAddress(filter, addressId), + [addressId] + ) + + const isTickActive = supportsTick && (tickStart !== undefined || tickEnd !== undefined) + const isEventTypeActive = eventTypes.length > 0 + const isDateActive = supportsDate && dateRange !== undefined + const isSourceActive = sourceFilter !== undefined + const isDestActive = destinationFilter !== undefined + const isAmountActive = + amountFilter !== undefined && (amountFilter.min !== undefined || amountFilter.max !== undefined) + const hasActiveFilters = + isTickActive || + isEventTypeActive || + isDateActive || + isSourceActive || + isDestActive || + isAmountActive + + // --- Tick Range --- + + const handleTickRangeChange = useCallback((value: TickRangeValue | undefined) => { + setTickRange(value) + setTickRangeError(null) + }, []) + + const handleTickRangeApply = useCallback((): boolean => { + const error = validateTickRange(tickRange?.start, tickRange?.end) + if (error) { + setTickRangeError(error) + return false + } + setTickRangeError(null) + setSearchParams((prev) => + updateSearchParams(prev, { + tickStart: tickRange?.start || undefined, + tickEnd: tickRange?.end || undefined + }) + ) + return true + }, [tickRange, setSearchParams]) + + const handleClearTick = useCallback(() => { + setSearchParams((prev) => + updateSearchParams(prev, { tickStart: undefined, tickEnd: undefined }) + ) + setTickRange(undefined) + setTickRangeError(null) + }, [setSearchParams]) + + // --- Event Type --- + + const handleToggleEventType = useCallback( + (type: number) => { + setSearchParams((prev) => { + const current = prev.get('eventType') + const types = current + ? current + .split(',') + .filter((s) => s !== '') + .map(Number) + .filter( + (n) => + !Number.isNaN(n) && (EVENT_TYPE_FILTER_OPTIONS as readonly number[]).includes(n) + ) + : [] + const index = types.indexOf(type) + let next: number[] + if (index >= 0) { + next = types.filter((t) => t !== type) + } else if (types.length < MAX_EVENT_TYPE_SELECTIONS) { + next = [...types, type] + } else { + return prev + } + return updateSearchParams(prev, { + eventType: next.length > 0 ? next.join(',') : undefined + }) + }) + }, + [setSearchParams] + ) + + const handleClearEventType = useCallback(() => { + setSearchParams((prev) => updateSearchParams(prev, { eventType: undefined })) + }, [setSearchParams]) + + // --- Direction --- + + const handleDirectionChange = useCallback( + (newDirection: TransactionDirection | undefined) => { + const updates: Record = { + direction: newDirection + } + if (addressId) { + const { sourceFilter: newSrc, destinationFilter: newDest } = applyEventDirectionSync( + newDirection, + addressId, + sourceFilter, + destinationFilter + ) + // Sync URL params + const srcAddresses = newSrc?.addresses.filter((a) => a.trim() !== '') ?? [] + updates.source = srcAddresses.length > 0 ? srcAddresses.join(',') : undefined + updates.sourceMode = srcAddresses.length > 0 ? newSrc?.mode : undefined + const dstAddresses = newDest?.addresses.filter((a) => a.trim() !== '') ?? [] + updates.destination = dstAddresses.length > 0 ? dstAddresses.join(',') : undefined + updates.destMode = dstAddresses.length > 0 ? newDest?.mode : undefined + // Sync local state + setLocalSourceFilter(newSrc) + setLocalDestFilter(newDest) + } + setSearchParams((prev) => updateSearchParams(prev, updates)) + }, + [setSearchParams, addressId, sourceFilter, destinationFilter] + ) + + // --- Date Range --- + + const handleDateRangeChange = useCallback((value: DateRangeValue | undefined) => { + setLocalDateRange(value) + setDateRangeError(null) + }, []) + + const handleDateRangeApply = useCallback((): boolean => { + const error = validateDateRange(localDateRange?.start, localDateRange?.end) + if (error) { + setDateRangeError(error) + return false + } + setDateRangeError(null) + setSearchParams((prev) => + updateSearchParams(prev, { + dateStart: localDateRange?.start || undefined, + dateEnd: localDateRange?.end || undefined, + datePresetDays: undefined + }) + ) + return true + }, [localDateRange, setSearchParams]) + + const handleDatePresetSelect = useCallback( + (presetDays: number) => { + setDateRangeError(null) + setLocalDateRange({ presetDays }) + setSearchParams((prev) => + updateSearchParams(prev, { + dateStart: undefined, + dateEnd: undefined, + datePresetDays: String(presetDays) + }) + ) + }, + [setSearchParams] + ) + + const handleClearDate = useCallback(() => { + setSearchParams((prev) => + updateSearchParams(prev, { + dateStart: undefined, + dateEnd: undefined, + datePresetDays: undefined + }) + ) + setLocalDateRange(undefined) + setDateRangeError(null) + }, [setSearchParams]) + + // --- Source --- + + const handleSourceApply = useCallback(() => { + const validAddresses = localSourceFilter?.addresses.filter((addr) => addr.trim() !== '') ?? [] + if (validAddresses.length === 0) return + const updates: Record = { + source: validAddresses.join(','), + sourceMode: localSourceFilter?.mode ?? 'include' + } + if (checkIsPageAddress(localSourceFilter)) { + updates.direction = DIRECTION.OUTGOING + } else if (direction === DIRECTION.OUTGOING) { + updates.direction = undefined + } + setSearchParams((prev) => updateSearchParams(prev, updates)) + }, [localSourceFilter, setSearchParams, checkIsPageAddress, direction]) + + const handleClearSource = useCallback(() => { + const updates: Record = { + source: undefined, + sourceMode: undefined + } + // Clear direction if it was auto-synced from source + if (direction === DIRECTION.OUTGOING) { + updates.direction = undefined + } + setSearchParams((prev) => updateSearchParams(prev, updates)) + setLocalSourceFilter(undefined) + }, [setSearchParams, direction]) + + // --- Destination --- + + const handleDestApply = useCallback(() => { + const validAddresses = localDestFilter?.addresses.filter((addr) => addr.trim() !== '') ?? [] + if (validAddresses.length === 0) return + const updates: Record = { + destination: validAddresses.join(','), + destMode: localDestFilter?.mode ?? 'include' + } + if (checkIsPageAddress(localDestFilter)) { + updates.direction = DIRECTION.INCOMING + } else if (direction === DIRECTION.INCOMING) { + updates.direction = undefined + } + setSearchParams((prev) => updateSearchParams(prev, updates)) + }, [localDestFilter, setSearchParams, checkIsPageAddress, direction]) + + const handleClearDest = useCallback(() => { + const updates: Record = { + destination: undefined, + destMode: undefined + } + // Clear direction if it was auto-synced from destination + if (direction === DIRECTION.INCOMING) { + updates.direction = undefined + } + setSearchParams((prev) => updateSearchParams(prev, updates)) + setLocalDestFilter(undefined) + }, [setSearchParams, direction]) + + // --- Amount --- + + const handleAmountChange = useCallback((value: EventAmountFilter | undefined) => { + setLocalAmountFilter(value) + setAmountFilterError(null) + }, []) + + const handleAmountApply = useCallback((): boolean => { + const error = validateAmountRange(localAmountFilter?.min, localAmountFilter?.max) + if (error) { + setAmountFilterError(error) + return false + } + setAmountFilterError(null) + setSearchParams((prev) => updateSearchParams(prev, amountFilterToParams(localAmountFilter))) + return true + }, [localAmountFilter, setSearchParams]) + + const handleClearAmount = useCallback(() => { + setSearchParams((prev) => + updateSearchParams(prev, { + amountMin: undefined, + amountMax: undefined, + amountAsset: undefined + }) + ) + setLocalAmountFilter(undefined) + setAmountFilterError(null) + }, [setSearchParams]) + + // --- Clear All --- + + const handleClearAll = useCallback(() => { + const updates: Record = { + eventType: undefined, + direction: undefined, + source: undefined, + sourceMode: undefined, + destination: undefined, + destMode: undefined, + amountMin: undefined, + amountMax: undefined, + amountAsset: undefined + } + if (supportsTick) { + updates.tickStart = undefined + updates.tickEnd = undefined + } + if (supportsDate) { + updates.dateStart = undefined + updates.dateEnd = undefined + updates.datePresetDays = undefined + } + setSearchParams((prev) => updateSearchParams(prev, updates)) + if (supportsTick) { + setTickRange(undefined) + setTickRangeError(null) + } + if (supportsDate) { + setLocalDateRange(undefined) + setDateRangeError(null) + } + setLocalSourceFilter(undefined) + setLocalDestFilter(undefined) + setLocalAmountFilter(undefined) + setAmountFilterError(null) + }, [setSearchParams, supportsTick, supportsDate]) + + // --- Mobile --- + + const handleMobileApplyFilters = useCallback( + (filters: { + tickRange?: TickRangeValue + eventTypes?: number[] + dateRange?: DateRangeValue + sourceFilter?: AddressFilter + destinationFilter?: AddressFilter + direction?: TransactionDirection + amountFilter?: EventAmountFilter + }) => { + const srcAddresses = + filters.sourceFilter?.addresses.filter((addr) => addr.trim() !== '') ?? [] + const dstAddresses = + filters.destinationFilter?.addresses.filter((addr) => addr.trim() !== '') ?? [] + + // Validate tick range — skip invalid values + const tickValid = !validateTickRange(filters.tickRange?.start, filters.tickRange?.end) + const validTickStart = tickValid ? filters.tickRange?.start || undefined : undefined + const validTickEnd = tickValid ? filters.tickRange?.end || undefined : undefined + + // Validate date range — skip invalid values, presets bypass validation + const dateIsPreset = filters.dateRange?.presetDays !== undefined + const dateValid = + dateIsPreset || !validateDateRange(filters.dateRange?.start, filters.dateRange?.end) + + const updates: Record = { + eventType: + filters.eventTypes && filters.eventTypes.length > 0 + ? filters.eventTypes.join(',') + : undefined, + direction: filters.direction, + source: srcAddresses.length > 0 ? srcAddresses.join(',') : undefined, + sourceMode: srcAddresses.length > 0 ? filters.sourceFilter?.mode ?? 'include' : undefined, + destination: dstAddresses.length > 0 ? dstAddresses.join(',') : undefined, + destMode: + dstAddresses.length > 0 ? filters.destinationFilter?.mode ?? 'include' : undefined, + ...amountFilterToParams(filters.amountFilter) + } + + if (supportsTick) { + updates.tickStart = validTickStart + updates.tickEnd = validTickEnd + } + if (supportsDate) { + updates.dateStart = + dateValid && !dateIsPreset ? filters.dateRange?.start || undefined : undefined + updates.dateEnd = + dateValid && !dateIsPreset ? filters.dateRange?.end || undefined : undefined + updates.datePresetDays = + dateIsPreset && filters.dateRange?.presetDays !== undefined + ? String(filters.dateRange.presetDays) + : undefined + } + + setSearchParams((prev) => updateSearchParams(prev, updates)) + + if (supportsTick) { + setTickRange(tickValid ? filters.tickRange : undefined) + setTickRangeError(null) + } + if (supportsDate) { + setLocalDateRange(dateValid ? filters.dateRange : undefined) + setDateRangeError(null) + } + setLocalSourceFilter(filters.sourceFilter) + setLocalDestFilter(filters.destinationFilter) + setLocalAmountFilter(filters.amountFilter) + setAmountFilterError(null) + }, + [setSearchParams, supportsTick, supportsDate] + ) + + return { + tickRange, + tickRangeError, + localDateRange, + dateRangeError, + localSourceFilter, + localDestFilter, + isTickActive, + isEventTypeActive, + isDateActive, + isSourceActive, + isDestActive, + hasActiveFilters, + handleTickRangeChange, + handleTickRangeApply, + handleClearTick, + handleToggleEventType, + handleClearEventType, + handleDirectionChange, + handleDateRangeChange, + handleDateRangeApply, + handleDatePresetSelect, + handleClearDate, + setLocalSourceFilter, + handleSourceApply, + handleClearSource, + setLocalDestFilter, + handleDestApply, + handleClearDest, + localAmountFilter, + amountFilterError, + isAmountActive, + handleAmountChange, + handleAmountApply, + handleClearAmount, + handleClearAll, + handleMobileApplyFilters + } +} diff --git a/src/pages/network/hooks/useFilterHelpers.ts b/src/pages/network/hooks/useFilterHelpers.ts index bbe2af6c..0ea0d9bd 100644 --- a/src/pages/network/hooks/useFilterHelpers.ts +++ b/src/pages/network/hooks/useFilterHelpers.ts @@ -135,6 +135,8 @@ type RangeValue = { type FormatValueFn = (value: string) => string +type TranslationFn = (key: string) => string + /** * Formats a range filter label for display. * Handles the common pattern: "Label: start - end" or "Label: >= start" or "Label: <= end" @@ -152,8 +154,29 @@ export function formatRangeLabel( if (!range?.start && !range?.end) return label const { start, end } = range - if (start && end) return `${label}: ${formatValue(start)} - ${formatValue(end)}` + if (start && end) { + return start === end + ? `${label}: ${formatValue(start)}` + : `${label}: ${formatValue(start)} - ${formatValue(end)}` + } if (start) return `${label}: >= ${formatValue(start)}` if (end) return `${label}: <= ${formatValue(end)}` return label } + +export function getAmountRangeLabel( + label: string, + range: AmountRange | undefined, + presets: AmountPreset[], + t: TranslationFn, + formatValue: (value: string | undefined, t: TranslationFn) => string +): string { + if (!range?.start && !range?.end && !range?.presetKey) return label + + if (range?.presetKey) { + const preset = presets.find((p) => p.labelKey === range.presetKey) + if (preset) return `${label}: ${t(preset.labelKey)}` + } + + return formatRangeLabel(label, range, (v) => formatValue(v, t)) +} diff --git a/src/pages/network/hooks/useTransactionEvents.ts b/src/pages/network/hooks/useTransactionEvents.ts new file mode 100644 index 00000000..ebe60edf --- /dev/null +++ b/src/pages/network/hooks/useTransactionEvents.ts @@ -0,0 +1,27 @@ +import { usePageAutoCorrect, useValidatedPage, useValidatedPageSize } from '@app/hooks' +import { useGetEventsQuery, type TransactionEvent } from '@app/store/apis/events' + +export default function useTransactionEvents(txId: string): { + events: TransactionEvent[] + total: number + isLoading: boolean +} { + const page = useValidatedPage() + const pageSize = useValidatedPageSize() + const offset = (page - 1) * pageSize + + const { data, isFetching } = useGetEventsQuery( + { transactionHash: txId, offset, size: pageSize }, + { skip: !txId } + ) + + const total = data?.total ?? 0 + + usePageAutoCorrect(!!data, total, pageSize) + + return { + events: data?.events ?? [], + total, + isLoading: isFetching + } +} diff --git a/src/pages/network/index.ts b/src/pages/network/index.ts index df111355..eb0ffd4f 100644 --- a/src/pages/network/index.ts +++ b/src/pages/network/index.ts @@ -5,6 +5,8 @@ export { default as OverviewPage } from './home/OverviewPage' export const AddressPageLazy = lazy(() => import('./address/AddressPage')) export const TickPageLazy = lazy(() => import('./tick/TickPage')) export const TxPageLazy = lazy(() => import('./TxPage')) +// blockchain +export const EventsPageLazy = lazy(() => import('./blockchain/events/EventsPage')) // wallets export const RichListPageLazy = lazy(() => import('./wallets/rich-list/RichListPage')) export const ExchangesPageLazy = lazy(() => import('./wallets/exchanges/ExchangesPage')) @@ -14,3 +16,5 @@ export const SmartContractsPageLazy = lazy( () => import('./assets/smart-contracts/SmartContractsPage') ) export const AssetsRichListPageLazy = lazy(() => import('./assets/rich-list/AssetsRichListPage')) + +export const EventDetailPageLazy = lazy(() => import('./event-detail/EventDetailPage')) diff --git a/src/pages/network/tick/TickPage.tsx b/src/pages/network/tick/TickPage.tsx index 67f9d6ba..7ffcb62a 100644 --- a/src/pages/network/tick/TickPage.tsx +++ b/src/pages/network/tick/TickPage.tsx @@ -1,16 +1,31 @@ +import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { useParams } from 'react-router-dom' +import { useParams, useSearchParams } from 'react-router-dom' import { withHelmet } from '@app/components/hocs' -import { Breadcrumbs } from '@app/components/ui' +import { Breadcrumbs, Tabs } from '@app/components/ui' import { PageLayout } from '@app/components/ui/layouts' import { formatString } from '@app/utils' import { HomeLink } from '../components' -import { TickDetails, TickTransactions } from './components' +import { TickDetails, TickEvents, TickTransactions } from './components' function TickPage() { const { t } = useTranslation('network-page') const { tick = '0' } = useParams() + const [searchParams, setSearchParams] = useSearchParams() + + const tabParam = searchParams.get('tab') + const selectedTabIndex = useMemo(() => { + if (tabParam === 'events') return 1 + return 0 + }, [tabParam]) + + const handleTabChange = useCallback( + (index: number) => { + setSearchParams(index === 1 ? { tab: 'events' } : {}, { replace: true }) + }, + [setSearchParams] + ) return ( @@ -21,7 +36,25 @@ function TickPage() {

    - + + + {t('transactions')} + {t('events')} + + + + + + + + + +
    ) } diff --git a/src/pages/network/tick/components/AddressFilterContent.tsx b/src/pages/network/tick/components/AddressFilterContent.tsx index a8a4d7b2..f1d5d9c0 100644 --- a/src/pages/network/tick/components/AddressFilterContent.tsx +++ b/src/pages/network/tick/components/AddressFilterContent.tsx @@ -49,7 +49,7 @@ export default function AddressFilterContent({ onChange={handleInputChange} onKeyDown={handleKeyDown} placeholder={placeholder || t('addressPlaceholder')} - className="w-full rounded border border-primary-60 bg-primary-70 px-10 py-8 font-space text-xs text-white placeholder:text-gray-50 focus:border-primary-30 focus:outline-none" + className="w-full rounded border border-primary-60 bg-primary-70 px-10 py-8 font-space text-base text-white placeholder:text-gray-50 focus:border-primary-30 focus:outline-none md:text-xs" autoComplete="off" spellCheck={false} /> diff --git a/src/pages/network/tick/components/TickDetails.tsx b/src/pages/network/tick/components/TickDetails.tsx index 725bec26..ca5e6b5a 100644 --- a/src/pages/network/tick/components/TickDetails.tsx +++ b/src/pages/network/tick/components/TickDetails.tsx @@ -1,6 +1,6 @@ import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { useNavigate } from 'react-router-dom' +import { useNavigate, useSearchParams } from 'react-router-dom' import { ChevronLeftIcon, ChevronRightIcon } from '@app/assets/icons' import { Skeleton } from '@app/components/ui' @@ -19,6 +19,7 @@ type Props = Readonly<{ export default function TickDetails({ tick }: Props) { const { t } = useTranslation('network-page') const navigate = useNavigate() + const [searchParams] = useSearchParams() const { data: tickData, @@ -33,9 +34,10 @@ export default function TickDetails({ tick }: Props) { const handleTickNavigation = useCallback( (direction: 'previous' | 'next') => () => { const newTick = Number(tick) + (direction === 'previous' ? -1 : 1) - navigate(Routes.NETWORK.TICK(newTick)) + const tab = searchParams.get('tab') + navigate(`${Routes.NETWORK.TICK(newTick)}${tab ? `?tab=${tab}` : ''}`) }, - [navigate, tick] + [navigate, tick, searchParams] ) const tickLeader = useMemo(() => { diff --git a/src/pages/network/tick/components/TickEvents.tsx b/src/pages/network/tick/components/TickEvents.tsx new file mode 100644 index 00000000..97b41fb7 --- /dev/null +++ b/src/pages/network/tick/components/TickEvents.tsx @@ -0,0 +1,64 @@ +import { useTranslation } from 'react-i18next' + +import { Alert } from '@app/components/ui' +import BetaBanner from '../../components/BetaBanner' +import { EventsFilterBar } from '../../components/filters' +import TransactionEvents from '../../components/TxItem/TransactionEvents' +import { useEventFilters } from '../../hooks' +import { useTickEvents } from '../hooks' + +type Props = Readonly<{ + tick: number +}> + +export default function TickEvents({ tick }: Props) { + const { t } = useTranslation('network-page') + const { + events, + total, + eventTypes, + sourceFilter, + destinationFilter, + amountFilter, + isLoading, + hasError + } = useTickEvents(tick) + + const filters = useEventFilters({ + eventTypes, + sourceFilter, + destinationFilter, + amountFilter, + supportsTick: false, + supportsDate: false + }) + + return ( +
    + + + + + {hasError ? ( + {t('eventsLoadFailed')} + ) : ( + + )} +
    + ) +} diff --git a/src/pages/network/tick/components/TickTransactionFiltersBar.tsx b/src/pages/network/tick/components/TickTransactionFiltersBar.tsx index d7131835..5d16a1c6 100644 --- a/src/pages/network/tick/components/TickTransactionFiltersBar.tsx +++ b/src/pages/network/tick/components/TickTransactionFiltersBar.tsx @@ -10,7 +10,12 @@ import { RangeFilterContent, ResetFiltersButton } from '../../components/filters' -import { formatRangeLabel, useAmountPresetHandler, useClearFilterHandler } from '../../hooks' +import { + formatRangeLabel, + getAmountRangeLabel, + useAmountPresetHandler, + useClearFilterHandler +} from '../../hooks' import AddressFilterContent from './AddressFilterContent' import TickMobileFiltersModal from './TickMobileFiltersModal' import type { TickTransactionFilters } from './tickFilterUtils' @@ -186,21 +191,13 @@ export default function TickTransactionFiltersBar({ return `${t('destination')}: ${formatAddressShort(destination)}` } - const getAmountLabel = () => { - if (!isAmountActive) return t('amount') - const { start, end, presetKey } = activeFilters.amountRange || {} - - if (presetKey) { - const preset = AMOUNT_PRESETS.find((p) => p.labelKey === presetKey) - if (preset) return `${t('amount')}: ${t(preset.labelKey)}` - } - - if (start && end) - return `${t('amount')}: ${formatAmountShort(start, t)} - ${formatAmountShort(end, t)}` - if (start) return `${t('amount')}: >= ${formatAmountShort(start, t)}` - if (end) return `${t('amount')}: <= ${formatAmountShort(end, t)}` - return t('amount') - } + const amountLabel = getAmountRangeLabel( + t('amount'), + activeFilters.amountRange, + AMOUNT_PRESETS, + t, + formatAmountShort + ) const getInputTypeLabel = () => formatRangeLabel(t('inputType'), activeFilters.inputTypeRange) @@ -220,9 +217,7 @@ export default function TickTransactionFiltersBar({ {isDestinationActive && ( )} - {isAmountActive && ( - - )} + {isAmountActive && } {isInputTypeActive && ( )} @@ -287,7 +282,7 @@ export default function TickTransactionFiltersBar({ {/* Amount Filter */} handleToggle('amount')} diff --git a/src/pages/network/tick/components/TickTransactions.tsx b/src/pages/network/tick/components/TickTransactions.tsx index 6ce58f9e..69a24116 100644 --- a/src/pages/network/tick/components/TickTransactions.tsx +++ b/src/pages/network/tick/components/TickTransactions.tsx @@ -1,14 +1,11 @@ -import { memo, useCallback, useEffect, useMemo, useState } from 'react' +import { memo, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { ChevronDownIcon } from '@app/assets/icons' -import { InfiniteScroll, Skeleton } from '@app/components/ui' -import { Button } from '@app/components/ui/buttons' -import { useTransactionExpandCollapse } from '@app/hooks' -import type { QueryServiceTransaction } from '@app/store/apis/query-service' +import { PageSizeSelect, PaginationBar } from '@app/components/ui' +import { usePaginationSearchParams, useValidatedPage, useValidatedPageSize } from '@app/hooks' import { useGetTransactionsForTickQuery } from '@app/store/apis/query-service' -import { TxItem } from '../../components' import TickTransactionFiltersBar from './TickTransactionFiltersBar' +import { TransactionRow, TransactionSkeletonRow } from '../../components' import type { TickTransactionFilters } from './tickFilterUtils' import { buildTickTransactionsRequest, @@ -16,14 +13,13 @@ import { parseFilterApiError } from './tickFilterUtils' -const PAGE_SIZE = 10 - -const TickTransactionsSkeleton = memo(() => ( -
    - {Array.from({ length: PAGE_SIZE / 2 }).map((_, index) => ( - +const TickTransactionsSkeletonRows = memo(({ count }: { count: number }) => ( + <> + {Array.from({ length: count }).map((_, index) => ( + // eslint-disable-next-line react/no-array-index-key + ))} -
    + )) type Props = Readonly<{ @@ -32,9 +28,10 @@ type Props = Readonly<{ export default function TickTransactions({ tick }: Props) { const { t } = useTranslation('network-page') - const [displayTransactions, setDisplayTransactions] = useState([]) - const [hasMore, setHasMore] = useState(true) const [activeFilters, setActiveFilters] = useState({}) + const { handlePageChange, handlePageSizeChange, resetPage } = usePaginationSearchParams() + const page = useValidatedPage() + const pageSize = useValidatedPageSize() // Build the API request with filters const request = useMemo( @@ -59,105 +56,112 @@ export default function TickTransactions({ tick }: Props) { return errorStr }, [tickTransactionsError, t]) - // Use shared expand/collapse hook with custom ID extractor - const { expandAll, expandedTxIds, handleExpandAllChange, handleTxToggle } = - useTransactionExpandCollapse({ - transactions: displayTransactions, - getTransactionId: (tx: QueryServiceTransaction) => tx.hash, - resetDependency: `${tick}-${JSON.stringify(activeFilters)}` - }) - - const loadMoreTransactions = useCallback(() => { - if (transactions && displayTransactions.length < transactions.length) { - const nextTransactions = transactions.slice( - displayTransactions.length, - displayTransactions.length + PAGE_SIZE - ) - setDisplayTransactions((prevTransactions) => [...prevTransactions, ...nextTransactions]) - setHasMore(displayTransactions.length + PAGE_SIZE < transactions.length) - } else { - setHasMore(false) - } - }, [displayTransactions, transactions]) + const totalCount = transactions?.length ?? 0 + const pageCount = Math.ceil(totalCount / pageSize) + + const paginatedTransactions = useMemo(() => { + if (!transactions) return [] + const start = (page - 1) * pageSize + return transactions.slice(start, start + pageSize) + }, [transactions, page, pageSize]) + + const handleApplyFilters = useCallback( + (filters: TickTransactionFilters) => { + setActiveFilters(filters) + resetPage() + }, + [resetPage] + ) - useEffect(() => { - if (transactions) { - setDisplayTransactions(transactions.slice(0, PAGE_SIZE)) - setHasMore(transactions.length > PAGE_SIZE) + const handleClearFilters = useCallback(() => { + setActiveFilters({}) + resetPage() + }, [resetPage]) + + const renderTableContent = useCallback(() => { + if (isTickTransactionsLoading) { + return } - }, [transactions]) - const handleApplyFilters = useCallback((filters: TickTransactionFilters) => { - setActiveFilters(filters) - }, []) + if (errorMessage) { + return ( + + + {errorMessage} + + + ) + } - const handleClearFilters = useCallback(() => { - setActiveFilters({}) - }, []) + if (paginatedTransactions.length === 0) { + return ( + + + {t('noTransactions')} + + + ) + } - const totalCount = transactions?.length ?? null + return paginatedTransactions.map((tx) => ( + + )) + }, [isTickTransactionsLoading, errorMessage, paginatedTransactions, pageSize, t, tick]) return (
    -

    {t('transactions')}

    - - {(totalCount !== null || displayTransactions.length > 0) && ( -
    - {totalCount !== null && totalCount > 0 ? ( - - {t('transactionsFound', { - count: totalCount.toLocaleString() - } as Record)} - - ) : null} - - {displayTransactions.length > 0 && ( - - )} +
    + {!isTickTransactionsLoading && totalCount > 0 ? ( + + {t('transactionsFound', { + count: totalCount.toLocaleString() + } as Record)} + + ) : ( + + )} + +
    + +
    +
    + + + + + + + + + + + + + + {renderTableContent()} +
    + {t('status')} + {t('txID')}{t('txType')}{t('tick')}{t('timestamp')}{t('source')} + {t('destination')} + + {t('amount')} +
    - )} - - } - error={errorMessage} - replaceContentOnLoading - replaceContentOnError - endMessage={ -

    - {displayTransactions.length === 0 ? t('noTransactions') : t('allTransactionsLoaded')} -

    - } - renderItem={(tx: QueryServiceTransaction) => ( - 1 && ( + )} - /> +
    ) } diff --git a/src/pages/network/tick/components/index.ts b/src/pages/network/tick/components/index.ts index cb6a906e..bee316ed 100644 --- a/src/pages/network/tick/components/index.ts +++ b/src/pages/network/tick/components/index.ts @@ -1,2 +1,3 @@ export { default as TickDetails } from './TickDetails' +export { default as TickEvents } from './TickEvents' export { default as TickTransactions } from './TickTransactions' diff --git a/src/pages/network/tick/hooks/index.ts b/src/pages/network/tick/hooks/index.ts new file mode 100644 index 00000000..57ab0241 --- /dev/null +++ b/src/pages/network/tick/hooks/index.ts @@ -0,0 +1 @@ +export { default as useTickEvents } from './useTickEvents' diff --git a/src/pages/network/tick/hooks/useTickEvents.ts b/src/pages/network/tick/hooks/useTickEvents.ts new file mode 100644 index 00000000..d9a89866 --- /dev/null +++ b/src/pages/network/tick/hooks/useTickEvents.ts @@ -0,0 +1,82 @@ +import { useMemo } from 'react' +import { useSearchParams } from 'react-router-dom' + +import { + usePageAutoCorrect, + useSanitizedEventTypes, + useValidatedPage, + useValidatedPageSize +} from '@app/hooks' +import { useGetEventsQuery, type TransactionEvent } from '@app/store/apis/events' +import type { AddressFilter } from '../../address/components/TransactionsOverview/filterUtils' +import { + type EventAmountFilter, + buildAmountFilter, + buildEventAddressFilter, + parseAddressFilter, + parseAmountFilter +} from '../../utils/eventFilterUtils' + +export default function useTickEvents(tick: number): { + events: TransactionEvent[] + total: number + eventTypes: number[] + sourceFilter: AddressFilter | undefined + destinationFilter: AddressFilter | undefined + amountFilter: EventAmountFilter | undefined + isLoading: boolean + hasError: boolean + refetch: () => void +} { + const [searchParams] = useSearchParams() + + const page = useValidatedPage() + const pageSize = useValidatedPageSize() + const offset = (page - 1) * pageSize + + const eventTypes = useSanitizedEventTypes() + + const sourceFilter = parseAddressFilter(searchParams, 'source', 'sourceMode') + const destinationFilter = parseAddressFilter(searchParams, 'destination', 'destMode') + + const amountFilter = parseAmountFilter(searchParams) + + const sourceResult = buildEventAddressFilter(sourceFilter) + const destResult = buildEventAddressFilter(destinationFilter) + const { amountShould, ...amountParams } = useMemo( + () => buildAmountFilter(amountFilter), + [amountFilter] + ) + + const { data, isFetching, isError, refetch } = useGetEventsQuery( + { + tickNumber: tick, + offset, + size: pageSize, + logType: eventTypes.length > 0 ? eventTypes : undefined, + source: sourceResult.include, + excludeSource: sourceResult.exclude, + destination: destResult.include, + excludeDestination: destResult.exclude, + ...(amountShould && { should: amountShould }), + ...amountParams + }, + { skip: !tick } + ) + + const total = data?.total ?? 0 + + usePageAutoCorrect(!!data, total, pageSize) + + return { + events: data?.events ?? [], + total, + eventTypes, + sourceFilter, + destinationFilter, + amountFilter, + isLoading: isFetching, + hasError: isError, + refetch + } +} diff --git a/src/pages/network/utils/eventFilterUtils.ts b/src/pages/network/utils/eventFilterUtils.ts new file mode 100644 index 00000000..82d9ce71 --- /dev/null +++ b/src/pages/network/utils/eventFilterUtils.ts @@ -0,0 +1,274 @@ +import type { EventRange, ShouldFilter } from '@app/store/apis/events' +import type { + AddressFilter, + TransactionDirection +} from '../address/components/TransactionsOverview/filterUtils' +import { + DATE_PRESETS, + DIRECTION, + getStartDateFromDays, + isOnlyPageAddress, + MODE +} from '../address/components/TransactionsOverview/filterUtils' +import { formatAddressShort } from './filterUtils' + +// ============================================================================ +// SHARED TYPES +// ============================================================================ + +export type AmountAssetType = 'any' | 'qubic' | 'other' + +export type EventAmountFilter = { + assetType: AmountAssetType + min?: string + max?: string +} + +export const ASSET_TYPE_OPTIONS: AmountAssetType[] = ['any', 'qubic', 'other'] + +export type TickRangeValue = { start?: string; end?: string } +export type DateRangeValue = { start?: string; end?: string; presetDays?: number } + +export function toTickRangeValue( + tickStart: string | undefined, + tickEnd: string | undefined +): TickRangeValue | undefined { + return tickStart || tickEnd ? { start: tickStart, end: tickEnd } : undefined +} + +// ============================================================================ +// URL PARAM PARSING +// ============================================================================ + +export function parseAddressFilter( + searchParams: URLSearchParams, + key: string, + modeKey: string +): AddressFilter | undefined { + const raw = searchParams.get(key) + if (!raw) return undefined + const addresses = raw + .split(',') + .map((a) => a.trim()) + .filter(Boolean) + if (addresses.length === 0) return undefined + const mode = searchParams.get(modeKey) === MODE.EXCLUDE ? MODE.EXCLUDE : MODE.INCLUDE + return { mode, addresses } +} + +export function parseTickRange(searchParams: URLSearchParams): TickRangeValue { + return { + start: searchParams.get('tickStart') || undefined, + end: searchParams.get('tickEnd') || undefined + } +} + +export function parseDateRange(searchParams: URLSearchParams): DateRangeValue | undefined { + const presetRaw = searchParams.get('datePresetDays') + if (presetRaw) { + const presetDays = parseFloat(presetRaw) + if (Number.isFinite(presetDays) && presetDays > 0) { + return { presetDays } + } + } + const start = searchParams.get('dateStart') || undefined + const end = searchParams.get('dateEnd') || undefined + if (start || end) return { start, end } + return undefined +} + +export function amountFilterToParams( + filter: EventAmountFilter | undefined +): Record { + return { + amountMin: filter?.min ?? undefined, + amountMax: filter?.max ?? undefined, + amountAsset: filter?.min || filter?.max ? filter?.assetType ?? 'any' : undefined + } +} + +export function parseAmountFilter(searchParams: URLSearchParams): EventAmountFilter | undefined { + const min = searchParams.get('amountMin') || undefined + const max = searchParams.get('amountMax') || undefined + if (!min && !max) return undefined + const rawAsset = searchParams.get('amountAsset') || 'any' + const assetType = (ASSET_TYPE_OPTIONS as readonly string[]).includes(rawAsset) + ? (rawAsset as EventAmountFilter['assetType']) + : 'any' + return { assetType, min, max } +} + +// ============================================================================ +// API REQUEST BUILDING +// ============================================================================ + +export function buildEventAddressFilter(filter: AddressFilter | undefined): { + include?: string + exclude?: string +} { + if (!filter?.addresses || filter.addresses.length === 0) return {} + const validAddresses = filter.addresses.filter((addr) => addr.trim() !== '') + if (validAddresses.length === 0) return {} + const commaSeparated = validAddresses.join(',') + return filter.mode === MODE.EXCLUDE ? { exclude: commaSeparated } : { include: commaSeparated } +} + +export function buildTickFilter( + tickStart: string | undefined, + tickEnd: string | undefined +): { tickNumber?: number; tickRange?: EventRange } { + if (!tickStart && !tickEnd) return {} + if (tickStart && tickEnd && tickStart === tickEnd) { + return { tickNumber: Number(tickStart) } + } + return { + tickRange: { + ...(tickStart && { gte: tickStart }), + ...(tickEnd && { lte: tickEnd }) + } + } +} + +export function buildTimestampRange(dateRange: DateRangeValue | undefined): EventRange | undefined { + if (!dateRange) return undefined + + const timestampStart = + dateRange.presetDays !== undefined + ? getStartDateFromDays(dateRange.presetDays) + : dateRange.start + + if (!timestampStart && !dateRange.end) return undefined + + return { + ...(timestampStart ? { gte: new Date(timestampStart).getTime().toString() } : {}), + ...(dateRange.end ? { lte: new Date(dateRange.end).getTime().toString() } : {}) + } +} + +export type AmountApiParams = { + amount?: string + numberOfShares?: string + amountRange?: EventRange + numberOfSharesRange?: EventRange + amountShould?: ShouldFilter[] +} + +/** + * Converts an EventAmountFilter into API request params. + * - QUBIC: uses `amount` filter (exact) or `amountRange` (range) + * - Other: uses `numberOfShares` filter (exact) or `numberOfSharesRange` (range) + * - Any: uses `should` with both amount and numberOfShares (OR logic) + * When min === max, uses exact value filter; otherwise uses range. + */ +export function buildAmountFilter(filter: EventAmountFilter | undefined): AmountApiParams { + if (!filter) return {} + const { assetType, min, max } = filter + if (!min && !max) return {} + + const isExact = min && max && min === max + const range: EventRange = { + ...(min && { gte: min }), + ...(max && { lte: max }) + } + + if (assetType === 'qubic') { + return isExact ? { amount: min } : { amountRange: range } + } + + if (assetType === 'other') { + return isExact ? { numberOfShares: min } : { numberOfSharesRange: range } + } + + // "any" — OR logic via should: both fields in a single entry + return isExact + ? { amountShould: [{ terms: { amount: min, numberOfShares: min } }] } + : { amountShould: [{ ranges: { amount: range, numberOfShares: range } }] } +} + +// ============================================================================ +// DIRECTION SYNC +// ============================================================================ + +/** + * Computes new source/dest filters when direction changes. + * Used by both the desktop direction handler and the mobile modal. + */ +export function applyEventDirectionSync( + newDirection: TransactionDirection | undefined, + addressId: string, + currentSource: AddressFilter | undefined, + currentDest: AddressFilter | undefined +): { sourceFilter: AddressFilter | undefined; destinationFilter: AddressFilter | undefined } { + const isPageAddr = (filter: AddressFilter | undefined): boolean => + !!filter && filter.mode === MODE.INCLUDE && isOnlyPageAddress(filter, addressId) + + if (newDirection === DIRECTION.INCOMING) { + return { + sourceFilter: isPageAddr(currentSource) ? undefined : currentSource, + destinationFilter: { mode: MODE.INCLUDE, addresses: [addressId] } + } + } + if (newDirection === DIRECTION.OUTGOING) { + return { + sourceFilter: { mode: MODE.INCLUDE, addresses: [addressId] }, + destinationFilter: isPageAddr(currentDest) ? undefined : currentDest + } + } + // "All" — clear page-address-only filters + return { + sourceFilter: isPageAddr(currentSource) ? undefined : currentSource, + destinationFilter: isPageAddr(currentDest) ? undefined : currentDest + } +} + +// ============================================================================ +// FILTER LABEL FORMATTING +// ============================================================================ + +type TranslationFn = (key: string, options?: Record) => string + +export function getAddressFilterLabel( + filterName: string, + filter: { mode: string; addresses: string[] } | undefined, + t: TranslationFn +): string { + if (!filter) return t(filterName) + const validAddresses = filter.addresses.filter((addr) => addr.trim() !== '') + if (validAddresses.length === 0) return t(filterName) + const prefix = filter.mode === 'exclude' ? `${t('exclude')} ` : '' + if (validAddresses.length === 1) { + return `${t(filterName)}: ${prefix}${formatAddressShort(validAddresses[0])}` + } + return `${t(filterName)}: ${prefix}${validAddresses.length}` +} + +function formatDateTimeShort(dateStr: string): string { + const date = new Date(dateStr) + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + const year = String(date.getFullYear()).slice(-2) + const hours = String(date.getHours()).padStart(2, '0') + const minutes = String(date.getMinutes()).padStart(2, '0') + return `${month}/${day}/${year} ${hours}:${minutes}` +} + +export function getDateRangeLabel(dateRange: DateRangeValue | undefined, t: TranslationFn): string { + if (!dateRange) return t('date') + const { start, end, presetDays } = dateRange + + if (presetDays !== undefined) { + const preset = DATE_PRESETS.find((p) => p.days === presetDays) + if (preset) { + const presetLabel = preset.daysCount + ? t(preset.labelKey, { count: preset.daysCount }) + : t(preset.labelKey) + return `${t('date')}: ${presetLabel}` + } + } + + if (start && end) + return `${t('date')}: ${formatDateTimeShort(start)} - ${formatDateTimeShort(end)}` + if (start) return `${t('date')}: >= ${formatDateTimeShort(start)}` + if (end) return `${t('date')}: <= ${formatDateTimeShort(end)}` + return t('date') +} diff --git a/src/pages/network/utils/filterUtils.ts b/src/pages/network/utils/filterUtils.ts index df7f77c5..7da6a170 100644 --- a/src/pages/network/utils/filterUtils.ts +++ b/src/pages/network/utils/filterUtils.ts @@ -130,7 +130,7 @@ export const MAX_UINT64 = 2n ** 64n - 1n * @param strictComparison - If true, start must be < end (not <=) * Returns an error message key or null if valid. */ -function validateNumericRange( +export function validateNumericRange( start: string | undefined, end: string | undefined, strictComparison = false @@ -196,6 +196,30 @@ export function validateInputTypeRange( return rangeError ? 'invalidRangeInputType' : null } +// ============================================================================ +// SEARCH PARAMS UTILITIES +// ============================================================================ + +/** + * Applies updates to URL search params and resets page to 1. + * Pass `undefined` as a value to remove that key. + */ +export function updateSearchParams( + prev: URLSearchParams, + updates: Record +): Record { + const next = Object.fromEntries(prev.entries()) + Object.entries(updates).forEach(([key, value]) => { + if (value === undefined) { + delete next[key] + } else { + next[key] = value + } + }) + next.page = '1' + return next +} + // ============================================================================ // AMOUNT FORMATTING UTILITIES // ============================================================================ @@ -235,9 +259,8 @@ export function formatAmountShort(value: string | undefined, t: TranslationFn): return num.toLocaleString('en-US') } -// Parse a formatted string back to raw number string -export function parseAmountFromDisplay(formatted: string): string { - // Remove all non-digit characters except for leading minus +// Parse a formatted string back to raw number string (removes commas, spaces, etc.) +export function parseNumericInput(formatted: string): string { return formatted.replace(/[^\d]/g, '') } diff --git a/src/pages/network/wallets/exchanges/ExchangesPage.tsx b/src/pages/network/wallets/exchanges/ExchangesPage.tsx index 18b48959..3aa5cd57 100644 --- a/src/pages/network/wallets/exchanges/ExchangesPage.tsx +++ b/src/pages/network/wallets/exchanges/ExchangesPage.tsx @@ -2,11 +2,11 @@ import { memo, useCallback } from 'react' import { useTranslation } from 'react-i18next' import { withHelmet } from '@app/components/hocs' -import { Breadcrumbs } from '@app/components/ui' +import { Breadcrumbs, TableErrorRow } from '@app/components/ui' import { PageLayout } from '@app/components/ui/layouts' import { useTailwindBreakpoint } from '@app/hooks' import { HomeLink } from '../../components' -import { ExchangeRow, ExchangesErrorRow, ExchangeSkeletonRow } from './components' +import { ExchangeRow, ExchangeSkeletonRow } from './components' import { EXCHANGES_SKELETON_ROWS } from './constants' import { useGetExchangesBalances } from './hooks' @@ -26,13 +26,13 @@ function ExchangesPage() { if (isLoading) return if (error || exchangeWallets.length === 0) { - return + return } return exchangeWallets.map((entity) => ( )) - }, [isLoading, error, exchangeWallets, isMobile]) + }, [isLoading, error, exchangeWallets, isMobile, t]) return ( @@ -47,7 +47,7 @@ function ExchangesPage() {
    -
    +
    diff --git a/src/pages/network/wallets/exchanges/components/ExchangesErrorRow.tsx b/src/pages/network/wallets/exchanges/components/ExchangesErrorRow.tsx deleted file mode 100644 index 32e1de44..00000000 --- a/src/pages/network/wallets/exchanges/components/ExchangesErrorRow.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { memo } from 'react' -import { useTranslation } from 'react-i18next' - -import { Alert } from '@app/components/ui' - -function ExchangesErrorRow() { - const { t } = useTranslation('network-page') - return ( - - - - ) -} - -const MemoizedExchangesErrorRow = memo(ExchangesErrorRow) - -export default MemoizedExchangesErrorRow diff --git a/src/pages/network/wallets/exchanges/components/index.ts b/src/pages/network/wallets/exchanges/components/index.ts index ecff6833..0abeab76 100644 --- a/src/pages/network/wallets/exchanges/components/index.ts +++ b/src/pages/network/wallets/exchanges/components/index.ts @@ -1,3 +1,2 @@ export { default as ExchangeRow } from './ExchangeRow' -export { default as ExchangesErrorRow } from './ExchangesErrorRow' export { default as ExchangeSkeletonRow } from './ExchangeSkeletonRow' diff --git a/src/pages/network/wallets/rich-list/RichListPage.tsx b/src/pages/network/wallets/rich-list/RichListPage.tsx index 2f2692fe..1923caf3 100644 --- a/src/pages/network/wallets/rich-list/RichListPage.tsx +++ b/src/pages/network/wallets/rich-list/RichListPage.tsx @@ -1,37 +1,15 @@ -import type { TFunction } from 'i18next' -import { memo, useCallback, useEffect, useMemo } from 'react' +import { useCallback, useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useSearchParams } from 'react-router-dom' import { withHelmet } from '@app/components/hocs' -import { Breadcrumbs, PaginationBar, Select } from '@app/components/ui' +import { Breadcrumbs, PageSizeSelect, PaginationBar, TableErrorRow } from '@app/components/ui' import { PageLayout } from '@app/components/ui/layouts' -import type { Option } from '@app/components/ui/Select' -import { useTailwindBreakpoint } from '@app/hooks' +import { RICH_LIST_DEFAULT_PAGE_SIZE, VALID_PAGE_SIZES } from '@app/constants' +import { usePaginationSearchParams, useTailwindBreakpoint } from '@app/hooks' import { useGetRichListQuery } from '@app/store/apis/rpc-stats' -import { HomeLink } from '../../components' -import { RichListErrorRow, RichListRow, RichListSkeletonRow } from './components' - -const DEFAULT_PAGE_SIZE = 15 - -const PAGE_SIZE_OPTIONS = [ - { i18nKey: 'showItemsPerPage', value: '15' }, - { i18nKey: 'showItemsPerPage', value: '30' }, - { i18nKey: 'showItemsPerPage', value: '50' }, - { i18nKey: 'showItemsPerPage', value: '100' } -] - -const getSelectOptions = (t: TFunction) => - PAGE_SIZE_OPTIONS.map((option) => ({ - label: t(option.i18nKey, { count: parseInt(option.value, 10) }), - value: option.value - })) - -const RichListLoadingRows = memo(({ pageSize }: { pageSize: number }) => - Array.from({ length: pageSize }).map((_, index) => ( - - )) -) +import { HomeLink, RichListLoadingRows } from '../../components' +import { RichListRow } from './components' function RichListPage() { const { t } = useTranslation('network-page') @@ -39,40 +17,21 @@ function RichListPage() { const [searchParams, setSearchParams] = useSearchParams() const page = parseInt(searchParams.get('page') || '1', 10) - const pageSize = parseInt(searchParams.get('pageSize') ?? String(DEFAULT_PAGE_SIZE), 10) - - const pageSizeOptions = useMemo(() => getSelectOptions(t), [t]) - const defaultPageSizeOption = useMemo( - () => pageSizeOptions.find((option) => option.value === String(pageSize)), - [pageSizeOptions, pageSize] + const pageSizeParam = parseInt( + searchParams.get('pageSize') ?? String(RICH_LIST_DEFAULT_PAGE_SIZE), + 10 ) + const pageSize = VALID_PAGE_SIZES.includes(pageSizeParam) + ? pageSizeParam + : RICH_LIST_DEFAULT_PAGE_SIZE + + const { handlePageChange, handlePageSizeChange } = usePaginationSearchParams() const { data, isFetching, error } = useGetRichListQuery({ page, pageSize }) - const handlePageChange = useCallback( - (value: number) => { - setSearchParams((prev) => ({ - ...Object.fromEntries(prev.entries()), - page: value.toString() - })) - }, - [setSearchParams] - ) - - const handlePageSizeChange = useCallback( - (option: Option) => { - setSearchParams((prev) => ({ - ...Object.fromEntries(prev.entries()), - pageSize: option.value, - page: '1' - })) - }, - [setSearchParams] - ) - const entitiesWithRank = useMemo( () => data?.richList.entities?.map((entity, index) => ({ @@ -91,7 +50,7 @@ function RichListPage() { (prev) => ({ ...Object.fromEntries(prev.entries()), ...(!prev.has('page') && { page: '1' }), - ...(!prev.has('pageSize') && { pageSize: String(DEFAULT_PAGE_SIZE) }) + ...(!prev.has('pageSize') && { pageSize: String(RICH_LIST_DEFAULT_PAGE_SIZE) }) }), { replace: true } ) @@ -102,13 +61,13 @@ function RichListPage() { if (isFetching) return if (error || entitiesWithRank?.length === 0) { - return + return } return entitiesWithRank?.map((entity) => ( )) - }, [entitiesWithRank, isFetching, error, isMobile, pageSize]) + }, [entitiesWithRank, isFetching, error, isMobile, pageSize, t]) return ( @@ -122,16 +81,10 @@ function RichListPage() {

    {t('richList')}

    {t('richListWarning')}

    -
    - {t('exchangesLoadFailed')} -
    diff --git a/src/pages/network/wallets/rich-list/components/RichListErrorRow.tsx b/src/pages/network/wallets/rich-list/components/RichListErrorRow.tsx deleted file mode 100644 index 9aaa4ff9..00000000 --- a/src/pages/network/wallets/rich-list/components/RichListErrorRow.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Alert } from '@app/components/ui' -import { memo } from 'react' -import { useTranslation } from 'react-i18next' - -function RichListErrorRow() { - const { t } = useTranslation('network-page') - return ( - - - - ) -} - -const MemoizedRichListErrorRow = memo(RichListErrorRow) - -export default MemoizedRichListErrorRow diff --git a/src/pages/network/wallets/rich-list/components/RichListSkeletonRow.tsx b/src/pages/network/wallets/rich-list/components/RichListSkeletonRow.tsx deleted file mode 100644 index a0bc4c38..00000000 --- a/src/pages/network/wallets/rich-list/components/RichListSkeletonRow.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Skeleton } from '@app/components/ui' - -const SKELETON_ROW_CELLS = [ - { - id: 'rank-skeleton-cell', - className: 'mx-auto size-16 xs:size-20' - }, - { - id: 'address-skeleton-cell', - - className: - 'h-16 w-96 xs:h-20 sm:h-40 sm:w-full sm:min-w-[248px] sm:max-w-[532px] md:w-[546px] 827px:h-20' - }, - { - id: 'name-skeleton-cell', - - className: 'size-16 xs:size-20 sm:w-84 w-72' - }, - { - id: 'amount-skeleton-cell', - - className: 'ml-auto h-16 w-136 xs:h-20' - } -] - -export default function RichListSkeletonRow() { - return ( - - {SKELETON_ROW_CELLS.map(({ id, className }) => ( - - ))} - - ) -} diff --git a/src/pages/network/wallets/rich-list/components/index.ts b/src/pages/network/wallets/rich-list/components/index.ts index 410296c0..fff91957 100644 --- a/src/pages/network/wallets/rich-list/components/index.ts +++ b/src/pages/network/wallets/rich-list/components/index.ts @@ -1,3 +1 @@ -export { default as RichListErrorRow } from './RichListErrorRow' export { default as RichListRow } from './RichListRow' -export { default as RichListSkeletonRow } from './RichListSkeletonRow' diff --git a/src/router/router.tsx b/src/router/router.tsx index 391a5962..a768232a 100644 --- a/src/router/router.tsx +++ b/src/router/router.tsx @@ -5,13 +5,15 @@ import { Error404Page } from '@app/pages' import { AddressPageLazy, AssetsRichListPageLazy, + EventsPageLazy, ExchangesPageLazy, OverviewPage, RichListPageLazy, SmartContractsPageLazy, TickPageLazy, TokensPageLazy, - TxPageLazy + TxPageLazy, + EventDetailPageLazy } from '@app/pages/network' import { Routes } from './routes' @@ -36,6 +38,10 @@ const router: ReturnType = createBrowserRouter([ path: Routes.NETWORK.TICK(':tick'), element: }, + { + path: Routes.NETWORK.EVENT(':tickNumber', ':logId'), + element: + }, { path: Routes.NETWORK.TX(':txId'), element: @@ -44,6 +50,10 @@ const router: ReturnType = createBrowserRouter([ path: Routes.NETWORK.ADDRESS(':addressId'), element: }, + { + path: Routes.NETWORK.BLOCKCHAIN.EVENTS, + element: + }, { path: Routes.NETWORK.WALLETS.RICH_LIST, element: diff --git a/src/router/routes.ts b/src/router/routes.ts index 6227b655..ae133eb6 100644 --- a/src/router/routes.ts +++ b/src/router/routes.ts @@ -19,8 +19,13 @@ export const Routes = { NETWORK: { ROOT: '/network', ADDRESS: (address: string) => `${Routes.NETWORK.ROOT}/address/${address}`, + EVENT: (tickNumber: string | number, logId: string | number) => + `${Routes.NETWORK.ROOT}/events/${tickNumber}/${logId}`, TX: (txId: string) => `${Routes.NETWORK.ROOT}/tx/${txId}`, TICK: (tick: string | number) => `${Routes.NETWORK.ROOT}/tick/${tick}`, + BLOCKCHAIN: { + EVENTS: '/network/blockchain/events' + }, WALLETS: { RICH_LIST: '/network/wallets/rich-list', EXCHANGES: '/network/wallets/exchanges' diff --git a/src/store/apis/events/events.api.ts b/src/store/apis/events/events.api.ts new file mode 100644 index 00000000..2f7b310a --- /dev/null +++ b/src/store/apis/events/events.api.ts @@ -0,0 +1,130 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' + +import { DEFAULT_PAGE_SIZE, QUERY_CACHE_TIME } from '@app/constants' +import { envConfig } from '@app/configs' +import type { RawGetEventsResponse, TransactionEvent } from './events.types' +import { adaptApiEvent } from './events.types' + +const BASE_URL = `${envConfig.EVENTS_API_URL}/query/v1` + +export interface PaginatedEvents { + events: TransactionEvent[] + total: number +} + +export interface ShouldFilter { + terms?: Record + ranges?: Record +} + +export interface EventRange { + gte?: string + lte?: string +} + +export interface GetEventsRequest { + tickNumber?: number + transactionHash?: string + logId?: number + offset?: number + size?: number + logType?: number[] + should?: ShouldFilter[] + source?: string + destination?: string + excludeSource?: string + excludeDestination?: string + category?: number + tickRange?: EventRange + timestampRange?: EventRange + amountRange?: EventRange + numberOfSharesRange?: EventRange + amount?: string + numberOfShares?: string +} + +function adaptEventsList(response: RawGetEventsResponse): TransactionEvent[] { + return (response?.events ?? []).flatMap((raw) => { + try { + return [adaptApiEvent(raw)] + } catch { + return [] + } + }) +} + +function adaptPaginatedEvents(response: RawGetEventsResponse): PaginatedEvents { + return { + events: adaptEventsList(response), + total: response?.hits?.total ?? 0 + } +} + +export const eventsApi = createApi({ + reducerPath: 'eventsApi', + baseQuery: fetchBaseQuery({ baseUrl: BASE_URL }), + keepUnusedDataFor: QUERY_CACHE_TIME, + endpoints: (builder) => ({ + getEvents: builder.query({ + query: ({ + tickNumber, + transactionHash, + logId, + offset = 0, + size = DEFAULT_PAGE_SIZE, + logType, + category, + should, + source, + destination, + excludeSource, + excludeDestination, + tickRange, + timestampRange, + amountRange, + numberOfSharesRange, + amount, + numberOfShares + }) => { + const filters: Record = { + ...(tickNumber !== undefined && { tickNumber: String(tickNumber) }), + ...(transactionHash && { transactionHash }), + ...(logId !== undefined && { logId: String(logId) }), + ...(logType && logType.length > 0 && { logType: logType.join(',') }), + ...(category !== undefined && { categories: String(category) }), + ...(source && { source }), + ...(destination && { destination }), + ...(amount !== undefined && { amount }), + ...(numberOfShares !== undefined && { numberOfShares }) + } + + const exclude: Record = { + ...(excludeSource && { source: excludeSource }), + ...(excludeDestination && { destination: excludeDestination }) + } + + const ranges: Record = { + ...(tickRange && { tickNumber: tickRange }), + ...(timestampRange && { timestamp: timestampRange }), + ...(amountRange && { amount: amountRange }), + ...(numberOfSharesRange && { numberOfShares: numberOfSharesRange }) + } + + return { + url: '/getEvents', + method: 'POST', + body: { + filters, + ...(Object.keys(exclude).length > 0 && { exclude }), + ...(Object.keys(ranges).length > 0 && { ranges }), + ...(should && { should }), + pagination: { offset, size } + } + } + }, + transformResponse: adaptPaginatedEvents + }) + }) +}) + +export const { useGetEventsQuery } = eventsApi diff --git a/src/store/apis/events/events.types.ts b/src/store/apis/events/events.types.ts new file mode 100644 index 00000000..f7d25cf0 --- /dev/null +++ b/src/store/apis/events/events.types.ts @@ -0,0 +1,329 @@ +// ============================================================================ +// EVENT TYPES & CONSTANTS +// ============================================================================ + +export type TransactionEvent = { + epoch: number + tickNumber: number + timestamp: number // epoch milliseconds (converted from API's epoch seconds) + contractIndex: number + transactionHash: string + logId: number + logDigest: string + type: number + categories: number[] + isVirtualTx: boolean + source: string + destination: string + amount?: number + assetName?: string + assetIssuer?: string + numberOfShares?: number + managingContractIndex?: number + unitOfMeasurement?: string + numberOfDecimalPlaces?: number + deductedAmount?: number + remainingAmount?: number + value?: string + contractMessageType?: number +} + +// Log type numeric codes (from core/src/logging.h) +export const EVENT_TYPES = { + QU_TRANSFER: 0, + ASSET_ISSUANCE: 1, + ASSET_OWNERSHIP_CHANGE: 2, + ASSET_POSSESSION_CHANGE: 3, + CONTRACT_ERROR: 4, + CONTRACT_WARNING: 5, + CONTRACT_INFO: 6, + CONTRACT_DEBUG: 7, + BURNING: 8, + DUST_BURNING: 9, + SPECTRUM_STATS: 10, + ASSET_OWNERSHIP_MANAGING_CONTRACT_CHANGE: 11, + ASSET_POSSESSION_MANAGING_CONTRACT_CHANGE: 12, + CONTRACT_RESERVE_DEDUCTION: 13, + ORACLE_QUERY_STATUS_CHANGE: 14, + CUSTOM_MESSAGE: 255 +} as const + +// Event types supported by the events API for filtering. +// ORACLE_QUERY_STATUS_CHANGE (14) is excluded — not yet supported by the events API. +export const EVENT_TYPE_FILTER_OPTIONS = [ + EVENT_TYPES.QU_TRANSFER, + EVENT_TYPES.ASSET_ISSUANCE, + EVENT_TYPES.ASSET_OWNERSHIP_CHANGE, + EVENT_TYPES.ASSET_POSSESSION_CHANGE, + EVENT_TYPES.CONTRACT_ERROR, + EVENT_TYPES.CONTRACT_WARNING, + EVENT_TYPES.CONTRACT_INFO, + EVENT_TYPES.CONTRACT_DEBUG, + EVENT_TYPES.BURNING, + EVENT_TYPES.DUST_BURNING, + EVENT_TYPES.SPECTRUM_STATS, + EVENT_TYPES.ASSET_OWNERSHIP_MANAGING_CONTRACT_CHANGE, + EVENT_TYPES.ASSET_POSSESSION_MANAGING_CONTRACT_CHANGE, + EVENT_TYPES.CONTRACT_RESERVE_DEDUCTION, + EVENT_TYPES.CUSTOM_MESSAGE +] as const + +export const EVENT_TYPE_LABELS: Record = { + [EVENT_TYPES.QU_TRANSFER]: 'QU_TRANSFER', + [EVENT_TYPES.ASSET_ISSUANCE]: 'ASSET_ISSUANCE', + [EVENT_TYPES.ASSET_OWNERSHIP_CHANGE]: 'ASSET_OWNERSHIP_CHANGE', + [EVENT_TYPES.ASSET_POSSESSION_CHANGE]: 'ASSET_POSSESSION_CHANGE', + [EVENT_TYPES.CONTRACT_ERROR]: 'CONTRACT_ERROR', + [EVENT_TYPES.CONTRACT_WARNING]: 'CONTRACT_WARNING', + [EVENT_TYPES.CONTRACT_INFO]: 'CONTRACT_INFO', + [EVENT_TYPES.CONTRACT_DEBUG]: 'CONTRACT_DEBUG', + [EVENT_TYPES.BURNING]: 'BURNING', + [EVENT_TYPES.DUST_BURNING]: 'DUST_BURNING', + [EVENT_TYPES.SPECTRUM_STATS]: 'SPECTRUM_STATS', + [EVENT_TYPES.ASSET_OWNERSHIP_MANAGING_CONTRACT_CHANGE]: + 'ASSET_OWNERSHIP_MANAGING_CONTRACT_CHANGE', + [EVENT_TYPES.ASSET_POSSESSION_MANAGING_CONTRACT_CHANGE]: + 'ASSET_POSSESSION_MANAGING_CONTRACT_CHANGE', + [EVENT_TYPES.CONTRACT_RESERVE_DEDUCTION]: 'CONTRACT_RESERVE_DEDUCTION', + [EVENT_TYPES.ORACLE_QUERY_STATUS_CHANGE]: 'ORACLE_QUERY_STATUS_CHANGE', + [EVENT_TYPES.CUSTOM_MESSAGE]: 'CUSTOM_MESSAGE' +} + +export function getEventTypeLabel(type: number): string { + return EVENT_TYPE_LABELS[type] ?? `UNKNOWN(${type})` +} + +/** + * Decode unitOfMeasurement from the API. + * The API returns a base64-encoded char[7] where each byte is a small integer (0–9). + * We base64-decode to get the raw bytes, then join them as a digit string. + */ +export function decodeUnitOfMeasurement(raw: string): string { + const bytes = Uint8Array.from(atob(raw), (ch) => ch.charCodeAt(0)) + const digits = Array.from(bytes, (b) => String(b)).join('') + return digits.replace(/0+$/, '') || '0' +} + +export const MAX_EVENT_TYPE_SELECTIONS = 5 + +export function parseEventTypesParam(raw: string | null): number[] { + if (raw === null || raw === '') return [] + return [ + ...new Set( + raw + .split(',') + .filter((s) => s !== '') + .map(Number) + .filter( + (n) => !Number.isNaN(n) && (EVENT_TYPE_FILTER_OPTIONS as readonly number[]).includes(n) + ) + ) + ].slice(0, MAX_EVENT_TYPE_SELECTIONS) +} + +// Event category codes (from sysTransactionMap) +const SYSTEM_TX_CATEGORIES: Record = { + 1: 'SC_INITIALIZE_TX', + 2: 'SC_BEGIN_EPOCH_TX', + 3: 'SC_BEGIN_TICK_TX', + 4: 'SC_END_TICK_TX', + 5: 'SC_END_EPOCH_TX', + 6: 'SC_NOTIFICATION_TX' +} + +const VIRTUAL_TX_CATEGORIES = new Set([2, 3, 4, 5]) + +export function isVirtualTxCategory(categories: number[]): boolean { + return categories.some((c) => VIRTUAL_TX_CATEGORIES.has(c)) +} + +function getVirtualCategory(categories: number[]): number | undefined { + return categories.find((c) => VIRTUAL_TX_CATEGORIES.has(c)) +} + +export function getVirtualTxId(categories: number[], tickNumber: number): string { + const virtualCategory = getVirtualCategory(categories) + const prefix = + virtualCategory != null + ? SYSTEM_TX_CATEGORIES[virtualCategory] ?? `CATEGORY_${virtualCategory}` + : 'UNKNOWN' + return `${prefix}_${tickNumber}` +} + +// Reverse lookup: prefix → category number +const CATEGORY_BY_PREFIX = new Map( + Object.entries(SYSTEM_TX_CATEGORIES) + .filter(([key]) => VIRTUAL_TX_CATEGORIES.has(Number(key))) + .map(([key, label]) => [label, Number(key)]) +) + +export interface ParsedVirtualTxId { + tickNumber: number + category: number +} + +export function parseVirtualTxId(txId: string): ParsedVirtualTxId | null { + const lastUnderscore = txId.lastIndexOf('_') + if (lastUnderscore === -1) return null + + const prefix = txId.slice(0, lastUnderscore) + const tickNumber = Number(txId.slice(lastUnderscore + 1)) + if (!Number.isFinite(tickNumber) || tickNumber <= 0) return null + + const category = CATEGORY_BY_PREFIX.get(prefix) + if (category === undefined) return null + + return { tickNumber, category } +} + +// ============================================================================ +// RAW API RESPONSE TYPES +// ============================================================================ + +interface QuTransferData { + source: string + destination: string + amount: string +} + +interface AssetIssuanceData { + assetIssuer: string + numberOfShares: string + managingContractIndex: string + assetName: string + numberOfDecimalPlaces: number + unitOfMeasurement: string +} + +interface AssetChangeData { + source: string + destination: string + assetIssuer: string + assetName: string + numberOfShares: string +} + +interface BurningData { + source: string + amount: string + contractIndex: string +} + +interface ContractReserveDeductionData { + contractIndex: string + deductedAmount: string + remainingAmount: string +} + +interface CustomMessageData { + value: string +} + +interface SmartContractMessageData { + contractIndex: string + contractMessageType: string +} + +export interface RawApiEvent { + epoch: number + tickNumber: number + timestamp: string + contractIndex: string + transactionHash: string + logId: string + logDigest: string + logType: number + categories: number[] + quTransfer?: QuTransferData + assetIssuance?: AssetIssuanceData + assetOwnershipChange?: AssetChangeData + assetPossessionChange?: AssetChangeData + burning?: BurningData + dustBurning?: BurningData + contractReserveDeduction?: ContractReserveDeductionData + customMessage?: CustomMessageData + smartContractMessage?: SmartContractMessageData +} + +export interface RawGetEventsResponse { + hits: { + total: number + from: number + size: number + } + events: RawApiEvent[] +} + +// ============================================================================ +// API RESPONSE ADAPTER +// ============================================================================ + +export function adaptApiEvent(raw: RawApiEvent): TransactionEvent { + const virtualTx = isVirtualTxCategory(raw.categories) + const base: TransactionEvent = { + epoch: raw.epoch, + tickNumber: raw.tickNumber, + timestamp: Number(raw.timestamp), + contractIndex: Number(raw.contractIndex), + transactionHash: virtualTx + ? getVirtualTxId(raw.categories, raw.tickNumber) + : raw.transactionHash, + logId: Number(raw.logId), + logDigest: raw.logDigest, + type: raw.logType, + categories: raw.categories, + isVirtualTx: virtualTx, + source: '', + destination: '' + } + + if (raw.quTransfer) { + base.source = raw.quTransfer.source + base.destination = raw.quTransfer.destination + base.amount = Number(raw.quTransfer.amount) + } else if (raw.assetIssuance) { + base.source = raw.assetIssuance.assetIssuer + base.assetName = raw.assetIssuance.assetName + base.assetIssuer = raw.assetIssuance.assetIssuer + base.numberOfShares = Number(raw.assetIssuance.numberOfShares) + base.managingContractIndex = Number(raw.assetIssuance.managingContractIndex) + base.unitOfMeasurement = decodeUnitOfMeasurement(raw.assetIssuance.unitOfMeasurement) + base.numberOfDecimalPlaces = raw.assetIssuance.numberOfDecimalPlaces + base.amount = Number(raw.assetIssuance.numberOfShares) + } else if (raw.assetOwnershipChange) { + base.source = raw.assetOwnershipChange.source + base.destination = raw.assetOwnershipChange.destination + base.assetName = raw.assetOwnershipChange.assetName + base.assetIssuer = raw.assetOwnershipChange.assetIssuer + base.numberOfShares = Number(raw.assetOwnershipChange.numberOfShares) + base.amount = Number(raw.assetOwnershipChange.numberOfShares) + } else if (raw.assetPossessionChange) { + base.source = raw.assetPossessionChange.source + base.destination = raw.assetPossessionChange.destination + base.assetName = raw.assetPossessionChange.assetName + base.assetIssuer = raw.assetPossessionChange.assetIssuer + base.numberOfShares = Number(raw.assetPossessionChange.numberOfShares) + base.amount = Number(raw.assetPossessionChange.numberOfShares) + } else if (raw.burning) { + base.source = raw.burning.source + base.amount = Number(raw.burning.amount) + base.contractIndex = Number(raw.burning.contractIndex) + } else if (raw.dustBurning) { + base.source = raw.dustBurning.source + base.amount = Number(raw.dustBurning.amount) + base.contractIndex = Number(raw.dustBurning.contractIndex) + } else if (raw.contractReserveDeduction) { + base.contractIndex = Number(raw.contractReserveDeduction.contractIndex) + base.deductedAmount = Number(raw.contractReserveDeduction.deductedAmount) + base.remainingAmount = Number(raw.contractReserveDeduction.remainingAmount) + } else if (raw.customMessage) { + base.value = raw.customMessage.value + } + + if (raw.smartContractMessage) { + base.contractIndex = Number(raw.smartContractMessage.contractIndex) + base.contractMessageType = Number(raw.smartContractMessage.contractMessageType) + } + + return base +} diff --git a/src/store/apis/events/index.ts b/src/store/apis/events/index.ts new file mode 100644 index 00000000..59ab6cde --- /dev/null +++ b/src/store/apis/events/index.ts @@ -0,0 +1,2 @@ +export * from './events.api' +export * from './events.types' diff --git a/src/store/apis/qubic-static/index.ts b/src/store/apis/qubic-static/index.ts index ae3fefdf..4b4ab9b1 100644 --- a/src/store/apis/qubic-static/index.ts +++ b/src/store/apis/qubic-static/index.ts @@ -3,6 +3,7 @@ export { useGetAddressLabelsQuery, useGetExchangesQuery, useGetExplorerTranslationsQuery, + useGetProtocolQuery, useGetSmartContractsQuery, useGetTokenCategoriesQuery, useGetTokensQuery diff --git a/src/store/apis/qubic-static/qubic-static.api.ts b/src/store/apis/qubic-static/qubic-static.api.ts index 0799d028..196a48e1 100644 --- a/src/store/apis/qubic-static/qubic-static.api.ts +++ b/src/store/apis/qubic-static/qubic-static.api.ts @@ -4,6 +4,7 @@ import type { ExplorerTranslations, GetAddressLabelsResponse, GetExchangesResponse, + GetProtocolResponse, GetSmartContractsResponse, GetTokenCategoriesResponse, GetTokensResponse @@ -43,6 +44,10 @@ export const qubicStaticApi = createApi({ getTokenCategories: build.query({ query: () => `${EXPLORER_DATA_URL}/token_categories.json` }), + getProtocol: build.query({ + query: () => `${GENERAL_DATA_URL}/protocol.json`, + transformResponse: (response: GetProtocolResponse) => response.transaction_input_types + }), getExplorerTranslations: build.query({ query: (lang) => `${EXPLORER_DATA_URL}/locales/${lang}.json` }) @@ -55,5 +60,6 @@ export const { useGetAddressLabelsQuery, useGetTokensQuery, useGetTokenCategoriesQuery, + useGetProtocolQuery, useGetExplorerTranslationsQuery } = qubicStaticApi diff --git a/src/store/apis/qubic-static/qubic-static.types.ts b/src/store/apis/qubic-static/qubic-static.types.ts index 4ddc8a87..ecd65aaf 100644 --- a/src/store/apis/qubic-static/qubic-static.types.ts +++ b/src/store/apis/qubic-static/qubic-static.types.ts @@ -11,6 +11,7 @@ export type SmartContract = { contractIndex: number address: string procedures: SmartContractProcedure[] + sharesAuctionEpoch?: number website?: string proposalUrl?: string } @@ -39,6 +40,7 @@ export type GetAddressLabelsResponse = { export type Token = { name: string + issuer?: string website: string } @@ -65,4 +67,13 @@ export type GetTokenCategoriesResponse = { allCategoryNameKey: string } +export type TransactionInputType = { + id: number + label: string +} + +export type GetProtocolResponse = { + transaction_input_types: TransactionInputType[] +} + export type ExplorerTranslations = Record diff --git a/src/store/apis/query-service/query-service.api.ts b/src/store/apis/query-service/query-service.api.ts index bcdd6be4..c280a633 100644 --- a/src/store/apis/query-service/query-service.api.ts +++ b/src/store/apis/query-service/query-service.api.ts @@ -1,5 +1,7 @@ -import { envConfig } from '@app/configs' import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' + +import { QUERY_CACHE_TIME } from '@app/constants' +import { envConfig } from '@app/configs' import type { ComputorList, GetComputorListsForEpochResponse, @@ -7,6 +9,7 @@ import type { GetTickDataResponse, GetTransactionsForIdentityRequest, GetTransactionsForTickRequest, + ProcessedTickInterval, QueryServiceResponse, QueryServiceTransaction, TickData @@ -17,6 +20,7 @@ const BASE_URL = `${envConfig.QUBIC_RPC_URL}/query/v1` export const rpcQueryServiceApi = createApi({ reducerPath: 'rpcQueryServiceApi', baseQuery: fetchBaseQuery({ baseUrl: BASE_URL }), + keepUnusedDataFor: QUERY_CACHE_TIME, endpoints: (builder) => ({ getTransactionsForIdentity: builder.mutation< QueryServiceResponse, @@ -62,6 +66,9 @@ export const rpcQueryServiceApi = createApi({ }), getLastProcessedTick: builder.query({ query: () => '/getLastProcessedTick' + }), + getProcessedTickIntervals: builder.query({ + query: () => '/getProcessedTickIntervals' }) }) }) @@ -72,5 +79,6 @@ export const { useGetTransactionsForTickQuery, useGetTickDataQuery, useGetComputorListsForEpochQuery, - useGetLastProcessedTickQuery + useGetLastProcessedTickQuery, + useGetProcessedTickIntervalsQuery } = rpcQueryServiceApi diff --git a/src/store/apis/query-service/query-service.types.ts b/src/store/apis/query-service/query-service.types.ts index c929eb2c..2b771e83 100644 --- a/src/store/apis/query-service/query-service.types.ts +++ b/src/store/apis/query-service/query-service.types.ts @@ -88,6 +88,13 @@ export interface GetLastProcessedTickResponse { intervalInitialTick: number } +// getProcessedTickIntervals types +export interface ProcessedTickInterval { + epoch: number + firstTick: number + lastTick: number +} + // getTransactionsForTick request with filters // Note: Unlike getTransactionsForIdentity, this endpoint only supports single address (no multi, no exclude) export interface GetTransactionsForTickRequest { diff --git a/src/store/index.ts b/src/store/index.ts index a3d8fd35..d4e27831 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,5 +1,6 @@ import { configureStore } from '@reduxjs/toolkit' +import { eventsApi } from './apis/events' import { archiverV2Api } from './apis/archiver-v2' import { qliApi } from './apis/qli' import { rpcQueryServiceApi } from './apis/query-service' @@ -18,7 +19,8 @@ export const store = configureStore({ [rpcQueryServiceApi.reducerPath]: rpcQueryServiceApi.reducer, [rpcLiveApi.reducerPath]: rpcLiveApi.reducer, [rpcStatsApi.reducerPath]: rpcStatsApi.reducer, - [qubicStaticApi.reducerPath]: qubicStaticApi.reducer + [qubicStaticApi.reducerPath]: qubicStaticApi.reducer, + [eventsApi.reducerPath]: eventsApi.reducer }, middleware: (getDefaultMiddleware) => @@ -29,6 +31,7 @@ export const store = configureStore({ .concat(rpcLiveApi.middleware) .concat(rpcStatsApi.middleware) .concat(qubicStaticApi.middleware) + .concat(eventsApi.middleware) }) export type RootState = ReturnType diff --git a/src/utils/date.ts b/src/utils/date.ts index 515221dc..4e4fa6c5 100644 --- a/src/utils/date.ts +++ b/src/utils/date.ts @@ -5,8 +5,9 @@ export function formatDate( const defaultResult = (options?.split ? { date: '', time: '' } : '') as T extends true ? { date: string; time: string } : string + const locale = options?.shortDate ? 'sv-SE' : 'en-US' const formatDateTime = (date: Date, dateTimeFormatOptions: Intl.DateTimeFormatOptions) => - new Intl.DateTimeFormat('en-US', dateTimeFormatOptions).format(date) + new Intl.DateTimeFormat(locale, dateTimeFormatOptions).format(date) if (!dateString) return defaultResult diff --git a/src/utils/epoch.ts b/src/utils/epoch.ts new file mode 100644 index 00000000..298ffd5b --- /dev/null +++ b/src/utils/epoch.ts @@ -0,0 +1,55 @@ +import type { ProcessedTickInterval } from '@app/store/apis/query-service' + +interface EpochRange { + epoch: number + minFirstTick: number + maxLastTick: number +} + +export function getEpochForTick( + intervals: ProcessedTickInterval[], + tickNumber: number +): number | undefined { + const epochRanges = intervals.reduce>((acc, interval) => { + const existing = acc[interval.epoch] + return { + ...acc, + [interval.epoch]: existing + ? { + epoch: interval.epoch, + minFirstTick: Math.min(existing.minFirstTick, interval.firstTick), + maxLastTick: Math.max(existing.maxLastTick, interval.lastTick) + } + : { + epoch: interval.epoch, + minFirstTick: interval.firstTick, + maxLastTick: interval.lastTick + } + } + }, {}) + + const sortedRanges = Object.values(epochRanges).sort((a, b) => a.epoch - b.epoch) + + const match = sortedRanges.find( + (range) => tickNumber >= range.minFirstTick && tickNumber <= range.maxLastTick + ) + + if (match) return match.epoch + + // If the tick falls in a gap between two epochs (e.g. last ticks of an epoch + // not covered by the intervals), attribute it to the earlier epoch + const gapMatch = sortedRanges.find( + (range, index) => + index < sortedRanges.length - 1 && + tickNumber > range.maxLastTick && + tickNumber < sortedRanges[index + 1].minFirstTick + ) + + if (gapMatch) return gapMatch.epoch + + // If the tick is beyond all known intervals, assume it belongs to the latest epoch + const lastRange = sortedRanges[sortedRanges.length - 1] + if (lastRange && tickNumber > lastRange.maxLastTick) return lastRange.epoch + + return undefined +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 2b6ff008..60e59778 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -17,6 +17,7 @@ function copyText(textToCopy: string) { } export * from './date' +export * from './epoch' export * from './qubic-ts' export * from './format' export * from './styles' diff --git a/src/utils/qubic.ts b/src/utils/qubic.ts index 6b2e490c..2c151d28 100644 --- a/src/utils/qubic.ts +++ b/src/utils/qubic.ts @@ -1,12 +1,28 @@ -import type { SmartContract } from '@app/store/apis/qubic-static' - -/** - * Get the procedure name for a smart contract transaction - * @param contractAddress - The smart contract address - * @param inputType - The transaction input type (procedure ID) - * @param smartContracts - The smart contracts data from the API - * @returns The procedure name if found, undefined otherwise - */ +import type { SmartContract, TransactionInputType } from '@app/store/apis/qubic-static' +import { isSmartContractTx } from '@app/utils/qubic-ts' + +export interface SharesAuctionBid { + price: bigint + quantity: number +} + +export const decodeSharesAuctionBid = (inputData: string): SharesAuctionBid | undefined => { + try { + const binaryString = atob(inputData) + const bytes = new Uint8Array(binaryString.length) + bytes.forEach((_, i) => { + bytes[i] = binaryString.charCodeAt(i) + }) + const view = new DataView(bytes.buffer) + return { + price: view.getBigInt64(0, true), + quantity: view.getInt16(8, true) + } + } catch { + return undefined + } +} + export const getProcedureName = ( contractAddress: string, inputType: number, @@ -20,3 +36,48 @@ export const getProcedureName = ( const procedure = contract.procedures.find((proc) => proc.id === inputType) return procedure?.name } + +export const getInputTypeLabel = ( + inputType: number, + transactionInputTypes?: TransactionInputType[] +): string | undefined => { + if (!transactionInputTypes) return undefined + return transactionInputTypes.find((t) => t.id === inputType)?.label +} + +export const isSharesAuctionBid = ( + destination: string, + inputType: number, + epoch?: number, + smartContracts?: SmartContract[] +): boolean => { + if (inputType !== 1 || epoch == null || !smartContracts) return false + const contract = smartContracts.find((sc) => sc.address === destination) + return !!contract && epoch === contract.sharesAuctionEpoch +} + +export const getTransactionTypeDisplay = ( + destination: string, + inputType: number, + smartContracts?: SmartContract[], + protocolData?: TransactionInputType[], + epoch?: number +): string => { + if (isSharesAuctionBid(destination, inputType, epoch, smartContracts)) { + return 'Place Bid' + } + if (isSmartContractTx(destination, inputType)) { + return getProcedureName(destination, inputType, smartContracts) || 'SC' + } + return getInputTypeLabel(inputType, protocolData) || 'Standard' +} + +export const getTransactionTypeDisplayLong = ( + destination: string, + inputType: number, + smartContracts?: SmartContract[], + protocolData?: TransactionInputType[], + epoch?: number +): string => { + return `${getTransactionTypeDisplay(destination, inputType, smartContracts, protocolData, epoch)} (${inputType})` +} diff --git a/tailwind.config.ts b/tailwind.config.ts index 20d9b9c0..61b7240d 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,8 +1,6 @@ import type { Config } from 'tailwindcss' -import aspectRatioPlugin from '@tailwindcss/aspect-ratio' import formsPlugin from '@tailwindcss/forms' -import typographyPlugin from '@tailwindcss/typography' import { colors } from './src/theme/colors' import { screens } from './src/theme/screens' @@ -234,9 +232,7 @@ const tailwindConfig: Config = { } }, plugins: [ - aspectRatioPlugin, formsPlugin, - typographyPlugin, scrollbarPlugin({ nocompatible: true, preferredStrategy: 'pseudoelements' }) ] } diff --git a/vite.config.ts b/vite.config.ts index b2ae0acb..bb400907 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,7 +3,7 @@ import { visualizer } from 'rollup-plugin-visualizer' import type { UserConfig } from 'vite' import { defineConfig, loadEnv } from 'vite' import svgr from 'vite-plugin-svgr' -import { qliApiProxy, rpcApiProxy, staticApiProxy } from './dev-proxy.config' +import { eventsApiProxy, qliApiProxy, rpcApiProxy, staticApiProxy } from './dev-proxy.config' const defaultConfig: UserConfig = { plugins: [ @@ -47,7 +47,8 @@ export default defineConfig(({ command, mode }) => { proxy: { '/dev-proxy-qli-api': qliApiProxy, '/dev-proxy-rpc-api': rpcApiProxy, - '/dev-proxy-static-api': staticApiProxy + '/dev-proxy-static-api': staticApiProxy, + '/dev-proxy-events-api': eventsApiProxy } } }
    - {t('richListLoadFailed')} -
    - -