diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6f3fc1d..8d8d247 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -139,6 +139,65 @@ Plugins should: - Be optional and not break core functionality if disabled - Follow the plugin configuration format +## 🌍 Contributing Translations + +NoteDiscovery supports multiple languages through JSON locale files. Adding a new language is easy! + +### How to Add a New Language + +1. **Copy the English locale file:** + ```bash + cp locales/en-US.json locales/xx-XX.json + ``` + Use the appropriate [BCP 47 language tag](https://en.wikipedia.org/wiki/IETF_language_tag) (e.g., `pt-BR`, `ja-JP`, `zh-CN`). + +2. **Update the `_meta` section:** + ```json + { + "_meta": { + "code": "xx-XX", + "name": "Language Name", + "flag": "🇽🇽" + }, + ... + } + ``` + +3. **Translate all string values:** + - Keep the keys exactly as they are (don't translate keys!) + - Translate only the values + - Preserve `{{placeholders}}` - they get replaced with dynamic values + - Keep emoji prefixes like `✓`, `💡`, `📂` as they are universal + +4. **Test your translation:** + - Restart the application + - Go to Settings → Language dropdown + - Your new language should appear automatically + - Click through the UI to verify translations + +### Translation Guidelines + +- **Be consistent** - Use the same terminology throughout +- **Match the tone** - NoteDiscovery uses friendly, concise language +- **Preserve formatting** - Keep `\n` for newlines in multi-line strings +- **Handle plurals simply** - It uses `{{count}}` placeholders (e.g., "hace {{count}}m") +- **Test date formats** - Dates are formatted using the browser's `Intl` API with your locale code + +### What Gets Translated + +| Category | Examples | +|----------|----------| +| UI Labels | Button text, panel titles, tooltips | +| Messages | Alerts, confirmations, prompts | +| Placeholders | Search box, editor hints | +| Relative times | "just now", "5m ago", "2d ago" | + +### What Stays in English + +- Technical terms: "Wikilinks", "Markdown", "HTML" +- Keyboard shortcuts in tooltips: "Ctrl+Z", "Esc" +- File extensions: ".md", ".json" + ## 📚 Contributing Documentation Documentation improvements are always welcome! Please: diff --git a/Dockerfile b/Dockerfile index 6a81c1f..0ae4ac4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,6 +28,7 @@ COPY config.yaml . COPY VERSION . COPY plugins ./plugins COPY themes ./themes +COPY locales ./locales COPY generate_password.py . # Create data directory diff --git a/README.md b/README.md index 53500fd..a441b97 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ # 📝 NoteDiscovery +![GitHub Stars](https://img.shields.io/github/stars/gamosoft/notediscovery?style=flat) +![Build](https://img.shields.io/github/actions/workflow/status/gamosoft/notediscovery/docker-publish.yml) +![Latest Version](https://img.shields.io/github/v/tag/gamosoft/notediscovery) +![License](https://img.shields.io/github/license/gamosoft/notediscovery) + + + > Your Self-Hosted Knowledge Base 🌐 **[Visit the official website](https://www.notediscovery.com)** @@ -20,6 +27,14 @@ NoteDiscovery is a **lightweight, self-hosted note-taking application** that put - **Teams** looking for a self-hosted alternative to commercial apps - **Anyone** who values simplicity, speed, and ownership + +## 💖 Thanks for using NoteDiscovery! +If this project has been useful to you, consider supporting its development, it truly makes a difference! + +

+ Buy Me a Coffee at ko-fi.com +

+ ## ✨ Why NoteDiscovery? ### vs. Commercial Apps (Notion, Evernote, Obsidian Sync) @@ -46,6 +61,7 @@ NoteDiscovery is a **lightweight, self-hosted note-taking application** that put - 🧮 **Math Support** - LaTeX/MathJax for beautiful equations - 📄 **HTML Export** - Share notes as standalone HTML files - 🕸️ **Graph View** - Interactive visualization of connected notes +- ⭐ **Favorites** - Star your most-used notes for instant access ## 🚀 Quick Start @@ -127,6 +143,7 @@ docker run -d \ -v $(pwd)/data:/app/data \ -v $(pwd)/plugins:/app/plugins \ -v $(pwd)/themes:/app/themes \ + -v $(pwd)/locales:/app/locales \ -v $(pwd)/config.yaml:/app/config.yaml \ --restart unless-stopped \ ghcr.io/gamosoft/notediscovery:latest @@ -140,6 +157,7 @@ docker run -d ` -v ${PWD}/data:/app/data ` -v ${PWD}/plugins:/app/plugins ` -v ${PWD}/themes:/app/themes ` + -v ${PWD}/locales:/app/locales ` -v ${PWD}/config.yaml:/app/config.yaml ` --restart unless-stopped ` ghcr.io/gamosoft/notediscovery:latest @@ -221,6 +239,25 @@ Want to learn more? - 🔐 **[AUTHENTICATION.md](documentation/AUTHENTICATION.md)** - Enable password protection for your instance - 🔧 **[ENVIRONMENT_VARIABLES.md](documentation/ENVIRONMENT_VARIABLES.md)** - Configure settings via environment variables +## 🌍 Multiple Languages + +NoteDiscovery supports multiple languages! Currently available: +- 🇺🇸 English (en-US) - Default +- 🇪🇸 Español (es-ES) +- 🇩🇪 Deutsch (de-DE) +- 🇫🇷 Français (fr-FR) + +**To change language:** Go to Settings (gear icon) → Language dropdown. + +**To add your own language:** See the [Contributing Guidelines](CONTRIBUTING.md#-contributing-translations) for instructions on creating translation files. + +**Docker users:** Mount your custom locales folder to add or override translations: + +```yaml +volumes: + - ./locales:/app/locales # Custom translations +``` + 💡 **Pro Tip:** If you clone this repository, you can mount the `documentation/` folder to view these docs inside the app: ```yaml @@ -232,10 +269,6 @@ volumes: Then access them at `http://localhost:8000` - the docs will appear as a `docs/` folder in the file browser! -## 💖 Support Development - -If you find NoteDiscovery useful, consider [☕ buying me a coffee](https://ko-fi.com/gamosoft) to help keep the project going. Every bit helps with new features, bug fixes, and improvements. Thank you! - ## 🤝 Contributing **Before submitting a pull request**, especially for major changes, please: diff --git a/backend/main.py b/backend/main.py index de63a08..fe27243 100644 --- a/backend/main.py +++ b/backend/main.py @@ -80,8 +80,9 @@ if 'AUTHENTICATION_SECRET_KEY' in os.environ: # Initialize app app = FastAPI( title=config['app']['name'], - description=config['app']['tagline'], - version=config['app']['version'] + version=config['app']['version'], + docs_url=None, # Disable Swagger UI at /docs + redoc_url=None # Disable ReDoc at /redoc ) # CORS middleware configuration @@ -96,7 +97,8 @@ app.add_middleware( ) print(f"🌐 CORS allowed origins: {allowed_origins}") -# ============================================================================ +# =========================================================== +# ================= # Security Helpers # ============================================================================ @@ -252,14 +254,7 @@ async def login_page(request: Request, error: str = None): async with aiofiles.open(login_path, 'r', encoding='utf-8') as f: content = await f.read() - # Inject error message if present - if error: - content = content.replace('', 'class="error"') - content = content.replace('', - f'
{error}
') - else: - content = content.replace('', '') - content = content.replace('', '') + # No server-side manipulation needed - frontend handles error display via URL params return content @@ -280,8 +275,8 @@ async def login(request: Request, password: str = Form(...)): request.session['authenticated'] = True return RedirectResponse(url="/", status_code=303) else: - # Redirect back to login with error message - return RedirectResponse(url="/login?error=Incorrect+password.+Please+try+again.", status_code=303) + # Redirect back to login with error code (frontend will translate) + return RedirectResponse(url="/login?error=incorrect_password", status_code=303) @app.get("/logout") @@ -325,8 +320,7 @@ async def api_documentation(): return { "app": { "name": config['app']['name'], - "version": config['app']['version'], - "description": config['app']['tagline'] + "version": config['app']['version'] }, "endpoints": [ { @@ -339,7 +333,7 @@ async def api_documentation(): "method": "GET", "path": "/api/config", "description": "Get application configuration", - "response": "{ name, tagline, version, searchEnabled }" + "response": "{ name, version, searchEnabled }" }, { "method": "GET", @@ -489,7 +483,6 @@ async def get_config(): """Get app configuration for frontend""" return { "name": config['app']['name'], - "tagline": config['app']['tagline'], "version": config['app']['version'], "searchEnabled": config['search']['enabled'], "demoMode": DEMO_MODE, # Expose demo mode flag to frontend @@ -519,6 +512,50 @@ async def get_theme(theme_id: str): return {"css": css, "theme_id": theme_id} +# Locales endpoints (unauthenticated - needed for login page and initial load) +@app.get("/api/locales") +async def get_available_locales(): + """Get list of available locales""" + import json + locales_dir = Path(__file__).parent.parent / "locales" + locales = [] + + if locales_dir.exists(): + for file in sorted(locales_dir.glob("*.json")): + try: + with open(file, 'r', encoding='utf-8') as f: + data = json.load(f) + meta = data.get('_meta', {}) + locales.append({ + "code": meta.get('code', file.stem), + "name": meta.get('name', file.stem), + "flag": meta.get('flag', '🌐') + }) + except (json.JSONDecodeError, IOError): + # Skip invalid locale files + continue + + return {"locales": locales} + + +@app.get("/api/locales/{locale_code}") +async def get_locale(locale_code: str): + """Get translations for a specific locale""" + import json + locales_dir = Path(__file__).parent.parent / "locales" + locale_file = locales_dir / f"{locale_code}.json" + + if not locale_file.exists(): + raise HTTPException(status_code=404, detail="Locale not found") + + try: + with open(locale_file, 'r', encoding='utf-8') as f: + translations = json.load(f) + return translations + except (json.JSONDecodeError, IOError) as e: + raise HTTPException(status_code=500, detail=f"Failed to load locale: {str(e)}") + + @api_router.post("/folders") @limiter.limit("30/minute") async def create_new_folder(request: Request, data: dict): diff --git a/config.yaml b/config.yaml index 5e88d3e..fdaf2a3 100644 --- a/config.yaml +++ b/config.yaml @@ -1,9 +1,8 @@ # NoteDiscovery Configuration -# Easy to rebrand: just change these values! app: name: "NoteDiscovery" - tagline: "Your Self-Hosted Knowledge Base" + # Tagline is now localized - see locales/*.json files (app.tagline key) server: host: "0.0.0.0" @@ -28,7 +27,7 @@ search: authentication: # Authentication settings # Set enabled to true to require login - enabled: false + enabled: true # ⚠️ SECURITY WARNING: Change these values before exposing to the internet! # Default values below are for LOCAL TESTING ONLY diff --git a/docker-compose.ghcr.yml b/docker-compose.ghcr.yml index 6101c02..f948465 100644 --- a/docker-compose.ghcr.yml +++ b/docker-compose.ghcr.yml @@ -11,6 +11,8 @@ services: - ./plugins:/app/plugins # Custom themes (REQUIRED - must contain theme .css files) - ./themes:/app/themes + # Custom locales/translations (optional - add your own language files) + - ./locales:/app/locales # Config file (REQUIRED - must exist as a file, not directory!) - ./config.yaml:/app/config.yaml restart: unless-stopped diff --git a/docker-compose.yml b/docker-compose.yml index 843eb96..1819ffd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,8 @@ services: - ./plugins:/app/plugins # Custom themes (REQUIRED - must contain theme .css files) - ./themes:/app/themes + # Custom locales/translations (optional - add your own language files) + - ./locales:/app/locales # Config file (REQUIRED - must exist as a file, not directory!) - ./config.yaml:/app/config.yaml restart: unless-stopped diff --git a/docs/index.html b/docs/index.html index 508c6f5..e57b741 100644 --- a/docs/index.html +++ b/docs/index.html @@ -844,6 +844,12 @@

