diff --git a/_batch_i18n.py b/_batch_i18n.py new file mode 100644 index 0000000..a123258 --- /dev/null +++ b/_batch_i18n.py @@ -0,0 +1,190 @@ +"""Batch i18n: replace all remaining French strings in JS modules with t() calls.""" +import re, json + +BASE = "frontend" + +# ── Load locale to build reverse map ── +with open(f"{BASE}/locales/fr.json", encoding="utf-8") as f: + fr = json.load(f) +text_to_key = {} +for k, v in fr.items(): + if k.startswith("_"): continue + if v not in text_to_key or len(k) < len(text_to_key[v]): + text_to_key[v] = k + +# ── Helper: quote for replacement ── +def esc(s): + return s.replace('\\', '\\\\').replace('"', '\\"') + +# ── Replacements: (file, old_pattern, new_pattern) ── +# Each old_pattern must be UNIQUE in the file +replacements = [] + +# === ui.js === +f = f"{BASE}/js/ui.js" + +# Delete dir modal template +r = replacements # shorthand + +# Delete dir action +r.append((f, + "confirmBtn.disabled = true;\n confirmBtn.textContent = 'Suppression...';\n \n try {\n const result = await api(`/api/directory", + "confirmBtn.disabled = true;\n confirmBtn.textContent = t('modal.creating');\n \n try {\n const result = await api(`/api/directory")) + +r.append((f, + "showToast(`Dossier supprimé (${result.deleted_count} fichiers)`, 'success');", + "showToast(t('toast.dir_deleted', {count: result.deleted_count}), 'success');")) + +r.append((f, + "showToast(err.message || 'Erreur lors de la suppression', 'error');\n confirmBtn.disabled = false;\n confirmBtn.textContent = 'Supprimer définitivement';", + "showToast(err.message || t('toast.delete_error'), 'error');\n confirmBtn.disabled = false;\n confirmBtn.textContent = t('modal.delete_permanently');")) + +# Rename toasts +r.append((f, + "showToast(type === 'directory' ? 'Dossier renommé' : 'Fichier renommé', 'success');", + "showToast(type === 'directory' ? t('toast.dir_renamed') : t('toast.file_renamed'), 'success');")) + +r.append((f, + "showToast(err.message || 'Erreur lors du renommage', 'error');", + "showToast(err.message || t('toast.rename_error'), 'error');")) + +# Copy path toast +r.append((f, + "showToast(`Chemin copié : ${path}`, 'success');", + "showToast(t('toast.path_copied', {path: path}), 'success');")) + +r.append((f, + "showToast('Erreur lors de la copie', 'error');", + "showToast(t('toast.copy_error'), 'error');")) + +# Bookmark toast +r.append((f, + 'showToast(data.bookmarked ? "Ajouté aux bookmarks" : "Retiré des bookmarks", "success");', + 'showToast(data.bookmarked ? t("toast.bookmark_added") : t("toast.bookmark_removed"), "success");')) + +# Dir saved search toast +r.append((f, + 'showToast("Repertoire sauvegarde dans les recherches", "success");', + 'showToast(t("toast.dir_saved_search"), "success");')) + +# Viewer error toast +r.append((f, + 'showToast("Erreur lors du chargement du viewer: " + err.message, "error");', + 'showToast(t("toast.viewer_error") + ": " + err.message, "error");')) + + +# === palette.js === +f = f"{BASE}/js/palette.js" +pal = text_to_key + +# Confirm dialogs +r.append((f, + 'confirm(`Supprimer le fichier \\"${path}\\" ?`)', + 'confirm(t("confirm.delete_file_named", {path: path}))')) + +r.append((f, + 'confirm(`Supprimer \\"${path}\\" et TOUT son contenu ?`)', + 'confirm(t("confirm.delete_dir_named", {path: path}))')) + +# Show toasts +r.append((f, + "showToast('Erreur de création','error')", + "showToast(t('toast.create_error'),'error')")) + +r.append((f, + "showToast('Erreur','error')", + "showToast(t('common.error'),'error')")) + +# === config.js === +f = f"{BASE}/js/config.js" + +# About sections +r.append((f, + 'title: "Application",', + 'title: t("about.section_app"),')) +r.append((f, + 'title: "Environnement",', + 'title: t("about.section_env"),')) +r.append((f, + 'title: "Composants",', + 'title: t("about.section_components"),')) + +# About rows - these are data, not labels per se, but let's translate the labels +for old_label, key in [ + ('"Nom"', "about.name"), ('"Version"', "about.version"), + ('"Version API"', "about.api_version"), ('"Statut"', "about.status"), + ('"Vaults configurés"', "about.vaults_configured"), + ('"Fichiers indexés"', "about.files_indexed"), + ('"Navigateur"', "about.browser"), ('"Plateforme"', "about.platform"), + ('"Langue"', "about.language"), ('"Backend"', "about.backend"), + ('"Rendu Markdown"', "about.markdown_renderer"), + ('"Surveillance fichiers"', "about.file_watcher"), + ('"Frontend"', "about.frontend"), ('"Icônes"', "about.icons"), + ('"Coloration syntaxe"', "about.syntax_highlight"), + ('"Éditeur"', "about.editor"), +]: + r.append((f, f'[{old_label},', f'[t("{key}"),')) + +# Admin-only / no webhooks +r.append((f, + "list.innerHTML = '
Admin uniquement
';", + "list.innerHTML = '
' + t('common.loading') + '
';")) +r.append((f, + "list.innerHTML = '
Aucun webhook configuré.
';", + "list.innerHTML = '
' + t('common.none') + '
';")) + +# === auth.js === +f = f"{BASE}/js/auth.js" +r.append((f, + 'Administration — Utilisateurs', + "' + t('admin.title') + '")) +r.append((f, + '+ Nouvel utilisateur', + "' + t('admin.new_user') + '")) +r.append((f, + "title=\"Fermer\"", + "title=\"' + t('common.close') + '\"")) + +# === viewer.js === +f = f"{BASE}/js/viewer.js" +r.append((f, + "titleInp.placeholder = 'Titre...'", + "titleInp.placeholder = t('forge.title_placeholder')")) +r.append((f, + "title: \"Ouvrir dans une nouvelle fenetre\"", + "title: t('forge.popout')")) +r.append((f, + 'document.createTextNode("pop-out")', + 'document.createTextNode(t("forge.popout_label"))')) + +# === dashboard.js === +f = f"{BASE}/js/dashboard.js" +r.append((f, + '"Copier"', + 't("dashboard.copy_btn")')) +r.append((f, + '"Ouvrir"', + 't("dashboard.open_btn")')) + +# === search.js === +f = f"{BASE}/js/search.js" +r.append((f, + "summaryText.textContent = `${data.total} résultat(s)", + "summaryText.textContent = t('search.results_count', {count: data.total})")) + +# ── Apply all replacements ── +for filepath, old, new in replacements: + with open(filepath, "r", encoding="utf-8") as fh: + content = fh.read() + count = content.count(old) + if count == 0: + print(f"SKIP {filepath}: pattern not found: {old[:60]}...") + continue + if count > 1: + print(f"WARN {filepath}: {count} matches for: {old[:60]}...") + content = content.replace(old, new) + with open(filepath, "w", encoding="utf-8") as fh: + fh.write(content) + print(f"OK {filepath}: {count} replacement(s)") + +print(f"\nTotal replacements applied: {len(replacements)}") diff --git a/_i18n_final.py b/_i18n_final.py new file mode 100644 index 0000000..23ad980 --- /dev/null +++ b/_i18n_final.py @@ -0,0 +1,202 @@ +"""One-pass i18n: replace all French strings in all JS modules.""" +import re + +BASE = "frontend/js" + +# All replacements as (file, find_unique_snippet, replacement) +# Each 'find' must be UNIQUE in the file +R = [] + +# ═══════ ui.js ═══════ +F = f"{BASE}/ui.js" + +# Create file modal template +R.append((F, + '

Créer un fichier

', + '

${t(\'modal.create_file\')}

')) +R.append((F, + '', + '')) +R.append((F, + '\n \n \n \n @@ -711,7 +711,7 @@ const FileOperations = { const path = parentPath ? `${parentPath}/${name}` : name; createBtn.disabled = true; - createBtn.textContent = 'Création...'; + createBtn.textContent = t('modal.creating'); try { await api(`/api/directory/${encodeURIComponent(vault)}`, { @@ -720,13 +720,13 @@ const FileOperations = { body: JSON.stringify({ path }), }); - showToast(`Dossier "${name}" créé`, 'success'); + showToast(t('toast.dir_created', {name: name}), 'success'); this._closeModal(overlay); await refreshSidebarTreePreservingState(); } catch (err) { - showToast(err.message || 'Erreur lors de la création', 'error'); + showToast(err.message || t('toast.create_error'), 'error'); createBtn.disabled = false; - createBtn.textContent = 'Créer'; + createBtn.textContent = t('modal.create_btn'); } }; @@ -745,17 +745,17 @@ const FileOperations = { modal.className = 'obsigate-modal'; modal.innerHTML = `
-

Créer un fichier

+

${t('modal.create_file')}