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:
Claude committed 2026-09-22 14:01:36 +00:00
1 parent fa69dac77f
commit 382c5dbdce
4 files changed
+504

No files matched your search

+6
View File
@@ -253,6 +253,12 @@ Héritées d'AJEAN :
la seule clé — de quoi remonter le conteneur ailleurs. C'est la sauvegarde de 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 l'amont sans son relais : ici rien ne part sur un service tiers, le fichier
reste chez toi. 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. - **Presets** de configuration par modèle, bench, auto-détection GPU.
- **Échantillonnage réglable par preset** : température, `top_p`, `top_k`, - **É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 `min_p`, pénalités de présence et de répétition, dans l'éditeur de preset. Ces
+249
View File
@@ -2860,6 +2860,15 @@ html[data-files="1"] #files-btn{color:var(--accent)}
<section class="set-pane" data-pane="appearance"> <section class="set-pane" data-pane="appearance">
<div class="set-pane-h"><span>Apparence</span></div> <div class="set-pane-h"><span>Apparence</span></div>
<div class="slist"> <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> <label class="switchrow rev"><span>mode sombre</span>
<span class="switch"><input type="checkbox" id="theme-dark" onchange="toggleThemeDark()"><span class="slider"></span></span> <span class="switch"><input type="checkbox" id="theme-dark" onchange="toggleThemeDark()"><span class="slider"></span></span>
</label> </label>
@@ -10915,4 +10924,244 @@ if(pushSupported() && window.isSecureContext){
navigator.serviceWorker.register('/sw.js', {scope:'/'}).then(r=>{ _swReg=r; }).catch(()=>{}); navigator.serviceWorker.register('/sw.js', {scope:'/'}).then(r=>{ _swReg=r; }).catch(()=>{});
} }
document.addEventListener('DOMContentLoaded', ()=>{ pushRefresh(); }); 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> </script></body></html>
+9
View File
@@ -659,6 +659,15 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
<section class="set-pane" data-pane="appearance"> <section class="set-pane" data-pane="appearance">
<div class="set-pane-h"><span>Apparence</span></div> <div class="set-pane-h"><span>Apparence</span></div>
<div class="slist"> <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> <label class="switchrow rev"><span>mode sombre</span>
<span class="switch"><input type="checkbox" id="theme-dark" onchange="toggleThemeDark()"><span class="slider"></span></span> <span class="switch"><input type="checkbox" id="theme-dark" onchange="toggleThemeDark()"><span class="slider"></span></span>
</label> </label>
+240
View File
@@ -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();
});