8 built-in themes with dark/light modes and full customization.

+
+
+

Favorites

+

Star your most-used notes for instant access from the sidebar.

+
+
🔌

Extensible

diff --git a/frontend/app.js b/frontend/app.js index 329f8d6..facaa34 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -25,6 +25,8 @@ const ErrorHandler = { // Show user-friendly alert if requested if (showAlert) { + // Note: ErrorHandler doesn't have access to Alpine's t() function + // This message remains in English as a fallback alert(`Failed to ${operation}. Please try again.`); } } @@ -34,7 +36,6 @@ function noteApp() { return { // App state appName: 'NoteDiscovery', - appTagline: 'Your Self-Hosted Knowledge Base', appVersion: '0.0.0', authEnabled: false, notes: [], @@ -83,6 +84,12 @@ function noteApp() { currentTheme: 'light', availableThemes: [], + // Locale/i18n state + currentLocale: localStorage.getItem('locale') || 'en-US', + availableLocales: [], + // Translations loaded from backend (preloaded before Alpine init via window.__preloadedTranslations) + translations: window.__preloadedTranslations || {}, + // Syntax highlighting syntaxHighlightEnabled: false, syntaxHighlightTimeout: null, @@ -229,7 +236,7 @@ function noteApp() { return this._homepageCache.breadcrumb; } - const breadcrumb = [{ name: 'Home', path: '' }]; + const breadcrumb = [{ name: this.t('homepage.title'), path: '' }]; if (this.selectedHomepageFolder) { const parts = this.selectedHomepageFolder.split('/').filter(Boolean); @@ -257,6 +264,18 @@ function noteApp() { return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; }, + // Helper: Format date using current locale + formatDate(dateStr) { + if (!dateStr) return ''; + const date = new Date(dateStr); + if (isNaN(date.getTime())) return ''; + return date.toLocaleDateString(this.currentLocale, { + year: 'numeric', + month: 'short', + day: 'numeric' + }); + }, + getFolderNode(folderPath = '') { if (!this.folderTree || typeof this.folderTree !== 'object') { return null; @@ -320,6 +339,9 @@ function noteApp() { await this.loadConfig(); await this.loadThemes(); await this.initTheme(); + await this.loadAvailableLocales(); + // Note: Translations are preloaded synchronously before Alpine init (see index.html) + // loadLocale() is only called when user changes language from settings await this.loadNotes(); await this.loadTemplates(); await this.checkStatsPlugin(); @@ -545,7 +567,6 @@ function noteApp() { const response = await fetch('/api/config'); const config = await response.json(); this.appName = config.name; - this.appTagline = config.tagline; this.appVersion = config.version || '0.0.0'; this.authEnabled = config.authentication?.enabled || false; } catch (error) { @@ -761,6 +782,68 @@ function noteApp() { } }, + // ==================== INTERNATIONALIZATION ==================== + + // Translation function - get translated string by key + t(key, params = {}) { + const keys = key.split('.'); + let value = this.translations; + + for (const k of keys) { + value = value?.[k]; + } + + // Fallback to key if translation not found (silently - default translations are inline) + if (typeof value !== 'string') { + return key; + } + + // Replace {{param}} placeholders + return value.replace(/\{\{(\w+)\}\}/g, (_, name) => params[name] ?? `{{${name}}}`); + }, + + // Load available locales from backend + async loadAvailableLocales() { + try { + const response = await fetch('/api/locales'); + const data = await response.json(); + this.availableLocales = data.locales || []; + } catch (error) { + console.error('Failed to load available locales:', error); + this.availableLocales = [{ code: 'en-US', name: 'English', flag: '🇺🇸' }]; + } + }, + + // Load translations for a specific locale + async loadLocale(localeCode = null) { + const targetLocale = localeCode || localStorage.getItem('locale') || 'en-US'; + + try { + const response = await fetch(`/api/locales/${targetLocale}`); + if (response.ok) { + this.translations = await response.json(); + this.currentLocale = targetLocale; + localStorage.setItem('locale', targetLocale); + } else if (targetLocale !== 'en-US') { + // Fallback to en-US if requested locale not found + await this.loadLocale('en-US'); + } + } catch (error) { + console.error('Failed to load locale:', error); + // If en-US also fails, translations will be empty and t() will return keys + if (targetLocale !== 'en-US') { + await this.loadLocale('en-US'); + } + } + }, + + // Change locale and reload translations + async changeLocale(localeCode) { + await this.loadLocale(localeCode); + }, + + // ==================== END INTERNATIONALIZATION ==================== + // Load all notes async loadNotes() { try { @@ -907,7 +990,7 @@ function noteApp() { // CRITICAL: Check if note already exists const existingNote = this.notes.find(note => note.path === notePath); if (existingNote) { - alert(`A note named "${this.newTemplateNoteName.trim()}" already exists in this location.\nPlease choose a different name.`); + alert(this.t('notes.already_exists', { name: this.newTemplateNoteName.trim() })); return; } @@ -923,7 +1006,7 @@ function noteApp() { if (!response.ok) { const error = await response.json(); - alert(error.detail || 'Failed to create note from template'); + alert(error.detail || this.t('templates.create_failed')); return; } @@ -1128,13 +1211,13 @@ function noteApp() { const diffHours = Math.floor(diffMins / 60); const diffDays = Math.floor(diffHours / 24); - if (diffSecs < 60) return 'just now'; - if (diffMins < 60) return `${diffMins}m ago`; - if (diffHours < 24) return `${diffHours}h ago`; - if (diffDays < 7) return `${diffDays}d ago`; + if (diffSecs < 60) return this.t('editor.just_now'); + if (diffMins < 60) return this.t('editor.minutes_ago', { count: diffMins }); + if (diffHours < 24) return this.t('editor.hours_ago', { count: diffHours }); + if (diffDays < 7) return this.t('editor.days_ago', { count: diffDays }); - // For older dates, show the date - return modified.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); + // For older dates, show the date in selected locale + return modified.toLocaleDateString(this.currentLocale, { month: 'short', day: 'numeric' }); }, // Parse tags from markdown content (matches backend logic) @@ -1356,7 +1439,7 @@ function noteApp() { ${folder.name} - ${folder.notes.length === 0 && (!folder.children || Object.keys(folder.children).length === 0) ? '(empty)' : ''} + ${folder.notes.length === 0 && (!folder.children || Object.keys(folder.children).length === 0) ? `(${this.t('folders.empty')})` : ''}
@@ -1678,7 +1761,7 @@ function noteApp() { // Handle image files dropped into editor async handleImageDrop(event) { if (!this.currentNote) { - alert('Please open a note first before uploading images.'); + alert(this.t('notes.open_first')); return; } @@ -1690,7 +1773,7 @@ function noteApp() { }); if (imageFiles.length === 0) { - alert('No valid image files found. Supported formats: JPG, PNG, GIF, WEBP'); + alert(this.t('images.no_valid_files')); return; } @@ -1823,7 +1906,7 @@ function noteApp() { // Delete an image async deleteImage(imagePath) { const filename = imagePath.split('/').pop(); - if (!confirm(`Delete image "${filename}"?`)) return; + if (!confirm(this.t('images.confirm_delete', { name: filename }))) return; try { const response = await fetch(`/api/notes/${encodeURIComponent(imagePath)}`, { @@ -1896,7 +1979,7 @@ function noteApp() { if (noteByPathCI) { this.loadNote(noteByPathCI.path); } else { - alert(`Note not found: ${notePath}`); + alert(this.t('notes.not_found', { path: notePath })); } } } @@ -1996,11 +2079,11 @@ function noteApp() { } } else { const errorData = await response.json().catch(() => ({})); - alert(errorData.detail || 'Failed to move note.'); + alert(errorData.detail || this.t('move.failed_note')); } } catch (error) { console.error('Failed to move note:', error); - alert('Failed to move note.'); + alert(this.t('move.failed_note')); } return; @@ -2011,7 +2094,7 @@ function noteApp() { // Prevent dropping folder into itself or its subfolders if (targetFolderPath === draggedFolderPath || targetFolderPath.startsWith(draggedFolderPath + '/')) { - alert('Cannot move folder into itself or its subfolder.'); + alert(this.t('folders.cannot_move_into_self')); return; } @@ -2058,11 +2141,11 @@ function noteApp() { } } else { const errorData = await response.json().catch(() => ({})); - alert(errorData.detail || 'Failed to move folder.'); + alert(errorData.detail || this.t('move.failed_folder')); } } catch (error) { console.error('Failed to move folder:', error); - alert('Failed to move folder.'); + alert(this.t('move.failed_folder')); } this.dropTarget = null; } @@ -2459,15 +2542,15 @@ function noteApp() { this.closeDropdown(); const promptText = targetFolder - ? `Create note in "${targetFolder}".\nEnter note name:` - : 'Enter note name (you can use folder/name):'; + ? this.t('notes.prompt_name_in_folder', { folder: targetFolder }) + : this.t('notes.prompt_name_with_path'); const noteName = prompt(promptText); if (!noteName) return; const sanitizedName = noteName.trim().replace(/[^a-zA-Z0-9-_\s\/]/g, ''); if (!sanitizedName) { - alert('Invalid note name.'); + alert(this.t('notes.invalid_name')); return; } @@ -2481,7 +2564,7 @@ function noteApp() { // CRITICAL: Check if note already exists const existingNote = this.notes.find(note => note.path === notePath); if (existingNote) { - alert(`A note named "${sanitizedName}" already exists in this location.\nPlease choose a different name.`); + alert(this.t('notes.already_exists', { name: sanitizedName })); return; } @@ -2520,15 +2603,15 @@ function noteApp() { this.closeDropdown(); const promptText = targetFolder - ? `Create subfolder in "${targetFolder}".\nEnter folder name:` - : 'Create new folder.\nEnter folder path (e.g., "Projects" or "Work/2025"):'; + ? this.t('folders.prompt_name_in_folder', { folder: targetFolder }) + : this.t('folders.prompt_name_with_path'); const folderName = prompt(promptText); if (!folderName) return; const sanitizedName = folderName.trim().replace(/[^a-zA-Z0-9-_\s\/]/g, ''); if (!sanitizedName) { - alert('Invalid folder name.'); + alert(this.t('folders.invalid_name')); return; } @@ -2537,7 +2620,7 @@ function noteApp() { // Check if folder already exists const existingFolder = this.allFolders.find(folder => folder === folderPath); if (existingFolder) { - alert(`A folder named "${sanitizedName}" already exists in this location.\nPlease choose a different name.`); + alert(this.t('folders.already_exists', { name: sanitizedName })); return; } @@ -2567,12 +2650,12 @@ function noteApp() { // Rename a folder async renameFolder(folderPath, currentName) { - const newName = prompt(`Rename folder "${currentName}" to:`, currentName); + const newName = prompt(this.t('folders.prompt_rename', { name: currentName }), currentName); if (!newName || newName === currentName) return; const sanitizedName = newName.trim().replace(/[^a-zA-Z0-9-_\s]/g, ''); if (!sanitizedName) { - alert('Invalid folder name.'); + alert(this.t('folders.invalid_name')); return; } @@ -2630,14 +2713,7 @@ function noteApp() { // Delete folder async deleteFolder(folderPath, folderName) { - const confirmation = confirm( - `⚠️ WARNING ⚠️\n\n` + - `Are you sure you want to delete the folder "${folderName}"?\n\n` + - `This will PERMANENTLY delete:\n` + - `• All notes inside this folder\n` + - `• All subfolders and their contents\n\n` + - `This action CANNOT be undone!` - ); + const confirmation = confirm(this.t('folders.confirm_delete', { name: folderName })); if (!confirmation) return; @@ -2945,7 +3021,7 @@ function noteApp() { const newName = this.currentNoteName.trim(); if (!newName) { - alert('Note name cannot be empty.'); + alert(this.t('notes.empty_name')); return; } @@ -2957,7 +3033,7 @@ function noteApp() { // Check if a note with the new name already exists const existingNote = this.notes.find(n => n.path.toLowerCase() === newPath.toLowerCase()); if (existingNote) { - alert(`A note named "${newName}" already exists in this folder.`); + alert(this.t('notes.already_exists', { name: newName })); // Reset the name in the UI this.currentNoteName = oldPath.split('/').pop().replace('.md', ''); return; @@ -3003,7 +3079,7 @@ function noteApp() { // Delete any note from sidebar async deleteNote(notePath, noteName) { - if (!confirm(`Delete "${noteName}"?`)) return; + if (!confirm(this.t('notes.confirm_delete', { name: noteName }))) return; try { const response = await fetch(`/api/notes/${notePath}`, { @@ -3799,7 +3875,7 @@ function noteApp() { date = new Date(value); } if (!isNaN(date.getTime())) { - return date.toLocaleDateString('en-US', { + return date.toLocaleDateString(this.currentLocale, { year: 'numeric', month: 'short', day: 'numeric' @@ -3809,7 +3885,7 @@ function noteApp() { // Booleans if (typeof value === 'boolean') { - return value ? '✓ Yes' : '✗ No'; + return value ? this.t('common.yes') : this.t('common.no'); } return String(value); @@ -4083,7 +4159,7 @@ function noteApp() { // Export current note as HTML async exportToHTML() { if (!this.currentNote || !this.noteContent) { - alert('No note content to export'); + alert(this.t('notes.no_content')); return; } @@ -4322,7 +4398,7 @@ function noteApp() { } catch (error) { console.error('HTML export failed:', error); - alert(`Failed to export HTML: ${error.message}`); + alert(this.t('export.failed', { error: error.message })); } }, @@ -4715,10 +4791,10 @@ function noteApp() {
- Markdown links + ${this.t('graph.markdown_links')}
- Click: select • Double-click: open + ${this.t('graph.click_hint')}
`; container.appendChild(legend); diff --git a/frontend/index.html b/frontend/index.html index bb34c5b..3b88d5e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -14,6 +14,35 @@ + + + @@ -1087,7 +1116,7 @@ x-transition:enter-end="opacity-60 translate-y-0" @click="toggleZenMode()" class="zen-exit-button" - title="Exit Zen Mode (Esc)" + :title="t('zen_mode.exit_hint')" > @@ -1119,7 +1148,7 @@ alt="NoteDiscovery" class="icon-rail-logo" @click="goHome()" - title="Go to homepage" + :title="t('sidebar.go_to_homepage')" > @@ -1127,7 +1156,7 @@ class="icon-rail-btn" :class="{'active': activePanel === 'files'}" @click="activePanel = 'files'" - title="Files" + :title="t('sidebar.files')" aria-label="Files panel" > @@ -1140,7 +1169,7 @@ class="icon-rail-btn relative" :class="{'active': activePanel === 'search'}" @click="activePanel = 'search'" - title="Search" + :title="t('sidebar.search')" aria-label="Search panel" > @@ -1161,7 +1190,7 @@ class="icon-rail-btn relative" :class="{'active': activePanel === 'tags'}" @click="activePanel = 'tags'" - title="Tags" + :title="t('tags.title')" aria-label="Tags panel" > @@ -1184,7 +1213,7 @@ class="icon-rail-btn" :class="{'active': showGraph}" @click="showGraph = true; initGraph()" - title="Graph View" + :title="t('graph.title')" aria-label="Graph view" > @@ -1197,7 +1226,7 @@ class="icon-rail-btn" :class="{'active': activePanel === 'settings'}" @click="activePanel = 'settings'" - title="Settings" + :title="t('sidebar.settings')" aria-label="Settings panel" > @@ -1214,7 +1243,7 @@
- Files +
@@ -1326,8 +1352,8 @@ @drop="if(draggedNote || draggedFolder) onFolderDrop('')" style="height: 28px; line-height: 20px; display: flex; align-items: center; justify-content: center;" > - 💡 Drag=Move - 📂 Root folder + + @@ -1356,7 +1382,7 @@ @click.stop="note.type === 'image' ? deleteImage(note.path) : deleteNote(note.path, note.name)" class="note-delete-btn absolute right-2 top-1/2 transform -translate-y-1/2 px-1 py-0.5 text-xs rounded hover:brightness-110 transition-opacity" style="opacity: 0; color: var(--error);" - :title="note.type === 'image' ? 'Delete image' : 'Delete note'" + :title="note.type === 'image' ? t('toolbar.delete_image') : t('toolbar.delete_note')" > @@ -1368,7 +1394,8 @@
- No notes or folders yet.
Create your first note or folder! +
+
@@ -1379,7 +1406,7 @@
- Search +
@@ -1389,7 +1416,7 @@ type="text" x-model="searchQuery" @input="searchNotes()" - placeholder="Search notes..." + :placeholder="t('sidebar.search_placeholder')" class="w-full px-2 py-1.5 pl-7 pr-7 text-xs rounded focus:outline-none focus:ring-1" style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-primary);" > @@ -1401,7 +1428,7 @@ @click="searchQuery = ''; searchResults = []; currentSearchHighlight = ''; clearSearchHighlights();" class="absolute right-1.5 top-1.5 w-4 h-4 flex items-center justify-center rounded-full hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors" style="color: var(--text-tertiary);" - title="Clear search" + :title="t('sidebar.clear_search')" > @@ -1417,10 +1444,10 @@ >
- -
@@ -1461,7 +1488,7 @@ -

Type to search your notes

+

@@ -1472,7 +1499,7 @@
- Tags +
@@ -1482,7 +1509,7 @@ @click="clearTagFilters()" class="w-full px-2 py-1.5 text-xs rounded flex items-center justify-center gap-1" style="background-color: var(--accent-primary); color: white;" - title="Clear tag filters" + :title="t('tags.clear_all')" > @@ -1504,7 +1531,7 @@ :style="selectedTags.includes(tag) ? 'background-color: var(--accent-primary); color: white; font-weight: 600;' : 'background-color: var(--bg-tertiary); color: var(--text-primary);'" - :title="`Filter by ${tag} (${count} notes)`" + :title="t('tags.filter_by', {tag: tag, count: count})" > @@ -1527,7 +1554,7 @@ @@ -1817,7 +1859,7 @@ style="position: absolute; top: 8px; right: 8px; opacity: 0; transition: opacity 0.2s; color: var(--error); padding: 4px; border-radius: 4px; background-color: var(--bg-secondary);" onmouseover="this.style.backgroundColor='var(--bg-hover)'" onmouseout="this.style.backgroundColor='var(--bg-secondary)'" - title="Delete folder" + :title="t('common.delete')" > @@ -1834,7 +1876,7 @@
- + @@ -1858,7 +1900,7 @@ style="position: absolute; top: 8px; right: 8px; opacity: 0; transition: opacity 0.2s; color: var(--error); padding: 4px; border-radius: 4px; background-color: var(--bg-secondary);" onmouseover="this.style.backgroundColor='var(--bg-hover)'" onmouseout="this.style.backgroundColor='var(--bg-secondary)'" - :title="note.type === 'image' ? 'Delete image' : 'Delete note'" + :title="note.type === 'image' ? t('toolbar.delete_image') : t('toolbar.delete_note')" > @@ -1870,7 +1912,7 @@
- +
@@ -1902,7 +1944,7 @@ style="color: var(--text-primary); display: none;" onmouseover="this.style.backgroundColor='var(--bg-hover)'" onmouseout="this.style.backgroundColor='transparent'" - title="Toggle sidebar" + :title="t('sidebar.toggle_sidebar')" > @@ -1925,7 +1967,7 @@ :style="undoHistory.length <= 1 ? 'color: var(--text-tertiary); opacity: 0.4; cursor: not-allowed;' : 'color: var(--text-secondary);'" onmouseover="if(!this.disabled) this.style.backgroundColor='var(--bg-hover)'" onmouseout="this.style.backgroundColor='transparent'" - title="Undo (Ctrl+Z)" + :title="t('toolbar.undo')" > @@ -1940,7 +1982,7 @@ :style="redoHistory.length === 0 ? 'color: var(--text-tertiary); opacity: 0.4; cursor: not-allowed;' : 'color: var(--text-secondary);'" onmouseover="if(!this.disabled) this.style.backgroundColor='var(--bg-hover)'" onmouseout="this.style.backgroundColor='transparent'" - title="Redo (Ctrl+Y)" + :title="t('toolbar.redo')" > @@ -1954,15 +1996,15 @@ style="color: var(--error);" onmouseover="this.style.backgroundColor='var(--bg-hover)'" onmouseout="this.style.backgroundColor='transparent'" - :title="currentImage ? 'Delete image' : 'Delete note'" + :title="currentImage ? t('toolbar.delete_image') : t('toolbar.delete_note')" > - Saving... - ✓ Saved - + + +
@@ -1982,8 +2024,8 @@ :style="viewMode === 'edit' ? 'background-color: var(--accent-primary); color: white;' : 'color: var(--text-secondary);'" @mouseenter="if (viewMode !== 'edit') $el.style.backgroundColor = 'var(--bg-secondary)'" @mouseleave="if (viewMode !== 'edit') $el.style.backgroundColor = 'transparent'" + x-text="t('editor.mode_edit')" > - Edit @@ -2014,7 +2056,7 @@ @@ -2467,19 +2509,18 @@ style="background-color: var(--bg-secondary); color: var(--text-primary);" @click.stop> -

Create from Template

+

-
diff --git a/frontend/login.html b/frontend/login.html index 2b7cf9a..6ae4e0c 100644 --- a/frontend/login.html +++ b/frontend/login.html @@ -6,6 +6,40 @@ Login - NoteDiscovery + + + diff --git a/locales/de-DE.json b/locales/de-DE.json new file mode 100644 index 0000000..bb05b67 --- /dev/null +++ b/locales/de-DE.json @@ -0,0 +1,216 @@ +{ + "_meta": { + "code": "de-DE", + "name": "Deutsch", + "flag": "🇩🇪" + }, + + "app": { + "tagline": "Deine Selbstgehostete Wissensdatenbank" + }, + + "common": { + "save": "Speichern", + "cancel": "Abbrechen", + "delete": "Löschen", + "rename": "Umbenennen", + "create": "Erstellen", + "close": "Schließen", + "yes": "✓ Ja", + "no": "✗ Nein", + "ok": "OK", + "error": "Fehler", + "loading": "Laden...", + "saved": "✓ Gespeichert", + "saving": "Speichern...", + "copied": "✓ Kopiert!", + "failed": "{{action}} fehlgeschlagen. Bitte versuche es erneut." + }, + + "sidebar": { + "title": "DATEIEN", + "favorites_title": "Favoriten", + "folders_and_notes": "Ordner & Notizen", + "new_button": "+ Neu", + "new_note": "Neue Notiz", + "new_folder": "Neuer Ordner", + "new_from_template": "Neu aus Vorlage", + "search_placeholder": "Notizen durchsuchen...", + "search_hint": "Tippe, um deine Notizen zu durchsuchen", + "clear_search": "Suche löschen", + "drag_hint": "💡 Ziehen=Verschieben", + "root_folder": "📂 Hauptordner", + "no_notes": "Noch keine Notizen. Erstelle deine erste Notiz!", + "no_notes_yet": "Noch keine Notizen oder Ordner.", + "create_first": "Erstelle deine erste Notiz oder Ordner!", + "no_favorites": "Noch keine Favoriten", + "no_results": "Keine Ergebnisse gefunden", + "expand_all": "Alle Ordner aufklappen", + "collapse_all": "Alle Ordner zuklappen", + "toggle_sidebar": "Seitenleiste umschalten", + "go_to_homepage": "Zur Startseite", + "files": "Dateien", + "search": "Suchen", + "search_title": "SUCHE", + "settings": "Einstellungen", + "settings_title": "EINSTELLUNGEN", + "filtered_notes": "Gefilterte Notizen" + }, + + "editor": { + "placeholder": "Beginne in Markdown zu schreiben...", + "drop_hint": "💡 Hier ablegen, um an Cursorposition einzufügen...", + "mode_edit": "Bearbeiten", + "mode_split": "Teilen", + "mode_preview": "Vorschau", + "edited": "Bearbeitet {{time}}", + "just_now": "gerade eben", + "minutes_ago": "vor {{count}}m", + "hours_ago": "vor {{count}}h", + "days_ago": "vor {{count}}T" + }, + + "notes": { + "confirm_delete": "\"{{name}}\" löschen?", + "already_exists": "Eine Notiz mit dem Namen \"{{name}}\" existiert bereits an diesem Ort.\nBitte wähle einen anderen Namen.", + "prompt_name": "Notizname eingeben:", + "prompt_name_in_folder": "Notiz in \"{{folder}}\" erstellen.\nNotizname eingeben:", + "prompt_name_with_path": "Notizname eingeben (du kannst Ordner/Name verwenden):", + "prompt_rename": "Neuen Namen eingeben:", + "invalid_name": "Ungültiger Notizname.", + "empty_name": "Der Notizname darf nicht leer sein.", + "not_found": "Notiz nicht gefunden: {{path}}", + "no_content": "Kein Inhalt zum Exportieren", + "open_first": "Bitte öffne zuerst eine Notiz, bevor du Bilder hochlädst." + }, + + "folders": { + "confirm_delete": "⚠️ WARNUNG ⚠️\n\nBist du sicher, dass du den Ordner \"{{name}}\" löschen möchtest?\n\nDies wird DAUERHAFT löschen:\n• Alle Notizen in diesem Ordner\n• Alle Unterordner und deren Inhalte\n\nDiese Aktion kann NICHT rückgängig gemacht werden!", + "already_exists": "Ein Ordner mit dem Namen \"{{name}}\" existiert bereits an diesem Ort.\nBitte wähle einen anderen Namen.", + "prompt_name": "Ordnername eingeben:", + "prompt_name_in_folder": "Unterordner in \"{{folder}}\" erstellen.\nOrdnername eingeben:", + "prompt_name_with_path": "Neuen Ordner erstellen.\nOrdnerpfad eingeben (z.B. \"Projekte\" oder \"Arbeit/2025\"):", + "prompt_rename": "Ordner \"{{name}}\" umbenennen zu:", + "invalid_name": "Ungültiger Ordnername.", + "cannot_move_into_self": "Ordner kann nicht in sich selbst oder einen Unterordner verschoben werden.", + "empty": "leer" + }, + + "toolbar": { + "undo": "Rückgängig (Strg+Z)", + "redo": "Wiederholen (Strg+Y)", + "delete_note": "Notiz löschen", + "delete_image": "Bild löschen", + "export_html": "Als HTML exportieren", + "copy_link": "Link in Zwischenablage kopieren", + "add_favorite": "Zu Favoriten hinzufügen", + "remove_favorite": "Aus Favoriten entfernen" + }, + + "zen_mode": { + "title": "Zen-Modus", + "tooltip": "Zen-Modus (Strg+Alt+Z)", + "exit": "Zen-Modus beenden", + "exit_hint": "Zen-Modus beenden (Esc)" + }, + + "tags": { + "title": "Schlagwörter", + "no_tags": "Keine Schlagwörter gefunden", + "clear_all": "Schlagwort-Filter löschen", + "filter_by": "Nach {{tag}} filtern ({{count}} Notizen)" + }, + + "stats": { + "words": "Wörter", + "reading_time": "~{{minutes}}m Lesezeit", + "links": "{{count}} Links", + "click_details": "▼ Klicken für Details" + }, + + "graph": { + "title": "Graphansicht", + "empty": "Keine Verbindungen anzuzeigen", + "markdown_links": "Markdown-Links", + "click_hint": "Klick: auswählen • Doppelklick: öffnen" + }, + + "templates": { + "title": "Vorlagen", + "select": "Vorlage auswählen...", + "prompt_name": "Notizname eingeben:", + "no_templates": "Keine Vorlagen gefunden. Erstelle Vorlagen im _templates Ordner.", + "create_failed": "Erstellung der Notiz aus Vorlage fehlgeschlagen", + "create_from_template": "Aus Vorlage erstellen", + "select_template": "Vorlage auswählen", + "choose_template": "Vorlage wählen", + "note_name": "Notizname", + "available_placeholders": "Verfügbare Platzhalter", + "create_note": "Notiz erstellen" + }, + + "export": { + "failed": "HTML-Export fehlgeschlagen: {{error}}" + }, + + "login": { + "title": "Anmelden", + "tagline": "Deine Selbstgehostete Wissensdatenbank", + "password_placeholder": "Passwort eingeben", + "unlock_button": "🔓 Entsperren", + "footer": "🔒 Sicher & Selbstgehostet", + "error_incorrect_password": "Falsches Passwort. Bitte versuche es erneut." + }, + + "images": { + "confirm_delete": "Bild \"{{name}}\" löschen?", + "upload_failed": "Bild-Upload fehlgeschlagen", + "no_valid_files": "Keine gültigen Bilddateien gefunden. Unterstützte Formate: JPG, PNG, GIF, WEBP", + "formats": "Unterstützt JPG, PNG, GIF, WebP (max 10MB)" + }, + + "move": { + "failed_note": "Verschieben der Notiz fehlgeschlagen.", + "failed_folder": "Verschieben des Ordners fehlgeschlagen." + }, + + "search": { + "previous": "Vorheriger (Umschalt+F3)", + "next": "Nächster (F3)", + "matches": "{{current}} von {{total}}" + }, + + "theme": { + "title": "Design" + }, + + "language": { + "title": "Sprache" + }, + + "syntax_highlight": { + "title": "Editor-Syntaxhervorhebung", + "description": "Markdown-Syntax im Editor einfärben", + "enable": "Syntaxhervorhebung aktivieren", + "disable": "Syntaxhervorhebung deaktivieren" + }, + + "settings": { + "account": "Konto", + "logout": "Abmelden" + }, + + "homepage": { + "title": "Startseite", + "welcome": "Willkommen bei NoteDiscovery", + "get_started": "Erstelle etwas, um loszulegen", + "no_notes_title": "Noch keine Notizen", + "no_notes_desc": "Erstelle deine erste Notiz oder Ordner", + "folder_empty": "Dieser Ordner ist leer", + "note_singular": "Notiz", + "note_plural": "Notizen", + "folder_singular": "Ordner", + "folder_plural": "Ordner" + } +} + diff --git a/locales/en-US.json b/locales/en-US.json new file mode 100644 index 0000000..03d32c7 --- /dev/null +++ b/locales/en-US.json @@ -0,0 +1,216 @@ +{ + "_meta": { + "code": "en-US", + "name": "English", + "flag": "🇺🇸" + }, + + "app": { + "tagline": "Your Self-Hosted Knowledge Base" + }, + + "common": { + "save": "Save", + "cancel": "Cancel", + "delete": "Delete", + "rename": "Rename", + "create": "Create", + "close": "Close", + "yes": "✓ Yes", + "no": "✗ No", + "ok": "OK", + "error": "Error", + "loading": "Loading...", + "saved": "✓ Saved", + "saving": "Saving...", + "copied": "✓ Copied!", + "failed": "Failed to {{action}}. Please try again." + }, + + "sidebar": { + "title": "FILES", + "favorites_title": "Favorites", + "folders_and_notes": "Folders & Notes", + "new_button": "+ New", + "new_note": "New Note", + "new_folder": "New Folder", + "new_from_template": "New from Template", + "search_placeholder": "Search notes...", + "search_hint": "Type to search your notes", + "clear_search": "Clear search", + "drag_hint": "💡 Drag=Move", + "root_folder": "📂 Root folder", + "no_notes": "No notes yet. Create your first note!", + "no_notes_yet": "No notes or folders yet.", + "create_first": "Create your first note or folder!", + "no_favorites": "No favorites yet", + "no_results": "No results found", + "expand_all": "Expand all folders", + "collapse_all": "Collapse all folders", + "toggle_sidebar": "Toggle sidebar", + "go_to_homepage": "Go to homepage", + "files": "Files", + "search": "Search", + "search_title": "SEARCH", + "settings": "Settings", + "settings_title": "SETTINGS", + "filtered_notes": "Filtered Notes" + }, + + "editor": { + "placeholder": "Start writing in markdown...", + "drop_hint": "💡 Drop here to insert at cursor position...", + "mode_edit": "Edit", + "mode_split": "Split", + "mode_preview": "Preview", + "edited": "Edited {{time}}", + "just_now": "just now", + "minutes_ago": "{{count}}m ago", + "hours_ago": "{{count}}h ago", + "days_ago": "{{count}}d ago" + }, + + "notes": { + "confirm_delete": "Delete \"{{name}}\"?", + "already_exists": "A note named \"{{name}}\" already exists in this location.\nPlease choose a different name.", + "prompt_name": "Enter note name:", + "prompt_name_in_folder": "Create note in \"{{folder}}\".\nEnter note name:", + "prompt_name_with_path": "Enter note name (you can use folder/name):", + "prompt_rename": "Enter new name:", + "invalid_name": "Invalid note name.", + "empty_name": "Note name cannot be empty.", + "not_found": "Note not found: {{path}}", + "no_content": "No note content to export", + "open_first": "Please open a note first before uploading images." + }, + + "folders": { + "confirm_delete": "⚠️ WARNING ⚠️\n\nAre you sure you want to delete the folder \"{{name}}\"?\n\nThis will PERMANENTLY delete:\n• All notes inside this folder\n• All subfolders and their contents\n\nThis action CANNOT be undone!", + "already_exists": "A folder named \"{{name}}\" already exists in this location.\nPlease choose a different name.", + "prompt_name": "Enter folder name:", + "prompt_name_in_folder": "Create subfolder in \"{{folder}}\".\nEnter folder name:", + "prompt_name_with_path": "Create new folder.\nEnter folder path (e.g., \"Projects\" or \"Work/2025\"):", + "prompt_rename": "Rename folder \"{{name}}\" to:", + "invalid_name": "Invalid folder name.", + "cannot_move_into_self": "Cannot move folder into itself or its subfolder.", + "empty": "empty" + }, + + "toolbar": { + "undo": "Undo (Ctrl+Z)", + "redo": "Redo (Ctrl+Y)", + "delete_note": "Delete note", + "delete_image": "Delete image", + "export_html": "Export as HTML", + "copy_link": "Copy link to clipboard", + "add_favorite": "Add to favorites", + "remove_favorite": "Remove from favorites" + }, + + "zen_mode": { + "title": "Zen Mode", + "tooltip": "Zen Mode (Ctrl+Alt+Z)", + "exit": "Exit Zen Mode", + "exit_hint": "Exit Zen Mode (Esc)" + }, + + "tags": { + "title": "Tags", + "no_tags": "No tags found", + "clear_all": "Clear tag filters", + "filter_by": "Filter by {{tag}} ({{count}} notes)" + }, + + "stats": { + "words": "words", + "reading_time": "~{{minutes}}m read", + "links": "{{count}} links", + "click_details": "▼ Click for details" + }, + + "graph": { + "title": "Graph View", + "empty": "No connections to display", + "markdown_links": "Markdown links", + "click_hint": "Click: select • Double-click: open" + }, + + "templates": { + "title": "Templates", + "select": "Select a template...", + "prompt_name": "Enter note name:", + "no_templates": "No templates found. Create templates in the _templates folder.", + "create_failed": "Failed to create note from template", + "create_from_template": "Create from Template", + "select_template": "Select Template", + "choose_template": "Choose a template", + "note_name": "Note Name", + "available_placeholders": "Available placeholders", + "create_note": "Create Note" + }, + + "export": { + "failed": "Failed to export HTML: {{error}}" + }, + + "login": { + "title": "Login", + "tagline": "Your Self-Hosted Knowledge Base", + "password_placeholder": "Enter your password", + "unlock_button": "🔓 Unlock", + "footer": "🔒 Secure & Self-Hosted", + "error_incorrect_password": "Incorrect password. Please try again." + }, + + "images": { + "confirm_delete": "Delete image \"{{name}}\"?", + "upload_failed": "Failed to upload image", + "no_valid_files": "No valid image files found. Supported formats: JPG, PNG, GIF, WEBP", + "formats": "Supports JPG, PNG, GIF, WebP (max 10MB)" + }, + + "move": { + "failed_note": "Failed to move note.", + "failed_folder": "Failed to move folder." + }, + + "search": { + "previous": "Previous (Shift+F3)", + "next": "Next (F3)", + "matches": "{{current}} of {{total}}" + }, + + "theme": { + "title": "Theme" + }, + + "language": { + "title": "Language" + }, + + "syntax_highlight": { + "title": "Editor Syntax Highlight", + "description": "Colorize markdown syntax in editor", + "enable": "Enable syntax highlighting", + "disable": "Disable syntax highlighting" + }, + + "settings": { + "account": "Account", + "logout": "Logout" + }, + + "homepage": { + "title": "Home", + "welcome": "Welcome to NoteDiscovery", + "get_started": "Create something to get started", + "no_notes_title": "No notes yet", + "no_notes_desc": "Create your first note or folder", + "folder_empty": "This folder is empty", + "note_singular": "note", + "note_plural": "notes", + "folder_singular": "folder", + "folder_plural": "folders" + } +} + diff --git a/locales/es-ES.json b/locales/es-ES.json new file mode 100644 index 0000000..098982e --- /dev/null +++ b/locales/es-ES.json @@ -0,0 +1,216 @@ +{ + "_meta": { + "code": "es-ES", + "name": "Español", + "flag": "🇪🇸" + }, + + "app": { + "tagline": "Tu Base de Conocimientos Autoalojada" + }, + + "common": { + "save": "Guardar", + "cancel": "Cancelar", + "delete": "Eliminar", + "rename": "Renombrar", + "create": "Crear", + "close": "Cerrar", + "yes": "✓ Sí", + "no": "✗ No", + "ok": "Aceptar", + "error": "Error", + "loading": "Cargando...", + "saved": "✓ Guardado", + "saving": "Guardando...", + "copied": "✓ ¡Copiado!", + "failed": "Error al {{action}}. Por favor, inténtalo de nuevo." + }, + + "sidebar": { + "title": "ARCHIVOS", + "favorites_title": "Favoritos", + "folders_and_notes": "Carpetas y Notas", + "new_button": "+ Nuevo", + "new_note": "Nueva Nota", + "new_folder": "Nueva Carpeta", + "new_from_template": "Nueva desde Plantilla", + "search_placeholder": "Buscar notas...", + "search_hint": "Escribe para buscar tus notas", + "clear_search": "Limpiar búsqueda", + "drag_hint": "💡 Arrastrar=Mover", + "root_folder": "📂 Carpeta raíz", + "no_notes": "Aún no hay notas. ¡Crea tu primera nota!", + "no_notes_yet": "Aún no hay notas ni carpetas.", + "create_first": "¡Crea tu primera nota o carpeta!", + "no_favorites": "Aún no hay favoritos", + "no_results": "No se encontraron resultados", + "expand_all": "Expandir todas las carpetas", + "collapse_all": "Contraer todas las carpetas", + "toggle_sidebar": "Alternar barra lateral", + "go_to_homepage": "Ir al inicio", + "files": "Archivos", + "search": "Buscar", + "search_title": "BÚSQUEDA", + "settings": "Configuración", + "settings_title": "CONFIGURACIÓN", + "filtered_notes": "Notas Filtradas" + }, + + "editor": { + "placeholder": "Empieza a escribir en markdown...", + "drop_hint": "💡 Suelta aquí para insertar en la posición del cursor...", + "mode_edit": "Editar", + "mode_split": "Dividir", + "mode_preview": "Vista previa", + "edited": "Editado {{time}}", + "just_now": "ahora mismo", + "minutes_ago": "hace {{count}}m", + "hours_ago": "hace {{count}}h", + "days_ago": "hace {{count}}d" + }, + + "notes": { + "confirm_delete": "¿Eliminar \"{{name}}\"?", + "already_exists": "Ya existe una nota llamada \"{{name}}\" en esta ubicación.\nPor favor, elige un nombre diferente.", + "prompt_name": "Introduce el nombre de la nota:", + "prompt_name_in_folder": "Crear nota en \"{{folder}}\".\nIntroduce el nombre de la nota:", + "prompt_name_with_path": "Introduce el nombre de la nota (puedes usar carpeta/nombre):", + "prompt_rename": "Introduce el nuevo nombre:", + "invalid_name": "Nombre de nota inválido.", + "empty_name": "El nombre de la nota no puede estar vacío.", + "not_found": "Nota no encontrada: {{path}}", + "no_content": "No hay contenido para exportar", + "open_first": "Por favor, abre una nota antes de subir imágenes." + }, + + "folders": { + "confirm_delete": "⚠️ ADVERTENCIA ⚠️\n\n¿Estás seguro de que quieres eliminar la carpeta \"{{name}}\"?\n\nEsto eliminará PERMANENTEMENTE:\n• Todas las notas dentro de esta carpeta\n• Todas las subcarpetas y su contenido\n\n¡Esta acción NO se puede deshacer!", + "already_exists": "Ya existe una carpeta llamada \"{{name}}\" en esta ubicación.\nPor favor, elige un nombre diferente.", + "prompt_name": "Introduce el nombre de la carpeta:", + "prompt_name_in_folder": "Crear subcarpeta en \"{{folder}}\".\nIntroduce el nombre de la carpeta:", + "prompt_name_with_path": "Crear nueva carpeta.\nIntroduce la ruta de la carpeta (ej., \"Proyectos\" o \"Trabajo/2025\"):", + "prompt_rename": "Renombrar carpeta \"{{name}}\" a:", + "invalid_name": "Nombre de carpeta inválido.", + "cannot_move_into_self": "No se puede mover una carpeta dentro de sí misma o de una subcarpeta.", + "empty": "vacía" + }, + + "toolbar": { + "undo": "Deshacer (Ctrl+Z)", + "redo": "Rehacer (Ctrl+Y)", + "delete_note": "Eliminar nota", + "delete_image": "Eliminar imagen", + "export_html": "Exportar como HTML", + "copy_link": "Copiar enlace al portapapeles", + "add_favorite": "Añadir a favoritos", + "remove_favorite": "Quitar de favoritos" + }, + + "zen_mode": { + "title": "Modo Zen", + "tooltip": "Modo Zen (Ctrl+Alt+Z)", + "exit": "Salir del Modo Zen", + "exit_hint": "Salir del Modo Zen (Esc)" + }, + + "tags": { + "title": "Etiquetas", + "no_tags": "No se encontraron etiquetas", + "clear_all": "Limpiar filtros de etiquetas", + "filter_by": "Filtrar por {{tag}} ({{count}} notas)" + }, + + "stats": { + "words": "palabras", + "reading_time": "~{{minutes}}m de lectura", + "links": "{{count}} enlaces", + "click_details": "▼ Clic para detalles" + }, + + "graph": { + "title": "Vista de Grafo", + "empty": "No hay conexiones para mostrar", + "markdown_links": "Enlaces Markdown", + "click_hint": "Clic: seleccionar • Doble clic: abrir" + }, + + "templates": { + "title": "Plantillas", + "select": "Selecciona una plantilla...", + "prompt_name": "Introduce el nombre de la nota:", + "no_templates": "No se encontraron plantillas. Crea plantillas en la carpeta _templates.", + "create_failed": "Error al crear nota desde plantilla", + "create_from_template": "Crear desde Plantilla", + "select_template": "Seleccionar Plantilla", + "choose_template": "Elige una plantilla", + "note_name": "Nombre de la Nota", + "available_placeholders": "Marcadores disponibles", + "create_note": "Crear Nota" + }, + + "export": { + "failed": "Error al exportar HTML: {{error}}" + }, + + "login": { + "title": "Iniciar sesión", + "tagline": "Tu Base de Conocimientos Autoalojada", + "password_placeholder": "Introduce tu contraseña", + "unlock_button": "🔓 Desbloquear", + "footer": "🔒 Seguro y Autoalojado", + "error_incorrect_password": "Contraseña incorrecta. Por favor, inténtalo de nuevo." + }, + + "images": { + "confirm_delete": "¿Eliminar imagen \"{{name}}\"?", + "upload_failed": "Error al subir imagen", + "no_valid_files": "No se encontraron archivos de imagen válidos. Formatos soportados: JPG, PNG, GIF, WEBP", + "formats": "Soporta JPG, PNG, GIF, WebP (máx 10MB)" + }, + + "move": { + "failed_note": "Error al mover la nota.", + "failed_folder": "Error al mover la carpeta." + }, + + "search": { + "previous": "Anterior (Shift+F3)", + "next": "Siguiente (F3)", + "matches": "{{current}} de {{total}}" + }, + + "theme": { + "title": "Tema" + }, + + "language": { + "title": "Idioma" + }, + + "syntax_highlight": { + "title": "Resaltado de Sintaxis del Editor", + "description": "Colorear sintaxis markdown en el editor", + "enable": "Activar resaltado de sintaxis", + "disable": "Desactivar resaltado de sintaxis" + }, + + "settings": { + "account": "Cuenta", + "logout": "Cerrar sesión" + }, + + "homepage": { + "title": "Inicio", + "welcome": "Bienvenido a NoteDiscovery", + "get_started": "Crea algo para empezar", + "no_notes_title": "Aún no hay notas", + "no_notes_desc": "Crea tu primera nota o carpeta", + "folder_empty": "Esta carpeta está vacía", + "note_singular": "nota", + "note_plural": "notas", + "folder_singular": "carpeta", + "folder_plural": "carpetas" + } +} + diff --git a/locales/fr-FR.json b/locales/fr-FR.json new file mode 100644 index 0000000..c3cc8e8 --- /dev/null +++ b/locales/fr-FR.json @@ -0,0 +1,216 @@ +{ + "_meta": { + "code": "fr-FR", + "name": "Français", + "flag": "🇫🇷" + }, + + "app": { + "tagline": "Votre Base de Connaissances Auto-Hébergée" + }, + + "common": { + "save": "Enregistrer", + "cancel": "Annuler", + "delete": "Supprimer", + "rename": "Renommer", + "create": "Créer", + "close": "Fermer", + "yes": "✓ Oui", + "no": "✗ Non", + "ok": "OK", + "error": "Erreur", + "loading": "Chargement...", + "saved": "✓ Enregistré", + "saving": "Enregistrement...", + "copied": "✓ Copié !", + "failed": "Échec de {{action}}. Veuillez réessayer." + }, + + "sidebar": { + "title": "FICHIERS", + "favorites_title": "Favoris", + "folders_and_notes": "Dossiers et Notes", + "new_button": "+ Nouveau", + "new_note": "Nouvelle Note", + "new_folder": "Nouveau Dossier", + "new_from_template": "Nouveau depuis Modèle", + "search_placeholder": "Rechercher des notes...", + "search_hint": "Tapez pour rechercher vos notes", + "clear_search": "Effacer la recherche", + "drag_hint": "💡 Glisser=Déplacer", + "root_folder": "📂 Dossier racine", + "no_notes": "Pas encore de notes. Créez votre première note !", + "no_notes_yet": "Pas encore de notes ni de dossiers.", + "create_first": "Créez votre première note ou dossier !", + "no_favorites": "Pas encore de favoris", + "no_results": "Aucun résultat trouvé", + "expand_all": "Développer tous les dossiers", + "collapse_all": "Réduire tous les dossiers", + "toggle_sidebar": "Afficher/Masquer la barre latérale", + "go_to_homepage": "Aller à l'accueil", + "files": "Fichiers", + "search": "Rechercher", + "search_title": "RECHERCHE", + "settings": "Paramètres", + "settings_title": "PARAMÈTRES", + "filtered_notes": "Notes Filtrées" + }, + + "editor": { + "placeholder": "Commencez à écrire en markdown...", + "drop_hint": "💡 Déposez ici pour insérer à la position du curseur...", + "mode_edit": "Éditer", + "mode_split": "Diviser", + "mode_preview": "Aperçu", + "edited": "Modifié {{time}}", + "just_now": "à l'instant", + "minutes_ago": "il y a {{count}}m", + "hours_ago": "il y a {{count}}h", + "days_ago": "il y a {{count}}j" + }, + + "notes": { + "confirm_delete": "Supprimer \"{{name}}\" ?", + "already_exists": "Une note nommée \"{{name}}\" existe déjà à cet emplacement.\nVeuillez choisir un autre nom.", + "prompt_name": "Entrez le nom de la note :", + "prompt_name_in_folder": "Créer une note dans \"{{folder}}\".\nEntrez le nom de la note :", + "prompt_name_with_path": "Entrez le nom de la note (vous pouvez utiliser dossier/nom) :", + "prompt_rename": "Entrez le nouveau nom :", + "invalid_name": "Nom de note invalide.", + "empty_name": "Le nom de la note ne peut pas être vide.", + "not_found": "Note introuvable : {{path}}", + "no_content": "Aucun contenu à exporter", + "open_first": "Veuillez d'abord ouvrir une note avant de télécharger des images." + }, + + "folders": { + "confirm_delete": "⚠️ ATTENTION ⚠️\n\nÊtes-vous sûr de vouloir supprimer le dossier \"{{name}}\" ?\n\nCeci supprimera DÉFINITIVEMENT :\n• Toutes les notes dans ce dossier\n• Tous les sous-dossiers et leur contenu\n\nCette action est IRRÉVERSIBLE !", + "already_exists": "Un dossier nommé \"{{name}}\" existe déjà à cet emplacement.\nVeuillez choisir un autre nom.", + "prompt_name": "Entrez le nom du dossier :", + "prompt_name_in_folder": "Créer un sous-dossier dans \"{{folder}}\".\nEntrez le nom du dossier :", + "prompt_name_with_path": "Créer un nouveau dossier.\nEntrez le chemin du dossier (ex. \"Projets\" ou \"Travail/2025\") :", + "prompt_rename": "Renommer le dossier \"{{name}}\" en :", + "invalid_name": "Nom de dossier invalide.", + "cannot_move_into_self": "Impossible de déplacer un dossier dans lui-même ou dans un sous-dossier.", + "empty": "vide" + }, + + "toolbar": { + "undo": "Annuler (Ctrl+Z)", + "redo": "Rétablir (Ctrl+Y)", + "delete_note": "Supprimer la note", + "delete_image": "Supprimer l'image", + "export_html": "Exporter en HTML", + "copy_link": "Copier le lien dans le presse-papiers", + "add_favorite": "Ajouter aux favoris", + "remove_favorite": "Retirer des favoris" + }, + + "zen_mode": { + "title": "Mode Zen", + "tooltip": "Mode Zen (Ctrl+Alt+Z)", + "exit": "Quitter le Mode Zen", + "exit_hint": "Quitter le Mode Zen (Échap)" + }, + + "tags": { + "title": "Étiquettes", + "no_tags": "Aucune étiquette trouvée", + "clear_all": "Effacer les filtres d'étiquettes", + "filter_by": "Filtrer par {{tag}} ({{count}} notes)" + }, + + "stats": { + "words": "mots", + "reading_time": "~{{minutes}}m de lecture", + "links": "{{count}} liens", + "click_details": "▼ Cliquez pour les détails" + }, + + "graph": { + "title": "Vue Graphique", + "empty": "Aucune connexion à afficher", + "markdown_links": "Liens Markdown", + "click_hint": "Clic : sélectionner • Double-clic : ouvrir" + }, + + "templates": { + "title": "Modèles", + "select": "Sélectionner un modèle...", + "prompt_name": "Entrez le nom de la note :", + "no_templates": "Aucun modèle trouvé. Créez des modèles dans le dossier _templates.", + "create_failed": "Échec de la création de la note depuis le modèle", + "create_from_template": "Créer depuis un Modèle", + "select_template": "Sélectionner un Modèle", + "choose_template": "Choisir un modèle", + "note_name": "Nom de la Note", + "available_placeholders": "Variables disponibles", + "create_note": "Créer la Note" + }, + + "export": { + "failed": "Échec de l'export HTML : {{error}}" + }, + + "login": { + "title": "Connexion", + "tagline": "Votre Base de Connaissances Auto-Hébergée", + "password_placeholder": "Entrez votre mot de passe", + "unlock_button": "🔓 Déverrouiller", + "footer": "🔒 Sécurisé et Auto-Hébergé", + "error_incorrect_password": "Mot de passe incorrect. Veuillez réessayer." + }, + + "images": { + "confirm_delete": "Supprimer l'image \"{{name}}\" ?", + "upload_failed": "Échec du téléchargement de l'image", + "no_valid_files": "Aucun fichier image valide trouvé. Formats supportés : JPG, PNG, GIF, WEBP", + "formats": "Supporte JPG, PNG, GIF, WebP (max 10 Mo)" + }, + + "move": { + "failed_note": "Échec du déplacement de la note.", + "failed_folder": "Échec du déplacement du dossier." + }, + + "search": { + "previous": "Précédent (Maj+F3)", + "next": "Suivant (F3)", + "matches": "{{current}} sur {{total}}" + }, + + "theme": { + "title": "Thème" + }, + + "language": { + "title": "Langue" + }, + + "syntax_highlight": { + "title": "Coloration Syntaxique de l'Éditeur", + "description": "Coloriser la syntaxe markdown dans l'éditeur", + "enable": "Activer la coloration syntaxique", + "disable": "Désactiver la coloration syntaxique" + }, + + "settings": { + "account": "Compte", + "logout": "Déconnexion" + }, + + "homepage": { + "title": "Accueil", + "welcome": "Bienvenue sur NoteDiscovery", + "get_started": "Créez quelque chose pour commencer", + "no_notes_title": "Pas encore de notes", + "no_notes_desc": "Créez votre première note ou dossier", + "folder_empty": "Ce dossier est vide", + "note_singular": "note", + "note_plural": "notes", + "folder_singular": "dossier", + "folder_plural": "dossiers" + } +} +