confluent/ConfluentTranslator/confluentToFrench.js
StillHammer e8d17ab0d5 Implement radical lookup system for Confluent translator (83% → 92% coverage)
Major features:
- Radical-based word matching for conjugated verbs
- Morphological decomposition for compound words
- Multi-index search (byWord + byFormeLiee)
- Cascade search strategy with confidence scoring

New files:
- ConfluentTranslator/radicalMatcher.js: Extract radicals from conjugated forms
- ConfluentTranslator/morphologicalDecomposer.js: Decompose compound words
- ConfluentTranslator/plans/radical-lookup-system.md: Implementation plan
- ConfluentTranslator/test-results-radical-system.md: Test results and analysis
- ancien-confluent/lexique/00-grammaire.json: Grammar particles
- ancien-confluent/lexique/lowercase-confluent.js: Lowercase utility

Modified files:
- ConfluentTranslator/reverseIndexBuilder.js: Added byFormeLiee index
- ConfluentTranslator/confluentToFrench.js: Cascade search with radicals
- Multiple lexique JSON files: Enhanced entries with forme_liee

Test results:
- Before: 83% coverage (101/122 tokens)
- After: 92% coverage (112/122 tokens)
- Improvement: +9 percentage points

Remaining work to reach 95%+:
- Add missing particles (ve, eol)
- Enrich VERBAL_SUFFIXES (aran, vis)
- Document missing words (tiru, kala, vulu)

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 22:24:56 +08:00

245 lines
6.8 KiB
JavaScript

