Skip to content
Closed
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"type-check": "tsc --noEmit",
"validate:ui": "node scripts/validate-ui.js",
"validate:web3": "node scripts/validate-web3.js",
"validate": "npm run validate:ui && npm run validate:web3"
"validate:i18n": "node scripts/validate-i18n.js",
"validate": "npm run validate:ui && npm run validate:web3 && npm run validate:i18n"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
Expand Down
111 changes: 111 additions & 0 deletions scripts/validate-i18n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* validate-i18n.js — Build-time translation key validation
*
* Loads en.json as the reference and compares every other locale file
* in src/locales/ to ensure key parity. Exits with code 1 if any
* locale has missing or extra keys.
*
* Usage: node scripts/validate-i18n.js
*/

import { readFileSync, readdirSync } from 'fs';
import { join, basename } from 'path';
import { fileURLToPath } from 'url';

const __dirname = fileURLToPath(new URL('.', import.meta.url));
const LOCALES_DIR = join(__dirname, '..', 'src', 'locales');
const REFERENCE_LANG = 'en';

// ── helpers ──────────────────────────────────────────────────────────

/** Recursively collect every leaf-key path from a nested object. */
function getKeys(obj, prefix = '') {
const keys = [];
for (const key of Object.keys(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof obj[key] === 'object' && obj[key] !== null) {
keys.push(...getKeys(obj[key], fullKey));
} else {
keys.push(fullKey);
}
}
return keys;
}

// ── main ─────────────────────────────────────────────────────────────

function main() {
// Discover locale JSON files
const files = readdirSync(LOCALES_DIR).filter(
(f) => f.endsWith('.json')
);

if (files.length === 0) {
console.error('❌ No locale JSON files found in', LOCALES_DIR);
process.exit(1);
}

// Load reference
const refFile = `${REFERENCE_LANG}.json`;
if (!files.includes(refFile)) {
console.error(`❌ Reference locale file "${refFile}" not found`);
process.exit(1);
}

const refPath = join(LOCALES_DIR, refFile);
const refData = JSON.parse(readFileSync(refPath, 'utf-8'));
const refKeys = getKeys(refData).sort();

console.log(`\n📖 Reference: ${refFile} (${refKeys.length} keys)\n`);

let hasErrors = false;

for (const file of files) {
if (file === refFile) continue;

const lang = basename(file, '.json');
const filePath = join(LOCALES_DIR, file);

let data;
try {
data = JSON.parse(readFileSync(filePath, 'utf-8'));
} catch (err) {
console.error(`❌ ${lang}: Failed to parse ${file} — ${err.message}`);
hasErrors = true;
continue;
}

const langKeys = getKeys(data).sort();

const missing = refKeys.filter((k) => !langKeys.includes(k));
const extra = langKeys.filter((k) => !refKeys.includes(k));

if (missing.length === 0 && extra.length === 0) {
console.log(` ✅ ${lang}: All ${refKeys.length} keys present`);
} else {
hasErrors = true;

if (missing.length > 0) {
console.error(` ❌ ${lang}: ${missing.length} MISSING key(s):`);
missing.forEach((k) => console.error(` - ${k}`));
}

if (extra.length > 0) {
console.warn(` ⚠️ ${lang}: ${extra.length} EXTRA key(s):`);
extra.forEach((k) => console.warn(` + ${k}`));
}
}
}

console.log('');

if (hasErrors) {
console.error('❌ Translation validation FAILED — fix the issues above.\n');
process.exit(1);
}

console.log('✅ All translations are complete and in sync.\n');
process.exit(0);
}

