diff --git a/frontend/index.html b/frontend/index.html index 7eb22c3..402d2a6 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -184,11 +184,11 @@ đź“– ObsiGate -

Accès sécurisé à vos vaults

+

Accès sécurisé à vos vaults

- +
- +
- Se souvenir de moi (30 jours) + Se souvenir de moi (30 jours)
@@ -290,6 +290,7 @@ @@ -2021,29 +2022,29 @@ class="config-section help-section" id="cfg-profile" > -

👤 Profil utilisateur

-

- Preferences personnelles pour personnaliser - l'experience ObsiGate. +

👤 Profil utilisateur

+

+ Préférences personnelles pour personnaliser + l'expérience ObsiGate.

- + - Utilisee par l'autocompletion et le ghost text. + Utilisée par l'autocomplétion et le ghost text.
- +
- - - + + +
diff --git a/frontend/js/app.js b/frontend/js/app.js index 3e8dfb1..61e2741 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -4,6 +4,7 @@ import * as UI from './ui.js'; import * as Utils from './utils.js'; +import { initI18n } from './i18n.js'; // Wire up AI toolbar toast (avoids circular import in utils.js) window._obsigateShowToast = UI.showToast; @@ -23,6 +24,7 @@ import { initMermaid, setupFocusMode } from './mermaid-viewer.js'; // ========================================================================= async function init() { UI.initTheme(); + await initI18n(); // Must run before any rendering that uses t() UI.initHeaderMenu(); UI.initCustomDropdowns(); diff --git a/frontend/js/auth.js b/frontend/js/auth.js index 5597a70..737a810 100644 --- a/frontend/js/auth.js +++ b/frontend/js/auth.js @@ -2,6 +2,7 @@ import { state } from './state.js'; import { safeCreateIcons } from './utils.js'; import { showToast, closeHeaderMenu } from './ui.js'; +import { t } from './i18n.js'; import { showWelcome } from './viewer.js'; // --------------------------------------------------------------------------- @@ -33,7 +34,7 @@ async function api(path, opts) { res = await fetch(path, mergedOpts); } catch (err) { if (err.name === "AbortError") throw err; // let callers handle abort - showToast("Erreur réseau — vérifiez votre connexion", "error"); + showToast(t("auth.network_error"), "error"); throw err; } if (res.status === 401 && AuthManager._authEnabled) { diff --git a/frontend/js/config.js b/frontend/js/config.js index 5c34f05..c0e1e6a 100644 --- a/frontend/js/config.js +++ b/frontend/js/config.js @@ -5,6 +5,7 @@ import { el, icon, openFile } from './viewer.js'; import { syncVaultSelectors, setSelectedVaultContext, refreshSidebarForContext, loadVaults, loadVaultSettings, loadTags, TagFilterService, refreshSidebarTreePreservingState } from './sidebar.js'; import { escapeHtml, safeCreateIcons } from './utils.js'; import { showToast, closeHeaderMenu, closeMobileSidebar } from './ui.js'; +import { t, setLocale, getLocale } from './i18n.js'; let _recentTimestampTimer = null; let _recentFilesCache = []; @@ -769,7 +770,7 @@ function saveFrontendConfig() { search_timeout_ms: _getFieldNum("cfg-timeout", 30000), }; localStorage.setItem(_FRONTEND_CONFIG_KEY, JSON.stringify(cfg)); - showToast("Paramètres client sauvegardés", "success"); + showToast(t("toast.config_client_saved"), "success"); } async function saveBackendConfig() { @@ -793,7 +794,7 @@ async function saveBackendConfig() { body: JSON.stringify(body), }); if (res.ok) { - showToast("Configuration backend sauvegardée", "success"); + showToast(t("toast.config_saved"), "success"); } else { const errorData = await res.json().catch(() => ({})); showToast(errorData.detail || "Erreur de sauvegarde", "error"); @@ -812,7 +813,7 @@ async function forceReindex() { } try { await api("/api/index/reload"); - showToast("Réindexation terminée", "success"); + showToast(t("toast.reindex_done"), "success"); loadDiagnostics(); await Promise.all([loadVaults(), loadTags()]); } catch (err) { @@ -853,7 +854,7 @@ async function resetConfigDefaults() { console.error("Reset config error:", err); } loadConfigFields(); - showToast("Configuration réinitialisée", "success"); + showToast(t("toast.config_reset"), "success"); } async function loadDiagnostics() { @@ -1549,15 +1550,16 @@ function initProfile() { // Load saved profile try { - var lang = localStorage.getItem('obsigate-lang') || 'fr'; + var lang = getLocale(); var name = localStorage.getItem('obsigate-name') || ''; if (langSel) langSel.value = lang; if (nameInp) nameInp.value = name; } catch(e) {} - saveBtn.addEventListener('click', function() { + saveBtn.addEventListener('click', async function() { try { - localStorage.setItem('obsigate-lang', langSel.value); + var newLang = langSel.value; + await setLocale(newLang); // Live-reload all translations if (nameInp) localStorage.setItem('obsigate-name', nameInp.value.trim()); if (savedEl) { savedEl.style.display = 'inline'; setTimeout(function() { savedEl.style.display = 'none'; }, 2000); } } catch(e) {} @@ -1566,7 +1568,7 @@ function initProfile() { // Logout button if (logoutBtn) { logoutBtn.addEventListener('click', function() { - if (confirm('Voulez-vous vraiment vous deconnecter ?')) { + if (confirm(t('confirm.logout'))) { fetch('/api/auth/logout', { method: 'POST' }) .then(function() { window.location.href = '/'; }) .catch(function() { window.location.href = '/'; }); diff --git a/frontend/js/i18n.js b/frontend/js/i18n.js new file mode 100644 index 0000000..664f253 --- /dev/null +++ b/frontend/js/i18n.js @@ -0,0 +1,146 @@ +/* ObsiGate — i18n (internationalisation). + * + * Architecture: + * Flat key→string JSON locale files in /static/locales/{fr,en,...}.json + * Static DOM: data-i18n="key" → textContent + * data-i18n-placeholder="key" → placeholder + * data-i18n-attr:title="key" → title attribute + * data-i18n-html="key" → innerHTML (use sparingly) + * Dynamic JS: import { t } from './i18n.js'; t('key', {param: 'val'}) + * Live reload: setLocale('en') updates every data-i18n element instantly. + * + * Key naming: section.what — lowercase, dot-separated, descriptive. + * e.g. header.search_placeholder button.save error.network + * + * Usage: + * import { t, setLocale, getLocale, initI18n } from './i18n.js'; + * await initI18n(); // early, before any rendering + * el.textContent = t('button.cancel'); // dynamic string + * setLocale('en'); // user switches language + */ + +// ── State ────────────────────────────────────────────────────────────────── +let _currentLocale = 'fr'; +let _strings = {}; // { key: translated_string } +let _fallbackStrings = {}; // FR fallback, loaded once +let _loaded = false; + +// ── Public helpers ───────────────────────────────────────────────────────── + +/** Return the currently active locale code ('fr', 'en', ...). */ +export function getLocale() { + return _currentLocale; +} + +/** Translate a key. If params provided, replace {name} placeholders. + * Falls back: key → French → key itself. Never throws. */ +export function t(key, params) { + let s = _strings[key]; + if (s === undefined) { + s = _fallbackStrings[key]; + if (s === undefined) { + console.warn('obsigate i18n: missing key', key); + s = key; // last resort — show the key so the dev notices + } + } + if (params) { + for (const [k, v] of Object.entries(params)) { + s = s.replace(new RegExp('\\{' + k + '\\}', 'g'), String(v)); + } + } + return s; +} + +/** Change locale and re-translate the entire DOM. + * Persists to localStorage. */ +export async function setLocale(locale) { + locale = locale || 'fr'; + if (!_loaded) { + // Too early — store intent and apply after init + try { localStorage.setItem('obsigate-lang', locale); } catch (e) { /* */ } + return; + } + await _loadLocaleStrings(locale); + _applyDOM(); + document.documentElement.lang = locale; + try { localStorage.setItem('obsigate-lang', locale); } catch (e) { /* */ } +} + +/** Initialise the i18n system. Call once, early, before any rendering. + * Reads saved preference from localStorage, falls back to navigator.language, + * then to 'fr'. */ +export async function initI18n() { + // 1. Determine locale: saved > navigator.language > 'fr' + let locale = null; + try { locale = localStorage.getItem('obsigate-lang'); } catch (e) { /* */ } + if (!locale) { + const nav = (navigator.language || '').toLowerCase(); + locale = nav.startsWith('fr') ? 'fr' + : nav.startsWith('en') ? 'en' + : 'fr'; + } + + // 2. Load fallback (FR) — always needed + _fallbackStrings = await _fetchLocale('fr'); + + // 3. Load selected locale + await _loadLocaleStrings(locale); + + // 4. Apply to DOM + _applyDOM(); + document.documentElement.lang = locale; + _loaded = true; +} + +// ── Internal ─────────────────────────────────────────────────────────────── + +async function _fetchLocale(locale) { + try { + const resp = await fetch('/static/locales/' + locale + '.json'); + if (!resp.ok) throw new Error('HTTP ' + resp.status); + return await resp.json(); + } catch (err) { + console.warn('obsigate i18n: failed to load locale ' + locale, err); + return {}; + } +} + +async function _loadLocaleStrings(locale) { + _currentLocale = locale; + if (locale === 'fr') { + _strings = _fallbackStrings; + } else { + _strings = await _fetchLocale(locale); + } +} + +/** Walk the DOM and update every element with a data-i18n* attribute. */ +function _applyDOM() { + // textContent + document.querySelectorAll('[data-i18n]').forEach(function (el) { + const key = el.getAttribute('data-i18n'); + el.textContent = t(key); + }); + + // placeholder + document.querySelectorAll('[data-i18n-placeholder]').forEach(function (el) { + const key = el.getAttribute('data-i18n-placeholder'); + el.setAttribute('placeholder', t(key)); + }); + + // innerHTML (use sparingly — XSS risk if keys ever hold user content) + document.querySelectorAll('[data-i18n-html]').forEach(function (el) { + const key = el.getAttribute('data-i18n-html'); + el.innerHTML = t(key); + }); + + // data-i18n-attr:TITLE → sets any attribute + document.querySelectorAll('[data-i18n-attr]').forEach(function (el) { + const raw = el.getAttribute('data-i18n-attr'); + const colon = raw.indexOf(':'); + if (colon === -1) return; + const attr = raw.substring(0, colon); + const key = raw.substring(colon + 1); + el.setAttribute(attr, t(key)); + }); +} diff --git a/frontend/locales/en.json b/frontend/locales/en.json new file mode 100644 index 0000000..6329d05 --- /dev/null +++ b/frontend/locales/en.json @@ -0,0 +1,271 @@ +{ + "_meta": { "locale": "en", "name": "English" }, + + "header.logo": "ObsiGate", + "header.search_placeholder": "Search all vaults...", + "header.config": "Settings", + "header.help": "Help", + "header.profile": "Profile", + "header.logout": "Sign out", + "header.refresh": "Refresh", + "header.install_app": "Install app", + "header.admin_panel": "Administration", + + "sidebar.filter_placeholder": "Filter files...", + "sidebar.tab_vaults": "Vaults", + "sidebar.tab_tags": "Tags", + "sidebar.tab_recent": "Recent", + "sidebar.tab_saved": "Searches", + "sidebar.loading": "Loading...", + "sidebar.no_results": "No results", + "sidebar.vault_context_all": "All vaults", + + "search.operators_help": "Operators: tag:word, vault:name, title:word, path:word, ext:md", + "search.results_count": "{count} result(s)", + "search.no_results": "No results found", + "search.loading": "Searching...", + "search.error": "Search error", + "search.save_search": "Save search", + "search.sort_relevance": "Relevance", + "search.sort_date": "Date", + "search.sort_title": "Title", + "search.facet_tags": "Tags", + "search.facet_vaults": "Vaults", + "search.filters": "Filters", + "search.case_sensitive": "Case sensitive", + "search.whole_word": "Whole word", + "search.regex": "Regular expression", + + "viewer.source": "Source", + "viewer.markdown": "Markdown", + "viewer.edit": "Edit", + "viewer.raw": "Raw", + "viewer.copy": "Copy", + "viewer.popout": "Open in popup", + "viewer.download": "Download", + "viewer.share": "Share", + "viewer.backlinks": "Backlinks", + "viewer.toc": "Table of contents", + "viewer.outline": "Outline", + "viewer.loading": "Loading file...", + "viewer.error": "Loading error", + "viewer.binary_unsupported": "Unsupported binary file", + "viewer.binary_download": "Download file", + "viewer.pdf_pages": "{count} pages", + + "editor.save": "Save", + "editor.cancel": "Cancel", + "editor.saved": "Saved ✓", + "editor.unsaved": "Unsaved changes", + "editor.find": "Find in file...", + "editor.replace": "Replace", + "editor.find_regex": "Regex", + "editor.find_case": "Case sensitive", + "editor.find_whole": "Whole word", + "editor.no_results": "No results", + "editor.close": "Close", + + "ai.toolbar_edit": "Edit", + "ai.toolbar_tone": "Tone", + "ai.toolbar_translate": "Translate", + "ai.toolbar_generate": "Generate", + "ai.toolbar_rewrite": "Rewrite", + "ai.toolbar_toolbox": "Toolbox", + "ai.improve": "Improve", + "ai.fix_spelling": "Fix spelling", + "ai.shorter": "Shorter", + "ai.longer": "Longer", + "ai.simplify": "Simplify", + "ai.professional": "Professional tone", + "ai.casual": "Casual tone", + "ai.translate_to": "Translate to {lang}", + "ai.explain": "Explain", + "ai.summarize": "Summarize", + "ai.continue": "Continue", + "ai.to_list": "Convert to list", + "ai.to_table": "Convert to table", + "ai.frontmatter": "Generate frontmatter", + "ai.to_canvas": "Convert to Canvas", + "ai.custom_rewrite": "Custom rewrite", + "ai.no_api_key": "No API key configured", + "ai.select_text": "Select text to process", + "ai.done": "AI: text processed", + + "dashboard.welcome": "Welcome to ObsiGate", + "dashboard.stats": "Statistics", + "dashboard.bookmarks": "Bookmarks", + "dashboard.conflicts": "Conflicts", + "dashboard.recent": "Recent", + "dashboard.total_files": "Total files", + "dashboard.total_tags": "Unique tags", + "dashboard.total_size": "Total size", + "dashboard.vaults_count": "Vaults", + "dashboard.no_bookmarks": "No bookmarks", + "dashboard.no_conflicts": "No conflicts", + "dashboard.no_recent": "No recent files", + + "config.title": "Settings", + "config.profile": "User profile", + "config.profile_desc": "Personal preferences to customize your ObsiGate experience.", + "config.lang_label": "Preferred language", + "config.lang_hint": "Used by autocomplete and ghost text.", + "config.display_name": "Display name", + "config.save": "Save", + "config.logout": "Sign out", + "config.saved": "✓ Saved", + "config.about": "About", + "config.about_loading": "Loading...", + "config.about_error": "Loading error", + "config.ai_keys": "AI API keys", + "config.ai_keys_desc": "Configure API keys for the AI editor.", + "config.hidden_files": "Hidden files", + "config.hidden_files_desc": "Manage display of hidden files and folders.", + "config.webhooks": "Webhooks", + "config.webhooks_desc": "HTTP notifications to external services on file changes.", + "config.shares": "Public shares", + "config.shares_desc": "Manage public share links for your documents.", + "config.backups": "Backups", + "config.backups_desc": "Manage automatic file backups.", + "config.diagnostics": "Diagnostics", + "config.diagnostics_desc": "Technical information about the server state.", + "config.reindex": "Reindex", + "config.reindex_desc": "Rebuild the search index for all vaults.", + "config.client_config": "Client configuration", + "config.server_config": "Server configuration", + + "help.title": "ObsiGate Help", + "help.shortcuts": "Keyboard shortcuts", + "help.search_operators": "Search operators", + "help.markdown": "Markdown syntax", + "help.about": "About", + + "auth.login_title": "Secure access to your vaults", + "auth.username": "Username", + "auth.password": "Password", + "auth.login_button": "Sign in", + "auth.remember_me": "Remember me", + "auth.login_error": "Invalid credentials", + "auth.network_error": "Network error — check your connection", + "auth.logged_out": "Signed out", + "auth.session_expired": "Session expired, please sign in again", + + "share.title": "Public share", + "share.create": "Create share link", + "share.copy_link": "Copy link", + "share.revoke": "Revoke", + "share.expires": "Expires on", + "share.never": "Never", + "share.copied": "Link copied!", + + "backup.title": "Backups", + "backup.restore": "Restore", + "backup.delete": "Delete", + "backup.diff": "Compare", + "backup.purge": "Purge", + "backup.no_backups": "No backups", + "backup.preview": "Preview", + "backup.version": "Version from {date}", + + "graph.title": "Graph", + "graph.fullscreen": "Full screen", + "graph.export_png": "Export PNG", + "graph.depth": "Depth", + "graph.filter_tag": "Filter by tag", + "graph.filter_type": "Filter by type", + "graph.focus_mode": "Focus mode", + "graph.back": "Back", + "graph.forward": "Forward", + "graph.parent": "Parent", + "graph.loading": "Building graph...", + + "mermaid.code": "Code", + "mermaid.preview": "Preview", + "mermaid.copy_svg": "Copy SVG", + "mermaid.download_png": "Download PNG", + "mermaid.zoom_in": "Zoom in", + "mermaid.zoom_out": "Zoom out", + + "palette.search_files": "Search files...", + "palette.search_commands": "Type a command...", + + "offline.status": "Offline", + "offline.pending": "{count} pending change(s)", + "offline.sync_now": "Sync now", + "offline.conflict_title": "Sync conflict", + "offline.keep_local": "Keep local version", + "offline.keep_server": "Keep server version", + + "conflict.resolve_local": "Keep local version", + "conflict.resolve_conflict": "Keep conflict version", + "conflict.no_conflicts": "No conflicts detected", + "conflict.resolved": "Conflict resolved", + + "common.loading": "Loading...", + "common.error": "Error", + "common.ok": "OK", + "common.cancel": "Cancel", + "common.close": "Close", + "common.save": "Save", + "common.delete": "Delete", + "common.confirm": "Confirm", + "common.yes": "Yes", + "common.no": "No", + "common.search": "Search", + "common.create": "Create", + "common.edit": "Edit", + "common.rename": "Rename", + "common.move": "Move", + "common.copy": "Copy", + "common.copied": "Copied!", + "common.download": "Download", + "common.refresh": "Refresh", + "common.back": "Back", + "common.next": "Next", + "common.previous": "Previous", + "common.all": "All", + "common.none": "None", + "common.actions": "Actions", + + "toast.saved": "Saved", + "toast.deleted": "Deleted", + "toast.created": "Created", + "toast.renamed": "Renamed", + "toast.moved": "Moved", + "toast.copied": "Copied!", + "toast.error_rename": "Cannot rename this item", + "toast.error_delete": "Delete error", + "toast.error_load": "Loading error", + "toast.error_network": "Network error", + "toast.reindex_started": "Reindexing...", + "toast.reindex_done": "Reindexing complete", + "toast.index_reloaded": "Full index reloaded", + "toast.installed": "ObsiGate installed successfully!", + "toast.not_found_tree": "Item not found in tree", + "toast.config_saved": "Configuration saved", + "toast.config_client_saved": "Client settings saved", + "toast.config_reset": "Configuration reset", + + "confirm.logout": "Are you sure you want to sign out?", + "confirm.delete_file": "Permanently delete this file?", + "confirm.delete_dir": "Delete this folder and all its contents?", + + "pwa.install_title": "Install ObsiGate", + "pwa.install_desc": "Install this app on your device for quick access.", + "pwa.install_button": "Install", + "pwa.dismiss": "Later", + + "file.new_file": "New file", + "file.new_folder": "New folder", + "file.rename": "Rename", + "file.delete": "Delete", + "file.copy_path": "Copy path", + "file.properties": "Properties", + "file.graph_view": "Graph view", + + "vault.add_dynamic": "Add vault", + "vault.remove_dynamic": "Remove vault", + "vault.reindex": "Reindex vault", + + "theme.light": "Light", + "theme.dark": "Dark" +} diff --git a/frontend/locales/fr.json b/frontend/locales/fr.json new file mode 100644 index 0000000..2a9663f --- /dev/null +++ b/frontend/locales/fr.json @@ -0,0 +1,271 @@ +{ + "_meta": { "locale": "fr", "name": "Français" }, + + "header.logo": "ObsiGate", + "header.search_placeholder": "Rechercher dans toutes les vaults...", + "header.config": "Configuration", + "header.help": "Aide", + "header.profile": "Profil", + "header.logout": "Déconnexion", + "header.refresh": "Rafraîchir", + "header.install_app": "Installer l'application", + "header.admin_panel": "Administration", + + "sidebar.filter_placeholder": "Filtrer fichiers...", + "sidebar.tab_vaults": "Vaults", + "sidebar.tab_tags": "Tags", + "sidebar.tab_recent": "Récents", + "sidebar.tab_saved": "Recherches", + "sidebar.loading": "Chargement...", + "sidebar.no_results": "Aucun résultat", + "sidebar.vault_context_all": "Toutes les vaults", + + "search.operators_help": "Opérateurs : tag:mot, vault:nom, title:mot, path:mot, ext:md", + "search.results_count": "{count} résultat(s)", + "search.no_results": "Aucun résultat trouvé", + "search.loading": "Recherche en cours...", + "search.error": "Erreur de recherche", + "search.save_search": "Sauvegarder la recherche", + "search.sort_relevance": "Pertinence", + "search.sort_date": "Date", + "search.sort_title": "Titre", + "search.facet_tags": "Tags", + "search.facet_vaults": "Vaults", + "search.filters": "Filtres", + "search.case_sensitive": "Respecter la casse", + "search.whole_word": "Mot entier", + "search.regex": "Expression régulière", + + "viewer.source": "Source", + "viewer.markdown": "Markdown", + "viewer.edit": "Éditer", + "viewer.raw": "Brut", + "viewer.copy": "Copier", + "viewer.popout": "Ouvrir dans une fenêtre", + "viewer.download": "Télécharger", + "viewer.share": "Partager", + "viewer.backlinks": "Backlinks", + "viewer.toc": "Table des matières", + "viewer.outline": "Plan", + "viewer.loading": "Chargement du fichier...", + "viewer.error": "Erreur de chargement", + "viewer.binary_unsupported": "Fichier binaire non supporté", + "viewer.binary_download": "Télécharger le fichier", + "viewer.pdf_pages": "{count} pages", + + "editor.save": "Enregistrer", + "editor.cancel": "Annuler", + "editor.saved": "Sauvegardé ✓", + "editor.unsaved": "Modifications non sauvegardées", + "editor.find": "Rechercher dans le fichier...", + "editor.replace": "Remplacer", + "editor.find_regex": "Regex", + "editor.find_case": "Respecter la casse", + "editor.find_whole": "Mot entier", + "editor.no_results": "Aucun résultat", + "editor.close": "Fermer", + + "ai.toolbar_edit": "Éditer", + "ai.toolbar_tone": "Ton", + "ai.toolbar_translate": "Traduire", + "ai.toolbar_generate": "Générer", + "ai.toolbar_rewrite": "Réécrire", + "ai.toolbar_toolbox": "Toolbox", + "ai.improve": "Améliorer", + "ai.fix_spelling": "Corriger l'orthographe", + "ai.shorter": "Raccourcir", + "ai.longer": "Allonger", + "ai.simplify": "Simplifier", + "ai.professional": "Ton professionnel", + "ai.casual": "Ton décontracté", + "ai.translate_to": "Traduire en {lang}", + "ai.explain": "Expliquer", + "ai.summarize": "Résumer", + "ai.continue": "Continuer", + "ai.to_list": "Convertir en liste", + "ai.to_table": "Convertir en tableau", + "ai.frontmatter": "Générer frontmatter", + "ai.to_canvas": "Convertir en Canvas", + "ai.custom_rewrite": "Réécriture personnalisée", + "ai.no_api_key": "Aucune clé API configurée", + "ai.select_text": "Sélectionnez du texte à traiter", + "ai.done": "AI: texte traité", + + "dashboard.welcome": "Bienvenue dans ObsiGate", + "dashboard.stats": "Statistiques", + "dashboard.bookmarks": "Favoris", + "dashboard.conflicts": "Conflits", + "dashboard.recent": "Récents", + "dashboard.total_files": "Fichiers totaux", + "dashboard.total_tags": "Tags uniques", + "dashboard.total_size": "Taille totale", + "dashboard.vaults_count": "Vaults", + "dashboard.no_bookmarks": "Aucun favori", + "dashboard.no_conflicts": "Aucun conflit", + "dashboard.no_recent": "Aucun fichier récent", + + "config.title": "Configuration", + "config.profile": "Profil utilisateur", + "config.profile_desc": "Préférences personnelles pour personnaliser l'expérience ObsiGate.", + "config.lang_label": "Langue préférée", + "config.lang_hint": "Utilisée par l'autocomplétion et le ghost text.", + "config.display_name": "Nom d'affichage", + "config.save": "Enregistrer", + "config.logout": "Déconnexion", + "config.saved": "✓ Sauvegardé", + "config.about": "À propos", + "config.about_loading": "Chargement...", + "config.about_error": "Erreur de chargement", + "config.ai_keys": "Clés API IA", + "config.ai_keys_desc": "Configurez les clés API pour l'éditeur IA.", + "config.hidden_files": "Fichiers cachés", + "config.hidden_files_desc": "Gérez l'affichage des fichiers et dossiers cachés.", + "config.webhooks": "Webhooks", + "config.webhooks_desc": "Notifications HTTP vers des services externes lors des changements de fichiers.", + "config.shares": "Partages publics", + "config.shares_desc": "Gérez les liens de partage public de vos documents.", + "config.backups": "Sauvegardes", + "config.backups_desc": "Gérez les sauvegardes automatiques de vos fichiers.", + "config.diagnostics": "Diagnostics", + "config.diagnostics_desc": "Informations techniques sur l'état du serveur.", + "config.reindex": "Réindexer", + "config.reindex_desc": "Reconstruire l'index de recherche pour toutes les vaults.", + "config.client_config": "Configuration client", + "config.server_config": "Configuration serveur", + + "help.title": "Aide ObsiGate", + "help.shortcuts": "Raccourcis clavier", + "help.search_operators": "Opérateurs de recherche", + "help.markdown": "Syntaxe Markdown", + "help.about": "À propos", + + "auth.login_title": "Connexion", + "auth.username": "Nom d'utilisateur", + "auth.password": "Mot de passe", + "auth.login_button": "Se connecter", + "auth.remember_me": "Se souvenir de moi", + "auth.login_error": "Identifiants invalides", + "auth.network_error": "Erreur réseau — vérifiez votre connexion", + "auth.logged_out": "Déconnecté", + "auth.session_expired": "Session expirée, veuillez vous reconnecter", + + "share.title": "Partage public", + "share.create": "Créer un lien de partage", + "share.copy_link": "Copier le lien", + "share.revoke": "Révoquer", + "share.expires": "Expire le", + "share.never": "Jamais", + "share.copied": "Lien copié !", + + "backup.title": "Sauvegardes", + "backup.restore": "Restaurer", + "backup.delete": "Supprimer", + "backup.diff": "Comparer", + "backup.purge": "Purger", + "backup.no_backups": "Aucune sauvegarde", + "backup.preview": "Aperçu", + "backup.version": "Version du {date}", + + "graph.title": "Graphe", + "graph.fullscreen": "Plein écran", + "graph.export_png": "Exporter PNG", + "graph.depth": "Profondeur", + "graph.filter_tag": "Filtrer par tag", + "graph.filter_type": "Filtrer par type", + "graph.focus_mode": "Mode focus", + "graph.back": "Retour", + "graph.forward": "Avancer", + "graph.parent": "Parent", + "graph.loading": "Construction du graphe...", + + "mermaid.code": "Code", + "mermaid.preview": "Aperçu", + "mermaid.copy_svg": "Copier SVG", + "mermaid.download_png": "Télécharger PNG", + "mermaid.zoom_in": "Zoom avant", + "mermaid.zoom_out": "Zoom arrière", + + "palette.search_files": "Rechercher un fichier...", + "palette.search_commands": "Tapez une commande...", + + "offline.status": "Hors-ligne", + "offline.pending": "{count} modification(s) en attente", + "offline.sync_now": "Synchroniser maintenant", + "offline.conflict_title": "Conflit de synchronisation", + "offline.keep_local": "Garder version locale", + "offline.keep_server": "Garder version serveur", + + "conflict.resolve_local": "Garder version locale", + "conflict.resolve_conflict": "Garder version conflit", + "conflict.no_conflicts": "Aucun conflit détecté", + "conflict.resolved": "Conflit résolu", + + "common.loading": "Chargement...", + "common.error": "Erreur", + "common.ok": "OK", + "common.cancel": "Annuler", + "common.close": "Fermer", + "common.save": "Enregistrer", + "common.delete": "Supprimer", + "common.confirm": "Confirmer", + "common.yes": "Oui", + "common.no": "Non", + "common.search": "Rechercher", + "common.create": "Créer", + "common.edit": "Éditer", + "common.rename": "Renommer", + "common.move": "Déplacer", + "common.copy": "Copier", + "common.copied": "Copié !", + "common.download": "Télécharger", + "common.refresh": "Rafraîchir", + "common.back": "Retour", + "common.next": "Suivant", + "common.previous": "Précédent", + "common.all": "Tous", + "common.none": "Aucun", + "common.actions": "Actions", + + "toast.saved": "Sauvegardé", + "toast.deleted": "Supprimé", + "toast.created": "Créé", + "toast.renamed": "Renommé", + "toast.moved": "Déplacé", + "toast.copied": "Copié !", + "toast.error_rename": "Impossible de renommer cet élément", + "toast.error_delete": "Erreur de suppression", + "toast.error_load": "Erreur de chargement", + "toast.error_network": "Erreur réseau", + "toast.reindex_started": "Réindexation en cours...", + "toast.reindex_done": "Réindexation terminée", + "toast.index_reloaded": "Index complet rechargé", + "toast.installed": "ObsiGate installé avec succès !", + "toast.not_found_tree": "Élément introuvable dans l'arborescence", + "toast.config_saved": "Configuration sauvegardée", + "toast.config_client_saved": "Paramètres client sauvegardés", + "toast.config_reset": "Configuration réinitialisée", + + "confirm.logout": "Voulez-vous vraiment vous déconnecter ?", + "confirm.delete_file": "Supprimer définitivement ce fichier ?", + "confirm.delete_dir": "Supprimer ce dossier et tout son contenu ?", + + "pwa.install_title": "Installer ObsiGate", + "pwa.install_desc": "Installez cette application sur votre appareil pour un accès rapide.", + "pwa.install_button": "Installer", + "pwa.dismiss": "Plus tard", + + "file.new_file": "Nouveau fichier", + "file.new_folder": "Nouveau dossier", + "file.rename": "Renommer", + "file.delete": "Supprimer", + "file.copy_path": "Copier le chemin", + "file.properties": "Propriétés", + "file.graph_view": "Vue graphe", + + "vault.add_dynamic": "Ajouter une vault", + "vault.remove_dynamic": "Retirer la vault", + "vault.reindex": "Réindexer la vault", + + "theme.light": "Clair", + "theme.dark": "Sombre" +}