/**
* Confluent to French Translator - Traduction brute mot-à-mot
*
* Fonctionnalités:
* 1. Tokenization du texte Confluent
* 2. Recherche dans l'index inversé
* 3. Traduction brute avec annotations grammaticales
* 4. Recherche par radicaux (nouveauté)
* 5. Décomposition morphologique (nouveauté)
*/
const { extractRadicals } = require('./radicalMatcher');
const { decomposeWord } = require('./morphologicalDecomposer');
/**
* Tokenize un texte Confluent
* - Retire la ponctuation
* - Lowercase tout
* - Split sur espaces
* @param {string} text - Texte Confluent
* @returns {string[]} - Liste de tokens
*/
function tokenizeConfluent(text) {
return text
.toLowerCase()
.replace(/[.,;:!?]/g, ' ') // Remplacer ponctuation par espaces
.trim()
.split(/\s+/)
.filter(token => token.length > 0);
}
/**
* Cherche un mot Confluent dans l'index inversé avec recherche en cascade
* Essaie plusieurs stratégies:
* 1. Correspondance exacte dans byWord
* 2. Correspondance case-insensitive dans byWord
* 3. NOUVEAU: Recherche par radicaux dans byFormeLiee
* 4. NOUVEAU: Décomposition morphologique
* @param {string} word - Mot Confluent
* @param {Object} reverseIndex - Index inversé Confluent → FR (avec byWord et byFormeLiee)
* @returns {Object|null} - Entrée trouvée ou null
*/
function searchConfluent(word, reverseIndex) {
// Support ancien format (simple dict) et nouveau format (avec byWord/byFormeLiee)
const byWord = reverseIndex.byWord || reverseIndex;
const byFormeLiee = reverseIndex.byFormeLiee || {};
// 1. Essai exact dans byWord
if (byWord[word]) {
return {
...byWord[word],
matchType: 'exact',
confidence: 1.0
};
}
// 2. Essai lowercase dans byWord
const lowerWord = word.toLowerCase();
if (byWord[lowerWord]) {
return {
...byWord[lowerWord],
matchType: 'exact',
confidence: 1.0
};
}
// 3. Essai avec première lettre en majuscule (pour noms propres)
const capitalizedWord = word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
if (byWord[capitalizedWord]) {
return {
...byWord[capitalizedWord],
matchType: 'exact',
confidence: 1.0
};
}
// 4. NOUVEAU: Recherche par radicaux verbaux
const radicals = extractRadicals(lowerWord);
for (const {radical, suffix, type, confidence} of radicals) {
// Chercher dans l'index par forme_liee
if (byFormeLiee[radical]) {
const match = byFormeLiee[radical][0]; // Prendre la première correspondance
return {
...match,
matchType: 'radical',
originalWord: word,
radical,
suffix,
suffixType: type,
confidence
};
}
}
// 5. NOUVEAU: Décomposition morphologique
const decompositions = decomposeWord(lowerWord);
for (const decomp of decompositions) {
const part1Match = searchConfluent(decomp.part1, reverseIndex);
const part2Match = searchConfluent(decomp.part2, reverseIndex);
if (part1Match && part2Match) {
return {
matchType: 'composition_inferred',
originalWord: word,
composition: `${decomp.part1}-${decomp.liaison}-${decomp.part2}`,
parts: {
part1: part1Match,
liaison: decomp.liaison,
liaisonMeaning: decomp.liaisonMeaning,
part2: part2Match
},
confidence: decomp.confidence * 0.7, // Pénalité pour inférence
francais: `${part1Match.francais} [${decomp.liaisonMeaning}] ${part2Match.francais}`,
type: 'composition'
};
}
}
// 6. Vraiment inconnu
return null;
}
/**
* Traduit un token Confluent en français (brut)
* @param {string} token - Token Confluent
* @param {Object} reverseIndex - Index inversé
* @returns {string} - Traduction brute
*/
function translateToken(token, reverseIndex) {
const entry = searchConfluent(token, reverseIndex);
if (!entry) {
return `[INCONNU:${token}]`;
}
// Si c'est une particule/marqueur grammatical, retourner l'annotation
const grammaticalTypes = [
'particule',
'marqueur_temps',
'negation',
'quantite',
'interrogation',
'demonstratif'
];
if (grammaticalTypes.includes(entry.type)) {
return entry.francais; // Déjà entre crochets
}
// Sinon retourner le mot français
let result = entry.francais;
// Ajouter les synonymes si présents (entre parenthèses)
if (entry.synonymes_fr && entry.synonymes_fr.length > 0) {
result += ` (ou: ${entry.synonymes_fr.join(', ')})`;
}
return result;
}
/**
* Traduit un texte Confluent en français (traduction brute mot-à-mot)
* @param {string} text - Texte Confluent
* @param {Object} reverseIndex - Index inversé
* @returns {Object} - Résultat avec traduction et métadonnées
*/
function translateConfluentToFrench(text, reverseIndex) {
const tokens = tokenizeConfluent(text);
const translations = [];
const unknownWords = [];
tokens.forEach(token => {
const translation = translateToken(token, reverseIndex);
translations.push(translation);
if (translation.startsWith('[INCONNU:')) {
unknownWords.push(token);
}
});
return {
original: text,
tokens: tokens,
translation: translations.join(' '),
translations: translations,
unknownWords: unknownWords,
tokenCount: tokens.length,
unknownCount: unknownWords.length,
coverage: tokens.length > 0
? Math.round(((tokens.length - unknownWords.length) / tokens.length) * 100)
: 0
};
}
/**
* Version enrichie avec détails sur chaque token
* @param {string} text - Texte Confluent
* @param {Object} reverseIndex - Index inversé
* @returns {Object} - Résultat détaillé
*/
function translateConfluentDetailed(text, reverseIndex) {
const tokens = tokenizeConfluent(text);
const detailed = [];
tokens.forEach(token => {
const entry = searchConfluent(token, reverseIndex);
detailed.push({
confluent: token,
francais: entry ? entry.francais : null,
type: entry ? entry.type : 'inconnu',
synonymes: entry?.synonymes_fr || [],
forme_liee: entry?.forme_liee || null,
composition: entry?.composition || null,
found: !!entry,
// NOUVEAU: métadonnées de recherche
matchType: entry?.matchType || null,
confidence: entry?.confidence || null,
radical: entry?.radical || null,
suffix: entry?.suffix || null,
parts: entry?.parts || null
});
});
const unknownCount = detailed.filter(d => !d.found).length;
return {
original: text,
tokens: detailed,
coverage: tokens.length > 0
? Math.round(((tokens.length - unknownCount) / tokens.length) * 100)
: 0,
unknownCount: unknownCount,
tokenCount: tokens.length
};
}
module.exports = {
tokenizeConfluent,
searchConfluent,
translateToken,
translateConfluentToFrench,
translateConfluentDetailed
};