diff --git a/PRE_COMMIT_SETUP.md b/PRE_COMMIT_SETUP.md index 3204fa17..75d90959 100644 --- a/PRE_COMMIT_SETUP.md +++ b/PRE_COMMIT_SETUP.md @@ -36,8 +36,6 @@ Extended pre-commit hook to run typecheck and affected tests. - Performance tips - Troubleshooting - - ## How It Works ### 1. Get Staged Files diff --git a/README.md b/README.md index a0da6d7f..79b88549 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,7 @@ To keep our translation files clean, you can run the unused keys script to find ```bash node scripts/find-unused-i18n-keys.js ``` + This script will output a report of keys present in `messages/en.json` but never referenced in `src/`. ## 🐳 Docker Support diff --git a/messages/en.json b/messages/en.json index 0ccad41a..56bc144e 100644 --- a/messages/en.json +++ b/messages/en.json @@ -461,6 +461,24 @@ "verifyWithVotes": "✓ Verify Campaign with Votes", "verifying": "Verifying..." }, + "Notifications": { + "title": "Notifications", + "markAllRead": "Mark all read", + "noNotifications": "No notifications yet", + "connectWallet": "Connect your wallet to see notifications", + "unreadLabel": "{count} unread notifications", + "viewDetails": "View details", + "justNow": "Just now", + "mAgo": "{count}m ago", + "hAgo": "{count}h ago", + "dAgo": "{count}d ago", + "settingsAriaLabel": "Notification settings", + "settingsTitle": "Notification Settings", + "prefContributions": "Contributions to my campaign", + "prefVerified": "Campaign verified", + "prefRefundAvailable": "Refund available", + "prefRevenueDeposited": "Revenue deposited" + }, "Observability": { "loading": "Loading observability metrics…", "metricsRequestFailed": "Metrics request failed ({status})", diff --git a/messages/es.json b/messages/es.json index bd884e70..e261621e 100644 --- a/messages/es.json +++ b/messages/es.json @@ -461,6 +461,24 @@ "verifyWithVotes": "✓ Verificar Campaña con Votos", "verifying": "Verificando..." }, + "Notifications": { + "title": "Notificaciones", + "markAllRead": "Marcar todo leído", + "noNotifications": "Aún no hay notificaciones", + "connectWallet": "Conecta tu wallet para ver notificaciones", + "unreadLabel": "{count} notificaciones sin leer", + "viewDetails": "Ver detalles", + "justNow": "Ahora mismo", + "mAgo": "hace {count}m", + "hAgo": "hace {count}h", + "dAgo": "hace {count}d", + "settingsAriaLabel": "Configuración de notificaciones", + "settingsTitle": "Configuración de Notificaciones", + "prefContributions": "Contribuciones a mi campaña", + "prefVerified": "Campaña verificada", + "prefRefundAvailable": "Reembolso disponible", + "prefRevenueDeposited": "Ingresos depositados" + }, "Observability": { "loading": "Cargando métricas de observabilidad…", "metricsRequestFailed": "Error al solicitar métricas ({status})", diff --git a/scripts/check-i18n.mjs b/scripts/check-i18n.mjs index a9b39974..ac3c69e2 100644 --- a/scripts/check-i18n.mjs +++ b/scripts/check-i18n.mjs @@ -1,18 +1,18 @@ -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const messagesDir = path.join(__dirname, '../messages'); -const srcDir = path.join(__dirname, '../src'); +const messagesDir = path.join(__dirname, "../messages"); +const srcDir = path.join(__dirname, "../src"); -function getAllKeys(obj, prefix = '') { +function getAllKeys(obj, prefix = "") { return Object.keys(obj).reduce((acc, key) => { const value = obj[key]; const newKey = prefix ? `${prefix}.${key}` : key; - if (typeof value === 'object' && value !== null) { + if (typeof value === "object" && value !== null) { acc.push(...getAllKeys(value, newKey)); } else { acc.push(newKey); @@ -36,19 +36,19 @@ function getAllFiles(dir, files = []) { } function checkUnusedKeys() { - const enPath = path.join(messagesDir, 'en.json'); - const enObj = JSON.parse(fs.readFileSync(enPath, 'utf8')); + const enPath = path.join(messagesDir, "en.json"); + const enObj = JSON.parse(fs.readFileSync(enPath, "utf8")); const allKeys = getAllKeys(enObj); const files = getAllFiles(srcDir); - const fileContents = files.map((f) => fs.readFileSync(f, 'utf8')).join('\n'); + const fileContents = files.map((f) => fs.readFileSync(f, "utf8")).join("\n"); const unusedKeys = []; for (const fullKey of allKeys) { - const parts = fullKey.split('.'); + const parts = fullKey.split("."); const key = parts[parts.length - 1]; - const namespace = parts.length > 1 ? parts[0] : ''; + const namespace = parts.length > 1 ? parts[0] : ""; // Check if the key appears in the source code // It could be t('key') or t("key") or next-intl dynamic keys @@ -61,17 +61,17 @@ function checkUnusedKeys() { } // Filter out known dynamic keys to avoid false positives - const knownDynamicPrefixes = ['step_']; + const knownDynamicPrefixes = ["step_"]; const filteredUnused = unusedKeys.filter((k) => { - const key = k.split('.').pop(); + const key = k.split(".").pop(); return !knownDynamicPrefixes.some((prefix) => key.startsWith(prefix)); }); if (filteredUnused.length > 0) { - console.warn('⚠️ Potentially unused translation keys found:'); + console.warn("⚠️ Potentially unused translation keys found:"); filteredUnused.forEach((k) => console.warn(` - ${k}`)); } else { - console.log('✅ No unused translation keys detected.'); + console.log("✅ No unused translation keys detected."); } } diff --git a/scripts/find-unused-i18n-keys.js b/scripts/find-unused-i18n-keys.js index a1b6c6d0..3c96bacf 100644 --- a/scripts/find-unused-i18n-keys.js +++ b/scripts/find-unused-i18n-keys.js @@ -1,5 +1,5 @@ -const fs = require('fs'); -const path = require('path'); +const fs = require("fs"); +const path = require("path"); function getFiles(dir, fileList = []) { const files = fs.readdirSync(dir); @@ -14,10 +14,10 @@ function getFiles(dir, fileList = []) { return fileList; } -function flattenKeys(obj, prefix = '') { +function flattenKeys(obj, prefix = "") { return Object.keys(obj).reduce((acc, k) => { - const pre = prefix.length ? prefix + '.' : ''; - if (typeof obj[k] === 'object' && obj[k] !== null) { + const pre = prefix.length ? prefix + "." : ""; + if (typeof obj[k] === "object" && obj[k] !== null) { Object.assign(acc, flattenKeys(obj[k], pre + k)); } else { acc[pre + k] = obj[k]; @@ -27,36 +27,36 @@ function flattenKeys(obj, prefix = '') { } function findUnusedKeys() { - const messagesPath = path.join(__dirname, '../messages/en.json'); - const srcPath = path.join(__dirname, '../src'); + const messagesPath = path.join(__dirname, "../messages/en.json"); + const srcPath = path.join(__dirname, "../src"); if (!fs.existsSync(messagesPath)) { - console.error('en.json not found at', messagesPath); + console.error("en.json not found at", messagesPath); process.exit(1); } - const enJson = JSON.parse(fs.readFileSync(messagesPath, 'utf8')); + const enJson = JSON.parse(fs.readFileSync(messagesPath, "utf8")); const flatKeys = flattenKeys(enJson); const keys = Object.keys(flatKeys); - + const files = getFiles(srcPath); - const fileContents = files.map(f => fs.readFileSync(f, 'utf8')); + const fileContents = files.map((f) => fs.readFileSync(f, "utf8")); const unusedKeys = []; for (const key of keys) { - const parts = key.split('.'); + const parts = key.split("."); const leaf = parts[parts.length - 1]; - + // Check if the leaf key or the full key is present in any file. - let isUsed = fileContents.some(content => content.includes(leaf) || content.includes(key)); + let isUsed = fileContents.some((content) => content.includes(leaf) || content.includes(key)); // Heuristic for dynamic keys (like step_connect_title or level_Bronze) // If the exact leaf is not found, check if its underscore-separated parts are all present in a single file - if (!isUsed && leaf.includes('_')) { - const leafParts = leaf.split('_'); - isUsed = fileContents.some(content => { - return leafParts.every(p => content.includes(p)); + if (!isUsed && leaf.includes("_")) { + const leafParts = leaf.split("_"); + isUsed = fileContents.some((content) => { + return leafParts.every((p) => content.includes(p)); }); } @@ -67,11 +67,13 @@ function findUnusedKeys() { if (unusedKeys.length > 0) { console.log(`Found ${unusedKeys.length} potentially unused i18n keys:\n`); - unusedKeys.forEach(k => console.log(`- ${k}`)); - console.log('\nNote: Some dynamic keys might be incorrectly flagged if they are constructed in complex ways.'); + unusedKeys.forEach((k) => console.log(`- ${k}`)); + console.log( + "\nNote: Some dynamic keys might be incorrectly flagged if they are constructed in complex ways.", + ); // Don't exit with error code so it doesn't fail CI if wired later } else { - console.log('No unused i18n keys found! 🎉'); + console.log("No unused i18n keys found! 🎉"); } } diff --git a/src/__tests__/hooks/usePlatformFee.test.tsx b/src/__tests__/hooks/usePlatformFee.test.tsx index 0fd932de..f5f31141 100644 --- a/src/__tests__/hooks/usePlatformFee.test.tsx +++ b/src/__tests__/hooks/usePlatformFee.test.tsx @@ -1,7 +1,11 @@ import { renderHook, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import type { ReactNode } from "react"; -import { usePlatformFee, DEFAULT_PLATFORM_FEE_BPS, PLATFORM_FEE_QUERY_KEY } from "@/hooks/usePlatformFee"; +import { + usePlatformFee, + DEFAULT_PLATFORM_FEE_BPS, + PLATFORM_FEE_QUERY_KEY, +} from "@/hooks/usePlatformFee"; jest.mock("@/lib/contractClient", () => ({ getPlatformFee: jest.fn(), diff --git a/src/app/api/admin-audit-log/route.ts b/src/app/api/admin-audit-log/route.ts index 92f581f2..6fe1a1b6 100644 --- a/src/app/api/admin-audit-log/route.ts +++ b/src/app/api/admin-audit-log/route.ts @@ -5,10 +5,7 @@ import { NextResponse } from "next/server"; export const runtime = "nodejs"; type AdminAuditAction = - | "verify_campaign" - | "reject_campaign" - | "update_platform_fee" - | "transfer_admin"; + "verify_campaign" | "reject_campaign" | "update_platform_fee" | "transfer_admin"; interface AdminAuditLogEntry { adminAddress: string; diff --git a/src/components/CampaignMap.tsx b/src/components/CampaignMap.tsx index 3d6cc5bc..3e959ea3 100644 --- a/src/components/CampaignMap.tsx +++ b/src/components/CampaignMap.tsx @@ -83,9 +83,7 @@ export default function CampaignMap({ campaigns }: CampaignMapProps) {

{t("emptyTitle")}

-

- {t("emptyBody")} -

+

{t("emptyBody")}

); } diff --git a/src/components/CancelDonationBanner.tsx b/src/components/CancelDonationBanner.tsx index 900e9823..13b6d13c 100644 --- a/src/components/CancelDonationBanner.tsx +++ b/src/components/CancelDonationBanner.tsx @@ -1,7 +1,7 @@ -'use client'; +"use client"; -import React, { useState, useEffect } from 'react'; -import { PendingDonation } from '../hooks/useDonationGracePeriod'; +import React, { useState, useEffect } from "react"; +import { PendingDonation } from "../hooks/useDonationGracePeriod"; interface CancelDonationBannerProps { pendingDonations: PendingDonation[]; @@ -31,10 +31,7 @@ export function CancelDonationBanner({ className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 max-w-md w-full px-4" > {pendingDonations.map((donation) => { - const remainingSeconds = Math.max( - 0, - Math.ceil((donation.expiresAt - Date.now()) / 1000) - ); + const remainingSeconds = Math.max(0, Math.ceil((donation.expiresAt - Date.now()) / 1000)); return (
-

- {t("emptyMessage")} -

+

{t("emptyMessage")}

) : (