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
+
+
+
+
+
+
+
> 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!
+
+
+
+
+
## ✨ 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() {