Start i18n Integration

This commit is contained in:
Ryan
2025-04-08 18:40:01 -04:00
committed by GitHub
parent 7aa4fe142a
commit a6c4c1d39c
17 changed files with 662 additions and 144 deletions

View File

@@ -1,5 +1,27 @@
# Changelog # Changelog
## Changes 4/8/2025
**May have missed some stuff or could have bugs. Please report any issue you may encounter.**
- **i18n Integration:**
- Implemented a complete internationalization (i18n) system for all user-facing texts in FileRise.
- Created an `i18n.js` module containing a translations object with full keys for English (en), Spanish (es), and French (fr).
- Updated JavaScript code to replace hard-coded strings with the `t()` translation function.
- Enhanced HTML and modal templates to support dynamic language translations using data attributes (data-i18n-key, data-i18n-placeholder, etc.).
- **Language Dropdown & Persistence:**
- Added a language dropdown to the user panel modal allowing users to select their preferred language.
- Persisted the selected language in localStorage, ensuring that the preferred language is automatically applied on page refresh.
- Updated main.js to load and set the users language preference on DOMContentLoaded by calling `setLocale()` and `applyTranslations()`.
- **Bug Fixes & Improvements:**
- Fixed issues with evaluation of translation function calls in template literals (ensured proper syntax with `${t("key")}`).
- Updated the t() function to be more defensive against missing keys.
- Provided instructions and code examples to ensure the language change settings are reliably saved and applied across sessions.
---
## Changes 4/7/2025 v1.0.9 ## Changes 4/7/2025 v1.0.9
- TOTP one time recovery code added - TOTP one time recovery code added

View File

@@ -26,6 +26,8 @@ Upload, organize, and share files through a sleek web interface. **FileRise** is
- 🎨 **Responsive UI (Dark/Light Mode):** FileRise is mobile-friendly out of the box manage files from your phone or tablet with a responsive layout. Choose between Dark mode or Light theme, or let it follow your system preference. The interface remembers your preferences (layout, items per page, last visited folder, etc.) for a personalized experience each time. - 🎨 **Responsive UI (Dark/Light Mode):** FileRise is mobile-friendly out of the box manage files from your phone or tablet with a responsive layout. Choose between Dark mode or Light theme, or let it follow your system preference. The interface remembers your preferences (layout, items per page, last visited folder, etc.) for a personalized experience each time.
- 🌐 **Internationalization & Localization:** FileRise supports multiple languages via an integrated i18n system. Users can switch languages through a user panel dropdown, and their choice is saved in local storage for a consistent experience across sessions. Currently available in English, Spanish, and French—please report any translation issues you encounter.
- 🗑️ **Trash & File Recovery:** Mistakenly deleted files? No worries deleted items go to the Trash instead of immediate removal. Admins can restore files from Trash or empty it to free space. FileRise auto-purges old trash entries (default 3 days) to keep your storage tidy. - 🗑️ **Trash & File Recovery:** Mistakenly deleted files? No worries deleted items go to the Trash instead of immediate removal. Admins can restore files from Trash or empty it to free space. FileRise auto-purges old trash entries (default 3 days) to keep your storage tidy.
- ⚙️ **Lightweight & Self-Contained:** FileRise runs on PHP 8.1+ with no external database required data is stored in files (users, metadata) for simplicity. Its a single-folder web app you can drop into any Apache/PHP server or run as a container. Docker & Unraid ready: use our pre-built image for a hassle-free setup. Memory and CPU footprint is minimal, yet the app scales to thousands of files with pagination and sorting features. - ⚙️ **Lightweight & Self-Contained:** FileRise runs on PHP 8.1+ with no external database required data is stored in files (users, metadata) for simplicity. Its a single-folder web app you can drop into any Apache/PHP server or run as a container. Docker & Unraid ready: use our pre-built image for a hassle-free setup. Memory and CPU footprint is minimal, yet the app scales to thousands of files with pagination and sorting features.

View File

@@ -4,7 +4,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FileRise</title> <title data-i18n-key="title">FileRise</title>
<script> <script>
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
if (params.get('logout') === '1') { if (params.get('logout') === '1') {
@@ -100,47 +100,47 @@
</div> </div>
</div> </div>
<div class="header-title"> <div class="header-title">
<h1>FileRise</h1> <h1 data-i18n-key="header_title">FileRise</h1>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="header-buttons-wrapper" style="display: flex; align-items: center; gap: 10px;"> <div class="header-buttons-wrapper" style="display: flex; align-items: center; gap: 10px;">
<!-- Your header drop zone --> <!-- Your header drop zone -->
<div id="headerDropArea" class="header-drop-zone"></div> <div id="headerDropArea" class="header-drop-zone"></div>
<div class="header-buttons"> <div class="header-buttons">
<button id="logoutBtn" title="Logout"> <button id="logoutBtn" data-i18n-title="logout">
<i class="material-icons">exit_to_app</i> <i class="material-icons">exit_to_app</i>
</button> </button>
<button id="changePasswordBtn" title="Change Password" style="display: none;"> <button id="changePasswordBtn" data-i18n-title="change_password" style="display: none;">
<i class="material-icons">vpn_key</i> <i class="material-icons">vpn_key</i>
</button> </button>
<div id="restoreFilesModal" class="modal centered-modal" style="display: none;"> <div id="restoreFilesModal" class="modal centered-modal" style="display: none;">
<div class="modal-content"> <div class="modal-content">
<h4 class="custom-restore-header"> <h4 class="custom-restore-header">
<i class="material-icons orange-icon">restore_from_trash</i> <i class="material-icons orange-icon">restore_from_trash</i>
<span>Restore or</span> <span data-i18n-key="restore_text">Restore or</span>
<i class="material-icons red-icon">delete_for_ever</i> <i class="material-icons red-icon">delete_for_ever</i>
<span>Delete Trash Items</span> <span data-i18n-key="delete_text">Delete Trash Items</span>
</h4> </h4>
<div id="restoreFilesList" <div id="restoreFilesList"
style="max-height:300px; overflow-y:auto; border:1px solid #ccc; padding:10px; margin-bottom:10px;"> style="max-height:300px; overflow-y:auto; border:1px solid #ccc; padding:10px; margin-bottom:10px;">
<!-- Trash items will be loaded here --> <!-- Trash items will be loaded here -->
</div> </div>
<div style="text-align: right;"> <div style="text-align: right;">
<button id="restoreSelectedBtn" class="btn btn-primary">Restore Selected</button> <button id="restoreSelectedBtn" class="btn btn-primary" data-i18n-key="restore_selected">Restore Selected</button>
<button id="restoreAllBtn" class="btn btn-secondary">Restore All</button> <button id="restoreAllBtn" class="btn btn-secondary" data-i18n-key="restore_all">Restore All</button>
<button id="deleteTrashSelectedBtn" class="btn btn-warning">Delete Selected</button> <button id="deleteTrashSelectedBtn" class="btn btn-warning" data-i18n-key="delete_selected_trash">Delete Selected</button>
<button id="deleteAllBtn" class="btn btn-danger">Delete All</button> <button id="deleteAllBtn" class="btn btn-danger" data-i18n-key="delete_all">Delete All</button>
<button id="closeRestoreModal" class="btn btn-dark">Close</button> <button id="closeRestoreModal" class="btn btn-dark" data-i18n-key="close">Close</button>
</div> </div>
</div> </div>
</div> </div>
<button id="addUserBtn" title="Add User" style="display: none;"> <button id="addUserBtn" data-i18n-title="add_user" style="display: none;">
<i class="material-icons">person_add</i> <i class="material-icons">person_add</i>
</button> </button>
<button id="removeUserBtn" title="Remove User" style="display: none;"> <button id="removeUserBtn" data-i18n-title="remove_user" style="display: none;">
<i class="material-icons">person_remove</i> <i class="material-icons">person_remove</i>
</button> </button>
<button id="darkModeToggle" class="dark-mode-toggle">Dark Mode</button> <button id="darkModeToggle" class="dark-mode-toggle" data-i18n-key="dark_mode_toggle">Dark Mode</button>
</div> </div>
</div> </div>
</div> </div>
@@ -162,26 +162,26 @@
<div class="col-12"> <div class="col-12">
<form id="authForm" method="post"> <form id="authForm" method="post">
<div class="form-group"> <div class="form-group">
<label for="loginUsername">User:</label> <label for="loginUsername" data-i18n-key="user">User:</label>
<input type="text" class="form-control" id="loginUsername" name="username" required /> <input type="text" class="form-control" id="loginUsername" name="username" required />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="loginPassword">Password:</label> <label for="loginPassword" data-i18n-key="password">Password:</label>
<input type="password" class="form-control" id="loginPassword" name="password" required /> <input type="password" class="form-control" id="loginPassword" name="password" required />
</div> </div>
<button type="submit" class="btn btn-primary btn-block btn-login">Login</button> <button type="submit" class="btn btn-primary btn-block btn-login" data-i18n-key="login">Login</button>
<div class="form-group remember-me-container"> <div class="form-group remember-me-container">
<input type="checkbox" id="rememberMeCheckbox" name="remember_me" /> <input type="checkbox" id="rememberMeCheckbox" name="remember_me" />
<label for="rememberMeCheckbox">Remember me</label> <label for="rememberMeCheckbox" data-i18n-key="remember_me">Remember me</label>
</div> </div>
</form> </form>
<!-- OIDC Login Option --> <!-- OIDC Login Option -->
<div class="text-center mt-3"> <div class="text-center mt-3">
<button id="oidcLoginBtn" class="btn btn-secondary">Login with OIDC</button> <button id="oidcLoginBtn" class="btn btn-secondary" data-i18n-key="login_oidc">Login with OIDC</button>
</div> </div>
<!-- Basic HTTP Login Option --> <!-- Basic HTTP Login Option -->
<div class="text-center mt-3"> <div class="text-center mt-3">
<a href="login_basic.php" class="btn btn-secondary">Use Basic HTTP Login</a> <a href="login_basic.php" class="btn btn-secondary" data-i18n-key="basic_http_login">Use Basic HTTP Login</a>
</div> </div>
</div> </div>
</div> </div>
@@ -194,20 +194,20 @@
<!-- Left Column (60% for Upload Card) --> <!-- Left Column (60% for Upload Card) -->
<div id="leftCol" class="col-md-7" style="display: flex; justify-content: center;"> <div id="leftCol" class="col-md-7" style="display: flex; justify-content: center;">
<div id="uploadCard" class="card" style="width: 100%;"> <div id="uploadCard" class="card" style="width: 100%;">
<div class="card-header">Upload Files/Folders</div> <div class="card-header" data-i18n-key="upload_header">Upload Files/Folders</div>
<div class="card-body d-flex flex-column"> <div class="card-body d-flex flex-column">
<form id="uploadFileForm" method="post" enctype="multipart/form-data" class="d-flex flex-column"> <form id="uploadFileForm" method="post" enctype="multipart/form-data" class="d-flex flex-column">
<div class="form-group flex-grow-1" style="margin-bottom: 1rem;"> <div class="form-group flex-grow-1" style="margin-bottom: 1rem;">
<div id="uploadDropArea" <div id="uploadDropArea"
style="border:2px dashed #ccc; padding:20px; cursor:pointer; display:flex; flex-direction:column; justify-content:center; align-items:center; position:relative;"> style="border:2px dashed #ccc; padding:20px; cursor:pointer; display:flex; flex-direction:column; justify-content:center; align-items:center; position:relative;">
<span>Drop files/folders here or click 'Choose Files'</span> <span data-i18n-key="upload_instruction">Drop files/folders here or click 'Choose Files'</span>
<br /> <br />
<input type="file" id="file" name="file[]" class="form-control-file" multiple <input type="file" id="file" name="file[]" class="form-control-file" multiple
style="opacity:0; position:absolute; width:1px; height:1px;" /> style="opacity:0; position:absolute; width:1px; height:1px;" />
<button type="button" id="customChooseBtn">Choose Files</button> <button type="button" id="customChooseBtn" data-i18n-key="choose_files">Choose Files</button>
</div> </div>
</div> </div>
<button type="submit" id="uploadBtn" class="btn btn-primary d-block mx-auto">Upload</button> <button type="submit" id="uploadBtn" class="btn btn-primary d-block mx-auto" data-i18n-key="upload">Upload</button>
<div id="uploadProgressContainer"></div> <div id="uploadProgressContainer"></div>
</form> </form>
</div> </div>
@@ -217,8 +217,8 @@
<div id="rightCol" class="col-md-5" style="display: flex; justify-content: center;"> <div id="rightCol" class="col-md-5" style="display: flex; justify-content: center;">
<div id="folderManagementCard" class="card" style="width: 100%; position: relative;"> <div id="folderManagementCard" class="card" style="width: 100%; position: relative;">
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center;"> <div class="card-header" style="display: flex; justify-content: space-between; align-items: center;">
<span>Folder Navigation &amp; Management</span> <span data-i18n-key="folder_navigation">Folder Navigation &amp; Management</span>
<button id="folderHelpBtn" class="btn btn-link" title="Folder Help" <button id="folderHelpBtn" class="btn btn-link" data-i18n-title="folder_help"
style="padding: 0; border: none; background: none;"> style="padding: 0; border: none; background: none;">
<i class="material-icons folder-help-icon" style="font-size: 24px;">info</i> <i class="material-icons folder-help-icon" style="font-size: 24px;">info</i>
</button> </button>
@@ -228,42 +228,41 @@
<div id="folderTreeContainer"></div> <div id="folderTreeContainer"></div>
</div> </div>
<div class="folder-actions mt-3"> <div class="folder-actions mt-3">
<button id="createFolderBtn" class="btn btn-primary">Create Folder</button> <button id="createFolderBtn" class="btn btn-primary" data-i18n-key="create_folder">Create Folder</button>
<div id="createFolderModal" class="modal"> <div id="createFolderModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<h4>Create Folder</h4> <h4 data-i18n-key="create_folder_title">Create Folder</h4>
<input type="text" id="newFolderName" class="form-control" placeholder="Enter folder name" <input type="text" id="newFolderName" class="form-control" data-i18n-placeholder="enter_folder_name" placeholder="Enter folder name"
style="margin-top:10px;" /> style="margin-top:10px;" />
<div style="margin-top:15px; text-align:right;"> <div style="margin-top:15px; text-align:right;">
<button id="cancelCreateFolder" class="btn btn-secondary">Cancel</button> <button id="cancelCreateFolder" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="submitCreateFolder" class="btn btn-primary">Create</button> <button id="submitCreateFolder" class="btn btn-primary" data-i18n-key="create">Create</button>
</div> </div>
</div> </div>
</div> </div>
<button id="renameFolderBtn" class="btn btn-secondary ml-2" title="Rename Folder"> <button id="renameFolderBtn" class="btn btn-secondary ml-2" data-i18n-title="rename_folder">
<i class="material-icons">drive_file_rename_outline</i> <i class="material-icons">drive_file_rename_outline</i>
</button> </button>
<div id="renameFolderModal" class="modal"> <div id="renameFolderModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<h4>Rename Folder</h4> <h4 data-i18n-key="rename_folder_title">Rename Folder</h4>
<input type="text" id="newRenameFolderName" class="form-control" <input type="text" id="newRenameFolderName" class="form-control" data-i18n-placeholder="rename_folder_placeholder" placeholder="Enter new folder name" style="margin-top:10px;" />
placeholder="Enter new folder name" style="margin-top:10px;" />
<div style="margin-top:15px; text-align:right;"> <div style="margin-top:15px; text-align:right;">
<button id="cancelRenameFolder" class="btn btn-secondary">Cancel</button> <button id="cancelRenameFolder" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="submitRenameFolder" class="btn btn-primary">Rename</button> <button id="submitRenameFolder" class="btn btn-primary" data-i18n-key="rename">Rename</button>
</div> </div>
</div> </div>
</div> </div>
<button id="deleteFolderBtn" class="btn btn-danger ml-2" title="Delete Folder"> <button id="deleteFolderBtn" class="btn btn-danger ml-2" data-i18n-title="delete_folder">
<i class="material-icons">delete</i> <i class="material-icons">delete</i>
</button> </button>
<div id="deleteFolderModal" class="modal"> <div id="deleteFolderModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<h4>Delete Folder</h4> <h4 data-i18n-key="delete_folder_title">Delete Folder</h4>
<p id="deleteFolderMessage">Are you sure you want to delete this folder?</p> <p id="deleteFolderMessage" data-i18n-key="delete_folder_message">Are you sure you want to delete this folder?</p>
<div style="margin-top:15px; text-align:right;"> <div style="margin-top:15px; text-align:right;">
<button id="cancelDeleteFolder" class="btn btn-secondary">Cancel</button> <button id="cancelDeleteFolder" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="confirmDeleteFolder" class="btn btn-danger">Delete</button> <button id="confirmDeleteFolder" class="btn btn-danger" data-i18n-key="delete">Delete</button>
</div> </div>
</div> </div>
</div> </div>
@@ -271,10 +270,10 @@
<div id="folderHelpTooltip" class="folder-help-tooltip" <div id="folderHelpTooltip" class="folder-help-tooltip"
style="display: none; position: absolute; top: 50px; right: 15px; background: #fff; border: 1px solid #ccc; padding: 10px; z-index: 1000; box-shadow: 2px 2px 6px rgba(0,0,0,0.2);"> style="display: none; position: absolute; top: 50px; right: 15px; background: #fff; border: 1px solid #ccc; padding: 10px; z-index: 1000; box-shadow: 2px 2px 6px rgba(0,0,0,0.2);">
<ul class="folder-help-list" style="margin: 0; padding-left: 20px;"> <ul class="folder-help-list" style="margin: 0; padding-left: 20px;">
<li>Click on a folder in the tree to view its files.</li> <li data-i18n-key="folder_help_item_1">Click on a folder in the tree to view its files.</li>
<li>Use [-] to collapse and [+] to expand folders.</li> <li data-i18n-key="folder_help_item_2">Use [-] to collapse and [+] to expand folders.</li>
<li>Select a folder and click "Create Folder" to add a subfolder.</li> <li data-i18n-key="folder_help_item_3">Select a folder and click "Create Folder" to add a subfolder.</li>
<li>To rename or delete a folder, select it and then click the appropriate button.</li> <li data-i18n-key="folder_help_item_4">To rename or delete a folder, select it and then click the appropriate button.</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -286,53 +285,53 @@
<!-- File List Section --> <!-- File List Section -->
<div id="fileListContainer" style="display: none;"> <div id="fileListContainer" style="display: none;">
<h2 id="fileListTitle">Files in (Root)</h2> <h2 id="fileListTitle" data-i18n-key="file_list_title">Files in (Root)</h2>
<div id="fileListActions" class="file-list-actions"> <div id="fileListActions" class="file-list-actions">
<button id="deleteSelectedBtn" class="btn action-btn" style="display: none;">Delete Files</button> <button id="deleteSelectedBtn" class="btn action-btn" style="display: none;" data-i18n-key="delete_files">Delete Files</button>
<div id="deleteFilesModal" class="modal"> <div id="deleteFilesModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<h4>Delete Selected Files</h4> <h4 data-i18n-key="delete_selected_files_title">Delete Selected Files</h4>
<p id="deleteFilesMessage">Are you sure you want to delete the selected files?</p> <p id="deleteFilesMessage" data-i18n-key="delete_files_message">Are you sure you want to delete the selected files?</p>
<div class="modal-footer"> <div class="modal-footer">
<button id="cancelDeleteFiles" class="btn btn-secondary">Cancel</button> <button id="cancelDeleteFiles" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="confirmDeleteFiles" class="btn btn-danger">Delete</button> <button id="confirmDeleteFiles" class="btn btn-danger" data-i18n-key="delete">Delete</button>
</div> </div>
</div> </div>
</div> </div>
<button id="copySelectedBtn" class="btn action-btn" style="display: none;" disabled>Copy Files</button> <button id="copySelectedBtn" class="btn action-btn" style="display: none;" disabled data-i18n-key="copy_files">Copy Files</button>
<div id="copyFilesModal" class="modal"> <div id="copyFilesModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<h4>Copy Selected Files</h4> <h4 data-i18n-key="copy_files_title">Copy Selected Files</h4>
<p id="copyFilesMessage">Select a target folder for copying the selected files:</p> <p id="copyFilesMessage" data-i18n-key="copy_files_message">Select a target folder for copying the selected files:</p>
<select id="copyTargetFolder" class="form-control modal-input"></select> <select id="copyTargetFolder" class="form-control modal-input"></select>
<div class="modal-footer"> <div class="modal-footer">
<button id="cancelCopyFiles" class="btn btn-secondary">Cancel</button> <button id="cancelCopyFiles" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="confirmCopyFiles" class="btn btn-primary">Copy</button> <button id="confirmCopyFiles" class="btn btn-primary" data-i18n-key="copy">Copy</button>
</div> </div>
</div> </div>
</div> </div>
<button id="moveSelectedBtn" class="btn action-btn" style="display: none;" disabled>Move Files</button> <button id="moveSelectedBtn" class="btn action-btn" style="display: none;" disabled data-i18n-key="move_files">Move Files</button>
<div id="moveFilesModal" class="modal"> <div id="moveFilesModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<h4>Move Selected Files</h4> <h4 data-i18n-key="move_files_title">Move Selected Files</h4>
<p id="moveFilesMessage">Select a target folder for moving the selected files:</p> <p id="moveFilesMessage" data-i18n-key="move_files_message">Select a target folder for moving the selected files:</p>
<select id="moveTargetFolder" class="form-control modal-input"></select> <select id="moveTargetFolder" class="form-control modal-input"></select>
<div class="modal-footer"> <div class="modal-footer">
<button id="cancelMoveFiles" class="btn btn-secondary">Cancel</button> <button id="cancelMoveFiles" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="confirmMoveFiles" class="btn btn-primary">Move</button> <button id="confirmMoveFiles" class="btn btn-primary" data-i18n-key="move">Move</button>
</div> </div>
</div> </div>
</div> </div>
<button id="downloadZipBtn" class="btn action-btn" style="display: none;" disabled>Download ZIP</button> <button id="downloadZipBtn" class="btn action-btn" style="display: none;" disabled data-i18n-key="download_zip">Download ZIP</button>
<button id="extractZipBtn" class="btn btn-sm btn-info" title="Extract Zip">Extract Zip</button> <button id="extractZipBtn" class="btn btn-sm btn-info" data-i18n-title="extract_zip" data-i18n-key="extract_zip_button">Extract Zip</button>
<div id="downloadZipModal" class="modal" style="display:none;"> <div id="downloadZipModal" class="modal" style="display:none;">
<div class="modal-content"> <div class="modal-content">
<h4>Download Selected Files as Zip</h4> <h4 data-i18n-key="download_zip_title">Download Selected Files as Zip</h4>
<p>Enter a name for the zip file:</p> <p data-i18n-key="download_zip_prompt">Enter a name for the zip file:</p>
<input type="text" id="zipFileNameInput" class="form-control" placeholder="files.zip" /> <input type="text" id="zipFileNameInput" class="form-control" data-i18n-placeholder="zip_placeholder" placeholder="files.zip" />
<div class="modal-footer" style="margin-top:15px; text-align:right;"> <div class="modal-footer" style="margin-top:15px; text-align:right;">
<button id="cancelDownloadZip" class="btn btn-secondary">Cancel</button> <button id="cancelDownloadZip" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="confirmDownloadZip" class="btn btn-primary">Download</button> <button id="confirmDownloadZip" class="btn btn-primary" data-i18n-key="download">Download</button>
</div> </div>
</div> </div>
</div> </div>
@@ -347,50 +346,49 @@
<div id="changePasswordModal" class="modal" style="display:none;"> <div id="changePasswordModal" class="modal" style="display:none;">
<div class="modal-content" style="max-width:400px; margin:auto;"> <div class="modal-content" style="max-width:400px; margin:auto;">
<span id="closeChangePasswordModal" style="cursor:pointer;">&times;</span> <span id="closeChangePasswordModal" style="cursor:pointer;">&times;</span>
<h3>Change Password</h3> <h3 data-i18n-key="change_password_title">Change Password</h3>
<input type="password" id="oldPassword" placeholder="Old Password" style="width:100%; margin: 5px 0;" /> <input type="password" id="oldPassword" class="form-control" data-i18n-placeholder="old_password" placeholder="Old Password" style="width:100%; margin: 5px 0;" />
<input type="password" id="newPassword" placeholder="New Password" style="width:100%; margin: 5px 0;" /> <input type="password" id="newPassword" class="form-control" data-i18n-placeholder="new_password" placeholder="New Password" style="width:100%; margin: 5px 0;" />
<input type="password" id="confirmPassword" placeholder="Confirm New Password" <input type="password" id="confirmPassword" class="form-control" data-i18n-placeholder="confirm_new_password" placeholder="Confirm New Password" style="width:100%; margin: 5px 0;" />
style="width:100%; margin: 5px 0;" /> <button id="saveNewPasswordBtn" class="btn btn-primary" data-i18n-key="save" style="width:100%;">Save</button>
<button id="saveNewPasswordBtn" class="btn btn-primary" style="width:100%;">Save</button>
</div> </div>
</div> </div>
<div id="addUserModal" class="modal"> <div id="addUserModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<h3>Create New User</h3> <h3 data-i18n-key="create_new_user_title">Create New User</h3>
<label for="newUsername">Username:</label> <label for="newUsername" data-i18n-key="username">Username:</label>
<input type="text" id="newUsername" class="form-control" /> <input type="text" id="newUsername" class="form-control" />
<label for="addUserPassword">Password:</label> <label for="addUserPassword" data-i18n-key="password">Password:</label>
<input type="password" id="addUserPassword" class="form-control" /> <input type="password" id="addUserPassword" class="form-control" />
<div id="adminCheckboxContainer"> <div id="adminCheckboxContainer">
<input type="checkbox" id="isAdmin" /> <input type="checkbox" id="isAdmin" />
<label for="isAdmin">Grant Admin Access</label> <label for="isAdmin" data-i18n-key="grant_admin">Grant Admin Access</label>
</div> </div>
<div class="button-container"> <div class="button-container">
<button id="cancelUserBtn" class="btn btn-secondary">Cancel</button> <button id="cancelUserBtn" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="saveUserBtn" class="btn btn-primary">Save User</button> <button id="saveUserBtn" class="btn btn-primary" data-i18n-key="save_user">Save User</button>
</div> </div>
</div> </div>
</div> </div>
<div id="removeUserModal" class="modal"> <div id="removeUserModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<h3>Remove User</h3> <h3 data-i18n-key="remove_user_title">Remove User</h3>
<label for="removeUsernameSelect">Select a user to remove:</label> <label for="removeUsernameSelect" data-i18n-key="select_user_remove">Select a user to remove:</label>
<select id="removeUsernameSelect" class="form-control"></select> <select id="removeUsernameSelect" class="form-control"></select>
<div class="button-container"> <div class="button-container">
<button id="cancelRemoveUserBtn" class="btn btn-secondary">Cancel</button> <button id="cancelRemoveUserBtn" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="deleteUserBtn" class="btn btn-danger">Delete User</button> <button id="deleteUserBtn" class="btn btn-danger" data-i18n-key="delete_user">Delete User</button>
</div> </div>
</div> </div>
</div> </div>
<div id="renameFileModal" class="modal"> <div id="renameFileModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<h4>Rename File</h4> <h4 data-i18n-key="rename_file_title">Rename File</h4>
<input type="text" id="newFileName" class="form-control" placeholder="Enter new file name" <input type="text" id="newFileName" class="form-control" data-i18n-placeholder="rename_file_placeholder" placeholder="Enter new file name"
style="margin-top:10px;" /> style="margin-top:10px;" />
<div style="margin-top:15px; text-align:right;"> <div style="margin-top:15px; text-align:right;">
<button id="cancelRenameFile" class="btn btn-secondary">Cancel</button> <button id="cancelRenameFile" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="submitRenameFile" class="btn btn-primary">Rename</button> <button id="submitRenameFile" class="btn btn-primary" data-i18n-key="rename">Rename</button>
</div> </div>
</div> </div>
</div> </div>
@@ -398,8 +396,8 @@
<div class="modal-content"> <div class="modal-content">
<p id="confirmMessage"></p> <p id="confirmMessage"></p>
<div class="modal-actions"> <div class="modal-actions">
<button id="confirmYesBtn" class="btn btn-primary">Yes</button> <button id="confirmYesBtn" class="btn btn-primary" data-i18n-key="yes">Yes</button>
<button id="confirmNoBtn" class="btn btn-secondary">No</button> <button id="confirmNoBtn" class="btn btn-secondary" data-i18n-key="no">No</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,5 @@
import { sendRequest } from './networkUtils.js'; import { sendRequest } from './networkUtils.js';
import { t } from './i18n.js';
import { import {
toggleVisibility, toggleVisibility,
showToast as originalShowToast, showToast as originalShowToast,
@@ -34,8 +35,9 @@ window.currentOIDCConfig = currentOIDCConfig;
window.pendingTOTP = new URLSearchParams(window.location.search).get('totp_required') === '1'; window.pendingTOTP = new URLSearchParams(window.location.search).get('totp_required') === '1';
// override showToast to suppress the "Please log in to continue." toast during TOTP // override showToast to suppress the "Please log in to continue." toast during TOTP
function showToast(msg) { function showToast(msgKey) {
if (window.pendingTOTP && msg === "Please log in to continue.") { const msg = t(msgKey);
if (window.pendingTOTP && msgKey === "please_log_in_to_continue") {
return; return;
} }
originalShowToast(msg); originalShowToast(msg);

View File

@@ -1,5 +1,6 @@
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js'; import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js';
import { sendRequest } from './networkUtils.js'; import { sendRequest } from './networkUtils.js';
import { t, applyTranslations, setLocale } from './i18n.js';
const version = "v1.0.9"; const version = "v1.0.9";
const adminTitle = `Admin Panel <small style="font-size: 12px; color: gray;">${version}</small>`; const adminTitle = `Admin Panel <small style="font-size: 12px; color: gray;">${version}</small>`;
@@ -32,14 +33,14 @@ export function openTOTPLoginModal() {
<div style="background: ${modalBg}; padding:20px; border-radius:8px; text-align:center; position:relative; color:${textColor};"> <div style="background: ${modalBg}; padding:20px; border-radius:8px; text-align:center; position:relative; color:${textColor};">
<span id="closeTOTPLoginModal" style="position:absolute; top:10px; right:10px; cursor:pointer; font-size:24px;">&times;</span> <span id="closeTOTPLoginModal" style="position:absolute; top:10px; right:10px; cursor:pointer; font-size:24px;">&times;</span>
<div id="totpSection"> <div id="totpSection">
<h3>Enter TOTP Code</h3> <h3>${t("enter_totp_code")}</h3>
<input type="text" id="totpLoginInput" maxlength="6" <input type="text" id="totpLoginInput" maxlength="6"
style="font-size:24px; text-align:center; width:100%; padding:10px;" style="font-size:24px; text-align:center; width:100%; padding:10px;"
placeholder="6-digit code" /> placeholder="6-digit code" />
</div> </div>
<a href="#" id="toggleRecovery" style="display:block; margin-top:10px; font-size:14px;">Use Recovery Code instead</a> <a href="#" id="toggleRecovery" style="display:block; margin-top:10px; font-size:14px;">${t("use_recovery_code_instead")}</a>
<div id="recoverySection" style="display:none; margin-top:10px;"> <div id="recoverySection" style="display:none; margin-top:10px;">
<h3>Enter Recovery Code</h3> <h3>${t("enter_recovery_code")}</h3>
<input type="text" id="recoveryInput" <input type="text" id="recoveryInput"
style="font-size:24px; text-align:center; width:100%; padding:10px;" style="font-size:24px; text-align:center; width:100%; padding:10px;"
placeholder="Recovery code" /> placeholder="Recovery code" />
@@ -168,6 +169,8 @@ export function openUserPanel() {
transform: none; transform: none;
transition: none; transition: none;
`; `;
// Retrieve the language setting from local storage, default to English ("en")
const savedLanguage = localStorage.getItem("language") || "en";
if (!userPanelModal) { if (!userPanelModal) {
userPanelModal = document.createElement("div"); userPanelModal = document.createElement("div");
userPanelModal.id = "userPanelModal"; userPanelModal.id = "userPanelModal";
@@ -195,15 +198,29 @@ export function openUserPanel() {
<input type="checkbox" id="userTOTPEnabled" style="vertical-align: middle;" /> <input type="checkbox" id="userTOTPEnabled" style="vertical-align: middle;" />
</div> </div>
</fieldset> </fieldset>
<fieldset style="margin-bottom: 15px;">
<legend>Language</legend>
<div class="form-group">
<label for="languageSelector">Select Language:</label>
<select id="languageSelector">
<option value="en">English</option>
<option value="es">Español</option>
<option value="fr">Français</option>
</select>
</div>
</fieldset>
</div> </div>
`; `;
document.body.appendChild(userPanelModal); document.body.appendChild(userPanelModal);
// Close button handler
document.getElementById("closeUserPanel").addEventListener("click", () => { document.getElementById("closeUserPanel").addEventListener("click", () => {
userPanelModal.style.display = "none"; userPanelModal.style.display = "none";
}); });
// Change Password button
document.getElementById("openChangePasswordModalBtn").addEventListener("click", () => { document.getElementById("openChangePasswordModalBtn").addEventListener("click", () => {
document.getElementById("changePasswordModal").style.display = "block"; document.getElementById("changePasswordModal").style.display = "block";
}); });
// TOTP checkbox behavior
const totpCheckbox = document.getElementById("userTOTPEnabled"); const totpCheckbox = document.getElementById("userTOTPEnabled");
totpCheckbox.checked = localStorage.getItem("userTOTPEnabled") === "true"; totpCheckbox.checked = localStorage.getItem("userTOTPEnabled") === "true";
totpCheckbox.addEventListener("change", function () { totpCheckbox.addEventListener("change", function () {
@@ -228,7 +245,17 @@ export function openUserPanel() {
}) })
.catch(() => { showToast("Error updating TOTP setting."); }); .catch(() => { showToast("Error updating TOTP setting."); });
}); });
// Language dropdown initialization
const languageSelector = document.getElementById("languageSelector");
languageSelector.value = savedLanguage;
languageSelector.addEventListener("change", function () {
const selectedLanguage = this.value;
localStorage.setItem("language", selectedLanguage);
setLocale(selectedLanguage);
applyTranslations();
});
} else { } else {
// If the modal already exists, update its colors
userPanelModal.style.backgroundColor = overlayBackground; userPanelModal.style.backgroundColor = overlayBackground;
const modalContent = userPanelModal.querySelector(".modal-content"); const modalContent = userPanelModal.querySelector(".modal-content");
modalContent.style.background = isDarkMode ? "#2c2c2c" : "#fff"; modalContent.style.background = isDarkMode ? "#2c2c2c" : "#fff";

View File

@@ -1,4 +1,5 @@
// domUtils.js // domUtils.js
import { t } from './i18n.js';
// Basic DOM Helpers // Basic DOM Helpers
export function toggleVisibility(elementId, shouldShow) { export function toggleVisibility(elementId, shouldShow) {
@@ -6,7 +7,7 @@ export function toggleVisibility(elementId, shouldShow) {
if (element) { if (element) {
element.style.display = shouldShow ? "block" : "none"; element.style.display = shouldShow ? "block" : "none";
} else { } else {
console.error(`Element with id "${elementId}" not found.`); console.error(t("element_not_found", { id: elementId }));
} }
} }
@@ -97,7 +98,7 @@ export function buildSearchAndPaginationControls({ currentPage, totalPages, sear
<i class="material-icons">search</i> <i class="material-icons">search</i>
</span> </span>
</div> </div>
<input type="text" id="searchInput" class="form-control" placeholder="Search files or tag..." value="${safeSearchTerm}" aria-describedby="searchIcon"> <input type="text" id="searchInput" class="form-control" placeholder="${t("search_placeholder")}" value="${safeSearchTerm}" aria-describedby="searchIcon">
</div> </div>
</div> </div>
<div class="col-12 col-md-4 text-left"> <div class="col-12 col-md-4 text-left">
@@ -117,11 +118,11 @@ export function buildFileTableHeader(sortOrder) {
<thead> <thead>
<tr> <tr>
<th class="checkbox-col"><input type="checkbox" id="selectAll" onclick="toggleAllCheckboxes(this)"></th> <th class="checkbox-col"><input type="checkbox" id="selectAll" onclick="toggleAllCheckboxes(this)"></th>
<th data-column="name" class="sortable-col">File Name ${sortOrder.column === "name" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th> <th data-column="name" class="sortable-col">${t("file_name")} ${sortOrder.column === "name" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th data-column="modified" class="hide-small sortable-col">Date Modified ${sortOrder.column === "modified" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th> <th data-column="modified" class="hide-small sortable-col">${t("date_modified")} ${sortOrder.column === "modified" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th data-column="uploaded" class="hide-small hide-medium sortable-col">Upload Date ${sortOrder.column === "uploaded" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th> <th data-column="uploaded" class="hide-small hide-medium sortable-col">${t("upload_date")} ${sortOrder.column === "uploaded" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th data-column="size" class="hide-small sortable-col">File Size ${sortOrder.column === "size" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th> <th data-column="size" class="hide-small sortable-col">${t("file_size")} ${sortOrder.column === "size" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th data-column="uploader" class="hide-small hide-medium sortable-col">Uploader ${sortOrder.column === "uploader" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th> <th data-column="uploader" class="hide-small hide-medium sortable-col">${t("uploader")} ${sortOrder.column === "uploader" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>

View File

@@ -2,18 +2,20 @@
import { showToast, attachEnterKeyListener } from './domUtils.js'; import { showToast, attachEnterKeyListener } from './domUtils.js';
import { loadFileList } from './fileListView.js'; import { loadFileList } from './fileListView.js';
import { formatFolderName } from './fileListView.js'; import { formatFolderName } from './fileListView.js';
import { t } from './i18n.js';
export function handleDeleteSelected(e) { export function handleDeleteSelected(e) {
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
const checkboxes = document.querySelectorAll(".file-checkbox:checked"); const checkboxes = document.querySelectorAll(".file-checkbox:checked");
if (checkboxes.length === 0) { if (checkboxes.length === 0) {
showToast("No files selected."); showToast("no_files_selected");
return; return;
} }
window.filesToDelete = Array.from(checkboxes).map(chk => chk.value); window.filesToDelete = Array.from(checkboxes).map(chk => chk.value);
document.getElementById("deleteFilesMessage").textContent = const count = window.filesToDelete.length;
"Are you sure you want to delete " + window.filesToDelete.length + " selected file(s)?"; document.getElementById("deleteFilesMessage").textContent = t("confirm_delete_files", { count: count });
document.getElementById("deleteFilesModal").style.display = "block"; document.getElementById("deleteFilesModal").style.display = "block";
attachEnterKeyListener("deleteFilesModal", "confirmDeleteFiles"); attachEnterKeyListener("deleteFilesModal", "confirmDeleteFiles");
} }

View File

@@ -1,6 +1,7 @@
// editor.js // editor.js
import { escapeHTML, showToast } from './domUtils.js'; import { escapeHTML, showToast } from './domUtils.js';
import { loadFileList } from './fileListView.js'; import { loadFileList } from './fileListView.js';
import { t } from './i18n.js';
function getModeForFile(fileName) { function getModeForFile(fileName) {
const ext = fileName.slice(fileName.lastIndexOf('.') + 1).toLowerCase(); const ext = fileName.slice(fileName.lastIndexOf('.') + 1).toLowerCase();
@@ -73,17 +74,17 @@ export function editFile(fileName, folder) {
modal.classList.add("modal", "editor-modal"); modal.classList.add("modal", "editor-modal");
modal.innerHTML = ` modal.innerHTML = `
<div class="editor-header"> <div class="editor-header">
<h3 class="editor-title">Editing: ${escapeHTML(fileName)}</h3> <h3 class="editor-title">${t("editing")}: ${escapeHTML(fileName)}</h3>
<div class="editor-controls"> <div class="editor-controls">
<button id="decreaseFont" class="btn btn-sm btn-secondary">A-</button> <button id="decreaseFont" class="btn btn-sm btn-secondary">${t("decrease_font")}</button>
<button id="increaseFont" class="btn btn-sm btn-secondary">A+</button> <button id="increaseFont" class="btn btn-sm btn-secondary">${t("increase_font")}</button>
</div> </div>
<button id="closeEditorX" class="editor-close-btn">&times;</button> <button id="closeEditorX" class="editor-close-btn">&times;</button>
</div> </div>
<textarea id="fileEditor" class="editor-textarea">${escapeHTML(content)}</textarea> <textarea id="fileEditor" class="editor-textarea">${escapeHTML(content)}</textarea>
<div class="editor-footer"> <div class="editor-footer">
<button id="saveBtn" class="btn btn-primary">Save</button> <button id="saveBtn" class="btn btn-primary">${t("save")}</button>
<button id="closeBtn" class="btn btn-secondary">Close</button> <button id="closeBtn" class="btn btn-secondary">${t("close")}</button>
</div> </div>
`; `;
document.body.appendChild(modal); document.body.appendChild(modal);

View File

@@ -12,7 +12,7 @@ import {
toggleRowSelection, toggleRowSelection,
attachEnterKeyListener attachEnterKeyListener
} from './domUtils.js'; } from './domUtils.js';
import { t } from './i18n.js';
import { bindFileListContextMenu } from './fileMenu.js'; import { bindFileListContextMenu } from './fileMenu.js';
export let fileData = []; export let fileData = [];
@@ -36,12 +36,12 @@ export function createViewToggleButton() {
titleElem.parentNode.insertBefore(toggleBtn, titleElem.nextSibling); titleElem.parentNode.insertBefore(toggleBtn, titleElem.nextSibling);
} }
} }
toggleBtn.textContent = window.viewMode === "gallery" ? "Switch to Table View" : "Switch to Gallery View"; toggleBtn.textContent = window.viewMode === "gallery" ? t("switch_to_table_view") : t("switch_to_gallery_view");
toggleBtn.onclick = () => { toggleBtn.onclick = () => {
window.viewMode = window.viewMode === "gallery" ? "table" : "gallery"; window.viewMode = window.viewMode === "gallery" ? "table" : "gallery";
localStorage.setItem("viewMode", window.viewMode); localStorage.setItem("viewMode", window.viewMode);
loadFileList(window.currentFolder); loadFileList(window.currentFolder);
toggleBtn.textContent = window.viewMode === "gallery" ? "Switch to Table View" : "Switch to Gallery View"; toggleBtn.textContent = window.viewMode === "gallery" ? t("switch_to_table_view") : t("switch_to_gallery_view");
}; };
return toggleBtn; return toggleBtn;
} }
@@ -97,7 +97,7 @@ export function loadFileList(folderParam) {
renderFileTable(folder); renderFileTable(folder);
} }
} else { } else {
fileListContainer.textContent = "No files found."; fileListContainer.textContent = t("no_files_found");
updateFileActionButtons(); updateFileActionButtons();
} }
return data.files || []; return data.files || [];

View File

@@ -5,6 +5,7 @@ import { previewFile } from './filePreview.js';
import { editFile } from './fileEditor.js'; import { editFile } from './fileEditor.js';
import { canEditFile, fileData } from './fileListView.js'; import { canEditFile, fileData } from './fileListView.js';
import { openTagModal, openMultiTagModal } from './fileTags.js'; import { openTagModal, openMultiTagModal } from './fileTags.js';
import { t } from './i18n.js';
export function showFileContextMenu(x, y, menuItems) { export function showFileContextMenu(x, y, menuItems) {
let menu = document.getElementById("fileContextMenu"); let menu = document.getElementById("fileContextMenu");
@@ -74,22 +75,22 @@ export function fileListContextMenuHandler(e) {
const selected = Array.from(document.querySelectorAll("#fileList .file-checkbox:checked")).map(chk => chk.value); const selected = Array.from(document.querySelectorAll("#fileList .file-checkbox:checked")).map(chk => chk.value);
let menuItems = [ let menuItems = [
{ label: "Delete Selected", action: () => { handleDeleteSelected(new Event("click")); } }, { label: t("delete_selected"), action: () => { handleDeleteSelected(new Event("click")); } },
{ label: "Copy Selected", action: () => { handleCopySelected(new Event("click")); } }, { label: t("copy_selected"), action: () => { handleCopySelected(new Event("click")); } },
{ label: "Move Selected", action: () => { handleMoveSelected(new Event("click")); } }, { label: t("move_selected"), action: () => { handleMoveSelected(new Event("click")); } },
{ label: "Download Zip", action: () => { handleDownloadZipSelected(new Event("click")); } } { label: t("download_zip"), action: () => { handleDownloadZipSelected(new Event("click")); } }
]; ];
if (selected.some(name => name.toLowerCase().endsWith(".zip"))) { if (selected.some(name => name.toLowerCase().endsWith(".zip"))) {
menuItems.push({ menuItems.push({
label: "Extract Zip", label: t("extract_zip"),
action: () => { handleExtractZipSelected(new Event("click")); } action: () => { handleExtractZipSelected(new Event("click")); }
}); });
} }
if (selected.length > 1) { if (selected.length > 1) {
menuItems.push({ menuItems.push({
label: "Tag Selected", label: t("tag_selected"),
action: () => { action: () => {
const files = fileData.filter(f => selected.includes(f.name)); const files = fileData.filter(f => selected.includes(f.name));
openMultiTagModal(files); openMultiTagModal(files);
@@ -100,7 +101,7 @@ export function fileListContextMenuHandler(e) {
const file = fileData.find(f => f.name === selected[0]); const file = fileData.find(f => f.name === selected[0]);
menuItems.push({ menuItems.push({
label: "Preview", label: t("preview"),
action: () => { action: () => {
const folder = window.currentFolder || "root"; const folder = window.currentFolder || "root";
const folderPath = folder === "root" const folderPath = folder === "root"
@@ -112,18 +113,18 @@ export function fileListContextMenuHandler(e) {
if (canEditFile(file.name)) { if (canEditFile(file.name)) {
menuItems.push({ menuItems.push({
label: "Edit", label: t("edit"),
action: () => { editFile(selected[0], window.currentFolder); } action: () => { editFile(selected[0], window.currentFolder); }
}); });
} }
menuItems.push({ menuItems.push({
label: "Rename", label: t("rename"),
action: () => { renameFile(selected[0], window.currentFolder); } action: () => { renameFile(selected[0], window.currentFolder); }
}); });
menuItems.push({ menuItems.push({
label: "Tag File", label: t("tag_file"),
action: () => { openTagModal(file); } action: () => { openTagModal(file); }
}); });
} }

View File

@@ -1,6 +1,7 @@
// filePreview.js // filePreview.js
import { escapeHTML, showToast } from './domUtils.js'; import { escapeHTML, showToast } from './domUtils.js';
import { fileData } from './fileListView.js'; import { fileData } from './fileListView.js';
import { t } from './i18n.js';
export function openShareModal(file, folder) { export function openShareModal(file, folder) {
const existing = document.getElementById("shareModal"); const existing = document.getElementById("shareModal");
@@ -12,11 +13,11 @@ export function openShareModal(file, folder) {
modal.innerHTML = ` modal.innerHTML = `
<div class="modal-content share-modal-content" style="width: 600px; max-width:90vw;"> <div class="modal-content share-modal-content" style="width: 600px; max-width:90vw;">
<div class="modal-header"> <div class="modal-header">
<h3>Share File: ${escapeHTML(file.name)}</h3> <h3>${t("share_file")}: ${escapeHTML(file.name)}</h3>
<span class="close-image-modal" id="closeShareModal" title="Close">&times;</span> <span class="close-image-modal" id="closeShareModal" title="Close">&times;</span>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p>Set Expiration:</p> <p>${t("set_expiration")}</p>
<select id="shareExpiration"> <select id="shareExpiration">
<option value="30">30 minutes</option> <option value="30">30 minutes</option>
<option value="60" selected>60 minutes</option> <option value="60" selected>60 minutes</option>
@@ -26,13 +27,13 @@ export function openShareModal(file, folder) {
<option value="1440">1 Day</option> <option value="1440">1 Day</option>
</select> </select>
<p>Password (optional):</p> <p>Password (optional):</p>
<input type="text" id="sharePassword" placeholder="No password by default" style="width: 100%;"/> <input type="text" id="sharePassword" placeholder=${t("password_optional")} style="width: 100%;"/>
<br> <br>
<button id="generateShareLinkBtn" class="btn btn-primary" style="margin-top:10px;">Generate Share Link</button> <button id="generateShareLinkBtn" class="btn btn-primary" style="margin-top:10px;">${t("generate_share_link")}</button>
<div id="shareLinkDisplay" style="margin-top: 10px; display:none;"> <div id="shareLinkDisplay" style="margin-top: 10px; display:none;">
<p>Shareable Link:</p> <p>${t("shareable_link")}</p>
<input type="text" id="shareLinkInput" readonly style="width:100%;"/> <input type="text" id="shareLinkInput" readonly style="width:100%;"/>
<button id="copyShareLinkBtn" class="btn btn-primary" style="margin-top:5px;">Copy Link</button> <button id="copyShareLinkBtn" class="btn btn-primary" style="margin-top:5px;">${t("copy_link")}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -4,6 +4,7 @@
// updating the file row display with tag badges, // updating the file row display with tag badges,
// filtering the file list by tag, and persisting tag data. // filtering the file list by tag, and persisting tag data.
import { escapeHTML } from './domUtils.js'; import { escapeHTML } from './domUtils.js';
import { t } from './i18n.js';
export function openTagModal(file) { export function openTagModal(file) {
// Create the modal element. // Create the modal element.
@@ -13,14 +14,14 @@ export function openTagModal(file) {
modal.innerHTML = ` modal.innerHTML = `
<div class="modal-content" style="width: 400px; max-width:90vw;"> <div class="modal-content" style="width: 400px; max-width:90vw;">
<div class="modal-header" style="display:flex; justify-content:space-between; align-items:center;"> <div class="modal-header" style="display:flex; justify-content:space-between; align-items:center;">
<h3 style="margin:0;">Tag File: ${file.name}</h3> <h3 style="margin:0;">${t("tag_file")}: ${file.name}</h3>
<span id="closeTagModal" style="cursor:pointer; font-size:24px;">&times;</span> <span id="closeTagModal" style="cursor:pointer; font-size:24px;">&times;</span>
</div> </div>
<div class="modal-body" style="margin-top:10px;"> <div class="modal-body" style="margin-top:10px;">
<label for="tagNameInput">Tag Name:</label> <label for="tagNameInput">${t("tag_name")}</label>
<input type="text" id="tagNameInput" placeholder="Enter tag name" style="width:100%; padding:5px;"/> <input type="text" id="tagNameInput" placeholder="Enter tag name" style="width:100%; padding:5px;"/>
<br><br> <br><br>
<label for="tagColorInput">Tag Color:</label> <label for="tagColorInput">${t("tag_name")}</label>
<input type="color" id="tagColorInput" value="#ff0000" style="width:100%; padding:5px;"/> <input type="color" id="tagColorInput" value="#ff0000" style="width:100%; padding:5px;"/>
<br><br> <br><br>
<div id="customTagDropdown" style="max-height:150px; overflow-y:auto; border:1px solid #ccc; margin-top:5px; padding:5px;"> <div id="customTagDropdown" style="max-height:150px; overflow-y:auto; border:1px solid #ccc; margin-top:5px; padding:5px;">
@@ -28,7 +29,7 @@ export function openTagModal(file) {
</div> </div>
<br> <br>
<div style="text-align:right;"> <div style="text-align:right;">
<button id="saveTagBtn" class="btn btn-primary">Save Tag</button> <button id="saveTagBtn" class="btn btn-primary">${t("save_tag")}</button>
</div> </div>
<div id="currentTags" style="margin-top:10px; font-size:0.9em;"> <div id="currentTags" style="margin-top:10px; font-size:0.9em;">
<!-- Existing tags will be listed here --> <!-- Existing tags will be listed here -->

View File

@@ -2,6 +2,7 @@
import { loadFileList } from './fileListView.js'; import { loadFileList } from './fileListView.js';
import { showToast, escapeHTML, attachEnterKeyListener } from './domUtils.js'; import { showToast, escapeHTML, attachEnterKeyListener } from './domUtils.js';
import { t } from './i18n.js';
/* ---------------------- /* ----------------------
Helper Functions (Data/State) Helper Functions (Data/State)
@@ -112,7 +113,7 @@ function breadcrumbClickHandler(e) {
// Update the container with sanitized breadcrumbs. // Update the container with sanitized breadcrumbs.
const container = document.getElementById("fileListTitle"); const container = document.getElementById("fileListTitle");
const sanitizedBreadcrumb = DOMPurify.sanitize(renderBreadcrumb(folder)); const sanitizedBreadcrumb = DOMPurify.sanitize(renderBreadcrumb(folder));
container.innerHTML = "Files in (" + sanitizedBreadcrumb + ")"; container.innerHTML = t("files_in") + " (" + sanitizedBreadcrumb + ")";
expandTreePath(folder); expandTreePath(folder);
document.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected")); document.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));

449
js/i18n.js Normal file
View File

@@ -0,0 +1,449 @@
/* i18n.js */
const translations = {
en: { /* English translations */
"please_log_in_to_continue": "Please log in to continue.",
"no_files_selected": "No files selected.",
"confirm_delete_files": "Are you sure you want to delete {count} selected file(s)?",
"element_not_found": "Element with id \"{id}\" not found.",
"search_placeholder": "Search files or tag...",
"file_name": "File Name",
"date_modified": "Date Modified",
"upload_date": "Upload Date",
"file_size": "File Size",
"uploader": "Uploader",
"enter_totp_code": "Enter TOTP Code",
"use_recovery_code_instead": "Use Recovery Code instead",
"enter_recovery_code": "Enter Recovery Code",
"editing": "Editing",
"decrease_font": "A-",
"increase_font": "A+",
"save": "Save",
"close": "Close",
"no_files_found": "No files found.",
"switch_to_table_view": "Switch to Table View",
"switch_to_gallery_view": "Switch to Gallery View",
"share_file": "Share File",
"set_expiration": "Set Expiration:",
"password_optional": "Password (optional):",
"generate_share_link": "Generate Share Link",
"shareable_link": "Shareable Link:",
"copy_link": "Copy Link",
"tag_file": "Tag File",
"tag_name": "Tag Name:",
"tag_color": "Tag Color:",
"save_tag": "Save Tag",
"files_in": "Files in",
"light_mode": "Light Mode",
"dark_mode": "Dark Mode",
"upload_instruction": "Drop files/folders here or click 'Choose files'",
"no_files_selected_default": "No files selected",
"choose_files": "Choose files",
"delete_selected": "Delete Selected",
"copy_selected": "Copy Selected",
"move_selected": "Move Selected",
"tag_selected": "Tag Selected",
"download_zip": "Download Zip",
"extract_zip": "Extract Zip",
"preview": "Preview",
"edit": "Edit",
"rename": "Rename",
"trash_empty": "Trash is empty.",
"no_trash_selected": "No trash items selected for restore.",
// Additional keys for HTML translations:
"title": "FileRise",
"header_title": "FileRise",
"logout": "Logout",
"change_password": "Change Password",
"restore_text": "Restore or",
"delete_text": "Delete Trash Items",
"restore_selected": "Restore Selected",
"restore_all": "Restore All",
"delete_selected_trash": "Delete Selected",
"delete_all": "Delete All",
"upload_header": "Upload Files/Folders",
// Folder Management keys:
"folder_navigation": "Folder Navigation & Management",
"create_folder": "Create Folder",
"create_folder_title": "Create Folder",
"enter_folder_name": "Enter folder name",
"cancel": "Cancel",
"create": "Create",
"rename_folder": "Rename Folder",
"rename_folder_title": "Rename Folder",
"rename_folder_placeholder": "Enter new folder name",
"delete_folder": "Delete Folder",
"delete_folder_title": "Delete Folder",
"delete_folder_message": "Are you sure you want to delete this folder?",
"folder_help": "Folder Help",
"folder_help_item_1": "Click on a folder in the tree to view its files.",
"folder_help_item_2": "Use [-] to collapse and [+] to expand folders.",
"folder_help_item_3": "Select a folder and click \"Create Folder\" to add a subfolder.",
"folder_help_item_4": "To rename or delete a folder, select it and then click the appropriate button.",
// File List keys:
"file_list_title": "Files in (Root)",
"delete_files": "Delete Files",
"delete_selected_files_title": "Delete Selected Files",
"delete_files_message": "Are you sure you want to delete the selected files?",
"copy_files": "Copy Files",
"copy_files_title": "Copy Selected Files",
"copy_files_message": "Select a target folder for copying the selected files:",
"move_files": "Move Files",
"move_files_title": "Move Selected Files",
"move_files_message": "Select a target folder for moving the selected files:",
"move": "Move",
"extract_zip_button": "Extract Zip",
"download_zip_title": "Download Selected Files as Zip",
"download_zip_prompt": "Enter a name for the zip file:",
"zip_placeholder": "files.zip",
// Login Form keys:
"login": "Login",
"remember_me": "Remember me",
"login_oidc": "Login with OIDC",
"basic_http_login": "Use Basic HTTP Login",
// Change Password keys:
"change_password_title": "Change Password",
"old_password": "Old Password",
"new_password": "New Password",
"confirm_new_password": "Confirm New Password",
// Add User keys:
"create_new_user_title": "Create New User",
"username": "Username:",
"password": "Password:",
"grant_admin": "Grant Admin Access",
"save_user": "Save User",
// Remove User keys:
"remove_user_title": "Remove User",
"select_user_remove": "Select a user to remove:",
"delete_user": "Delete User",
// Rename File keys:
"rename_file_title": "Rename File",
"rename_file_placeholder": "Enter new file name",
// Custom Confirm Modal keys:
"yes": "Yes",
"no": "No",
"delete": "Delete",
"download": "Download",
"upload": "Upload",
"copy": "Copy",
"extract": "Extract",
// Dark Mode Toggle
"dark_mode_toggle": "Dark Mode"
},
es: { /* Spanish translations */
"please_log_in_to_continue": "Por favor, inicie sesión para continuar.",
"no_files_selected": "No se seleccionaron archivos.",
"confirm_delete_files": "¿Está seguro de que desea eliminar {count} archivo(s) seleccionado(s)?",
"element_not_found": "Elemento con id \"{id}\" no encontrado.",
"search_placeholder": "Buscar archivos o etiqueta...",
"file_name": "Nombre del archivo",
"date_modified": "Fecha de modificación",
"upload_date": "Fecha de carga",
"file_size": "Tamaño del archivo",
"uploader": "Cargado por",
"enter_totp_code": "Ingrese el código TOTP",
"use_recovery_code_instead": "Usar código de recuperación en su lugar",
"enter_recovery_code": "Ingrese el código de recuperación",
"editing": "Editando",
"decrease_font": "A-",
"increase_font": "A+",
"save": "Guardar",
"close": "Cerrar",
"no_files_found": "No se encontraron archivos.",
"switch_to_table_view": "Cambiar a vista de tabla",
"switch_to_gallery_view": "Cambiar a vista de galería",
"share_file": "Compartir archivo",
"set_expiration": "Establecer vencimiento:",
"password_optional": "Contraseña (opcional):",
"generate_share_link": "Generar enlace para compartir",
"shareable_link": "Enlace para compartir:",
"copy_link": "Copiar enlace",
"tag_file": "Etiquetar archivo",
"tag_name": "Nombre de la etiqueta:",
"tag_color": "Color de la etiqueta:",
"save_tag": "Guardar etiqueta",
"files_in": "Archivos en",
"light_mode": "Modo claro",
"dark_mode": "Modo oscuro",
"upload_instruction": "Suelte archivos/carpetas o haga clic en 'Elegir archivos'",
"no_files_selected_default": "No se seleccionaron archivos",
"choose_files": "Elegir archivos",
"delete_selected": "Eliminar seleccionados",
"copy_selected": "Copiar seleccionados",
"move_selected": "Mover seleccionados",
"tag_selected": "Etiquetar seleccionados",
"download_zip": "Descargar Zip",
"extract_zip": "Extraer Zip",
"preview": "Vista previa",
"edit": "Editar",
"rename": "Renombrar",
"trash_empty": "La papelera está vacía.",
"no_trash_selected": "No se seleccionaron elementos de la papelera para restaurar.",
// Additional keys for HTML translations:
"title": "FileRise",
"header_title": "FileRise",
"logout": "Cerrar sesión",
"change_password": "Cambiar contraseña",
"restore_text": "Restaurar o",
"delete_text": "Eliminar elementos de la papelera",
"restore_selected": "Restaurar seleccionados",
"restore_all": "Restaurar todo",
"delete_selected_trash": "Eliminar seleccionados",
"delete_all": "Eliminar todo",
"upload_header": "Cargar archivos/carpetas",
// Folder Management keys:
"folder_navigation": "Navegación y gestión de carpetas",
"create_folder": "Crear carpeta",
"create_folder_title": "Crear carpeta",
"enter_folder_name": "Ingrese el nombre de la carpeta",
"cancel": "Cancelar",
"create": "Crear",
"rename_folder": "Renombrar carpeta",
"rename_folder_title": "Renombrar carpeta",
"rename_folder_placeholder": "Ingrese el nuevo nombre de la carpeta",
"delete_folder": "Eliminar carpeta",
"delete_folder_title": "Eliminar carpeta",
"delete_folder_message": "¿Está seguro de que desea eliminar esta carpeta?",
"folder_help": "Ayuda de carpetas",
"folder_help_item_1": "Haga clic en una carpeta en el árbol para ver sus archivos.",
"folder_help_item_2": "Utilice [-] para colapsar y [+] para expandir carpetas.",
"folder_help_item_3": "Seleccione una carpeta y haga clic en \"Crear carpeta\" para agregar una subcarpeta.",
"folder_help_item_4": "Para renombrar o eliminar una carpeta, selecciónela y luego haga clic en el botón correspondiente.",
// File List keys:
"file_list_title": "Archivos en (Raíz)",
"delete_files": "Eliminar archivos",
"delete_selected_files_title": "Eliminar archivos seleccionados",
"delete_files_message": "¿Está seguro de que desea eliminar los archivos seleccionados?",
"copy_files": "Copiar archivos",
"copy_files_title": "Copiar archivos seleccionados",
"copy_files_message": "Seleccione una carpeta destino para copiar los archivos seleccionados:",
"move_files": "Mover archivos",
"move_files_title": "Mover archivos seleccionados",
"move_files_message": "Seleccione una carpeta destino para mover los archivos seleccionados:",
"move": "Mover",
"extract_zip_button": "Extraer Zip",
"download_zip_title": "Descargar archivos seleccionados en Zip",
"download_zip_prompt": "Ingrese un nombre para el archivo Zip:",
"zip_placeholder": "archivos.zip",
// Login Form keys:
"login": "Iniciar sesión",
"remember_me": "Recuérdame",
"login_oidc": "Iniciar sesión con OIDC",
"basic_http_login": "Usar autenticación HTTP básica",
// Change Password keys:
"change_password_title": "Cambiar contraseña",
"old_password": "Contraseña antigua",
"new_password": "Nueva contraseña",
"confirm_new_password": "Confirmar nueva contraseña",
// Add User keys:
"create_new_user_title": "Crear nuevo usuario",
"username": "Usuario:",
"password": "Contraseña:",
"grant_admin": "Otorgar acceso de administrador",
"save_user": "Guardar usuario",
// Remove User keys:
"remove_user_title": "Eliminar usuario",
"select_user_remove": "Seleccione un usuario para eliminar:",
"delete_user": "Eliminar usuario",
// Rename File keys:
"rename_file_title": "Renombrar archivo",
"rename_file_placeholder": "Ingrese el nuevo nombre del archivo",
// Custom Confirm Modal keys:
"yes": "Sí",
"no": "No",
"delete": "Eliminar",
"download": "Descargar",
"upload": "Cargar",
"copy": "Copiar",
"extract": "Extraer",
// Dark Mode Toggle
"dark_mode_toggle": "Modo oscuro"
},
fr: { /* French translations */
"please_log_in_to_continue": "Veuillez vous connecter pour continuer.",
"no_files_selected": "Aucun fichier sélectionné.",
"confirm_delete_files": "Êtes-vous sûr de vouloir supprimer {count} fichier(s) sélectionné(s) ?",
"element_not_found": "Élément avec l'id \"{id}\" non trouvé.",
"search_placeholder": "Rechercher des fichiers ou un tag...",
"file_name": "Nom du fichier",
"date_modified": "Date de modification",
"upload_date": "Date de téléchargement",
"file_size": "Taille du fichier",
"uploader": "Téléversé par",
"enter_totp_code": "Entrez le code TOTP",
"use_recovery_code_instead": "Utilisez le code de récupération à la place",
"enter_recovery_code": "Entrez le code de récupération",
"editing": "Modification",
"decrease_font": "A-",
"increase_font": "A+",
"save": "Enregistrer",
"close": "Fermer",
"no_files_found": "Aucun fichier trouvé.",
"switch_to_table_view": "Passer à la vue tableau",
"switch_to_gallery_view": "Passer à la vue galerie",
"share_file": "Partager le fichier",
"set_expiration": "Définir l'expiration :",
"password_optional": "Mot de passe (facultatif) :",
"generate_share_link": "Générer un lien de partage",
"shareable_link": "Lien partageable :",
"copy_link": "Copier le lien",
"tag_file": "Marquer le fichier",
"tag_name": "Nom du tag :",
"tag_color": "Couleur du tag :",
"save_tag": "Enregistrer le tag",
"files_in": "Fichiers dans",
"light_mode": "Mode clair",
"dark_mode": "Mode sombre",
"upload_instruction": "Déposez vos fichiers/dossiers ici ou cliquez sur 'Choisir des fichiers'",
"no_files_selected_default": "Aucun fichier sélectionné",
"choose_files": "Choisir des fichiers",
"delete_selected": "Supprimer la sélection",
"copy_selected": "Copier la sélection",
"move_selected": "Déplacer la sélection",
"tag_selected": "Marquer la sélection",
"download_zip": "Télécharger en Zip",
"extract_zip": "Extraire le Zip",
"preview": "Aperçu",
"edit": "Modifier",
"rename": "Renommer",
"trash_empty": "La corbeille est vide.",
"no_trash_selected": "Aucun élément de la corbeille sélectionné pour restauration.",
// Additional keys for HTML translations:
"title": "FileRise",
"header_title": "FileRise",
"logout": "Se déconnecter",
"change_password": "Changer le mot de passe",
"restore_text": "Restaurer ou",
"delete_text": "Supprimer les éléments de la corbeille",
"restore_selected": "Restaurer la sélection",
"restore_all": "Restaurer tout",
"delete_selected_trash": "Supprimer la sélection",
"delete_all": "Supprimer tout",
"upload_header": "Téléverser des fichiers/dossiers",
// Folder Management keys:
"folder_navigation": "Navigation et gestion des dossiers",
"create_folder": "Créer un dossier",
"create_folder_title": "Créer un dossier",
"enter_folder_name": "Entrez le nom du dossier",
"cancel": "Annuler",
"create": "Créer",
"rename_folder": "Renommer le dossier",
"rename_folder_title": "Renommer le dossier",
"rename_folder_placeholder": "Entrez le nouveau nom du dossier",
"delete_folder": "Supprimer le dossier",
"delete_folder_title": "Supprimer le dossier",
"delete_folder_message": "Êtes-vous sûr de vouloir supprimer ce dossier ?",
"folder_help": "Aide des dossiers",
"folder_help_item_1": "Cliquez sur un dossier dans l'arborescence pour voir ses fichiers.",
"folder_help_item_2": "Utilisez [-] pour réduire et [+] pour développer les dossiers.",
"folder_help_item_3": "Sélectionnez un dossier et cliquez sur \"Créer un dossier\" pour ajouter un sous-dossier.",
"folder_help_item_4": "Pour renommer ou supprimer un dossier, sélectionnez-le puis cliquez sur le bouton approprié.",
// File List keys:
"file_list_title": "Fichiers dans (Racine)",
"delete_files": "Supprimer les fichiers",
"delete_selected_files_title": "Supprimer les fichiers sélectionnés",
"delete_files_message": "Êtes-vous sûr de vouloir supprimer les fichiers sélectionnés ?",
"copy_files": "Copier les fichiers",
"copy_files_title": "Copier les fichiers sélectionnés",
"copy_files_message": "Sélectionnez un dossier de destination pour copier les fichiers sélectionnés :",
"move_files": "Déplacer les fichiers",
"move_files_title": "Déplacer les fichiers sélectionnés",
"move_files_message": "Sélectionnez un dossier de destination pour déplacer les fichiers sélectionnés :",
"move": "Déplacer",
"extract_zip_button": "Extraire le Zip",
"download_zip_title": "Télécharger les fichiers sélectionnés en Zip",
"download_zip_prompt": "Entrez un nom pour le fichier Zip :",
"zip_placeholder": "fichiers.zip",
// Login Form keys:
"login": "Connexion",
"remember_me": "Se souvenir de moi",
"login_oidc": "Connexion avec OIDC",
"basic_http_login": "Utiliser l'authentification HTTP basique",
// Change Password keys:
"change_password_title": "Changer le mot de passe",
"old_password": "Ancien mot de passe",
"new_password": "Nouveau mot de passe",
"confirm_new_password": "Confirmer le nouveau mot de passe",
// Add User keys:
"create_new_user_title": "Créer un nouvel utilisateur",
"username": "Nom d'utilisateur :",
"password": "Mot de passe :",
"grant_admin": "Accorder les droits d'administrateur",
"save_user": "Enregistrer l'utilisateur",
// Remove User keys:
"remove_user_title": "Supprimer l'utilisateur",
"select_user_remove": "Sélectionnez un utilisateur à supprimer :",
"delete_user": "Supprimer l'utilisateur",
// Rename File keys:
"rename_file_title": "Renommer le fichier",
"rename_file_placeholder": "Entrez le nouveau nom du fichier",
// Custom Confirm Modal keys:
"yes": "Oui",
"no": "Non",
"delete": "Supprimer",
"download": "Télécharger",
"upload": "Téléverser",
"copy": "Copier",
"extract": "Extraire",
// Dark Mode Toggle
"dark_mode_toggle": "Mode sombre"
}
};
let currentLocale = 'en';
export function setLocale(locale) {
currentLocale = locale;
}
export function t(key, placeholders) {
const localeTranslations = translations[currentLocale] || {};
let translation = localeTranslations[key] || key;
if (placeholders) {
Object.keys(placeholders).forEach(ph => {
translation = translation.replace(`{${ph}}`, placeholders[ph]);
});
}
return translation;
}
export function applyTranslations() {
document.querySelectorAll('[data-i18n-key]').forEach(el => {
el.innerText = t(el.getAttribute('data-i18n-key'));
});
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
el.setAttribute('placeholder', t(el.getAttribute('data-i18n-placeholder')));
});
document.querySelectorAll('[data-i18n-title]').forEach(el => {
el.setAttribute('title', t(el.getAttribute('data-i18n-title')));
});
}

View File

@@ -10,6 +10,7 @@ import { displayFilePreview } from './filePreview.js';
import { loadFileList } from './fileListView.js'; import { loadFileList } from './fileListView.js';
import { initFileActions, renameFile } from './fileActions.js'; import { initFileActions, renameFile } from './fileActions.js';
import { editFile, saveFile } from './fileEditor.js'; import { editFile, saveFile } from './fileEditor.js';
import { t, applyTranslations, setLocale } from './i18n.js';
function loadCsrfTokenWithRetry(retries = 3, delay = 1000) { function loadCsrfTokenWithRetry(retries = 3, delay = 1000) {
return fetch('token.php', { credentials: 'include' }) return fetch('token.php', { credentials: 'include' })
@@ -55,6 +56,7 @@ function loadCsrfTokenWithRetry(retries = 3, delay = 1000) {
}); });
} }
// Expose functions for inline handlers. // Expose functions for inline handlers.
window.sendRequest = sendRequest; window.sendRequest = sendRequest;
window.toggleVisibility = toggleVisibility; window.toggleVisibility = toggleVisibility;
@@ -67,6 +69,12 @@ window.renameFile = renameFile;
window.currentFolder = "root"; window.currentFolder = "root";
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
// Retrieve the saved language from localStorage; default to "en"
const savedLanguage = localStorage.getItem("language") || "en";
// Set the locale based on the saved language
setLocale(savedLanguage);
// Apply the translations to update the UI
applyTranslations();
// First, load the CSRF token (with retry). // First, load the CSRF token (with retry).
loadCsrfTokenWithRetry().then(() => { loadCsrfTokenWithRetry().then(() => {
// Once CSRF token is loaded, initialize authentication. // Once CSRF token is loaded, initialize authentication.
@@ -104,7 +112,7 @@ document.addEventListener("DOMContentLoaded", function () {
// Other DOM initialization that can happen after CSRF is ready. // Other DOM initialization that can happen after CSRF is ready.
const newPasswordInput = document.getElementById("newPassword"); const newPasswordInput = document.getElementById("newPassword");
if (newPasswordInput) { if (newPasswordInput) {
newPasswordInput.addEventListener("input", function() { newPasswordInput.addEventListener("input", function () {
console.log("newPassword input event:", this.value); console.log("newPassword input event:", this.value);
}); });
} else { } else {
@@ -149,10 +157,10 @@ document.addEventListener("DOMContentLoaded", function () {
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (event) => { window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (event) => {
if (event.matches) { if (event.matches) {
document.body.classList.add("dark-mode"); document.body.classList.add("dark-mode");
if (darkModeToggle) darkModeToggle.textContent = "Light Mode"; if (darkModeToggle) darkModeToggle.textContent = t("light_mode");
} else { } else {
document.body.classList.remove("dark-mode"); document.body.classList.remove("dark-mode");
if (darkModeToggle) darkModeToggle.textContent = "Dark Mode"; if (darkModeToggle) darkModeToggle.textContent = t("dark_mode");
} }
}); });
} }

View File

@@ -3,6 +3,7 @@ import { sendRequest } from './networkUtils.js';
import { toggleVisibility, showToast } from './domUtils.js'; import { toggleVisibility, showToast } from './domUtils.js';
import { loadFileList } from './fileListView.js'; import { loadFileList } from './fileListView.js';
import { loadFolderTree } from './folderManager.js'; import { loadFolderTree } from './folderManager.js';
import { t } from './i18n.js';
function showConfirm(message, onConfirm) { function showConfirm(message, onConfirm) {
const modal = document.getElementById("customConfirmModal"); const modal = document.getElementById("customConfirmModal");
@@ -65,7 +66,7 @@ export function setupTrashRestoreDelete() {
const files = Array.from(selected).map(chk => chk.value); const files = Array.from(selected).map(chk => chk.value);
console.log("Restore Selected clicked, files:", files); console.log("Restore Selected clicked, files:", files);
if (files.length === 0) { if (files.length === 0) {
showToast("No trash items selected for restore."); showToast(t("no_trash_selected"));
return; return;
} }
fetch("restoreFiles.php", { fetch("restoreFiles.php", {
@@ -105,7 +106,7 @@ export function setupTrashRestoreDelete() {
const files = Array.from(allChk).map(chk => chk.value); const files = Array.from(allChk).map(chk => chk.value);
console.log("Restore All clicked, files:", files); console.log("Restore All clicked, files:", files);
if (files.length === 0) { if (files.length === 0) {
showToast("Trash is empty."); showToast(t("trash_empty"));
return; return;
} }
fetch("restoreFiles.php", { fetch("restoreFiles.php", {

View File

@@ -3,6 +3,7 @@ import { displayFilePreview } from './filePreview.js';
import { showToast, escapeHTML } from './domUtils.js'; import { showToast, escapeHTML } from './domUtils.js';
import { loadFolderTree } from './folderManager.js'; import { loadFolderTree } from './folderManager.js';
import { loadFileList } from './fileListView.js'; import { loadFileList } from './fileListView.js';
import { t } from './i18n.js';
/* ----------------------------------------------------- /* -----------------------------------------------------
Helpers for DragandDrop Folder Uploads (Original Code) Helpers for DragandDrop Folder Uploads (Original Code)
@@ -55,14 +56,14 @@ function setDropAreaDefault() {
if (dropArea) { if (dropArea) {
dropArea.innerHTML = ` dropArea.innerHTML = `
<div id="uploadInstruction" class="upload-instruction"> <div id="uploadInstruction" class="upload-instruction">
Drop files/folders here or click 'Choose files' ${t("upload_instruction")}
</div> </div>
<div id="uploadFileRow" class="upload-file-row"> <div id="uploadFileRow" class="upload-file-row">
<button id="customChooseBtn" type="button">Choose files</button> <button id="customChooseBtn" type="button">${t("choose_files")}</button>
</div> </div>
<div id="fileInfoWrapper" class="file-info-wrapper"> <div id="fileInfoWrapper" class="file-info-wrapper">
<div id="fileInfoContainer" class="file-info-container"> <div id="fileInfoContainer" class="file-info-container">
<span id="fileInfoDefault">No files selected</span> <span id="fileInfoDefault"> ${t("no_files_selected_default")}</span>
</div> </div>
</div> </div>
<!-- File input for file picker (files only) --> <!-- File input for file picker (files only) -->