Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
25 changes: 25 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,8 @@
"labelDescription": "Description",
"placeholderDescription": "Describe your campaign, what it aims to achieve, and how funds will be used",
"labelCreatorEmailOptional": "Creator Email (Optional)",
"labelCoverImageUrl": "Cover Image URL",
"optional": "(optional)",
"placeholderCreatorEmail": "name@example.com",
"creatorEmailNote": "Optional, off-chain only. Used for campaign milestone and deadline notifications.",
"labelFundingGoal": "Funding Goal (XLM)",
Expand All @@ -288,6 +290,8 @@
"revenueSharingDesc": "Allow contributors to earn a share of future revenue.",
"revenueSharingAriaLabel": "Revenue Sharing",
"labelRevenueSharePct": "Revenue Share Percentage",
"labelExactBps": "Exact bps:",
"revenueShareBpsDenominator": "/ 10 000",
"cancel": "Cancel",
"launchCampaign": "Launch Campaign",
"reviewTitle": "Review Campaign Before Signing",
Expand Down Expand Up @@ -321,6 +325,9 @@
"labelDescriptionLanguage": "Description Language",
"tabEnglish": "English",
"tabSpanish": "Spanish (optional)",
"tabWrite": "Write",
"tabPreview": "Preview",
"nothingToPreview": "Nothing to preview",
"placeholderDescriptionEs": "Describe tu campaΓ±a en espaΓ±ol (opcional)",
"validationDescriptionEsTooLong": "Spanish description must be 1,000 characters or fewer."
},
Expand Down Expand Up @@ -557,6 +564,24 @@
"nextXlm": "Next: {amount} XLM",
"earnedBadges": "Earned Badges"
},
"Notifications": {
"title": "Notifications",
"justNow": "Just now",
"mAgo": "{count}m ago",
"hAgo": "{count}h ago",
"dAgo": "{count}d ago",
"unreadLabel": "{count} unread notifications",
"markAllRead": "Mark all read",
"noNotifications": "No notifications yet",
"connectWallet": "Connect wallet to see notifications",
"viewDetails": "View details",
"settingsTitle": "Notification Preferences",
"settingsAriaLabel": "Open notification settings",
"prefContributions": "Contribution confirmations",
"prefVerified": "Campaign verified",
"prefRefundAvailable": "Refund available",
"prefRevenueDeposited": "Revenue deposited"
},
"CauseDetail": {
"noDescription": "No description provided by the creator."
},
Expand Down
25 changes: 25 additions & 0 deletions messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,8 @@
"labelDescription": "DescripciΓ³n",
"placeholderDescription": "Describe tu campaΓ±a, quΓ© pretende lograr y cΓ³mo se utilizarΓ‘n los fondos",
"labelCreatorEmailOptional": "Email del Creador (Opcional)",
"labelCoverImageUrl": "URL de la Imagen de Portada",
"optional": "(opcional)",
"placeholderCreatorEmail": "nombre@ejemplo.com",
"creatorEmailNote": "Opcional, solo fuera de la cadena. Se usa para notificaciones de hitos y vencimiento de campaΓ±a.",
"labelFundingGoal": "Meta de FinanciaciΓ³n (XLM)",
Expand All @@ -288,6 +290,8 @@
"revenueSharingDesc": "Permite a los contribuyentes ganar una parte de los ingresos futuros.",
"revenueSharingAriaLabel": "ParticipaciΓ³n en Ingresos",
"labelRevenueSharePct": "Porcentaje de ParticipaciΓ³n en Ingresos",
"labelExactBps": "Puntos base exactos:",
"revenueShareBpsDenominator": "/ 10.000",
"cancel": "Cancelar",
"launchCampaign": "Lanzar CampaΓ±a",
"reviewTitle": "Revisar CampaΓ±a Antes de Firmar",
Expand Down Expand Up @@ -321,6 +325,9 @@
"labelDescriptionLanguage": "Idioma de la descripciΓ³n",
"tabEnglish": "InglΓ©s",
"tabSpanish": "EspaΓ±ol (opcional)",
"tabWrite": "Escribir",
"tabPreview": "Vista previa",
"nothingToPreview": "Nada que previsualizar",
"placeholderDescriptionEs": "Describe tu campaΓ±a en espaΓ±ol (opcional)",
"validationDescriptionEsTooLong": "La descripciΓ³n en espaΓ±ol debe tener 1.000 caracteres o menos."
},
Expand Down Expand Up @@ -557,6 +564,24 @@
"nextXlm": "Siguiente: {amount} XLM",
"earnedBadges": "Insignias Ganadas"
},
"Notifications": {
"title": "Notificaciones",
"justNow": "Justo ahora",
"mAgo": "hace {count}m",
"hAgo": "hace {count}h",
"dAgo": "hace {count}d",
"unreadLabel": "{count} notificaciones no leΓ­das",
"markAllRead": "Marcar todo como leΓ­do",
"noNotifications": "Sin notificaciones aΓΊn",
"connectWallet": "Conecta tu billetera para ver notificaciones",
"viewDetails": "Ver detalles",
"settingsTitle": "Preferencias de Notificaciones",
"settingsAriaLabel": "Abrir configuraciΓ³n de notificaciones",
"prefContributions": "Confirmaciones de contribuciΓ³n",
"prefVerified": "CampaΓ±a verificada",
"prefRefundAvailable": "Reembolso disponible",
"prefRevenueDeposited": "Ingresos depositados"
},
"CauseDetail": {
"noDescription": "El creador no proporcionΓ³ una descripciΓ³n."
},
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
Loading
Loading