Replace hardcoded French strings with i18n calls
CI / lint (push) Failing after 1m28s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / e2e (push) Has been skipped
CI / security (push) Failing after 17s

Initialize i18n before auth so t() works when rendering
auth-dependent UI, and build the command palette lazily so
translations are loaded first. Bump service worker cache version.
This commit is contained in:
2026-07-20 20:16:34 -04:00
parent cc22ffe6df
commit 790f97e501
17 changed files with 378 additions and 147 deletions
+6 -4
View File
@@ -23,12 +23,14 @@ import { initMermaid, setupFocusMode } from './mermaid-viewer.js';
// Initialization — mirrors the original app.js init() ordering
// =========================================================================
async function init() {
// 1. Init auth first — needed for server-side language preference
const authOk = await Auth.AuthManager.initAuth();
// 2. Init i18n after auth so server language preference is available
// 1. Init i18n FIRST — loads fallback (FR) + selected locale
// Server language check may fail (no auth yet), falls to localStorage
// LocalStorage has correct language if user set it previously
await initI18n();
// 2. Init auth — now t() works inside showApp()/renderUserMenu()
const authOk = await Auth.AuthManager.initAuth();
UI.initTheme();
UI.initHeaderMenu();
UI.initCustomDropdowns();
+1 -1
View File
@@ -221,7 +221,7 @@ async function previewBackup(fpath) {
var content = document.getElementById('bm-preview-content');
var title = document.getElementById('bm-preview-title');
panel.style.display = 'block';
content.textContent = 'Chargement...';
content.textContent = t("common.loading");
try {
var data = await api('/api/backups/content?path=' + encodeURIComponent(fpath));
+1 -1
View File
@@ -180,7 +180,7 @@ function renderMainView() {
var html =
'<div class="backup-header">' +
'<div class="backup-header-left">' +
'<h3>Backups</h3>' +
'<h3>'+t('backups.title')+'</h3>' +
'<span class="backup-file-path">' + escapeHtml(state.path) + '</span>' +
'</div>' +
'<div class="backup-header-right">' +
+1 -1
View File
@@ -96,7 +96,7 @@ function renderRecentList(files) {
function _humanizeDelta(mtime) {
const delta = Date.now() / 1000 - mtime;
if (delta < 60) return "à l'instant";
if (delta < 60) return t("config.just_now");
if (delta < 3600) return `il y a ${Math.floor(delta / 60)} min`;
if (delta < 86400) return `il y a ${Math.floor(delta / 3600)} h`;
if (delta < 604800) return `il y a ${Math.floor(delta / 86400)} j`;
+11 -11
View File
@@ -88,8 +88,8 @@ export const GraphViewManager = {
this._depth = depthSlider ? parseInt(depthSlider.value) : 1;
this._scope = 'directory';
title.textContent = `Vue Graphique — ${vault}${path ? '/' + path : ''}`;
statusEl.textContent = 'Chargement...';
title.textContent = t('graph.title', { vault, path: path ? '/' + path : '' });
statusEl.textContent = t('graph.loading');
modal.classList.add('active');
this._canvas = canvas;
@@ -120,8 +120,8 @@ export const GraphViewManager = {
this._nodes = cached.nodes;
this._edges = cached.edges;
this._scope = cached.scope;
const scopeLabel = this._scope === 'full' ? 'Vault complet' : 'Dossier';
statusEl.textContent = `${this._nodes.length} nœuds, ${this._edges.length} liens · ${scopeLabel} · prof=${this._depth} (cache)`;
const scopeLabel = this._scope === 'full' ? t('graph.scope_full') : t('graph.scope_directory');
statusEl.textContent = `${t('graph.nodes_count', { count: this._nodes.length })}, ${t('graph.edges_count', { count: this._edges.length })} · ${scopeLabel} · ${t('graph.depth_label', { depth: this._depth })} ${t('graph.cache_label')}`;
this._initLayout();
this._startRender();
return;
@@ -149,12 +149,12 @@ export const GraphViewManager = {
};
this._cacheKey = cacheKey;
const scopeLabel = this._scope === 'full' ? 'Vault complet' : 'Dossier';
statusEl.textContent = `${this._nodes.length} nœuds, ${this._edges.length} liens · ${scopeLabel} · prof=${this._depth}`;
const scopeLabel = this._scope === 'full' ? t('graph.scope_full') : t('graph.scope_directory');
statusEl.textContent = `${t('graph.nodes_count', { count: this._nodes.length })}, ${t('graph.edges_count', { count: this._edges.length })} · ${scopeLabel} · ${t('graph.depth_label', { depth: this._depth })}`;
this._initLayout();
this._startRender();
} catch (err) {
statusEl.textContent = 'Erreur de chargement';
statusEl.textContent = t('graph.error_loading');
console.error('Graph error:', err);
}
},
@@ -173,7 +173,7 @@ export const GraphViewManager = {
toggleScope() {
this._scope = this._scope === 'full' ? 'directory' : 'full';
const btn = document.getElementById('graph-full-vault');
if (btn) btn.textContent = this._scope === 'full' ? '📁 Dossier' : '🌐 Tout';
if (btn) btn.textContent = this._scope === 'full' ? t('graph.btn_directory') : t('graph.btn_full');
this.reload();
},
@@ -627,14 +627,14 @@ export const GraphViewManager = {
if (fm.statut) rows.push(`<div><span style="color:var(--text-muted)">📌 Statut</span> <span>${fm.statut}</span></div>`);
if (fm.catégorie || fm.categorie) rows.push(`<div><span style="color:var(--text-muted)">${t('graph.category')}</span> <span>${fm.catégorie || fm.categorie}</span></div>`);
if (fm.publish) rows.push(`<div><span style="color:var(--text-muted)">✅</span> <span>${t('graph.published')}</span></div>`);
if (fm.favoris) rows.push('<div><span style="color:var(--text-muted)">⭐</span> <span>Favoris</span></div>');
if (fm.favoris) rows.push('<div><span style="color:var(--text-muted)">⭐</span> <span>' + t('graph.favorites') + '</span></div>');
if (rows.length > 0) metaRows = rows.join('');
}
const tags = (meta?.tags || node.tags || []).slice(0, 6);
const inc = node.incoming_count || 0;
const out = node.outgoing_count || 0;
const typeIcon = node.type === 'directory' ? '📁' : '📄';
const typeLabel = node.type === 'directory' ? 'Dossier' : (node.path || '').endsWith('.md') ? 'Markdown' : 'Fichier';
const typeLabel = node.type === 'directory' ? t('graph.type_directory') : (node.path || '').endsWith('.md') ? t('graph.type_markdown') : t('graph.type_file');
this._infoContent.innerHTML = `
<div style="display:flex;align-items:flex-start;gap:8px;margin-bottom:8px;padding-right:16px">
@@ -796,7 +796,7 @@ export const GraphViewManager = {
return;
}
// Show loading indicator
this._showPreviewTooltip(node, '<em>Chargement...</em>', screenX, screenY);
this._showPreviewTooltip(node, '<em>' + t('common.loading') + '</em>', screenX, screenY);
try {
const data = await api(`/api/file/${encodeURIComponent(this._vault)}?path=${encodeURIComponent(node.path)}`);
// Strip HTML tags, keep line breaks, limit to 600 chars
+1 -1
View File
@@ -137,7 +137,7 @@ export function initMobileToolbar() {
if (!pill) return;
const vaultName = state.selectedContextVault || 'all';
if (vaultName === 'all') {
pill.textContent = '📂 Toutes les vaults';
pill.textContent = t("mobile.all_vaults");
} else {
pill.textContent = '📂 ' + vaultName;
}
+37 -32
View File
@@ -33,22 +33,27 @@ let _browse = { action: null, vault: null, path: '' };
let _browseFilterEl = null, _browseFilter = '', _allItems = [];
let _browseFilterTimer = null; // debounce for global search
// ── Commands ──
const CMDS = [
{ id:'home', label:'🏠 Accueil', desc:"Retour à l'accueil", cat:'Navigation', act:()=>{close();triggerGoHome();} },
{ id:'edit-current', label:'✏️ Éditer fichier courant', desc:'Ouvrir le fichier actif dans l\'éditeur', cat:'Fichiers', act:()=>{close();const f=getCurrentFile();if(f)openEditor(f.vault,f.path);else showToast(t('toast.no_open_file'),'error');} },
{ id:'popout', label:'🪟 Pop-out', desc:'Ouvrir le document dans une fenêtre', cat:'Fichiers', act:()=>{close();const f=getCurrentFile();if(f)window.open(`/popout/${encodeURIComponent(f.vault)}/${encodeURIComponent(f.path)}`,'popout','width=1000,height=700');else showToast(t('toast.no_open_file'),'error');} },
{ id:'create-file', label:'📄 Créer fichier markdown', desc:'Choisir la vault, naviguer, puis nommer le fichier', cat:'Fichiers', act:()=>startBrowse('create-file') },
{ id:'delete-file', label:'🗑️ Supprimer fichier', desc:'Rechercher un fichier à supprimer', cat:'Fichiers', act:()=>startBrowse('delete-file') },
{ id:'delete-cur', label:'🗑️ Supprimer fichier courant', desc:'Supprimer le fichier de l\'onglet actif', cat:'Fichiers', act:async()=>{close();await delCurFile();} },
{ id:'close-cur', label:'✕ Fermer fichier courant', desc:'Fermer l\'onglet actif sans supprimer', cat:'Fichiers', act:()=>{close();const f=getCurrentFile();if(f){const tab=TabManager._tabs.find(t=>t.vault===f.vault&&t.path===f.path);if(tab)TabManager.close(tab.id);else triggerGoHome();}else{showToast(t('toast.no_open_file'),'error');}} },
{ id:'create-dir', label:'📁 Créer répertoire', desc:'Choisir la vault, naviguer, puis nommer le dossier', cat:'Répertoires', act:()=>startBrowse('create-dir') },
{ id:'delete-dir', label:'🗑️ Supprimer répertoire', desc:'Choisir la vault, naviguer et supprimer', cat:'Répertoires', act:()=>startBrowse('delete-dir') },
{ id:'theme', label:'🌓 Changer le thème', desc:'Basculer clair/sombre', cat:'Actions', act:()=>{close();toggleTheme();} },
{ id:'reindex', label:'🔄 Réindexer', desc:'Forcer la réindexation complète', cat:'Actions', act:async()=>{close();try{await api('/api/index/reload');showToast('Index rechargé','success');}catch{showToast(t('common.error'),'error');}} },
{ id:'help', label:'❓ Aide', desc:'Ouvrir le guide', cat:'Système', act:()=>{close();const b=document.getElementById('help-open-btn');if(b)b.click();} },
{ id:'config', label:'⚙️ Configuration', desc:'Ouvrir les paramètres', cat:'Système', act:()=>{close();const b=document.getElementById('config-open-btn');if(b)b.click();} },
];
// ── Commands ── (lazy: built on first access so i18n is ready)
let _cmdsCache = null;
function getCMDS() {
if (_cmdsCache) return _cmdsCache;
_cmdsCache = [
{ id:'home', label:t('palette.cmd_back_home'), desc:t('palette.cmd_back_home_desc'), cat:'Navigation', act:()=>{close();triggerGoHome();} },
{ id:'edit-current', label:'✏️ '+t('editor.edit'), desc:t('editor.edit_current_desc'), cat:'Fichiers', act:()=>{close();const f=getCurrentFile();if(f)openEditor(f.vault,f.path);else showToast(t('toast.no_open_file'),'error');} },
{ id:'popout', label:'🪟 Pop-out', desc:t('palette.popout_desc'), cat:'Fichiers', act:()=>{close();const f=getCurrentFile();if(f)window.open(`/popout/${encodeURIComponent(f.vault)}/${encodeURIComponent(f.path)}`,'popout','width=1000,height=700');else showToast(t('toast.no_open_file'),'error');} },
{ id:'create-file', label:t('palette.cmd_create_file'), desc:t('palette.cmd_create_file_desc'), cat:'Fichiers', act:()=>startBrowse('create-file') },
{ id:'delete-file', label:'🗑️ '+t('file.delete'), desc:t('palette.delete_file_desc'), cat:'Fichiers', act:()=>startBrowse('delete-file') },
{ id:'delete-cur', label:'🗑️ '+t('palette.delete_current'), desc:t('palette.delete_current_desc'), cat:'Fichiers', act:async()=>{close();await delCurFile();} },
{ id:'close-cur', label:'✕ '+t('palette.close_current'), desc:t('palette.close_current_desc'), cat:'Fichiers', act:()=>{close();const f=getCurrentFile();if(f){const tab=TabManager._tabs.find(t=>t.vault===f.vault&&t.path===f.path);if(tab)TabManager.close(tab.id);else triggerGoHome();}else{showToast(t('toast.no_open_file'),'error');}} },
{ id:'create-dir', label:t('palette.cmd_create_dir'), desc:t('palette.cmd_create_dir_desc'), cat:'Répertoires', act:()=>startBrowse('create-dir') },
{ id:'delete-dir', label:t('palette.cmd_delete_dir'), desc:t('palette.cmd_delete_dir_desc'), cat:'Répertoires', act:()=>startBrowse('delete-dir') },
{ id:'theme', label:t('palette.cmd_theme'), desc:t('palette.cmd_theme_desc'), cat:'Actions', act:()=>{close();toggleTheme();} },
{ id:'reindex', label:'🔄 '+t('vault.reindex'), desc:t('palette.reindex_desc'), cat:'Actions', act:async()=>{close();try{await api('/api/index/reload');showToast(t('sync.index_reloaded'),'success');}catch{showToast(t('common.error'),'error');}} },
{ id:'help', label:'❓ '+t('help.help_title'), desc:t('palette.help_desc'), cat:'Système', act:()=>{close();const b=document.getElementById('help-open-btn');if(b)b.click();} },
{ id:'config', label:t('palette.cmd_config'), desc:t('palette.cmd_config_desc'), cat:'Système', act:()=>{close();const b=document.getElementById('config-open-btn');if(b)b.click();} },
];
return _cmdsCache;
}
// ── Browse: vault picker ──
function startBrowse(action) {
@@ -62,13 +67,13 @@ async function renderVaultPicker() {
try {
const data = await api('/api/vaults');
const vaults = Array.isArray(data) ? data : [];
_items = vaults.map(v => ({ type:'_vault', vault: v.name, label: (v.type==='DIR'?'📂':'🗂️')+' '+v.name, sublabel: v.file_count+' fichiers' }));
_items = vaults.map(v => ({ type:'_vault', vault: v.name, label: (v.type==='DIR'?'📂':'🗂️')+' '+v.name, sublabel: t('palette.vault_count', {count: v.file_count}) }));
if (_items.length === 0) {
_items = [{ type:'_empty', label: 'Aucune vault trouvée', sublabel: '' }];
_items = [{ type:'_empty', label: t('palette.no_vaults'), sublabel: '' }];
}
_allItems = _items;
render();
} catch { _items = [{ type:'_error', label: 'Erreur de chargement', sublabel: '' }]; _allItems = _items; render(); }
} catch { _items = [{ type:'_error', label: t('palette.error_loading'), sublabel: '' }]; _allItems = _items; render(); }
}
// ── Browse: directory navigation ──
@@ -95,11 +100,11 @@ async function renderBrowse() {
// Action row
const actLabel = {
'create-file':'📄 Créer fichier ici',
'create-dir':'📁 Créer dossier ici',
'delete-dir':'🗑️ Supprimer ce dossier',
'delete-file':'🗑️ Supprimer un fichier dans ce dossier',
'open-file':'📂 Ouvrir un fichier dans ce dossier'
'create-file': t('palette.cmd_create_file'),
'create-dir': t('palette.cmd_create_dir'),
'delete-dir': t('palette.cmd_delete_dir'),
'delete-file': t('palette.delete_file_desc'),
'open-file': t('palette.browse_open_file')
};
const isDeleteDir = _browse.action === 'delete-dir' && path;
const isDeleteFile = _browse.action === 'delete-file';
@@ -107,9 +112,9 @@ async function renderBrowse() {
const needsName = _browse.action === 'create-file' || _browse.action === 'create-dir';
if (isDeleteDir) {
_items.push({ type:'_action', label: '🗑️ Supprimer '+ (path.split('/').pop()||path), sublabel: 'Tout le contenu sera supprimé', act:()=>execDelDir(path) });
_items.push({ type:'_action', label: '🗑️ '+t('file.delete')+' '+(path.split('/').pop()||path), sublabel: t('palette.dir_delete_confirm'), act:()=>execDelDir(path) });
} else if (needsName) {
_items.push({ type:'_action', label: actLabel[_browse.action]||'', sublabel: 'Dans '+(path||'racine'), act:()=>askName() });
_items.push({ type:'_action', label: actLabel[_browse.action]||'', sublabel: t('palette.browse_in', {path: path||t('palette.browse_root')}), act:()=>askName() });
}
// Parent
@@ -135,8 +140,8 @@ async function renderBrowse() {
}
}
if (_items.length === 0) _items.push({ type:'_empty', label: '📂 Dossier vide', sublabel: '' });
} catch { _items = [{ type:'_error', label: 'Erreur', sublabel: '' }]; }
if (_items.length === 0) _items.push({ type:'_empty', label: t('palette.empty_dir'), sublabel: '' });
} catch { _items = [{ type:'_error', label: t('common.error'), sublabel: '' }]; }
_allItems = _items;
applyBrowseFilter();
@@ -178,7 +183,7 @@ async function searchBrowseGlobal(rawQ) {
});
if (sug.length === 0) {
_items = [{ type:'_empty', label: 'Aucun résultat pour "'+rawQ+'"', sublabel: '' }];
_items = [{ type:'_empty', label: t('palette.no_results', {query: rawQ}), sublabel: '' }];
render();
return;
}
@@ -220,7 +225,7 @@ async function searchBrowseGlobal(rawQ) {
}
render();
} catch { _items = [{ type:'_error', label: 'Erreur de recherche', sublabel: '' }]; render(); }
} catch { _items = [{ type:'_error', label: t('palette.search_error'), sublabel: '' }]; render(); }
}
// Navigate to a directory from global search
@@ -501,9 +506,9 @@ async function searchFiles(rawQ) {
}
function searchCmd(q) {
if (!q) { _items = CMDS.map(c => ({ type:'cmd', ...c })); render(); return; }
if (!q) { _items = getCMDS().map(c => ({ type:'cmd', ...c })); render(); return; }
const lq = q.toLowerCase();
_items = CMDS.filter(c => c.label.toLowerCase().includes(lq)||c.desc.toLowerCase().includes(lq)||(c.cat&&c.cat.toLowerCase().includes(lq))).map(c => ({ type:'cmd', ...c }));
_items = getCMDS().filter(c => c.label.toLowerCase().includes(lq)||c.desc.toLowerCase().includes(lq)||(c.cat&&c.cat.toLowerCase().includes(lq))).map(c => ({ type:'cmd', ...c }));
render();
}
+3 -3
View File
@@ -847,7 +847,7 @@ export function renderSearchResults(data, query, tagFilter) {
const header = buildSearchResultsHeader(data, query, tagFilter);
area.appendChild(header);
if (data.results.length === 0) {
area.appendChild(el("p", { style: "color:var(--text-muted);margin-top:20px" }, [document.createTextNode("Aucun résultat trouvé.")]));
area.appendChild(el("p", { style: "color:var(--text-muted);margin-top:20px" }, [document.createTextNode(t("search.no_results"))]));
return;
}
const container = el("div", { class: "search-results" });
@@ -965,7 +965,7 @@ export function renderAdvancedSearchResults(data, query, tagFilter) {
header.appendChild(sortDiv);
// Save search button
const saveBtn = el("button", { class: "search-save-btn", type: "button", title: "Sauvegarder cette recherche" });
const saveBtn = el("button", { class: "search-save-btn", type: "button", title: t("search.save_search") });
saveBtn.innerHTML = '<i data-lucide="bookmark-plus" style="width:14px;height:14px"></i> Sauver';
saveBtn.addEventListener("click", async () => {
const inclEl = document.getElementById("search-include-input");
@@ -1063,7 +1063,7 @@ export function renderAdvancedSearchResults(data, query, tagFilter) {
// Empty state
if (data.results.length === 0) {
area.appendChild(el("p", { style: "color:var(--text-muted);margin-top:20px" }, [document.createTextNode("Aucun résultat trouvé.")]));
area.appendChild(el("p", { style: "color:var(--text-muted);margin-top:20px" }, [document.createTextNode(t("search.no_results"))]));
return;
}
+1 -1
View File
@@ -788,7 +788,7 @@ function renderFilteredSidebarResults(query, results) {
});
if (grouped.size === 0) {
container.appendChild(el("div", { class: "sidebar-filter-empty" }, [document.createTextNode("Aucun répertoire ou fichier correspondant.")]));
container.appendChild(el("div", { class: "sidebar-filter-empty" }, [document.createTextNode(t("sidebar.no_results"))]));
return;
}
+1 -1
View File
@@ -757,7 +757,7 @@ function renderFilteredSidebarResults(query, results) {
});
if (grouped.size === 0) {
container.appendChild(el("div", { class: "sidebar-filter-empty" }, [document.createTextNode("Aucun répertoire ou fichier correspondant.")]));
container.appendChild(el("div", { class: "sidebar-filter-empty" }, [document.createTextNode(t("sidebar.no_results"))]));
return;
}
+3 -3
View File
@@ -319,7 +319,7 @@ function _renderSyncPanel(panel) {
const label = typeLabels[ev.type] || ev.type;
let detail = ev.data.vaults ? ev.data.vaults.join(", ") : ev.data.vault || "";
if (ev.type === "index_start") detail = `${ev.data.total_vaults} vaults à traiter`;
if (ev.type === "index_progress") detail = `${ev.data.vault} (${ev.data.files} fichiers)`;
if (ev.type === "index_progress") detail = t("sync.vault_files", {vault: ev.data.vault, files: ev.data.files});
if (ev.type === "index_complete" && ev.data.total_files !== undefined) detail = `${ev.data.total_files} fichiers total`;
html += `<div class="sync-panel__event">
<span class="sync-panel__event-type">${label}</span>
@@ -367,7 +367,7 @@ function _renderAIStatusSection(panel) {
let extIcon, extText, extClass;
if (_aiProcessing) {
extIcon = 'loader';
extText = 'Traitement en cours…';
extText = t('sync.processing');
extClass = 'sync-panel__ai--processing';
} else if (_aiConfigured === true) {
extIcon = 'check-circle';
@@ -375,7 +375,7 @@ function _renderAIStatusSection(panel) {
extClass = 'sync-panel__ai--active';
} else if (_aiConfigured === false) {
extIcon = 'x-circle';
extText = 'Aucun fournisseur';
extText = t('sync.no_provider');
extClass = 'sync-panel__ai--inactive';
} else {
extIcon = 'help-circle';
+3 -3
View File
@@ -1701,9 +1701,9 @@ export const ContextMenuManager = {
this._addItem('edit', 'Renommer', () => this._renameItem(), isReadonly);
this._addItem('trash-2', 'Supprimer', () => this._deleteFile(), isReadonly);
this._addSeparator();
this._addItem('bookmark-plus', 'Ajouter aux bookmarks', () => this._toggleBookmark(), false);
this._addItem('bookmark-plus', t("viewer.bookmark_add"), () => this._toggleBookmark(), false);
this._addSeparator();
this._addItem('history', 'Backups / Versions', () => this._openBackupViewer(), false);
this._addItem('history', t("ui.backups_versions"), () => this._openBackupViewer(), false);
}
this._menu.classList.add('active');
@@ -1750,7 +1750,7 @@ export const ContextMenuManager = {
callback();
});
} else {
item.title = 'Vault en lecture seule';
item.title = t("ui.vault_readonly");
}
this._menu.appendChild(item);
+51 -31
View File
@@ -2,9 +2,9 @@
* Vault Home Page module for ObsiGate.
*
* When a specific vault is selected (not "All"), replaces the dashboard
* with a recursive file listing sorted by modification time (newest first).
* Files from the selected directory AND all its subdirectories are shown
* with their relative path displayed in the metadata line.
* with a non-recursive file listing sorted by modification time (newest first).
* Only files from the selected directory are shown (not subdirectories)
* to keep the listing fast and lightweight.
* The listing updates automatically when browsing into subdirectories.
*
* The visual style mirrors the search results (.search-result-item) for
@@ -18,6 +18,7 @@ import { api } from './auth.js';
import { el } from './viewer.js';
import { getFileIcon, safeCreateIcons } from './utils.js';
import { TabManager } from './ui.js';
import { t, getLocale } from './i18n.js';
// ---------------------------------------------------------------------------
// State
@@ -77,7 +78,7 @@ export async function showVaultHome(vaultName, dirPath = '') {
resultsArea.innerHTML = `
<div class="tree-loading" style="padding:2rem;text-align:center">
<div class="loading-spinner" style="width:24px;height:24px;border-width:3px;margin:0 auto"></div>
<p style="margin-top:0.75rem;color:var(--text-muted);font-size:0.85rem">Chargement des fichiers...</p>
<p data-i18n="vaulthome.loading" style="margin-top:0.75rem;color:var(--text-muted);font-size:0.85rem">${t('vaulthome.loading')}</p>
</div>`;
await loadAndRender(vaultName, state.currentDir, resultsArea);
@@ -110,6 +111,10 @@ export async function updateVaultHomeDir(vaultName, dirPath) {
safeCreateIcons();
}
// Remove recent section when navigating into subdirectory
const recentSection = container.querySelector('.vault-home-recent');
if (recentSection) recentSection.remove();
// Reload results
const resultsArea = document.getElementById('vault-home-results');
if (resultsArea) {
@@ -125,9 +130,31 @@ export async function updateVaultHomeDir(vaultName, dirPath) {
// Tree click delegation — update vault home on directory clicks
// ---------------------------------------------------------------------------
let _treeDelegationInstalled = false;
function _initTreeDelegation() {
if (_treeDelegationInstalled) return;
const tree = document.getElementById('vault-tree');
if (!tree) return;
if (!tree) {
// Retry once when DOM might not be ready — use MutationObserver as fallback
const observer = new MutationObserver(() => {
const t = document.getElementById('vault-tree');
if (t) {
observer.disconnect();
_attachTreeListener(t);
}
});
observer.observe(document.body, { childList: true, subtree: true });
return;
}
_attachTreeListener(tree);
}
function _attachTreeListener(tree) {
if (_treeDelegationInstalled) return;
_treeDelegationInstalled = true;
tree.addEventListener('click', (e) => {
if (!state.active) return;
@@ -192,14 +219,14 @@ function buildBreadcrumbHTML(parentEl, vaultName, dirPath) {
async function loadAndRender(vaultName, dirPath, container) {
try {
const url = `/api/vault/${encodeURIComponent(vaultName)}/files?dir=${encodeURIComponent(dirPath)}&limit=200&recursive=true`;
const url = `/api/vault/${encodeURIComponent(vaultName)}/files?dir=${encodeURIComponent(dirPath)}&limit=200&recursive=false`;
const data = await api(url);
if (!data.files || data.files.length === 0) {
container.innerHTML = `
<div class="vault-home-empty">
<i data-lucide="folder-open" style="width:32px;height:32px;color:var(--text-muted)"></i>
<p>Aucun fichier dans ce répertoire</p>
<p data-i18n="vaulthome.empty">${t('vaulthome.empty')}</p>
</div>`;
safeCreateIcons();
return;
@@ -207,21 +234,15 @@ async function loadAndRender(vaultName, dirPath, container) {
// Header
const header = el('div', { class: 'vault-home-header' });
const count = el('span', { class: 'vault-home-count' }, [
document.createTextNode(`${data.files.length} fichier${data.files.length > 1 ? 's' : ''}`),
const countKey = data.files.length > 1 ? 'vaulthome.file_count_plural' : 'vaulthome.file_count';
const countStr = t(countKey, { count: data.files.length });
const count = el('span', { class: 'vault-home-count', 'data-i18n': countKey }, [
document.createTextNode(countStr),
]);
header.appendChild(count);
// Recursive indicator
if (data.recursive) {
const recursiveBadge = el('span', { class: 'vault-home-recursive-badge' }, [
document.createTextNode('récursif'),
]);
header.appendChild(recursiveBadge);
}
// Directory label
const dirLabel = dirPath ? `/${dirPath}` : ' (racine)';
const dirLabel = dirPath ? `/${dirPath}` : ` ${t('common.root')}`;
const dirInfo = el('span', { class: 'vault-home-dir-info' }, [
document.createTextNode(dirLabel),
]);
@@ -232,28 +253,26 @@ async function loadAndRender(vaultName, dirPath, container) {
// File list — using search-result-item style
const list = el('div', { class: 'search-results' });
const locale = getLocale();
data.files.forEach((f) => {
const fileIconName = getFileIcon ? getFileIcon(f.name) : 'file';
const displayName = f.name.match(/\.md$/i) ? f.name.replace(/\.md$/i, '') : f.name;
const displayName = f.name.endsWith('.md') ? f.name.slice(0, -3) : f.name;
const ext = f.extension || '';
const titleDiv = el('div', { class: 'search-result-title' }, [
document.createTextNode(displayName),
]);
// Format date
// Format date using current locale
const modDate = new Date(f.modified * 1000);
const dateStr = modDate.toLocaleDateString('fr-CA', {
const dateStr = modDate.toLocaleDateString(locale === 'fr' ? 'fr-CA' : 'en-CA', {
year: 'numeric', month: 'short', day: 'numeric',
hour: '2-digit', minute: '2-digit',
});
// Build metadata line: rel_dir (if any) + date + size
// Build metadata line: date + size
const metaParts = [];
if (f.rel_dir) {
metaParts.push(f.rel_dir + '/');
}
metaParts.push(dateStr);
metaParts.push(formatSize(f.size));
@@ -284,9 +303,10 @@ async function loadAndRender(vaultName, dirPath, container) {
container.appendChild(list);
safeCreateIcons();
} catch (err) {
const errMsg = t('vaulthome.error_loading') + ' ' + escapeHtml(err?.detail || err?.message || t('common.unknown'));
container.innerHTML = `
<div class="vault-home-error">
<p>Erreur de chargement: ${escapeHtml(err?.detail || err?.message || 'Inconnue')}</p>
<p data-i18n="vaulthome.error_loading">${errMsg}</p>
</div>`;
}
}
@@ -308,13 +328,13 @@ async function loadRecentSection(vaultName, container) {
const header = el('div', { class: 'vault-home-recent-header' });
header.innerHTML = `
<i data-lucide="clock" style="width:14px;height:14px"></i>
<span>Récents</span>`;
<span data-i18n="dashboard.recent">${t('dashboard.recent')}</span>`;
section.appendChild(header);
const list = el('div', { class: 'vault-home-recent-list' });
files.forEach((f) => {
const title = f.title || f.path.split('/').pop() || '';
const displayTitle = title.match(/\.md$/i) ? title.replace(/\.md$/i, '') : title;
const displayTitle = title.endsWith('.md') ? title.slice(0, -3) : title;
const dir = f.path.includes('/') ? f.path.split('/').slice(0, -1).join('/') + '/' : '';
const item = el('div', { class: 'vault-home-recent-item' });
@@ -352,9 +372,9 @@ async function loadRecentSection(vaultName, container) {
// ---------------------------------------------------------------------------
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' o';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' Ko';
return (bytes / (1024 * 1024)).toFixed(1) + ' Mo';
if (bytes < 1024) return bytes + ' ' + t('vaulthome.size_bytes');
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' ' + t('vaulthome.size_kb');
return (bytes / (1024 * 1024)).toFixed(1) + ' ' + t('vaulthome.size_mb');
}
function escapeHtml(str) {
+13 -13
View File
@@ -499,7 +499,7 @@ export function renderFile(data) {
const sourceBtn = el("button", { class: "btn-action", title: "Voir la source" }, [icon("code", 14), document.createTextNode("Source")]);
// MD download button
const mdBtn = el("button", { class: "btn-action", title: "Télécharger en .md" }, [icon("file-text", 14), document.createTextNode(".md")]);
const mdBtn = el("button", { class: "btn-action", title: t("viewer.download_md") }, [icon("file-text", 14), document.createTextNode(".md")]);
mdBtn.addEventListener("click", () => {
const dlUrl = `/api/file/${encodeURIComponent(data.vault)}/download?path=${encodeURIComponent(data.path)}`;
const a = document.createElement("a");
@@ -511,7 +511,7 @@ export function renderFile(data) {
});
// PDF download button
const pdfBtn = el("button", { class: "btn-action", title: "Télécharger en PDF" }, [icon("file", 14), document.createTextNode("PDF")]);
const pdfBtn = el("button", { class: "btn-action", title: t("viewer.download_pdf") }, [icon("file", 14), document.createTextNode("PDF")]);
pdfBtn.addEventListener("click", () => {
const pdfUrl = `/api/file/${encodeURIComponent(data.vault)}/pdf?path=${encodeURIComponent(data.path)}`;
window.open(pdfUrl, "_blank");
@@ -522,7 +522,7 @@ export function renderFile(data) {
openEditor(data.vault, data.path);
});
const forgeBtn = el("button", { class: "btn-action btn-forge", title: "Forge (nouvel editeur)" }, [icon("hammer", 14), document.createTextNode("Forge")]);
const forgeBtn = el("button", { class: "btn-action btn-forge", title: t("viewer.forge_title") }, [icon("hammer", 14), document.createTextNode("Forge")]);
forgeBtn.addEventListener("click", () => {
var modal = document.getElementById("editor-modal");
var container = modal.querySelector(".editor-container");
@@ -534,7 +534,7 @@ export function renderFile(data) {
var brand = modal.querySelector(".editor-brand");
var titleInp = document.getElementById("editor-title-input");
var fileInp = document.getElementById("editor-filename-input");
if (brand) brand.textContent = "Forge";
if (brand) brand.textContent = t("viewer.forge_brand");
if (titleInp) { titleInp.value = data.title || ''; titleInp.placeholder = t('forge.title_placeholder'); }
if (fileInp) fileInp.value = data.path.split('/').pop();
bodyEl.innerHTML = "";
@@ -574,14 +574,14 @@ export function renderFile(data) {
shareBtn.addEventListener("click", () => openShareDialog(data.vault, data.path));
// Bookmark button — check if already bookmarked
const bookmarkBtn = el("button", { class: "btn-action btn-bookmark", title: "Ajouter/Retirer des bookmarks" }, [icon("bookmark-plus", 14), document.createTextNode("Bookmark")]);
const bookmarkBtn = el("button", { class: "btn-action btn-bookmark", title: t("viewer.bookmark_toggle") }, [icon("bookmark-plus", 14), document.createTextNode("Bookmark")]);
// Check bookmark status and color the button
(async () => {
try {
const bms = await api("/api/bookmarks");
if (Array.isArray(bms) && bms.some(b => b.vault === data.vault && b.path === data.path)) {
bookmarkBtn.classList.add("active");
bookmarkBtn.title = "Retirer des bookmarks";
bookmarkBtn.title = t("viewer.bookmark_remove");
}
} catch (e) { /* ignore */ }
})();
@@ -589,7 +589,7 @@ export function renderFile(data) {
try {
const res = await api("/api/bookmarks/toggle", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ vault: data.vault, path: data.path, title: data.title }) });
bookmarkBtn.classList.toggle("active", res.bookmarked);
bookmarkBtn.title = res.bookmarked ? "Retirer des bookmarks" : "Ajouter aux bookmarks";
bookmarkBtn.title = res.bookmarked ? t("viewer.bookmark_remove") : t("viewer.bookmark_add");
showToast(res.bookmarked ? "Ajouté aux bookmarks" : "Retiré des bookmarks", "success");
if (typeof DashboardBookmarkWidget !== "undefined") DashboardBookmarkWidget.load();
} catch (err) { showToast("Erreur: " + err.message, "error"); }
@@ -1351,7 +1351,7 @@ const IndexUpdateManager = (() => {
disconnected: "Déconnecté",
connecting: "Connexion...",
connected: "Synchronisé",
syncing: "Mise à jour...",
syncing: t("viewer.syncing"),
};
badge.title = labels[connectionState] || connectionState;
}
@@ -1444,12 +1444,12 @@ function _renderSyncPanel(panel) {
events.slice(0, 10).forEach((ev) => {
const time = new Date(ev.timestamp).toLocaleTimeString();
const typeLabels = {
index_updated: "Mise à jour",
index_updated: t("viewer.index_updated"),
index_reloaded: "Rechargement",
vault_added: "Vault ajouté",
vault_removed: "Vault supprimé",
index_start: "Démarrage index.",
index_progress: "Vault indexé",
vault_added: t("sync.vault_added_label"),
vault_removed: t("sync.vault_removed_label"),
index_start: t("viewer.index_start"),
index_progress: t("sync.vault_indexed_label"),
index_complete: "Indexation tech.",
};
const label = typeLabels[ev.type] || ev.type;