Files
Loki/internal/loki/ui/src/js/28-i18n.js
T
MichaelandClaude Opus 5.5 4a9a5a1712 Moteur Strata : un second moteur, choisi par preset (ENGINE=strata)
Strata (github.com/Niko1221/Strata, MIT) fait tourner Qwen3.8 Flash Next
(MoE 125 B) sur une carte de joueur en répartissant les experts entre VRAM,
RAM et disque. Intégration reprise d'AJEAN (backend_moe.go, « AJEAN MoE
1.0 ») : paquet figé de la release moe-v1.0, vérifié par SHA-256.

- backend_strata.go : détection de la machine, quant conseillé, installation
  en tâche (même suivi que llama.cpp), preset ENGINE=strata, lancement du
  serveur de Strata par `loki serve`, réglages d'un modèle installé
- pré-vol, vision, liste des presets, journal de chargement : Strata reconnu
- proxy OpenAI et relais : Host local pour Strata (il refuse un Host inconnu)
- benchmark et optimiseur refusés sur Strata (protocole propre à llama-server)
- UI : ligne Strata dans Paramètres → Moteur (visible aussi avec le moteur de
  l'image), fenêtre d'installation et de réglages, moteur actif affiché
- image : python3 + python3-venv ; compose : memlock illimité

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 09:03:22 +02:00

256 lines
9.3 KiB
JavaScript

// 28-i18n.js — interface en anglais, par-dessus une source française.
//
// L'amont (AJEAN) a un vrai système de clés : chaque texte de l'UI porte un
// data-i18n et vit dans une table FR/EN. Traduire Loki de la même façon
// demanderait de réécrire tous ses écrans d'un coup, avec le risque d'en casser
// un pour une clé oubliée. On prend l'autre chemin, additif : la source RESTE le
// français, et passer en anglais applique un DICTIONNAIRE sur le texte affiché.
//
// Conséquences, assumées :
// - une chaîne absente du dictionnaire reste en français (dégradation douce,
// jamais de « settings.memory.title » à l'écran) ;
// - le dictionnaire s'enrichit sans toucher au reste de l'interface ;
// - la correspondance est EXACTE (texte complet d'un nœud, une fois les
// espaces retirés) : aucune substitution partielle, donc pas de mot traduit
// au milieu d'une phrase qui ne l'est pas.
//
// Ce qui n'est JAMAIS traduit : le fil de discussion (#chat) — c'est ton
// contenu et celui du modèle —, le code (pre/code), les zones de saisie, et
// tout ce qui porte data-no-i18n.
const LANG_KEY = 'loki-lang';
function uiLang(){ try{ return localStorage.getItem(LANG_KEY) || 'fr'; }catch(e){ return 'fr'; } }
// Dictionnaire français → anglais. Couvre la « coque » : navigation des
// réglages, intitulés de sections, libellés de lignes, boutons, interrupteurs.
// Les longues bulles d'aide restent en français pour l'instant — elles sont
// nombreuses et leur traduction n'apporte rien tant que la coque ne l'est pas.
const I18N_EN = {
// Navigation des réglages
'Paramètres': 'Settings',
'Intelligence artificielle': 'Artificial intelligence',
'Moteur': 'Engine',
'Application': 'Application',
'Projets': 'Projects',
'Config active': 'Active config',
'Presets': 'Presets',
'Mode agent': 'Agent mode',
'Tâches planifiées': 'Scheduled tasks',
'System prompt': 'System prompt',
'Moteur llama.cpp': 'llama.cpp engine',
// Moteur Strata (10b-strata.js)
'Version': 'Version',
'Qualité': 'Quality',
'Configuration': 'Configuration',
'Installer': 'Install',
'Activer': 'Activate',
'Déjà actif': 'Already active',
'Cache KV': 'KV cache',
'Prédiction (MTP)': 'Prediction (MTP)',
'Lire les images': 'Read images',
"Carte d'appoint": 'Helper card',
'Carte principale': 'Main card',
'conseillé': 'recommended',
'installé': 'installed',
'actif': 'active',
'Journal du moteur': 'Engine log',
'Dictée': 'Dictation',
'Identité': 'Identity',
'Apparence': 'Appearance',
'Accès OpenAI': 'OpenAI access',
'Actions': 'Actions',
// Sections
'Mémoire': 'Memory',
'Accès internet': 'Internet access',
'Contrôle du navigateur': 'Browser control',
'Serveurs MCP': 'MCP servers',
'Service du moteur': 'Engine service',
'Modèle': 'Model',
'Reconnaissance': 'Recognition',
'Affichage': 'Display',
'Endpoint compatible OpenAI': 'OpenAI-compatible endpoint',
'Clé API': 'API key',
'Connexion': 'Connection',
'Format': 'Format',
'Portée': 'Scope',
'Contenu': 'Content',
'Nom': 'Name',
'Type': 'Type',
'Consigne': 'Instruction',
'Script': 'Script',
'Preset': 'Preset',
'Fréquence': 'Frequency',
'Accès': 'Access',
'Cartes graphiques': 'Graphics cards',
'Réglages': 'Settings',
'Échantillonnage': 'Sampling',
'Sauvegarde chiffrée': 'Encrypted backup',
'Ton avatar': 'Your avatar',
'Avatar de Loki': "Loki's avatar",
'À exécuter': 'To run',
'Modèle utilisé': 'Model used',
'Projet visé': 'Target project',
'Toutes les': 'Every',
'Accès à la mémoire': 'Memory access',
'Accès au web': 'Web access',
'Tâche active': 'Task enabled',
'Fichier': 'File',
'Destination': 'Destination',
'Contexte': 'Context',
'Vision': 'Vision',
'Vérifier': 'Check',
// Interrupteurs
'activer le mode agent': 'enable agent mode',
"me prévenir quand c'est prêt": 'notify me when it is ready',
'chiffrer la mémoire sur le disque': 'encrypt memory on disk',
"donner internet à l'IA": 'give the AI internet access',
"laisser l'IA piloter un navigateur": 'let the AI drive a browser',
'compactage automatique du contexte': 'automatic context compaction',
'suspendre toutes les tâches': 'suspend all tasks',
'masquer le raisonnement': 'hide reasoning',
"masquer les appels d'outils": 'hide tool calls',
'garder les bulles repliées': 'keep bubbles collapsed',
'masquer la vitesse de génération': 'hide generation speed',
'mode': 'mode',
// Boutons
'Libérer la VRAM': 'Free VRAM',
'libérer la VRAM': 'free VRAM',
'Chat': 'Chat',
'Code': 'Code',
'compacter': 'compact',
'fermer': 'close',
'relancer': 'restart',
'Tester la connexion': 'Test connection',
'supprimer': 'delete',
'annuler': 'cancel',
'Annuler': 'Cancel',
'Confirmer': 'Confirm',
'enregistrer': 'save',
'Enregistrer': 'Save',
'+ ajouter un projet': '+ add a project',
'+ ajouter une tâche': '+ add a task',
'+ ajouter un serveur': '+ add a server',
'+ ajouter un point': '+ add an entry',
'+ ajouter': '+ add',
'exporter': 'export',
'importer': 'import',
'retirer': 'remove',
'catalogue': 'catalog',
'renommer': 'rename',
'déplacer': 'move',
'vérifier les mises à jour': 'check for updates',
'↥ Vérifier les mises à jour': '↥ Check for updates',
'mettre à jour le moteur': 'update the engine',
"revenir au moteur de l'image": 'go back to the image engine',
'démarrer': 'start',
'redémarrer': 'restart',
'arrêter': 'stop',
'Télécharger ce modèle': 'Download this model',
'Annuler le téléchargement': 'Cancel download',
'Retirer le jeton': 'Remove token',
'Tester le micro (3 s)': 'Test the mic (3 s)',
'tester maintenant': 'test now',
'afficher': 'show',
'générer': 'generate',
'définir…': 'set…',
'⤓ exporter…': '⤓ export…',
'⤓ télécharger': '⤓ download',
'bench': 'bench',
'Installer & compiler': 'Install & build',
'voir plus': 'show more',
'voir moins': 'show less',
'copier': 'copy',
'copié': 'copied',
'réponse': 'response',
'modifications': 'changes',
'écriture en cours': 'writing',
'exécution en cours…': 'running…',
// Divers visibles
'Nouvelle discussion': 'New chat',
'Discussions': 'Chats',
'Langue': 'Language',
'Français': 'French',
'Anglais': 'English',
};
// Sélecteurs dont le CONTENU n'est jamais traduit : ton texte et celui du
// modèle, le code, les champs de saisie.
const I18N_SKIP = '#chat, pre, code, textarea, input, [data-no-i18n]';
function i18nText(fr){
const key = (fr || '').trim();
if (!key) return null;
const en = I18N_EN[key];
if (en === undefined) return null;
// On rend la traduction en conservant l'espacement d'origine autour du texte
// (un « ' ' + label » dans le HTML garde son espace).
const lead = fr.slice(0, fr.length - fr.trimStart().length);
const tail = fr.slice(fr.trimEnd().length);
return lead + en + tail;
}
// Traduit les nœuds texte et quelques attributs sous `root`.
function i18nApply(root){
if (uiLang() !== 'en' || !root) return;
const el = root.nodeType === 1 ? root : root.parentElement;
if (el && el.closest && el.closest(I18N_SKIP)) return;
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode(n){
if (!n.nodeValue || !n.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
if (n.parentElement && n.parentElement.closest(I18N_SKIP)) return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
}
});
const todo = [];
for (let n = walker.nextNode(); n; n = walker.nextNode()) todo.push(n);
for (const n of todo){
const tr = i18nText(n.nodeValue);
if (tr !== null) n.nodeValue = tr;
}
// Attributs visibles (infobulles, champs vides, accessibilité).
const scope = root.nodeType === 1 ? root : document.body;
const all = scope.querySelectorAll ? [scope, ...scope.querySelectorAll('[placeholder],[title],[aria-label]')] : [];
for (const e of all){
if (!e.getAttribute) continue;
for (const attr of ['placeholder', 'title', 'aria-label']){
const v = e.getAttribute(attr);
if (!v) continue;
const tr = i18nText(v);
if (tr !== null) e.setAttribute(attr, tr);
}
}
}
// Les panneaux sont rendus en JS bien après le chargement : un observateur
// traduit ce qui apparaît. Il ignore #chat, donc le fil ne passe jamais dessous.
function i18nWatch(){
if (uiLang() !== 'en') return;
const obs = new MutationObserver(muts => {
for (const m of muts){
for (const n of m.addedNodes){
if (n.nodeType === 1 || n.nodeType === 3) i18nApply(n);
}
}
});
obs.observe(document.body, {childList: true, subtree: true});
}
// setUiLang : bascule la langue. Vers l'anglais, on applique tout de suite ;
// vers le français, on RECHARGE — le texte français d'origine a été remplacé
// dans le DOM, le recharger est le moyen le plus sûr de le retrouver intact.
function setUiLang(lang){
const next = lang === 'en' ? 'en' : 'fr';
const prev = uiLang();
try{ localStorage.setItem(LANG_KEY, next); }catch(e){}
if (next === prev) return;
if (next === 'en'){ i18nApply(document.body); i18nWatch(); }
else location.reload();
}
document.addEventListener('DOMContentLoaded', () => {
const sel = document.getElementById('ui-lang');
if (sel) sel.value = uiLang();
i18nApply(document.body);
i18nWatch();
});