diff --git a/backend/export.py b/backend/export.py
index 0a0bd8f..4feb441 100644
--- a/backend/export.py
+++ b/backend/export.py
@@ -49,7 +49,7 @@ def get_media_as_base64(media_path: Path) -> Optional[Tuple[str, str]]:
def get_image_as_base64(image_path: Path) -> Optional[str]:
"""Read an image file and return it as a base64 data URL."""
result = get_media_as_base64(image_path)
- if result and result[1] == 'image':
+ if result and result[1] in ('image', 'drawing'):
return result[0]
return None
diff --git a/backend/main.py b/backend/main.py
index 6499f8b..bbb0b2a 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -625,9 +625,59 @@ async def get_media(media_path: str):
raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to load media file"))
+@api_router.put("/media/{media_path:path}", tags=["Media"])
+@limiter.limit("30/minute")
+async def put_media(media_path: str, request: Request):
+ """
+ Overwrite an existing media file in place (used for saving drawing PNGs).
+ Only files named drawing-*.png are accepted to avoid accidental overwrites.
+ """
+ try:
+ from backend.utils import ALL_MEDIA_EXTENSIONS
+
+ notes_dir = config['storage']['notes_dir']
+ full_path = Path(notes_dir) / media_path
+
+ if not validate_path_security(notes_dir, full_path):
+ raise HTTPException(status_code=403, detail="Access denied")
+
+ name_lower = full_path.name.lower()
+ if not (name_lower.startswith('drawing-') and name_lower.endswith('.png')):
+ raise HTTPException(
+ status_code=400,
+ detail="Only drawing PNG files (drawing-*.png) can be updated in place",
+ )
+
+ if full_path.suffix.lower() not in ALL_MEDIA_EXTENSIONS:
+ raise HTTPException(status_code=400, detail="Not an allowed media file")
+
+ if not full_path.exists() or not full_path.is_file():
+ raise HTTPException(status_code=404, detail="File not found")
+
+ body = await request.body()
+ max_size = UPLOAD_MAX_IMAGE_MB * 1024 * 1024
+ if len(body) > max_size:
+ raise HTTPException(
+ status_code=400,
+ detail=f"File too large. Maximum size: {UPLOAD_MAX_IMAGE_MB}MB",
+ )
+
+ try:
+ with open(full_path, 'wb') as f:
+ f.write(body)
+ except OSError as e:
+ raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to save file"))
+
+ return {"success": True, "path": media_path}
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to update media file"))
+
+
@api_router.post("/upload-media", tags=["Media"])
@limiter.limit("20/minute")
-async def upload_media(request: Request, file: UploadFile = File(...), note_path: str = Form(...)):
+async def upload_media(request: Request, file: UploadFile = File(...), note_path: str = Form("")):
"""
Upload a media file (image, audio, video, PDF) and save it to the attachments directory.
Returns the relative path for markdown linking.
diff --git a/backend/utils.py b/backend/utils.py
index 26629e0..e639c93 100644
--- a/backend/utils.py
+++ b/backend/utils.py
@@ -671,8 +671,13 @@ ALL_MEDIA_EXTENSIONS = set().union(*MEDIA_EXTENSIONS.values())
def get_media_type(filename: str) -> Optional[str]:
"""
Determine the media type based on file extension.
- Returns: 'image', 'audio', 'video', 'document', or None if not a media file.
+ Returns: 'image', 'audio', 'video', 'document', 'drawing', or None if not a media file.
+
+ Drawings are PNG files stored like images but named drawing-*.png (editable canvas in the app).
"""
+ name_lower = Path(filename).name.lower()
+ if name_lower.startswith('drawing-') and name_lower.endswith('.png'):
+ return 'drawing'
ext = Path(filename).suffix.lower()
for media_type, extensions in MEDIA_EXTENSIONS.items():
if ext in extensions:
diff --git a/frontend/app.js b/frontend/app.js
index d171759..b27d0b1 100644
--- a/frontend/app.js
+++ b/frontend/app.js
@@ -494,7 +494,16 @@ function noteApp() {
// Media viewer state
currentMedia: '', // Path to current media file (kept as 'currentMedia' for compatibility)
- currentMediaType: 'image', // 'image', 'audio', 'video', 'document'
+ currentMediaType: 'image', // 'image', 'audio', 'video', 'document', 'drawing'
+
+ // Drawing canvas (drawing-*.png only) — ops are session-only until Save flattens to PNG
+ drawingTool: 'freehand',
+ drawingColor: '#1a1a1a',
+ drawingLineWidth: 4,
+ drawingOps: [],
+ drawingRedoStack: [],
+ drawingDraft: null,
+ drawingIsPointerDown: false,
// DOM element cache (to avoid repeated querySelector calls)
_domCache: {
@@ -638,10 +647,15 @@ function noteApp() {
window.addEventListener('keydown', (e) => {
// Use e.key (not e.code) for letter keys to support non-QWERTY keyboard layouts
- // Ctrl/Cmd + S to save
+ // Ctrl/Cmd + S to save (drawing saves PNG; notes save markdown)
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') {
- e.preventDefault();
- this.saveNote();
+ if (this.currentMedia && this.currentMediaType === 'drawing') {
+ e.preventDefault();
+ this.drawingSave();
+ } else {
+ e.preventDefault();
+ this.saveNote();
+ }
}
// Ctrl/Cmd + Alt + P for Quick Switcher
@@ -663,21 +677,35 @@ function noteApp() {
this.createFolder();
}
- // Ctrl/Cmd + Z for undo (without shift or alt)
- // Use e.key instead of e.code to support non-QWERTY keyboard layouts
+ // Ctrl/Cmd + Z for undo (drawing vs note editor)
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 'z') {
- e.preventDefault();
- this.undo();
+ if (this.currentMedia && this.currentMediaType === 'drawing') {
+ e.preventDefault();
+ this.drawingUndo();
+ } else {
+ e.preventDefault();
+ this.undo();
+ }
}
// Ctrl/Cmd + Y OR Ctrl/Cmd+Shift+Z for redo
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'y') {
- e.preventDefault();
- this.redo();
+ if (this.currentMedia && this.currentMediaType === 'drawing') {
+ e.preventDefault();
+ this.drawingRedo();
+ } else {
+ e.preventDefault();
+ this.redo();
+ }
}
if ((e.ctrlKey || e.metaKey) && e.shiftKey && !e.altKey && e.key.toLowerCase() === 'z') {
- e.preventDefault();
- this.redo();
+ if (this.currentMedia && this.currentMediaType === 'drawing') {
+ e.preventDefault();
+ this.drawingRedo();
+ } else {
+ e.preventDefault();
+ this.redo();
+ }
}
// F3 for next search match
@@ -1484,13 +1512,7 @@ function noteApp() {
notePath += '.md';
}
- // Determine target folder: use dropdown context if set, otherwise homepage folder
- let targetFolder;
- if (this.dropdownTargetFolder !== null && this.dropdownTargetFolder !== undefined) {
- targetFolder = this.dropdownTargetFolder; // Can be '' for root or a folder path
- } else {
- targetFolder = this.selectedHomepageFolder || '';
- }
+ const targetFolder = this.inferredNewItemTargetFolder();
// If we have a target folder, create note in that folder
if (targetFolder) {
@@ -2082,7 +2104,7 @@ function noteApp() {
},
handleDeleteItemClick(el, event) {
event.stopPropagation();
- if (el.dataset.type === 'image') {
+ if (el.dataset.type !== 'note') {
this.deleteMedia(el.dataset.path);
} else {
this.deleteNote(el.dataset.path, el.dataset.name);
@@ -2208,6 +2230,11 @@ function noteApp() {
const isShared = !isMediaFile && this.isNoteShared(note.path);
const shareIcon = isShared ? '' : '';
const icon = this.getMediaIcon(note.type);
+ const deleteTitle = !isMediaFile
+ ? this.t('toolbar.delete_note')
+ : note.type === 'drawing'
+ ? this.t('toolbar.delete_drawing')
+ : this.t('toolbar.delete_image');
return `
+
+
+
+
+
+
+
+
+
+
+
+
@@ -2875,8 +2949,9 @@
@@ -2921,6 +2996,30 @@
:title="currentMedia.split('/').pop()"
>
+
+
+
+
+
@@ -3115,6 +3214,16 @@
📄
+
diff --git a/locales/de-DE.json b/locales/de-DE.json
index b2310bc..974befa 100644
--- a/locales/de-DE.json
+++ b/locales/de-DE.json
@@ -42,6 +42,7 @@
"new_note": "Neue Notiz",
"new_folder": "Neuer Ordner",
"new_from_template": "Neu aus Vorlage",
+ "new_drawing": "Neue Zeichnung",
"rename_folder": "Ordner umbenennen",
"delete_folder": "Ordner löschen",
"search_placeholder": "Notizen durchsuchen...",
@@ -124,6 +125,7 @@
"redo": "Wiederholen (Strg+Y)",
"delete_note": "Notiz löschen",
"delete_image": "Bild löschen",
+ "delete_drawing": "Zeichnung löschen",
"export_html": "Als HTML exportieren",
"print_preview": "Druckvorschau",
"copy_link": "Link in Zwischenablage kopieren",
@@ -226,6 +228,17 @@
"error_incorrect_password": "Falsches Passwort. Bitte versuche es erneut."
},
+ "drawing": {
+ "tool_freehand": "Stift — Freihand",
+ "tool_line": "Gerade Linie",
+ "tool_rect": "Rechteck",
+ "tool_ellipse": "Kreis",
+ "color": "Farbe",
+ "width": "Strichstärke",
+ "saved": "Zeichnung gespeichert",
+ "save_hint": "Zeichnung speichern (Strg+S)"
+ },
+
"media": {
"confirm_delete": "\"{{name}}\" löschen?",
"upload_failed": "Datei-Upload fehlgeschlagen",
diff --git a/locales/en-GB.json b/locales/en-GB.json
index 52054f6..2ddaf96 100644
--- a/locales/en-GB.json
+++ b/locales/en-GB.json
@@ -42,6 +42,7 @@
"new_note": "New Note",
"new_folder": "New Folder",
"new_from_template": "New from Template",
+ "new_drawing": "New Drawing",
"rename_folder": "Rename folder",
"delete_folder": "Delete folder",
"search_placeholder": "Search notes...",
@@ -123,6 +124,7 @@
"redo": "Redo (Ctrl+Y)",
"delete_note": "Delete note",
"delete_image": "Delete image",
+ "delete_drawing": "Delete drawing",
"export_html": "Export as HTML",
"print_preview": "Print preview",
"copy_link": "Copy link to clipboard",
@@ -225,6 +227,17 @@
"error_incorrect_password": "Incorrect password. Please try again."
},
+ "drawing": {
+ "tool_freehand": "Pencil — freehand drawing",
+ "tool_line": "Straight line",
+ "tool_rect": "Rectangle",
+ "tool_ellipse": "Circle",
+ "color": "Colour",
+ "width": "Stroke width",
+ "saved": "Drawing saved",
+ "save_hint": "Save drawing (Ctrl+S)"
+ },
+
"media": {
"confirm_delete": "Delete \"{{name}}\"?",
"upload_failed": "Failed to upload file",
diff --git a/locales/en-US.json b/locales/en-US.json
index af4614b..92fd810 100644
--- a/locales/en-US.json
+++ b/locales/en-US.json
@@ -42,6 +42,7 @@
"new_note": "New Note",
"new_folder": "New Folder",
"new_from_template": "New from Template",
+ "new_drawing": "New Drawing",
"rename_folder": "Rename folder",
"delete_folder": "Delete folder",
"search_placeholder": "Search notes...",
@@ -124,6 +125,7 @@
"redo": "Redo (Ctrl+Y)",
"delete_note": "Delete note",
"delete_image": "Delete image",
+ "delete_drawing": "Delete drawing",
"export_html": "Export as HTML",
"print_preview": "Print preview",
"copy_link": "Copy link to clipboard",
@@ -226,6 +228,17 @@
"error_incorrect_password": "Incorrect password. Please try again."
},
+ "drawing": {
+ "tool_freehand": "Pencil — freehand drawing",
+ "tool_line": "Straight line",
+ "tool_rect": "Rectangle",
+ "tool_ellipse": "Circle",
+ "color": "Color",
+ "width": "Stroke width",
+ "saved": "Drawing saved",
+ "save_hint": "Save drawing (Ctrl+S)"
+ },
+
"media": {
"confirm_delete": "Delete \"{{name}}\"?",
"upload_failed": "Failed to upload file",
diff --git a/locales/es-ES.json b/locales/es-ES.json
index a4a9f2e..199cf26 100644
--- a/locales/es-ES.json
+++ b/locales/es-ES.json
@@ -42,6 +42,7 @@
"new_note": "Nueva Nota",
"new_folder": "Nueva Carpeta",
"new_from_template": "Nueva desde Plantilla",
+ "new_drawing": "Nuevo dibujo",
"rename_folder": "Renombrar carpeta",
"delete_folder": "Eliminar carpeta",
"search_placeholder": "Buscar notas...",
@@ -124,6 +125,7 @@
"redo": "Rehacer (Ctrl+Y)",
"delete_note": "Eliminar nota",
"delete_image": "Eliminar imagen",
+ "delete_drawing": "Eliminar dibujo",
"export_html": "Exportar como HTML",
"print_preview": "Vista previa de impresión",
"copy_link": "Copiar enlace al portapapeles",
@@ -226,6 +228,17 @@
"error_incorrect_password": "Contraseña incorrecta. Por favor, inténtalo de nuevo."
},
+ "drawing": {
+ "tool_freehand": "Lápiz — trazo libre",
+ "tool_line": "Línea recta",
+ "tool_rect": "Rectángulo",
+ "tool_ellipse": "Círculo",
+ "color": "Color",
+ "width": "Grosor del trazo",
+ "saved": "Dibujo guardado",
+ "save_hint": "Guardar dibujo (Ctrl+S)"
+ },
+
"media": {
"confirm_delete": "¿Eliminar \"{{name}}\"?",
"upload_failed": "Error al subir archivo",
diff --git a/locales/fr-FR.json b/locales/fr-FR.json
index 5f3e08d..0de70c2 100644
--- a/locales/fr-FR.json
+++ b/locales/fr-FR.json
@@ -42,6 +42,7 @@
"new_note": "Nouvelle Note",
"new_folder": "Nouveau Dossier",
"new_from_template": "Nouveau depuis Modèle",
+ "new_drawing": "Nouveau dessin",
"rename_folder": "Renommer le dossier",
"delete_folder": "Supprimer le dossier",
"search_placeholder": "Rechercher des notes...",
@@ -124,6 +125,7 @@
"redo": "Rétablir (Ctrl+Y)",
"delete_note": "Supprimer la note",
"delete_image": "Supprimer l'image",
+ "delete_drawing": "Supprimer le dessin",
"export_html": "Exporter en HTML",
"print_preview": "Aperçu avant impression",
"copy_link": "Copier le lien dans le presse-papiers",
@@ -226,6 +228,17 @@
"error_incorrect_password": "Mot de passe incorrect. Veuillez réessayer."
},
+ "drawing": {
+ "tool_freehand": "Crayon — main levée",
+ "tool_line": "Ligne droite",
+ "tool_rect": "Rectangle",
+ "tool_ellipse": "Cercle",
+ "color": "Couleur",
+ "width": "Épaisseur du trait",
+ "saved": "Dessin enregistré",
+ "save_hint": "Enregistrer le dessin (Ctrl+S)"
+ },
+
"media": {
"confirm_delete": "Supprimer \"{{name}}\" ?",
"upload_failed": "Échec du téléchargement du fichier",
diff --git a/locales/hu-HU.json b/locales/hu-HU.json
index 298d945..63aa67c 100644
--- a/locales/hu-HU.json
+++ b/locales/hu-HU.json
@@ -42,6 +42,7 @@
"new_note": "Új Jegyzet",
"new_folder": "Új Mappa",
"new_from_template": "Új Sablonból",
+ "new_drawing": "Új rajz",
"rename_folder": "Mappa átnevezése",
"delete_folder": "Mappa törlése",
"search_placeholder": "Jegyzetek keresése...",
@@ -124,6 +125,7 @@
"redo": "Helyrehoz (Ctrl+Y)",
"delete_note": "Jegyzet törlése",
"delete_image": "Kép törlése",
+ "delete_drawing": "Rajz törlése",
"export_html": "Exportálás HTML-ként",
"print_preview": "Nyomtatási előnézet",
"copy_link": "Link másolása",
@@ -226,6 +228,17 @@
"error_incorrect_password": "Helytelen jelszó. Próbáld újra."
},
+ "drawing": {
+ "tool_freehand": "Ceruza — szabadkézi",
+ "tool_line": "Egyenes vonal",
+ "tool_rect": "Téglalap",
+ "tool_ellipse": "Kör",
+ "color": "Szín",
+ "width": "Vonalvastagság",
+ "saved": "Rajz elmentve",
+ "save_hint": "Rajz mentése (Ctrl+S)"
+ },
+
"media": {
"confirm_delete": "Biztos törlöd a következőt: \"{{name}}\"?",
"upload_failed": "Hiba a fájl feltöltése során",
diff --git a/locales/it-IT.json b/locales/it-IT.json
index c6c6e43..c216a2b 100644
--- a/locales/it-IT.json
+++ b/locales/it-IT.json
@@ -42,6 +42,7 @@
"new_note": "Nuova Nota",
"new_folder": "Nuova Cartella",
"new_from_template": "Nuovo da Modello",
+ "new_drawing": "Nuovo disegno",
"rename_folder": "Rinomina cartella",
"delete_folder": "Elimina cartella",
"search_placeholder": "Cerca note...",
@@ -123,6 +124,7 @@
"redo": "Ripeti (Ctrl+Y)",
"delete_note": "Elimina nota",
"delete_image": "Elimina immagine",
+ "delete_drawing": "Elimina disegno",
"export_html": "Esporta come HTML",
"print_preview": "Anteprima di stampa",
"copy_link": "Copia link negli appunti",
@@ -225,6 +227,17 @@
"error_incorrect_password": "Password errata. Riprova."
},
+ "drawing": {
+ "tool_freehand": "Matita — mano libera",
+ "tool_line": "Linea retta",
+ "tool_rect": "Rettangolo",
+ "tool_ellipse": "Cerchio",
+ "color": "Colore",
+ "width": "Spessore tratto",
+ "saved": "Disegno salvato",
+ "save_hint": "Salva disegno (Ctrl+S)"
+ },
+
"media": {
"confirm_delete": "Eliminare \"{{name}}\"?",
"upload_failed": "Caricamento file fallito",
diff --git a/locales/ja-JP.json b/locales/ja-JP.json
index 93067c7..2932ab2 100644
--- a/locales/ja-JP.json
+++ b/locales/ja-JP.json
@@ -42,6 +42,7 @@
"new_note": "新規ノート",
"new_folder": "新規フォルダ",
"new_from_template": "テンプレートから作成",
+ "new_drawing": "新規お絵かき",
"rename_folder": "フォルダの名前を変更",
"delete_folder": "フォルダを削除",
"search_placeholder": "ノートを検索...",
@@ -123,6 +124,7 @@
"redo": "やり直す (Ctrl+Y)",
"delete_note": "ノートを削除",
"delete_image": "画像を削除",
+ "delete_drawing": "お絵かきを削除",
"export_html": "HTMLとしてエクスポート",
"print_preview": "印刷プレビュー",
"copy_link": "リンクをクリップボードにコピー",
@@ -225,6 +227,17 @@
"error_incorrect_password": "パスワードが正しくありません。もう一度お試しください。"
},
+ "drawing": {
+ "tool_freehand": "鉛筆(フリーハンド)",
+ "tool_line": "直線",
+ "tool_rect": "長方形",
+ "tool_ellipse": "円",
+ "color": "色",
+ "width": "線の太さ",
+ "saved": "保存しました",
+ "save_hint": "お絵かきを保存(Ctrl+S)"
+ },
+
"media": {
"confirm_delete": "「{{name}}」を削除しますか?",
"upload_failed": "ファイルのアップロードに失敗しました",
diff --git a/locales/ru-RU.json b/locales/ru-RU.json
index 14690b1..dd1685e 100644
--- a/locales/ru-RU.json
+++ b/locales/ru-RU.json
@@ -42,6 +42,7 @@
"new_note": "Новая заметка",
"new_folder": "Новая папка",
"new_from_template": "Из шаблона",
+ "new_drawing": "Новый рисунок",
"rename_folder": "Переименовать папку",
"delete_folder": "Удалить папку",
"search_placeholder": "Поиск заметок...",
@@ -123,6 +124,7 @@
"redo": "Повторить (Ctrl+Y)",
"delete_note": "Удалить заметку",
"delete_image": "Удалить изображение",
+ "delete_drawing": "Удалить рисунок",
"export_html": "Экспорт в HTML",
"print_preview": "Предварительный просмотр печати",
"copy_link": "Копировать ссылку",
@@ -225,6 +227,17 @@
"error_incorrect_password": "Неверный пароль. Попробуйте снова."
},
+ "drawing": {
+ "tool_freehand": "Карандаш — от руки",
+ "tool_line": "Прямая линия",
+ "tool_rect": "Прямоугольник",
+ "tool_ellipse": "Круг",
+ "color": "Цвет",
+ "width": "Толщина линии",
+ "saved": "Рисунок сохранён",
+ "save_hint": "Сохранить рисунок (Ctrl+S)"
+ },
+
"media": {
"confirm_delete": "Удалить «{{name}}»?",
"upload_failed": "Не удалось загрузить файл",
diff --git a/locales/sl-SI.json b/locales/sl-SI.json
index b80d9b7..b219d6b 100644
--- a/locales/sl-SI.json
+++ b/locales/sl-SI.json
@@ -42,6 +42,7 @@
"new_note": "Nov zapis",
"new_folder": "Nova mapa",
"new_from_template": "Novo iz predloge",
+ "new_drawing": "Nov risba",
"rename_folder": "Preimenuj mapo",
"delete_folder": "Izbriši mapo",
"search_placeholder": "Išči zapiske...",
@@ -123,6 +124,7 @@
"redo": "Ponovi (Ctrl+Y)",
"delete_note": "Izbriši zapis",
"delete_image": "Izbriši sliko",
+ "delete_drawing": "Izbriši risbo",
"export_html": "Izvozi kot HTML",
"print_preview": "Predogled tiskanja",
"copy_link": "Kopiraj povezavo v odložišče",
@@ -225,6 +227,17 @@
"error_incorrect_password": "Napačno geslo. Prosimo, poskusite znova."
},
+ "drawing": {
+ "tool_freehand": "Svinčnik — prostoročno",
+ "tool_line": "Ravna črta",
+ "tool_rect": "Pravokotnik",
+ "tool_ellipse": "Krog",
+ "color": "Barva",
+ "width": "Debelina črte",
+ "saved": "Risba shranjena",
+ "save_hint": "Shrani risbo (Ctrl+S)"
+ },
+
"media": {
"confirm_delete": "Izbrišem \"{{name}}\"?",
"upload_failed": "Nalaganje datoteke ni uspelo",
diff --git a/locales/zh-CN.json b/locales/zh-CN.json
index 981cc76..0d603f0 100644
--- a/locales/zh-CN.json
+++ b/locales/zh-CN.json
@@ -42,6 +42,7 @@
"new_note": "新建笔记",
"new_folder": "新建文件夹",
"new_from_template": "从模板新建",
+ "new_drawing": "新建绘图",
"rename_folder": "重命名文件夹",
"delete_folder": "删除文件夹",
"search_placeholder": "搜索笔记...",
@@ -123,6 +124,7 @@
"redo": "重做 (Ctrl+Y)",
"delete_note": "删除笔记",
"delete_image": "删除图片",
+ "delete_drawing": "删除绘图",
"export_html": "导出为 HTML",
"print_preview": "打印预览",
"copy_link": "复制链接到剪贴板",
@@ -225,6 +227,17 @@
"error_incorrect_password": "密码错误。请重试。"
},
+ "drawing": {
+ "tool_freehand": "铅笔 — 自由绘制",
+ "tool_line": "直线",
+ "tool_rect": "矩形",
+ "tool_ellipse": "圆形",
+ "color": "颜色",
+ "width": "线条粗细",
+ "saved": "绘图已保存",
+ "save_hint": "保存绘图(Ctrl+S)"
+ },
+
"media": {
"confirm_delete": "删除 \"{{name}}\"?",
"upload_failed": "上传文件失败",