diff --git a/frontend/js/app.js b/frontend/js/app.js index 679a308..3be469e 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -19,11 +19,41 @@ import * as admin from './views/admin.js'; import * as designer from './views/designer.js'; import * as playlists from './views/playlists.js'; import { applyBranding } from './branding.js'; +import { t } from './i18n.js'; const app = document.getElementById('app'); const sidebar = document.querySelector('.sidebar'); let currentView = null; +// Map nav-link data-view to its translation key. +const NAV_LABEL_KEYS = { + dashboard: 'nav.displays', + content: 'nav.content', + playlists: 'nav.playlists', + layouts: 'nav.layouts', + widgets: 'nav.widgets', + schedule: 'nav.schedule', + walls: 'nav.walls', + reports: 'nav.reports', + kiosk: 'nav.kiosk', + designer: 'nav.designer', + activity: 'nav.activity', + teams: 'nav.teams', + help: 'nav.help', + settings: 'nav.settings', + billing: 'nav.subscription', + admin: 'nav.admin', +}; + +function renderNavLabels() { + document.querySelectorAll('.nav-link').forEach((link) => { + const key = NAV_LABEL_KEYS[link.dataset.view]; + if (!key) return; + const span = link.querySelector('span'); + if (span) span.textContent = t(key); + }); +} + function isAuthenticated() { return !!localStorage.getItem('token'); } @@ -200,7 +230,7 @@ function updateSidebarUser() {
${user.name || user.email}
${user.role}
- +
@@ -61,24 +62,24 @@ export function render(container) { - YouTube + ${t('content.youtube')}
-

Embed a YouTube video on your displays.

- - - +

${t('content.youtube_desc')}

+ + +
- - + +
-

Loading...

+

${t('common.loading')}

`; @@ -114,12 +115,12 @@ export function render(container) { const name = document.getElementById('remoteNameInput').value.trim(); const mimeType = document.getElementById('remoteMimeType').value; if (!url) { - showToast('Enter a URL', 'error'); + showToast(t('content.error_enter_url'), 'error'); return; } try { await api.addRemoteContent(url, name, mimeType); - showToast('Remote content added', 'success'); + showToast(t('content.toast.remote_added'), 'success'); document.getElementById('remoteUrlInput').value = ''; document.getElementById('remoteNameInput').value = ''; loadContent(); @@ -133,12 +134,12 @@ export function render(container) { const url = document.getElementById('youtubeUrlInput').value.trim(); const name = document.getElementById('youtubeNameInput').value.trim(); if (!url) { - showToast('Enter a YouTube URL', 'error'); + showToast(t('content.error_enter_youtube_url'), 'error'); return; } try { await api.addYoutubeContent(url, name); - showToast('YouTube video added', 'success'); + showToast(t('content.toast.youtube_added'), 'success'); document.getElementById('youtubeUrlInput').value = ''; document.getElementById('youtubeNameInput').value = ''; loadContent(); @@ -163,11 +164,11 @@ export function render(container) { // Create folder in the current folder. document.getElementById('newFolderBtn').onclick = async () => { - const name = prompt('Folder name:'); + const name = prompt(t('content.prompt_folder_name')); if (!name || !name.trim()) return; try { await api.createFolder(name.trim(), state.currentFolderId); - showToast(`Folder "${name}" created`, 'success'); + showToast(t('content.toast.folder_created_named', { name }), 'success'); loadContent(); } catch (err) { showToast(err.message, 'error'); } }; @@ -190,16 +191,16 @@ async function handleFiles(files) { for (const file of files) { progress.style.display = 'block'; progressFill.style.width = '0%'; - progressText.textContent = `Uploading ${file.name}...`; + progressText.textContent = t('content.upload_progress_named', { name: file.name }); try { await api.uploadContent(file, (pct) => { progressFill.style.width = pct + '%'; - progressText.textContent = `Uploading ${file.name}... ${pct}%`; + progressText.textContent = t('content.upload_progress_named_pct', { name: file.name, pct }); }); - showToast(`${file.name} uploaded successfully`, 'success'); + showToast(t('content.toast.uploaded_named', { name: file.name }), 'success'); } catch (err) { - showToast(`Failed to upload ${file.name}: ${err.message}`, 'error'); + showToast(t('content.toast.upload_failed_named', { name: file.name, error: err.message }), 'error'); } } @@ -229,14 +230,14 @@ async function loadContent() { cursor = cursor.parent_id ? folderById.get(cursor.parent_id) : null; } breadcrumb.innerHTML = ` - All Content + ${t('content.breadcrumb_root')} ${path.map(f => ` / ${esc(f.name)} `).join('')} ${state.currentFolderId ? ` - - + + ` : ''} `; breadcrumb.querySelectorAll('[data-folder-nav]').forEach(a => { @@ -271,7 +272,7 @@ async function loadContent() { const targetFolderId = a.dataset.folderNav || null; // empty string = root try { await api.moveContent(contentId, targetFolderId); - showToast(targetFolderId ? 'Moved' : 'Moved to root', 'success'); + showToast(targetFolderId ? t('content.toast.moved') : t('content.toast.moved_to_root'), 'success'); loadContent(); } catch (err) { showToast(err.message, 'error'); } }); @@ -279,21 +280,21 @@ async function loadContent() { const renameBtn = breadcrumb.querySelector('#renameFolderBtn'); if (renameBtn) renameBtn.onclick = async () => { const current = folderById.get(state.currentFolderId); - const name = prompt('Rename folder:', current?.name || ''); + const name = prompt(t('content.prompt_rename_folder'), current?.name || ''); if (!name || !name.trim() || name === current?.name) return; try { await api.renameFolder(state.currentFolderId, name.trim()); - showToast('Folder renamed', 'success'); + showToast(t('content.toast.folder_renamed'), 'success'); loadContent(); } catch (err) { showToast(err.message, 'error'); } }; const deleteBtn = breadcrumb.querySelector('#deleteFolderBtn'); if (deleteBtn) deleteBtn.onclick = async () => { - if (!confirm('Delete this folder? Content inside moves back to the root level. Subfolders will also be deleted.')) return; + if (!confirm(t('content.confirm_delete_folder'))) return; try { const parentId = folderById.get(state.currentFolderId)?.parent_id || null; await api.deleteFolder(state.currentFolderId); - showToast('Folder deleted', 'success'); + showToast(t('content.toast.folder_deleted'), 'success'); state.currentFolderId = parentId; loadContent(); } catch (err) { showToast(err.message, 'error'); } @@ -326,7 +327,7 @@ async function loadContent() { if (!contentId) return; try { await api.moveContent(contentId, card.dataset.folderId); - showToast('Moved', 'success'); + showToast(t('content.toast.moved'), 'success'); loadContent(); } catch (err) { showToast(err.message, 'error'); } }); @@ -339,8 +340,8 @@ async function loadContent() { -

${state.currentFolderId ? 'This folder is empty' : 'No content yet'}

-

${state.currentFolderId ? 'Drag content here, or use the Move action.' : 'Upload videos and images to get started.'}

+

${state.currentFolderId ? t('content.empty_folder_title') : t('content.no_content')}

+

${state.currentFolderId ? t('content.empty_folder_desc') : t('content.no_content_desc')}

`; return; @@ -365,7 +366,7 @@ async function loadContent() { - Remote + ${t('content.type_remote_short')} ` : c.thumbnail_path ? `${esc(c.filename)}` @@ -381,26 +382,26 @@ async function loadContent() {
${esc(c.filename)}
- ${c.mime_type === 'video/youtube' ? 'YouTube' : c.remote_url ? 'Remote URL' : (c.mime_type?.startsWith('video/') ? 'Video' : 'Image')} + ${c.mime_type === 'video/youtube' ? t('content.type_youtube') : c.remote_url ? t('content.type_remote') : (c.mime_type?.startsWith('video/') ? t('content.type_video') : t('content.type_image'))} ${c.duration_sec ? ` · ${Math.floor(c.duration_sec / 60)}:${String(Math.floor(c.duration_sec % 60)).padStart(2, '0')}` : ''} ${c.file_size ? ' · ' + formatFileSize(c.file_size) : ''} ${c.width && c.height ? ` · ${c.width}x${c.height}` : ''}
- -
@@ -446,14 +447,14 @@ async function loadContent() { if (btn.dataset.confirming === 'true') { try { btn.disabled = true; - btn.textContent = 'Deleting...'; + btn.textContent = t('content.btn_deleting'); await api.deleteContent(id); - showToast('Content deleted', 'success'); + showToast(t('content.toast.deleted'), 'success'); loadContent(); } catch (err) { showToast(err.message, 'error'); btn.disabled = false; - btn.textContent = 'Delete'; + btn.textContent = t('content.btn_delete'); btn.dataset.confirming = 'false'; } return; @@ -461,14 +462,14 @@ async function loadContent() { // First click - show confirm state btn.dataset.confirming = 'true'; - btn.innerHTML = 'Confirm Delete?'; + btn.innerHTML = t('content.btn_confirm_delete'); btn.style.background = 'var(--danger)'; btn.style.color = 'white'; // Reset after 3 seconds if not clicked setTimeout(() => { if (btn.dataset.confirming === 'true') { btn.dataset.confirming = 'false'; - btn.innerHTML = ` Delete`; + btn.innerHTML = ` ${t('content.btn_delete')}`; btn.style.background = ''; btn.style.color = ''; } @@ -476,7 +477,7 @@ async function loadContent() { }; } catch (err) { - grid.innerHTML = `

Failed to load content

${esc(err.message)}

`; + grid.innerHTML = `

${t('content.failed_to_load')}

${esc(err.message)}

`; } } @@ -490,51 +491,51 @@ function showEditModal(contentItem, onSave) { overlay.innerHTML = ` `; @@ -582,10 +583,10 @@ function showEditModal(contentItem, onSave) { } overlay.remove(); - showToast('Content updated', 'success'); + showToast(t('content.toast.updated'), 'success'); if (onSave) onSave(); } catch (err) { - showToast(err.message || 'Update failed', 'error'); + showToast(err.message || t('content.error_update_failed'), 'error'); } }; } @@ -611,7 +612,7 @@ function showPreview(content) {
${esc(content.filename)}
-
${esc(content.mime_type)} ${content.remote_url ? '(Remote URL)' : ''}
+
${esc(content.mime_type)} ${content.remote_url ? `(${t('content.type_remote')})` : ''}
`; diff --git a/frontend/js/views/dashboard.js b/frontend/js/views/dashboard.js index 2d06248..3d36152 100644 --- a/frontend/js/views/dashboard.js +++ b/frontend/js/views/dashboard.js @@ -2,28 +2,38 @@ import { api } from '../api.js'; import { on, off, requestScreenshot } from '../socket.js'; import { showToast } from '../components/toast.js'; import { esc } from '../utils.js'; +import { t, tn } from '../i18n.js'; const DESTRUCTIVE_COMMANDS = ['reboot', 'shutdown']; +// Command types only — labels resolved through t('dashboard.cmd.') const GROUP_COMMANDS = [ - { type: 'screen_on', label: 'Screen On' }, - { type: 'screen_off', label: 'Screen Off' }, - { type: 'launch', label: 'Restart App' }, - { type: 'update', label: 'Check Update' }, - { type: 'reboot', label: 'Reboot', destructive: true }, - { type: 'shutdown', label: 'Shutdown', destructive: true }, + { type: 'screen_on' }, + { type: 'screen_off' }, + { type: 'launch' }, + { type: 'update' }, + { type: 'reboot', destructive: true }, + { type: 'shutdown', destructive: true }, ]; +const CMD_LABEL_KEY = { + screen_on: 'dashboard.cmd.screen_on', + screen_off: 'dashboard.cmd.screen_off', + launch: 'dashboard.cmd.restart_app', + update: 'dashboard.cmd.check_update', + reboot: 'dashboard.cmd.reboot', + shutdown: 'dashboard.cmd.shutdown', +}; let statusHandler = null; let screenshotHandler = null; let refreshInterval = null; function formatTimeAgo(timestamp) { - if (!timestamp) return 'Never'; + if (!timestamp) return t('common.never'); const seconds = Math.floor(Date.now() / 1000 - timestamp); - if (seconds < 60) return 'Just now'; - if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`; - if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`; - return `${Math.floor(seconds / 86400)}d ago`; + if (seconds < 60) return t('common.just_now'); + if (seconds < 3600) return t('common.minutes_ago', { n: Math.floor(seconds / 60) }); + if (seconds < 86400) return t('common.hours_ago', { n: Math.floor(seconds / 3600) }); + return t('common.days_ago', { n: Math.floor(seconds / 86400) }); } function formatBytes(mb) { @@ -49,12 +59,12 @@ function renderDeviceCard(device) { - No preview available + ${t('dashboard.no_preview')} ` }
- ${device.status === 'provisioning' ? 'Awaiting Pairing' : device.status} + ${device.status === 'provisioning' ? t('dashboard.awaiting_pairing') : device.status}
${device.status === 'provisioning' && device.pairing_code ? `
@@ -112,9 +122,9 @@ function getGroupPlaylistLabel(devices, playlists) { const unique = [...new Set(assigned)]; if (unique.length === 1) { const pl = playlistMap.get(unique[0]); - return pl ? esc(pl.name) : 'Unknown playlist'; + return pl ? esc(pl.name) : t('dashboard.unknown_playlist'); } - return 'Mixed playlists'; + return t('dashboard.mixed_playlists'); } function renderGroupSection(group, devices, playlists) { @@ -125,26 +135,26 @@ function renderGroupSection(group, devices, playlists) {
${esc(group.name)} - ${devices.length} device${devices.length !== 1 ? 's' : ''} · ${onlineCount} online - ${playlistLabel ? `Playlist: ${playlistLabel}` : ''} + ${tn('dashboard.devices_count', devices.length)} · ${t('dashboard.online_count', { n: onlineCount })} + ${playlistLabel ? `${t('dashboard.playlist_label', { name: playlistLabel })}` : ''}
${devices.length > 0 ? ` ` : ''} - - + +
- ${devices.length > 0 ? devices.map(renderDeviceCard).join('') : '
No devices in this group. Click Manage to add some.
'} + ${devices.length > 0 ? devices.map(renderDeviceCard).join('') : `
${t('dashboard.no_devices_in_group')}
`}
`; @@ -154,26 +164,26 @@ export function render(container) { container.innerHTML = `
- +
@@ -209,13 +219,13 @@ export function render(container) { const code = document.getElementById('pairingCodeInput').value.trim(); const name = document.getElementById('deviceNameInput').value.trim(); if (!code || code.length !== 6) { - showToast('Enter a valid 6-digit pairing code', 'error'); + showToast(t('dashboard.error_pairing_code'), 'error'); return; } try { await api.pairDevice(code, name || undefined); document.getElementById('addDeviceModal').style.display = 'none'; - showToast('Display paired successfully!', 'success'); + showToast(t('dashboard.toast.display_paired'), 'success'); loadDashboard(); } catch (err) { showToast(err.message, 'error'); @@ -224,11 +234,11 @@ export function render(container) { // Create group container.querySelector('#createGroupBtn').addEventListener('click', async () => { - const name = prompt('Group name:'); + const name = prompt(t('dashboard.prompt_group_name')); if (!name) return; try { await api.createGroup(name); - showToast('Group created', 'success'); + showToast(t('dashboard.toast.group_created'), 'success'); loadDashboard(); } catch (e) { showToast(e.message, 'error'); } }); @@ -301,20 +311,20 @@ async function loadDashboard() { if (statsEl) { statsEl.innerHTML = `
-
Total Displays
+
${t('dashboard.total_displays')}
${devices.length}
-
Online
+
${t('dashboard.online')}
${online}
-
Offline
+
${t('dashboard.offline')}
${offline}
${provisioning > 0 ? `
-
Awaiting Pairing
+
${t('dashboard.awaiting_pairing')}
${provisioning}
` : ''} `; @@ -328,8 +338,8 @@ async function loadDashboard() { -

No displays yet

-

Install the ScreenTinker app on your Apolosign TV and pair it using the button above.

+

${t('dashboard.no_displays')}

+

${t('dashboard.no_displays_desc')}

`; return; @@ -371,8 +381,8 @@ async function loadDashboard() {
${groups.length > 0 ? `
- Ungrouped - ${ungrouped.length} device${ungrouped.length !== 1 ? 's' : ''} + ${t('dashboard.ungrouped')} + ${tn('dashboard.devices_count', ungrouped.length)}
` : ''}
${ungrouped.map(renderDeviceCard).join('')} @@ -385,7 +395,7 @@ async function loadDashboard() { attachGroupHandlers(groupsWithDevices, devices); } catch (err) { - main.innerHTML = `

Failed to load displays

${esc(err.message)}

`; + main.innerHTML = `

${t('dashboard.failed_to_load')}

${esc(err.message)}

`; } } @@ -435,17 +445,17 @@ function attachGroupHandlers(groupsWithDevices, allDevices) { if (!targetGroup) return; // Already in this group — no-op. if (targetGroup.memberIds.has(deviceId)) { - showToast(`${deviceName} is already in ${targetGroup.name}`, 'info'); + showToast(t('dashboard.toast.already_in_group', { name: deviceName, group: targetGroup.name }), 'info'); return; } // If the device is in another group, mirror the Manage modal's confirm. const others = (groupsByDeviceId.get(deviceId) || []).map(g => g.name); if (others.length > 0) { - if (!confirm(`${deviceName} is already in: ${others.join(', ')}\n\nAdd it to "${targetGroup.name}" too?`)) return; + if (!confirm(t('dashboard.confirm_add_to_group', { name: deviceName, groups: others.join(', '), target: targetGroup.name }))) return; } try { await api.addDeviceToGroup(groupId, deviceId); - showToast(`Moved ${deviceName} to ${targetGroup.name}`, 'success'); + showToast(t('dashboard.toast.moved_device', { name: deviceName, group: targetGroup.name }), 'success'); loadDashboard(); } catch (err) { showToast(err.message, 'error'); } }); @@ -472,7 +482,7 @@ function attachGroupHandlers(groupsWithDevices, allDevices) { if (memberships.length === 0) return; // already ungrouped try { await Promise.all(memberships.map(m => api.removeDeviceFromGroup(m.id, deviceId))); - showToast(`Removed ${deviceName} from ${memberships.length} group${memberships.length !== 1 ? 's' : ''}`, 'success'); + showToast(tn('dashboard.toast.removed_device', memberships.length, { name: deviceName }), 'success'); loadDashboard(); } catch (err) { showToast(err.message, 'error'); } }); @@ -487,14 +497,14 @@ function attachGroupHandlers(groupsWithDevices, allDevices) { const groupName = e.target.dataset.groupName; const playlistName = e.target.options[e.target.selectedIndex].textContent; - if (!confirm(`Assign playlist "${playlistName}" to all devices in "${groupName}"?`)) { + if (!confirm(t('dashboard.confirm_assign_playlist', { playlist: playlistName, group: groupName }))) { e.target.value = ''; return; } try { const result = await api.groupAssignPlaylist(groupId, playlistId); - showToast(`Playlist assigned to ${result.devices_updated} device${result.devices_updated !== 1 ? 's' : ''}`, 'success'); + showToast(tn('dashboard.toast.playlist_assigned', result.devices_updated), 'success'); } catch (err) { showToast(err.message, 'error'); } @@ -510,9 +520,10 @@ function attachGroupHandlers(groupsWithDevices, allDevices) { const groupId = e.target.dataset.groupId; const groupName = e.target.dataset.groupName; const count = e.target.dataset.deviceCount; + const cmdLabel = t(CMD_LABEL_KEY[type] || type); if (DESTRUCTIVE_COMMANDS.includes(type)) { - if (!confirm(`${type.toUpperCase()} all ${count} device${count !== '1' ? 's' : ''} in "${groupName}"?\n\nThis cannot be undone.`)) { + if (!confirm(t('dashboard.confirm_destructive_command', { cmd: cmdLabel.toUpperCase(), n: count, group: groupName }))) { e.target.value = ''; return; } @@ -520,7 +531,10 @@ function attachGroupHandlers(groupsWithDevices, allDevices) { try { const result = await api.sendGroupCommand(groupId, type); - showToast(`${type} sent to ${result.sent}/${result.total} devices${result.offline > 0 ? ` (${result.offline} offline)` : ''}`, result.offline > 0 ? 'warning' : 'success'); + const msg = result.offline > 0 + ? t('dashboard.toast.command_sent_with_offline', { cmd: cmdLabel, sent: result.sent, total: result.total, offline: result.offline }) + : t('dashboard.toast.command_sent', { cmd: cmdLabel, sent: result.sent, total: result.total }); + showToast(msg, result.offline > 0 ? 'warning' : 'success'); } catch (err) { showToast(err.message, 'error'); } @@ -533,10 +547,10 @@ function attachGroupHandlers(groupsWithDevices, allDevices) { btn.addEventListener('click', async (e) => { e.stopPropagation(); const id = btn.dataset.groupDelete; - if (!confirm('Delete this group? Devices will not be affected.')) return; + if (!confirm(t('dashboard.confirm_delete_group'))) return; try { await api.deleteGroup(id); - showToast('Group deleted', 'success'); + showToast(t('dashboard.toast.group_deleted'), 'success'); loadDashboard(); } catch (e) { showToast(e.message, 'error'); } }); @@ -558,7 +572,7 @@ function attachGroupHandlers(groupsWithDevices, allDevices) { modal.innerHTML = `

${esc(group.name)}

-

Check devices to add them to this group

+

${t('dashboard.manage_group_subtitle')}

${allDevices.filter(d => d.status !== 'provisioning').map(d => { const inOther = otherGroups.filter(g => g.memberIds.has(d.id)).map(g => g.name); @@ -573,7 +587,7 @@ function attachGroupHandlers(groupsWithDevices, allDevices) { }).join('')}
- +
`; @@ -586,9 +600,10 @@ function attachGroupHandlers(groupsWithDevices, allDevices) { cb.addEventListener('change', async () => { const deviceId = cb.dataset.deviceId; const existingGroups = cb.dataset.inGroups; + const cbName = cb.closest('label')?.querySelector('span:not(.status-dot)')?.textContent || ''; try { if (cb.checked && existingGroups) { - if (!confirm(`This device is already in: ${existingGroups}\n\nAdd it to "${group.name}" too?`)) { + if (!confirm(t('dashboard.confirm_add_to_group', { name: cbName, groups: existingGroups, target: group.name }))) { cb.checked = false; return; } diff --git a/frontend/js/views/login.js b/frontend/js/views/login.js index de67329..a09de58 100644 --- a/frontend/js/views/login.js +++ b/frontend/js/views/login.js @@ -1,4 +1,5 @@ import { showToast } from '../components/toast.js'; +import { t } from '../i18n.js'; let authConfig = null; @@ -26,34 +27,34 @@ export async function render(container) {

ScreenTinker

- ${isSetup ? 'Create your admin account to get started' : 'Sign in to manage your displays'} + ${isSetup ? t('auth.subtitle_setup') : t('auth.subtitle_signin')}

- ${!isSetup && canRegister ? '

New accounts get a 14-day free Pro trial

' : ''} + ${!isSetup && canRegister ? `

${t('auth.trial_notice')}

` : ''}
- - + +
- - + +
${isSetup ? `
- - + +
` : ''} ${!isSetup && canRegister ? ` ` : ''}
@@ -61,29 +62,29 @@ export async function render(container) { ${config.googleEnabled || config.microsoftEnabled ? `

- OR + ${t('auth.divider_or')}
` : ''} @@ -97,7 +98,7 @@ export async function render(container) { - Sign in with Google + ${t('auth.signin_google')}
` : ''} @@ -110,25 +111,25 @@ export async function render(container) { - Sign in with Microsoft + ${t('auth.signin_microsoft')} ` : ''}
- Support Access + ${t('auth.support_access')}
- - + +

- Terms of Service + ${t('auth.terms')}  ·  - Privacy Policy + ${t('auth.privacy')}

@@ -147,7 +148,7 @@ function setupHandlers(config, isSetup) { // Support token login document.getElementById('supportLoginBtn')?.addEventListener('click', async () => { const token = document.getElementById('supportToken')?.value.trim(); - if (!token) { showError('Paste a support token'); return; } + if (!token) { showError(t('auth.error_paste_support_token')); return; } try { const res = await fetch('/api/auth/support', { method: 'POST', @@ -157,7 +158,7 @@ function setupHandlers(config, isSetup) { const data = await res.json(); if (!res.ok) { showError(data.error); return; } onAuthSuccess(data); - } catch (err) { showError('Support login failed'); } + } catch (err) { showError(t('auth.error_support_failed')); } }); // Local login/register @@ -184,7 +185,7 @@ function setupHandlers(config, isSetup) { async function doLogin() { const email = document.getElementById('loginEmail').value.trim(); const password = document.getElementById('loginPassword').value; - if (!email || !password) { showError('Email and password required'); return; } + if (!email || !password) { showError(t('auth.error_email_password_required')); return; } try { const res = await fetch('/api/auth/login', { @@ -196,7 +197,7 @@ function setupHandlers(config, isSetup) { if (!res.ok) { showError(data.error); return; } onAuthSuccess(data); } catch (err) { - showError('Login failed'); + showError(t('auth.error_login_failed')); } } @@ -204,8 +205,8 @@ function setupHandlers(config, isSetup) { const email = document.getElementById(isFirstUser ? 'loginEmail' : 'regEmail').value.trim(); const password = document.getElementById(isFirstUser ? 'loginPassword' : 'regPassword').value; const name = document.getElementById(isFirstUser ? 'loginName' : 'regName')?.value.trim() || ''; - if (!email || !password) { showError('Email and password required'); return; } - if (password.length < 6) { showError('Password must be at least 6 characters'); return; } + if (!email || !password) { showError(t('auth.error_email_password_required')); return; } + if (password.length < 6) { showError(t('auth.error_password_min_6')); return; } try { const res = await fetch('/api/auth/register', { @@ -217,7 +218,7 @@ function setupHandlers(config, isSetup) { if (!res.ok) { showError(data.error); return; } onAuthSuccess(data); } catch (err) { - showError('Registration failed'); + showError(t('auth.error_registration_failed')); } } @@ -248,7 +249,7 @@ function setupHandlers(config, isSetup) { }); client.requestAccessToken(); } catch (err) { - showError('Google sign-in failed'); + showError(t('auth.error_google_failed')); } }); } @@ -278,7 +279,7 @@ function setupHandlers(config, isSetup) { else showError(data.error); } } catch (err) { - showError('Microsoft sign-in failed'); + showError(t('auth.error_microsoft_failed')); } }); } diff --git a/frontend/js/views/settings.js b/frontend/js/views/settings.js index 5c7c335..f237b04 100644 --- a/frontend/js/views/settings.js +++ b/frontend/js/views/settings.js @@ -281,8 +281,9 @@ export async function render(container) { }); document.getElementById('langSelect')?.addEventListener('change', (e) => { + // setLanguage dispatches hashchange so the router re-renders the current + // view (including this settings page) with new strings — no refresh needed. setLanguage(e.target.value); - showToast('Language changed. Refresh for full effect.', 'info'); }); document.getElementById('saveAcctBtn')?.addEventListener('click', async () => {