commit
f54a9dd40f
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
41
README.md
41
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!
|
||||
|
||||
<p align="center">
|
||||
<a href="https://ko-fi.com/gamosoft"><img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="Buy Me a Coffee at ko-fi.com"></a>
|
||||
</p>
|
||||
|
||||
## ✨ 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:
|
||||
|
|
|
|||
|
|
@ -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('<!-- ERROR_CLASS_PLACEHOLDER -->', 'class="error"')
|
||||
content = content.replace('<!-- ERROR_MESSAGE_PLACEHOLDER -->',
|
||||
f'<div class="error-message">{error}</div>')
|
||||
else:
|
||||
content = content.replace('<!-- ERROR_CLASS_PLACEHOLDER -->', '')
|
||||
content = content.replace('<!-- ERROR_MESSAGE_PLACEHOLDER -->', '')
|
||||
# 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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -844,6 +844,12 @@
|
|||
<p>8 built-in themes with dark/light modes and full customization.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature animate-on-scroll">
|
||||
<div class="feature-icon">⭐</div>
|
||||
<h3>Favorites</h3>
|
||||
<p>Star your most-used notes for instant access from the sidebar.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature animate-on-scroll">
|
||||
<div class="feature-icon">🔌</div>
|
||||
<h3>Extensible</h3>
|
||||
|
|
|
|||
172
frontend/app.js
172
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() {
|
|||
</button>
|
||||
<span class="flex items-center gap-1 flex-1" style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; pointer-events: none;">
|
||||
<span>${folder.name}</span>
|
||||
${folder.notes.length === 0 && (!folder.children || Object.keys(folder.children).length === 0) ? '<span class="text-xs" style="color: var(--text-tertiary); font-weight: 400;">(empty)</span>' : ''}
|
||||
${folder.notes.length === 0 && (!folder.children || Object.keys(folder.children).length === 0) ? `<span class="text-xs" style="color: var(--text-tertiary); font-weight: 400;">(${this.t('folders.empty')})</span>` : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div class="hover-buttons flex gap-1 transition-opacity absolute right-2 top-1/2 transform -translate-y-1/2" style="opacity: 0; pointer-events: none; background: linear-gradient(to right, transparent, var(--bg-hover) 20%, var(--bg-hover)); padding-left: 20px;" onclick="event.stopPropagation()">
|
||||
|
|
@ -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() {
|
|||
</div>
|
||||
<div class="graph-legend-item">
|
||||
<span class="graph-legend-dot" style="background: ${mdColor};"></span>
|
||||
<span style="color: ${textColor};">Markdown links</span>
|
||||
<span style="color: ${textColor};">${this.t('graph.markdown_links')}</span>
|
||||
</div>
|
||||
<div style="margin-top: 8px; font-size: 10px; color: ${textColor}; opacity: 0.7;">
|
||||
Click: select • Double-click: open
|
||||
${this.t('graph.click_hint')}
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(legend);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,35 @@
|
|||
<!-- Tailwind CSS from CDN (v3.4.17) -->
|
||||
<script src="https://cdn.tailwindcss.com/3.4.17"></script>
|
||||
|
||||
<!-- Preload translations before Alpine initializes -->
|
||||
<script>
|
||||
(function() {
|
||||
// Load translations synchronously to ensure they're available before Alpine renders
|
||||
const locale = localStorage.getItem('locale') || 'en-US';
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', '/api/locales/' + locale, false); // Synchronous request
|
||||
try {
|
||||
xhr.send();
|
||||
if (xhr.status === 200) {
|
||||
window.__preloadedTranslations = JSON.parse(xhr.responseText);
|
||||
} else {
|
||||
// Fallback to en-US if preferred locale fails
|
||||
if (locale !== 'en-US') {
|
||||
xhr.open('GET', '/api/locales/en-US', false);
|
||||
xhr.send();
|
||||
if (xhr.status === 200) {
|
||||
window.__preloadedTranslations = JSON.parse(xhr.responseText);
|
||||
localStorage.setItem('locale', 'en-US');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to preload translations:', e);
|
||||
window.__preloadedTranslations = {};
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Alpine.js (v3.14.1) -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/cdn.min.js"></script>
|
||||
|
||||
|
|
@ -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')"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
|
|
@ -1119,7 +1148,7 @@
|
|||
alt="NoteDiscovery"
|
||||
class="icon-rail-logo"
|
||||
@click="goHome()"
|
||||
title="Go to homepage"
|
||||
:title="t('sidebar.go_to_homepage')"
|
||||
>
|
||||
|
||||
<!-- Files -->
|
||||
|
|
@ -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"
|
||||
>
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -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"
|
||||
>
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -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"
|
||||
>
|
||||
<svg fill="currentColor" viewBox="0 0 20 20">
|
||||
|
|
@ -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"
|
||||
>
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -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"
|
||||
>
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -1214,7 +1243,7 @@
|
|||
<div x-show="activePanel === 'files'" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" class="flex flex-col h-full">
|
||||
<!-- Header with + New -->
|
||||
<div class="flex-shrink-0 px-2 py-2 border-b flex items-center justify-between" style="border-color: var(--border-primary);">
|
||||
<span class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-tertiary);">Files</span>
|
||||
<span class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-tertiary);" x-text="t('sidebar.files')"></span>
|
||||
<button
|
||||
@click="dropdownTargetFolder = ''; toggleNewDropdown($event)"
|
||||
class="px-2 py-1 text-xs font-medium text-white rounded focus:outline-none flex items-center gap-1"
|
||||
|
|
@ -1242,10 +1271,7 @@
|
|||
@mouseleave="$el.style.backgroundColor = 'transparent'"
|
||||
>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24" style="color: var(--warning, #eab308);">
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"></path>
|
||||
</svg>
|
||||
<span class="text-xs font-semibold uppercase tracking-wide">Favorites</span>
|
||||
<span class="text-xs font-semibold uppercase tracking-wide" x-text="t('sidebar.favorites_title')"></span>
|
||||
</div>
|
||||
<svg
|
||||
class="w-3 h-3 transition-transform"
|
||||
|
|
@ -1275,7 +1301,7 @@
|
|||
<button
|
||||
@click.stop="toggleFavorite(fav.path)"
|
||||
class="opacity-0 group-hover:opacity-100 p-0.5 rounded transition-opacity"
|
||||
title="Remove from favorites"
|
||||
:title="t('toolbar.remove_favorite')"
|
||||
@mouseenter="$el.style.backgroundColor = 'var(--bg-secondary)'"
|
||||
@mouseleave="$el.style.backgroundColor = 'transparent'"
|
||||
>
|
||||
|
|
@ -1290,19 +1316,19 @@
|
|||
|
||||
<div class="text-xs px-2 py-1 mb-2" style="color: var(--text-tertiary);">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span>Folders & Notes</span>
|
||||
<span x-text="t('sidebar.folders_and_notes')"></span>
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
@click="expandAllFolders()"
|
||||
class="text-xs px-2 py-0.5 rounded hover:bg-opacity-20"
|
||||
style="color: var(--text-secondary);"
|
||||
title="Expand all folders"
|
||||
:title="t('sidebar.expand_all')"
|
||||
>⊞</button>
|
||||
<button
|
||||
@click="collapseAllFolders()"
|
||||
class="text-xs px-2 py-0.5 rounded hover:bg-opacity-20"
|
||||
style="color: var(--text-secondary);"
|
||||
title="Collapse all folders"
|
||||
:title="t('sidebar.collapse_all')"
|
||||
>⊟</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1326,8 +1352,8 @@
|
|||
@drop="if(draggedNote || draggedFolder) onFolderDrop('')"
|
||||
style="height: 28px; line-height: 20px; display: flex; align-items: center; justify-content: center;"
|
||||
>
|
||||
<span x-show="!draggedNote && !draggedFolder && !draggedItem">💡 Drag=Move </span>
|
||||
<span x-show="draggedNote || draggedFolder">📂 Root folder</span>
|
||||
<span x-show="!draggedNote && !draggedFolder && !draggedItem" x-text="t('sidebar.drag_hint')"></span>
|
||||
<span x-show="draggedNote || draggedFolder" x-text="t('sidebar.root_folder')"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -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')"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||
|
|
@ -1368,7 +1394,8 @@
|
|||
<!-- Empty state -->
|
||||
<template x-if="notes.length === 0 && allFolders.length === 0">
|
||||
<div class="px-3 py-4 text-sm text-center" style="color: var(--text-tertiary);">
|
||||
No notes or folders yet.<br>Create your first note or folder!
|
||||
<span x-text="t('sidebar.no_notes_yet')"></span><br>
|
||||
<span x-text="t('sidebar.create_first')"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
|
@ -1379,7 +1406,7 @@
|
|||
<div x-show="activePanel === 'search'" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" class="flex flex-col h-full">
|
||||
<!-- Search Header -->
|
||||
<div class="flex-shrink-0 px-3 py-2 border-b" style="border-color: var(--border-primary);">
|
||||
<span class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-tertiary);">Search</span>
|
||||
<span class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-tertiary);" x-text="t('sidebar.search_title')"></span>
|
||||
</div>
|
||||
|
||||
<!-- Search Input -->
|
||||
|
|
@ -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')"
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
|
|
@ -1417,10 +1444,10 @@
|
|||
>
|
||||
<span x-text="`Match ${currentMatchIndex + 1} of ${totalMatches}`"></span>
|
||||
<div class="flex gap-1">
|
||||
<button @click="previousMatch()" class="px-2 py-1 rounded hover:bg-opacity-80" style="background-color: var(--bg-primary); color: var(--text-primary);" title="Previous (Shift+F3)">
|
||||
<button @click="previousMatch()" class="px-2 py-1 rounded hover:bg-opacity-80" style="background-color: var(--bg-primary); color: var(--text-primary);" :title="t('search.previous')">
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
|
||||
</button>
|
||||
<button @click="nextMatch()" class="px-2 py-1 rounded hover:bg-opacity-80" style="background-color: var(--bg-primary); color: var(--text-primary);" title="Next (F3)">
|
||||
<button @click="nextMatch()" class="px-2 py-1 rounded hover:bg-opacity-80" style="background-color: var(--bg-primary); color: var(--text-primary);" :title="t('search.next')">
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -1461,7 +1488,7 @@
|
|||
<svg class="w-12 h-12 mx-auto mb-3 opacity-40" style="color: var(--text-tertiary);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
<p class="text-sm" style="color: var(--text-tertiary);">Type to search your notes</p>
|
||||
<p class="text-sm" style="color: var(--text-tertiary);" x-text="t('sidebar.search_hint')"></p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
|
@ -1472,7 +1499,7 @@
|
|||
<div x-show="activePanel === 'tags'" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" class="flex flex-col h-full">
|
||||
<!-- Tags Header -->
|
||||
<div class="flex-shrink-0 px-3 py-2 border-b flex items-center justify-between" style="border-color: var(--border-primary);">
|
||||
<span class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-tertiary);">Tags</span>
|
||||
<span class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-tertiary);" x-text="t('tags.title')"></span>
|
||||
<span class="text-xs px-1.5 py-0.5 rounded" style="background-color: var(--bg-tertiary); color: var(--text-tertiary);" x-text="Object.keys(allTags).length"></span>
|
||||
</div>
|
||||
|
||||
|
|
@ -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')"
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
|
|
@ -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})"
|
||||
>
|
||||
<span x-text="tag"></span>
|
||||
<span class="opacity-75" x-text="` (${count})`"></span>
|
||||
|
|
@ -1527,7 +1554,7 @@
|
|||
<template x-if="selectedTags.length > 0">
|
||||
<div class="border-t" style="border-color: var(--border-primary);">
|
||||
<div class="px-3 py-2 text-xs font-semibold uppercase tracking-wide" style="color: var(--text-tertiary);">
|
||||
Filtered Notes (<span x-text="searchResults.length"></span>)
|
||||
<span x-text="t('sidebar.filtered_notes')"></span> (<span x-text="searchResults.length"></span>)
|
||||
</div>
|
||||
<div>
|
||||
<template x-for="note in searchResults" :key="note.path">
|
||||
|
|
@ -1556,7 +1583,7 @@
|
|||
<div class="flex flex-col h-full">
|
||||
<!-- Settings Header -->
|
||||
<div class="flex-shrink-0 px-3 py-2 border-b" style="border-color: var(--border-primary);">
|
||||
<span class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-tertiary);">Settings</span>
|
||||
<span class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-tertiary);" x-text="t('sidebar.settings_title')"></span>
|
||||
</div>
|
||||
|
||||
<!-- Scrollable content -->
|
||||
|
|
@ -1564,7 +1591,7 @@
|
|||
<div class="p-3">
|
||||
<!-- Theme -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-xs font-medium mb-2" style="color: var(--text-secondary);">Theme</label>
|
||||
<label class="block text-xs font-medium mb-2" style="color: var(--text-secondary);" x-text="t('theme.title')">Theme</label>
|
||||
<select
|
||||
x-model="currentTheme"
|
||||
@change="setTheme(currentTheme)"
|
||||
|
|
@ -1577,10 +1604,25 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Language -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-xs font-medium mb-2" style="color: var(--text-secondary);" x-text="t('language.title')">Language</label>
|
||||
<select
|
||||
x-model="currentLocale"
|
||||
@change="changeLocale(currentLocale)"
|
||||
class="w-full px-2 py-2 text-sm rounded focus:outline-none focus:ring-2 focus:ring-opacity-50"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-primary); cursor: pointer;"
|
||||
>
|
||||
<template x-for="loc in availableLocales" :key="loc.code">
|
||||
<option :value="loc.code" :selected="loc.code === currentLocale" x-text="loc.flag + ' ' + loc.name"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Syntax Highlighting Toggle -->
|
||||
<div class="mb-4">
|
||||
<label class="flex items-center justify-between cursor-pointer">
|
||||
<span class="text-xs font-medium" style="color: var(--text-secondary);">Editor Syntax Highlight</span>
|
||||
<span class="text-xs font-medium" style="color: var(--text-secondary);" x-text="t('syntax_highlight.title')"></span>
|
||||
<div
|
||||
@click="toggleSyntaxHighlight()"
|
||||
class="relative w-10 h-5 rounded-full transition-colors cursor-pointer"
|
||||
|
|
@ -1592,20 +1634,20 @@
|
|||
></div>
|
||||
</div>
|
||||
</label>
|
||||
<p class="text-xs mt-1" style="color: var(--text-tertiary);">Colorize markdown syntax in editor</p>
|
||||
<p class="text-xs mt-1" style="color: var(--text-tertiary);" x-text="t('syntax_highlight.description')"></p>
|
||||
</div>
|
||||
|
||||
<!-- Logout (if auth enabled) - uses authEnabled from main app state -->
|
||||
<div x-show="authEnabled" class="mb-4">
|
||||
<label class="block text-xs font-medium mb-2" style="color: var(--text-secondary);">Account</label>
|
||||
<label class="block text-xs font-medium mb-2" style="color: var(--text-secondary);" x-text="t('settings.account')"></label>
|
||||
<a
|
||||
href="/logout"
|
||||
class="flex items-center justify-center gap-2 px-3 py-2 text-sm rounded transition-colors"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-primary);"
|
||||
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
|
||||
onmouseout="this.style.backgroundColor='var(--bg-primary)'"
|
||||
x-text="'🔒 ' + t('settings.logout')"
|
||||
>
|
||||
🔒 Logout
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1613,7 +1655,7 @@
|
|||
<!-- Footer Info (inside scroll area) -->
|
||||
<div class="p-3 mt-4 border-t text-center" style="border-color: var(--border-primary);">
|
||||
<div class="text-xs" style="color: var(--text-tertiary);">
|
||||
<span x-text="notes.length"></span> notes
|
||||
<span x-text="notes.length"></span> <span x-text="notes.length === 1 ? t('homepage.note_singular') : t('homepage.note_plural')"></span>
|
||||
<span class="mx-1">·</span>
|
||||
<span x-text="'v' + appVersion"></span>
|
||||
</div>
|
||||
|
|
@ -1688,7 +1730,7 @@
|
|||
style="color: var(--text-primary);"
|
||||
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
|
||||
onmouseout="this.style.backgroundColor='transparent'"
|
||||
title="Toggle sidebar"
|
||||
:title="t('sidebar.toggle_sidebar')"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
|
|
@ -1702,8 +1744,8 @@
|
|||
<svg class="mx-auto h-24 w-24 mb-4" style="color: var(--text-tertiary); opacity: 0.5;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary);">No notes yet</h2>
|
||||
<p class="mb-6" style="color: var(--text-secondary);">Create your first note or folder</p>
|
||||
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary);" x-text="t('homepage.no_notes_title')"></h2>
|
||||
<p class="mb-6" style="color: var(--text-secondary);" x-text="t('homepage.no_notes_desc')"></p>
|
||||
<button
|
||||
@click="dropdownTargetFolder = selectedHomepageFolder; toggleNewDropdown($event)"
|
||||
class="px-6 py-3 text-sm font-medium text-white rounded-lg transition-colors"
|
||||
|
|
@ -1711,7 +1753,7 @@
|
|||
onmouseover="this.style.backgroundColor='var(--accent-hover)'"
|
||||
onmouseout="this.style.backgroundColor='var(--accent-primary)'"
|
||||
>
|
||||
+ New
|
||||
<span x-text="t('sidebar.new_button')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1722,7 +1764,7 @@
|
|||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary);" x-text="appName"></h1>
|
||||
<p class="text-lg" style="color: var(--text-secondary);" x-text="appTagline"></p>
|
||||
<p class="text-lg" style="color: var(--text-secondary);" x-text="t('app.tagline')"></p>
|
||||
<!-- Demo Mode Warning Banner -->
|
||||
<div x-data="{ showDemoBanner: false }" x-init="fetch('/api/config').then(r => r.json()).then(d => showDemoBanner = d.demoMode || false)" x-show="showDemoBanner" class="mt-4 flex items-center px-4 py-2.5 text-sm rounded-lg" style="background: linear-gradient(135deg, #ff6b6b 0%, #ff8e53 100%); color: white; font-weight: 500;">
|
||||
<svg class="w-5 h-5 mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -1759,11 +1801,11 @@
|
|||
<div class="flex flex-wrap items-center gap-4 text-sm" style="color: var(--text-secondary);">
|
||||
<span class="font-semibold" style="color: var(--text-primary);">
|
||||
<span x-text="homepageNotes().length"></span>
|
||||
<span x-text="homepageNotes().length === 1 ? 'note' : 'notes'"></span>
|
||||
<span x-text="homepageNotes().length === 1 ? t('homepage.note_singular') : t('homepage.note_plural')"></span>
|
||||
</span>
|
||||
<span x-show="homepageFolders().length > 0">
|
||||
<span x-text="homepageFolders().length"></span>
|
||||
<span x-text="homepageFolders().length === 1 ? 'folder' : 'folders'"></span>
|
||||
<span x-text="homepageFolders().length === 1 ? t('homepage.folder_singular') : t('homepage.folder_plural')"></span>
|
||||
</span>
|
||||
<button
|
||||
@click="dropdownTargetFolder = selectedHomepageFolder; toggleNewDropdown($event)"
|
||||
|
|
@ -1772,7 +1814,7 @@
|
|||
onmouseover="this.style.backgroundColor='var(--accent-hover)'"
|
||||
onmouseout="this.style.backgroundColor='var(--accent-primary)'"
|
||||
>
|
||||
+ New
|
||||
<span x-text="t('sidebar.new_button')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1784,8 +1826,8 @@
|
|||
<svg class="mx-auto h-24 w-24 mb-4" style="color: var(--text-tertiary); opacity: 0.5;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary);" x-text="selectedHomepageFolder ? 'This folder is empty' : 'No notes yet'"></h2>
|
||||
<p class="mb-6" style="color: var(--text-secondary);" x-text="selectedHomepageFolder ? 'Create something to get started' : 'Create your first note or folder'"></p>
|
||||
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary);" x-text="selectedHomepageFolder ? t('homepage.folder_empty') : t('homepage.no_notes_title')"></h2>
|
||||
<p class="mb-6" style="color: var(--text-secondary);" x-text="selectedHomepageFolder ? t('homepage.get_started') : t('homepage.no_notes_desc')"></p>
|
||||
<button
|
||||
@click="dropdownTargetFolder = selectedHomepageFolder; toggleNewDropdown($event)"
|
||||
class="px-6 py-3 text-sm font-medium text-white rounded-lg transition-colors"
|
||||
|
|
@ -1793,7 +1835,7 @@
|
|||
onmouseover="this.style.backgroundColor='var(--accent-hover)'"
|
||||
onmouseout="this.style.backgroundColor='var(--accent-primary)'"
|
||||
>
|
||||
+ New
|
||||
<span x-text="t('sidebar.new_button')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -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')"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||
|
|
@ -1834,7 +1876,7 @@
|
|||
|
||||
<!-- Folder Metadata -->
|
||||
<div class="flex items-center justify-between text-xs mt-auto pt-2" style="color: var(--text-tertiary);">
|
||||
<span x-text="folder.noteCount + ' ' + (folder.noteCount === 1 ? 'note' : 'notes')"></span>
|
||||
<span x-text="folder.noteCount + ' ' + (folder.noteCount === 1 ? t('homepage.note_singular') : t('homepage.note_plural'))"></span>
|
||||
<svg class="w-4 h-4" style="color: var(--accent-primary);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
|
|
@ -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')"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||
|
|
@ -1870,7 +1912,7 @@
|
|||
|
||||
<!-- Metadata -->
|
||||
<div class="flex items-center justify-between text-xs mt-auto pt-2" style="color: var(--text-tertiary);">
|
||||
<span x-text="new Date(note.modified).toLocaleDateString()"></span>
|
||||
<span x-text="formatDate(note.modified)"></span>
|
||||
<span x-text="formatSize(note.size)"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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')"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
|
|
@ -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')"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"></path>
|
||||
|
|
@ -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')"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 10h-10a8 8 0 00-8 8v2m18-10l-6 6m6-6l-6-6"></path>
|
||||
|
|
@ -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')"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<span x-show="isSaving" class="text-xs" style="color: var(--text-secondary);">Saving...</span>
|
||||
<span x-show="!isSaving && lastSaved" class="text-xs" style="color: var(--success);">✓ Saved</span>
|
||||
<span x-show="!isSaving && !lastSaved && lastEditedText" class="text-xs hidden md:inline" style="color: var(--text-tertiary);" x-text="'Edited ' + lastEditedText"></span>
|
||||
<span x-show="isSaving" class="text-xs" style="color: var(--text-secondary);" x-text="t('common.saving')"></span>
|
||||
<span x-show="!isSaving && lastSaved" class="text-xs" style="color: var(--success);" x-text="t('common.saved')"></span>
|
||||
<span x-show="!isSaving && !lastSaved && lastEditedText" class="text-xs hidden md:inline" style="color: var(--text-tertiary);" x-text="t('editor.edited', {time: lastEditedText})"></span>
|
||||
</div>
|
||||
|
||||
<!-- Demo Mode Warning Banner -->
|
||||
|
|
@ -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
|
||||
</button>
|
||||
<button
|
||||
@click="viewMode = 'split'"
|
||||
|
|
@ -1991,8 +2033,8 @@
|
|||
:style="viewMode === 'split' ? 'background-color: var(--accent-primary); color: white;' : 'color: var(--text-secondary);'"
|
||||
@mouseenter="if (viewMode !== 'split') $el.style.backgroundColor = 'var(--bg-secondary)'"
|
||||
@mouseleave="if (viewMode !== 'split') $el.style.backgroundColor = 'transparent'"
|
||||
x-text="t('editor.mode_split')"
|
||||
>
|
||||
Split
|
||||
</button>
|
||||
<button
|
||||
@click="viewMode = 'preview'"
|
||||
|
|
@ -2000,8 +2042,8 @@
|
|||
:style="viewMode === 'preview' ? 'background-color: var(--accent-primary); color: white;' : 'color: var(--text-secondary);'"
|
||||
@mouseenter="if (viewMode !== 'preview') $el.style.backgroundColor = 'var(--bg-secondary)'"
|
||||
@mouseleave="if (viewMode !== 'preview') $el.style.backgroundColor = 'transparent'"
|
||||
x-text="t('editor.mode_preview')"
|
||||
>
|
||||
Preview
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -2014,7 +2056,7 @@
|
|||
<button
|
||||
@click="toggleFavorite()"
|
||||
class="p-2 transition"
|
||||
:title="isFavorite(currentNote) ? 'Remove from favorites' : 'Add to favorites'"
|
||||
:title="isFavorite(currentNote) ? t('toolbar.remove_favorite') : t('toolbar.add_favorite')"
|
||||
:style="'color: ' + (isFavorite(currentNote) ? 'var(--warning, #eab308)' : 'var(--text-secondary)') + ';'"
|
||||
@mouseenter="$el.style.backgroundColor = 'var(--bg-secondary)'"
|
||||
@mouseleave="$el.style.backgroundColor = 'transparent'"
|
||||
|
|
@ -2033,7 +2075,7 @@
|
|||
@click="exportToHTML()"
|
||||
class="p-2 transition"
|
||||
style="color: var(--text-secondary);"
|
||||
title="Export as HTML"
|
||||
:title="t('toolbar.export_html')"
|
||||
@mouseenter="$el.style.backgroundColor = 'var(--bg-secondary)'"
|
||||
@mouseleave="$el.style.backgroundColor = 'transparent'"
|
||||
>
|
||||
|
|
@ -2045,7 +2087,7 @@
|
|||
<button
|
||||
@click="copyNoteLink()"
|
||||
class="p-2 transition hover:bg-opacity-80"
|
||||
:title="linkCopied ? 'Copied!' : 'Copy link to clipboard'"
|
||||
:title="linkCopied ? t('common.copied') : t('toolbar.copy_link')"
|
||||
:style="'color: ' + (linkCopied ? 'var(--success, #22c55e)' : 'var(--text-secondary)') + ';'"
|
||||
@mouseenter="$el.style.backgroundColor = 'var(--bg-secondary)'"
|
||||
@mouseleave="$el.style.backgroundColor = 'transparent'"
|
||||
|
|
@ -2064,7 +2106,7 @@
|
|||
@click="toggleZenMode()"
|
||||
class="p-2 transition"
|
||||
style="color: var(--text-secondary);"
|
||||
title="Zen Mode (Ctrl+Alt+Z)"
|
||||
:title="t('zen_mode.tooltip')"
|
||||
@mouseenter="$el.style.backgroundColor = 'var(--bg-secondary)'"
|
||||
@mouseleave="$el.style.backgroundColor = 'transparent'"
|
||||
>
|
||||
|
|
@ -2108,7 +2150,7 @@
|
|||
@paste="handlePaste($event)"
|
||||
:class="'flex-1 w-full resize-none focus:outline-none editor-textarea' + (draggedItem && dropTarget === 'editor' ? ' link-drop-target' : '')"
|
||||
:style="'background-color: var(--bg-primary); color: var(--text-primary);' + (draggedItem && dropTarget === 'editor' ? ' outline: 2px dashed var(--accent-primary); outline-offset: -2px; box-shadow: 0 0 20px var(--accent-light);' : '')"
|
||||
:placeholder="draggedItem && dropTarget === 'editor' ? '💡 Drop here to insert at cursor position...' : 'Start writing in markdown...'"
|
||||
:placeholder="draggedItem && dropTarget === 'editor' ? t('editor.drop_hint') : t('editor.placeholder')"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -2431,7 +2473,7 @@
|
|||
onmouseout="this.style.backgroundColor='transparent'"
|
||||
>
|
||||
<span class="text-lg">📝</span>
|
||||
<span>New Note</span>
|
||||
<span x-text="t('sidebar.new_note')"></span>
|
||||
</button>
|
||||
<button
|
||||
@click="createFolder()"
|
||||
|
|
@ -2441,7 +2483,7 @@
|
|||
onmouseout="this.style.backgroundColor='transparent'"
|
||||
>
|
||||
<span class="text-lg">📁</span>
|
||||
<span>New Folder</span>
|
||||
<span x-text="t('sidebar.new_folder')"></span>
|
||||
</button>
|
||||
<button
|
||||
@click.stop="showTemplateModal = true; showNewDropdown = false; mobileSidebarOpen = false"
|
||||
|
|
@ -2451,7 +2493,7 @@
|
|||
onmouseout="this.style.backgroundColor='transparent'"
|
||||
>
|
||||
<span class="text-lg">📄</span>
|
||||
<span>New from Template</span>
|
||||
<span x-text="t('sidebar.new_from_template')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -2467,19 +2509,18 @@
|
|||
style="background-color: var(--bg-secondary); color: var(--text-primary);"
|
||||
@click.stop>
|
||||
|
||||
<h2 class="text-xl font-bold mb-4" style="color: var(--text-primary);">Create from Template</h2>
|
||||
<h2 class="text-xl font-bold mb-4" style="color: var(--text-primary);" x-text="t('templates.create_from_template')"></h2>
|
||||
|
||||
<!-- Template Selection -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);">
|
||||
Select Template
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);" x-text="t('templates.select_template')">
|
||||
</label>
|
||||
<select
|
||||
x-model="selectedTemplate"
|
||||
class="w-full px-3 py-2 rounded border"
|
||||
style="background-color: var(--bg-tertiary); color: var(--text-primary); border-color: var(--border-primary);"
|
||||
>
|
||||
<option value="">-- Choose a template --</option>
|
||||
<option value="" x-text="'-- ' + t('templates.choose_template') + ' --'"></option>
|
||||
<template x-for="template in availableTemplates" :key="template.name">
|
||||
<option :value="template.name" x-text="template.name"></option>
|
||||
</template>
|
||||
|
|
@ -2488,13 +2529,12 @@
|
|||
|
||||
<!-- Note Name Input -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);">
|
||||
Note Name
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);" x-text="t('templates.note_name')">
|
||||
</label>
|
||||
<input
|
||||
x-model="newTemplateNoteName"
|
||||
type="text"
|
||||
placeholder="Enter note name"
|
||||
:placeholder="t('notes.prompt_name')"
|
||||
class="w-full px-3 py-2 rounded border"
|
||||
style="background-color: var(--bg-tertiary); color: var(--text-primary); border-color: var(--border-primary);"
|
||||
@keydown.enter="createNoteFromTemplate()"
|
||||
|
|
@ -2503,7 +2543,7 @@
|
|||
|
||||
<!-- Info about placeholders -->
|
||||
<div class="mb-4 text-xs p-3 rounded" style="background-color: var(--bg-tertiary); color: var(--text-secondary);">
|
||||
<strong>Available placeholders:</strong><br>
|
||||
<strong x-text="t('templates.available_placeholders') + ':'"></strong><br>
|
||||
<code style="font-family: monospace;">{{date}}</code>,
|
||||
<code style="font-family: monospace;">{{time}}</code>,
|
||||
<code style="font-family: monospace;">{{datetime}}</code>,
|
||||
|
|
@ -2512,8 +2552,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Empty state when no templates available -->
|
||||
<div x-show="availableTemplates.length === 0" class="mb-4 text-sm p-3 rounded" style="background-color: var(--bg-tertiary); color: var(--text-secondary);">
|
||||
No templates found. Create templates in the <code style="font-family: monospace;">_templates</code> folder.
|
||||
<div x-show="availableTemplates.length === 0" class="mb-4 text-sm p-3 rounded" style="background-color: var(--bg-tertiary); color: var(--text-secondary);" x-text="t('templates.no_templates')">
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
|
|
@ -2526,8 +2565,8 @@
|
|||
:style="(!selectedTemplate || !newTemplateNoteName.trim() || availableTemplates.length === 0) ? 'opacity: 0.5; cursor: not-allowed;' : ''"
|
||||
onmouseover="if(!this.disabled) this.style.backgroundColor='var(--accent-hover)'"
|
||||
onmouseout="if(!this.disabled) this.style.backgroundColor='var(--accent-primary)'"
|
||||
x-text="t('templates.create_note')"
|
||||
>
|
||||
Create Note
|
||||
</button>
|
||||
<button
|
||||
@click="showTemplateModal = false; selectedTemplate = ''; newTemplateNoteName = ''"
|
||||
|
|
@ -2535,8 +2574,8 @@
|
|||
style="background-color: var(--bg-tertiary); color: var(--text-primary);"
|
||||
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
|
||||
onmouseout="this.style.backgroundColor='var(--bg-tertiary)'"
|
||||
x-text="t('common.cancel')"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,40 @@
|
|||
<title>Login - NoteDiscovery</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||
|
||||
<!-- Preload translations (same approach as index.html) -->
|
||||
<script>
|
||||
(function() {
|
||||
const locale = localStorage.getItem('locale') || 'en-US';
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', '/api/locales/' + locale, false);
|
||||
try {
|
||||
xhr.send();
|
||||
if (xhr.status === 200) {
|
||||
window.__translations = JSON.parse(xhr.responseText);
|
||||
} else if (locale !== 'en-US') {
|
||||
xhr.open('GET', '/api/locales/en-US', false);
|
||||
xhr.send();
|
||||
if (xhr.status === 200) {
|
||||
window.__translations = JSON.parse(xhr.responseText);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to preload translations:', e);
|
||||
window.__translations = {};
|
||||
}
|
||||
|
||||
// Simple translation function
|
||||
window.t = function(key) {
|
||||
const keys = key.split('.');
|
||||
let value = window.__translations;
|
||||
for (const k of keys) {
|
||||
value = value?.[k];
|
||||
}
|
||||
return typeof value === 'string' ? value : key;
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Theme styles will be loaded dynamically -->
|
||||
<script>
|
||||
// Load theme immediately (same approach as index.html)
|
||||
|
|
@ -191,7 +225,7 @@
|
|||
<div class="login-container">
|
||||
<img src="/static/logo.svg" alt="NoteDiscovery" class="logo">
|
||||
<h1>NoteDiscovery</h1>
|
||||
<p class="tagline">Your Self-Hosted Knowledge Base</p>
|
||||
<p class="tagline" id="tagline"></p>
|
||||
|
||||
<form method="post" action="/login">
|
||||
<div>
|
||||
|
|
@ -199,18 +233,16 @@
|
|||
type="password"
|
||||
name="password"
|
||||
id="password"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
autofocus
|
||||
<!-- ERROR_CLASS_PLACEHOLDER -->
|
||||
>
|
||||
<!-- ERROR_MESSAGE_PLACEHOLDER -->
|
||||
<div class="error-message" id="error-message" style="display: none;"></div>
|
||||
</div>
|
||||
<button type="submit">🔓 Unlock</button>
|
||||
<button type="submit" id="submit-btn"></button>
|
||||
</form>
|
||||
|
||||
<div class="footer">
|
||||
🔒 Secure & Self-Hosted
|
||||
<span id="footer-text"></span>
|
||||
<div style="margin-top: 8px; font-size: 12px; opacity: 0.6;">
|
||||
<a href="https://www.notediscovery.com" target="_blank" rel="noopener noreferrer" style="color: inherit; text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
|
||||
<svg style="width: 14px; height: 14px;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -221,6 +253,26 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Apply translations after DOM is ready
|
||||
document.getElementById('tagline').textContent = t('login.tagline');
|
||||
document.getElementById('password').placeholder = t('login.password_placeholder');
|
||||
document.getElementById('submit-btn').textContent = t('login.unlock_button');
|
||||
document.getElementById('footer-text').textContent = t('login.footer');
|
||||
document.title = t('login.title') + ' - NoteDiscovery';
|
||||
|
||||
// Handle error from URL params
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
var errorCode = urlParams.get('error');
|
||||
if (errorCode) {
|
||||
var errorEl = document.getElementById('error-message');
|
||||
var passwordEl = document.getElementById('password');
|
||||
errorEl.textContent = t('login.error_' + errorCode);
|
||||
errorEl.style.display = 'flex';
|
||||
passwordEl.classList.add('error');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue