"""Robust one-pass i18n: read once, replace all, write once per file."""
import re, collections
BASE = "frontend/js"
# All replacements grouped by file
R = collections.defaultdict(list)
def add(f, find, repl):
R[f].append((find, repl))
F = f"{BASE}/ui.js"
# Create file modal
add(F, '
Créer un fichier ', '${t(\'modal.create_file\')} ')
add(F, 'Nom du fichier ', '${t(\'modal.file_name\')} ')
add(F, 'Dans: ${parentPath', '
${t(\'modal.in_folder\', {path: parentPath')
add(F, 'Type de fichier ', '${t(\'modal.file_type\')} ')
add(F, 'Annuler ', '${t(\'common.cancel\')} ')
add(F, 'Créer ', '${t(\'modal.create_btn\')} ')
# Create file action (toasts, textContent)
add(F, "createBtn.textContent = 'Création...';\n \n try {\n await api(`/api/file",
"createBtn.textContent = t('modal.creating');\n \n try {\n await api(`/api/file")
add(F, 'showToast(`Fichier "${name}" créé`, \'success\');\n this._closeModal(overlay);\n await refreshSidebarTreePreservingState();\n openFile(vault, path);',
'showToast(t(\'toast.file_created\', {name: name}), \'success\');\n this._closeModal(overlay);\n await refreshSidebarTreePreservingState();\n openFile(vault, path);')
add(F, "showToast(err.message || 'Erreur lors de la création', 'error');\n createBtn.disabled = false;\n createBtn.textContent = 'Créer';\n }\n };\n \n createBtn.addEventListener('click', create);",
"showToast(err.message || t('toast.create_error'), 'error');\n createBtn.disabled = false;\n createBtn.textContent = t('modal.create_btn');\n }\n };\n \n createBtn.addEventListener('click', create);")
# Create dir action
add(F, "createBtn.textContent = 'Création...';\n \n try {\n await api(`/api/directory",
"createBtn.textContent = t('modal.creating');\n \n try {\n await api(`/api/directory")
add(F, 'showToast(`Dossier "${name}" créé`, \'success\');\n this._closeModal(overlay);\n await refreshSidebarTreePreservingState();',
'showToast(t(\'toast.dir_created\', {name: name}), \'success\');\n this._closeModal(overlay);\n await refreshSidebarTreePreservingState();')
add(F, "showToast(err.message || 'Erreur lors de la création', 'error');\n createBtn.disabled = false;\n createBtn.textContent = 'Créer';\n }\n };\n \n createBtn.addEventListener('click', create);\n cancelBtn.addEventListener('click', () => this._closeModal(overlay));\n \n input.addEventListener('keydown', (e) => {\n if (e.key === 'Enter') create();\n if (e.key === 'Escape') this._closeModal(overlay);\n });\n },\n\n showCreateFileModal",
"showToast(err.message || t('toast.create_error'), 'error');\n createBtn.disabled = false;\n createBtn.textContent = t('modal.create_btn');\n }\n };\n \n createBtn.addEventListener('click', create);\n cancelBtn.addEventListener('click', () => this._closeModal(overlay));\n \n input.addEventListener('keydown', (e) => {\n if (e.key === 'Enter') create();\n if (e.key === 'Escape') this._closeModal(overlay);\n });\n },\n\n showCreateFileModal")
# Delete dir modal template
add(F, '
Supprimer le dossier ', '${t(\'modal.delete_dir\')} ')
add(F, 'Attention ! Cette action est irréversible.\n Tous les fichiers et sous-dossiers seront supprimés définitivement.',
'${t(\'modal.delete_warning\')} ${t(\'modal.delete_irreversible\')}\n ${t(\'modal.delete_dir_warning\')}')
add(F, 'Dossier à supprimer: ', '${t(\'modal.dir_to_delete\')} ')
# Delete dir action
add(F, "confirmBtn.textContent = 'Suppression...';\n \n try {\n const result = await api(`/api/directory",
"confirmBtn.textContent = t('modal.creating');\n \n try {\n const result = await api(`/api/directory")
add(F, "showToast(`Dossier supprimé (${result.deleted_count} fichiers)`, 'success');",
"showToast(t('toast.dir_deleted', {count: result.deleted_count}), 'success');")
add(F, "showToast(err.message || 'Erreur lors de la suppression', 'error');\n confirmBtn.disabled = false;\n confirmBtn.textContent = 'Supprimer définitivement';\n }\n };\n \n confirmBtn.addEventListener('click', deleteDir);",
"showToast(err.message || t('toast.delete_error'), 'error');\n confirmBtn.disabled = false;\n confirmBtn.textContent = t('modal.delete_permanently');\n }\n };\n \n confirmBtn.addEventListener('click', deleteDir);")
# Delete file modal template + action
add(F, 'Supprimer le fichier ', '${t(\'modal.delete_file\')} ')
add(F, 'Attention ! Cette action est irréversible.\n \n
\n \n Fichier à supprimer:',
'${t(\'modal.delete_warning\')} ${t(\'modal.delete_irreversible\')}\n
\n \n \n ${t(\'modal.file_to_delete\')}')
add(F, 'data-i18n="common.cancel"', 'data-i18n="common.cancel"') # no-op, placeholder
# Replace all buttons in delete modals
add(F, 'Annuler ', '${t(\'common.cancel\')} ')
add(F, 'Supprimer définitivement ', '${t(\'modal.delete_permanently\')} ')
# Delete file action
add(F, "confirmBtn.textContent = 'Suppression...';\n \n try {\n await api(`/api/file/${encodeURIComponent(vault)}",
"confirmBtn.textContent = t('modal.creating');\n \n try {\n await api(`/api/file/${encodeURIComponent(vault)}")
add(F, "showToast('Fichier supprimé', 'success');\n this._closeModal(overlay);\n await refreshSidebarTreePreservingState();\n \n if (state.currentVault === vault",
"showToast(t('toast.file_deleted'), 'success');\n this._closeModal(overlay);\n await refreshSidebarTreePreservingState();\n \n if (state.currentVault === vault")
add(F, "showToast(err.message || 'Erreur lors de la suppression', 'error');\n confirmBtn.disabled = false;\n confirmBtn.textContent = 'Supprimer définitivement';\n }\n };\n \n confirmBtn.addEventListener('click', deleteFile);",
"showToast(err.message || t('toast.delete_error'), 'error');\n confirmBtn.disabled = false;\n confirmBtn.textContent = t('modal.delete_permanently');\n }\n };\n \n confirmBtn.addEventListener('click', deleteFile);")
# Rename toasts
add(F, "showToast(type === 'directory' ? 'Dossier renommé' : 'Fichier renommé', 'success');",
"showToast(type === 'directory' ? t('toast.dir_renamed') : t('toast.file_renamed'), 'success');")
add(F, "showToast(err.message || 'Erreur lors du renommage', 'error');",
"showToast(err.message || t('toast.rename_error'), 'error');")
# Copy path
add(F, "showToast(`Chemin copié : ${path}`, 'success');",
"showToast(t('toast.path_copied', {path: path}), 'success');")
add(F, "showToast('Erreur lors de la copie', 'error');",
"showToast(t('toast.copy_error'), 'error');")
# Bookmark + dir saved search + viewer error
add(F, 'showToast(data.bookmarked ? "Ajouté aux bookmarks" : "Retiré des bookmarks", "success");',
'showToast(data.bookmarked ? t("toast.bookmark_added") : t("toast.bookmark_removed"), "success");')
add(F, 'showToast("Repertoire sauvegarde dans les recherches", "success");',
'showToast(t("toast.dir_saved_search"), "success");')
add(F, 'showToast("Erreur lors du chargement du viewer: " + err.message, "error");',
'showToast(t("toast.viewer_error") + ": " + err.message, "error");')
# ═══════ palette.js ═══════
F = f"{BASE}/palette.js"
add(F, 'confirm(`Supprimer le fichier \\"${path}\\" ?`)', 'confirm(t(\'confirm.delete_file_named\', {path: path}))')
add(F, 'confirm(`Supprimer \\"${path}\\" et TOUT son contenu ?`)', 'confirm(t(\'confirm.delete_dir_named\', {path: path}))')
add(F, "showToast('Erreur de création','error')", "showToast(t('toast.create_error'),'error')")
add(F, "showToast('Erreur','error')", "showToast(t('common.error'),'error')")
add(F, "showToast('Aucun fichier ouvert','error')", "showToast(t('toast.no_open_file'),'error')")
# ═══════ search.js ═══════
F = f"{BASE}/search.js"
add(F, 'summaryText.textContent = `${data.total} résultat(s) pour "${freeText}" avec filtres`;',
'summaryText.textContent = t(\'search.results_filtered\', {count: data.total, query: freeText});')
add(F, 'summaryText.textContent = `${data.total} résultat(s) pour "${freeText}"`;',
'summaryText.textContent = t(\'search.results_for\', {count: data.total, query: freeText});')
add(F, 'summaryText.textContent = `${data.total} fichier(s) avec filtres`;',
'summaryText.textContent = t(\'search.files_filtered\', {count: data.total});')
add(F, 'summaryText.textContent = `${data.total} résultat(s)`;',
'summaryText.textContent = t(\'search.results_count\', {count: data.total});')
add(F, 'timeBadge.textContent = `(${data.query_time_ms} ms)`;',
'timeBadge.textContent = t(\'search.query_time\', {time: data.query_time_ms});')
# ═══════ config.js ═══════
F = f"{BASE}/config.js"
add(F, 'title: "Application",', 'title: t("about.section_app"),')
add(F, 'title: "Environnement",', 'title: t("about.section_env"),')
add(F, 'title: "Composants",', 'title: t("about.section_components"),')
for old_lbl, 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")]:
add(F, f'[{old_lbl},', f'[t("{key}"),')
# ═══════ auth.js ═══════
F = f"{BASE}/auth.js"
add(F, 'Administration — Utilisateurs', "' + t('admin.title') + '")
add(F, '+ Nouvel utilisateur', "' + t('admin.new_user') + '")
# ═══════ viewer.js ═══════
F = f"{BASE}/viewer.js"
add(F, "titleInp.placeholder = 'Titre...'", "titleInp.placeholder = t('forge.title_placeholder')")
add(F, 'title: "Ouvrir dans une nouvelle fenetre"', 'title: t("forge.popout")')
add(F, 'document.createTextNode("pop-out")', 'document.createTextNode(t("forge.popout_label"))')
# ═══════ dashboard.js ═══════
F = f"{BASE}/dashboard.js"
add(F, '\ud83d\udccb Copier', "' + t('dashboard.copy_btn') + '")
add(F, '\ud83d\udcc2 Ouvrir', "' + t('dashboard.open_btn') + '")
# ── APPLY (one read + write per file) ──
print("Applying replacements per file...")
for filepath in R:
with open(filepath, "r", encoding="utf-8") as fh:
content = fh.read()
changed = False
for find, repl in R[filepath]:
count = content.count(find)
if count == 0:
print(f" SKIP {filepath.split('/')[-1]}: {find[:50]}...")
continue
content = content.replace(find, repl)
changed = True
print(f" OK {filepath.split('/')[-1]}: {count}× {find[:40]}")
if changed:
with open(filepath, "w", encoding="utf-8") as fh:
fh.write(content)
print("Done.")