main();
11 changes: 9 additions & 2 deletions src/hooks/useInternationalization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,16 @@ export function I18nProvider({
// Translation function
const t = useCallback(
(key: string, params?: Record<string, string | number>) => {
return getTranslation(translations, key, params);
const result = getTranslation(translations, key, params);

// Warn in development when a key resolves to its raw path (missing translation)
if (process.env.NODE_ENV === 'development' && result === key) {
console.warn(`[i18n] Missing translation key: "${key}" for language "${language}"`);
}

return result;
},
[translations],
[translations, language],
);

// Formatting functions
Expand Down
3 changes: 2 additions & 1 deletion src/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"submit": "إرسال",
"confirm": "تأكيد",
"yes": "نعم",
"no": "لا"
"no": "لا",
"welcome": "مرحباً"
},
"navigation": {
"home": "الرئيسية",
Expand Down
89 changes: 89 additions & 0 deletions src/locales/de.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{
"common": {
"loading": "Laden...",
"error": "Fehler",
"success": "Erfolg",
"cancel": "Abbrechen",
"save": "Speichern",
"delete": "Löschen",
"edit": "Bearbeiten",
"create": "Erstellen",
"search": "Suchen",
"filter": "Filtern",
"close": "Schließen",
"back": "Zurück",
"next": "Weiter",
"previous": "Zurück",
"submit": "Absenden",
"confirm": "Bestätigen",
"yes": "Ja",
"no": "Nein",
"welcome": "Willkommen"
},
"navigation": {
"home": "Startseite",
"courses": "Kurse",
"dashboard": "Dashboard",
"profile": "Profil",
"settings": "Einstellungen",
"messages": "Nachrichten",
"notifications": "Benachrichtigungen",
"logout": "Abmelden",
"login": "Anmelden",
"signup": "Registrieren"
},
"course": {
"title": "Kurs",
"enroll": "Einschreiben",
"enrolled": "Eingeschrieben",
"progress": "Fortschritt",
"lessons": "Lektionen",
"duration": "Dauer",
"instructor": "Dozent",
"reviews": "Bewertungen",
"rating": "Bewertung",
"description": "Beschreibung",
"syllabus": "Lehrplan",
"resources": "Ressourcen"
},
"dashboard": {
"welcome": "Willkommen",
"myCourses": "Meine Kurse",
"recentActivity": "Letzte Aktivität",
"upcomingDeadlines": "Anstehende Fristen",
"recommendedCourses": "Empfohlene Kurse",
"learningStreak": "Lernserie",
"progressSummary": "Fortschrittsübersicht"
},
"profile": {
"editProfile": "Profil Bearbeiten",
"preferences": "Einstellungen",
"language": "Sprache",
"theme": "Design",
"notifications": "Benachrichtigungen",
"privacy": "Datenschutz",
"account": "Konto"
},
"i18n": {
"selectLanguage": "Sprache Auswählen",
"currentLanguage": "Aktuelle Sprache",
"languageChanged": "Sprache erfolgreich geändert",
"rtlMode": "Rechts-nach-Links-Modus",
"ltrMode": "Links-nach-Rechts-Modus"
},
"validation": {
"required": "Dieses Feld ist erforderlich",
"email": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
"minLength": "Mindestlänge beträgt {{min}} Zeichen",
"maxLength": "Maximale Länge beträgt {{max}} Zeichen",
"passwordMismatch": "Passwörter stimmen nicht überein"
},
"errors": {
"generic": "Ein Fehler ist aufgetreten",
"network": "Netzwerkfehler. Bitte überprüfen Sie Ihre Verbindung",
"notFound": "Nicht gefunden",
"unauthorized": "Nicht autorisiert",
"forbidden": "Verboten",
"serverError": "Serverfehler"
}
}
3 changes: 2 additions & 1 deletion src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"submit": "Submit",
"confirm": "Confirm",
"yes": "Yes",
"no": "No"
"no": "No",
"welcome": "Welcome"
},
"navigation": {
"home": "Home",
Expand Down
3 changes: 2 additions & 1 deletion src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"submit": "Enviar",
"confirm": "Confirmar",
"yes": "Sí",
"no": "No"
"no": "No",
"welcome": "Bienvenido"
},
"navigation": {
"home": "Inicio",
Expand Down
89 changes: 89 additions & 0 deletions src/locales/fr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{
"common": {
"loading": "Chargement...",
"error": "Erreur",
"success": "Succès",
"cancel": "Annuler",
"save": "Enregistrer",
"delete": "Supprimer",
"edit": "Modifier",
"create": "Créer",
"search": "Rechercher",
"filter": "Filtrer",
"close": "Fermer",
"back": "Retour",
"next": "Suivant",
"previous": "Précédent",
"submit": "Soumettre",
"confirm": "Confirmer",
"yes": "Oui",
"no": "Non",
"welcome": "Bienvenue"
},
"navigation": {
"home": "Accueil",
"courses": "Cours",
"dashboard": "Tableau de bord",
"profile": "Profil",
"settings": "Paramètres",
"messages": "Messages",
"notifications": "Notifications",
"logout": "Déconnexion",
"login": "Connexion",
"signup": "S'inscrire"
},
"course": {
"title": "Cours",
"enroll": "S'inscrire",
"enrolled": "Inscrit",
"progress": "Progression",
"lessons": "Leçons",
"duration": "Durée",
"instructor": "Instructeur",
"reviews": "Avis",
"rating": "Note",
"description": "Description",
"syllabus": "Programme",
"resources": "Ressources"
},
"dashboard": {
"welcome": "Bienvenue",
"myCourses": "Mes Cours",
"recentActivity": "Activité Récente",
"upcomingDeadlines": "Échéances à Venir",
"recommendedCourses": "Cours Recommandés",
"learningStreak": "Série d'Apprentissage",
"progressSummary": "Résumé de Progression"
},
"profile": {
"editProfile": "Modifier le Profil",
"preferences": "Préférences",
"language": "Langue",
"theme": "Thème",
"notifications": "Notifications",
"privacy": "Confidentialité",
"account": "Compte"
},
"i18n": {
"selectLanguage": "Sélectionner la Langue",
"currentLanguage": "Langue Actuelle",
"languageChanged": "Langue modifiée avec succès",
"rtlMode": "Mode Droite à Gauche",
"ltrMode": "Mode Gauche à Droite"
},
"validation": {
"required": "Ce champ est obligatoire",
"email": "Veuillez entrer une adresse e-mail valide",
"minLength": "La longueur minimale est de {{min}} caractères",
"maxLength": "La longueur maximale est de {{max}} caractères",
"passwordMismatch": "Les mots de passe ne correspondent pas"
},
"errors": {
"generic": "Une erreur est survenue",
"network": "Erreur réseau. Veuillez vérifier votre connexion",
"notFound": "Non trouvé",
"unauthorized": "Non autorisé",
"forbidden": "Interdit",
"serverError": "Erreur du serveur"
}
}
Loading
Loading