refactor(ui): i18n: remove unnecessary wrappers

This commit is contained in:
Edouard Vanbelle
2026-04-25 23:19:36 +02:00
parent 4e2029969e
commit 75d3984b8f
8 changed files with 223 additions and 272 deletions
+3 -4
View File
@@ -212,7 +212,6 @@ function showUserProfileModal() {
// FIXME: use classes // FIXME: use classes
const barColor = percentage > 90 ? '#ef4444' : percentage > 70 ? '#f59e0b' : '#22c55e'; const barColor = percentage > 90 ? '#ef4444' : percentage > 70 ? '#f59e0b' : '#22c55e';
const t = (key, fallback) => i18n.t(key) || fallback;
const existing = document.getElementById('profile-modal-overlay'); const existing = document.getElementById('profile-modal-overlay');
if (existing) existing.remove(); if (existing) existing.remove();
@@ -226,11 +225,11 @@ function showUserProfileModal() {
<div class="about-modal-avatar">${initials}</div> <div class="about-modal-avatar">${initials}</div>
<h3 class="about-modal-username">${username}</h3> <h3 class="about-modal-username">${username}</h3>
<p class="about-modal-email">${email}</p> <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>
<div class="about-modal-storage"> <div class="about-modal-storage">
<div class="about-modal-storage-label"> <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>
<div class="about-modal-bar-bg"> <div class="about-modal-bar-bg">
<div class="about-modal-bar-fill" id="about-bar-fill"></div> <div class="about-modal-bar-fill" id="about-bar-fill"></div>
@@ -238,7 +237,7 @@ function showUserProfileModal() {
<div class="about-modal-bar-text">${percentage}% · ${formatFileSize(usedBytes)} / ${formatQuotaSize(quotaBytes)}</div> <div class="about-modal-bar-text">${percentage}% · ${formatFileSize(usedBytes)} / ${formatQuotaSize(quotaBytes)}</div>
</div> </div>
<div class="about-modal-footer"> <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>
</div> </div>
`; `;
+7 -12
View File
@@ -137,14 +137,12 @@ const Modal = {
* @returns {Promise<string|null>} * @returns {Promise<string|null>}
*/ */
promptNewFolder() { promptNewFolder() {
const t = i18n.t.bind(i18n);
return this.prompt({ return this.prompt({
title: t('dialogs.new_folder_title') || 'New folder', title: i18n.t('dialogs.new_folder_title'),
label: t('dialogs.folder_name') || 'Folder name', label: i18n.t('dialogs.folder_name'),
placeholder: t('dialogs.folder_placeholder') || 'My folder', placeholder: i18n.t('dialogs.folder_placeholder'),
icon: 'fa-folder-plus', icon: 'fa-folder-plus',
confirmText: t('actions.create') || 'Create' confirmText: i18n.t('actions.create')
}); });
}, },
@@ -155,18 +153,15 @@ const Modal = {
* @returns {Promise<string|null>} * @returns {Promise<string|null>}
*/ */
promptRename(currentName, isFolder = false) { promptRename(currentName, isFolder = false) {
const t = i18n.t.bind(i18n);
// For files, we want to select only the name part (without extension)
this._selectNameOnly = !isFolder; this._selectNameOnly = !isFolder;
return this.prompt({ return this.prompt({
title: t('dialogs.rename_title') || 'Renombrar', title: i18n.t('dialogs.rename_title'),
label: t('dialogs.new_name') || 'Nuevo nombre', label: i18n.t('dialogs.new_name'),
placeholder: '', placeholder: '',
value: currentName, value: currentName,
icon: isFolder ? 'fa-folder' : 'fa-file', 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.className = 'notif-item';
item.id = batchId; item.id = batchId;
const t = i18n.t.bind(i18n); const uploadingText = folderName ? `📁 ${i18n.t('upload.uploading')} ${_esc(folderName)}…` : i18n.t('upload.uploading');
const uploadingText = folderName ? `📁 ${t('upload.uploading')} ${_esc(folderName)}…` : t('upload.uploading'); const filesLabel = i18n.t('upload.files');
const filesLabel = t('upload.files');
item.innerHTML = ` item.innerHTML = `
<div class="notif-item-icon upload"><i class="fas fa-cloud-upload-alt"></i></div> <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 pctEl = $(`${batchId}-pct`);
const statsEl = $(`${batchId}-stats`); const statsEl = $(`${batchId}-stats`);
const t = i18n.t.bind(i18n); const filesLabel = i18n.t('upload.files');
const filesLabel = t('upload.files');
if (fillEl) fillEl.style.width = `${pctVal}%`; if (fillEl) fillEl.style.width = `${pctVal}%`;
if (pctEl) pctEl.textContent = `${pctVal}%`; if (pctEl) pctEl.textContent = `${pctVal}%`;
@@ -282,8 +280,7 @@ const notifications = (() => {
const curEl = $(`${batchId}-current`); const curEl = $(`${batchId}-current`);
if (curEl) curEl.textContent = ''; if (curEl) curEl.textContent = '';
const t = i18n.t.bind(i18n); const completeText = i18n.t('upload.complete', {
const completeText = t('upload.complete', {
count: successCount, count: successCount,
total: totalFiles total: totalFiles
}); });
+2 -4
View File
@@ -1115,12 +1115,10 @@ const contextMenus = {
}, },
_renderPlaylistSelect(container, playlists) { _renderPlaylistSelect(container, playlists) {
const t = i18n.t.bind(i18n);
container.innerHTML = ''; container.innerHTML = '';
if (playlists.length === 0) { 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; return;
} }
@@ -1131,7 +1129,7 @@ const contextMenus = {
item.innerHTML = ` item.innerHTML = `
<i class="fas fa-list"></i> <i class="fas fa-list"></i>
<span>${this._escapeHtml(playlist.name)}</span> <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', () => { item.addEventListener('click', () => {
+100 -125
View File
@@ -87,9 +87,6 @@ const musicView = {
if (!this._container) return; if (!this._container) return;
// FIXME should call directly // FIXME should call directly
const t = (key, _fallback = '') => {
return i18n.t(key);
};
// Empty state: no playlists at all — show full-width centered onboarding // Empty state: no playlists at all — show full-width centered onboarding
if (this.playlists.length === 0) { if (this.playlists.length === 0) {
@@ -98,11 +95,11 @@ const musicView = {
<div class="music-empty-state-icon"> <div class="music-empty-state-icon">
<i class="fas fa-music"></i> <i class="fas fa-music"></i>
</div> </div>
<h3 class="music-empty-state-title">${t('music.no_playlists', 'No playlists yet')}</h3> <h3 class="music-empty-state-title">${i18n.t('music.no_playlists')}</h3>
<p class="music-empty-state-desc">${t('music.empty_hint', 'Create your first playlist to start organizing your music')}</p> <p class="music-empty-state-desc">${i18n.t('music.empty_hint')}</p>
<button class="btn btn-primary" id="music-create-playlist-btn"> <button class="btn btn-primary" id="music-create-playlist-btn">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
<span>${t('music.create_playlist', 'Create Playlist')}</span> <span>${i18n.t('music.create_playlist')}</span>
</button> </button>
</div> </div>
`; `;
@@ -118,8 +115,8 @@ const musicView = {
<div class="music-content"> <div class="music-content">
<div class="music-sidebar"> <div class="music-sidebar">
<div class="music-sidebar-header"> <div class="music-sidebar-header">
<h3>${t('music.playlists', 'Playlists')}</h3> <h3>${i18n.t('music.playlists')}</h3>
<button class="music-sidebar-add-btn" id="music-create-playlist-btn" title="${t('music.create_playlist', 'Create Playlist')}"> <button class="music-sidebar-add-btn" id="music-create-playlist-btn" title="${i18n.t('music.create_playlist')}">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
</button> </button>
</div> </div>
@@ -128,44 +125,44 @@ const musicView = {
<div class="music-main"> <div class="music-main">
<div class="music-welcome"> <div class="music-welcome">
<i class="fas fa-music"></i> <i class="fas fa-music"></i>
<h3>${t('music.select_playlist', 'Select a playlist')}</h3> <h3>${i18n.t('music.select_playlist')}</h3>
<p>${t('music.select_hint', 'Choose a playlist from the sidebar or create a new one')}</p> <p>${i18n.t('music.select_hint')}</p>
</div> </div>
<div class="music-playlist-detail hidden" id="music-playlist-detail"> <div class="music-playlist-detail hidden" id="music-playlist-detail">
<div class="music-playlist-header"> <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> <i class="fas fa-music"></i>
</div> </div>
<div class="music-playlist-info"> <div class="music-playlist-info">
<h2 id="music-playlist-name"></h2> <h2 id="music-playlist-name"></h2>
<p id="music-playlist-meta"></p> <p id="music-playlist-meta"></p>
<span class="music-public-badge hidden" id="music-public-badge"> <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> </span>
</div> </div>
</div> </div>
<div class="music-playlist-actions"> <div class="music-playlist-actions">
<button class="btn btn-secondary" id="music-play-all-btn"> <button class="btn btn-secondary" id="music-play-all-btn">
<i class="fas fa-play"></i> <i class="fas fa-play"></i>
<span>${t('music.play_all', 'Play All')}</span> <span>${i18n.t('music.play_all')}</span>
</button> </button>
<button class="btn btn-secondary" id="music-shuffle-btn"> <button class="btn btn-secondary" id="music-shuffle-btn">
<i class="fas fa-shuffle"></i> <i class="fas fa-shuffle"></i>
</button> </button>
<button class="btn btn-secondary" id="music-add-tracks-btn"> <button class="btn btn-secondary" id="music-add-tracks-btn">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
<span>${t('music.add_tracks', 'Add Tracks')}</span> <span>${i18n.t('music.add_tracks')}</span>
</button> </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> <i class="fas fa-pen"></i>
</button> </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> <i class="fas fa-share-alt"></i>
</button> </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> <i class="fas fa-users"></i>
</button> </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> <i class="fas fa-globe"></i>
</button> </button>
<button class="btn btn-secondary" id="music-delete-playlist-btn"> <button class="btn btn-secondary" id="music-delete-playlist-btn">
@@ -186,13 +183,12 @@ const musicView = {
const listEl = document.getElementById('music-playlist-list'); const listEl = document.getElementById('music-playlist-list');
if (!listEl) return; if (!listEl) return;
const t = (key) => i18n.t(key);
if (this.playlists.length === 0) { if (this.playlists.length === 0) {
listEl.innerHTML = ` listEl.innerHTML = `
<div class="music-empty"> <div class="music-empty">
<i class="fas fa-music"></i> <i class="fas fa-music"></i>
<p>${t('music.no_playlists', 'No playlists yet')}</p> <p>${i18n.t('music.no_playlists')}</p>
</div> </div>
`; `;
return; return;
@@ -207,7 +203,7 @@ const musicView = {
</div> </div>
<div class="music-playlist-item-info"> <div class="music-playlist-item-info">
<span class="music-playlist-item-name">${this._escapeHtml(p.name)}</span> <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>
</div> </div>
` `
@@ -289,8 +285,7 @@ const musicView = {
if (detailEl) detailEl.classList.remove('hidden'); if (detailEl) detailEl.classList.remove('hidden');
if (nameEl) nameEl.textContent = playlist.name; if (nameEl) nameEl.textContent = playlist.name;
if (metaEl) { if (metaEl) {
const t = (key) => i18n.t(key); metaEl.textContent = `${playlist.track_count || 0} ${i18n.t('music.tracks')}`;
metaEl.textContent = `${playlist.track_count || 0} ${t('music.tracks', 'tracks')}`;
} }
// Cover art // Cover art
@@ -310,8 +305,7 @@ const musicView = {
} }
const togglePublicBtn = document.getElementById('music-toggle-public-btn'); const togglePublicBtn = document.getElementById('music-toggle-public-btn');
if (togglePublicBtn) { if (togglePublicBtn) {
const t2 = (key) => i18n.t(key); togglePublicBtn.title = playlist.is_public ? i18n.t('music.make_private') : i18n.t('music.make_public');
togglePublicBtn.title = playlist.is_public ? t2('music.make_private', 'Make private') : t2('music.make_public', 'Make public');
togglePublicBtn.classList.toggle('active', playlist.is_public); togglePublicBtn.classList.toggle('active', playlist.is_public);
} }
@@ -348,13 +342,12 @@ const musicView = {
const trackListEl = document.getElementById('music-track-list'); const trackListEl = document.getElementById('music-track-list');
if (!trackListEl) return; if (!trackListEl) return;
const t = (key) => i18n.t(key);
if (this.currentTracks.length === 0) { if (this.currentTracks.length === 0) {
trackListEl.innerHTML = ` trackListEl.innerHTML = `
<div class="music-empty"> <div class="music-empty">
<i class="fas fa-music"></i> <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> </div>
`; `;
return; return;
@@ -364,9 +357,9 @@ const musicView = {
<div class="music-track-header"> <div class="music-track-header">
<span class="music-track-col music-track-drag"></span> <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-num">#</span>
<span class="music-track-col music-track-title">${t('music.title', 'Title')}</span> <span class="music-track-col music-track-title">${i18n.t('music.title')}</span>
<span class="music-track-col music-track-artist">${t('music.artist', 'Artist')}</span> <span class="music-track-col music-track-artist">${i18n.t('music.artist')}</span>
<span class="music-track-col music-track-album">${t('music.album', 'Album')}</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-duration"><i class="far fa-clock"></i></span>
<span class="music-track-col music-track-actions"></span> <span class="music-track-col music-track-actions"></span>
</div> </div>
@@ -381,13 +374,13 @@ const musicView = {
</span> </span>
<span class="music-track-col music-track-title"> <span class="music-track-col music-track-title">
<i class="fas fa-music music-track-icon"></i> <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>
<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-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-duration">${this._formatDuration(track.duration_secs)}</span>
<span class="music-track-col music-track-actions"> <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> </span>
</div> </div>
` `
@@ -461,8 +454,7 @@ const musicView = {
_playTrack(idx) { _playTrack(idx) {
if (!this.currentTracks[idx]) return; if (!this.currentTracks[idx]) return;
const t = (key) => i18n.t(key); musicPlayer.setQueue(this.currentTracks, this.currentPlaylist?.name || i18n.t('music.playlists'));
musicPlayer.setQueue(this.currentTracks, this.currentPlaylist?.name || t('music.playlists', 'Playlist'));
musicPlayer.playTrack(idx); musicPlayer.playTrack(idx);
}, },
@@ -479,21 +471,19 @@ const musicView = {
const j = Math.floor(Math.random() * (i + 1)); const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
} }
const t = (key) => i18n.t(key); musicPlayer.setQueue(shuffled, this.currentPlaylist?.name || i18n.t('music.shuffle'));
musicPlayer.setQueue(shuffled, this.currentPlaylist?.name || t('music.shuffle', 'Shuffle'));
musicPlayer.playTrack(0); musicPlayer.playTrack(0);
} }
}, },
async _showCreatePlaylistDialog() { async _showCreatePlaylistDialog() {
const t = (key) => i18n.t(key);
const name = await Modal.prompt({ const name = await Modal.prompt({
title: t('music.create_playlist', 'Create Playlist'), title: i18n.t('music.create_playlist'),
label: t('music.playlist_name', 'Playlist name'), label: i18n.t('music.playlist_name'),
placeholder: t('music.playlist_name', 'Playlist name'), placeholder: i18n.t('music.playlist_name'),
icon: 'fa-music', icon: 'fa-music',
confirmText: t('music.create', 'Create') confirmText: i18n.t('music.create')
}); });
if (!name?.trim()) return; if (!name?.trim()) return;
@@ -501,7 +491,6 @@ const musicView = {
}, },
async _createPlaylist(name) { async _createPlaylist(name) {
const t = (key) => i18n.t(key);
const createBtn = document.getElementById('music-create-playlist-btn'); const createBtn = document.getElementById('music-create-playlist-btn');
if (createBtn) createBtn.disabled = true; if (createBtn) createBtn.disabled = true;
try { try {
@@ -522,7 +511,7 @@ const musicView = {
notifications.addNotification({ notifications.addNotification({
icon: 'fa-check-circle', icon: 'fa-check-circle',
iconClass: 'upload', iconClass: 'upload',
title: t('music.create_playlist', 'Create Playlist'), title: i18n.t('music.create_playlist'),
text: name text: name
}); });
} }
@@ -532,7 +521,7 @@ const musicView = {
notifications.addNotification({ notifications.addNotification({
icon: 'fa-exclamation-circle', icon: 'fa-exclamation-circle',
iconClass: 'error', iconClass: 'error',
title: t('music.error', 'Error'), title: i18n.t('music.error'),
text: err.message text: err.message
}); });
} }
@@ -542,18 +531,17 @@ const musicView = {
}, },
async _deletePlaylist() { async _deletePlaylist() {
const t = (key) => i18n.t(key);
if (!this.currentPlaylist) return; if (!this.currentPlaylist) return;
const confirmed = await new Promise((resolve) => { const confirmed = await new Promise((resolve) => {
Modal.prompt({ Modal.prompt({
title: t('music.delete', 'Delete'), title: i18n.t('music.delete'),
label: t('music.confirm_delete', 'Delete this playlist?'), label: i18n.t('music.confirm_delete'),
placeholder: '', placeholder: '',
value: this.currentPlaylist.name, value: this.currentPlaylist.name,
icon: 'fa-trash', icon: 'fa-trash',
confirmText: t('music.delete', 'Delete') confirmText: i18n.t('music.delete')
}).then((val) => resolve(val !== null)); }).then((val) => resolve(val !== null));
}); });
if (!confirmed) return; if (!confirmed) return;
@@ -575,7 +563,7 @@ const musicView = {
this.currentTracks = []; this.currentTracks = [];
this._renderPlaylists(); this._renderPlaylists();
if (notifications) { 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) { } catch (err) {
console.error('Delete playlist error:', err); console.error('Delete playlist error:', err);
@@ -583,7 +571,7 @@ const musicView = {
notifications.addNotification({ notifications.addNotification({
icon: 'fa-exclamation-circle', icon: 'fa-exclamation-circle',
iconClass: 'error', iconClass: 'error',
title: t('music.error', 'Error'), title: i18n.t('music.error'),
text: err.message text: err.message
}); });
} }
@@ -628,15 +616,14 @@ const musicView = {
async _showEditPlaylistDialog() { async _showEditPlaylistDialog() {
if (!this.currentPlaylist) return; if (!this.currentPlaylist) return;
const t = (key) => i18n.t(key);
const newName = await Modal.prompt({ const newName = await Modal.prompt({
title: t('music.edit', 'Edit'), title: i18n.t('music.edit'),
label: t('music.playlist_name', 'Playlist name'), label: i18n.t('music.playlist_name'),
placeholder: t('music.playlist_name', 'Playlist name'), placeholder: i18n.t('music.playlist_name'),
value: this.currentPlaylist.name, value: this.currentPlaylist.name,
icon: 'fa-pen', icon: 'fa-pen',
confirmText: t('actions.confirm', 'Save') confirmText: i18n.t('actions.confirm')
}); });
if (!newName?.trim() || newName.trim() === this.currentPlaylist.name) return; if (!newName?.trim() || newName.trim() === this.currentPlaylist.name) return;
@@ -663,7 +650,7 @@ const musicView = {
notifications.addNotification({ notifications.addNotification({
icon: 'fa-exclamation-circle', icon: 'fa-exclamation-circle',
iconClass: 'error', iconClass: 'error',
title: t('music.error', 'Error'), title: i18n.t('music.error'),
text: err.message text: err.message
}); });
} }
@@ -672,14 +659,13 @@ const musicView = {
async _showSharePlaylistDialog() { async _showSharePlaylistDialog() {
if (!this.currentPlaylist) return; if (!this.currentPlaylist) return;
const t = (key) => i18n.t(key);
const userId = await Modal.prompt({ const userId = await Modal.prompt({
title: t('music.share', 'Share'), title: i18n.t('music.share'),
label: t('music.share_with_user', 'User ID or email'), label: i18n.t('music.share_with_user'),
placeholder: t('music.share_with_user', 'User ID or email'), placeholder: i18n.t('music.share_with_user'),
icon: 'fa-share-alt', icon: 'fa-share-alt',
confirmText: t('music.share', 'Share') confirmText: i18n.t('music.share')
}); });
if (!userId?.trim()) return; if (!userId?.trim()) return;
@@ -697,8 +683,8 @@ const musicView = {
notifications.addNotification({ notifications.addNotification({
icon: 'fa-check-circle', icon: 'fa-check-circle',
iconClass: 'upload', iconClass: 'upload',
title: t('music.share', 'Share'), title: i18n.t('music.share'),
text: t('music.added', 'Added!') text: i18n.t('music.added')
}); });
} }
} catch (err) { } catch (err) {
@@ -707,7 +693,7 @@ const musicView = {
notifications.addNotification({ notifications.addNotification({
icon: 'fa-exclamation-circle', icon: 'fa-exclamation-circle',
iconClass: 'error', iconClass: 'error',
title: t('music.error', 'Error'), title: i18n.t('music.error'),
text: err.message text: err.message
}); });
} }
@@ -716,7 +702,6 @@ const musicView = {
async _showAddTracksDialog() { async _showAddTracksDialog() {
if (!this.currentPlaylist) return; if (!this.currentPlaylist) return;
const t = (key) => i18n.t(key);
// ── Build modal overlay ── // ── Build modal overlay ──
const overlay = document.createElement('div'); const overlay = document.createElement('div');
@@ -724,23 +709,23 @@ const musicView = {
overlay.innerHTML = ` overlay.innerHTML = `
<div class="music-picker-modal"> <div class="music-picker-modal">
<div class="music-picker-header"> <div class="music-picker-header">
<h3><i class="fas fa-music"></i> ${t('music.add_tracks', 'Add Tracks')}</h3> <h3><i class="fas fa-music"></i> ${i18n.t('music.add_tracks')}</h3>
<button class="music-picker-close" title="${t('common.close', 'Close')}">&times;</button> <button class="music-picker-close" title="${i18n.t('common.close')}">&times;</button>
</div> </div>
<div class="music-picker-search"> <div class="music-picker-search">
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
<input type="text" id="music-picker-query" <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>
<div class="music-picker-list" id="music-picker-list"> <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>
<div class="music-picker-footer"> <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"> <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('common.cancel')}</button>
<button class="btn btn-primary music-picker-add" id="music-picker-add-btn" disabled> <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> </button>
</div> </div>
</div> </div>
@@ -770,7 +755,7 @@ const musicView = {
const AUDIO_EXTENSIONS = 'mp3,ogg,flac,wav,aac,m4a,wma,opus,webm'; const AUDIO_EXTENSIONS = 'mp3,ogg,flac,wav,aac,m4a,wma,opus,webm';
const fetchAudioFiles = async (query = '') => { 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 { try {
const params = new URLSearchParams({ type_filter: AUDIO_EXTENSIONS, limit: '200', recursive: 'true' }); const params = new URLSearchParams({ type_filter: AUDIO_EXTENSIONS, limit: '200', recursive: 'true' });
if (query.trim()) params.set('query', query.trim()); if (query.trim()) params.set('query', query.trim());
@@ -780,13 +765,13 @@ const musicView = {
renderFiles(data.files || []); renderFiles(data.files || []);
} catch (err) { } catch (err) {
console.error('Audio search error:', 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) => { const renderFiles = (files) => {
if (files.length === 0) { 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; return;
} }
listEl.innerHTML = ''; listEl.innerHTML = '';
@@ -809,7 +794,7 @@ const musicView = {
selectedIds.delete(file.id); selectedIds.delete(file.id);
row.classList.remove('selected'); 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; addBtn.disabled = selectedIds.size === 0;
}); });
listEl.appendChild(row); listEl.appendChild(row);
@@ -827,7 +812,7 @@ const musicView = {
addBtn.addEventListener('click', async () => { addBtn.addEventListener('click', async () => {
if (selectedIds.size === 0) return; if (selectedIds.size === 0) return;
addBtn.disabled = true; 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 { try {
const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/tracks`, { const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/tracks`, {
@@ -842,8 +827,8 @@ const musicView = {
notifications.addNotification({ notifications.addNotification({
icon: 'fa-check-circle', icon: 'fa-check-circle',
iconClass: 'upload', iconClass: 'upload',
title: t('music.add_tracks', 'Add Tracks'), title: i18n.t('music.add_tracks'),
text: `${selectedIds.size} ${t('music.added_to_playlist', 'added to playlist')}` text: `${selectedIds.size} ${i18n.t('music.added_to_playlist')}`
}); });
} }
close(); close();
@@ -854,7 +839,7 @@ const musicView = {
this.currentPlaylist.track_count = playlist.track_count; this.currentPlaylist.track_count = playlist.track_count;
this._renderPlaylistList(); this._renderPlaylistList();
const metaEl = document.getElementById('music-playlist-meta'); 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) { } catch (err) {
console.error('Add tracks error:', err); console.error('Add tracks error:', err);
@@ -862,12 +847,12 @@ const musicView = {
notifications.addNotification({ notifications.addNotification({
icon: 'fa-exclamation-circle', icon: 'fa-exclamation-circle',
iconClass: 'error', iconClass: 'error',
title: t('music.error', 'Error'), title: i18n.t('music.error'),
text: t('music.add_error', 'Could not add tracks to playlist') text: i18n.t('music.add_error')
}); });
} }
addBtn.disabled = false; 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')}`;
} }
}); });
@@ -878,7 +863,6 @@ const musicView = {
async _removeTrackFromPlaylist(_trackId, fileId) { async _removeTrackFromPlaylist(_trackId, fileId) {
if (!this.currentPlaylist) return; if (!this.currentPlaylist) return;
const t = (key) => i18n.t(key);
try { try {
const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/tracks/${encodeURIComponent(fileId)}`, { const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/tracks/${encodeURIComponent(fileId)}`, {
@@ -892,8 +876,8 @@ const musicView = {
notifications.addNotification({ notifications.addNotification({
icon: 'fa-check-circle', icon: 'fa-check-circle',
iconClass: 'upload', iconClass: 'upload',
title: t('music.remove', 'Remove'), title: i18n.t('music.remove'),
text: t('music.track_removed', 'Track removed') text: i18n.t('music.track_removed')
}); });
} }
await this._loadPlaylistTracks(this.currentPlaylist.id); await this._loadPlaylistTracks(this.currentPlaylist.id);
@@ -903,7 +887,7 @@ const musicView = {
this.currentPlaylist.track_count = playlist.track_count; this.currentPlaylist.track_count = playlist.track_count;
this._renderPlaylistList(); this._renderPlaylistList();
const metaEl = document.getElementById('music-playlist-meta'); 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) { } catch (err) {
console.error('Remove track error:', err); console.error('Remove track error:', err);
@@ -911,7 +895,7 @@ const musicView = {
notifications.addNotification({ notifications.addNotification({
icon: 'fa-exclamation-circle', icon: 'fa-exclamation-circle',
iconClass: 'error', iconClass: 'error',
title: t('music.error', 'Error'), title: i18n.t('music.error'),
text: err.message text: err.message
}); });
} }
@@ -920,7 +904,6 @@ const musicView = {
async _reorderTrack(fromIdx, toIdx) { async _reorderTrack(fromIdx, toIdx) {
if (!this.currentPlaylist) return; if (!this.currentPlaylist) return;
const t = (key) => i18n.t(key);
const tracks = [...this.currentTracks]; const tracks = [...this.currentTracks];
const [moved] = tracks.splice(fromIdx, 1); const [moved] = tracks.splice(fromIdx, 1);
@@ -943,7 +926,7 @@ const musicView = {
notifications.addNotification({ notifications.addNotification({
icon: 'fa-exclamation-circle', icon: 'fa-exclamation-circle',
iconClass: 'error', iconClass: 'error',
title: t('music.error', 'Error'), title: i18n.t('music.error'),
text: err.message text: err.message
}); });
} }
@@ -953,7 +936,6 @@ const musicView = {
async _showManageSharesDialog() { async _showManageSharesDialog() {
if (!this.currentPlaylist) return; if (!this.currentPlaylist) return;
const t = (key) => i18n.t(key);
const existing = document.getElementById('music-shares-dialog'); const existing = document.getElementById('music-shares-dialog');
if (existing) existing.remove(); if (existing) existing.remove();
@@ -964,19 +946,19 @@ const musicView = {
dialog.innerHTML = ` dialog.innerHTML = `
<div class="music-shares-panel"> <div class="music-shares-panel">
<div class="music-shares-header"> <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> <button class="music-shares-close-btn"><i class="fas fa-times"></i></button>
</div> </div>
<div class="music-shares-body"> <div class="music-shares-body">
<div class="music-shares-loading"><i class="fas fa-spinner fa-spin"></i></div> <div class="music-shares-loading"><i class="fas fa-spinner fa-spin"></i></div>
</div> </div>
<div class="music-shares-add"> <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"> <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> </label>
<button class="btn btn-primary btn-sm" id="music-share-add-btn"> <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> </button>
</div> </div>
</div> </div>
@@ -1009,8 +991,8 @@ const musicView = {
notifications.addNotification({ notifications.addNotification({
icon: 'fa-check-circle', icon: 'fa-check-circle',
iconClass: 'upload', iconClass: 'upload',
title: t('music.share', 'Share'), title: i18n.t('music.share'),
text: t('music.added', 'Added!') text: i18n.t('music.added')
}); });
} }
} catch (err) { } catch (err) {
@@ -1018,7 +1000,7 @@ const musicView = {
notifications.addNotification({ notifications.addNotification({
icon: 'fa-exclamation-circle', icon: 'fa-exclamation-circle',
iconClass: 'error', iconClass: 'error',
title: t('music.error', 'Error'), title: i18n.t('music.error'),
text: err.message text: err.message
}); });
} }
@@ -1030,7 +1012,6 @@ const musicView = {
async _loadSharesList(dialog) { async _loadSharesList(dialog) {
if (!this.currentPlaylist) return; if (!this.currentPlaylist) return;
const t = (key) => i18n.t(key);
const body = dialog.querySelector('.music-shares-body'); const body = dialog.querySelector('.music-shares-body');
if (!body) return; if (!body) return;
@@ -1045,7 +1026,7 @@ const musicView = {
const shares = await resp.json(); const shares = await resp.json();
if (shares.length === 0) { 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; return;
} }
@@ -1054,8 +1035,8 @@ const musicView = {
(s) => ` (s) => `
<div class="music-share-item" data-user-id="${this._escapeHtml(s.user_id)}"> <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-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> <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="${t('music.remove_share', 'Remove share')}"><i class="fas fa-times"></i></button> <button class="music-share-remove-btn" title="${i18n.t('music.remove_share')}"><i class="fas fa-times"></i></button>
</div> </div>
` `
) )
@@ -1075,7 +1056,6 @@ const musicView = {
async _removeShare(userId, dialog) { async _removeShare(userId, dialog) {
if (!this.currentPlaylist) return; if (!this.currentPlaylist) return;
const t = (key) => i18n.t(key);
try { try {
const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/share/${encodeURIComponent(userId)}`, { const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/share/${encodeURIComponent(userId)}`, {
@@ -1090,7 +1070,7 @@ const musicView = {
notifications.addNotification({ notifications.addNotification({
icon: 'fa-exclamation-circle', icon: 'fa-exclamation-circle',
iconClass: 'error', iconClass: 'error',
title: t('music.error', 'Error'), title: i18n.t('music.error'),
text: err.message text: err.message
}); });
} }
@@ -1099,7 +1079,6 @@ const musicView = {
async _togglePublic() { async _togglePublic() {
if (!this.currentPlaylist) return; if (!this.currentPlaylist) return;
const t = (key) => i18n.t(key);
const newValue = !this.currentPlaylist.is_public; const newValue = !this.currentPlaylist.is_public;
try { try {
@@ -1120,16 +1099,16 @@ const musicView = {
const btn = document.getElementById('music-toggle-public-btn'); const btn = document.getElementById('music-toggle-public-btn');
if (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); btn.classList.toggle('active', newValue);
} }
if (notifications) { 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({ notifications.addNotification({
icon: 'fa-check-circle', icon: 'fa-check-circle',
iconClass: 'upload', iconClass: 'upload',
title: t('music.toggle_public', 'Visibility'), title: i18n.t('music.toggle_public'),
text: status text: status
}); });
} }
@@ -1139,7 +1118,7 @@ const musicView = {
notifications.addNotification({ notifications.addNotification({
icon: 'fa-exclamation-circle', icon: 'fa-exclamation-circle',
iconClass: 'error', iconClass: 'error',
title: t('music.error', 'Error'), title: i18n.t('music.error'),
text: err.message text: err.message
}); });
} }
@@ -1148,7 +1127,6 @@ const musicView = {
async _showCoverPicker() { async _showCoverPicker() {
if (!this.currentPlaylist) return; if (!this.currentPlaylist) return;
const t = (key) => i18n.t(key);
const input = document.createElement('input'); const input = document.createElement('input');
input.type = 'file'; input.type = 'file';
@@ -1198,8 +1176,8 @@ const musicView = {
notifications.addNotification({ notifications.addNotification({
icon: 'fa-check-circle', icon: 'fa-check-circle',
iconClass: 'upload', iconClass: 'upload',
title: t('music.set_cover', 'Set cover'), title: i18n.t('music.set_cover'),
text: t('music.cover_updated', 'Cover updated') text: i18n.t('music.cover_updated')
}); });
} }
} catch (err) { } catch (err) {
@@ -1208,7 +1186,7 @@ const musicView = {
notifications.addNotification({ notifications.addNotification({
icon: 'fa-exclamation-circle', icon: 'fa-exclamation-circle',
iconClass: 'error', iconClass: 'error',
title: t('music.error', 'Error'), title: i18n.t('music.error'),
text: err.message text: err.message
}); });
} }
@@ -1626,14 +1604,13 @@ const musicPlayer = {
console.error('Audio error:', e); console.error('Audio error:', e);
this.isPlaying = false; this.isPlaying = false;
this._updateUI(); this._updateUI();
const t = (key) => i18n.t(key);
if (notifications) { 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({ notifications.addNotification({
icon: 'fa-exclamation-circle', icon: 'fa-exclamation-circle',
iconClass: 'error', iconClass: 'error',
title: t('music.error', 'Error'), title: i18n.t('music.error'),
text: `${t('music.playback_error', 'Playback failed')}: ${trackName}` text: `${i18n.t('music.playback_error')}: ${trackName}`
}); });
} }
}, },
@@ -1657,10 +1634,9 @@ const musicPlayer = {
} }
if (trackName) { if (trackName) {
const t = (key) => i18n.t(key);
trackName.textContent = this.currentTrack trackName.textContent = this.currentTrack
? this.currentTrack.title || this.currentTrack.file_name || t('music.unknown_title', 'Unknown') ? this.currentTrack.title || this.currentTrack.file_name || i18n.t('music.unknown_title')
: t('music.not_playing', 'Not playing'); : i18n.t('music.not_playing');
} }
if (trackArtist) { if (trackArtist) {
@@ -1710,13 +1686,12 @@ const musicPlayer = {
const queueList = document.getElementById('player-queue-list'); const queueList = document.getElementById('player-queue-list');
if (!queueList) return; if (!queueList) return;
const t = (key) => i18n.t(key);
if (this.queue.length === 0) { if (this.queue.length === 0) {
queueList.innerHTML = ` queueList.innerHTML = `
<div class="player-queue-empty"> <div class="player-queue-empty">
<i class="fas fa-music"></i> <i class="fas fa-music"></i>
<p>${t('music.queue_empty', 'Queue is empty')}</p> <p>${i18n.t('music.queue_empty')}</p>
</div> </div>
`; `;
return; return;
@@ -1728,8 +1703,8 @@ const musicPlayer = {
<div class="player-queue-item ${idx === this.currentIndex ? 'active' : ''}" data-idx="${idx}"> <div class="player-queue-item ${idx === this.currentIndex ? 'active' : ''}" data-idx="${idx}">
<span class="queue-item-num">${idx + 1}</span> <span class="queue-item-num">${idx + 1}</span>
<span class="queue-item-info"> <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-name">${this._escapeHtml(track.title || track.file_name || i18n.t('music.unknown_title'))}</span>
<span class="queue-item-artist">${this._escapeHtml(track.artist || t('music.unknown_artist', 'Unknown Artist'))}</span> <span class="queue-item-artist">${this._escapeHtml(track.artist || i18n.t('music.unknown_artist'))}</span>
</span> </span>
<span class="queue-item-duration">${this._formatDuration(track.duration_secs)}</span> <span class="queue-item-duration">${this._formatDuration(track.duration_secs)}</span>
<button class="queue-item-remove" data-idx="${idx}"> <button class="queue-item-remove" data-idx="${idx}">
+6 -9
View File
@@ -410,11 +410,10 @@ const photosView = {
/** Render the group mode toolbar */ /** Render the group mode toolbar */
_renderToolbar() { _renderToolbar() {
const t = (k) => i18n.t(k);
const modes = [ const modes = [
['daily', t('photos.view_daily', 'Day')], ['daily', i18n.t('photos.view_daily')],
['monthly', t('photos.view_monthly', 'Month')], ['monthly', i18n.t('photos.view_monthly')],
['yearly', t('photos.view_yearly', 'Year')] ['yearly', i18n.t('photos.view_yearly')]
]; ];
let html = '<div class="photos-toolbar"><div class="view-toggle">'; let html = '<div class="photos-toolbar"><div class="view-toggle">';
for (const [mode, label] of modes) { for (const [mode, label] of modes) {
@@ -427,12 +426,11 @@ const photosView = {
/** Render empty state */ /** Render empty state */
_renderEmpty() { _renderEmpty() {
const t = (k) => i18n.t(k);
this._container.innerHTML = ` this._container.innerHTML = `
<div class="photos-empty"> <div class="photos-empty">
<i class="fas fa-images"></i> <i class="fas fa-images"></i>
<p class="photos-empty-title">${t('photos.empty_state', 'No photos yet')}</p> <p class="photos-empty-title">${i18n.t('photos.empty_state')}</p>
<p>${t('photos.empty_hint', 'Upload images or videos to see them here')}</p> <p>${i18n.t('photos.empty_hint')}</p>
</div>`; </div>`;
}, },
@@ -520,10 +518,9 @@ const photosView = {
document.body.appendChild(bar); document.body.appendChild(bar);
} }
const t = (k) => i18n.t(k);
const count = this.selected.size; const count = this.selected.size;
bar.innerHTML = ` 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-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-delete" title="Delete"><i class="fas fa-trash"></i></button>
<button id="photos-sel-clear" title="Clear"><i class="fas fa-times"></i></button> <button id="photos-sel-clear" title="Clear"><i class="fas fa-times"></i></button>
+74 -79
View File
@@ -8,11 +8,6 @@ let usersPage = 0;
const PAGE_SIZE = 50; const PAGE_SIZE = 50;
let totalUsers = 0; let totalUsers = 0;
/* ── i18n helper ── */
function t(key, params) {
return i18n.t(key, params);
}
/** Escape a string for safe embedding inside a JS string literal within an HTML attribute. */ /** Escape a string for safe embedding inside a JS string literal within an HTML attribute. */
function _escJs(s) { function _escJs(s) {
if (typeof s !== 'string') return ''; if (typeof s !== 'string') return '';
@@ -52,14 +47,14 @@ function formatBytes(bytes) {
} }
function timeAgo(dateStr) { function timeAgo(dateStr) {
if (!dateStr) return t('admin.never'); if (!dateStr) return i18n.t('admin.never');
const d = new Date(dateStr); const d = new Date(dateStr);
const now = new Date(); const now = new Date();
const secs = Math.floor((now - d) / 1000); const secs = Math.floor((now - d) / 1000);
if (secs < 60) return t('admin.just_now'); if (secs < 60) return i18n.t('admin.just_now');
if (secs < 3600) return t('admin.minutes_ago', { n: Math.floor(secs / 60) }); if (secs < 3600) return i18n.t('admin.minutes_ago', { n: Math.floor(secs / 60) });
if (secs < 86400) return t('admin.hours_ago', { n: Math.floor(secs / 3600) }); if (secs < 86400) return i18n.t('admin.hours_ago', { n: Math.floor(secs / 3600) });
if (secs < 2592000) return t('admin.days_ago', { n: Math.floor(secs / 86400) }); if (secs < 2592000) return i18n.t('admin.days_ago', { n: Math.floor(secs / 86400) });
return d.toLocaleDateString(); return d.toLocaleDateString();
} }
@@ -157,9 +152,9 @@ async function loadDashboard() {
const bar = document.getElementById('ds-bar'); const bar = document.getElementById('ds-bar');
bar.style.width = `${Math.min(d.storage_usage_percent, 100)}%`; 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'}`; 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-auth').textContent = d.auth_enabled ? i18n.t('admin.enabled') : i18n.t('admin.disabled');
document.getElementById('ds-oidc').textContent = d.oidc_configured ? t('admin.active') : t('admin.off'); 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 ? t('admin.enabled') : t('admin.disabled'); document.getElementById('ds-quotas-flag').textContent = d.quotas_enabled ? i18n.t('admin.enabled') : i18n.t('admin.disabled');
if (typeof d.registration_enabled !== 'undefined') { if (typeof d.registration_enabled !== 'undefined') {
document.getElementById('ds-registration').checked = d.registration_enabled; document.getElementById('ds-registration').checked = d.registration_enabled;
@@ -182,7 +177,7 @@ async function loadDashboard() {
async function loadUsers() { async function loadUsers() {
const tbody = document.getElementById('users-tbody'); 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 { try {
const resp = await fetch(`${API}/admin/users?limit=${PAGE_SIZE}&offset=${usersPage * PAGE_SIZE}`, { const resp = await fetch(`${API}/admin/users?limit=${PAGE_SIZE}&offset=${usersPage * PAGE_SIZE}`, {
headers: headers(), headers: headers(),
@@ -191,7 +186,7 @@ async function loadUsers() {
if (!resp.ok) { if (!resp.ok) {
tbody.innerHTML = tbody.innerHTML =
'<tr><td colspan="7" class="table-status-error"><i class="fas fa-exclamation-circle"></i> ' + '<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>'; '</td></tr>';
return; return;
} }
@@ -199,7 +194,7 @@ async function loadUsers() {
totalUsers = data.total; totalUsers = data.total;
const users = data.users; const users = data.users;
if (users.length === 0) { 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; return;
} }
@@ -219,12 +214,12 @@ async function loadUsers() {
'"><i class="fas fa-key badge-admin-icon-small"></i> ' + '"><i class="fas fa-key badge-admin-icon-small"></i> ' +
escapeHtml(u.auth_provider) + escapeHtml(u.auth_provider) +
'</span>' '</span>'
: `<span class="badge badge-local">${escapeHtml(t('admin.local'))}</span>`; : `<span class="badge badge-local">${escapeHtml(i18n.t('admin.local'))}</span>`;
return ( return (
'<tr>' + '<tr>' +
'<td><div class="user-info"><span class="user-name">' + '<td><div class="user-info"><span class="user-name">' +
escapeHtml(u.username) + 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">' + '</span><span class="user-email">' +
escapeHtml(u.email) + escapeHtml(u.email) +
'</span></div></td>' + '</span></div></td>' +
@@ -240,7 +235,7 @@ async function loadUsers() {
'<td><span class="badge badge-' + '<td><span class="badge badge-' +
(u.active ? 'active' : 'inactive') + (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>' + '</span></td>' +
'<td><div class="quota-bar"><div class="progress-bar quota-progress-fixed"><div class="progress-fill ' + '<td><div class="quota-bar"><div class="progress-bar quota-progress-fixed"><div class="progress-fill ' +
quotaColor + quotaColor +
@@ -260,7 +255,7 @@ async function loadUsers() {
'" data-quota="' + '" data-quota="' +
u.storage_quota_bytes + u.storage_quota_bytes +
'" title="' + '" title="' +
escapeHtml(t('admin.edit_quota_title')) + escapeHtml(i18n.t('admin.edit_quota_title')) +
'"><i class="fas fa-box"></i></button>' + '"><i class="fas fa-box"></i></button>' +
(isOidc (isOidc
? '' ? ''
@@ -269,14 +264,14 @@ async function loadUsers() {
'" data-uname="' + '" data-uname="' +
_escJs(u.username) + _escJs(u.username) +
'" title="' + '" title="' +
escapeHtml(t('admin.reset_password_title')) + escapeHtml(i18n.t('admin.reset_password_title')) +
'"><i class="fas fa-key"></i></button>') + '"><i class="fas fa-key"></i></button>') +
'<button class="btn btn-sm btn-secondary admin-action-btn" data-action="toggle-role" data-uid="' + '<button class="btn btn-sm btn-secondary admin-action-btn" data-action="toggle-role" data-uid="' +
_escJs(u.id) + _escJs(u.id) +
'" data-role="' + '" data-role="' +
_escJs(u.role) + _escJs(u.role) +
'" title="' + '" title="' +
escapeHtml(t('admin.toggle_role_title')) + escapeHtml(i18n.t('admin.toggle_role_title')) +
'"' + '"' +
(isSelf ? ' disabled' : '') + (isSelf ? ' disabled' : '') +
'><i class="fas fa-' + '><i class="fas fa-' +
@@ -289,7 +284,7 @@ async function loadUsers() {
'" data-active="' + '" data-active="' +
u.active + u.active +
'" title="' + '" 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' : '') + (isSelf && u.active ? ' disabled' : '') +
'><i class="fas fa-' + '><i class="fas fa-' +
@@ -300,7 +295,7 @@ async function loadUsers() {
'" data-uname="' + '" data-uname="' +
_escJs(u.username) + _escJs(u.username) +
'" title="' + '" title="' +
escapeHtml(t('admin.delete_title')) + escapeHtml(i18n.t('admin.delete_title')) +
'"' + '"' +
(isSelf ? ' disabled' : '') + (isSelf ? ' disabled' : '') +
'><i class="fas fa-trash-alt"></i></button>' + '><i class="fas fa-trash-alt"></i></button>' +
@@ -329,13 +324,13 @@ async function loadUsers() {
const from = usersPage * PAGE_SIZE + 1; const from = usersPage * PAGE_SIZE + 1;
const to = Math.min((usersPage + 1) * PAGE_SIZE, totalUsers); 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('prev-btn').disabled = usersPage === 0;
document.getElementById('next-btn').disabled = (usersPage + 1) * PAGE_SIZE >= totalUsers; document.getElementById('next-btn').disabled = (usersPage + 1) * PAGE_SIZE >= totalUsers;
} catch (e) { } catch (e) {
tbody.innerHTML = tbody.innerHTML =
'<tr><td colspan="7" class="table-status-error"><i class="fas fa-exclamation-circle"></i> ' + '<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>'; '</td></tr>';
} }
} }
@@ -355,7 +350,7 @@ function nextPage() {
async function toggleRole(userId, currentRole) { async function toggleRole(userId, currentRole) {
const newRole = currentRole === 'admin' ? 'user' : 'admin'; 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; if (!ok) return;
try { try {
const resp = await fetch(`${API}/admin/users/${userId}/role`, { const resp = await fetch(`${API}/admin/users/${userId}/role`, {
@@ -367,15 +362,15 @@ async function toggleRole(userId, currentRole) {
if (resp.ok) loadUsers(); if (resp.ok) loadUsers();
else { else {
const e = await resp.json(); const e = await resp.json();
alert(e.message || t('admin.error_generic')); alert(e.message || i18n.t('admin.error_generic'));
} }
} catch (e) { } catch (e) {
alert(t('admin.error_network', { message: e.message })); alert(i18n.t('admin.error_network', { message: e.message }));
} }
} }
async function toggleActive(userId, currentActive) { 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); const ok = await showConfirm(msg);
if (!ok) return; if (!ok) return;
try { try {
@@ -388,15 +383,15 @@ async function toggleActive(userId, currentActive) {
if (resp.ok) loadUsers(); if (resp.ok) loadUsers();
else { else {
const e = await resp.json(); const e = await resp.json();
alert(e.message || t('admin.error_generic')); alert(e.message || i18n.t('admin.error_generic'));
} }
} catch (e) { } catch (e) {
alert(t('admin.error_network', { message: e.message })); alert(i18n.t('admin.error_network', { message: e.message }));
} }
} }
async function deleteUser(userId, username) { 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; if (!ok) return;
try { try {
const resp = await fetch(`${API}/admin/users/${userId}`, { const resp = await fetch(`${API}/admin/users/${userId}`, {
@@ -409,10 +404,10 @@ async function deleteUser(userId, username) {
loadDashboard(); loadDashboard();
} else { } else {
const e = await resp.json(); const e = await resp.json();
alert(e.message || t('admin.error_generic')); alert(e.message || i18n.t('admin.error_generic'));
} }
} catch (e) { } catch (e) {
alert(t('admin.error_network', { message: e.message })); alert(i18n.t('admin.error_network', { message: e.message }));
} }
} }
@@ -446,10 +441,10 @@ async function saveQuota() {
loadDashboard(); loadDashboard();
} else { } else {
const e = await resp.json(); const e = await resp.json();
alert(e.message || t('admin.error_generic')); alert(e.message || i18n.t('admin.error_generic'));
} }
} catch (e) { } catch (e) {
alert(t('admin.error_network', { message: e.message })); alert(i18n.t('admin.error_network', { message: e.message }));
} }
} }
@@ -480,19 +475,19 @@ async function submitCreateUser() {
const errorEl = document.getElementById('cu-error'); const errorEl = document.getElementById('cu-error');
if (username.length < 3) { if (username.length < 3) {
errorEl.textContent = t('admin.error_username_short'); errorEl.textContent = i18n.t('admin.error_username_short');
errorEl.className = 'alert alert-error'; errorEl.className = 'alert alert-error';
return; return;
} }
if (password.length < 8) { if (password.length < 8) {
errorEl.textContent = t('admin.error_password_short'); errorEl.textContent = i18n.t('admin.error_password_short');
errorEl.className = 'alert alert-error'; errorEl.className = 'alert alert-error';
return; return;
} }
const btn = document.getElementById('cu-submit'); const btn = document.getElementById('cu-submit');
btn.disabled = true; 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 { try {
const resp = await fetch(`${API}/admin/users`, { const resp = await fetch(`${API}/admin/users`, {
method: 'POST', method: 'POST',
@@ -512,15 +507,15 @@ async function submitCreateUser() {
loadDashboard(); loadDashboard();
} else { } else {
const e = await resp.json().catch(() => ({})); 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'; errorEl.className = 'alert alert-error';
} }
} catch (e) { } 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'; errorEl.className = 'alert alert-error';
} }
btn.disabled = false; 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 = ''; let resetPwUserId = '';
@@ -541,14 +536,14 @@ async function submitResetPassword() {
const password = document.getElementById('rp-password').value; const password = document.getElementById('rp-password').value;
const errorEl = document.getElementById('rp-error'); const errorEl = document.getElementById('rp-error');
if (password.length < 8) { if (password.length < 8) {
errorEl.textContent = t('admin.error_password_short'); errorEl.textContent = i18n.t('admin.error_password_short');
errorEl.className = 'alert alert-error'; errorEl.className = 'alert alert-error';
return; return;
} }
const btn = document.getElementById('rp-submit'); const btn = document.getElementById('rp-submit');
btn.disabled = true; 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 { try {
const resp = await fetch(`${API}/admin/users/${resetPwUserId}/password`, { const resp = await fetch(`${API}/admin/users/${resetPwUserId}/password`, {
method: 'PUT', method: 'PUT',
@@ -560,15 +555,15 @@ async function submitResetPassword() {
closeResetPasswordModal(); closeResetPasswordModal();
} else { } else {
const e = await resp.json().catch(() => ({})); 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'; errorEl.className = 'alert alert-error';
} }
} catch (e) { } 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'; errorEl.className = 'alert alert-error';
} }
btn.disabled = false; 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) { async function toggleRegistration(enabled) {
@@ -586,13 +581,13 @@ async function toggleRegistration(enabled) {
if (!enabled) showElement('registration-warning', 'flex'); if (!enabled) showElement('registration-warning', 'flex');
else hideElement('registration-warning'); else hideElement('registration-warning');
const e = await resp.json().catch(() => ({})); const e = await resp.json().catch(() => ({}));
alert(e.message || t('admin.error_generic')); alert(e.message || i18n.t('admin.error_generic'));
} }
} catch (e) { } catch (e) {
document.getElementById('ds-registration').checked = !enabled; document.getElementById('ds-registration').checked = !enabled;
if (!enabled) showElement('registration-warning', 'flex'); if (!enabled) showElement('registration-warning', 'flex');
else hideElement('registration-warning'); else hideElement('registration-warning');
alert(t('admin.error_network', { message: e.message })); alert(i18n.t('admin.error_network', { message: e.message }));
} }
} }
@@ -624,7 +619,7 @@ async function testConnection() {
} }
const btn = document.getElementById('discover-btn'); const btn = document.getElementById('discover-btn');
btn.disabled = true; 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'); const resultDiv = document.getElementById('discovery-result');
try { try {
const resp = await fetch(`${API}/admin/settings/oidc/test`, { const resp = await fetch(`${API}/admin/settings/oidc/test`, {
@@ -652,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>`; resultDiv.innerHTML = `<div class="discovery-result fail"><i class="fas fa-times-circle"></i> Error: ${escapeHtml(e.message)}</div>`;
} }
btn.disabled = false; 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() { async function saveOidcSettings() {
const btn = document.getElementById('save-btn'); const btn = document.getElementById('save-btn');
btn.disabled = true; 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 = { const body = {
enabled: document.getElementById('oidc-enabled').checked, enabled: document.getElementById('oidc-enabled').checked,
issuer_url: document.getElementById('issuer-url').value.trim(), issuer_url: document.getElementById('issuer-url').value.trim(),
@@ -678,18 +673,18 @@ async function saveOidcSettings() {
body: JSON.stringify(body) body: JSON.stringify(body)
}); });
if (resp.ok) { if (resp.ok) {
const status = body.enabled ? t('admin.active').toLowerCase() : t('admin.disabled').toLowerCase(); const status = body.enabled ? i18n.t('admin.active').toLowerCase() : i18n.t('admin.disabled').toLowerCase();
showOidcStatus(t('admin.settings_saved', { status: status }), 'success'); showOidcStatus(i18n.t('admin.settings_saved', { status: status }), 'success');
loadDashboard(); loadDashboard();
} else { } else {
const e = await resp.json().catch(() => ({})); const e = await resp.json().catch(() => ({}));
showOidcStatus(`Error: ${e.message || resp.statusText}`, 'error'); showOidcStatus(`Error: ${e.message || resp.statusText}`, 'error');
} }
} catch (e) { } 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.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 ── */ /* ── Storage tab ── */
@@ -750,7 +745,7 @@ async function loadStorage() {
// Secret hints // Secret hints
if (s.s3_access_key_set) { 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) { if (s.s3_secret_key_set) {
showElement('storage-secret-hint'); showElement('storage-secret-hint');
@@ -770,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-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` : '—'; document.getElementById('storage-dedup-ratio').textContent = s.dedup_ratio != null ? `${s.dedup_ratio.toFixed(2)}x` : '—';
} catch (e) { } 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 // Also load migration status
@@ -780,7 +775,7 @@ async function loadStorage() {
async function saveStorageSettings() { async function saveStorageSettings() {
const btn = document.getElementById('btn-save-storage'); const btn = document.getElementById('btn-save-storage');
btn.disabled = true; 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 backend = document.querySelector('input[name="storage-backend"]:checked').value;
const body = { const body = {
@@ -801,23 +796,23 @@ async function saveStorageSettings() {
body: JSON.stringify(body) body: JSON.stringify(body)
}); });
if (resp.ok) { 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(); loadStorage();
} else { } else {
const e = await resp.json().catch(() => ({})); const e = await resp.json().catch(() => ({}));
showStorageStatus(`Error: ${e.message || resp.statusText}`, 'error'); showStorageStatus(`Error: ${e.message || resp.statusText}`, 'error');
} }
} catch (e) { } 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.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() { async function testStorageConnection() {
const btn = document.getElementById('btn-test-storage'); const btn = document.getElementById('btn-test-storage');
btn.disabled = true; 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 backend = document.querySelector('input[name="storage-backend"]:checked').value;
const body = { const body = {
@@ -839,17 +834,17 @@ async function testStorageConnection() {
}); });
const r = await resp.json(); const r = await resp.json();
if (r.connected) { 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`; if (r.available_bytes != null) msg += ` — ${formatBytes(r.available_bytes)} available`;
showStorageStatus(msg, 'success'); showStorageStatus(msg, 'success');
} else { } 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) { } 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.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 ── */ /* ── Migration ── */
@@ -951,14 +946,14 @@ async function startMigration() {
body: JSON.stringify({ concurrency: 4 }) body: JSON.stringify({ concurrency: 4 })
}); });
if (resp.ok) { if (resp.ok) {
showMigrationMsg(t('admin.migration_started') || 'Migration started', 'success'); showMigrationMsg(i18n.t('admin.migration_started') || 'Migration started', 'success');
loadMigrationStatus(); loadMigrationStatus();
} else { } else {
const e = await resp.json().catch(() => ({})); const e = await resp.json().catch(() => ({}));
showMigrationMsg(`Error: ${e.message || resp.statusText}`, 'error'); showMigrationMsg(`Error: ${e.message || resp.statusText}`, 'error');
} }
} catch (e) { } catch (e) {
showMigrationMsg(t('admin.error_network', { message: e.message }), 'error'); showMigrationMsg(i18n.t('admin.error_network', { message: e.message }), 'error');
} }
btn.disabled = false; btn.disabled = false;
} }
@@ -971,7 +966,7 @@ async function pauseMigration() {
credentials: 'same-origin' credentials: 'same-origin'
}); });
if (resp.ok) { if (resp.ok) {
showMigrationMsg(t('admin.migration_paused_msg') || 'Migration paused', 'success'); showMigrationMsg(i18n.t('admin.migration_paused_msg') || 'Migration paused', 'success');
loadMigrationStatus(); loadMigrationStatus();
} }
} catch (_e) { } catch (_e) {
@@ -987,7 +982,7 @@ async function resumeMigration() {
credentials: 'same-origin' credentials: 'same-origin'
}); });
if (resp.ok) { if (resp.ok) {
showMigrationMsg(t('admin.migration_resumed_msg') || 'Migration resumed', 'success'); showMigrationMsg(i18n.t('admin.migration_resumed_msg') || 'Migration resumed', 'success');
loadMigrationStatus(); loadMigrationStatus();
} }
} catch (_e) { } catch (_e) {
@@ -998,7 +993,7 @@ async function resumeMigration() {
async function verifyMigration() { async function verifyMigration() {
const btn = document.getElementById('btn-verify-migration'); const btn = document.getElementById('btn-verify-migration');
btn.disabled = true; 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'); const resultDiv = document.getElementById('migration-verify-result');
try { try {
const resp = await fetch(`${API}/admin/storage/migration/verify`, { const resp = await fetch(`${API}/admin/storage/migration/verify`, {
@@ -1010,19 +1005,19 @@ async function verifyMigration() {
const r = await resp.json(); const r = await resp.json();
resultDiv.style.display = ''; resultDiv.style.display = '';
if (r.passed) { 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 { } else {
const issues = []; const issues = [];
if (r.missing_in_target.length) issues.push(`${r.missing_in_target.length} missing`); 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`); 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) { } catch (e) {
resultDiv.style.display = ''; resultDiv.style.display = '';
resultDiv.innerHTML = `<div class="discovery-result fail"><i class="fas fa-times-circle"></i> Error: ${escapeHtml(e.message)}</div>`; resultDiv.innerHTML = `<div class="discovery-result fail"><i class="fas fa-times-circle"></i> Error: ${escapeHtml(e.message)}</div>`;
} }
btn.disabled = false; 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() { async function completeMigration() {
@@ -1033,14 +1028,14 @@ async function completeMigration() {
credentials: 'same-origin' credentials: 'same-origin'
}); });
if (resp.ok) { 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(); loadMigrationStatus();
} else { } else {
const e = await resp.json().catch(() => ({})); const e = await resp.json().catch(() => ({}));
showMigrationMsg(`Error: ${e.message || resp.statusText}`, 'error'); showMigrationMsg(`Error: ${e.message || resp.statusText}`, 'error');
} }
} catch (e) { } catch (e) {
showMigrationMsg(t('admin.error_network', { message: e.message }), 'error'); showMigrationMsg(i18n.t('admin.error_network', { message: e.message }), 'error');
} }
} }
@@ -1104,7 +1099,7 @@ function showAccessDenied() {
/* ── Apply i18n when translations load / change ── */ /* ── Apply i18n when translations load / change ── */
document.addEventListener('translationsLoaded', () => { document.addEventListener('translationsLoaded', () => {
i18n.translatePage(); i18n.translatePage();
// Re-render dynamic content that uses t() // Re-render dynamic content that uses i18n.t()
loadDashboard(); loadDashboard();
if (activeTabName === 'users') loadUsers(); if (activeTabName === 'users') loadUsers();
}); });
+27 -32
View File
@@ -3,11 +3,6 @@ import { i18n } from '../../core/i18n.js';
const API = '/api'; const API = '/api';
/* ── i18n helper ── */
function t(key, params) {
return i18n.t(key, params);
}
function headers() { function headers() {
return { 'Content-Type': 'application/json', ...getCsrfHeaders() }; return { 'Content-Type': 'application/json', ...getCsrfHeaders() };
} }
@@ -21,14 +16,14 @@ function formatBytes(bytes) {
} }
function timeAgo(dateStr) { function timeAgo(dateStr) {
if (!dateStr) return t('profile.never'); if (!dateStr) return i18n.t('profile.never');
const d = new Date(dateStr); const d = new Date(dateStr);
const now = new Date(); const now = new Date();
const secs = Math.floor((now - d) / 1000); const secs = Math.floor((now - d) / 1000);
if (secs < 60) return t('profile.just_now'); if (secs < 60) return i18n.t('profile.just_now');
if (secs < 3600) return t('profile.minutes_ago', { n: Math.floor(secs / 60) }); if (secs < 3600) return i18n.t('profile.minutes_ago', { n: Math.floor(secs / 60) });
if (secs < 86400) return t('profile.hours_ago', { n: Math.floor(secs / 3600) }); if (secs < 86400) return i18n.t('profile.hours_ago', { n: Math.floor(secs / 3600) });
if (secs < 2592000) return t('profile.days_ago', { n: Math.floor(secs / 86400) }); if (secs < 2592000) return i18n.t('profile.days_ago', { n: Math.floor(secs / 86400) });
return d.toLocaleDateString(); return d.toLocaleDateString();
} }
@@ -52,15 +47,15 @@ async function init() {
const badge = document.getElementById('p-role-badge'); const badge = document.getElementById('p-role-badge');
if (user.role === 'admin') { if (user.role === 'admin') {
badge.className = 'role-badge role-badge-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 { } else {
badge.className = 'role-badge role-badge-user'; 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-username').textContent = user.username;
document.getElementById('p-detail-email').textContent = user.email || '—'; 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); document.getElementById('p-detail-login').textContent = timeAgo(user.last_login_at);
const used = user.storage_used_bytes || 0; const used = user.storage_used_bytes || 0;
@@ -74,7 +69,7 @@ async function init() {
const bar = document.getElementById('p-storage-bar'); const bar = document.getElementById('p-storage-bar');
bar.style.width = `${pct}%`; bar.style.width = `${pct}%`;
bar.className = `storage-fill ${pct > 90 ? 'red' : pct > 70 ? 'orange' : 'green'}`; 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') { if (user.auth_provider && user.auth_provider !== 'local') {
document.getElementById('password-section').classList.add('hidden'); document.getElementById('password-section').classList.add('hidden');
@@ -115,18 +110,18 @@ async function changePassword(e) {
const statusEl = document.getElementById('pw-status'); const statusEl = document.getElementById('pw-status');
if (newPw !== confirmPw) { 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; return false;
} }
if (newPw.length < 8) { 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; return false;
} }
const btn = document.getElementById('pw-submit'); const btn = document.getElementById('pw-submit');
btn.disabled = true; 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 { try {
const resp = await fetch(`${API}/auth/change-password`, { const resp = await fetch(`${API}/auth/change-password`, {
@@ -140,24 +135,24 @@ async function changePassword(e) {
}); });
if (resp.ok) { 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(); document.getElementById('password-form').reset();
} else { } else {
const err = await resp.json().catch(() => ({})); const err = await resp.json().catch(() => ({}));
statusEl.innerHTML = statusEl.innerHTML =
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + '<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>'; '</div>';
} }
} catch (err) { } catch (err) {
statusEl.innerHTML = statusEl.innerHTML =
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + '<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>'; '</div>';
} }
btn.disabled = false; 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; return false;
} }
@@ -176,15 +171,15 @@ function renderPwRow(pw) {
const created = document.createElement('td'); const created = document.createElement('td');
created.textContent = new Date(pw.created_at).toLocaleDateString(); created.textContent = new Date(pw.created_at).toLocaleDateString();
const lastUsed = document.createElement('td'); 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 status = document.createElement('td');
const badge = document.createElement('span'); const badge = document.createElement('span');
if (pw.active !== false) { if (pw.active !== false) {
badge.className = 'badge badge-active'; badge.className = 'badge badge-active';
badge.textContent = t('profile.active'); badge.textContent = i18n.t('profile.active');
} else { } else {
badge.className = 'badge badge-expired'; badge.className = 'badge badge-expired';
badge.textContent = t('profile.revoked'); badge.textContent = i18n.t('profile.revoked');
} }
status.appendChild(badge); status.appendChild(badge);
const actions = document.createElement('td'); const actions = document.createElement('td');
@@ -192,7 +187,7 @@ function renderPwRow(pw) {
const btn = document.createElement('button'); const btn = document.createElement('button');
btn.className = 'btn btn-danger-sm'; btn.className = 'btn btn-danger-sm';
btn.innerHTML = '<i class="fas fa-trash"></i>'; btn.innerHTML = '<i class="fas fa-trash"></i>';
btn.title = t('profile.revoke_title'); btn.title = i18n.t('profile.revoke_title');
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
revokeAppPassword(pw.id, pw.label); revokeAppPassword(pw.id, pw.label);
}); });
@@ -264,12 +259,12 @@ async function createAppPassword() {
const btn = document.getElementById('app-pw-generate'); const btn = document.getElementById('app-pw-generate');
if (!label) { 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; return;
} }
btn.disabled = true; 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 = ''; statusEl.innerHTML = '';
try { try {
@@ -283,7 +278,7 @@ async function createAppPassword() {
const err = await resp.json().catch(() => ({})); const err = await resp.json().catch(() => ({}));
statusEl.innerHTML = statusEl.innerHTML =
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + '<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>'; '</div>';
return; return;
} }
@@ -297,7 +292,7 @@ async function createAppPassword() {
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${err.message}</div>`; statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${err.message}</div>`;
} finally { } finally {
btn.disabled = false; 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'))}`;
} }
} }
@@ -313,7 +308,7 @@ function copyAppPassword() {
} }
async function revokeAppPassword(id, label) { 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 { try {
const resp = await fetch(`${API}/auth/app-passwords/${encodeURIComponent(id)}`, { const resp = await fetch(`${API}/auth/app-passwords/${encodeURIComponent(id)}`, {
method: 'DELETE', method: 'DELETE',
@@ -325,10 +320,10 @@ async function revokeAppPassword(id, label) {
loadAppPasswords(); loadAppPasswords();
} else { } else {
const err = await resp.json().catch(() => ({})); const err = await resp.json().catch(() => ({}));
alert(err.message || t('profile.error_revoke')); alert(err.message || i18n.t('profile.error_revoke'));
} }
} catch (err) { } catch (err) {
alert(t('profile.error_network', { message: err.message })); alert(i18n.t('profile.error_network', { message: err.message }));
} }
} }