Merge pull request #317 from EdouardVanbelle/refactor/i18n

This commit is contained in:
Dionisio Pozo
2026-04-26 09:17:06 +02:00
committed by GitHub
35 changed files with 1026 additions and 528 deletions
+1 -1
View File
@@ -134,7 +134,7 @@ async function loadFiles(options = { insertHistory: true }) {
ui.showError(`
<div class="files-loading-spinner">
<div class="spinner"></div>
<span>${i18n ? i18n.t('files.loading') : 'Loading files…'}</span>
<span>${i18n.t('files.loading')}</span>
</div>
`);
}, 100);
+8 -15
View File
@@ -166,9 +166,7 @@ function setActionsBarMode(mode, force = false) {
elements.gridViewBtn = document.getElementById('grid-view-btn');
elements.listViewBtn = document.getElementById('list-view-btn');
if (i18n?.translateElement) {
i18n.translateElement(elements.actionsBar);
}
i18n.translateElement(elements.actionsBar);
if (mode === 'files') {
setupUploadDropdown();
@@ -395,7 +393,7 @@ function initApp() {
});
// Wait for translations to load before checking authentication
if (i18n?.isLoaded?.()) {
if (i18n.isLoaded()) {
// Translations already loaded, proceed with authentication
checkAuthentication();
} else {
@@ -408,7 +406,7 @@ function initApp() {
// Set a timeout as a fallback in case translations take too long
setTimeout(() => {
if (!i18n?.isLoaded?.()) {
if (!i18n.isLoaded()) {
console.warn('Translations loading timeout, proceeding with authentication anyway');
checkAuthentication();
}
@@ -727,16 +725,11 @@ function updateStorageUsageDisplay(userData) {
// Remove data-i18n attribute to prevent i18n from overwriting our value
storageInfo.removeAttribute('data-i18n');
// Use i18n if available
if (i18n?.t) {
storageInfo.textContent = i18n.t('storage.used', {
percentage: usagePercentage,
used: usedFormatted,
total: quotaFormatted
});
} else {
storageInfo.textContent = `${usagePercentage}% used (${usedFormatted} / ${quotaFormatted})`;
}
storageInfo.textContent = i18n.t('storage.used', {
percentage: usagePercentage,
used: usedFormatted,
total: quotaFormatted
});
}
console.log(`Updated storage display: ${usagePercentage}% (${usedFormatted} / ${quotaFormatted})`);
+2 -2
View File
@@ -151,8 +151,8 @@ function setCurrentSection(section) {
// Update page title
const titleKey = `nav.${section}`;
const defaultTitle = section.charAt(0).toUpperCase() + section.slice(1);
appElements.pageTitle.textContent = i18n ? i18n.t(titleKey) : defaultTitle;
// TODO check why no more used: const defaultTitle = section.charAt(0).toUpperCase() + section.slice(1);
appElements.pageTitle.textContent = i18n.t(titleKey);
appElements.pageTitle.setAttribute('data-i18n', titleKey);
// Hide sharedView when switching to any other section
+10 -11
View File
@@ -15,14 +15,13 @@ async function loadTrashItems() {
try {
if (multiSelect) multiSelect.clear();
ui.resetFilesList(); // ensure also list visible & error hidden
const _tt = i18n?.t ? i18n.t : (k) => k.split('.').pop();
elements.filesList.innerHTML = `
<div class="list-header trash-header">
<div data-i18n="files.name">${_tt('files.name')}</div>
<div data-i18n="files.type">${_tt('files.type')}</div>
<div data-i18n="trash.original_location">${_tt('trash.original_location')}</div>
<div data-i18n="trash.deleted_date">${_tt('trash.deleted_date')}</div>
<div data-i18n="trash.actions">${_tt('trash.actions')}</div>
<div data-i18n="files.name">${i18n.t('files.name')}</div>
<div data-i18n="files.type">${i18n.t('files.type')}</div>
<div data-i18n="trash.original_location">${i18n.t('trash.original_location')}</div>
<div data-i18n="trash.deleted_date">${i18n.t('trash.deleted_date')}</div>
<div data-i18n="trash.actions">${i18n.t('trash.actions')}</div>
</div>
`;
@@ -33,7 +32,7 @@ async function loadTrashItems() {
if (trashItems.length === 0) {
ui.showError(`
<i class="fas fa-trash empty-state-icon"></i>
<p>${i18n ? i18n.t('trash.empty_state') : 'The trash is empty'}</p>
<p>${i18n.t('trash.empty_state')}</p>
`);
return;
}
@@ -58,12 +57,12 @@ function addTrashItemToView(item) {
let iconSpecialClass = '';
if (!isFile) {
iconClass = item.icon_class || 'fas fa-folder';
typeLabel = i18n ? i18n.t('files.file_types.folder') : 'Folder';
typeLabel = i18n.t('files.file_types.folder');
} else {
iconClass = item.icon_class || (ui?.getIconClass ? ui.getIconClass(item.name) : 'fas fa-file');
iconSpecialClass = ui?.getIconSpecialClass ? ui.getIconSpecialClass(item.name) : '';
const cat = item.category || '';
typeLabel = cat ? (i18n ? i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat : cat) : i18n ? i18n.t('files.file_types.document') : 'Document';
typeLabel = cat ? i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat : i18n.t('files.file_types.document');
}
const isFolder = !isFile;
@@ -86,10 +85,10 @@ function addTrashItemToView(item) {
<div class="path-cell">${escapeHtml(item.original_path || '--')}</div>
<div class="date-cell">${escapeHtml(formattedDate)}</div>
<div class="actions-cell">
<button class="btn-restore" title="${i18n ? i18n.t('trash.restore') : 'Restore'}">
<button class="btn-restore" title="${i18n.t('trash.restore')}">
<i class="fas fa-undo"></i>
</button>
<button class="btn-delete" title="${i18n ? i18n.t('trash.delete_permanently') : 'Delete permanently'}">
<button class="btn-delete" title="${i18n.t('trash.delete_permanently')}">
<i class="fas fa-trash"></i>
</button>
</div>
+8 -14
View File
@@ -542,17 +542,11 @@ const ui = {
breadcrumb.innerHTML = '';
const path = app.breadcrumbPath; // [{id, name}, ...]
// Helper function to safely get translation text
const getTranslatedText = (key, defaultValue) => {
if (!i18n?.t) return defaultValue;
return i18n.t(key);
};
// -- Home icon (always present, clickable to go to root) --
const homeIcon = document.createElement('span');
homeIcon.className = 'breadcrumb-item breadcrumb-home';
homeIcon.innerHTML = '<i class="fas fa-home"></i>';
homeIcon.title = getTranslatedText('breadcrumb.home', 'Home');
homeIcon.title = i18n.t('breadcrumb.home');
// Home is always clickable if we have a home folder
if (app.userHomeFolderId) {
@@ -1265,7 +1259,7 @@ const ui = {
<div class="file-badge file-badge-favorite ${isFav ? '' : 'hidden'}"><i class="fas fa-star favorite-star-inline"></i></div>
<div class="file-badge file-badge-shared ${isShared ? '' : 'hidden'}"><i class="fas fa-share-alt"></i></div>
</div>
<div class="type-cell">${i18n ? i18n.t('files.file_types.folder') : 'Folder'}</div>
<div class="type-cell">${i18n.t('files.file_types.folder')}</div>
<div class="size-cell">--</div>
<div class="date-cell">${formattedDate}</div>
<div class="action-cell">
@@ -1288,7 +1282,7 @@ const ui = {
const iconClass = file.icon_class || this.getIconClass(file.name);
const iconSpecialClass = file.icon_special_class || this.getIconSpecialClass(file.name);
const cat = file.category || '';
const typeLabel = cat ? (i18n ? i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat : cat) : i18n ? i18n.t('files.file_types.document') : 'Document';
const typeLabel = cat ? i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat : i18n.t('files.file_types.document');
const fileSize = file.size_formatted || formatFileSize(file.size);
const formattedDate = formatDateTime(file.modified_at);
const isFav = favorites?.isFavorite(file.id, 'file');
@@ -1352,7 +1346,7 @@ const ui = {
<div></div><!-- actions -->
</div>`;
if (i18n?.translateElement) i18n.translateElement(filesList);
i18n.translateElement(filesList);
filesList.classList.remove('hidden');
filesContainerError?.classList.add('hidden');
@@ -1376,7 +1370,7 @@ const ui = {
const filesList = document.getElementById('files-list');
if (filesContainerError) filesContainerError.innerHTML = content;
if (i18n?.translateElement) i18n.translateElement(filesContainerError);
i18n.translateElement(filesContainerError);
filesContainerError?.classList.remove('hidden');
filesList?.classList.add('hidden');
@@ -1642,9 +1636,9 @@ if (document.readyState === 'loading') {
* @returns {Promise<boolean>} true if confirmed, false if cancelled
*/
function showConfirmDialog({ title, message, confirmText, cancelText, danger = true } = {}) {
const ct = confirmText || (i18n ? i18n.t('actions.delete') : 'Delete');
const cc = cancelText || (i18n ? i18n.t('actions.cancel') : 'Cancel');
const t = title || (i18n ? i18n.t('dialogs.confirm_title') : 'Confirm action');
const ct = confirmText || i18n.t('actions.delete');
const cc = cancelText || i18n.t('actions.cancel');
const t = title || i18n.t('dialogs.confirm_title');
return new Promise((resolve) => {
// Remove any previous confirm dialog
+4 -5
View File
@@ -209,10 +209,9 @@ function showUserProfileModal() {
const usedBytes = userData.storage_used_bytes || 0;
const quotaBytes = userData.storage_quota_bytes == null ? 10 * 1024 * 1024 * 1024 : userData.storage_quota_bytes;
const percentage = quotaBytes > 0 ? Math.min(Math.round((usedBytes / quotaBytes) * 100), 100) : 0;
// FIXME: use classes
const barColor = percentage > 90 ? '#ef4444' : percentage > 70 ? '#f59e0b' : '#22c55e';
const t = (key, fallback) => (i18n?.t ? i18n.t(key) || fallback : fallback);
const existing = document.getElementById('profile-modal-overlay');
if (existing) existing.remove();
@@ -225,11 +224,11 @@ function showUserProfileModal() {
<div class="about-modal-avatar">${initials}</div>
<h3 class="about-modal-username">${username}</h3>
<p class="about-modal-email">${email}</p>
<span class="about-modal-role ${role === 'admin' ? 'about-modal-role-admin' : 'about-modal-role-user'}">${role === 'admin' ? '🛡️ Admin' : `👤 ${t('user_menu.role_user', 'User')}`}</span>
<span class="about-modal-role ${role === 'admin' ? 'about-modal-role-admin' : 'about-modal-role-user'}">${role === 'admin' ? '🛡️ Admin' : `👤 ${i18n.t('user_menu.role_user')}`}</span>
</div>
<div class="about-modal-storage">
<div class="about-modal-storage-label">
<i class="fas fa-database"></i>${t('storage.title', 'Storage')}
<i class="fas fa-database"></i>${i18n.t('storage.title')}
</div>
<div class="about-modal-bar-bg">
<div class="about-modal-bar-fill" id="about-bar-fill"></div>
@@ -237,7 +236,7 @@ function showUserProfileModal() {
<div class="about-modal-bar-text">${percentage}% · ${formatFileSize(usedBytes)} / ${formatQuotaSize(quotaBytes)}</div>
</div>
<div class="about-modal-footer">
<button id="profile-modal-close" class="about-modal-close-btn">${t('actions.close', 'Close')}</button>
<button id="profile-modal-close" class="about-modal-close-btn">${i18n.t('actions.close')}</button>
</div>
</div>
`;
+2 -5
View File
@@ -66,7 +66,7 @@ function createLanguageSelector(containerId = 'language-selector') {
// Get current language
const languages = getAvailableLanguages();
const currentLocale = i18n ? i18n.getCurrentLocale() : 'en';
const currentLocale = i18n.getCurrentLocale();
const currentLang = languages.find((l) => l.code === currentLocale) || languages[0];
// Set initial HTML attributes
@@ -185,10 +185,7 @@ function closeDropdown(container) {
* Select a language
*/
async function selectLanguage(langCode, container) {
// Update i18n if available
if (i18n) {
await i18n.setLocale(langCode);
}
await i18n.setLocale(langCode);
// Update HTML lang attribute and dir for RTL languages
updateHtmlAttributes(langCode);
+9 -15
View File
@@ -108,16 +108,15 @@ const Modal = {
this.input.placeholder = placeholder;
this.input.value = value;
// Set button text (use i18n if available)
if (confirmText) {
this.confirmBtn.textContent = confirmText;
} else if (i18n) {
} else {
this.confirmBtn.textContent = i18n.t('actions.confirm');
}
if (cancelText) {
this.cancelBtn.textContent = cancelText;
} else if (i18n) {
} else {
this.cancelBtn.textContent = i18n.t('actions.cancel');
}
@@ -138,14 +137,12 @@ const Modal = {
* @returns {Promise<string|null>}
*/
promptNewFolder() {
const t = i18n ? i18n.t.bind(i18n) : (k) => k;
return this.prompt({
title: t('dialogs.new_folder_title') || 'New folder',
label: t('dialogs.folder_name') || 'Folder name',
placeholder: t('dialogs.folder_placeholder') || 'My folder',
title: i18n.t('dialogs.new_folder_title'),
label: i18n.t('dialogs.folder_name'),
placeholder: i18n.t('dialogs.folder_placeholder'),
icon: 'fa-folder-plus',
confirmText: t('actions.create') || 'Create'
confirmText: i18n.t('actions.create')
});
},
@@ -156,18 +153,15 @@ const Modal = {
* @returns {Promise<string|null>}
*/
promptRename(currentName, isFolder = false) {
const t = i18n ? i18n.t.bind(i18n) : (k) => k;
// For files, we want to select only the name part (without extension)
this._selectNameOnly = !isFolder;
return this.prompt({
title: t('dialogs.rename_title') || 'Renombrar',
label: t('dialogs.new_name') || 'Nuevo nombre',
title: i18n.t('dialogs.rename_title'),
label: i18n.t('dialogs.new_name'),
placeholder: '',
value: currentName,
icon: isFolder ? 'fa-folder' : 'fa-file',
confirmText: t('actions.rename') || 'Renombrar'
confirmText: i18n.t('actions.rename')
});
},
+4 -7
View File
@@ -149,9 +149,8 @@ const notifications = (() => {
item.className = 'notif-item';
item.id = batchId;
const t = i18n?.t || ((k) => k);
const uploadingText = folderName ? `📁 ${t('upload.uploading')} ${_esc(folderName)}…` : t('upload.uploading');
const filesLabel = t('upload.files');
const uploadingText = folderName ? `📁 ${i18n.t('upload.uploading')} ${_esc(folderName)}…` : i18n.t('upload.uploading');
const filesLabel = i18n.t('upload.files');
item.innerHTML = `
<div class="notif-item-icon upload"><i class="fas fa-cloud-upload-alt"></i></div>
@@ -254,8 +253,7 @@ const notifications = (() => {
const pctEl = $(`${batchId}-pct`);
const statsEl = $(`${batchId}-stats`);
const t = i18n?.t || ((k) => k);
const filesLabel = t('upload.files');
const filesLabel = i18n.t('upload.files');
if (fillEl) fillEl.style.width = `${pctVal}%`;
if (pctEl) pctEl.textContent = `${pctVal}%`;
@@ -282,8 +280,7 @@ const notifications = (() => {
const curEl = $(`${batchId}-current`);
if (curEl) curEl.textContent = '';
const t = i18n?.t || ((k) => k);
const completeText = t('upload.complete', {
const completeText = i18n.t('upload.complete', {
count: successCount,
total: totalFiles
});
+8 -20
View File
@@ -511,10 +511,7 @@ function initLanguageSelector() {
localStorage.setItem(LOCALE_KEY, selectedLanguage);
localStorage.setItem(FIRST_RUN_KEY, 'true');
// Update i18n if available
if (i18n?.setLocale) {
await i18n.setLocale(selectedLanguage);
}
await i18n.setLocale(selectedLanguage);
// Hide language panel
hidePanel(languagePanel);
@@ -635,7 +632,7 @@ async function configureOidcLoginUI() {
// Update button text with provider name
const btnTextEl = oidcBtn.querySelector('span');
if (btnTextEl && oidcInfo.provider_name) {
const template = i18n?.t ? i18n.t('auth.sso_login_provider') : 'Sign in with {{provider}}';
const template = i18n.t('auth.sso_login_provider');
btnTextEl.textContent = template.replace('{{provider}}', oidcInfo.provider_name);
}
@@ -942,8 +939,7 @@ if (isLoginPage && registerForm) {
// Validate passwords match
if (password !== confirmPassword) {
const errorMsg = i18n ? i18n.t('auth.passwords_mismatch') : 'Passwords do not match';
registerError.textContent = errorMsg;
registerError.textContent = i18n.t('auth.passwords_mismatch');
registerError.style.display = 'block';
return;
}
@@ -951,9 +947,7 @@ if (isLoginPage && registerForm) {
try {
await register(username, email, password);
// Show success message
const successMsg = i18n ? i18n.t('auth.account_success') : 'Account created successfully! You can now log in.';
registerSuccess.textContent = successMsg;
registerSuccess.textContent = i18n.t('auth.account_success');
registerSuccess.style.display = 'block';
// Clear form
@@ -965,8 +959,7 @@ if (isLoginPage && registerForm) {
hidePanel(registerPanel);
}, 2000);
} catch (error) {
const errorMsg = i18n ? i18n.t('auth.admin_create_error') : 'Error registering account';
registerError.textContent = error.message || errorMsg;
registerError.textContent = error.message || i18n.t('auth.admin_create_error');
registerError.style.display = 'block';
}
});
@@ -988,8 +981,7 @@ if (isLoginPage && adminSetupForm) {
// Validate passwords match
if (password !== confirmPassword) {
const errorMsg = i18n ? i18n.t('auth.passwords_mismatch') : 'Passwords do not match';
adminSetupError.textContent = errorMsg;
adminSetupError.textContent = i18n.t('auth.passwords_mismatch');
adminSetupError.style.display = 'block';
return;
}
@@ -1012,11 +1004,8 @@ if (isLoginPage && adminSetupForm) {
}
await response.json();
// Show success message in the GUI instead of alert
const successMsg = i18n ? i18n.t('auth.admin_success') : 'Admin account created successfully! You can now log in.';
if (adminSetupSuccess) {
adminSetupSuccess.textContent = successMsg;
adminSetupSuccess.textContent = i18n.t('auth.admin_success');
adminSetupSuccess.style.display = 'block';
}
@@ -1027,8 +1016,7 @@ if (isLoginPage && adminSetupForm) {
if (adminSetupSuccess) adminSetupSuccess.style.display = 'none';
}, 2000);
} catch (error) {
const errorMsg = i18n ? i18n.t('auth.admin_create_error') : 'Error creating admin account';
adminSetupError.textContent = error.message || errorMsg;
adminSetupError.textContent = error.message || i18n.t('auth.admin_create_error');
adminSetupError.style.display = 'block';
}
});
+21 -32
View File
@@ -27,7 +27,7 @@ const contextMenus = {
if (!option) return;
const label = option.querySelector('span');
if (!label) return;
label.textContent = i18n ? i18n.t(isFavorite ? 'actions.unfavorite' : 'actions.favorite') : isFavorite ? 'Remove from favorites' : 'Add to favorites';
label.textContent = i18n.t(isFavorite ? 'actions.unfavorite' : 'actions.favorite');
},
/**
@@ -382,7 +382,7 @@ const contextMenus = {
renameInput.value = folder.name;
// Update header text
const headerSpan = renameDialog.querySelector('.rename-dialog-header span');
if (headerSpan) headerSpan.textContent = i18n ? i18n.t('dialogs.rename_folder') : 'Rename folder';
if (headerSpan) headerSpan.textContent = i18n.t('dialogs.rename_folder');
renameDialog?.classList.remove('hidden');
renameInput.focus();
renameInput.select();
@@ -402,7 +402,7 @@ const contextMenus = {
renameInput.value = file.name;
// Update header text
const headerSpan = renameDialog.querySelector('.rename-dialog-header span');
if (headerSpan) headerSpan.textContent = i18n ? i18n.t('dialogs.rename_file') : 'Rename file';
if (headerSpan) headerSpan.textContent = i18n.t('dialogs.rename_file');
renameDialog?.classList.remove('hidden');
renameInput.focus();
renameInput.select();
@@ -471,7 +471,7 @@ const contextMenus = {
// Update dialog title (preserve icon)
const dialogHeader = document.getElementById('move-file-dialog').querySelector('.rename-dialog-header');
const titleText = mode === 'file' ? (i18n ? i18n.t('dialogs.move_file') : 'Move file') : i18n ? i18n.t('dialogs.move_folder') : 'Move folder';
const titleText = mode === 'file' ? i18n.t('dialogs.move_file') : i18n.t('dialogs.move_folder');
dialogHeader.innerHTML = `<i class="fas fa-arrows-alt dialog-header-icon"></i> <span>${titleText}</span>`;
// Load folders for the starting location
@@ -496,7 +496,7 @@ const contextMenus = {
async renameItem() {
const newName = document.getElementById('rename-input').value.trim();
if (!newName) {
alert(i18n ? i18n.t('errors.empty_name') : 'Name cannot be empty');
alert(i18n.t('errors.empty_name'));
return;
}
@@ -587,7 +587,7 @@ const contextMenus = {
currentFolderOption.className = 'folder-select-item folder-select-current';
currentFolderOption.innerHTML = `
<i class="fas fa-check-circle check-icon"></i>
<span>${i18n ? i18n.t('dialogs.select_this_folder') : 'Select this folder'}</span>
<span>${i18n.t('dialogs.select_this_folder')}</span>
`;
currentFolderOption.addEventListener('click', () => {
document.querySelectorAll('.folder-select-item').forEach((item) => {
@@ -606,7 +606,7 @@ const contextMenus = {
parentOption.className = 'folder-select-item folder-navigate-up';
parentOption.innerHTML = `
<i class="fas fa-level-up-alt"></i>
<span>${i18n ? i18n.t('dialogs.go_to_parent') : '.. (parent folder)'}</span>
<span>${i18n.t('dialogs.go_to_parent')}</span>
`;
parentOption.addEventListener('click', () => {
// Navigate to parent folder
@@ -664,7 +664,7 @@ const contextMenus = {
homeOption.className = 'folder-select-item folder-select-current';
homeOption.innerHTML = `
<i class="fas fa-check-circle check-icon"></i>
<span>${i18n ? i18n.t('dialogs.move_to_home') : 'Move to Home folder'}</span>
<span>${i18n.t('dialogs.move_to_home')}</span>
`;
homeOption.addEventListener('click', () => {
document.querySelectorAll('.folder-select-item').forEach((item) => {
@@ -678,7 +678,7 @@ const contextMenus = {
// Inside a subfolder with no children - show empty message
const emptyMsg = document.createElement('div');
emptyMsg.className = 'folder-select-empty';
emptyMsg.innerHTML = `<i class="fas fa-folder-open"></i> <span>${i18n ? i18n.t('dialogs.no_subfolders') : 'No subfolders to navigate'}</span>`;
emptyMsg.innerHTML = `<i class="fas fa-folder-open"></i> <span>${i18n.t('dialogs.no_subfolders')}</span>`;
folderSelectContainer.appendChild(emptyMsg);
}
@@ -686,9 +686,7 @@ const contextMenus = {
app.selectedTargetFolderId = parentFolderId || '';
// Translate new elements
if (i18n?.translateElement) {
i18n.translateElement(folderSelectContainer);
}
i18n.translateElement(folderSelectContainer);
} catch (error) {
console.error('Error loading folders:', error);
}
@@ -798,8 +796,7 @@ const contextMenus = {
const dialogHeader = shareDialog.querySelector('.share-dialog-header');
if (dialogHeader) {
const headerSpan = dialogHeader.querySelector('span');
const titleText =
itemType === 'file' ? (i18n ? i18n.t('dialogs.share_file') : 'Share file') : i18n ? i18n.t('dialogs.share_folder') : 'Share folder';
const titleText = itemType === 'file' ? i18n.t('dialogs.share_file') : i18n.t('dialogs.share_folder');
if (headerSpan) {
headerSpan.textContent = titleText;
} else {
@@ -900,9 +897,9 @@ const contextMenus = {
const shareId = btn.getAttribute('data-share-id');
showConfirmDialog({
title: i18n ? i18n.t('dialogs.confirm_delete_share') : 'Delete link',
message: i18n ? i18n.t('dialogs.confirm_delete_share_msg') : 'Are you sure you want to delete this shared link?',
confirmText: i18n ? i18n.t('actions.delete') : 'Delete'
title: i18n.t('dialogs.confirm_delete_share'),
message: i18n.t('dialogs.confirm_delete_share_msg'),
confirmText: i18n.t('actions.delete')
}).then(async (confirmed) => {
if (confirmed) {
await fileSharing.removeSharedLink(shareId);
@@ -997,10 +994,7 @@ const contextMenus = {
ui.setSharedVisualState(item.id, item.type, true);
// Show success message
ui.showNotification(
i18n ? i18n.t('notifications.link_created') : 'Link created',
i18n ? i18n.t('notifications.share_success') : 'Shared link created successfully'
);
ui.showNotification(i18n.t('notifications.link_created'), i18n.t('notifications.share_success'));
} catch (error) {
console.error('Error creating shared link:', error);
ui.showNotification('Error', error.message || 'Could not create shared link');
@@ -1088,7 +1082,7 @@ const contextMenus = {
// Update files info
if (filesInfo) {
filesInfo.innerHTML = `<strong>${i18n ? i18n.t('music.selected_files', 'Selected:') : 'Selected:'} </strong>${file.name}`;
filesInfo.innerHTML = `<strong>${i18n.t('music.selected_files')} </strong>${file.name}`;
}
// Reset selection
@@ -1112,17 +1106,15 @@ const contextMenus = {
this._renderPlaylistSelect(container, playlists);
} catch (err) {
console.error('Error loading playlists:', err);
container.innerHTML = `<div class="folder-select-empty">${i18n ? i18n.t('music.load_error', 'Error loading playlists') : 'Error loading playlists'}</div>`;
container.innerHTML = `<div class="folder-select-empty">${i18n.t('music.load_error')}</div>`;
}
},
_renderPlaylistSelect(container, playlists) {
const t = (key, fallback) => (i18n ? i18n.t(key, fallback) : fallback);
container.innerHTML = '';
if (playlists.length === 0) {
container.innerHTML = `<div class="folder-select-empty">${t('music.no_playlists', 'No playlists yet. Create one first!')}</div>`;
container.innerHTML = `<div class="folder-select-empty">${i18n.t('music.no_playlists')}</div>`;
return;
}
@@ -1133,7 +1125,7 @@ const contextMenus = {
item.innerHTML = `
<i class="fas fa-list"></i>
<span>${this._escapeHtml(playlist.name)}</span>
<span class="playlist-track-count">${playlist.track_count || 0} ${t('music.tracks', 'tracks')}</span>
<span class="playlist-track-count">${playlist.track_count || 0} ${i18n.t('music.tracks')}</span>
`;
item.addEventListener('click', () => {
@@ -1176,10 +1168,7 @@ const contextMenus = {
}
await resp.json();
ui.showNotification(
i18n ? i18n.t('music.added', 'Added!') : 'Added!',
`${files.length} ${files.length === 1 ? 'track' : 'tracks'} ${i18n ? i18n.t('music.added_to_playlist', 'added to playlist') : 'added to playlist'}`
);
ui.showNotification(i18n.t('music.added'), `${files.length} ${files.length === 1 ? 'track' : 'tracks'} ${i18n.t('music.added_to_playlist')}`);
this.closePlaylistDialog();
@@ -1189,7 +1178,7 @@ const contextMenus = {
}
} catch (err) {
console.error('Error adding to playlist:', err);
ui.showNotification(i18n ? i18n.t('music.error', 'Error') : 'Error', err.message || i18n.t('music.add_error', 'Could not add tracks to playlist'));
ui.showNotification(i18n.t('music.error'), err.message || i18n.t('music.add_error'));
if (addBtn) addBtn.disabled = false;
}
},
+19 -26
View File
@@ -30,12 +30,12 @@ const fileOps = {
/** Start a new upload batch in the notification bell */
_initUploadToast(totalFiles, folderName) {
this._currentBatchId = notifications ? notifications.addUploadBatch(totalFiles, folderName) : null;
this._currentBatchId = notifications.addUploadBatch(totalFiles, folderName);
},
/** Finalise the batch in the notification bell */
_finishUploadToast(successCount, totalFiles) {
if (notifications && this._currentBatchId) {
if (this._currentBatchId) {
notifications.finishBatch(this._currentBatchId, successCount, totalFiles);
}
},
@@ -361,7 +361,7 @@ const fileOps = {
progressBar.style.width = `${(uploadedCount / totalFiles) * 100}%`;
}
// Notify bell of per-file completion
if (notifications && batchId) {
if (batchId) {
try {
notifications.fileCompleted(batchId, result.ok);
} catch (e) {
@@ -383,7 +383,7 @@ const fileOps = {
});
}
if (result.isQuotaError) {
const msg = result.errorMsg || i18n?.t('storage_quota_exceeded') || 'Storage quota exceeded';
const msg = result.errorMsg || i18n.t('storage_quota_exceeded');
if (notifications) {
notifications.addNotification({
icon: 'fa-exclamation-triangle',
@@ -591,7 +591,7 @@ const fileOps = {
console.warn(`[SKIP] #${idx} ${rel} — cannot read 0-byte file (FIFO/pipe?), skipping`);
uploadedCount++;
successCount++;
if (notifications && batchId) {
if (batchId) {
try {
notifications.fileCompleted(batchId, true);
} catch (_) {}
@@ -623,7 +623,7 @@ const fileOps = {
uploadedCount++;
if (notifications && batchId) {
if (batchId) {
try {
notifications.fileCompleted(batchId, result.ok);
} catch (_) {}
@@ -994,10 +994,7 @@ const fileOps = {
console.log('Response status:', response.status);
if (response.ok) {
ui.showNotification(
i18n ? i18n.t('notifications.file_renamed') : 'File renamed',
i18n ? i18n.t('notifications.file_renamed_to', { name: newName }) : `File renamed to "${newName}"`
);
ui.showNotification(i18n.t('notifications.file_renamed'), i18n.t('notifications.file_renamed_to', { name: newName }));
return true;
} else {
const errorText = await response.text();
@@ -1075,9 +1072,9 @@ const fileOps = {
*/
async deleteFile(fileId, fileName) {
const confirmed = await showConfirmDialog({
title: i18n ? i18n.t('dialogs.confirm_delete') : 'Move to trash',
message: i18n ? i18n.t('dialogs.confirm_delete_file', { name: fileName }) : `Are you sure you want to move the file "${fileName}" to trash?`,
confirmText: i18n ? i18n.t('actions.delete') : 'Delete'
title: i18n.t('dialogs.confirm_delete'),
message: i18n.t('dialogs.confirm_delete_file', { name: fileName }),
confirmText: i18n.t('actions.delete')
});
if (!confirmed) return false;
@@ -1123,11 +1120,9 @@ const fileOps = {
*/
async deleteFolder(folderId, folderName) {
const confirmed = await showConfirmDialog({
title: i18n ? i18n.t('dialogs.confirm_delete') : 'Move to trash',
message: i18n
? i18n.t('dialogs.confirm_delete_folder', { name: folderName })
: `Are you sure you want to move the folder "${folderName}" and all its contents to trash?`,
confirmText: i18n ? i18n.t('actions.delete') : 'Delete'
title: i18n.t('dialogs.confirm_delete'),
message: i18n.t('dialogs.confirm_delete_folder', { name: folderName }),
confirmText: i18n.t('actions.delete')
});
if (!confirmed) return false;
@@ -1234,11 +1229,9 @@ const fileOps = {
*/
async deletePermanently(trashId) {
const confirmed = await showConfirmDialog({
title: i18n ? i18n.t('dialogs.confirm_permanent_delete') : 'Delete permanently',
message: i18n
? i18n.t('dialogs.confirm_permanent_delete_msg')
: 'Are you sure you want to permanently delete this item? This action cannot be undone.',
confirmText: i18n ? i18n.t('actions.delete_permanently') : 'Delete permanently'
title: i18n.t('dialogs.confirm_permanent_delete'),
message: i18n.t('dialogs.confirm_permanent_delete_msg'),
confirmText: i18n.t('actions.delete_permanently')
});
if (!confirmed) return false;
@@ -1268,9 +1261,9 @@ const fileOps = {
*/
async emptyTrash() {
const confirmed = await showConfirmDialog({
title: i18n ? i18n.t('dialogs.confirm_empty_trash') : 'Empty trash',
message: i18n ? i18n.t('trash.empty_confirm') : 'Are you sure you want to empty the trash? This action will permanently delete all items.',
confirmText: i18n ? i18n.t('actions.empty_trash') : 'Empty trash'
title: i18n.t('dialogs.confirm_empty_trash'),
message: i18n.t('trash.empty_confirm'),
confirmText: i18n.t('actions.empty_trash')
});
if (!confirmed) return false;
+2 -6
View File
@@ -50,12 +50,8 @@ const multiSelect = {
// ── Helpers for i18n ────────────────────────────────────
_t(key, vars) {
if (i18n && typeof i18n.t === 'function') {
const val = i18n.t(key, vars);
// If i18n returned the key itself, it's missing → fall back
if (val && val !== key) return val;
}
return null;
const val = i18n.t(key, vars);
return val !== key ? val : null;
},
// ── Selection state management ──────────────────────────
+4 -10
View File
@@ -110,10 +110,7 @@ const favorites = {
// Notify user
if (ui?.showNotification) {
ui.showNotification(
i18n ? i18n.t('favorites.added_title') : 'Added to favorites',
`"${name}" ${i18n ? i18n.t('favorites.added_msg') : 'added to favorites'}`
);
ui.showNotification(i18n.t('favorites.added_title'), `"${name}" ${i18n.t('favorites.added_msg')}`);
}
return true;
@@ -145,10 +142,7 @@ const favorites = {
this._cache.delete(this._cacheKey(id, type));
if (ui?.showNotification) {
ui.showNotification(
i18n ? i18n.t('favorites.removed_title') : 'Removed from favorites',
`"${itemName}" ${i18n ? i18n.t('favorites.removed_msg') : 'removed from favorites'}`
);
ui.showNotification(i18n.t('favorites.removed_title'), `"${itemName}" ${i18n.t('favorites.removed_msg')}`);
}
return true;
@@ -182,8 +176,8 @@ const favorites = {
if (this._cache.size === 0) {
ui.showError(`
<i class="fas fa-star empty-state-icon"></i>
<p>${i18n ? i18n.t('favorites.empty_state') : 'No favorite items'}</p>
<p>${i18n ? i18n.t('favorites.empty_hint') : 'To mark as favorite, right-click on any file or folder'}</p>
<p>${i18n.t('favorites.empty_state')}</p>
<p>${i18n.t('favorites.empty_hint')}</p>
`);
return;
}
+110 -168
View File
@@ -87,9 +87,6 @@ const musicView = {
if (!this._container) return;
// FIXME should call directly
const t = (key, _fallback = '') => {
return i18n.t(key);
};
// Empty state: no playlists at all — show full-width centered onboarding
if (this.playlists.length === 0) {
@@ -98,11 +95,11 @@ const musicView = {
<div class="music-empty-state-icon">
<i class="fas fa-music"></i>
</div>
<h3 class="music-empty-state-title">${t('music.no_playlists', 'No playlists yet')}</h3>
<p class="music-empty-state-desc">${t('music.empty_hint', 'Create your first playlist to start organizing your music')}</p>
<h3 class="music-empty-state-title">${i18n.t('music.no_playlists')}</h3>
<p class="music-empty-state-desc">${i18n.t('music.empty_hint')}</p>
<button class="btn btn-primary" id="music-create-playlist-btn">
<i class="fas fa-plus"></i>
<span>${t('music.create_playlist', 'Create Playlist')}</span>
<span>${i18n.t('music.create_playlist')}</span>
</button>
</div>
`;
@@ -118,8 +115,8 @@ const musicView = {
<div class="music-content">
<div class="music-sidebar">
<div class="music-sidebar-header">
<h3>${t('music.playlists', 'Playlists')}</h3>
<button class="music-sidebar-add-btn" id="music-create-playlist-btn" title="${t('music.create_playlist', 'Create Playlist')}">
<h3>${i18n.t('music.playlists')}</h3>
<button class="music-sidebar-add-btn" id="music-create-playlist-btn" title="${i18n.t('music.create_playlist')}">
<i class="fas fa-plus"></i>
</button>
</div>
@@ -128,44 +125,44 @@ const musicView = {
<div class="music-main">
<div class="music-welcome">
<i class="fas fa-music"></i>
<h3>${t('music.select_playlist', 'Select a playlist')}</h3>
<p>${t('music.select_hint', 'Choose a playlist from the sidebar or create a new one')}</p>
<h3>${i18n.t('music.select_playlist')}</h3>
<p>${i18n.t('music.select_hint')}</p>
</div>
<div class="music-playlist-detail hidden" id="music-playlist-detail">
<div class="music-playlist-header">
<div class="music-playlist-cover" id="music-playlist-cover" title="${t('music.set_cover', 'Set cover')}">
<div class="music-playlist-cover" id="music-playlist-cover" title="${i18n.t('music.set_cover')}">
<i class="fas fa-music"></i>
</div>
<div class="music-playlist-info">
<h2 id="music-playlist-name"></h2>
<p id="music-playlist-meta"></p>
<span class="music-public-badge hidden" id="music-public-badge">
<i class="fas fa-globe"></i> <span id="music-public-text">${t('music.public', 'Public')}</span>
<i class="fas fa-globe"></i> <span id="music-public-text">${i18n.t('music.public')}</span>
</span>
</div>
</div>
<div class="music-playlist-actions">
<button class="btn btn-secondary" id="music-play-all-btn">
<i class="fas fa-play"></i>
<span>${t('music.play_all', 'Play All')}</span>
<span>${i18n.t('music.play_all')}</span>
</button>
<button class="btn btn-secondary" id="music-shuffle-btn">
<i class="fas fa-shuffle"></i>
</button>
<button class="btn btn-secondary" id="music-add-tracks-btn">
<i class="fas fa-plus"></i>
<span>${t('music.add_tracks', 'Add Tracks')}</span>
<span>${i18n.t('music.add_tracks')}</span>
</button>
<button class="btn btn-secondary" id="music-edit-playlist-btn" title="${t('music.edit', 'Edit')}">
<button class="btn btn-secondary" id="music-edit-playlist-btn" title="${i18n.t('music.edit')}">
<i class="fas fa-pen"></i>
</button>
<button class="btn btn-secondary" id="music-share-playlist-btn" title="${t('music.share', 'Share')}">
<button class="btn btn-secondary" id="music-share-playlist-btn" title="${i18n.t('music.share')}">
<i class="fas fa-share-alt"></i>
</button>
<button class="btn btn-secondary" id="music-manage-shares-btn" title="${t('music.manage_shares', 'Manage Shares')}">
<button class="btn btn-secondary" id="music-manage-shares-btn" title="${i18n.t('music.manage_shares')}">
<i class="fas fa-users"></i>
</button>
<button class="btn btn-secondary" id="music-toggle-public-btn" title="${t('music.toggle_public', 'Toggle public')}">
<button class="btn btn-secondary" id="music-toggle-public-btn" title="${i18n.t('music.toggle_public')}">
<i class="fas fa-globe"></i>
</button>
<button class="btn btn-secondary" id="music-delete-playlist-btn">
@@ -186,15 +183,11 @@ const musicView = {
const listEl = document.getElementById('music-playlist-list');
if (!listEl) return;
const t = (key, fallback = '') => {
return i18n?.t ? i18n.t(key) : fallback || key;
};
if (this.playlists.length === 0) {
listEl.innerHTML = `
<div class="music-empty">
<i class="fas fa-music"></i>
<p>${t('music.no_playlists', 'No playlists yet')}</p>
<p>${i18n.t('music.no_playlists')}</p>
</div>
`;
return;
@@ -209,7 +202,7 @@ const musicView = {
</div>
<div class="music-playlist-item-info">
<span class="music-playlist-item-name">${this._escapeHtml(p.name)}</span>
<span class="music-playlist-item-count">${p.track_count || 0} ${t('music.tracks', 'tracks')}</span>
<span class="music-playlist-item-count">${p.track_count || 0} ${i18n.t('music.tracks')}</span>
</div>
</div>
`
@@ -291,10 +284,7 @@ const musicView = {
if (detailEl) detailEl.classList.remove('hidden');
if (nameEl) nameEl.textContent = playlist.name;
if (metaEl) {
const t = (key, fallback = '') => {
return i18n?.t ? i18n.t(key) : fallback || key;
};
metaEl.textContent = `${playlist.track_count || 0} ${t('music.tracks', 'tracks')}`;
metaEl.textContent = `${playlist.track_count || 0} ${i18n.t('music.tracks')}`;
}
// Cover art
@@ -314,8 +304,7 @@ const musicView = {
}
const togglePublicBtn = document.getElementById('music-toggle-public-btn');
if (togglePublicBtn) {
const t2 = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
togglePublicBtn.title = playlist.is_public ? t2('music.make_private', 'Make private') : t2('music.make_public', 'Make public');
togglePublicBtn.title = playlist.is_public ? i18n.t('music.make_private') : i18n.t('music.make_public');
togglePublicBtn.classList.toggle('active', playlist.is_public);
}
@@ -352,15 +341,11 @@ const musicView = {
const trackListEl = document.getElementById('music-track-list');
if (!trackListEl) return;
const t = (key, fallback = '') => {
return i18n?.t ? i18n.t(key) : fallback || key;
};
if (this.currentTracks.length === 0) {
trackListEl.innerHTML = `
<div class="music-empty">
<i class="fas fa-music"></i>
<p>${t('music.no_tracks', 'No tracks in this playlist')}</p>
<p>${i18n.t('music.no_tracks')}</p>
</div>
`;
return;
@@ -370,9 +355,9 @@ const musicView = {
<div class="music-track-header">
<span class="music-track-col music-track-drag"></span>
<span class="music-track-col music-track-num">#</span>
<span class="music-track-col music-track-title">${t('music.title', 'Title')}</span>
<span class="music-track-col music-track-artist">${t('music.artist', 'Artist')}</span>
<span class="music-track-col music-track-album">${t('music.album', 'Album')}</span>
<span class="music-track-col music-track-title">${i18n.t('music.title')}</span>
<span class="music-track-col music-track-artist">${i18n.t('music.artist')}</span>
<span class="music-track-col music-track-album">${i18n.t('music.album')}</span>
<span class="music-track-col music-track-duration"><i class="far fa-clock"></i></span>
<span class="music-track-col music-track-actions"></span>
</div>
@@ -387,13 +372,13 @@ const musicView = {
</span>
<span class="music-track-col music-track-title">
<i class="fas fa-music music-track-icon"></i>
<span class="music-track-name">${this._escapeHtml(track.title || track.file_name || t('music.unknown_title', 'Unknown'))}</span>
<span class="music-track-name">${this._escapeHtml(track.title || track.file_name || i18n.t('music.unknown_title'))}</span>
</span>
<span class="music-track-col music-track-artist">${this._escapeHtml(track.artist || t('music.unknown_artist', 'Unknown Artist'))}</span>
<span class="music-track-col music-track-artist">${this._escapeHtml(track.artist || i18n.t('music.unknown_artist'))}</span>
<span class="music-track-col music-track-album">${this._escapeHtml(track.album || '-')}</span>
<span class="music-track-col music-track-duration">${this._formatDuration(track.duration_secs)}</span>
<span class="music-track-col music-track-actions">
<button class="music-track-remove-btn" title="${t('music.remove', 'Remove')}"><i class="fas fa-times"></i></button>
<button class="music-track-remove-btn" title="${i18n.t('music.remove')}"><i class="fas fa-times"></i></button>
</span>
</div>
`
@@ -467,10 +452,7 @@ const musicView = {
_playTrack(idx) {
if (!this.currentTracks[idx]) return;
const t = (key, fallback = '') => {
return i18n?.t ? i18n.t(key) : fallback || key;
};
musicPlayer.setQueue(this.currentTracks, this.currentPlaylist?.name || t('music.playlists', 'Playlist'));
musicPlayer.setQueue(this.currentTracks, this.currentPlaylist?.name || i18n.t('music.playlists'));
musicPlayer.playTrack(idx);
},
@@ -487,25 +469,18 @@ const musicView = {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
const t = (key, fallback = '') => {
return i18n?.t ? i18n.t(key) : fallback || key;
};
musicPlayer.setQueue(shuffled, this.currentPlaylist?.name || t('music.shuffle', 'Shuffle'));
musicPlayer.setQueue(shuffled, this.currentPlaylist?.name || i18n.t('music.shuffle'));
musicPlayer.playTrack(0);
}
},
async _showCreatePlaylistDialog() {
const t = (key, fallback = '') => {
return i18n?.t ? i18n.t(key) : fallback || key;
};
const name = await Modal.prompt({
title: t('music.create_playlist', 'Create Playlist'),
label: t('music.playlist_name', 'Playlist name'),
placeholder: t('music.playlist_name', 'Playlist name'),
title: i18n.t('music.create_playlist'),
label: i18n.t('music.playlist_name'),
placeholder: i18n.t('music.playlist_name'),
icon: 'fa-music',
confirmText: t('music.create', 'Create')
confirmText: i18n.t('music.create')
});
if (!name?.trim()) return;
@@ -513,9 +488,6 @@ const musicView = {
},
async _createPlaylist(name) {
const t = (key, fallback = '') => {
return i18n?.t ? i18n.t(key) : fallback || key;
};
const createBtn = document.getElementById('music-create-playlist-btn');
if (createBtn) createBtn.disabled = true;
try {
@@ -536,7 +508,7 @@ const musicView = {
notifications.addNotification({
icon: 'fa-check-circle',
iconClass: 'upload',
title: t('music.create_playlist', 'Create Playlist'),
title: i18n.t('music.create_playlist'),
text: name
});
}
@@ -546,7 +518,7 @@ const musicView = {
notifications.addNotification({
icon: 'fa-exclamation-circle',
iconClass: 'error',
title: t('music.error', 'Error'),
title: i18n.t('music.error'),
text: err.message
});
}
@@ -556,20 +528,16 @@ const musicView = {
},
async _deletePlaylist() {
const t = (key, fallback = '') => {
return i18n?.t ? i18n.t(key) : fallback || key;
};
if (!this.currentPlaylist) return;
const confirmed = await new Promise((resolve) => {
Modal.prompt({
title: t('music.delete', 'Delete'),
label: t('music.confirm_delete', 'Delete this playlist?'),
title: i18n.t('music.delete'),
label: i18n.t('music.confirm_delete'),
placeholder: '',
value: this.currentPlaylist.name,
icon: 'fa-trash',
confirmText: t('music.delete', 'Delete')
confirmText: i18n.t('music.delete')
}).then((val) => resolve(val !== null));
});
if (!confirmed) return;
@@ -591,7 +559,7 @@ const musicView = {
this.currentTracks = [];
this._renderPlaylists();
if (notifications) {
notifications.addNotification({ icon: 'fa-check-circle', iconClass: 'upload', title: t('music.delete', 'Delete'), text: deletedName });
notifications.addNotification({ icon: 'fa-check-circle', iconClass: 'upload', title: i18n.t('music.delete'), text: deletedName });
}
} catch (err) {
console.error('Delete playlist error:', err);
@@ -599,7 +567,7 @@ const musicView = {
notifications.addNotification({
icon: 'fa-exclamation-circle',
iconClass: 'error',
title: t('music.error', 'Error'),
title: i18n.t('music.error'),
text: err.message
});
}
@@ -644,17 +612,14 @@ const musicView = {
async _showEditPlaylistDialog() {
if (!this.currentPlaylist) return;
const t = (key, fallback = '') => {
return i18n?.t ? i18n.t(key) : fallback || key;
};
const newName = await Modal.prompt({
title: t('music.edit', 'Edit'),
label: t('music.playlist_name', 'Playlist name'),
placeholder: t('music.playlist_name', 'Playlist name'),
title: i18n.t('music.edit'),
label: i18n.t('music.playlist_name'),
placeholder: i18n.t('music.playlist_name'),
value: this.currentPlaylist.name,
icon: 'fa-pen',
confirmText: t('actions.confirm', 'Save')
confirmText: i18n.t('actions.confirm')
});
if (!newName?.trim() || newName.trim() === this.currentPlaylist.name) return;
@@ -681,7 +646,7 @@ const musicView = {
notifications.addNotification({
icon: 'fa-exclamation-circle',
iconClass: 'error',
title: t('music.error', 'Error'),
title: i18n.t('music.error'),
text: err.message
});
}
@@ -690,16 +655,13 @@ const musicView = {
async _showSharePlaylistDialog() {
if (!this.currentPlaylist) return;
const t = (key, fallback = '') => {
return i18n?.t ? i18n.t(key) : fallback || key;
};
const userId = await Modal.prompt({
title: t('music.share', 'Share'),
label: t('music.share_with_user', 'User ID or email'),
placeholder: t('music.share_with_user', 'User ID or email'),
title: i18n.t('music.share'),
label: i18n.t('music.share_with_user'),
placeholder: i18n.t('music.share_with_user'),
icon: 'fa-share-alt',
confirmText: t('music.share', 'Share')
confirmText: i18n.t('music.share')
});
if (!userId?.trim()) return;
@@ -717,8 +679,8 @@ const musicView = {
notifications.addNotification({
icon: 'fa-check-circle',
iconClass: 'upload',
title: t('music.share', 'Share'),
text: t('music.added', 'Added!')
title: i18n.t('music.share'),
text: i18n.t('music.added')
});
}
} catch (err) {
@@ -727,7 +689,7 @@ const musicView = {
notifications.addNotification({
icon: 'fa-exclamation-circle',
iconClass: 'error',
title: t('music.error', 'Error'),
title: i18n.t('music.error'),
text: err.message
});
}
@@ -736,9 +698,6 @@ const musicView = {
async _showAddTracksDialog() {
if (!this.currentPlaylist) return;
const t = (key, fallback = '') => {
return i18n?.t ? i18n.t(key) : fallback || key;
};
// ── Build modal overlay ──
const overlay = document.createElement('div');
@@ -746,23 +705,23 @@ const musicView = {
overlay.innerHTML = `
<div class="music-picker-modal">
<div class="music-picker-header">
<h3><i class="fas fa-music"></i> ${t('music.add_tracks', 'Add Tracks')}</h3>
<button class="music-picker-close" title="${t('common.close', 'Close')}">&times;</button>
<h3><i class="fas fa-music"></i> ${i18n.t('music.add_tracks')}</h3>
<button class="music-picker-close" title="${i18n.t('actions.close')}">&times;</button>
</div>
<div class="music-picker-search">
<i class="fas fa-search"></i>
<input type="text" id="music-picker-query"
placeholder="${t('music.search_audio', 'Search audio files…')}" autocomplete="off">
placeholder="${i18n.t('music.search_audio')}" autocomplete="off">
</div>
<div class="music-picker-list" id="music-picker-list">
<div class="music-picker-loading"><i class="fas fa-spinner fa-spin"></i> ${t('music.loading', 'Loading…')}</div>
<div class="music-picker-loading"><i class="fas fa-spinner fa-spin"></i> ${i18n.t('music.loading')}</div>
</div>
<div class="music-picker-footer">
<span class="music-picker-selected-count" id="music-picker-count">0 ${t('music.selected', 'selected')}</span>
<span class="music-picker-selected-count" id="music-picker-count">0 ${i18n.t('music.selected')}</span>
<div class="music-picker-actions">
<button class="btn btn-secondary music-picker-cancel">${t('common.cancel', 'Cancel')}</button>
<button class="btn btn-secondary music-picker-cancel">${i18n.t('actions.cancel')}</button>
<button class="btn btn-primary music-picker-add" id="music-picker-add-btn" disabled>
<i class="fas fa-plus"></i> ${t('music.add', 'Add')}
<i class="fas fa-plus"></i> ${i18n.t('music.add')}
</button>
</div>
</div>
@@ -792,7 +751,7 @@ const musicView = {
const AUDIO_EXTENSIONS = 'mp3,ogg,flac,wav,aac,m4a,wma,opus,webm';
const fetchAudioFiles = async (query = '') => {
listEl.innerHTML = `<div class="music-picker-loading"><i class="fas fa-spinner fa-spin"></i> ${t('music.loading', 'Loading…')}</div>`;
listEl.innerHTML = `<div class="music-picker-loading"><i class="fas fa-spinner fa-spin"></i> ${i18n.t('music.loading')}</div>`;
try {
const params = new URLSearchParams({ type_filter: AUDIO_EXTENSIONS, limit: '200', recursive: 'true' });
if (query.trim()) params.set('query', query.trim());
@@ -802,13 +761,13 @@ const musicView = {
renderFiles(data.files || []);
} catch (err) {
console.error('Audio search error:', err);
listEl.innerHTML = `<div class="music-picker-empty"><i class="fas fa-exclamation-triangle"></i> ${t('music.search_error', 'Could not load audio files')}</div>`;
listEl.innerHTML = `<div class="music-picker-empty"><i class="fas fa-exclamation-triangle"></i> ${i18n.t('music.search_error')}</div>`;
}
};
const renderFiles = (files) => {
if (files.length === 0) {
listEl.innerHTML = `<div class="music-picker-empty"><i class="fas fa-folder-open"></i> ${t('music.no_audio_files', 'No audio files found')}</div>`;
listEl.innerHTML = `<div class="music-picker-empty"><i class="fas fa-folder-open"></i> ${i18n.t('music.no_audio_files')}</div>`;
return;
}
listEl.innerHTML = '';
@@ -831,7 +790,7 @@ const musicView = {
selectedIds.delete(file.id);
row.classList.remove('selected');
}
countEl.textContent = `${selectedIds.size} ${t('music.selected', 'selected')}`;
countEl.textContent = `${selectedIds.size} ${i18n.t('music.selected')}`;
addBtn.disabled = selectedIds.size === 0;
});
listEl.appendChild(row);
@@ -849,7 +808,7 @@ const musicView = {
addBtn.addEventListener('click', async () => {
if (selectedIds.size === 0) return;
addBtn.disabled = true;
addBtn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${t('music.adding', 'Adding…')}`;
addBtn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${i18n.t('music.adding')}`;
try {
const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/tracks`, {
@@ -864,8 +823,8 @@ const musicView = {
notifications.addNotification({
icon: 'fa-check-circle',
iconClass: 'upload',
title: t('music.add_tracks', 'Add Tracks'),
text: `${selectedIds.size} ${t('music.added_to_playlist', 'added to playlist')}`
title: i18n.t('music.add_tracks'),
text: `${selectedIds.size} ${i18n.t('music.added_to_playlist')}`
});
}
close();
@@ -876,7 +835,7 @@ const musicView = {
this.currentPlaylist.track_count = playlist.track_count;
this._renderPlaylistList();
const metaEl = document.getElementById('music-playlist-meta');
if (metaEl) metaEl.textContent = `${playlist.track_count} ${t('music.tracks', 'tracks')}`;
if (metaEl) metaEl.textContent = `${playlist.track_count} ${i18n.t('music.tracks')}`;
}
} catch (err) {
console.error('Add tracks error:', err);
@@ -884,12 +843,12 @@ const musicView = {
notifications.addNotification({
icon: 'fa-exclamation-circle',
iconClass: 'error',
title: t('music.error', 'Error'),
text: t('music.add_error', 'Could not add tracks to playlist')
title: i18n.t('music.error'),
text: i18n.t('music.add_error')
});
}
addBtn.disabled = false;
addBtn.innerHTML = `<i class="fas fa-plus"></i> ${t('music.add', 'Add')}`;
addBtn.innerHTML = `<i class="fas fa-plus"></i> ${i18n.t('music.add')}`;
}
});
@@ -900,7 +859,6 @@ const musicView = {
async _removeTrackFromPlaylist(_trackId, fileId) {
if (!this.currentPlaylist) return;
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
try {
const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/tracks/${encodeURIComponent(fileId)}`, {
@@ -914,8 +872,8 @@ const musicView = {
notifications.addNotification({
icon: 'fa-check-circle',
iconClass: 'upload',
title: t('music.remove', 'Remove'),
text: t('music.track_removed', 'Track removed')
title: i18n.t('music.remove'),
text: i18n.t('music.track_removed')
});
}
await this._loadPlaylistTracks(this.currentPlaylist.id);
@@ -925,7 +883,7 @@ const musicView = {
this.currentPlaylist.track_count = playlist.track_count;
this._renderPlaylistList();
const metaEl = document.getElementById('music-playlist-meta');
if (metaEl) metaEl.textContent = `${playlist.track_count} ${t('music.tracks', 'tracks')}`;
if (metaEl) metaEl.textContent = `${playlist.track_count} ${i18n.t('music.tracks')}`;
}
} catch (err) {
console.error('Remove track error:', err);
@@ -933,7 +891,7 @@ const musicView = {
notifications.addNotification({
icon: 'fa-exclamation-circle',
iconClass: 'error',
title: t('music.error', 'Error'),
title: i18n.t('music.error'),
text: err.message
});
}
@@ -942,7 +900,6 @@ const musicView = {
async _reorderTrack(fromIdx, toIdx) {
if (!this.currentPlaylist) return;
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
const tracks = [...this.currentTracks];
const [moved] = tracks.splice(fromIdx, 1);
@@ -965,7 +922,7 @@ const musicView = {
notifications.addNotification({
icon: 'fa-exclamation-circle',
iconClass: 'error',
title: t('music.error', 'Error'),
title: i18n.t('music.error'),
text: err.message
});
}
@@ -975,7 +932,6 @@ const musicView = {
async _showManageSharesDialog() {
if (!this.currentPlaylist) return;
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
const existing = document.getElementById('music-shares-dialog');
if (existing) existing.remove();
@@ -986,19 +942,19 @@ const musicView = {
dialog.innerHTML = `
<div class="music-shares-panel">
<div class="music-shares-header">
<h3><i class="fas fa-users"></i> ${t('music.manage_shares', 'Manage Shares')}</h3>
<h3><i class="fas fa-users"></i> ${i18n.t('music.manage_shares')}</h3>
<button class="music-shares-close-btn"><i class="fas fa-times"></i></button>
</div>
<div class="music-shares-body">
<div class="music-shares-loading"><i class="fas fa-spinner fa-spin"></i></div>
</div>
<div class="music-shares-add">
<input type="text" id="music-share-user-input" placeholder="${t('music.share_with_user', 'User ID or email')}" class="music-shares-input">
<input type="text" id="music-share-user-input" placeholder="${i18n.t('music.share_with_user')}" class="music-shares-input">
<label class="music-shares-write-label">
<input type="checkbox" id="music-share-write-input"> ${t('music.can_write', 'Can edit')}
<input type="checkbox" id="music-share-write-input"> ${i18n.t('music.can_write')}
</label>
<button class="btn btn-primary btn-sm" id="music-share-add-btn">
<i class="fas fa-plus"></i> ${t('music.share', 'Share')}
<i class="fas fa-plus"></i> ${i18n.t('music.share')}
</button>
</div>
</div>
@@ -1031,8 +987,8 @@ const musicView = {
notifications.addNotification({
icon: 'fa-check-circle',
iconClass: 'upload',
title: t('music.share', 'Share'),
text: t('music.added', 'Added!')
title: i18n.t('music.share'),
text: i18n.t('music.added')
});
}
} catch (err) {
@@ -1040,7 +996,7 @@ const musicView = {
notifications.addNotification({
icon: 'fa-exclamation-circle',
iconClass: 'error',
title: t('music.error', 'Error'),
title: i18n.t('music.error'),
text: err.message
});
}
@@ -1052,7 +1008,6 @@ const musicView = {
async _loadSharesList(dialog) {
if (!this.currentPlaylist) return;
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
const body = dialog.querySelector('.music-shares-body');
if (!body) return;
@@ -1067,7 +1022,7 @@ const musicView = {
const shares = await resp.json();
if (shares.length === 0) {
body.innerHTML = `<p class="music-shares-empty">${t('music.no_shares', 'No shares yet')}</p>`;
body.innerHTML = `<p class="music-shares-empty">${i18n.t('music.no_shares')}</p>`;
return;
}
@@ -1076,8 +1031,8 @@ const musicView = {
(s) => `
<div class="music-share-item" data-user-id="${this._escapeHtml(s.user_id)}">
<span class="music-share-user"><i class="fas fa-user"></i> ${this._escapeHtml(s.user_id)}</span>
<span class="music-share-perm">${s.can_write ? t('music.can_write', 'Can edit') : t('music.read_only', 'Read only')}</span>
<button class="music-share-remove-btn" title="${t('music.remove_share', 'Remove share')}"><i class="fas fa-times"></i></button>
<span class="music-share-perm">${s.can_write ? i18n.t('music.can_write') : i18n.t('music.read_only')}</span>
<button class="music-share-remove-btn" title="${i18n.t('music.remove_share')}"><i class="fas fa-times"></i></button>
</div>
`
)
@@ -1097,7 +1052,6 @@ const musicView = {
async _removeShare(userId, dialog) {
if (!this.currentPlaylist) return;
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
try {
const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/share/${encodeURIComponent(userId)}`, {
@@ -1112,7 +1066,7 @@ const musicView = {
notifications.addNotification({
icon: 'fa-exclamation-circle',
iconClass: 'error',
title: t('music.error', 'Error'),
title: i18n.t('music.error'),
text: err.message
});
}
@@ -1121,7 +1075,6 @@ const musicView = {
async _togglePublic() {
if (!this.currentPlaylist) return;
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
const newValue = !this.currentPlaylist.is_public;
try {
@@ -1142,16 +1095,16 @@ const musicView = {
const btn = document.getElementById('music-toggle-public-btn');
if (btn) {
btn.title = newValue ? t('music.make_private', 'Make private') : t('music.make_public', 'Make public');
btn.title = newValue ? i18n.t('music.make_private') : i18n.t('music.make_public');
btn.classList.toggle('active', newValue);
}
if (notifications) {
const status = newValue ? t('music.public', 'Public') : t('music.private', 'Private');
const status = newValue ? i18n.t('music.public') : i18n.t('music.private');
notifications.addNotification({
icon: 'fa-check-circle',
iconClass: 'upload',
title: t('music.toggle_public', 'Visibility'),
title: i18n.t('music.toggle_public'),
text: status
});
}
@@ -1161,7 +1114,7 @@ const musicView = {
notifications.addNotification({
icon: 'fa-exclamation-circle',
iconClass: 'error',
title: t('music.error', 'Error'),
title: i18n.t('music.error'),
text: err.message
});
}
@@ -1170,7 +1123,6 @@ const musicView = {
async _showCoverPicker() {
if (!this.currentPlaylist) return;
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
const input = document.createElement('input');
input.type = 'file';
@@ -1220,8 +1172,8 @@ const musicView = {
notifications.addNotification({
icon: 'fa-check-circle',
iconClass: 'upload',
title: t('music.set_cover', 'Set cover'),
text: t('music.cover_updated', 'Cover updated')
title: i18n.t('music.set_cover'),
text: i18n.t('music.cover_updated')
});
}
} catch (err) {
@@ -1230,7 +1182,7 @@ const musicView = {
notifications.addNotification({
icon: 'fa-exclamation-circle',
iconClass: 'error',
title: t('music.error', 'Error'),
title: i18n.t('music.error'),
text: err.message
});
}
@@ -1289,25 +1241,25 @@ const musicPlayer = {
<i class="fas fa-music"></i>
</div>
<div class="player-track-details">
<span class="player-track-name">${i18n?.t('music.not_playing', 'Not playing') || 'Not playing'}</span>
<span class="player-track-name">${i18n.t('music.not_playing')}</span>
<span class="player-track-artist"></span>
</div>
</div>
<div class="player-controls">
<div class="player-buttons">
<button class="player-btn" id="player-shuffle-btn" title="${i18n?.t('music.shuffle', 'Shuffle') || 'Shuffle'}">
<button class="player-btn" id="player-shuffle-btn" title="${i18n.t('music.shuffle')}">
<i class="fas fa-shuffle"></i>
</button>
<button class="player-btn" id="player-prev-btn" title="${i18n?.t('music.previous', 'Previous') || 'Previous'}">
<button class="player-btn" id="player-prev-btn" title="${i18n.t('music.previous')}">
<i class="fas fa-backward"></i>
</button>
<button class="player-btn player-btn-main" id="player-play-btn" title="${i18n?.t('music.play', 'Play') || 'Play'}">
<button class="player-btn player-btn-main" id="player-play-btn" title="${i18n.t('music.play')}">
<i class="fas fa-play"></i>
</button>
<button class="player-btn" id="player-next-btn" title="${i18n?.t('music.next', 'Next') || 'Next'}">
<button class="player-btn" id="player-next-btn" title="${i18n.t('music.next')}">
<i class="fas fa-forward"></i>
</button>
<button class="player-btn" id="player-repeat-btn" title="${i18n?.t('music.repeat', 'Repeat') || 'Repeat'}">
<button class="player-btn" id="player-repeat-btn" title="${i18n.t('music.repeat')}">
<i class="fas fa-repeat"></i>
</button>
</div>
@@ -1321,22 +1273,22 @@ const musicPlayer = {
</div>
</div>
<div class="player-extra">
<button class="player-btn player-btn-small" id="player-playlist-btn" title="${i18n?.t('music.queue', 'Queue') || 'Queue'}">
<button class="player-btn player-btn-small" id="player-playlist-btn" title="${i18n.t('music.queue')}">
<i class="fas fa-list"></i>
</button>
<button class="player-btn player-btn-small" id="player-vol-btn" title="${i18n?.t('music.volume', 'Volume') || 'Volume'}">
<button class="player-btn player-btn-small" id="player-vol-btn" title="${i18n.t('music.volume')}">
<i class="fas fa-volume-up"></i>
</button>
<div class="player-volume-slider" id="player-volume-slider">
<input type="range" min="0" max="100" value="70" id="player-volume-input">
</div>
<button class="player-btn player-btn-small player-close-btn" id="player-close-btn" title="${i18n?.t('actions.close', 'Close') || 'Close'}">
<button class="player-btn player-btn-small player-close-btn" id="player-close-btn" title="${i18n.t('actions.close')}">
<i class="fas fa-times"></i>
</button>
</div>
<div class="player-queue hidden" id="player-queue">
<div class="player-queue-header">
<h3>${i18n?.t('music.queue', 'Queue') || 'Queue'}</h3>
<h3>${i18n.t('music.queue')}</h3>
<button class="player-btn player-btn-small" id="player-close-queue-btn">
<i class="fas fa-times"></i>
</button>
@@ -1648,16 +1600,13 @@ const musicPlayer = {
console.error('Audio error:', e);
this.isPlaying = false;
this._updateUI();
const t = (key, fallback = '') => {
return i18n?.t ? i18n.t(key) : fallback || key;
};
if (notifications) {
const trackName = this.currentTrack?.title || this.currentTrack?.file_name || t('music.unknown_title', 'Unknown');
const trackName = this.currentTrack?.title || this.currentTrack?.file_name || i18n.t('music.unknown_title');
notifications.addNotification({
icon: 'fa-exclamation-circle',
iconClass: 'error',
title: t('music.error', 'Error'),
text: `${t('music.playback_error', 'Playback failed')}: ${trackName}`
title: i18n.t('music.error'),
text: `${i18n.t('music.playback_error')}: ${trackName}`
});
}
},
@@ -1681,12 +1630,9 @@ const musicPlayer = {
}
if (trackName) {
const t = (key, fallback = '') => {
return i18n?.t ? i18n.t(key) : fallback || key;
};
trackName.textContent = this.currentTrack
? this.currentTrack.title || this.currentTrack.file_name || t('music.unknown_title', 'Unknown')
: t('music.not_playing', 'Not playing');
? this.currentTrack.title || this.currentTrack.file_name || i18n.t('music.unknown_title')
: i18n.t('music.not_playing');
}
if (trackArtist) {
@@ -1736,15 +1682,11 @@ const musicPlayer = {
const queueList = document.getElementById('player-queue-list');
if (!queueList) return;
const t = (key, fallback = '') => {
return i18n?.t ? i18n.t(key) : fallback || key;
};
if (this.queue.length === 0) {
queueList.innerHTML = `
<div class="player-queue-empty">
<i class="fas fa-music"></i>
<p>${t('music.queue_empty', 'Queue is empty')}</p>
<p>${i18n.t('music.queue_empty')}</p>
</div>
`;
return;
@@ -1756,8 +1698,8 @@ const musicPlayer = {
<div class="player-queue-item ${idx === this.currentIndex ? 'active' : ''}" data-idx="${idx}">
<span class="queue-item-num">${idx + 1}</span>
<span class="queue-item-info">
<span class="queue-item-name">${this._escapeHtml(track.title || track.file_name || t('music.unknown_title', 'Unknown'))}</span>
<span class="queue-item-artist">${this._escapeHtml(track.artist || t('music.unknown_artist', 'Unknown Artist'))}</span>
<span class="queue-item-name">${this._escapeHtml(track.title || track.file_name || i18n.t('music.unknown_title'))}</span>
<span class="queue-item-artist">${this._escapeHtml(track.artist || i18n.t('music.unknown_artist'))}</span>
</span>
<span class="queue-item-duration">${this._formatDuration(track.duration_secs)}</span>
<button class="queue-item-remove" data-idx="${idx}">
+6 -9
View File
@@ -410,11 +410,10 @@ const photosView = {
/** Render the group mode toolbar */
_renderToolbar() {
const t = (k, d) => (i18n ? i18n.t(k) : d);
const modes = [
['daily', t('photos.view_daily', 'Day')],
['monthly', t('photos.view_monthly', 'Month')],
['yearly', t('photos.view_yearly', 'Year')]
['daily', i18n.t('photos.view_daily')],
['monthly', i18n.t('photos.view_monthly')],
['yearly', i18n.t('photos.view_yearly')]
];
let html = '<div class="photos-toolbar"><div class="view-toggle">';
for (const [mode, label] of modes) {
@@ -427,12 +426,11 @@ const photosView = {
/** Render empty state */
_renderEmpty() {
const t = (k, d) => (i18n ? i18n.t(k) : d);
this._container.innerHTML = `
<div class="photos-empty">
<i class="fas fa-images"></i>
<p class="photos-empty-title">${t('photos.empty_state', 'No photos yet')}</p>
<p>${t('photos.empty_hint', 'Upload images or videos to see them here')}</p>
<p class="photos-empty-title">${i18n.t('photos.empty_state')}</p>
<p>${i18n.t('photos.empty_hint')}</p>
</div>`;
},
@@ -520,10 +518,9 @@ const photosView = {
document.body.appendChild(bar);
}
const t = (k, d) => (i18n ? i18n.t(k) : d);
const count = this.selected.size;
bar.innerHTML = `
<span class="selection-count">${count} ${t('photos.items_selected', 'selected')}</span>
<span class="selection-count">${count} ${i18n.t('photos.items_selected')}</span>
<button id="photos-sel-download" title="Download"><i class="fas fa-download"></i></button>
<button id="photos-sel-delete" title="Delete"><i class="fas fa-trash"></i></button>
<button id="photos-sel-clear" title="Clear"><i class="fas fa-times"></i></button>
+2 -2
View File
@@ -114,8 +114,8 @@ const recent = {
if (recentItems.length === 0) {
ui.showError(`
<i class="fas fa-clock empty-state-icon"></i>
<p>${i18n ? i18n.t('recent.empty_state') : 'No recent files'}</p>
<p>${i18n ? i18n.t('recent.empty_hint') : 'Files you open will appear here'}</p>
<p>${i18n.t('recent.empty_state')}</p>
<p>${i18n.t('recent.empty_hint')}</p>
`);
}
+76 -83
View File
@@ -8,13 +8,6 @@ let usersPage = 0;
const PAGE_SIZE = 50;
let totalUsers = 0;
/* ── i18n helper — falls back to key if i18n not ready ── */
function t(key, params) {
if (i18n && typeof i18n.t === 'function') return i18n.t(key, params);
// fallback: strip prefix and humanise
return key.split('.').pop().replace(/_/g, ' ');
}
/** Escape a string for safe embedding inside a JS string literal within an HTML attribute. */
function _escJs(s) {
if (typeof s !== 'string') return '';
@@ -54,14 +47,14 @@ function formatBytes(bytes) {
}
function timeAgo(dateStr) {
if (!dateStr) return t('admin.never');
if (!dateStr) return i18n.t('admin.never');
const d = new Date(dateStr);
const now = new Date();
const secs = Math.floor((now - d) / 1000);
if (secs < 60) return t('admin.just_now');
if (secs < 3600) return t('admin.minutes_ago', { n: Math.floor(secs / 60) });
if (secs < 86400) return t('admin.hours_ago', { n: Math.floor(secs / 3600) });
if (secs < 2592000) return t('admin.days_ago', { n: Math.floor(secs / 86400) });
if (secs < 60) return i18n.t('admin.just_now');
if (secs < 3600) return i18n.t('admin.minutes_ago', { n: Math.floor(secs / 60) });
if (secs < 86400) return i18n.t('admin.hours_ago', { n: Math.floor(secs / 3600) });
if (secs < 2592000) return i18n.t('admin.days_ago', { n: Math.floor(secs / 86400) });
return d.toLocaleDateString();
}
@@ -159,9 +152,9 @@ async function loadDashboard() {
const bar = document.getElementById('ds-bar');
bar.style.width = `${Math.min(d.storage_usage_percent, 100)}%`;
bar.className = `progress-fill ${d.storage_usage_percent > 90 ? 'red' : d.storage_usage_percent > 70 ? 'orange' : 'green'}`;
document.getElementById('ds-auth').textContent = d.auth_enabled ? t('admin.enabled') : t('admin.disabled');
document.getElementById('ds-oidc').textContent = d.oidc_configured ? t('admin.active') : t('admin.off');
document.getElementById('ds-quotas-flag').textContent = d.quotas_enabled ? t('admin.enabled') : t('admin.disabled');
document.getElementById('ds-auth').textContent = d.auth_enabled ? i18n.t('admin.enabled') : i18n.t('admin.disabled');
document.getElementById('ds-oidc').textContent = d.oidc_configured ? i18n.t('admin.active') : i18n.t('admin.off');
document.getElementById('ds-quotas-flag').textContent = d.quotas_enabled ? i18n.t('admin.enabled') : i18n.t('admin.disabled');
if (typeof d.registration_enabled !== 'undefined') {
document.getElementById('ds-registration').checked = d.registration_enabled;
@@ -184,7 +177,7 @@ async function loadDashboard() {
async function loadUsers() {
const tbody = document.getElementById('users-tbody');
tbody.innerHTML = `<tr><td colspan="7" class="table-loading-cell"><i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.loading_users'))}</td></tr>`;
tbody.innerHTML = `<tr><td colspan="7" class="table-loading-cell"><i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('admin.loading_users'))}</td></tr>`;
try {
const resp = await fetch(`${API}/admin/users?limit=${PAGE_SIZE}&offset=${usersPage * PAGE_SIZE}`, {
headers: headers(),
@@ -193,7 +186,7 @@ async function loadUsers() {
if (!resp.ok) {
tbody.innerHTML =
'<tr><td colspan="7" class="table-status-error"><i class="fas fa-exclamation-circle"></i> ' +
escapeHtml(t('admin.failed_load_users')) +
escapeHtml(i18n.t('admin.failed_load_users')) +
'</td></tr>';
return;
}
@@ -201,7 +194,7 @@ async function loadUsers() {
totalUsers = data.total;
const users = data.users;
if (users.length === 0) {
tbody.innerHTML = `<tr><td colspan="7" class="table-status-empty">${escapeHtml(t('admin.no_users_found'))}</td></tr>`;
tbody.innerHTML = `<tr><td colspan="7" class="table-status-empty">${escapeHtml(i18n.t('admin.no_users_found'))}</td></tr>`;
return;
}
@@ -221,12 +214,12 @@ async function loadUsers() {
'"><i class="fas fa-key badge-admin-icon-small"></i> ' +
escapeHtml(u.auth_provider) +
'</span>'
: `<span class="badge badge-local">${escapeHtml(t('admin.local'))}</span>`;
: `<span class="badge badge-local">${escapeHtml(i18n.t('admin.local'))}</span>`;
return (
'<tr>' +
'<td><div class="user-info"><span class="user-name">' +
escapeHtml(u.username) +
(isSelf ? ` <span class="user-self-badge">${escapeHtml(t('admin.you_badge'))}</span>` : '') +
(isSelf ? ` <span class="user-self-badge">${escapeHtml(i18n.t('admin.you_badge'))}</span>` : '') +
'</span><span class="user-email">' +
escapeHtml(u.email) +
'</span></div></td>' +
@@ -242,7 +235,7 @@ async function loadUsers() {
'<td><span class="badge badge-' +
(u.active ? 'active' : 'inactive') +
'">' +
(u.active ? escapeHtml(t('admin.active')) : escapeHtml(t('admin.inactive'))) +
(u.active ? escapeHtml(i18n.t('admin.active')) : escapeHtml(i18n.t('admin.inactive'))) +
'</span></td>' +
'<td><div class="quota-bar"><div class="progress-bar quota-progress-fixed"><div class="progress-fill ' +
quotaColor +
@@ -262,7 +255,7 @@ async function loadUsers() {
'" data-quota="' +
u.storage_quota_bytes +
'" title="' +
escapeHtml(t('admin.edit_quota_title')) +
escapeHtml(i18n.t('admin.edit_quota_title')) +
'"><i class="fas fa-box"></i></button>' +
(isOidc
? ''
@@ -271,14 +264,14 @@ async function loadUsers() {
'" data-uname="' +
_escJs(u.username) +
'" title="' +
escapeHtml(t('admin.reset_password_title')) +
escapeHtml(i18n.t('admin.reset_password_title')) +
'"><i class="fas fa-key"></i></button>') +
'<button class="btn btn-sm btn-secondary admin-action-btn" data-action="toggle-role" data-uid="' +
_escJs(u.id) +
'" data-role="' +
_escJs(u.role) +
'" title="' +
escapeHtml(t('admin.toggle_role_title')) +
escapeHtml(i18n.t('admin.toggle_role_title')) +
'"' +
(isSelf ? ' disabled' : '') +
'><i class="fas fa-' +
@@ -291,7 +284,7 @@ async function loadUsers() {
'" data-active="' +
u.active +
'" title="' +
(u.active ? escapeHtml(t('admin.deactivate_title')) : escapeHtml(t('admin.activate_title'))) +
(u.active ? escapeHtml(i18n.t('admin.deactivate_title')) : escapeHtml(i18n.t('admin.activate_title'))) +
'"' +
(isSelf && u.active ? ' disabled' : '') +
'><i class="fas fa-' +
@@ -302,7 +295,7 @@ async function loadUsers() {
'" data-uname="' +
_escJs(u.username) +
'" title="' +
escapeHtml(t('admin.delete_title')) +
escapeHtml(i18n.t('admin.delete_title')) +
'"' +
(isSelf ? ' disabled' : '') +
'><i class="fas fa-trash-alt"></i></button>' +
@@ -331,13 +324,13 @@ async function loadUsers() {
const from = usersPage * PAGE_SIZE + 1;
const to = Math.min((usersPage + 1) * PAGE_SIZE, totalUsers);
document.getElementById('users-info').textContent = t('admin.showing_users', { from: from, to: to, total: totalUsers });
document.getElementById('users-info').textContent = i18n.t('admin.showing_users', { from: from, to: to, total: totalUsers });
document.getElementById('prev-btn').disabled = usersPage === 0;
document.getElementById('next-btn').disabled = (usersPage + 1) * PAGE_SIZE >= totalUsers;
} catch (e) {
tbody.innerHTML =
'<tr><td colspan="7" class="table-status-error"><i class="fas fa-exclamation-circle"></i> ' +
escapeHtml(t('admin.error_network', { message: e.message })) +
escapeHtml(i18n.t('admin.error_network', { message: e.message })) +
'</td></tr>';
}
}
@@ -357,7 +350,7 @@ function nextPage() {
async function toggleRole(userId, currentRole) {
const newRole = currentRole === 'admin' ? 'user' : 'admin';
const ok = await showConfirm(t('admin.confirm_role_change', { role: newRole }));
const ok = await showConfirm(i18n.t('admin.confirm_role_change', { role: newRole }));
if (!ok) return;
try {
const resp = await fetch(`${API}/admin/users/${userId}/role`, {
@@ -369,15 +362,15 @@ async function toggleRole(userId, currentRole) {
if (resp.ok) loadUsers();
else {
const e = await resp.json();
alert(e.message || t('admin.error_generic'));
alert(e.message || i18n.t('admin.error_generic'));
}
} catch (e) {
alert(t('admin.error_network', { message: e.message }));
alert(i18n.t('admin.error_network', { message: e.message }));
}
}
async function toggleActive(userId, currentActive) {
const msg = currentActive ? t('admin.confirm_deactivate') : t('admin.confirm_activate');
const msg = currentActive ? i18n.t('admin.confirm_deactivate') : i18n.t('admin.confirm_activate');
const ok = await showConfirm(msg);
if (!ok) return;
try {
@@ -390,15 +383,15 @@ async function toggleActive(userId, currentActive) {
if (resp.ok) loadUsers();
else {
const e = await resp.json();
alert(e.message || t('admin.error_generic'));
alert(e.message || i18n.t('admin.error_generic'));
}
} catch (e) {
alert(t('admin.error_network', { message: e.message }));
alert(i18n.t('admin.error_network', { message: e.message }));
}
}
async function deleteUser(userId, username) {
const ok = await showConfirm(t('admin.confirm_delete_user', { name: username }));
const ok = await showConfirm(i18n.t('admin.confirm_delete_user', { name: username }));
if (!ok) return;
try {
const resp = await fetch(`${API}/admin/users/${userId}`, {
@@ -411,10 +404,10 @@ async function deleteUser(userId, username) {
loadDashboard();
} else {
const e = await resp.json();
alert(e.message || t('admin.error_generic'));
alert(e.message || i18n.t('admin.error_generic'));
}
} catch (e) {
alert(t('admin.error_network', { message: e.message }));
alert(i18n.t('admin.error_network', { message: e.message }));
}
}
@@ -448,10 +441,10 @@ async function saveQuota() {
loadDashboard();
} else {
const e = await resp.json();
alert(e.message || t('admin.error_generic'));
alert(e.message || i18n.t('admin.error_generic'));
}
} catch (e) {
alert(t('admin.error_network', { message: e.message }));
alert(i18n.t('admin.error_network', { message: e.message }));
}
}
@@ -482,19 +475,19 @@ async function submitCreateUser() {
const errorEl = document.getElementById('cu-error');
if (username.length < 3) {
errorEl.textContent = t('admin.error_username_short');
errorEl.textContent = i18n.t('admin.error_username_short');
errorEl.className = 'alert alert-error';
return;
}
if (password.length < 8) {
errorEl.textContent = t('admin.error_password_short');
errorEl.textContent = i18n.t('admin.error_password_short');
errorEl.className = 'alert alert-error';
return;
}
const btn = document.getElementById('cu-submit');
btn.disabled = true;
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.creating'))}`;
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('admin.creating'))}`;
try {
const resp = await fetch(`${API}/admin/users`, {
method: 'POST',
@@ -514,15 +507,15 @@ async function submitCreateUser() {
loadDashboard();
} else {
const e = await resp.json().catch(() => ({}));
errorEl.textContent = e.message || t('admin.error_create_user');
errorEl.textContent = e.message || i18n.t('admin.error_create_user');
errorEl.className = 'alert alert-error';
}
} catch (e) {
errorEl.textContent = t('admin.error_network', { message: e.message });
errorEl.textContent = i18n.t('admin.error_network', { message: e.message });
errorEl.className = 'alert alert-error';
}
btn.disabled = false;
btn.innerHTML = `<i class="fas fa-user-plus"></i> ${escapeHtml(t('admin.create_user'))}`;
btn.innerHTML = `<i class="fas fa-user-plus"></i> ${escapeHtml(i18n.t('admin.create_user'))}`;
}
let resetPwUserId = '';
@@ -543,14 +536,14 @@ async function submitResetPassword() {
const password = document.getElementById('rp-password').value;
const errorEl = document.getElementById('rp-error');
if (password.length < 8) {
errorEl.textContent = t('admin.error_password_short');
errorEl.textContent = i18n.t('admin.error_password_short');
errorEl.className = 'alert alert-error';
return;
}
const btn = document.getElementById('rp-submit');
btn.disabled = true;
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.resetting'))}`;
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('admin.resetting'))}`;
try {
const resp = await fetch(`${API}/admin/users/${resetPwUserId}/password`, {
method: 'PUT',
@@ -562,15 +555,15 @@ async function submitResetPassword() {
closeResetPasswordModal();
} else {
const e = await resp.json().catch(() => ({}));
errorEl.textContent = e.message || t('admin.error_generic');
errorEl.textContent = e.message || i18n.t('admin.error_generic');
errorEl.className = 'alert alert-error';
}
} catch (e) {
errorEl.textContent = t('admin.error_network', { message: e.message });
errorEl.textContent = i18n.t('admin.error_network', { message: e.message });
errorEl.className = 'alert alert-error';
}
btn.disabled = false;
btn.innerHTML = `<i class="fas fa-save"></i> ${escapeHtml(t('admin.reset_btn'))}`;
btn.innerHTML = `<i class="fas fa-save"></i> ${escapeHtml(i18n.t('admin.reset_btn'))}`;
}
async function toggleRegistration(enabled) {
@@ -588,13 +581,13 @@ async function toggleRegistration(enabled) {
if (!enabled) showElement('registration-warning', 'flex');
else hideElement('registration-warning');
const e = await resp.json().catch(() => ({}));
alert(e.message || t('admin.error_generic'));
alert(e.message || i18n.t('admin.error_generic'));
}
} catch (e) {
document.getElementById('ds-registration').checked = !enabled;
if (!enabled) showElement('registration-warning', 'flex');
else hideElement('registration-warning');
alert(t('admin.error_network', { message: e.message }));
alert(i18n.t('admin.error_network', { message: e.message }));
}
}
@@ -626,7 +619,7 @@ async function testConnection() {
}
const btn = document.getElementById('discover-btn');
btn.disabled = true;
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.discovering'))}`;
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('admin.discovering'))}`;
const resultDiv = document.getElementById('discovery-result');
try {
const resp = await fetch(`${API}/admin/settings/oidc/test`, {
@@ -654,13 +647,13 @@ async function testConnection() {
resultDiv.innerHTML = `<div class="discovery-result fail"><i class="fas fa-times-circle"></i> Error: ${escapeHtml(e.message)}</div>`;
}
btn.disabled = false;
btn.innerHTML = `<i class="fas fa-search"></i> ${escapeHtml(t('admin.auto_discover'))}`;
btn.innerHTML = `<i class="fas fa-search"></i> ${escapeHtml(i18n.t('admin.auto_discover'))}`;
}
async function saveOidcSettings() {
const btn = document.getElementById('save-btn');
btn.disabled = true;
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.saving'))}`;
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('admin.saving'))}`;
const body = {
enabled: document.getElementById('oidc-enabled').checked,
issuer_url: document.getElementById('issuer-url').value.trim(),
@@ -680,18 +673,18 @@ async function saveOidcSettings() {
body: JSON.stringify(body)
});
if (resp.ok) {
const status = body.enabled ? t('admin.active').toLowerCase() : t('admin.disabled').toLowerCase();
showOidcStatus(t('admin.settings_saved', { status: status }), 'success');
const status = body.enabled ? i18n.t('admin.active').toLowerCase() : i18n.t('admin.disabled').toLowerCase();
showOidcStatus(i18n.t('admin.settings_saved', { status: status }), 'success');
loadDashboard();
} else {
const e = await resp.json().catch(() => ({}));
showOidcStatus(`Error: ${e.message || resp.statusText}`, 'error');
}
} catch (e) {
showOidcStatus(t('admin.error_network', { message: e.message }), 'error');
showOidcStatus(i18n.t('admin.error_network', { message: e.message }), 'error');
}
btn.disabled = false;
btn.innerHTML = `<i class="fas fa-save"></i> ${escapeHtml(t('admin.save_btn'))}`;
btn.innerHTML = `<i class="fas fa-save"></i> ${escapeHtml(i18n.t('admin.save_btn'))}`;
}
/* ── Storage tab ── */
@@ -752,7 +745,7 @@ async function loadStorage() {
// Secret hints
if (s.s3_access_key_set) {
document.getElementById('storage-access-key').placeholder = t('admin.storage_key_placeholder') || 'Leave empty to keep current value';
document.getElementById('storage-access-key').placeholder = i18n.t('admin.storage_key_placeholder') || 'Leave empty to keep current value';
}
if (s.s3_secret_key_set) {
showElement('storage-secret-hint');
@@ -772,7 +765,7 @@ async function loadStorage() {
document.getElementById('storage-total-size').textContent = s.total_bytes_stored != null ? formatBytes(s.total_bytes_stored) : '—';
document.getElementById('storage-dedup-ratio').textContent = s.dedup_ratio != null ? `${s.dedup_ratio.toFixed(2)}x` : '—';
} catch (e) {
showStorageStatus(t('admin.error_network', { message: e.message }), 'error');
showStorageStatus(i18n.t('admin.error_network', { message: e.message }), 'error');
}
// Also load migration status
@@ -782,7 +775,7 @@ async function loadStorage() {
async function saveStorageSettings() {
const btn = document.getElementById('btn-save-storage');
btn.disabled = true;
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.saving'))}`;
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('admin.saving'))}`;
const backend = document.querySelector('input[name="storage-backend"]:checked').value;
const body = {
@@ -803,23 +796,23 @@ async function saveStorageSettings() {
body: JSON.stringify(body)
});
if (resp.ok) {
showStorageStatus(t('admin.storage_saved') || 'Storage settings saved successfully', 'success');
showStorageStatus(i18n.t('admin.storage_saved') || 'Storage settings saved successfully', 'success');
loadStorage();
} else {
const e = await resp.json().catch(() => ({}));
showStorageStatus(`Error: ${e.message || resp.statusText}`, 'error');
}
} catch (e) {
showStorageStatus(t('admin.error_network', { message: e.message }), 'error');
showStorageStatus(i18n.t('admin.error_network', { message: e.message }), 'error');
}
btn.disabled = false;
btn.innerHTML = `<i class="fas fa-save"></i> ${escapeHtml(t('admin.storage_save') || 'Save')}`;
btn.innerHTML = `<i class="fas fa-save"></i> ${escapeHtml(i18n.t('admin.storage_save') || 'Save')}`;
}
async function testStorageConnection() {
const btn = document.getElementById('btn-test-storage');
btn.disabled = true;
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.testing') || 'Testing...')}`;
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('admin.testing') || 'Testing...')}`;
const backend = document.querySelector('input[name="storage-backend"]:checked').value;
const body = {
@@ -841,17 +834,17 @@ async function testStorageConnection() {
});
const r = await resp.json();
if (r.connected) {
let msg = `${t('admin.storage_test_success') || 'Connection successful'} (${escapeHtml(r.backend_type)})`;
let msg = `${i18n.t('admin.storage_test_success') || 'Connection successful'} (${escapeHtml(r.backend_type)})`;
if (r.available_bytes != null) msg += ` — ${formatBytes(r.available_bytes)} available`;
showStorageStatus(msg, 'success');
} else {
showStorageStatus(`${t('admin.storage_test_failure') || 'Connection failed'}: ${escapeHtml(r.message)}`, 'error');
showStorageStatus(`${i18n.t('admin.storage_test_failure') || 'Connection failed'}: ${escapeHtml(r.message)}`, 'error');
}
} catch (e) {
showStorageStatus(t('admin.error_network', { message: e.message }), 'error');
showStorageStatus(i18n.t('admin.error_network', { message: e.message }), 'error');
}
btn.disabled = false;
btn.innerHTML = `<i class="fas fa-vial"></i> ${escapeHtml(t('admin.storage_test_connection') || 'Test Connection')}`;
btn.innerHTML = `<i class="fas fa-vial"></i> ${escapeHtml(i18n.t('admin.storage_test_connection') || 'Test Connection')}`;
}
/* ── Migration ── */
@@ -953,14 +946,14 @@ async function startMigration() {
body: JSON.stringify({ concurrency: 4 })
});
if (resp.ok) {
showMigrationMsg(t('admin.migration_started') || 'Migration started', 'success');
showMigrationMsg(i18n.t('admin.migration_started') || 'Migration started', 'success');
loadMigrationStatus();
} else {
const e = await resp.json().catch(() => ({}));
showMigrationMsg(`Error: ${e.message || resp.statusText}`, 'error');
}
} catch (e) {
showMigrationMsg(t('admin.error_network', { message: e.message }), 'error');
showMigrationMsg(i18n.t('admin.error_network', { message: e.message }), 'error');
}
btn.disabled = false;
}
@@ -973,7 +966,7 @@ async function pauseMigration() {
credentials: 'same-origin'
});
if (resp.ok) {
showMigrationMsg(t('admin.migration_paused_msg') || 'Migration paused', 'success');
showMigrationMsg(i18n.t('admin.migration_paused_msg') || 'Migration paused', 'success');
loadMigrationStatus();
}
} catch (_e) {
@@ -989,7 +982,7 @@ async function resumeMigration() {
credentials: 'same-origin'
});
if (resp.ok) {
showMigrationMsg(t('admin.migration_resumed_msg') || 'Migration resumed', 'success');
showMigrationMsg(i18n.t('admin.migration_resumed_msg') || 'Migration resumed', 'success');
loadMigrationStatus();
}
} catch (_e) {
@@ -1000,7 +993,7 @@ async function resumeMigration() {
async function verifyMigration() {
const btn = document.getElementById('btn-verify-migration');
btn.disabled = true;
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.migration_verifying') || 'Verifying...')}`;
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('admin.migration_verifying') || 'Verifying...')}`;
const resultDiv = document.getElementById('migration-verify-result');
try {
const resp = await fetch(`${API}/admin/storage/migration/verify`, {
@@ -1012,19 +1005,19 @@ async function verifyMigration() {
const r = await resp.json();
resultDiv.style.display = '';
if (r.passed) {
resultDiv.innerHTML = `<div class="discovery-result ok"><strong><i class="fas fa-check-circle"></i> ${escapeHtml(t('admin.migration_verify_passed') || 'Verification passed')}</strong><p>${r.sample_checked} blobs checked, ${r.pg_blob_count} total in database</p></div>`;
resultDiv.innerHTML = `<div class="discovery-result ok"><strong><i class="fas fa-check-circle"></i> ${escapeHtml(i18n.t('admin.migration_verify_passed') || 'Verification passed')}</strong><p>${r.sample_checked} blobs checked, ${r.pg_blob_count} total in database</p></div>`;
} else {
const issues = [];
if (r.missing_in_target.length) issues.push(`${r.missing_in_target.length} missing`);
if (r.size_mismatches.length) issues.push(`${r.size_mismatches.length} size mismatches`);
resultDiv.innerHTML = `<div class="discovery-result fail"><strong><i class="fas fa-times-circle"></i> ${escapeHtml(t('admin.migration_verify_failed') || 'Verification failed')}</strong><p>${issues.join(', ')}</p></div>`;
resultDiv.innerHTML = `<div class="discovery-result fail"><strong><i class="fas fa-times-circle"></i> ${escapeHtml(i18n.t('admin.migration_verify_failed') || 'Verification failed')}</strong><p>${issues.join(', ')}</p></div>`;
}
} catch (e) {
resultDiv.style.display = '';
resultDiv.innerHTML = `<div class="discovery-result fail"><i class="fas fa-times-circle"></i> Error: ${escapeHtml(e.message)}</div>`;
}
btn.disabled = false;
btn.innerHTML = `<i class="fas fa-check-double"></i> ${escapeHtml(t('admin.migration_verify') || 'Verify Integrity')}`;
btn.innerHTML = `<i class="fas fa-check-double"></i> ${escapeHtml(i18n.t('admin.migration_verify') || 'Verify Integrity')}`;
}
async function completeMigration() {
@@ -1035,14 +1028,14 @@ async function completeMigration() {
credentials: 'same-origin'
});
if (resp.ok) {
showMigrationMsg(t('admin.migration_completed_msg') || 'Migration finalized. Restart the server to use the new backend.', 'success');
showMigrationMsg(i18n.t('admin.migration_completed_msg') || 'Migration finalized. Restart the server to use the new backend.', 'success');
loadMigrationStatus();
} else {
const e = await resp.json().catch(() => ({}));
showMigrationMsg(`Error: ${e.message || resp.statusText}`, 'error');
}
} catch (e) {
showMigrationMsg(t('admin.error_network', { message: e.message }), 'error');
showMigrationMsg(i18n.t('admin.error_network', { message: e.message }), 'error');
}
}
@@ -1105,13 +1098,13 @@ function showAccessDenied() {
/* ── Apply i18n when translations load / change ── */
document.addEventListener('translationsLoaded', () => {
if (i18n?.translatePage) i18n.translatePage();
// Re-render dynamic content that uses t()
i18n.translatePage();
// Re-render dynamic content that uses i18n.t()
loadDashboard();
if (activeTabName === 'users') loadUsers();
});
document.addEventListener('localeChanged', () => {
if (i18n?.translatePage) i18n.translatePage();
i18n.translatePage();
loadDashboard();
if (activeTabName === 'users') loadUsers();
});
+27 -33
View File
@@ -3,12 +3,6 @@ import { i18n } from '../../core/i18n.js';
const API = '/api';
/* ── i18n helper — falls back to key if i18n not ready ── */
function t(key, params) {
if (i18n && typeof i18n.t === 'function') return i18n.t(key, params);
return key.split('.').pop().replace(/_/g, ' ');
}
function headers() {
return { 'Content-Type': 'application/json', ...getCsrfHeaders() };
}
@@ -22,14 +16,14 @@ function formatBytes(bytes) {
}
function timeAgo(dateStr) {
if (!dateStr) return t('profile.never');
if (!dateStr) return i18n.t('profile.never');
const d = new Date(dateStr);
const now = new Date();
const secs = Math.floor((now - d) / 1000);
if (secs < 60) return t('profile.just_now');
if (secs < 3600) return t('profile.minutes_ago', { n: Math.floor(secs / 60) });
if (secs < 86400) return t('profile.hours_ago', { n: Math.floor(secs / 3600) });
if (secs < 2592000) return t('profile.days_ago', { n: Math.floor(secs / 86400) });
if (secs < 60) return i18n.t('profile.just_now');
if (secs < 3600) return i18n.t('profile.minutes_ago', { n: Math.floor(secs / 60) });
if (secs < 86400) return i18n.t('profile.hours_ago', { n: Math.floor(secs / 3600) });
if (secs < 2592000) return i18n.t('profile.days_ago', { n: Math.floor(secs / 86400) });
return d.toLocaleDateString();
}
@@ -53,15 +47,15 @@ async function init() {
const badge = document.getElementById('p-role-badge');
if (user.role === 'admin') {
badge.className = 'role-badge role-badge-admin';
badge.innerHTML = `<i class="fas fa-shield-alt"></i> ${t('profile.role_admin')}`;
badge.innerHTML = `<i class="fas fa-shield-alt"></i> ${i18n.t('profile.role_admin')}`;
} else {
badge.className = 'role-badge role-badge-user';
badge.innerHTML = `<i class="fas fa-user"></i> ${t('profile.role_user')}`;
badge.innerHTML = `<i class="fas fa-user"></i> ${i18n.t('profile.role_user')}`;
}
document.getElementById('p-detail-username').textContent = user.username;
document.getElementById('p-detail-email').textContent = user.email || '—';
document.getElementById('p-detail-role').textContent = user.role === 'admin' ? t('profile.role_admin') : t('profile.role_user');
document.getElementById('p-detail-role').textContent = user.role === 'admin' ? i18n.t('profile.role_admin') : i18n.t('profile.role_user');
document.getElementById('p-detail-login').textContent = timeAgo(user.last_login_at);
const used = user.storage_used_bytes || 0;
@@ -75,7 +69,7 @@ async function init() {
const bar = document.getElementById('p-storage-bar');
bar.style.width = `${pct}%`;
bar.className = `storage-fill ${pct > 90 ? 'red' : pct > 70 ? 'orange' : 'green'}`;
document.getElementById('p-storage-text').textContent = `${formatBytes(used)} / ${quota > 0 ? formatBytes(quota) : t('profile.unlimited')}`;
document.getElementById('p-storage-text').textContent = `${formatBytes(used)} / ${quota > 0 ? formatBytes(quota) : i18n.t('profile.unlimited')}`;
if (user.auth_provider && user.auth_provider !== 'local') {
document.getElementById('password-section').classList.add('hidden');
@@ -116,18 +110,18 @@ async function changePassword(e) {
const statusEl = document.getElementById('pw-status');
if (newPw !== confirmPw) {
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${escapeHtml(t('profile.passwords_no_match'))}</div>`;
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${escapeHtml(i18n.t('profile.passwords_no_match'))}</div>`;
return false;
}
if (newPw.length < 8) {
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${escapeHtml(t('profile.password_too_short'))}</div>`;
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${escapeHtml(i18n.t('profile.password_too_short'))}</div>`;
return false;
}
const btn = document.getElementById('pw-submit');
btn.disabled = true;
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('profile.updating'))}`;
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('profile.updating'))}`;
try {
const resp = await fetch(`${API}/auth/change-password`, {
@@ -141,24 +135,24 @@ async function changePassword(e) {
});
if (resp.ok) {
statusEl.innerHTML = `<div class="alert alert-success"><i class="fas fa-check-circle"></i> ${escapeHtml(t('profile.password_updated'))}</div>`;
statusEl.innerHTML = `<div class="alert alert-success"><i class="fas fa-check-circle"></i> ${escapeHtml(i18n.t('profile.password_updated'))}</div>`;
document.getElementById('password-form').reset();
} else {
const err = await resp.json().catch(() => ({}));
statusEl.innerHTML =
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' +
escapeHtml(err.message || t('profile.password_change_failed')) +
escapeHtml(err.message || i18n.t('profile.password_change_failed')) +
'</div>';
}
} catch (err) {
statusEl.innerHTML =
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' +
escapeHtml(t('profile.error_network', { message: err.message })) +
escapeHtml(i18n.t('profile.error_network', { message: err.message })) +
'</div>';
}
btn.disabled = false;
btn.innerHTML = `<i class="fas fa-save"></i> ${escapeHtml(t('profile.update_password'))}`;
btn.innerHTML = `<i class="fas fa-save"></i> ${escapeHtml(i18n.t('profile.update_password'))}`;
return false;
}
@@ -177,15 +171,15 @@ function renderPwRow(pw) {
const created = document.createElement('td');
created.textContent = new Date(pw.created_at).toLocaleDateString();
const lastUsed = document.createElement('td');
lastUsed.textContent = pw.last_used_at ? timeAgo(pw.last_used_at) : t('profile.never');
lastUsed.textContent = pw.last_used_at ? timeAgo(pw.last_used_at) : i18n.t('profile.never');
const status = document.createElement('td');
const badge = document.createElement('span');
if (pw.active !== false) {
badge.className = 'badge badge-active';
badge.textContent = t('profile.active');
badge.textContent = i18n.t('profile.active');
} else {
badge.className = 'badge badge-expired';
badge.textContent = t('profile.revoked');
badge.textContent = i18n.t('profile.revoked');
}
status.appendChild(badge);
const actions = document.createElement('td');
@@ -193,7 +187,7 @@ function renderPwRow(pw) {
const btn = document.createElement('button');
btn.className = 'btn btn-danger-sm';
btn.innerHTML = '<i class="fas fa-trash"></i>';
btn.title = t('profile.revoke_title');
btn.title = i18n.t('profile.revoke_title');
btn.addEventListener('click', () => {
revokeAppPassword(pw.id, pw.label);
});
@@ -265,12 +259,12 @@ async function createAppPassword() {
const btn = document.getElementById('app-pw-generate');
if (!label) {
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${escapeHtml(t('profile.error_label_required'))}</div>`;
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${escapeHtml(i18n.t('profile.error_label_required'))}</div>`;
return;
}
btn.disabled = true;
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('profile.generating'))}`;
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('profile.generating'))}`;
statusEl.innerHTML = '';
try {
@@ -284,7 +278,7 @@ async function createAppPassword() {
const err = await resp.json().catch(() => ({}));
statusEl.innerHTML =
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' +
escapeHtml(err.message || t('profile.error_create_pw')) +
escapeHtml(err.message || i18n.t('profile.error_create_pw')) +
'</div>';
return;
}
@@ -298,7 +292,7 @@ async function createAppPassword() {
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${err.message}</div>`;
} finally {
btn.disabled = false;
btn.innerHTML = `<i class="fas fa-plus"></i> ${escapeHtml(t('profile.generate'))}`;
btn.innerHTML = `<i class="fas fa-plus"></i> ${escapeHtml(i18n.t('profile.generate'))}`;
}
}
@@ -314,7 +308,7 @@ function copyAppPassword() {
}
async function revokeAppPassword(id, label) {
if (!confirm(t('profile.confirm_revoke', { label: label }))) return;
if (!confirm(i18n.t('profile.confirm_revoke', { label: label }))) return;
try {
const resp = await fetch(`${API}/auth/app-passwords/${encodeURIComponent(id)}`, {
method: 'DELETE',
@@ -326,10 +320,10 @@ async function revokeAppPassword(id, label) {
loadAppPasswords();
} else {
const err = await resp.json().catch(() => ({}));
alert(err.message || t('profile.error_revoke'));
alert(err.message || i18n.t('profile.error_revoke'));
}
} catch (err) {
alert(t('profile.error_network', { message: err.message }));
alert(i18n.t('profile.error_network', { message: err.message }));
}
}
+6 -9
View File
@@ -178,14 +178,14 @@ const sharedView = {
<label><input type="checkbox" id="sv-permission-reshare"> <span data-i18n="share.permissionReshare">Reshare</span></label>
</div>
<div class="share-password-section">
<label><input type="checkbox" id="sv-enable-password"> <span data-i18n="share.enablePassword">Password protection</span></label>
<label><input type="checkbox" id="sv-enable-password"> <span data-i18n="share.password">Password protection</span></label>
<div class="password-input-group">
<input type="text" id="sv-share-password" disabled placeholder="Enter password">
<button id="sv-generate-password" class="button small" data-i18n="share.generatePassword">Generate</button>
</div>
</div>
<div class="share-expiration-section">
<label><input type="checkbox" id="sv-enable-expiration"> <span data-i18n="share.enableExpiration">Set expiration</span></label>
<label><input type="checkbox" id="sv-enable-expiration"> <span data-i18n="share.expiration">Set expiration</span></label>
<input type="date" id="sv-share-expiration" disabled>
</div>
<div class="share-actions">
@@ -205,11 +205,11 @@ const sharedView = {
</div>
<div class="notification-form">
<div class="form-group">
<label data-i18n="share.notifyEmail">Email:</label>
<label data-i18n="share.notifyEmailLabel">Email:</label>
<input type="email" id="sv-notification-email" placeholder="recipient@example.com">
</div>
<div class="form-group">
<label data-i18n="share.notifyMessage">Message (optional):</label>
<label data-i18n="share.notifyMessageLabel">Message (optional):</label>
<textarea id="sv-notification-message" rows="3"></textarea>
</div>
</div>
@@ -220,9 +220,7 @@ const sharedView = {
</div>
`;
if (i18n?.translateElement) {
i18n.translateElement(container);
}
i18n.translateElement(container);
},
// Attach event listeners
@@ -662,8 +660,7 @@ const sharedView = {
},
translate(key, defaultText) {
if (i18n?.t) return i18n.t(key, defaultText);
return defaultText;
return i18n.t(key, defaultText);
}
};
+56 -4
View File
@@ -307,7 +307,8 @@
"generated_link": "الرابط المُنشأ",
"notify": "إرسال إشعار",
"recipient": "المستلم",
"message": "الرسالة"
"message": "الرسالة",
"move_to_home": "نقل إلى المجلد الرئيسي"
},
"dropzone": {
"drag_files": "اسحب الملفات هنا أو انقر للاختيار",
@@ -439,7 +440,9 @@
"item_deleted_permanently": "تم حذف العنصر نهائياً",
"trash_emptied": "تم تفريغ سلة المهملات بنجاح",
"title": "الإشعارات",
"empty": "لا توجد إشعارات"
"empty": "لا توجد إشعارات",
"link_created": "تم إنشاء الرابط",
"share_success": "تم إنشاء رابط المشاركة بنجاح"
},
"batch": {
"one_selected": "عنصر واحد محدد",
@@ -566,7 +569,50 @@
"error_password_short": "كلمة المرور 8 أحرف على الأقل",
"error_generic": "فشل",
"error_network": "خطأ في الشبكة: {{message}}",
"error_create_user": "فشل إنشاء المستخدم"
"error_create_user": "فشل إنشاء المستخدم",
"tab_storage": "التخزين",
"storage_title": "إعداد التخزين",
"storage_current_backend": "الواجهة الخلفية الحالية",
"storage_total_blobs": "إجمالي الكتل",
"storage_total_size": "الحجم الإجمالي",
"storage_dedup_ratio": "نسبة إزالة التكرار",
"storage_backend": "الواجهة الخلفية",
"storage_local": "محلي",
"storage_s3": "متوافق مع S3",
"storage_provider_preset": "إعداد مسبق للمزود",
"storage_preset_custom": "مخصص",
"storage_endpoint_url": "رابط نقطة النهاية",
"storage_endpoint_hint": "اتركه فارغاً لـ AWS S3",
"storage_bucket": "الحاوية",
"storage_region": "المنطقة",
"storage_access_key": "مفتاح الوصول",
"storage_secret_key": "المفتاح السري",
"storage_secret_configured": "تم إعداد المفتاح",
"storage_key_placeholder": "أدخل مفتاحاً جديداً",
"storage_path_style": "فرض أسلوب المسار",
"storage_path_style_hint": "مطلوب لـ MinIO وبعض الخدمات المتوافقة مع S3",
"storage_test_connection": "اختبار الاتصال",
"storage_test_success": "نجح الاتصال",
"storage_test_failure": "فشل الاتصال",
"storage_save": "حفظ الإعداد",
"storage_saved": "تم حفظ الإعداد",
"storage_migration": "ترحيل البيانات",
"storage_migration_coming_soon": "أدوات الترحيل قريباً",
"migration_status_label": "حالة الترحيل",
"migration_start": "بدء الترحيل",
"migration_pause": "إيقاف مؤقت",
"migration_resume": "استئناف",
"migration_verify": "التحقق",
"migration_complete": "إكمال",
"migration_started": "بدأ الترحيل",
"migration_paused_msg": "الترحيل متوقف مؤقتاً",
"migration_resumed_msg": "استُؤنف الترحيل",
"migration_completed_msg": "اكتمل الترحيل بنجاح",
"migration_verifying": "جارٍ التحقق...",
"migration_verify_passed": "اجتاز التحقق",
"migration_verify_failed": "فشل التحقق",
"migration_failed_blobs": "كتل فاشلة",
"testing": "جارٍ الاختبار..."
},
"profile": {
"page_title": "الملف الشخصي",
@@ -627,5 +673,11 @@
"error_create_pw": "فشل إنشاء كلمة المرور",
"confirm_revoke": "إلغاء كلمة المرور \"{{label}}\"؟ ستتوقف العملاء عن العمل.",
"error_revoke": "فشل الإلغاء"
}
},
"upload": {
"uploading": "جارٍ الرفع...",
"files": "ملفات",
"complete": "{{count}} / {{total}} تم الرفع"
},
"storage_quota_exceeded": "تجاوز حصة التخزين"
}
+56 -4
View File
@@ -307,7 +307,8 @@
"message": "Nachricht",
"go_to_parent": ".. (parent folder)",
"no_subfolders": "No subfolders",
"select_this_folder": "Select this folder"
"select_this_folder": "Select this folder",
"move_to_home": "In den Home-Ordner verschieben"
},
"dropzone": {
"drag_files": "Dateien hierher ziehen oder klicken zum Auswählen",
@@ -439,7 +440,9 @@
"item_deleted_permanently": "Element endgültig gelöscht",
"trash_emptied": "Papierkorb erfolgreich geleert",
"title": "Benachrichtigungen",
"empty": "Keine Benachrichtigungen"
"empty": "Keine Benachrichtigungen",
"link_created": "Link erstellt",
"share_success": "Freigabelink erfolgreich erstellt"
},
"batch": {
"one_selected": "1 Element ausgewählt",
@@ -566,7 +569,50 @@
"error_password_short": "Passwort muss mindestens 8 Zeichen haben",
"error_generic": "Fehlgeschlagen",
"error_network": "Netzwerkfehler: {{message}}",
"error_create_user": "Benutzer erstellen fehlgeschlagen"
"error_create_user": "Benutzer erstellen fehlgeschlagen",
"tab_storage": "Speicher",
"storage_title": "Speicherkonfiguration",
"storage_current_backend": "Aktuelles Backend",
"storage_total_blobs": "Gesamt-Blobs",
"storage_total_size": "Gesamtgröße",
"storage_dedup_ratio": "Deduplizierungsrate",
"storage_backend": "Backend",
"storage_local": "Lokal",
"storage_s3": "S3-kompatibel",
"storage_provider_preset": "Anbieter-Voreinstellung",
"storage_preset_custom": "Benutzerdefiniert",
"storage_endpoint_url": "Endpunkt-URL",
"storage_endpoint_hint": "Leer lassen für AWS S3",
"storage_bucket": "Bucket",
"storage_region": "Region",
"storage_access_key": "Zugriffsschlüssel",
"storage_secret_key": "Geheimschlüssel",
"storage_secret_configured": "Schlüssel konfiguriert",
"storage_key_placeholder": "Neuen Schlüssel eingeben",
"storage_path_style": "Pfadstil erzwingen",
"storage_path_style_hint": "Erforderlich für MinIO und einige S3-kompatible Dienste",
"storage_test_connection": "Verbindung testen",
"storage_test_success": "Verbindung erfolgreich",
"storage_test_failure": "Verbindung fehlgeschlagen",
"storage_save": "Konfiguration speichern",
"storage_saved": "Konfiguration gespeichert",
"storage_migration": "Datenmigration",
"storage_migration_coming_soon": "Migrationstools demnächst verfügbar",
"migration_status_label": "Migrationsstatus",
"migration_start": "Migration starten",
"migration_pause": "Pausieren",
"migration_resume": "Fortsetzen",
"migration_verify": "Verifizieren",
"migration_complete": "Abschließen",
"migration_started": "Migration gestartet",
"migration_paused_msg": "Migration pausiert",
"migration_resumed_msg": "Migration fortgesetzt",
"migration_completed_msg": "Migration erfolgreich abgeschlossen",
"migration_verifying": "Wird verifiziert...",
"migration_verify_passed": "Verifizierung erfolgreich",
"migration_verify_failed": "Verifizierung fehlgeschlagen",
"migration_failed_blobs": "Fehlgeschlagene Blobs",
"testing": "Wird getestet..."
},
"profile": {
"page_title": "Profil",
@@ -627,5 +673,11 @@
"error_create_pw": "App-Passwort erstellen fehlgeschlagen",
"confirm_revoke": "App-Passwort \"{{label}}\" widerrufen? Clients werden nicht mehr funktionieren.",
"error_revoke": "Widerrufen fehlgeschlagen"
}
},
"upload": {
"uploading": "Wird hochgeladen...",
"files": "Dateien",
"complete": "{{count}} / {{total}} hochgeladen"
},
"storage_quota_exceeded": "Speicherplatz erschöpft"
}
+12 -3
View File
@@ -307,7 +307,8 @@
"generated_link": "Generated Link",
"notify": "Send Notification",
"recipient": "Recipient",
"message": "Message"
"message": "Message",
"move_to_home": "Move to Home folder"
},
"dropzone": {
"drag_files": "Drag files here or click to select",
@@ -439,7 +440,9 @@
"item_deleted_permanently": "Item permanently deleted",
"trash_emptied": "Trash emptied successfully",
"title": "Notifications",
"empty": "No notifications"
"empty": "No notifications",
"link_created": "Link created",
"share_success": "Shared link created successfully"
},
"batch": {
"one_selected": "1 item selected",
@@ -670,5 +673,11 @@
"error_create_pw": "Failed to create app password",
"confirm_revoke": "Revoke app password \"{{label}}\"? Clients using this password will stop working.",
"error_revoke": "Failed to revoke app password"
}
},
"upload": {
"uploading": "Uploading...",
"files": "files",
"complete": "{{count}} / {{total}} uploaded"
},
"storage_quota_exceeded": "Storage quota exceeded"
}
+12 -3
View File
@@ -307,7 +307,8 @@
"generated_link": "Enlace Generado",
"notify": "Enviar Notificación",
"recipient": "Destinatario",
"message": "Mensaje"
"message": "Mensaje",
"move_to_home": "Mover a la carpeta de inicio"
},
"dropzone": {
"drag_files": "Arrastra archivos aquí o haz clic para seleccionar",
@@ -439,7 +440,9 @@
"item_deleted_permanently": "Elemento eliminado permanentemente",
"trash_emptied": "Papelera vaciada correctamente",
"title": "Notificaciones",
"empty": "Sin notificaciones"
"empty": "Sin notificaciones",
"link_created": "Enlace creado",
"share_success": "Enlace compartido creado correctamente"
},
"batch": {
"one_selected": "1 elemento seleccionado",
@@ -670,5 +673,11 @@
"error_create_pw": "Error al crear contraseña de aplicación",
"confirm_revoke": "¿Revocar contraseña \"{{label}}\"? Los clientes que la usen dejarán de funcionar.",
"error_revoke": "Error al revocar contraseña"
}
},
"upload": {
"uploading": "Subiendo...",
"files": "archivos",
"complete": "{{count}} / {{total}} subidos"
},
"storage_quota_exceeded": "Cuota de almacenamiento superada"
}
+56 -4
View File
@@ -307,7 +307,8 @@
"move_folder": "Move folder",
"no_subfolders": "No subfolders",
"rename_file": "Rename file",
"select_this_folder": "Select this folder"
"select_this_folder": "Select this folder",
"move_to_home": "انتقال به پوشه خانگی"
},
"dropzone": {
"drag_files": "پرونده‌ها را اینجا بکشید یا برای انتخاب کلیک کنید",
@@ -553,7 +554,50 @@
"error_password_short": "رمز عبور حداقل ۸ کاراکتر",
"error_generic": "خطا",
"error_network": "خطای شبکه: {{message}}",
"error_create_user": "خطا در ایجاد کاربر"
"error_create_user": "خطا در ایجاد کاربر",
"tab_storage": "فضای ذخیره‌سازی",
"storage_title": "تنظیمات فضای ذخیره‌سازی",
"storage_current_backend": "بک‌اند فعلی",
"storage_total_blobs": "مجموع بلوب‌ها",
"storage_total_size": "حجم کل",
"storage_dedup_ratio": "نسبت حذف تکراری",
"storage_backend": "بک‌اند",
"storage_local": "محلی",
"storage_s3": "سازگار با S3",
"storage_provider_preset": "پیش‌تنظیم ارائه‌دهنده",
"storage_preset_custom": "سفارشی",
"storage_endpoint_url": "آدرس نقطه پایانی",
"storage_endpoint_hint": "برای AWS S3 خالی بگذارید",
"storage_bucket": "باکت",
"storage_region": "منطقه",
"storage_access_key": "کلید دسترسی",
"storage_secret_key": "کلید مخفی",
"storage_secret_configured": "کلید تنظیم شد",
"storage_key_placeholder": "کلید جدید وارد کنید",
"storage_path_style": "اجبار سبک مسیر",
"storage_path_style_hint": "برای MinIO و برخی سرویس‌های سازگار با S3 لازم است",
"storage_test_connection": "آزمایش اتصال",
"storage_test_success": "اتصال موفق",
"storage_test_failure": "اتصال ناموفق",
"storage_save": "ذخیره تنظیمات",
"storage_saved": "تنظیمات ذخیره شد",
"storage_migration": "انتقال داده",
"storage_migration_coming_soon": "ابزارهای انتقال به زودی",
"migration_status_label": "وضعیت انتقال",
"migration_start": "شروع انتقال",
"migration_pause": "توقف",
"migration_resume": "ادامه",
"migration_verify": "تأیید",
"migration_complete": "تکمیل",
"migration_started": "انتقال شروع شد",
"migration_paused_msg": "انتقال متوقف شد",
"migration_resumed_msg": "انتقال ادامه یافت",
"migration_completed_msg": "انتقال با موفقیت تکمیل شد",
"migration_verifying": "در حال تأیید...",
"migration_verify_passed": "تأیید موفق",
"migration_verify_failed": "تأیید ناموفق",
"migration_failed_blobs": "بلوب‌های ناموفق",
"testing": "در حال آزمایش..."
},
"profile": {
"page_title": "پروفایل",
@@ -626,6 +670,14 @@
"item_deleted_permanently": "آیتم برای همیشه حذف شد",
"trash_emptied": "زباله‌دان خالی شد",
"title": "اعلان‌ها",
"empty": "بدون اعلان"
}
"empty": "بدون اعلان",
"link_created": "پیوند ایجاد شد",
"share_success": "پیوند اشتراک‌گذاری با موفقیت ایجاد شد"
},
"upload": {
"uploading": "در حال آپلود...",
"files": "فایل‌ها",
"complete": "{{count}} / {{total}} آپلود شد"
},
"storage_quota_exceeded": "سهمیه فضای ذخیره‌سازی تجاوز کرده است"
}
+56 -4
View File
@@ -307,7 +307,8 @@
"message": "Message",
"go_to_parent": ".. (parent folder)",
"no_subfolders": "No subfolders",
"select_this_folder": "Select this folder"
"select_this_folder": "Select this folder",
"move_to_home": "Déplacer vers le dossier personnel"
},
"dropzone": {
"drag_files": "Glissez des fichiers ici ou cliquez pour sélectionner",
@@ -439,7 +440,9 @@
"item_deleted_permanently": "Élément supprimé définitivement",
"trash_emptied": "Corbeille vidée avec succès",
"empty": "No notifications",
"title": "Notifications"
"title": "Notifications",
"link_created": "Lien créé",
"share_success": "Lien de partage créé avec succès"
},
"batch": {
"one_selected": "1 élément sélectionné",
@@ -566,7 +569,50 @@
"error_password_short": "Le mot de passe doit contenir au moins 8 caractères",
"error_generic": "Échec",
"error_network": "Erreur réseau : {{message}}",
"error_create_user": "Impossible de créer l'utilisateur"
"error_create_user": "Impossible de créer l'utilisateur",
"tab_storage": "Stockage",
"storage_title": "Configuration du stockage",
"storage_current_backend": "Backend actuel",
"storage_total_blobs": "Total des blobs",
"storage_total_size": "Taille totale",
"storage_dedup_ratio": "Taux de déduplication",
"storage_backend": "Backend",
"storage_local": "Local",
"storage_s3": "Compatible S3",
"storage_provider_preset": "Préréglage du fournisseur",
"storage_preset_custom": "Personnalisé",
"storage_endpoint_url": "URL du point de terminaison",
"storage_endpoint_hint": "Laisser vide pour AWS S3",
"storage_bucket": "Bucket",
"storage_region": "Région",
"storage_access_key": "Clé d'accès",
"storage_secret_key": "Clé secrète",
"storage_secret_configured": "Clé configurée",
"storage_key_placeholder": "Saisir une nouvelle clé",
"storage_path_style": "Forcer le style de chemin",
"storage_path_style_hint": "Requis pour MinIO et certains services compatibles S3",
"storage_test_connection": "Tester la connexion",
"storage_test_success": "Connexion réussie",
"storage_test_failure": "Échec de la connexion",
"storage_save": "Enregistrer la configuration",
"storage_saved": "Configuration enregistrée",
"storage_migration": "Migration des données",
"storage_migration_coming_soon": "Outils de migration bientôt disponibles",
"migration_status_label": "État de la migration",
"migration_start": "Démarrer la migration",
"migration_pause": "Pause",
"migration_resume": "Reprendre",
"migration_verify": "Vérifier",
"migration_complete": "Terminer",
"migration_started": "Migration démarrée",
"migration_paused_msg": "Migration en pause",
"migration_resumed_msg": "Migration reprise",
"migration_completed_msg": "Migration terminée avec succès",
"migration_verifying": "Vérification en cours...",
"migration_verify_passed": "Vérification réussie",
"migration_verify_failed": "Échec de la vérification",
"migration_failed_blobs": "Blobs échoués",
"testing": "Test en cours..."
},
"profile": {
"page_title": "Profil",
@@ -627,5 +673,11 @@
"error_create_pw": "Impossible de créer le mot de passe",
"confirm_revoke": "Révoquer le mot de passe \"{{label}}\" ? Les clients l'utilisant ne fonctionneront plus.",
"error_revoke": "Échec de la révocation"
}
},
"upload": {
"uploading": "Téléchargement en cours...",
"files": "fichiers",
"complete": "{{count}} / {{total}} téléchargés"
},
"storage_quota_exceeded": "Quota de stockage dépassé"
}
+56 -4
View File
@@ -307,7 +307,8 @@
"generated_link": "जनरेट किया गया लिंक",
"notify": "सूचना भेजें",
"recipient": "प्राप्तकर्ता",
"message": "संदेश"
"message": "संदेश",
"move_to_home": "होम फ़ोल्डर में ले जाएं"
},
"dropzone": {
"drag_files": "फ़ाइलें यहाँ खींचें या चुनने के लिए क्लिक करें",
@@ -439,7 +440,9 @@
"item_deleted_permanently": "आइटम स्थायी रूप से हटाया गया",
"trash_emptied": "रद्दी सफलतापूर्वक खाली की गई",
"title": "सूचनाएँ",
"empty": "कोई सूचना नहीं"
"empty": "कोई सूचना नहीं",
"link_created": "लिंक बनाया गया",
"share_success": "शेयर लिंक सफलतापूर्वक बनाया गया"
},
"batch": {
"one_selected": "1 आइटम चयनित",
@@ -566,7 +569,50 @@
"error_password_short": "पासवर्ड कम से कम 8 अक्षर",
"error_generic": "विफल",
"error_network": "नेटवर्क त्रुटि: {{message}}",
"error_create_user": "उपयोगकर्ता बनाने में विफल"
"error_create_user": "उपयोगकर्ता बनाने में विफल",
"tab_storage": "स्टोरेज",
"storage_title": "स्टोरेज कॉन्फ़िगरेशन",
"storage_current_backend": "वर्तमान बैकएंड",
"storage_total_blobs": "कुल ब्लॉब्स",
"storage_total_size": "कुल आकार",
"storage_dedup_ratio": "डीडुप्लिकेशन अनुपात",
"storage_backend": "बैकएंड",
"storage_local": "स्थानीय",
"storage_s3": "S3 संगत",
"storage_provider_preset": "प्रदाता प्रीसेट",
"storage_preset_custom": "कस्टम",
"storage_endpoint_url": "एंडपॉइंट URL",
"storage_endpoint_hint": "AWS S3 के लिए खाली छोड़ें",
"storage_bucket": "बकेट",
"storage_region": "क्षेत्र",
"storage_access_key": "एक्सेस की",
"storage_secret_key": "सीक्रेट की",
"storage_secret_configured": "की कॉन्फ़िगर की गई",
"storage_key_placeholder": "नई की दर्ज करें",
"storage_path_style": "पाथ स्टाइल फ़ोर्स करें",
"storage_path_style_hint": "MinIO और कुछ S3-संगत सेवाओं के लिए आवश्यक",
"storage_test_connection": "कनेक्शन परीक्षण",
"storage_test_success": "कनेक्शन सफल",
"storage_test_failure": "कनेक्शन विफल",
"storage_save": "कॉन्फ़िगरेशन सहेजें",
"storage_saved": "कॉन्फ़िगरेशन सहेजी गई",
"storage_migration": "डेटा माइग्रेशन",
"storage_migration_coming_soon": "माइग्रेशन टूल्स जल्द आ रहे हैं",
"migration_status_label": "माइग्रेशन स्थिति",
"migration_start": "माइग्रेशन शुरू करें",
"migration_pause": "रोकें",
"migration_resume": "फिर से शुरू करें",
"migration_verify": "सत्यापित करें",
"migration_complete": "पूर्ण करें",
"migration_started": "माइग्रेशन शुरू हुआ",
"migration_paused_msg": "माइग्रेशन रोका गया",
"migration_resumed_msg": "माइग्रेशन फिर से शुरू हुआ",
"migration_completed_msg": "माइग्रेशन सफलतापूर्वक पूर्ण हुआ",
"migration_verifying": "सत्यापन हो रहा है...",
"migration_verify_passed": "सत्यापन पास",
"migration_verify_failed": "सत्यापन विफल",
"migration_failed_blobs": "विफल ब्लॉब्स",
"testing": "परीक्षण हो रहा है..."
},
"profile": {
"page_title": "प्रोफ़ाइल",
@@ -627,5 +673,11 @@
"error_create_pw": "ऐप पासवर्ड बनाने में विफल",
"confirm_revoke": "ऐप पासवर्ड \"{{label}}\" रद्द करें? इसका उपयोग करने वाले क्लाइंट काम करना बंद कर देंगे।",
"error_revoke": "रद्द करने में विफल"
}
},
"upload": {
"uploading": "अपलोड हो रहा है...",
"files": "फ़ाइलें",
"complete": "{{count}} / {{total}} अपलोड हुए"
},
"storage_quota_exceeded": "स्टोरेज कोटा पार हो गया"
}
+56 -4
View File
@@ -307,7 +307,8 @@
"message": "Messaggio",
"go_to_parent": ".. (parent folder)",
"no_subfolders": "No subfolders",
"select_this_folder": "Select this folder"
"select_this_folder": "Select this folder",
"move_to_home": "Sposta nella cartella home"
},
"dropzone": {
"drag_files": "Trascina i file qui o clicca per selezionare",
@@ -439,7 +440,9 @@
"item_deleted_permanently": "Elemento eliminato definitivamente",
"trash_emptied": "Cestino svuotato con successo",
"empty": "No notifications",
"title": "Notifications"
"title": "Notifications",
"link_created": "Link creato",
"share_success": "Link di condivisione creato con successo"
},
"batch": {
"one_selected": "1 elemento selezionato",
@@ -566,7 +569,50 @@
"error_password_short": "La password deve avere almeno 8 caratteri",
"error_generic": "Fallito",
"error_network": "Errore di rete: {{message}}",
"error_create_user": "Impossibile creare l'utente"
"error_create_user": "Impossibile creare l'utente",
"tab_storage": "Archiviazione",
"storage_title": "Configurazione archiviazione",
"storage_current_backend": "Backend corrente",
"storage_total_blobs": "Blob totali",
"storage_total_size": "Dimensione totale",
"storage_dedup_ratio": "Rapporto deduplicazione",
"storage_backend": "Backend",
"storage_local": "Locale",
"storage_s3": "Compatibile S3",
"storage_provider_preset": "Preset fornitore",
"storage_preset_custom": "Personalizzato",
"storage_endpoint_url": "URL endpoint",
"storage_endpoint_hint": "Lasciare vuoto per AWS S3",
"storage_bucket": "Bucket",
"storage_region": "Regione",
"storage_access_key": "Chiave di accesso",
"storage_secret_key": "Chiave segreta",
"storage_secret_configured": "Chiave configurata",
"storage_key_placeholder": "Inserisci nuova chiave",
"storage_path_style": "Forza stile percorso",
"storage_path_style_hint": "Richiesto per MinIO e alcuni servizi compatibili S3",
"storage_test_connection": "Testa connessione",
"storage_test_success": "Connessione riuscita",
"storage_test_failure": "Connessione fallita",
"storage_save": "Salva configurazione",
"storage_saved": "Configurazione salvata",
"storage_migration": "Migrazione dati",
"storage_migration_coming_soon": "Strumenti di migrazione in arrivo",
"migration_status_label": "Stato migrazione",
"migration_start": "Avvia migrazione",
"migration_pause": "Pausa",
"migration_resume": "Riprendi",
"migration_verify": "Verifica",
"migration_complete": "Completa",
"migration_started": "Migrazione avviata",
"migration_paused_msg": "Migrazione in pausa",
"migration_resumed_msg": "Migrazione ripresa",
"migration_completed_msg": "Migrazione completata con successo",
"migration_verifying": "Verifica in corso...",
"migration_verify_passed": "Verifica superata",
"migration_verify_failed": "Verifica fallita",
"migration_failed_blobs": "Blob falliti",
"testing": "Test in corso..."
},
"profile": {
"page_title": "Profilo",
@@ -627,5 +673,11 @@
"error_create_pw": "Impossibile creare la password",
"confirm_revoke": "Revocare la password \"{{label}}\"? I client che la usano smetteranno di funzionare.",
"error_revoke": "Revoca fallita"
}
},
"upload": {
"uploading": "Caricamento in corso...",
"files": "file",
"complete": "{{count}} / {{total}} caricati"
},
"storage_quota_exceeded": "Quota di archiviazione superata"
}
+56 -4
View File
@@ -307,7 +307,8 @@
"generated_link": "生成されたリンク",
"notify": "通知を送信",
"recipient": "宛先",
"message": "メッセージ"
"message": "メッセージ",
"move_to_home": "ホームフォルダへ移動"
},
"dropzone": {
"drag_files": "ファイルをここにドラッグするか、クリックして選択",
@@ -439,7 +440,9 @@
"item_deleted_permanently": "アイテムを完全に削除しました",
"trash_emptied": "ゴミ箱を正常に空にしました",
"title": "通知",
"empty": "通知はありません"
"empty": "通知はありません",
"link_created": "リンクを作成しました",
"share_success": "共有リンクを正常に作成しました"
},
"batch": {
"one_selected": "1件選択中",
@@ -566,7 +569,50 @@
"error_password_short": "パスワードは8文字以上",
"error_generic": "失敗",
"error_network": "ネットワークエラー: {{message}}",
"error_create_user": "ユーザー作成失敗"
"error_create_user": "ユーザー作成失敗",
"tab_storage": "ストレージ",
"storage_title": "ストレージ設定",
"storage_current_backend": "現在のバックエンド",
"storage_total_blobs": "総ブロブ数",
"storage_total_size": "合計サイズ",
"storage_dedup_ratio": "重複排除率",
"storage_backend": "バックエンド",
"storage_local": "ローカル",
"storage_s3": "S3互換",
"storage_provider_preset": "プロバイダープリセット",
"storage_preset_custom": "カスタム",
"storage_endpoint_url": "エンドポイントURL",
"storage_endpoint_hint": "AWS S3の場合は空欄のまま",
"storage_bucket": "バケット",
"storage_region": "リージョン",
"storage_access_key": "アクセスキー",
"storage_secret_key": "シークレットキー",
"storage_secret_configured": "キーが設定済み",
"storage_key_placeholder": "新しいキーを入力",
"storage_path_style": "パススタイルを強制",
"storage_path_style_hint": "MinIOおよび一部のS3互換サービスに必要",
"storage_test_connection": "接続テスト",
"storage_test_success": "接続成功",
"storage_test_failure": "接続失敗",
"storage_save": "設定を保存",
"storage_saved": "設定を保存しました",
"storage_migration": "データ移行",
"storage_migration_coming_soon": "移行ツールは近日公開予定",
"migration_status_label": "移行状況",
"migration_start": "移行を開始",
"migration_pause": "一時停止",
"migration_resume": "再開",
"migration_verify": "検証",
"migration_complete": "完了",
"migration_started": "移行を開始しました",
"migration_paused_msg": "移行を一時停止しました",
"migration_resumed_msg": "移行を再開しました",
"migration_completed_msg": "移行が正常に完了しました",
"migration_verifying": "検証中...",
"migration_verify_passed": "検証に合格",
"migration_verify_failed": "検証に失敗",
"migration_failed_blobs": "失敗したブロブ",
"testing": "テスト中..."
},
"profile": {
"page_title": "プロフィール",
@@ -627,5 +673,11 @@
"error_create_pw": "アプリパスワードの作成に失敗しました",
"confirm_revoke": "アプリパスワード「{{label}}」を失効させますか?使用中のクライアントは動作しなくなります。",
"error_revoke": "失効に失敗しました"
}
},
"upload": {
"uploading": "アップロード中...",
"files": "ファイル",
"complete": "{{count}} / {{total}} アップロード済み"
},
"storage_quota_exceeded": "ストレージ容量を超過しました"
}
+56 -4
View File
@@ -307,7 +307,8 @@
"generated_link": "생성된 링크",
"notify": "알림 보내기",
"recipient": "수신자",
"message": "메시지"
"message": "메시지",
"move_to_home": "홈 폴더로 이동"
},
"dropzone": {
"drag_files": "여기에 파일을 드래그하거나 클릭하여 선택하세요",
@@ -439,7 +440,9 @@
"item_deleted_permanently": "항목이 영구적으로 삭제되었습니다",
"trash_emptied": "휴지통이 성공적으로 비워졌습니다",
"title": "알림",
"empty": "알림이 없습니다"
"empty": "알림이 없습니다",
"link_created": "링크 생성됨",
"share_success": "공유 링크가 성공적으로 생성되었습니다"
},
"batch": {
"one_selected": "1개 선택됨",
@@ -566,7 +569,50 @@
"error_password_short": "비밀번호 최소 8자",
"error_generic": "실패",
"error_network": "네트워크 오류: {{message}}",
"error_create_user": "사용자 생성 실패"
"error_create_user": "사용자 생성 실패",
"tab_storage": "저장소",
"storage_title": "저장소 구성",
"storage_current_backend": "현재 백엔드",
"storage_total_blobs": "총 블롭 수",
"storage_total_size": "총 크기",
"storage_dedup_ratio": "중복 제거 비율",
"storage_backend": "백엔드",
"storage_local": "로컬",
"storage_s3": "S3 호환",
"storage_provider_preset": "공급자 프리셋",
"storage_preset_custom": "사용자 지정",
"storage_endpoint_url": "엔드포인트 URL",
"storage_endpoint_hint": "AWS S3의 경우 비워두세요",
"storage_bucket": "버킷",
"storage_region": "지역",
"storage_access_key": "액세스 키",
"storage_secret_key": "시크릿 키",
"storage_secret_configured": "키 구성됨",
"storage_key_placeholder": "새 키 입력",
"storage_path_style": "경로 스타일 강제",
"storage_path_style_hint": "MinIO 및 일부 S3 호환 서비스에 필요",
"storage_test_connection": "연결 테스트",
"storage_test_success": "연결 성공",
"storage_test_failure": "연결 실패",
"storage_save": "구성 저장",
"storage_saved": "구성이 저장되었습니다",
"storage_migration": "데이터 마이그레이션",
"storage_migration_coming_soon": "마이그레이션 도구 곧 출시",
"migration_status_label": "마이그레이션 상태",
"migration_start": "마이그레이션 시작",
"migration_pause": "일시 중지",
"migration_resume": "재개",
"migration_verify": "확인",
"migration_complete": "완료",
"migration_started": "마이그레이션 시작됨",
"migration_paused_msg": "마이그레이션 일시 중지됨",
"migration_resumed_msg": "마이그레이션 재개됨",
"migration_completed_msg": "마이그레이션이 성공적으로 완료되었습니다",
"migration_verifying": "확인 중...",
"migration_verify_passed": "확인 통과",
"migration_verify_failed": "확인 실패",
"migration_failed_blobs": "실패한 블롭",
"testing": "테스트 중..."
},
"profile": {
"page_title": "프로필",
@@ -627,5 +673,11 @@
"error_create_pw": "앱 비밀번호 생성 실패",
"confirm_revoke": "앱 비밀번호 \"{{label}}\"을(를) 취소하시겠습니까? 이 비밀번호를 사용하는 클라이언트가 작동하지 않게 됩니다.",
"error_revoke": "취소 실패"
}
},
"upload": {
"uploading": "업로드 중...",
"files": "파일",
"complete": "{{count}} / {{total}} 업로드됨"
},
"storage_quota_exceeded": "저장 공간 할당량 초과"
}
+56 -4
View File
@@ -307,7 +307,8 @@
"message": "Bericht",
"go_to_parent": ".. (parent folder)",
"no_subfolders": "No subfolders",
"select_this_folder": "Select this folder"
"select_this_folder": "Select this folder",
"move_to_home": "Verplaatsen naar de thuismap"
},
"dropzone": {
"drag_files": "Sleep bestanden hierheen of klik om te selecteren",
@@ -439,7 +440,9 @@
"item_deleted_permanently": "Item permanent verwijderd",
"trash_emptied": "Prullenbak succesvol geleegd",
"title": "Notificaties",
"empty": "Geen notificaties"
"empty": "Geen notificaties",
"link_created": "Link aangemaakt",
"share_success": "Deellink succesvol aangemaakt"
},
"batch": {
"one_selected": "1 item geselecteerd",
@@ -566,7 +569,50 @@
"error_password_short": "Wachtwoord moet minimaal 8 tekens bevatten",
"error_generic": "Mislukt",
"error_network": "Netwerkfout: {{message}}",
"error_create_user": "Kan gebruiker niet aanmaken"
"error_create_user": "Kan gebruiker niet aanmaken",
"tab_storage": "Opslag",
"storage_title": "Opslagconfiguratie",
"storage_current_backend": "Huidig backend",
"storage_total_blobs": "Totaal blobs",
"storage_total_size": "Totale grootte",
"storage_dedup_ratio": "Deduplicatieverhouding",
"storage_backend": "Backend",
"storage_local": "Lokaal",
"storage_s3": "S3-compatibel",
"storage_provider_preset": "Providerinstelling",
"storage_preset_custom": "Aangepast",
"storage_endpoint_url": "Eindpunt-URL",
"storage_endpoint_hint": "Leeg laten voor AWS S3",
"storage_bucket": "Bucket",
"storage_region": "Regio",
"storage_access_key": "Toegangssleutel",
"storage_secret_key": "Geheime sleutel",
"storage_secret_configured": "Sleutel geconfigureerd",
"storage_key_placeholder": "Nieuwe sleutel invoeren",
"storage_path_style": "Padstijl forceren",
"storage_path_style_hint": "Vereist voor MinIO en sommige S3-compatibele diensten",
"storage_test_connection": "Verbinding testen",
"storage_test_success": "Verbinding geslaagd",
"storage_test_failure": "Verbinding mislukt",
"storage_save": "Configuratie opslaan",
"storage_saved": "Configuratie opgeslagen",
"storage_migration": "Gegevensmigratie",
"storage_migration_coming_soon": "Migratietools binnenkort beschikbaar",
"migration_status_label": "Migratiestatus",
"migration_start": "Migratie starten",
"migration_pause": "Pauzeren",
"migration_resume": "Hervatten",
"migration_verify": "Verifiëren",
"migration_complete": "Voltooien",
"migration_started": "Migratie gestart",
"migration_paused_msg": "Migratie gepauzeerd",
"migration_resumed_msg": "Migratie hervat",
"migration_completed_msg": "Migratie succesvol voltooid",
"migration_verifying": "Bezig met verifiëren...",
"migration_verify_passed": "Verificatie geslaagd",
"migration_verify_failed": "Verificatie mislukt",
"migration_failed_blobs": "Mislukte blobs",
"testing": "Bezig met testen..."
},
"profile": {
"page_title": "Profiel",
@@ -627,5 +673,11 @@
"error_create_pw": "App-wachtwoord aanmaken mislukt",
"confirm_revoke": "App-wachtwoord \"{{label}}\" intrekken? Clients die dit wachtwoord gebruiken zullen stoppen.",
"error_revoke": "Intrekken mislukt"
}
},
"upload": {
"uploading": "Bezig met uploaden...",
"files": "bestanden",
"complete": "{{count}} / {{total}} geüpload"
},
"storage_quota_exceeded": "Opslagquotum overschreden"
}
+56 -4
View File
@@ -307,7 +307,8 @@
"message": "Mensagem",
"go_to_parent": ".. (parent folder)",
"no_subfolders": "No subfolders",
"select_this_folder": "Select this folder"
"select_this_folder": "Select this folder",
"move_to_home": "Mover para a pasta inicial"
},
"dropzone": {
"drag_files": "Arraste arquivos aqui ou clique para selecionar",
@@ -439,7 +440,9 @@
"item_deleted_permanently": "Item excluído permanentemente",
"trash_emptied": "Lixeira esvaziada com sucesso",
"empty": "No notifications",
"title": "Notifications"
"title": "Notifications",
"link_created": "Link criado",
"share_success": "Link de partilha criado com sucesso"
},
"batch": {
"one_selected": "1 item selecionado",
@@ -566,7 +569,50 @@
"error_password_short": "A senha deve ter pelo menos 8 caracteres",
"error_generic": "Falha",
"error_network": "Erro de rede: {{message}}",
"error_create_user": "Falha ao criar usuário"
"error_create_user": "Falha ao criar usuário",
"tab_storage": "Armazenamento",
"storage_title": "Configuração de armazenamento",
"storage_current_backend": "Backend atual",
"storage_total_blobs": "Total de blobs",
"storage_total_size": "Tamanho total",
"storage_dedup_ratio": "Taxa de deduplicação",
"storage_backend": "Backend",
"storage_local": "Local",
"storage_s3": "Compatível com S3",
"storage_provider_preset": "Predefinição do fornecedor",
"storage_preset_custom": "Personalizado",
"storage_endpoint_url": "URL do endpoint",
"storage_endpoint_hint": "Deixar em branco para AWS S3",
"storage_bucket": "Bucket",
"storage_region": "Região",
"storage_access_key": "Chave de acesso",
"storage_secret_key": "Chave secreta",
"storage_secret_configured": "Chave configurada",
"storage_key_placeholder": "Introduzir nova chave",
"storage_path_style": "Forçar estilo de caminho",
"storage_path_style_hint": "Necessário para MinIO e alguns serviços compatíveis com S3",
"storage_test_connection": "Testar ligação",
"storage_test_success": "Ligação bem-sucedida",
"storage_test_failure": "Falha na ligação",
"storage_save": "Guardar configuração",
"storage_saved": "Configuração guardada",
"storage_migration": "Migração de dados",
"storage_migration_coming_soon": "Ferramentas de migração em breve",
"migration_status_label": "Estado da migração",
"migration_start": "Iniciar migração",
"migration_pause": "Pausar",
"migration_resume": "Retomar",
"migration_verify": "Verificar",
"migration_complete": "Concluir",
"migration_started": "Migração iniciada",
"migration_paused_msg": "Migração pausada",
"migration_resumed_msg": "Migração retomada",
"migration_completed_msg": "Migração concluída com sucesso",
"migration_verifying": "A verificar...",
"migration_verify_passed": "Verificação aprovada",
"migration_verify_failed": "Verificação falhou",
"migration_failed_blobs": "Blobs com falha",
"testing": "A testar..."
},
"profile": {
"page_title": "Perfil",
@@ -627,5 +673,11 @@
"error_create_pw": "Falha ao criar senha de aplicativo",
"confirm_revoke": "Revogar senha \"{{label}}\"? Clientes que usam esta senha deixarão de funcionar.",
"error_revoke": "Falha ao revogar"
}
},
"upload": {
"uploading": "A carregar...",
"files": "ficheiros",
"complete": "{{count}} / {{total}} carregados"
},
"storage_quota_exceeded": "Cota de armazenamento excedida"
}
+56 -4
View File
@@ -307,7 +307,8 @@
"generated_link": "Сгенерированная ссылка",
"notify": "Отправить уведомление",
"recipient": "Получатель",
"message": "Сообщение"
"message": "Сообщение",
"move_to_home": "Переместить в домашнюю папку"
},
"dropzone": {
"drag_files": "Перетащите файлы сюда или нажмите для выбора",
@@ -439,7 +440,9 @@
"item_deleted_permanently": "Элемент удалён навсегда",
"trash_emptied": "Корзина успешно очищена",
"title": "Уведомления",
"empty": "Нет уведомлений"
"empty": "Нет уведомлений",
"link_created": "Ссылка создана",
"share_success": "Ссылка для общего доступа успешно создана"
},
"batch": {
"one_selected": "Выбран 1 элемент",
@@ -566,7 +569,50 @@
"error_password_short": "Пароль минимум 8 символов",
"error_generic": "Ошибка",
"error_network": "Ошибка сети: {{message}}",
"error_create_user": "Не удалось создать"
"error_create_user": "Не удалось создать",
"tab_storage": "Хранилище",
"storage_title": "Настройка хранилища",
"storage_current_backend": "Текущий бэкенд",
"storage_total_blobs": "Всего блобов",
"storage_total_size": "Общий размер",
"storage_dedup_ratio": "Коэффициент дедупликации",
"storage_backend": "Бэкенд",
"storage_local": "Локальный",
"storage_s3": "Совместимый с S3",
"storage_provider_preset": "Пресет провайдера",
"storage_preset_custom": "Пользовательский",
"storage_endpoint_url": "URL конечной точки",
"storage_endpoint_hint": "Оставьте пустым для AWS S3",
"storage_bucket": "Бакет",
"storage_region": "Регион",
"storage_access_key": "Ключ доступа",
"storage_secret_key": "Секретный ключ",
"storage_secret_configured": "Ключ настроен",
"storage_key_placeholder": "Введите новый ключ",
"storage_path_style": "Принудительный стиль пути",
"storage_path_style_hint": "Требуется для MinIO и некоторых S3-совместимых сервисов",
"storage_test_connection": "Проверить соединение",
"storage_test_success": "Соединение успешно",
"storage_test_failure": "Ошибка соединения",
"storage_save": "Сохранить конфигурацию",
"storage_saved": "Конфигурация сохранена",
"storage_migration": "Миграция данных",
"storage_migration_coming_soon": "Инструменты миграции скоро появятся",
"migration_status_label": "Статус миграции",
"migration_start": "Начать миграцию",
"migration_pause": "Пауза",
"migration_resume": "Возобновить",
"migration_verify": "Проверить",
"migration_complete": "Завершить",
"migration_started": "Миграция начата",
"migration_paused_msg": "Миграция приостановлена",
"migration_resumed_msg": "Миграция возобновлена",
"migration_completed_msg": "Миграция успешно завершена",
"migration_verifying": "Проверка...",
"migration_verify_passed": "Проверка пройдена",
"migration_verify_failed": "Проверка не пройдена",
"migration_failed_blobs": "Неудачные блобы",
"testing": "Тестирование..."
},
"profile": {
"page_title": "Профиль",
@@ -627,5 +673,11 @@
"error_create_pw": "Не удалось создать пароль",
"confirm_revoke": "Отозвать пароль «{{label}}»? Клиенты перестанут работать.",
"error_revoke": "Не удалось отозвать"
}
},
"upload": {
"uploading": "Загрузка...",
"files": "файлов",
"complete": "{{count}} / {{total}} загружено"
},
"storage_quota_exceeded": "Превышена квота хранилища"
}
+56 -4
View File
@@ -307,7 +307,8 @@
"move_folder": "Move folder",
"no_subfolders": "No subfolders",
"rename_file": "Rename file",
"select_this_folder": "Select this folder"
"select_this_folder": "Select this folder",
"move_to_home": "移动到主文件夹"
},
"dropzone": {
"drag_files": "将文件拖到这里,或点击选择",
@@ -553,7 +554,50 @@
"error_password_short": "密码至少需要8个字符",
"error_generic": "失败",
"error_network": "网络错误:{{message}}",
"error_create_user": "创建用户失败"
"error_create_user": "创建用户失败",
"tab_storage": "存储",
"storage_title": "存储配置",
"storage_current_backend": "当前后端",
"storage_total_blobs": "总块数",
"storage_total_size": "总大小",
"storage_dedup_ratio": "去重比率",
"storage_backend": "后端",
"storage_local": "本地",
"storage_s3": "S3 兼容",
"storage_provider_preset": "提供商预设",
"storage_preset_custom": "自定义",
"storage_endpoint_url": "端点 URL",
"storage_endpoint_hint": "AWS S3 请留空",
"storage_bucket": "存储桶",
"storage_region": "地区",
"storage_access_key": "访问密钥",
"storage_secret_key": "密钥",
"storage_secret_configured": "密钥已配置",
"storage_key_placeholder": "输入新密钥",
"storage_path_style": "强制路径风格",
"storage_path_style_hint": "MinIO 及某些 S3 兼容服务需要此选项",
"storage_test_connection": "测试连接",
"storage_test_success": "连接成功",
"storage_test_failure": "连接失败",
"storage_save": "保存配置",
"storage_saved": "配置已保存",
"storage_migration": "数据迁移",
"storage_migration_coming_soon": "迁移工具即将推出",
"migration_status_label": "迁移状态",
"migration_start": "开始迁移",
"migration_pause": "暂停",
"migration_resume": "继续",
"migration_verify": "验证",
"migration_complete": "完成",
"migration_started": "迁移已开始",
"migration_paused_msg": "迁移已暂停",
"migration_resumed_msg": "迁移已继续",
"migration_completed_msg": "迁移成功完成",
"migration_verifying": "正在验证...",
"migration_verify_passed": "验证通过",
"migration_verify_failed": "验证失败",
"migration_failed_blobs": "失败的块",
"testing": "正在测试..."
},
"profile": {
"page_title": "个人资料",
@@ -626,6 +670,14 @@
"item_deleted_permanently": "项目已永久删除",
"trash_emptied": "回收站已清空",
"title": "通知",
"empty": "暂无通知"
}
"empty": "暂无通知",
"link_created": "链接已创建",
"share_success": "分享链接创建成功"
},
"upload": {
"uploading": "正在上传...",
"files": "个文件",
"complete": "已上传 {{count}} / {{total}}"
},
"storage_quota_exceeded": "存储配额已超限"
}
+1 -1
View File
@@ -1,6 +1,6 @@
// OxiCloud Service Worker
// FIXME: generate cache name according build ?
const CACHE_NAME = 'oxicloud-cache-v19';
const CACHE_NAME = 'oxicloud-cache-v20';
// Only cache static assets — NOT HTML files.
// HTML files are served network-first so browsers always get the latest