diff --git a/README.fr.md b/README.fr.md new file mode 100644 index 0000000..dfa8370 --- /dev/null +++ b/README.fr.md @@ -0,0 +1,99 @@ +# ObsiGate — Gestionnaire de connaissances personnel + +**ObsiGate** est une application web moderne de gestion de notes Markdown, conçue pour +remplacer des outils comme Obsidian avec une expérience auto-hébergée complète. + +## ✨ Fonctionnalités principales + +- **📝 Éditeur Markdown** avancé avec aperçu en temps réel +- **🤖 IA intégrée** : DeepSeek, OpenRouter, Gemini — complétion, réécriture, traduction +- **🔍 Recherche full-text** avec TF-IDF et filtres avancés +- **📊 Dashboard** avec statistiques, fichiers récents, et contenu partagé +- **🏷️ Tags** : organisation et filtrage par tags +- **🗺️ Vue Graphe** : visualisation des liens entre fichiers +- **🔄 Synchronisation** en temps réel via SSE (Server-Sent Events) +- **🔗 Webhooks** : notifications automatiques vers des services externes +- **💾 Sauvegardes** automatiques et restauration +- **🎨 15 thèmes** avec modes clair et sombre +- **🌐 Internationalisation** : anglais et français (sélecteur de langue intégré) +- **📱 PWA** : installation en tant qu'application native +- **🔒 Authentification** JWT avec tokens refresh + +## 🚀 Installation rapide + +### Docker (recommandé) +```bash +git clone https://git.dracodev.net/Projets/ObsiGate.git +cd ObsiGate +cp .env.example .env +# Configurer DEEPSEEK_API_KEY dans .env +docker compose up -d --build +``` + +### Accès +- **Interface web** : http://localhost:2020 +- **Utilisateur par défaut** : admin / admin (à changer immédiatement) + +## 📋 Prérequis + +- Docker & Docker Compose +- Clé API DeepSeek, OpenRouter, ou Gemini (optionnelle, pour l'IA) +- Python 3.11+ (pour le développement) + +## 🛠️ Développement + +```bash +pip install -r requirements.txt +python backend/main.py +# Dans un autre terminal : +cd frontend && python -m http.server 8080 +``` + +## 📁 Structure du projet + +``` +ObsiGate/ + backend/ # API FastAPI (Python) + frontend/ # Interface utilisateur (HTML/CSS/JS vanilla) + locales/ # Traductions (en.json, fr.json) + docker/ # Configuration Docker + docs/ # Documentation et roadmap +``` + +## 🌍 Internationalisation + +ObsiGate supporte l'anglais et le français. Le sélecteur de langue se trouve dans +**Paramètres > Langue**. La traduction couvre : + +- ✅ Interface utilisateur complète +- ✅ Guide d'utilisation (18 sections) +- ✅ Messages système et toasts +- ✅ Descriptions des thèmes et commandes + +## 📖 Documentation + +- [README.md](README.md) — Documentation complète en anglais +- [docs/ROADMAP.md](docs/ROADMAP.md) — Feuille de route du projet +- [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) — Guide de contribution +- [docs/TROUBLESHOOTING.md](TROUBLESHOOTING.md) — Dépannage + +## 🔧 Configuration + +Variables d'environnement principales (`.env`) : + +| Variable | Description | +|----------|-------------| +| `DEEPSEEK_API_KEY` | Clé API pour l'IA DeepSeek | +| `OPENROUTER_API_KEY` | Clé API pour OpenRouter | +| `GEMINI_API_KEY` | Clé API pour Gemini | +| `ADMIN_PASSWORD` | Mot de passe administrateur | +| `JWT_SECRET` | Secret pour les tokens JWT | +| `WEBHOOK_SECRET` | Secret pour la signature des webhooks | + +## 📝 Licence + +Ce projet est sous licence propriétaire. Voir le fichier [LICENSE](LICENSE) pour plus de détails. + +--- + +**ObsiGate** — Votre savoir, votre serveur. 🏰 diff --git a/frontend/js/ai.js b/frontend/js/ai.js index 928ab58..2fe9d62 100644 --- a/frontend/js/ai.js +++ b/frontend/js/ai.js @@ -9,6 +9,7 @@ - AI Toolbox (to list, to table, frontmatter, to canvas) */ import { api } from './auth.js'; +import { t } from './i18n.js'; // ── API call helper ── async function aiAction(endpoint, text, extra = {}) { @@ -270,7 +271,7 @@ export async function createAIToolbar(container, getEditorView) { replaceSelection(v, result, 'append'); showToast(t('ai.completion_added'), 'success'); } catch (e) { - showToast('AI: ' + (String(e.message || e).includes('401') ? 'clé API invalide' : e.message), 'error'); + showToast('AI: ' + (String(e.message || e).includes('401') ? t('ai.error_invalid_key') : e.message), 'error'); } finally { aiBtn.innerHTML = origHTML; aiBtn.disabled = false; @@ -278,13 +279,13 @@ export async function createAIToolbar(container, getEditorView) { }); // ── Edit menu ── - const editBtn = createDropdownBtn('Éditer', [ - { label: '🪄 Improve writing', action: () => action('improve'), hint: 'Améliore la qualité du texte' }, + const editBtn = createDropdownBtn(t('ai.edit'), [ + { label: '🪄 Improve writing', action: () => action('improve'), hint: t('ai.hint_improve') }, { label: '🔤 Fix spelling & grammar', action: () => action('fix-spelling'), hint: 'Corrige les fautes' }, { label: '📏 Make shorter', action: () => action('make-shorter'), hint: 'Rend le texte plus concis' }, - { label: '📐 Make longer', action: () => action('make-longer'), hint: 'Ajoute des détails' }, + { label: '📐 Make longer', action: () => action('make-longer'), hint: t('ai.hint_add_details') }, { label: '📋 Simplify language', action: () => action('simplify'), hint: 'Simplifie le langage' }, - ], 'Modifie le texte sélectionné'); + ], t('ai.hint_edit_selection')); // ── Tone menu ── const toneBtn = createDropdownBtn('Ton', [ @@ -300,14 +301,14 @@ export async function createAIToolbar(container, getEditorView) { { label: '🇩🇪 German', action: () => action('translate', { target_lang: 'German' }) }, { label: '🇫🇷 French', action: () => action('translate', { target_lang: 'French' }) }, { label: '🇪🇸 Spanish', action: () => action('translate', { target_lang: 'Spanish' }) }, - ], 'Traduit le texte sélectionné'); + ], t('ai.hint_translate')); // ── Generate menu ── - const genBtn = createDropdownBtn('Générer', [ + const genBtn = createDropdownBtn(t('ai.generate'), [ { label: 'ℹ️ Explain this', action: () => action('explain', {}, 'append') }, { label: '📝 Summarize', action: () => action('summarize', {}, 'append') }, { label: '✏️ Continue writing', action: () => action('continue', {}, 'append') }, - ], 'Génère du contenu à partir de la sélection'); + ], t('ai.hint_generate')); // ── Custom Rewrite ── const rewriteBtn = document.createElement('button'); @@ -327,19 +328,19 @@ export async function createAIToolbar(container, getEditorView) { const v = ev(); if (!v) return; const text = getSelection(v); - const instruction = prompt('Instruction de réécriture :', ''); + const instruction = prompt(t('ai.rewrite_prompt'), ''); if (!instruction) return; try { const result = await aiAction('rewrite', text, { instruction }); replaceSelection(v, result); showToast(t('ai.text_rewritten'), 'success'); } catch (e) { - showToast('AI: ' + (String(e.message || e).includes('401') ? 'clé API invalide' : e.message), 'error'); + showToast('AI: ' + (String(e.message || e).includes('401') ? t('ai.error_invalid_key') : e.message), 'error'); } }); // ── Toolbox menu ── - const toolboxBtn = createDropdownBtn('🧰 Boîte', [ + const toolboxBtn = createDropdownBtn(t('ai.toolbox'), [ { label: '📋 Convert to list', action: () => action('to-list') }, { label: '📊 Convert to table', action: () => action('to-table') }, { label: '⚙️ Generate frontmatter', action: () => action('frontmatter', {}, 'before') }, diff --git a/frontend/js/graph.js b/frontend/js/graph.js index 5f3c362..780279b 100644 --- a/frontend/js/graph.js +++ b/frontend/js/graph.js @@ -1,6 +1,7 @@ /* ObsiGate — Graph View: interactive file/folder relationship visualization. Phase 2: theme colors, tooltips, depth slider, full-vault, search, backlinks. */ import { api } from './auth.js'; +import { t } from './i18n.js'; import { safeCreateIcons } from './utils.js'; import { openFile } from './viewer.js'; @@ -624,8 +625,8 @@ export const GraphViewManager = { if (fm.auteur) rows.push(`
✍️ Auteur ${fm.auteur}
`); if (fm.date || fm.creation_date) rows.push(`
📅 Date ${fm.date || fm.creation_date}
`); if (fm.statut) rows.push(`
📌 Statut ${fm.statut}
`); - if (fm.catégorie || fm.categorie) rows.push(`
📂 Catégorie ${fm.catégorie || fm.categorie}
`); - if (fm.publish) rows.push('
✅ Publié
'); + 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 (rows.length > 0) metaRows = rows.join(''); } @@ -649,7 +650,7 @@ export const GraphViewManager = { ${metaRows ? `
${metaRows}
` : ''} ${tags.length > 0 ? `
${tags.map(t => `#${t}`).join('')}
` : ''} - ${node.type === 'file' ? '
Ctrl+survol = aperçu complet
' : ''} + ${node.type === 'file' ? '
${t('graph.ctrl_hover_preview')}
' : ''} `; this._infoPanel.style.display = 'block'; }, @@ -671,7 +672,7 @@ export const GraphViewManager = { if (fm.auteur) items.push(`✍️ ${fm.auteur}`); if (fm.date || fm.creation_date) items.push(`📅 ${fm.date || fm.creation_date}`); if (fm.statut) items.push(`📌 ${fm.statut}`); - if (fm.publish) items.push('✅ publié'); + if (fm.publish) items.push('${t('graph.published_badge')}'); if (fm.favoris) items.push('⭐ favoris'); if (items.length > 0) fmSummary = `
${items.join(' · ')}
`; } @@ -764,7 +765,7 @@ export const GraphViewManager = { if (hit) { this._canvas.style.cursor = 'pointer'; this._canvas.title = hit.node.type === 'file' - ? `📄 ${hit.node.name} (cliquer pour ouvrir${e.ctrlKey ? ', Ctrl+survol = aperçu' : ''})` + ? `📄 ${hit.node.name} (${t('graph.click_to_open')}${e.ctrlKey ? ', ${t('graph.ctrl_hover_preview_short')}' : ''})` : `📁 ${hit.node.name} (cliquer pour explorer)`; // Ctrl+hover: fetch content preview for file nodes diff --git a/frontend/js/sync.js b/frontend/js/sync.js index d745e2f..72ab0cf 100644 --- a/frontend/js/sync.js +++ b/frontend/js/sync.js @@ -1,5 +1,6 @@ /* ObsiGate — Sync: SSE client + PWA registration + Offline mode */ import { state } from './state.js'; +import { t } from './i18n.js'; import { showToast } from './ui.js'; import { api } from './auth.js'; import { loadVaults, loadTags, refreshTagsForContext, refreshSidebarTreePreservingState } from './sidebar.js'; @@ -59,7 +60,7 @@ export const IndexUpdateManager = (() => { try { const data = JSON.parse(e.data); _addEvent("vault_added", data); - showToast(`Vault "${data.vault}" ajouté (${data.stats.file_count} fichiers)`, "info"); + showToast(t('sync.vault_added', { vault: data.vault, count: data.stats.file_count }), "info"); loadVaults(); loadTags(); } catch (err) { @@ -71,7 +72,7 @@ export const IndexUpdateManager = (() => { try { const data = JSON.parse(e.data); _addEvent("vault_removed", data); - showToast(`Vault "${data.vault}" supprimé`, "info"); + showToast(t('sync.vault_removed', { vault: data.vault }), "info"); loadVaults(); loadTags(); } catch (err) { @@ -85,7 +86,7 @@ export const IndexUpdateManager = (() => { _addEvent("index_start", data); connectionState = "syncing"; _updateBadge(); - showToast(`Indexation démarrée (${data.total_vaults} vaults)`, "info"); + showToast(t('sync.index_started', { count: data.total_vaults }), "info"); } catch (err) { console.error("SSE parse error:", err); } @@ -110,7 +111,7 @@ export const IndexUpdateManager = (() => { _addEvent("index_complete", data); connectionState = "connected"; _updateBadge(); - showToast(`Indexation terminée (${data.total_files} fichiers)`, "success"); + showToast(t('sync.index_complete', { count: data.total_files }), "success"); loadVaults(); loadTags(); } catch (err) { @@ -204,10 +205,10 @@ export const IndexUpdateManager = (() => { if (!badge) return; badge.className = "sync-badge sync-badge--" + connectionState; const labels = { - disconnected: "Déconnecté", + disconnected: t('sync.disconnected'), connecting: "Connexion...", - connected: "Synchronisé", - syncing: "Mise à jour...", + connected: t('sync.connected'), + syncing: t('sync.updating'), }; badge.title = labels[connectionState] || connectionState; } @@ -289,9 +290,9 @@ function _renderSyncPanel(panel) { const events = IndexUpdateManager.getRecentEvents(); const stateLabels = { - disconnected: "Déconnecté", + disconnected: t('sync.disconnected'), connecting: "Connexion...", - connected: "Connecté", + connected: t('sync.connected_short'), syncing: "Synchronisation...", }; @@ -301,18 +302,18 @@ function _renderSyncPanel(panel) { `; if (events.length === 0) { - html += `
Aucun événement récent
`; + html += `
${t('sync.no_recent_events')}
`; } else { html += `
`; events.slice(0, 10).forEach((ev) => { const time = new Date(ev.timestamp).toLocaleTimeString(); const typeLabels = { - index_updated: "Mise à jour", + index_updated: t('sync.event_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.event_vault_added'), + vault_removed: t('sync.event_vault_removed'), + index_start: t('sync.event_index_start'), + index_progress: t('sync.event_index_progress'), index_complete: "Indexation tech.", }; const label = typeLabels[ev.type] || ev.type; @@ -378,7 +379,7 @@ function _renderAIStatusSection(panel) { extClass = 'sync-panel__ai--inactive'; } else { extIcon = 'help-circle'; - extText = 'Vérification…'; + extText = t('sync.verifying'); extClass = 'sync-panel__ai--unknown'; } @@ -386,11 +387,11 @@ function _renderAIStatusSection(panel) { let acIcon, acText, acClass; if (_aiAutocomplete && _aiAutocomplete.available) { acIcon = 'check-circle'; - acText = 'Auto-complétion active'; + acText = t('sync.autocomplete_active'); acClass = 'sync-panel__ai--active'; } else if (_aiAutocomplete && _aiAutocomplete.server_ok && !_aiAutocomplete.model_loaded) { acIcon = 'alert-triangle'; - acText = 'Modèle non chargé'; + acText = t('sync.model_not_loaded'); acClass = 'sync-panel__ai--inactive'; } else if (_aiAutocomplete && _aiAutocomplete.error) { acIcon = 'x-circle'; @@ -398,11 +399,11 @@ function _renderAIStatusSection(panel) { acClass = 'sync-panel__ai--inactive'; } else if (_aiAutocomplete === null) { acIcon = 'help-circle'; - acText = 'Vérification…'; + acText = t('sync.verifying'); acClass = 'sync-panel__ai--unknown'; } else { acIcon = 'x-circle'; - acText = 'Non configuré'; + acText = t('sync.not_configured'); acClass = 'sync-panel__ai--inactive'; } @@ -416,7 +417,7 @@ function _renderAIStatusSection(panel) { '' + '
' + '
' + - 'Auto-complétion' + + '' + t('sync.autocomplete') + '' + '' + '' + acText + diff --git a/frontend/locales/en.json b/frontend/locales/en.json index c97e593..ef852c6 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -672,7 +672,6 @@ "auto.caac40b9": "Choose a visual theme. Each theme offers", "auto.a3319169": "📦 About", "auto.6c166d35": "ObsiGate is your web gateway to your Obsidian vaults. Access, browse, and search all your notes from any device via a modern and responsive interface.", - "auto.ca0aceee": "Access", "auto.209c5490": "The top bar contains the essential elements:", "auto.4c908ade": "Click a vault to open it and see its", "auto.1abf6661": "The path displayed above the content is", @@ -1172,5 +1171,41 @@ "help.pal_open___select": "Open / select", "help.pal_close_the_palette": "Close the palette", "help.pal_popout_desc": "Open the document in a detached window", - "help.pal_move_desc": "Navigate the list" + "help.pal_move_desc": "Navigate the list", + "ai.error_invalid_key": "Invalid API key", + "ai.edit": "Edit", + "ai.hint_improve": "Improves text quality", + "ai.hint_add_details": "Adds details", + "ai.hint_edit_selection": "Modifies the selected text", + "ai.hint_translate": "Translates the selected text", + "ai.generate": "Generate", + "ai.hint_generate": "Generates content from the selection", + "ai.toolbox": "🧰 Toolbox", + "ai.rewrite_prompt": "Rewrite instruction:", + "sync.vault_added": "Vault \"{vault}\" added ({count} files)", + "sync.vault_removed": "Vault \"{vault}\" removed", + "sync.index_started": "Indexing started ({count} vaults)", + "sync.index_complete": "Indexing complete ({count} files)", + "sync.disconnected": "Disconnected", + "sync.connected": "Synchronized", + "sync.updating": "Updating...", + "sync.connected_short": "Connected", + "sync.no_recent_events": "No recent events", + "sync.event_index_updated": "Update", + "sync.event_vault_added": "Vault added", + "sync.event_vault_removed": "Vault removed", + "sync.event_index_start": "Index starting", + "sync.event_index_progress": "Vault indexed", + "sync.verifying": "Verifying…", + "sync.autocomplete_active": "Autocomplete active", + "sync.model_not_loaded": "Model not loaded", + "sync.not_configured": "Not configured", + "sync.autocomplete": "Autocomplete", + "sync.update": "Update", + "graph.category": "📂 Category", + "graph.published": "Published", + "graph.ctrl_hover_preview": "Ctrl+hover = full preview", + "graph.published_badge": "✅ published", + "graph.click_to_open": "click to open", + "graph.ctrl_hover_preview_short": "Ctrl+hover = preview" } diff --git a/frontend/locales/fr.json b/frontend/locales/fr.json index d804e37..03cccd1 100644 --- a/frontend/locales/fr.json +++ b/frontend/locales/fr.json @@ -672,7 +672,6 @@ "auto.caac40b9": "Choisissez un thème visuel. Chaque thème\n offre un mode sombre et clair.", "auto.a3319169": "📦 À propos", "auto.6c166d35": "ObsiGate est votre porte d'entrée web vers\n vos vaults Obsidian. Accédez, naviguez et\n recherchez dans toutes vos notes depuis\n n'importe quel appareil via une interface\n moderne et responsive.", - "auto.ca0aceee": "Accès", "auto.209c5490": "La barre supérieure contient les éléments\n essentiels :", "auto.4c908ade": "Cliquez sur un vault pour l'ouvrir et voir son\n contenu. Les dossiers affichent un chevron et un\n badge avec le nombre d'éléments.", "auto.1abf6661": "Le chemin affiché au-dessus du contenu est\n interactif :", @@ -1172,5 +1171,41 @@ "help.pal_open___select": "Ouvrir/sélectionner", "help.pal_close_the_palette": "Fermer la palette", "help.pal_popout_desc": "Ouvre le document dans une fenêtre détachée", - "help.pal_move_desc": "Se déplacer dans la liste" + "help.pal_move_desc": "Se déplacer dans la liste", + "ai.error_invalid_key": "clé API invalide", + "ai.edit": "Éditer", + "ai.hint_improve": "Améliore la qualité du texte", + "ai.hint_add_details": "Ajoute des détails", + "ai.hint_edit_selection": "Modifie le texte sélectionné", + "ai.hint_translate": "Traduit le texte sélectionné", + "ai.generate": "Générer", + "ai.hint_generate": "Génère du contenu à partir de la sélection", + "ai.toolbox": "🧰 Boîte", + "ai.rewrite_prompt": "Instruction de réécriture :", + "sync.vault_added": "Vault \"{vault}\" ajouté ({count} fichiers)", + "sync.vault_removed": "Vault \"{vault}\" supprimé", + "sync.index_started": "Indexation démarrée ({count} vaults)", + "sync.index_complete": "Indexation terminée ({count} fichiers)", + "sync.disconnected": "Déconnecté", + "sync.connected": "Synchronisé", + "sync.updating": "Mise à jour...", + "sync.connected_short": "Connecté", + "sync.no_recent_events": "Aucun événement récent", + "sync.event_index_updated": "Mise à jour", + "sync.event_vault_added": "Vault ajouté", + "sync.event_vault_removed": "Vault supprimé", + "sync.event_index_start": "Démarrage index.", + "sync.event_index_progress": "Vault indexé", + "sync.verifying": "Vérification…", + "sync.autocomplete_active": "Auto-complétion active", + "sync.model_not_loaded": "Modèle non chargé", + "sync.not_configured": "Non configuré", + "sync.autocomplete": "Auto-complétion", + "sync.update": "Mettre à jour", + "graph.category": "📂 Catégorie", + "graph.published": "Publié", + "graph.ctrl_hover_preview": "Ctrl+survol = aperçu complet", + "graph.published_badge": "✅ publié", + "graph.click_to_open": "cliquer pour ouvrir", + "graph.ctrl_hover_preview_short": "Ctrl+survol = aperçu" }