feat: Add 'Use Trash' functionality and fix various UI bugs
- Add a 'use_trash' column to the shopping_lists table to control whether a list uses the trash functionality. - Add a switch in the list settings to toggle the 'use_trash' flag for each list. - Add a switch in the admin panel to enable/disable the 'use_trash' flag for all lists at once. - Modify the delete functionality to respect the 'use_trash' flag. - Fix a bug where the search placeholder was not translated. - Fix a bug where the 'Restore to' text was not translated and was displayed in the dark theme with the wrong color. - Fix a bug where the title was always 'Noteshop' instead of 'Geteilte Einkaufsliste'. - Fix a bug where the 'send notification without username' switch was missing its label.
This commit is contained in:
76
main.py
76
main.py
@@ -1,4 +1,3 @@
|
||||
|
||||
from collections import defaultdict
|
||||
from passlib.context import CryptContext
|
||||
import os
|
||||
@@ -72,10 +71,12 @@ class ItemRestore(BaseModel):
|
||||
class ShoppingList(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
use_trash: bool
|
||||
|
||||
|
||||
class ShoppingListCreate(BaseModel):
|
||||
name: str
|
||||
use_trash: Optional[bool] = None
|
||||
|
||||
|
||||
class NewUser(BaseModel):
|
||||
@@ -137,7 +138,6 @@ def get_user(username: str):
|
||||
def verify_password(plain_password, hashed_password):
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
@@ -225,9 +225,13 @@ def init_db():
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS shopping_lists (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
use_trash BOOLEAN NOT NULL DEFAULT 1
|
||||
)
|
||||
""")
|
||||
if not column_exists(cursor, "shopping_lists", "use_trash"):
|
||||
cursor.execute("ALTER TABLE shopping_lists ADD COLUMN use_trash BOOLEAN NOT NULL DEFAULT 1")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -290,8 +294,13 @@ def init_db():
|
||||
print("Please change this password in a production environment.")
|
||||
|
||||
# Ensure default list exists even on fresh install
|
||||
cursor.execute("INSERT OR IGNORE INTO shopping_lists (name) VALUES ('Standard')")
|
||||
cursor.execute("INSERT OR IGNORE INTO shopping_lists (name) VALUES ('Papierkorb')")
|
||||
cursor.execute("SELECT COUNT(*) FROM shopping_lists WHERE name = 'Standard'")
|
||||
if cursor.fetchone()[0] == 0:
|
||||
cursor.execute("INSERT INTO shopping_lists (name) VALUES ('Standard')")
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM shopping_lists WHERE name = 'Papierkorb'")
|
||||
if cursor.fetchone()[0] == 0:
|
||||
cursor.execute("INSERT INTO shopping_lists (name) VALUES ('Papierkorb')")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -318,7 +327,7 @@ async def get_lists(current_user: User = Depends(get_current_active_user)):
|
||||
conn = sqlite3.connect(DB_FILE)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT id, name FROM shopping_lists ORDER BY name")
|
||||
cursor.execute("SELECT id, name, use_trash FROM shopping_lists ORDER BY name")
|
||||
lists = cursor.fetchall()
|
||||
conn.close()
|
||||
return [dict(row) for row in lists]
|
||||
@@ -358,7 +367,7 @@ async def create_list(list_data: ShoppingListCreate, current_user: User = Depend
|
||||
|
||||
conn.close()
|
||||
await manager.broadcast_update()
|
||||
return {"id": new_list_id, "name": name_to_insert}
|
||||
return {"id": new_list_id, "name": name_to_insert, "use_trash": True}
|
||||
|
||||
|
||||
@app.put("/api/lists/{list_id}", response_model=ShoppingList)
|
||||
@@ -366,14 +375,14 @@ async def rename_list(list_id: int, list_data: ShoppingListCreate, current_user:
|
||||
conn = sqlite3.connect(DB_FILE)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("SELECT name FROM shopping_lists WHERE id = ?", (list_id,))
|
||||
list_name_result = cursor.fetchone()
|
||||
if list_name_result and list_name_result[0] == "Papierkorb":
|
||||
cursor.execute("SELECT name, use_trash FROM shopping_lists WHERE id = ?", (list_id,))
|
||||
list_result = cursor.fetchone()
|
||||
if list_result and list_result[0] == "Papierkorb":
|
||||
conn.close()
|
||||
raise HTTPException(status_code=400, detail="Cannot rename the trash bin.")
|
||||
|
||||
try:
|
||||
cursor.execute("UPDATE shopping_lists SET name = ? WHERE id = ?", (list_data.name, list_id))
|
||||
cursor.execute("UPDATE shopping_lists SET name = ?, use_trash = ? WHERE id = ?", (list_data.name, list_data.use_trash, list_id))
|
||||
conn.commit()
|
||||
except sqlite3.IntegrityError:
|
||||
conn.close()
|
||||
@@ -382,7 +391,7 @@ async def rename_list(list_id: int, list_data: ShoppingListCreate, current_user:
|
||||
conn.close()
|
||||
|
||||
await manager.broadcast_update()
|
||||
return {"id": list_id, "name": list_data.name}
|
||||
return {"id": list_id, "name": list_data.name, "use_trash": list_data.use_trash}
|
||||
|
||||
|
||||
@app.delete("/api/lists/{list_id}", status_code=status.HTTP_200_OK)
|
||||
@@ -730,9 +739,16 @@ async def delete_marked_items(request: DeletionRequest, current_user: User = Dep
|
||||
# Permanent deletion from trash
|
||||
cursor.execute("DELETE FROM items WHERE marked = 1 AND list_id = ?", (trash_list_id,))
|
||||
else:
|
||||
# Move to trash and unmark
|
||||
cursor.execute("UPDATE items SET list_id = ?, marked = 0 WHERE marked = 1 AND list_id = ?", (trash_list_id, request.list_id))
|
||||
|
||||
# Check if the list uses the trash
|
||||
cursor.execute("SELECT use_trash FROM shopping_lists WHERE id = ?", (request.list_id,))
|
||||
use_trash_result = cursor.fetchone()
|
||||
if use_trash_result and use_trash_result[0]:
|
||||
# Move to trash and unmark
|
||||
cursor.execute("UPDATE items SET list_id = ?, marked = 0 WHERE marked = 1 AND list_id = ?", (trash_list_id, request.list_id))
|
||||
else:
|
||||
# Delete directly
|
||||
cursor.execute("DELETE FROM items WHERE marked = 1 AND list_id = ?", (request.list_id,))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
await manager.broadcast_update()
|
||||
@@ -838,10 +854,6 @@ async def restore_item(item_id: int, request: ItemRestore, current_user: User =
|
||||
|
||||
@app.delete("/api/items/{item_id}")
|
||||
async def delete_item(item_id: int, current_user: User = Depends(get_current_active_user)):
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only admins can delete items")
|
||||
|
||||
conn = sqlite3.connect(DB_FILE)
|
||||
cursor = conn.cursor()
|
||||
|
||||
@@ -861,8 +873,12 @@ async def delete_item(item_id: int, current_user: User = Depends(get_current_act
|
||||
raise HTTPException(status_code=500, detail="Trash bin list not found.")
|
||||
trash_list_id = trash_list_id_result[0]
|
||||
|
||||
if item_list_id == trash_list_id:
|
||||
# Permanent deletion from trash
|
||||
# Check if the list uses the trash
|
||||
cursor.execute("SELECT use_trash FROM shopping_lists WHERE id = ?", (item_list_id,))
|
||||
use_trash_result = cursor.fetchone()
|
||||
|
||||
if item_list_id == trash_list_id or (use_trash_result and not use_trash_result[0]):
|
||||
# Permanent deletion from trash or if trash is disabled
|
||||
cursor.execute("DELETE FROM items WHERE id = ?", (item_id,))
|
||||
else:
|
||||
# Move to trash and unmark
|
||||
@@ -950,6 +966,24 @@ async def websocket_endpoint(websocket: WebSocket, token: str):
|
||||
await manager.broadcast_user_list()
|
||||
|
||||
|
||||
class UseTrashSetting(BaseModel):
|
||||
enabled: bool
|
||||
|
||||
@app.post("/api/settings/all-lists-use-trash")
|
||||
async def set_all_lists_use_trash(request: UseTrashSetting, current_user: User = Depends(get_current_active_user)):
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Only admins can change this setting")
|
||||
|
||||
conn = sqlite3.connect(DB_FILE)
|
||||
cursor = conn.cursor()
|
||||
value_to_store = 1 if request.enabled else 0
|
||||
cursor.execute("UPDATE shopping_lists SET use_trash = ?", (value_to_store,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
await manager.broadcast_update()
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
# --- Statische Dateien (Frontend) ---
|
||||
# Create data directory if it doesn't exist
|
||||
os.makedirs(os.path.dirname(DB_FILE), exist_ok=True)
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
}
|
||||
|
||||
[data-theme="dark"] #trash-search-input,
|
||||
[data-theme="dark"] #restore-to-text {
|
||||
[data-theme="dark"] .restore-to-text {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
@@ -319,7 +319,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="restore-to-container" class="col-auto align-items-center" style="display: none;">
|
||||
<span id="restore-to-text" class="text-muted"></span>
|
||||
<span id="restore-to-text" class="restore-to-text"></span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="d-grid">
|
||||
@@ -409,6 +409,14 @@
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="use-trash-all-switch">
|
||||
<label class="form-check-label" for="use-trash-all-switch" id="use-trash-all-label"></label>
|
||||
</div>
|
||||
<div id="use-trash-all-status" class="mt-2"></div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h5 id="set-deletion-password-title" class="card-title"></h5>
|
||||
<div id="password-set-message" style="display: none;">
|
||||
<p>Passwort ist gesetzt.</p>
|
||||
@@ -460,6 +468,10 @@
|
||||
<label for="new-list-name-input" class="form-label">Neuer Name</label>
|
||||
<input type="text" class="form-control" id="new-list-name-input" required>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="use-trash-switch">
|
||||
<label class="form-check-label" for="use-trash-switch" id="use-trash-label"></label>
|
||||
</div>
|
||||
<div id="rename-list-error" class="text-danger"></div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -585,6 +597,7 @@
|
||||
const newListNameInput = document.getElementById('new-list-name-input');
|
||||
const renameListError = document.getElementById('rename-list-error');
|
||||
const confirmRenameBtn = document.getElementById('confirm-rename-btn');
|
||||
const useTrashSwitch = document.getElementById('use-trash-switch');
|
||||
|
||||
const deleteListModal = new bootstrap.Modal(document.getElementById('delete-list-modal'));
|
||||
const confirmDeleteListBtn = document.getElementById('confirm-delete-list-btn');
|
||||
@@ -601,6 +614,8 @@
|
||||
const setDeletionPasswordStatus = document.getElementById('set-deletion-password-status');
|
||||
const selectAllBtn = document.getElementById('select-all-btn');
|
||||
|
||||
const useTrashAllSwitch = document.getElementById('use-trash-all-switch');
|
||||
|
||||
let allLists = [];
|
||||
let currentListId = null;
|
||||
let currentListName = '';
|
||||
@@ -788,7 +803,7 @@
|
||||
// User View
|
||||
pageTitle.textContent = translations.title;
|
||||
itemNameInput.placeholder = translations.placeholder;
|
||||
trashSearchInput.placeholder = translations.trash_search_placeholder || 'Search in trash...';
|
||||
trashSearchInput.placeholder = translations.trash_search_placeholder || "Search in trash...";
|
||||
addButton.textContent = translations.add_button;
|
||||
addOneButton.textContent = translations.add_one_button;
|
||||
notifyBtn.textContent = translations.notify_button;
|
||||
@@ -816,6 +831,8 @@
|
||||
document.getElementById('set-deletion-password-title').textContent = translations.set_deletion_password_title;
|
||||
document.getElementById('deletion-password').placeholder = translations.deletion_password_placeholder;
|
||||
document.getElementById('set-deletion-password-button').textContent = translations.set_deletion_password_button;
|
||||
document.getElementById('use-trash-label').textContent = translations.use_trash_label;
|
||||
document.getElementById('use-trash-all-label').textContent = translations.use_trash_all_label;
|
||||
}
|
||||
|
||||
// Deletion Modal
|
||||
@@ -1186,7 +1203,8 @@
|
||||
function renameCurrentList(event) {
|
||||
event.preventDefault();
|
||||
if (!currentListId) return;
|
||||
|
||||
const currentList = allLists.find(l => l.id === currentListId);
|
||||
useTrashSwitch.checked = currentList.use_trash;
|
||||
newListNameInput.value = currentListName;
|
||||
renameListError.textContent = '';
|
||||
renameListModal.show();
|
||||
@@ -1235,7 +1253,7 @@
|
||||
const response = await fetch(`/api/lists/${currentListId}`, {
|
||||
method: 'PUT',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ name: newName })
|
||||
body: JSON.stringify({ name: newName, use_trash: useTrashSwitch.checked })
|
||||
});
|
||||
|
||||
if (response.status === 401) return handleLogout();
|
||||
@@ -1671,6 +1689,31 @@
|
||||
confirmRenameBtn.addEventListener('click', handleConfirmRename);
|
||||
confirmDeleteListBtn.addEventListener('click', handleConfirmDeleteList);
|
||||
|
||||
useTrashAllSwitch.addEventListener('change', async (e) => {
|
||||
const isEnabled = e.target.checked;
|
||||
const statusEl = document.getElementById('use-trash-all-status');
|
||||
statusEl.textContent = '';
|
||||
|
||||
const response = await fetch('/api/settings/all-lists-use-trash', {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ enabled: isEnabled })
|
||||
});
|
||||
|
||||
if (response.status === 401) return handleLogout();
|
||||
|
||||
if (response.ok) {
|
||||
statusEl.textContent = translations.notification_settings_saved;
|
||||
statusEl.className = 'mt-2 text-success';
|
||||
setTimeout(() => { statusEl.textContent = ''; }, 3000);
|
||||
await fetchListsAndSetCurrent();
|
||||
} else {
|
||||
const data = await response.json();
|
||||
statusEl.textContent = `Error: ${data.detail}`;
|
||||
statusEl.className = 'mt-2 text-danger';
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const toggleDeletionPassword = document.getElementById('toggle-deletion-password');
|
||||
toggleDeletionPassword.addEventListener('click', () => {
|
||||
@@ -1890,4 +1933,4 @@
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
193
translations.py
193
translations.py
@@ -1,87 +1,75 @@
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import List, Dict
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
def get_standard_items(lang: str) -> List[str]:
|
||||
"""
|
||||
Parses the strings.xml file for the given language and returns the
|
||||
standard shopping list items.
|
||||
"""
|
||||
if lang == "de":
|
||||
filepath = "translations/de/strings.xml"
|
||||
else:
|
||||
filepath = "translations/en/strings.xml"
|
||||
|
||||
try:
|
||||
tree = ET.parse(filepath)
|
||||
if lang not in ['de', 'en']:
|
||||
lang = 'de' # Fallback to German
|
||||
|
||||
tree = ET.parse(f'translations/{lang}/strings.xml')
|
||||
root = tree.getroot()
|
||||
for string_array in root.findall('string-array'):
|
||||
if string_array.get('name') == 'standard_list_items':
|
||||
return [item.text for item in string_array.findall('item')]
|
||||
except (ET.ParseError, FileNotFoundError):
|
||||
|
||||
items = []
|
||||
for string_array in root.findall(".//string-array[@name='standard_list_items']"):
|
||||
for item in string_array.findall('item'):
|
||||
if item.text:
|
||||
items.append(item.text)
|
||||
return items
|
||||
except (FileNotFoundError, ET.ParseError) as e:
|
||||
print(f"Error reading standard items for language '{lang}': {e}")
|
||||
return []
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def get_all_translations(lang: str) -> Dict[str, str]:
|
||||
"""
|
||||
Returns a dictionary with all UI and notification translations
|
||||
for the web app.
|
||||
"""
|
||||
if lang == "de":
|
||||
if lang not in ['de', 'en']:
|
||||
lang = 'de'
|
||||
|
||||
if lang == 'de':
|
||||
return {
|
||||
"title": "Geteilte Einkaufs/Aufgabenliste",
|
||||
"placeholder": "Artikel eingeben...",
|
||||
"title": "Geteilte Einkaufsliste",
|
||||
"placeholder": "Neuer Eintrag, mehrere mit Komma getrennt",
|
||||
"add_button": "Hinzufügen",
|
||||
"add_one_button": "+1",
|
||||
"notify_button": "Benachrichtigen",
|
||||
"sending_notification": "Sende...",
|
||||
"notification_sent": "Benachrichtigung gesendet!",
|
||||
"notification_title": "Einkaufsliste aktualisiert",
|
||||
"empty_list_message": "Die Einkaufsliste ist leer.",
|
||||
"db_version": "Datenbankversion: {version}",
|
||||
"db_error": "Datenbankfehler: {error}",
|
||||
"generic_notification_error": "Fehler beim Senden der Benachrichtigung.",
|
||||
"example_button_text": "Beispiel",
|
||||
"info_text_existing": "= ist schon in der Liste",
|
||||
"info_text_new": "= noch nicht in der Liste",
|
||||
"admin_panel_title": "Admin Panel",
|
||||
"create_user_title": "Neuen Benutzer erstellen",
|
||||
"username_label": "Benutzername",
|
||||
"password_label": "Passwort",
|
||||
"is_admin_label": "Ist Admin?",
|
||||
"create_user_button": "Benutzer erstellen",
|
||||
"change_password_title": "Passwort ändern",
|
||||
"current_password_label": "Aktuelles Passwort",
|
||||
"new_password_label": "Neues Passwort",
|
||||
"confirm_password_label": "Neues Passwort bestätigen",
|
||||
"change_password_button": "Passwort ändern",
|
||||
"manage_users_title": "Benutzer verwalten",
|
||||
"delete_button": "Löschen",
|
||||
"logout_button": "Abmelden",
|
||||
"notify_button": "Änderungen abschließen & Benachrichtigen",
|
||||
"login_title": "Anmelden",
|
||||
"login_username_label": "Benutzername",
|
||||
"login_password_label": "Passwort",
|
||||
"login_button": "Anmelden",
|
||||
"user_created_success": "Benutzer '{username}' erfolgreich erstellt.",
|
||||
"login_error_incorrect": "Benutzername oder Passwort inkorrekt.",
|
||||
"login_error_incorrect": "Falscher Benutzername oder Passwort.",
|
||||
"login_error_generic": "Ein Fehler ist aufgetreten. Bitte erneut versuchen.",
|
||||
"logout_button": "Abmelden",
|
||||
"admin_panel_title": "Admin-Panel",
|
||||
"create_user_title": "Neuen Benutzer anlegen",
|
||||
"username_label": "Benutzername",
|
||||
"password_label": "Passwort",
|
||||
"is_admin_label": "Ist Admin",
|
||||
"create_user_button": "Benutzer anlegen",
|
||||
"user_created_success": "Benutzer '{username}' erfolgreich angelegt.",
|
||||
"change_password_title": "Passwort ändern",
|
||||
"current_password_label": "Aktuelles Passwort",
|
||||
"new_password_label": "Neues Passwort",
|
||||
"confirm_password_label": "Passwort bestätigen",
|
||||
"change_password_button": "Passwort ändern",
|
||||
"manage_users_title": "Benutzer verwalten",
|
||||
"delete_button": "Löschen",
|
||||
"db_version": "Datenbankversion: {version}",
|
||||
"db_error": "Datenbankfehler: {error}",
|
||||
"sending_notification": "Sende...",
|
||||
"notification_sent": "Benachrichtigung gesendet!",
|
||||
"generic_notification_error": "Fehler beim Senden der Benachrichtigung.",
|
||||
"empty_list_message": "Die Einkaufsliste ist leer.",
|
||||
"notification_title": "Einkaufsliste Aktualisiert",
|
||||
"set_deletion_password_title": "Löschpasswort festlegen",
|
||||
"deletion_password_placeholder": "Löschpasswort (Optional)",
|
||||
"deletion_password_placeholder": "Löschpasswort (leer lassen zum Deaktivieren)",
|
||||
"set_deletion_password_button": "Passwort festlegen",
|
||||
"password_set_success": "Passwort erfolgreich festgelegt.",
|
||||
"delete_password_modal_title": "Löschen bestätigen",
|
||||
"delete_password_label": "Passwort zur Bestätigung eingeben",
|
||||
"cancel_button": "Abbrechen",
|
||||
"select_list_placeholder": "Liste auswählen",
|
||||
"password_required": "Passwort erforderlich.",
|
||||
"delete_password_label": "Bitte geben Sie das Löschpasswort ein.",
|
||||
"incorrect_password": "Falsches Passwort.",
|
||||
"generic_error": "Ein Fehler ist aufgetreten.",
|
||||
"admin_permission_required": "Administratorberechtigung zum Löschen von Elementen erforderlich.",
|
||||
"notification_send_list_only_label": "Nur Listennamen mitsenden",
|
||||
"notification_settings_saved": "Einstellung gespeichert.",
|
||||
"cancel_button": "Abbrechen",
|
||||
"info_text_existing": "Artikel bereits in der Liste",
|
||||
"info_text_new": "Neuer Artikel",
|
||||
"example_button_text": "Beispiel",
|
||||
"select_list_placeholder": "Liste auswählen",
|
||||
"delete_list_modal_title": "Liste löschen",
|
||||
"delete_list_confirm_text": "Sind Sie sicher, dass Sie die Liste \"{listName}\" löschen möchten? Alle zugehörigen Artikel werden ebenfalls entfernt.",
|
||||
"delete_list_menu_item": "Aktuelle löschen",
|
||||
@@ -93,60 +81,59 @@ def get_all_translations(lang: str) -> Dict[str, str]:
|
||||
"no_list_to_restore_to": "Keine Liste zum Wiederherstellen vorhanden.",
|
||||
"permanent_delete_modal_title": "Endgültig löschen",
|
||||
"permanent_delete_confirm_text": "Sind Sie sicher, dass Sie die markierten Elemente endgültig löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"permanent_delete_button": "Endgültig löschen"
|
||||
"permanent_delete_button": "Endgültig löschen",
|
||||
"trash_search_placeholder": "Papierkorb durchsuchen...",
|
||||
"restore_to_list": "Wiederherstellen nach: {listName}",
|
||||
"use_trash_label": "Papierkorb für diese Liste verwenden",
|
||||
"use_trash_all_label": "Papierkorb für alle Listen verwenden"
|
||||
}
|
||||
else: # Fallback to English
|
||||
return {
|
||||
"title": "Shared Shopping List",
|
||||
"placeholder": "Enter item...",
|
||||
"placeholder": "New item, separate multiple with comma",
|
||||
"add_button": "Add",
|
||||
"add_one_button": "+1",
|
||||
"notify_button": "Notify",
|
||||
"sending_notification": "Sending...",
|
||||
"notification_sent": "Notification sent!",
|
||||
"notification_title": "Shopping List Updated",
|
||||
"empty_list_message": "The shopping list is empty.",
|
||||
"db_version": "Database version: {version}",
|
||||
"db_error": "Database error: {error}",
|
||||
"generic_notification_error": "Error sending notification.",
|
||||
"example_button_text": "Example",
|
||||
"info_text_existing": "= is already on the list",
|
||||
"info_text_new": "= not yet on the list",
|
||||
"admin_panel_title": "Admin Panel",
|
||||
"create_user_title": "Create New User",
|
||||
"username_label": "Username",
|
||||
"password_label": "Password",
|
||||
"is_admin_label": "Is Admin?",
|
||||
"create_user_button": "Create User",
|
||||
"change_password_title": "Change Your Password",
|
||||
"current_password_label": "Current Password",
|
||||
"new_password_label": "New Password",
|
||||
"confirm_password_label": "Confirm New Password",
|
||||
"change_password_button": "Change Password",
|
||||
"manage_users_title": "Manage Users",
|
||||
"delete_button": "Delete",
|
||||
"logout_button": "Logout",
|
||||
"notify_button": "Finalize Changes & Notify",
|
||||
"login_title": "Login",
|
||||
"login_username_label": "Username",
|
||||
"login_password_label": "Password",
|
||||
"login_button": "Login",
|
||||
"user_created_success": "User '{username}' created successfully.",
|
||||
"login_error_incorrect": "Incorrect username or password.",
|
||||
"login_error_generic": "An error occurred. Please try again.",
|
||||
"logout_button": "Logout",
|
||||
"admin_panel_title": "Admin Panel",
|
||||
"create_user_title": "Create New User",
|
||||
"username_label": "Username",
|
||||
"password_label": "Password",
|
||||
"is_admin_label": "Is Admin",
|
||||
"create_user_button": "Create User",
|
||||
"user_created_success": "User '{username}' created successfully.",
|
||||
"change_password_title": "Change Password",
|
||||
"current_password_label": "Current Password",
|
||||
"new_password_label": "New Password",
|
||||
"confirm_password_label": "Confirm Password",
|
||||
"change_password_button": "Change Password",
|
||||
"manage_users_title": "Manage Users",
|
||||
"delete_button": "Delete",
|
||||
"db_version": "Database version: {version}",
|
||||
"db_error": "Database error: {error}",
|
||||
"sending_notification": "Sending...",
|
||||
"notification_sent": "Notification sent!",
|
||||
"generic_notification_error": "Error sending notification.",
|
||||
"empty_list_message": "The shopping list is empty.",
|
||||
"notification_title": "Shopping List Updated",
|
||||
"set_deletion_password_title": "Set Deletion Password",
|
||||
"deletion_password_placeholder": "Deletion Password (Optional)",
|
||||
"deletion_password_placeholder": "Deletion password (leave empty to disable)",
|
||||
"set_deletion_password_button": "Set Password",
|
||||
"password_set_success": "Password set successfully.",
|
||||
"delete_password_modal_title": "Confirm Deletion",
|
||||
"delete_password_label": "Enter password to confirm",
|
||||
"cancel_button": "Cancel",
|
||||
"select_list_placeholder": "Select List",
|
||||
"password_required": "Password required.",
|
||||
"delete_password_label": "Please enter the deletion password.",
|
||||
"incorrect_password": "Incorrect password.",
|
||||
"generic_error": "An error occurred.",
|
||||
"admin_permission_required": "Admin permission required to delete items.",
|
||||
"notification_send_list_only_label": "Send list name only",
|
||||
"notification_settings_saved": "Setting saved.",
|
||||
"cancel_button": "Cancel",
|
||||
"info_text_existing": "Item already in list",
|
||||
"info_text_new": "New item",
|
||||
"example_button_text": "Example",
|
||||
"select_list_placeholder": "Select list",
|
||||
"delete_list_modal_title": "Delete List",
|
||||
"delete_list_confirm_text": "Are you sure you want to delete the list \"{listName}\"? All associated items will also be removed.",
|
||||
"delete_list_menu_item": "Delete current",
|
||||
@@ -158,5 +145,9 @@ def get_all_translations(lang: str) -> Dict[str, str]:
|
||||
"no_list_to_restore_to": "No list to restore to.",
|
||||
"permanent_delete_modal_title": "Permanently delete",
|
||||
"permanent_delete_confirm_text": "Are you sure you want to permanently delete the marked items? This action cannot be undone.",
|
||||
"permanent_delete_button": "Permanently delete"
|
||||
"permanent_delete_button": "Permanently delete",
|
||||
"trash_search_placeholder": "Search in trash...",
|
||||
"restore_to_list": "Restore to: {listName}",
|
||||
"use_trash_label": "Use trash for this list",
|
||||
"use_trash_all_label": "Use trash for all lists"
|
||||
}
|
||||
|
||||
@@ -116,4 +116,6 @@
|
||||
<string name="restore_to_list">Wiederherstellen nach: {listName}</string>
|
||||
<string name="no_list_to_restore_to">Keine Liste zum Wiederherstellen ausgewählt.</string>
|
||||
<string name="restore_item_error">Fehler beim Wiederherstellen des Elements.</string>
|
||||
</resources>
|
||||
<string name="use_trash_label">Papierkorb für diese Liste verwenden</string>
|
||||
<string name="use_trash_all_label">Papierkorb für alle Listen verwenden</string>
|
||||
</resources>
|
||||
@@ -116,4 +116,6 @@
|
||||
<string name="restore_to_list">Restore to: {listName}</string>
|
||||
<string name="no_list_to_restore_to">No list selected for restore.</string>
|
||||
<string name="restore_item_error">Error restoring item.</string>
|
||||
</resources>
|
||||
<string name="use_trash_label">Use trash for this list</string>
|
||||
<string name="use_trash_all_label">Use trash for all lists</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user