Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions PRE_COMMIT_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ Extended pre-commit hook to run typecheck and affected tests.
- Performance tips
- Troubleshooting



## How It Works

### 1. Get Staged Files
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 16 additions & 16 deletions scripts/check-i18n.mjs
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -36,19 +36,19 @@
}

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 notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused variable namespace.

// Check if the key appears in the source code
// It could be t('key') or t("key") or next-intl dynamic keys
Expand All @@ -61,17 +61,17 @@
}

// 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.");
}
}

Expand Down
44 changes: 23 additions & 21 deletions scripts/find-unused-i18n-keys.js
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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];
Expand All @@ -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));
});
}

Expand All @@ -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! 🎉");
}
}

Expand Down
6 changes: 5 additions & 1 deletion src/__tests__/hooks/usePlatformFee.test.tsx
Original file line number Diff line number Diff line change
@@ -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(),
Expand Down
5 changes: 1 addition & 4 deletions src/app/api/admin-audit-log/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 1 addition & 3 deletions src/components/CampaignMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,7 @@ export default function CampaignMap({ campaigns }: CampaignMapProps) {
<h3 className="text-lg font-semibold text-zinc-900 dark:text-zinc-50 mb-2">
{t("emptyTitle")}
</h3>
<p className="text-zinc-600 dark:text-zinc-400 max-w-md">
{t("emptyBody")}
</p>
<p className="text-zinc-600 dark:text-zinc-400 max-w-md">{t("emptyBody")}</p>
</div>
);
}
Expand Down
11 changes: 4 additions & 7 deletions src/components/CancelDonationBanner.tsx
Original file line number Diff line number Diff line change
@@ -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[];
Expand Down Expand Up @@ -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 (
<div
Expand Down
12 changes: 5 additions & 7 deletions src/components/ContributorLeaderboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,7 @@ export default function ContributorLeaderboard({

{contributors.length === 0 ? (
<div className="text-center py-6 border border-dashed border-zinc-200 dark:border-zinc-700 rounded-lg">
<p className="text-sm text-zinc-600 dark:text-zinc-400">
{t("emptyMessage")}
</p>
<p className="text-sm text-zinc-600 dark:text-zinc-400">{t("emptyMessage")}</p>
</div>
) : (
<ul className="space-y-2.5" aria-label="Top supporters list">
Expand All @@ -113,7 +111,9 @@ export default function ContributorLeaderboard({
<p className="font-mono text-zinc-800 dark:text-zinc-200 truncate flex items-center gap-1.5">
<span>{item.truncatedAddress}</span>
{(() => {
const amountXlm = item.totalAmountStroops ? Number(item.totalAmountStroops) / 10_000_000 : 0;
const amountXlm = item.totalAmountStroops
? Number(item.totalAmountStroops) / 10_000_000
: 0;
const profile = calculateGamificationProfile(amountXlm);
return (
<span className="px-1.5 py-0.5 rounded bg-rose-500/10 text-rose-600 dark:text-rose-400 text-[10px] font-sans font-medium border border-rose-500/20">
Expand Down Expand Up @@ -164,9 +164,7 @@ export default function ContributorLeaderboard({

<p className="text-[11px] text-zinc-400 dark:text-zinc-500 leading-tight flex items-start gap-1">
<ShieldCheck className="w-3.5 h-3.5 text-zinc-400 shrink-0 mt-0.5" />
<span>
{t("optOutTooltip")}
</span>
<span>{t("optOutTooltip")}</span>
</p>
</div>
</div>
Expand Down
21 changes: 12 additions & 9 deletions src/components/DonatorBadges.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'use client';
"use client";

import React from 'react';
import { calculateGamificationProfile } from '../lib/gamification';
import { useTranslations } from 'next-intl';
import React from "react";
import { calculateGamificationProfile } from "../lib/gamification";
import { useTranslations } from "next-intl";

interface DonatorBadgesProps {
totalDonated: number;
Expand All @@ -15,8 +15,8 @@ export function DonatorBadges({
donationCount = 0,
isEarlyBacker = false,
}: DonatorBadgesProps) {
const t = useTranslations('DonatorBadges');
const tGamification = useTranslations('Gamification');
const t = useTranslations("DonatorBadges");
const tGamification = useTranslations("Gamification");
const profile = calculateGamificationProfile(totalDonated, donationCount, isEarlyBacker);

return (
Expand All @@ -26,7 +26,10 @@ export function DonatorBadges({
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-xs font-semibold px-2.5 py-1 rounded-full bg-rose-500/20 text-rose-400 border border-rose-500/30">
{t("level", { levelNumber: profile.levelNumber, levelName: tGamification(`level_${profile.levelId}`) })}
{t("level", {
levelNumber: profile.levelNumber,
levelName: tGamification(`level_${profile.levelId}`),
})}
</span>
<span className="text-xs text-slate-400">
{t("totalXlm", { amount: profile.totalDonated })}
Expand Down Expand Up @@ -60,14 +63,14 @@ export function DonatorBadges({
className={`flex items-center gap-2.5 p-2.5 rounded-xl border transition-all ${
badge.unlocked
? `${badge.color} shadow-sm`
: 'bg-slate-900/40 text-slate-500 border-slate-800/60 opacity-60'
: "bg-slate-900/40 text-slate-500 border-slate-800/60 opacity-60"
}`}
>
<span className="text-xl leading-none">{badge.icon}</span>
<div className="flex flex-col min-w-0">
<span className="text-xs font-semibold truncate">{tGamification(badge.name)}</span>
<span className="text-[10px] text-slate-400 truncate">
{badge.unlocked ? tGamification(badge.description) : tGamification('locked')}
{badge.unlocked ? tGamification(badge.description) : tGamification("locked")}
</span>
</div>
</div>
Expand Down
15 changes: 9 additions & 6 deletions src/components/NotificationSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,15 @@ export default function NotificationSettings() {
[publicKey],
);

const PREF_LABELS: Record<keyof NotificationPreferences, string> = useMemo(() => ({
contributions: t("prefContributions"),
verified: t("prefVerified"),
refundAvailable: t("prefRefundAvailable"),
revenueDeposited: t("prefRevenueDeposited"),
}), [t]);
const PREF_LABELS: Record<keyof NotificationPreferences, string> = useMemo(
() => ({
contributions: t("prefContributions"),
verified: t("prefVerified"),
refundAvailable: t("prefRefundAvailable"),
revenueDeposited: t("prefRevenueDeposited"),
}),
[t],
);
const [localPrefs, setLocalPrefs] = useState<NotificationPreferences | null>(null);

const prefs = localPrefs ?? storedPrefs;
Expand Down
20 changes: 18 additions & 2 deletions src/components/WalletContext.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
"use client";
import * as StellarSdk from "@stellar/stellar-sdk";
import { getAddress, getNetwork, isConnected, isAllowed } from "@stellar/freighter-api";
import React, { createContext, useContext, useEffect, useState, useMemo, ReactNode, useRef } from "react";
import React, {
createContext,
useContext,
useEffect,
useState,
useMemo,
ReactNode,
useRef,
} from "react";
import { useToast } from "./ToastProvider";
import { useQueryClient } from "@tanstack/react-query";
import { IS_MOCK_MODE } from "@/lib/runtimeEnv";
Expand Down Expand Up @@ -370,7 +378,15 @@ export const WalletProvider = ({ children }: { children: ReactNode }) => {
isSocialLoginAvailable: isSocialLoginConfigured(),
connectWithSocial,
}),
[publicKey, isWalletConnected, walletNetworkWarning, isLoading, walletKind, socialProfile, connectWithSocial]
[
publicKey,
isWalletConnected,
walletNetworkWarning,
isLoading,
walletKind,
socialProfile,
connectWithSocial,
],
);

return (
Expand Down
Loading
Loading