From a6529c9e22fdfa65c4af67d466550dd9deeb3996 Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Tue, 1 Jul 2025 15:03:38 +0530 Subject: [PATCH 01/32] Update utils.py --- packages/backend-server/app/utils/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend-server/app/utils/utils.py b/packages/backend-server/app/utils/utils.py index 03bced9e..916058d0 100644 --- a/packages/backend-server/app/utils/utils.py +++ b/packages/backend-server/app/utils/utils.py @@ -41,7 +41,7 @@ def create_uuid(): def get_origins(env): data = { "production": ["https://app.zecrypt.io"], - "dev": ["http://localhost:3000", "https://preview.app.zecrypt.io"], + "dev": ["http://localhost:3000", "https://preview.app.zecrypt.io", "chrome-extension://*"], } return data.get(env, ["*"]) From 09a17bff56bf1f5bdbe4f6ad13230dc9fd448793 Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Thu, 10 Jul 2025 14:03:49 +0530 Subject: [PATCH 02/32] Update jwt_utils.py --- packages/backend-server/app/utils/jwt_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend-server/app/utils/jwt_utils.py b/packages/backend-server/app/utils/jwt_utils.py index b2d4a434..b62006eb 100644 --- a/packages/backend-server/app/utils/jwt_utils.py +++ b/packages/backend-server/app/utils/jwt_utils.py @@ -16,13 +16,13 @@ def create_jwt_token( if expires_delta: expire = datetime.utcnow() + expires_delta else: - expire = datetime.utcnow() + timedelta(days=2) + expire = datetime.utcnow() + timedelta(days=30) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, jwt_secret, algorithm=jwt_algorithm) return encoded_jwt -def encode_token(userid, days=30, ttl=None, device_id=None): +def encode_token(userid, days=60, ttl=None, device_id=None): user_id = userid payload = { "user": str(user_id), From c3522cf992c7299c5b22c66bc094a78e21350c56 Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Wed, 24 Sep 2025 10:41:38 +0530 Subject: [PATCH 03/32] French --- packages/frontend-web/messages/fr/common.json | 1424 +++++++++++++++++ packages/frontend-web/middleware.ts | 2 +- 2 files changed, 1425 insertions(+), 1 deletion(-) create mode 100644 packages/frontend-web/messages/fr/common.json diff --git a/packages/frontend-web/messages/fr/common.json b/packages/frontend-web/messages/fr/common.json new file mode 100644 index 00000000..dc6070c7 --- /dev/null +++ b/packages/frontend-web/messages/fr/common.json @@ -0,0 +1,1424 @@ +{ + "app": { + "title": "Zecrypt", + "description": "Sécurisez vos données avec Zecrypt" + }, + "common": { + "loading": "Chargement...", + "saving": "Sauvegarde...", + "deleting": "Suppression...", + "error": "Erreur", + "success": "Succès", + "cancel": "Annuler", + "add": "Ajouter", + "save": "Sauvegarder", + "submitting": "Envoi...", + "try_again": "Réessayer", + "sort": "Trier", + "sort_options": "Options de tri", + "sort_by_name": "Trier par nom", + "sort_by_created": "Trier par date de création", + "sort_by_updated": "Trier par date de modification", + "ascending": "Croissant", + "descending": "Décroissant", + "clear_sort": "Effacer le tri", + "add_item_description": "Saisissez les détails ci-dessous", + "edit_item_description": "Mettre à jour les détails" + }, + "pagination": { + "showing_x_of_y": "Affichage de {start} - {end} sur {total} éléments", + "page_of": "Page {currentPage} sur {totalPages}", + "next": "Suivant", + "previous": "Précédent" + }, + "env": { + "env_variables": "Variables d'environnement", + "manage_your_env": "Manage your environment variables securely", + "add_env": "Ajouter Variables d'environnement", + "loading_env": "Chargement environment variables...", + "name": "Nom", + "env_value": "Value", + "tags": "Étiquettes", + "created_at": "Créé le", + "actions": "Actions", + "no_env_yet": "Non environment variables added yet", + "no_matching_env": "Non matching environment variables found", + "search_across_all_fields": "Rechercher across all fields...", + "show_env": "Afficher environment variables", + "hide_env": "Masquer environment variables", + "copy_env": "Copier environment variables", + "env_copied": "Environment variables copié to clipboard", + "failed_to_copy_env": "Échec de copy environment variables to clipboard", + "delete_env_confirmation": "Êtes-vous sûr you want to delete this environment variables? Cette action ne peut pas être annulée.", + "showing_x_of_y": "Affichage de {start} - {end} sur {total} éléments", + "failed_to_decrypt": "Échec de decrypt environment variables", + "add_new_env": "Ajouter New Variables d'environnement", + "add_new_env_description": "Entrer your environment variables details below", + "edit_env": "Modifier Variables d'environnement", + "edit_env_description": "Mettre à jour your environment variables", + "env_name": "Environment Nom", + "enter_env_name": "Entrer environment name", + "enter_env_value": "Entrer environment value", + "enter_env_variables": "KEY=value\nANOTHER_KEY=another_value", + "env_format_hint": "Entrer your environment variables in KEY=value format, one per line", + "notes": "Notes", + "enter_notes": "Entrer notes (optional)", + "add_a_tag": "Ajouter une étiquette", + "add_tag": "Ajouter une étiquette", + "press_enter_to_add": "Press Entrer to add a tag", + "cancel": "Annuler", + "add": "Ajouter", + "update": "Mettre à jour", + "env_added_successfully": "Environment variables added successfully", + "env_updated_successfully": "Environment variables updated successfully", + "env_deleted_successfully": "Environment variables deleted successfully", + "error_adding_env": "Échec de add environment variables", + "error_updating_env": "Échec de update environment variables", + "error_deleting_env": "Échec de delete environment variables", + "error_fetching_env": "Échec de fetch environment variables", + "please_fill_all_required_fields": "Veuillez remplir tous les champs requis", + "no_project_selected": "Non project selected. Please select a project.", + "clear_filters": "Effacer les filtres", + "decrypting": "Decrypting environment variables...", + "env_updated_description": "The environment variables have been updated.", + "env_added_description": "The environment variables have been added.", + "env_already_exists": "Environment variables with this name already exists.", + "date_created": "Date de création", + "sort": "Trier", + "sort_options": "Options de tri", + "sort_by_name": "Trier par nom", + "sort_by_created": "Trier par date de création", + "sort_by_updated": "Trier par date de modification", + "view_env_code": "Voir in code editor", + "click_to_add_env": "Click to add environment variables", + "click_to_edit_env": "Click to edit environment variables", + "env_hidden": "••••••••", + "copied": "Copié" + }, + "licenses": { + "software_licenses": "Licences logicielles", + "add_license": "Ajouter License", + "filter_by_tag": "Filtrer par étiquette", + "all_tags": "Toutes les étiquettes", + "search": "Rechercher", + "loading_licenses": "Chargement licenses...", + "no_licenses_found": "Non licenses found", + "clear_filters": "Effacer les filtres", + "name": "Nom", + "software": "Ssurtware", + "license_key": "License Key", + "expiry_date": "Expiry Date", + "tags": "Étiquettes", + "actions": "Actions", + "add_new_license": "Ajouter New License", + "add_new_license_description": "Entrer your ssurtware license details below", + "cancel": "Annuler", + "save": "Sauvegarder", + "edit_license": "Modifier License", + "edit_license_description": "Mettre à jour your ssurtware license details", + "update": "Mettre à jour", + "delete": "Supprimer", + "license_deleted": "License deleted", + "license_deleted_description": "The license has been deleted successfully", + "copied": "Copié", + "field_copied": "Field copié to clipboard", + "copy_failed": "Échec de la copie", + "failed_to_copy_field": "Échec de copy field to clipboard", + "hide_license_key": "Masquer license key", + "show_license_key": "Afficher license key", + "showing_results": "Affichering {startIdx}-{endIdx} sur {totalNombre} résultats", + "page_of": "Page {currentPage} sur {totalPages}", + "expires": "Expire", + "expired": "Expiré", + "expiring_soon": "Expire bientôt", + "edit": "Modifier", + "enter_license_name": "Entrer license name", + "enter_software_name": "Entrer ssurtware name", + "enter_license_key": "Entrer license key", + "enter_notes": "Saisir les notes", + "confirm_deletion": "Confirmer Deletion", + "confirm_delete_license_description": "Êtes-vous sûr you want to delete this license? Cette action ne peut pas être annulée.", + "license_added_successfully": "License added successfully", + "failed_to_add_license": "Échec de add license", + "invalid_input": "Invalid input", + "validation_error": "Erreur de validation", + "network_error": "Erreur réseau", + "error_adding_license": "Erreur adding license", + "license_updated_successfully": "License updated successfully", + "failed_to_update_license": "Échec de update license", + "license_not_found": "License not found", + "error_updating_license": "Erreur updating license", + "missing_auth_or_selection": "Missing authentication or selection", + "license_deleted_successfully": "License deleted successfully", + "error_deleting_license": "Erreur deleting license", + "error_fetching_licenses": "Erreur fetching licenses", + "adding": "Ajoutering...", + "updating": "Updating...", + "deleting": "Suppression...", + "notes": "Notes", + "software_name": "Ssurtware Nom", + "rows_per_page": "Lignes par page", + "sort": "Trier", + "sort_options": "Options de tri", + "sort_by_name": "Trier par nom", + "sort_by_created": "Trier par date de création", + "sort_by_updated": "Trier par date de modification", + "ascending": "Croissant", + "descending": "Décroissant", + "clear_sort": "Effacer le tri", + "search_across_all_fields": "Rechercher", + "update_api_key": "Mettre à jour API Key", + "manage_your_software_licenses": "Manage your ssurtware licenses securely", + "date_created": "Date de création", + "add_a_tag": "Ajouter une étiquette" + }, + "navigation": { + "home": "Accueil", + "dashboard": "Tableau de bord", + "settings": "Paramètres" + }, + "auth": { + "login_to_continue": "Connexion to continue", + "sign_in_with_sso": "Se connecter with your organization account", + "login": "Connexion", + "signup": "S'inscrire", + "cancel": "Annuler", + "logout": "Déconnexion", + "sign_up": "S'inscrire", + "try_again": "Réessayer", + "dont_have_account": "Don't have an account?", + "email": "E-mail", + "password": "Mot de passe", + "agreement": "When signing in, you agree to our terms and conditions", + "terms": "Terms", + "privacy_policy": "Privacy Policy", + "terms_and_conditions": "Terms and Conditions", + "service_status": "Service Statut", + "generate_password": "Générer un mot de passe", + "verification_code": "Verification Code", + "2fa_verify": "2FA Verification", + "2fa_enter_code": "Entrer the 6-digit code from your authenticator app", + "2fa_placeholder": "e.g., 123456", + "verify": "Verify", + "verifying_authentication": "Verifying authentication...", + "2fa_verification_error": "Invalid verification code. Veuillez réessayer.", + "2fa_verification_success": "2FA verification successful", + "verifying": "Verifying 2FA...", + "authentication_error": "Authentication error", + "2fa_verified": "2FA Verified", + "redirecting_to_dashboard": "Rougeirecting you to dashboard...", + "setup_encryption_keys": "Setup Encryption Keys", + "encryption_setup_description": "Créer a master mot de passe to encrypt your data. This mot de passe is used to protect your prinuméro de TVAe key.", + "encryption_password_warning": "Remember this mot de passe carefully! If you forget it, you will lose access to all your encrypted data.", + "encryption_password": "Master Mot de passe", + "enter_strong_password": "Entrer a strong mot de passe", + "confirm_encryption_password": "Confirmer Master Mot de passe", + "confirm_password": "Confirmer your mot de passe", + "encryption_password_requirements": "Mot de passe must be at least 8 characters and should include letters, chiffres (0-9), and special characters for maximum security.", + "generating_keys": "Generating keys...", + "setup_encryption": "Setup Encryption", + "password_too_short": "Mot de passe must be at least 8 characters long", + "passwords_dont_match": "Mot de passes don't match", + "encryption_setup_success": "Encryption Setup Compléter", + "encryption_keys_created": "Your encryption keys have been created successfully", + "encryption_setup_failed": "Échec de set up encryption keys", + "checking_encryption_keys": "Checking encryption keys...", + "encryption_key_check_failed": "Encryption Key Check Failed", + "encryption_key_check_failed_desc": "We couldn't verify your encryption keys. This could be due to a network issue or missing keys.", + "encryption_security_error": "Encryption security error. Please try logging in again.", + "login_successful": "Connexion Succèsful", + "2fa_setup_required": "2FA Setup Requis", + "scan_qr_with_authenticator": "Scan the QR code with your authenticator app", + "unknown_error": "Erreur inconnue occurred", + "auth_network_error": "Erreur réseau during authentication", + "auth_process_failed": "Authentication process failed", + "2fa_invalid_code": "Invalid verification code", + "2fa_verification_failed": "Verification failed: {message}", + "copied_to_clipboard": "Copié to clipboard", + "paste_in_authenticator": "Coller this code into your authenticator app", + "2fa_setup": "Two-Factor Authentication Setup", + "2fa_scan": "Scan the QR code with your authenticator app", + "2fa_scan_difficulty": "Having trouble scanning? You can copy the setup key below:", + "copy_setup_key": "Copier Setup Key", + "unlock_encryption": "Unlock Encryption", + "enter_encryption_password": "Entrer your master mot de passe", + "enter_encryption_password_desc": "Entrer your master mot de passe to decrypt your prinuméro de TVAe key.", + "unlocking": "Unlocking...", + "unlock": "Unlock", + "encryption_unlocked": "Encryption Déverrouillé", + "encryption_key_decrypted": "Your encryption key has been successfully decrypted", + "incorrect_encryption_password": "Incorrect master mot de passe. Veuillez réessayer.", + "multi_device_login": "Connexion from New Device", + "multi_device_desc": "You're logging in from a new device. Please enter your master mot de passe to access your encrypted data.", + "authenticating": "Authenticating...", + "download_password_title": "Your Zecrypt Encryption Mot de passe:", + "download_password_important": "IMPORTANT: Keep this mot de passe safe. You will need it to access your encrypted data.", + "download_filename": "zecrypt-encryption-mot de passe.txt" + }, + "features": { + "trial_title": "Votre gestionnaire de stockage sécurisé", + "unlimited_devices": "Appareils illimités — complètement gratuit pour toujours", + "shared_vaults": "Partagez facilement les identifiants avec vos collègues - (Bientôt disponible)", + "advanced_security": "Chiffrement de niveau militaire (AES-GCM) pour une sécurité à toute épreuve", + "security_alerts": "Alertes de violation instantanées pour toujours rester un pas en avant - (Bientôt disponible)", + "multi_platform": "Disponible sur Mac, iOS, Windows, Android, Chrome OS et Linux - (bientôt disponible)", + "organization": "Organisation", + "organization_desc": "Organisez les identifiants en projets pour une meilleure gestion et un accès rapide.", + "secure_storage": "Stockage sécurisé", + "secure_storage_desc": "Stockez tous vos mots de passe et données sensibles avec un chiffrement de niveau militaire.", + "secure_password_manager": "Stockage cloud chiffré de bout en bout" + }, + "actions": { + "add": "Ajouter", + "edit": "Modifier", + "delete": "Supprimer", + "cancel": "Annuler", + "close": "Fermer", + "save": "Sauvegarder", + "saving": "Sauvegarde...", + "deleting": "Suppression...", + "submit": "Soumettre", + "ok": "OK", + "yes": "Oui", + "no": "Non", + "confirm": "Confirmer", + "confirm_delete": "Confirmer la suppression", + "continue": "Continuer", + "back": "Retour", + "next": "Suivant", + "search": "Rechercher", + "filter": "Filtrer", + "clear": "Effacer", + "reset": "Réinitialiser", + "refresh": "Actualiser", + "upload": "Télécharger", + "download": "Télécharger", + "copy": "Copier", + "paste": "Coller", + "cut": "Couper", + "undo": "Annuler", + "redo": "Refaire", + "select_all": "Tout sélectionner", + "select_none": "Ne rien sélectionner", + "create": "Créer", + "update": "Mettre à jour", + "remove": "Supprimer", + "error": "Erreur", + "view": "Voir" + }, + "dashboard": { + "project_limit_reached": "Limite de projet atteinte", + "alpha_one_project_limit": "Nous fonctionnons actuellement en alpha, un seul projet est autorisé par espace de travail.", + "select_project": "Sélectionner les projets", + "manage_projects": "Gérer les projets", + "select_or_create_project": "Sélectionner ou créer un projet", + "welcome_back": "Welcome back to your secure workspace", + "total_passwords": "Total des mots de passe", + "total_accounts": "Total des comptes", + "total_folders": "Total des dossiers", + "recently_added_accounts": "Comptes récemment ajoutés", + "from_last_month": "du mois dernier", + "create_project_details": "Créer les détails du projet", + "recent_activity": "Activité récente", + "keyboard_shortcuts": "Keyboard shortcuts", + "generate_password": "Générer un mot de passe", + "favourites": "Favourites", + "no_description": "Aucune description", + "notes": "Notes", + "confirm_delete_project": "Confirmer la suppression du projet", + "create_project": "Créer un projet", + "set_as_default_project": "Définir comme projet par défaut", + "project_name": "Nom du projet", + "describe_project_purpose": "Décrire l'objectif du projet", + "free_plan_limit": "Limite du plan gratuit", + "save_changes": "Sauvegarder les modifications", + "no_project_availbale": "Aucun projet disponible", + "cancel": "Annuler", + "delete": "Supprimer", + "add_favorite_tag": "Ajouter Favori Tag", + "remove_tag": "Supprimer {tag}", + "dashboard": "Tableau de bord", + "show_more": "Afficher plus", + "show_less": "Afficher moins", + "overview": "Aperçu", + "project": "Projet", + "notifications": "Notifications", + "settings": "Paramètres", + "logout": "Déconnexion", + "current_project": "Projet actuel", + "default": "Par défaut", + "no_project_selected": "Aucun projet sélectionné", + "quick_stats": "Statistiques rapides", + "workspaces": "Espaces de travail", + "projects": "Projets", + "available_features": "Fonctionnalités disponibles", + "team_members": "Membres de l'équipe", + "total_projects": "Total des projets", + "total_workspaces": "Total des espaces de travail", + "total_team_members": "Total des membres de l'équipe", + "total_records": "Total des enregistrements", + "my_account": "Mon compte", + "change_language": "Changer de langue", + "language": "Langue", + "toggle_theme": "Basculer le thème", + "wallet_passphrases": "Phrases secrètes de portefeuille", + "wi_fi": "Wi-Fi", + "env": "Variables d'environnement", + "env_variables": "Variables d'environnement", + "search": "Rechercher", + "create_new_project": "Créer un nouveau projet", + "add_new_project_description": "Ajouter un nouveau projet pour organiser vos données sécurisées", + "description": "Description", + "optional": "Optionnel", + "project_color": "Couleur du projet", + "enabled_modules": "Modules activés", + "edit_project": "Modifier le projet", + "update_project_details": "Mettre à jour les détails de votre projet", + "my_project": "Mon projet", + "creating": "Création...", + "delete_project": "Supprimer le projet", + "identity_coming_soon": "Le module d'identité est en cours de développement.", + "cards_coming_soon": "Le module de cartes est en cours de développement.", + "software_licenses_coming_soon": "Le module de licences logicielles est en cours de développement.", + "wifi_coming_soon": "Le module Wi-Fi est en cours de développement.", + "profile": "Profil", + "search_modules": "Rechercher des modules...", + "no_modules_found": "Aucun module trouvé", + "general": "Général", + "security_access": "Sécurité et accès", + "business_finance": "Entreprise et finance", + "licenses_services": "Licences et services", + "accounts": "Comptes", + "identity": "Identité", + "email": "E-mail", + "wifi": "WiFi", + "api_keys": "Clés API", + "ssh_keys": "Clés SSH", + "cards": "Cards", + "software_licenses": "Licences logicielles", + "recent_accounts": "Comptes récents", + "no_recent_accounts": "Aucun compte récent", + "last_24_hours": "Dernières 24 heures", + "no_accounts_create": "Aucun compte trouvé. Créez-en un pour commencer.", + "adjust_search_filter": "Essayez d'ajuster vos critères de recherche ou de filtre", + "no_recent_activity": "Aucune activité récente", + "no_projects_available": "Aucun projet disponible" + }, + "activity": { + "password_updated": "Mot de passe mis à jour", + "note_created": "Sécurisé note created", + "you_create_login": "Vous avez créé une nouvelle connexion", + "you_create_api_key": "Vous avez créé une nouvelle clé API", + "you_update_login": "Vous avez mis à jour une connexion", + "you_update_api_key": "Vous avez mis à jour une clé API", + "you_delete_login": "Vous avez supprimé une connexion", + "you_delete_api_key": "Vous avez supprimé une clé API", + "create_login": "Nouvelle connexion créée", + "create_api_key": "Nouvelle clé API créée", + "update_login": "Connexion mise à jour", + "update_api_key": "Clé API mise à jour", + "delete_login": "Connexion supprimée", + "delete_api_key": "Clé API supprimée" + }, + "time": { + "today": "Aujourd'hui", + "yesterday": "Hier", + "last_7_days": "7 derniers jours", + "last_30_days": "30 derniers jours", + "all_time": "Tout le temps", + "just_now": "À l'instant", + "hours_ago": "il y a {hours} heures", + "days_ago": "il y a {days} jours" + }, + "password_generator": { + "title": "Générateur de mots de passe", + "strong": "Fort", + "memorable": "Mémorable", + "pin": "PIN", + "passphrase": "Phrase secrète", + "chars": "caractères", + "digits": "chiffres", + "generate_new": "Générer un nouveau mot de passe", + "generate_password": "Générer un mot de passe", + "click_generate_button": "Cliquez sur le bouton Générer pour générer un nouveau mot de passe", + "copy": "Copier le mot de passe", + "copied": "Copié !", + "save": "Sauvegarder le mot de passe", + "close": "Fermer", + "copy_password": "Copier le mot de passe", + "password_length": "Longueur du mot de passe", + "options": "Options", + "advanced": "Avancé", + "advanced_options": "Options avancées", + "history": "Historique", + "uppercase": "Majuscules (A-Z)", + "lowercase": "Minuscules (a-z)", + "numbers": "Chiffres (0-9)", + "symbols": "Symboles (!@#$%^&*)", + "password_analysis": "Analyse du mot de passe", + "strong_enough": "Assez fort pour la plupart des sites", + "not_strong_enough": "Pas assez fort pour les comptes sensibles", + "good_length": "Bonne longueur", + "consider_longer": "Considérez utiliser un mot de passe plus long", + "using_all_types": "Utilisation de tous les types de caractères", + "consider_more_types": "Considérez utiliser plus de types de caractères", + "all_characters": "Tous les caractères", + "easy_to_read": "Facile à lire (pas de caractères similaires comme 1, l, I, 0, O)", + "easy_to_say": "Facile à dire (pas de chiffres ou caractères spéciaux)", + "password_tips": "Conseils pour les mots de passe", + "tip_min_length": "• Utilisez un minimum de 14 caractères pour une haute sécurité", + "tip_mix": "• Incluez un mélange de lettres, chiffres et symboles", + "tip_no_personal": "• Évitez d'utiliser des informations personnelles", + "tip_unique": "• Utilisez un mot de passe différent pour chaque compte", + "tip_manager": "• Considérez utiliser un gestionnaire de mots de passe pour stocker vos mots de passe", + "recently_generated": "Récemment généré", + "clear_history": "Effacer l'historique", + "no_history": "Aucun historique de mots de passe pour le moment", + "history_hint": "Les mots de passe générés apparaîtront ici", + "weak": "Faible", + "fair": "Correct", + "good": "Bon", + "strong_strength": "Fort", + "strength": "{strength}% de force", + "current_session": "Session actuelle", + "create_secure_passwords": "Créez des mots de passe sécurisés pour vos comptes" + }, + "user_settings": { + "title": "Paramètres utilisateur", + "description": "Gérez vos paramètres de compte et préférences", + "profile": "Profil", + "security": "Sécurité", + "notifications": "Notifications", + "integrations": "Intégrations", + "billing": "Facturation", + "logout": "Déconnexion", + "login_history": "Historique de connexion", + "audit_log": "Journal d'audit", + "profile_title": "Profil et informations du compte", + "profile_description": "Gérez vos détails personnels et de compte", + "language": "Langue", + "select_language": "Sélectionner la langue", + "email_address": "Adresse e-mail", + "full_name": "Nom complet", + "previous": "Précédent", + "next": "Suivant", + "page": "Page", + "of": "sur", + "saving": "Sauvegarde...", + "save_changes": "Sauvegarder les modifications", + "profile_picture": "Photo de profil", + "loading_history": "L'historique de connexion se charge...", + "no_history_available": "Aucun historique de connexion disponible", + "try_again": "Réessayer", + "device_browser": "Appareil et navigateur", + "ip_address": "Adresse IP", + "time": "Heure" + }, + "accounts": { + "no_accounts": "Non accounts", + "no_matching_accounts": "Non matching accounts found", + "loading_accounts": "Chargement accounts...", + "error_fetching_accounts": "Échec de fetch accounts", + "account_deleted_successfully": "Account deleted successfully", + "error_deleting_account": "Échec de delete account", + "account_updated_successfully": "Account updated successfully", + "all_fields_required": "Veuillez remplir tous les champs requis", + "account_already_exists": "An account with this name already exists", + "validation_error": "Données d'entrée invalides. Please check your entries", + "failed_to_add_account": "Échec de add account. Veuillez réessayer", + "missing_auth_or_selection": "Please log in or select a workspace and project", + "please_log_in": "Please log in to continue", + "server_error": "A server error occurred. Veuillez réessayer later", + "adding": "Ajoutering...", + "ascending": "Croissant", + "descending": "Décroissant", + "add": "Ajouter", + "cancel": "Annuler", + "account_name": "Account Nom", + "enter_account_name": "Entrer account name", + "username": "Nom d'utilisateur", + "enter_username": "Entrer nom d'utilisateur", + "add_account": "Ajouter Account", + "add_new_account": "Ajouter New Account", + "password": "Mot de passe", + "enter_password": "Saisir le mot de passe", + "website": "Site web", + "enter_website": "Saisir l'URL du site web", + "enter_website_url": "Saisir l'URL du site web", + "notes": "Notes", + "enter_notes": "Saisir les notes", + "tags": "Étiquettes", + "enter_tags_comma_separated": "Entrer tags (comma-separated)", + "accounts": "Comptes", + "manage_your_saved_accounts_and_passwords": "Manage your saved accounts and mot de passes", + "encryption_key_not_found": "Clé de chiffrement du projet non trouvée. Impossible de sauvegarder le compte.", + "project_not_found": "Projet non trouvé. Veuillez sélectionner un projet valide.", + "encryption_failed": "Échec du chiffrement des données du compte. Veuillez réessayer.", + "decryption_failed": "Échec du déchiffrement des données du compte. Cela pourrait être dû à une clé de chiffrement manquante ou invalide.", + "please_fill_all_required_fields": "Veuillez remplir tous les champs requis", + "select_project": "Select Projet", + "no_project_selected": "Non Projet Selected", + "please_select_a_project": "Please select a project to view accounts", + "loading": "Chargement...", + "error": "Erreur", + "try_again": "Try Again", + "no_accounts_found": "Non Comptes Found", + "no_accounts_exist": "Non accounts exist for this project", + "adjust_search_or_filter": "Adjust your search or filter to find accounts", + "generate_password": "Générer un mot de passe", + "account": "Account", + "category": "Category", + "last_modified": "Dernière modification", + "date_created": "Date de création", + "actions": "Actions", + "copy_username": "Copier Nom d'utilisateur", + "copied": "Copié", + "copy_password": "Copier le mot de passe", + "show_password": "Afficher Mot de passe", + "hide_password": "Masquer Mot de passe", + "edit": "Modifier", + "delete": "Supprimer", + "confirm_deletion": "Confirmer Deletion", + "confirm_delete_account": "Êtes-vous sûr you want to delete this account?", + "confirm_delete_account_description": "Cette action ne peut pas être annulée and will permanently delete the account credentials.", + "deleting": "Suppression...", + "showing": "Affichage", + "of": "sur", + "rows_per_page": "Lignes par page", + "clear_filters": "Effacer les filtres", + "accounts_not_found": "Comptes not found", + "account_not_found": "Account not found", + "failed_to_delete_account": "Échec de delete account", + "failed_to_update_account": "Échec de update account", + "updating": "Updating...", + "update": "Mettre à jour", + "view_details": "Voir Details", + "filter_warning": "Filtrers may affect pagination accuracy as server-side filtering is not supported.", + "search": "Rechercher", + "search_accounts": "Rechercher accounts", + "all_accounts": "Tous Comptes", + "personal": "Personnel", + "work": "Travail", + "finance": "Finance", + "clear": "Effacer", + "clear_sort": "Effacer", + "favorite": "Favori", + "no_accounts_create": "Aucun compte trouvé. Créez-en un pour commencer.", + "adjust_search_filter": "Essayez d'ajuster vos critères de recherche ou de filtre", + "error_loading_workspace": "Échec de load workspace data", + "focus_search": "Focus sur la recherche", + "switch_project": "Changer de projet", + "add_a_tag": "Ajouter une étiquette", + "edit_account": "Modifier le compte", + "update_account": "Mettre à jour le compte", + "filter_by_tag": "Filtrer par étiquette", + "confirm_delete": "Confirmer la suppression", + "delete_account_confirmation": "Êtes-vous sûr de vouloir supprimer ce compte ? Cette action ne peut pas être annulée.", + "sort": "Trier", + "sort_options": "Options de tri", + "sort_by_name": "Trier par nom", + "sort_by_created": "Trier par date de création", + "sort_by_updated": "Trier par date de modification" + }, + "api_keys": { + "title": "Clés API", + "description": "Manage your API keys for programmatic access to Zecrypt services.", + "edit_api_key": "Modifier API Key", + "search": "Rechercher", + "name": "Nom", + "key": "Key", + "api_key": "API Key", + "enter_api_key": "Entrer API key", + "type": "Type", + "created": "Créerd", + "expires": "Expire", + "status": "Statut", + "actions": "Actions", + "clear": "Effacer", + "sort": "Trier", + "last_used": "Environment", + "sort_options": "Options de tri", + "sort_by_name": "Trier par nom", + "sort_by_created": "Trier par date de création", + "sort_by_updated": "Trier par date de modification", + "ascending": "Croissant", + "descending": "Décroissant", + "clear_sort": "Effacer le tri", + "clear_filters": "Effacer les filtres", + "create_new_api_key": "Créer New API Key", + "enter_details_for_new_api_key": "Entrer the details for your new API Key", + "api_key_name": "API Key Nom", + "enter_api_key_name": "e.g., Production Retourend", + "key_type": "Key Type", + "key_preview": "Key Preview", + "key_preview_description": "The actual key will be générerd when you click \"Créer API Key\"", + "create_api_key": "Créer API Key", + "cancel": "Annuler", + "rows_per_page": "Lignes par page", + "add_api_key": "Ajouter API Key", + "no_api_keys_found": "Non Clés API Found", + "try_adjusting_search_or_filter": "Essayez d'ajuster vos critères de recherche ou de filtre", + "error_fetching_api_keys": "Échec de fetch API keys", + "failed_to_fetch_api_keys": "Unable to load API keys. Veuillez réessayer.", + "confirm_delete_api_key": "Êtes-vous sûr you want to delete this API key?", + "api_key_deleted_successfully": "API Key-phase-2 Key Supprimerd", + "api_key_deleted_description": "The API key has been successfully deleted.", + "error_deleting_api_key": "Échec de Supprimer API Key", + "failed_to_delete_api_key": "Unable to delete the API key. Veuillez réessayer.", + "api_key_updated_successfully": "API Key Mettre à jourd", + "api_key_updated_description": "The API key has been successfully updated.", + "api_key_added_successfully": "API Key Ajoutered", + "api_key_added_description": "The API key has been successfully added.", + "failed_to_update_api_key": "Unable to update the API key. Veuillez réessayer.", + "manage_your_api_keys": "Manage your API keys", + "showing": "Affichage", + "of": "sur", + "add_new_api_key": "Ajouter New API Key", + "add_new_api_key_description": "Entrer your API key details below", + "api_key_already_exists": "An API key with this name already exists.", + "error_adding_api_key": "Échec de add API key due to a server error.", + "failed_to_add_api_key": "Unable to add the API key. Veuillez réessayer.", + "error_updating_api_key": "Échec de update API key due to a server error.", + "invalid_input_data": "Données d'entrée invalides. Please check your entries.", + "network_error": "Erreur réseau. Please check your connection and try again.", + "no_api_keys_for_env": "Non API keys found for environment '{env}'. Try a different environment or clear filters.", + "no_api_keys_match_search": "Non API keys match the search '{search}'. Try adjusting your search.", + "adjust_search_filter": "Essayez d'ajuster vos critères de recherche ou de filtre.", + "copied": "Copié", + "api_key_copied": "The API key has been copié to your clipboard.", + "copy_failed": "Copier Failed", + "failed_to_copy_api_key": "Unable to copy the API key to clipboard.", + "show_key": "Afficher Key", + "hide_key": "Masquer Key", + "copy_key": "Copier Key", + "edit": "Modifier", + "delete": "Supprimer", + "decryption_error": "Decryption error - please edit to update format", + "adding": "Ajoutering...", + "updating": "Updating", + "no_project_selected": "Non project selected. Please select a project to continue.", + "please_fill_all_required_fields": "Veuillez remplir tous les champs requis", + "loading_api_keys": "Chargement API keys...", + "api_keys": "Clés API", + "env": "Environment", + "tags": "Étiquettes", + "last_modified": "Dernière modification", + "api_key_description": "API Key Description", + "enter_description": "Entrer a description for this API key", + "environment": "Environment", + "select_environment": "Select environment", + "development": "Development", + "staging": "Staging", + "production": "Production", + "testing": "Testing", + "local": "Local", + "uat": "UAT", + "add": "Ajouter", + "add_a_tag": "Ajouter une étiquette", + "update_api_key": "Mettre à jour API Key", + "all_envs": "Tous Environments", + "search_across_all_fields": "Rechercher", + "delete_api_key_confirmation": "Êtes-vous sûr you want to delete this API key? Cette action ne peut pas être annulée.", + "deleting": "Suppression...", + "confirm_deletion": "Confirmer Deletion", + "edit_api_key_description": "Mettre à jour your API key details", + "showing_results": "Affichering {startIdx}-{endIdx} sur {totalNombre} résultats", + "notes": "Notes", + "enter_notes": "Saisir les notes", + "filter_by_env": "Filtrer by environment", + "date_created": "Date de création" + }, + "wallet_passphrases": { + "wallet_passphrases": "Phrases secrètes de portefeuille", + "securely_store_and_manage_your_wallet_recovery_phrases": "Sécurisély store and manage your wallet recovery phrases", + "add_passphrase": "Ajouter Phrase secrète", + "enter_wallet_address": "Entrer wallet address", + "copy_wallet_address": "Copier wallet address", + "wallet_address": "Wallet Ajouterress", + "add_your_first_passphrase": "Ajouter Your First Phrase secrète", + "search_passphrases": "Rechercher", + "no_wallet_passphrases_found": "Non Wallet Phrase secrète Found", + "wallet_address_encryption_note": "Wallet address is encrypted for security", + "no_passphrases_found": "Non Phrase secrètes Found", + "no_passphrases_found_message": "You haven't added any wallet phrase secrètes yet. Ajouter one to get started.", + "no_passphrases_found_search": "Non phrase secrètes match your search criteria.", + "loading_passphrases": "Chargement phrase secrètes...", + "please_wait_while_we_fetch_your_secure_passphrases": "Veuillez patienter while we fetch your secure phrase secrètes", + "error_loading_passphrases": "Erreur Chargement Phrase secrètes", + "failed_to_load_passphrases": "Échec de load phrase secrètes. Veuillez réessayer.", + "passphrase_added_successfully": "Phrase secrète Ajoutered Succèsfully", + "passphrase_added_description": "Your wallet phrase secrète has been securely stored.", + "passphrase_updated_successfully": "Phrase secrète Mettre à jourd Succèsfully", + "passphrase_updated_description": "Your wallet phrase secrète has been updated.", + "failed_to_add_passphrase": "Échec de Ajouter Phrase secrète", + "failed_to_update_passphrase": "Échec de Mettre à jour Phrase secrète", + "wallet_phrase_already_exists": "This phrase secrète or name already exists in your wallet", + "invalid_input_data": "Données d'entrée invalides. Please check your entries.", + "no_project_selected": "Non project selected. Please select a project.", + "passphrase_required": "Phrase secrète is required", + "passphrase_must_be_exactly_12_words": "Phrase secrète must contain exactly 12 words (currently {count})", + "please_fill_all_required_fields": "Veuillez remplir tous les champs requis", + "name": "Nom", + "add": "Ajouter", + "sort": "Trier", + "sort_by_name": "Trier par nom", + "sort_by_created": "Trier par date de création", + "sort_by_updated": "Trier par date de modification", + "sort_options": "Options de tri", + "enter_passphrase_name": "Entrer phrase secrète name", + "passphrase": "Phrase secrète", + "enter_wallet_recovery_phrase": "Entrer wallet recovery phrase", + "notes": "Notes", + "enter_notes": "Saisir les notes", + "wallet_type": "Wallet Type", + "select_wallet_type": "Select wallet type", + "tags": "Étiquettes", + "add_a_tag": "Ajouter une étiquette", + "save_passphrase": "Sauvegarder Phrase secrète", + "update_passphrase": "Mettre à jour Phrase secrète", + "passphrase_encryption_note": "Your phrase secrète will be encrypted for security.", + "adding": "Ajoutering...", + "updating": "Updating...", + "search_across_all_fields": "Rechercher", + "filter_by_wallet_type": "Filtrer by wallet type", + "all_wallet_types": "Tous Wallet Types", + "clear_filters": "Effacer les filtres", + "last_modified": "Dernière modification", + "actions": "Actions", + "hide_passphrase": "Masquer phrase secrète", + "show_passphrase": "Afficher phrase secrète", + "copy_passphrase": "Copier phrase secrète", + "edit": "Modifier", + "delete": "Supprimer", + "showing_results": "Affichering {startIdx}-{endIdx} sur {totalNombre} résultats", + "confirm_deletion": "Confirmer Deletion", + "delete_passphrase_confirmation": "Êtes-vous sûr you want to delete this wallet phrase secrète? Cette action ne peut pas être annulée.", + "deleting": "Suppression...", + "add_new_passphrase_description": "Entrer your wallet phrase secrète details below", + "edit_passphrase": "Modifier Phrase secrète", + "edit_passphrase_description": "Mettre à jour your wallet phrase secrète details", + "copied": "Copié", + "passphrase_copied": "Phrase secrète copié to clipboard", + "copy_failed": "Échec de la copie", + "failed_to_copy_passphrase": "Échec de copy phrase secrète to clipboard", + "cancel": "Annuler", + "date_created": "Date de création" + }, + "identity": { + "title": "Identité", + "description": "Manage your personal identity information securely", + "add_identity": "Ajouter Identité", + "no_identities_found": "Non identities found", + "no_identities_found_message": "You haven't added any identity information yet.", + "add_your_first_identity": "Ajouter Your First Identité", + "loading_identities": "Chargement identities...", + "search_identities": "Rechercher", + "full_name": "Nom complet", + "country": "Nombrery", + "enter_full_name": "Entrer full name", + "date_of_birth": "Date sur Birth", + "enter_date_of_birth": "Entrer date sur birth (YYYY-MM-DD)", + "address": "Ajouterress", + "enter_address": "Entrer address", + "phone_number": "Phone Number", + "enter_phone_number": "Entrer numéro de téléphone number", + "identity_number": "Identité Number", + "enter_identity_number": "Entrer identity number (e.g., passport, ID)", + "tags": "Étiquettes", + "enter_tags_comma_separated": "Entrer tags (comma-separated)", + "last_modified": "Dernière modification", + "actions": "Actions", + "edit": "Modifier", + "delete": "Supprimer", + "copy_field": "Copier {field}", + "copied": "Copié", + "identity_deleted_successfully": "Identité deleted successfully", + "error_deleting_identity": "Échec de delete identity", + "identity_updated_successfully": "Identité updated successfully", + "failed_to_add_identity": "Échec de add identity", + "all_fields_required": "Veuillez remplir tous les champs requis", + "validation_error": "Données d'entrée invalides. Please check your entries", + "server_error": "A server error occurred. Veuillez réessayer later", + "identities": "Identities", + "filter_by_tag": "Filtrer par étiquette", + "all_tags": "Toutes les étiquettes", + "search": "Rechercher", + "clear_filters": "Effacer les filtres", + "name": "Nom", + "contact": "Contact", + "passport": "Passport", + "national_id": "National ID", + "add_new_identity": "Ajouter New Identité", + "add_new_identity_description": "Entrer your identity details below", + "first_name": "First Nom", + "last_name": "Last Nom", + "email": "E-mail", + "phone": "Phone", + "passport_number": "Passport Number", + "cancel": "Annuler", + "save": "Sauvegarder", + "edit_identity": "Modifier Identité", + "edit_identity_description": "Mettre à jour your identity details", + "update": "Mettre à jour", + "identity_deleted": "Identité deleted", + "identity_deleted_description": "The identity has been deleted successfully", + "field_copied": "Field copié to clipboard", + "copy_failed": "Échec de la copie", + "failed_to_copy_field": "Échec de copy field to clipboard", + "hide_sensitive_data": "Masquer sensitive data", + "show_sensitive_data": "Afficher sensitive data", + "showing_results": "Affichering {startIdx}-{endIdx} sur {totalNombre} résultats", + "page_of": "Page {currentPage} sur {totalPages}", + "copy_passport": "Copier passport number", + "copy_national_id": "Copier national ID", + "identity_added_successfully": "Identité added successfully", + "error_adding_identity": "Erreur adding identity", + "error_updating_identity": "Erreur updating identity", + "missing_workspace_project": "Missing workspace or project", + "first_name_required": "First name is required", + "last_name_required": "Last name is required", + "invalid_email": "Invalid adresse e-mail format", + "invalid_date_format": "Invalid date format (YYYY-MM-DD)", + "title_required": "Titre is required", + "title_placeholder": "Personnel Identité", + "first_name_placeholder": "John", + "last_name_placeholder": "Doe", + "email_placeholder": "john.doe@example.com", + "phone_placeholder": "+1 (555) 123-4567", + "address_placeholder": "123 Main St, Anytown, CA 94001", + "passport_placeholder": "A12345678", + "national_id_placeholder": "123-45-6789", + "notes": "Notes", + "notes_placeholder": "Ajouteritional information", + "tags_placeholder": "Ajouter tags", + "adding": "Ajoutering...", + "updating": "Updating...", + "deleting": "Suppression...", + "add": "Ajouter", + "confirm_deletion": "Confirmer Deletion", + "delete_identity_confirmation": "Êtes-vous sûr you want to delete this identity? Cette action ne peut pas être annulée.", + "error_fetching_identities": "Erreur fetching identities", + "national_id_required": "National ID is required", + "identity_type": "Identité Type", + "sort": "Trier", + "sort_options": "Options de tri", + "sort_by_name": "Trier par nom", + "sort_by_created": "Trier par date de création", + "sort_by_updated": "Trier par date de modification", + "ascending": "Croissant", + "descending": "Décroissant", + "clear_sort": "Effacer le tri", + "search_across_all_fields": "Rechercher", + "searches_all_fields": "Rechercheres name, type, contact, address, ID and tags", + "encryption_key_not_found": "Projet encryption key not found. Cannot save identity.", + "project_not_found": "Projet non trouvé. Veuillez sélectionner un projet valide.", + "encryption_failed": "Échec de encrypt identity data. Veuillez réessayer.", + "decryption_failed": "Échec de decrypt identity data. This could be due to a missing or invalid encryption key.", + "manage_your_personal_identities": "Manage your personal identity information securely", + "date_created": "Date de création" + }, + "cards": { + "title": "Cards", + "description": "Manage your credit and debit card information securely", + "add_card": "Ajouter Card", + "no_cards_found": "Non cards found", + "no_cards_found_message": "You haven't added any card information yet.", + "add_your_first_card": "Ajouter Your First Card", + "loading_cards": "Chargement cards...", + "search_cards": "Rechercher", + "card_name": "Card Nom", + "name": "Nom", + "enter_card_name": "Entrer card name (e.g., Visa Finiring 1234)", + "card_number": "Card Number", + "enter_card_number": "Entrer card number", + "expiry_date": "Expiry Date", + "enter_expiry_date": "Entrer expiry date (MM/YY)", + "cvv": "CVV", + "enter_cvv": "Entrer CVV", + "cardholder_name": "Cardholder Nom", + "card_holder_name": "Card Holder Nom", + "enter_cardholder_name": "Entrer cardholder name", + "tags": "Étiquettes", + "enter_tags_comma_separated": "Entrer tags (comma-separated)", + "last_modified": "Dernière modification", + "actions": "Actions", + "edit": "Modifier", + "delete": "Supprimer", + "copy_field": "Copier {field}", + "copied": "Copié", + "card_deleted_successfully": "Card deleted successfully", + "error_deleting_card": "Échec de delete card", + "card_updated_successfully": "Card updated successfully", + "card_added_successfully": "Card added successfully", + "failed_to_add_card": "Échec de add card", + "all_fields_required": "Veuillez remplir tous les champs requis", + "validation_error": "Données d'entrée invalides. Please check your entries", + "server_error": "A server error occurred. Veuillez réessayer later", + "credit_cards": "Cartes de crédit", + "filter_by_brand": "Filtrer by brand", + "all_brands": "Tous Brands", + "search": "Rechercher", + "clear_filters": "Effacer les filtres", + "card_holder": "Card Holder", + "brand": "Brand", + "expiry": "Expiry", + "add_new_card": "Ajouter New Card", + "add_new_card_description": "Entrer your credit card details below", + "select_brand": "Select brand", + "expiry_month": "Expiry Month", + "expiry_year": "Expiry Year", + "cancel": "Annuler", + "save": "Sauvegarder", + "edit_card": "Modifier Card", + "edit_card_description": "Mettre à jour your credit card details", + "update": "Mettre à jour", + "card_deleted": "Card deleted", + "card_deleted_description": "The card has been deleted successfully", + "field_copied": "Field copié to clipboard", + "copy_failed": "Échec de la copie", + "failed_to_copy_field": "Échec de copy field to clipboard", + "hide_card_number": "Masquer card number", + "show_card_number": "Afficher card number", + "copy_to_clipboard": "Copier to clipboard", + "showing_results": "Affichering {startIdx}-{endIdx} sur {totalNombre} résultats", + "page_of": "Page {currentPage} sur {totalPages}", + "title_placeholder": "Personnel Visa Card", + "card_holder_name_placeholder": "John Doe", + "card_number_placeholder": "4111 1111 1111 1111", + "notes": "Notes", + "notes_placeholder": "Ajouteritional information about this card", + "tags_placeholder": "Ajouter tags", + "add": "Ajouter", + "adding": "Ajoutering...", + "updating": "Updating...", + "deleting": "Suppression...", + "confirm_delete": "Confirmer Deletion", + "delete_card_confirmation": "Êtes-vous sûr you want to delete this card? Cette action ne peut pas être annulée.", + "error_fetching_cards": "Erreur fetching cards", + "missing_workspace_project": "Missing workspace or project", + "title_required": "Titre is required", + "card_holder_name_required": "Card holder name is required", + "card_number_required": "Card number is required", + "invalid_card_number": "Invalid card number", + "cvv_required": "CVV is required", + "invalid_cvv": "Invalid CVV", + "brand_required": "Brand is required", + "error_adding_card": "Erreur adding card", + "error_updating_card": "Erreur updating card", + "other": "Other", + "filter_by_tag": "Filtrer par étiquette", + "all_tags": "Toutes les étiquettes", + "sort": "Trier", + "sort_options": "Options de tri", + "sort_by_name": "Trier par nom", + "sort_by_created": "Trier par date de création", + "sort_by_updated": "Trier par date de modification", + "ascending": "Croissant", + "descending": "Décroissant", + "clear_sort": "Effacer le tri", + "search_across_all_fields": "Rechercher", + "searches_all_fields": "Rechercheres title, card holder, card number, brand and tags", + "missing_auth_or_selection": "Missing authentication or selection", + "encryption_key_not_found": "Projet encryption key not found. Cannot save card.", + "project_not_found": "Projet non trouvé. Veuillez sélectionner un projet valide.", + "encryption_failed": "Échec de encrypt card data. Veuillez réessayer.", + "decryption_failed": "Échec de decrypt card data. This could be due to a missing or invalid encryption key.", + "manage_your_cards": "Manage your credit and debit card information securely", + "date_created": "Date de création" + }, + "wifi": { + "wifi_networks": "Wi-Fi Networks", + "manage_your_wifi_networks": "Manage your Wi-Fi networks securely.", + "add_wifi": "Ajouter Wi-Fi", + "add_new_wifi": "Ajouter New Wi-Fi", + "add_new_wifi_description": "Entrer your Wi-Fi network details below", + "edit_wifi": "Modifier Wi-Fi", + "edit_wifi_description": "Mettre à jour your Wi-Fi network details", + "search_wifi": "Rechercher", + "filter_by_security": "Filtrer by security", + "all_security_types": "Tous Sécurité Types", + "none": "Aucun", + "clear": "Effacer", + "clear_filters": "Effacer les filtres", + "loading_wifi": "Chargement Wi-Fi networks...", + "no_wifi_found": "Non Wi-Fi networks found", + "no_wifi_for_security_type": "Non Wi-Fi networks found for {security_type}", + "no_wifi_match_search": "Non Wi-Fi networks match your search '{search}'", + "name": "Nom", + "loading_wifi_networks": "Chargement Wifi networks", + "no_wifi_networks_found": "Non Wifi Network Found", + "add_wifi_network": "Ajouter Wifi Network", + "no_wifi_for_tag": "Non WiFi networks found with tag {tag}", + "adjust_search_filter": "Try adjusting your search or filter to find what you're looking for.", + "ssid": "SSID", + "security": "Sécurité", + "security_type": "Sécurité Type", + "select_security_type": "Select security type", + "notes": "Notes", + "enter_notes": "Saisir les notes", + "password": "Mot de passe", + "enter_password": "Saisir le mot de passe", + "enter_new_password": "Entrer new mot de passe (leave blank to keep unchanged)", + "show_password": "Afficher le mot de passe", + "hide_password": "Masquer le mot de passe", + "copy_password": "Copier le mot de passe", + "tags": "Étiquettes", + "add_a_tag": "Ajouter une étiquette", + "add": "Ajouter", + "last_modified": "Dernière modification", + "date_created": "Date de création", + "actions": "Actions", + "showing": "Affichage", + "of": "sur", + "rows_per_page": "Lignes par page", + "edit": "Modifier", + "copy_ssid": "Copier SSID", + "delete": "Supprimer", + "confirm_delete_wifi": "Confirmer Supprimer WiFi", + "delete_wifi_confirmation": "Êtes-vous sûr you want to delete this WiFi network? Cette action ne peut pas être annulée.", + "wifi_deleted": "WiFi Supprimerd", + "wifi_deleted_description": "The WiFi network has been successfully deleted.", + "failed_to_delete_wifi": "Échec de delete WiFi network", + "error_deleting_wifi": "Erreur deleting WiFi", + "error_fetching_wifi_networks": "Erreur fetching WiFi networks", + "failed_to_fetch_wifi_networks": "Échec de fetch WiFi networks", + "no_project_selected": "Non project selected. Please select a project to proceed.", + "copied": "Copié", + "field_copied": "Field copié to clipboard", + "copy_failed": "Échec de la copie", + "failed_to_copy_field": "Échec de copy field to clipboard", + "show_qr_code": "Afficher QR Code", + "wifi_qr_code": "WiFi QR Code", + "wifi_qr_code_description": "Scan this QR code to connect to the WiFi network.", + "qr_code_placeholder": "QR Code", + "wifi_credentials": "WiFi Credentials", + "close": "Fermer", + "please_fill_all_required_fields": "Veuillez remplir tous les champs requis", + "wifi_added_successfully": "WiFi added successfully", + "wifi_added_description": "The WiFi network has been added.", + "wifi_already_exists": "A WiFi network with this SSID already exists", + "failed_to_add_wifi": "Échec de add WiFi network", + "enter_ssid": "Entrer SSID", + "update_wifi": "Mettre à jour WiFi", + "wifi_updated_successfully": "WiFi updated successfully", + "wifi_updated_description": "The WiFi network has been updated.", + "wifi_not_found": "WiFi network not found", + "failed_to_update_wifi": "Échec de update WiFi network", + "invalid_input_data": "Données d'entrée invalides", + "adding": "Ajoutering...", + "updating": "Updating...", + "deleting": "Suppression...", + "cancel": "Annuler", + "encryption_key_not_found": "Projet encryption key not found. Cannot save WiFi network.", + "project_not_found": "Projet non trouvé. Veuillez sélectionner un projet valide.", + "encryption_failed": "Échec de encrypt WiFi data. Veuillez réessayer.", + "decryption_failed": "Échec de decrypt WiFi data. This could be due to a missing or invalid encryption key.", + "sort": "Trier", + "sort_options": "Options de tri", + "sort_by_name": "Trier par nom", + "sort_by_created": "Trier par date de création", + "sort_by_updated": "Trier par date de modification", + "ascending": "Croissant", + "descending": "Décroissant", + "clear_sort": "Effacer le tri", + "filter_by_tag": "Filtrer par étiquette", + "all_tags": "Toutes les étiquettes", + "scan_with_phone": "Scan this QR code with your numéro de téléphone to connect to the network" + }, + "key_shortcuts": { + "title": "Raccourcis clavier" + }, + "notes": { + "add_new_note": "Ajouter New Nonte", + "note_title": "Nonte Titre", + "note_content": "Nonte Content", + "tags": "Étiquettes", + "update_note": "Mettre à jour Nonte", + "enter_note_title": "Entrer note title", + "add_a_tag": "Ajouter une étiquette", + "add_note": "Ajouter Nonte", + "cancel": "Annuler" + }, + "emails": { + "email_accounts": "Comptes e-mail", + "add_email": "Ajouter E-mail", + "search_email_accounts": "Rechercher", + "per_page": "par page", + "clear_filters": "Effacer les filtres", + "title": "Titre", + "email_address": "Adresse e-mail", + "servers": "Servers", + "username": "Nom d'utilisateur", + "password": "Mot de passe", + "last_modified": "Dernière modification", + "tags": "Étiquettes", + "loading_emails": "Chargement E-mails", + "actions": "Actions", + "loading": "Chargement...", + "no_results_found": "Aucun résultat trouvé", + "no_email_accounts": "Non adresse e-mail accounts", + "copy": "Copier", + "copy_to_clipboard": "Copier to clipboard", + "show_password": "Afficher le mot de passe", + "hide_password": "Masquer le mot de passe", + "open_menu": "Ouvrir le menu", + "edit": "Modifier", + "delete": "Supprimer", + "deleted": "Supprimé", + "showing_results": "Affichering {start} to {end} sur {total} résultats", + "confirm_delete": "Confirmer la suppression", + "delete_email_confirmation": "Êtes-vous sûr you want to delete this adresse e-mail account? Cette action ne peut pas être annulée.", + "cancel": "Annuler", + "deleting": "Suppression...", + "copied": "Copié", + "field_copied": "Value copié to clipboard", + "copy_failed": "Échec de la copie", + "failed_to_copy_value": "Échec de copy value to clipboard", + "encryption_error": "Erreur de chiffrement", + "could_not_encrypt_data": "Impossible de chiffrer les données. Veuillez réessayer.", + "success": "Succès", + "email_added_successfully": "E-mail account added successfully", + "error_adding_email": "Erreur adding adresse e-mail account. Veuillez réessayer.", + "email_deleted_successfully": "E-mail credentials deleted successfully", + "failed_to_delete_email": "Échec de delete adresse e-mail credentials", + "error_fetching_emails": "Échec de fetch adresse e-mails", + "error": "Erreur", + "sort": "Trier", + "sort_options": "Options de tri", + "sort_by_name": "Trier par nom", + "sort_by_created": "Trier par date de création", + "sort_by_updated": "Trier par date de modification", + "ascending": "Croissant", + "descending": "Décroissant", + "clear_sort": "Effacer le tri", + "add_email_description": "Ajouter a new adresse e-mail account to your secure vault", + "title_placeholder": "Travail E-mail", + "email_address_placeholder": "your.adresse e-mail@example.com", + "imap_server": "IMAP Server", + "imap_server_placeholder": "imap.example.com", + "smtp_server": "SMTP Server", + "smtp_server_placeholder": "smtp.example.com", + "username_placeholder": "Optionnel, defaults to adresse e-mail address", + "password_placeholder": "Saisir le mot de passe", + "notes": "Notes (optionnel)", + "notes_placeholder": "Ajouteritional information about this adresse e-mail account", + "add_tag": "Ajouter une étiquette", + "remove": "Supprimer", + "adding": "Ajoutering...", + "add": "Ajouter", + "please_fill_all_required_fields": "Veuillez remplir tous les champs requis", + "email_title_already_exists": "E-mail with this title already exists", + "invalid_email_format": "Invalid adresse e-mail format", + "missing_required_data": "Données de projet requises manquantes", + "edit_email": "Modifier E-mail Account", + "edit_email_description": "Mettre à jour your adresse e-mail account details", + "save_changes": "Sauvegarder les modifications", + "email_updated_successfully": "E-mail account updated successfully", + "error_updating_email": "Erreur updating adresse e-mail account. Veuillez réessayer.", + "manage_your_email_accounts": "Manage your adresse e-mail accounts and server settings securely", + "date_created": "Date de création", + "filter_by_tag": "Filtrer par étiquette", + "all_tags": "Toutes les étiquettes" + }, + "ssh_keys": { + "ssh_keys": "Clés SSH", + "add_ssh_key": "Ajouter SSH Key", + "search_ssh_keys": "Rechercher", + "per_page": "par page", + "clear_filters": "Effacer les filtres", + "name": "Nom", + "ssh_key": "SSH Key", + "tags": "Étiquettes", + "loading_ssh_keys": "Chargement Clés SSH", + "actions": "Actions", + "loading": "Chargement...", + "no_results_found": "Aucun résultat trouvé", + "no_ssh_keys": "Non SSH keys", + "hide_ssh_key": "Masquer SSH key", + "show_ssh_key": "Afficher full SSH key", + "copied": "Copié", + "ssh_key_copied": "SSH key copié to clipboard", + "copy_failed": "Échec de la copie", + "failed_to_copy_ssh_key": "Échec de copy SSH key to clipboard", + "copy_to_clipboard": "Copier to clipboard", + "open_menu": "Ouvrir le menu", + "edit": "Modifier", + "delete": "Supprimer", + "fingerprint": "Fingerprint", + "last_modified": "Dernière modification", + "deleted": "Supprimé", + "showing_results": "Affichering {start} to {end} sur {total} résultats", + "confirm_delete": "Confirmer la suppression", + "delete_ssh_key_confirmation": "Êtes-vous sûr you want to delete this SSH key? Cette action ne peut pas être annulée.", + "cancel": "Annuler", + "deleting": "Suppression...", + "please_fill_all_required_fields": "Veuillez remplir tous les champs requis", + "ssh_key_name_already_exists": "SSH key with this name already exists", + "missing_required_data": "Données de projet requises manquantes", + "encryption_error": "Erreur de chiffrement", + "could_not_encrypt_data": "Impossible de chiffrer les données. Veuillez réessayer.", + "success": "Succès", + "ssh_key_added_successfully": "SSH key added successfully", + "error_adding_ssh_key": "Erreur adding SSH key. Veuillez réessayer.", + "ssh_key_updated_successfully": "SSH key updated successfully", + "error_updating_ssh_key": "Erreur updating SSH key. Veuillez réessayer.", + "ssh_key_deleted_successfully": "SSH key deleted successfully", + "error_deleting_ssh_key": "Erreur deleting SSH key", + "error_fetching_ssh_keys": "Échec de fetch SSH keys", + "add_a_tag": "Ajouter une étiquette", + "sort": "Trier", + "sort_options": "Options de tri", + "sort_by_name": "Trier par nom", + "sort_by_created": "Trier par date de création", + "sort_by_updated": "Trier par date de modification", + "ascending": "Croissant", + "descending": "Décroissant", + "clear_sort": "Effacer le tri", + "error": "Erreur", + "add_ssh_key_description": "Ajouter a new SSH key to your secure vault", + "name_placeholder": "GitHub SSH Key", + "ssh_key_placeholder": "Coller your SSH prinuméro de TVAe key here", + "notes": "Notes (optionnel)", + "notes_placeholder": "Ajouteritional information about this SSH key", + "add_tag": "Ajouter une étiquette", + "remove": "Supprimer", + "adding": "Ajoutering...", + "add": "Ajouter", + "edit_ssh_key": "Modifier SSH Key", + "edit_ssh_key_description": "Mettre à jour your SSH key details", + "save_changes": "Sauvegarder les modifications", + "search_licenses": "Rechercher", + "manage_your_ssh_keys": "Manage your SSH keys securely", + "filter_by_tag": "Filtrer par étiquette", + "all_tags": "Toutes les étiquettes", + "date_created": "Date de création" + }, + "files": { + "files": "Fichiers", + "file_manager": "Gestionnaire de fichiers", + "add_file": "Télécharger un fichier", + "create_folder": "Créer un dossier", + "search_files": "Rechercher des fichiers", + "per_page": "par page", + "clear_filters": "Effacer les filtres", + "name": "Nom", + "size": "Taille", + "type": "Type", + "modified": "Modifié", + "folder": "Dossier", + "file": "Fichier", + "loading_files": "Chargement des fichiers...", + "actions": "Actions", + "loading": "Chargement...", + "no_results_found": "Aucun résultat trouvé", + "no_files": "Aucun fichier", + "no_folders": "Aucun dossier", + "download": "Télécharger", + "rename": "Renommer", + "move": "Déplacer", + "delete": "Supprimer", + "copied": "Copié", + "file_copied": "Fichier copié dans le presse-papiers", + "copy_failed": "Échec de la copie", + "failed_to_copy_file": "Échec de copy file to clipboard", + "copy_to_clipboard": "Copier to clipboard", + "open_menu": "Ouvrir le menu", + "edit": "Modifier", + "deleted": "Supprimé", + "showing_results": "Affichering {start} to {end} sur {total} résultats", + "confirm_delete": "Confirmer la suppression", + "delete_file_confirmation": "Êtes-vous sûr de vouloir supprimer ce fichier ? Cette action ne peut pas être annulée.", + "delete_folder_confirmation": "Êtes-vous sûr de vouloir supprimer ce dossier ? Cette action ne peut pas être annulée.", + "cancel": "Annuler", + "deleting": "Suppression...", + "please_fill_all_required_fields": "Veuillez remplir tous les champs requis", + "file_name_already_exists": "Un fichier avec ce nom existe déjà dans ce dossier", + "folder_name_already_exists": "Un dossier avec ce nom existe déjà dans cet emplacement", + "missing_required_data": "Données de projet requises manquantes", + "encryption_error": "Erreur de chiffrement", + "could_not_encrypt_data": "Impossible de chiffrer les données. Veuillez réessayer.", + "success": "Succès", + "file_uploaded_successfully": "Fichier téléchargé avec succès", + "error_uploading_file": "Erreur lors du téléchargement du fichier. Veuillez réessayer.", + "file_updated_successfully": "Fichier mis à jour avec succès", + "error_updating_file": "Erreur lors de la mise à jour du fichier. Veuillez réessayer.", + "file_deleted_successfully": "Fichier supprimé avec succès", + "error_deleting_file": "Erreur lors de la suppression du fichier", + "error_fetching_files": "Échec de la récupération des fichiers", + "folder_created_successfully": "Dossier créé avec succès", + "error_creating_folder": "Erreur lors de la création du dossier. Veuillez réessayer.", + "folder_updated_successfully": "Dossier mis à jour avec succès", + "error_updating_folder": "Erreur lors de la mise à jour du dossier. Veuillez réessayer.", + "folder_deleted_successfully": "Dossier supprimé avec succès", + "error_deleting_folder": "Erreur lors de la suppression du dossier", + "error_fetching_folders": "Échec de la récupération des dossiers", + "file_too_large": "La taille du fichier dépasse la limite de 50MB", + "invalid_file_type": "Type de fichier invalide", + "upload_failed": "Échec du téléchargement", + "download_failed": "Échec du téléchargement", + "sort": "Trier", + "sort_options": "Options de tri", + "sort_by_name": "Trier par nom", + "sort_by_size": "Trier par taille", + "sort_by_modified": "Trier par date de modification", + "sort_by_type": "Trier par type", + "ascending": "Croissant", + "descending": "Décroissant", + "clear_sort": "Effacer le tri", + "error": "Erreur", + "upload_file_description": "Télécharger un fichier vers votre coffre-fort sécurisé", + "create_folder_description": "Créer un nouveau dossier pour organiser vos fichiers", + "name_placeholder": "Saisir le nom du fichier ou dossier", + "folder_name_placeholder": "Saisir le nom du dossier", + "notes": "Notes (optionnel)", + "notes_placeholder": "Informations supplémentaires sur ce fichier", + "add_tag": "Ajouter une étiquette", + "remove": "Supprimer", + "uploading": "Téléchargement...", + "creating": "Création...", + "add": "Ajouter", + "upload": "Télécharger", + "create": "Créer", + "edit_file": "Modifier le fichier", + "edit_file_description": "Mettre à jour les détails de votre fichier", + "edit_folder": "Modifier le dossier", + "edit_folder_description": "Mettre à jour les détails de votre dossier", + "save_changes": "Sauvegarder les modifications", + "manage_your_files": "Gérez vos fichiers en toute sécurité", + "filter_by_type": "Filtrer par type", + "all_types": "Tous les types", + "date_created": "Date de création", + "file_size": "Taille du fichier", + "folder_path": "Chemin du dossier", + "select_folder": "Sélectionner le dossier", + "move_to_folder": "Déplacer vers le dossier", + "drag_and_drop_files": "Glissez-déposez les fichiers ici, ou cliquez pour sélectionner", + "select_files": "Sélectionner les fichiers", + "upload_progress": "Progression du téléchargement", + "upload_complete": "Téléchargement terminé", + "bytes": "octets", + "kb": "Ko", + "mb": "Mo", + "gb": "Go", + "tb": "To" + }, + "project_colors": { + "red": "Rouge", + "orange": "Orange", + "amber": "Ambre", + "yellow": "Jaune", + "lime": "Citron vert", + "green": "Vert", + "emerald": "Émeraude", + "teal": "Sarcelle", + "cyan": "Cyan", + "sky": "Ciel", + "blue": "Bleu", + "indigo": "Indigo", + "violet": "Violet", + "purple": "Violet", + "fuchsia": "Fuchsia", + "pink": "Rose", + "rose": "Rose" + } +} \ No newline at end of file diff --git a/packages/frontend-web/middleware.ts b/packages/frontend-web/middleware.ts index e80547b8..a51446cc 100644 --- a/packages/frontend-web/middleware.ts +++ b/packages/frontend-web/middleware.ts @@ -3,7 +3,7 @@ import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; // Available locales -export const locales = ['en', 'de']; +export const locales = ['en', 'de', 'fr']; export const defaultLocale = 'en'; // Protected routes that require authentication From 5b50b306678dde75db233b8c6d10b1688fde484d Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Wed, 24 Sep 2025 11:18:48 +0530 Subject: [PATCH 04/32] Spanish --- packages/frontend-web/messages/de/common.json | 1921 ++++++++--------- packages/frontend-web/messages/en/common.json | 3 +- packages/frontend-web/messages/es/common.json | 1426 ++++++++++++ packages/frontend-web/messages/fr/common.json | 4 +- packages/frontend-web/middleware.ts | 2 +- 5 files changed, 2351 insertions(+), 1005 deletions(-) create mode 100644 packages/frontend-web/messages/es/common.json diff --git a/packages/frontend-web/messages/de/common.json b/packages/frontend-web/messages/de/common.json index 26762965..56561acb 100644 --- a/packages/frontend-web/messages/de/common.json +++ b/packages/frontend-web/messages/de/common.json @@ -1,34 +1,103 @@ { "app": { "title": "Zecrypt", - "description": "Schützen Sie Ihre Daten mit Zecrypt" + "description": "Sichern Sie Ihre Daten mit Zecrypt" }, "common": { - "loading": "Wird geladen...", - "saving": "Wird gespeichert...", - "deleting": "Wird gelöscht...", + "loading": "Laden...", + "saving": "Speichern...", + "deleting": "Löschen...", "error": "Fehler", "success": "Erfolg", "cancel": "Abbrechen", + "add": "Hinzufügen", + "save": "Speichern", + "submitting": "Übermitteln...", "try_again": "Erneut versuchen", - "add": "Add", - "save": "Save", - "submitting": "Submitting...", - "sort": "Sort", - "sort_options": "Sort Options", - "sort_by_name": "Sort by Name", - "sort_by_created": "Sort by Created Date", - "sort_by_updated": "Sort by Updated Date", - "ascending": "Ascending", - "descending": "Descending", - "clear_sort": "Clear Sort", - "add_item_description": "Enter the details below", - "edit_item_description": "Update the details" + "sort": "Sortieren", + "sort_options": "Sortieroptionen", + "sort_by_name": "Nach Name sortieren", + "sort_by_created": "Nach Erstellungsdatum sortieren", + "sort_by_updated": "Nach Aktualisierungsdatum sortieren", + "ascending": "Aufsteigend", + "descending": "Absteigend", + "clear_sort": "Sortierung löschen", + "add_item_description": "Geben Sie die Details unten ein", + "edit_item_description": "Aktualisieren Sie die Details" + }, + "pagination": { + "showing_x_of_y": "Zeige {start} - {end} von {total} Elementen", + "page_of": "Seite {currentPage} von {totalPages}", + "next": "Weiter", + "previous": "Zurück" + }, + "env": { + "env_variables": "Umgebungsvariablen", + "manage_your_env": "Verwalten Sie Ihre Umgebungsvariablen sicher", + "add_env": "Umgebungsvariablen hinzufügen", + "loading_env": "Umgebungsvariablen werden geladen...", + "name": "Name", + "env_value": "Wert", + "tags": "Tags", + "created_at": "Erstellt am", + "actions": "Aktionen", + "no_env_yet": "Noch keine Umgebungsvariablen hinzugefügt", + "no_matching_env": "Keine passenden Umgebungsvariablen gefunden", + "search_across_all_fields": "Alle Felder durchsuchen...", + "show_env": "Umgebungsvariablen anzeigen", + "hide_env": "Umgebungsvariablen ausblenden", + "copy_env": "Umgebungsvariablen kopieren", + "env_copied": "Umgebungsvariablen in die Zwischenablage kopiert", + "failed_to_copy_env": "Fehler beim Kopieren der Umgebungsvariablen in die Zwischenablage", + "delete_env_confirmation": "Sind Sie sicher, dass Sie diese Umgebungsvariablen löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", + "showing_x_of_y": "Zeige {start} - {end} von {total} Elementen", + "failed_to_decrypt": "Fehler beim Entschlüsseln der Umgebungsvariablen", + "add_new_env": "Neue Umgebungsvariablen hinzufügen", + "add_new_env_description": "Geben Sie Ihre Umgebungsvariablen-Details unten ein", + "edit_env": "Umgebungsvariablen bearbeiten", + "edit_env_description": "Aktualisieren Sie Ihre Umgebungsvariablen", + "env_name": "Umgebungsname", + "enter_env_name": "Umgebungsname eingeben", + "enter_env_value": "Umgebungswert eingeben", + "enter_env_variables": "KEY=value\nANOTHER_KEY=another_value", + "env_format_hint": "Geben Sie Ihre Umgebungsvariablen im KEY=value Format ein, eine pro Zeile", + "notes": "Notizen", + "enter_notes": "Notizen eingeben (optional)", + "add_a_tag": "Tag hinzufügen", + "add_tag": "Tag hinzufügen", + "press_enter_to_add": "Enter drücken, um einen Tag hinzuzufügen", + "cancel": "Abbrechen", + "add": "Hinzufügen", + "update": "Aktualisieren", + "env_added_successfully": "Umgebungsvariablen erfolgreich hinzugefügt", + "env_updated_successfully": "Umgebungsvariablen erfolgreich aktualisiert", + "env_deleted_successfully": "Umgebungsvariablen erfolgreich gelöscht", + "error_adding_env": "Fehler beim Hinzufügen der Umgebungsvariablen", + "error_updating_env": "Fehler beim Aktualisieren der Umgebungsvariablen", + "error_deleting_env": "Fehler beim Löschen der Umgebungsvariablen", + "error_fetching_env": "Fehler beim Laden der Umgebungsvariablen", + "please_fill_all_required_fields": "Bitte füllen Sie alle erforderlichen Felder aus", + "no_project_selected": "Kein Projekt ausgewählt. Bitte wählen Sie ein Projekt aus.", + "clear_filters": "Filter löschen", + "decrypting": "Umgebungsvariablen werden entschlüsselt...", + "env_updated_description": "Die Umgebungsvariablen wurden aktualisiert.", + "env_added_description": "Die Umgebungsvariablen wurden hinzugefügt.", + "env_already_exists": "Umgebungsvariablen mit diesem Namen existieren bereits.", + "date_created": "Erstellungsdatum", + "sort": "Sortieren", + "sort_options": "Sortieroptionen", + "sort_by_name": "Nach Name sortieren", + "sort_by_created": "Nach Erstellungsdatum sortieren", + "sort_by_updated": "Nach Aktualisierungsdatum sortieren", + "view_env_code": "Im Code-Editor anzeigen", + "click_to_add_env": "Klicken, um Umgebungsvariablen hinzuzufügen", + "click_to_edit_env": "Klicken, um Umgebungsvariablen zu bearbeiten", + "env_hidden": "••••••••", + "copied": "Kopiert" }, "licenses": { "software_licenses": "Software-Lizenzen", "add_license": "Lizenz hinzufügen", - "search_licenses": "Lizenzen durchsuchen...", "filter_by_tag": "Nach Tag filtern", "all_tags": "Alle Tags", "search": "Suchen", @@ -42,11 +111,11 @@ "tags": "Tags", "actions": "Aktionen", "add_new_license": "Neue Lizenz hinzufügen", - "add_new_license_description": "Geben Sie Ihre Software-Lizenzdaten unten ein", + "add_new_license_description": "Geben Sie Ihre Software-Lizenz-Details unten ein", "cancel": "Abbrechen", "save": "Speichern", "edit_license": "Lizenz bearbeiten", - "edit_license_description": "Aktualisieren Sie Ihre Software-Lizenzdaten", + "edit_license_description": "Aktualisieren Sie Ihre Software-Lizenz-Details", "update": "Aktualisieren", "delete": "Löschen", "license_deleted": "Lizenz gelöscht", @@ -63,45 +132,45 @@ "expired": "Abgelaufen", "expiring_soon": "Läuft bald ab", "edit": "Bearbeiten", - "manage_your_software_licenses": "Verwalten Sie Ihre Software-Lizenzen und Schlüssel", + "enter_license_name": "Lizenzname eingeben", + "enter_software_name": "Softwarename eingeben", + "enter_license_key": "Lizenzschlüssel eingeben", + "enter_notes": "Notizen eingeben", + "confirm_deletion": "Löschung bestätigen", + "confirm_delete_license_description": "Sind Sie sicher, dass Sie diese Lizenz löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", + "license_added_successfully": "Lizenz erfolgreich hinzugefügt", + "failed_to_add_license": "Fehler beim Hinzufügen der Lizenz", + "invalid_input": "Ungültige Eingabe", + "validation_error": "Validierungsfehler", + "network_error": "Netzwerkfehler", + "error_adding_license": "Fehler beim Hinzufügen der Lizenz", + "license_updated_successfully": "Lizenz erfolgreich aktualisiert", + "failed_to_update_license": "Fehler beim Aktualisieren der Lizenz", + "license_not_found": "Lizenz nicht gefunden", + "error_updating_license": "Fehler beim Aktualisieren der Lizenz", + "missing_auth_or_selection": "Authentifizierung oder Auswahl fehlt", + "license_deleted_successfully": "Lizenz erfolgreich gelöscht", + "error_deleting_license": "Fehler beim Löschen der Lizenz", + "error_fetching_licenses": "Fehler beim Laden der Lizenzen", + "adding": "Hinzufügen...", + "updating": "Aktualisieren...", + "deleting": "Löschen...", + "notes": "Notizen", + "software_name": "Softwarename", + "rows_per_page": "Zeilen pro Seite", + "sort": "Sortieren", + "sort_options": "Sortieroptionen", + "sort_by_name": "Nach Name sortieren", + "sort_by_created": "Nach Erstellungsdatum sortieren", + "sort_by_updated": "Nach Aktualisierungsdatum sortieren", + "ascending": "Aufsteigend", + "descending": "Absteigend", + "clear_sort": "Sortierung löschen", + "search_across_all_fields": "Suchen", + "update_api_key": "API-Schlüssel aktualisieren", + "manage_your_software_licenses": "Verwalten Sie Ihre Software-Lizenzen sicher", "date_created": "Erstellungsdatum", - "add_a_tag": "Tag hinzufügen", - "enter_license_name": "Enter license name", - "enter_software_name": "Enter software name", - "enter_license_key": "Enter license key", - "enter_notes": "Enter notes", - "confirm_deletion": "Confirm Deletion", - "confirm_delete_license_description": "Are you sure you want to delete this license? This action cannot be undone.", - "license_added_successfully": "License added successfully", - "failed_to_add_license": "Failed to add license", - "invalid_input": "Invalid input", - "validation_error": "Validation error", - "network_error": "Network error", - "error_adding_license": "Error adding license", - "license_updated_successfully": "License updated successfully", - "failed_to_update_license": "Failed to update license", - "license_not_found": "License not found", - "error_updating_license": "Error updating license", - "missing_auth_or_selection": "Missing authentication or selection", - "license_deleted_successfully": "License deleted successfully", - "error_deleting_license": "Error deleting license", - "error_fetching_licenses": "Error fetching licenses", - "adding": "Adding...", - "updating": "Updating...", - "deleting": "Deleting...", - "notes": "Notes", - "software_name": "Software Name", - "rows_per_page": "Rows per page", - "sort": "Sort", - "sort_options": "Sort Options", - "sort_by_name": "Sort by Name", - "sort_by_created": "Sort by Created Date", - "sort_by_updated": "Sort by Updated Date", - "ascending": "Ascending", - "descending": "Descending", - "clear_sort": "Clear Sort", - "search_across_all_fields": "Search", - "update_api_key": "Update API Key" + "add_a_tag": "Tag hinzufügen" }, "navigation": { "home": "Startseite", @@ -109,319 +178,348 @@ "settings": "Einstellungen" }, "auth": { + "login_to_continue": "Anmelden, um fortzufahren", + "sign_in_with_sso": "Mit Ihrem Organisationskonto anmelden", "login": "Anmelden", "signup": "Registrieren", + "cancel": "Abbrechen", "logout": "Abmelden", - "dont_have_account": "Noch kein Konto?", - "email": "Email", + "sign_up": "Registrieren", + "try_again": "Erneut versuchen", + "dont_have_account": "Haben Sie noch kein Konto?", + "email": "E-Mail", "password": "Passwort", - "agreement": "Beim Anmelden stimmen Sie unseren", - "terms": "Nutzungsbedingungen", - "verification_code": "Bestätigungscodes", + "agreement": "Bei der Anmeldung stimmen Sie unseren Geschäftsbedingungen zu", + "terms": "Bedingungen", + "privacy_policy": "Datenschutzrichtlinie", + "terms_and_conditions": "Geschäftsbedingungen", + "service_status": "Service-Status", + "generate_password": "Passwort generieren", + "verification_code": "Bestätigungscode", "2fa_verify": "2FA-Bestätigung", - "2fa_enter_code": "Geben Sie den 6-stelligen Code aus Ihrer Authentifizierungs-App ein", + "2fa_enter_code": "Geben Sie den 6-stelligen Code aus Ihrer Authenticator-App ein", "2fa_placeholder": "z.B. 123456", - "cancel": "Abbrechen", - "verify": "Überprüfen", + "verify": "Bestätigen", "verifying_authentication": "Authentifizierung wird überprüft...", - "2fa_verification_error": "Ungültige Bestätigungscodes. Bitte versuchen Sie es erneut.", + "2fa_verification_error": "Ungültiger Bestätigungscode. Bitte versuchen Sie es erneut.", "2fa_verification_success": "2FA-Bestätigung erfolgreich", - "verifying": "2FA-Bestätigung wird überprüft...", + "verifying": "2FA wird überprüft...", "authentication_error": "Authentifizierungsfehler", - "try_again": "Erneut versuchen", - "2fa_verified": "2FA Bestätigt", - "redirecting_to_dashboard": "Weiterleitung zum Dashboard...", - "setup_encryption_keys": "Verschlüsselung einrichten", - "encryption_setup_description": "Erstellen Sie ein Master-Passwort zur Verschlüsselung Ihrer Daten. Dieses Passwort wird zum Schutz Ihres privaten Schlüssels verwendet.", - "encryption_password_warning": "Merken Sie sich dieses Passwort sorgfältig! Wenn Sie es vergessen, verlieren Sie den Zugriff auf alle Ihre verschlüsselten Daten.", - "encryption_password": "Master-Passwort", - "enter_strong_password": "Geben Sie ein sicheres Passwort ein", - "confirm_encryption_password": "Master-Passwort bestätigen", + "2fa_verified": "2FA bestätigt", + "redirecting_to_dashboard": "Sie werden zum Dashboard weitergeleitet...", + "setup_encryption_keys": "Verschlüsselungsschlüssel einrichten", + "encryption_setup_description": "Erstellen Sie ein Hauptpasswort, um Ihre Daten zu verschlüsseln. Dieses Passwort wird zum Schutz Ihres privaten Schlüssels verwendet.", + "encryption_password_warning": "Merken Sie sich dieses Passwort sorgfältig! Wenn Sie es vergessen, verlieren Sie den Zugang zu all Ihren verschlüsselten Daten.", + "encryption_password": "Hauptpasswort", + "enter_strong_password": "Geben Sie ein starkes Passwort ein", + "confirm_encryption_password": "Hauptpasswort bestätigen", "confirm_password": "Passwort bestätigen", "encryption_password_requirements": "Das Passwort muss mindestens 8 Zeichen lang sein und sollte Buchstaben, Zahlen und Sonderzeichen für maximale Sicherheit enthalten.", "generating_keys": "Schlüssel werden generiert...", "setup_encryption": "Verschlüsselung einrichten", "password_too_short": "Das Passwort muss mindestens 8 Zeichen lang sein", "passwords_dont_match": "Passwörter stimmen nicht überein", - "encryption_setup_success": "Verschlüsselung erfolgreich eingerichtet", + "encryption_setup_success": "Verschlüsselungseinrichtung abgeschlossen", "encryption_keys_created": "Ihre Verschlüsselungsschlüssel wurden erfolgreich erstellt", "encryption_setup_failed": "Fehler beim Einrichten der Verschlüsselungsschlüssel", "checking_encryption_keys": "Verschlüsselungsschlüssel werden überprüft...", - "encryption_key_check_failed": "Überprüfung der Verschlüsselungsschlüssel fehlgeschlagen", + "encryption_key_check_failed": "Verschlüsselungsschlüssel-Überprüfung fehlgeschlagen", "encryption_key_check_failed_desc": "Wir konnten Ihre Verschlüsselungsschlüssel nicht überprüfen. Dies könnte an einem Netzwerkproblem oder fehlenden Schlüsseln liegen.", - "encryption_security_error": "Verschlüsselungssicherheitsfehler. Bitte melden Sie sich erneut an.", + "encryption_security_error": "Verschlüsselungssicherheitsfehler. Bitte versuchen Sie sich erneut anzumelden.", "login_successful": "Anmeldung erfolgreich", "2fa_setup_required": "2FA-Einrichtung erforderlich", - "scan_qr_with_authenticator": "Scannen Sie den QR-Code mit Ihrer Authentifizierungs-App", + "scan_qr_with_authenticator": "Scannen Sie den QR-Code mit Ihrer Authenticator-App", "unknown_error": "Unbekannter Fehler aufgetreten", - "auth_network_error": "Netzwerkfehler während der Authentifizierung", + "auth_network_error": "Netzwerkfehler bei der Authentifizierung", "auth_process_failed": "Authentifizierungsprozess fehlgeschlagen", "2fa_invalid_code": "Ungültiger Bestätigungscode", - "2fa_verification_failed": "Überprüfung fehlgeschlagen: {message}", + "2fa_verification_failed": "Bestätigung fehlgeschlagen: {message}", "copied_to_clipboard": "In die Zwischenablage kopiert", - "paste_in_authenticator": "Fügen Sie diesen Code in Ihre Authentifizierungs-App ein", + "paste_in_authenticator": "Fügen Sie diesen Code in Ihre Authenticator-App ein", "2fa_setup": "Zwei-Faktor-Authentifizierung einrichten", - "2fa_scan": "Scannen Sie den QR-Code mit Ihrer Authentifizierungs-App", + "2fa_scan": "Scannen Sie den QR-Code mit Ihrer Authenticator-App", "2fa_scan_difficulty": "Probleme beim Scannen? Sie können den Einrichtungsschlüssel unten kopieren:", "copy_setup_key": "Einrichtungsschlüssel kopieren", - "login_to_continue": "Login to continue", - "sign_in_with_sso": "Sign in with your organization account", - "sign_up": "Sign up", - "privacy_policy": "Privacy Policy", - "terms_and_conditions": "Terms and Conditions", - "service_status": "Service Status", - "generate_password": "Generate Password", - "unlock_encryption": "Unlock Encryption", - "enter_encryption_password": "Enter your master password", - "enter_encryption_password_desc": "Enter your master password to decrypt your private key.", - "unlocking": "Unlocking...", - "unlock": "Unlock", - "encryption_unlocked": "Encryption Unlocked", - "encryption_key_decrypted": "Your encryption key has been successfully decrypted", - "incorrect_encryption_password": "Incorrect master password. Please try again.", - "multi_device_login": "Login von neuem Gerät", - "multi_device_desc": "Sie melden sich von einem neuen Gerät an. Bitte geben Sie Ihr Master-Passwort ein, um auf Ihre verschlüsselten Daten zuzugreifen.", - "authenticating": "Authentifizierung läuft...", + "unlock_encryption": "Verschlüsselung entsperren", + "enter_encryption_password": "Geben Sie Ihr Hauptpasswort ein", + "enter_encryption_password_desc": "Geben Sie Ihr Hauptpasswort ein, um Ihren privaten Schlüssel zu entschlüsseln.", + "unlocking": "Entsperren...", + "unlock": "Entsperren", + "encryption_unlocked": "Verschlüsselung entsperrt", + "encryption_key_decrypted": "Ihr Verschlüsselungsschlüssel wurde erfolgreich entschlüsselt", + "incorrect_encryption_password": "Falsches Hauptpasswort. Bitte versuchen Sie es erneut.", + "multi_device_login": "Anmeldung von neuem Gerät", + "multi_device_desc": "Sie melden sich von einem neuen Gerät an. Bitte geben Sie Ihr Hauptpasswort ein, um auf Ihre verschlüsselten Daten zuzugreifen.", + "authenticating": "Authentifizierung...", "download_password_title": "Ihr Zecrypt-Verschlüsselungspasswort:", "download_password_important": "WICHTIG: Bewahren Sie dieses Passwort sicher auf. Sie benötigen es, um auf Ihre verschlüsselten Daten zuzugreifen.", - "download_filename": "zecrypt-verschluesselungspasswort.txt" + "download_filename": "zecrypt-encryption-password.txt" }, "features": { - "trial_title": "Ihr passwortmanager für alles", - "unlimited_devices": "Bis zu 5 Projekte pro Benutzer, unbegrenzte Geräte - komplett kostenlos für immer", - "shared_vaults": "Freigegebene Tresore lassen Familienmitglieder auswählen, was sie teilen (oder privat halten)", - "advanced_security": "Erweiterte Sicherheit mit authentifiziertem Verschlüsselung, PAKE und mehr", - "security_alerts": "Alerts für kompromittierte Websites und schwache Passwörter", - "multi_platform": "Verfügbar auf Mac, iOS, Windows, Android, Chrome OS und Linux - ( bald verfügbar )", - "looking_for_options": "Suchen Sie nach einem individuellen, Team- oder Geschäftskonto?", - "see_options": "Alle Kontenoptionen anzeigen", - "organization": "Organization", - "organization_desc": "Organize credentials into projects for better management and quick access.", - "secure_storage": "Secure Storage", - "secure_storage_desc": "Store all your passwords and sensitive data with military-grade encryption.", - "secure_password_manager": "End to end encrypted cloud storage" + "trial_title": "Ihr sicherer Speicher-Manager", + "unlimited_devices": "Unbegrenzte Geräte — völlig kostenlos für immer", + "shared_vaults": "Teilen Sie Anmeldedaten mühelos mit Teammitgliedern - (Demnächst)", + "advanced_security": "Militärische Verschlüsselung (AES-GCM) für bombensichere Sicherheit", + "security_alerts": "Sofortige Sicherheitswarnungen, damit Sie immer einen Schritt voraus sind - (Demnächst)", + "multi_platform": "Verfügbar auf Mac, iOS, Windows, Android, Chrome OS und Linux - (Demnächst)", + "organization": "Organisation", + "organization_desc": "Organisieren Sie Anmeldedaten in Projekten für besseres Management und schnellen Zugriff.", + "secure_storage": "Sicherer Speicher", + "secure_storage_desc": "Speichern Sie alle Ihre Passwörter und sensiblen Daten mit militärischer Verschlüsselung.", + "secure_password_manager": "End-to-End verschlüsselter Cloud-Speicher" }, "actions": { - "save": "Speichern", + "add": "Hinzufügen", + "edit": "Bearbeiten", + "delete": "Löschen", "cancel": "Abbrechen", - "submit": "Senden", - "confirm": "Bestätigen", - "success": "Erfolg", - "error": "Fehler", - "add": "Add", - "edit": "Edit", - "delete": "Delete", - "close": "Close", - "saving": "Saving...", - "deleting": "Deleting...", + "close": "Schließen", + "save": "Speichern", + "saving": "Speichern...", + "deleting": "Löschen...", + "submit": "Absenden", "ok": "OK", - "yes": "Yes", - "no": "No", - "confirm_delete": "Confirm Delete", - "continue": "Continue", - "back": "Back", - "next": "Next", - "search": "Search", + "yes": "Ja", + "no": "Nein", + "confirm": "Bestätigen", + "confirm_delete": "Löschen bestätigen", + "continue": "Weiter", + "back": "Zurück", + "next": "Weiter", + "search": "Suchen", "filter": "Filter", - "clear": "Clear", - "reset": "Reset", - "refresh": "Refresh", - "upload": "Upload", - "download": "Download", - "copy": "Copy", - "paste": "Paste", - "cut": "Cut", - "undo": "Undo", - "redo": "Redo", - "select_all": "Select All", - "select_none": "Select None", - "create": "Create", - "update": "Update", - "remove": "Remove", - "view": "View" + "clear": "Löschen", + "reset": "Zurücksetzen", + "refresh": "Aktualisieren", + "upload": "Hochladen", + "download": "Herunterladen", + "copy": "Kopieren", + "paste": "Einfügen", + "cut": "Ausschneiden", + "undo": "Rückgängig", + "redo": "Wiederholen", + "select_all": "Alle auswählen", + "select_none": "Keine auswählen", + "create": "Erstellen", + "update": "Aktualisieren", + "remove": "Entfernen", + "error": "Fehler", + "view": "Anzeigen" }, "dashboard": { + "project_limit_reached": "Projektlimit erreicht", + "alpha_one_project_limit": "Wir laufen derzeit im Alpha-Modus, nur ein Projekt pro Arbeitsbereich ist erlaubt.", + "select_project": "Projekte auswählen", + "manage_projects": "Projekte verwalten", + "select_or_create_project": "Projekt auswählen oder erstellen", "welcome_back": "Willkommen zurück in Ihrem sicheren Arbeitsbereich", - "total_passwords": "Gesamte Passwörter", - "total_accounts": "Gesamte Konten", - "total_folders": "Gesamte Ordner", - "from_last_month": "Von letztem Monat", - "recent_activity": "Neueste Aktivitäten", + "total_passwords": "Gesamtpasswörter", + "total_accounts": "Gesamtkonten", + "total_folders": "Gesamtordner", + "recently_added_accounts": "Kürzlich hinzugefügte Konten", + "from_last_month": "vom letzten Monat", + "create_project_details": "Projektdetails erstellen", + "recent_activity": "Letzte Aktivität", "keyboard_shortcuts": "Tastenkürzel", "generate_password": "Passwort generieren", "favourites": "Favoriten", - "add_favorite_tag": "Favorit-Tag hinzufügen", - "remove_tag": "Entfernen {tag}", + "no_description": "Keine Beschreibung", + "notes":"Notizen", + "confirm_delete_project": "Projekt löschen bestätigen", + "create_project": "Projekt erstellen", + "set_as_default_project": "Als Standardprojekt festlegen", + "project_name":"Projektname", + "describe_project_purpose": "Projektzweck beschreiben", + "free_plan_limit": "Kostenloser Plan Limit", + "save_changes": "Änderungen speichern", + "no_project_availbale": "Kein Projekt verfügbar", + "cancel": "Abbrechen", + "delete": "Löschen", + "add_favorite_tag": "Favoriten-Tag hinzufügen", + "remove_tag": "{tag} entfernen", "dashboard": "Dashboard", + "show_more": "Mehr anzeigen", + "show_less": "Weniger anzeigen", "overview": "Übersicht", "project": "Projekt", "notifications": "Benachrichtigungen", - "search": "Suchen...", "settings": "Einstellungen", "logout": "Abmelden", "current_project": "Aktuelles Projekt", "default": "Standard", "no_project_selected": "Kein Projekt ausgewählt", - "quick_stats": "Quick Stats", + "quick_stats": "Schnellstatistiken", "workspaces": "Arbeitsbereiche", "projects": "Projekte", + "available_features":"Verfügbare Funktionen", "team_members": "Teammitglieder", - "total_projects": "Gesamte Projekte", - "total_workspaces": "Gesamte Arbeitsbereiche", - "total_team_members": "Gesamte Teammitglieder", + "total_projects": "Gesamtprojekte", + "total_workspaces": "Gesamtarbeitsbereiche", + "total_team_members": "Gesamtteammitglieder", + "total_records": "Gesamteinträge", "my_account": "Mein Konto", "change_language": "Sprache ändern", "language": "Sprache", - "toggle_theme": "Thema wechseln", + "toggle_theme": "Design umschalten", + "wallet_passphrases": "Wallet-Passphrasen", + "wi_fi": "Wi-Fi", + "env": "Umgebungsvariablen", + "env_variables": "Umgebungsvariablen", + "search": "Suchen", + "create_new_project": "Neues Projekt erstellen", + "add_new_project_description": "Fügen Sie ein neues Projekt hinzu, um Ihre sicheren Daten zu organisieren", + "description": "Beschreibung", + "optional": "Optional", + "project_color": "Projektfarbe", + "enabled_modules": "Aktivierte Module", + "edit_project": "Projekt bearbeiten", + "update_project_details": "Aktualisieren Sie die Details für Ihr Projekt", + "my_project": "Mein Projekt", + "creating": "Erstellen...", + "delete_project": "Projekt löschen", + "identity_coming_soon": "Identitätsmodul ist in Entwicklung.", + "cards_coming_soon": "Kartenmodul ist in Entwicklung.", + "software_licenses_coming_soon": "Software-Lizenzen-Modul ist in Entwicklung.", + "wifi_coming_soon": "Wi-Fi-Modul ist in Entwicklung.", + "profile": "Profil", + "search_modules": "Module suchen...", + "no_modules_found": "Keine Module gefunden", + "general": "Allgemein", + "security_access": "Sicherheit & Zugriff", + "business_finance": "Geschäft & Finanzen", + "licenses_services": "Lizenzen & Services", "accounts": "Konten", - "api_keys": "API Schlüssel", - "wallet_passphrases": "Wallet Passphrasen", "identity": "Identität", - "cards": "Karten", - "software_licenses": "Software-Lizenzen", - "wifi": "WLAN", "email": "E-Mail", + "wifi": "WiFi", + "api_keys": "API-Schlüssel", "ssh_keys": "SSH-Schlüssel", + "cards": "Karten", + "software_licenses": "Software-Lizenzen", + "recent_accounts": "Letzte Konten", + "no_recent_accounts": "Keine letzten Konten", + "last_24_hours": "Letzte 24 Stunden", + "no_accounts_create": "Keine Konten gefunden. Erstellen Sie eines, um zu beginnen.", + "adjust_search_filter": "Versuchen Sie, Ihre Such- oder Filterkriterien anzupassen", + "no_recent_activity": "Keine letzte Aktivität", "no_projects_available": "Keine Projekte verfügbar", - "no_recent_activity": "Keine neuen Aktivitäten", - "project_limit_reached": "Project limit reached", - "alpha_one_project_limit": "We are currently running on alpha, only one project is allowed per workspace.", - "select_project": "Select Projects", - "manage_projects": "Manage projects", - "select_or_create_project": "Select or create a project", - "recently_added_accounts": "Recently Added Accounts", - "create_project_details": "Create Project Details", - "no_description": "No Description", - "notes": "Notes", - "confirm_delete_project": "Confirm Delete Project", - "create_project": "Create Project", - "set_as_default_project": "Set as default project", - "project_name": "Project Name", - "describe_project_purpose": "Describe Project Purpose", - "free_plan_limit": "Free Plan Limit", - "save_changes": "Save Changes", - "no_project_availbale": "No Project Available", - "cancel": "Cancel", - "delete": "Delete", - "show_more": "Show More", - "show_less": "Show Less", - "available_features": "Available Features", - "total_records": "Total Records", - "wi_fi": "Wi-Fi", - "env": "Environment Variables", - "env_variables": "Environment Variables", - "create_new_project": "Create New Project", - "add_new_project_description": "Add a new project to organize your secure data", - "description": "Description", - "optional": "Optional", - "project_color": "Project Color", - "enabled_modules": "Enabled Modules", - "edit_project": "Edit Project", - "update_project_details": "Update the details for your project", - "my_project": "My Project", - "creating": "Creating...", - "delete_project": "Delete Project", - "identity_coming_soon": "Identity module is under development.", - "cards_coming_soon": "Cards module is under development.", - "software_licenses_coming_soon": "Software Licenses module is under development.", - "wifi_coming_soon": "Wi-Fi module is under development.", - "profile": "Profile", - "search_modules": "Search modules...", - "no_modules_found": "No modules found", - "general": "General", - "security_access": "Security & Access", - "business_finance": "Business & Finance", - "licenses_services": "Licenses & Services", - "recent_accounts": "Recent Accounts", - "no_recent_accounts": "No recent accounts", - "last_24_hours": "Last 24 hours", - "no_accounts_create": "No accounts found. Create one to get started.", - "adjust_search_filter": "Try adjusting your search or filter criteria" + "no_workspace_selected": "Kein Arbeitsbereich ausgewählt" }, "activity": { "password_updated": "Passwort aktualisiert", "note_created": "Sichere Notiz erstellt", - "you_create_login": "You created a new login", - "you_create_api_key": "You created a new API key", - "you_update_login": "You updated a login", - "you_update_api_key": "You updated an API key", - "you_delete_login": "You deleted a login", - "you_delete_api_key": "You deleted an API key", - "create_login": "Created a new login", - "create_api_key": "Created a new API key", - "update_login": "Updated a login", - "update_api_key": "Updated an API key", - "delete_login": "Deleted a login", - "delete_api_key": "Deleted an API key" + "you_create_login": "Sie haben eine neue Anmeldung erstellt", + "you_create_api_key": "Sie haben einen neuen API-Schlüssel erstellt", + "you_update_login": "Sie haben eine Anmeldung aktualisiert", + "you_update_api_key": "Sie haben einen API-Schlüssel aktualisiert", + "you_delete_login": "Sie haben eine Anmeldung gelöscht", + "you_delete_api_key": "Sie haben einen API-Schlüssel gelöscht", + "create_login": "Neue Anmeldung erstellt", + "create_api_key": "Neuer API-Schlüssel erstellt", + "update_login": "Anmeldung aktualisiert", + "update_api_key": "API-Schlüssel aktualisiert", + "delete_login": "Anmeldung gelöscht", + "delete_api_key": "API-Schlüssel gelöscht" }, "time": { "today": "Heute", "yesterday": "Gestern", "last_7_days": "Letzte 7 Tage", "last_30_days": "Letzte 30 Tage", - "all_time": "Gesamt", - "just_now": "Gerade jetzt", - "hours_ago": "{hours} Stunden her", - "days_ago": "{days} Tage her" + "all_time": "Alle Zeiten", + "just_now": "Gerade eben", + "hours_ago": "vor {hours} Stunden", + "days_ago": "vor {days} Tagen" }, "password_generator": { - "title": "Passwort Generator", + "title": "Passwort-Generator", "strong": "Stark", - "memorable": "Merken", + "memorable": "Einprägsam", "pin": "PIN", - "passphrase": "Passphrasen", + "passphrase": "Passphrase", "chars": "Zeichen", "digits": "Ziffern", "generate_new": "Neues Passwort generieren", + "generate_password": "Passwort generieren", + "click_generate_button": "Klicken Sie auf die Generieren-Schaltfläche, um ein neues Passwort zu generieren", "copy": "Passwort kopieren", "copied": "Kopiert!", "save": "Passwort speichern", "close": "Schließen", "copy_password": "Passwort kopieren", - "password_length": "Passwort Länge", + "password_length": "Passwortlänge", "options": "Optionen", "advanced": "Erweitert", + "advanced_options": "Erweiterte Optionen", "history": "Verlauf", "uppercase": "Großbuchstaben (A-Z)", "lowercase": "Kleinbuchstaben (a-z)", "numbers": "Zahlen (0-9)", - "symbols": "Sonderzeichen (!@#$%^&*)", - "password_analysis": "Passwort Analyse", - "strong_enough": "Stark genug für die meisten Sites", + "symbols": "Symbole (!@#$%^&*)", + "password_analysis": "Passwort-Analyse", + "strong_enough": "Stark genug für die meisten Websites", "not_strong_enough": "Nicht stark genug für sensible Konten", "good_length": "Gute Länge", - "consider_longer": "Betrachten Sie die Verwendung eines längeren Passworts", - "using_all_types": "Alle Zeichenarten verwenden", - "consider_more_types": "Betrachten Sie die Verwendung mehrerer Zeichenarten", + "consider_longer": "Erwägen Sie ein längeres Passwort", + "using_all_types": "Verwendet alle Zeichentypen", + "consider_more_types": "Erwägen Sie die Verwendung von mehr Zeichentypen", "all_characters": "Alle Zeichen", - "easy_to_read": "Leicht zu lesen (keine ähnlichen Zeichen wie 1, l, I, 0, O)", - "easy_to_say": "Leicht zu sagen (keine Zahlen oder Sonderzeichen)", - "password_tips": "Passwort Tipps", + "easy_to_read": "Leicht lesbar (keine ähnlichen Zeichen wie 1, l, I, 0, O)", + "easy_to_say": "Leicht auszusprechen (keine Zahlen oder Sonderzeichen)", + "password_tips": "Passwort-Tipps", "tip_min_length": "• Verwenden Sie mindestens 14 Zeichen für hohe Sicherheit", - "tip_mix": "• Inkludieren Sie eine Mischung aus Buchstaben, Zahlen und Sonderzeichen", - "tip_no_personal": "• Vermeiden Sie die Verwendung persönlicher Informationen", + "tip_mix": "• Verwenden Sie eine Mischung aus Buchstaben, Zahlen und Symbolen", + "tip_no_personal": "• Vermeiden Sie persönliche Informationen", "tip_unique": "• Verwenden Sie ein anderes Passwort für jedes Konto", - "tip_manager": "• Verwenden Sie einen Passwortmanager, um Ihre Passwörter zu speichern", + "tip_manager": "• Erwägen Sie die Verwendung eines Passwort-Managers zum Speichern Ihrer Passwörter", "recently_generated": "Kürzlich generiert", "clear_history": "Verlauf löschen", - "no_history": "Noch kein Passwortverlauf", + "no_history": "Noch kein Passwort-Verlauf", "history_hint": "Generierte Passwörter werden hier angezeigt", "weak": "Schwach", - "fair": "Fair", + "fair": "Mittelmäßig", "good": "Gut", "strong_strength": "Stark", "strength": "{strength}% Stärke", - "generate_password": "Generate Password", - "click_generate_button": "Click the Generate button to generate a new password", - "advanced_options": "Advanced Options", - "current_session": "Current Session", - "create_secure_passwords": "Create secure passwords for your accounts" + "current_session": "Aktuelle Sitzung", + "create_secure_passwords": "Erstellen Sie sichere Passwörter für Ihre Konten" + }, + "user_settings": { + "title": "Benutzereinstellungen", + "description": "Verwalten Sie Ihre Kontoeinstellungen und Präferenzen", + "profile": "Profil", + "security": "Sicherheit", + "notifications": "Benachrichtigungen", + "integrations": "Integrationen", + "billing": "Abrechnung", + "logout": "Abmelden", + "login_history": "Anmeldeverlauf", + "audit_log": "Audit-Protokoll", + "profile_title": "Profil & Kontoinformationen", + "profile_description": "Verwalten Sie Ihre persönlichen und Kontodetails", + "language": "Sprache", + "select_language": "Sprache auswählen", + "email_address": "E-Mail-Adresse", + "full_name": "Vollständiger Name", + "previous": "Vorherige", + "next": "Weiter", + "page": "Seite", + "of": "von", + "saving": "Speichern...", + "save_changes": "Änderungen speichern", + "profile_picture": "Profilbild", + "loading_history": "Anmeldeverlauf wird geladen...", + "no_history_available": "Kein Anmeldeverlauf verfügbar", + "try_again": "Erneut versuchen", + "device_browser": "Gerät & Browser", + "ip_address": "IP-Adresse", + "time": "Zeit" }, "accounts": { "no_accounts": "Keine Konten", "no_matching_accounts": "Keine passenden Konten gefunden", "loading_accounts": "Konten werden geladen...", - "error_fetching_accounts": "Fehler beim Abrufen der Konten", + "error_fetching_accounts": "Fehler beim Laden der Konten", "account_deleted_successfully": "Konto erfolgreich gelöscht", "error_deleting_account": "Fehler beim Löschen des Kontos", "account_updated_successfully": "Konto erfolgreich aktualisiert", @@ -429,10 +527,12 @@ "account_already_exists": "Ein Konto mit diesem Namen existiert bereits", "validation_error": "Ungültige Eingabedaten. Bitte überprüfen Sie Ihre Einträge", "failed_to_add_account": "Fehler beim Hinzufügen des Kontos. Bitte versuchen Sie es erneut", - "missing_auth_or_selection": "Bitte melden Sie sich an oder wählen Sie einen Arbeitsbereich und ein Projekt", + "missing_auth_or_selection": "Bitte melden Sie sich an oder wählen Sie einen Arbeitsbereich und ein Projekt aus", "please_log_in": "Bitte melden Sie sich an, um fortzufahren", - "server_error": "Ein Serverfehler ist aufgetreten. Bitte versuchen Sie es erneut", + "server_error": "Ein Serverfehler ist aufgetreten. Bitte versuchen Sie es später erneut", "adding": "Hinzufügen...", + "ascending": "Aufsteigend", + "descending": "Absteigend", "add": "Hinzufügen", "cancel": "Abbrechen", "account_name": "Kontoname", @@ -444,11 +544,19 @@ "password": "Passwort", "enter_password": "Passwort eingeben", "website": "Website", - "enter_website": "Website URL eingeben", + "enter_website": "Website-URL eingeben", + "enter_website_url": "Website-URL eingeben", + "notes": "Notizen", + "enter_notes": "Notizen eingeben", "tags": "Tags", - "enter_tags_comma_separated": "Tags (komma-getrennt)", + "enter_tags_comma_separated": "Tags eingeben (durch Komma getrennt)", "accounts": "Konten", "manage_your_saved_accounts_and_passwords": "Verwalten Sie Ihre gespeicherten Konten und Passwörter", + "encryption_key_not_found": "Projekt-Verschlüsselungsschlüssel nicht gefunden. Konto kann nicht gespeichert werden.", + "project_not_found": "Projekt nicht gefunden. Bitte wählen Sie ein gültiges Projekt aus.", + "encryption_failed": "Fehler beim Verschlüsseln der Kontodaten. Bitte versuchen Sie es erneut.", + "decryption_failed": "Fehler beim Entschlüsseln der Kontodaten. Dies könnte an einem fehlenden oder ungültigen Verschlüsselungsschlüssel liegen.", + "please_fill_all_required_fields": "Bitte füllen Sie alle erforderlichen Felder aus", "select_project": "Projekt auswählen", "no_project_selected": "Kein Projekt ausgewählt", "please_select_a_project": "Bitte wählen Sie ein Projekt aus, um Konten anzuzeigen", @@ -456,12 +564,12 @@ "error": "Fehler", "try_again": "Erneut versuchen", "no_accounts_found": "Keine Konten gefunden", - "no_accounts_exist": "Keine Konten existieren für dieses Projekt", + "no_accounts_exist": "Für dieses Projekt existieren keine Konten", "adjust_search_or_filter": "Passen Sie Ihre Suche oder Filter an, um Konten zu finden", "generate_password": "Passwort generieren", "account": "Konto", "category": "Kategorie", - "last_modified": "Letzte Änderung", + "last_modified": "Zuletzt geändert", "date_created": "Erstellungsdatum", "actions": "Aktionen", "copy_username": "Benutzername kopieren", @@ -469,15 +577,13 @@ "copy_password": "Passwort kopieren", "show_password": "Passwort anzeigen", "hide_password": "Passwort ausblenden", - "show_username": "Benutzername anzeigen", - "hide_username": "Benutzername ausblenden", "edit": "Bearbeiten", "delete": "Löschen", - "confirm_deletion": "Löschen bestätigen", + "confirm_deletion": "Löschung bestätigen", "confirm_delete_account": "Sind Sie sicher, dass Sie dieses Konto löschen möchten?", - "confirm_delete_account_description": "Diese Aktion kann nicht rückgängig gemacht werden und löscht die Kontoanmeldedaten dauerhaft.", + "confirm_delete_account_description": "Diese Aktion kann nicht rückgängig gemacht werden und wird die Kontodaten dauerhaft löschen.", "deleting": "Löschen...", - "showing": "Anzeigen", + "showing": "Zeige", "of": "von", "rows_per_page": "Zeilen pro Seite", "clear_filters": "Filter löschen", @@ -488,19 +594,20 @@ "updating": "Aktualisieren...", "update": "Aktualisieren", "view_details": "Details anzeigen", - "filter_warning": "Filter können die Seitenzahl beeinflussen, da serverseitige Filterung nicht unterstützt wird.", + "filter_warning": "Filter können die Paginierungsgenauigkeit beeinträchtigen, da serverseitige Filterung nicht unterstützt wird.", "search": "Suchen", - "search_accounts": "Konten durchsuchen", + "search_accounts": "Konten suchen", "all_accounts": "Alle Konten", "personal": "Persönlich", "work": "Arbeit", "finance": "Finanzen", "clear": "Löschen", + "clear_sort": "Löschen", "favorite": "Favorit", "no_accounts_create": "Keine Konten gefunden. Erstellen Sie eines, um zu beginnen.", - "adjust_search_filter": "Versuchen Sie, Ihre Suche oder Filterkriterien anzupassen", + "adjust_search_filter": "Versuchen Sie, Ihre Such- oder Filterkriterien anzupassen", "error_loading_workspace": "Fehler beim Laden der Arbeitsbereichsdaten", - "focus_search": "Suchfeld fokussieren", + "focus_search": "Suche fokussieren", "switch_project": "Projekt wechseln", "add_a_tag": "Tag hinzufügen", "edit_account": "Konto bearbeiten", @@ -512,255 +619,238 @@ "sort_options": "Sortieroptionen", "sort_by_name": "Nach Name sortieren", "sort_by_created": "Nach Erstellungsdatum sortieren", - "sort_by_updated": "Nach Änderungsdatum sortieren", - "ascending": "Ascending", - "descending": "Descending", - "enter_website_url": "Enter website URL", - "notes": "Notes", - "enter_notes": "Enter notes", - "encryption_key_not_found": "Project encryption key not found. Cannot save account.", - "project_not_found": "Project not found. Please select a valid project.", - "encryption_failed": "Failed to encrypt account data. Please try again.", - "decryption_failed": "Failed to decrypt account data. This could be due to a missing or invalid encryption key.", - "please_fill_all_required_fields": "Please fill all required fields", - "clear_sort": "Clear" - }, - "user_settings": { - "title": "Benutzer Einstellungen", - "description": "Verwalten Sie Ihre Konteneinstellungen und Präferenzen", - "profile": "Profil", - "security": "Sicherheit", - "notifications": "Benachrichtigungen", - "integrations": "Integrationen", - "billing": "Abrechnung", - "logout": "Abmelden", - "login_history": "Login-Historie", - "audit_log": "Audit-Log", - "profile_title": "Profil & Kontoinformationen", - "profile_description": "Verwalten Sie Ihre persönlichen und Kontodetails", - "language": "Sprache", - "select_language": "Sprache auswählen", - "email_address": "Email-Adresse", - "full_name": "Vollständiger Name", - "previous": "Zurück", - "next": "Weiter", - "page": "Seite", - "of": "von", - "saving": "Speichern...", - "save_changes": "Änderungen speichern", - "profile_picture": "Profilbild", - "loading_history": "Login-Historie wird geladen...", - "no_history_available": "Keine Login-Historie verfügbar", - "try_again": "Versuchen Sie es erneut", - "device_browser": "Gerät & Browser", - "ip_address": "IP-Adresse", - "time": "Zeit" + "sort_by_updated": "Nach Aktualisierungsdatum sortieren" }, "api_keys": { - "title": "API Schlüssel", - "description": "Verwalten Sie Ihre API-Schlüssel für programmatischen Zecrypt-Zugriff.", - "edit_api_key": "API Schlüssel bearbeiten", - "search": "API Schlüssel suchen...", + "title": "API-Schlüssel", + "description": "Verwalten Sie Ihre API-Schlüssel für programmatischen Zugriff auf Zecrypt-Services.", + "edit_api_key": "API-Schlüssel bearbeiten", + "search": "Suchen", "name": "Name", "key": "Schlüssel", + "api_key": "API-Schlüssel", + "enter_api_key": "API-Schlüssel eingeben", "type": "Typ", "created": "Erstellt", "expires": "Läuft ab", "status": "Status", "actions": "Aktionen", "clear": "Löschen", - "create_new_api_key": "Neuer API Schlüssel erstellen", - "enter_details_for_new_api_key": "Geben Sie die Details für Ihren neuen API Schlüssel ein", - "api_key_name": "API Schlüssel Name", - "enter_api_key_name": "z.B. Produktions-Backend", + "sort": "Sortieren", + "last_used": "Umgebung", + "sort_options":"Sortieroptionen", + "sort_by_name":"Nach Name sortieren", + "sort_by_created":"Nach Erstellungsdatum sortieren", + "sort_by_updated":"Nach Aktualisierungsdatum sortieren", + "ascending": "Aufsteigend", + "descending": "Absteigend", + "clear_sort": "Sortierung löschen", + "clear_filters": "Filter löschen", + "create_new_api_key": "Neuen API-Schlüssel erstellen", + "enter_details_for_new_api_key": "Geben Sie die Details für Ihren neuen API-Schlüssel ein", + "api_key_name": "API-Schlüssel-Name", + "enter_api_key_name": "z.B. Production Backend", "key_type": "Schlüsseltyp", - "key_preview": "Schlüsselvorschau", - "key_preview_description": "Der tatsächliche Schlüssel wird generiert, wenn Sie \"API Schlüssel erstellen\" klicken", - "create_api_key": "API Schlüssel erstellen", + "key_preview": "Schlüssel-Vorschau", + "key_preview_description": "Der tatsächliche Schlüssel wird generiert, wenn Sie auf \"API-Schlüssel erstellen\" klicken", + "create_api_key": "API-Schlüssel erstellen", "cancel": "Abbrechen", - "rows_per_page": "Zeilen pro Seite:", - "add_api_key": "API Schlüssel hinzufügen", - "no_api_keys_found": "Keine API Schlüssel gefunden", - "try_adjusting_search_or_filter": "Versuchen Sie, Ihre Suche oder Filterkriterien anzupassen", - "api_key": "API Schlüssel", - "enter_api_key": "API Schlüssel eingeben", + "rows_per_page": "Zeilen pro Seite", + "add_api_key": "API-Schlüssel hinzufügen", + "no_api_keys_found": "Keine API-Schlüssel gefunden", + "try_adjusting_search_or_filter": "Versuchen Sie, Ihre Such- oder Filterkriterien anzupassen", + "error_fetching_api_keys": "Fehler beim Laden der API-Schlüssel", + "failed_to_fetch_api_keys": "API-Schlüssel konnten nicht geladen werden. Bitte versuchen Sie es erneut.", + "confirm_delete_api_key": "Sind Sie sicher, dass Sie diesen API-Schlüssel löschen möchten?", + "api_key_deleted_successfully": "API-Schlüssel erfolgreich gelöscht", + "api_key_deleted_description": "Der API-Schlüssel wurde erfolgreich gelöscht.", + "error_deleting_api_key": "Fehler beim Löschen des API-Schlüssels", + "failed_to_delete_api_key": "API-Schlüssel konnte nicht gelöscht werden. Bitte versuchen Sie es erneut.", + "api_key_updated_successfully": "API-Schlüssel aktualisiert", + "api_key_updated_description": "Der API-Schlüssel wurde erfolgreich aktualisiert.", + "api_key_added_successfully": "API-Schlüssel hinzugefügt", + "api_key_added_description": "Der API-Schlüssel wurde erfolgreich hinzugefügt.", + "failed_to_update_api_key": "API-Schlüssel konnte nicht aktualisiert werden. Bitte versuchen Sie es erneut.", + "manage_your_api_keys": "Verwalten Sie Ihre API-Schlüssel", + "showing": "Zeige", + "of": "von", + "add_new_api_key": "Neuen API-Schlüssel hinzufügen", + "add_new_api_key_description": "Geben Sie Ihre API-Schlüssel-Details unten ein", + "api_key_already_exists": "Ein API-Schlüssel mit diesem Namen existiert bereits.", + "error_adding_api_key": "Fehler beim Hinzufügen des API-Schlüssels aufgrund eines Serverfehlers.", + "failed_to_add_api_key": "API-Schlüssel konnte nicht hinzugefügt werden. Bitte versuchen Sie es erneut.", + "error_updating_api_key": "Fehler beim Aktualisieren des API-Schlüssels aufgrund eines Serverfehlers.", + "invalid_input_data": "Ungültige Eingabedaten. Bitte überprüfen Sie Ihre Einträge.", + "network_error": "Netzwerkfehler. Bitte überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.", + "no_api_keys_for_env": "Keine API-Schlüssel für Umgebung '{env}' gefunden. Versuchen Sie eine andere Umgebung oder löschen Sie die Filter.", + "no_api_keys_match_search": "Keine API-Schlüssel entsprechen der Suche '{search}'. Versuchen Sie, Ihre Suche anzupassen.", + "adjust_search_filter": "Versuchen Sie, Ihre Such- oder Filterkriterien anzupassen.", + "copied": "Kopiert", + "api_key_copied": "Der API-Schlüssel wurde in Ihre Zwischenablage kopiert.", + "copy_failed": "Kopieren fehlgeschlagen", + "failed_to_copy_api_key": "API-Schlüssel konnte nicht in die Zwischenablage kopiert werden.", + "show_key": "Schlüssel anzeigen", + "hide_key": "Schlüssel ausblenden", + "copy_key": "Schlüssel kopieren", + "edit": "Bearbeiten", + "delete": "Löschen", + "decryption_error": "Entschlüsselungsfehler - bitte bearbeiten, um das Format zu aktualisieren", + "adding": "Hinzufügen...", + "updating": "Aktualisieren", + "no_project_selected": "Kein Projekt ausgewählt. Bitte wählen Sie ein Projekt aus, um fortzufahren.", + "please_fill_all_required_fields": "Bitte füllen Sie alle erforderlichen Felder aus", + "loading_api_keys": "API-Schlüssel werden geladen...", + "api_keys": "API-Schlüssel", + "env": "Umgebung", + "tags": "Tags", + "last_modified": "Zuletzt geändert", + "api_key_description": "API-Schlüssel-Beschreibung", + "enter_description": "Geben Sie eine Beschreibung für diesen API-Schlüssel ein", "environment": "Umgebung", "select_environment": "Umgebung auswählen", "development": "Entwicklung", "staging": "Staging", "production": "Produktion", - "testing": "Testing", + "testing": "Test", "local": "Lokal", "uat": "UAT", "add": "Hinzufügen", - "add_new_api_key": "Neuen API Schlüssel hinzufügen", - "add_new_api_key_description": "Geben Sie Ihre API Schlüssel-Details unten ein", - "adding": "Hinzufügen...", - "updating": "Aktualisieren...", - "api_key_added_successfully": "API Schlüssel hinzugefügt", - "api_key_added_description": "Der API Schlüssel wurde erfolgreich hinzugefügt.", - "api_key_already_exists": "Ein API Schlüssel mit diesem Namen existiert bereits.", - "failed_to_add_api_key": "Der API Schlüssel konnte nicht hinzugefügt werden. Bitte versuchen Sie es erneut.", - "invalid_input_data": "Ungültige Eingabedaten. Bitte überprüfen Sie Ihre Eingaben.", - "no_project_selected": "Kein Projekt ausgewählt. Bitte wählen Sie ein Projekt aus.", - "please_fill_all_required_fields": "Bitte füllen Sie alle erforderlichen Felder aus", - "notes": "Notizen", - "enter_notes": "Notizen eingeben", - "tags": "Tags", "add_a_tag": "Tag hinzufügen", - "api_key_updated_successfully": "API Schlüssel aktualisiert", - "api_key_updated_description": "Der API Schlüssel wurde erfolgreich aktualisiert.", - "edit_api_key_description": "Aktualisieren Sie Ihre API Schlüssel-Details", - "failed_to_update_api_key": "Der API Schlüssel konnte nicht aktualisiert werden. Bitte versuchen Sie es erneut.", - "env": "Umgebung", - "last_modified": "Zuletzt geändert", - "copied": "Kopiert", - "api_key_copied": "API Schlüssel in die Zwischenablage kopiert", - "copy_failed": "Kopieren fehlgeschlagen", - "failed_to_copy_api_key": "API Schlüssel konnte nicht in die Zwischenablage kopiert werden", - "show_key": "Schlüssel anzeigen", - "hide_key": "Schlüssel verbergen", - "copy_key": "Schlüssel kopieren", - "edit": "Bearbeiten", - "delete": "Löschen", + "update_api_key": "API-Schlüssel aktualisieren", + "all_envs": "Alle Umgebungen", + "search_across_all_fields": "Suchen", + "delete_api_key_confirmation": "Sind Sie sicher, dass Sie diesen API-Schlüssel löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", "deleting": "Löschen...", - "delete_api_key_confirmation": "Sind Sie sicher, dass Sie diesen API Schlüssel löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", - "confirm_deletion": "Löschen bestätigen", + "confirm_deletion": "Löschung bestätigen", + "edit_api_key_description": "Aktualisieren Sie Ihre API-Schlüssel-Details", "showing_results": "Zeige {startIdx}-{endIdx} von {totalCount} Ergebnissen", - "all_envs": "Alle Umgebungen", - "search_across_all_fields": "Über alle Felder suchen...", + "notes": "Notizen", + "enter_notes": "Notizen eingeben", "filter_by_env": "Nach Umgebung filtern", - "sort": "Sortieren", - "sort_options": "Sortieroptionen", - "sort_by_name": "Nach Name sortieren", - "sort_by_created": "Nach Erstellungsdatum sortieren", - "sort_by_updated": "Nach Aktualisierungsdatum sortieren", - "ascending": "Aufsteigend", - "descending": "Absteigend", - "clear_sort": "Sortierung aufheben", - "date_created": "Erstellungsdatum", - "last_used": "Environment", - "clear_filters": "Clear Filters", - "error_fetching_api_keys": "Failed to fetch API keys", - "failed_to_fetch_api_keys": "Unable to load API keys. Please try again.", - "confirm_delete_api_key": "Are you sure you want to delete this API key?", - "api_key_deleted_successfully": "API Key-phase-2 Key Deleted", - "api_key_deleted_description": "The API key has been successfully deleted.", - "error_deleting_api_key": "Failed to Delete API Key", - "failed_to_delete_api_key": "Unable to delete the API key. Please try again.", - "manage_your_api_keys": "Manage your API keys", - "showing": "Showing", - "of": "of", - "error_adding_api_key": "Failed to add API key due to a server error.", - "error_updating_api_key": "Failed to update API key due to a server error.", - "network_error": "Network error. Please check your connection and try again.", - "no_api_keys_for_env": "No API keys found for environment '{env}'. Try a different environment or clear filters.", - "no_api_keys_match_search": "No API keys match the search '{search}'. Try adjusting your search.", - "adjust_search_filter": "Try adjusting your search or filter criteria.", - "decryption_error": "Decryption error - please edit to update format", - "loading_api_keys": "Loading API keys...", - "api_keys": "API Keys", - "api_key_description": "API Key Description", - "enter_description": "Enter a description for this API key", - "update_api_key": "Update API Key" + "date_created": "Erstellungsdatum" }, "wallet_passphrases": { - "no_passphrases_found_search": "Keine Passphrasen passen zu Ihren Suchkriterien.", + "wallet_passphrases": "Wallet-Passphrasen", + "securely_store_and_manage_your_wallet_recovery_phrases": "Speichern und verwalten Sie Ihre Wallet-Wiederherstellungsphrasen sicher", "add_passphrase": "Passphrase hinzufügen", + "enter_wallet_address": "Wallet-Adresse eingeben", + "copy_wallet_address": "Wallet-Adresse kopieren", + "wallet_address": "Wallet-Adresse", + "add_your_first_passphrase": "Fügen Sie Ihre erste Passphrase hinzu", + "search_passphrases": "Suchen", + "no_wallet_passphrases_found": "Keine Wallet-Passphrase gefunden", + "wallet_address_encryption_note": "Wallet-Adresse ist aus Sicherheitsgründen verschlüsselt", "no_passphrases_found": "Keine Passphrasen gefunden", - "try_adjusting_search_or_filter": "Versuchen Sie, Ihre Suche oder Filterkriterien anzupassen", - "no_passphrases_found_message": "Sie haben noch keine Wallet Passphrasen hinzugefügt.", - "add_your_first_passphrase": "Ihre erste Passphrase hinzufügen", + "no_passphrases_found_message": "Sie haben noch keine Wallet-Passphrasen hinzugefügt. Fügen Sie eine hinzu, um zu beginnen.", + "no_passphrases_found_search": "Keine Passphrasen entsprechen Ihren Suchkriterien.", "loading_passphrases": "Passphrasen werden geladen...", - "please_wait_while_we_fetch_your_secure_passphrases": "Bitte warten Sie, während wir Ihre sicheren Passphrasen abrufen...", - "search_passphrases": "Passphrasen suchen...", - "wallet_passphrases": "Wallet Passphrasen", - "securely_store_and_manage_your_wallet_recovery_phrases": "Sichern Sie und verwalten Sie Ihre Wallet Passphrasen", - "cancel": "Abbrechen", - "date_created": "Erstellungsdatum", - "enter_wallet_address": "Enter wallet address", - "copy_wallet_address": "Copy wallet address", - "wallet_address": "Wallet Address", - "no_wallet_passphrases_found": "No Wallet Passphrase Found", - "wallet_address_encryption_note": "Wallet address is encrypted for security", - "error_loading_passphrases": "Error Loading Passphrases", - "failed_to_load_passphrases": "Failed to load passphrases. Please try again.", - "passphrase_added_successfully": "Passphrase Added Successfully", - "passphrase_added_description": "Your wallet passphrase has been securely stored.", - "passphrase_updated_successfully": "Passphrase Updated Successfully", - "passphrase_updated_description": "Your wallet passphrase has been updated.", - "failed_to_add_passphrase": "Failed to Add Passphrase", - "failed_to_update_passphrase": "Failed to Update Passphrase", - "wallet_phrase_already_exists": "This passphrase or name already exists in your wallet", - "invalid_input_data": "Invalid input data. Please check your entries.", - "no_project_selected": "No project selected. Please select a project.", - "passphrase_required": "Passphrase is required", - "passphrase_must_be_exactly_12_words": "Passphrase must contain exactly 12 words (currently {count})", - "please_fill_all_required_fields": "Please fill all required fields", + "please_wait_while_we_fetch_your_secure_passphrases": "Bitte warten Sie, während wir Ihre sicheren Passphrasen laden", + "error_loading_passphrases": "Fehler beim Laden der Passphrasen", + "failed_to_load_passphrases": "Passphrasen konnten nicht geladen werden. Bitte versuchen Sie es erneut.", + "passphrase_added_successfully": "Passphrase erfolgreich hinzugefügt", + "passphrase_added_description": "Ihre Wallet-Passphrase wurde sicher gespeichert.", + "passphrase_updated_successfully": "Passphrase erfolgreich aktualisiert", + "passphrase_updated_description": "Ihre Wallet-Passphrase wurde aktualisiert.", + "failed_to_add_passphrase": "Fehler beim Hinzufügen der Passphrase", + "failed_to_update_passphrase": "Fehler beim Aktualisieren der Passphrase", + "wallet_phrase_already_exists": "Diese Passphrase oder dieser Name existiert bereits in Ihrem Wallet", + "invalid_input_data": "Ungültige Eingabedaten. Bitte überprüfen Sie Ihre Einträge.", + "no_project_selected": "Kein Projekt ausgewählt. Bitte wählen Sie ein Projekt aus.", + "passphrase_required": "Passphrase ist erforderlich", + "passphrase_must_be_exactly_12_words": "Passphrase muss genau 12 Wörter enthalten (aktuell {count})", + "please_fill_all_required_fields": "Bitte füllen Sie alle erforderlichen Felder aus", "name": "Name", - "add": "Add", - "sort": "Sort", - "sort_by_name": "Sort by Name", - "sort_by_created": "Sort by Created Date", - "sort_by_updated": "Sort by Updated Date", - "sort_options": "Sort Options", - "enter_passphrase_name": "Enter passphrase name", + "add": "Hinzufügen", + "sort": "Sortieren", + "sort_by_name": "Nach Name sortieren", + "sort_by_created": "Nach Erstellungsdatum sortieren", + "sort_by_updated": "Nach Aktualisierungsdatum sortieren", + "sort_options":"Sortieroptionen", + "enter_passphrase_name": "Passphrase-Name eingeben", "passphrase": "Passphrase", - "enter_wallet_recovery_phrase": "Enter wallet recovery phrase", - "notes": "Notes", - "enter_notes": "Enter notes", - "wallet_type": "Wallet Type", - "select_wallet_type": "Select wallet type", + "enter_wallet_recovery_phrase": "Wallet-Wiederherstellungsphrase eingeben", + "notes": "Notizen", + "enter_notes": "Notizen eingeben", + "wallet_type": "Wallet-Typ", + "select_wallet_type": "Wallet-Typ auswählen", "tags": "Tags", - "add_a_tag": "Add a tag", - "save_passphrase": "Save Passphrase", - "update_passphrase": "Update Passphrase", - "passphrase_encryption_note": "Your passphrase will be encrypted for security.", - "adding": "Adding...", - "updating": "Updating...", - "search_across_all_fields": "Search", - "filter_by_wallet_type": "Filter by wallet type", - "all_wallet_types": "All Wallet Types", - "clear_filters": "Clear filters", - "last_modified": "Last Modified", - "actions": "Actions", - "hide_passphrase": "Hide passphrase", - "show_passphrase": "Show passphrase", - "copy_passphrase": "Copy passphrase", - "edit": "Edit", - "delete": "Delete", - "showing_results": "Showing {startIdx}-{endIdx} of {totalCount} results", - "confirm_deletion": "Confirm Deletion", - "delete_passphrase_confirmation": "Are you sure you want to delete this wallet passphrase? This action cannot be undone.", - "deleting": "Deleting...", - "add_new_passphrase_description": "Enter your wallet passphrase details below", - "edit_passphrase": "Edit Passphrase", - "edit_passphrase_description": "Update your wallet passphrase details", - "copied": "Copied", - "passphrase_copied": "Passphrase copied to clipboard", - "copy_failed": "Copy failed", - "failed_to_copy_passphrase": "Failed to copy passphrase to clipboard" + "add_a_tag": "Tag hinzufügen", + "save_passphrase": "Passphrase speichern", + "update_passphrase": "Passphrase aktualisieren", + "passphrase_encryption_note": "Ihre Passphrase wird aus Sicherheitsgründen verschlüsselt.", + "adding": "Hinzufügen...", + "updating": "Aktualisieren...", + "search_across_all_fields": "Suchen", + "filter_by_wallet_type": "Nach Wallet-Typ filtern", + "all_wallet_types": "Alle Wallet-Typen", + "clear_filters": "Filter löschen", + "last_modified": "Zuletzt geändert", + "actions": "Aktionen", + "hide_passphrase": "Passphrase ausblenden", + "show_passphrase": "Passphrase anzeigen", + "copy_passphrase": "Passphrase kopieren", + "edit": "Bearbeiten", + "delete": "Löschen", + "showing_results": "Zeige {startIdx}-{endIdx} von {totalCount} Ergebnissen", + "confirm_deletion": "Löschung bestätigen", + "delete_passphrase_confirmation": "Sind Sie sicher, dass Sie diese Wallet-Passphrase löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", + "deleting": "Löschen...", + "add_new_passphrase_description": "Geben Sie Ihre Wallet-Passphrase-Details unten ein", + "edit_passphrase": "Passphrase bearbeiten", + "edit_passphrase_description": "Aktualisieren Sie Ihre Wallet-Passphrase-Details", + "copied": "Kopiert", + "passphrase_copied": "Passphrase in die Zwischenablage kopiert", + "copy_failed": "Kopieren fehlgeschlagen", + "failed_to_copy_passphrase": "Fehler beim Kopieren der Passphrase in die Zwischenablage", + "cancel": "Abbrechen", + "date_created": "Erstellungsdatum" }, "identity": { - "identities": "Identitäten", + "title": "Identität", + "description": "Verwalten Sie Ihre persönlichen Identitätsinformationen sicher", "add_identity": "Identität hinzufügen", - "search_identities": "Identitäten durchsuchen...", + "no_identities_found": "Keine Identitäten gefunden", + "no_identities_found_message": "Sie haben noch keine Identitätsinformationen hinzugefügt.", + "add_your_first_identity": "Fügen Sie Ihre erste Identität hinzu", + "loading_identities": "Identitäten werden geladen...", + "search_identities": "Suchen", + "full_name": "Vollständiger Name", + "country": "Land", + "enter_full_name": "Vollständigen Namen eingeben", + "date_of_birth": "Geburtsdatum", + "enter_date_of_birth": "Geburtsdatum eingeben (YYYY-MM-DD)", + "address": "Adresse", + "enter_address": "Adresse eingeben", + "phone_number": "Telefonnummer", + "enter_phone_number": "Telefonnummer eingeben", + "identity_number": "Identitätsnummer", + "enter_identity_number": "Identitätsnummer eingeben (z.B. Reisepass, Ausweis)", + "tags": "Tags", + "enter_tags_comma_separated": "Tags eingeben (durch Komma getrennt)", + "last_modified": "Zuletzt geändert", + "actions": "Aktionen", + "edit": "Bearbeiten", + "delete": "Löschen", + "copy_field": "{field} kopieren", + "copied": "Kopiert", + "identity_deleted_successfully": "Identität erfolgreich gelöscht", + "error_deleting_identity": "Fehler beim Löschen der Identität", + "identity_updated_successfully": "Identität erfolgreich aktualisiert", + "failed_to_add_identity": "Fehler beim Hinzufügen der Identität", + "all_fields_required": "Bitte füllen Sie alle erforderlichen Felder aus", + "validation_error": "Ungültige Eingabedaten. Bitte überprüfen Sie Ihre Einträge", + "server_error": "Ein Serverfehler ist aufgetreten. Bitte versuchen Sie es später erneut", + "identities": "Identitäten", "filter_by_tag": "Nach Tag filtern", "all_tags": "Alle Tags", "search": "Suchen", - "loading_identities": "Identitäten werden geladen...", - "no_identities_found": "Keine Identitäten gefunden", "clear_filters": "Filter löschen", "name": "Name", "contact": "Kontakt", - "address": "Adresse", "passport": "Reisepass", "national_id": "Personalausweis", - "date_of_birth": "Geburtsdatum", - "tags": "Tags", - "actions": "Aktionen", "add_new_identity": "Neue Identität hinzufügen", - "add_new_identity_description": "Geben Sie Ihre Identitätsdaten unten ein", + "add_new_identity_description": "Geben Sie Ihre Identitätsdetails unten ein", "first_name": "Vorname", "last_name": "Nachname", "email": "E-Mail", @@ -769,12 +859,10 @@ "cancel": "Abbrechen", "save": "Speichern", "edit_identity": "Identität bearbeiten", - "edit_identity_description": "Aktualisieren Sie Ihre Identitätsdaten", + "edit_identity_description": "Aktualisieren Sie Ihre Identitätsdetails", "update": "Aktualisieren", - "delete": "Löschen", "identity_deleted": "Identität gelöscht", "identity_deleted_description": "Die Identität wurde erfolgreich gelöscht", - "copied": "Kopiert", "field_copied": "Feld in die Zwischenablage kopiert", "copy_failed": "Kopieren fehlgeschlagen", "failed_to_copy_field": "Fehler beim Kopieren des Feldes in die Zwischenablage", @@ -782,256 +870,198 @@ "show_sensitive_data": "Sensible Daten anzeigen", "showing_results": "Zeige {startIdx}-{endIdx} von {totalCount} Ergebnissen", "page_of": "Seite {currentPage} von {totalPages}", - "edit": "Bearbeiten", "copy_passport": "Reisepassnummer kopieren", "copy_national_id": "Personalausweisnummer kopieren", - "manage_your_personal_identities": "Verwalten Sie Ihre persönlichen Identitätsinformationen sicher", - "date_created": "Erstellungsdatum", - "title": "Identity", - "description": "Manage your personal identity information securely", - "no_identities_found_message": "You haven't added any identity information yet.", - "add_your_first_identity": "Add Your First Identity", - "full_name": "Full Name", - "country": "Country", - "enter_full_name": "Enter full name", - "enter_date_of_birth": "Enter date of birth (YYYY-MM-DD)", - "enter_address": "Enter address", - "phone_number": "Phone Number", - "enter_phone_number": "Enter phone number", - "identity_number": "Identity Number", - "enter_identity_number": "Enter identity number (e.g., passport, ID)", - "enter_tags_comma_separated": "Enter tags (comma-separated)", - "last_modified": "Last Modified", - "copy_field": "Copy {field}", - "identity_deleted_successfully": "Identity deleted successfully", - "error_deleting_identity": "Failed to delete identity", - "identity_updated_successfully": "Identity updated successfully", - "failed_to_add_identity": "Failed to add identity", - "all_fields_required": "Please fill in all required fields", - "validation_error": "Invalid input data. Please check your entries", - "server_error": "A server error occurred. Please try again later", - "identity_added_successfully": "Identity added successfully", - "error_adding_identity": "Error adding identity", - "error_updating_identity": "Error updating identity", - "missing_workspace_project": "Missing workspace or project", - "first_name_required": "First name is required", - "last_name_required": "Last name is required", - "invalid_email": "Invalid email format", - "invalid_date_format": "Invalid date format (YYYY-MM-DD)", - "title_required": "Title is required", - "title_placeholder": "Personal Identity", - "first_name_placeholder": "John", - "last_name_placeholder": "Doe", - "email_placeholder": "john.doe@example.com", - "phone_placeholder": "+1 (555) 123-4567", - "address_placeholder": "123 Main St, Anytown, CA 94001", + "identity_added_successfully": "Identität erfolgreich hinzugefügt", + "error_adding_identity": "Fehler beim Hinzufügen der Identität", + "error_updating_identity": "Fehler beim Aktualisieren der Identität", + "missing_workspace_project": "Arbeitsbereich oder Projekt fehlt", + "first_name_required": "Vorname ist erforderlich", + "last_name_required": "Nachname ist erforderlich", + "invalid_email": "Ungültiges E-Mail-Format", + "invalid_date_format": "Ungültiges Datumsformat (YYYY-MM-DD)", + "title_required": "Titel ist erforderlich", + "title_placeholder": "Persönliche Identität", + "first_name_placeholder": "Max", + "last_name_placeholder": "Mustermann", + "email_placeholder": "max.mustermann@beispiel.com", + "phone_placeholder": "+49 (0) 123 456789", + "address_placeholder": "Musterstraße 123, 12345 Musterstadt", "passport_placeholder": "A12345678", - "national_id_placeholder": "123-45-6789", - "notes": "Notes", - "notes_placeholder": "Additional information", - "tags_placeholder": "Add tags", - "adding": "Adding...", - "updating": "Updating...", - "deleting": "Deleting...", - "add": "Add", - "confirm_deletion": "Confirm Deletion", - "delete_identity_confirmation": "Are you sure you want to delete this identity? This action cannot be undone.", - "error_fetching_identities": "Error fetching identities", - "national_id_required": "National ID is required", - "identity_type": "Identity Type", - "sort": "Sort", - "sort_options": "Sort Options", - "sort_by_name": "Sort by Name", - "sort_by_created": "Sort by Created Date", - "sort_by_updated": "Sort by Updated Date", - "ascending": "Ascending", - "descending": "Descending", - "clear_sort": "Clear Sort", - "search_across_all_fields": "Search", - "searches_all_fields": "Searches name, type, contact, address, ID and tags", - "encryption_key_not_found": "Project encryption key not found. Cannot save identity.", - "project_not_found": "Project not found. Please select a valid project.", - "encryption_failed": "Failed to encrypt identity data. Please try again.", - "decryption_failed": "Failed to decrypt identity data. This could be due to a missing or invalid encryption key." + "national_id_placeholder": "12345678901", + "notes": "Notizen", + "notes_placeholder": "Zusätzliche Informationen", + "tags_placeholder": "Tags hinzufügen", + "adding": "Hinzufügen...", + "updating": "Aktualisieren...", + "deleting": "Löschen...", + "add": "Hinzufügen", + "confirm_deletion": "Löschung bestätigen", + "delete_identity_confirmation": "Sind Sie sicher, dass Sie diese Identität löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", + "error_fetching_identities": "Fehler beim Laden der Identitäten", + "national_id_required": "Personalausweisnummer ist erforderlich", + "identity_type": "Identitätstyp", + "sort": "Sortieren", + "sort_options": "Sortieroptionen", + "sort_by_name": "Nach Name sortieren", + "sort_by_created": "Nach Erstellungsdatum sortieren", + "sort_by_updated": "Nach Aktualisierungsdatum sortieren", + "ascending": "Aufsteigend", + "descending": "Absteigend", + "clear_sort": "Sortierung löschen", + "search_across_all_fields": "Suchen", + "searches_all_fields": "Durchsucht Name, Typ, Kontakt, Adresse, ID und Tags", + "encryption_key_not_found": "Projekt-Verschlüsselungsschlüssel nicht gefunden. Identität kann nicht gespeichert werden.", + "project_not_found": "Projekt nicht gefunden. Bitte wählen Sie ein gültiges Projekt aus.", + "encryption_failed": "Fehler beim Verschlüsseln der Identitätsdaten. Bitte versuchen Sie es erneut.", + "decryption_failed": "Fehler beim Entschlüsseln der Identitätsdaten. Dies könnte an einem fehlenden oder ungültigen Verschlüsselungsschlüssel liegen.", + "manage_your_personal_identities": "Verwalten Sie Ihre persönlichen Identitätsinformationen sicher", + "date_created": "Erstellungsdatum" }, "cards": { - "credit_cards": "Kreditkarten", + "title": "Karten", + "description": "Verwalten Sie Ihre Kredit- und Debitkarteninformationen sicher", "add_card": "Karte hinzufügen", - "search_cards": "Karten durchsuchen...", + "no_cards_found": "Keine Karten gefunden", + "no_cards_found_message": "Sie haben noch keine Karteninformationen hinzugefügt.", + "add_your_first_card": "Fügen Sie Ihre erste Karte hinzu", + "loading_cards": "Karten werden geladen...", + "search_cards": "Suchen", + "card_name": "Kartenname", + "name": "Name", + "enter_card_name": "Kartenname eingeben (z.B. Visa Endung 1234)", + "card_number": "Kartennummer", + "enter_card_number": "Kartennummer eingeben", + "expiry_date": "Ablaufdatum", + "enter_expiry_date": "Ablaufdatum eingeben (MM/JJ)", + "cvv": "CVV", + "enter_cvv": "CVV eingeben", + "cardholder_name": "Karteninhaber", + "card_holder_name": "Karteninhaber", + "enter_cardholder_name": "Karteninhaber eingeben", + "tags": "Tags", + "enter_tags_comma_separated": "Tags eingeben (durch Komma getrennt)", + "last_modified": "Zuletzt geändert", + "actions": "Aktionen", + "edit": "Bearbeiten", + "delete": "Löschen", + "copy_field": "{field} kopieren", + "copied": "Kopiert", + "card_deleted_successfully": "Karte erfolgreich gelöscht", + "error_deleting_card": "Fehler beim Löschen der Karte", + "card_updated_successfully": "Karte erfolgreich aktualisiert", + "card_added_successfully": "Karte erfolgreich hinzugefügt", + "failed_to_add_card": "Fehler beim Hinzufügen der Karte", + "all_fields_required": "Bitte füllen Sie alle erforderlichen Felder aus", + "validation_error": "Ungültige Eingabedaten. Bitte überprüfen Sie Ihre Einträge", + "server_error": "Ein Serverfehler ist aufgetreten. Bitte versuchen Sie es später erneut", + "credit_cards": "Kreditkarten", "filter_by_brand": "Nach Marke filtern", "all_brands": "Alle Marken", "search": "Suchen", - "loading_cards": "Karten werden geladen...", - "no_cards_found": "Keine Karten gefunden", "clear_filters": "Filter löschen", "card_holder": "Karteninhaber", "brand": "Marke", - "card_number": "Kartennummer", - "expiry": "Gültig bis", - "cvv": "CVV", - "tags": "Tags", - "actions": "Aktionen", + "expiry": "Ablauf", "add_new_card": "Neue Karte hinzufügen", - "add_new_card_description": "Geben Sie Ihre Kreditkartendaten unten ein", - "card_holder_name": "Name des Karteninhabers", + "add_new_card_description": "Geben Sie Ihre Kreditkartendetails unten ein", "select_brand": "Marke auswählen", "expiry_month": "Ablaufmonat", "expiry_year": "Ablaufjahr", "cancel": "Abbrechen", "save": "Speichern", "edit_card": "Karte bearbeiten", - "edit_card_description": "Aktualisieren Sie Ihre Kreditkartendaten", + "edit_card_description": "Aktualisieren Sie Ihre Kreditkartendetails", "update": "Aktualisieren", - "delete": "Löschen", "card_deleted": "Karte gelöscht", "card_deleted_description": "Die Karte wurde erfolgreich gelöscht", - "copied": "Kopiert", "field_copied": "Feld in die Zwischenablage kopiert", "copy_failed": "Kopieren fehlgeschlagen", "failed_to_copy_field": "Fehler beim Kopieren des Feldes in die Zwischenablage", - "hide_sensitive_data": "Sensible Daten ausblenden", - "show_sensitive_data": "Sensible Daten anzeigen", + "hide_card_number": "Kartennummer ausblenden", + "show_card_number": "Kartennummer anzeigen", + "copy_to_clipboard": "In die Zwischenablage kopieren", "showing_results": "Zeige {startIdx}-{endIdx} von {totalCount} Ergebnissen", "page_of": "Seite {currentPage} von {totalPages}", - "edit": "Bearbeiten", - "manage_your_cards": "Verwalten Sie Ihre Kredit- und Debitkarten sicher", - "date_created": "Erstellungsdatum", - "title": "Cards", - "description": "Manage your credit and debit card information securely", - "no_cards_found_message": "You haven't added any card information yet.", - "add_your_first_card": "Add Your First Card", - "card_name": "Card Name", - "name": "Name", - "enter_card_name": "Enter card name (e.g., Visa Ending 1234)", - "enter_card_number": "Enter card number", - "expiry_date": "Expiry Date", - "enter_expiry_date": "Enter expiry date (MM/YY)", - "enter_cvv": "Enter CVV", - "cardholder_name": "Cardholder Name", - "enter_cardholder_name": "Enter cardholder name", - "enter_tags_comma_separated": "Enter tags (comma-separated)", - "last_modified": "Last Modified", - "copy_field": "Copy {field}", - "card_deleted_successfully": "Card deleted successfully", - "error_deleting_card": "Failed to delete card", - "card_updated_successfully": "Card updated successfully", - "card_added_successfully": "Card added successfully", - "failed_to_add_card": "Failed to add card", - "all_fields_required": "Please fill in all required fields", - "validation_error": "Invalid input data. Please check your entries", - "server_error": "A server error occurred. Please try again later", - "hide_card_number": "Hide card number", - "show_card_number": "Show card number", - "copy_to_clipboard": "Copy to clipboard", - "title_placeholder": "Personal Visa Card", - "card_holder_name_placeholder": "John Doe", + "title_placeholder": "Persönliche Visa-Karte", + "card_holder_name_placeholder": "Max Mustermann", "card_number_placeholder": "4111 1111 1111 1111", - "notes": "Notes", - "notes_placeholder": "Additional information about this card", - "tags_placeholder": "Add tags", - "add": "Add", - "adding": "Adding...", - "updating": "Updating...", - "deleting": "Deleting...", - "confirm_delete": "Confirm Deletion", - "delete_card_confirmation": "Are you sure you want to delete this card? This action cannot be undone.", - "error_fetching_cards": "Error fetching cards", - "missing_workspace_project": "Missing workspace or project", - "title_required": "Title is required", - "card_holder_name_required": "Card holder name is required", - "card_number_required": "Card number is required", - "invalid_card_number": "Invalid card number", - "cvv_required": "CVV is required", - "invalid_cvv": "Invalid CVV", - "brand_required": "Brand is required", - "error_adding_card": "Error adding card", - "error_updating_card": "Error updating card", - "other": "Other", - "filter_by_tag": "Filter by tag", - "all_tags": "All Tags", - "sort": "Sort", - "sort_options": "Sort Options", - "sort_by_name": "Sort by Name", - "sort_by_created": "Sort by Created Date", - "sort_by_updated": "Sort by Updated Date", - "ascending": "Ascending", - "descending": "Descending", - "clear_sort": "Clear Sort", - "search_across_all_fields": "Search", - "searches_all_fields": "Searches title, card holder, card number, brand and tags", - "missing_auth_or_selection": "Missing authentication or selection", - "encryption_key_not_found": "Project encryption key not found. Cannot save card.", - "project_not_found": "Project not found. Please select a valid project.", - "encryption_failed": "Failed to encrypt card data. Please try again.", - "decryption_failed": "Failed to decrypt card data. This could be due to a missing or invalid encryption key." - }, - "software_licenses": { - "software_licenses": "Software-Lizenzen", - "add_license": "Lizenz hinzufügen", - "search_licenses": "Lizenzen durchsuchen...", + "notes": "Notizen", + "notes_placeholder": "Zusätzliche Informationen zu dieser Karte", + "tags_placeholder": "Tags hinzufügen", + "add": "Hinzufügen", + "adding": "Hinzufügen...", + "updating": "Aktualisieren...", + "deleting": "Löschen...", + "confirm_delete": "Löschung bestätigen", + "delete_card_confirmation": "Sind Sie sicher, dass Sie diese Karte löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", + "error_fetching_cards": "Fehler beim Laden der Karten", + "missing_workspace_project": "Arbeitsbereich oder Projekt fehlt", + "title_required": "Titel ist erforderlich", + "card_holder_name_required": "Karteninhaber ist erforderlich", + "card_number_required": "Kartennummer ist erforderlich", + "invalid_card_number": "Ungültige Kartennummer", + "cvv_required": "CVV ist erforderlich", + "invalid_cvv": "Ungültiger CVV", + "brand_required": "Marke ist erforderlich", + "error_adding_card": "Fehler beim Hinzufügen der Karte", + "error_updating_card": "Fehler beim Aktualisieren der Karte", + "other": "Andere", "filter_by_tag": "Nach Tag filtern", "all_tags": "Alle Tags", - "search": "Suchen", - "loading_licenses": "Lizenzen werden geladen...", - "no_licenses_found": "Keine Lizenzen gefunden", - "clear_filters": "Filter löschen", - "name": "Name", - "software": "Software", - "license_key": "Lizenzschlüssel", - "expiry_date": "Ablaufdatum", - "tags": "Tags", - "actions": "Aktionen", - "add_new_license": "Neue Lizenz hinzufügen", - "add_new_license_description": "Geben Sie Ihre Software-Lizenzdaten unten ein", - "cancel": "Abbrechen", - "save": "Speichern", - "edit_license": "Lizenz bearbeiten", - "edit_license_description": "Aktualisieren Sie Ihre Software-Lizenzdaten", - "update": "Aktualisieren", - "delete": "Löschen", - "license_deleted": "Lizenz gelöscht", - "license_deleted_description": "Die Lizenz wurde erfolgreich gelöscht", - "copied": "Kopiert", - "field_copied": "Feld in die Zwischenablage kopiert", - "copy_failed": "Kopieren fehlgeschlagen", - "failed_to_copy_field": "Fehler beim Kopieren des Feldes in die Zwischenablage", - "hide_license_key": "Lizenzschlüssel ausblenden", - "show_license_key": "Lizenzschlüssel anzeigen", - "showing_results": "Zeige {startIdx}-{endIdx} von {totalCount} Ergebnissen", - "page_of": "Seite {currentPage} von {totalPages}", - "edit": "Bearbeiten", - "copy_license_key": "Lizenzschlüssel kopieren", - "expires": "Läuft ab", - "expired": "Abgelaufen", - "expiring_soon": "Läuft bald ab", - "add_a_tag": "Tag hinzufügen", + "sort": "Sortieren", + "sort_options": "Sortieroptionen", + "sort_by_name": "Nach Name sortieren", + "sort_by_created": "Nach Erstellungsdatum sortieren", + "sort_by_updated": "Nach Aktualisierungsdatum sortieren", + "ascending": "Aufsteigend", + "descending": "Absteigend", + "clear_sort": "Sortierung löschen", + "search_across_all_fields": "Suchen", + "searches_all_fields": "Durchsucht Titel, Karteninhaber, Kartennummer, Marke und Tags", + "missing_auth_or_selection": "Authentifizierung oder Auswahl fehlt", + "encryption_key_not_found": "Projekt-Verschlüsselungsschlüssel nicht gefunden. Karte kann nicht gespeichert werden.", + "project_not_found": "Projekt nicht gefunden. Bitte wählen Sie ein gültiges Projekt aus.", + "encryption_failed": "Fehler beim Verschlüsseln der Kartendaten. Bitte versuchen Sie es erneut.", + "decryption_failed": "Fehler beim Entschlüsseln der Kartendaten. Dies könnte an einem fehlenden oder ungültigen Verschlüsselungsschlüssel liegen.", + "manage_your_cards": "Verwalten Sie Ihre Kredit- und Debitkarteninformationen sicher", "date_created": "Erstellungsdatum" }, "wifi": { - "wifi_networks": "WLAN-Netzwerke", - "add_wifi": "WLAN hinzufügen", - "search_wifi": "WLAN-Netzwerke durchsuchen...", - "filter_by_security": "Nach Sicherheitstyp filtern", + "wifi_networks": "Wi-Fi-Netzwerke", + "manage_your_wifi_networks": "Verwalten Sie Ihre Wi-Fi-Netzwerke sicher.", + "add_wifi": "Wi-Fi hinzufügen", + "add_new_wifi": "Neues Wi-Fi hinzufügen", + "add_new_wifi_description": "Geben Sie Ihre Wi-Fi-Netzwerkdetails unten ein", + "edit_wifi": "Wi-Fi bearbeiten", + "edit_wifi_description": "Aktualisieren Sie Ihre Wi-Fi-Netzwerkdetails", + "search_wifi": "Suchen", + "filter_by_security": "Nach Sicherheit filtern", "all_security_types": "Alle Sicherheitstypen", + "none": "Keine", "clear": "Löschen", "clear_filters": "Filter löschen", - "loading_wifi": "WLAN-Netzwerke werden geladen...", - "no_wifi_found": "Keine WLAN-Netzwerke gefunden", - "no_wifi_match_search": "Keine WLAN-Netzwerke entsprechen Ihrer Suche '{search}'", - "no_wifi_for_security_type": "Keine WLAN-Netzwerke für Sicherheitstyp '{security_type}' gefunden", - "no_wifi_for_tag": "Keine WLAN-Netzwerke mit Tag '{tag}' gefunden", + "loading_wifi": "Wi-Fi-Netzwerke werden geladen...", + "no_wifi_found": "Keine Wi-Fi-Netzwerke gefunden", + "no_wifi_for_security_type": "Keine Wi-Fi-Netzwerke für {security_type} gefunden", + "no_wifi_match_search": "Keine Wi-Fi-Netzwerke entsprechen Ihrer Suche '{search}'", + "name": "Name", + "loading_wifi_networks": "Wi-Fi-Netzwerke werden geladen", + "no_wifi_networks_found": "Kein Wi-Fi-Netzwerk gefunden", + "add_wifi_network": "Wi-Fi-Netzwerk hinzufügen", + "no_wifi_for_tag": "Keine Wi-Fi-Netzwerke mit Tag {tag} gefunden", + "adjust_search_filter": "Versuchen Sie, Ihre Suche oder Filter anzupassen, um zu finden, wonach Sie suchen.", "ssid": "SSID", "security": "Sicherheit", "security_type": "Sicherheitstyp", "select_security_type": "Sicherheitstyp auswählen", - "none": "Keine", "notes": "Notizen", "enter_notes": "Notizen eingeben", "password": "Passwort", "enter_password": "Passwort eingeben", - "enter_new_password": "Neues Passwort eingeben (leer lassen, um unverändert zu behalten)", + "enter_new_password": "Neues Passwort eingeben (leer lassen, um unverändert zu lassen)", "show_password": "Passwort anzeigen", - "hide_password": "Passwort verbergen", + "hide_password": "Passwort ausblenden", "copy_password": "Passwort kopieren", "tags": "Tags", "add_a_tag": "Tag hinzufügen", @@ -1039,60 +1069,51 @@ "last_modified": "Zuletzt geändert", "date_created": "Erstellungsdatum", "actions": "Aktionen", - "showing": "Anzeige", + "showing": "Zeige", "of": "von", "rows_per_page": "Zeilen pro Seite", "edit": "Bearbeiten", "copy_ssid": "SSID kopieren", "delete": "Löschen", - "deleting": "Wird gelöscht...", - "confirm_delete": "Löschen bestätigen", - "confirm_delete_wifi": "WLAN-Löschung bestätigen", - "delete_wifi_confirmation": "Sind Sie sicher, dass Sie dieses WLAN-Netzwerk löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", - "wifi_deleted": "WLAN gelöscht", - "wifi_deleted_description": "Das WLAN-Netzwerk wurde erfolgreich gelöscht", - "failed_to_delete_wifi": "Fehler beim Löschen des WLAN-Netzwerks", - "error_deleting_wifi": "Fehler beim Löschen des WLAN", - "error_fetching_wifi_networks": "Fehler beim Abrufen der WLAN-Netzwerke", - "failed_to_fetch_wifi_networks": "Fehler beim Abrufen der WLAN-Netzwerke", + "confirm_delete_wifi": "Wi-Fi löschen bestätigen", + "delete_wifi_confirmation": "Sind Sie sicher, dass Sie dieses Wi-Fi-Netzwerk löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", + "wifi_deleted": "Wi-Fi gelöscht", + "wifi_deleted_description": "Das Wi-Fi-Netzwerk wurde erfolgreich gelöscht.", + "failed_to_delete_wifi": "Fehler beim Löschen des Wi-Fi-Netzwerks", + "error_deleting_wifi": "Fehler beim Löschen des Wi-Fi", + "error_fetching_wifi_networks": "Fehler beim Laden der Wi-Fi-Netzwerke", + "failed_to_fetch_wifi_networks": "Wi-Fi-Netzwerke konnten nicht geladen werden", "no_project_selected": "Kein Projekt ausgewählt. Bitte wählen Sie ein Projekt aus, um fortzufahren.", - "wifi_qr_code": "WLAN QR-Code für {ssid}", - "wifi_qr_code_description": "Scannen Sie diesen Code, um eine Verbindung zum WLAN-Netzwerk herzustellen", - "qr_code_placeholder": "WLAN-Verbindungs-QR-Code", - "wifi_credentials": "WLAN-Zugangsdaten", - "scan_with_phone": "Scannen Sie diesen QR-Code mit Ihrem Telefon, um eine Verbindung zum Netzwerk herzustellen", + "copied": "Kopiert", + "field_copied": "Feld in die Zwischenablage kopiert", + "copy_failed": "Kopieren fehlgeschlagen", + "failed_to_copy_field": "Fehler beim Kopieren des Feldes in die Zwischenablage", + "show_qr_code": "QR-Code anzeigen", + "wifi_qr_code": "Wi-Fi QR-Code", + "wifi_qr_code_description": "Scannen Sie diesen QR-Code, um sich mit dem Wi-Fi-Netzwerk zu verbinden.", + "qr_code_placeholder": "QR-Code", + "wifi_credentials": "Wi-Fi-Anmeldedaten", "close": "Schließen", - "cancel": "Abbrechen", "please_fill_all_required_fields": "Bitte füllen Sie alle erforderlichen Felder aus", - "wifi_added_successfully": "WLAN erfolgreich hinzugefügt", - "wifi_added_description": "Das WLAN-Netzwerk wurde hinzugefügt", - "wifi_already_exists": "Ein WLAN-Netzwerk mit dieser SSID existiert bereits", - "failed_to_add_wifi": "Fehler beim Hinzufügen des WLAN-Netzwerks", + "wifi_added_successfully": "Wi-Fi erfolgreich hinzugefügt", + "wifi_added_description": "Das Wi-Fi-Netzwerk wurde hinzugefügt.", + "wifi_already_exists": "Ein Wi-Fi-Netzwerk mit dieser SSID existiert bereits", + "failed_to_add_wifi": "Fehler beim Hinzufügen des Wi-Fi-Netzwerks", "enter_ssid": "SSID eingeben", - "add_new_wifi": "Neues WLAN hinzufügen", - "add_new_wifi_description": "Geben Sie Ihre WLAN-Netzwerkdetails unten ein", - "edit_wifi": "WLAN bearbeiten", - "edit_wifi_description": "Aktualisieren Sie Ihre WLAN-Netzwerkdetails", - "update_wifi": "WLAN aktualisieren", - "wifi_updated_successfully": "WLAN erfolgreich aktualisiert", - "wifi_updated_description": "Das WLAN-Netzwerk wurde aktualisiert", - "wifi_not_found": "WLAN-Netzwerk nicht gefunden", - "failed_to_update_wifi": "Fehler beim Aktualisieren des WLAN-Netzwerks", + "update_wifi": "Wi-Fi aktualisieren", + "wifi_updated_successfully": "Wi-Fi erfolgreich aktualisiert", + "wifi_updated_description": "Das Wi-Fi-Netzwerk wurde aktualisiert.", + "wifi_not_found": "Wi-Fi-Netzwerk nicht gefunden", + "failed_to_update_wifi": "Fehler beim Aktualisieren des Wi-Fi-Netzwerks", "invalid_input_data": "Ungültige Eingabedaten", - "adding": "Wird hinzugefügt...", - "updating": "Wird aktualisiert...", - "encryption_key_not_found": "Projekt-Verschlüsselungsschlüssel nicht gefunden. WLAN-Netzwerk kann nicht gespeichert werden.", + "adding": "Hinzufügen...", + "updating": "Aktualisieren...", + "deleting": "Löschen...", + "cancel": "Abbrechen", + "encryption_key_not_found": "Projekt-Verschlüsselungsschlüssel nicht gefunden. Wi-Fi-Netzwerk kann nicht gespeichert werden.", "project_not_found": "Projekt nicht gefunden. Bitte wählen Sie ein gültiges Projekt aus.", - "encryption_failed": "Fehler beim Verschlüsseln der WLAN-Daten. Bitte versuchen Sie es erneut.", - "decryption_failed": "Fehler beim Entschlüsseln der WLAN-Daten. Dies könnte an einem fehlenden oder ungültigen Verschlüsselungsschlüssel liegen.", - "adjust_search_filter": "Versuchen Sie, Ihre Suche oder Filter anzupassen, um zu finden, wonach Sie suchen.", - "manage_your_wifi_networks": "Verwalten Sie Ihre WLAN-Netzwerk-Anmeldedaten und Verbindungsdetails", - "filter_by_tag": "Nach Tag filtern", - "all_tags": "Alle Tags", - "copied": "Kopiert", - "field_copied": "Feld in die Zwischenablage kopiert", - "copy_failed": "Kopieren fehlgeschlagen", - "failed_to_copy_field": "Fehler beim Kopieren des Feldes in die Zwischenablage", + "encryption_failed": "Fehler beim Verschlüsseln der Wi-Fi-Daten. Bitte versuchen Sie es erneut.", + "decryption_failed": "Fehler beim Entschlüsseln der Wi-Fi-Daten. Dies könnte an einem fehlenden oder ungültigen Verschlüsselungsschlüssel liegen.", "sort": "Sortieren", "sort_options": "Sortieroptionen", "sort_by_name": "Nach Name sortieren", @@ -1100,21 +1121,29 @@ "sort_by_updated": "Nach Aktualisierungsdatum sortieren", "ascending": "Aufsteigend", "descending": "Absteigend", - "clear_sort": "Sortierung aufheben", - "show_qr_code": "QR-Code anzeigen", - "per_page": "pro Seite", - "name": "Name", - "loading_wifi_networks": "Loading Wifi networks", - "no_wifi_networks_found": "No Wifi Network Found", - "add_wifi_network": "Add Wifi Network" + "clear_sort": "Sortierung löschen", + "filter_by_tag": "Nach Tag filtern", + "all_tags": "Alle Tags", + "scan_with_phone": "Scannen Sie diesen QR-Code mit Ihrem Telefon, um sich mit dem Netzwerk zu verbinden" }, "key_shortcuts": { "title": "Tastenkürzel" }, + "notes": { + "add_new_note":"Neue Notiz hinzufügen", + "note_title": "Notiztitel", + "note_content": "Notizinhalt", + "tags": "Tags", + "update_note": "Notiz aktualisieren", + "enter_note_title": "Notiztitel eingeben", + "add_a_tag": "Tag hinzufügen", + "add_note": "Notiz hinzufügen", + "cancel": "Abbrechen" + }, "emails": { "email_accounts": "E-Mail-Konten", "add_email": "E-Mail hinzufügen", - "search_email_accounts": "E-Mail-Konten durchsuchen...", + "search_email_accounts": "Suchen", "per_page": "pro Seite", "clear_filters": "Filter löschen", "title": "Titel", @@ -1122,9 +1151,11 @@ "servers": "Server", "username": "Benutzername", "password": "Passwort", + "last_modified": "Zuletzt geändert", "tags": "Tags", + "loading_emails": "E-Mails werden geladen", "actions": "Aktionen", - "loading": "Wird geladen...", + "loading": "Laden...", "no_results_found": "Keine Ergebnisse gefunden", "no_email_accounts": "Keine E-Mail-Konten", "copy": "Kopieren", @@ -1136,14 +1167,14 @@ "delete": "Löschen", "deleted": "Gelöscht", "showing_results": "Zeige {start} bis {end} von {total} Ergebnissen", - "confirm_delete": "Löschen bestätigen", + "confirm_delete": "Löschung bestätigen", "delete_email_confirmation": "Sind Sie sicher, dass Sie dieses E-Mail-Konto löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", "cancel": "Abbrechen", - "deleting": "Wird gelöscht...", + "deleting": "Löschen...", "copied": "Kopiert", "field_copied": "Wert in die Zwischenablage kopiert", "copy_failed": "Kopieren fehlgeschlagen", - "failed_to_copy_value": "Wert konnte nicht in die Zwischenablage kopiert werden", + "failed_to_copy_value": "Fehler beim Kopieren des Wertes in die Zwischenablage", "encryption_error": "Verschlüsselungsfehler", "could_not_encrypt_data": "Daten konnten nicht verschlüsselt werden. Bitte versuchen Sie es erneut.", "success": "Erfolg", @@ -1151,7 +1182,7 @@ "error_adding_email": "Fehler beim Hinzufügen des E-Mail-Kontos. Bitte versuchen Sie es erneut.", "email_deleted_successfully": "E-Mail-Anmeldedaten erfolgreich gelöscht", "failed_to_delete_email": "Fehler beim Löschen der E-Mail-Anmeldedaten", - "error_fetching_emails": "Fehler beim Abrufen der E-Mails", + "error_fetching_emails": "Fehler beim Laden der E-Mails", "error": "Fehler", "sort": "Sortieren", "sort_options": "Sortieroptionen", @@ -1160,49 +1191,48 @@ "sort_by_updated": "Nach Aktualisierungsdatum sortieren", "ascending": "Aufsteigend", "descending": "Absteigend", - "clear_sort": "Sortierung aufheben", + "clear_sort": "Sortierung löschen", "add_email_description": "Fügen Sie ein neues E-Mail-Konto zu Ihrem sicheren Tresor hinzu", "title_placeholder": "Arbeits-E-Mail", - "email_address_placeholder": "ihre.email@beispiel.de", + "email_address_placeholder": "ihre.email@beispiel.com", "imap_server": "IMAP-Server", - "imap_server_placeholder": "imap.beispiel.de", + "imap_server_placeholder": "imap.beispiel.com", "smtp_server": "SMTP-Server", - "smtp_server_placeholder": "smtp.beispiel.de", - "username_placeholder": "Optional, standardmäßig die E-Mail-Adresse", + "smtp_server_placeholder": "smtp.beispiel.com", + "username_placeholder": "Optional, standardmäßig E-Mail-Adresse", "password_placeholder": "Passwort eingeben", "notes": "Notizen (optional)", "notes_placeholder": "Zusätzliche Informationen zu diesem E-Mail-Konto", "add_tag": "Tag hinzufügen", "remove": "Entfernen", - "adding": "Wird hinzugefügt...", + "adding": "Hinzufügen...", "add": "Hinzufügen", "please_fill_all_required_fields": "Bitte füllen Sie alle erforderlichen Felder aus", - "email_title_already_exists": "Eine E-Mail mit diesem Titel existiert bereits", + "email_title_already_exists": "E-Mail mit diesem Titel existiert bereits", "invalid_email_format": "Ungültiges E-Mail-Format", "missing_required_data": "Erforderliche Projektdaten fehlen", "edit_email": "E-Mail-Konto bearbeiten", - "edit_email_description": "Aktualisieren Sie Ihre E-Mail-Konto-Details", + "edit_email_description": "Aktualisieren Sie Ihre E-Mail-Kontodetails", "save_changes": "Änderungen speichern", "email_updated_successfully": "E-Mail-Konto erfolgreich aktualisiert", "error_updating_email": "Fehler beim Aktualisieren des E-Mail-Kontos. Bitte versuchen Sie es erneut.", - "manage_your_email_accounts": "Verwalten Sie Ihre E-Mail-Konten und Servereinstellungen sicher", + "manage_your_email_accounts": "Verwalten Sie Ihre E-Mail-Konten und Server-Einstellungen sicher", "date_created": "Erstellungsdatum", "filter_by_tag": "Nach Tag filtern", - "all_tags": "Alle Tags", - "last_modified": "Last Modified", - "loading_emails": "Loading Emails" + "all_tags": "Alle Tags" }, "ssh_keys": { "ssh_keys": "SSH-Schlüssel", "add_ssh_key": "SSH-Schlüssel hinzufügen", - "search_ssh_keys": "SSH-Schlüssel durchsuchen...", + "search_ssh_keys": "Suchen", "per_page": "pro Seite", "clear_filters": "Filter löschen", "name": "Name", "ssh_key": "SSH-Schlüssel", "tags": "Tags", + "loading_ssh_keys": "SSH-Schlüssel werden geladen", "actions": "Aktionen", - "loading": "Wird geladen...", + "loading": "Laden...", "no_results_found": "Keine Ergebnisse gefunden", "no_ssh_keys": "Keine SSH-Schlüssel", "hide_ssh_key": "SSH-Schlüssel ausblenden", @@ -1210,17 +1240,22 @@ "copied": "Kopiert", "ssh_key_copied": "SSH-Schlüssel in die Zwischenablage kopiert", "copy_failed": "Kopieren fehlgeschlagen", - "failed_to_copy_ssh_key": "SSH-Schlüssel konnte nicht in die Zwischenablage kopiert werden", + "failed_to_copy_ssh_key": "Fehler beim Kopieren des SSH-Schlüssels in die Zwischenablage", "copy_to_clipboard": "In die Zwischenablage kopieren", "open_menu": "Menü öffnen", "edit": "Bearbeiten", "delete": "Löschen", + "fingerprint": "Fingerabdruck", + "last_modified": "Zuletzt geändert", "deleted": "Gelöscht", "showing_results": "Zeige {start} bis {end} von {total} Ergebnissen", - "confirm_delete": "Löschen bestätigen", + "confirm_delete": "Löschung bestätigen", "delete_ssh_key_confirmation": "Sind Sie sicher, dass Sie diesen SSH-Schlüssel löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", "cancel": "Abbrechen", - "deleting": "Wird gelöscht...", + "deleting": "Löschen...", + "please_fill_all_required_fields": "Bitte füllen Sie alle erforderlichen Felder aus", + "ssh_key_name_already_exists": "SSH-Schlüssel mit diesem Namen existiert bereits", + "missing_required_data": "Erforderliche Projektdaten fehlen", "encryption_error": "Verschlüsselungsfehler", "could_not_encrypt_data": "Daten konnten nicht verschlüsselt werden. Bitte versuchen Sie es erneut.", "success": "Erfolg", @@ -1230,7 +1265,7 @@ "error_updating_ssh_key": "Fehler beim Aktualisieren des SSH-Schlüssels. Bitte versuchen Sie es erneut.", "ssh_key_deleted_successfully": "SSH-Schlüssel erfolgreich gelöscht", "error_deleting_ssh_key": "Fehler beim Löschen des SSH-Schlüssels", - "error_fetching_ssh_keys": "Fehler beim Abrufen der SSH-Schlüssel", + "error_fetching_ssh_keys": "Fehler beim Laden der SSH-Schlüssel", "add_a_tag": "Tag hinzufügen", "sort": "Sortieren", "sort_options": "Sortieroptionen", @@ -1239,270 +1274,152 @@ "sort_by_updated": "Nach Aktualisierungsdatum sortieren", "ascending": "Aufsteigend", "descending": "Absteigend", - "clear_sort": "Sortierung aufheben", + "clear_sort": "Sortierung löschen", "error": "Fehler", "add_ssh_key_description": "Fügen Sie einen neuen SSH-Schlüssel zu Ihrem sicheren Tresor hinzu", "name_placeholder": "GitHub SSH-Schlüssel", - "ssh_key_placeholder": "Fügen Sie Ihren SSH-Privatschlüssel hier ein", + "ssh_key_placeholder": "Fügen Sie hier Ihren SSH-Privatschlüssel ein", "notes": "Notizen (optional)", "notes_placeholder": "Zusätzliche Informationen zu diesem SSH-Schlüssel", "add_tag": "Tag hinzufügen", "remove": "Entfernen", - "adding": "Wird hinzugefügt...", + "adding": "Hinzufügen...", "add": "Hinzufügen", - "please_fill_all_required_fields": "Bitte füllen Sie alle erforderlichen Felder aus", - "ssh_key_name_already_exists": "SSH-Schlüssel mit diesem Namen existiert bereits", - "missing_required_data": "Erforderliche Projektdaten fehlen", "edit_ssh_key": "SSH-Schlüssel bearbeiten", "edit_ssh_key_description": "Aktualisieren Sie Ihre SSH-Schlüssel-Details", "save_changes": "Änderungen speichern", + "search_licenses": "Suchen", "manage_your_ssh_keys": "Verwalten Sie Ihre SSH-Schlüssel sicher", "filter_by_tag": "Nach Tag filtern", "all_tags": "Alle Tags", - "date_created": "Erstellungsdatum", - "loading_ssh_keys": "Loading SSH Keys", - "fingerprint": "Fingerprint", - "last_modified": "Last Modified", - "search_licenses": "Search" - }, - "generator": { - "password": "Passwort", - "username": "Benutzername", - "pin": "PIN", - "social_security": "Sozialversicherungsnummer", - "email": "E-Mail-Adresse", - "phone": "Telefonnummer", - "credit_card": "Kreditkartennummer", - "vat": "Umsatzsteuer-Identifikationsnummer", - "iban": "IBAN-Nummer", - "passphrase": "Passphrase", - "password_length": "Passwortlänge", - "pincode_length": "PIN-Länge", - "uppercase": "Großbuchstaben (A-Z)", - "lowercase": "Kleinbuchstaben (a-z)", - "numbers": "Zahlen (0-9)", - "symbols": "Symbole (!@#$%)", - "include_spaces": "Leerzeichen einbeziehen", - "exclude_similar": "Ähnliche Zeichen ausschließen (i, l, 1, o, 0)", - "generate": "Generieren", - "generated_result": "Generiertes Ergebnis", - "regenerate": "Neu generieren", - "copy_to_clipboard": "In die Zwischenablage kopieren", - "copied": "Kopiert", - "copy_failed": "Kopieren fehlgeschlagen", - "word_count": "Wortanzahl", - "word_separator": "Worttrennzeichen", - "capitalize": "Großschreibung", - "include_number": "Zahl einbeziehen", - "dice_rollable": "Würfelbar (Diceware)" + "date_created": "Erstellungsdatum" }, "files": { - "files": "Files", - "file_manager": "File Manager", - "add_file": "Upload File", - "create_folder": "Create Folder", - "search_files": "Search files", - "per_page": "per page", - "clear_filters": "Clear filters", + "files": "Dateien", + "file_manager": "Datei-Manager", + "add_file": "Datei hochladen", + "create_folder": "Ordner erstellen", + "search_files": "Dateien suchen", + "per_page": "pro Seite", + "clear_filters": "Filter löschen", "name": "Name", - "size": "Size", - "type": "Type", - "modified": "Modified", - "folder": "Folder", - "file": "File", - "loading_files": "Loading files...", - "actions": "Actions", - "loading": "Loading...", - "no_results_found": "No results found", - "no_files": "No files", - "no_folders": "No folders", - "download": "Download", - "rename": "Rename", - "move": "Move", - "delete": "Delete", - "copied": "Copied", - "file_copied": "File copied to clipboard", - "copy_failed": "Copy failed", - "failed_to_copy_file": "Failed to copy file to clipboard", - "copy_to_clipboard": "Copy to clipboard", - "open_menu": "Open menu", - "edit": "Edit", - "deleted": "Deleted", - "showing_results": "Showing {start} to {end} of {total} results", - "confirm_delete": "Confirm deletion", - "delete_file_confirmation": "Are you sure you want to delete this file? This action cannot be undone.", - "delete_folder_confirmation": "Are you sure you want to delete this folder? This action cannot be undone.", - "cancel": "Cancel", - "deleting": "Deleting...", - "please_fill_all_required_fields": "Please fill all required fields", - "file_name_already_exists": "File with this name already exists in this folder", - "folder_name_already_exists": "Folder with this name already exists in this location", - "missing_required_data": "Missing required project data", - "encryption_error": "Encryption Error", - "could_not_encrypt_data": "Could not encrypt data. Please try again.", - "success": "Success", - "file_uploaded_successfully": "File uploaded successfully", - "error_uploading_file": "Error uploading file. Please try again.", - "file_updated_successfully": "File updated successfully", - "error_updating_file": "Error updating file. Please try again.", - "file_deleted_successfully": "File deleted successfully", - "error_deleting_file": "Error deleting file", - "error_fetching_files": "Failed to fetch files", - "folder_created_successfully": "Folder created successfully", - "error_creating_folder": "Error creating folder. Please try again.", - "folder_updated_successfully": "Folder updated successfully", - "error_updating_folder": "Error updating folder. Please try again.", - "folder_deleted_successfully": "Folder deleted successfully", - "error_deleting_folder": "Error deleting folder", - "error_fetching_folders": "Failed to fetch folders", - "file_too_large": "File size exceeds 50MB limit", - "invalid_file_type": "Invalid file type", - "upload_failed": "Upload failed", - "download_failed": "Download failed", - "sort": "Sort", - "sort_options": "Sort Options", - "sort_by_name": "Sort by Name", - "sort_by_size": "Sort by Size", - "sort_by_modified": "Sort by Modified Date", - "sort_by_type": "Sort by Type", - "ascending": "Ascending", - "descending": "Descending", - "clear_sort": "Clear Sort", - "error": "Error", - "upload_file_description": "Upload a file to your secure vault", - "create_folder_description": "Create a new folder to organize your files", - "name_placeholder": "Enter file or folder name", - "folder_name_placeholder": "Enter folder name", - "notes": "Notes (optional)", - "notes_placeholder": "Additional information about this file", - "add_tag": "Add tag", - "remove": "Remove", - "uploading": "Uploading...", - "creating": "Creating...", - "add": "Add", - "upload": "Upload", - "create": "Create", - "edit_file": "Edit File", - "edit_file_description": "Update your file details", - "edit_folder": "Edit Folder", - "edit_folder_description": "Update your folder details", - "save_changes": "Save Changes", - "manage_your_files": "Manage your files securely", - "filter_by_type": "Filter by type", - "all_types": "All Types", - "date_created": "Date Created", - "file_size": "File Size", - "folder_path": "Folder Path", - "select_folder": "Select Folder", - "move_to_folder": "Move to Folder", - "drag_and_drop_files": "Drag and drop files here, or click to select", - "select_files": "Select Files", - "upload_progress": "Upload Progress", - "upload_complete": "Upload Complete", - "bytes": "bytes", + "size": "Größe", + "type": "Typ", + "modified": "Geändert", + "folder": "Ordner", + "file": "Datei", + "loading_files": "Dateien werden geladen...", + "actions": "Aktionen", + "loading": "Laden...", + "no_results_found": "Keine Ergebnisse gefunden", + "no_files": "Keine Dateien", + "no_folders": "Keine Ordner", + "download": "Herunterladen", + "rename": "Umbenennen", + "move": "Verschieben", + "delete": "Löschen", + "copied": "Kopiert", + "file_copied": "Datei in die Zwischenablage kopiert", + "copy_failed": "Kopieren fehlgeschlagen", + "failed_to_copy_file": "Fehler beim Kopieren der Datei in die Zwischenablage", + "copy_to_clipboard": "In die Zwischenablage kopieren", + "open_menu": "Menü öffnen", + "edit": "Bearbeiten", + "deleted": "Gelöscht", + "showing_results": "Zeige {start} bis {end} von {total} Ergebnissen", + "confirm_delete": "Löschung bestätigen", + "delete_file_confirmation": "Sind Sie sicher, dass Sie diese Datei löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", + "delete_folder_confirmation": "Sind Sie sicher, dass Sie diesen Ordner löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", + "cancel": "Abbrechen", + "deleting": "Löschen...", + "please_fill_all_required_fields": "Bitte füllen Sie alle erforderlichen Felder aus", + "file_name_already_exists": "Datei mit diesem Namen existiert bereits in diesem Ordner", + "folder_name_already_exists": "Ordner mit diesem Namen existiert bereits an diesem Ort", + "missing_required_data": "Erforderliche Projektdaten fehlen", + "encryption_error": "Verschlüsselungsfehler", + "could_not_encrypt_data": "Daten konnten nicht verschlüsselt werden. Bitte versuchen Sie es erneut.", + "success": "Erfolg", + "file_uploaded_successfully": "Datei erfolgreich hochgeladen", + "error_uploading_file": "Fehler beim Hochladen der Datei. Bitte versuchen Sie es erneut.", + "file_updated_successfully": "Datei erfolgreich aktualisiert", + "error_updating_file": "Fehler beim Aktualisieren der Datei. Bitte versuchen Sie es erneut.", + "file_deleted_successfully": "Datei erfolgreich gelöscht", + "error_deleting_file": "Fehler beim Löschen der Datei", + "error_fetching_files": "Fehler beim Laden der Dateien", + "folder_created_successfully": "Ordner erfolgreich erstellt", + "error_creating_folder": "Fehler beim Erstellen des Ordners. Bitte versuchen Sie es erneut.", + "folder_updated_successfully": "Ordner erfolgreich aktualisiert", + "error_updating_folder": "Fehler beim Aktualisieren des Ordners. Bitte versuchen Sie es erneut.", + "folder_deleted_successfully": "Ordner erfolgreich gelöscht", + "error_deleting_folder": "Fehler beim Löschen des Ordners", + "error_fetching_folders": "Fehler beim Laden der Ordner", + "file_too_large": "Dateigröße überschreitet das 50MB-Limit", + "invalid_file_type": "Ungültiger Dateityp", + "upload_failed": "Upload fehlgeschlagen", + "download_failed": "Download fehlgeschlagen", + "sort": "Sortieren", + "sort_options": "Sortieroptionen", + "sort_by_name": "Nach Name sortieren", + "sort_by_size": "Nach Größe sortieren", + "sort_by_modified": "Nach Änderungsdatum sortieren", + "sort_by_type": "Nach Typ sortieren", + "ascending": "Aufsteigend", + "descending": "Absteigend", + "clear_sort": "Sortierung löschen", + "error": "Fehler", + "upload_file_description": "Laden Sie eine Datei in Ihren sicheren Tresor hoch", + "create_folder_description": "Erstellen Sie einen neuen Ordner, um Ihre Dateien zu organisieren", + "name_placeholder": "Datei- oder Ordnername eingeben", + "folder_name_placeholder": "Ordnername eingeben", + "notes": "Notizen (optional)", + "notes_placeholder": "Zusätzliche Informationen zu dieser Datei", + "add_tag": "Tag hinzufügen", + "remove": "Entfernen", + "uploading": "Hochladen...", + "creating": "Erstellen...", + "add": "Hinzufügen", + "upload": "Hochladen", + "create": "Erstellen", + "edit_file": "Datei bearbeiten", + "edit_file_description": "Aktualisieren Sie Ihre Dateidetails", + "edit_folder": "Ordner bearbeiten", + "edit_folder_description": "Aktualisieren Sie Ihre Ordnerdetails", + "save_changes": "Änderungen speichern", + "manage_your_files": "Verwalten Sie Ihre Dateien sicher", + "filter_by_type": "Nach Typ filtern", + "all_types": "Alle Typen", + "date_created": "Erstellungsdatum", + "file_size": "Dateigröße", + "folder_path": "Ordnerpfad", + "select_folder": "Ordner auswählen", + "move_to_folder": "In Ordner verschieben", + "drag_and_drop_files": "Dateien hier hineinziehen und ablegen, oder klicken zum Auswählen", + "select_files": "Dateien auswählen", + "upload_progress": "Upload-Fortschritt", + "upload_complete": "Upload abgeschlossen", + "bytes": "Bytes", "kb": "KB", "mb": "MB", "gb": "GB", "tb": "TB" }, "project_colors": { - "red": "Red", + "red": "Rot", "orange": "Orange", - "amber": "Amber", - "yellow": "Yellow", - "lime": "Lime", - "green": "Green", - "emerald": "Emerald", - "teal": "Teal", + "amber": "Bernstein", + "yellow": "Gelb", + "lime": "Limetten", + "green": "Grün", + "emerald": "Smaragd", + "teal": "Türkis", "cyan": "Cyan", - "sky": "Sky", - "blue": "Blue", + "sky": "Himmelblau", + "blue": "Blau", "indigo": "Indigo", - "violet": "Violet", - "purple": "Purple", + "violet": "Violett", + "purple": "Lila", "fuchsia": "Fuchsia", - "pink": "Pink", + "pink": "Rosa", "rose": "Rose" - }, - "pagination": { - "showing_x_of_y": "Showing {start} - {end} of {total} items", - "page_of": "Page {currentPage} of {totalPages}", - "next": "Next", - "previous": "Previous" - }, - "env": { - "env_variables": "Environment Variables", - "manage_your_env": "Manage your environment variables securely", - "add_env": "Add Environment Variables", - "loading_env": "Loading environment variables...", - "name": "Name", - "env_value": "Value", - "tags": "Tags", - "created_at": "Created At", - "actions": "Actions", - "no_env_yet": "No environment variables added yet", - "no_matching_env": "No matching environment variables found", - "search_across_all_fields": "Search across all fields...", - "show_env": "Show environment variables", - "hide_env": "Hide environment variables", - "copy_env": "Copy environment variables", - "env_copied": "Environment variables copied to clipboard", - "failed_to_copy_env": "Failed to copy environment variables to clipboard", - "delete_env_confirmation": "Are you sure you want to delete this environment variables? This action cannot be undone.", - "showing_x_of_y": "Showing {start} - {end} of {total} items", - "failed_to_decrypt": "Failed to decrypt environment variables", - "add_new_env": "Add New Environment Variables", - "add_new_env_description": "Enter your environment variables details below", - "edit_env": "Edit Environment Variables", - "edit_env_description": "Update your environment variables", - "env_name": "Environment Name", - "enter_env_name": "Enter environment name", - "enter_env_value": "Enter environment value", - "enter_env_variables": "KEY=value\nANOTHER_KEY=another_value", - "env_format_hint": "Enter your environment variables in KEY=value format, one per line", - "notes": "Notes", - "enter_notes": "Enter notes (optional)", - "add_a_tag": "Add a tag", - "add_tag": "Add tag", - "press_enter_to_add": "Press Enter to add a tag", - "cancel": "Cancel", - "add": "Add", - "update": "Update", - "env_added_successfully": "Environment variables added successfully", - "env_updated_successfully": "Environment variables updated successfully", - "env_deleted_successfully": "Environment variables deleted successfully", - "error_adding_env": "Failed to add environment variables", - "error_updating_env": "Failed to update environment variables", - "error_deleting_env": "Failed to delete environment variables", - "error_fetching_env": "Failed to fetch environment variables", - "please_fill_all_required_fields": "Please fill all required fields", - "no_project_selected": "No project selected. Please select a project.", - "clear_filters": "Clear Filters", - "decrypting": "Decrypting environment variables...", - "env_updated_description": "The environment variables have been updated.", - "env_added_description": "The environment variables have been added.", - "env_already_exists": "Environment variables with this name already exists.", - "date_created": "Date Created", - "sort": "Sort", - "sort_options": "Sort Options", - "sort_by_name": "Sort by Name", - "sort_by_created": "Sort by Created Date", - "sort_by_updated": "Sort by Updated Date", - "view_env_code": "View in code editor", - "click_to_add_env": "Click to add environment variables", - "click_to_edit_env": "Click to edit environment variables", - "env_hidden": "••••••••", - "copied": "Copied" - }, - "notes": { - "add_new_note": "Add New Note", - "note_title": "Note Title", - "note_content": "Note Content", - "tags": "Tags", - "update_note": "Update Note", - "enter_note_title": "Enter note title", - "add_a_tag": "Add a tag", - "add_note": "Add Note", - "cancel": "Cancel" } } \ No newline at end of file diff --git a/packages/frontend-web/messages/en/common.json b/packages/frontend-web/messages/en/common.json index 4ccfae03..7303d64c 100644 --- a/packages/frontend-web/messages/en/common.json +++ b/packages/frontend-web/messages/en/common.json @@ -402,7 +402,8 @@ "no_accounts_create": "No accounts found. Create one to get started.", "adjust_search_filter": "Try adjusting your search or filter criteria", "no_recent_activity": "No recent activity", - "no_projects_available": "No projects available" + "no_projects_available": "No projects available", + "no_workspace_selected": "No workspace selected" }, "activity": { "password_updated": "Password updated", diff --git a/packages/frontend-web/messages/es/common.json b/packages/frontend-web/messages/es/common.json new file mode 100644 index 00000000..07f87889 --- /dev/null +++ b/packages/frontend-web/messages/es/common.json @@ -0,0 +1,1426 @@ +{ + "app": { + "title": "Zecrypt", + "description": "Proteja sus datos con Zecrypt" + }, + "common": { + "loading": "Cargando...", + "saving": "Guardando...", + "deleting": "Eliminando...", + "error": "Error", + "success": "Éxito", + "cancel": "Cancelar", + "add": "Agregar", + "save": "Guardar", + "submitting": "Enviando...", + "try_again": "Intentar de nuevo", + "sort": "Ordenar", + "sort_options": "Opciones de ordenamiento", + "sort_by_name": "Ordenar por nombre", + "sort_by_created": "Ordenar por fecha de creación", + "sort_by_updated": "Ordenar por fecha de actualización", + "ascending": "Ascendente", + "descending": "Descendente", + "clear_sort": "Limpiar ordenamiento", + "add_item_description": "Ingrese los detalles a continuación", + "edit_item_description": "Actualizar los detalles" + }, + "pagination": { + "showing_x_of_y": "Mostrando {start} - {end} de {total} elementos", + "page_of": "Página {currentPage} de {totalPages}", + "next": "Siguiente", + "previous": "Anterior" + }, + "env": { + "env_variables": "Variables de entorno", + "manage_your_env": "Gestione sus variables de entorno de forma segura", + "add_env": "Agregar variables de entorno", + "loading_env": "Cargando environment variables...", + "name": "Nombre", + "env_value": "Value", + "tags": "Etiquetas", + "created_at": "Creado el", + "actions": "Acciones", + "no_env_yet": "No environment variables added yet", + "no_matching_env": "No matching environment variables found", + "search_across_all_fields": "Buscar across all fields...", + "show_env": "Mostrar environment variables", + "hide_env": "Ocultar environment variables", + "copy_env": "Copiar environment variables", + "env_copied": "Environment variables copiado to clipboard", + "failed_to_copy_env": "Error al copy environment variables to clipboard", + "delete_env_confirmation": "¿Está seguro you want to delete this environment variables? Esta acción no se puede deshacer.", + "showing_x_of_y": "Mostrando {start} - {end} de {total} elementos", + "failed_to_decrypt": "Error al decrypt environment variables", + "add_new_env": "Agregar New Variables de entorno", + "add_new_env_description": "Entrar your environment variables details below", + "edit_env": "Editar Variables de entorno", + "edit_env_description": "Actualizar your environment variables", + "env_name": "Environment Nombre", + "enter_env_name": "Entrar environment name", + "enter_env_value": "Entrar environment value", + "enter_env_variables": "KEY=value\nANOTHER_KEY=another_value", + "env_format_hint": "Entrar your environment variables in KEY=value format, one per line", + "notes": "Notas", + "enter_notes": "Entrar notes (optional)", + "add_a_tag": "Agregar una etiqueta", + "add_tag": "Agregar etiqueta", + "press_enter_to_add": "Press Entrar to add a tag", + "cancel": "Cancelar", + "add": "Agregar", + "update": "Actualizar", + "env_added_successfully": "Environment variables added successfully", + "env_updated_successfully": "Environment variables updated successfully", + "env_deleted_successfully": "Environment variables deleted successfully", + "error_adding_env": "Error al add environment variables", + "error_updating_env": "Error al update environment variables", + "error_deleting_env": "Error al delete environment variables", + "error_fetching_env": "Error al fetch environment variables", + "please_fill_all_required_fields": "Por favor complete todos los campos requeridos", + "no_project_selected": "No hay proyecto seleccionado. Please select a project.", + "clear_filters": "Limpiar filtros", + "decrypting": "Decrypting environment variables...", + "env_updated_description": "The environment variables have been updated.", + "env_added_description": "The environment variables have been added.", + "env_already_exists": "Environment variables with this name already exists.", + "date_created": "Fecha de creación", + "sort": "Ordenar", + "sort_options": "Opciones de ordenamiento", + "sort_by_name": "Ordenar por nombre", + "sort_by_created": "Ordenar por fecha de creación", + "sort_by_updated": "Ordenar por fecha de actualización", + "view_env_code": "Ver in code editor", + "click_to_add_env": "Click to add environment variables", + "click_to_edit_env": "Click to edit environment variables", + "env_hidden": "••••••••", + "copied": "Copiado" + }, + "licenses": { + "software_licenses": "Licencias de software", + "add_license": "Agregar License", + "filter_by_tag": "Filtrar por etiqueta", + "all_tags": "Todas las etiquetas", + "search": "Buscar", + "loading_licenses": "Cargando licenses...", + "no_licenses_found": "No licenses found", + "clear_filters": "Limpiar filtros", + "name": "Nombre", + "software": "Sdetware", + "license_key": "License Key", + "expiry_date": "Expiry Date", + "tags": "Etiquetas", + "actions": "Acciones", + "add_new_license": "Agregar New License", + "add_new_license_description": "Entrar your sdetware license details below", + "cancel": "Cancelar", + "save": "Guardar", + "edit_license": "Editar License", + "edit_license_description": "Actualizar your sdetware license details", + "update": "Actualizar", + "delete": "Eliminar", + "license_deleted": "License deleted", + "license_deleted_description": "The license has been deleted successfully", + "copied": "Copiado", + "field_copied": "Field copiado to clipboard", + "copy_failed": "Error al copiar", + "failed_to_copy_field": "Error al copy field to clipboard", + "hide_license_key": "Ocultar license key", + "show_license_key": "Mostrar license key", + "showing_results": "Mostraring {startIdx}-{endIdx} de {totalCantidad} resultados", + "page_of": "Página {currentPage} de {totalPages}", + "expires": "Expira", + "expired": "Expirado", + "expiring_soon": "Expira pronto", + "edit": "Editar", + "enter_license_name": "Entrar license name", + "enter_software_name": "Entrar sdetware name", + "enter_license_key": "Entrar license key", + "enter_notes": "Ingresar notas", + "confirm_deletion": "Confirmar Deletion", + "confirm_delete_license_description": "¿Está seguro you want to delete this license? Esta acción no se puede deshacer.", + "license_added_successfully": "License added successfully", + "failed_to_add_license": "Error al add license", + "invalid_input": "Invalid input", + "validation_error": "Error de validación", + "network_error": "Error de red", + "error_adding_license": "Error al agregar license", + "license_updated_successfully": "License updated successfully", + "failed_to_update_license": "Error al update license", + "license_not_found": "License not found", + "error_updating_license": "Error al actualizar license", + "missing_auth_or_selection": "Missing authentication or selection", + "license_deleted_successfully": "License deleted successfully", + "error_deleting_license": "Error al eliminar license", + "error_fetching_licenses": "Error fetching licenses", + "adding": "Agregaring...", + "updating": "Updating...", + "deleting": "Eliminando...", + "notes": "Notas", + "software_name": "Sdetware Nombre", + "rows_per_page": "Filas por página", + "sort": "Ordenar", + "sort_options": "Opciones de ordenamiento", + "sort_by_name": "Ordenar por nombre", + "sort_by_created": "Ordenar por fecha de creación", + "sort_by_updated": "Ordenar por fecha de actualización", + "ascending": "Ascendente", + "descending": "Descendente", + "clear_sort": "Limpiar ordenamiento", + "search_across_all_fields": "Buscar", + "update_api_key": "Actualizar API Key", + "manage_your_software_licenses": "Manage your sdetware licenses securely", + "date_created": "Fecha de creación", + "add_a_tag": "Agregar una etiqueta" + }, + "navigation": { + "home": "Inicio", + "dashboard": "Panel de control", + "settings": "Configuración" + }, + "auth": { + "login_to_continue": "Iniciar sesión to continue", + "sign_in_with_sso": "Iniciar sesión with your organization account", + "login": "Iniciar sesión", + "signup": "Registrarse", + "cancel": "Cancelar", + "logout": "Cerrar sesión", + "sign_up": "Registrarse", + "try_again": "Intentar de nuevo", + "dont_have_account": "Don't have an account?", + "email": "Correo electrónico", + "password": "Contraseña", + "agreement": "When signing in, you agree to our terms and conditions", + "terms": "Terms", + "privacy_policy": "Privacy Policy", + "terms_and_conditions": "Terms and Conditions", + "service_status": "Service Estado", + "generate_password": "Generar contraseña", + "verification_code": "Verification Code", + "2fa_verify": "2FA Verification", + "2fa_enter_code": "Entrar the 6-digit code from your authenticator app", + "2fa_placeholder": "e.g., 123456", + "verify": "Verify", + "verifying_authentication": "Verifying authentication...", + "2fa_verification_error": "Invalid verification code. Por favor intente de nuevo.", + "2fa_verification_success": "2FA verification successful", + "verifying": "Verifying 2FA...", + "authentication_error": "Authentication error", + "2fa_verified": "2FA Verified", + "redirecting_to_dashboard": "Rojoirecting you to dashboard...", + "setup_encryption_keys": "Setup Encryption Keys", + "encryption_setup_description": "Crear a master contraseña to encrypt your data. This contraseña is used to protect your prinúmero de IVAe key.", + "encryption_password_warning": "Remember this contraseña carefully! If you forget it, you will lose access to all your encrypted data.", + "encryption_password": "Master Contraseña", + "enter_strong_password": "Entrar a strong contraseña", + "confirm_encryption_password": "Confirmar Master Contraseña", + "confirm_password": "Confirmar your contraseña", + "encryption_password_requirements": "Contraseña must be at least 8 characters and should include letters, números (0-9), and special characters for maximum security.", + "generating_keys": "Generating keys...", + "setup_encryption": "Setup Encryption", + "password_too_short": "Contraseña must be at least 8 characters long", + "passwords_dont_match": "Contraseñas don't match", + "encryption_setup_success": "Encryption Setup Completar", + "encryption_keys_created": "Your encryption keys have been created successfully", + "encryption_setup_failed": "Error al set up encryption keys", + "checking_encryption_keys": "Checking encryption keys...", + "encryption_key_check_failed": "Encryption Key Check Failed", + "encryption_key_check_failed_desc": "We couldn't verify your encryption keys. This could be due to a network issue or missing keys.", + "encryption_security_error": "Encryption security error. Please try logging in again.", + "login_successful": "Iniciar sesión Éxitdeul", + "2fa_setup_required": "2FA Setup Requerido", + "scan_qr_with_authenticator": "Scan the QR code with your authenticator app", + "unknown_error": "Error desconocido occurred", + "auth_network_error": "Error de red during authentication", + "auth_process_failed": "Authentication process failed", + "2fa_invalid_code": "Invalid verification code", + "2fa_verification_failed": "Verification failed: {message}", + "copied_to_clipboard": "Copiado to clipboard", + "paste_in_authenticator": "Pegar this code into your authenticator app", + "2fa_setup": "Two-Factor Authentication Setup", + "2fa_scan": "Scan the QR code with your authenticator app", + "2fa_scan_difficulty": "Having trouble scanning? You can copy the setup key below:", + "copy_setup_key": "Copiar Setup Key", + "unlock_encryption": "Unlock Encryption", + "enter_encryption_password": "Entrar your master contraseña", + "enter_encryption_password_desc": "Entrar your master contraseña to decrypt your prinúmero de IVAe key.", + "unlocking": "Unlocking...", + "unlock": "Unlock", + "encryption_unlocked": "Encryption Desbloqueado", + "encryption_key_decrypted": "Your encryption key has been successfully decrypted", + "incorrect_encryption_password": "Incorrect master contraseña. Por favor intente de nuevo.", + "multi_device_login": "Iniciar sesión from New Device", + "multi_device_desc": "You're logging in from a new device. Please enter your master contraseña to access your encrypted data.", + "authenticating": "Authenticating...", + "download_password_title": "Your Zecrypt Encryption Contraseña:", + "download_password_important": "IMPORTANT: Keep this contraseña safe. You will need it to access your encrypted data.", + "download_filename": "zecrypt-encryption-contraseña.txt" + }, + "features": { + "trial_title": "Su administrador de almacenamiento seguro", + "unlimited_devices": "Dispositivos ilimitados — completamente gratuito para siempre", + "shared_vaults": "Comparta credenciales sin esfuerzo con compañeros de equipo - (Próximamente)", + "advanced_security": "Cifrado de grado militar (AES-GCM) para seguridad sólida", + "security_alerts": "Alertas de violación instantáneas para que siempre esté un paso adelante - (Próximamente)", + "multi_platform": "Disponible en Mac, iOS, Windows, Android, Chrome OS y Linux - (próximamente)", + "organization": "Organización", + "organization_desc": "Organice credenciales en proyectos para mejor gestión y acceso rápido.", + "secure_storage": "Almacenamiento seguro", + "secure_storage_desc": "Almacene todas sus contraseñas y datos sensibles con cifrado de grado militar.", + "secure_password_manager": "Almacenamiento en la nube cifrado de extremo a extremo" + }, + "actions": { + "add": "Agregar", + "edit": "Editar", + "delete": "Eliminar", + "cancel": "Cancelar", + "close": "Cerrar", + "save": "Guardar", + "saving": "Guardando...", + "deleting": "Eliminando...", + "submit": "Enviar", + "ok": "OK", + "yes": "Sí", + "no": "No", + "confirm": "Confirmar", + "confirm_delete": "Confirmar eliminación", + "continue": "Continuar", + "back": "Atrás", + "next": "Siguiente", + "search": "Buscar", + "filter": "Filtrar", + "clear": "Limpiar", + "reset": "Restablecer", + "refresh": "Actualizar", + "upload": "Subir", + "download": "Descargar", + "copy": "Copiar", + "paste": "Pegar", + "cut": "Cortar", + "undo": "Deshacer", + "redo": "Rehacer", + "select_all": "Seleccionar todo", + "select_none": "No seleccionar nada", + "create": "Crear", + "update": "Actualizar", + "remove": "Eliminar", + "error": "Error", + "view": "Ver" + }, + "dashboard": { + "project_limit_reached": "Límite de proyectos alcanzado", + "alpha_one_project_limit": "Actualmente estamos ejecutando en alpha, solo se permite un proyecto por espacio de trabajo.", + "select_project": "Seleccionar proyectos", + "manage_projects": "Gestionar proyectos", + "select_or_create_project": "Seleccionar o crear un proyecto", + "welcome_back": "Welcome back to your secure workspace", + "total_passwords": "Total de contraseñas", + "total_accounts": "Total de cuentas", + "total_folders": "Total de carpetas", + "recently_added_accounts": "Cuentas agregadas recientemente", + "from_last_month": "del mes pasado", + "create_project_details": "Crear detalles del proyecto", + "recent_activity": "Actividad reciente", + "keyboard_shortcuts": "Keyboard shortcuts", + "generate_password": "Generar contraseña", + "favourites": "Favourites", + "no_description": "Sin descripción", + "notes": "Notas", + "confirm_delete_project": "Confirmar eliminación del proyecto", + "create_project": "Crear proyecto", + "set_as_default_project": "Establecer como proyecto predeterminado", + "project_name": "Nombre del proyecto", + "describe_project_purpose": "Describir el propósito del proyecto", + "free_plan_limit": "Límite del plan gratuito", + "save_changes": "Guardar cambios", + "no_project_availbale": "No hay proyecto disponible", + "cancel": "Cancelar", + "delete": "Eliminar", + "add_favorite_tag": "Agregar Favorito Tag", + "remove_tag": "Eliminar {tag}", + "dashboard": "Panel de control", + "show_more": "Mostrar más", + "show_less": "Mostrar menos", + "overview": "Resumen", + "project": "Proyecto", + "notifications": "Notificaciones", + "settings": "Configuración", + "logout": "Cerrar sesión", + "current_project": "Proyecto actual", + "default": "Predeterminado", + "no_project_selected": "No hay proyecto seleccionado", + "quick_stats": "Estadísticas rápidas", + "workspaces": "Espacios de trabajo", + "projects": "Proyectos", + "available_features": "Características disponibles", + "team_members": "Miembros del equipo", + "total_projects": "Total de proyectos", + "total_workspaces": "Total de espacios de trabajo", + "total_team_members": "Total de miembros del equipo", + "total_records": "Total de registros", + "my_account": "Mi cuenta", + "change_language": "Cambiar idioma", + "language": "Idioma", + "toggle_theme": "Alternar tema", + "wallet_passphrases": "Frases de contraseña de billetera", + "wi_fi": "Wi-Fi", + "env": "Variables de entorno", + "env_variables": "Variables de entorno", + "search": "Buscar", + "create_new_project": "Crear nuevo proyecto", + "add_new_project_description": "Agregar un nuevo proyecto para organizar sus datos seguros", + "description": "Descripción", + "optional": "Opcional", + "project_color": "Color del proyecto", + "enabled_modules": "Módulos habilitados", + "edit_project": "Editar proyecto", + "update_project_details": "Actualizar los detalles de su proyecto", + "my_project": "Mi proyecto", + "creating": "Creando...", + "delete_project": "Eliminar proyecto", + "identity_coming_soon": "El módulo de identidad está en desarrollo.", + "cards_coming_soon": "El módulo de tarjetas está en desarrollo.", + "software_licenses_coming_soon": "El módulo de licencias de software está en desarrollo.", + "wifi_coming_soon": "El módulo Wi-Fi está en desarrollo.", + "profile": "Perfil", + "search_modules": "Buscar módulos...", + "no_modules_found": "No se encontraron módulos", + "general": "General", + "security_access": "Seguridad y acceso", + "business_finance": "Negocios y finanzas", + "licenses_services": "Licencias y servicios", + "accounts": "Cuentas", + "identity": "Identidad", + "email": "Correo electrónico", + "wifi": "WiFi", + "api_keys": "Claves API", + "ssh_keys": "Claves SSH", + "cards": "Cards", + "software_licenses": "Licencias de software", + "recent_accounts": "Cuentas recientes", + "no_recent_accounts": "Sin cuentas recientes", + "last_24_hours": "Últimas 24 horas", + "no_accounts_create": "No se encontraron cuentas. Cree una para comenzar.", + "adjust_search_filter": "Intente ajustar sus criterios de búsqueda o filtro", + "no_recent_activity": "Sin actividad reciente", + "no_projects_available": "No hay proyectos disponibles", + "no_workspace_selected": "No hay espacio de trabajo seleccionado" + + }, + "activity": { + "password_updated": "Contraseña actualizada", + "note_created": "Seguro note created", + "you_create_login": "Creó un nuevo inicio de sesión", + "you_create_api_key": "Creó una nueva clave API", + "you_update_login": "Actualizó un inicio de sesión", + "you_update_api_key": "Actualizó una clave API", + "you_delete_login": "Eliminó un inicio de sesión", + "you_delete_api_key": "Eliminó una clave API", + "create_login": "Nuevo inicio de sesión creado", + "create_api_key": "Nueva clave API creada", + "update_login": "Inicio de sesión actualizado", + "update_api_key": "Clave API actualizada", + "delete_login": "Inicio de sesión eliminado", + "delete_api_key": "Clave API eliminada" + }, + "time": { + "today": "Hoy", + "yesterday": "Ayer", + "last_7_days": "Últimos 7 días", + "last_30_days": "Últimos 30 días", + "all_time": "Todo el tiempo", + "just_now": "Ahora mismo", + "hours_ago": "hace {hours} horas", + "days_ago": "hace {days} días" + }, + "password_generator": { + "title": "Generador de contraseñas", + "strong": "Fuerte", + "memorable": "Memorable", + "pin": "PIN", + "passphrase": "Frase de contraseña", + "chars": "caracteres", + "digits": "dígitos", + "generate_new": "Generar nueva contraseña", + "generate_password": "Generar contraseña", + "click_generate_button": "Haga clic en el botón Generar para generar una nueva contraseña", + "copy": "Copiar contraseña", + "copied": "¡Copiado!", + "save": "Guardar contraseña", + "close": "Cerrar", + "copy_password": "Copiar contraseña", + "password_length": "Longitud de contraseña", + "options": "Opciones", + "advanced": "Avanzado", + "advanced_options": "Opciones avanzadas", + "history": "Historial", + "uppercase": "Mayúsculas (A-Z)", + "lowercase": "Minúsculas (a-z)", + "numbers": "Números (0-9)", + "symbols": "Símbolos (!@#$%^&*)", + "password_analysis": "Análisis de contraseña", + "strong_enough": "Suficientemente fuerte para la mayoría de sitios", + "not_strong_enough": "No suficientemente fuerte para cuentas sensibles", + "good_length": "Buena longitud", + "consider_longer": "Considere usar una contraseña más larga", + "using_all_types": "Usando todos los tipos de caracteres", + "consider_more_types": "Considere usar más tipos de caracteres", + "all_characters": "Todos los caracteres", + "easy_to_read": "Fácil de leer (sin caracteres similares como 1, l, I, 0, O)", + "easy_to_say": "Fácil de decir (sin números o caracteres especiales)", + "password_tips": "Consejos para contraseñas", + "tip_min_length": "• Use un mínimo de 14 caracteres para alta seguridad", + "tip_mix": "• Incluya una mezcla de letras, números y símbolos", + "tip_no_personal": "• Evite usar información personal", + "tip_unique": "• Use una contraseña diferente para cada cuenta", + "tip_manager": "• Considere usar un administrador de contraseñas para almacenar sus contraseñas", + "recently_generated": "Recientemente generado", + "clear_history": "Limpiar historial", + "no_history": "Aún no hay historial de contraseñas", + "history_hint": "Las contraseñas generadas aparecerán aquí", + "weak": "Débil", + "fair": "Regular", + "good": "Bueno", + "strong_strength": "Fuerte", + "strength": "{strength}% de fuerza", + "current_session": "Sesión actual", + "create_secure_passwords": "Crear contraseñas seguras para sus cuentas" + }, + "user_settings": { + "title": "Configuración de usuario", + "description": "Gestionar la configuración y preferencias de su cuenta", + "profile": "Perfil", + "security": "Seguridad", + "notifications": "Notificaciones", + "integrations": "Integraciones", + "billing": "Facturación", + "logout": "Cerrar sesión", + "login_history": "Historial de inicio de sesión", + "audit_log": "Registro de auditoría", + "profile_title": "Perfil e información de cuenta", + "profile_description": "Gestionar sus detalles personales y de cuenta", + "language": "Idioma", + "select_language": "Seleccionar idioma", + "email_address": "Dirección de correo electrónico", + "full_name": "Nombre completo", + "previous": "Anterior", + "next": "Siguiente", + "page": "Página", + "of": "de", + "saving": "Guardando...", + "save_changes": "Guardar cambios", + "profile_picture": "Foto de perfil", + "loading_history": "El historial de inicio de sesión se está cargando...", + "no_history_available": "No hay historial de inicio de sesión disponible", + "try_again": "Intentar de nuevo", + "device_browser": "Dispositivo y navegador", + "ip_address": "Dirección IP", + "time": "Hora" + }, + "accounts": { + "no_accounts": "No accounts", + "no_matching_accounts": "No matching accounts found", + "loading_accounts": "Cargando accounts...", + "error_fetching_accounts": "Error al fetch accounts", + "account_deleted_successfully": "Account deleted successfully", + "error_deleting_account": "Error al delete account", + "account_updated_successfully": "Account updated successfully", + "all_fields_required": "Por favor complete todos los campos requeridos", + "account_already_exists": "An account with this name already exists", + "validation_error": "Datos de entrada inválidos. Please check your entries", + "failed_to_add_account": "Error al add account. Por favor intente de nuevo", + "missing_auth_or_selection": "Please log in or select a workspace and project", + "please_log_in": "Please log in to continue", + "server_error": "A server error occurred. Por favor intente de nuevo later", + "adding": "Agregaring...", + "ascending": "Ascendente", + "descending": "Descendente", + "add": "Agregar", + "cancel": "Cancelar", + "account_name": "Account Nombre", + "enter_account_name": "Entrar account name", + "username": "Nombre de usuario", + "enter_username": "Entrar nombre de usuario", + "add_account": "Agregar Account", + "add_new_account": "Agregar New Account", + "password": "Contraseña", + "enter_password": "Ingresar contraseña", + "website": "Sitio web", + "enter_website": "Ingresar URL del sitio web", + "enter_website_url": "Ingresar URL del sitio web", + "notes": "Notas", + "enter_notes": "Ingresar notas", + "tags": "Etiquetas", + "enter_tags_comma_separated": "Entrar tags (comma-separated)", + "accounts": "Cuentas", + "manage_your_saved_accounts_and_passwords": "Manage your saved accounts and contraseñas", + "encryption_key_not_found": "Clave de cifrado del proyecto no encontrada. No se puede guardar la cuenta.", + "project_not_found": "Proyecto no encontrado. Por favor seleccione un proyecto válido.", + "encryption_failed": "Error al cifrar los datos de la cuenta. Por favor intente de nuevo.", + "decryption_failed": "Error al descifrar los datos de la cuenta. Esto podría deberse a una clave de cifrado faltante o inválida.", + "please_fill_all_required_fields": "Por favor complete todos los campos requeridos", + "select_project": "Select Proyecto", + "no_project_selected": "No Proyecto Selected", + "please_select_a_project": "Please select a project to view accounts", + "loading": "Cargando...", + "error": "Error", + "try_again": "Try Again", + "no_accounts_found": "No Cuentas Found", + "no_accounts_exist": "No accounts exist for this project", + "adjust_search_or_filter": "Adjust your search or filter to find accounts", + "generate_password": "Generar contraseña", + "account": "Account", + "category": "Category", + "last_modified": "Última modificación", + "date_created": "Fecha de creación", + "actions": "Acciones", + "copy_username": "Copiar Nombre de usuario", + "copied": "Copiado", + "copy_password": "Copiar contraseña", + "show_password": "Mostrar Contraseña", + "hide_password": "Ocultar Contraseña", + "edit": "Editar", + "delete": "Eliminar", + "confirm_deletion": "Confirmar Deletion", + "confirm_delete_account": "¿Está seguro you want to delete this account?", + "confirm_delete_account_description": "Esta acción no se puede deshacer and will permanently delete the account credentials.", + "deleting": "Eliminando...", + "showing": "Mostrando", + "of": "de", + "rows_per_page": "Filas por página", + "clear_filters": "Limpiar filtros", + "accounts_not_found": "Cuentas not found", + "account_not_found": "Account not found", + "failed_to_delete_account": "Error al delete account", + "failed_to_update_account": "Error al update account", + "updating": "Updating...", + "update": "Actualizar", + "view_details": "Ver Details", + "filter_warning": "Filtrars may affect pagination accuracy as server-side filtering is not supported.", + "search": "Buscar", + "search_accounts": "Buscar accounts", + "all_accounts": "Todos Cuentas", + "personal": "Personal", + "work": "Trabajo", + "finance": "Finanzas", + "clear": "Limpiar", + "clear_sort": "Limpiar", + "favorite": "Favorito", + "no_accounts_create": "No se encontraron cuentas. Cree una para comenzar.", + "adjust_search_filter": "Intente ajustar sus criterios de búsqueda o filtro", + "error_loading_workspace": "Error al load workspace data", + "focus_search": "Enfocar búsqueda", + "switch_project": "Cambiar proyecto", + "add_a_tag": "Agregar una etiqueta", + "edit_account": "Editar cuenta", + "update_account": "Actualizar cuenta", + "filter_by_tag": "Filtrar por etiqueta", + "confirm_delete": "Confirmar eliminación", + "delete_account_confirmation": "¿Está seguro de que desea eliminar esta cuenta? Esta acción no se puede deshacer.", + "sort": "Ordenar", + "sort_options": "Opciones de ordenamiento", + "sort_by_name": "Ordenar por nombre", + "sort_by_created": "Ordenar por fecha de creación", + "sort_by_updated": "Ordenar por fecha de actualización" + }, + "api_keys": { + "title": "Claves API", + "description": "Manage your API keys for programmatic access to Zecrypt services.", + "edit_api_key": "Editar API Key", + "search": "Buscar", + "name": "Nombre", + "key": "Key", + "api_key": "API Key", + "enter_api_key": "Entrar API key", + "type": "Tipo", + "created": "Creard", + "expires": "Expira", + "status": "Estado", + "actions": "Acciones", + "clear": "Limpiar", + "sort": "Ordenar", + "last_used": "Environment", + "sort_options": "Opciones de ordenamiento", + "sort_by_name": "Ordenar por nombre", + "sort_by_created": "Ordenar por fecha de creación", + "sort_by_updated": "Ordenar por fecha de actualización", + "ascending": "Ascendente", + "descending": "Descendente", + "clear_sort": "Limpiar ordenamiento", + "clear_filters": "Limpiar filtros", + "create_new_api_key": "Crear New API Key", + "enter_details_for_new_api_key": "Entrar the details for your new API Key", + "api_key_name": "API Key Nombre", + "enter_api_key_name": "e.g., Production Atrásend", + "key_type": "Key Tipo", + "key_preview": "Key Preview", + "key_preview_description": "The actual key will be generard when you click \"Crear API Key\"", + "create_api_key": "Crear API Key", + "cancel": "Cancelar", + "rows_per_page": "Filas por página", + "add_api_key": "Agregar API Key", + "no_api_keys_found": "No Claves API Found", + "try_adjusting_search_or_filter": "Intente ajustar sus criterios de búsqueda o filtro", + "error_fetching_api_keys": "Error al fetch API keys", + "failed_to_fetch_api_keys": "Unable to load API keys. Por favor intente de nuevo.", + "confirm_delete_api_key": "¿Está seguro you want to delete this API key?", + "api_key_deleted_successfully": "API Key-phase-2 Key Eliminard", + "api_key_deleted_description": "The API key has been successfully deleted.", + "error_deleting_api_key": "Error al Eliminar API Key", + "failed_to_delete_api_key": "Unable to delete the API key. Por favor intente de nuevo.", + "api_key_updated_successfully": "API Key Actualizard", + "api_key_updated_description": "The API key has been successfully updated.", + "api_key_added_successfully": "API Key Agregared", + "api_key_added_description": "The API key has been successfully added.", + "failed_to_update_api_key": "Unable to update the API key. Por favor intente de nuevo.", + "manage_your_api_keys": "Manage your API keys", + "showing": "Mostrando", + "of": "de", + "add_new_api_key": "Agregar New API Key", + "add_new_api_key_description": "Entrar your API key details below", + "api_key_already_exists": "An API key with this name already exists.", + "error_adding_api_key": "Error al add API key due to a server error.", + "failed_to_add_api_key": "Unable to add the API key. Por favor intente de nuevo.", + "error_updating_api_key": "Error al update API key due to a server error.", + "invalid_input_data": "Datos de entrada inválidos. Please check your entries.", + "network_error": "Error de red. Please check your connection and try again.", + "no_api_keys_for_env": "No API keys found for environment '{env}'. Try a different environment or clear filters.", + "no_api_keys_match_search": "No API keys match the search '{search}'. Try adjusting your search.", + "adjust_search_filter": "Intente ajustar sus criterios de búsqueda o filtro.", + "copied": "Copiado", + "api_key_copied": "The API key has been copiado to your clipboard.", + "copy_failed": "Copiar Failed", + "failed_to_copy_api_key": "Unable to copy the API key to clipboard.", + "show_key": "Mostrar Key", + "hide_key": "Ocultar Key", + "copy_key": "Copiar Key", + "edit": "Editar", + "delete": "Eliminar", + "decryption_error": "Decryption error - please edit to update format", + "adding": "Agregaring...", + "updating": "Updating", + "no_project_selected": "No hay proyecto seleccionado. Please select a project to continue.", + "please_fill_all_required_fields": "Por favor complete todos los campos requeridos", + "loading_api_keys": "Cargando API keys...", + "api_keys": "Claves API", + "env": "Environment", + "tags": "Etiquetas", + "last_modified": "Última modificación", + "api_key_description": "API Key Descripción", + "enter_description": "Entrar a description for this API key", + "environment": "Environment", + "select_environment": "Select environment", + "development": "Development", + "staging": "Staging", + "production": "Production", + "testing": "Testing", + "local": "Local", + "uat": "UAT", + "add": "Agregar", + "add_a_tag": "Agregar una etiqueta", + "update_api_key": "Actualizar API Key", + "all_envs": "Todos Environments", + "search_across_all_fields": "Buscar", + "delete_api_key_confirmation": "¿Está seguro you want to delete this API key? Esta acción no se puede deshacer.", + "deleting": "Eliminando...", + "confirm_deletion": "Confirmar Deletion", + "edit_api_key_description": "Actualizar your API key details", + "showing_results": "Mostraring {startIdx}-{endIdx} de {totalCantidad} resultados", + "notes": "Notas", + "enter_notes": "Ingresar notas", + "filter_by_env": "Filtrar by environment", + "date_created": "Fecha de creación" + }, + "wallet_passphrases": { + "wallet_passphrases": "Frases de contraseña de billetera", + "securely_store_and_manage_your_wallet_recovery_phrases": "Seguroly store and manage your wallet recovery phrases", + "add_passphrase": "Agregar Frase de contraseña", + "enter_wallet_address": "Entrar wallet address", + "copy_wallet_address": "Copiar wallet address", + "wallet_address": "Wallet Agregarress", + "add_your_first_passphrase": "Agregar Your First Frase de contraseña", + "search_passphrases": "Buscar", + "no_wallet_passphrases_found": "No Wallet Frase de contraseña Found", + "wallet_address_encryption_note": "Wallet address is encrypted for security", + "no_passphrases_found": "No Frase de contraseñas Found", + "no_passphrases_found_message": "You haven't added any wallet frase de contraseñas yet. Agregar one to get started.", + "no_passphrases_found_search": "No frase de contraseñas match your search criteria.", + "loading_passphrases": "Cargando frase de contraseñas...", + "please_wait_while_we_fetch_your_secure_passphrases": "Por favor espere while we fetch your secure frase de contraseñas", + "error_loading_passphrases": "Error Cargando Frase de contraseñas", + "failed_to_load_passphrases": "Error al load frase de contraseñas. Por favor intente de nuevo.", + "passphrase_added_successfully": "Frase de contraseña Agregared Éxitdeully", + "passphrase_added_description": "Your wallet frase de contraseña has been securely stored.", + "passphrase_updated_successfully": "Frase de contraseña Actualizard Éxitdeully", + "passphrase_updated_description": "Your wallet frase de contraseña has been updated.", + "failed_to_add_passphrase": "Error al Agregar Frase de contraseña", + "failed_to_update_passphrase": "Error al Actualizar Frase de contraseña", + "wallet_phrase_already_exists": "This frase de contraseña or name already exists in your wallet", + "invalid_input_data": "Datos de entrada inválidos. Please check your entries.", + "no_project_selected": "No hay proyecto seleccionado. Please select a project.", + "passphrase_required": "Frase de contraseña is required", + "passphrase_must_be_exactly_12_words": "Frase de contraseña must contain exactly 12 words (currently {count})", + "please_fill_all_required_fields": "Por favor complete todos los campos requeridos", + "name": "Nombre", + "add": "Agregar", + "sort": "Ordenar", + "sort_by_name": "Ordenar por nombre", + "sort_by_created": "Ordenar por fecha de creación", + "sort_by_updated": "Ordenar por fecha de actualización", + "sort_options": "Opciones de ordenamiento", + "enter_passphrase_name": "Entrar frase de contraseña name", + "passphrase": "Frase de contraseña", + "enter_wallet_recovery_phrase": "Entrar wallet recovery phrase", + "notes": "Notas", + "enter_notes": "Ingresar notas", + "wallet_type": "Wallet Tipo", + "select_wallet_type": "Select wallet type", + "tags": "Etiquetas", + "add_a_tag": "Agregar una etiqueta", + "save_passphrase": "Guardar Frase de contraseña", + "update_passphrase": "Actualizar Frase de contraseña", + "passphrase_encryption_note": "Your frase de contraseña will be encrypted for security.", + "adding": "Agregaring...", + "updating": "Updating...", + "search_across_all_fields": "Buscar", + "filter_by_wallet_type": "Filtrar by wallet type", + "all_wallet_types": "Todos Wallet Tipos", + "clear_filters": "Limpiar filtros", + "last_modified": "Última modificación", + "actions": "Acciones", + "hide_passphrase": "Ocultar frase de contraseña", + "show_passphrase": "Mostrar frase de contraseña", + "copy_passphrase": "Copiar frase de contraseña", + "edit": "Editar", + "delete": "Eliminar", + "showing_results": "Mostraring {startIdx}-{endIdx} de {totalCantidad} resultados", + "confirm_deletion": "Confirmar Deletion", + "delete_passphrase_confirmation": "¿Está seguro you want to delete this wallet frase de contraseña? Esta acción no se puede deshacer.", + "deleting": "Eliminando...", + "add_new_passphrase_description": "Entrar your wallet frase de contraseña details below", + "edit_passphrase": "Editar Frase de contraseña", + "edit_passphrase_description": "Actualizar your wallet frase de contraseña details", + "copied": "Copiado", + "passphrase_copied": "Frase de contraseña copiado to clipboard", + "copy_failed": "Error al copiar", + "failed_to_copy_passphrase": "Error al copy frase de contraseña to clipboard", + "cancel": "Cancelar", + "date_created": "Fecha de creación" + }, + "identity": { + "title": "Identidad", + "description": "Manage your personal identity information securely", + "add_identity": "Agregar Identidad", + "no_identities_found": "No identities found", + "no_identities_found_message": "You haven't added any identity information yet.", + "add_your_first_identity": "Agregar Your First Identidad", + "loading_identities": "Cargando identities...", + "search_identities": "Buscar", + "full_name": "Nombre completo", + "country": "Cantidadry", + "enter_full_name": "Entrar full name", + "date_of_birth": "Date de Birth", + "enter_date_of_birth": "Entrar date de birth (YYYY-MM-DD)", + "address": "Agregarress", + "enter_address": "Entrar address", + "phone_number": "Phone Number", + "enter_phone_number": "Entrar número de teléfono number", + "identity_number": "Identidad Number", + "enter_identity_number": "Entrar identity number (e.g., passport, ID)", + "tags": "Etiquetas", + "enter_tags_comma_separated": "Entrar tags (comma-separated)", + "last_modified": "Última modificación", + "actions": "Acciones", + "edit": "Editar", + "delete": "Eliminar", + "copy_field": "Copiar {field}", + "copied": "Copiado", + "identity_deleted_successfully": "Identidad deleted successfully", + "error_deleting_identity": "Error al delete identity", + "identity_updated_successfully": "Identidad updated successfully", + "failed_to_add_identity": "Error al add identity", + "all_fields_required": "Por favor complete todos los campos requeridos", + "validation_error": "Datos de entrada inválidos. Please check your entries", + "server_error": "A server error occurred. Por favor intente de nuevo later", + "identities": "Identities", + "filter_by_tag": "Filtrar por etiqueta", + "all_tags": "Todas las etiquetas", + "search": "Buscar", + "clear_filters": "Limpiar filtros", + "name": "Nombre", + "contact": "Contact", + "passport": "Passport", + "national_id": "National ID", + "add_new_identity": "Agregar New Identidad", + "add_new_identity_description": "Entrar your identity details below", + "first_name": "First Nombre", + "last_name": "Last Nombre", + "email": "Correo electrónico", + "phone": "Phone", + "passport_number": "Passport Number", + "cancel": "Cancelar", + "save": "Guardar", + "edit_identity": "Editar Identidad", + "edit_identity_description": "Actualizar your identity details", + "update": "Actualizar", + "identity_deleted": "Identidad deleted", + "identity_deleted_description": "The identity has been deleted successfully", + "field_copied": "Field copiado to clipboard", + "copy_failed": "Error al copiar", + "failed_to_copy_field": "Error al copy field to clipboard", + "hide_sensitive_data": "Ocultar sensitive data", + "show_sensitive_data": "Mostrar sensitive data", + "showing_results": "Mostraring {startIdx}-{endIdx} de {totalCantidad} resultados", + "page_of": "Página {currentPage} de {totalPages}", + "copy_passport": "Copiar passport number", + "copy_national_id": "Copiar national ID", + "identity_added_successfully": "Identidad added successfully", + "error_adding_identity": "Error al agregar identity", + "error_updating_identity": "Error al actualizar identity", + "missing_workspace_project": "Missing workspace or project", + "first_name_required": "First name is required", + "last_name_required": "Last name is required", + "invalid_email": "Invalid dirección de correo electrónico format", + "invalid_date_format": "Invalid date format (YYYY-MM-DD)", + "title_required": "Título is required", + "title_placeholder": "Personal Identidad", + "first_name_placeholder": "John", + "last_name_placeholder": "Doe", + "email_placeholder": "john.doe@example.com", + "phone_placeholder": "+1 (555) 123-4567", + "address_placeholder": "123 Main St, Anytown, CA 94001", + "passport_placeholder": "A12345678", + "national_id_placeholder": "123-45-6789", + "notes": "Notas", + "notes_placeholder": "Agregaritional information", + "tags_placeholder": "Agregar tags", + "adding": "Agregaring...", + "updating": "Updating...", + "deleting": "Eliminando...", + "add": "Agregar", + "confirm_deletion": "Confirmar Deletion", + "delete_identity_confirmation": "¿Está seguro you want to delete this identity? Esta acción no se puede deshacer.", + "error_fetching_identities": "Error fetching identities", + "national_id_required": "National ID is required", + "identity_type": "Identidad Tipo", + "sort": "Ordenar", + "sort_options": "Opciones de ordenamiento", + "sort_by_name": "Ordenar por nombre", + "sort_by_created": "Ordenar por fecha de creación", + "sort_by_updated": "Ordenar por fecha de actualización", + "ascending": "Ascendente", + "descending": "Descendente", + "clear_sort": "Limpiar ordenamiento", + "search_across_all_fields": "Buscar", + "searches_all_fields": "Buscares name, type, contact, address, ID and tags", + "encryption_key_not_found": "Proyecto encryption key not found. Cannot save identity.", + "project_not_found": "Proyecto no encontrado. Por favor seleccione un proyecto válido.", + "encryption_failed": "Error al encrypt identity data. Por favor intente de nuevo.", + "decryption_failed": "Error al decrypt identity data. This could be due to a missing or invalid encryption key.", + "manage_your_personal_identities": "Manage your personal identity information securely", + "date_created": "Fecha de creación" + }, + "cards": { + "title": "Cards", + "description": "Manage your credit and debit card information securely", + "add_card": "Agregar Card", + "no_cards_found": "No cards found", + "no_cards_found_message": "You haven't added any card information yet.", + "add_your_first_card": "Agregar Your First Card", + "loading_cards": "Cargando cards...", + "search_cards": "Buscar", + "card_name": "Card Nombre", + "name": "Nombre", + "enter_card_name": "Entrar card name (e.g., Visa Finalizaring 1234)", + "card_number": "Card Number", + "enter_card_number": "Entrar card number", + "expiry_date": "Expiry Date", + "enter_expiry_date": "Entrar expiry date (MM/YY)", + "cvv": "CVV", + "enter_cvv": "Entrar CVV", + "cardholder_name": "Cardholder Nombre", + "card_holder_name": "Card Holder Nombre", + "enter_cardholder_name": "Entrar cardholder name", + "tags": "Etiquetas", + "enter_tags_comma_separated": "Entrar tags (comma-separated)", + "last_modified": "Última modificación", + "actions": "Acciones", + "edit": "Editar", + "delete": "Eliminar", + "copy_field": "Copiar {field}", + "copied": "Copiado", + "card_deleted_successfully": "Card deleted successfully", + "error_deleting_card": "Error al delete card", + "card_updated_successfully": "Card updated successfully", + "card_added_successfully": "Card added successfully", + "failed_to_add_card": "Error al add card", + "all_fields_required": "Por favor complete todos los campos requeridos", + "validation_error": "Datos de entrada inválidos. Please check your entries", + "server_error": "A server error occurred. Por favor intente de nuevo later", + "credit_cards": "Tarjetas de crédito", + "filter_by_brand": "Filtrar by brand", + "all_brands": "Todos Brands", + "search": "Buscar", + "clear_filters": "Limpiar filtros", + "card_holder": "Card Holder", + "brand": "Brand", + "expiry": "Expiry", + "add_new_card": "Agregar New Card", + "add_new_card_description": "Entrar your credit card details below", + "select_brand": "Select brand", + "expiry_month": "Expiry Month", + "expiry_year": "Expiry Year", + "cancel": "Cancelar", + "save": "Guardar", + "edit_card": "Editar Card", + "edit_card_description": "Actualizar your credit card details", + "update": "Actualizar", + "card_deleted": "Card deleted", + "card_deleted_description": "The card has been deleted successfully", + "field_copied": "Field copiado to clipboard", + "copy_failed": "Error al copiar", + "failed_to_copy_field": "Error al copy field to clipboard", + "hide_card_number": "Ocultar card number", + "show_card_number": "Mostrar card number", + "copy_to_clipboard": "Copiar to clipboard", + "showing_results": "Mostraring {startIdx}-{endIdx} de {totalCantidad} resultados", + "page_of": "Página {currentPage} de {totalPages}", + "title_placeholder": "Personal Visa Card", + "card_holder_name_placeholder": "John Doe", + "card_number_placeholder": "4111 1111 1111 1111", + "notes": "Notas", + "notes_placeholder": "Agregaritional information about this card", + "tags_placeholder": "Agregar tags", + "add": "Agregar", + "adding": "Agregaring...", + "updating": "Updating...", + "deleting": "Eliminando...", + "confirm_delete": "Confirmar Deletion", + "delete_card_confirmation": "¿Está seguro you want to delete this card? Esta acción no se puede deshacer.", + "error_fetching_cards": "Error fetching cards", + "missing_workspace_project": "Missing workspace or project", + "title_required": "Título is required", + "card_holder_name_required": "Card holder name is required", + "card_number_required": "Card number is required", + "invalid_card_number": "Invalid card number", + "cvv_required": "CVV is required", + "invalid_cvv": "Invalid CVV", + "brand_required": "Brand is required", + "error_adding_card": "Error al agregar card", + "error_updating_card": "Error al actualizar card", + "other": "Other", + "filter_by_tag": "Filtrar por etiqueta", + "all_tags": "Todas las etiquetas", + "sort": "Ordenar", + "sort_options": "Opciones de ordenamiento", + "sort_by_name": "Ordenar por nombre", + "sort_by_created": "Ordenar por fecha de creación", + "sort_by_updated": "Ordenar por fecha de actualización", + "ascending": "Ascendente", + "descending": "Descendente", + "clear_sort": "Limpiar ordenamiento", + "search_across_all_fields": "Buscar", + "searches_all_fields": "Buscares title, card holder, card number, brand and tags", + "missing_auth_or_selection": "Missing authentication or selection", + "encryption_key_not_found": "Proyecto encryption key not found. Cannot save card.", + "project_not_found": "Proyecto no encontrado. Por favor seleccione un proyecto válido.", + "encryption_failed": "Error al encrypt card data. Por favor intente de nuevo.", + "decryption_failed": "Error al decrypt card data. This could be due to a missing or invalid encryption key.", + "manage_your_cards": "Manage your credit and debit card information securely", + "date_created": "Fecha de creación" + }, + "wifi": { + "wifi_networks": "Wi-Fi Networks", + "manage_your_wifi_networks": "Manage your Wi-Fi networks securely.", + "add_wifi": "Agregar Wi-Fi", + "add_new_wifi": "Agregar New Wi-Fi", + "add_new_wifi_description": "Entrar your Wi-Fi network details below", + "edit_wifi": "Editar Wi-Fi", + "edit_wifi_description": "Actualizar your Wi-Fi network details", + "search_wifi": "Buscar", + "filter_by_security": "Filtrar by security", + "all_security_types": "Todos Seguridad Tipos", + "none": "Ninguno", + "clear": "Limpiar", + "clear_filters": "Limpiar filtros", + "loading_wifi": "Cargando Wi-Fi networks...", + "no_wifi_found": "No Wi-Fi networks found", + "no_wifi_for_security_type": "No Wi-Fi networks found for {security_type}", + "no_wifi_match_search": "No Wi-Fi networks match your search '{search}'", + "name": "Nombre", + "loading_wifi_networks": "Cargando Wifi networks", + "no_wifi_networks_found": "No Wifi Network Found", + "add_wifi_network": "Agregar Wifi Network", + "no_wifi_for_tag": "No WiFi networks found with tag {tag}", + "adjust_search_filter": "Try adjusting your search or filter to find what you're looking for.", + "ssid": "SSID", + "security": "Seguridad", + "security_type": "Seguridad Tipo", + "select_security_type": "Select security type", + "notes": "Notas", + "enter_notes": "Ingresar notas", + "password": "Contraseña", + "enter_password": "Ingresar contraseña", + "enter_new_password": "Entrar new contraseña (leave blank to keep unchanged)", + "show_password": "Mostrar contraseña", + "hide_password": "Ocultar contraseña", + "copy_password": "Copiar contraseña", + "tags": "Etiquetas", + "add_a_tag": "Agregar una etiqueta", + "add": "Agregar", + "last_modified": "Última modificación", + "date_created": "Fecha de creación", + "actions": "Acciones", + "showing": "Mostrando", + "of": "de", + "rows_per_page": "Filas por página", + "edit": "Editar", + "copy_ssid": "Copiar SSID", + "delete": "Eliminar", + "confirm_delete_wifi": "Confirmar Eliminar WiFi", + "delete_wifi_confirmation": "¿Está seguro you want to delete this WiFi network? Esta acción no se puede deshacer.", + "wifi_deleted": "WiFi Eliminard", + "wifi_deleted_description": "The WiFi network has been successfully deleted.", + "failed_to_delete_wifi": "Error al delete WiFi network", + "error_deleting_wifi": "Error al eliminar WiFi", + "error_fetching_wifi_networks": "Error fetching WiFi networks", + "failed_to_fetch_wifi_networks": "Error al fetch WiFi networks", + "no_project_selected": "No hay proyecto seleccionado. Please select a project to proceed.", + "copied": "Copiado", + "field_copied": "Field copiado to clipboard", + "copy_failed": "Error al copiar", + "failed_to_copy_field": "Error al copy field to clipboard", + "show_qr_code": "Mostrar QR Code", + "wifi_qr_code": "WiFi QR Code", + "wifi_qr_code_description": "Scan this QR code to connect to the WiFi network.", + "qr_code_placeholder": "QR Code", + "wifi_credentials": "WiFi Credentials", + "close": "Cerrar", + "please_fill_all_required_fields": "Por favor complete todos los campos requeridos", + "wifi_added_successfully": "WiFi added successfully", + "wifi_added_description": "The WiFi network has been added.", + "wifi_already_exists": "A WiFi network with this SSID already exists", + "failed_to_add_wifi": "Error al add WiFi network", + "enter_ssid": "Entrar SSID", + "update_wifi": "Actualizar WiFi", + "wifi_updated_successfully": "WiFi updated successfully", + "wifi_updated_description": "The WiFi network has been updated.", + "wifi_not_found": "WiFi network not found", + "failed_to_update_wifi": "Error al update WiFi network", + "invalid_input_data": "Datos de entrada inválidos", + "adding": "Agregaring...", + "updating": "Updating...", + "deleting": "Eliminando...", + "cancel": "Cancelar", + "encryption_key_not_found": "Proyecto encryption key not found. Cannot save WiFi network.", + "project_not_found": "Proyecto no encontrado. Por favor seleccione un proyecto válido.", + "encryption_failed": "Error al encrypt WiFi data. Por favor intente de nuevo.", + "decryption_failed": "Error al decrypt WiFi data. This could be due to a missing or invalid encryption key.", + "sort": "Ordenar", + "sort_options": "Opciones de ordenamiento", + "sort_by_name": "Ordenar por nombre", + "sort_by_created": "Ordenar por fecha de creación", + "sort_by_updated": "Ordenar por fecha de actualización", + "ascending": "Ascendente", + "descending": "Descendente", + "clear_sort": "Limpiar ordenamiento", + "filter_by_tag": "Filtrar por etiqueta", + "all_tags": "Todas las etiquetas", + "scan_with_phone": "Scan this QR code with your número de teléfono to connect to the network" + }, + "key_shortcuts": { + "title": "Atajos de teclado" + }, + "notes": { + "add_new_note": "Agregar New Note", + "note_title": "Note Título", + "note_content": "Note Content", + "tags": "Etiquetas", + "update_note": "Actualizar Note", + "enter_note_title": "Entrar note title", + "add_a_tag": "Agregar una etiqueta", + "add_note": "Agregar Note", + "cancel": "Cancelar" + }, + "emails": { + "email_accounts": "Cuentas de correo", + "add_email": "Agregar Correo electrónico", + "search_email_accounts": "Buscar", + "per_page": "por página", + "clear_filters": "Limpiar filtros", + "title": "Título", + "email_address": "Dirección de correo electrónico", + "servers": "Servers", + "username": "Nombre de usuario", + "password": "Contraseña", + "last_modified": "Última modificación", + "tags": "Etiquetas", + "loading_emails": "Cargando Correo electrónicos", + "actions": "Acciones", + "loading": "Cargando...", + "no_results_found": "No se encontraron resultados", + "no_email_accounts": "No dirección de correo electrónico accounts", + "copy": "Copiar", + "copy_to_clipboard": "Copiar to clipboard", + "show_password": "Mostrar contraseña", + "hide_password": "Ocultar contraseña", + "open_menu": "Abrir menú", + "edit": "Editar", + "delete": "Eliminar", + "deleted": "Eliminado", + "showing_results": "Mostraring {start} to {end} de {total} resultados", + "confirm_delete": "Confirmar eliminación", + "delete_email_confirmation": "¿Está seguro you want to delete this dirección de correo electrónico account? Esta acción no se puede deshacer.", + "cancel": "Cancelar", + "deleting": "Eliminando...", + "copied": "Copiado", + "field_copied": "Value copiado to clipboard", + "copy_failed": "Error al copiar", + "failed_to_copy_value": "Error al copy value to clipboard", + "encryption_error": "Error de cifrado", + "could_not_encrypt_data": "No se pudieron cifrar los datos. Por favor intente de nuevo.", + "success": "Éxito", + "email_added_successfully": "Correo electrónico account added successfully", + "error_adding_email": "Error al agregar dirección de correo electrónico account. Por favor intente de nuevo.", + "email_deleted_successfully": "Correo electrónico credentials deleted successfully", + "failed_to_delete_email": "Error al delete dirección de correo electrónico credentials", + "error_fetching_emails": "Error al fetch dirección de correo electrónicos", + "error": "Error", + "sort": "Ordenar", + "sort_options": "Opciones de ordenamiento", + "sort_by_name": "Ordenar por nombre", + "sort_by_created": "Ordenar por fecha de creación", + "sort_by_updated": "Ordenar por fecha de actualización", + "ascending": "Ascendente", + "descending": "Descendente", + "clear_sort": "Limpiar ordenamiento", + "add_email_description": "Agregar a new dirección de correo electrónico account to your secure vault", + "title_placeholder": "Trabajo Correo electrónico", + "email_address_placeholder": "your.dirección de correo electrónico@example.com", + "imap_server": "IMAP Server", + "imap_server_placeholder": "imap.example.com", + "smtp_server": "SMTP Server", + "smtp_server_placeholder": "smtp.example.com", + "username_placeholder": "Opcional, defaults to dirección de correo electrónico address", + "password_placeholder": "Ingresar contraseña", + "notes": "Notas (opcional)", + "notes_placeholder": "Agregaritional information about this dirección de correo electrónico account", + "add_tag": "Agregar etiqueta", + "remove": "Eliminar", + "adding": "Agregaring...", + "add": "Agregar", + "please_fill_all_required_fields": "Por favor complete todos los campos requeridos", + "email_title_already_exists": "Correo electrónico with this title already exists", + "invalid_email_format": "Invalid dirección de correo electrónico format", + "missing_required_data": "Faltan datos de proyecto requeridos", + "edit_email": "Editar Correo electrónico Account", + "edit_email_description": "Actualizar your dirección de correo electrónico account details", + "save_changes": "Guardar cambios", + "email_updated_successfully": "Correo electrónico account updated successfully", + "error_updating_email": "Error al actualizar dirección de correo electrónico account. Por favor intente de nuevo.", + "manage_your_email_accounts": "Manage your dirección de correo electrónico accounts and server settings securely", + "date_created": "Fecha de creación", + "filter_by_tag": "Filtrar por etiqueta", + "all_tags": "Todas las etiquetas" + }, + "ssh_keys": { + "ssh_keys": "Claves SSH", + "add_ssh_key": "Agregar SSH Key", + "search_ssh_keys": "Buscar", + "per_page": "por página", + "clear_filters": "Limpiar filtros", + "name": "Nombre", + "ssh_key": "SSH Key", + "tags": "Etiquetas", + "loading_ssh_keys": "Cargando Claves SSH", + "actions": "Acciones", + "loading": "Cargando...", + "no_results_found": "No se encontraron resultados", + "no_ssh_keys": "No SSH keys", + "hide_ssh_key": "Ocultar SSH key", + "show_ssh_key": "Mostrar full SSH key", + "copied": "Copiado", + "ssh_key_copied": "SSH key copiado to clipboard", + "copy_failed": "Error al copiar", + "failed_to_copy_ssh_key": "Error al copy SSH key to clipboard", + "copy_to_clipboard": "Copiar to clipboard", + "open_menu": "Abrir menú", + "edit": "Editar", + "delete": "Eliminar", + "fingerprint": "Fingerprint", + "last_modified": "Última modificación", + "deleted": "Eliminado", + "showing_results": "Mostraring {start} to {end} de {total} resultados", + "confirm_delete": "Confirmar eliminación", + "delete_ssh_key_confirmation": "¿Está seguro you want to delete this SSH key? Esta acción no se puede deshacer.", + "cancel": "Cancelar", + "deleting": "Eliminando...", + "please_fill_all_required_fields": "Por favor complete todos los campos requeridos", + "ssh_key_name_already_exists": "SSH key with this name already exists", + "missing_required_data": "Faltan datos de proyecto requeridos", + "encryption_error": "Error de cifrado", + "could_not_encrypt_data": "No se pudieron cifrar los datos. Por favor intente de nuevo.", + "success": "Éxito", + "ssh_key_added_successfully": "SSH key added successfully", + "error_adding_ssh_key": "Error al agregar SSH key. Por favor intente de nuevo.", + "ssh_key_updated_successfully": "SSH key updated successfully", + "error_updating_ssh_key": "Error al actualizar SSH key. Por favor intente de nuevo.", + "ssh_key_deleted_successfully": "SSH key deleted successfully", + "error_deleting_ssh_key": "Error al eliminar SSH key", + "error_fetching_ssh_keys": "Error al fetch SSH keys", + "add_a_tag": "Agregar una etiqueta", + "sort": "Ordenar", + "sort_options": "Opciones de ordenamiento", + "sort_by_name": "Ordenar por nombre", + "sort_by_created": "Ordenar por fecha de creación", + "sort_by_updated": "Ordenar por fecha de actualización", + "ascending": "Ascendente", + "descending": "Descendente", + "clear_sort": "Limpiar ordenamiento", + "error": "Error", + "add_ssh_key_description": "Agregar a new SSH key to your secure vault", + "name_placeholder": "GitHub SSH Key", + "ssh_key_placeholder": "Pegar your SSH prinúmero de IVAe key here", + "notes": "Notas (opcional)", + "notes_placeholder": "Agregaritional information about this SSH key", + "add_tag": "Agregar etiqueta", + "remove": "Eliminar", + "adding": "Agregaring...", + "add": "Agregar", + "edit_ssh_key": "Editar SSH Key", + "edit_ssh_key_description": "Actualizar your SSH key details", + "save_changes": "Guardar cambios", + "search_licenses": "Buscar", + "manage_your_ssh_keys": "Manage your SSH keys securely", + "filter_by_tag": "Filtrar por etiqueta", + "all_tags": "Todas las etiquetas", + "date_created": "Fecha de creación" + }, + "files": { + "files": "Archivos", + "file_manager": "Administrador de archivos", + "add_file": "Subir archivo", + "create_folder": "Crear carpeta", + "search_files": "Buscar archivos", + "per_page": "por página", + "clear_filters": "Limpiar filtros", + "name": "Nombre", + "size": "Tamaño", + "type": "Tipo", + "modified": "Modificado", + "folder": "Carpeta", + "file": "Archivo", + "loading_files": "Cargando archivos...", + "actions": "Acciones", + "loading": "Cargando...", + "no_results_found": "No se encontraron resultados", + "no_files": "Sin archivos", + "no_folders": "Sin carpetas", + "download": "Descargar", + "rename": "Renombrar", + "move": "Mover", + "delete": "Eliminar", + "copied": "Copiado", + "file_copied": "Archivo copiado al portapapeles", + "copy_failed": "Error al copiar", + "failed_to_copy_file": "Error al copy file to clipboard", + "copy_to_clipboard": "Copiar to clipboard", + "open_menu": "Abrir menú", + "edit": "Editar", + "deleted": "Eliminado", + "showing_results": "Mostraring {start} to {end} de {total} resultados", + "confirm_delete": "Confirmar eliminación", + "delete_file_confirmation": "¿Está seguro de que desea eliminar este archivo? Esta acción no se puede deshacer.", + "delete_folder_confirmation": "¿Está seguro de que desea eliminar esta carpeta? Esta acción no se puede deshacer.", + "cancel": "Cancelar", + "deleting": "Eliminando...", + "please_fill_all_required_fields": "Por favor complete todos los campos requeridos", + "file_name_already_exists": "Ya existe un archivo con este nombre en esta carpeta", + "folder_name_already_exists": "Ya existe una carpeta con este nombre en esta ubicación", + "missing_required_data": "Faltan datos de proyecto requeridos", + "encryption_error": "Error de cifrado", + "could_not_encrypt_data": "No se pudieron cifrar los datos. Por favor intente de nuevo.", + "success": "Éxito", + "file_uploaded_successfully": "Archivo subido exitosamente", + "error_uploading_file": "Error al subir archivo. Por favor intente de nuevo.", + "file_updated_successfully": "Archivo actualizado exitosamente", + "error_updating_file": "Error al actualizar archivo. Por favor intente de nuevo.", + "file_deleted_successfully": "Archivo eliminado exitosamente", + "error_deleting_file": "Error al eliminar archivo", + "error_fetching_files": "Error al obtener archivos", + "folder_created_successfully": "Carpeta creada exitosamente", + "error_creating_folder": "Error al crear carpeta. Por favor intente de nuevo.", + "folder_updated_successfully": "Carpeta actualizada exitosamente", + "error_updating_folder": "Error al actualizar carpeta. Por favor intente de nuevo.", + "folder_deleted_successfully": "Carpeta eliminada exitosamente", + "error_deleting_folder": "Error al eliminar carpeta", + "error_fetching_folders": "Error al obtener carpetas", + "file_too_large": "El tamaño del archivo excede el límite de 50MB", + "invalid_file_type": "Tipo de archivo inválido", + "upload_failed": "Error al subir", + "download_failed": "Error al descargar", + "sort": "Ordenar", + "sort_options": "Opciones de ordenamiento", + "sort_by_name": "Ordenar por nombre", + "sort_by_size": "Ordenar por tamaño", + "sort_by_modified": "Ordenar por fecha de modificación", + "sort_by_type": "Ordenar por tipo", + "ascending": "Ascendente", + "descending": "Descendente", + "clear_sort": "Limpiar ordenamiento", + "error": "Error", + "upload_file_description": "Subir un archivo a su bóveda segura", + "create_folder_description": "Crear una nueva carpeta para organizar sus archivos", + "name_placeholder": "Ingresar nombre de archivo o carpeta", + "folder_name_placeholder": "Ingresar nombre de carpeta", + "notes": "Notas (opcional)", + "notes_placeholder": "Información adicional sobre este archivo", + "add_tag": "Agregar etiqueta", + "remove": "Eliminar", + "uploading": "Subiendo...", + "creating": "Creando...", + "add": "Agregar", + "upload": "Subir", + "create": "Crear", + "edit_file": "Editar archivo", + "edit_file_description": "Actualizar los detalles de su archivo", + "edit_folder": "Editar carpeta", + "edit_folder_description": "Actualizar los detalles de su carpeta", + "save_changes": "Guardar cambios", + "manage_your_files": "Gestionar sus archivos de forma segura", + "filter_by_type": "Filtrar por tipo", + "all_types": "Todos los tipos", + "date_created": "Fecha de creación", + "file_size": "Tamaño del archivo", + "folder_path": "Ruta de la carpeta", + "select_folder": "Seleccionar carpeta", + "move_to_folder": "Mover a carpeta", + "drag_and_drop_files": "Arrastre y suelte archivos aquí, o haga clic para seleccionar", + "select_files": "Seleccionar archivos", + "upload_progress": "Progreso de subida", + "upload_complete": "Subida completa", + "bytes": "bytes", + "kb": "KB", + "mb": "MB", + "gb": "GB", + "tb": "TB" + }, + "project_colors": { + "red": "Rojo", + "orange": "Naranja", + "amber": "Ámbar", + "yellow": "Amarillo", + "lime": "Lima", + "green": "Verde", + "emerald": "Esmeralda", + "teal": "Verde azulado", + "cyan": "Cian", + "sky": "Cielo", + "blue": "Azul", + "indigo": "Índigo", + "violet": "Violeta", + "purple": "Púrpura", + "fuchsia": "Fucsia", + "pink": "Rosa", + "rose": "Rosa" + } +} \ No newline at end of file diff --git a/packages/frontend-web/messages/fr/common.json b/packages/frontend-web/messages/fr/common.json index dc6070c7..4fe3db2c 100644 --- a/packages/frontend-web/messages/fr/common.json +++ b/packages/frontend-web/messages/fr/common.json @@ -402,7 +402,9 @@ "no_accounts_create": "Aucun compte trouvé. Créez-en un pour commencer.", "adjust_search_filter": "Essayez d'ajuster vos critères de recherche ou de filtre", "no_recent_activity": "Aucune activité récente", - "no_projects_available": "Aucun projet disponible" + "no_projects_available": "Aucun projet disponible", + "no_workspace_selected": "Aucun espace de travail sélectionné" + }, "activity": { "password_updated": "Mot de passe mis à jour", diff --git a/packages/frontend-web/middleware.ts b/packages/frontend-web/middleware.ts index a51446cc..30624c1a 100644 --- a/packages/frontend-web/middleware.ts +++ b/packages/frontend-web/middleware.ts @@ -3,7 +3,7 @@ import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; // Available locales -export const locales = ['en', 'de', 'fr']; +export const locales = ['en', 'de', 'fr', 'es']; export const defaultLocale = 'en'; // Protected routes that require authentication From 629fc6059e7e7d2cb2ae5d5e06f8873f232cec46 Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Wed, 24 Sep 2025 12:01:08 +0530 Subject: [PATCH 05/32] Afrikans --- .../components/dashboard-layout.tsx | 2 +- packages/frontend-web/messages/af/common.json | 1425 +++++++++++++++++ packages/frontend-web/middleware.ts | 2 +- 3 files changed, 1427 insertions(+), 2 deletions(-) create mode 100644 packages/frontend-web/messages/af/common.json diff --git a/packages/frontend-web/components/dashboard-layout.tsx b/packages/frontend-web/components/dashboard-layout.tsx index 745a765a..6a071dfb 100644 --- a/packages/frontend-web/components/dashboard-layout.tsx +++ b/packages/frontend-web/components/dashboard-layout.tsx @@ -260,7 +260,7 @@ export function DashboardLayout({ children, locale = "en" }: DashboardLayoutProp const [currentLocale, setCurrentLocale] = useState(locale); const languageLabels: Record = { - af: "Afrikaans", + af: "Afrikaans (Afrikaans)", ar: "Arabic (عربى)", ca: "Catalan (Català)", cs: "Czech (Čeština)", diff --git a/packages/frontend-web/messages/af/common.json b/packages/frontend-web/messages/af/common.json new file mode 100644 index 00000000..e74ba767 --- /dev/null +++ b/packages/frontend-web/messages/af/common.json @@ -0,0 +1,1425 @@ +{ + "app": { + "title": "Zecrypt", + "description": "Beveilig jou data met Zecrypt" + }, + "common": { + "loading": "Laai...", + "saving": "Stoor...", + "deleting": "Skrap...", + "error": "Fout", + "success": "Sukses", + "cancel": "Kanselleer", + "add": "Voeg by", + "save": "Stoor", + "submitting": "Dien in...", + "try_again": "Probeer weer", + "sort": "Sorteer", + "sort_options": "Sorteer Opsies", + "sort_by_name": "Sorteer volgens Naam", + "sort_by_created": "Sorteer volgens Geskep Datum", + "sort_by_updated": "Sorteer volgens Opgedateer Datum", + "ascending": "Toenemend", + "descending": "Afnemend", + "clear_sort": "Maak Sorteer Skoon", + "add_item_description": "Voer die besonderhede hieronder in", + "edit_item_description": "Dateer die besonderhede op" + }, + "pagination": { + "showing_x_of_y": "Wys {start} - {end} van {total} items", + "page_of": "Bladsy {currentPage} van {totalPages}", + "next": "Volgende", + "previous": "Vorige" + }, + "env": { + "env_variables": "Omgewing Veranderlikes", + "manage_your_env": "Bestuur jou omgewing veranderlikes veilig", + "add_env": "Voeg Omgewing Veranderlikes by", + "loading_env": "Laai omgewing veranderlikes...", + "name": "Naam", + "env_value": "Waarde", + "tags": "Etikette", + "created_at": "Geskep op", + "actions": "Aksies", + "no_env_yet": "Geen omgewing veranderlikes bygevoeg nie", + "no_matching_env": "Geen ooreenstemmende omgewing veranderlikes gevind nie", + "search_across_all_fields": "Soek oor alle velde...", + "show_env": "Wys omgewing veranderlikes", + "hide_env": "Versteek omgewing veranderlikes", + "copy_env": "Kopieer omgewing veranderlikes", + "env_copied": "Omgewing veranderlikes na knipbord gekopieer", + "failed_to_copy_env": "Kon nie omgewing veranderlikes na knipbord kopieer nie", + "delete_env_confirmation": "Is jy seker jy wil hierdie omgewing veranderlikes skrap? Hierdie aksie kan nie ongedaan gemaak word nie.", + "showing_x_of_y": "Wys {start} - {end} van {total} items", + "failed_to_decrypt": "Kon nie omgewing veranderlikes dekripteer nie", + "add_new_env": "Voeg Nuwe Omgewing Veranderlikes by", + "add_new_env_description": "Voer jou omgewing veranderlikes besonderhede hieronder in", + "edit_env": "Redigeer Omgewing Veranderlikes", + "edit_env_description": "Dateer jou omgewing veranderlikes op", + "env_name": "Omgewing Naam", + "enter_env_name": "Voer omgewing naam in", + "enter_env_value": "Voer omgewing waarde in", + "enter_env_variables": "KEY=waarde\nANOTHER_KEY=ander_waarde", + "env_format_hint": "Voer jou omgewing veranderlikes in KEY=waarde formaat in, een per lyn", + "notes": "Notas", + "enter_notes": "Voer notas in (opsioneel)", + "add_a_tag": "Voeg 'n etiket by", + "add_tag": "Voeg etiket by", + "press_enter_to_add": "Druk Enter om 'n etiket by te voeg", + "cancel": "Kanselleer", + "add": "Voeg by", + "update": "Dateer op", + "env_added_successfully": "Omgewing veranderlikes suksesvol bygevoeg", + "env_updated_successfully": "Omgewing veranderlikes suksesvol opgedateer", + "env_deleted_successfully": "Omgewing veranderlikes suksesvol geskrap", + "error_adding_env": "Kon nie omgewing veranderlikes byvoeg nie", + "error_updating_env": "Kon nie omgewing veranderlikes opdateer nie", + "error_deleting_env": "Kon nie omgewing veranderlikes skrap nie", + "error_fetching_env": "Kon nie omgewing veranderlikes haal nie", + "please_fill_all_required_fields": "Vul asseblief alle verpligte velde in", + "no_project_selected": "Geen projek gekies nie. Kies asseblief 'n projek.", + "clear_filters": "Maak Filters Skoon", + "decrypting": "Dekripteer omgewing veranderlikes...", + "env_updated_description": "Die omgewing veranderlikes is opgedateer.", + "env_added_description": "Die omgewing veranderlikes is bygevoeg.", + "env_already_exists": "Omgewing veranderlikes met hierdie naam bestaan reeds.", + "date_created": "Datum Geskep", + "sort": "Sorteer", + "sort_options": "Sorteer Opsies", + "sort_by_name": "Sorteer volgens Naam", + "sort_by_created": "Sorteer volgens Geskep Datum", + "sort_by_updated": "Sorteer volgens Opgedateer Datum", + "view_env_code": "Bekyk in kode redigeerder", + "click_to_add_env": "Klik om omgewing veranderlikes by te voeg", + "click_to_edit_env": "Klik om omgewing veranderlikes te redigeer", + "env_hidden": "••••••••", + "copied": "Gekopieer" + }, + "licenses": { + "software_licenses": "Sagteware Lisensies", + "add_license": "Voeg Lisensie by", + "filter_by_tag": "Filter volgens etiket", + "all_tags": "Alle Etikette", + "search": "Soek", + "loading_licenses": "Laai lisensies...", + "no_licenses_found": "Geen lisensies gevind nie", + "clear_filters": "Maak filters skoon", + "name": "Naam", + "software": "Sagteware", + "license_key": "Lisensie Sleutel", + "expiry_date": "Verval Datum", + "tags": "Etikette", + "actions": "Aksies", + "add_new_license": "Voeg Nuwe Lisensie by", + "add_new_license_description": "Voer jou sagteware lisensie besonderhede hieronder in", + "cancel": "Kanselleer", + "save": "Stoor", + "edit_license": "Redigeer Lisensie", + "edit_license_description": "Dateer jou sagteware lisensie besonderhede op", + "update": "Dateer op", + "delete": "Skrap", + "license_deleted": "Lisensie geskrap", + "license_deleted_description": "Die lisensie is suksesvol geskrap", + "copied": "Gekopieer", + "field_copied": "Veld na knipbord gekopieer", + "copy_failed": "Kopiëring het misluk", + "failed_to_copy_field": "Kon nie veld na knipbord kopieer nie", + "hide_license_key": "Versteek lisensie sleutel", + "show_license_key": "Wys lisensie sleutel", + "showing_results": "Wys {startIdx}-{endIdx} van {totalCount} resultate", + "page_of": "Bladsy {currentPage} van {totalPages}", + "expires": "Verval", + "expired": "Verval", + "expiring_soon": "Verval binnekort", + "edit": "Redigeer", + "enter_license_name": "Voer lisensie naam in", + "enter_software_name": "Voer sagteware naam in", + "enter_license_key": "Voer lisensie sleutel in", + "enter_notes": "Voer notas in", + "confirm_deletion": "Bevestig Skrapping", + "confirm_delete_license_description": "Is jy seker jy wil hierdie lisensie skrap? Hierdie aksie kan nie ongedaan gemaak word nie.", + "license_added_successfully": "Lisensie suksesvol bygevoeg", + "failed_to_add_license": "Kon nie lisensie byvoeg nie", + "invalid_input": "Ongeldige invoer", + "validation_error": "Validasie fout", + "network_error": "Netwerk fout", + "error_adding_license": "Fout by byvoeging van lisensie", + "license_updated_successfully": "Lisensie suksesvol opgedateer", + "failed_to_update_license": "Kon nie lisensie opdateer nie", + "license_not_found": "Lisensie nie gevind nie", + "error_updating_license": "Fout by opdatering van lisensie", + "missing_auth_or_selection": "Ontbrekende outentikasie of keuse", + "license_deleted_successfully": "Lisensie suksesvol geskrap", + "error_deleting_license": "Fout by skrapping van lisensie", + "error_fetching_licenses": "Fout by haal van lisensies", + "adding": "Voeg by...", + "updating": "Dateer op...", + "deleting": "Skrap...", + "notes": "Notas", + "software_name": "Sagteware Naam", + "rows_per_page": "Rye per bladsy", + "sort": "Sorteer", + "sort_options": "Sorteer Opsies", + "sort_by_name": "Sorteer volgens Naam", + "sort_by_created": "Sorteer volgens Geskep Datum", + "sort_by_updated": "Sorteer volgens Opgedateer Datum", + "ascending": "Toenemend", + "descending": "Afnemend", + "clear_sort": "Maak Sorteer Skoon", + "search_across_all_fields": "Soek", + "update_api_key": "Dateer API Sleutel op", + "manage_your_software_licenses": "Bestuur jou sagteware lisensies veilig", + "date_created": "Datum Geskep", + "add_a_tag": "Voeg 'n etiket by" + }, + "navigation": { + "home": "Tuis", + "dashboard": "Dashboard", + "settings": "Instellings" + }, + "auth": { + "login_to_continue": "Teken in om voort te gaan", + "sign_in_with_sso": "Teken in met jou organisasie rekening", + "login": "Teken in", + "signup": "Registreer", + "cancel": "Kanselleer", + "logout": "Teken uit", + "sign_up": "Registreer", + "try_again": "Probeer weer", + "dont_have_account": "Het jy nie 'n rekening nie?", + "email": "E-pos", + "password": "Wagwoord", + "agreement": "Wanneer jy inteken, stem jy in tot ons bepalings en voorwaardes", + "terms": "Bepalings", + "privacy_policy": "Privaatheid Beleid", + "terms_and_conditions": "Bepalings en Voorwaardes", + "service_status": "Diens Status", + "generate_password": "Genereer Wagwoord", + "verification_code": "Verifikasie Kode", + "2fa_verify": "2FA Verifikasie", + "2fa_enter_code": "Voer die 6-syfer kode van jou outentikator app in", + "2fa_placeholder": "bv. 123456", + "verify": "Verifieer", + "verifying_authentication": "Verifieer outentikasie...", + "2fa_verification_error": "Ongeldige verifikasie kode. Probeer asseblief weer.", + "2fa_verification_success": "2FA verifikasie suksesvol", + "verifying": "Verifieer 2FA...", + "authentication_error": "Outentikasie fout", + "2fa_verified": "2FA Geverifieer", + "redirecting_to_dashboard": "Herlei jou na dashboard...", + "setup_encryption_keys": "Stel Enkripsie Sleutels op", + "encryption_setup_description": "Skep 'n hoof wagwoord om jou data te enkripteer. Hierdie wagwoord word gebruik om jou private sleutel te beskerm.", + "encryption_password_warning": "Onthou hierdie wagwoord sorgvuldig! As jy dit vergeet, sal jy toegang tot al jou geënkripteerde data verloor.", + "encryption_password": "Hoof Wagwoord", + "enter_strong_password": "Voer 'n sterk wagwoord in", + "confirm_encryption_password": "Bevestig Hoof Wagwoord", + "confirm_password": "Bevestig jou wagwoord", + "encryption_password_requirements": "Wagwoord moet ten minste 8 karakters wees en moet letters, syfers en spesiale karakters insluit vir maksimum sekuriteit.", + "generating_keys": "Genereer sleutels...", + "setup_encryption": "Stel Enkripsie op", + "password_too_short": "Wagwoord moet ten minste 8 karakters lank wees", + "passwords_dont_match": "Wagwoorde stem nie ooreen nie", + "encryption_setup_success": "Enkripsie Opset Voltooi", + "encryption_keys_created": "Jou enkripsie sleutels is suksesvol geskep", + "encryption_setup_failed": "Kon nie enkripsie sleutels opstel nie", + "checking_encryption_keys": "Kontroleer enkripsie sleutels...", + "encryption_key_check_failed": "Enkripsie Sleutel Kontrole het misluk", + "encryption_key_check_failed_desc": "Ons kon nie jou enkripsie sleutels verifieer nie. Dit kan wees as gevolg van 'n netwerk probleem of ontbrekende sleutels.", + "encryption_security_error": "Enkripsie sekuriteit fout. Probeer asseblief weer in te teken.", + "login_successful": "Intekening Suksesvol", + "2fa_setup_required": "2FA Opset Vereis", + "scan_qr_with_authenticator": "Skandeer die QR kode met jou outentikator app", + "unknown_error": "Onbekende fout het voorgekom", + "auth_network_error": "Netwerk fout tydens outentikasie", + "auth_process_failed": "Outentikasie proses het misluk", + "2fa_invalid_code": "Ongeldige verifikasie kode", + "2fa_verification_failed": "Verifikasie het misluk: {message}", + "copied_to_clipboard": "Na knipbord gekopieer", + "paste_in_authenticator": "Plak hierdie kode in jou outentikator app", + "2fa_setup": "Twee-Faktor Outentikasie Opset", + "2fa_scan": "Skandeer die QR kode met jou outentikator app", + "2fa_scan_difficulty": "Het jy probleme met skandering? Jy kan die opset sleutel hieronder kopieer:", + "copy_setup_key": "Kopieer Opset Sleutel", + "unlock_encryption": "Ontsluit Enkripsie", + "enter_encryption_password": "Voer jou hoof wagwoord in", + "enter_encryption_password_desc": "Voer jou hoof wagwoord in om jou private sleutel te dekripteer.", + "unlocking": "Ontsluit...", + "unlock": "Ontsluit", + "encryption_unlocked": "Enkripsie Ontsluit", + "encryption_key_decrypted": "Jou enkripsie sleutel is suksesvol gedekripteer", + "incorrect_encryption_password": "Verkeerde hoof wagwoord. Probeer asseblief weer.", + "multi_device_login": "Teken in vanaf Nuwe Toestel", + "multi_device_desc": "Jy teken in vanaf 'n nuwe toestel. Voer asseblief jou hoof wagwoord in om toegang tot jou geënkripteerde data te kry.", + "authenticating": "Outentikeer...", + "download_password_title": "Jou Zecrypt Enkripsie Wagwoord:", + "download_password_important": "BELANGRIK: Hou hierdie wagwoord veilig. Jy sal dit nodig hê om toegang tot jou geënkripteerde data te kry.", + "download_filename": "zecrypt-encryption-password.txt" + }, + "features": { + "trial_title": "Jou Beveiligde Stoor Bestuurder", + "unlimited_devices": "Onbeperkte toestelle — heeltemal gratis vir altyd", + "shared_vaults": "Deel kredensiale moeiteloos met spanlede - (Binnekort)", + "advanced_security": "Militêre graad enkripsie (AES-GCM) vir rotsvaste sekuriteit", + "security_alerts": "Onmiddellike oortreding waarskuwings sodat jy altyd 'n stap voor is - (Binnekort)", + "multi_platform": "Beskikbaar op Mac, iOS, Windows, Android, Chrome OS, en Linux - (binnekort)", + "organization": "Organisasie", + "organization_desc": "Organiseer kredensiale in projekte vir beter bestuur en vinnige toegang.", + "secure_storage": "Beveiligde Stoor", + "secure_storage_desc": "Stoor al jou wagwoorde en sensitiewe data met militêre graad enkripsie.", + "secure_password_manager": "End-to-end geënkripteerde wolk stoor" + }, + "actions": { + "add": "Voeg by", + "edit": "Redigeer", + "delete": "Skrap", + "cancel": "Kanselleer", + "close": "Sluit", + "save": "Stoor", + "saving": "Stoor...", + "deleting": "Skrap...", + "submit": "Dien in", + "ok": "OK", + "yes": "Ja", + "no": "Nee", + "confirm": "Bevestig", + "confirm_delete": "Bevestig Skrap", + "continue": "Gaan voort", + "back": "Terug", + "next": "Volgende", + "search": "Soek", + "filter": "Filter", + "clear": "Maak skoon", + "reset": "Herstel", + "refresh": "Verfris", + "upload": "Laai op", + "download": "Aflaai", + "copy": "Kopieer", + "paste": "Plak", + "cut": "Knip", + "undo": "Ontdoen", + "redo": "Oordoen", + "select_all": "Kies Alles", + "select_none": "Kies Geen", + "create": "Skep", + "update": "Dateer op", + "remove": "Verwyder", + "error": "Fout", + "view": "Bekyk" + }, + "dashboard": { + "project_limit_reached": "Projek limiet bereik", + "alpha_one_project_limit": "Ons hardloop tans op alfa, slegs een projek word per werkruimte toegelaat.", + "select_project": "Kies Projekte", + "manage_projects": "Bestuur projekte", + "select_or_create_project": "Kies of skep 'n projek", + "welcome_back": "Welkom terug na jou veilige werkruimte", + "total_passwords": "Totale Wagwoorde", + "total_accounts": "Totale Rekeninge", + "total_folders": "Totale Vouers", + "recently_added_accounts": "Onlangs Bygevoegde Rekeninge", + "from_last_month": "van laaste maand", + "create_project_details": "Skep Projek Besonderhede", + "recent_activity": "Onlangse Aktiwiteit", + "keyboard_shortcuts": "Sleutelbord kortpaaie", + "generate_password": "Genereer Wagwoord", + "favourites": "Gunstelinge", + "no_description": "Geen Beskrywing", + "notes": "Notas", + "confirm_delete_project": "Bevestig Skrap Projek", + "create_project": "Skep Projek", + "set_as_default_project": "Stel as verstek projek", + "project_name": "Projek Naam", + "describe_project_purpose": "Beskryf Projek Doel", + "free_plan_limit": "Gratis Plan Limiet", + "save_changes": "Stoor Veranderinge", + "no_project_availbale": "Geen Projek Beskikbaar", + "cancel": "Kanselleer", + "delete": "Skrap", + "add_favorite_tag": "Voeg Gunsteling Etiket by", + "remove_tag": "Verwyder {tag}", + "dashboard": "Dashboard", + "show_more": "Wys Meer", + "show_less": "Wys Minder", + "overview": "Oorsig", + "project": "Projek", + "notifications": "Kennisgewings", + "settings": "Instellings", + "logout": "Teken uit", + "current_project": "Huidige Projek", + "default": "Verstek", + "no_project_selected": "Geen projek gekies nie", + "quick_stats": "Vinnige Statistieke", + "workspaces": "Werkruimtes", + "projects": "Projekte", + "available_features": "Beskikbare Funksies", + "team_members": "Span Lede", + "total_projects": "Totale Projekte", + "total_workspaces": "Totale Werkruimtes", + "total_team_members": "Totale Span Lede", + "total_records": "Totale Rekords", + "my_account": "My Rekening", + "change_language": "Verander taal", + "language": "Taal", + "toggle_theme": "Wissel tema", + "wallet_passphrases": "Beursie Wagtale", + "wi_fi": "Wi-Fi", + "env": "Omgewing Veranderlikes", + "env_variables": "Omgewing Veranderlikes", + "search": "Soek", + "create_new_project": "Skep Nuwe Projek", + "add_new_project_description": "Voeg 'n nuwe projek by om jou veilige data te organiseer", + "description": "Beskrywing", + "optional": "Opsioneel", + "project_color": "Projek Kleur", + "enabled_modules": "Geaktiveerde Modules", + "edit_project": "Redigeer Projek", + "update_project_details": "Dateer die besonderhede vir jou projek op", + "my_project": "My Projek", + "creating": "Skep...", + "delete_project": "Skrap Projek", + "identity_coming_soon": "Identiteit module is onder ontwikkeling.", + "cards_coming_soon": "Kaarte module is onder ontwikkeling.", + "software_licenses_coming_soon": "Sagteware Lisensies module is onder ontwikkeling.", + "wifi_coming_soon": "Wi-Fi module is onder ontwikkeling.", + "profile": "Profiel", + "search_modules": "Soek modules...", + "no_modules_found": "Geen modules gevind nie", + "general": "Algemeen", + "security_access": "Sekuriteit & Toegang", + "business_finance": "Besigheid & Finansies", + "licenses_services": "Lisensies & Dienste", + "accounts": "Rekeninge", + "identity": "Identiteit", + "email": "E-pos", + "wifi": "WiFi", + "api_keys": "API Sleutels", + "ssh_keys": "SSH Sleutels", + "cards": "Kaarte", + "software_licenses": "Sagteware Lisensies", + "recent_accounts": "Onlangse Rekeninge", + "no_recent_accounts": "Geen onlangse rekeninge nie", + "last_24_hours": "Laaste 24 ure", + "no_accounts_create": "Geen rekeninge gevind nie. Skep een om te begin.", + "adjust_search_filter": "Probeer jou soek of filter kriteria aan te pas", + "no_recent_activity": "Geen onlangse aktiwiteit nie", + "no_projects_available": "Geen projekte beskikbaar nie", + "no_workspace_selected": "Geen werkruimte gekies nie" + }, + "activity": { + "password_updated": "Wagwoord opgedateer", + "note_created": "Veilige nota geskep", + "you_create_login": "Jy het 'n nuwe intekening geskep", + "you_create_api_key": "Jy het 'n nuwe API sleutel geskep", + "you_update_login": "Jy het 'n intekening opgedateer", + "you_update_api_key": "Jy het 'n API sleutel opgedateer", + "you_delete_login": "Jy het 'n intekening geskrap", + "you_delete_api_key": "Jy het 'n API sleutel geskrap", + "create_login": "Nuwe intekening geskep", + "create_api_key": "Nuwe API sleutel geskep", + "update_login": "Intekening opgedateer", + "update_api_key": "API sleutel opgedateer", + "delete_login": "Intekening geskrap", + "delete_api_key": "API sleutel geskrap" + }, + "time": { + "today": "Vandag", + "yesterday": "Gister", + "last_7_days": "Laaste 7 dae", + "last_30_days": "Laaste 30 dae", + "all_time": "Alle tyd", + "just_now": "Net nou", + "hours_ago": "{hours} ure gelede", + "days_ago": "{days} dae gelede" + }, + "password_generator": { + "title": "Wagwoord Generator", + "strong": "Sterk", + "memorable": "Onthoubaar", + "pin": "PIN", + "passphrase": "Wagwoord Frase", + "chars": "karakters", + "digits": "syfers", + "generate_new": "Genereer nuwe wagwoord", + "generate_password": "Genereer Wagwoord", + "click_generate_button": "Klik die Genereer knoppie om 'n nuwe wagwoord te genereer", + "copy": "Kopieer wagwoord", + "copied": "Gekopieer!", + "save": "Stoor wagwoord", + "close": "Sluit", + "copy_password": "Kopieer Wagwoord", + "password_length": "Wagwoord Lengte", + "options": "Opsies", + "advanced": "Gevorderd", + "advanced_options": "Gevorderde Opsies", + "history": "Geskiedenis", + "uppercase": "Hoofletters (A-Z)", + "lowercase": "Kleinletters (a-z)", + "numbers": "Syfers (0-9)", + "symbols": "Simbole (!@#$%^&*)", + "password_analysis": "Wagwoord Analise", + "strong_enough": "Sterk genoeg vir die meeste webwerwe", + "not_strong_enough": "Nie sterk genoeg vir sensitiewe rekeninge nie", + "good_length": "Goeie lengte", + "consider_longer": "Oorweeg 'n langer wagwoord", + "using_all_types": "Gebruik alle karakter tipes", + "consider_more_types": "Oorweeg die gebruik van meer karakter tipes", + "all_characters": "Alle Karakters", + "easy_to_read": "Maklik om te lees (geen soortgelyke karakters soos 1, l, I, 0, O nie)", + "easy_to_say": "Maklik om te sê (geen syfers of spesiale karakters nie)", + "password_tips": "Wagwoord Wenke", + "tip_min_length": "• Gebruik 'n minimum van 14 karakters vir hoë sekuriteit", + "tip_mix": "• Sluit 'n mengsel van letters, syfers en simbole in", + "tip_no_personal": "• Vermy die gebruik van persoonlike inligting", + "tip_unique": "• Gebruik 'n ander wagwoord vir elke rekening", + "tip_manager": "• Oorweeg die gebruik van 'n wagwoord bestuurder om jou wagwoorde te stoor", + "recently_generated": "Onlangs Gegenereer", + "clear_history": "Maak Geskiedenis Skoon", + "no_history": "Nog geen wagwoord geskiedenis nie", + "history_hint": "Gegenereerde wagwoorde sal hier verskyn", + "weak": "Swak", + "fair": "Redelik", + "good": "Goed", + "strong_strength": "Sterk", + "strength": "{strength}% Sterkte", + "current_session": "Huidige Sessie", + "create_secure_passwords": "Skep veilige wagwoorde vir jou rekeninge" + }, + "user_settings": { + "title": "Gebruiker Instellings", + "description": "Bestuur jou rekening instellings en voorkeure", + "profile": "Profiel", + "security": "Sekuriteit", + "notifications": "Kennisgewings", + "integrations": "Integrasies", + "billing": "Fakturering", + "logout": "Teken uit", + "login_history": "Intekening Geskiedenis", + "audit_log": "Oudit Log", + "profile_title": "Profiel & Rekening Inligting", + "profile_description": "Bestuur jou persoonlike en rekening besonderhede", + "language": "Taal", + "select_language": "Kies Taal", + "email_address": "E-pos Adres", + "full_name": "Volle Naam", + "previous": "Vorige", + "next": "Volgende", + "page": "Bladsy", + "of": "van", + "saving": "Stoor...", + "save_changes": "Stoor Veranderinge", + "profile_picture": "Profiel Prent", + "loading_history": "Intekening Geskiedenis word gelaai...", + "no_history_available": "Geen intekening geskiedenis beskikbaar nie", + "try_again": "Probeer weer", + "device_browser": "Toestel & Blaaier", + "ip_address": "IP Adres", + "time": "Tyd" + }, + "accounts": { + "no_accounts": "Geen rekeninge nie", + "no_matching_accounts": "Geen ooreenstemmende rekeninge gevind nie", + "loading_accounts": "Laai rekeninge...", + "error_fetching_accounts": "Kon nie rekeninge haal nie", + "account_deleted_successfully": "Rekening suksesvol geskrap", + "error_deleting_account": "Kon nie rekening skrap nie", + "account_updated_successfully": "Rekening suksesvol opgedateer", + "all_fields_required": "Vul asseblief alle verpligte velde in", + "account_already_exists": "'n Rekening met hierdie naam bestaan reeds", + "validation_error": "Ongeldige invoer data. Gaan jou inskrywings na", + "failed_to_add_account": "Kon nie rekening byvoeg nie. Probeer asseblief weer", + "missing_auth_or_selection": "Teken asseblief in of kies 'n werkruimte en projek", + "please_log_in": "Teken asseblief in om voort te gaan", + "server_error": "'n Bediener fout het voorgekom. Probeer asseblief later weer", + "adding": "Voeg by...", + "ascending": "Toenemend", + "descending": "Afnemend", + "add": "Voeg by", + "cancel": "Kanselleer", + "account_name": "Rekening Naam", + "enter_account_name": "Voer rekening naam in", + "username": "Gebruikersnaam", + "enter_username": "Voer gebruikersnaam in", + "add_account": "Voeg Rekening by", + "add_new_account": "Voeg Nuwe Rekening by", + "password": "Wagwoord", + "enter_password": "Voer wagwoord in", + "website": "Webwerf", + "enter_website": "Voer webwerf URL in", + "enter_website_url": "Voer webwerf URL in", + "notes": "Notas", + "enter_notes": "Voer notas in", + "tags": "Etikette", + "enter_tags_comma_separated": "Voer etikette in (komma geskei)", + "accounts": "Rekeninge", + "manage_your_saved_accounts_and_passwords": "Bestuur jou gestoorde rekeninge en wagwoorde", + "encryption_key_not_found": "Projek enkripsie sleutel nie gevind nie. Kan nie rekening stoor nie.", + "project_not_found": "Projek nie gevind nie. Kies asseblief 'n geldige projek.", + "encryption_failed": "Kon nie rekening data enkripteer nie. Probeer asseblief weer.", + "decryption_failed": "Kon nie rekening data dekripteer nie. Dit kan wees as gevolg van 'n ontbrekende of ongeldige enkripsie sleutel.", + "please_fill_all_required_fields": "Vul asseblief alle verpligte velde in", + "select_project": "Kies Projek", + "no_project_selected": "Geen Projek Gekies nie", + "please_select_a_project": "Kies asseblief 'n projek om rekeninge te sien", + "loading": "Laai...", + "error": "Fout", + "try_again": "Probeer Weer", + "no_accounts_found": "Geen Rekeninge Gevind nie", + "no_accounts_exist": "Geen rekeninge bestaan vir hierdie projek nie", + "adjust_search_or_filter": "Pas jou soek of filter aan om rekeninge te vind", + "generate_password": "Genereer Wagwoord", + "account": "Rekening", + "category": "Kategorie", + "last_modified": "Laaste Gewysig", + "date_created": "Datum Geskep", + "actions": "Aksies", + "copy_username": "Kopieer Gebruikersnaam", + "copied": "Gekopieer", + "copy_password": "Kopieer Wagwoord", + "show_password": "Wys Wagwoord", + "hide_password": "Versteek Wagwoord", + "edit": "Redigeer", + "delete": "Skrap", + "confirm_deletion": "Bevestig Skrapping", + "confirm_delete_account": "Is jy seker jy wil hierdie rekening skrap?", + "confirm_delete_account_description": "Hierdie aksie kan nie ongedaan gemaak word nie en sal die rekening kredensiale permanent skrap.", + "deleting": "Skrap...", + "showing": "Wys", + "of": "van", + "rows_per_page": "Rye per bladsy", + "clear_filters": "Maak Filters Skoon", + "accounts_not_found": "Rekeninge nie gevind nie", + "account_not_found": "Rekening nie gevind nie", + "failed_to_delete_account": "Kon nie rekening skrap nie", + "failed_to_update_account": "Kon nie rekening opdateer nie", + "updating": "Dateer op...", + "update": "Dateer op", + "view_details": "Bekyk Besonderhede", + "filter_warning": "Filters kan paginasie akkuraatheid beïnvloed aangesien bediener-kant filtering nie ondersteun word nie.", + "search": "Soek", + "search_accounts": "Soek rekeninge", + "all_accounts": "Alle Rekeninge", + "personal": "Persoonlik", + "work": "Werk", + "finance": "Finansies", + "clear": "Maak skoon", + "clear_sort": "Maak skoon", + "favorite": "Gunsteling", + "no_accounts_create": "Geen rekeninge gevind nie. Skep een om te begin.", + "adjust_search_filter": "Probeer jou soek of filter kriteria aan te pas", + "error_loading_workspace": "Kon nie werkruimte data laai nie", + "focus_search": "Fokus Soek", + "switch_project": "Wissel Projek", + "add_a_tag": "Voeg 'n etiket by", + "edit_account": "Redigeer Rekening", + "update_account": "Dateer Rekening op", + "filter_by_tag": "Filter volgens etiket", + "confirm_delete": "Bevestig Skrap", + "delete_account_confirmation": "Is jy seker jy wil hierdie rekening skrap? Hierdie aksie kan nie ongedaan gemaak word nie.", + "sort": "Sorteer", + "sort_options": "Sorteer Opsies", + "sort_by_name": "Sorteer volgens Naam", + "sort_by_created": "Sorteer volgens Geskep Datum", + "sort_by_updated": "Sorteer volgens Opgedateer Datum" + }, + "api_keys": { + "title": "API Sleutels", + "description": "Bestuur jou API sleutels vir programmatiese toegang tot Zecrypt dienste.", + "edit_api_key": "Redigeer API Sleutel", + "search": "Soek", + "name": "Naam", + "key": "Sleutel", + "api_key": "API Sleutel", + "enter_api_key": "Voer API sleutel in", + "type": "Tipe", + "created": "Geskep", + "expires": "Verval", + "status": "Status", + "actions": "Aksies", + "clear": "Maak skoon", + "sort": "Sorteer", + "last_used": "Omgewing", + "sort_options": "Sorteer Opsies", + "sort_by_name": "Sorteer volgens Naam", + "sort_by_created": "Sorteer volgens Geskep Datum", + "sort_by_updated": "Sorteer volgens Opgedateer Datum", + "ascending": "Toenemend", + "descending": "Afnemend", + "clear_sort": "Maak Sorteer Skoon", + "clear_filters": "Maak Filters Skoon", + "create_new_api_key": "Skep Nuwe API Sleutel", + "enter_details_for_new_api_key": "Voer die besonderhede vir jou nuwe API Sleutel in", + "api_key_name": "API Sleutel Naam", + "enter_api_key_name": "bv. Produksie Agterkant", + "key_type": "Sleutel Tipe", + "key_preview": "Sleutel Voorskou", + "key_preview_description": "Die werklike sleutel sal gegenereer word wanneer jy op \"Skep API Sleutel\" klik", + "create_api_key": "Skep API Sleutel", + "cancel": "Kanselleer", + "rows_per_page": "Rye per bladsy", + "add_api_key": "Voeg API Sleutel by", + "no_api_keys_found": "Geen API Sleutels Gevind nie", + "try_adjusting_search_or_filter": "Probeer jou soek of filter kriteria aan te pas", + "error_fetching_api_keys": "Kon nie API sleutels haal nie", + "failed_to_fetch_api_keys": "Kon nie API sleutels laai nie. Probeer asseblief weer.", + "confirm_delete_api_key": "Is jy seker jy wil hierdie API sleutel skrap?", + "api_key_deleted_successfully": "API Sleutel suksesvol geskrap", + "api_key_deleted_description": "Die API sleutel is suksesvol geskrap.", + "error_deleting_api_key": "Kon nie API Sleutel skrap nie", + "failed_to_delete_api_key": "Kon nie die API sleutel skrap nie. Probeer asseblief weer.", + "api_key_updated_successfully": "API Sleutel Opgedateer", + "api_key_updated_description": "Die API sleutel is suksesvol opgedateer.", + "api_key_added_successfully": "API Sleutel Bygevoeg", + "api_key_added_description": "Die API sleutel is suksesvol bygevoeg.", + "failed_to_update_api_key": "Kon nie die API sleutel opdateer nie. Probeer asseblief weer.", + "manage_your_api_keys": "Bestuur jou API sleutels", + "showing": "Wys", + "of": "van", + "add_new_api_key": "Voeg Nuwe API Sleutel by", + "add_new_api_key_description": "Voer jou API sleutel besonderhede hieronder in", + "api_key_already_exists": "'n API sleutel met hierdie naam bestaan reeds.", + "error_adding_api_key": "Kon nie API sleutel byvoeg nie as gevolg van 'n bediener fout.", + "failed_to_add_api_key": "Kon nie die API sleutel byvoeg nie. Probeer asseblief weer.", + "error_updating_api_key": "Kon nie API sleutel opdateer nie as gevolg van 'n bediener fout.", + "invalid_input_data": "Ongeldige invoer data. Gaan jou inskrywings na.", + "network_error": "Netwerk fout. Gaan jou verbinding na en probeer weer.", + "no_api_keys_for_env": "Geen API sleutels gevind vir omgewing '{env}' nie. Probeer 'n ander omgewing of maak filters skoon.", + "no_api_keys_match_search": "Geen API sleutels stem ooreen met die soek '{search}' nie. Probeer jou soek aan te pas.", + "adjust_search_filter": "Probeer jou soek of filter kriteria aan te pas.", + "copied": "Gekopieer", + "api_key_copied": "Die API sleutel is na jou knipbord gekopieer.", + "copy_failed": "Kopiëring het misluk", + "failed_to_copy_api_key": "Kon nie die API sleutel na knipbord kopieer nie.", + "show_key": "Wys Sleutel", + "hide_key": "Versteek Sleutel", + "copy_key": "Kopieer Sleutel", + "edit": "Redigeer", + "delete": "Skrap", + "decryption_error": "Dekripsie fout - redigeer asseblief om formaat op te dateer", + "adding": "Voeg by...", + "updating": "Dateer op", + "no_project_selected": "Geen projek gekies nie. Kies asseblief 'n projek om voort te gaan.", + "please_fill_all_required_fields": "Vul asseblief alle verpligte velde in", + "loading_api_keys": "Laai API sleutels...", + "api_keys": "API Sleutels", + "env": "Omgewing", + "tags": "Etikette", + "last_modified": "Laaste Gewysig", + "api_key_description": "API Sleutel Beskrywing", + "enter_description": "Voer 'n beskrywing vir hierdie API sleutel in", + "environment": "Omgewing", + "select_environment": "Kies omgewing", + "development": "Ontwikkeling", + "staging": "Staging", + "production": "Produksie", + "testing": "Toets", + "local": "Lokaal", + "uat": "UAT", + "add": "Voeg by", + "add_a_tag": "Voeg 'n etiket by", + "update_api_key": "Dateer API Sleutel op", + "all_envs": "Alle Omgewings", + "search_across_all_fields": "Soek", + "delete_api_key_confirmation": "Is jy seker jy wil hierdie API sleutel skrap? Hierdie aksie kan nie ongedaan gemaak word nie.", + "deleting": "Skrap...", + "confirm_deletion": "Bevestig Skrapping", + "edit_api_key_description": "Dateer jou API sleutel besonderhede op", + "showing_results": "Wys {startIdx}-{endIdx} van {totalCount} resultate", + "notes": "Notas", + "enter_notes": "Voer notas in", + "filter_by_env": "Filter volgens omgewing", + "date_created": "Datum Geskep" + }, + "wallet_passphrases": { + "wallet_passphrases": "Beursie Wagtale", + "securely_store_and_manage_your_wallet_recovery_phrases": "Stoor en bestuur jou beursie herstel frases veilig", + "add_passphrase": "Voeg Wagtale by", + "enter_wallet_address": "Voer beursie adres in", + "copy_wallet_address": "Kopieer beursie adres", + "wallet_address": "Beursie Adres", + "add_your_first_passphrase": "Voeg Jou Eerste Wagtale by", + "search_passphrases": "Soek", + "no_wallet_passphrases_found": "Geen Beursie Wagtale Gevind nie", + "wallet_address_encryption_note": "Beursie adres is geënkripteer vir sekuriteit", + "no_passphrases_found": "Geen Wagtale Gevind nie", + "no_passphrases_found_message": "Jy het nog geen beursie wagtale bygevoeg nie. Voeg een by om te begin.", + "no_passphrases_found_search": "Geen wagtale stem ooreen met jou soek kriteria nie.", + "loading_passphrases": "Laai wagtale...", + "please_wait_while_we_fetch_your_secure_passphrases": "Wag asseblief terwyl ons jou veilige wagtale haal", + "error_loading_passphrases": "Fout by Laai van Wagtale", + "failed_to_load_passphrases": "Kon nie wagtale laai nie. Probeer asseblief weer.", + "passphrase_added_successfully": "Wagtale Suksesvol Bygevoeg", + "passphrase_added_description": "Jou beursie wagtale is veilig gestoor.", + "passphrase_updated_successfully": "Wagtale Suksesvol Opgedateer", + "passphrase_updated_description": "Jou beursie wagtale is opgedateer.", + "failed_to_add_passphrase": "Kon nie Wagtale byvoeg nie", + "failed_to_update_passphrase": "Kon nie Wagtale opdateer nie", + "wallet_phrase_already_exists": "Hierdie wagtale of naam bestaan reeds in jou beursie", + "invalid_input_data": "Ongeldige invoer data. Gaan jou inskrywings na.", + "no_project_selected": "Geen projek gekies nie. Kies asseblief 'n projek.", + "passphrase_required": "Wagtale is vereis", + "passphrase_must_be_exactly_12_words": "Wagtale moet presies 12 woorde bevat (tans {count})", + "please_fill_all_required_fields": "Vul asseblief alle verpligte velde in", + "name": "Naam", + "add": "Voeg by", + "sort": "Sorteer", + "sort_by_name": "Sorteer volgens Naam", + "sort_by_created": "Sorteer volgens Geskep Datum", + "sort_by_updated": "Sorteer volgens Opgedateer Datum", + "sort_options": "Sorteer Opsies", + "enter_passphrase_name": "Voer wagtale naam in", + "passphrase": "Wagtale", + "enter_wallet_recovery_phrase": "Voer beursie herstel frase in", + "notes": "Notas", + "enter_notes": "Voer notas in", + "wallet_type": "Beursie Tipe", + "select_wallet_type": "Kies beursie tipe", + "tags": "Etikette", + "add_a_tag": "Voeg 'n etiket by", + "save_passphrase": "Stoor Wagtale", + "update_passphrase": "Dateer Wagtale op", + "passphrase_encryption_note": "Jou wagtale sal geënkripteer word vir sekuriteit.", + "adding": "Voeg by...", + "updating": "Dateer op...", + "search_across_all_fields": "Soek", + "filter_by_wallet_type": "Filter volgens beursie tipe", + "all_wallet_types": "Alle Beursie Tipes", + "clear_filters": "Maak filters skoon", + "last_modified": "Laaste Gewysig", + "actions": "Aksies", + "hide_passphrase": "Versteek wagtale", + "show_passphrase": "Wys wagtale", + "copy_passphrase": "Kopieer wagtale", + "edit": "Redigeer", + "delete": "Skrap", + "showing_results": "Wys {startIdx}-{endIdx} van {totalCount} resultate", + "confirm_deletion": "Bevestig Skrapping", + "delete_passphrase_confirmation": "Is jy seker jy wil hierdie beursie wagtale skrap? Hierdie aksie kan nie ongedaan gemaak word nie.", + "deleting": "Skrap...", + "add_new_passphrase_description": "Voer jou beursie wagtale besonderhede hieronder in", + "edit_passphrase": "Redigeer Wagtale", + "edit_passphrase_description": "Dateer jou beursie wagtale besonderhede op", + "copied": "Gekopieer", + "passphrase_copied": "Wagtale na knipbord gekopieer", + "copy_failed": "Kopiëring het misluk", + "failed_to_copy_passphrase": "Kon nie wagtale na knipbord kopieer nie", + "cancel": "Kanselleer", + "date_created": "Datum Geskep" + }, + "identity": { + "title": "Identiteit", + "description": "Bestuur jou persoonlike identiteit inligting veilig", + "add_identity": "Voeg Identiteit by", + "no_identities_found": "Geen identiteite gevind nie", + "no_identities_found_message": "Jy het nog geen identiteit inligting bygevoeg nie.", + "add_your_first_identity": "Voeg Jou Eerste Identiteit by", + "loading_identities": "Laai identiteite...", + "search_identities": "Soek", + "full_name": "Volle Naam", + "country": "Land", + "enter_full_name": "Voer volle naam in", + "date_of_birth": "Geboorte Datum", + "enter_date_of_birth": "Voer geboorte datum in (YYYY-MM-DD)", + "address": "Adres", + "enter_address": "Voer adres in", + "phone_number": "Telefoon Nommer", + "enter_phone_number": "Voer telefoon nommer in", + "identity_number": "Identiteit Nommer", + "enter_identity_number": "Voer identiteit nommer in (bv. paspoort, ID)", + "tags": "Etikette", + "enter_tags_comma_separated": "Voer etikette in (komma geskei)", + "last_modified": "Laaste Gewysig", + "actions": "Aksies", + "edit": "Redigeer", + "delete": "Skrap", + "copy_field": "Kopieer {field}", + "copied": "Gekopieer", + "identity_deleted_successfully": "Identiteit suksesvol geskrap", + "error_deleting_identity": "Kon nie identiteit skrap nie", + "identity_updated_successfully": "Identiteit suksesvol opgedateer", + "failed_to_add_identity": "Kon nie identiteit byvoeg nie", + "all_fields_required": "Vul asseblief alle verpligte velde in", + "validation_error": "Ongeldige invoer data. Gaan jou inskrywings na", + "server_error": "'n Bediener fout het voorgekom. Probeer asseblief later weer", + "identities": "Identiteite", + "filter_by_tag": "Filter volgens etiket", + "all_tags": "Alle Etikette", + "search": "Soek", + "clear_filters": "Maak filters skoon", + "name": "Naam", + "contact": "Kontak", + "passport": "Paspoort", + "national_id": "Nasionale ID", + "add_new_identity": "Voeg Nuwe Identiteit by", + "add_new_identity_description": "Voer jou identiteit besonderhede hieronder in", + "first_name": "Voornaam", + "last_name": "Van", + "email": "E-pos", + "phone": "Telefoon", + "passport_number": "Paspoort Nommer", + "cancel": "Kanselleer", + "save": "Stoor", + "edit_identity": "Redigeer Identiteit", + "edit_identity_description": "Dateer jou identiteit besonderhede op", + "update": "Dateer op", + "identity_deleted": "Identiteit geskrap", + "identity_deleted_description": "Die identiteit is suksesvol geskrap", + "field_copied": "Veld na knipbord gekopieer", + "copy_failed": "Kopiëring het misluk", + "failed_to_copy_field": "Kon nie veld na knipbord kopieer nie", + "hide_sensitive_data": "Versteek sensitiewe data", + "show_sensitive_data": "Wys sensitiewe data", + "showing_results": "Wys {startIdx}-{endIdx} van {totalCount} resultate", + "page_of": "Bladsy {currentPage} van {totalPages}", + "copy_passport": "Kopieer paspoort nommer", + "copy_national_id": "Kopieer nasionale ID", + "identity_added_successfully": "Identiteit suksesvol bygevoeg", + "error_adding_identity": "Fout by byvoeging van identiteit", + "error_updating_identity": "Fout by opdatering van identiteit", + "missing_workspace_project": "Ontbrekende werkruimte of projek", + "first_name_required": "Voornaam is vereis", + "last_name_required": "Van is vereis", + "invalid_email": "Ongeldige e-pos formaat", + "invalid_date_format": "Ongeldige datum formaat (YYYY-MM-DD)", + "title_required": "Titel is vereis", + "title_placeholder": "Persoonlike Identiteit", + "first_name_placeholder": "Jan", + "last_name_placeholder": "van der Merwe", + "email_placeholder": "jan.vandermerwe@voorbeeld.co.za", + "phone_placeholder": "+27 (0) 12 345 6789", + "address_placeholder": "123 Voorstraat, Pretoria, 0001", + "passport_placeholder": "A12345678", + "national_id_placeholder": "1234567890123", + "notes": "Notas", + "notes_placeholder": "Addisionele inligting", + "tags_placeholder": "Voeg etikette by", + "adding": "Voeg by...", + "updating": "Dateer op...", + "deleting": "Skrap...", + "add": "Voeg by", + "confirm_deletion": "Bevestig Skrapping", + "delete_identity_confirmation": "Is jy seker jy wil hierdie identiteit skrap? Hierdie aksie kan nie ongedaan gemaak word nie.", + "error_fetching_identities": "Fout by haal van identiteite", + "national_id_required": "Nasionale ID is vereis", + "identity_type": "Identiteit Tipe", + "sort": "Sorteer", + "sort_options": "Sorteer Opsies", + "sort_by_name": "Sorteer volgens Naam", + "sort_by_created": "Sorteer volgens Geskep Datum", + "sort_by_updated": "Sorteer volgens Opgedateer Datum", + "ascending": "Toenemend", + "descending": "Afnemend", + "clear_sort": "Maak Sorteer Skoon", + "search_across_all_fields": "Soek", + "searches_all_fields": "Soek naam, tipe, kontak, adres, ID en etikette", + "encryption_key_not_found": "Projek enkripsie sleutel nie gevind nie. Kan nie identiteit stoor nie.", + "project_not_found": "Projek nie gevind nie. Kies asseblief 'n geldige projek.", + "encryption_failed": "Kon nie identiteit data enkripteer nie. Probeer asseblief weer.", + "decryption_failed": "Kon nie identiteit data dekripteer nie. Dit kan wees as gevolg van 'n ontbrekende of ongeldige enkripsie sleutel.", + "manage_your_personal_identities": "Bestuur jou persoonlike identiteit inligting veilig", + "date_created": "Datum Geskep" + }, + "cards": { + "title": "Kaarte", + "description": "Bestuur jou krediet en debiet kaart inligting veilig", + "add_card": "Voeg Kaart by", + "no_cards_found": "Geen kaarte gevind nie", + "no_cards_found_message": "Jy het nog geen kaart inligting bygevoeg nie.", + "add_your_first_card": "Voeg Jou Eerste Kaart by", + "loading_cards": "Laai kaarte...", + "search_cards": "Soek", + "card_name": "Kaart Naam", + "name": "Naam", + "enter_card_name": "Voer kaart naam in (bv. Visa Eindig 1234)", + "card_number": "Kaart Nommer", + "enter_card_number": "Voer kaart nommer in", + "expiry_date": "Verval Datum", + "enter_expiry_date": "Voer verval datum in (MM/JJ)", + "cvv": "CVV", + "enter_cvv": "Voer CVV in", + "cardholder_name": "Kaarthouer", + "card_holder_name": "Kaarthouer", + "enter_cardholder_name": "Voer kaarthouer in", + "tags": "Etikette", + "enter_tags_comma_separated": "Voer etikette in (komma geskei)", + "last_modified": "Laaste Gewysig", + "actions": "Aksies", + "edit": "Redigeer", + "delete": "Skrap", + "copy_field": "Kopieer {field}", + "copied": "Gekopieer", + "card_deleted_successfully": "Kaart suksesvol geskrap", + "error_deleting_card": "Kon nie kaart skrap nie", + "card_updated_successfully": "Kaart suksesvol opgedateer", + "card_added_successfully": "Kaart suksesvol bygevoeg", + "failed_to_add_card": "Kon nie kaart byvoeg nie", + "all_fields_required": "Vul asseblief alle verpligte velde in", + "validation_error": "Ongeldige invoer data. Gaan jou inskrywings na", + "server_error": "'n Bediener fout het voorgekom. Probeer asseblief later weer", + "credit_cards": "Kredietkaarte", + "filter_by_brand": "Filter volgens handelsmerk", + "all_brands": "Alle Handelsmerke", + "search": "Soek", + "clear_filters": "Maak filters skoon", + "card_holder": "Kaarthouer", + "brand": "Handelsmerk", + "expiry": "Verval", + "add_new_card": "Voeg Nuwe Kaart by", + "add_new_card_description": "Voer jou kredietkaart besonderhede hieronder in", + "select_brand": "Kies handelsmerk", + "expiry_month": "Verval Maand", + "expiry_year": "Verval Jaar", + "cancel": "Kanselleer", + "save": "Stoor", + "edit_card": "Redigeer Kaart", + "edit_card_description": "Dateer jou kredietkaart besonderhede op", + "update": "Dateer op", + "card_deleted": "Kaart geskrap", + "card_deleted_description": "Die kaart is suksesvol geskrap", + "field_copied": "Veld na knipbord gekopieer", + "copy_failed": "Kopiëring het misluk", + "failed_to_copy_field": "Kon nie veld na knipbord kopieer nie", + "hide_card_number": "Versteek kaart nommer", + "show_card_number": "Wys kaart nommer", + "copy_to_clipboard": "Na knipbord kopieer", + "showing_results": "Wys {startIdx}-{endIdx} van {totalCount} resultate", + "page_of": "Bladsy {currentPage} van {totalPages}", + "title_placeholder": "Persoonlike Visa Kaart", + "card_holder_name_placeholder": "Jan van der Merwe", + "card_number_placeholder": "4111 1111 1111 1111", + "notes": "Notas", + "notes_placeholder": "Addisionele inligting oor hierdie kaart", + "tags_placeholder": "Voeg etikette by", + "add": "Voeg by", + "adding": "Voeg by...", + "updating": "Dateer op...", + "deleting": "Skrap...", + "confirm_delete": "Bevestig Skrapping", + "delete_card_confirmation": "Is jy seker jy wil hierdie kaart skrap? Hierdie aksie kan nie ongedaan gemaak word nie.", + "error_fetching_cards": "Fout by haal van kaarte", + "missing_workspace_project": "Ontbrekende werkruimte of projek", + "title_required": "Titel is vereis", + "card_holder_name_required": "Kaarthouer is vereis", + "card_number_required": "Kaart nommer is vereis", + "invalid_card_number": "Ongeldige kaart nommer", + "cvv_required": "CVV is vereis", + "invalid_cvv": "Ongeldige CVV", + "brand_required": "Handelsmerk is vereis", + "error_adding_card": "Fout by byvoeging van kaart", + "error_updating_card": "Fout by opdatering van kaart", + "other": "Ander", + "filter_by_tag": "Filter volgens etiket", + "all_tags": "Alle Etikette", + "sort": "Sorteer", + "sort_options": "Sorteer Opsies", + "sort_by_name": "Sorteer volgens Naam", + "sort_by_created": "Sorteer volgens Geskep Datum", + "sort_by_updated": "Sorteer volgens Opgedateer Datum", + "ascending": "Toenemend", + "descending": "Afnemend", + "clear_sort": "Maak Sorteer Skoon", + "search_across_all_fields": "Soek", + "searches_all_fields": "Soek titel, kaarthouer, kaart nommer, handelsmerk en etikette", + "missing_auth_or_selection": "Ontbrekende outentikasie of keuse", + "encryption_key_not_found": "Projek enkripsie sleutel nie gevind nie. Kan nie kaart stoor nie.", + "project_not_found": "Projek nie gevind nie. Kies asseblief 'n geldige projek.", + "encryption_failed": "Kon nie kaart data enkripteer nie. Probeer asseblief weer.", + "decryption_failed": "Kon nie kaart data dekripteer nie. Dit kan wees as gevolg van 'n ontbrekende of ongeldige enkripsie sleutel.", + "manage_your_cards": "Bestuur jou krediet en debiet kaart inligting veilig", + "date_created": "Datum Geskep" + }, + "wifi": { + "wifi_networks": "Wi-Fi Netwerke", + "manage_your_wifi_networks": "Bestuur jou Wi-Fi netwerke veilig.", + "add_wifi": "Voeg Wi-Fi by", + "add_new_wifi": "Voeg Nuwe Wi-Fi by", + "add_new_wifi_description": "Voer jou Wi-Fi netwerk besonderhede hieronder in", + "edit_wifi": "Redigeer Wi-Fi", + "edit_wifi_description": "Dateer jou Wi-Fi netwerk besonderhede op", + "search_wifi": "Soek", + "filter_by_security": "Filter volgens sekuriteit", + "all_security_types": "Alle Sekuriteit Tipes", + "none": "Geen", + "clear": "Maak skoon", + "clear_filters": "Maak Filters Skoon", + "loading_wifi": "Laai Wi-Fi netwerke...", + "no_wifi_found": "Geen Wi-Fi netwerke gevind nie", + "no_wifi_for_security_type": "Geen Wi-Fi netwerke gevind vir {security_type} nie", + "no_wifi_match_search": "Geen Wi-Fi netwerke stem ooreen met jou soek '{search}' nie", + "name": "Naam", + "loading_wifi_networks": "Laai Wi-Fi netwerke", + "no_wifi_networks_found": "Geen Wi-Fi Netwerk Gevind nie", + "add_wifi_network": "Voeg Wi-Fi Netwerk by", + "no_wifi_for_tag": "Geen WiFi netwerke gevind met etiket {tag} nie", + "adjust_search_filter": "Probeer jou soek of filter aan te pas om te vind waarna jy soek.", + "ssid": "SSID", + "security": "Sekuriteit", + "security_type": "Sekuriteit Tipe", + "select_security_type": "Kies sekuriteit tipe", + "notes": "Notas", + "enter_notes": "Voer notas in", + "password": "Wagwoord", + "enter_password": "Voer wagwoord in", + "enter_new_password": "Voer nuwe wagwoord in (laat leeg om onveranderd te laat)", + "show_password": "Wys wagwoord", + "hide_password": "Versteek wagwoord", + "copy_password": "Kopieer wagwoord", + "tags": "Etikette", + "add_a_tag": "Voeg 'n etiket by", + "add": "Voeg by", + "last_modified": "Laaste Gewysig", + "date_created": "Datum Geskep", + "actions": "Aksies", + "showing": "Wys", + "of": "van", + "rows_per_page": "Rye per bladsy", + "edit": "Redigeer", + "copy_ssid": "Kopieer SSID", + "delete": "Skrap", + "confirm_delete_wifi": "Bevestig Skrap WiFi", + "delete_wifi_confirmation": "Is jy seker jy wil hierdie WiFi netwerk skrap? Hierdie aksie kan nie ongedaan gemaak word nie.", + "wifi_deleted": "WiFi Geskrap", + "wifi_deleted_description": "Die WiFi netwerk is suksesvol geskrap.", + "failed_to_delete_wifi": "Kon nie WiFi netwerk skrap nie", + "error_deleting_wifi": "Fout by skrapping van WiFi", + "error_fetching_wifi_networks": "Fout by haal van WiFi netwerke", + "failed_to_fetch_wifi_networks": "Kon nie WiFi netwerke haal nie", + "no_project_selected": "Geen projek gekies nie. Kies asseblief 'n projek om voort te gaan.", + "copied": "Gekopieer", + "field_copied": "Veld na knipbord gekopieer", + "copy_failed": "Kopiëring het misluk", + "failed_to_copy_field": "Kon nie veld na knipbord kopieer nie", + "show_qr_code": "Wys QR Kode", + "wifi_qr_code": "WiFi QR Kode", + "wifi_qr_code_description": "Skandeer hierdie QR kode om met die WiFi netwerk te verbind.", + "qr_code_placeholder": "QR Kode", + "wifi_credentials": "WiFi Kredensiale", + "close": "Sluit", + "please_fill_all_required_fields": "Vul asseblief alle verpligte velde in", + "wifi_added_successfully": "WiFi suksesvol bygevoeg", + "wifi_added_description": "Die WiFi netwerk is bygevoeg.", + "wifi_already_exists": "'n WiFi netwerk met hierdie SSID bestaan reeds", + "failed_to_add_wifi": "Kon nie WiFi netwerk byvoeg nie", + "enter_ssid": "Voer SSID in", + "update_wifi": "Dateer WiFi op", + "wifi_updated_successfully": "WiFi suksesvol opgedateer", + "wifi_updated_description": "Die WiFi netwerk is opgedateer.", + "wifi_not_found": "WiFi netwerk nie gevind nie", + "failed_to_update_wifi": "Kon nie WiFi netwerk opdateer nie", + "invalid_input_data": "Ongeldige invoer data", + "adding": "Voeg by...", + "updating": "Dateer op...", + "deleting": "Skrap...", + "cancel": "Kanselleer", + "encryption_key_not_found": "Projek enkripsie sleutel nie gevind nie. Kan nie WiFi netwerk stoor nie.", + "project_not_found": "Projek nie gevind nie. Kies asseblief 'n geldige projek.", + "encryption_failed": "Kon nie WiFi data enkripteer nie. Probeer asseblief weer.", + "decryption_failed": "Kon nie WiFi data dekripteer nie. Dit kan wees as gevolg van 'n ontbrekende of ongeldige enkripsie sleutel.", + "sort": "Sorteer", + "sort_options": "Sorteer Opsies", + "sort_by_name": "Sorteer volgens Naam", + "sort_by_created": "Sorteer volgens Geskep Datum", + "sort_by_updated": "Sorteer volgens Opgedateer Datum", + "ascending": "Toenemend", + "descending": "Afnemend", + "clear_sort": "Maak Sorteer Skoon", + "filter_by_tag": "Filter volgens etiket", + "all_tags": "Alle Etikette", + "scan_with_phone": "Skandeer hierdie QR kode met jou foon om met die netwerk te verbind" + }, + "key_shortcuts": { + "title": "Sleutelbord Kortpaaie" + }, + "notes": { + "add_new_note": "Voeg Nuwe Nota by", + "note_title": "Nota Titel", + "note_content": "Nota Inhoud", + "tags": "Etikette", + "update_note": "Dateer Nota op", + "enter_note_title": "Voer nota titel in", + "add_a_tag": "Voeg 'n etiket by", + "add_note": "Voeg Nota by", + "cancel": "Kanselleer" + }, + "emails": { + "email_accounts": "E-pos Rekeninge", + "add_email": "Voeg E-pos by", + "search_email_accounts": "Soek", + "per_page": "per bladsy", + "clear_filters": "Maak filters skoon", + "title": "Titel", + "email_address": "E-pos Adres", + "servers": "Bediener", + "username": "Gebruikersnaam", + "password": "Wagwoord", + "last_modified": "Laaste Gewysig", + "tags": "Etikette", + "loading_emails": "Laai E-posse", + "actions": "Aksies", + "loading": "Laai...", + "no_results_found": "Geen resultate gevind nie", + "no_email_accounts": "Geen e-pos rekeninge nie", + "copy": "Kopieer", + "copy_to_clipboard": "Na knipbord kopieer", + "show_password": "Wys wagwoord", + "hide_password": "Versteek wagwoord", + "open_menu": "Maak menu oop", + "edit": "Redigeer", + "delete": "Skrap", + "deleted": "Geskrap", + "showing_results": "Wys {start} tot {end} van {total} resultate", + "confirm_delete": "Bevestig skrapping", + "delete_email_confirmation": "Is jy seker jy wil hierdie e-pos rekening skrap? Hierdie aksie kan nie ongedaan gemaak word nie.", + "cancel": "Kanselleer", + "deleting": "Skrap...", + "copied": "Gekopieer", + "field_copied": "Waarde na knipbord gekopieer", + "copy_failed": "Kopiëring het misluk", + "failed_to_copy_value": "Kon nie waarde na knipbord kopieer nie", + "encryption_error": "Enkripsie Fout", + "could_not_encrypt_data": "Kon nie data enkripteer nie. Probeer asseblief weer.", + "success": "Sukses", + "email_added_successfully": "E-pos rekening suksesvol bygevoeg", + "error_adding_email": "Fout by byvoeging van e-pos rekening. Probeer asseblief weer.", + "email_deleted_successfully": "E-pos kredensiale suksesvol geskrap", + "failed_to_delete_email": "Kon nie e-pos kredensiale skrap nie", + "error_fetching_emails": "Kon nie e-posse haal nie", + "error": "Fout", + "sort": "Sorteer", + "sort_options": "Sorteer Opsies", + "sort_by_name": "Sorteer volgens Naam", + "sort_by_created": "Sorteer volgens Geskep Datum", + "sort_by_updated": "Sorteer volgens Opgedateer Datum", + "ascending": "Toenemend", + "descending": "Afnemend", + "clear_sort": "Maak Sorteer Skoon", + "add_email_description": "Voeg 'n nuwe e-pos rekening by jou veilige kluis", + "title_placeholder": "Werk E-pos", + "email_address_placeholder": "jou.e-pos@voorbeeld.co.za", + "imap_server": "IMAP Bediener", + "imap_server_placeholder": "imap.voorbeeld.co.za", + "smtp_server": "SMTP Bediener", + "smtp_server_placeholder": "smtp.voorbeeld.co.za", + "username_placeholder": "Opsioneel, verstek na e-pos adres", + "password_placeholder": "Voer wagwoord in", + "notes": "Notas (opsioneel)", + "notes_placeholder": "Addisionele inligting oor hierdie e-pos rekening", + "add_tag": "Voeg etiket by", + "remove": "Verwyder", + "adding": "Voeg by...", + "add": "Voeg by", + "please_fill_all_required_fields": "Vul asseblief alle verpligte velde in", + "email_title_already_exists": "E-pos met hierdie titel bestaan reeds", + "invalid_email_format": "Ongeldige e-pos formaat", + "missing_required_data": "Ontbrekende verpligte projek data", + "edit_email": "Redigeer E-pos Rekening", + "edit_email_description": "Dateer jou e-pos rekening besonderhede op", + "save_changes": "Stoor Veranderinge", + "email_updated_successfully": "E-pos rekening suksesvol opgedateer", + "error_updating_email": "Fout by opdatering van e-pos rekening. Probeer asseblief weer.", + "manage_your_email_accounts": "Bestuur jou e-pos rekeninge en bediener instellings veilig", + "date_created": "Datum Geskep", + "filter_by_tag": "Filter volgens etiket", + "all_tags": "Alle Etikette" + }, + "ssh_keys": { + "ssh_keys": "SSH Sleutels", + "add_ssh_key": "Voeg SSH Sleutel by", + "search_ssh_keys": "Soek", + "per_page": "per bladsy", + "clear_filters": "Maak filters skoon", + "name": "Naam", + "ssh_key": "SSH Sleutel", + "tags": "Etikette", + "loading_ssh_keys": "Laai SSH Sleutels", + "actions": "Aksies", + "loading": "Laai...", + "no_results_found": "Geen resultate gevind nie", + "no_ssh_keys": "Geen SSH sleutels nie", + "hide_ssh_key": "Versteek SSH sleutel", + "show_ssh_key": "Wys volledige SSH sleutel", + "copied": "Gekopieer", + "ssh_key_copied": "SSH sleutel na knipbord gekopieer", + "copy_failed": "Kopiëring het misluk", + "failed_to_copy_ssh_key": "Kon nie SSH sleutel na knipbord kopieer nie", + "copy_to_clipboard": "Na knipbord kopieer", + "open_menu": "Maak menu oop", + "edit": "Redigeer", + "delete": "Skrap", + "fingerprint": "Vingerafdruk", + "last_modified": "Laaste Gewysig", + "deleted": "Geskrap", + "showing_results": "Wys {start} tot {end} van {total} resultate", + "confirm_delete": "Bevestig skrapping", + "delete_ssh_key_confirmation": "Is jy seker jy wil hierdie SSH sleutel skrap? Hierdie aksie kan nie ongedaan gemaak word nie.", + "cancel": "Kanselleer", + "deleting": "Skrap...", + "please_fill_all_required_fields": "Vul asseblief alle verpligte velde in", + "ssh_key_name_already_exists": "SSH sleutel met hierdie naam bestaan reeds", + "missing_required_data": "Ontbrekende verpligte projek data", + "encryption_error": "Enkripsie Fout", + "could_not_encrypt_data": "Kon nie data enkripteer nie. Probeer asseblief weer.", + "success": "Sukses", + "ssh_key_added_successfully": "SSH sleutel suksesvol bygevoeg", + "error_adding_ssh_key": "Fout by byvoeging van SSH sleutel. Probeer asseblief weer.", + "ssh_key_updated_successfully": "SSH sleutel suksesvol opgedateer", + "error_updating_ssh_key": "Fout by opdatering van SSH sleutel. Probeer asseblief weer.", + "ssh_key_deleted_successfully": "SSH sleutel suksesvol geskrap", + "error_deleting_ssh_key": "Fout by skrapping van SSH sleutel", + "error_fetching_ssh_keys": "Kon nie SSH sleutels haal nie", + "add_a_tag": "Voeg 'n etiket by", + "sort": "Sorteer", + "sort_options": "Sorteer Opsies", + "sort_by_name": "Sorteer volgens Naam", + "sort_by_created": "Sorteer volgens Geskep Datum", + "sort_by_updated": "Sorteer volgens Opgedateer Datum", + "ascending": "Toenemend", + "descending": "Afnemend", + "clear_sort": "Maak Sorteer Skoon", + "error": "Fout", + "add_ssh_key_description": "Voeg 'n nuwe SSH sleutel by jou veilige kluis", + "name_placeholder": "GitHub SSH Sleutel", + "ssh_key_placeholder": "Plak jou SSH private sleutel hier", + "notes": "Notas (opsioneel)", + "notes_placeholder": "Addisionele inligting oor hierdie SSH sleutel", + "add_tag": "Voeg etiket by", + "remove": "Verwyder", + "adding": "Voeg by...", + "add": "Voeg by", + "edit_ssh_key": "Redigeer SSH Sleutel", + "edit_ssh_key_description": "Dateer jou SSH sleutel besonderhede op", + "save_changes": "Stoor Veranderinge", + "search_licenses": "Soek", + "manage_your_ssh_keys": "Bestuur jou SSH sleutels veilig", + "filter_by_tag": "Filter volgens etiket", + "all_tags": "Alle Etikette", + "date_created": "Datum Geskep" + }, + "files": { + "files": "Lêers", + "file_manager": "Lêer Bestuurder", + "add_file": "Laai Lêer op", + "create_folder": "Skep Vouer", + "search_files": "Soek lêers", + "per_page": "per bladsy", + "clear_filters": "Maak filters skoon", + "name": "Naam", + "size": "Grootte", + "type": "Tipe", + "modified": "Gewysig", + "folder": "Vouer", + "file": "Lêer", + "loading_files": "Laai lêers...", + "actions": "Aksies", + "loading": "Laai...", + "no_results_found": "Geen resultate gevind nie", + "no_files": "Geen lêers nie", + "no_folders": "Geen vouers nie", + "download": "Aflaai", + "rename": "Hernoem", + "move": "Skuif", + "delete": "Skrap", + "copied": "Gekopieer", + "file_copied": "Lêer na knipbord gekopieer", + "copy_failed": "Kopiëring het misluk", + "failed_to_copy_file": "Kon nie lêer na knipbord kopieer nie", + "copy_to_clipboard": "Na knipbord kopieer", + "open_menu": "Maak menu oop", + "edit": "Redigeer", + "deleted": "Geskrap", + "showing_results": "Wys {start} tot {end} van {total} resultate", + "confirm_delete": "Bevestig skrapping", + "delete_file_confirmation": "Is jy seker jy wil hierdie lêer skrap? Hierdie aksie kan nie ongedaan gemaak word nie.", + "delete_folder_confirmation": "Is jy seker jy wil hierdie vouer skrap? Hierdie aksie kan nie ongedaan gemaak word nie.", + "cancel": "Kanselleer", + "deleting": "Skrap...", + "please_fill_all_required_fields": "Vul asseblief alle verpligte velde in", + "file_name_already_exists": "Lêer met hierdie naam bestaan reeds in hierdie vouer", + "folder_name_already_exists": "Vouer met hierdie naam bestaan reeds op hierdie plek", + "missing_required_data": "Ontbrekende verpligte projek data", + "encryption_error": "Enkripsie Fout", + "could_not_encrypt_data": "Kon nie data enkripteer nie. Probeer asseblief weer.", + "success": "Sukses", + "file_uploaded_successfully": "Lêer suksesvol opgelaai", + "error_uploading_file": "Fout by oplaaing van lêer. Probeer asseblief weer.", + "file_updated_successfully": "Lêer suksesvol opgedateer", + "error_updating_file": "Fout by opdatering van lêer. Probeer asseblief weer.", + "file_deleted_successfully": "Lêer suksesvol geskrap", + "error_deleting_file": "Fout by skrapping van lêer", + "error_fetching_files": "Kon nie lêers haal nie", + "folder_created_successfully": "Vouer suksesvol geskep", + "error_creating_folder": "Fout by skep van vouer. Probeer asseblief weer.", + "folder_updated_successfully": "Vouer suksesvol opgedateer", + "error_updating_folder": "Fout by opdatering van vouer. Probeer asseblief weer.", + "folder_deleted_successfully": "Vouer suksesvol geskrap", + "error_deleting_folder": "Fout by skrapping van vouer", + "error_fetching_folders": "Kon nie vouers haal nie", + "file_too_large": "Lêer grootte oorskry 50MB limiet", + "invalid_file_type": "Ongeldige lêer tipe", + "upload_failed": "Oplaaing het misluk", + "download_failed": "Aflaai het misluk", + "sort": "Sorteer", + "sort_options": "Sorteer Opsies", + "sort_by_name": "Sorteer volgens Naam", + "sort_by_size": "Sorteer volgens Grootte", + "sort_by_modified": "Sorteer volgens Gewysig Datum", + "sort_by_type": "Sorteer volgens Tipe", + "ascending": "Toenemend", + "descending": "Afnemend", + "clear_sort": "Maak Sorteer Skoon", + "error": "Fout", + "upload_file_description": "Laai 'n lêer op na jou veilige kluis", + "create_folder_description": "Skep 'n nuwe vouer om jou lêers te organiseer", + "name_placeholder": "Voer lêer of vouer naam in", + "folder_name_placeholder": "Voer vouer naam in", + "notes": "Notas (opsioneel)", + "notes_placeholder": "Addisionele inligting oor hierdie lêer", + "add_tag": "Voeg etiket by", + "remove": "Verwyder", + "uploading": "Laai op...", + "creating": "Skep...", + "add": "Voeg by", + "upload": "Laai op", + "create": "Skep", + "edit_file": "Redigeer Lêer", + "edit_file_description": "Dateer jou lêer besonderhede op", + "edit_folder": "Redigeer Vouer", + "edit_folder_description": "Dateer jou vouer besonderhede op", + "save_changes": "Stoor Veranderinge", + "manage_your_files": "Bestuur jou lêers veilig", + "filter_by_type": "Filter volgens tipe", + "all_types": "Alle Tipes", + "date_created": "Datum Geskep", + "file_size": "Lêer Grootte", + "folder_path": "Vouer Pad", + "select_folder": "Kies Vouer", + "move_to_folder": "Skuif na Vouer", + "drag_and_drop_files": "Sleep en los lêers hier, of klik om te kies", + "select_files": "Kies Lêers", + "upload_progress": "Oplaaing Vordering", + "upload_complete": "Oplaaing Voltooi", + "bytes": "grepe", + "kb": "KB", + "mb": "MB", + "gb": "GB", + "tb": "TB" + }, + "project_colors": { + "red": "Rooi", + "orange": "Oranje", + "amber": "Amber", + "yellow": "Geel", + "lime": "Lemmetjie", + "green": "Groen", + "emerald": "Smaragd", + "teal": "Teal", + "cyan": "Cyaan", + "sky": "Hemelblou", + "blue": "Blou", + "indigo": "Indigo", + "violet": "Violet", + "purple": "Pers", + "fuchsia": "Fuchsia", + "pink": "Pienk", + "rose": "Roos" + } +} diff --git a/packages/frontend-web/middleware.ts b/packages/frontend-web/middleware.ts index 30624c1a..e0becdd2 100644 --- a/packages/frontend-web/middleware.ts +++ b/packages/frontend-web/middleware.ts @@ -3,7 +3,7 @@ import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; // Available locales -export const locales = ['en', 'de', 'fr', 'es']; +export const locales = ['en', 'de', 'fr', 'es','af']; export const defaultLocale = 'en'; // Protected routes that require authentication From fefc925a923c7d2fdfbef9018211b7d8af27ae73 Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Wed, 24 Sep 2025 12:19:27 +0530 Subject: [PATCH 06/32] Added arabic --- packages/frontend-web/messages/ar/common.json | 1425 +++++++++++++++++ packages/frontend-web/middleware.ts | 2 +- 2 files changed, 1426 insertions(+), 1 deletion(-) create mode 100644 packages/frontend-web/messages/ar/common.json diff --git a/packages/frontend-web/messages/ar/common.json b/packages/frontend-web/messages/ar/common.json new file mode 100644 index 00000000..1a486781 --- /dev/null +++ b/packages/frontend-web/messages/ar/common.json @@ -0,0 +1,1425 @@ +{ + "app": { + "title": "Zecrypt", + "description": "أمان بياناتك مع Zecrypt" + }, + "common": { + "loading": "جاري التحميل...", + "saving": "جاري الحفظ...", + "deleting": "جاري الحذف...", + "error": "خطأ", + "success": "نجح", + "cancel": "إلغاء", + "add": "إضافة", + "save": "حفظ", + "submitting": "جاري الإرسال...", + "try_again": "حاول مرة أخرى", + "sort": "ترتيب", + "sort_options": "خيارات الترتيب", + "sort_by_name": "ترتيب حسب الاسم", + "sort_by_created": "ترتيب حسب تاريخ الإنشاء", + "sort_by_updated": "ترتيب حسب تاريخ التحديث", + "ascending": "تصاعدي", + "descending": "تنازلي", + "clear_sort": "مسح الترتيب", + "add_item_description": "أدخل التفاصيل أدناه", + "edit_item_description": "تحديث التفاصيل" + }, + "pagination": { + "showing_x_of_y": "عرض {start} - {end} من {total} عنصر", + "page_of": "صفحة {currentPage} من {totalPages}", + "next": "التالي", + "previous": "السابق" + }, + "env": { + "env_variables": "متغيرات البيئة", + "manage_your_env": "إدارة متغيرات البيئة بأمان", + "add_env": "إضافة متغيرات البيئة", + "loading_env": "جاري تحميل متغيرات البيئة...", + "name": "الاسم", + "env_value": "القيمة", + "tags": "العلامات", + "created_at": "تاريخ الإنشاء", + "actions": "الإجراءات", + "no_env_yet": "لم يتم إضافة متغيرات البيئة بعد", + "no_matching_env": "لم يتم العثور على متغيرات بيئة مطابقة", + "search_across_all_fields": "البحث في جميع الحقول...", + "show_env": "إظهار متغيرات البيئة", + "hide_env": "إخفاء متغيرات البيئة", + "copy_env": "نسخ متغيرات البيئة", + "env_copied": "تم نسخ متغيرات البيئة إلى الحافظة", + "failed_to_copy_env": "فشل في نسخ متغيرات البيئة إلى الحافظة", + "delete_env_confirmation": "هل أنت متأكد من أنك تريد حذف متغيرات البيئة هذه؟ لا يمكن التراجع عن هذا الإجراء.", + "showing_x_of_y": "عرض {start} - {end} من {total} عنصر", + "failed_to_decrypt": "فشل في فك تشفير متغيرات البيئة", + "add_new_env": "إضافة متغيرات بيئة جديدة", + "add_new_env_description": "أدخل تفاصيل متغيرات البيئة أدناه", + "edit_env": "تحرير متغيرات البيئة", + "edit_env_description": "تحديث متغيرات البيئة", + "env_name": "اسم البيئة", + "enter_env_name": "أدخل اسم البيئة", + "enter_env_value": "أدخل قيمة البيئة", + "enter_env_variables": "KEY=value\nANOTHER_KEY=another_value", + "env_format_hint": "أدخل متغيرات البيئة بصيغة KEY=value، واحد في كل سطر", + "notes": "الملاحظات", + "enter_notes": "أدخل الملاحظات (اختياري)", + "add_a_tag": "إضافة علامة", + "add_tag": "إضافة علامة", + "press_enter_to_add": "اضغط Enter لإضافة علامة", + "cancel": "إلغاء", + "add": "إضافة", + "update": "تحديث", + "env_added_successfully": "تم إضافة متغيرات البيئة بنجاح", + "env_updated_successfully": "تم تحديث متغيرات البيئة بنجاح", + "env_deleted_successfully": "تم حذف متغيرات البيئة بنجاح", + "error_adding_env": "فشل في إضافة متغيرات البيئة", + "error_updating_env": "فشل في تحديث متغيرات البيئة", + "error_deleting_env": "فشل في حذف متغيرات البيئة", + "error_fetching_env": "فشل في جلب متغيرات البيئة", + "please_fill_all_required_fields": "يرجى ملء جميع الحقول المطلوبة", + "no_project_selected": "لم يتم اختيار مشروع. يرجى اختيار مشروع.", + "clear_filters": "مسح المرشحات", + "decrypting": "جاري فك تشفير متغيرات البيئة...", + "env_updated_description": "تم تحديث متغيرات البيئة.", + "env_added_description": "تم إضافة متغيرات البيئة.", + "env_already_exists": "متغيرات البيئة بهذا الاسم موجودة بالفعل.", + "date_created": "تاريخ الإنشاء", + "sort": "ترتيب", + "sort_options": "خيارات الترتيب", + "sort_by_name": "ترتيب حسب الاسم", + "sort_by_created": "ترتيب حسب تاريخ الإنشاء", + "sort_by_updated": "ترتيب حسب تاريخ التحديث", + "view_env_code": "عرض في محرر الكود", + "click_to_add_env": "انقر لإضافة متغيرات البيئة", + "click_to_edit_env": "انقر لتحرير متغيرات البيئة", + "env_hidden": "••••••••", + "copied": "تم النسخ" + }, + "licenses": { + "software_licenses": "تراخيص البرمجيات", + "add_license": "إضافة ترخيص", + "filter_by_tag": "تصفية حسب العلامة", + "all_tags": "جميع العلامات", + "search": "بحث", + "loading_licenses": "جاري تحميل التراخيص...", + "no_licenses_found": "لم يتم العثور على تراخيص", + "clear_filters": "مسح المرشحات", + "name": "الاسم", + "software": "البرمجيات", + "license_key": "مفتاح الترخيص", + "expiry_date": "تاريخ الانتهاء", + "tags": "العلامات", + "actions": "الإجراءات", + "add_new_license": "إضافة ترخيص جديد", + "add_new_license_description": "أدخل تفاصيل ترخيص البرمجيات أدناه", + "cancel": "إلغاء", + "save": "حفظ", + "edit_license": "تحرير الترخيص", + "edit_license_description": "تحديث تفاصيل ترخيص البرمجيات", + "update": "تحديث", + "delete": "حذف", + "license_deleted": "تم حذف الترخيص", + "license_deleted_description": "تم حذف الترخيص بنجاح", + "copied": "تم النسخ", + "field_copied": "تم نسخ الحقل إلى الحافظة", + "copy_failed": "فشل النسخ", + "failed_to_copy_field": "فشل في نسخ الحقل إلى الحافظة", + "hide_license_key": "إخفاء مفتاح الترخيص", + "show_license_key": "إظهار مفتاح الترخيص", + "showing_results": "عرض {startIdx}-{endIdx} من {totalCount} نتيجة", + "page_of": "صفحة {currentPage} من {totalPages}", + "expires": "ينتهي", + "expired": "منتهي الصلاحية", + "expiring_soon": "ينتهي قريباً", + "edit": "تحرير", + "enter_license_name": "أدخل اسم الترخيص", + "enter_software_name": "أدخل اسم البرنامج", + "enter_license_key": "أدخل مفتاح الترخيص", + "enter_notes": "أدخل الملاحظات", + "confirm_deletion": "تأكيد الحذف", + "confirm_delete_license_description": "هل أنت متأكد من أنك تريد حذف هذا الترخيص؟ لا يمكن التراجع عن هذا الإجراء.", + "license_added_successfully": "تم إضافة الترخيص بنجاح", + "failed_to_add_license": "فشل في إضافة الترخيص", + "invalid_input": "إدخال غير صحيح", + "validation_error": "خطأ في التحقق", + "network_error": "خطأ في الشبكة", + "error_adding_license": "خطأ في إضافة الترخيص", + "license_updated_successfully": "تم تحديث الترخيص بنجاح", + "failed_to_update_license": "فشل في تحديث الترخيص", + "license_not_found": "لم يتم العثور على الترخيص", + "error_updating_license": "خطأ في تحديث الترخيص", + "missing_auth_or_selection": "مصادقة أو اختيار مفقود", + "license_deleted_successfully": "تم حذف الترخيص بنجاح", + "error_deleting_license": "خطأ في حذف الترخيص", + "error_fetching_licenses": "خطأ في جلب التراخيص", + "adding": "جاري الإضافة...", + "updating": "جاري التحديث...", + "deleting": "جاري الحذف...", + "notes": "الملاحظات", + "software_name": "اسم البرنامج", + "rows_per_page": "صفوف في الصفحة", + "sort": "ترتيب", + "sort_options": "خيارات الترتيب", + "sort_by_name": "ترتيب حسب الاسم", + "sort_by_created": "ترتيب حسب تاريخ الإنشاء", + "sort_by_updated": "ترتيب حسب تاريخ التحديث", + "ascending": "تصاعدي", + "descending": "تنازلي", + "clear_sort": "مسح الترتيب", + "search_across_all_fields": "بحث", + "update_api_key": "تحديث مفتاح API", + "manage_your_software_licenses": "إدارة تراخيص البرمجيات بأمان", + "date_created": "تاريخ الإنشاء", + "add_a_tag": "إضافة علامة" + }, + "navigation": { + "home": "الرئيسية", + "dashboard": "لوحة التحكم", + "settings": "الإعدادات" + }, + "auth": { + "login_to_continue": "تسجيل الدخول للمتابعة", + "sign_in_with_sso": "تسجيل الدخول بحساب مؤسستك", + "login": "تسجيل الدخول", + "signup": "التسجيل", + "cancel": "إلغاء", + "logout": "تسجيل الخروج", + "sign_up": "التسجيل", + "try_again": "حاول مرة أخرى", + "dont_have_account": "ليس لديك حساب؟", + "email": "البريد الإلكتروني", + "password": "كلمة المرور", + "agreement": "عند التسجيل، فإنك توافق على شروطنا وأحكامنا", + "terms": "الشروط", + "privacy_policy": "سياسة الخصوصية", + "terms_and_conditions": "الشروط والأحكام", + "service_status": "حالة الخدمة", + "generate_password": "إنشاء كلمة مرور", + "verification_code": "رمز التحقق", + "2fa_verify": "التحقق بخطوتين", + "2fa_enter_code": "أدخل الرمز المكون من 6 أرقام من تطبيق المصادقة الخاص بك", + "2fa_placeholder": "مثال: 123456", + "verify": "تحقق", + "verifying_authentication": "جاري التحقق من المصادقة...", + "2fa_verification_error": "رمز التحقق غير صحيح. يرجى المحاولة مرة أخرى.", + "2fa_verification_success": "تم التحقق بخطوتين بنجاح", + "verifying": "جاري التحقق بخطوتين...", + "authentication_error": "خطأ في المصادقة", + "2fa_verified": "تم التحقق بخطوتين", + "redirecting_to_dashboard": "جاري التوجيه إلى لوحة التحكم...", + "setup_encryption_keys": "إعداد مفاتيح التشفير", + "encryption_setup_description": "إنشاء كلمة مرور رئيسية لتشفير بياناتك. ستُستخدم هذه كلمة المرور لحماية مفتاحك الخاص.", + "encryption_password_warning": "تذكر هذه كلمة المرور بعناية! إذا نسيتها، ستفقد الوصول إلى جميع بياناتك المشفرة.", + "encryption_password": "كلمة المرور الرئيسية", + "enter_strong_password": "أدخل كلمة مرور قوية", + "confirm_encryption_password": "تأكيد كلمة المرور الرئيسية", + "confirm_password": "تأكيد كلمة المرور", + "encryption_password_requirements": "يجب أن تكون كلمة المرور 8 أحرف على الأقل وتتضمن أحرف وأرقام ورموز خاصة لأقصى أمان.", + "generating_keys": "جاري إنشاء المفاتيح...", + "setup_encryption": "إعداد التشفير", + "password_too_short": "كلمة المرور قصيرة جداً (8 أحرف على الأقل)", + "passwords_dont_match": "كلمات المرور غير متطابقة", + "encryption_setup_success": "تم إعداد التشفير بنجاح", + "encryption_keys_created": "تم إنشاء مفاتيح التشفير بنجاح", + "encryption_setup_failed": "فشل في إعداد مفاتيح التشفير", + "checking_encryption_keys": "جاري فحص مفاتيح التشفير...", + "encryption_key_check_failed": "فشل فحص مفتاح التشفير", + "encryption_key_check_failed_desc": "لم نتمكن من التحقق من مفاتيح التشفير. قد يكون هذا بسبب مشكلة في الشبكة أو مفاتيح مفقودة.", + "encryption_security_error": "خطأ أمني في التشفير. يرجى تسجيل الدخول مرة أخرى.", + "login_successful": "تم تسجيل الدخول بنجاح", + "2fa_setup_required": "مطلوب إعداد التحقق بخطوتين", + "scan_qr_with_authenticator": "امسح رمز QR باستخدام تطبيق المصادقة", + "unknown_error": "حدث خطأ غير معروف", + "auth_network_error": "خطأ في الشبكة أثناء المصادقة", + "auth_process_failed": "فشل عملية المصادقة", + "2fa_invalid_code": "رمز التحقق غير صحيح", + "2fa_verification_failed": "فشل التحقق: {message}", + "copied_to_clipboard": "تم النسخ إلى الحافظة", + "paste_in_authenticator": "الصق هذا الرمز في تطبيق المصادقة", + "2fa_setup": "إعداد المصادقة بخطوتين", + "2fa_scan": "امسح رمز QR باستخدام تطبيق المصادقة", + "2fa_scan_difficulty": "هل تواجه مشكلة في المسح؟ يمكنك نسخ مفتاح الإعداد أدناه:", + "copy_setup_key": "نسخ مفتاح الإعداد", + "unlock_encryption": "فتح التشفير", + "enter_encryption_password": "أدخل كلمة المرور الرئيسية", + "enter_encryption_password_desc": "أدخل كلمة المرور الرئيسية لفك تشفير مفتاحك الخاص.", + "unlocking": "جاري الفتح...", + "unlock": "فتح", + "encryption_unlocked": "تم فتح التشفير", + "encryption_key_decrypted": "تم فك تشفير مفتاح التشفير بنجاح", + "incorrect_encryption_password": "كلمة المرور الرئيسية غير صحيحة. يرجى المحاولة مرة أخرى.", + "multi_device_login": "تسجيل الدخول من جهاز جديد", + "multi_device_desc": "تسجل الدخول من جهاز جديد. يرجى إدخال كلمة المرور الرئيسية للوصول إلى بياناتك المشفرة.", + "authenticating": "جاري المصادقة...", + "download_password_title": "كلمة مرور تشفير Zecrypt:", + "download_password_important": "مهم: احتفظ بكلمة المرور هذه بأمان. ستحتاجها للوصول إلى بياناتك المشفرة.", + "download_filename": "zecrypt-encryption-password.txt" + }, + "features": { + "trial_title": "مدير التخزين الآمن الخاص بك", + "unlimited_devices": "أجهزة غير محدودة — مجاني تماماً إلى الأبد", + "shared_vaults": "مشاركة بيانات الاعتماد بسهولة مع أعضاء الفريق - (قريباً)", + "advanced_security": "تشفير عسكري المستوى (AES-GCM) لأمان صلب", + "security_alerts": "تنبيهات فورية للانتهاكات لتكون دائماً خطوة متقدمة - (قريباً)", + "multi_platform": "متاح على Mac و iOS و Windows و Android و Chrome OS و Linux - (قريباً)", + "organization": "التنظيم", + "organization_desc": "تنظيم بيانات الاعتماد في مشاريع لإدارة أفضل ووصول سريع.", + "secure_storage": "تخزين آمن", + "secure_storage_desc": "تخزين جميع كلمات المرور والبيانات الحساسة بتشفير عسكري المستوى.", + "secure_password_manager": "تخزين سحابي مشفر من طرف إلى طرف" + }, + "actions": { + "add": "إضافة", + "edit": "تحرير", + "delete": "حذف", + "cancel": "إلغاء", + "close": "إغلاق", + "save": "حفظ", + "saving": "جاري الحفظ...", + "deleting": "جاري الحذف...", + "submit": "إرسال", + "ok": "موافق", + "yes": "نعم", + "no": "لا", + "confirm": "تأكيد", + "confirm_delete": "تأكيد الحذف", + "continue": "متابعة", + "back": "رجوع", + "next": "التالي", + "search": "بحث", + "filter": "تصفية", + "clear": "مسح", + "reset": "إعادة تعيين", + "refresh": "تحديث", + "upload": "رفع", + "download": "تحميل", + "copy": "نسخ", + "paste": "لصق", + "cut": "قص", + "undo": "تراجع", + "redo": "إعادة", + "select_all": "تحديد الكل", + "select_none": "عدم تحديد شيء", + "create": "إنشاء", + "update": "تحديث", + "remove": "إزالة", + "error": "خطأ", + "view": "عرض" + }, + "dashboard": { + "project_limit_reached": "تم الوصول إلى حد المشروع", + "alpha_one_project_limit": "نحن نعمل حالياً على ألفا، يُسمح بمشروع واحد فقط لكل مساحة عمل.", + "select_project": "اختر المشاريع", + "manage_projects": "إدارة المشاريع", + "select_or_create_project": "اختر أو أنشئ مشروعاً", + "welcome_back": "مرحباً بعودتك إلى مساحة العمل الآمنة", + "total_passwords": "إجمالي كلمات المرور", + "total_accounts": "إجمالي الحسابات", + "total_folders": "إجمالي المجلدات", + "recently_added_accounts": "الحسابات المضافة مؤخراً", + "from_last_month": "من الشهر الماضي", + "create_project_details": "إنشاء تفاصيل المشروع", + "recent_activity": "النشاط الأخير", + "keyboard_shortcuts": "اختصارات لوحة المفاتيح", + "generate_password": "إنشاء كلمة مرور", + "favourites": "المفضلة", + "no_description": "لا يوجد وصف", + "notes": "الملاحظات", + "confirm_delete_project": "تأكيد حذف المشروع", + "create_project": "إنشاء مشروع", + "set_as_default_project": "تعيين كمشروع افتراضي", + "project_name": "اسم المشروع", + "describe_project_purpose": "وصف الغرض من المشروع", + "free_plan_limit": "حد الخطة المجانية", + "save_changes": "حفظ التغييرات", + "no_project_availbale": "لا يوجد مشروع متاح", + "cancel": "إلغاء", + "delete": "حذف", + "add_favorite_tag": "إضافة علامة مفضلة", + "remove_tag": "إزالة {tag}", + "dashboard": "لوحة التحكم", + "show_more": "عرض المزيد", + "show_less": "عرض أقل", + "overview": "نظرة عامة", + "project": "المشروع", + "notifications": "الإشعارات", + "settings": "الإعدادات", + "logout": "تسجيل الخروج", + "current_project": "المشروع الحالي", + "default": "افتراضي", + "no_project_selected": "لم يتم اختيار مشروع", + "quick_stats": "إحصائيات سريعة", + "workspaces": "مساحات العمل", + "projects": "المشاريع", + "available_features": "الميزات المتاحة", + "team_members": "أعضاء الفريق", + "total_projects": "إجمالي المشاريع", + "total_workspaces": "إجمالي مساحات العمل", + "total_team_members": "إجمالي أعضاء الفريق", + "total_records": "إجمالي السجلات", + "my_account": "حسابي", + "change_language": "تغيير اللغة", + "language": "اللغة", + "toggle_theme": "تبديل المظهر", + "wallet_passphrases": "عبارات مرور المحفظة", + "wi_fi": "Wi-Fi", + "env": "متغيرات البيئة", + "env_variables": "متغيرات البيئة", + "search": "بحث", + "create_new_project": "إنشاء مشروع جديد", + "add_new_project_description": "إضافة مشروع جديد لتنظيم بياناتك الآمنة", + "description": "الوصف", + "optional": "اختياري", + "project_color": "لون المشروع", + "enabled_modules": "الوحدات المفعلة", + "edit_project": "تحرير المشروع", + "update_project_details": "تحديث تفاصيل مشروعك", + "my_project": "مشروعي", + "creating": "جاري الإنشاء...", + "delete_project": "حذف المشروع", + "identity_coming_soon": "وحدة الهوية قيد التطوير.", + "cards_coming_soon": "وحدة البطاقات قيد التطوير.", + "software_licenses_coming_soon": "وحدة تراخيص البرمجيات قيد التطوير.", + "wifi_coming_soon": "وحدة Wi-Fi قيد التطوير.", + "profile": "الملف الشخصي", + "search_modules": "البحث في الوحدات...", + "no_modules_found": "لم يتم العثور على وحدات", + "general": "عام", + "security_access": "الأمان والوصول", + "business_finance": "الأعمال والمالية", + "licenses_services": "التراخيص والخدمات", + "accounts": "الحسابات", + "identity": "الهوية", + "email": "البريد الإلكتروني", + "wifi": "WiFi", + "api_keys": "مفاتيح API", + "ssh_keys": "مفاتيح SSH", + "cards": "البطاقات", + "software_licenses": "تراخيص البرمجيات", + "recent_accounts": "الحسابات الأخيرة", + "no_recent_accounts": "لا توجد حسابات حديثة", + "last_24_hours": "آخر 24 ساعة", + "no_accounts_create": "لم يتم العثور على حسابات. أنشئ واحداً للبدء.", + "adjust_search_filter": "حاول تعديل معايير البحث أو التصفية", + "no_recent_activity": "لا يوجد نشاط حديث", + "no_projects_available": "لا توجد مشاريع متاحة", + "no_workspace_selected": "لم يتم اختيار مساحة عمل" + }, + "activity": { + "password_updated": "تم تحديث كلمة المرور", + "note_created": "تم إنشاء ملاحظة آمنة", + "you_create_login": "أنشأت تسجيل دخول جديد", + "you_create_api_key": "أنشأت مفتاح API جديد", + "you_update_login": "حدثت تسجيل الدخول", + "you_update_api_key": "حدثت مفتاح API", + "you_delete_login": "حذفت تسجيل الدخول", + "you_delete_api_key": "حذفت مفتاح API", + "create_login": "تم إنشاء تسجيل دخول جديد", + "create_api_key": "تم إنشاء مفتاح API جديد", + "update_login": "تم تحديث تسجيل الدخول", + "update_api_key": "تم تحديث مفتاح API", + "delete_login": "تم حذف تسجيل الدخول", + "delete_api_key": "تم حذف مفتاح API" + }, + "time": { + "today": "اليوم", + "yesterday": "أمس", + "last_7_days": "آخر 7 أيام", + "last_30_days": "آخر 30 يوماً", + "all_time": "كل الأوقات", + "just_now": "الآن", + "hours_ago": "منذ {hours} ساعة", + "days_ago": "منذ {days} يوم" + }, + "password_generator": { + "title": "مولد كلمات المرور", + "strong": "قوي", + "memorable": "سهل التذكر", + "pin": "PIN", + "passphrase": "عبارة المرور", + "chars": "أحرف", + "digits": "أرقام", + "generate_new": "إنشاء كلمة مرور جديدة", + "generate_password": "إنشاء كلمة مرور", + "click_generate_button": "انقر على زر الإنشاء لإنشاء كلمة مرور جديدة", + "copy": "نسخ كلمة المرور", + "copied": "تم النسخ!", + "save": "حفظ كلمة المرور", + "close": "إغلاق", + "copy_password": "نسخ كلمة المرور", + "password_length": "طول كلمة المرور", + "options": "الخيارات", + "advanced": "متقدم", + "advanced_options": "خيارات متقدمة", + "history": "السجل", + "uppercase": "أحرف كبيرة (A-Z)", + "lowercase": "أحرف صغيرة (a-z)", + "numbers": "أرقام (0-9)", + "symbols": "رموز (!@#$%^&*)", + "password_analysis": "تحليل كلمة المرور", + "strong_enough": "قوي بما يكفي لمعظم المواقع", + "not_strong_enough": "ليس قوياً بما يكفي للحسابات الحساسة", + "good_length": "طول جيد", + "consider_longer": "فكر في كلمة مرور أطول", + "using_all_types": "استخدام جميع أنواع الأحرف", + "consider_more_types": "فكر في استخدام المزيد من أنواع الأحرف", + "all_characters": "جميع الأحرف", + "easy_to_read": "سهل القراءة (بدون أحرف متشابهة مثل 1، l، I، 0، O)", + "easy_to_say": "سهل النطق (بدون أرقام أو رموز خاصة)", + "password_tips": "نصائح كلمة المرور", + "tip_min_length": "• استخدم 14 حرفاً على الأقل لأمان عالي", + "tip_mix": "• أدرج مزيجاً من الأحرف والأرقام والرموز", + "tip_no_personal": "• تجنب استخدام المعلومات الشخصية", + "tip_unique": "• استخدم كلمة مرور مختلفة لكل حساب", + "tip_manager": "• فكر في استخدام مدير كلمات المرور لتخزين كلمات المرور", + "recently_generated": "تم إنشاؤها مؤخراً", + "clear_history": "مسح السجل", + "no_history": "لا يوجد سجل كلمات مرور بعد", + "history_hint": "ستظهر كلمات المرور المُنشأة هنا", + "weak": "ضعيف", + "fair": "مقبول", + "good": "جيد", + "strong_strength": "قوي", + "strength": "{strength}% قوة", + "current_session": "الجلسة الحالية", + "create_secure_passwords": "إنشاء كلمات مرور آمنة لحساباتك" + }, + "user_settings": { + "title": "إعدادات المستخدم", + "description": "إدارة إعدادات الحساب والتفضيلات", + "profile": "الملف الشخصي", + "security": "الأمان", + "notifications": "الإشعارات", + "integrations": "التكاملات", + "billing": "الفوترة", + "logout": "تسجيل الخروج", + "login_history": "سجل تسجيل الدخول", + "audit_log": "سجل التدقيق", + "profile_title": "معلومات الملف الشخصي والحساب", + "profile_description": "إدارة تفاصيلك الشخصية والحساب", + "language": "اللغة", + "select_language": "اختر اللغة", + "email_address": "عنوان البريد الإلكتروني", + "full_name": "الاسم الكامل", + "previous": "السابق", + "next": "التالي", + "page": "صفحة", + "of": "من", + "saving": "جاري الحفظ...", + "save_changes": "حفظ التغييرات", + "profile_picture": "صورة الملف الشخصي", + "loading_history": "جاري تحميل سجل تسجيل الدخول...", + "no_history_available": "لا يوجد سجل تسجيل دخول متاح", + "try_again": "حاول مرة أخرى", + "device_browser": "الجهاز والمتصفح", + "ip_address": "عنوان IP", + "time": "الوقت" + }, + "accounts": { + "no_accounts": "لا توجد حسابات", + "no_matching_accounts": "لم يتم العثور على حسابات مطابقة", + "loading_accounts": "جاري تحميل الحسابات...", + "error_fetching_accounts": "فشل في جلب الحسابات", + "account_deleted_successfully": "تم حذف الحساب بنجاح", + "error_deleting_account": "فشل في حذف الحساب", + "account_updated_successfully": "تم تحديث الحساب بنجاح", + "all_fields_required": "يرجى ملء جميع الحقول المطلوبة", + "account_already_exists": "حساب بهذا الاسم موجود بالفعل", + "validation_error": "بيانات إدخال غير صحيحة. تحقق من إدخالاتك", + "failed_to_add_account": "فشل في إضافة الحساب. يرجى المحاولة مرة أخرى", + "missing_auth_or_selection": "يرجى تسجيل الدخول أو اختيار مساحة عمل ومشروع", + "please_log_in": "يرجى تسجيل الدخول للمتابعة", + "server_error": "حدث خطأ في الخادم. يرجى المحاولة لاحقاً", + "adding": "جاري الإضافة...", + "ascending": "تصاعدي", + "descending": "تنازلي", + "add": "إضافة", + "cancel": "إلغاء", + "account_name": "اسم الحساب", + "enter_account_name": "أدخل اسم الحساب", + "username": "اسم المستخدم", + "enter_username": "أدخل اسم المستخدم", + "add_account": "إضافة حساب", + "add_new_account": "إضافة حساب جديد", + "password": "كلمة المرور", + "enter_password": "أدخل كلمة المرور", + "website": "الموقع", + "enter_website": "أدخل رابط الموقع", + "enter_website_url": "أدخل رابط الموقع", + "notes": "الملاحظات", + "enter_notes": "أدخل الملاحظات", + "tags": "العلامات", + "enter_tags_comma_separated": "أدخل العلامات (مفصولة بفواصل)", + "accounts": "الحسابات", + "manage_your_saved_accounts_and_passwords": "إدارة حساباتك وكلمات المرور المحفوظة", + "encryption_key_not_found": "مفتاح تشفير المشروع غير موجود. لا يمكن حفظ الحساب.", + "project_not_found": "المشروع غير موجود. يرجى اختيار مشروع صحيح.", + "encryption_failed": "فشل في تشفير بيانات الحساب. يرجى المحاولة مرة أخرى.", + "decryption_failed": "فشل في فك تشفير بيانات الحساب. قد يكون هذا بسبب مفتاح تشفير مفقود أو غير صحيح.", + "please_fill_all_required_fields": "يرجى ملء جميع الحقول المطلوبة", + "select_project": "اختر المشروع", + "no_project_selected": "لم يتم اختيار مشروع", + "please_select_a_project": "يرجى اختيار مشروع لرؤية الحسابات", + "loading": "جاري التحميل...", + "error": "خطأ", + "try_again": "حاول مرة أخرى", + "no_accounts_found": "لم يتم العثور على حسابات", + "no_accounts_exist": "لا توجد حسابات لهذا المشروع", + "adjust_search_or_filter": "حاول تعديل البحث أو التصفية للعثور على الحسابات", + "generate_password": "إنشاء كلمة مرور", + "account": "الحساب", + "category": "الفئة", + "last_modified": "آخر تعديل", + "date_created": "تاريخ الإنشاء", + "actions": "الإجراءات", + "copy_username": "نسخ اسم المستخدم", + "copied": "تم النسخ", + "copy_password": "نسخ كلمة المرور", + "show_password": "إظهار كلمة المرور", + "hide_password": "إخفاء كلمة المرور", + "edit": "تحرير", + "delete": "حذف", + "confirm_deletion": "تأكيد الحذف", + "confirm_delete_account": "هل أنت متأكد من أنك تريد حذف هذا الحساب؟", + "confirm_delete_account_description": "لا يمكن التراجع عن هذا الإجراء وسيؤدي إلى حذف بيانات اعتماد الحساب نهائياً.", + "deleting": "جاري الحذف...", + "showing": "عرض", + "of": "من", + "rows_per_page": "صفوف في الصفحة", + "clear_filters": "مسح المرشحات", + "accounts_not_found": "لم يتم العثور على حسابات", + "account_not_found": "لم يتم العثور على الحساب", + "failed_to_delete_account": "فشل في حذف الحساب", + "failed_to_update_account": "فشل في تحديث الحساب", + "updating": "جاري التحديث...", + "update": "تحديث", + "view_details": "عرض التفاصيل", + "filter_warning": "قد تؤثر المرشحات على دقة الصفحات حيث أن التصفية من جانب الخادم غير مدعومة.", + "search": "بحث", + "search_accounts": "البحث في الحسابات", + "all_accounts": "جميع الحسابات", + "personal": "شخصي", + "work": "عمل", + "finance": "مالية", + "clear": "مسح", + "clear_sort": "مسح", + "favorite": "مفضل", + "no_accounts_create": "لم يتم العثور على حسابات. أنشئ واحداً للبدء.", + "adjust_search_filter": "حاول تعديل معايير البحث أو التصفية", + "error_loading_workspace": "فشل في تحميل بيانات مساحة العمل", + "focus_search": "تركيز البحث", + "switch_project": "تبديل المشروع", + "add_a_tag": "إضافة علامة", + "edit_account": "تحرير الحساب", + "update_account": "تحديث الحساب", + "filter_by_tag": "تصفية حسب العلامة", + "confirm_delete": "تأكيد الحذف", + "delete_account_confirmation": "هل أنت متأكد من أنك تريد حذف هذا الحساب؟ لا يمكن التراجع عن هذا الإجراء.", + "sort": "ترتيب", + "sort_options": "خيارات الترتيب", + "sort_by_name": "ترتيب حسب الاسم", + "sort_by_created": "ترتيب حسب تاريخ الإنشاء", + "sort_by_updated": "ترتيب حسب تاريخ التحديث" + }, + "api_keys": { + "title": "مفاتيح API", + "description": "إدارة مفاتيح API للوصول البرمجي إلى خدمات Zecrypt.", + "edit_api_key": "تحرير مفتاح API", + "search": "بحث", + "name": "الاسم", + "key": "المفتاح", + "api_key": "مفتاح API", + "enter_api_key": "أدخل مفتاح API", + "type": "النوع", + "created": "تاريخ الإنشاء", + "expires": "ينتهي", + "status": "الحالة", + "actions": "الإجراءات", + "clear": "مسح", + "sort": "ترتيب", + "last_used": "البيئة", + "sort_options": "خيارات الترتيب", + "sort_by_name": "ترتيب حسب الاسم", + "sort_by_created": "ترتيب حسب تاريخ الإنشاء", + "sort_by_updated": "ترتيب حسب تاريخ التحديث", + "ascending": "تصاعدي", + "descending": "تنازلي", + "clear_sort": "مسح الترتيب", + "clear_filters": "مسح المرشحات", + "create_new_api_key": "إنشاء مفتاح API جديد", + "enter_details_for_new_api_key": "أدخل تفاصيل مفتاح API الجديد", + "api_key_name": "اسم مفتاح API", + "enter_api_key_name": "مثال: خادم الإنتاج", + "key_type": "نوع المفتاح", + "key_preview": "معاينة المفتاح", + "key_preview_description": "سيتم إنشاء المفتاح الفعلي عند النقر على \"إنشاء مفتاح API\"", + "create_api_key": "إنشاء مفتاح API", + "cancel": "إلغاء", + "rows_per_page": "صفوف في الصفحة", + "add_api_key": "إضافة مفتاح API", + "no_api_keys_found": "لم يتم العثور على مفاتيح API", + "try_adjusting_search_or_filter": "حاول تعديل معايير البحث أو التصفية", + "error_fetching_api_keys": "فشل في جلب مفاتيح API", + "failed_to_fetch_api_keys": "فشل في تحميل مفاتيح API. يرجى المحاولة مرة أخرى.", + "confirm_delete_api_key": "هل أنت متأكد من أنك تريد حذف مفتاح API هذا؟", + "api_key_deleted_successfully": "تم حذف مفتاح API بنجاح", + "api_key_deleted_description": "تم حذف مفتاح API بنجاح.", + "error_deleting_api_key": "فشل في حذف مفتاح API", + "failed_to_delete_api_key": "فشل في حذف مفتاح API. يرجى المحاولة مرة أخرى.", + "api_key_updated_successfully": "تم تحديث مفتاح API", + "api_key_updated_description": "تم تحديث مفتاح API بنجاح.", + "api_key_added_successfully": "تم إضافة مفتاح API", + "api_key_added_description": "تم إضافة مفتاح API بنجاح.", + "failed_to_update_api_key": "فشل في تحديث مفتاح API. يرجى المحاولة مرة أخرى.", + "manage_your_api_keys": "إدارة مفاتيح API", + "showing": "عرض", + "of": "من", + "add_new_api_key": "إضافة مفتاح API جديد", + "add_new_api_key_description": "أدخل تفاصيل مفتاح API أدناه", + "api_key_already_exists": "مفتاح API بهذا الاسم موجود بالفعل.", + "error_adding_api_key": "فشل في إضافة مفتاح API بسبب خطأ في الخادم.", + "failed_to_add_api_key": "فشل في إضافة مفتاح API. يرجى المحاولة مرة أخرى.", + "error_updating_api_key": "فشل في تحديث مفتاح API بسبب خطأ في الخادم.", + "invalid_input_data": "بيانات إدخال غير صحيحة. تحقق من إدخالاتك.", + "network_error": "خطأ في الشبكة. تحقق من اتصالك وحاول مرة أخرى.", + "no_api_keys_for_env": "لم يتم العثور على مفاتيح API للبيئة '{env}'. جرب بيئة أخرى أو امسح المرشحات.", + "no_api_keys_match_search": "لا توجد مفاتيح API تطابق البحث '{search}'. حاول تعديل بحثك.", + "adjust_search_filter": "حاول تعديل معايير البحث أو التصفية.", + "copied": "تم النسخ", + "api_key_copied": "تم نسخ مفتاح API إلى الحافظة.", + "copy_failed": "فشل النسخ", + "failed_to_copy_api_key": "فشل في نسخ مفتاح API إلى الحافظة.", + "show_key": "إظهار المفتاح", + "hide_key": "إخفاء المفتاح", + "copy_key": "نسخ المفتاح", + "edit": "تحرير", + "delete": "حذف", + "decryption_error": "خطأ فك التشفير - يرجى التحرير لتحديث التنسيق", + "adding": "جاري الإضافة...", + "updating": "تحديث", + "no_project_selected": "لم يتم اختيار مشروع. يرجى اختيار مشروع للمتابعة.", + "please_fill_all_required_fields": "يرجى ملء جميع الحقول المطلوبة", + "loading_api_keys": "جاري تحميل مفاتيح API...", + "api_keys": "مفاتيح API", + "env": "البيئة", + "tags": "العلامات", + "last_modified": "آخر تعديل", + "api_key_description": "وصف مفتاح API", + "enter_description": "أدخل وصفاً لمفتاح API هذا", + "environment": "البيئة", + "select_environment": "اختر البيئة", + "development": "التطوير", + "staging": "المرحلة التجريبية", + "production": "الإنتاج", + "testing": "الاختبار", + "local": "محلي", + "uat": "UAT", + "add": "إضافة", + "add_a_tag": "إضافة علامة", + "update_api_key": "تحديث مفتاح API", + "all_envs": "جميع البيئات", + "search_across_all_fields": "بحث", + "delete_api_key_confirmation": "هل أنت متأكد من أنك تريد حذف مفتاح API هذا؟ لا يمكن التراجع عن هذا الإجراء.", + "deleting": "جاري الحذف...", + "confirm_deletion": "تأكيد الحذف", + "edit_api_key_description": "تحديث تفاصيل مفتاح API", + "showing_results": "عرض {startIdx}-{endIdx} من {totalCount} نتيجة", + "notes": "الملاحظات", + "enter_notes": "أدخل الملاحظات", + "filter_by_env": "تصفية حسب البيئة", + "date_created": "تاريخ الإنشاء" + }, + "wallet_passphrases": { + "wallet_passphrases": "عبارات مرور المحفظة", + "securely_store_and_manage_your_wallet_recovery_phrases": "تخزين وإدارة عبارات استرداد المحفظة بأمان", + "add_passphrase": "إضافة عبارة المرور", + "enter_wallet_address": "أدخل عنوان المحفظة", + "copy_wallet_address": "نسخ عنوان المحفظة", + "wallet_address": "عنوان المحفظة", + "add_your_first_passphrase": "إضافة عبارة المرور الأولى", + "search_passphrases": "بحث", + "no_wallet_passphrases_found": "لم يتم العثور على عبارات مرور المحفظة", + "wallet_address_encryption_note": "عنوان المحفظة مشفر للأمان", + "no_passphrases_found": "لم يتم العثور على عبارات مرور", + "no_passphrases_found_message": "لم تقم بإضافة عبارات مرور المحفظة بعد. أضف واحدة للبدء.", + "no_passphrases_found_search": "لا توجد عبارات مرور تطابق معايير البحث.", + "loading_passphrases": "جاري تحميل عبارات المرور...", + "please_wait_while_we_fetch_your_secure_passphrases": "يرجى الانتظار بينما نجلب عبارات المرور الآمنة", + "error_loading_passphrases": "خطأ في تحميل عبارات المرور", + "failed_to_load_passphrases": "فشل في تحميل عبارات المرور. يرجى المحاولة مرة أخرى.", + "passphrase_added_successfully": "تم إضافة عبارة المرور بنجاح", + "passphrase_added_description": "تم تخزين عبارة مرور المحفظة بأمان.", + "passphrase_updated_successfully": "تم تحديث عبارة المرور بنجاح", + "passphrase_updated_description": "تم تحديث عبارة مرور المحفظة.", + "failed_to_add_passphrase": "فشل في إضافة عبارة المرور", + "failed_to_update_passphrase": "فشل في تحديث عبارة المرور", + "wallet_phrase_already_exists": "عبارة المرور أو الاسم موجود بالفعل في محفظتك", + "invalid_input_data": "بيانات إدخال غير صحيحة. تحقق من إدخالاتك.", + "no_project_selected": "لم يتم اختيار مشروع. يرجى اختيار مشروع.", + "passphrase_required": "عبارة المرور مطلوبة", + "passphrase_must_be_exactly_12_words": "يجب أن تحتوي عبارة المرور على 12 كلمة بالضبط (حالياً {count})", + "please_fill_all_required_fields": "يرجى ملء جميع الحقول المطلوبة", + "name": "الاسم", + "add": "إضافة", + "sort": "ترتيب", + "sort_by_name": "ترتيب حسب الاسم", + "sort_by_created": "ترتيب حسب تاريخ الإنشاء", + "sort_by_updated": "ترتيب حسب تاريخ التحديث", + "sort_options": "خيارات الترتيب", + "enter_passphrase_name": "أدخل اسم عبارة المرور", + "passphrase": "عبارة المرور", + "enter_wallet_recovery_phrase": "أدخل عبارة استرداد المحفظة", + "notes": "الملاحظات", + "enter_notes": "أدخل الملاحظات", + "wallet_type": "نوع المحفظة", + "select_wallet_type": "اختر نوع المحفظة", + "tags": "العلامات", + "add_a_tag": "إضافة علامة", + "save_passphrase": "حفظ عبارة المرور", + "update_passphrase": "تحديث عبارة المرور", + "passphrase_encryption_note": "ستتم تشفير عبارة المرور للأمان.", + "adding": "جاري الإضافة...", + "updating": "جاري التحديث...", + "search_across_all_fields": "بحث", + "filter_by_wallet_type": "تصفية حسب نوع المحفظة", + "all_wallet_types": "جميع أنواع المحافظ", + "clear_filters": "مسح المرشحات", + "last_modified": "آخر تعديل", + "actions": "الإجراءات", + "hide_passphrase": "إخفاء عبارة المرور", + "show_passphrase": "إظهار عبارة المرور", + "copy_passphrase": "نسخ عبارة المرور", + "edit": "تحرير", + "delete": "حذف", + "showing_results": "عرض {startIdx}-{endIdx} من {totalCount} نتيجة", + "confirm_deletion": "تأكيد الحذف", + "delete_passphrase_confirmation": "هل أنت متأكد من أنك تريد حذف عبارة مرور المحفظة هذه؟ لا يمكن التراجع عن هذا الإجراء.", + "deleting": "جاري الحذف...", + "add_new_passphrase_description": "أدخل تفاصيل عبارة مرور المحفظة أدناه", + "edit_passphrase": "تحرير عبارة المرور", + "edit_passphrase_description": "تحديث تفاصيل عبارة مرور المحفظة", + "copied": "تم النسخ", + "passphrase_copied": "تم نسخ عبارة المرور إلى الحافظة", + "copy_failed": "فشل النسخ", + "failed_to_copy_passphrase": "فشل في نسخ عبارة المرور إلى الحافظة", + "cancel": "إلغاء", + "date_created": "تاريخ الإنشاء" + }, + "identity": { + "title": "الهوية", + "description": "إدارة معلومات الهوية الشخصية بأمان", + "add_identity": "إضافة هوية", + "no_identities_found": "لم يتم العثور على هويات", + "no_identities_found_message": "لم تقم بإضافة معلومات الهوية بعد.", + "add_your_first_identity": "إضافة هويتك الأولى", + "loading_identities": "جاري تحميل الهويات...", + "search_identities": "بحث", + "full_name": "الاسم الكامل", + "country": "البلد", + "enter_full_name": "أدخل الاسم الكامل", + "date_of_birth": "تاريخ الميلاد", + "enter_date_of_birth": "أدخل تاريخ الميلاد (YYYY-MM-DD)", + "address": "العنوان", + "enter_address": "أدخل العنوان", + "phone_number": "رقم الهاتف", + "enter_phone_number": "أدخل رقم الهاتف", + "identity_number": "رقم الهوية", + "enter_identity_number": "أدخل رقم الهوية (مثل جواز السفر، الهوية)", + "tags": "العلامات", + "enter_tags_comma_separated": "أدخل العلامات (مفصولة بفواصل)", + "last_modified": "آخر تعديل", + "actions": "الإجراءات", + "edit": "تحرير", + "delete": "حذف", + "copy_field": "نسخ {field}", + "copied": "تم النسخ", + "identity_deleted_successfully": "تم حذف الهوية بنجاح", + "error_deleting_identity": "فشل في حذف الهوية", + "identity_updated_successfully": "تم تحديث الهوية بنجاح", + "failed_to_add_identity": "فشل في إضافة الهوية", + "all_fields_required": "يرجى ملء جميع الحقول المطلوبة", + "validation_error": "بيانات إدخال غير صحيحة. تحقق من إدخالاتك", + "server_error": "حدث خطأ في الخادم. يرجى المحاولة لاحقاً", + "identities": "الهويات", + "filter_by_tag": "تصفية حسب العلامة", + "all_tags": "جميع العلامات", + "search": "بحث", + "clear_filters": "مسح المرشحات", + "name": "الاسم", + "contact": "جهة الاتصال", + "passport": "جواز السفر", + "national_id": "الهوية الوطنية", + "add_new_identity": "إضافة هوية جديدة", + "add_new_identity_description": "أدخل تفاصيل الهوية أدناه", + "first_name": "الاسم الأول", + "last_name": "الاسم الأخير", + "email": "البريد الإلكتروني", + "phone": "الهاتف", + "passport_number": "رقم جواز السفر", + "cancel": "إلغاء", + "save": "حفظ", + "edit_identity": "تحرير الهوية", + "edit_identity_description": "تحديث تفاصيل الهوية", + "update": "تحديث", + "identity_deleted": "تم حذف الهوية", + "identity_deleted_description": "تم حذف الهوية بنجاح", + "field_copied": "تم نسخ الحقل إلى الحافظة", + "copy_failed": "فشل النسخ", + "failed_to_copy_field": "فشل في نسخ الحقل إلى الحافظة", + "hide_sensitive_data": "إخفاء البيانات الحساسة", + "show_sensitive_data": "إظهار البيانات الحساسة", + "showing_results": "عرض {startIdx}-{endIdx} من {totalCount} نتيجة", + "page_of": "صفحة {currentPage} من {totalPages}", + "copy_passport": "نسخ رقم جواز السفر", + "copy_national_id": "نسخ الهوية الوطنية", + "identity_added_successfully": "تم إضافة الهوية بنجاح", + "error_adding_identity": "خطأ في إضافة الهوية", + "error_updating_identity": "خطأ في تحديث الهوية", + "missing_workspace_project": "مساحة عمل أو مشروع مفقود", + "first_name_required": "الاسم الأول مطلوب", + "last_name_required": "الاسم الأخير مطلوب", + "invalid_email": "تنسيق بريد إلكتروني غير صحيح", + "invalid_date_format": "تنسيق تاريخ غير صحيح (YYYY-MM-DD)", + "title_required": "العنوان مطلوب", + "title_placeholder": "الهوية الشخصية", + "first_name_placeholder": "أحمد", + "last_name_placeholder": "محمد", + "email_placeholder": "ahmed.mohamed@example.com", + "phone_placeholder": "+966 50 123 4567", + "address_placeholder": "123 شارع الملك فهد، الرياض، 12345", + "passport_placeholder": "A12345678", + "national_id_placeholder": "1234567890123", + "notes": "الملاحظات", + "notes_placeholder": "معلومات إضافية", + "tags_placeholder": "إضافة علامات", + "adding": "جاري الإضافة...", + "updating": "جاري التحديث...", + "deleting": "جاري الحذف...", + "add": "إضافة", + "confirm_deletion": "تأكيد الحذف", + "delete_identity_confirmation": "هل أنت متأكد من أنك تريد حذف هذه الهوية؟ لا يمكن التراجع عن هذا الإجراء.", + "error_fetching_identities": "خطأ في جلب الهويات", + "national_id_required": "الهوية الوطنية مطلوبة", + "identity_type": "نوع الهوية", + "sort": "ترتيب", + "sort_options": "خيارات الترتيب", + "sort_by_name": "ترتيب حسب الاسم", + "sort_by_created": "ترتيب حسب تاريخ الإنشاء", + "sort_by_updated": "ترتيب حسب تاريخ التحديث", + "ascending": "تصاعدي", + "descending": "تنازلي", + "clear_sort": "مسح الترتيب", + "search_across_all_fields": "بحث", + "searches_all_fields": "البحث في الاسم، النوع، جهة الاتصال، العنوان، الهوية والعلامات", + "encryption_key_not_found": "مفتاح تشفير المشروع غير موجود. لا يمكن حفظ الهوية.", + "project_not_found": "المشروع غير موجود. يرجى اختيار مشروع صحيح.", + "encryption_failed": "فشل في تشفير بيانات الهوية. يرجى المحاولة مرة أخرى.", + "decryption_failed": "فشل في فك تشفير بيانات الهوية. قد يكون هذا بسبب مفتاح تشفير مفقود أو غير صحيح.", + "manage_your_personal_identities": "إدارة معلومات الهوية الشخصية بأمان", + "date_created": "تاريخ الإنشاء" + }, + "cards": { + "title": "البطاقات", + "description": "إدارة معلومات بطاقات الائتمان والخصم بأمان", + "add_card": "إضافة بطاقة", + "no_cards_found": "لم يتم العثور على بطاقات", + "no_cards_found_message": "لم تقم بإضافة معلومات البطاقة بعد.", + "add_your_first_card": "إضافة بطاقتك الأولى", + "loading_cards": "جاري تحميل البطاقات...", + "search_cards": "بحث", + "card_name": "اسم البطاقة", + "name": "الاسم", + "enter_card_name": "أدخل اسم البطاقة (مثل Visa تنتهي بـ 1234)", + "card_number": "رقم البطاقة", + "enter_card_number": "أدخل رقم البطاقة", + "expiry_date": "تاريخ الانتهاء", + "enter_expiry_date": "أدخل تاريخ الانتهاء (MM/YY)", + "cvv": "CVV", + "enter_cvv": "أدخل CVV", + "cardholder_name": "حامل البطاقة", + "card_holder_name": "حامل البطاقة", + "enter_cardholder_name": "أدخل حامل البطاقة", + "tags": "العلامات", + "enter_tags_comma_separated": "أدخل العلامات (مفصولة بفواصل)", + "last_modified": "آخر تعديل", + "actions": "الإجراءات", + "edit": "تحرير", + "delete": "حذف", + "copy_field": "نسخ {field}", + "copied": "تم النسخ", + "card_deleted_successfully": "تم حذف البطاقة بنجاح", + "error_deleting_card": "فشل في حذف البطاقة", + "card_updated_successfully": "تم تحديث البطاقة بنجاح", + "card_added_successfully": "تم إضافة البطاقة بنجاح", + "failed_to_add_card": "فشل في إضافة البطاقة", + "all_fields_required": "يرجى ملء جميع الحقول المطلوبة", + "validation_error": "بيانات إدخال غير صحيحة. تحقق من إدخالاتك", + "server_error": "حدث خطأ في الخادم. يرجى المحاولة لاحقاً", + "credit_cards": "بطاقات الائتمان", + "filter_by_brand": "تصفية حسب العلامة التجارية", + "all_brands": "جميع العلامات التجارية", + "search": "بحث", + "clear_filters": "مسح المرشحات", + "card_holder": "حامل البطاقة", + "brand": "العلامة التجارية", + "expiry": "الانتهاء", + "add_new_card": "إضافة بطاقة جديدة", + "add_new_card_description": "أدخل تفاصيل بطاقة الائتمان أدناه", + "select_brand": "اختر العلامة التجارية", + "expiry_month": "شهر الانتهاء", + "expiry_year": "سنة الانتهاء", + "cancel": "إلغاء", + "save": "حفظ", + "edit_card": "تحرير البطاقة", + "edit_card_description": "تحديث تفاصيل بطاقة الائتمان", + "update": "تحديث", + "card_deleted": "تم حذف البطاقة", + "card_deleted_description": "تم حذف البطاقة بنجاح", + "field_copied": "تم نسخ الحقل إلى الحافظة", + "copy_failed": "فشل النسخ", + "failed_to_copy_field": "فشل في نسخ الحقل إلى الحافظة", + "hide_card_number": "إخفاء رقم البطاقة", + "show_card_number": "إظهار رقم البطاقة", + "copy_to_clipboard": "نسخ إلى الحافظة", + "showing_results": "عرض {startIdx}-{endIdx} من {totalCount} نتيجة", + "page_of": "صفحة {currentPage} من {totalPages}", + "title_placeholder": "بطاقة Visa الشخصية", + "card_holder_name_placeholder": "أحمد محمد", + "card_number_placeholder": "4111 1111 1111 1111", + "notes": "الملاحظات", + "notes_placeholder": "معلومات إضافية حول هذه البطاقة", + "tags_placeholder": "إضافة علامات", + "add": "إضافة", + "adding": "جاري الإضافة...", + "updating": "جاري التحديث...", + "deleting": "جاري الحذف...", + "confirm_delete": "تأكيد الحذف", + "delete_card_confirmation": "هل أنت متأكد من أنك تريد حذف هذه البطاقة؟ لا يمكن التراجع عن هذا الإجراء.", + "error_fetching_cards": "خطأ في جلب البطاقات", + "missing_workspace_project": "مساحة عمل أو مشروع مفقود", + "title_required": "العنوان مطلوب", + "card_holder_name_required": "حامل البطاقة مطلوب", + "card_number_required": "رقم البطاقة مطلوب", + "invalid_card_number": "رقم بطاقة غير صحيح", + "cvv_required": "CVV مطلوب", + "invalid_cvv": "CVV غير صحيح", + "brand_required": "العلامة التجارية مطلوبة", + "error_adding_card": "خطأ في إضافة البطاقة", + "error_updating_card": "خطأ في تحديث البطاقة", + "other": "أخرى", + "filter_by_tag": "تصفية حسب العلامة", + "all_tags": "جميع العلامات", + "sort": "ترتيب", + "sort_options": "خيارات الترتيب", + "sort_by_name": "ترتيب حسب الاسم", + "sort_by_created": "ترتيب حسب تاريخ الإنشاء", + "sort_by_updated": "ترتيب حسب تاريخ التحديث", + "ascending": "تصاعدي", + "descending": "تنازلي", + "clear_sort": "مسح الترتيب", + "search_across_all_fields": "بحث", + "searches_all_fields": "البحث في العنوان، حامل البطاقة، رقم البطاقة، العلامة التجارية والعلامات", + "missing_auth_or_selection": "مصادقة أو اختيار مفقود", + "encryption_key_not_found": "مفتاح تشفير المشروع غير موجود. لا يمكن حفظ البطاقة.", + "project_not_found": "المشروع غير موجود. يرجى اختيار مشروع صحيح.", + "encryption_failed": "فشل في تشفير بيانات البطاقة. يرجى المحاولة مرة أخرى.", + "decryption_failed": "فشل في فك تشفير بيانات البطاقة. قد يكون هذا بسبب مفتاح تشفير مفقود أو غير صحيح.", + "manage_your_cards": "إدارة معلومات بطاقات الائتمان والخصم بأمان", + "date_created": "تاريخ الإنشاء" + }, + "wifi": { + "wifi_networks": "شبكات Wi-Fi", + "manage_your_wifi_networks": "إدارة شبكات Wi-Fi بأمان.", + "add_wifi": "إضافة Wi-Fi", + "add_new_wifi": "إضافة Wi-Fi جديد", + "add_new_wifi_description": "أدخل تفاصيل شبكة Wi-Fi أدناه", + "edit_wifi": "تحرير Wi-Fi", + "edit_wifi_description": "تحديث تفاصيل شبكة Wi-Fi", + "search_wifi": "بحث", + "filter_by_security": "تصفية حسب الأمان", + "all_security_types": "جميع أنواع الأمان", + "none": "لا شيء", + "clear": "مسح", + "clear_filters": "مسح المرشحات", + "loading_wifi": "جاري تحميل شبكات Wi-Fi...", + "no_wifi_found": "لم يتم العثور على شبكات Wi-Fi", + "no_wifi_for_security_type": "لم يتم العثور على شبكات Wi-Fi لـ {security_type}", + "no_wifi_match_search": "لا توجد شبكات Wi-Fi تطابق بحثك '{search}'", + "name": "الاسم", + "loading_wifi_networks": "تحميل شبكات Wi-Fi", + "no_wifi_networks_found": "لم يتم العثور على شبكات Wi-Fi", + "add_wifi_network": "إضافة شبكة Wi-Fi", + "no_wifi_for_tag": "لم يتم العثور على شبكات WiFi بالعلامة {tag}", + "adjust_search_filter": "حاول تعديل البحث أو التصفية للعثور على ما تبحث عنه.", + "ssid": "SSID", + "security": "الأمان", + "security_type": "نوع الأمان", + "select_security_type": "اختر نوع الأمان", + "notes": "الملاحظات", + "enter_notes": "أدخل الملاحظات", + "password": "كلمة المرور", + "enter_password": "أدخل كلمة المرور", + "enter_new_password": "أدخل كلمة مرور جديدة (اترك فارغاً للاحتفاظ بالتغيير)", + "show_password": "إظهار كلمة المرور", + "hide_password": "إخفاء كلمة المرور", + "copy_password": "نسخ كلمة المرور", + "tags": "العلامات", + "add_a_tag": "إضافة علامة", + "add": "إضافة", + "last_modified": "آخر تعديل", + "date_created": "تاريخ الإنشاء", + "actions": "الإجراءات", + "showing": "عرض", + "of": "من", + "rows_per_page": "صفوف في الصفحة", + "edit": "تحرير", + "copy_ssid": "نسخ SSID", + "delete": "حذف", + "confirm_delete_wifi": "تأكيد حذف WiFi", + "delete_wifi_confirmation": "هل أنت متأكد من أنك تريد حذف شبكة WiFi هذه؟ لا يمكن التراجع عن هذا الإجراء.", + "wifi_deleted": "تم حذف WiFi", + "wifi_deleted_description": "تم حذف شبكة WiFi بنجاح.", + "failed_to_delete_wifi": "فشل في حذف شبكة WiFi", + "error_deleting_wifi": "خطأ في حذف WiFi", + "error_fetching_wifi_networks": "خطأ في جلب شبكات WiFi", + "failed_to_fetch_wifi_networks": "فشل في جلب شبكات WiFi", + "no_project_selected": "لم يتم اختيار مشروع. يرجى اختيار مشروع للمتابعة.", + "copied": "تم النسخ", + "field_copied": "تم نسخ الحقل إلى الحافظة", + "copy_failed": "فشل النسخ", + "failed_to_copy_field": "فشل في نسخ الحقل إلى الحافظة", + "show_qr_code": "إظهار رمز QR", + "wifi_qr_code": "رمز QR لـ WiFi", + "wifi_qr_code_description": "امسح رمز QR هذا للاتصال بشبكة WiFi.", + "qr_code_placeholder": "رمز QR", + "wifi_credentials": "بيانات اعتماد WiFi", + "close": "إغلاق", + "please_fill_all_required_fields": "يرجى ملء جميع الحقول المطلوبة", + "wifi_added_successfully": "تم إضافة WiFi بنجاح", + "wifi_added_description": "تم إضافة شبكة WiFi.", + "wifi_already_exists": "شبكة WiFi بهذا SSID موجودة بالفعل", + "failed_to_add_wifi": "فشل في إضافة شبكة WiFi", + "enter_ssid": "أدخل SSID", + "update_wifi": "تحديث WiFi", + "wifi_updated_successfully": "تم تحديث WiFi بنجاح", + "wifi_updated_description": "تم تحديث شبكة WiFi.", + "wifi_not_found": "شبكة WiFi غير موجودة", + "failed_to_update_wifi": "فشل في تحديث شبكة WiFi", + "invalid_input_data": "بيانات إدخال غير صحيحة", + "adding": "جاري الإضافة...", + "updating": "جاري التحديث...", + "deleting": "جاري الحذف...", + "cancel": "إلغاء", + "encryption_key_not_found": "مفتاح تشفير المشروع غير موجود. لا يمكن حفظ شبكة WiFi.", + "project_not_found": "المشروع غير موجود. يرجى اختيار مشروع صحيح.", + "encryption_failed": "فشل في تشفير بيانات WiFi. يرجى المحاولة مرة أخرى.", + "decryption_failed": "فشل في فك تشفير بيانات WiFi. قد يكون هذا بسبب مفتاح تشفير مفقود أو غير صحيح.", + "sort": "ترتيب", + "sort_options": "خيارات الترتيب", + "sort_by_name": "ترتيب حسب الاسم", + "sort_by_created": "ترتيب حسب تاريخ الإنشاء", + "sort_by_updated": "ترتيب حسب تاريخ التحديث", + "ascending": "تصاعدي", + "descending": "تنازلي", + "clear_sort": "مسح الترتيب", + "filter_by_tag": "تصفية حسب العلامة", + "all_tags": "جميع العلامات", + "scan_with_phone": "امسح رمز QR هذا بهاتفك للاتصال بالشبكة" + }, + "key_shortcuts": { + "title": "اختصارات لوحة المفاتيح" + }, + "notes": { + "add_new_note": "إضافة ملاحظة جديدة", + "note_title": "عنوان الملاحظة", + "note_content": "محتوى الملاحظة", + "tags": "العلامات", + "update_note": "تحديث الملاحظة", + "enter_note_title": "أدخل عنوان الملاحظة", + "add_a_tag": "إضافة علامة", + "add_note": "إضافة ملاحظة", + "cancel": "إلغاء" + }, + "emails": { + "email_accounts": "حسابات البريد الإلكتروني", + "add_email": "إضافة بريد إلكتروني", + "search_email_accounts": "بحث", + "per_page": "في الصفحة", + "clear_filters": "مسح المرشحات", + "title": "العنوان", + "email_address": "عنوان البريد الإلكتروني", + "servers": "الخوادم", + "username": "اسم المستخدم", + "password": "كلمة المرور", + "last_modified": "آخر تعديل", + "tags": "العلامات", + "loading_emails": "جاري تحميل البريد الإلكتروني", + "actions": "الإجراءات", + "loading": "جاري التحميل...", + "no_results_found": "لم يتم العثور على نتائج", + "no_email_accounts": "لا توجد حسابات بريد إلكتروني", + "copy": "نسخ", + "copy_to_clipboard": "نسخ إلى الحافظة", + "show_password": "إظهار كلمة المرور", + "hide_password": "إخفاء كلمة المرور", + "open_menu": "فتح القائمة", + "edit": "تحرير", + "delete": "حذف", + "deleted": "تم الحذف", + "showing_results": "عرض {start} إلى {end} من {total} نتيجة", + "confirm_delete": "تأكيد الحذف", + "delete_email_confirmation": "هل أنت متأكد من أنك تريد حذف حساب البريد الإلكتروني هذا؟ لا يمكن التراجع عن هذا الإجراء.", + "cancel": "إلغاء", + "deleting": "جاري الحذف...", + "copied": "تم النسخ", + "field_copied": "تم نسخ القيمة إلى الحافظة", + "copy_failed": "فشل النسخ", + "failed_to_copy_value": "فشل في نسخ القيمة إلى الحافظة", + "encryption_error": "خطأ التشفير", + "could_not_encrypt_data": "فشل في تشفير البيانات. يرجى المحاولة مرة أخرى.", + "success": "نجح", + "email_added_successfully": "تم إضافة حساب البريد الإلكتروني بنجاح", + "error_adding_email": "خطأ في إضافة حساب البريد الإلكتروني. يرجى المحاولة مرة أخرى.", + "email_deleted_successfully": "تم حذف بيانات اعتماد البريد الإلكتروني بنجاح", + "failed_to_delete_email": "فشل في حذف بيانات اعتماد البريد الإلكتروني", + "error_fetching_emails": "فشل في جلب البريد الإلكتروني", + "error": "خطأ", + "sort": "ترتيب", + "sort_options": "خيارات الترتيب", + "sort_by_name": "ترتيب حسب الاسم", + "sort_by_created": "ترتيب حسب تاريخ الإنشاء", + "sort_by_updated": "ترتيب حسب تاريخ التحديث", + "ascending": "تصاعدي", + "descending": "تنازلي", + "clear_sort": "مسح الترتيب", + "add_email_description": "إضافة حساب بريد إلكتروني جديد إلى خزانتك الآمنة", + "title_placeholder": "بريد العمل", + "email_address_placeholder": "your.email@example.com", + "imap_server": "خادم IMAP", + "imap_server_placeholder": "imap.example.com", + "smtp_server": "خادم SMTP", + "smtp_server_placeholder": "smtp.example.com", + "username_placeholder": "اختياري، افتراضي إلى عنوان البريد الإلكتروني", + "password_placeholder": "أدخل كلمة المرور", + "notes": "الملاحظات (اختياري)", + "notes_placeholder": "معلومات إضافية حول حساب البريد الإلكتروني هذا", + "add_tag": "إضافة علامة", + "remove": "إزالة", + "adding": "جاري الإضافة...", + "add": "إضافة", + "please_fill_all_required_fields": "يرجى ملء جميع الحقول المطلوبة", + "email_title_already_exists": "بريد إلكتروني بهذا العنوان موجود بالفعل", + "invalid_email_format": "تنسيق بريد إلكتروني غير صحيح", + "missing_required_data": "بيانات مشروع مطلوبة مفقودة", + "edit_email": "تحرير حساب البريد الإلكتروني", + "edit_email_description": "تحديث تفاصيل حساب البريد الإلكتروني", + "save_changes": "حفظ التغييرات", + "email_updated_successfully": "تم تحديث حساب البريد الإلكتروني بنجاح", + "error_updating_email": "خطأ في تحديث حساب البريد الإلكتروني. يرجى المحاولة مرة أخرى.", + "manage_your_email_accounts": "إدارة حسابات البريد الإلكتروني وإعدادات الخادم بأمان", + "date_created": "تاريخ الإنشاء", + "filter_by_tag": "تصفية حسب العلامة", + "all_tags": "جميع العلامات" + }, + "ssh_keys": { + "ssh_keys": "مفاتيح SSH", + "add_ssh_key": "إضافة مفتاح SSH", + "search_ssh_keys": "بحث", + "per_page": "في الصفحة", + "clear_filters": "مسح المرشحات", + "name": "الاسم", + "ssh_key": "مفتاح SSH", + "tags": "العلامات", + "loading_ssh_keys": "جاري تحميل مفاتيح SSH", + "actions": "الإجراءات", + "loading": "جاري التحميل...", + "no_results_found": "لم يتم العثور على نتائج", + "no_ssh_keys": "لا توجد مفاتيح SSH", + "hide_ssh_key": "إخفاء مفتاح SSH", + "show_ssh_key": "إظهار مفتاح SSH الكامل", + "copied": "تم النسخ", + "ssh_key_copied": "تم نسخ مفتاح SSH إلى الحافظة", + "copy_failed": "فشل النسخ", + "failed_to_copy_ssh_key": "فشل في نسخ مفتاح SSH إلى الحافظة", + "copy_to_clipboard": "نسخ إلى الحافظة", + "open_menu": "فتح القائمة", + "edit": "تحرير", + "delete": "حذف", + "fingerprint": "البصمة", + "last_modified": "آخر تعديل", + "deleted": "تم الحذف", + "showing_results": "عرض {start} إلى {end} من {total} نتيجة", + "confirm_delete": "تأكيد الحذف", + "delete_ssh_key_confirmation": "هل أنت متأكد من أنك تريد حذف مفتاح SSH هذا؟ لا يمكن التراجع عن هذا الإجراء.", + "cancel": "إلغاء", + "deleting": "جاري الحذف...", + "please_fill_all_required_fields": "يرجى ملء جميع الحقول المطلوبة", + "ssh_key_name_already_exists": "مفتاح SSH بهذا الاسم موجود بالفعل", + "missing_required_data": "بيانات مشروع مطلوبة مفقودة", + "encryption_error": "خطأ التشفير", + "could_not_encrypt_data": "فشل في تشفير البيانات. يرجى المحاولة مرة أخرى.", + "success": "نجح", + "ssh_key_added_successfully": "تم إضافة مفتاح SSH بنجاح", + "error_adding_ssh_key": "خطأ في إضافة مفتاح SSH. يرجى المحاولة مرة أخرى.", + "ssh_key_updated_successfully": "تم تحديث مفتاح SSH بنجاح", + "error_updating_ssh_key": "خطأ في تحديث مفتاح SSH. يرجى المحاولة مرة أخرى.", + "ssh_key_deleted_successfully": "تم حذف مفتاح SSH بنجاح", + "error_deleting_ssh_key": "خطأ في حذف مفتاح SSH", + "error_fetching_ssh_keys": "فشل في جلب مفاتيح SSH", + "add_a_tag": "إضافة علامة", + "sort": "ترتيب", + "sort_options": "خيارات الترتيب", + "sort_by_name": "ترتيب حسب الاسم", + "sort_by_created": "ترتيب حسب تاريخ الإنشاء", + "sort_by_updated": "ترتيب حسب تاريخ التحديث", + "ascending": "تصاعدي", + "descending": "تنازلي", + "clear_sort": "مسح الترتيب", + "error": "خطأ", + "add_ssh_key_description": "إضافة مفتاح SSH جديد إلى خزانتك الآمنة", + "name_placeholder": "مفتاح SSH لـ GitHub", + "ssh_key_placeholder": "الصق مفتاح SSH الخاص هنا", + "notes": "الملاحظات (اختياري)", + "notes_placeholder": "معلومات إضافية حول مفتاح SSH هذا", + "add_tag": "إضافة علامة", + "remove": "إزالة", + "adding": "جاري الإضافة...", + "add": "إضافة", + "edit_ssh_key": "تحرير مفتاح SSH", + "edit_ssh_key_description": "تحديث تفاصيل مفتاح SSH", + "save_changes": "حفظ التغييرات", + "search_licenses": "بحث", + "manage_your_ssh_keys": "إدارة مفاتيح SSH بأمان", + "filter_by_tag": "تصفية حسب العلامة", + "all_tags": "جميع العلامات", + "date_created": "تاريخ الإنشاء" + }, + "files": { + "files": "الملفات", + "file_manager": "مدير الملفات", + "add_file": "رفع ملف", + "create_folder": "إنشاء مجلد", + "search_files": "البحث في الملفات", + "per_page": "في الصفحة", + "clear_filters": "مسح المرشحات", + "name": "الاسم", + "size": "الحجم", + "type": "النوع", + "modified": "معدل", + "folder": "مجلد", + "file": "ملف", + "loading_files": "جاري تحميل الملفات...", + "actions": "الإجراءات", + "loading": "جاري التحميل...", + "no_results_found": "لم يتم العثور على نتائج", + "no_files": "لا توجد ملفات", + "no_folders": "لا توجد مجلدات", + "download": "تحميل", + "rename": "إعادة تسمية", + "move": "نقل", + "delete": "حذف", + "copied": "تم النسخ", + "file_copied": "تم نسخ الملف إلى الحافظة", + "copy_failed": "فشل النسخ", + "failed_to_copy_file": "فشل في نسخ الملف إلى الحافظة", + "copy_to_clipboard": "نسخ إلى الحافظة", + "open_menu": "فتح القائمة", + "edit": "تحرير", + "deleted": "تم الحذف", + "showing_results": "عرض {start} إلى {end} من {total} نتيجة", + "confirm_delete": "تأكيد الحذف", + "delete_file_confirmation": "هل أنت متأكد من أنك تريد حذف هذا الملف؟ لا يمكن التراجع عن هذا الإجراء.", + "delete_folder_confirmation": "هل أنت متأكد من أنك تريد حذف هذا المجلد؟ لا يمكن التراجع عن هذا الإجراء.", + "cancel": "إلغاء", + "deleting": "جاري الحذف...", + "please_fill_all_required_fields": "يرجى ملء جميع الحقول المطلوبة", + "file_name_already_exists": "ملف بهذا الاسم موجود بالفعل في هذا المجلد", + "folder_name_already_exists": "مجلد بهذا الاسم موجود بالفعل في هذا المكان", + "missing_required_data": "بيانات مشروع مطلوبة مفقودة", + "encryption_error": "خطأ التشفير", + "could_not_encrypt_data": "فشل في تشفير البيانات. يرجى المحاولة مرة أخرى.", + "success": "نجح", + "file_uploaded_successfully": "تم رفع الملف بنجاح", + "error_uploading_file": "خطأ في رفع الملف. يرجى المحاولة مرة أخرى.", + "file_updated_successfully": "تم تحديث الملف بنجاح", + "error_updating_file": "خطأ في تحديث الملف. يرجى المحاولة مرة أخرى.", + "file_deleted_successfully": "تم حذف الملف بنجاح", + "error_deleting_file": "خطأ في حذف الملف", + "error_fetching_files": "فشل في جلب الملفات", + "folder_created_successfully": "تم إنشاء المجلد بنجاح", + "error_creating_folder": "خطأ في إنشاء المجلد. يرجى المحاولة مرة أخرى.", + "folder_updated_successfully": "تم تحديث المجلد بنجاح", + "error_updating_folder": "خطأ في تحديث المجلد. يرجى المحاولة مرة أخرى.", + "folder_deleted_successfully": "تم حذف المجلد بنجاح", + "error_deleting_folder": "خطأ في حذف المجلد", + "error_fetching_folders": "فشل في جلب المجلدات", + "file_too_large": "حجم الملف يتجاوز حد 50MB", + "invalid_file_type": "نوع ملف غير صحيح", + "upload_failed": "فشل الرفع", + "download_failed": "فشل التحميل", + "sort": "ترتيب", + "sort_options": "خيارات الترتيب", + "sort_by_name": "ترتيب حسب الاسم", + "sort_by_size": "ترتيب حسب الحجم", + "sort_by_modified": "ترتيب حسب تاريخ التعديل", + "sort_by_type": "ترتيب حسب النوع", + "ascending": "تصاعدي", + "descending": "تنازلي", + "clear_sort": "مسح الترتيب", + "error": "خطأ", + "upload_file_description": "رفع ملف إلى خزانتك الآمنة", + "create_folder_description": "إنشاء مجلد جديد لتنظيم ملفاتك", + "name_placeholder": "أدخل اسم الملف أو المجلد", + "folder_name_placeholder": "أدخل اسم المجلد", + "notes": "الملاحظات (اختياري)", + "notes_placeholder": "معلومات إضافية حول هذا الملف", + "add_tag": "إضافة علامة", + "remove": "إزالة", + "uploading": "جاري الرفع...", + "creating": "جاري الإنشاء...", + "add": "إضافة", + "upload": "رفع", + "create": "إنشاء", + "edit_file": "تحرير الملف", + "edit_file_description": "تحديث تفاصيل الملف", + "edit_folder": "تحرير المجلد", + "edit_folder_description": "تحديث تفاصيل المجلد", + "save_changes": "حفظ التغييرات", + "manage_your_files": "إدارة ملفاتك بأمان", + "filter_by_type": "تصفية حسب النوع", + "all_types": "جميع الأنواع", + "date_created": "تاريخ الإنشاء", + "file_size": "حجم الملف", + "folder_path": "مسار المجلد", + "select_folder": "اختر المجلد", + "move_to_folder": "نقل إلى مجلد", + "drag_and_drop_files": "اسحب وأفلت الملفات هنا، أو انقر للاختيار", + "select_files": "اختر الملفات", + "upload_progress": "تقدم الرفع", + "upload_complete": "اكتمل الرفع", + "bytes": "بايت", + "kb": "كيلوبايت", + "mb": "ميجابايت", + "gb": "جيجابايت", + "tb": "تيرابايت" + }, + "project_colors": { + "red": "أحمر", + "orange": "برتقالي", + "amber": "كهرماني", + "yellow": "أصفر", + "lime": "ليموني", + "green": "أخضر", + "emerald": "زمردي", + "teal": "بترقي", + "cyan": "سماوي", + "sky": "أزرق سماوي", + "blue": "أزرق", + "indigo": "نيلي", + "violet": "بنفسجي", + "purple": "بنفسجي", + "fuchsia": "فوشي", + "pink": "وردي", + "rose": "وردي" + } +} diff --git a/packages/frontend-web/middleware.ts b/packages/frontend-web/middleware.ts index e0becdd2..0fc75f5b 100644 --- a/packages/frontend-web/middleware.ts +++ b/packages/frontend-web/middleware.ts @@ -3,7 +3,7 @@ import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; // Available locales -export const locales = ['en', 'de', 'fr', 'es','af']; +export const locales = ['en', 'de', 'fr', 'es','af','ar']; export const defaultLocale = 'en'; // Protected routes that require authentication From e2d110d98e0b6f56dff55ff43eb99461e5216f19 Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Fri, 3 Oct 2025 11:32:56 +0530 Subject: [PATCH 07/32] drive apis --- packages/backend-server/app/locales/en.json | 7 +++++++ packages/backend-server/app/managers/collection_names.py | 4 ++++ packages/backend-server/app/utils/utils.py | 9 ++++++++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/backend-server/app/locales/en.json b/packages/backend-server/app/locales/en.json index b57c3018..35884ec1 100644 --- a/packages/backend-server/app/locales/en.json +++ b/packages/backend-server/app/locales/en.json @@ -145,6 +145,13 @@ "dashboard":{ "overview": "Dashboard overview loaded successfully", "recent_activity": "Recent activity loaded successfully" + }, + "drive":{ + "folders":{ + "created": "Folder created successfully", + "deleted": "Folder deleted successfully", + "already_exists": "Folder already exists" + } } } diff --git a/packages/backend-server/app/managers/collection_names.py b/packages/backend-server/app/managers/collection_names.py index 90e20578..5d6803cf 100644 --- a/packages/backend-server/app/managers/collection_names.py +++ b/packages/backend-server/app/managers/collection_names.py @@ -12,3 +12,7 @@ PROJECT_KEYS = "project_keys" PASSWORD_HISTORY = "password_history" PROJECT_ACTIVITY = "project_activity" + + +FOLDERS = "folders" +FILES = "files" \ No newline at end of file diff --git a/packages/backend-server/app/utils/utils.py b/packages/backend-server/app/utils/utils.py index a08e2b02..9f43745c 100644 --- a/packages/backend-server/app/utils/utils.py +++ b/packages/backend-server/app/utils/utils.py @@ -1,9 +1,12 @@ from uuid_extensions import uuid7 - +from datetime import datetime from fastapi.encoders import jsonable_encoder from starlette.responses import JSONResponse import hashlib import json +from datetime import datetime +import pytz + def response_helper( @@ -56,3 +59,7 @@ def generate_query_hash(query, projection=None): combined_str = query_string + projection_str return hashlib.md5(combined_str.encode()).hexdigest() + + +def create_timestamp(): + return datetime.now(pytz.utc) From 10b827eccc74cac5e24d8b7319dee348223139ea Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Fri, 3 Oct 2025 12:56:05 +0530 Subject: [PATCH 08/32] Update api.py --- packages/backend-server/app/api/v1/web/api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/backend-server/app/api/v1/web/api.py b/packages/backend-server/app/api/v1/web/api.py index a2db900f..2992df45 100644 --- a/packages/backend-server/app/api/v1/web/api.py +++ b/packages/backend-server/app/api/v1/web/api.py @@ -6,7 +6,7 @@ from app.api.v1.web.user import api as user_router from app.api.v1.web.secrets import api as secrets_router from app.api.v1.web.dashboard import api as dashboard_router - +from app.api.v1.web.drive import api as drive_router api_router = APIRouter() api_router.prefix = "/web" @@ -18,3 +18,5 @@ api_router.include_router(user_router.router, tags=["User"]) api_router.include_router(workspace_router.router, tags=["Workspace"]) api_router.include_router(dashboard_router.router, tags=["Dashboard"]) +api_router.include_router(drive_router.router) + From 487853b26991888d3061925acdb0ed07b731cc85 Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Fri, 3 Oct 2025 13:47:25 +0530 Subject: [PATCH 09/32] folders --- .../app/api/v1/web/drive/api.py | 9 +++ .../app/api/v1/web/drive/folders/api.py | 28 +++++++ .../app/api/v1/web/drive/folders/schema.py | 19 +++++ .../app/api/v1/web/drive/folders/services.py | 75 +++++++++++++++++++ 4 files changed, 131 insertions(+) create mode 100644 packages/backend-server/app/api/v1/web/drive/api.py create mode 100644 packages/backend-server/app/api/v1/web/drive/folders/api.py create mode 100644 packages/backend-server/app/api/v1/web/drive/folders/schema.py create mode 100644 packages/backend-server/app/api/v1/web/drive/folders/services.py diff --git a/packages/backend-server/app/api/v1/web/drive/api.py b/packages/backend-server/app/api/v1/web/drive/api.py new file mode 100644 index 00000000..3c3cb709 --- /dev/null +++ b/packages/backend-server/app/api/v1/web/drive/api.py @@ -0,0 +1,9 @@ +from fastapi import APIRouter +from app.api.v1.web.drive.folders import api as folders_router + + +api_router = APIRouter() + +api_router.prefix = "/drive" + +api_router.include_router(folders_router.router, tags=["Drive: Folders"]) \ No newline at end of file diff --git a/packages/backend-server/app/api/v1/web/drive/folders/api.py b/packages/backend-server/app/api/v1/web/drive/folders/api.py new file mode 100644 index 00000000..faed047b --- /dev/null +++ b/packages/backend-server/app/api/v1/web/drive/folders/api.py @@ -0,0 +1,28 @@ +from fastapi import APIRouter,Depends +from app.api.v1.web.drive.folders.schema import CreateFolder, RenameFolder, MoveFolder, DeleteFolders +from app.api.v1.web.auth.schema import UserDetails +from app.framework.permission_services.service import get_current_user +from app.api.v1.web.drive.folders.services import create_folder,delete_folders,rename_folder, move_folders + +FOLDER_URL ="/folder" +router = APIRouter() + +@router.post(FOLDER_URL+"/create") +async def create_folder_api(payload: CreateFolder, user: UserDetails = Depends(get_current_user)): + return await create_folder(user, payload.model_dump()) + + +@router.delete(FOLDER_URL+"/delete") +async def delete_folder_api(payload: DeleteFolders, user: UserDetails = Depends(get_current_user)): + return await delete_folders(user, payload.model_dump()) + + +@router.post(FOLDER_URL+"/rename") +async def rename_folder_api(payload: RenameFolder, user: UserDetails = Depends(get_current_user)): + return await rename_folder(user, payload.model_dump()) + + +@router.post(FOLDER_URL+"/move") +async def move_folder_api(payload: MoveFolder, user: UserDetails = Depends(get_current_user)): + return await move_folders(user, payload.model_dump()) + diff --git a/packages/backend-server/app/api/v1/web/drive/folders/schema.py b/packages/backend-server/app/api/v1/web/drive/folders/schema.py new file mode 100644 index 00000000..4d5d1224 --- /dev/null +++ b/packages/backend-server/app/api/v1/web/drive/folders/schema.py @@ -0,0 +1,19 @@ +from pydantic import BaseModel, Field +from typing import Optional, List +from datetime import datetime + +class CreateFolder(BaseModel): + name: str + parent_id: str + +class RenameFolder(BaseModel): + name: str + folder_id: str + parent_id: Optional[str] = None + +class MoveFolder(BaseModel): + folder_ids: List[str] + parent_id: str + +class DeleteFolders(BaseModel): + folder_ids: List[str] \ No newline at end of file diff --git a/packages/backend-server/app/api/v1/web/drive/folders/services.py b/packages/backend-server/app/api/v1/web/drive/folders/services.py new file mode 100644 index 00000000..d645d76d --- /dev/null +++ b/packages/backend-server/app/api/v1/web/drive/folders/services.py @@ -0,0 +1,75 @@ + +from app.framework.mongo_db import base_manager as db_manager +from app.managers.collection_names import FILES, FOLDERS +from app.utils.utils import response_helper, create_uuid, create_timestamp +from app.utils.i8ns import translate + +async def create_folder(user, payload): + db = user.get("db") + folder_id = create_uuid() + query={ + "created_by": user.get("user_id"), + "lower_name": payload.get("name").strip().lower(), + } + if payload.get("parent_id"): + query["parent_id"] = payload.get("parent_id") + folder = db_manager.find_one(db, FOLDERS, query) + if folder: + return response_helper(400, translate("drive.folders.already_exists")) + + folder = { + "doc_id": folder_id, + "name": payload.get("name"), + "parent_id": payload.get("parent_id"), + "created_by": user.get("user_id"), + "lower_name": payload.get("name").strip().lower(), + "created_at": create_timestamp(), + + } + db_manager.insert_one(db, FOLDERS, folder) + return response_helper(200, translate("drive.folders.created")) + + +async def delete_folders(user, payload): + db = user.get("db") + folder_ids = payload.get("folder_ids") + user_id = user.get("user_id") + db_manager.update_many(db, FOLDERS, {"doc_id": {"$in":folder_ids}, "created_by": user_id}, {"$set": {"access": False,"delete_by":user_id, "deleted_at": create_timestamp()}}) + return response_helper(200, translate("drive.folders.deleted")) + + +async def rename_folder(user, payload): + db = user.get("db") + name = payload.get("name").strip() + folder_id = payload.get("folder_id") + lower_name= name.lower() + query={"created_by": user.get("user_id"), "lower_name": lower_name} + + if payload.get("parent_id"): + query["parent_id"] = payload.get("parent_id") + folder = db_manager.find_one(db, FOLDERS, query) + + if folder: + return response_helper(400, translate("drive.folders.already_exists")) + + db_manager.update_one(db, FOLDERS, {"doc_id": folder_id, "created_by": user.get("user_id")}, {"$set": {"name": name, "lower_name": lower_name}}) + return response_helper(200, translate("drive.folders.renamed")) + + +async def move_folders(user, payload): + db = user.get("db") + folder_ids = payload.get("folder_ids") + parent_id = payload.get("parent_id") + query={"created_by": user.get("user_id"), "doc_id": {"$in":folder_ids}} + folders = db_manager.find(db, FOLDERS, query) + folders_moved=[] + for item in folders: + individual_query={"created_by": user.get("user_id"), "parent_id": parent_id, "lower_name": item.get("lower_name")} + if not db_manager.find_one(db, FOLDERS, individual_query): + db_manager.update_one(db, FOLDERS, {"doc_id": item.get("doc_id")}, {"$set": {"parent_id": parent_id}}) + folders_moved.append(item.get("name")) + + + db_manager.update_many(db, FOLDERS, {"doc_id": {"$in":folder_ids}, "created_by": user.get("user_id")}, {"$set": {"parent_id": parent_id}}) + return response_helper(200, translate("drive.folders.moved"), folders_moved=folders_moved) + From 3acd743678b61c65fe68962f23ab4d03250cabfb Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Fri, 3 Oct 2025 15:40:56 +0530 Subject: [PATCH 10/32] Update api.py --- packages/backend-server/app/api/v1/web/drive/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend-server/app/api/v1/web/drive/api.py b/packages/backend-server/app/api/v1/web/drive/api.py index 3c3cb709..843855bd 100644 --- a/packages/backend-server/app/api/v1/web/drive/api.py +++ b/packages/backend-server/app/api/v1/web/drive/api.py @@ -2,8 +2,8 @@ from app.api.v1.web.drive.folders import api as folders_router -api_router = APIRouter() +router = APIRouter() -api_router.prefix = "/drive" +router.prefix = "/drive" -api_router.include_router(folders_router.router, tags=["Drive: Folders"]) \ No newline at end of file +router.include_router(folders_router.router, tags=["Drive: Folders"]) \ No newline at end of file From 097f45ecb48bf1aa839cc1f80500d42ec2014025 Mon Sep 17 00:00:00 2001 From: ananducv Date: Mon, 6 Oct 2025 18:14:49 +0530 Subject: [PATCH 11/32] basic ui and api integration done --- packages/frontend-web/DRIVE_IMPLEMENTATION.md | 1590 +++++++++++++++++ .../app/[locale]/dashboard/drive/page.tsx | 13 + .../components/add-folder-dialog.tsx | 150 ++ .../frontend-web/components/drive-content.tsx | 371 ++++ .../components/move-folder-dialog.tsx | 182 ++ .../components/project-dialog.tsx | 9 +- .../components/rename-folder-dialog.tsx | 141 ++ .../frontend-web/components/sidebar-nav.tsx | 12 +- .../hooks/use-drive-management.ts | 293 +++ packages/frontend-web/messages/en/common.json | 48 + 10 files changed, 2807 insertions(+), 2 deletions(-) create mode 100644 packages/frontend-web/DRIVE_IMPLEMENTATION.md create mode 100644 packages/frontend-web/app/[locale]/dashboard/drive/page.tsx create mode 100644 packages/frontend-web/components/add-folder-dialog.tsx create mode 100644 packages/frontend-web/components/drive-content.tsx create mode 100644 packages/frontend-web/components/move-folder-dialog.tsx create mode 100644 packages/frontend-web/components/rename-folder-dialog.tsx create mode 100644 packages/frontend-web/hooks/use-drive-management.ts diff --git a/packages/frontend-web/DRIVE_IMPLEMENTATION.md b/packages/frontend-web/DRIVE_IMPLEMENTATION.md new file mode 100644 index 00000000..946da8f4 --- /dev/null +++ b/packages/frontend-web/DRIVE_IMPLEMENTATION.md @@ -0,0 +1,1590 @@ +# Drive Feature Implementation Plan + +## Overview +This document outlines the phase-wise implementation of the Drive feature in the Zecrypt web application. The Drive section will allow users to manage folders with create, rename, move, and delete operations. + +## Tech Stack +- **Framework**: Next.js 15 (App Router) +- **Language**: TypeScript +- **UI Components**: Shadcn UI +- **Styling**: Tailwind CSS +- **State Management**: React Hooks + Redux (for workspace/project selection) +- **HTTP Client**: Axios (with automatic access-token header injection) + +## Backend API Reference + +All APIs require the `access-token` header (automatically added by `axiosInstance`): + +### 1. Create Folder +- **Endpoint**: `POST /api/v1/web/drive/folder/create` +- **Request Body**: + ```json + { + "name": "string", + "parent_id": "string" (optional) + } + ``` +- **Response**: `200 OK` with folder ID string + +### 2. Delete Folder(s) +- **Endpoint**: `DELETE /api/v1/web/drive/folder/delete` +- **Request Body**: + ```json + { + "folder_ids": ["string"] + } + ``` +- **Response**: `200 OK` + +### 3. Rename Folder +- **Endpoint**: `POST /api/v1/web/drive/folder/rename` +- **Request Body**: + ```json + { + "name": "string", + "folder_id": "string", + "parent_id": "string" (optional) + } + ``` +- **Response**: `200 OK` + +### 4. Move Folder(s) +- **Endpoint**: `POST /api/v1/web/drive/folder/move` +- **Request Body**: + ```json + { + "folder_ids": ["string"], + "parent_id": "string" + } + ``` +- **Response**: `200 OK` + +--- + +## Phase 1: Core Data Structure & Hook Setup + +### 1.1 Create TypeScript Interfaces +**File**: `hooks/use-drive-management.ts` (start of file) + +Define the core data structures: + +```typescript +interface Folder { + folder_id: string; + name: string; + parent_id: string | null; + created_at: string; + updated_at: string | null; + created_by: string; + workspace_id: string; + project_id: string; +} + +interface UseDriveManagementProps { + selectedWorkspaceId: string | null; + selectedProjectId: string | null; +} + +interface UseDriveManagementReturn { + folders: Folder[]; + isLoading: boolean; + createFolder: (name: string, parentId?: string | null) => Promise; + renameFolder: (folderId: string, newName: string, parentId?: string | null) => Promise; + moveFolder: (folderIds: string[], parentId: string) => Promise; + deleteFolders: (folderIds: string[]) => Promise; + fetchFolders: () => Promise; + currentFolder: Folder | null; + setCurrentFolder: (folder: Folder | null) => void; + getFolderPath: (folderId: string | null) => Folder[]; + getSubfolders: (parentId: string | null) => Folder[]; +} +``` + +### 1.2 Implement Base Hook Structure +**File**: `hooks/use-drive-management.ts` + +Follow the pattern from `use-email-management.ts`: + +```typescript +"use client"; + +import { useState, useEffect, useCallback, useMemo } from "react"; +import { useSelector } from "react-redux"; +import { RootState } from "@/libs/Redux/store"; +import axiosInstance from "@/libs/Middleware/axiosInstace"; +import { toast } from "@/components/ui/use-toast"; +import { useTranslator } from "@/hooks/use-translations"; + +export function useDriveManagement({ + selectedWorkspaceId, + selectedProjectId, +}: UseDriveManagementProps): UseDriveManagementReturn { + const { translate } = useTranslator(); + const [folders, setFolders] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [currentFolder, setCurrentFolder] = useState(null); + + // TODO: Implement fetchFolders + // TODO: Implement createFolder + // TODO: Implement renameFolder + // TODO: Implement moveFolder + // TODO: Implement deleteFolders + // TODO: Implement helper functions (getFolderPath, getSubfolders) + + return { + folders, + isLoading, + createFolder, + renameFolder, + moveFolder, + deleteFolders, + fetchFolders, + currentFolder, + setCurrentFolder, + getFolderPath, + getSubfolders, + }; +} +``` + +### 1.3 Implement Folder Fetching +**File**: `hooks/use-drive-management.ts` + +**Note**: Since no GET endpoint is provided yet, we'll use an empty array for now and add fetch logic when the API is ready. + +```typescript +const fetchFolders = useCallback(async () => { + if (!selectedWorkspaceId || !selectedProjectId) { + setIsLoading(false); + return; + } + + setIsLoading(true); + + try { + // TODO: Replace with actual GET endpoint when available + // const response = await axiosInstance.get( + // `/api/v1/web/drive/${selectedWorkspaceId}/${selectedProjectId}/folders` + // ); + // setFolders(response.data.data || []); + + // For now, use empty array or mock data + setFolders([]); + } catch (error) { + console.error("Error fetching folders:", error); + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("error_fetching_folders", "drive", { + default: "Failed to fetch folders", + }), + variant: "destructive", + }); + setFolders([]); + } finally { + setIsLoading(false); + } +}, [selectedWorkspaceId, selectedProjectId, translate]); + +useEffect(() => { + if (selectedWorkspaceId && selectedProjectId) { + fetchFolders(); + } +}, [selectedWorkspaceId, selectedProjectId, fetchFolders]); +``` + +### 1.4 Implement Helper Functions + +```typescript +// Get breadcrumb path for a folder +const getFolderPath = useCallback((folderId: string | null): Folder[] => { + if (!folderId) return []; + + const path: Folder[] = []; + let currentId: string | null = folderId; + + while (currentId) { + const folder = folders.find(f => f.folder_id === currentId); + if (!folder) break; + path.unshift(folder); + currentId = folder.parent_id; + } + + return path; +}, [folders]); + +// Get subfolders of a parent +const getSubfolders = useCallback((parentId: string | null): Folder[] => { + return folders.filter(f => f.parent_id === parentId); +}, [folders]); +``` + +**Deliverables**: +- ✅ `hooks/use-drive-management.ts` with TypeScript interfaces +- ✅ Base hook structure with state management +- ✅ Fetch folders logic (ready for API integration) +- ✅ Helper functions for folder hierarchy + +--- + +## Phase 2: CRUD Operations Implementation + +### 2.1 Create Folder Function +**File**: `hooks/use-drive-management.ts` + +```typescript +const createFolder = useCallback( + async (name: string, parentId: string | null = null) => { + if (!selectedWorkspaceId || !selectedProjectId) { + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("missing_workspace_project", "drive", { + default: "Missing workspace or project", + }), + variant: "destructive", + }); + return; + } + + try { + const payload: any = { name }; + if (parentId) { + payload.parent_id = parentId; + } + + await axiosInstance.post( + `/api/v1/web/drive/folder/create`, + payload + ); + + toast({ + title: translate("success", "drive", { default: "Success" }), + description: translate("folder_created", "drive", { + default: "Folder created successfully", + }), + }); + + // Refresh folders list + await fetchFolders(); + } catch (error) { + console.error("Error creating folder:", error); + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("error_creating_folder", "drive", { + default: "Failed to create folder", + }), + variant: "destructive", + }); + throw error; + } + }, + [selectedWorkspaceId, selectedProjectId, fetchFolders, translate] +); +``` + +### 2.2 Rename Folder Function + +```typescript +const renameFolder = useCallback( + async (folderId: string, newName: string, parentId: string | null = null) => { + try { + const payload: any = { + name: newName, + folder_id: folderId, + }; + if (parentId) { + payload.parent_id = parentId; + } + + await axiosInstance.post( + `/api/v1/web/drive/folder/rename`, + payload + ); + + toast({ + title: translate("success", "drive", { default: "Success" }), + description: translate("folder_renamed", "drive", { + default: "Folder renamed successfully", + }), + }); + + await fetchFolders(); + } catch (error) { + console.error("Error renaming folder:", error); + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("error_renaming_folder", "drive", { + default: "Failed to rename folder", + }), + variant: "destructive", + }); + throw error; + } + }, + [fetchFolders, translate] +); +``` + +### 2.3 Move Folder Function + +```typescript +const moveFolder = useCallback( + async (folderIds: string[], parentId: string) => { + try { + await axiosInstance.post( + `/api/v1/web/drive/folder/move`, + { + folder_ids: folderIds, + parent_id: parentId, + } + ); + + toast({ + title: translate("success", "drive", { default: "Success" }), + description: translate("folder_moved", "drive", { + default: `${folderIds.length} folder(s) moved successfully`, + }), + }); + + await fetchFolders(); + } catch (error) { + console.error("Error moving folder:", error); + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("error_moving_folder", "drive", { + default: "Failed to move folder(s)", + }), + variant: "destructive", + }); + throw error; + } + }, + [fetchFolders, translate] +); +``` + +### 2.4 Delete Folder Function + +```typescript +const deleteFolders = useCallback( + async (folderIds: string[]) => { + try { + await axiosInstance.delete( + `/api/v1/web/drive/folder/delete`, + { + data: { folder_ids: folderIds } + } + ); + + toast({ + title: translate("success", "drive", { default: "Success" }), + description: translate("folder_deleted", "drive", { + default: `${folderIds.length} folder(s) deleted successfully`, + }), + }); + + await fetchFolders(); + } catch (error) { + console.error("Error deleting folder:", error); + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("error_deleting_folder", "drive", { + default: "Failed to delete folder(s)", + }), + variant: "destructive", + }); + throw error; + } + }, + [fetchFolders, translate] +); +``` + +**Deliverables**: +- ✅ Create folder API integration +- ✅ Rename folder API integration +- ✅ Move folder API integration +- ✅ Delete folder API integration +- ✅ Toast notifications for all operations +- ✅ Error handling + +--- + +## Phase 3: Dialog Components + +### 3.1 Create Folder Dialog +**File**: `components/add-folder-dialog.tsx` + +Follow the pattern from `add-email-dialog.tsx`: + +```typescript +"use client"; + +import { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { AlertCircle } from "lucide-react"; +import { useTranslator } from "@/hooks/use-translations"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; + +interface AddFolderDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onFolderCreated: () => void; + parentFolder: { folder_id: string; name: string } | null; + onCreateFolder: (name: string, parentId: string | null) => Promise; +} + +export function AddFolderDialog({ + open, + onOpenChange, + onFolderCreated, + parentFolder, + onCreateFolder, +}: AddFolderDialogProps) { + const { translate } = useTranslator(); + const [folderName, setFolderName] = useState(""); + const [error, setError] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + const validateForm = () => { + if (!folderName.trim()) { + setError( + translate("folder_name_required", "drive", { + default: "Folder name is required", + }) + ); + return false; + } + setError(""); + return true; + }; + + const handleSubmit = async () => { + if (!validateForm()) return; + + setIsSubmitting(true); + try { + await onCreateFolder(folderName, parentFolder?.folder_id || null); + resetForm(); + onFolderCreated(); + onOpenChange(false); + } catch (error) { + console.error("Error creating folder:", error); + setError( + translate("error_creating_folder", "drive", { + default: "Error creating folder. Please try again.", + }) + ); + } finally { + setIsSubmitting(false); + } + }; + + const resetForm = () => { + setFolderName(""); + setError(""); + }; + + useEffect(() => { + if (open) { + resetForm(); + } + }, [open]); + + return ( + + + + + {translate("create_folder", "drive", { default: "Create Folder" })} + + + {parentFolder + ? translate("create_subfolder_in", "drive", { + default: `Create a new folder in "${parentFolder.name}"`, + folderName: parentFolder.name, + }) + : translate("create_new_folder", "drive", { + default: "Create a new folder in root", + })} + + + +
+ {error && ( +
+ +
{error}
+
+ )} + +
+ + setFolderName(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && !isSubmitting) { + handleSubmit(); + } + }} + autoFocus + /> +
+
+ + + + + +
+
+ ); +} +``` + +### 3.2 Rename Folder Dialog +**File**: `components/rename-folder-dialog.tsx` + +```typescript +"use client"; + +import { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { AlertCircle } from "lucide-react"; +import { useTranslator } from "@/hooks/use-translations"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; + +interface Folder { + folder_id: string; + name: string; + parent_id: string | null; +} + +interface RenameFolderDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onFolderRenamed: () => void; + folder: Folder; + onRenameFolder: (folderId: string, newName: string, parentId: string | null) => Promise; +} + +export function RenameFolderDialog({ + open, + onOpenChange, + onFolderRenamed, + folder, + onRenameFolder, +}: RenameFolderDialogProps) { + const { translate } = useTranslator(); + const [folderName, setFolderName] = useState(folder.name); + const [error, setError] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + useEffect(() => { + setFolderName(folder.name); + }, [folder]); + + const validateForm = () => { + if (!folderName.trim()) { + setError( + translate("folder_name_required", "drive", { + default: "Folder name is required", + }) + ); + return false; + } + setError(""); + return true; + }; + + const handleSubmit = async () => { + if (!validateForm()) return; + + setIsSubmitting(true); + try { + await onRenameFolder(folder.folder_id, folderName, folder.parent_id); + onFolderRenamed(); + onOpenChange(false); + } catch (error) { + console.error("Error renaming folder:", error); + setError( + translate("error_renaming_folder", "drive", { + default: "Error renaming folder. Please try again.", + }) + ); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + + + {translate("rename_folder", "drive", { default: "Rename Folder" })} + + + {translate("rename_folder_description", "drive", { + default: `Rename "${folder.name}"`, + folderName: folder.name, + })} + + + +
+ {error && ( +
+ +
{error}
+
+ )} + +
+ + setFolderName(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && !isSubmitting) { + handleSubmit(); + } + }} + autoFocus + /> +
+
+ + + + + +
+
+ ); +} +``` + +### 3.3 Move Folder Dialog +**File**: `components/move-folder-dialog.tsx` + +```typescript +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { AlertCircle, Folder as FolderIcon, ChevronRight } from "lucide-react"; +import { useTranslator } from "@/hooks/use-translations"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { cn } from "@/libs/utils"; + +interface Folder { + folder_id: string; + name: string; + parent_id: string | null; +} + +interface MoveFolderDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onFolderMoved: () => void; + selectedFolders: Folder[]; + allFolders: Folder[]; + onMoveFolder: (folderIds: string[], parentId: string) => Promise; +} + +export function MoveFolderDialog({ + open, + onOpenChange, + onFolderMoved, + selectedFolders, + allFolders, + onMoveFolder, +}: MoveFolderDialogProps) { + const { translate } = useTranslator(); + const [selectedDestination, setSelectedDestination] = useState(null); + const [error, setError] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Filter out folders being moved and their descendants + const availableDestinations = allFolders.filter(folder => { + const selectedIds = selectedFolders.map(f => f.folder_id); + if (selectedIds.includes(folder.folder_id)) return false; + + // Check if this folder is a descendant of any selected folder + let current = folder; + while (current.parent_id) { + if (selectedIds.includes(current.parent_id)) return false; + const parent = allFolders.find(f => f.folder_id === current.parent_id); + if (!parent) break; + current = parent; + } + return true; + }); + + const handleSubmit = async () => { + if (!selectedDestination) { + setError( + translate("select_destination", "drive", { + default: "Please select a destination folder", + }) + ); + return; + } + + setIsSubmitting(true); + try { + await onMoveFolder( + selectedFolders.map(f => f.folder_id), + selectedDestination + ); + onFolderMoved(); + onOpenChange(false); + } catch (error) { + console.error("Error moving folder:", error); + setError( + translate("error_moving_folder", "drive", { + default: "Error moving folder(s). Please try again.", + }) + ); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + + + {translate("move_folder", "drive", { default: "Move Folder" })} + + + {translate("move_folder_description", "drive", { + default: `Moving ${selectedFolders.length} folder(s)`, + count: selectedFolders.length, + })} + + + +
+ {error && ( +
+ +
{error}
+
+ )} + +
+ + +
+ + + {availableDestinations.map((folder) => ( + + ))} +
+
+
+
+ + + + + +
+
+ ); +} +``` + +**Deliverables**: +- ✅ `components/add-folder-dialog.tsx` - Create folder UI +- ✅ `components/rename-folder-dialog.tsx` - Rename folder UI +- ✅ `components/move-folder-dialog.tsx` - Move folder UI with folder tree selection +- ✅ All dialogs follow Shadcn UI patterns +- ✅ Proper validation and error handling +- ✅ Translation support + +--- + +## Phase 4: Main Drive Content Component + +### 4.1 Create Drive Content Component +**File**: `components/drive-content.tsx` + +This is the main UI component that displays the folder grid/list: + +```typescript +"use client"; + +import { useState, useCallback } from "react"; +import { useSelector } from "react-redux"; +import { RootState } from "@/libs/Redux/store"; +import { Button } from "@/components/ui/button"; +import { + Folder as FolderIcon, + Plus, + MoreHorizontal, + ArrowLeft, + Home, + Pencil, + Move, + Trash2, +} from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { useTranslator } from "@/hooks/use-translations"; +import { useDriveManagement } from "@/hooks/use-drive-management"; +import { AddFolderDialog } from "./add-folder-dialog"; +import { RenameFolderDialog } from "./rename-folder-dialog"; +import { MoveFolderDialog } from "./move-folder-dialog"; +import { cn } from "@/libs/utils"; + +interface Folder { + folder_id: string; + name: string; + parent_id: string | null; + created_at: string; + updated_at: string | null; +} + +export function DriveContent() { + const { translate } = useTranslator(); + const selectedWorkspaceId = useSelector( + (state: RootState) => state.workspace.selectedWorkspaceId + ); + const selectedProjectId = useSelector( + (state: RootState) => state.workspace.selectedProjectId + ); + + const { + folders, + isLoading, + createFolder, + renameFolder, + moveFolder, + deleteFolders, + fetchFolders, + currentFolder, + setCurrentFolder, + getFolderPath, + getSubfolders, + } = useDriveManagement({ + selectedWorkspaceId, + selectedProjectId, + }); + + const [showAddFolder, setShowAddFolder] = useState(false); + const [showRenameFolder, setShowRenameFolder] = useState(false); + const [showMoveFolder, setShowMoveFolder] = useState(false); + const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false); + const [selectedFolder, setSelectedFolder] = useState(null); + const [isProcessingDelete, setIsProcessingDelete] = useState(false); + + // Get current subfolders to display + const currentSubfolders = getSubfolders(currentFolder?.folder_id || null); + + // Breadcrumb path + const breadcrumbPath = getFolderPath(currentFolder?.folder_id || null); + + const handleFolderClick = (folder: Folder) => { + setCurrentFolder(folder); + }; + + const handleBackClick = () => { + if (!currentFolder) return; + + if (currentFolder.parent_id) { + const parentFolder = folders.find(f => f.folder_id === currentFolder.parent_id); + setCurrentFolder(parentFolder || null); + } else { + setCurrentFolder(null); + } + }; + + const handleHomeClick = () => { + setCurrentFolder(null); + }; + + const handleCreateFolder = () => { + setShowAddFolder(true); + }; + + const handleRenameFolder = (folder: Folder) => { + setSelectedFolder(folder); + setShowRenameFolder(true); + }; + + const handleMoveFolder = (folder: Folder) => { + setSelectedFolder(folder); + setShowMoveFolder(true); + }; + + const confirmDelete = (folder: Folder) => { + setSelectedFolder(folder); + setShowDeleteConfirmation(true); + }; + + const handleDeleteFolder = async () => { + if (!selectedFolder) return; + + setIsProcessingDelete(true); + try { + await deleteFolders([selectedFolder.folder_id]); + setShowDeleteConfirmation(false); + setSelectedFolder(null); + } finally { + setIsProcessingDelete(false); + } + }; + + return ( +
+ {/* Header */} +
+
+

+ {translate("drive", "drive", { default: "Drive" })} +

+

+ {translate("manage_your_folders", "drive", { + default: "Organize and manage your folders", + })} +

+
+
+ + {/* Breadcrumb and Actions */} +
+
+ + + {breadcrumbPath.map((folder, index) => ( +
+ / + +
+ ))} +
+ +
+ {currentFolder && ( + + )} + +
+
+ + {/* Folders Grid */} +
+ {isLoading ? ( +
+

+ {translate("loading_folders", "drive", { default: "Loading folders..." })} +

+
+ ) : currentSubfolders.length > 0 ? ( +
+ {currentSubfolders.map((folder) => ( +
+
handleFolderClick(folder)} + > + + + {folder.name} + +
+ + {/* Context Menu */} + + + + + + handleFolderClick(folder)}> + {translate("open", "drive", { default: "Open" })} + + handleRenameFolder(folder)}> + + {translate("rename", "drive", { default: "Rename" })} + + handleMoveFolder(folder)}> + + {translate("move", "drive", { default: "Move" })} + + confirmDelete(folder)} + > + + {translate("delete", "drive", { default: "Delete" })} + + + +
+ ))} +
+ ) : ( +
+ +

+ {translate("no_folders", "drive", { + default: "No folders in this location", + })} +

+ +
+ )} +
+ + {/* Dialogs */} + + + {selectedFolder && ( + <> + + + + + )} + + {/* Delete Confirmation */} + + + + + {translate("confirm_delete", "drive", { default: "Confirm deletion" })} + + + {translate("delete_folder_confirmation", "drive", { + default: `Are you sure you want to delete "${selectedFolder?.name}"? This action cannot be undone.`, + folderName: selectedFolder?.name, + })} + + + + + {translate("cancel", "drive", { default: "Cancel" })} + + { + e.preventDefault(); + handleDeleteFolder(); + }} + disabled={isProcessingDelete} + className="bg-destructive hover:bg-destructive/90" + > + {isProcessingDelete + ? translate("deleting", "drive", { default: "Deleting..." }) + : translate("delete", "drive", { default: "Delete" })} + + + + +
+ ); +} +``` + +**Deliverables**: +- ✅ `components/drive-content.tsx` - Main drive UI component +- ✅ Folder grid/list view +- ✅ Breadcrumb navigation +- ✅ Context menu for folder actions +- ✅ Double-click to open folders +- ✅ Empty state handling +- ✅ Loading state + +--- + +## Phase 5: Dashboard Page Integration + +### 5.1 Create Drive Dashboard Page +**File**: `app/[locale]/dashboard/drive/page.tsx` + +Follow the exact pattern from `emails/page.tsx`: + +```typescript +import { DashboardLayout } from "@/components/dashboard-layout"; +import { DriveContent } from "@/components/drive-content"; + +export default async function DrivePage({ params }: { params: Promise<{ locale: string }> }) { + const { locale } = await params; + + return ( + + + + ); +} +``` + +### 5.2 Update Navigation (if needed) + +Check if `components/sidebar-nav.tsx` needs to be updated to include the Drive link: + +```typescript +// Add this to the navigation items +{ + name: translate("drive", "navigation", { default: "Drive" }), + href: "/dashboard/drive", + icon: , +} +``` + +**Deliverables**: +- ✅ `app/[locale]/dashboard/drive/page.tsx` - Drive dashboard page +- ✅ Integration with existing navigation (if applicable) +- ✅ Proper locale handling + +--- + +## Phase 6: Polish & Enhancements + +### 6.1 Add Keyboard Shortcuts + +Update `components/drive-content.tsx` to add keyboard shortcuts: + +```typescript +useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // Cmd/Ctrl + N to create new folder + if ((e.metaKey || e.ctrlKey) && e.key === "n") { + e.preventDefault(); + setShowAddFolder(true); + } + + // Backspace to go back + if (e.key === "Backspace" && currentFolder && !isDialogOpen()) { + e.preventDefault(); + handleBackClick(); + } + }; + + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); +}, [currentFolder]); +``` + +### 6.2 Add Animations (Optional) + +Install and use Framer Motion for smooth transitions: + +```typescript +import { motion } from "framer-motion"; + +// Wrap folder grid items + + {/* Folder content */} + +``` + +### 6.3 Add Folder Count & Metadata + +Show folder metadata (subfolder count, created date): + +```typescript +
+ {getSubfolders(folder.folder_id).length} items +
+``` + +### 6.4 Add Search/Filter (Optional) + +Add search functionality similar to emails: + +```typescript +const [searchQuery, setSearchQuery] = useState(""); + +const filteredFolders = currentSubfolders.filter(folder => + folder.name.toLowerCase().includes(searchQuery.toLowerCase()) +); +``` + +**Deliverables**: +- ✅ Keyboard shortcuts for common actions +- ✅ Smooth animations (optional) +- ✅ Folder metadata display +- ✅ Search/filter capability (optional) + +--- + +## Testing Checklist + +### Functional Testing +- [ ] Create folder in root +- [ ] Create subfolder inside another folder +- [ ] Rename folder +- [ ] Move folder to different parent +- [ ] Move folder to root +- [ ] Delete folder +- [ ] Navigate folder hierarchy with breadcrumbs +- [ ] Navigate back button +- [ ] Double-click to open folder +- [ ] Context menu actions work correctly + +### Error Handling +- [ ] Network errors show proper toast notifications +- [ ] Validation errors prevent form submission +- [ ] Cannot move folder into itself +- [ ] Cannot move folder into its descendant +- [ ] Loading states display correctly + +### UI/UX Testing +- [ ] Responsive design works on mobile +- [ ] Dialogs are accessible (keyboard navigation) +- [ ] Empty states display properly +- [ ] Loading states don't flicker +- [ ] Breadcrumb navigation is clear +- [ ] Folder grid scales properly + +### Integration Testing +- [ ] Works with workspace/project selection from Redux +- [ ] Access token is sent with all requests +- [ ] Translations work in different locales +- [ ] Toast notifications appear correctly +- [ ] Navigation integrates with sidebar (if applicable) + +--- + +## File Structure Summary + +``` +packages/frontend-web/ +├── app/ +│ └── [locale]/ +│ └── dashboard/ +│ └── drive/ +│ └── page.tsx # Main page route +├── components/ +│ ├── add-folder-dialog.tsx # Create folder dialog +│ ├── rename-folder-dialog.tsx # Rename folder dialog +│ ├── move-folder-dialog.tsx # Move folder dialog +│ └── drive-content.tsx # Main drive UI component +└── hooks/ + └── use-drive-management.ts # Drive state & API logic +``` + +--- + +## Translation Keys to Add + +Add these keys to `messages/en.json` (and other locales): + +```json +{ + "drive": { + "drive": "Drive", + "manage_your_folders": "Organize and manage your folders", + "create_folder": "Create Folder", + "new_folder": "New Folder", + "folder_name": "Folder Name", + "folder_name_placeholder": "My Folder", + "folder_name_required": "Folder name is required", + "rename_folder": "Rename Folder", + "move_folder": "Move Folder", + "delete_folder": "Delete Folder", + "no_folders": "No folders in this location", + "loading_folders": "Loading folders...", + "root": "Root", + "back": "Back", + "open": "Open", + "rename": "Rename", + "move": "Move", + "delete": "Delete", + "cancel": "Cancel", + "create": "Create", + "creating": "Creating...", + "renaming": "Renaming...", + "moving": "Moving...", + "deleting": "Deleting...", + "success": "Success", + "error": "Error", + "folder_created": "Folder created successfully", + "folder_renamed": "Folder renamed successfully", + "folder_moved": "Folder(s) moved successfully", + "folder_deleted": "Folder(s) deleted successfully", + "error_creating_folder": "Failed to create folder", + "error_renaming_folder": "Failed to rename folder", + "error_moving_folder": "Failed to move folder(s)", + "error_deleting_folder": "Failed to delete folder(s)", + "error_fetching_folders": "Failed to fetch folders", + "confirm_delete": "Confirm deletion", + "delete_folder_confirmation": "Are you sure you want to delete this folder? This action cannot be undone.", + "select_destination": "Please select a destination folder", + "destination_folder": "Select destination folder", + "root_folder": "Root", + "create_new_folder": "Create a new folder in root", + "create_subfolder_in": "Create a new folder in {folderName}", + "rename_folder_description": "Rename {folderName}", + "move_folder_description": "Moving {count} folder(s)", + "missing_workspace_project": "Missing workspace or project" + } +} +``` + +--- + +## API Integration Notes + +### Missing GET Endpoint +Currently, there's no GET endpoint for fetching folders. The implementation assumes: + +**Recommended endpoint**: `GET /api/v1/web/drive/folder/list` + +**Query parameters**: +- `workspace_id`: string +- `project_id`: string + +**Response**: +```json +{ + "data": [ + { + "folder_id": "string", + "name": "string", + "parent_id": "string | null", + "created_at": "string", + "updated_at": "string", + "created_by": "string", + "workspace_id": "string", + "project_id": "string" + } + ] +} +``` + +Once this endpoint is available, update `hooks/use-drive-management.ts` in the `fetchFolders` function. + +--- + +## Next Steps & Future Enhancements + +### Immediate Next Steps (After Phase 6) +1. Add file upload/download capabilities +2. Add file listing within folders +3. Add folder sharing/permissions +4. Add bulk operations (multi-select) + +### Future Enhancements +1. **Drag & Drop**: Drag folders to move them +2. **Grid/List Toggle**: Switch between grid and list views +3. **Sorting**: Sort by name, date, size +4. **Favorites**: Star/favorite folders for quick access +5. **Recent Items**: Show recently accessed folders +6. **Folder Colors/Icons**: Customize folder appearance +7. **Breadcrumb Dropdown**: Click breadcrumb to see siblings +8. **Quick Actions**: Right-click context menu +9. **Keyboard Navigation**: Arrow keys to navigate grid +10. **Undo/Redo**: Undo delete/move operations + +--- + +## Development Tips + +1. **Start with Phase 1-2**: Get the hook working with API integration first +2. **Test with Mock Data**: Use mock folder data while developing UI +3. **Progressive Enhancement**: Build basic functionality first, add polish later +4. **Follow Patterns**: Stick closely to existing component patterns (emails, cards) +5. **Use Existing Components**: Leverage Shadcn UI components already in the project +6. **Translations**: Add translation keys as you build each component +7. **Error Handling**: Always handle loading and error states +8. **Responsive Design**: Test on different screen sizes as you build + +--- + +## Known Limitations + +1. **No File Support Yet**: This implementation only handles folders +2. **No Permissions**: No folder-level permissions or sharing +3. **No Search**: Search functionality is optional in Phase 6 +4. **No Bulk Operations**: Can only operate on one folder at a time (except delete) +5. **No Drag & Drop**: Manual move operation required +6. **No Sorting**: Folders displayed in API order +7. **Single Selection**: Cannot select multiple folders for bulk operations + +--- + +## Conclusion + +This implementation plan provides a comprehensive, phase-by-phase approach to building the Drive feature. Each phase builds upon the previous one, following the established patterns in the codebase. The implementation prioritizes: + +- **Consistency**: Follows existing code patterns +- **Maintainability**: Clean separation of concerns +- **User Experience**: Intuitive navigation and actions +- **Scalability**: Ready for future enhancements + +Follow each phase sequentially, test thoroughly, and refer back to this document as your single source of truth for the Drive implementation. + diff --git a/packages/frontend-web/app/[locale]/dashboard/drive/page.tsx b/packages/frontend-web/app/[locale]/dashboard/drive/page.tsx new file mode 100644 index 00000000..474ae595 --- /dev/null +++ b/packages/frontend-web/app/[locale]/dashboard/drive/page.tsx @@ -0,0 +1,13 @@ +import { DashboardLayout } from "@/components/dashboard-layout"; +import { DriveContent } from "@/components/drive-content"; + +export default async function DrivePage({ params }: { params: Promise<{ locale: string }> }) { + const { locale } = await params; + + return ( + + + + ); +} + diff --git a/packages/frontend-web/components/add-folder-dialog.tsx b/packages/frontend-web/components/add-folder-dialog.tsx new file mode 100644 index 00000000..7f8cac0b --- /dev/null +++ b/packages/frontend-web/components/add-folder-dialog.tsx @@ -0,0 +1,150 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { AlertCircle } from "lucide-react"; +import { useTranslator } from "@/hooks/use-translations"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; + +interface AddFolderDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onFolderCreated: () => void; + parentFolder: { folder_id: string; name: string } | null; + onCreateFolder: (name: string, parentId: string | null) => Promise; +} + +export function AddFolderDialog({ + open, + onOpenChange, + onFolderCreated, + parentFolder, + onCreateFolder, +}: AddFolderDialogProps) { + const { translate } = useTranslator(); + const [folderName, setFolderName] = useState(""); + const [error, setError] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + const validateForm = () => { + if (!folderName.trim()) { + setError( + translate("folder_name_required", "drive", { + default: "Folder name is required", + }) + ); + return false; + } + setError(""); + return true; + }; + + const handleSubmit = async () => { + if (!validateForm()) return; + + setIsSubmitting(true); + try { + await onCreateFolder(folderName, parentFolder?.folder_id || null); + resetForm(); + onFolderCreated(); + onOpenChange(false); + } catch (error) { + console.error("Error creating folder:", error); + setError( + translate("error_creating_folder", "drive", { + default: "Error creating folder. Please try again.", + }) + ); + } finally { + setIsSubmitting(false); + } + }; + + const resetForm = () => { + setFolderName(""); + setError(""); + }; + + useEffect(() => { + if (open) { + resetForm(); + } + }, [open]); + + return ( + + + + + {translate("create_folder", "drive", { default: "Create Folder" })} + + + {parentFolder + ? translate("create_subfolder_in", "drive", { + default: `Create a new folder in "${parentFolder.name}"`, + folderName: parentFolder.name, + }) + : translate("create_new_folder", "drive", { + default: "Create a new folder in root", + })} + + + +
+ {error && ( +
+ +
{error}
+
+ )} + +
+ + setFolderName(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && !isSubmitting) { + handleSubmit(); + } + }} + autoFocus + /> +
+
+ + + + + +
+
+ ); +} + diff --git a/packages/frontend-web/components/drive-content.tsx b/packages/frontend-web/components/drive-content.tsx new file mode 100644 index 00000000..ea06acfa --- /dev/null +++ b/packages/frontend-web/components/drive-content.tsx @@ -0,0 +1,371 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { useSelector } from "react-redux"; +import { RootState } from "@/libs/Redux/store"; +import { Button } from "@/components/ui/button"; +import { + Folder as FolderIcon, + Plus, + MoreHorizontal, + ArrowLeft, + Home, + Pencil, + Move, + Trash2, +} from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { useTranslator } from "@/hooks/use-translations"; +import { useDriveManagement } from "@/hooks/use-drive-management"; +import { AddFolderDialog } from "./add-folder-dialog"; +import { RenameFolderDialog } from "./rename-folder-dialog"; +import { MoveFolderDialog } from "./move-folder-dialog"; + +interface Folder { + folder_id: string; + name: string; + parent_id: string | null; + created_at: string; + updated_at: string | null; +} + +export function DriveContent() { + const { translate } = useTranslator(); + const selectedWorkspaceId = useSelector( + (state: RootState) => state.workspace.selectedWorkspaceId + ); + const selectedProjectId = useSelector( + (state: RootState) => state.workspace.selectedProjectId + ); + + const { + folders, + isLoading, + createFolder, + renameFolder, + moveFolder, + deleteFolders, + fetchFolders, + currentFolder, + setCurrentFolder, + getFolderPath, + getSubfolders, + } = useDriveManagement({ + selectedWorkspaceId, + selectedProjectId, + }); + + const [showAddFolder, setShowAddFolder] = useState(false); + const [showRenameFolder, setShowRenameFolder] = useState(false); + const [showMoveFolder, setShowMoveFolder] = useState(false); + const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false); + const [selectedFolder, setSelectedFolder] = useState(null); + const [isProcessingDelete, setIsProcessingDelete] = useState(false); + + // Get current subfolders to display + const currentSubfolders = getSubfolders(currentFolder?.folder_id || null); + + // Breadcrumb path + const breadcrumbPath = getFolderPath(currentFolder?.folder_id || null); + + const handleFolderClick = (folder: Folder) => { + setCurrentFolder(folder); + }; + + const handleBackClick = () => { + if (!currentFolder) return; + + if (currentFolder.parent_id) { + const parentFolder = folders.find(f => f.folder_id === currentFolder.parent_id); + setCurrentFolder(parentFolder || null); + } else { + setCurrentFolder(null); + } + }; + + const handleHomeClick = () => { + setCurrentFolder(null); + }; + + const handleCreateFolder = () => { + setShowAddFolder(true); + }; + + const handleRenameFolder = (folder: Folder) => { + setSelectedFolder(folder); + setShowRenameFolder(true); + }; + + const handleMoveFolder = (folder: Folder) => { + setSelectedFolder(folder); + setShowMoveFolder(true); + }; + + const confirmDelete = (folder: Folder) => { + setSelectedFolder(folder); + setShowDeleteConfirmation(true); + }; + + const handleDeleteFolder = async () => { + if (!selectedFolder) return; + + setIsProcessingDelete(true); + try { + await deleteFolders([selectedFolder.folder_id]); + setShowDeleteConfirmation(false); + setSelectedFolder(null); + } finally { + setIsProcessingDelete(false); + } + }; + + // Keyboard shortcuts + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // Check if any dialog is open or input is focused + const isDialogOpen = showAddFolder || showRenameFolder || showMoveFolder || showDeleteConfirmation; + const isInputFocused = document.activeElement?.tagName === 'INPUT' || + document.activeElement?.tagName === 'TEXTAREA'; + + if (isDialogOpen || isInputFocused) return; + + // Cmd/Ctrl + N to create new folder + if ((e.metaKey || e.ctrlKey) && e.key === "n") { + e.preventDefault(); + setShowAddFolder(true); + } + + // Backspace to go back + if (e.key === "Backspace" && currentFolder) { + e.preventDefault(); + handleBackClick(); + } + }; + + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [currentFolder, showAddFolder, showRenameFolder, showMoveFolder, showDeleteConfirmation]); + + return ( +
+ {/* Header */} +
+
+

+ {translate("drive", "drive", { default: "Drive" })} +

+

+ {translate("manage_your_folders", "drive", { + default: "Organize and manage your folders", + })} +

+
+
+ + {/* Breadcrumb and Actions */} +
+
+ + + {breadcrumbPath.map((folder) => ( +
+ / + +
+ ))} +
+ +
+ {currentFolder && ( + + )} + +
+
+ + {/* Folders Grid */} +
+ {isLoading ? ( +
+

+ {translate("loading_folders", "drive", { default: "Loading folders..." })} +

+
+ ) : currentSubfolders.length > 0 ? ( +
+ {currentSubfolders.map((folder) => ( +
+
handleFolderClick(folder)} + > + + + {folder.name} + +
+ + {/* Context Menu */} + + + + + + handleFolderClick(folder)}> + {translate("open", "drive", { default: "Open" })} + + handleRenameFolder(folder)}> + + {translate("rename", "drive", { default: "Rename" })} + + handleMoveFolder(folder)}> + + {translate("move", "drive", { default: "Move" })} + + confirmDelete(folder)} + > + + {translate("delete", "drive", { default: "Delete" })} + + + +
+ ))} +
+ ) : ( +
+ +

+ {translate("no_folders", "drive", { + default: "No folders in this location", + })} +

+ +
+ )} +
+ + {/* Dialogs */} + + + {selectedFolder && ( + <> + + + + + )} + + {/* Delete Confirmation */} + + + + + {translate("confirm_delete", "drive", { default: "Confirm deletion" })} + + + {translate("delete_folder_confirmation", "drive", { + default: `Are you sure you want to delete "${selectedFolder?.name}"? This action cannot be undone.`, + folderName: selectedFolder?.name, + })} + + + + + {translate("cancel", "drive", { default: "Cancel" })} + + { + e.preventDefault(); + handleDeleteFolder(); + }} + disabled={isProcessingDelete} + className="bg-destructive hover:bg-destructive/90" + > + {isProcessingDelete + ? translate("deleting", "drive", { default: "Deleting..." }) + : translate("delete", "drive", { default: "Delete" })} + + + + +
+ ); +} + diff --git a/packages/frontend-web/components/move-folder-dialog.tsx b/packages/frontend-web/components/move-folder-dialog.tsx new file mode 100644 index 00000000..03c0ea49 --- /dev/null +++ b/packages/frontend-web/components/move-folder-dialog.tsx @@ -0,0 +1,182 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { AlertCircle, Folder as FolderIcon } from "lucide-react"; +import { useTranslator } from "@/hooks/use-translations"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { cn } from "@/libs/utils"; + +interface Folder { + folder_id: string; + name: string; + parent_id: string | null; +} + +interface MoveFolderDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onFolderMoved: () => void; + selectedFolders: Folder[]; + allFolders: Folder[]; + onMoveFolder: (folderIds: string[], parentId: string) => Promise; +} + +export function MoveFolderDialog({ + open, + onOpenChange, + onFolderMoved, + selectedFolders, + allFolders, + onMoveFolder, +}: MoveFolderDialogProps) { + const { translate } = useTranslator(); + const [selectedDestination, setSelectedDestination] = useState(null); + const [error, setError] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Filter out folders being moved and their descendants + const availableDestinations = allFolders.filter(folder => { + const selectedIds = selectedFolders.map(f => f.folder_id); + if (selectedIds.includes(folder.folder_id)) return false; + + // Check if this folder is a descendant of any selected folder + let current = folder; + while (current.parent_id) { + if (selectedIds.includes(current.parent_id)) return false; + const parent = allFolders.find(f => f.folder_id === current.parent_id); + if (!parent) break; + current = parent; + } + return true; + }); + + const handleSubmit = async () => { + if (!selectedDestination) { + setError( + translate("select_destination", "drive", { + default: "Please select a destination folder", + }) + ); + return; + } + + setIsSubmitting(true); + try { + await onMoveFolder( + selectedFolders.map(f => f.folder_id), + selectedDestination + ); + onFolderMoved(); + onOpenChange(false); + setSelectedDestination(null); + } catch (error) { + console.error("Error moving folder:", error); + setError( + translate("error_moving_folder", "drive", { + default: "Error moving folder(s). Please try again.", + }) + ); + } finally { + setIsSubmitting(false); + } + }; + + const handleOpenChange = (open: boolean) => { + if (!open) { + setSelectedDestination(null); + setError(""); + } + onOpenChange(open); + }; + + return ( + + + + + {translate("move_folder", "drive", { default: "Move Folder" })} + + + {translate("move_folder_description", "drive", { + default: `Moving ${selectedFolders.length} folder(s)`, + count: selectedFolders.length, + })} + + + +
+ {error && ( +
+ +
{error}
+
+ )} + +
+ + +
+ + + {availableDestinations.map((folder) => ( + + ))} +
+
+
+
+ + + + + +
+
+ ); +} + diff --git a/packages/frontend-web/components/project-dialog.tsx b/packages/frontend-web/components/project-dialog.tsx index 9bd9b11d..165ed336 100644 --- a/packages/frontend-web/components/project-dialog.tsx +++ b/packages/frontend-web/components/project-dialog.tsx @@ -4,7 +4,7 @@ import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; -import { Plus, Edit, Trash, Check, ChevronRight, User } from "lucide-react"; +import { Plus, Edit, Trash, Check, ChevronRight, User, Folder } from "lucide-react"; import { Dialog, DialogContent, @@ -54,6 +54,7 @@ const defaultFeatures = { ssh_key: { enabled: false, is_client_side_encryption: false }, env: { enabled: false, is_client_side_encryption: false }, notes: { enabled: false, is_client_side_encryption: false }, + drive: { enabled: false, is_client_side_encryption: false }, }; // Define featureMenuItems to include icons for each module @@ -298,6 +299,12 @@ const featureMenuItems: { ), }, + { + key: "drive", + labelKey: "drive", + path: "/dashboard/drive", + icon: , + }, ]; export function ProjectDialog({ onClose, forceCreate = false }: ProjectDialogProps) { diff --git a/packages/frontend-web/components/rename-folder-dialog.tsx b/packages/frontend-web/components/rename-folder-dialog.tsx new file mode 100644 index 00000000..e64256d7 --- /dev/null +++ b/packages/frontend-web/components/rename-folder-dialog.tsx @@ -0,0 +1,141 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { AlertCircle } from "lucide-react"; +import { useTranslator } from "@/hooks/use-translations"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; + +interface Folder { + folder_id: string; + name: string; + parent_id: string | null; +} + +interface RenameFolderDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onFolderRenamed: () => void; + folder: Folder; + onRenameFolder: (folderId: string, newName: string, parentId: string | null) => Promise; +} + +export function RenameFolderDialog({ + open, + onOpenChange, + onFolderRenamed, + folder, + onRenameFolder, +}: RenameFolderDialogProps) { + const { translate } = useTranslator(); + const [folderName, setFolderName] = useState(folder.name); + const [error, setError] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + useEffect(() => { + setFolderName(folder.name); + }, [folder]); + + const validateForm = () => { + if (!folderName.trim()) { + setError( + translate("folder_name_required", "drive", { + default: "Folder name is required", + }) + ); + return false; + } + setError(""); + return true; + }; + + const handleSubmit = async () => { + if (!validateForm()) return; + + setIsSubmitting(true); + try { + await onRenameFolder(folder.folder_id, folderName, folder.parent_id); + onFolderRenamed(); + onOpenChange(false); + } catch (error) { + console.error("Error renaming folder:", error); + setError( + translate("error_renaming_folder", "drive", { + default: "Error renaming folder. Please try again.", + }) + ); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + + + {translate("rename_folder", "drive", { default: "Rename Folder" })} + + + {translate("rename_folder_description", "drive", { + default: `Rename "${folder.name}"`, + folderName: folder.name, + })} + + + +
+ {error && ( +
+ +
{error}
+
+ )} + +
+ + setFolderName(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && !isSubmitting) { + handleSubmit(); + } + }} + autoFocus + /> +
+
+ + + + + +
+
+ ); +} + diff --git a/packages/frontend-web/components/sidebar-nav.tsx b/packages/frontend-web/components/sidebar-nav.tsx index 6368b88e..20a5b7e9 100644 --- a/packages/frontend-web/components/sidebar-nav.tsx +++ b/packages/frontend-web/components/sidebar-nav.tsx @@ -9,6 +9,7 @@ import { ChevronRight, Clock, FileText, + Folder, Globe, HardDrive, Home, @@ -178,6 +179,13 @@ const navigationCategories: NavigationCategory[] = [ ), feature_key: "notes" + }, + { + key: "drive", + labelKey: "drive", + path: "/dashboard/drive", + icon: , + feature_key: "drive" } ] }, @@ -352,7 +360,9 @@ const defaultFeatures = { software_license: { enabled: false, is_client_side_encryption: false }, email: { enabled: false, is_client_side_encryption: false }, ssh_key: { enabled: false, is_client_side_encryption: false }, - env: { enabled: false, is_client_side_encryption: false } + env: { enabled: false, is_client_side_encryption: false }, + notes: { enabled: false, is_client_side_encryption: false }, + drive: { enabled: false, is_client_side_encryption: false } } export function SidebarNav({ diff --git a/packages/frontend-web/hooks/use-drive-management.ts b/packages/frontend-web/hooks/use-drive-management.ts new file mode 100644 index 00000000..9efa757f --- /dev/null +++ b/packages/frontend-web/hooks/use-drive-management.ts @@ -0,0 +1,293 @@ +"use client"; + +import { useState, useEffect, useCallback, useMemo } from "react"; +import { useSelector } from "react-redux"; +import { RootState } from "@/libs/Redux/store"; +import axiosInstance from "@/libs/Middleware/axiosInstace"; +import { toast } from "@/components/ui/use-toast"; +import { useTranslator } from "@/hooks/use-translations"; + +interface Folder { + folder_id: string; + name: string; + parent_id: string | null; + created_at: string; + updated_at: string | null; + created_by: string; + workspace_id: string; + project_id: string; +} + +interface UseDriveManagementProps { + selectedWorkspaceId: string | null; + selectedProjectId: string | null; +} + +interface UseDriveManagementReturn { + folders: Folder[]; + isLoading: boolean; + createFolder: (name: string, parentId?: string | null) => Promise; + renameFolder: (folderId: string, newName: string, parentId?: string | null) => Promise; + moveFolder: (folderIds: string[], parentId: string) => Promise; + deleteFolders: (folderIds: string[]) => Promise; + fetchFolders: () => Promise; + currentFolder: Folder | null; + setCurrentFolder: (folder: Folder | null) => void; + getFolderPath: (folderId: string | null) => Folder[]; + getSubfolders: (parentId: string | null) => Folder[]; +} + +export function useDriveManagement({ + selectedWorkspaceId, + selectedProjectId, +}: UseDriveManagementProps): UseDriveManagementReturn { + const { translate } = useTranslator(); + const [folders, setFolders] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [currentFolder, setCurrentFolder] = useState(null); + + // Fetch folders from API + const fetchFolders = useCallback(async () => { + if (!selectedWorkspaceId || !selectedProjectId) { + setIsLoading(false); + return; + } + + setIsLoading(true); + + try { + // TODO: Replace with actual GET endpoint when available + // Recommended endpoint: GET /api/v1/web/drive/folder/list + // const response = await axiosInstance.get( + // `/api/v1/web/drive/folder/list`, + // { + // params: { + // workspace_id: selectedWorkspaceId, + // project_id: selectedProjectId, + // }, + // } + // ); + // setFolders(response.data.data || []); + + console.log("Fetching folders for workspace", selectedWorkspaceId, "project", selectedProjectId); + + // For now, use empty array until API is ready + setFolders([]); + } catch (error) { + console.error("Error fetching folders:", error); + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("error_fetching_folders", "drive", { + default: "Failed to fetch folders", + }), + variant: "destructive", + }); + setFolders([]); + } finally { + setIsLoading(false); + } + }, [selectedWorkspaceId, selectedProjectId, translate]); + + // Create folder + const createFolder = useCallback( + async (name: string, parentId: string | null = null) => { + if (!selectedWorkspaceId || !selectedProjectId) { + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("missing_workspace_project", "drive", { + default: "Missing workspace or project", + }), + variant: "destructive", + }); + return; + } + + try { + const payload: any = { name }; + if (parentId) { + payload.parent_id = parentId; + } + + await axiosInstance.post( + `/api/v1/web/drive/folder/create`, + payload + ); + + toast({ + title: translate("success", "drive", { default: "Success" }), + description: translate("folder_created", "drive", { + default: "Folder created successfully", + }), + }); + + // Refresh folders list + await fetchFolders(); + } catch (error) { + console.error("Error creating folder:", error); + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("error_creating_folder", "drive", { + default: "Failed to create folder", + }), + variant: "destructive", + }); + throw error; + } + }, + [selectedWorkspaceId, selectedProjectId, fetchFolders, translate] + ); + + // Rename folder + const renameFolder = useCallback( + async (folderId: string, newName: string, parentId: string | null = null) => { + try { + const payload: any = { + name: newName, + folder_id: folderId, + }; + if (parentId) { + payload.parent_id = parentId; + } + + await axiosInstance.post( + `/api/v1/web/drive/folder/rename`, + payload + ); + + toast({ + title: translate("success", "drive", { default: "Success" }), + description: translate("folder_renamed", "drive", { + default: "Folder renamed successfully", + }), + }); + + await fetchFolders(); + } catch (error) { + console.error("Error renaming folder:", error); + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("error_renaming_folder", "drive", { + default: "Failed to rename folder", + }), + variant: "destructive", + }); + throw error; + } + }, + [fetchFolders, translate] + ); + + // Move folder(s) + const moveFolder = useCallback( + async (folderIds: string[], parentId: string) => { + try { + await axiosInstance.post( + `/api/v1/web/drive/folder/move`, + { + folder_ids: folderIds, + parent_id: parentId, + } + ); + + toast({ + title: translate("success", "drive", { default: "Success" }), + description: translate("folder_moved", "drive", { + default: `${folderIds.length} folder(s) moved successfully`, + }), + }); + + await fetchFolders(); + } catch (error) { + console.error("Error moving folder:", error); + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("error_moving_folder", "drive", { + default: "Failed to move folder(s)", + }), + variant: "destructive", + }); + throw error; + } + }, + [fetchFolders, translate] + ); + + // Delete folder(s) + const deleteFolders = useCallback( + async (folderIds: string[]) => { + try { + await axiosInstance.delete( + `/api/v1/web/drive/folder/delete`, + { + data: { folder_ids: folderIds } + } + ); + + toast({ + title: translate("success", "drive", { default: "Success" }), + description: translate("folder_deleted", "drive", { + default: `${folderIds.length} folder(s) deleted successfully`, + }), + }); + + await fetchFolders(); + } catch (error) { + console.error("Error deleting folder:", error); + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("error_deleting_folder", "drive", { + default: "Failed to delete folder(s)", + }), + variant: "destructive", + }); + throw error; + } + }, + [fetchFolders, translate] + ); + + // Get breadcrumb path for a folder + const getFolderPath = useCallback((folderId: string | null): Folder[] => { + if (!folderId) return []; + + const path: Folder[] = []; + let currentId: string | null = folderId; + + while (currentId) { + const folder = folders.find(f => f.folder_id === currentId); + if (!folder) break; + path.unshift(folder); + currentId = folder.parent_id; + } + + return path; + }, [folders]); + + // Get subfolders of a parent + const getSubfolders = useCallback((parentId: string | null): Folder[] => { + return folders.filter(f => f.parent_id === parentId); + }, [folders]); + + // Effect to fetch data when dependencies change + useEffect(() => { + if (selectedWorkspaceId && selectedProjectId) { + console.log("Running fetchFolders effect"); + fetchFolders(); + } + }, [selectedWorkspaceId, selectedProjectId, fetchFolders]); + + return { + folders, + isLoading, + createFolder, + renameFolder, + moveFolder, + deleteFolders, + fetchFolders, + currentFolder, + setCurrentFolder, + getFolderPath, + getSubfolders, + }; +} + diff --git a/packages/frontend-web/messages/en/common.json b/packages/frontend-web/messages/en/common.json index 4ccfae03..6b9996c2 100644 --- a/packages/frontend-web/messages/en/common.json +++ b/packages/frontend-web/messages/en/common.json @@ -1420,5 +1420,53 @@ "fuchsia": "Fuchsia", "pink": "Pink", "rose": "Rose" + }, + "drive": { + "drive": "Drive", + "manage_your_folders": "Organize and manage your folders", + "create_folder": "Create Folder", + "new_folder": "New Folder", + "folder_name": "Folder Name", + "folder_name_placeholder": "My Folder", + "folder_name_required": "Folder name is required", + "rename_folder": "Rename Folder", + "move_folder": "Move Folder", + "delete_folder": "Delete Folder", + "no_folders": "No folders in this location", + "loading_folders": "Loading folders...", + "root": "Root", + "back": "Back", + "open": "Open", + "rename": "Rename", + "move": "Move", + "delete": "Delete", + "cancel": "Cancel", + "create": "Create", + "creating": "Creating...", + "renaming": "Renaming...", + "moving": "Moving...", + "deleting": "Deleting...", + "success": "Success", + "error": "Error", + "folder_created": "Folder created successfully", + "folder_renamed": "Folder renamed successfully", + "folder_moved": "Folder(s) moved successfully", + "folder_deleted": "Folder(s) deleted successfully", + "error_creating_folder": "Failed to create folder", + "error_renaming_folder": "Failed to rename folder", + "error_moving_folder": "Failed to move folder(s)", + "error_deleting_folder": "Failed to delete folder(s)", + "error_fetching_folders": "Failed to fetch folders", + "confirm_delete": "Confirm deletion", + "delete_folder_confirmation": "Are you sure you want to delete this folder? This action cannot be undone.", + "select_destination": "Please select a destination folder", + "destination_folder": "Select destination folder", + "root_folder": "Root", + "create_new_folder": "Create a new folder in root", + "create_subfolder_in": "Create a new folder in \"{folderName}\"", + "rename_folder_description": "Rename \"{folderName}\"", + "move_folder_description": "Moving {count} folder(s)", + "missing_workspace_project": "Missing workspace or project", + "open_menu": "Open menu" } } \ No newline at end of file From 7bc28945258968056577e0d25de697dfaa972067 Mon Sep 17 00:00:00 2001 From: ananducv Date: Tue, 7 Oct 2025 12:34:21 +0530 Subject: [PATCH 12/32] depth issue while creating folder fixed --- packages/frontend-web/hooks/use-drive-management.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/frontend-web/hooks/use-drive-management.ts b/packages/frontend-web/hooks/use-drive-management.ts index 9efa757f..851c5392 100644 --- a/packages/frontend-web/hooks/use-drive-management.ts +++ b/packages/frontend-web/hooks/use-drive-management.ts @@ -86,7 +86,7 @@ export function useDriveManagement({ } finally { setIsLoading(false); } - }, [selectedWorkspaceId, selectedProjectId, translate]); + }, [selectedWorkspaceId, selectedProjectId]); // Create folder const createFolder = useCallback( @@ -134,7 +134,7 @@ export function useDriveManagement({ throw error; } }, - [selectedWorkspaceId, selectedProjectId, fetchFolders, translate] + [selectedWorkspaceId, selectedProjectId, fetchFolders] ); // Rename folder @@ -174,7 +174,7 @@ export function useDriveManagement({ throw error; } }, - [fetchFolders, translate] + [fetchFolders] ); // Move folder(s) @@ -209,7 +209,7 @@ export function useDriveManagement({ throw error; } }, - [fetchFolders, translate] + [fetchFolders] ); // Delete folder(s) @@ -243,7 +243,7 @@ export function useDriveManagement({ throw error; } }, - [fetchFolders, translate] + [fetchFolders] ); // Get breadcrumb path for a folder From 87d172b3ea9773bc2208d695fdeee8d89aca4a52 Mon Sep 17 00:00:00 2001 From: ananducv Date: Tue, 7 Oct 2025 12:49:29 +0530 Subject: [PATCH 13/32] api url construction issue fixed --- packages/frontend-web/hooks/use-drive-management.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/frontend-web/hooks/use-drive-management.ts b/packages/frontend-web/hooks/use-drive-management.ts index 851c5392..21fbff39 100644 --- a/packages/frontend-web/hooks/use-drive-management.ts +++ b/packages/frontend-web/hooks/use-drive-management.ts @@ -57,9 +57,9 @@ export function useDriveManagement({ try { // TODO: Replace with actual GET endpoint when available - // Recommended endpoint: GET /api/v1/web/drive/folder/list + // Recommended endpoint: GET /drive/folder/list // const response = await axiosInstance.get( - // `/api/v1/web/drive/folder/list`, + // `/drive/folder/list`, // { // params: { // workspace_id: selectedWorkspaceId, @@ -109,7 +109,7 @@ export function useDriveManagement({ } await axiosInstance.post( - `/api/v1/web/drive/folder/create`, + `/drive/folder/create`, payload ); @@ -150,7 +150,7 @@ export function useDriveManagement({ } await axiosInstance.post( - `/api/v1/web/drive/folder/rename`, + `/drive/folder/rename`, payload ); @@ -182,7 +182,7 @@ export function useDriveManagement({ async (folderIds: string[], parentId: string) => { try { await axiosInstance.post( - `/api/v1/web/drive/folder/move`, + `/drive/folder/move`, { folder_ids: folderIds, parent_id: parentId, @@ -217,7 +217,7 @@ export function useDriveManagement({ async (folderIds: string[]) => { try { await axiosInstance.delete( - `/api/v1/web/drive/folder/delete`, + `/drive/folder/delete`, { data: { folder_ids: folderIds } } From 6974bcb638fa78654b17ec1e76f21ae9ef20ec4e Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Tue, 7 Oct 2025 15:29:48 +0530 Subject: [PATCH 14/32] Update schema.py --- packages/backend-server/app/api/v1/web/drive/folders/schema.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/backend-server/app/api/v1/web/drive/folders/schema.py b/packages/backend-server/app/api/v1/web/drive/folders/schema.py index 4d5d1224..8f5766f9 100644 --- a/packages/backend-server/app/api/v1/web/drive/folders/schema.py +++ b/packages/backend-server/app/api/v1/web/drive/folders/schema.py @@ -1,10 +1,11 @@ +from token import OP from pydantic import BaseModel, Field from typing import Optional, List from datetime import datetime class CreateFolder(BaseModel): name: str - parent_id: str + parent_id: Optional[str] class RenameFolder(BaseModel): name: str From 8a48034d0bd58d26caf398045864342bc54d694a Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Tue, 7 Oct 2025 15:30:27 +0530 Subject: [PATCH 15/32] Update schema.py --- packages/backend-server/app/api/v1/web/drive/folders/schema.py | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend-server/app/api/v1/web/drive/folders/schema.py b/packages/backend-server/app/api/v1/web/drive/folders/schema.py index 8f5766f9..df17d56d 100644 --- a/packages/backend-server/app/api/v1/web/drive/folders/schema.py +++ b/packages/backend-server/app/api/v1/web/drive/folders/schema.py @@ -1,4 +1,3 @@ -from token import OP from pydantic import BaseModel, Field from typing import Optional, List from datetime import datetime From 39a8ca89c04b5e6fb5b5dbe00b21b7b8657b56ab Mon Sep 17 00:00:00 2001 From: ananducv Date: Tue, 7 Oct 2025 16:16:26 +0530 Subject: [PATCH 16/32] api payload uploaded to not include parent id in root folder --- packages/frontend-web/hooks/use-drive-management.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend-web/hooks/use-drive-management.ts b/packages/frontend-web/hooks/use-drive-management.ts index 21fbff39..76ffb6dd 100644 --- a/packages/frontend-web/hooks/use-drive-management.ts +++ b/packages/frontend-web/hooks/use-drive-management.ts @@ -103,7 +103,7 @@ export function useDriveManagement({ } try { - const payload: any = { name }; + const payload: { name: string; parent_id?: string } = { name }; if (parentId) { payload.parent_id = parentId; } @@ -141,7 +141,7 @@ export function useDriveManagement({ const renameFolder = useCallback( async (folderId: string, newName: string, parentId: string | null = null) => { try { - const payload: any = { + const payload: { name: string; folder_id: string; parent_id?: string } = { name: newName, folder_id: folderId, }; From e06b3459bd404a6a0ce8cc70bde385fe859e29f4 Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Tue, 7 Oct 2025 16:54:59 +0530 Subject: [PATCH 17/32] files --- .../app/api/v1/web/drive/folders/schema.py | 2 +- packages/backend-server/app/locales/en.json | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/backend-server/app/api/v1/web/drive/folders/schema.py b/packages/backend-server/app/api/v1/web/drive/folders/schema.py index df17d56d..b12de497 100644 --- a/packages/backend-server/app/api/v1/web/drive/folders/schema.py +++ b/packages/backend-server/app/api/v1/web/drive/folders/schema.py @@ -4,7 +4,7 @@ class CreateFolder(BaseModel): name: str - parent_id: Optional[str] + parent_id: Optional[str]= None class RenameFolder(BaseModel): name: str diff --git a/packages/backend-server/app/locales/en.json b/packages/backend-server/app/locales/en.json index 35884ec1..ecdbc480 100644 --- a/packages/backend-server/app/locales/en.json +++ b/packages/backend-server/app/locales/en.json @@ -150,8 +150,19 @@ "folders":{ "created": "Folder created successfully", "deleted": "Folder deleted successfully", + "renamed": "Folder renamed successfully", + "moved": "Folder moved successfully", "already_exists": "Folder already exists" + }, + "files":{ + "deleted": "File deleted successfully", + "renamed": "File renamed successfully", + "moved": "File moved successfully", + "already_exists": "File already exists" } + }, + "file":{ + "get_presigned_url": "Presigned URL generated successfully" } } From 64d7e8c6fdaf7ce4bd62fd2cc1304bad83501952 Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Tue, 7 Oct 2025 17:38:08 +0530 Subject: [PATCH 18/32] files --- .../app/api/v1/web/drive/files/__init__.py | 0 .../app/api/v1/web/drive/files/api.py | 27 ++++ .../app/api/v1/web/drive/files/schema.py | 23 ++++ .../app/api/v1/web/drive/files/services.py | 117 ++++++++++++++++++ packages/backend-server/app/core/config.py | 5 + packages/backend-server/app/utils/s3_utils.py | 38 ++++++ packages/backend-server/app/utils/utils.py | 11 ++ 7 files changed, 221 insertions(+) create mode 100644 packages/backend-server/app/api/v1/web/drive/files/__init__.py create mode 100644 packages/backend-server/app/api/v1/web/drive/files/api.py create mode 100644 packages/backend-server/app/api/v1/web/drive/files/schema.py create mode 100644 packages/backend-server/app/api/v1/web/drive/files/services.py create mode 100644 packages/backend-server/app/utils/s3_utils.py diff --git a/packages/backend-server/app/api/v1/web/drive/files/__init__.py b/packages/backend-server/app/api/v1/web/drive/files/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/packages/backend-server/app/api/v1/web/drive/files/api.py b/packages/backend-server/app/api/v1/web/drive/files/api.py new file mode 100644 index 00000000..fa6a23ab --- /dev/null +++ b/packages/backend-server/app/api/v1/web/drive/files/api.py @@ -0,0 +1,27 @@ +from fastapi import APIRouter,Request,Depends,UploadFile +from app.api.v1.web.drive.files.schema import RenameFile, MoveFile, DeleteFiles, GetPresignedUrl +from app.api.v1.web.auth.schema import UserDetails +from app.framework.permission_services.service import get_current_user +from app.api.v1.web.drive.files.services import rename_file, get_presigned_url, delete_files, move_files + +router = APIRouter() + +FILE_URL ="/file" + +@router.post(FILE_URL+"/get-presigned-url") +async def get_presigned_url_api(payload: GetPresignedUrl, user: UserDetails = Depends(get_current_user)): + return await get_presigned_url(user, payload.model_dump()) + +@router.post(FILE_URL+"/rename") +async def rename_file_api(payload: RenameFile, user: UserDetails = Depends(get_current_user)): + return await rename_file(user, payload.model_dump()) + + +@router.delete(FILE_URL+"/delete") +async def delete_files_api(payload: DeleteFiles, user: UserDetails = Depends(get_current_user)): + return await delete_files(user, payload.model_dump()) + + +@router.post(FILE_URL+"/move") +async def move_files_api(payload: MoveFile, user: UserDetails = Depends(get_current_user)): + return await move_files(user, payload.model_dump()) diff --git a/packages/backend-server/app/api/v1/web/drive/files/schema.py b/packages/backend-server/app/api/v1/web/drive/files/schema.py new file mode 100644 index 00000000..a4283531 --- /dev/null +++ b/packages/backend-server/app/api/v1/web/drive/files/schema.py @@ -0,0 +1,23 @@ +from pydantic import BaseModel, Field +from typing import Optional, List +from datetime import datetime + + +class RenameFile(BaseModel): + name: str = Field(..., description="File name") + file_id: str = Field(..., description="File ID") + parent_id: Optional[str] = Field(None, description="Parent folder ID, None for root") + +class MoveFile(BaseModel): + file_ids: List[str] = Field(..., description="File IDs") + parent_id: str = Field(..., description="Parent folder ID") + +class DeleteFiles(BaseModel): + file_ids: List[str] = Field(..., description="File IDs") + +class GetPresignedUrl(BaseModel): + file_type: str = Field(..., description="File type") + name: str = Field(..., description="File name") + size: int = Field(..., description="File size") + file_path: str = Field(..., description="File path") + parent_id: Optional[str] = Field(None, description="Parent folder ID") \ No newline at end of file diff --git a/packages/backend-server/app/api/v1/web/drive/files/services.py b/packages/backend-server/app/api/v1/web/drive/files/services.py new file mode 100644 index 00000000..37156d7f --- /dev/null +++ b/packages/backend-server/app/api/v1/web/drive/files/services.py @@ -0,0 +1,117 @@ +from app.framework.mongo_db import base_manager as db_manager +from app.managers.collection_names import FILES, FOLDERS +from app.utils.utils import response_helper, create_uuid, get_file_extension, get_folders_from_path, create_timestamp +from app.utils.i8ns import translate +from app.utils.s3_utils import generate_upload_url + +async def get_presigned_url(user, payload): + db = user.get("db") + user_id = user.get("user_id") + file_name = payload.get("name") + + # Check if file already exists for this user + query = { + "lower_name": file_name.strip().lower(), + "created_by": user_id + } + if payload.get("parent_id"): + query["parent_id"] = payload.get("parent_id") + + file = db_manager.find_one(db, FILES, query) + if file: + return response_helper(400, translate("drive.files.already_exists")) + + # Generate file paths + file_id = create_uuid() + file_extension = get_file_extension(file_name) + final_file_path = f"{user_id}/files/{file_id}.{file_extension}" + + # Get folders from path + folders = get_folders_from_path(payload.get("file_path")) + parent_id = payload.get("parent_id") + + # Create folder hierarchy if folders exist + for folder_name in folders: + query = { + "created_by": user_id, + "lower_name": folder_name.strip().lower() + } + if parent_id: + query["parent_id"] = parent_id + + folder_details = db_manager.find_one(db, FOLDERS, query) + if not folder_details: + # Create new folder + folder_data = { + "doc_id": create_uuid(), + "name": folder_name.strip(), + "parent_id": parent_id, + "created_by": user_id, + "lower_name": folder_name.strip().lower(), + } + db_manager.insert_one(db, FOLDERS, folder_data) + parent_id = folder_data.get("doc_id") + else: + # Use existing folder + parent_id = folder_details.get("doc_id") + + # Insert file record (always, regardless of whether folders exist) + file_data = { + "doc_id": file_id, + "type": payload.get("file_type"), + "name": file_name, + "lower_name": file_name.strip().lower(), + "size": payload.get("size"), + "path": payload.get("file_path"), + "key": final_file_path, + "parent_id": parent_id, + "created_by": user_id, + } + db_manager.insert_one(db, FILES, file_data) + + # Generate presigned upload URL + upload_url = generate_upload_url(final_file_path) + + return response_helper(200, translate("file.get_presigned_url"), data=upload_url) + + +async def rename_file(user, payload): + db = user.get("db") + name = payload.get("name").strip() + file_id = payload.get("file_id") + lower_name = name.lower() + query = {"created_by": user.get("user_id"), "lower_name": lower_name, "doc_id": {"$ne": file_id}} + + if payload.get("parent_id"): + query["parent_id"] = payload.get("parent_id") + + file = db_manager.find_one(db, FILES, query) + if file: + return response_helper(400, translate("drive.files.already_exists")) + + db_manager.update_one(db, FILES, {"doc_id": file_id, "created_by": user.get("user_id")}, {"$set": {"name": name, "lower_name": lower_name}}) + return response_helper(200, translate("drive.files.renamed")) + + +async def delete_files(user, payload): + db = user.get("db") + file_ids = payload.get("file_ids") + user_id = user.get("user_id") + db_manager.update_many(db, FILES, {"doc_id": {"$in": file_ids}, "created_by": user_id}, {"$set": {"access": False, "delete_by": user_id, "deleted_at": create_timestamp()}}) + return response_helper(200, translate("drive.files.deleted")) + + +async def move_files(user, payload): + db = user.get("db") + file_ids = payload.get("file_ids") + parent_id = payload.get("parent_id") + query = {"created_by": user.get("user_id"), "doc_id": {"$in": file_ids}} + files = db_manager.find(db, FILES, query) + files_moved = [] + for item in files: + individual_query = {"created_by": user.get("user_id"), "parent_id": parent_id, "lower_name": item.get("lower_name")} + if not db_manager.find_one(db, FILES, individual_query): + db_manager.update_one(db, FILES, {"doc_id": item.get("doc_id")}, {"$set": {"parent_id": parent_id}}) + files_moved.append(item.get("name")) + + return response_helper(200, translate("drive.files.moved"), files_moved=files_moved) diff --git a/packages/backend-server/app/core/config.py b/packages/backend-server/app/core/config.py index 6d49ab9e..39f27e54 100644 --- a/packages/backend-server/app/core/config.py +++ b/packages/backend-server/app/core/config.py @@ -15,6 +15,11 @@ class Settings(BaseSettings): VALKEY_URL: str + DO_SPACES_KEY:str + DO_SPACES_SECRET:str + DO_SPACES_REGION:str + DO_SPACES_BUCKET:str + DO_SPACES_ENDPOINT:str class Config: case_sensitive = True env_file = ".env" diff --git a/packages/backend-server/app/utils/s3_utils.py b/packages/backend-server/app/utils/s3_utils.py new file mode 100644 index 00000000..917d64fb --- /dev/null +++ b/packages/backend-server/app/utils/s3_utils.py @@ -0,0 +1,38 @@ +import boto3 +from app.core.config import settings + + +# DigitalOcean Spaces config +session = boto3.session.Session() +client = session.client( + "s3", + region_name=settings.DO_SPACES_REGION, + endpoint_url=f"https://{settings.DO_SPACES_REGION}.digitaloceanspaces.com", + aws_access_key_id=settings.DO_SPACES_KEY, + aws_secret_access_key=settings.DO_SPACES_SECRET, +) + +BUCKET_NAME = settings.DO_SPACES_BUCKET + + +def generate_upload_url(file_name,expires_in=3600): + try: + url = client.generate_presigned_url( + "put_object", + Params={"Bucket": BUCKET_NAME, "Key": file_name}, + ExpiresIn=expires_in, + ) + return {"upload_url": url} + except Exception as e: + return {"error": str(e)} + +def generate_download_url(file_name,expires_in=3600): + try: + url = client.generate_presigned_url( + "get_object", + Params={"Bucket": BUCKET_NAME, "Key": file_name}, + ExpiresIn=expires_in, + ) + return {"download_url": url} + except Exception as e: + return {"error": str(e)} \ No newline at end of file diff --git a/packages/backend-server/app/utils/utils.py b/packages/backend-server/app/utils/utils.py index 9f43745c..c71f2345 100644 --- a/packages/backend-server/app/utils/utils.py +++ b/packages/backend-server/app/utils/utils.py @@ -6,7 +6,9 @@ import json from datetime import datetime import pytz +import os +from pathlib import Path def response_helper( @@ -63,3 +65,12 @@ def generate_query_hash(query, projection=None): def create_timestamp(): return datetime.now(pytz.utc) + +def get_file_extension(filename): + _, ext = os.path.splitext(filename) + return ext.lstrip(".") + + + +def get_folders_from_path(path: str): + return [part for part in Path(path).parent.parts] \ No newline at end of file From 0058080a072116e4c1c8b6cb54074875552e2b39 Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Thu, 9 Oct 2025 10:58:27 +0530 Subject: [PATCH 19/32] folders list --- .../app/api/v1/web/drive/api.py | 4 +++- .../app/api/v1/web/drive/folders/api.py | 8 ++++++-- .../app/api/v1/web/drive/folders/services.py | 19 +++++++++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/packages/backend-server/app/api/v1/web/drive/api.py b/packages/backend-server/app/api/v1/web/drive/api.py index 843855bd..33d81166 100644 --- a/packages/backend-server/app/api/v1/web/drive/api.py +++ b/packages/backend-server/app/api/v1/web/drive/api.py @@ -1,9 +1,11 @@ from fastapi import APIRouter from app.api.v1.web.drive.folders import api as folders_router +from app.api.v1.web.drive.files import api as files_router router = APIRouter() router.prefix = "/drive" -router.include_router(folders_router.router, tags=["Drive: Folders"]) \ No newline at end of file +router.include_router(folders_router.router, tags=["Drive: Folders"]) +router.include_router(files_router.router, tags=["Drive: Files"]) \ No newline at end of file diff --git a/packages/backend-server/app/api/v1/web/drive/folders/api.py b/packages/backend-server/app/api/v1/web/drive/folders/api.py index faed047b..76c144f3 100644 --- a/packages/backend-server/app/api/v1/web/drive/folders/api.py +++ b/packages/backend-server/app/api/v1/web/drive/folders/api.py @@ -1,8 +1,8 @@ -from fastapi import APIRouter,Depends +from fastapi import APIRouter,Depends,Query from app.api.v1.web.drive.folders.schema import CreateFolder, RenameFolder, MoveFolder, DeleteFolders from app.api.v1.web.auth.schema import UserDetails from app.framework.permission_services.service import get_current_user -from app.api.v1.web.drive.folders.services import create_folder,delete_folders,rename_folder, move_folders +from app.api.v1.web.drive.folders.services import create_folder,delete_folders,rename_folder, move_folders, get_folders_list FOLDER_URL ="/folder" router = APIRouter() @@ -26,3 +26,7 @@ async def rename_folder_api(payload: RenameFolder, user: UserDetails = Depends(g async def move_folder_api(payload: MoveFolder, user: UserDetails = Depends(get_current_user)): return await move_folders(user, payload.model_dump()) + +@router.get(FOLDER_URL+"/list") +async def get_all_folders_api(parent_id: str = Query(None, description="Parent folder ID", ge=1), user: UserDetails = Depends(get_current_user)): + return await get_folders_list(user, parent_id) \ No newline at end of file diff --git a/packages/backend-server/app/api/v1/web/drive/folders/services.py b/packages/backend-server/app/api/v1/web/drive/folders/services.py index d645d76d..9fa0a5e5 100644 --- a/packages/backend-server/app/api/v1/web/drive/folders/services.py +++ b/packages/backend-server/app/api/v1/web/drive/folders/services.py @@ -73,3 +73,22 @@ async def move_folders(user, payload): db_manager.update_many(db, FOLDERS, {"doc_id": {"$in":folder_ids}, "created_by": user.get("user_id")}, {"$set": {"parent_id": parent_id}}) return response_helper(200, translate("drive.folders.moved"), folders_moved=folders_moved) +async def get_folders_list(user, parent_id): + db = user.get("db") + query = {"created_by": user.get("user_id")} + if parent_id: + query["parent_id"] = parent_id + + folders = db_manager.find(db, FOLDERS, query) + + for folder in folders: + parent_query={"parent_id": folder.get("doc_id"),"created_by": user.get("user_id")} + files = db_manager.find(db, FILES,parent_query ) + folder["files"] = files + folder["sub_folders"] = db_manager.find(db, FOLDERS, parent_query) + + data= { + "folders": folders, + "files": db_manager.find(db, FILES, query) + } + return response_helper(200, translate("drive.folders.list"), data=data) \ No newline at end of file From 67f124b0896f44148457a7b8e99eb010073302a3 Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Thu, 9 Oct 2025 13:48:59 +0530 Subject: [PATCH 20/32] Update requirements.txt --- packages/backend-server/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/backend-server/requirements.txt b/packages/backend-server/requirements.txt index 46a5bc4b..c86e669e 100644 --- a/packages/backend-server/requirements.txt +++ b/packages/backend-server/requirements.txt @@ -17,3 +17,4 @@ cryptography fastapi-limiter pyotp valkey +boto3 From da321b315c911ab02f6d9047d767d077ecd1acd1 Mon Sep 17 00:00:00 2001 From: ananducv Date: Thu, 9 Oct 2025 15:15:26 +0530 Subject: [PATCH 21/32] all folder apis integrated --- .../hooks/use-drive-management.ts | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/frontend-web/hooks/use-drive-management.ts b/packages/frontend-web/hooks/use-drive-management.ts index 76ffb6dd..e12c07f6 100644 --- a/packages/frontend-web/hooks/use-drive-management.ts +++ b/packages/frontend-web/hooks/use-drive-management.ts @@ -56,23 +56,30 @@ export function useDriveManagement({ setIsLoading(true); try { - // TODO: Replace with actual GET endpoint when available - // Recommended endpoint: GET /drive/folder/list - // const response = await axiosInstance.get( - // `/drive/folder/list`, - // { - // params: { - // workspace_id: selectedWorkspaceId, - // project_id: selectedProjectId, - // }, - // } - // ); - // setFolders(response.data.data || []); - console.log("Fetching folders for workspace", selectedWorkspaceId, "project", selectedProjectId); - // For now, use empty array until API is ready - setFolders([]); + // Fetch all folders (parent_id is optional, omitting it returns all folders) + const response = await axiosInstance.get(`/drive/folder/list`); + + console.log("API Response:", response.data); + + // The API returns folders at response.data.data.folders + const apiFolders = response.data?.data?.folders || []; + + // Map API response to match our Folder interface (doc_id -> folder_id) + const mappedFolders: Folder[] = apiFolders.map((folder: any) => ({ + folder_id: folder.doc_id, + name: folder.name, + parent_id: folder.parent_id, + created_at: folder.created_at, + updated_at: folder.updated_at, + created_by: folder.created_by, + workspace_id: folder.workspace_id || selectedWorkspaceId, + project_id: folder.project_id || selectedProjectId, + })); + + console.log("Mapped folders:", mappedFolders); + setFolders(mappedFolders); } catch (error) { console.error("Error fetching folders:", error); toast({ From e22f023d4a2a448b3b55d87337a77160078d861d Mon Sep 17 00:00:00 2001 From: ananducv Date: Thu, 9 Oct 2025 18:14:09 +0530 Subject: [PATCH 22/32] file upload apis done (remaining preview and download) --- .../components/delete-file-dialog.tsx | 93 ++++++ .../frontend-web/components/drive-content.tsx | 137 ++++++++- .../frontend-web/components/file-card.tsx | 112 +++++++ .../components/move-file-dialog.tsx | 155 ++++++++++ .../components/rename-file-dialog.tsx | 143 +++++++++ .../components/upload-file-dialog.tsx | 246 +++++++++++++++ .../hooks/use-drive-management.ts | 288 +++++++++++++++++- packages/frontend-web/libs/file-encryption.ts | 139 +++++++++ .../frontend-web/libs/file-icon-mappings.tsx | 82 +++++ packages/frontend-web/libs/utils.ts | 18 ++ packages/frontend-web/messages/en/common.json | 24 +- 11 files changed, 1420 insertions(+), 17 deletions(-) create mode 100644 packages/frontend-web/components/delete-file-dialog.tsx create mode 100644 packages/frontend-web/components/file-card.tsx create mode 100644 packages/frontend-web/components/move-file-dialog.tsx create mode 100644 packages/frontend-web/components/rename-file-dialog.tsx create mode 100644 packages/frontend-web/components/upload-file-dialog.tsx create mode 100644 packages/frontend-web/libs/file-encryption.ts create mode 100644 packages/frontend-web/libs/file-icon-mappings.tsx diff --git a/packages/frontend-web/components/delete-file-dialog.tsx b/packages/frontend-web/components/delete-file-dialog.tsx new file mode 100644 index 00000000..75fbf6ba --- /dev/null +++ b/packages/frontend-web/components/delete-file-dialog.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { useState } from "react"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { useTranslator } from "@/hooks/use-translations"; + +interface DriveFile { + file_id: string; + name: string; +} + +interface DeleteFileDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + file: DriveFile | null; + onDelete: (fileIds: string[]) => Promise; +} + +export function DeleteFileDialog({ + open, + onOpenChange, + file, + onDelete, +}: DeleteFileDialogProps) { + const { translate } = useTranslator(); + const [isDeleting, setIsDeleting] = useState(false); + + const handleDelete = async () => { + if (!file) return; + + setIsDeleting(true); + + try { + await onDelete([file.file_id]); + onOpenChange(false); + } catch (error) { + console.error("Error deleting file:", error); + } finally { + setIsDeleting(false); + } + }; + + return ( + + + + + {translate("delete_file", "drive", { default: "Delete File" })} + + + {file && ( + <> + {translate("delete_file_confirmation", "drive", { + default: "Are you sure you want to delete this file? This action cannot be undone.", + })} +
+
+ {file.name} + + )} +
+
+ + + {translate("cancel", "actions", { default: "Cancel" })} + + { + e.preventDefault(); + handleDelete(); + }} + disabled={isDeleting} + className="bg-destructive hover:bg-destructive/90" + > + {isDeleting + ? translate("deleting", "drive", { default: "Deleting..." }) + : translate("delete", "actions", { default: "Delete" })} + + +
+
+ ); +} + diff --git a/packages/frontend-web/components/drive-content.tsx b/packages/frontend-web/components/drive-content.tsx index ea06acfa..fdeca1e5 100644 --- a/packages/frontend-web/components/drive-content.tsx +++ b/packages/frontend-web/components/drive-content.tsx @@ -13,6 +13,7 @@ import { Pencil, Move, Trash2, + Upload as UploadIcon, } from "lucide-react"; import { DropdownMenu, @@ -35,6 +36,11 @@ import { useDriveManagement } from "@/hooks/use-drive-management"; import { AddFolderDialog } from "./add-folder-dialog"; import { RenameFolderDialog } from "./rename-folder-dialog"; import { MoveFolderDialog } from "./move-folder-dialog"; +import { UploadFileDialog } from "./upload-file-dialog"; +import { FileCard } from "./file-card"; +import { RenameFileDialog } from "./rename-file-dialog"; +import { MoveFileDialog } from "./move-file-dialog"; +import { DeleteFileDialog } from "./delete-file-dialog"; interface Folder { folder_id: string; @@ -44,6 +50,21 @@ interface Folder { updated_at: string | null; } +interface DriveFile { + file_id: string; + name: string; + size: number; + original_size: number; + type: string; + iv: string; + parent_id: string | null; + created_at: string; + updated_at: string | null; + created_by: string; + workspace_id: string; + project_id: string; +} + export function DriveContent() { const { translate } = useTranslator(); const selectedWorkspaceId = useSelector( @@ -55,6 +76,7 @@ export function DriveContent() { const { folders, + files, isLoading, createFolder, renameFolder, @@ -65,6 +87,12 @@ export function DriveContent() { setCurrentFolder, getFolderPath, getSubfolders, + uploadFile, + renameFile, + moveFile, + deleteFiles, + isUploading, + uploadProgress, } = useDriveManagement({ selectedWorkspaceId, selectedProjectId, @@ -77,9 +105,19 @@ export function DriveContent() { const [selectedFolder, setSelectedFolder] = useState(null); const [isProcessingDelete, setIsProcessingDelete] = useState(false); + // File dialog states + const [showUploadFile, setShowUploadFile] = useState(false); + const [showRenameFile, setShowRenameFile] = useState(false); + const [showMoveFile, setShowMoveFile] = useState(false); + const [showDeleteFile, setShowDeleteFile] = useState(false); + const [selectedFile, setSelectedFile] = useState(null); + // Get current subfolders to display const currentSubfolders = getSubfolders(currentFolder?.folder_id || null); + // Get current files to display + const currentFiles = files.filter(f => f.parent_id === (currentFolder?.folder_id || null)); + // Breadcrumb path const breadcrumbPath = getFolderPath(currentFolder?.folder_id || null); @@ -134,6 +172,26 @@ export function DriveContent() { } }; + // File handlers + const handleUploadFile = () => { + setShowUploadFile(true); + }; + + const handleRenameFile = (file: DriveFile) => { + setSelectedFile(file); + setShowRenameFile(true); + }; + + const handleMoveFile = (file: DriveFile) => { + setSelectedFile(file); + setShowMoveFile(true); + }; + + const handleDeleteFile = (file: DriveFile) => { + setSelectedFile(file); + setShowDeleteFile(true); + }; + // Keyboard shortcuts useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -216,6 +274,10 @@ export function DriveContent() { {translate("back", "drive", { default: "Back" })} )} + +
+ + +
)} - {/* Dialogs */} + {/* Folder Dialogs */} )} + {/* File Dialogs */} + + + {selectedFile && ( + <> + + + + + + + )} + {/* Delete Confirmation */} diff --git a/packages/frontend-web/components/file-card.tsx b/packages/frontend-web/components/file-card.tsx new file mode 100644 index 00000000..a1e185f8 --- /dev/null +++ b/packages/frontend-web/components/file-card.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { useState } from "react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Button } from "@/components/ui/button"; +import { MoreVertical, Edit2, Move, Trash2 } from "lucide-react"; +import { getFileIcon, getFileIconColor } from "@/libs/file-icon-mappings"; +import { formatBytes } from "@/libs/utils"; +import { formatDate } from "@/libs/utils"; +import { useTranslator } from "@/hooks/use-translations"; + +interface DriveFile { + file_id: string; + name: string; + size: number; + original_size: number; + type: string; + created_at: string; + updated_at: string | null; +} + +interface FileCardProps { + file: DriveFile; + onRename: (file: DriveFile) => void; + onMove: (file: DriveFile) => void; + onDelete: (file: DriveFile) => void; +} + +export function FileCard({ file, onRename, onMove, onDelete }: FileCardProps) { + const { translate } = useTranslator(); + const [showMenu, setShowMenu] = useState(false); + + const FileIcon = getFileIcon(file.type); + const iconColor = getFileIconColor(file.type); + + return ( +
+
+ {/* File icon */} +
+ +
+ + {/* File name */} +
+

+ {file.name} +

+

+ {formatBytes(file.original_size)} +

+

+ {formatDate(file.created_at)} +

+
+ + {/* Actions menu */} +
+ + + + + + { + onRename(file); + setShowMenu(false); + }} + > + + {translate("rename", "actions", { default: "Rename" })} + + { + onMove(file); + setShowMenu(false); + }} + > + + {translate("move", "actions", { default: "Move" })} + + + { + onDelete(file); + setShowMenu(false); + }} + className="text-destructive focus:text-destructive" + > + + {translate("delete", "actions", { default: "Delete" })} + + + +
+
+
+ ); +} + diff --git a/packages/frontend-web/components/move-file-dialog.tsx b/packages/frontend-web/components/move-file-dialog.tsx new file mode 100644 index 00000000..35ea1360 --- /dev/null +++ b/packages/frontend-web/components/move-file-dialog.tsx @@ -0,0 +1,155 @@ +"use client"; + +import { useState } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Folder as FolderIcon, Home } from "lucide-react"; +import { useTranslator } from "@/hooks/use-translations"; + +interface Folder { + folder_id: string; + name: string; + parent_id: string | null; +} + +interface DriveFile { + file_id: string; + name: string; + parent_id: string | null; +} + +interface MoveFileDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + file: DriveFile | null; + folders: Folder[]; + onMove: (fileIds: string[], parentId: string) => Promise; +} + +export function MoveFileDialog({ + open, + onOpenChange, + file, + folders, + onMove, +}: MoveFileDialogProps) { + const { translate } = useTranslator(); + const [selectedFolderId, setSelectedFolderId] = useState("root"); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Filter out folders that would create circular references + const availableFolders = folders.filter(folder => { + // Don't show the file's current parent + if (file && folder.folder_id === file.parent_id) return false; + return true; + }); + + const handleSubmit = async () => { + if (!file) return; + + setIsSubmitting(true); + + try { + const targetParentId = selectedFolderId === "root" ? "null" : selectedFolderId; + await onMove([file.file_id], targetParentId); + onOpenChange(false); + setSelectedFolderId("root"); + } catch (error) { + console.error("Error moving file:", error); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + + + {translate("move_file", "drive", { default: "Move File" })} + + + {file && ( + <> + {translate("moving", "drive", { default: "Moving" })}: {file.name} + + )} + + + +
+ + + + {/* Root option */} +
+ + +
+ + {/* Folder options */} + {availableFolders.map((folder) => ( +
+ + +
+ ))} +
+ + {availableFolders.length === 0 && ( +

+ {translate("no_folders_available", "drive", { + default: "No folders available", + })} +

+ )} +
+ + + + + +
+
+ ); +} + diff --git a/packages/frontend-web/components/rename-file-dialog.tsx b/packages/frontend-web/components/rename-file-dialog.tsx new file mode 100644 index 00000000..4c215bd7 --- /dev/null +++ b/packages/frontend-web/components/rename-file-dialog.tsx @@ -0,0 +1,143 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useTranslator } from "@/hooks/use-translations"; + +interface DriveFile { + file_id: string; + name: string; +} + +interface RenameFileDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + file: DriveFile | null; + onRename: (fileId: string, newName: string) => Promise; +} + +export function RenameFileDialog({ + open, + onOpenChange, + file, + onRename, +}: RenameFileDialogProps) { + const { translate } = useTranslator(); + const [newName, setNewName] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(""); + + // Initialize name when file changes + useEffect(() => { + if (file) { + setNewName(file.name); + setError(""); + } + }, [file]); + + const handleSubmit = async () => { + if (!file) return; + + // Validation + if (!newName.trim()) { + setError(translate("name_required", "drive", { default: "File name is required" })); + return; + } + + if (newName.trim() === file.name) { + setError(translate("name_unchanged", "drive", { default: "Please enter a different name" })); + return; + } + + setIsSubmitting(true); + setError(""); + + try { + await onRename(file.file_id, newName.trim()); + onOpenChange(false); + setNewName(""); + } catch (error) { + console.error("Error renaming file:", error); + setError(translate("rename_failed", "drive", { default: "Failed to rename file" })); + } finally { + setIsSubmitting(false); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !isSubmitting) { + e.preventDefault(); + handleSubmit(); + } + }; + + return ( + + + + + {translate("rename_file", "drive", { default: "Rename File" })} + + + {file && ( + <> + {translate("renaming", "drive", { default: "Renaming" })}: {file.name} + + )} + + + +
+
+ + { + setNewName(e.target.value); + setError(""); + }} + onKeyDown={handleKeyDown} + placeholder={translate("enter_file_name", "drive", { + default: "Enter file name", + })} + disabled={isSubmitting} + autoFocus + /> + {error && ( +

{error}

+ )} +
+
+ + + + + +
+
+ ); +} + diff --git a/packages/frontend-web/components/upload-file-dialog.tsx b/packages/frontend-web/components/upload-file-dialog.tsx new file mode 100644 index 00000000..03493983 --- /dev/null +++ b/packages/frontend-web/components/upload-file-dialog.tsx @@ -0,0 +1,246 @@ +"use client"; + +import { useState, useCallback } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Progress } from "@/components/ui/progress"; +import { Upload, X, FileIcon } from "lucide-react"; +import { useTranslator } from "@/hooks/use-translations"; +import { validateFileSize, formatBytes } from "@/libs/file-encryption"; + +interface Folder { + folder_id: string; + name: string; + parent_id: string | null; +} + +interface UploadFileDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onFileUploaded: () => void; + currentFolder: Folder | null; + uploadFile: (file: File, parentId?: string | null) => Promise; + isUploading: boolean; + uploadProgress: number; +} + +export function UploadFileDialog({ + open, + onOpenChange, + onFileUploaded, + currentFolder, + uploadFile, + isUploading, + uploadProgress, +}: UploadFileDialogProps) { + const { translate } = useTranslator(); + const [selectedFile, setSelectedFile] = useState(null); + const [dragActive, setDragActive] = useState(false); + const [error, setError] = useState(""); + + const handleFileSelect = useCallback((file: File | null) => { + setError(""); + + if (!file) { + setSelectedFile(null); + return; + } + + // Validate file size (50MB max) + if (!validateFileSize(file, 50)) { + setError(translate("file_too_large", "drive", { + default: "File size exceeds 50MB limit", + })); + setSelectedFile(null); + return; + } + + setSelectedFile(file); + }, [translate]); + + const handleDrag = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (e.type === "dragenter" || e.type === "dragover") { + setDragActive(true); + } else if (e.type === "dragleave") { + setDragActive(false); + } + }, []); + + const handleDrop = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setDragActive(false); + + if (e.dataTransfer.files && e.dataTransfer.files[0]) { + handleFileSelect(e.dataTransfer.files[0]); + } + }, [handleFileSelect]); + + const handleInputChange = useCallback((e: React.ChangeEvent) => { + if (e.target.files && e.target.files[0]) { + handleFileSelect(e.target.files[0]); + } + }, [handleFileSelect]); + + const handleUpload = async () => { + if (!selectedFile) return; + + try { + await uploadFile(selectedFile, currentFolder?.folder_id || null); + setSelectedFile(null); + setError(""); + onFileUploaded(); + onOpenChange(false); + } catch (error) { + console.error("Upload error:", error); + // Error is handled in the hook with a toast + } + }; + + const handleCancel = () => { + if (!isUploading) { + setSelectedFile(null); + setError(""); + onOpenChange(false); + } + }; + + return ( + + + + + {translate("upload_file", "drive", { default: "Upload File" })} + + + + {currentFolder + ? translate("upload_to_folder", "drive", { + default: `Upload a file to ${currentFolder.name}`, + }).replace("${currentFolder.name}", currentFolder.name) + : translate("upload_to_root", "drive", { + default: "Upload a file to root", + })} + + + {translate("max_file_size", "drive", { default: "Maximum file size: 50MB" })} + + + + +
+ {/* Drag and drop zone */} +
+ + +
+ +
+ + + {" "} + {translate("or_drag_drop", "drive", { default: "or drag and drop" })} + +
+
+
+ + {/* Selected file display */} + {selectedFile && !isUploading && ( +
+ +
+

+ {selectedFile.name} +

+

{formatBytes(selectedFile.size)}

+
+ +
+ )} + + {/* Upload progress */} + {isUploading && ( +
+
+ + {translate("uploading", "drive", { default: "Uploading..." })} + + {uploadProgress}% +
+ + {selectedFile && ( +

+ {selectedFile.name} +

+ )} +
+ )} + + {/* Error message */} + {error && ( +
+

{error}

+
+ )} +
+ + + + + +
+
+ ); +} + diff --git a/packages/frontend-web/hooks/use-drive-management.ts b/packages/frontend-web/hooks/use-drive-management.ts index e12c07f6..2132ae94 100644 --- a/packages/frontend-web/hooks/use-drive-management.ts +++ b/packages/frontend-web/hooks/use-drive-management.ts @@ -6,6 +6,8 @@ import { RootState } from "@/libs/Redux/store"; import axiosInstance from "@/libs/Middleware/axiosInstace"; import { toast } from "@/components/ui/use-toast"; import { useTranslator } from "@/hooks/use-translations"; +import { encryptFileBlob, validateFileSize } from "@/libs/file-encryption"; +import { secureGetItem } from "@/libs/local-storage-utils"; interface Folder { folder_id: string; @@ -18,6 +20,22 @@ interface Folder { project_id: string; } +interface DriveFile { + file_id: string; + name: string; + size: number; // encrypted size + original_size: number; // original file size before encryption + type: string; // MIME type + iv: string; // hex string for decryption + parent_id: string | null; + created_at: string; + updated_at: string | null; + created_by: string; + workspace_id: string; + project_id: string; + encrypted_data?: string; // base64 encoded encrypted blob (from API) +} + interface UseDriveManagementProps { selectedWorkspaceId: string | null; selectedProjectId: string | null; @@ -25,6 +43,7 @@ interface UseDriveManagementProps { interface UseDriveManagementReturn { folders: Folder[]; + files: DriveFile[]; isLoading: boolean; createFolder: (name: string, parentId?: string | null) => Promise; renameFolder: (folderId: string, newName: string, parentId?: string | null) => Promise; @@ -35,6 +54,12 @@ interface UseDriveManagementReturn { setCurrentFolder: (folder: Folder | null) => void; getFolderPath: (folderId: string | null) => Folder[]; getSubfolders: (parentId: string | null) => Folder[]; + uploadFile: (file: File, parentId?: string | null) => Promise; + renameFile: (fileId: string, newName: string) => Promise; + moveFile: (fileIds: string[], parentId: string) => Promise; + deleteFiles: (fileIds: string[]) => Promise; + isUploading: boolean; + uploadProgress: number; } export function useDriveManagement({ @@ -43,10 +68,16 @@ export function useDriveManagement({ }: UseDriveManagementProps): UseDriveManagementReturn { const { translate } = useTranslator(); const [folders, setFolders] = useState([]); + const [files, setFiles] = useState([]); const [isLoading, setIsLoading] = useState(true); const [currentFolder, setCurrentFolder] = useState(null); + const [isUploading, setIsUploading] = useState(false); + const [uploadProgress, setUploadProgress] = useState(0); + + // Get workspaces from Redux store for project name lookup + const workspaces = useSelector((state: RootState) => state.workspace.workspaces); - // Fetch folders from API + // Fetch folders and files from API const fetchFolders = useCallback(async () => { if (!selectedWorkspaceId || !selectedProjectId) { setIsLoading(false); @@ -65,6 +96,7 @@ export function useDriveManagement({ // The API returns folders at response.data.data.folders const apiFolders = response.data?.data?.folders || []; + const apiFiles = response.data?.data?.files || []; // Map API response to match our Folder interface (doc_id -> folder_id) const mappedFolders: Folder[] = apiFolders.map((folder: any) => ({ @@ -78,8 +110,27 @@ export function useDriveManagement({ project_id: folder.project_id || selectedProjectId, })); + // Map files from API response + const mappedFiles: DriveFile[] = apiFiles.map((file: any) => ({ + file_id: file.doc_id, + name: file.name, + size: file.size, + original_size: file.original_size || file.size, + type: file.type || 'application/octet-stream', + iv: file.iv || '', + parent_id: file.parent_id, + created_at: file.created_at, + updated_at: file.updated_at, + created_by: file.created_by, + workspace_id: file.workspace_id || selectedWorkspaceId, + project_id: file.project_id || selectedProjectId, + encrypted_data: file.data, + })); + console.log("Mapped folders:", mappedFolders); + console.log("Mapped files:", mappedFiles); setFolders(mappedFolders); + setFiles(mappedFiles); } catch (error) { console.error("Error fetching folders:", error); toast({ @@ -90,6 +141,7 @@ export function useDriveManagement({ variant: "destructive", }); setFolders([]); + setFiles([]); } finally { setIsLoading(false); } @@ -141,7 +193,7 @@ export function useDriveManagement({ throw error; } }, - [selectedWorkspaceId, selectedProjectId, fetchFolders] + [selectedWorkspaceId, selectedProjectId, fetchFolders, translate] ); // Rename folder @@ -181,7 +233,7 @@ export function useDriveManagement({ throw error; } }, - [fetchFolders] + [fetchFolders, translate] ); // Move folder(s) @@ -216,7 +268,7 @@ export function useDriveManagement({ throw error; } }, - [fetchFolders] + [fetchFolders, translate] ); // Delete folder(s) @@ -250,7 +302,7 @@ export function useDriveManagement({ throw error; } }, - [fetchFolders] + [fetchFolders, translate] ); // Get breadcrumb path for a folder @@ -275,16 +327,234 @@ export function useDriveManagement({ return folders.filter(f => f.parent_id === parentId); }, [folders]); + // Upload file with encryption + const uploadFile = useCallback( + async (file: File, parentId: string | null = null) => { + if (!selectedWorkspaceId || !selectedProjectId) { + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("missing_workspace_project", "drive", { + default: "Missing workspace or project", + }), + variant: "destructive", + }); + return; + } + + // 1. Validate file size (max 50MB) + if (!validateFileSize(file, 50)) { + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("file_too_large", "drive", { + default: "File size exceeds 50MB limit", + }), + variant: "destructive", + }); + return; + } + + setIsUploading(true); + setUploadProgress(0); + + try { + // 2. Get project info for key lookup + const currentProject = workspaces + .find(ws => ws.workspaceId === selectedWorkspaceId) + ?.projects.find(p => p.project_id === selectedProjectId); + + if (!currentProject) { + throw new Error("Project not found"); + } + + // 3. Get project AES key + const projectKeyName = `projectKey_${currentProject.name}`; + const projectKey = await secureGetItem(projectKeyName); + + if (!projectKey) { + throw new Error("Project encryption key not found"); + } + + setUploadProgress(10); + + // 4. Encrypt file blob + console.log("Encrypting file:", file.name); + const { encryptedBlob, iv, originalSize } = await encryptFileBlob(file, projectKey); + + setUploadProgress(40); + + // 5. Get presigned URL + console.log("Getting presigned URL"); + console.log("Payload data:", { + file_type: file.type, + name: file.name, + size: encryptedBlob.size, + original_size: originalSize, + iv: iv, + parent_id: parentId || undefined + }); + + const payload: any = { + file_type: file.type || 'application/octet-stream', + name: file.name, + file_path: file.name, // Use filename as file path + size: encryptedBlob.size.toString(), + iv: iv, // Critical: IV needed for backend to store for future decryption + parent_id: parentId || '', // Send empty string if null (backend may require it) + }; + + console.log("Sending payload:", payload); + const presignedResponse = await axiosInstance.post('/drive/file/get-presigned-url', payload); + + console.log("Presigned response:", presignedResponse); + const presignedUrl = presignedResponse.data; + + setUploadProgress(50); + + // 6. Upload encrypted blob to presigned URL + console.log("Uploading encrypted file to presigned URL"); + await fetch(presignedUrl, { + method: 'PUT', + body: encryptedBlob, + headers: { 'Content-Type': 'application/octet-stream' }, + }); + + setUploadProgress(90); + + toast({ + title: translate("success", "drive", { default: "Success" }), + description: translate("file_uploaded", "drive", { + default: "File uploaded successfully", + }), + }); + + // 7. Refresh file list + await fetchFolders(); + setUploadProgress(100); + } catch (error) { + console.error("Error uploading file:", error); + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("error_uploading_file", "drive", { + default: "Failed to upload file", + }), + variant: "destructive", + }); + throw error; + } finally { + setIsUploading(false); + setUploadProgress(0); + } + }, + [selectedWorkspaceId, selectedProjectId, workspaces, fetchFolders, translate] + ); + + // Rename file + const renameFile = useCallback( + async (fileId: string, newName: string) => { + try { + await axiosInstance.post('/drive/file/rename', { + name: newName, + file_id: fileId, + }); + + toast({ + title: translate("success", "drive", { default: "Success" }), + description: translate("file_renamed", "drive", { + default: "File renamed successfully", + }), + }); + + await fetchFolders(); + } catch (error) { + console.error("Error renaming file:", error); + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("error_renaming_file", "drive", { + default: "Failed to rename file", + }), + variant: "destructive", + }); + throw error; + } + }, + [fetchFolders, translate] + ); + + // Move file(s) + const moveFile = useCallback( + async (fileIds: string[], parentId: string) => { + try { + await axiosInstance.post('/drive/file/move', { + file_ids: fileIds, + parent_id: parentId, + }); + + toast({ + title: translate("success", "drive", { default: "Success" }), + description: translate("file_moved", "drive", { + default: `${fileIds.length} file(s) moved successfully`, + }), + }); + + await fetchFolders(); + } catch (error) { + console.error("Error moving file:", error); + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("error_moving_file", "drive", { + default: "Failed to move file(s)", + }), + variant: "destructive", + }); + throw error; + } + }, + [fetchFolders, translate] + ); + + // Delete file(s) + const deleteFiles = useCallback( + async (fileIds: string[]) => { + try { + await axiosInstance.delete('/drive/file/delete', { + data: { file_ids: fileIds } + }); + + toast({ + title: translate("success", "drive", { default: "Success" }), + description: translate("file_deleted", "drive", { + default: `${fileIds.length} file(s) deleted successfully`, + }), + }); + + await fetchFolders(); + } catch (error) { + console.error("Error deleting file:", error); + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("error_deleting_file", "drive", { + default: "Failed to delete file(s)", + }), + variant: "destructive", + }); + throw error; + } + }, + [fetchFolders, translate] + ); + // Effect to fetch data when dependencies change useEffect(() => { if (selectedWorkspaceId && selectedProjectId) { console.log("Running fetchFolders effect"); fetchFolders(); } - }, [selectedWorkspaceId, selectedProjectId, fetchFolders]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedWorkspaceId, selectedProjectId]); return { folders, + files, isLoading, createFolder, renameFolder, @@ -295,6 +565,12 @@ export function useDriveManagement({ setCurrentFolder, getFolderPath, getSubfolders, + uploadFile, + renameFile, + moveFile, + deleteFiles, + isUploading, + uploadProgress, }; } diff --git a/packages/frontend-web/libs/file-encryption.ts b/packages/frontend-web/libs/file-encryption.ts new file mode 100644 index 00000000..9e2b9565 --- /dev/null +++ b/packages/frontend-web/libs/file-encryption.ts @@ -0,0 +1,139 @@ +/** + * File encryption utilities for Drive feature + * Encrypts/decrypts file blobs using AES-GCM with project keys + */ + +import { importKey } from './encryption'; + +const MAX_FILE_SIZE_MB = 50; +const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024; + +/** + * Validate file size (max 50MB by default) + */ +export function validateFileSize(file: File, maxSizeMB: number = MAX_FILE_SIZE_MB): boolean { + const maxBytes = maxSizeMB * 1024 * 1024; + return file.size <= maxBytes; +} + +/** + * Read a file as ArrayBuffer using FileReader + */ +function readFileAsArrayBuffer(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + if (reader.result instanceof ArrayBuffer) { + resolve(reader.result); + } else { + reject(new Error('Failed to read file as ArrayBuffer')); + } + }; + reader.onerror = () => reject(reader.error); + reader.readAsArrayBuffer(file); + }); +} + +/** + * Encrypt a file blob using AES-GCM + * @param file The file to encrypt + * @param projectAesKey The project's AES key in hex format + * @returns Object with encrypted blob, IV (hex string), and original size + */ +export async function encryptFileBlob( + file: File, + projectAesKey: string +): Promise<{ encryptedBlob: Blob; iv: string; originalSize: number }> { + try { + // 1. Read file as ArrayBuffer + const fileBuffer = await readFileAsArrayBuffer(file); + const originalSize = fileBuffer.byteLength; + + // 2. Import the AES key + const rawKey = await importKey(projectAesKey); + + // 3. Generate random IV (12 bytes for AES-GCM) + const iv = window.crypto.getRandomValues(new Uint8Array(12)); + + // 4. Encrypt the file data + const encryptedBuffer = await window.crypto.subtle.encrypt( + { name: 'AES-GCM', iv }, + rawKey, + fileBuffer + ); + + // 5. Convert IV to hex string for storage + const ivHex = Array.from(iv) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + + // 6. Create blob from encrypted data + const encryptedBlob = new Blob([encryptedBuffer], { type: 'application/octet-stream' }); + + return { + encryptedBlob, + iv: ivHex, + originalSize, + }; + } catch (error) { + console.error('Error encrypting file blob:', error); + throw new Error('Failed to encrypt file'); + } +} + +/** + * Decrypt an encrypted file blob + * @param encryptedBlob The encrypted blob to decrypt + * @param ivHex The IV as hex string + * @param projectAesKey The project's AES key in hex format + * @returns Decrypted blob + */ +export async function decryptFileBlob( + encryptedBlob: Blob, + ivHex: string, + projectAesKey: string +): Promise { + try { + // 1. Read encrypted blob as ArrayBuffer + const encryptedBuffer = await encryptedBlob.arrayBuffer(); + + // 2. Convert hex IV to Uint8Array + const iv = new Uint8Array( + ivHex.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16)) + ); + + // 3. Import the AES key + const rawKey = await importKey(projectAesKey); + + // 4. Decrypt the data + const decryptedBuffer = await window.crypto.subtle.decrypt( + { name: 'AES-GCM', iv }, + rawKey, + encryptedBuffer + ); + + // 5. Create blob from decrypted data + const decryptedBlob = new Blob([decryptedBuffer]); + + return decryptedBlob; + } catch (error) { + console.error('Error decrypting file blob:', error); + throw new Error('Failed to decrypt file'); + } +} + +/** + * Format file size in human-readable format + */ +export function formatBytes(bytes: number, decimals: number = 2): string { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} + diff --git a/packages/frontend-web/libs/file-icon-mappings.tsx b/packages/frontend-web/libs/file-icon-mappings.tsx new file mode 100644 index 00000000..5f56700b --- /dev/null +++ b/packages/frontend-web/libs/file-icon-mappings.tsx @@ -0,0 +1,82 @@ +/** + * File type icon mappings for Drive feature + * Maps MIME types to appropriate Lucide icons + */ + +import { + FileIcon, + FileText, + Image, + Film, + Music, + Archive, + FileCode, + FileSpreadsheet, + FileType, + File +} from 'lucide-react'; + +/** + * Get the appropriate icon for a file based on its MIME type + */ +export function getFileIcon(mimeType: string) { + // Images + if (mimeType.startsWith('image/')) return Image; + + // Videos + if (mimeType.startsWith('video/')) return Film; + + // Audio + if (mimeType.startsWith('audio/')) return Music; + + // Documents + if (mimeType.includes('pdf')) return FileText; + if (mimeType.includes('word') || mimeType.includes('document')) return FileText; + if (mimeType.includes('text/')) return FileType; + + // Spreadsheets + if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) return FileSpreadsheet; + + // Code files + if ( + mimeType.includes('javascript') || + mimeType.includes('typescript') || + mimeType.includes('json') || + mimeType.includes('html') || + mimeType.includes('css') || + mimeType.includes('python') || + mimeType.includes('java') + ) { + return FileCode; + } + + // Archives + if ( + mimeType.includes('zip') || + mimeType.includes('compressed') || + mimeType.includes('tar') || + mimeType.includes('rar') || + mimeType.includes('7z') + ) { + return Archive; + } + + // Default + return FileIcon; +} + +/** + * Get color class for file icon based on MIME type + */ +export function getFileIconColor(mimeType: string): string { + if (mimeType.startsWith('image/')) return 'text-purple-500'; + if (mimeType.startsWith('video/')) return 'text-red-500'; + if (mimeType.startsWith('audio/')) return 'text-green-500'; + if (mimeType.includes('pdf')) return 'text-red-600'; + if (mimeType.includes('word') || mimeType.includes('document')) return 'text-blue-600'; + if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) return 'text-green-600'; + if (mimeType.includes('zip') || mimeType.includes('compressed')) return 'text-yellow-600'; + + return 'text-gray-500'; +} + diff --git a/packages/frontend-web/libs/utils.ts b/packages/frontend-web/libs/utils.ts index 39553a1d..cd169a6a 100644 --- a/packages/frontend-web/libs/utils.ts +++ b/packages/frontend-web/libs/utils.ts @@ -244,3 +244,21 @@ export function formatDate(date: Date | string): string { return "-"; } } + +/** + * Format file size in human-readable format + * @param bytes Number of bytes + * @param decimals Number of decimal places to show + * @returns Formatted file size string (e.g., "1.5 MB") + */ +export function formatBytes(bytes: number, decimals: number = 2): string { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} diff --git a/packages/frontend-web/messages/en/common.json b/packages/frontend-web/messages/en/common.json index 6b9996c2..e232b3ae 100644 --- a/packages/frontend-web/messages/en/common.json +++ b/packages/frontend-web/messages/en/common.json @@ -1467,6 +1467,28 @@ "rename_folder_description": "Rename \"{folderName}\"", "move_folder_description": "Moving {count} folder(s)", "missing_workspace_project": "Missing workspace or project", - "open_menu": "Open menu" + "open_menu": "Open menu", + "upload_file": "Upload File", + "no_items": "No folders or files in this location", + "upload_to_folder": "Upload to \"{folderName}\"", + "max_file_size": "Maximum file size: 50MB", + "choose_file": "Choose a file", + "or_drag_drop": "or drag and drop here", + "uploading": "Uploading...", + "error_uploading_file": "Failed to upload file", + "file_uploaded": "File uploaded successfully", + "upload": "Upload", + "file_too_large": "File is too large. Maximum size is 50MB", + "no_file_selected": "No file selected", + "rename_file": "Rename File", + "move_file": "Move File", + "delete_file": "Delete File", + "delete_file_confirmation": "Are you sure you want to delete this file? This action cannot be undone.", + "file_renamed": "File renamed successfully", + "file_moved": "File(s) moved successfully", + "file_deleted": "File(s) deleted successfully", + "error_renaming_file": "Failed to rename file", + "error_moving_file": "Failed to move file(s)", + "error_deleting_file": "Failed to delete file(s)" } } \ No newline at end of file From 5705c8ca44411081f9d28038227abe119d102fcb Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Fri, 10 Oct 2025 11:33:08 +0530 Subject: [PATCH 23/32] files --- .../app/api/v1/web/drive/files/services.py | 14 ++++++++++- .../app/api/v1/web/drive/folders/services.py | 24 ++++++++++++------- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/packages/backend-server/app/api/v1/web/drive/files/services.py b/packages/backend-server/app/api/v1/web/drive/files/services.py index 37156d7f..bb651768 100644 --- a/packages/backend-server/app/api/v1/web/drive/files/services.py +++ b/packages/backend-server/app/api/v1/web/drive/files/services.py @@ -2,7 +2,19 @@ from app.managers.collection_names import FILES, FOLDERS from app.utils.utils import response_helper, create_uuid, get_file_extension, get_folders_from_path, create_timestamp from app.utils.i8ns import translate -from app.utils.s3_utils import generate_upload_url +from app.utils.s3_utils import generate_upload_url, generate_download_url + + + +def get_files_list(user, parent_id=None): + db = user.get("db") + query = {"created_by": user.get("user_id")} + if parent_id: + query["parent_id"] = parent_id + files = db_manager.find(db, FILES, query) + for file in files: + file["file_url"]= generate_download_url(file.get("key"), 360000) + return files async def get_presigned_url(user, payload): db = user.get("db") diff --git a/packages/backend-server/app/api/v1/web/drive/folders/services.py b/packages/backend-server/app/api/v1/web/drive/folders/services.py index 9fa0a5e5..3c9dd280 100644 --- a/packages/backend-server/app/api/v1/web/drive/folders/services.py +++ b/packages/backend-server/app/api/v1/web/drive/folders/services.py @@ -3,6 +3,7 @@ from app.managers.collection_names import FILES, FOLDERS from app.utils.utils import response_helper, create_uuid, create_timestamp from app.utils.i8ns import translate +from app.api.v1.web.drive.files.services import get_files_list async def create_folder(user, payload): db = user.get("db") @@ -74,21 +75,28 @@ async def move_folders(user, payload): return response_helper(200, translate("drive.folders.moved"), folders_moved=folders_moved) async def get_folders_list(user, parent_id): - db = user.get("db") query = {"created_by": user.get("user_id")} if parent_id: query["parent_id"] = parent_id - folders = db_manager.find(db, FOLDERS, query) + folders = get_folders(user, parent_id) for folder in folders: - parent_query={"parent_id": folder.get("doc_id"),"created_by": user.get("user_id")} - files = db_manager.find(db, FILES,parent_query ) - folder["files"] = files - folder["sub_folders"] = db_manager.find(db, FOLDERS, parent_query) + folder["files"] = get_files_list(user, folder.get("doc_id")) + folder["sub_folders"] = get_folders(user, folder.get("doc_id")) data= { "folders": folders, - "files": db_manager.find(db, FILES, query) + "files": get_files_list(user, parent_id) } - return response_helper(200, translate("drive.folders.list"), data=data) \ No newline at end of file + return response_helper(200, translate("drive.folders.list"), data=data) + + + +def get_folders(user, parent_id=None): + db = user.get("db") + query = {"created_by": user.get("user_id")} + if parent_id: + query["parent_id"] = parent_id + folders = db_manager.find(db, FOLDERS, query) + return folders \ No newline at end of file From 66a8e51321410a98ff5626b94e06bd3d245534db Mon Sep 17 00:00:00 2001 From: ananducv Date: Fri, 10 Oct 2025 14:52:00 +0530 Subject: [PATCH 24/32] translations added for drive --- packages/frontend-web/messages/en/common.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/frontend-web/messages/en/common.json b/packages/frontend-web/messages/en/common.json index e232b3ae..12108f21 100644 --- a/packages/frontend-web/messages/en/common.json +++ b/packages/frontend-web/messages/en/common.json @@ -304,7 +304,9 @@ "update": "Update", "remove": "Remove", "error": "Error", - "view": "View" + "view": "View", + "rename": "Rename", + "move": "Move" }, "dashboard": { "project_limit_reached": "Project limit reached", @@ -396,6 +398,7 @@ "ssh_keys": "SSH Keys", "cards": "Cards", "software_licenses": "Software Licenses", + "drive": "Drive", "recent_accounts": "Recent Accounts", "no_recent_accounts": "No recent accounts", "last_24_hours": "Last 24 hours", @@ -1471,7 +1474,9 @@ "upload_file": "Upload File", "no_items": "No folders or files in this location", "upload_to_folder": "Upload to \"{folderName}\"", + "upload_to_root": "Upload a file to root", "max_file_size": "Maximum file size: 50MB", + "loading": "Loading...", "choose_file": "Choose a file", "or_drag_drop": "or drag and drop here", "uploading": "Uploading...", From adc4d1c508d572a43d333364dc80fa879323a61d Mon Sep 17 00:00:00 2001 From: ananducv Date: Fri, 10 Oct 2025 15:07:12 +0530 Subject: [PATCH 25/32] files in root not visible issue fix --- .../hooks/use-drive-management.ts | 22 ++++++++++++++----- packages/frontend-web/messages/en/common.json | 3 ++- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/frontend-web/hooks/use-drive-management.ts b/packages/frontend-web/hooks/use-drive-management.ts index 2132ae94..db2e4d6c 100644 --- a/packages/frontend-web/hooks/use-drive-management.ts +++ b/packages/frontend-web/hooks/use-drive-management.ts @@ -102,7 +102,7 @@ export function useDriveManagement({ const mappedFolders: Folder[] = apiFolders.map((folder: any) => ({ folder_id: folder.doc_id, name: folder.name, - parent_id: folder.parent_id, + parent_id: folder.parent_id || null, // Normalize empty string to null created_at: folder.created_at, updated_at: folder.updated_at, created_by: folder.created_by, @@ -118,7 +118,7 @@ export function useDriveManagement({ original_size: file.original_size || file.size, type: file.type || 'application/octet-stream', iv: file.iv || '', - parent_id: file.parent_id, + parent_id: file.parent_id || null, // Normalize empty string to null created_at: file.created_at, updated_at: file.updated_at, created_by: file.created_by, @@ -430,13 +430,23 @@ export function useDriveManagement({ // 7. Refresh file list await fetchFolders(); setUploadProgress(100); - } catch (error) { + } catch (error: any) { console.error("Error uploading file:", error); + + // Check if it's a "file already exists" error + const isFileExistsError = + error?.response?.data?.status_code === 400 && + error?.response?.data?.message?.toLowerCase().includes("file already exists"); + toast({ title: translate("error", "drive", { default: "Error" }), - description: translate("error_uploading_file", "drive", { - default: "Failed to upload file", - }), + description: isFileExistsError + ? translate("file_already_exists", "drive", { + default: "A file with this name already exists in this location", + }) + : translate("error_uploading_file", "drive", { + default: "Failed to upload file", + }), variant: "destructive", }); throw error; diff --git a/packages/frontend-web/messages/en/common.json b/packages/frontend-web/messages/en/common.json index 12108f21..b1e80a23 100644 --- a/packages/frontend-web/messages/en/common.json +++ b/packages/frontend-web/messages/en/common.json @@ -1473,7 +1473,7 @@ "open_menu": "Open menu", "upload_file": "Upload File", "no_items": "No folders or files in this location", - "upload_to_folder": "Upload to \"{folderName}\"", + "upload_to_folder": "Upload to {folderName}", "upload_to_root": "Upload a file to root", "max_file_size": "Maximum file size: 50MB", "loading": "Loading...", @@ -1485,6 +1485,7 @@ "upload": "Upload", "file_too_large": "File is too large. Maximum size is 50MB", "no_file_selected": "No file selected", + "file_already_exists": "A file with this name already exists in this location", "rename_file": "Rename File", "move_file": "Move File", "delete_file": "Delete File", From 39d7977bb5d0f0b312faaf1e0db650893f772ad6 Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Sat, 11 Oct 2025 17:23:43 +0530 Subject: [PATCH 26/32] Update s3_utils.py --- packages/backend-server/app/utils/s3_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend-server/app/utils/s3_utils.py b/packages/backend-server/app/utils/s3_utils.py index 917d64fb..d115d051 100644 --- a/packages/backend-server/app/utils/s3_utils.py +++ b/packages/backend-server/app/utils/s3_utils.py @@ -33,6 +33,6 @@ def generate_download_url(file_name,expires_in=3600): Params={"Bucket": BUCKET_NAME, "Key": file_name}, ExpiresIn=expires_in, ) - return {"download_url": url} + return url except Exception as e: - return {"error": str(e)} \ No newline at end of file + return None \ No newline at end of file From 8a3bd5a10e1aff0b0b5221b1806edcd205e5dc78 Mon Sep 17 00:00:00 2001 From: ananducv Date: Mon, 13 Oct 2025 12:36:28 +0530 Subject: [PATCH 27/32] file upload to digitalocean working (duplicate issue fix needed) --- .../frontend-web/components/drive-content.tsx | 18 + .../frontend-web/components/file-card.tsx | 14 +- .../components/upload-file-dialog.tsx | 5 +- .../drive_download_implementation.md | 550 ++++++++++++++++++ .../hooks/use-drive-management.ts | 277 ++++++++- packages/frontend-web/libs/file-encryption.ts | 52 ++ packages/frontend-web/messages/en/common.json | 19 +- packages/frontend-web/package-lock.json | 100 ++++ packages/frontend-web/package.json | 2 + 9 files changed, 1018 insertions(+), 19 deletions(-) create mode 100644 packages/frontend-web/drive_download_implementation.md diff --git a/packages/frontend-web/components/drive-content.tsx b/packages/frontend-web/components/drive-content.tsx index fdeca1e5..f9a69e18 100644 --- a/packages/frontend-web/components/drive-content.tsx +++ b/packages/frontend-web/components/drive-content.tsx @@ -14,6 +14,7 @@ import { Move, Trash2, Upload as UploadIcon, + Download, } from "lucide-react"; import { DropdownMenu, @@ -91,8 +92,12 @@ export function DriveContent() { renameFile, moveFile, deleteFiles, + downloadFile, + downloadFolder, isUploading, uploadProgress, + isDownloading, + downloadProgress, } = useDriveManagement({ selectedWorkspaceId, selectedProjectId, @@ -192,6 +197,14 @@ export function DriveContent() { setShowDeleteFile(true); }; + const handleDownloadFile = async (file: DriveFile) => { + await downloadFile(file.file_id); + }; + + const handleDownloadFolder = async (folder: Folder) => { + await downloadFolder(folder.folder_id); + }; + // Keyboard shortcuts useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -337,6 +350,10 @@ export function DriveContent() { {translate("move", "drive", { default: "Move" })} + handleDownloadFolder(folder)}> + + {translate("download", "drive", { default: "Download" })} + confirmDelete(folder)} @@ -357,6 +374,7 @@ export function DriveContent() { onRename={handleRenameFile} onMove={handleMoveFile} onDelete={handleDeleteFile} + onDownload={handleDownloadFile} /> ))} diff --git a/packages/frontend-web/components/file-card.tsx b/packages/frontend-web/components/file-card.tsx index a1e185f8..f202a369 100644 --- a/packages/frontend-web/components/file-card.tsx +++ b/packages/frontend-web/components/file-card.tsx @@ -9,7 +9,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Button } from "@/components/ui/button"; -import { MoreVertical, Edit2, Move, Trash2 } from "lucide-react"; +import { MoreVertical, Edit2, Move, Trash2, Download } from "lucide-react"; import { getFileIcon, getFileIconColor } from "@/libs/file-icon-mappings"; import { formatBytes } from "@/libs/utils"; import { formatDate } from "@/libs/utils"; @@ -30,9 +30,10 @@ interface FileCardProps { onRename: (file: DriveFile) => void; onMove: (file: DriveFile) => void; onDelete: (file: DriveFile) => void; + onDownload: (file: DriveFile) => void; } -export function FileCard({ file, onRename, onMove, onDelete }: FileCardProps) { +export function FileCard({ file, onRename, onMove, onDelete, onDownload }: FileCardProps) { const { translate } = useTranslator(); const [showMenu, setShowMenu] = useState(false); @@ -91,6 +92,15 @@ export function FileCard({ file, onRename, onMove, onDelete }: FileCardProps) { {translate("move", "actions", { default: "Move" })} + { + onDownload(file); + setShowMenu(false); + }} + > + + {translate("download", "actions", { default: "Download" })} + { diff --git a/packages/frontend-web/components/upload-file-dialog.tsx b/packages/frontend-web/components/upload-file-dialog.tsx index 03493983..86f023f3 100644 --- a/packages/frontend-web/components/upload-file-dialog.tsx +++ b/packages/frontend-web/components/upload-file-dialog.tsx @@ -125,8 +125,9 @@ export function UploadFileDialog({ {currentFolder ? translate("upload_to_folder", "drive", { - default: `Upload a file to ${currentFolder.name}`, - }).replace("${currentFolder.name}", currentFolder.name) + default: "Upload to {folderName}", + folderName: currentFolder.name, + }) : translate("upload_to_root", "drive", { default: "Upload a file to root", })} diff --git a/packages/frontend-web/drive_download_implementation.md b/packages/frontend-web/drive_download_implementation.md new file mode 100644 index 00000000..76fbdf16 --- /dev/null +++ b/packages/frontend-web/drive_download_implementation.md @@ -0,0 +1,550 @@ +# Drive Download Implementation Plan + +## **Implementation Plan: Download Functionality for Drive Files and Folders** + +### **Overview** +Add download capability for individual files (with decryption) and folders (as zip archives) in the Drive section. Files will be fetched via their Digital Ocean download URLs, decrypted client-side, and saved to the user's device. Folders will recursively collect all files, decrypt them, and package them into a zip file. + +--- + +### **Phase 1: Dependencies and Type Updates** + +#### 1.1 Install JSZip for Zip Creation +**File**: `package.json` +- Add `jszip` and `@types/jszip` dependencies +- Required for client-side zip file creation + +#### 1.2 Update DriveFile Interface +**File**: `hooks/use-drive-management.ts` (lines 23-37) +- Add `download_url?: string` field to DriveFile interface +- This will store the presigned URL from Digital Ocean + +#### 1.3 Update API Response Mapping +**File**: `hooks/use-drive-management.ts` (lines 114-128) +- Update the file mapping to extract `file_url.download_url` from API response +- Store it in the `download_url` field + +--- + +### **Phase 2: Download Utilities** + +#### 2.1 Create Download Helper Functions +**File**: `libs/file-encryption.ts` (new functions at end) +- **`downloadFileFromUrl(url: string): Promise`** + - Fetches encrypted file from download URL + - Returns blob for decryption + +- **`saveDecryptedFile(blob: Blob, fileName: string, mimeType: string): void`** + - Creates download link and triggers browser download + - Handles proper file naming and MIME type + +- **`decryptAndDownloadFile(downloadUrl: string, fileName: string, mimeType: string, ivHex: string, projectAesKey: string): Promise`** + - Orchestrates: fetch → decrypt → save + - Main function for single file download + +--- + +### **Phase 3: Drive Management Hook Updates** + +#### 3.1 Add Download Functions to Hook +**File**: `hooks/use-drive-management.ts` + +**New functions to add:** + +- **`downloadFile(fileId: string): Promise`** + - Downloads and decrypts a single file + - Gets project key from secure storage + - Shows toast notifications for success/error + - Uses loading state + +- **`downloadFolder(folderId: string | null): Promise`** + - Recursively collects all files in folder and subfolders + - Downloads and decrypts each file + - Creates zip archive with proper folder structure + - Shows progress (e.g., "Downloading 3 of 10 files...") + - Handles errors gracefully (continues on file errors) + +**Helper function:** +- **`getAllFilesInFolder(folderId: string | null): { file: DriveFile; path: string }[]`** + - Recursively traverses folder tree + - Returns array of files with their relative paths for zip structure + - Path format: "folder1/subfolder2/file.txt" + +#### 3.2 Update Hook Return Type +**File**: `hooks/use-drive-management.ts` (lines 44-63) +- Add `downloadFile: (fileId: string) => Promise` +- Add `downloadFolder: (folderId: string | null) => Promise` +- Add `isDownloading: boolean` state +- Add `downloadProgress: string` for progress messages + +--- + +### **Phase 4: UI Component Updates** + +#### 4.1 Update FileCard Component +**File**: `components/file-card.tsx` + +- Add `onDownload: (file: DriveFile) => void` prop +- Add Download menu item in dropdown (after Move, before separator) +- Icon: `Download` from lucide-react +- Label: Translation key "download" + +**Menu structure:** +``` +- Rename +- Move +- Download (NEW) +--- +- Delete +``` + +#### 4.2 Update DriveContent Component (File Downloads) +**File**: `components/drive-content.tsx` + +- Import `downloadFile` from hook +- Add handler: `handleDownloadFile(file: DriveFile)` +- Pass `onDownload={handleDownloadFile}` to FileCard +- Add loading state handling during download + +#### 4.3 Update DriveContent Component (Folder Downloads) +**File**: `components/drive-content.tsx` + +- Import `downloadFolder` from hook +- Add Download option to folder dropdown menu (lines 315-350) +- Add handler: `handleDownloadFolder(folder: Folder)` +- Add `Download` icon from lucide-react +- Show loading indicator during folder download + +**Folder menu structure:** +``` +- Open +- Rename +- Move +- Download (NEW) +--- +- Delete +``` + +#### 4.4 Add Download Progress Indicator +**File**: `components/drive-content.tsx` + +- Show toast or inline message when downloading folder +- Display: "Downloading folder: 3 of 10 files..." +- Use `downloadProgress` from hook + +--- + +### **Phase 5: Translation Keys** + +#### 5.1 Add Translation Keys +**File**: `messages/en/common.json` + +Add to "drive" section: +```json +{ + "download": "Download", + "downloading": "Downloading...", + "download_file": "Download file", + "download_folder": "Download folder", + "downloading_file": "Downloading file...", + "downloading_folder": "Downloading folder...", + "download_success": "Download completed", + "download_error": "Failed to download", + "preparing_download": "Preparing download...", + "downloading_progress": "Downloading {current} of {total} files...", + "creating_zip": "Creating zip archive...", + "no_files_in_folder": "Folder is empty", + "decrypting_files": "Decrypting files..." +} +``` + +--- + +### **Phase 6: Error Handling and Edge Cases** + +#### 6.1 Handle Missing download_url +- Check if `download_url` exists before attempting download +- Show error: "Download URL not available" + +#### 6.2 Handle Decryption Errors +- Catch decryption failures +- Show specific error message +- Don't crash entire folder download on single file error + +#### 6.3 Handle Empty Folders +- Check if folder has any files (recursively) +- Show toast: "Folder is empty" +- Don't create empty zip + +#### 6.4 Handle Large Folder Downloads +- Show warning if folder has >50 files (optional) +- Implement cancellation (future enhancement) +- Show progress clearly + +#### 6.5 Handle Missing Project Key +- Check for project AES key in secure storage +- Show error if not found +- Guide user to unlock project first + +--- + +### **Detailed Implementation Code Snippets** + +#### **File**: `libs/file-encryption.ts` (new functions) + +```typescript +/** + * Download file from URL + */ +export async function downloadFileFromUrl(url: string): Promise { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to download file: ${response.statusText}`); + } + return await response.blob(); +} + +/** + * Save decrypted blob as file download + */ +export function saveDecryptedFile(blob: Blob, fileName: string, mimeType: string): void { + const blobWithType = new Blob([blob], { type: mimeType }); + const url = URL.createObjectURL(blobWithType); + const link = document.createElement('a'); + link.href = url; + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); +} + +/** + * Download, decrypt and save a file + */ +export async function decryptAndDownloadFile( + downloadUrl: string, + fileName: string, + mimeType: string, + ivHex: string, + projectAesKey: string +): Promise { + // 1. Download encrypted file + const encryptedBlob = await downloadFileFromUrl(downloadUrl); + + // 2. Decrypt the file + const decryptedBlob = await decryptFileBlob(encryptedBlob, ivHex, projectAesKey); + + // 3. Trigger download + saveDecryptedFile(decryptedBlob, fileName, mimeType); +} +``` + +#### **File**: `hooks/use-drive-management.ts` (new state and functions) + +```typescript +// Add to state (around line 75) +const [isDownloading, setIsDownloading] = useState(false); +const [downloadProgress, setDownloadProgress] = useState(''); + +// Add helper function to get all files recursively +const getAllFilesInFolder = useCallback((folderId: string | null): { file: DriveFile; path: string }[] => { + const result: { file: DriveFile; path: string }[] = []; + + const traverse = (parentId: string | null, currentPath: string = '') => { + // Get files in current folder + const folderFiles = files.filter(f => f.parent_id === parentId); + folderFiles.forEach(file => { + result.push({ + file, + path: currentPath ? `${currentPath}/${file.name}` : file.name + }); + }); + + // Get subfolders and recurse + const subfolders = folders.filter(f => f.parent_id === parentId); + subfolders.forEach(folder => { + const newPath = currentPath ? `${currentPath}/${folder.name}` : folder.name; + traverse(folder.folder_id, newPath); + }); + }; + + traverse(folderId); + return result; +}, [files, folders]); + +// Download single file +const downloadFile = useCallback(async (fileId: string) => { + const file = files.find(f => f.file_id === fileId); + if (!file) { + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("file_not_found", "drive", { default: "File not found" }), + variant: "destructive", + }); + return; + } + + if (!file.download_url) { + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("download_url_missing", "drive", { default: "Download URL not available" }), + variant: "destructive", + }); + return; + } + + setIsDownloading(true); + try { + // Get project AES key + const projectKey = secureGetItem(`project_${selectedProjectId}_aes_key`); + if (!projectKey) { + throw new Error("Project key not found"); + } + + toast({ + title: translate("downloading", "drive", { default: "Downloading..." }), + description: file.name, + }); + + await decryptAndDownloadFile( + file.download_url, + file.name, + file.type, + file.iv, + projectKey + ); + + toast({ + title: translate("download_success", "drive", { default: "Download completed" }), + description: file.name, + }); + } catch (error) { + console.error("Error downloading file:", error); + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("download_error", "drive", { default: "Failed to download file" }), + variant: "destructive", + }); + } finally { + setIsDownloading(false); + } +}, [files, selectedProjectId, translate]); + +// Download folder as zip +const downloadFolder = useCallback(async (folderId: string | null) => { + const folderName = folderId + ? folders.find(f => f.folder_id === folderId)?.name || 'folder' + : 'root'; + + // Get all files in folder + const allFiles = getAllFilesInFolder(folderId); + + if (allFiles.length === 0) { + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("no_files_in_folder", "drive", { default: "Folder is empty" }), + variant: "destructive", + }); + return; + } + + setIsDownloading(true); + try { + // Get project AES key + const projectKey = secureGetItem(`project_${selectedProjectId}_aes_key`); + if (!projectKey) { + throw new Error("Project key not found"); + } + + // Import JSZip + const JSZip = (await import('jszip')).default; + const zip = new JSZip(); + + // Download and decrypt each file + for (let i = 0; i < allFiles.length; i++) { + const { file, path } = allFiles[i]; + + setDownloadProgress(`${i + 1} of ${allFiles.length}`); + + toast({ + title: translate("downloading_progress", "drive", { + default: "Downloading {current} of {total} files...", + current: (i + 1).toString(), + total: allFiles.length.toString() + }), + }); + + try { + if (!file.download_url) continue; + + // Download and decrypt + const encryptedBlob = await downloadFileFromUrl(file.download_url); + const decryptedBlob = await decryptFileBlob(encryptedBlob, file.iv, projectKey); + + // Add to zip with proper path + zip.file(path, decryptedBlob); + } catch (error) { + console.error(`Error downloading file ${file.name}:`, error); + // Continue with other files + } + } + + // Generate zip + toast({ + title: translate("creating_zip", "drive", { default: "Creating zip archive..." }), + }); + + const zipBlob = await zip.generateAsync({ type: 'blob' }); + + // Download zip + saveDecryptedFile(zipBlob, `${folderName}.zip`, 'application/zip'); + + toast({ + title: translate("download_success", "drive", { default: "Download completed" }), + description: `${allFiles.length} files downloaded`, + }); + } catch (error) { + console.error("Error downloading folder:", error); + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("download_error", "drive", { default: "Failed to download folder" }), + variant: "destructive", + }); + } finally { + setIsDownloading(false); + setDownloadProgress(''); + } +}, [folders, files, getAllFilesInFolder, selectedProjectId, translate]); + +// Update return statement to include new functions (around line 580) +return { + // ... existing properties + downloadFile, + downloadFolder, + isDownloading, + downloadProgress, +}; +``` + +#### **File**: `components/file-card.tsx` (update) + +```typescript +// Add to props interface (line 28-33) +interface FileCardProps { + file: DriveFile; + onRename: (file: DriveFile) => void; + onMove: (file: DriveFile) => void; + onDelete: (file: DriveFile) => void; + onDownload: (file: DriveFile) => void; // NEW +} + +// Update component signature (line 35) +export function FileCard({ file, onRename, onMove, onDelete, onDownload }: FileCardProps) { + +// Add Download import at top +import { MoreVertical, Edit2, Move, Trash2, Download } from "lucide-react"; + +// Add menu item in dropdown (after Move, before separator around line 89) + { + onDownload(file); + setShowMenu(false); + }} +> + + {translate("download", "actions", { default: "Download" })} + + +``` + +#### **File**: `components/drive-content.tsx` (update) + +```typescript +// Add Download to imports (line 17) +import { + // ... existing imports + Download, +} from "lucide-react"; + +// Import download functions (around line 77-99) +const { + // ... existing properties + downloadFile, + downloadFolder, + isDownloading, + downloadProgress, +} = useDriveManagement({ + selectedWorkspaceId, + selectedProjectId, +}); + +// Add handlers (around line 200) +const handleDownloadFile = async (file: DriveFile) => { + await downloadFile(file.file_id); +}; + +const handleDownloadFolder = async (folder: Folder) => { + await downloadFolder(folder.folder_id); +}; + +// Update FileCard usage (around line 365) + + +// Add Download option to folder dropdown (around line 340, after Move) + handleDownloadFolder(folder)}> + + {translate("download", "drive", { default: "Download" })} + +``` + +--- + +### **Testing Checklist** + +- [ ] Download single file (small < 1MB) +- [ ] Download single file (large ~10MB) +- [ ] Download folder with single file +- [ ] Download folder with multiple files +- [ ] Download nested folder (folder with subfolders) +- [ ] Download folder preserves folder structure in zip +- [ ] Download root folder (all files) +- [ ] Error handling: missing download URL +- [ ] Error handling: decryption failure +- [ ] Error handling: missing project key +- [ ] Error handling: empty folder +- [ ] Progress indicators show correctly +- [ ] Toast notifications appear +- [ ] Downloaded files open correctly +- [ ] File MIME types preserved +- [ ] Large folder downloads (20+ files) +- [ ] Concurrent download prevention (disable UI during download) + +--- + +### **File Summary** + +**New Dependencies:** +- `jszip` (^3.10.1) +- `@types/jszip` (^3.4.1) + +**Modified Files:** +1. `package.json` - Add JSZip dependency +2. `hooks/use-drive-management.ts` - Add download functions, update interface +3. `libs/file-encryption.ts` - Add download utility functions +4. `components/file-card.tsx` - Add download option +5. `components/drive-content.tsx` - Add folder download option and handlers +6. `messages/en/common.json` - Add translation keys + +**Total Changes:** 6 files + +--- + +This implementation provides a complete, production-ready download solution with proper encryption/decryption, folder zip support, progress indicators, and comprehensive error handling. The solution follows existing codebase patterns and conventions. diff --git a/packages/frontend-web/hooks/use-drive-management.ts b/packages/frontend-web/hooks/use-drive-management.ts index db2e4d6c..270ea9d1 100644 --- a/packages/frontend-web/hooks/use-drive-management.ts +++ b/packages/frontend-web/hooks/use-drive-management.ts @@ -6,7 +6,7 @@ import { RootState } from "@/libs/Redux/store"; import axiosInstance from "@/libs/Middleware/axiosInstace"; import { toast } from "@/components/ui/use-toast"; import { useTranslator } from "@/hooks/use-translations"; -import { encryptFileBlob, validateFileSize } from "@/libs/file-encryption"; +import { encryptFileBlob, validateFileSize, decryptAndDownloadFile, downloadFileFromUrl, decryptFileBlob, saveDecryptedFile } from "@/libs/file-encryption"; import { secureGetItem } from "@/libs/local-storage-utils"; interface Folder { @@ -34,6 +34,7 @@ interface DriveFile { workspace_id: string; project_id: string; encrypted_data?: string; // base64 encoded encrypted blob (from API) + download_url?: string; // presigned URL from Digital Ocean } interface UseDriveManagementProps { @@ -58,8 +59,12 @@ interface UseDriveManagementReturn { renameFile: (fileId: string, newName: string) => Promise; moveFile: (fileIds: string[], parentId: string) => Promise; deleteFiles: (fileIds: string[]) => Promise; + downloadFile: (fileId: string) => Promise; + downloadFolder: (folderId: string | null) => Promise; isUploading: boolean; uploadProgress: number; + isDownloading: boolean; + downloadProgress: string; } export function useDriveManagement({ @@ -73,6 +78,8 @@ export function useDriveManagement({ const [currentFolder, setCurrentFolder] = useState(null); const [isUploading, setIsUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); + const [isDownloading, setIsDownloading] = useState(false); + const [downloadProgress, setDownloadProgress] = useState(''); // Get workspaces from Redux store for project name lookup const workspaces = useSelector((state: RootState) => state.workspace.workspaces); @@ -87,16 +94,12 @@ export function useDriveManagement({ setIsLoading(true); try { - console.log("Fetching folders for workspace", selectedWorkspaceId, "project", selectedProjectId); - // Fetch all folders (parent_id is optional, omitting it returns all folders) const response = await axiosInstance.get(`/drive/folder/list`); - console.log("API Response:", response.data); - // The API returns folders at response.data.data.folders const apiFolders = response.data?.data?.folders || []; - const apiFiles = response.data?.data?.files || []; + const rootLevelFiles = response.data?.data?.files || []; // Map API response to match our Folder interface (doc_id -> folder_id) const mappedFolders: Folder[] = apiFolders.map((folder: any) => ({ @@ -110,8 +113,18 @@ export function useDriveManagement({ project_id: folder.project_id || selectedProjectId, })); + // Extract all files from both root level and inside folders + const allApiFiles: any[] = [...rootLevelFiles]; + + // Also extract files from inside each folder + apiFolders.forEach((folder: any) => { + if (folder.files && Array.isArray(folder.files)) { + allApiFiles.push(...folder.files); + } + }); + // Map files from API response - const mappedFiles: DriveFile[] = apiFiles.map((file: any) => ({ + const mappedFiles: DriveFile[] = allApiFiles.map((file: any) => ({ file_id: file.doc_id, name: file.name, size: file.size, @@ -125,10 +138,9 @@ export function useDriveManagement({ workspace_id: file.workspace_id || selectedWorkspaceId, project_id: file.project_id || selectedProjectId, encrypted_data: file.data, + download_url: file.file_url, // file_url is directly the URL string })); - console.log("Mapped folders:", mappedFolders); - console.log("Mapped files:", mappedFiles); setFolders(mappedFolders); setFiles(mappedFiles); } catch (error) { @@ -406,18 +418,60 @@ export function useDriveManagement({ const presignedResponse = await axiosInstance.post('/drive/file/get-presigned-url', payload); console.log("Presigned response:", presignedResponse); - const presignedUrl = presignedResponse.data; + console.log("Presigned response data:", presignedResponse.data); + + // Extract presigned URL from response + // Backend might return either direct URL string or nested in data.data or data.url + let presignedUrl: string; + if (typeof presignedResponse.data === 'string') { + presignedUrl = presignedResponse.data; + } else if (presignedResponse.data?.data) { + presignedUrl = typeof presignedResponse.data.data === 'string' + ? presignedResponse.data.data + : presignedResponse.data.data?.upload_url || presignedResponse.data.data?.url || presignedResponse.data.data?.presigned_url; + } else if (presignedResponse.data?.upload_url) { + presignedUrl = presignedResponse.data.upload_url; + } else if (presignedResponse.data?.url) { + presignedUrl = presignedResponse.data.url; + } else if (presignedResponse.data?.presigned_url) { + presignedUrl = presignedResponse.data.presigned_url; + } else { + console.error("Could not extract presigned URL from response:", presignedResponse.data); + throw new Error("Invalid presigned URL response from server"); + } + + console.log("Extracted presigned URL:", presignedUrl); + + if (!presignedUrl || typeof presignedUrl !== 'string') { + throw new Error("Invalid presigned URL received from server"); + } setUploadProgress(50); // 6. Upload encrypted blob to presigned URL - console.log("Uploading encrypted file to presigned URL"); - await fetch(presignedUrl, { + console.log("Uploading encrypted file to presigned URL:", presignedUrl); + console.log("Encrypted blob size:", encryptedBlob.size); + + const uploadResponse = await fetch(presignedUrl, { method: 'PUT', body: encryptedBlob, - headers: { 'Content-Type': 'application/octet-stream' }, + // Don't set Content-Type explicitly - let browser handle it to avoid CORS preflight }); + console.log("Upload response status:", uploadResponse.status, uploadResponse.statusText); + + // Critical: Check if upload was successful + if (!uploadResponse.ok) { + const errorText = await uploadResponse.text(); + console.error("Upload failed:", { + status: uploadResponse.status, + statusText: uploadResponse.statusText, + error: errorText + }); + throw new Error(`Failed to upload file to storage: ${uploadResponse.status} ${uploadResponse.statusText}`); + } + + console.log("File successfully uploaded to Digital Ocean"); setUploadProgress(90); toast({ @@ -556,12 +610,203 @@ export function useDriveManagement({ // Effect to fetch data when dependencies change useEffect(() => { if (selectedWorkspaceId && selectedProjectId) { - console.log("Running fetchFolders effect"); fetchFolders(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedWorkspaceId, selectedProjectId]); + // Helper function to get all files recursively in a folder + const getAllFilesInFolder = useCallback((folderId: string | null): { file: DriveFile; path: string }[] => { + const result: { file: DriveFile; path: string }[] = []; + + const traverse = (parentId: string | null, currentPath: string = '') => { + // Get files in current folder + const folderFiles = files.filter(f => f.parent_id === parentId); + folderFiles.forEach(file => { + result.push({ + file, + path: currentPath ? `${currentPath}/${file.name}` : file.name + }); + }); + + // Get subfolders and recurse + const subfolders = folders.filter(f => f.parent_id === parentId); + subfolders.forEach(folder => { + const newPath = currentPath ? `${currentPath}/${folder.name}` : folder.name; + traverse(folder.folder_id, newPath); + }); + }; + + traverse(folderId); + return result; + }, [files, folders]); + + // Download single file + const downloadFile = useCallback(async (fileId: string) => { + const file = files.find(f => f.file_id === fileId); + + if (!file) { + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("file_not_found", "drive", { default: "File not found" }), + variant: "destructive", + }); + return; + } + + if (!file.download_url) { + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("download_url_missing", "drive", { default: "Download URL not available" }), + variant: "destructive", + }); + return; + } + + setIsDownloading(true); + try { + // Get project info for key lookup + const currentProject = workspaces + .find(ws => ws.workspaceId === selectedWorkspaceId) + ?.projects.find(p => p.project_id === selectedProjectId); + + if (!currentProject) { + throw new Error("Project not found"); + } + + // Get project AES key using project name + const projectKeyName = `projectKey_${currentProject.name}`; + const projectKey = await secureGetItem(projectKeyName); + + if (!projectKey) { + throw new Error("Project key not found"); + } + + toast({ + title: translate("downloading", "drive", { default: "Downloading..." }), + description: file.name, + }); + + await decryptAndDownloadFile( + file.download_url, + file.name, + file.type, + file.iv, + projectKey + ); + + toast({ + title: translate("download_success", "drive", { default: "Download completed" }), + description: file.name, + }); + } catch (error) { + console.error("Error downloading file:", error); + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("download_error", "drive", { default: "Failed to download file" }), + variant: "destructive", + }); + } finally { + setIsDownloading(false); + } + }, [files, workspaces, selectedWorkspaceId, selectedProjectId, translate]); + + // Download folder as zip + const downloadFolder = useCallback(async (folderId: string | null) => { + const folderName = folderId + ? folders.find(f => f.folder_id === folderId)?.name || 'folder' + : 'root'; + + // Get all files in folder + const allFiles = getAllFilesInFolder(folderId); + + if (allFiles.length === 0) { + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("no_files_in_folder", "drive", { default: "Folder is empty" }), + variant: "destructive", + }); + return; + } + + setIsDownloading(true); + try { + // Get project info for key lookup + const currentProject = workspaces + .find(ws => ws.workspaceId === selectedWorkspaceId) + ?.projects.find(p => p.project_id === selectedProjectId); + + if (!currentProject) { + throw new Error("Project not found"); + } + + // Get project AES key using project name + const projectKeyName = `projectKey_${currentProject.name}`; + const projectKey = await secureGetItem(projectKeyName); + if (!projectKey) { + throw new Error("Project key not found"); + } + + // Import JSZip + const JSZip = (await import('jszip')).default; + const zip = new JSZip(); + + // Download and decrypt each file + for (let i = 0; i < allFiles.length; i++) { + const { file, path } = allFiles[i]; + + setDownloadProgress(`${i + 1} of ${allFiles.length}`); + + toast({ + title: translate("downloading_progress", "drive", { + default: "Downloading {current} of {total} files...", + current: (i + 1).toString(), + total: allFiles.length.toString() + }), + }); + + try { + if (!file.download_url) continue; + + // Download and decrypt + const encryptedBlob = await downloadFileFromUrl(file.download_url); + const decryptedBlob = await decryptFileBlob(encryptedBlob, file.iv, projectKey); + + // Add to zip with proper path + zip.file(path, decryptedBlob); + } catch (error) { + console.error(`Error downloading file ${file.name}:`, error); + // Continue with other files + } + } + + // Generate zip + toast({ + title: translate("creating_zip", "drive", { default: "Creating zip archive..." }), + }); + + const zipBlob = await zip.generateAsync({ type: 'blob' }); + + // Download zip + saveDecryptedFile(zipBlob, `${folderName}.zip`, 'application/zip'); + + toast({ + title: translate("download_success", "drive", { default: "Download completed" }), + description: `${allFiles.length} files downloaded`, + }); + } catch (error) { + console.error("Error downloading folder:", error); + toast({ + title: translate("error", "drive", { default: "Error" }), + description: translate("download_error", "drive", { default: "Failed to download folder" }), + variant: "destructive", + }); + } finally { + setIsDownloading(false); + setDownloadProgress(''); + } + }, [folders, files, workspaces, getAllFilesInFolder, selectedWorkspaceId, selectedProjectId, translate]); + return { folders, files, @@ -579,8 +824,12 @@ export function useDriveManagement({ renameFile, moveFile, deleteFiles, + downloadFile, + downloadFolder, isUploading, uploadProgress, + isDownloading, + downloadProgress, }; } diff --git a/packages/frontend-web/libs/file-encryption.ts b/packages/frontend-web/libs/file-encryption.ts index 9e2b9565..e31e7b5d 100644 --- a/packages/frontend-web/libs/file-encryption.ts +++ b/packages/frontend-web/libs/file-encryption.ts @@ -137,3 +137,55 @@ export function formatBytes(bytes: number, decimals: number = 2): string { return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } +/** + * Download file from URL + */ +export async function downloadFileFromUrl(url: string): Promise { + const response = await fetch(url); + + if (!response.ok) { + if (response.status === 404) { + throw new Error('File not found in storage. The file may have been deleted or never uploaded.'); + } + throw new Error(`Failed to download file: ${response.statusText}`); + } + + const blob = await response.blob(); + return blob; +} + +/** + * Save decrypted blob as file download + */ +export function saveDecryptedFile(blob: Blob, fileName: string, mimeType: string): void { + const blobWithType = new Blob([blob], { type: mimeType }); + const url = URL.createObjectURL(blobWithType); + const link = document.createElement('a'); + link.href = url; + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); +} + +/** + * Download, decrypt and save a file + */ +export async function decryptAndDownloadFile( + downloadUrl: string, + fileName: string, + mimeType: string, + ivHex: string, + projectAesKey: string +): Promise { + // 1. Download encrypted file + const encryptedBlob = await downloadFileFromUrl(downloadUrl); + + // 2. Decrypt the file + const decryptedBlob = await decryptFileBlob(encryptedBlob, ivHex, projectAesKey); + + // 3. Trigger download + saveDecryptedFile(decryptedBlob, fileName, mimeType); +} + diff --git a/packages/frontend-web/messages/en/common.json b/packages/frontend-web/messages/en/common.json index b1e80a23..2009aafc 100644 --- a/packages/frontend-web/messages/en/common.json +++ b/packages/frontend-web/messages/en/common.json @@ -1486,6 +1486,8 @@ "file_too_large": "File is too large. Maximum size is 50MB", "no_file_selected": "No file selected", "file_already_exists": "A file with this name already exists in this location", + "file_name": "File Name", + "enter_file_name": "Enter file name", "rename_file": "Rename File", "move_file": "Move File", "delete_file": "Delete File", @@ -1495,6 +1497,21 @@ "file_deleted": "File(s) deleted successfully", "error_renaming_file": "Failed to rename file", "error_moving_file": "Failed to move file(s)", - "error_deleting_file": "Failed to delete file(s)" + "error_deleting_file": "Failed to delete file(s)", + "download": "Download", + "downloading": "Downloading...", + "download_file": "Download file", + "download_folder": "Download folder", + "downloading_file": "Downloading file...", + "downloading_folder": "Downloading folder...", + "download_success": "Download completed", + "download_error": "Failed to download", + "preparing_download": "Preparing download...", + "downloading_progress": "Downloading {current} of {total} files...", + "creating_zip": "Creating zip archive...", + "no_files_in_folder": "Folder is empty", + "decrypting_files": "Decrypting files...", + "file_not_found": "File not found", + "download_url_missing": "Download URL not available" } } \ No newline at end of file diff --git a/packages/frontend-web/package-lock.json b/packages/frontend-web/package-lock.json index 41e47784..af384dd5 100644 --- a/packages/frontend-web/package-lock.json +++ b/packages/frontend-web/package-lock.json @@ -54,6 +54,7 @@ "embla-carousel-react": "8.5.1", "input-otp": "1.4.1", "js-cookie": "^3.0.5", + "jszip": "^3.10.1", "lucide-react": "^0.454.0", "next": "15.2.4", "next-intl": "^4.1.0", @@ -78,6 +79,7 @@ "zod": "^3.24.1" }, "devDependencies": { + "@types/jszip": "^3.4.1", "@types/lodash": "^4.17.16", "@types/node": "^22", "@types/react": "^19", @@ -3391,6 +3393,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jszip": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@types/jszip/-/jszip-3.4.1.tgz", + "integrity": "sha512-TezXjmf3lj+zQ651r6hPqvSScqBLvyPI9FxdXBqpEwBijNGQ2NXpaFW/7joGzveYkKQUil7iiDHLo6LV71Pc0A==", + "deprecated": "This is a stub types definition. jszip provides its own type definitions, so you do not need this installed.", + "dev": true, + "license": "MIT", + "dependencies": { + "jszip": "*" + } + }, "node_modules/@types/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", @@ -4728,6 +4741,12 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/crelt": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", @@ -6651,6 +6670,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/immer": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", @@ -7340,6 +7365,54 @@ "node": ">=4.0" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -7384,6 +7457,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -8141,6 +8223,12 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -8419,6 +8507,12 @@ "node": ">=6" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -9361,6 +9455,12 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/sharp": { "version": "0.33.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", diff --git a/packages/frontend-web/package.json b/packages/frontend-web/package.json index 98ca0b51..c4715d61 100644 --- a/packages/frontend-web/package.json +++ b/packages/frontend-web/package.json @@ -56,6 +56,7 @@ "embla-carousel-react": "8.5.1", "input-otp": "1.4.1", "js-cookie": "^3.0.5", + "jszip": "^3.10.1", "lucide-react": "^0.454.0", "next": "15.2.4", "next-intl": "^4.1.0", @@ -80,6 +81,7 @@ "zod": "^3.24.1" }, "devDependencies": { + "@types/jszip": "^3.4.1", "@types/lodash": "^4.17.16", "@types/node": "^22", "@types/react": "^19", From 612e2d8500725491a7a1aa1d761fb9e2b174ed47 Mon Sep 17 00:00:00 2001 From: ananducv Date: Mon, 13 Oct 2025 12:53:14 +0530 Subject: [PATCH 28/32] duplicate file issue fixed --- packages/frontend-web/hooks/use-drive-management.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/frontend-web/hooks/use-drive-management.ts b/packages/frontend-web/hooks/use-drive-management.ts index 270ea9d1..64624d34 100644 --- a/packages/frontend-web/hooks/use-drive-management.ts +++ b/packages/frontend-web/hooks/use-drive-management.ts @@ -123,8 +123,17 @@ export function useDriveManagement({ } }); + // Remove duplicates based on doc_id (file_id) + const uniqueApiFiles = allApiFiles.reduce((acc: any[], file: any) => { + const exists = acc.find(f => f.doc_id === file.doc_id); + if (!exists) { + acc.push(file); + } + return acc; + }, []); + // Map files from API response - const mappedFiles: DriveFile[] = allApiFiles.map((file: any) => ({ + const mappedFiles: DriveFile[] = uniqueApiFiles.map((file: any) => ({ file_id: file.doc_id, name: file.name, size: file.size, From 0ce4196e92a796b5a5ed49534f22ac7cc641ad4e Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Tue, 14 Oct 2025 11:41:34 +0530 Subject: [PATCH 29/32] Update schema.py --- packages/backend-server/app/api/v1/web/drive/files/schema.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/backend-server/app/api/v1/web/drive/files/schema.py b/packages/backend-server/app/api/v1/web/drive/files/schema.py index a4283531..e454b7cf 100644 --- a/packages/backend-server/app/api/v1/web/drive/files/schema.py +++ b/packages/backend-server/app/api/v1/web/drive/files/schema.py @@ -20,4 +20,5 @@ class GetPresignedUrl(BaseModel): name: str = Field(..., description="File name") size: int = Field(..., description="File size") file_path: str = Field(..., description="File path") - parent_id: Optional[str] = Field(None, description="Parent folder ID") \ No newline at end of file + parent_id: Optional[str] = Field(None, description="Parent folder ID") + iv: str = Field(..., description="IV") \ No newline at end of file From ce924b62a4710cccda4ddf13998f6e4ecfb14d9a Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Tue, 14 Oct 2025 12:34:39 +0530 Subject: [PATCH 30/32] Update services.py --- packages/backend-server/app/api/v1/web/drive/files/services.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/backend-server/app/api/v1/web/drive/files/services.py b/packages/backend-server/app/api/v1/web/drive/files/services.py index bb651768..336efa3b 100644 --- a/packages/backend-server/app/api/v1/web/drive/files/services.py +++ b/packages/backend-server/app/api/v1/web/drive/files/services.py @@ -78,6 +78,7 @@ async def get_presigned_url(user, payload): "key": final_file_path, "parent_id": parent_id, "created_by": user_id, + "iv": payload.get("iv"), } db_manager.insert_one(db, FILES, file_data) From 7d225be33354d7bc4a392f54a6cc55de11c99975 Mon Sep 17 00:00:00 2001 From: ananducv Date: Tue, 14 Oct 2025 15:02:33 +0530 Subject: [PATCH 31/32] file decryption and download working --- .../hooks/use-drive-management.ts | 42 ++++++++++++------- packages/frontend-web/libs/file-encryption.ts | 13 +++++- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/packages/frontend-web/hooks/use-drive-management.ts b/packages/frontend-web/hooks/use-drive-management.ts index 64624d34..23c38671 100644 --- a/packages/frontend-web/hooks/use-drive-management.ts +++ b/packages/frontend-web/hooks/use-drive-management.ts @@ -133,22 +133,29 @@ export function useDriveManagement({ }, []); // Map files from API response - const mappedFiles: DriveFile[] = uniqueApiFiles.map((file: any) => ({ - file_id: file.doc_id, - name: file.name, - size: file.size, - original_size: file.original_size || file.size, - type: file.type || 'application/octet-stream', - iv: file.iv || '', - parent_id: file.parent_id || null, // Normalize empty string to null - created_at: file.created_at, - updated_at: file.updated_at, - created_by: file.created_by, - workspace_id: file.workspace_id || selectedWorkspaceId, - project_id: file.project_id || selectedProjectId, - encrypted_data: file.data, - download_url: file.file_url, // file_url is directly the URL string - })); + const mappedFiles: DriveFile[] = uniqueApiFiles.map((file: any) => { + // Log if IV is missing - critical for decryption + if (!file.iv) { + console.warn(`⚠️ File "${file.name}" is missing IV (initialization vector). This file cannot be decrypted!`, file); + } + + return { + file_id: file.doc_id, + name: file.name, + size: file.size, + original_size: file.original_size || file.size, + type: file.type || 'application/octet-stream', + iv: file.iv || '', + parent_id: file.parent_id || null, // Normalize empty string to null + created_at: file.created_at, + updated_at: file.updated_at, + created_by: file.created_by, + workspace_id: file.workspace_id || selectedWorkspaceId, + project_id: file.project_id || selectedProjectId, + encrypted_data: file.data, + download_url: file.file_url, // file_url is directly the URL string + }; + }); setFolders(mappedFolders); setFiles(mappedFiles); @@ -401,6 +408,9 @@ export function useDriveManagement({ console.log("Encrypting file:", file.name); const { encryptedBlob, iv, originalSize } = await encryptFileBlob(file, projectKey); + console.log("✅ Encryption complete. IV generated:", iv); + console.log("Original size:", originalSize, "Encrypted size:", encryptedBlob.size); + setUploadProgress(40); // 5. Get presigned URL diff --git a/packages/frontend-web/libs/file-encryption.ts b/packages/frontend-web/libs/file-encryption.ts index e31e7b5d..4c6d422c 100644 --- a/packages/frontend-web/libs/file-encryption.ts +++ b/packages/frontend-web/libs/file-encryption.ts @@ -94,12 +94,23 @@ export async function decryptFileBlob( projectAesKey: string ): Promise { try { + // Validate IV + if (!ivHex || typeof ivHex !== 'string' || ivHex.trim() === '') { + console.error('Invalid IV:', ivHex); + throw new Error('Missing or invalid IV (initialization vector). The file may not have been properly encrypted during upload.'); + } + // 1. Read encrypted blob as ArrayBuffer const encryptedBuffer = await encryptedBlob.arrayBuffer(); // 2. Convert hex IV to Uint8Array + const ivMatch = ivHex.match(/.{1,2}/g); + if (!ivMatch) { + throw new Error(`Invalid IV format: "${ivHex}". Expected hex string.`); + } + const iv = new Uint8Array( - ivHex.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16)) + ivMatch.map((byte) => parseInt(byte, 16)) ); // 3. Import the AES key From f9884d54a15142ef82c25828dea667aec4174697 Mon Sep 17 00:00:00 2001 From: ananducv Date: Tue, 14 Oct 2025 16:08:09 +0530 Subject: [PATCH 32/32] files preview feature implemented --- .../frontend-web/components/drive-content.tsx | 43 ++++ .../frontend-web/components/file-card.tsx | 19 +- .../components/file-preview-modal.tsx | 222 ++++++++++++++++++ .../hooks/use-drive-management.ts | 66 ++++++ packages/frontend-web/libs/file-type-utils.ts | 101 ++++++++ packages/frontend-web/messages/de/common.json | 5 +- packages/frontend-web/messages/en/common.json | 8 +- 7 files changed, 458 insertions(+), 6 deletions(-) create mode 100644 packages/frontend-web/components/file-preview-modal.tsx create mode 100644 packages/frontend-web/libs/file-type-utils.ts diff --git a/packages/frontend-web/components/drive-content.tsx b/packages/frontend-web/components/drive-content.tsx index f9a69e18..e5b68d10 100644 --- a/packages/frontend-web/components/drive-content.tsx +++ b/packages/frontend-web/components/drive-content.tsx @@ -42,6 +42,7 @@ import { FileCard } from "./file-card"; import { RenameFileDialog } from "./rename-file-dialog"; import { MoveFileDialog } from "./move-file-dialog"; import { DeleteFileDialog } from "./delete-file-dialog"; +import { FilePreviewModal } from "./file-preview-modal"; interface Folder { folder_id: string; @@ -94,6 +95,7 @@ export function DriveContent() { deleteFiles, downloadFile, downloadFolder, + previewFile, isUploading, uploadProgress, isDownloading, @@ -116,6 +118,11 @@ export function DriveContent() { const [showMoveFile, setShowMoveFile] = useState(false); const [showDeleteFile, setShowDeleteFile] = useState(false); const [selectedFile, setSelectedFile] = useState(null); + + // Preview states + const [showPreviewFile, setShowPreviewFile] = useState(false); + const [previewBlobUrl, setPreviewBlobUrl] = useState(null); + const [previewingFile, setPreviewingFile] = useState(null); // Get current subfolders to display const currentSubfolders = getSubfolders(currentFolder?.folder_id || null); @@ -205,6 +212,32 @@ export function DriveContent() { await downloadFolder(folder.folder_id); }; + const handlePreviewFile = async (file: DriveFile) => { + setPreviewingFile(file); + setShowPreviewFile(true); + const result = await previewFile(file.file_id); + if (result) { + setPreviewBlobUrl(result.blobUrl); + } + }; + + const handlePreviewClose = (open: boolean) => { + if (!open && previewBlobUrl) { + URL.revokeObjectURL(previewBlobUrl); + setPreviewBlobUrl(null); + } + setShowPreviewFile(open); + if (!open) { + setPreviewingFile(null); + } + }; + + const handleDownloadFromPreview = () => { + if (previewingFile) { + downloadFile(previewingFile.file_id); + } + }; + // Keyboard shortcuts useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -375,6 +408,7 @@ export function DriveContent() { onMove={handleMoveFile} onDelete={handleDeleteFile} onDownload={handleDownloadFile} + onPreview={handlePreviewFile} /> ))} @@ -467,6 +501,15 @@ export function DriveContent() { )} + {/* File Preview Modal */} + + {/* Delete Confirmation */} diff --git a/packages/frontend-web/components/file-card.tsx b/packages/frontend-web/components/file-card.tsx index f202a369..4b2a5a9e 100644 --- a/packages/frontend-web/components/file-card.tsx +++ b/packages/frontend-web/components/file-card.tsx @@ -9,7 +9,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Button } from "@/components/ui/button"; -import { MoreVertical, Edit2, Move, Trash2, Download } from "lucide-react"; +import { MoreVertical, Edit2, Move, Trash2, Download, Eye } from "lucide-react"; import { getFileIcon, getFileIconColor } from "@/libs/file-icon-mappings"; import { formatBytes } from "@/libs/utils"; import { formatDate } from "@/libs/utils"; @@ -31,9 +31,10 @@ interface FileCardProps { onMove: (file: DriveFile) => void; onDelete: (file: DriveFile) => void; onDownload: (file: DriveFile) => void; + onPreview: (file: DriveFile) => void; } -export function FileCard({ file, onRename, onMove, onDelete, onDownload }: FileCardProps) { +export function FileCard({ file, onRename, onMove, onDelete, onDownload, onPreview }: FileCardProps) { const { translate } = useTranslator(); const [showMenu, setShowMenu] = useState(false); @@ -41,7 +42,10 @@ export function FileCard({ file, onRename, onMove, onDelete, onDownload }: FileC const iconColor = getFileIconColor(file.type); return ( -
+
onPreview(file)} + >
{/* File icon */}
@@ -92,6 +96,15 @@ export function FileCard({ file, onRename, onMove, onDelete, onDownload }: FileC {translate("move", "actions", { default: "Move" })} + { + onPreview(file); + setShowMenu(false); + }} + > + + {translate("preview", "actions", { default: "Preview" })} + { onDownload(file); diff --git a/packages/frontend-web/components/file-preview-modal.tsx b/packages/frontend-web/components/file-preview-modal.tsx new file mode 100644 index 00000000..f8abe59c --- /dev/null +++ b/packages/frontend-web/components/file-preview-modal.tsx @@ -0,0 +1,222 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Download, FileX, Loader2 } from "lucide-react"; +import { useTranslator } from "@/hooks/use-translations"; +import { getPreviewType, getModalSizeClass } from "@/libs/file-type-utils"; +import { formatBytes } from "@/libs/utils"; +import { cn } from "@/libs/utils"; + +interface DriveFile { + file_id: string; + name: string; + size: number; + original_size: number; + type: string; + iv: string; + parent_id: string | null; + created_at: string; + updated_at: string | null; +} + +interface FilePreviewModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + file: DriveFile | null; + blobUrl: string | null; + onDownload: () => void; +} + +export function FilePreviewModal({ + open, + onOpenChange, + file, + blobUrl, + onDownload, +}: FilePreviewModalProps) { + const { translate } = useTranslator(); + const [textContent, setTextContent] = useState(""); + const [isLoadingText, setIsLoadingText] = useState(false); + + const previewType = file ? getPreviewType(file.type, file.name) : 'unsupported'; + const modalSizeClass = getModalSizeClass(previewType); + + // Load text content for text files + useEffect(() => { + if (previewType === 'text' && blobUrl && open) { + setIsLoadingText(true); + fetch(blobUrl) + .then(response => response.text()) + .then(text => { + setTextContent(text); + setIsLoadingText(false); + }) + .catch(error => { + console.error("Error loading text content:", error); + setTextContent("Error loading file content"); + setIsLoadingText(false); + }); + } + }, [previewType, blobUrl, open]); + + // Cleanup: revoke blob URL when modal closes + useEffect(() => { + return () => { + if (blobUrl) { + URL.revokeObjectURL(blobUrl); + } + }; + }, [blobUrl]); + + const renderPreview = () => { + if (!blobUrl || !file) { + return ( +
+ +
+ ); + } + + switch (previewType) { + case 'image': + return ( +
+ {file.name} +
+ ); + + case 'pdf': + return ( +
+