diff --git a/frontend/js/app.js b/frontend/js/app.js index bf6de33..9e15afa 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -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(); diff --git a/frontend/js/backup-manager.js b/frontend/js/backup-manager.js index ba7bc00..d546cc1 100644 --- a/frontend/js/backup-manager.js +++ b/frontend/js/backup-manager.js @@ -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)); diff --git a/frontend/js/backups.js b/frontend/js/backups.js index fb3fdd2..7ef01a9 100644 --- a/frontend/js/backups.js +++ b/frontend/js/backups.js @@ -180,7 +180,7 @@ function renderMainView() { var html = '
' + '
' + - '

Backups

' + + '

'+t('backups.title')+'

' + '' + escapeHtml(state.path) + '' + '
' + '
' + diff --git a/frontend/js/config.js b/frontend/js/config.js index 72a1375..8a458f4 100644 --- a/frontend/js/config.js +++ b/frontend/js/config.js @@ -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`; diff --git a/frontend/js/graph.js b/frontend/js/graph.js index 5c6c860..3ba24c0 100644 --- a/frontend/js/graph.js +++ b/frontend/js/graph.js @@ -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(`
📌 Statut ${fm.statut}
`); if (fm.catégorie || fm.categorie) rows.push(`
${t('graph.category')} ${fm.catégorie || fm.categorie}
`); if (fm.publish) rows.push(`
✅ ${t('graph.published')}
`); - if (fm.favoris) rows.push('
⭐ Favoris
'); + if (fm.favoris) rows.push('
⭐ ' + t('graph.favorites') + '
'); 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 = `
@@ -796,7 +796,7 @@ export const GraphViewManager = { return; } // Show loading indicator - this._showPreviewTooltip(node, 'Chargement...', screenX, screenY); + this._showPreviewTooltip(node, '' + t('common.loading') + '', 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 diff --git a/frontend/js/mobile-toolbar.js b/frontend/js/mobile-toolbar.js index ecb9ecd..b1adc19 100644 --- a/frontend/js/mobile-toolbar.js +++ b/frontend/js/mobile-toolbar.js @@ -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; } diff --git a/frontend/js/palette.js b/frontend/js/palette.js index 4790aa9..963b920 100644 --- a/frontend/js/palette.js +++ b/frontend/js/palette.js @@ -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(); } diff --git a/frontend/js/search.js b/frontend/js/search.js index 4012cde..2da17ae 100644 --- a/frontend/js/search.js +++ b/frontend/js/search.js @@ -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 = ' 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; } diff --git a/frontend/js/sidebar.js b/frontend/js/sidebar.js index 8611353..b4146e0 100644 --- a/frontend/js/sidebar.js +++ b/frontend/js/sidebar.js @@ -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; } diff --git a/frontend/js/sidebar_raw.js b/frontend/js/sidebar_raw.js index e454eda..2c5a070 100644 --- a/frontend/js/sidebar_raw.js +++ b/frontend/js/sidebar_raw.js @@ -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; } diff --git a/frontend/js/sync.js b/frontend/js/sync.js index 72ab0cf..2f3db21 100644 --- a/frontend/js/sync.js +++ b/frontend/js/sync.js @@ -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 += `
${label} @@ -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'; diff --git a/frontend/js/ui.js b/frontend/js/ui.js index 596a029..0c4ec8e 100644 --- a/frontend/js/ui.js +++ b/frontend/js/ui.js @@ -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); diff --git a/frontend/js/vaulthome.js b/frontend/js/vaulthome.js index 15351e2..f21f44b 100644 --- a/frontend/js/vaulthome.js +++ b/frontend/js/vaulthome.js @@ -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 = `
-

Chargement des fichiers...

+

${t('vaulthome.loading')}

`; 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 = `
-

Aucun fichier dans ce répertoire

+

${t('vaulthome.empty')}

`; 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 = `
-

Erreur de chargement: ${escapeHtml(err?.detail || err?.message || 'Inconnue')}

+

${errMsg}

`; } } @@ -308,13 +328,13 @@ async function loadRecentSection(vaultName, container) { const header = el('div', { class: 'vault-home-recent-header' }); header.innerHTML = ` - Récents`; + ${t('dashboard.recent')}`; 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) { diff --git a/frontend/js/viewer.js b/frontend/js/viewer.js index fbfdfd1..da7f7a8 100644 --- a/frontend/js/viewer.js +++ b/frontend/js/viewer.js @@ -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; diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 2d9bb8b..e2e71fa 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -104,7 +104,9 @@ "auth.login_title": "Secure access to your vaults", "auth.logout_title": "Sign out", "auth.network_error": "Network error — check your connection", + "auth.new_password": "New password (empty = unchanged)", "auth.password": "Password", + "auth.password_min": "Min. 8 characters", "auth.remember_me": "Remember me (30 days)", "auth.session_expired": "Session expired, please sign in again", "auth.user_deleted": "User deleted", @@ -201,6 +203,14 @@ "backup.timestamp_not_found": "Cannot find backup timestamp", "backup.title": "Backups", "backup.version": "Version from {date}", + "backups.delete_confirm": "Delete this backup?", + "backups.empty": "No backups available for this file.", + "backups.hint": "Backups are created automatically before each modification.", + "backups.loading": "Loading backups...", + "backups.purge_file": "Delete ALL backups of", + "backups.purge_vault": "Delete ALL backups from vault", + "backups.restore_confirm": "to the selected backup version?\nThe current version will be saved.", + "backups.title": "Backups", "button.cancel": "Cancel", "common.actions": "Actions", "common.all": "All", @@ -225,8 +235,10 @@ "common.previous": "Previous", "common.refresh": "Refresh", "common.rename": "Rename", + "common.root": "(root)", "common.save": "Save", "common.search": "Search", + "common.unknown": "Unknown", "common.yes": "Yes", "config.about": "About", "config.about_error": "Loading error", @@ -252,23 +264,25 @@ "config.error_unknown": "Unknown error", "config.force_reindex": "Force reindex", "config.hidden_files": "Hidden files", - "config.hidden_files_desc": "Manage display of hidden files and folders.", + "config.hidden_files_desc": "Hide files/folders starting with a dot in the interface (they remain indexed and searchable)", + "config.hidden_files_toggle": "Hide hidden files/folders", "config.hidden_include_label": "Include hidden files", "config.hidden_whitelist_label": "Whitelist (comma-separated)", - "config.hint_activer_d_sactiver_la_surveillance_automatique_des": "Activer/désactiver la surveillance automatique des fichiers", + "config.hint_activer_d_sactiver_la_surveillance_automatique_des": "Enable/disable automatic file monitoring", "config.hint_annuler_la_recherche_apr_s_ce_d_lai_5000_120000_": "Cancel search after this delay (5000-120000)", "config.hint_contenu_index_par_fichier_10k_1m_r_indexation_requ": "Max content size per file (bytes)", "config.hint_d_lai_avant_ex_cution_de_la_recherche_100_2000_": "Delay before search execution (100-2000)", - "config.hint_d_lai_avant_traitement_des_changements_group_s_0_5": "Délai avant traitement des changements groupés (0.5-10 secondes)", + "config.hint_d_lai_avant_traitement_des_changements_group_s_0_5": "Delay before processing grouped changes (0.5-10 seconds)", "config.hint_entre_5_et_100_fichiers_sauvegarde_sur_le_serveur_": "Between 5 and 100 files (stored on server)", "config.hint_forcer_le_mode_polling_au_lieu_de_inotify_natif_ut": "Forcer le mode polling au lieu de inotify natif (utile si le mode natif ne fonctionne pas)", "config.hint_intervalle_de_scrutation_en_mode_polling_1_30_seco": "Intervalle de scrutation en mode polling (1-30 secondes)", "config.hint_multiplicateur_de_pertinence_pour_les_correspondan": "Multiplicateur de pertinence pour les correspondances dans le titre", "config.hint_nombre_de_r_sultats_affich_s_par_page_10_200_": "Number of results displayed per page (10-200)", - "config.hint_nombre_max_de_tokens_largis_par_pr_fixe_10_200_": "Nombre max de tokens élargis par préfixe (10-200)", + "config.hint_nombre_max_de_tokens_largis_par_pr_fixe_10_200_": "Max expanded tokens per prefix (10-200)", "config.hint_nombre_maximum_de_versions_backup_conservees_par_f": "Maximum backup versions kept per file (1-50)", "config.hint_nombre_minimum_de_caract_res_avant_recherche_1_5_": "Minimum characters before search triggers (1-5)", - "config.hint_threads_d_di_s_la_recherche_1_8_": "Threads dédiés à la recherche (1-8)", + "config.hint_threads_d_di_s_la_recherche_1_8_": "Dedicated search threads (1-8)", + "config.just_now": "just now", "config.lang_hint": "Used by autocomplete and ghost text.", "config.lang_label": "Preferred language", "config.link_copied": "Link copied!", @@ -314,9 +328,9 @@ "config.search_workers": "Search workers", "config.search_workers_hint": "Dedicated search threads (1-8)", "config.search_workers_label": "Search workers", - "config.section_a-propos": "📦 À propos", + "config.section_a-propos": "📦 About", "config.section_about": "About", - "config.section_actions-sur-fichiers": "Actions sur fichiers", + "config.section_actions-sur-fichiers": "File actions", "config.section_activation": "Activation", "config.section_affichage": "Affichage", "config.section_ai": "AI API keys", @@ -324,56 +338,56 @@ "config.section_astuces-et-bonnes-pratiques": "💡 Astuces et bonnes pratiques", "config.section_audit-log": "Audit log", "config.section_authentification": "Authentification", - "config.section_autocompletion": "Autocomplétion", + "config.section_autocompletion": "Autocompletion", "config.section_backend": "🖥️ Backend Settings", "config.section_backup-automatique": "Backup automatique", "config.section_backups": "Backups", "config.section_barre-d-outils-ai": "Barre d'outils AI", "config.section_breadcrumb-et-repositionnement": "Breadcrumb et repositionnement", "config.section_cas-d-usage": "Cas d'usage", - "config.section_cas-d-usage-avances": "Cas d'usage avancés", + "config.section_cas-d-usage-avances": "Advanced use cases", "config.section_chips-de-filtres-actifs": "Chips de filtres actifs", "config.section_cles-api-intelligence-artificielle": "🤖 Cles API Intelligence Artificielle", "config.section_configuration": "Configuration", - "config.section_configuration-avancee": "Configuration avancée", + "config.section_configuration-avancee": "Advanced configuration", "config.section_construit-avec": "Construit avec", "config.section_diag": "Diagnostics", "config.section_diagnostics": "Diagnostics", - "config.section_editeur-codemirror": "Éditeur CodeMirror", - "config.section_editeur-codemirror-6": "Éditeur CodeMirror 6", - "config.section_edition-avancee": "✏️ Édition avancée", - "config.section_edition-avec-codemirror-6": "Édition avec CodeMirror 6", - "config.section_en-tete": "En-tête", - "config.section_evenements-supportes": "Événements supportés", + "config.section_editeur-codemirror": "CodeMirror Editor", + "config.section_editeur-codemirror-6": "CodeMirror 6 Editor", + "config.section_edition-avancee": "✏️ Advanced Editing", + "config.section_edition-avec-codemirror-6": "Editing with CodeMirror 6", + "config.section_en-tete": "Header", + "config.section_evenements-supportes": "Supported events", "config.section_exemples-concrets": "Exemples concrets", "config.section_facettes-et-tri": "Facettes et tri", - "config.section_fichiers": "📄 Fichiers", + "config.section_fichiers": "📄 Files", "config.section_filtrage-de-la-sidebar": "Filtrage de la sidebar", "config.section_filtrage-de-tags": "Filtrage de tags", "config.section_filtrage-de-tags-template": "Filtrage de tags template", - "config.section_fonctionnalites": "Fonctionnalités", + "config.section_fonctionnalites": "Features", "config.section_format-du-payload": "Format du payload", "config.section_gestion-des-onglets": "📑 Gestion des onglets", - "config.section_hidden": "🗂️ Hidden Files", - "config.section_historique-recent-redemarrage-non-requis": "📋 Historique récent\n Redémarrage non requis", + "config.section_hidden": "Hidden files", + "config.section_historique-recent-redemarrage-non-requis": "📋 Recent History\n No restart required", "config.section_indicateurs-visuels": "Indicateurs visuels", - "config.section_intelligence-artificielle-dans-l-editeur": "🤖 Intelligence Artificielle dans l'Éditeur", + "config.section_intelligence-artificielle-dans-l-editeur": "🤖 AI in the Editor", "config.section_interaction": "Interaction", "config.section_interface": "Interface", "config.section_interface-utilisateur": "🧭 Interface utilisateur", "config.section_limitations-actuelles": "Limitations actuelles", "config.section_mode-commandes": "Mode commandes", - "config.section_mode-fichiers": "Mode fichiers", + "config.section_mode-fichiers": "File mode", "config.section_navigation": "Navigation", "config.section_navigation-entre-onglets": "Navigation entre onglets", "config.section_onglets": "Onglets", - "config.section_operateurs-avances": "Opérateurs avancés", + "config.section_operateurs-avances": "Advanced operators", "config.section_optimisations": "Optimisations", "config.section_ouverture": "Ouverture", "config.section_ouverture-d-onglets": "Ouverture d'onglets", "config.section_palette-de-commandes": "⌨️ Palette de commandes", - "config.section_parametres-backend-redemarrage-requis": "Paramètres backend\n Redémarrage requis", - "config.section_parametres-de-recherche": "Paramètres de recherche", + "config.section_parametres-backend-redemarrage-requis": "Backend settings\n Restart required", + "config.section_parametres-de-recherche": "Search settings", "config.section_partage-et-webhooks": "🔗 Partage et Webhooks", "config.section_partages-publics": "📤 Partages publics", "config.section_perf": "⚡ Performance", @@ -382,36 +396,36 @@ "config.section_profil-utilisateur": "👤 Profil utilisateur", "config.section_profile": "User profile", "config.section_protection": "Protection", - "config.section_providers-supportes": "Providers supportés", + "config.section_providers-supportes": "Supported providers", "config.section_publication-publique": "Publication publique", "config.section_raccourcis-clavier": "⌨️ Raccourcis clavier", "config.section_recent": "📜 Recent History", "config.section_recherche": "🔍 Recherche", - "config.section_recherche-et-autocompletion": "Recherche et autocomplétion", + "config.section_recherche-et-autocompletion": "Search and autocompletion", "config.section_recherche-simple": "Recherche simple", "config.section_redimensionnement-de-la-sidebar": "Redimensionnement de la sidebar", "config.section_sauvegarde-automatique": "Sauvegarde automatique", "config.section_sauvegardes-et-audits": "💾 Sauvegardes et Audits", "config.section_search": "Search settings", - "config.section_securite": "🛡️ Sécurité", - "config.section_securite-signature-hmac-sha256": "Sécurité : signature HMAC-SHA256", - "config.section_selection-de-vault": "Sélection de vault", + "config.section_securite": "🛡️ Security", + "config.section_securite-signature-hmac-sha256": "Security: HMAC-SHA256 signature", + "config.section_selection-de-vault": "Vault selection", "config.section_server": "Server", "config.section_shares": "📤 Public Shares", - "config.section_sidebar-barre-laterale": "Sidebar (barre latérale)", + "config.section_sidebar-barre-laterale": "Sidebar", "config.section_synchronisation-automatique": "Synchronisation automatique", "config.section_tag-cloud": "Tag cloud", "config.section_tags": "🏷️ Tag Filtering", "config.section_tags-inline-vs-frontmatter": "Tags inline vs frontmatter", - "config.section_theme-clair-sombre": "Thème clair/sombre", - "config.section_themes": "🎨 Thèmes", + "config.section_theme-clair-sombre": "Light/Dark theme", + "config.section_themes": "🎨 Themes", "config.section_trucs-et-astuces": "Trucs et astuces", - "config.section_types-de-fichiers-supportes": "Types de fichiers supportés", + "config.section_types-de-fichiers-supportes": "Supported file types", "config.section_visualisation": "Visualisation", "config.section_vue-graphe": "🗺️ Vue Graphe", "config.section_watcher": "Auto sync", "config.section_webhooks": "🔗 Webhooks", - "config.section_workflow-recommande": "Workflow recommandé", + "config.section_workflow-recommande": "Recommended workflow", "config.section_zone-de-contenu": "Zone de contenu", "config.server_config": "Server config", "config.settings_saved": "✓ Settings saved", @@ -506,6 +520,9 @@ "dragdrop.move_failed": "Move failed", "editor.cancel": "Cancel", "editor.close": "Close", + "editor.delete_error": "Delete error", + "editor.edit": "Edit current file", + "editor.edit_current_desc": "Open current file in editor", "editor.find": "Find in file...", "editor.find_case": "Case sensitive", "editor.find_regex": "Regex", @@ -515,6 +532,7 @@ "editor.no_results": "No results", "editor.replace": "Replace", "editor.save": "Save", + "editor.save_error": "Save error", "editor.saved": "Saved ✓", "editor.unsaved": "Unsaved changes", "error.initialization": "Initialization error", @@ -529,22 +547,35 @@ "forge.popout_label": "pop-out", "forge.title_placeholder": "Title...", "graph.back": "Back", + "graph.btn_directory": "📁 Directory", + "graph.btn_full": "🌐 All", + "graph.cache_label": "(cached)", "graph.category": "📂 Category", "graph.click_to_open": "click to open", "graph.ctrl_hover_preview": "Ctrl+hover = full preview", "graph.ctrl_hover_preview_short": "Ctrl+hover = preview", "graph.depth": "Depth", + "graph.depth_label": "depth={depth}", + "graph.edges_count": "{count} links", + "graph.error_loading": "Loading error", "graph.export_png": "Export PNG", + "graph.favorites": "Favorites", "graph.filter_tag": "Filter by tag", "graph.filter_type": "Filter by type", "graph.focus_mode": "Focus mode", "graph.forward": "Forward", "graph.fullscreen": "Full screen", "graph.loading": "Building graph...", + "graph.nodes_count": "{count} nodes", "graph.parent": "Parent", "graph.published": "Published", "graph.published_badge": "✅ published", - "graph.title": "Graph", + "graph.scope_directory": "Directory", + "graph.scope_full": "Full vault", + "graph.title": "Graph View — {vault}{path}", + "graph.type_directory": "Directory", + "graph.type_file": "File", + "graph.type_markdown": "Markdown", "header.admin_panel": "Administration", "header.config": "Settings", "header.help": "Help", @@ -972,6 +1003,7 @@ "help.guide_official": "Official guide", "help.hamburger_btn": "Hamburger menu button", "help.header_section": "Header", + "help.help_title": "Help", "help.hide": "Hide", "help.hide_help": "Hide help", "help.highlighted_snippets": "Highlighted snippets", @@ -1131,6 +1163,7 @@ "mermaid.preview": "Preview", "mermaid.zoom_in": "Zoom in", "mermaid.zoom_out": "Zoom out", + "mobile.all_vaults": "📂 All vaults", "modal.create_btn": "Create", "modal.create_file": "Create a file", "modal.creating": "Creating...", @@ -1166,10 +1199,50 @@ "offline.reconnected": "Connection restored — synchronizing...", "offline.status": "Offline", "offline.sync_now": "Sync now", + "palette.browse_dirs": "📂 Browse directories...", + "palette.browse_hint": "Navigate vaults to find a file", + "palette.browse_in": "In {path}", + "palette.browse_no_results": "No results — browse manually", + "palette.browse_open_file": "📂 Open a file in this directory", + "palette.browse_root": "root", + "palette.close_current": "Close current file", + "palette.close_current_desc": "Close active tab without deleting", + "palette.cmd_back_home": "🏠 Home", + "palette.cmd_back_home_desc": "Back to home", + "palette.cmd_config": "⚙️ Settings", + "palette.cmd_config_desc": "Open settings", + "palette.cmd_create_dir": "📁 Create directory", + "palette.cmd_create_dir_desc": "Choose vault, navigate, then name the directory", + "palette.cmd_create_file": "📄 Create markdown file", + "palette.cmd_create_file_desc": "Choose vault, navigate, then name the file", + "palette.cmd_delete_dir": "🗑️ Delete directory", + "palette.cmd_delete_dir_desc": "Choose vault, navigate and delete", + "palette.cmd_theme": "🌓 Toggle theme", + "palette.cmd_theme_desc": "Toggle light/dark", + "palette.del_all_content": "All content will be deleted", + "palette.delete_current": "Delete current file", + "palette.delete_current_desc": "Delete file from active tab", + "palette.delete_file_desc": "Search for a file to delete", + "palette.dir_delete_confirm": "All content will be deleted", + "palette.dir_name": "Directory name", + "palette.empty_dir": "📂 Empty directory", + "palette.error_loading": "Loading error", + "palette.error_prefix": "Error: {msg}", + "palette.file_name": "File name", + "palette.help_desc": "Open the guide", + "palette.md_hint": "(.md added if needed)", "palette.mode_commands": "Commands", "palette.mode_files": "Files", + "palette.no_results": "No results for \"{query}\"", + "palette.no_vaults": "No vaults found", + "palette.popout_desc": "Open document in a window", + "palette.reindex_desc": "Force full reindex", "palette.search_commands": "Type a command...", + "palette.search_error": "Search error", "palette.search_files": "Search files...", + "palette.type_command": "Type a command...", + "palette.type_search": "Search files... (+include -exclude)", + "palette.vault_count": "{count} files", "pwa.dismiss": "Later", "pwa.install_button": "Install", "pwa.install_desc": "Install this app on your device for quick access.", @@ -1258,7 +1331,7 @@ "sidebar.filter_results_grouped": "Grouped results", "sidebar.loading": "Loading...", "sidebar.no_recent_files": "No recent files", - "sidebar.no_results": "No results", + "sidebar.no_results": "No matching directory or file.", "sidebar.no_saved_searches": "No saved searches", "sidebar.open_file": "Open file", "sidebar.saved_dirs_tab": "Folders", @@ -1287,17 +1360,25 @@ "sync.index_started": "Indexing started ({count} vaults)", "sync.install_success": "ObsiGate installed successfully!", "sync.model_not_loaded": "Model not loaded", + "sync.no_provider": "No provider", "sync.no_recent_events": "No recent events", "sync.not_configured": "Not configured", + "sync.processing": "Processing…", "sync.update": "Update", "sync.updating": "Updating...", "sync.vault_added": "Vault \"{vault}\" added ({count} files)", + "sync.vault_added_label": "Vault added", + "sync.vault_files": "{vault} ({files} files)", + "sync.vault_indexed_label": "Vault indexed", "sync.vault_removed": "Vault \"{vault}\" removed", + "sync.vault_removed_label": "Vault removed", "sync.verifying": "Verifying…", "theme.change": "Change theme", "theme.dark": "Dark", "theme.light": "Light", "theme.title": "Theme", + "themes.name_gradients": "Subtle gradients", + "themes.name_terminal": "Pure black & green", "toast.ai_keys_saved": "API keys saved", "toast.ai_no_key": "No keys to save", "toast.bookmark_added": "Added to bookmarks", @@ -1351,22 +1432,42 @@ "tree.new_file": "New file", "tree.new_folder": "New folder", "tree.vault_home": "Vault home", + "ui.backups_versions": "Backups / Versions", "ui.cannot_rename": "Cannot rename this item", + "ui.delete_name_empty": "Name cannot be empty", "ui.element_not_found": "Element not found in tree", + "ui.vault_readonly": "Read-only vault", "vault.add_dynamic": "Add vault", "vault.reindex": "Reindex vault", "vault.remove_dynamic": "Remove vault", + "vaulthome.empty": "No files in this directory", + "vaulthome.error_loading": "Loading error:", + "vaulthome.file_count": "{count} file", + "vaulthome.file_count_plural": "{count} files", + "vaulthome.loading": "Loading files...", + "vaulthome.size_bytes": "B", + "vaulthome.size_kb": "KB", + "vaulthome.size_mb": "MB", "viewer.backlinks": "Backlinks", "viewer.binary_download": "Download file", "viewer.binary_unsupported": "Unsupported binary file", + "viewer.bookmark_add": "Add to bookmarks", + "viewer.bookmark_remove": "Remove bookmarks", + "viewer.bookmark_toggle": "Add/Remove bookmarks", "viewer.breadcrumb_home": "Home", "viewer.click_to_download": "Click to download", "viewer.copied": "Copied!", "viewer.copy": "Copy", "viewer.copy_error": "Copy error", "viewer.download": "Download", + "viewer.download_md": "Download as .md", + "viewer.download_pdf": "Download as PDF", "viewer.edit": "Edit", "viewer.error": "Loading error", + "viewer.forge_brand": "Forge", + "viewer.forge_title": "Forge (new editor)", + "viewer.index_start": "Starting index...", + "viewer.index_updated": "Updated", "viewer.loaded_from_cache": "File loaded from offline cache", "viewer.loading": "Loading file...", "viewer.markdown": "Markdown", @@ -1385,6 +1486,7 @@ "viewer.share": "Share", "viewer.shared_doc_title": "Shared document — click to manage", "viewer.source": "Source", + "viewer.syncing": "Updating...", "viewer.toc": "Table of contents", "viewer.unsupported_file": "This file is binary and cannot be displayed", "webhook.trigger_create": "Creation via API or palette", diff --git a/frontend/locales/fr.json b/frontend/locales/fr.json index ccb213c..ca7eb73 100644 --- a/frontend/locales/fr.json +++ b/frontend/locales/fr.json @@ -104,7 +104,9 @@ "auth.login_title": "Accès sécurisé à vos vaults", "auth.logout_title": "Déconnexion", "auth.network_error": "Erreur réseau — vérifiez votre connexion", + "auth.new_password": "Nouveau mot de passe (vide = inchangé)", "auth.password": "Mot de passe", + "auth.password_min": "Min. 8 caracteres", "auth.remember_me": "Se souvenir de moi (30 jours)", "auth.session_expired": "Session expirée, veuillez vous reconnecter", "auth.user_deleted": "Utilisateur supprimé", @@ -201,6 +203,14 @@ "backup.timestamp_not_found": "Impossible de trouver le timestamp du backup", "backup.title": "Sauvegardes", "backup.version": "Version du {date}", + "backups.delete_confirm": "Supprimer ce backup ?", + "backups.empty": "Aucun backup disponible pour ce fichier.", + "backups.hint": "Les backups sont crees automatiquement avant chaque modification.", + "backups.loading": "Chargement des backups...", + "backups.purge_file": "Supprimer TOUS les backups de", + "backups.purge_vault": "Supprimer TOUS les backups du vault", + "backups.restore_confirm": "a la version du backup selectionne ?\nLa version actuelle sera sauvegardee.", + "backups.title": "Backups", "button.cancel": "Annuler", "common.actions": "Actions", "common.all": "Tous", @@ -225,8 +235,10 @@ "common.previous": "Précédent", "common.refresh": "Rafraîchir", "common.rename": "Renommer", + "common.root": "(racine)", "common.save": "Enregistrer", "common.search": "Rechercher", + "common.unknown": "Inconnue", "common.yes": "Oui", "config.about": "À propos", "config.about_error": "Erreur de chargement", @@ -252,7 +264,8 @@ "config.error_unknown": "Erreur inconnue", "config.force_reindex": "Forcer réindexation", "config.hidden_files": "Fichiers cachés", - "config.hidden_files_desc": "Gérez l'affichage des fichiers et dossiers cachés.", + "config.hidden_files_desc": "Masquer les fichiers/dossiers commencant par un point dans l'interface (ils restent indexes et cherchables)", + "config.hidden_files_toggle": "Masquer les fichiers/dossiers caches", "config.hidden_include_label": "Inclure les fichiers cachés", "config.hidden_whitelist_label": "Whitelist (séparés par des virgules)", "config.hint_activer_d_sactiver_la_surveillance_automatique_des": "Activer/désactiver la surveillance automatique des fichiers", @@ -269,6 +282,7 @@ "config.hint_nombre_maximum_de_versions_backup_conservees_par_f": "Nombre maximum de versions backup conservees par fichier (1-50)", "config.hint_nombre_minimum_de_caract_res_avant_recherche_1_5_": "Nombre minimum de caractères avant recherche (1-5)", "config.hint_threads_d_di_s_la_recherche_1_8_": "Threads dédiés à la recherche (1-8)", + "config.just_now": "a l'instant", "config.lang_hint": "Utilisée par l'autocomplétion et le ghost text.", "config.lang_label": "Langue préférée", "config.link_copied": "Lien copié !", @@ -506,6 +520,9 @@ "dragdrop.move_failed": "Move failed", "editor.cancel": "Annuler", "editor.close": "Fermer", + "editor.delete_error": "Erreur de suppression", + "editor.edit": "Éditer fichier courant", + "editor.edit_current_desc": "Ouvrir le fichier actif dans l'éditeur", "editor.find": "Rechercher dans le fichier...", "editor.find_case": "Respecter la casse", "editor.find_regex": "Regex", @@ -515,6 +532,7 @@ "editor.no_results": "Aucun résultat", "editor.replace": "Remplacer", "editor.save": "Enregistrer", + "editor.save_error": "Erreur de sauvegarde", "editor.saved": "Sauvegardé ✓", "editor.unsaved": "Modifications non sauvegardées", "error.initialization": "Erreur lors de l'initialisation", @@ -529,22 +547,35 @@ "forge.popout_label": "pop-out", "forge.title_placeholder": "Titre...", "graph.back": "Retour", + "graph.btn_directory": "📁 Dossier", + "graph.btn_full": "🌐 Tout", + "graph.cache_label": "(cache)", "graph.category": "📂 Catégorie", "graph.click_to_open": "cliquer pour ouvrir", "graph.ctrl_hover_preview": "Ctrl+survol = aperçu complet", "graph.ctrl_hover_preview_short": "Ctrl+survol = aperçu", "graph.depth": "Profondeur", + "graph.depth_label": "prof={depth}", + "graph.edges_count": "{count} liens", + "graph.error_loading": "Erreur de chargement", "graph.export_png": "Exporter PNG", + "graph.favorites": "Favoris", "graph.filter_tag": "Filtrer par tag", "graph.filter_type": "Filtrer par type", "graph.focus_mode": "Mode focus", "graph.forward": "Avancer", "graph.fullscreen": "Plein écran", "graph.loading": "Construction du graphe...", + "graph.nodes_count": "{count} nœuds", "graph.parent": "Parent", "graph.published": "Publié", "graph.published_badge": "✅ publié", - "graph.title": "Graphe", + "graph.scope_directory": "Dossier", + "graph.scope_full": "Vault complet", + "graph.title": "Vue Graphique — {vault}{path}", + "graph.type_directory": "Dossier", + "graph.type_file": "Fichier", + "graph.type_markdown": "Markdown", "header.admin_panel": "Administration", "header.config": "Configuration", "header.help": "Aide", @@ -972,6 +1003,7 @@ "help.guide_official": "Guide officiel", "help.hamburger_btn": "Bouton menu hamburger", "help.header_section": "En-tête", + "help.help_title": "Aide", "help.hide": "Masquage", "help.hide_help": "Masquer l'aide", "help.highlighted_snippets": "Snippets surlignés", @@ -1131,6 +1163,7 @@ "mermaid.preview": "Aperçu", "mermaid.zoom_in": "Zoom avant", "mermaid.zoom_out": "Zoom arrière", + "mobile.all_vaults": "📂 Toutes les vaults", "modal.create_btn": "Créer", "modal.create_file": "Créer un fichier", "modal.creating": "Création...", @@ -1166,10 +1199,50 @@ "offline.reconnected": "Connexion rétablie — synchronisation en cours...", "offline.status": "Hors-ligne", "offline.sync_now": "Synchroniser maintenant", + "palette.browse_dirs": "📂 Parcourir les répertoires...", + "palette.browse_hint": "Naviguer dans les vaults pour trouver un fichier", + "palette.browse_in": "Dans {path}", + "palette.browse_no_results": "Aucun résultat — naviguer manuellement", + "palette.browse_open_file": "📂 Ouvrir un fichier dans ce dossier", + "palette.browse_root": "racine", + "palette.close_current": "Fermer fichier courant", + "palette.close_current_desc": "Fermer l'onglet actif sans supprimer", + "palette.cmd_back_home": "🏠 Accueil", + "palette.cmd_back_home_desc": "Retour à l'accueil", + "palette.cmd_config": "⚙️ Configuration", + "palette.cmd_config_desc": "Ouvrir les paramètres", + "palette.cmd_create_dir": "📁 Créer répertoire", + "palette.cmd_create_dir_desc": "Choisir la vault, naviguer, puis nommer le dossier", + "palette.cmd_create_file": "📄 Créer fichier markdown", + "palette.cmd_create_file_desc": "Choisir la vault, naviguer, puis nommer le fichier", + "palette.cmd_delete_dir": "🗑️ Supprimer répertoire", + "palette.cmd_delete_dir_desc": "Choisir la vault, naviguer et supprimer", + "palette.cmd_theme": "🌓 Changer le thème", + "palette.cmd_theme_desc": "Basculer clair/sombre", + "palette.del_all_content": "Tout le contenu sera supprimé", + "palette.delete_current": "Supprimer fichier courant", + "palette.delete_current_desc": "Supprimer le fichier de l'onglet actif", + "palette.delete_file_desc": "Rechercher un fichier à supprimer", + "palette.dir_delete_confirm": "Tout le contenu sera supprimé", + "palette.dir_name": "Nom du dossier", + "palette.empty_dir": "📂 Dossier vide", + "palette.error_loading": "Erreur de chargement", + "palette.error_prefix": "Erreur : {msg}", + "palette.file_name": "Nom du fichier", + "palette.help_desc": "Ouvrir le guide", + "palette.md_hint": "(.md ajouté si besoin)", "palette.mode_commands": "Commandes", "palette.mode_files": "Fichiers", + "palette.no_results": "Aucun résultat pour \"{query}\"", + "palette.no_vaults": "Aucune vault trouvée", + "palette.popout_desc": "Ouvrir le document dans une fenêtre", + "palette.reindex_desc": "Forcer la réindexation complète", "palette.search_commands": "Tapez une commande...", + "palette.search_error": "Erreur de recherche", "palette.search_files": "Rechercher un fichier...", + "palette.type_command": "Tapez une commande...", + "palette.type_search": "Rechercher un fichier... (+inclure -exclure)", + "palette.vault_count": "{count} fichiers", "pwa.dismiss": "Plus tard", "pwa.install_button": "Installer", "pwa.install_desc": "Installez cette application sur votre appareil pour un accès rapide.", @@ -1258,7 +1331,7 @@ "sidebar.filter_results_grouped": "Résultats groupés", "sidebar.loading": "Chargement...", "sidebar.no_recent_files": "Aucun fichier récent", - "sidebar.no_results": "Aucun résultat", + "sidebar.no_results": "Aucun repertoire ou fichier correspondant.", "sidebar.no_saved_searches": "Aucune recherche sauvegardée", "sidebar.open_file": "Ouvrir un fichier", "sidebar.saved_dirs_tab": "Répertoires", @@ -1287,17 +1360,25 @@ "sync.index_started": "Indexation démarrée ({count} vaults)", "sync.install_success": "ObsiGate installé avec succès !", "sync.model_not_loaded": "Modèle non chargé", + "sync.no_provider": "Aucun fournisseur", "sync.no_recent_events": "Aucun événement récent", "sync.not_configured": "Non configuré", + "sync.processing": "Traitement en cours…", "sync.update": "Mettre à jour", "sync.updating": "Mise à jour...", "sync.vault_added": "Vault \"{vault}\" ajouté ({count} fichiers)", + "sync.vault_added_label": "Vault ajouté", + "sync.vault_files": "{vault} ({files} fichiers)", + "sync.vault_indexed_label": "Vault indexé", "sync.vault_removed": "Vault \"{vault}\" supprimé", + "sync.vault_removed_label": "Vault supprimé", "sync.verifying": "Vérification…", "theme.change": "Changer le thème", "theme.dark": "Sombre", "theme.light": "Clair", "theme.title": "Thème", + "themes.name_gradients": "Degrades subtils", + "themes.name_terminal": "Noir pur & vert", "toast.ai_keys_saved": "Clés API sauvegardées", "toast.ai_no_key": "Aucune clé à sauvegarder", "toast.bookmark_added": "Ajouté aux favoris", @@ -1351,22 +1432,42 @@ "tree.new_file": "Nouveau fichier", "tree.new_folder": "Nouveau dossier", "tree.vault_home": "Accueil du vault", + "ui.backups_versions": "Backups / Versions", "ui.cannot_rename": "Impossible de renommer cet élément", + "ui.delete_name_empty": "Le nom ne peut pas etre vide", "ui.element_not_found": "Élément introuvable dans l'arborescence", + "ui.vault_readonly": "Vault en lecture seule", "vault.add_dynamic": "Ajouter une vault", "vault.reindex": "Réindexer la vault", "vault.remove_dynamic": "Retirer la vault", + "vaulthome.empty": "Aucun fichier dans ce repertoire", + "vaulthome.error_loading": "Erreur de chargement:", + "vaulthome.file_count": "{count} fichier", + "vaulthome.file_count_plural": "{count} fichiers", + "vaulthome.loading": "Chargement des fichiers...", + "vaulthome.size_bytes": "o", + "vaulthome.size_kb": "Ko", + "vaulthome.size_mb": "Mo", "viewer.backlinks": "Backlinks", "viewer.binary_download": "Télécharger le fichier", "viewer.binary_unsupported": "Fichier binaire non supporté", + "viewer.bookmark_add": "Ajouter aux bookmarks", + "viewer.bookmark_remove": "Retirer des bookmarks", + "viewer.bookmark_toggle": "Ajouter/Retirer des bookmarks", "viewer.breadcrumb_home": "Accueil", "viewer.click_to_download": "Cliquez pour le télécharger", "viewer.copied": "Copié !", "viewer.copy": "Copier", "viewer.copy_error": "Erreur lors de la copie", "viewer.download": "Télécharger", + "viewer.download_md": "Télécharger en .md", + "viewer.download_pdf": "Télécharger en PDF", "viewer.edit": "Éditer", "viewer.error": "Erreur de chargement", + "viewer.forge_brand": "Forge", + "viewer.forge_title": "Forge (nouvel éditeur)", + "viewer.index_start": "Démarrage index.", + "viewer.index_updated": "Mise à jour", "viewer.loaded_from_cache": "Fichier chargé depuis le cache hors-ligne", "viewer.loading": "Chargement du fichier...", "viewer.markdown": "Markdown", @@ -1385,6 +1486,7 @@ "viewer.share": "Partager", "viewer.shared_doc_title": "Document partagé — cliquer pour gérer", "viewer.source": "Source", + "viewer.syncing": "Mise à jour...", "viewer.toc": "Table des matières", "viewer.unsupported_file": "Ce fichier est binaire et ne peut pas être affiché", "webhook.trigger_create": "Création via API ou palette", diff --git a/frontend/sw.js b/frontend/sw.js index d28b879..073b93d 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,6 +1,6 @@ /* ObsiGate Service Worker - PWA v2 with IndexedDB offline support */ -const CACHE_VERSION = 'obsigate-v5-pwa'; +const CACHE_VERSION = 'obsigate-v6-i18n'; const STATIC_CACHE = CACHE_VERSION + '-static'; const DYNAMIC_CACHE = CACHE_VERSION + '-dynamic'; const MAX_DYNAMIC_CACHE_SIZE = 100;