Major Changes: - Moved legacy system to Legacy/ folder for archival - Built new modular architecture with strict separation of concerns - Created core system: Module, EventBus, ModuleLoader, Router - Added Application bootstrap with auto-start functionality - Implemented development server with ES6 modules support - Created comprehensive documentation and project context - Converted SBS-7-8 content to JSON format - Copied all legacy games and content to new structure New Architecture Features: - Sealed modules with WeakMap private data - Strict dependency injection system - Event-driven communication only - Inviolable responsibility patterns - Auto-initialization without commands - Component-based UI foundation ready Technical Stack: - Vanilla JS/HTML/CSS only - ES6 modules with proper imports/exports - HTTP development server (no file:// protocol) - Modular CSS with component scoping - Comprehensive error handling and debugging Ready for Phase 2: Converting legacy modules to new architecture 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
561 lines
18 KiB
JavaScript
561 lines
18 KiB
JavaScript
// === JSON CONTENT LOADER ===
|
|
// Transforme les contenus JSON en format compatible avec les jeux existants
|
|
|
|
class JSONContentLoader {
|
|
constructor() {
|
|
this.loadedContent = new Map();
|
|
}
|
|
|
|
/**
|
|
* Charge et adapte un contenu JSON pour les jeux
|
|
* @param {Object} jsonContent - Le contenu JSON brut
|
|
* @returns {Object} - Contenu adapté au format legacy
|
|
*/
|
|
adapt(jsonContent) {
|
|
if (!jsonContent) {
|
|
logSh('⚠️ JSONContentLoader - Contenu vide reçu', 'WARN');
|
|
return this.createEmptyContent();
|
|
}
|
|
|
|
logSh(`🔄 JSONContentLoader - Adaptation du contenu: ${jsonContent.name || 'Sans nom'}`, 'INFO');
|
|
|
|
// Créer l'objet de base au format legacy avec nouvelles métadonnées
|
|
const adaptedContent = {
|
|
// Métadonnées ultra-modulaires
|
|
id: jsonContent.id,
|
|
name: jsonContent.name || 'Contenu Sans Nom',
|
|
description: jsonContent.description || '',
|
|
|
|
// Difficulty system (1-10 scale support)
|
|
difficulty: this.adaptDifficulty(jsonContent.difficulty_level || jsonContent.difficulty),
|
|
difficulty_level: jsonContent.difficulty_level, // Preserve numeric scale
|
|
|
|
// Language system (original_lang/user_lang pattern)
|
|
language: jsonContent.user_lang || jsonContent.language || 'english',
|
|
original_lang: jsonContent.original_lang,
|
|
user_lang: jsonContent.user_lang,
|
|
|
|
// Icon system with fallback
|
|
icon: this.adaptIcon(jsonContent.icon, jsonContent.fallback_icon),
|
|
|
|
// New metadata fields
|
|
tags: jsonContent.tags || [],
|
|
skills_covered: jsonContent.skills_covered || [],
|
|
prerequisites: jsonContent.prerequisites || [],
|
|
estimated_duration: jsonContent.estimated_duration,
|
|
target_audience: jsonContent.target_audience || {},
|
|
pedagogical_approach: jsonContent.pedagogical_approach,
|
|
|
|
// === VOCABULAIRE ===
|
|
vocabulary: this.adaptVocabulary(jsonContent.vocabulary),
|
|
|
|
// === PHRASES ET SENTENCES ===
|
|
sentences: this.adaptSentences(jsonContent.sentences),
|
|
|
|
// === STORY STRUCTURE (Dragon's Pearl format) ===
|
|
story: jsonContent.story ? this.adaptStory(jsonContent.story) : null,
|
|
|
|
// === TEXTES ===
|
|
texts: this.adaptTexts(jsonContent.texts),
|
|
|
|
// === DIALOGUES ===
|
|
dialogues: this.adaptDialogues(jsonContent.dialogues),
|
|
|
|
// === GRAMMAIRE ===
|
|
grammar: this.adaptGrammar(jsonContent.grammar),
|
|
|
|
// === AUDIO ===
|
|
audio: this.adaptAudio(jsonContent.audio),
|
|
|
|
// === POÈMES ET CULTURE ===
|
|
poems: this.adaptPoems(jsonContent.poems),
|
|
|
|
// === EXERCICES AVANCÉS ===
|
|
fillInBlanks: this.adaptFillInBlanks(jsonContent.fillInBlanks),
|
|
corrections: this.adaptCorrections(jsonContent.corrections),
|
|
comprehension: this.adaptComprehension(jsonContent.comprehension),
|
|
matching: this.adaptMatching(jsonContent.matching),
|
|
|
|
// === EXERCICES GÉNÉRIQUES ===
|
|
exercises: this.adaptExercises(jsonContent.exercises),
|
|
|
|
// === CULTURE CONTENT ===
|
|
culture: this.adaptCulture(jsonContent.culture),
|
|
|
|
// === PARAMETRIC SENTENCES ===
|
|
parametric_sentences: this.adaptParametricSentences(jsonContent.parametric_sentences),
|
|
|
|
// === LISTENING (format SBS) ===
|
|
listening: this.adaptListening(jsonContent.listening),
|
|
|
|
// === MÉTADONNÉES DE COMPATIBILITÉ ===
|
|
_adapted: true,
|
|
_source: 'json',
|
|
_adaptedAt: new Date().toISOString()
|
|
};
|
|
|
|
// Nettoyage des valeurs undefined
|
|
this.cleanUndefinedValues(adaptedContent);
|
|
|
|
logSh(`✅ JSONContentLoader - Contenu adapté avec succès`, 'INFO');
|
|
logSh(`📊 Stats: ${Object.keys(adaptedContent.vocabulary || {}).length} mots, ${(adaptedContent.sentences || []).length} phrases`, 'INFO');
|
|
|
|
return adaptedContent;
|
|
}
|
|
|
|
/**
|
|
* Adapte le vocabulaire (format JSON ultra-modulaire → Legacy)
|
|
* Supports 6 levels of vocabulary complexity from ultra_commented specification
|
|
*/
|
|
adaptVocabulary(vocabulary) {
|
|
if (!vocabulary || typeof vocabulary !== 'object') {
|
|
return {};
|
|
}
|
|
|
|
const adapted = {};
|
|
|
|
for (const [word, definition] of Object.entries(vocabulary)) {
|
|
if (typeof definition === 'string') {
|
|
// Level 1: Simple string "cat": "chat"
|
|
adapted[word] = definition;
|
|
} else if (typeof definition === 'object') {
|
|
// Levels 2-6: Rich vocabulary objects
|
|
adapted[word] = {
|
|
// Core translation (supports original_lang/user_lang pattern)
|
|
translation: definition.user_language || definition.translation || definition.french || definition.chinese || '',
|
|
original: definition.original_language || word,
|
|
english: word,
|
|
|
|
// Metadata
|
|
type: definition.type || 'word',
|
|
|
|
// Pronunciation (IPA format support)
|
|
pronunciation: definition.pronunciation || definition.prononciation,
|
|
ipa: definition.ipa, // IPA phonetic notation
|
|
audio: definition.audio_file || definition.audio || definition.pronunciation,
|
|
|
|
// Learning data
|
|
examples: definition.examples || definition.example_sentences,
|
|
grammarNotes: definition.grammar_notes || definition.grammarNotes,
|
|
usage_context: definition.usage_context,
|
|
|
|
// Advanced features (Level 4+)
|
|
etymology: definition.etymology,
|
|
word_family: definition.word_family,
|
|
|
|
// Cultural context (Level 5+)
|
|
cultural_significance: definition.cultural_significance,
|
|
regional_usage: definition.regional_usage,
|
|
|
|
// Memory aids (Level 6)
|
|
memory_techniques: definition.memory_techniques,
|
|
visual_associations: definition.visual_associations,
|
|
|
|
// Legacy compatibility
|
|
french: definition.french || definition.user_language || definition.translation,
|
|
chinese: definition.chinese || definition.user_language || definition.translation,
|
|
image: definition.image || definition.visual_aid,
|
|
difficulty: definition.difficulty_level
|
|
};
|
|
}
|
|
}
|
|
|
|
return adapted;
|
|
}
|
|
|
|
/**
|
|
* Adapte les phrases
|
|
*/
|
|
adaptSentences(sentences) {
|
|
if (!Array.isArray(sentences)) {
|
|
return [];
|
|
}
|
|
|
|
return sentences.map(sentence => {
|
|
if (typeof sentence === 'string') {
|
|
return { english: sentence, translation: '' };
|
|
}
|
|
|
|
return {
|
|
english: sentence.english || '',
|
|
french: sentence.french || sentence.translation,
|
|
chinese: sentence.chinese || sentence.translation,
|
|
translation: sentence.translation || sentence.french || sentence.chinese,
|
|
prononciation: sentence.prononciation || sentence.pinyin,
|
|
audio: sentence.audio,
|
|
difficulty: sentence.difficulty
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Adapte les textes
|
|
*/
|
|
adaptTexts(texts) {
|
|
if (!Array.isArray(texts)) {
|
|
return [];
|
|
}
|
|
|
|
return texts.map(text => ({
|
|
title: text.title || 'Sans titre',
|
|
content: text.content || '',
|
|
translation: text.translation || '',
|
|
french: text.french || text.translation,
|
|
chinese: text.chinese || text.translation,
|
|
audio: text.audio,
|
|
difficulty: text.difficulty
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Adapte les dialogues
|
|
*/
|
|
adaptDialogues(dialogues) {
|
|
if (!Array.isArray(dialogues)) {
|
|
return [];
|
|
}
|
|
|
|
return dialogues.map(dialogue => ({
|
|
title: dialogue.title || 'Dialogue',
|
|
conversation: Array.isArray(dialogue.conversation)
|
|
? dialogue.conversation.map(line => ({
|
|
speaker: line.speaker || 'Speaker',
|
|
english: line.english || '',
|
|
french: line.french || line.translation,
|
|
chinese: line.chinese || line.translation,
|
|
translation: line.translation || line.french || line.chinese,
|
|
prononciation: line.prononciation || line.pinyin,
|
|
audio: line.audio
|
|
}))
|
|
: []
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Adapte la grammaire
|
|
*/
|
|
adaptGrammar(grammar) {
|
|
if (!grammar || typeof grammar !== 'object') {
|
|
return {};
|
|
}
|
|
|
|
// La grammaire reste en format objet (compatible)
|
|
return grammar;
|
|
}
|
|
|
|
/**
|
|
* Adapte le contenu audio
|
|
*/
|
|
adaptAudio(audio) {
|
|
if (!audio || typeof audio !== 'object') {
|
|
return {};
|
|
}
|
|
|
|
return {
|
|
withText: Array.isArray(audio.withText) ? audio.withText : [],
|
|
withoutText: Array.isArray(audio.withoutText) ? audio.withoutText : []
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Adapte les poèmes
|
|
*/
|
|
adaptPoems(poems) {
|
|
if (!Array.isArray(poems)) {
|
|
return [];
|
|
}
|
|
|
|
return poems.map(poem => ({
|
|
title: poem.title || 'Poème',
|
|
content: poem.content || '',
|
|
translation: poem.translation || '',
|
|
audioFile: poem.audioFile,
|
|
culturalContext: poem.culturalContext
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Adapte les exercices Fill-in-the-Blanks
|
|
*/
|
|
adaptFillInBlanks(fillInBlanks) {
|
|
if (!Array.isArray(fillInBlanks)) {
|
|
return [];
|
|
}
|
|
|
|
return fillInBlanks;
|
|
}
|
|
|
|
/**
|
|
* Adapte les exercices de correction
|
|
*/
|
|
adaptCorrections(corrections) {
|
|
if (!Array.isArray(corrections)) {
|
|
return [];
|
|
}
|
|
|
|
return corrections;
|
|
}
|
|
|
|
/**
|
|
* Adapte les exercices de compréhension
|
|
*/
|
|
adaptComprehension(comprehension) {
|
|
if (!Array.isArray(comprehension)) {
|
|
return [];
|
|
}
|
|
|
|
return comprehension;
|
|
}
|
|
|
|
/**
|
|
* Adapte les exercices de matching (supports multi-column system)
|
|
*/
|
|
adaptMatching(matching) {
|
|
if (!Array.isArray(matching)) {
|
|
return [];
|
|
}
|
|
|
|
return matching.map(exercise => {
|
|
// Handle traditional two-column format
|
|
if (exercise.leftColumn && exercise.rightColumn) {
|
|
return exercise;
|
|
}
|
|
|
|
// Handle new multi-column format with numeric IDs
|
|
if (exercise.columns && Array.isArray(exercise.columns)) {
|
|
return {
|
|
...exercise,
|
|
title: exercise.title || 'Matching Exercise',
|
|
type: exercise.type || 'multi_column',
|
|
columns: exercise.columns,
|
|
correct_matches: exercise.correct_matches || exercise.correctMatches || []
|
|
};
|
|
}
|
|
|
|
return exercise;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Adapte les exercices génériques
|
|
*/
|
|
adaptExercises(exercises) {
|
|
if (!exercises || typeof exercises !== 'object') {
|
|
return {};
|
|
}
|
|
|
|
return exercises;
|
|
}
|
|
|
|
/**
|
|
* Adapte le contenu listening (format SBS)
|
|
*/
|
|
adaptListening(listening) {
|
|
if (!listening || typeof listening !== 'object') {
|
|
return {};
|
|
}
|
|
|
|
return listening;
|
|
}
|
|
|
|
/**
|
|
* Nettoie les valeurs undefined de l'objet
|
|
*/
|
|
cleanUndefinedValues(obj) {
|
|
Object.keys(obj).forEach(key => {
|
|
if (obj[key] === undefined) {
|
|
delete obj[key];
|
|
} else if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
|
|
this.cleanUndefinedValues(obj[key]);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Adapte le système de difficulté (1-10 scale → legacy)
|
|
*/
|
|
adaptDifficulty(difficulty) {
|
|
if (typeof difficulty === 'number') {
|
|
// Convert 1-10 scale to legacy terms
|
|
if (difficulty <= 3) return 'easy';
|
|
if (difficulty <= 6) return 'medium';
|
|
if (difficulty <= 8) return 'hard';
|
|
return 'expert';
|
|
}
|
|
return difficulty || 'medium';
|
|
}
|
|
|
|
/**
|
|
* Adapte le système d'icônes avec fallback
|
|
*/
|
|
adaptIcon(icon, fallbackIcon) {
|
|
if (typeof icon === 'object') {
|
|
return icon.primary || icon.emoji || fallbackIcon || '📝';
|
|
}
|
|
return icon || fallbackIcon || '📝';
|
|
}
|
|
|
|
/**
|
|
* Adapte une structure story (Dragon's Pearl format)
|
|
*/
|
|
adaptStory(story) {
|
|
if (!story || typeof story !== 'object') {
|
|
return null;
|
|
}
|
|
|
|
logSh(`🐉 JSONContentLoader - Adaptation de la structure story: ${story.title}`, 'DEBUG');
|
|
|
|
const adaptedStory = {
|
|
title: story.title || 'Histoire Sans Titre',
|
|
totalSentences: story.totalSentences || 0,
|
|
chapters: []
|
|
};
|
|
|
|
if (story.chapters && Array.isArray(story.chapters)) {
|
|
logSh(`🐉 JSONContentLoader - ${story.chapters.length} chapitres trouvés`, 'DEBUG');
|
|
|
|
adaptedStory.chapters = story.chapters.map((chapter, index) => {
|
|
const adaptedChapter = {
|
|
title: chapter.title || `Chapitre ${index + 1}`,
|
|
sentences: []
|
|
};
|
|
|
|
if (chapter.sentences && Array.isArray(chapter.sentences)) {
|
|
logSh(`🐉 JSONContentLoader - Chapitre ${index}: ${chapter.sentences.length} phrases`, 'DEBUG');
|
|
|
|
adaptedChapter.sentences = chapter.sentences.map(sentence => ({
|
|
id: sentence.id,
|
|
original: sentence.original,
|
|
translation: sentence.translation,
|
|
pronunciation: sentence.pronunciation,
|
|
words: sentence.words || []
|
|
}));
|
|
}
|
|
|
|
return adaptedChapter;
|
|
});
|
|
|
|
const totalSentences = adaptedStory.chapters.reduce((sum, chapter) =>
|
|
sum + (chapter.sentences ? chapter.sentences.length : 0), 0);
|
|
|
|
logSh(`🐉 JSONContentLoader - Story adaptée: ${totalSentences} phrases au total`, 'INFO');
|
|
}
|
|
|
|
return adaptedStory;
|
|
}
|
|
|
|
/**
|
|
* Adapte le contenu culturel
|
|
*/
|
|
adaptCulture(culture) {
|
|
if (!culture || typeof culture !== 'object') {
|
|
return {};
|
|
}
|
|
return culture;
|
|
}
|
|
|
|
/**
|
|
* Adapte les phrases paramétriques
|
|
*/
|
|
adaptParametricSentences(parametricSentences) {
|
|
if (!Array.isArray(parametricSentences)) {
|
|
return [];
|
|
}
|
|
return parametricSentences;
|
|
}
|
|
|
|
/**
|
|
* Crée un contenu vide par défaut
|
|
*/
|
|
createEmptyContent() {
|
|
return {
|
|
id: 'empty_content_' + Date.now(),
|
|
name: 'Contenu Vide',
|
|
description: 'Aucun contenu disponible',
|
|
difficulty: 'easy',
|
|
difficulty_level: 1,
|
|
language: 'english',
|
|
user_lang: 'french',
|
|
original_lang: 'english',
|
|
tags: [],
|
|
skills_covered: [],
|
|
vocabulary: {},
|
|
sentences: [],
|
|
texts: [],
|
|
dialogues: [],
|
|
grammar: {},
|
|
exercises: {},
|
|
culture: {},
|
|
_adapted: true,
|
|
_source: 'empty',
|
|
_adaptedAt: new Date().toISOString()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Analyse la richesse du contenu et génère des statistiques
|
|
*/
|
|
analyzeContent(adaptedContent) {
|
|
const stats = {
|
|
vocabularyCount: Object.keys(adaptedContent.vocabulary || {}).length,
|
|
sentenceCount: (adaptedContent.sentences || []).length,
|
|
textCount: (adaptedContent.texts || []).length,
|
|
dialogueCount: (adaptedContent.dialogues || []).length,
|
|
grammarTopics: Object.keys(adaptedContent.grammar || {}).length,
|
|
audioContent: !!(adaptedContent.audio && (adaptedContent.audio.withText || adaptedContent.audio.withoutText)),
|
|
poemCount: (adaptedContent.poems || []).length,
|
|
exerciseTypes: Object.keys(adaptedContent.exercises || {}).length,
|
|
totalItems: 0
|
|
};
|
|
|
|
stats.totalItems = stats.vocabularyCount + stats.sentenceCount +
|
|
stats.textCount + stats.dialogueCount + stats.poemCount;
|
|
|
|
stats.richness = stats.totalItems > 50 ? 'rich' :
|
|
stats.totalItems > 20 ? 'medium' : 'basic';
|
|
|
|
return stats;
|
|
}
|
|
|
|
/**
|
|
* Teste si un objet est au format JSON (vs legacy JS)
|
|
*/
|
|
isJSONFormat(content) {
|
|
return content && (
|
|
content.hasOwnProperty('name') ||
|
|
content.hasOwnProperty('description') ||
|
|
content.hasOwnProperty('language') ||
|
|
content._adapted === true
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Point d'entrée principal - décide s'il faut adapter ou pas
|
|
*/
|
|
loadContent(content) {
|
|
if (!content) {
|
|
return this.createEmptyContent();
|
|
}
|
|
|
|
// Si déjà adapté, retourner tel quel
|
|
if (content._adapted === true) {
|
|
return content;
|
|
}
|
|
|
|
// Si format JSON, adapter
|
|
if (this.isJSONFormat(content)) {
|
|
return this.adapt(content);
|
|
}
|
|
|
|
// Sinon, c'est du format legacy, retourner tel quel
|
|
return content;
|
|
}
|
|
}
|
|
|
|
// Export global
|
|
window.JSONContentLoader = JSONContentLoader;
|
|
|
|
// Export Node.js (optionnel)
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = JSONContentLoader;
|
|
} |