mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Interface en anglais, par-dessus une source française
L'amont tient une table de clés FR/EN et marque chaque texte d'un data-i18n. Reproduire ça ici demanderait de réécrire tous les écrans d'un coup, avec le risque d'en casser un pour une clé oubliée — et un « settings.memory.title » affiché en production. Chemin additif : la source RESTE le français, et « English » applique un dictionnaire sur le texte affiché (correspondance exacte, nœud par nœud). Une chaîne absente du dictionnaire reste en français ; le dictionnaire s'enrichit sans toucher au reste de l'interface. Jamais traduits : le fil de discussion (#chat), le code, les zones de saisie, et tout ce qui porte data-no-i18n. Un observateur couvre les panneaux rendus en JS après le chargement. Revenir au français recharge la page — le texte d'origine a été remplacé dans le DOM, c'est le moyen sûr de le retrouver intact. Couverture de départ : navigation des réglages, intitulés de sections, libellés de lignes, boutons et interrupteurs. Sélecteur dans Apparence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K6CAgoLJzufeA8rZTSpSpY
This commit is contained in:
4 files changed
+504
No files matched your search
@@ -253,6 +253,12 @@ Héritées d'AJEAN :
|
||||
la seule clé — de quoi remonter le conteneur ailleurs. C'est la sauvegarde de
|
||||
l'amont sans son relais : ici rien ne part sur un service tiers, le fichier
|
||||
reste chez toi.
|
||||
- **Interface en anglais** (*Réglages → Apparence → Langue*) : la source reste
|
||||
française et « English » applique un dictionnaire sur la coque — navigation,
|
||||
intitulés, boutons, interrupteurs. Ce qui n'y figure pas **reste en
|
||||
français** plutôt que d'afficher une clé technique, et le fil de discussion
|
||||
n'est jamais touché : c'est ton contenu. Le dictionnaire s'enrichit sans
|
||||
toucher au reste de l'interface (`ui/src/js/28-i18n.js`).
|
||||
- **Presets** de configuration par modèle, bench, auto-détection GPU.
|
||||
- **Échantillonnage réglable par preset** : température, `top_p`, `top_k`,
|
||||
`min_p`, pénalités de présence et de répétition, dans l'éditeur de preset. Ces
|
||||
|
||||
@@ -2860,6 +2860,15 @@ html[data-files="1"] #files-btn{color:var(--accent)}
|
||||
<section class="set-pane" data-pane="appearance">
|
||||
<div class="set-pane-h"><span>Apparence</span></div>
|
||||
<div class="slist">
|
||||
<!-- Langue de l'interface. La source est française ; « English » applique
|
||||
un dictionnaire sur la coque (navigation, libellés, boutons) — ce qui
|
||||
n'y figure pas reste en français plutôt que d'afficher une clé. -->
|
||||
<label class="switchrow"><span>Langue</span>
|
||||
<select id="ui-lang" class="sctl" onchange="setUiLang(this.value)">
|
||||
<option value="fr">Français</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="switchrow rev"><span>mode sombre</span>
|
||||
<span class="switch"><input type="checkbox" id="theme-dark" onchange="toggleThemeDark()"><span class="slider"></span></span>
|
||||
</label>
|
||||
@@ -10915,4 +10924,244 @@ if(pushSupported() && window.isSecureContext){
|
||||
navigator.serviceWorker.register('/sw.js', {scope:'/'}).then(r=>{ _swReg=r; }).catch(()=>{});
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', ()=>{ pushRefresh(); });
|
||||
// 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',
|
||||
'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();
|
||||
});
|
||||
</script></body></html>
|
||||
@@ -659,6 +659,15 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
|
||||
<section class="set-pane" data-pane="appearance">
|
||||
<div class="set-pane-h"><span>Apparence</span></div>
|
||||
<div class="slist">
|
||||
<!-- Langue de l'interface. La source est française ; « English » applique
|
||||
un dictionnaire sur la coque (navigation, libellés, boutons) — ce qui
|
||||
n'y figure pas reste en français plutôt que d'afficher une clé. -->
|
||||
<label class="switchrow"><span>Langue</span>
|
||||
<select id="ui-lang" class="sctl" onchange="setUiLang(this.value)">
|
||||
<option value="fr">Français</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="switchrow rev"><span>mode sombre</span>
|
||||
<span class="switch"><input type="checkbox" id="theme-dark" onchange="toggleThemeDark()"><span class="slider"></span></span>
|
||||
</label>
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
// 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',
|
||||
'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();
|
||||
});
|
||||
Reference in new issue
Block a user