diff --git a/frontend/js/i18n/de.js b/frontend/js/i18n/de.js index ff1e3a0..66d387d 100644 --- a/frontend/js/i18n/de.js +++ b/frontend/js/i18n/de.js @@ -983,4 +983,69 @@ export default { 'billing.toast.checkout_failed': 'Bezahlung konnte nicht gestartet werden: {error}', 'billing.toast.portal_failed': 'Abrechnungsportal konnte nicht geöffnet werden: {error}', 'billing.toast.payment_success': 'Bezahlung erfolgreich! Ihr Plan wurde aktualisiert.', + + // Teams + 'team.title': 'Teams', + 'team.subtitle': 'Teams und gemeinsamen Zugriff verwalten', + 'team.help_tip': 'Erstellen Sie Teams, um Geräte mit anderen Nutzern zu teilen. Eigentümer verwalten das Team, Editoren ändern Inhalte/Playlists, Betrachter überwachen nur.', + 'team.new_team': 'Neues Team', + 'team.prompt_name': 'Teamname:', + 'team.empty_title': 'Noch keine Teams', + 'team.empty_desc': 'Erstellen Sie ein Team, um Geräte mit anderen Nutzern zu teilen.', + 'team.your_role': 'Ihre Rolle: {role}', + 'team.member_count_one': '1 Mitglied', + 'team.member_count_other': '{n} Mitglieder', + 'team.not_found': 'Team nicht gefunden', + 'team.back': 'Zurück zu Teams', + 'team.delete_team': 'Team löschen', + 'team.members_count': 'Mitglieder ({n})', + 'team.invite': '+ Einladen', + 'team.role_viewer': 'Betrachter', + 'team.role_editor': 'Editor', + 'team.role_owner': 'Eigentümer', + 'team.remove': 'Entfernen', + 'team.remove_from_team': 'Aus Team entfernen', + 'team.no_members': 'Keine Mitglieder', + 'team.shared_devices': 'Geteilte Geräte ({n})', + 'team.add_device': '+ Gerät hinzufügen...', + 'team.no_devices': 'Keine Geräte mit diesem Team geteilt', + 'team.prompt_email': 'E-Mail-Adresse zum Einladen:', + 'team.prompt_role': 'Rolle (viewer, editor oder owner):', + 'team.toast.invalid_role': 'Ungültige Rolle', + 'team.toast.invitation_sent': 'Einladung gesendet', + 'team.toast.role_updated': 'Rolle aktualisiert', + 'team.toast.member_removed': 'Mitglied entfernt', + 'team.toast.device_added': 'Gerät zum Team hinzugefügt', + 'team.toast.device_removed': 'Gerät aus Team entfernt', + 'team.toast.deleted': 'Team gelöscht', + + // Activity + 'activity.title': 'Aktivitätsprotokoll', + 'activity.subtitle': 'Audit-Trail aller Aktionen', + 'activity.load_more': 'Mehr laden', + 'activity.empty_title': 'Noch keine Aktivität', + 'activity.empty_desc': 'Aktionen erscheinen hier, sobald Sie das System nutzen.', + 'activity.system': 'System', + 'activity.verb_created': 'hat erstellt', + 'activity.verb_updated': 'hat aktualisiert', + 'activity.verb_deleted': 'hat gelöscht', + 'activity.action_paired_device': 'hat ein Gerät gekoppelt', + 'activity.action_added_remote_content': 'hat Remote-Inhalt hinzugefügt', + 'activity.noun_content': 'Inhalt', + 'activity.noun_device': 'Gerät', + 'activity.noun_playlist_assignment': 'Playlist-Zuweisung', + 'activity.noun_assignment': 'Zuweisung', + 'activity.noun_layout': 'Layout', + 'activity.noun_widget': 'Widget', + 'activity.noun_schedule': 'Zeitplan', + 'activity.noun_video_wall': 'Videowand', + 'activity.alert_device_offline': 'Alarm: Gerät offline', + + // Help + 'help.title': 'Hilfe-Center', + 'help.subtitle': 'Schnellanleitungen und FAQ', + 'help.faq': 'Häufig gestellte Fragen', + 'help.shortcuts': 'Tastaturkürzel', + 'help.shortcut_esc': 'Web-Player zurücksetzen (auf Player-Seite)', + 'help.shortcut_f': 'Vollbild umschalten (Web-Player)', }; diff --git a/frontend/js/i18n/en.js b/frontend/js/i18n/en.js index 3b41c66..37bef7c 100644 --- a/frontend/js/i18n/en.js +++ b/frontend/js/i18n/en.js @@ -1019,4 +1019,69 @@ export default { 'billing.toast.checkout_failed': 'Failed to start checkout: {error}', 'billing.toast.portal_failed': 'Failed to open billing portal: {error}', 'billing.toast.payment_success': 'Payment successful! Your plan has been upgraded.', + + // Teams + 'team.title': 'Teams', + 'team.subtitle': 'Manage teams and shared access', + 'team.help_tip': 'Create teams to share devices with other users. Owners manage the team, editors can change content/playlists, viewers can only monitor.', + 'team.new_team': 'New Team', + 'team.prompt_name': 'Team name:', + 'team.empty_title': 'No teams yet', + 'team.empty_desc': 'Create a team to share devices with other users.', + 'team.your_role': 'Your role: {role}', + 'team.member_count_one': '1 member', + 'team.member_count_other': '{n} members', + 'team.not_found': 'Team not found', + 'team.back': 'Back to Teams', + 'team.delete_team': 'Delete Team', + 'team.members_count': 'Members ({n})', + 'team.invite': '+ Invite', + 'team.role_viewer': 'Viewer', + 'team.role_editor': 'Editor', + 'team.role_owner': 'Owner', + 'team.remove': 'Remove', + 'team.remove_from_team': 'Remove from team', + 'team.no_members': 'No members yet', + 'team.shared_devices': 'Shared Devices ({n})', + 'team.add_device': '+ Add device...', + 'team.no_devices': 'No devices shared with this team', + 'team.prompt_email': 'Email address to invite:', + 'team.prompt_role': 'Role (viewer, editor, or owner):', + 'team.toast.invalid_role': 'Invalid role', + 'team.toast.invitation_sent': 'Invitation sent', + 'team.toast.role_updated': 'Role updated', + 'team.toast.member_removed': 'Member removed', + 'team.toast.device_added': 'Device added to team', + 'team.toast.device_removed': 'Device removed from team', + 'team.toast.deleted': 'Team deleted', + + // Activity log + 'activity.title': 'Activity Log', + 'activity.subtitle': 'Audit trail of all actions', + 'activity.load_more': 'Load More', + 'activity.empty_title': 'No activity yet', + 'activity.empty_desc': 'Actions will appear here as you use the system.', + 'activity.system': 'System', + 'activity.verb_created': 'created', + 'activity.verb_updated': 'updated', + 'activity.verb_deleted': 'deleted', + 'activity.action_paired_device': 'paired a device', + 'activity.action_added_remote_content': 'added remote content', + 'activity.noun_content': 'content', + 'activity.noun_device': 'device', + 'activity.noun_playlist_assignment': 'playlist assignment', + 'activity.noun_assignment': 'assignment', + 'activity.noun_layout': 'layout', + 'activity.noun_widget': 'widget', + 'activity.noun_schedule': 'schedule', + 'activity.noun_video_wall': 'video wall', + 'activity.alert_device_offline': 'alert: device went offline', + + // Help + 'help.title': 'Help Center', + 'help.subtitle': 'Quick guides and FAQ', + 'help.faq': 'Frequently Asked Questions', + 'help.shortcuts': 'Keyboard Shortcuts', + 'help.shortcut_esc': 'Reset web player (on player page)', + 'help.shortcut_f': 'Toggle fullscreen (web player)', }; diff --git a/frontend/js/i18n/es.js b/frontend/js/i18n/es.js index 4f2c131..452ef50 100644 --- a/frontend/js/i18n/es.js +++ b/frontend/js/i18n/es.js @@ -982,4 +982,69 @@ export default { 'billing.toast.checkout_failed': 'Error al iniciar el pago: {error}', 'billing.toast.portal_failed': 'Error al abrir el portal de facturación: {error}', 'billing.toast.payment_success': '¡Pago exitoso! Tu plan ha sido actualizado.', + + // Teams + 'team.title': 'Equipos', + 'team.subtitle': 'Gestiona equipos y acceso compartido', + 'team.help_tip': 'Crea equipos para compartir dispositivos con otros usuarios. Los propietarios gestionan el equipo, los editores pueden cambiar contenido/listas, los espectadores solo monitorean.', + 'team.new_team': 'Nuevo equipo', + 'team.prompt_name': 'Nombre del equipo:', + 'team.empty_title': 'Aún no hay equipos', + 'team.empty_desc': 'Crea un equipo para compartir dispositivos con otros usuarios.', + 'team.your_role': 'Tu rol: {role}', + 'team.member_count_one': '1 miembro', + 'team.member_count_other': '{n} miembros', + 'team.not_found': 'Equipo no encontrado', + 'team.back': 'Volver a equipos', + 'team.delete_team': 'Eliminar equipo', + 'team.members_count': 'Miembros ({n})', + 'team.invite': '+ Invitar', + 'team.role_viewer': 'Espectador', + 'team.role_editor': 'Editor', + 'team.role_owner': 'Propietario', + 'team.remove': 'Eliminar', + 'team.remove_from_team': 'Quitar del equipo', + 'team.no_members': 'Sin miembros', + 'team.shared_devices': 'Dispositivos compartidos ({n})', + 'team.add_device': '+ Agregar dispositivo...', + 'team.no_devices': 'No hay dispositivos compartidos con este equipo', + 'team.prompt_email': 'Email para invitar:', + 'team.prompt_role': 'Rol (viewer, editor o owner):', + 'team.toast.invalid_role': 'Rol no válido', + 'team.toast.invitation_sent': 'Invitación enviada', + 'team.toast.role_updated': 'Rol actualizado', + 'team.toast.member_removed': 'Miembro eliminado', + 'team.toast.device_added': 'Dispositivo agregado al equipo', + 'team.toast.device_removed': 'Dispositivo quitado del equipo', + 'team.toast.deleted': 'Equipo eliminado', + + // Activity + 'activity.title': 'Registro de actividad', + 'activity.subtitle': 'Historial de auditoría de todas las acciones', + 'activity.load_more': 'Cargar más', + 'activity.empty_title': 'Aún no hay actividad', + 'activity.empty_desc': 'Las acciones aparecerán aquí a medida que uses el sistema.', + 'activity.system': 'Sistema', + 'activity.verb_created': 'creó', + 'activity.verb_updated': 'actualizó', + 'activity.verb_deleted': 'eliminó', + 'activity.action_paired_device': 'vinculó un dispositivo', + 'activity.action_added_remote_content': 'agregó contenido remoto', + 'activity.noun_content': 'contenido', + 'activity.noun_device': 'dispositivo', + 'activity.noun_playlist_assignment': 'asignación de lista', + 'activity.noun_assignment': 'asignación', + 'activity.noun_layout': 'diseño', + 'activity.noun_widget': 'widget', + 'activity.noun_schedule': 'horario', + 'activity.noun_video_wall': 'muro de video', + 'activity.alert_device_offline': 'alerta: dispositivo desconectado', + + // Help + 'help.title': 'Centro de ayuda', + 'help.subtitle': 'Guías rápidas y preguntas frecuentes', + 'help.faq': 'Preguntas frecuentes', + 'help.shortcuts': 'Atajos de teclado', + 'help.shortcut_esc': 'Reiniciar reproductor web (en la página del reproductor)', + 'help.shortcut_f': 'Alternar pantalla completa (reproductor web)', }; diff --git a/frontend/js/i18n/fr.js b/frontend/js/i18n/fr.js index 5d336f8..9a5ba4b 100644 --- a/frontend/js/i18n/fr.js +++ b/frontend/js/i18n/fr.js @@ -983,4 +983,69 @@ export default { 'billing.toast.checkout_failed': 'Échec du paiement : {error}', 'billing.toast.portal_failed': 'Échec d\'ouverture du portail : {error}', 'billing.toast.payment_success': 'Paiement réussi ! Votre plan a été mis à niveau.', + + // Teams + 'team.title': 'Équipes', + 'team.subtitle': 'Gérez les équipes et l\'accès partagé', + 'team.help_tip': 'Créez des équipes pour partager des appareils. Les propriétaires gèrent l\'équipe, les éditeurs modifient le contenu/listes, les spectateurs ne font que surveiller.', + 'team.new_team': 'Nouvelle équipe', + 'team.prompt_name': 'Nom de l\'équipe :', + 'team.empty_title': 'Aucune équipe', + 'team.empty_desc': 'Créez une équipe pour partager des appareils avec d\'autres utilisateurs.', + 'team.your_role': 'Votre rôle : {role}', + 'team.member_count_one': '1 membre', + 'team.member_count_other': '{n} membres', + 'team.not_found': 'Équipe introuvable', + 'team.back': 'Retour aux équipes', + 'team.delete_team': 'Supprimer l\'équipe', + 'team.members_count': 'Membres ({n})', + 'team.invite': '+ Inviter', + 'team.role_viewer': 'Spectateur', + 'team.role_editor': 'Éditeur', + 'team.role_owner': 'Propriétaire', + 'team.remove': 'Retirer', + 'team.remove_from_team': 'Retirer de l\'équipe', + 'team.no_members': 'Aucun membre', + 'team.shared_devices': 'Appareils partagés ({n})', + 'team.add_device': '+ Ajouter un appareil...', + 'team.no_devices': 'Aucun appareil partagé avec cette équipe', + 'team.prompt_email': 'E-mail à inviter :', + 'team.prompt_role': 'Rôle (viewer, editor ou owner) :', + 'team.toast.invalid_role': 'Rôle invalide', + 'team.toast.invitation_sent': 'Invitation envoyée', + 'team.toast.role_updated': 'Rôle mis à jour', + 'team.toast.member_removed': 'Membre retiré', + 'team.toast.device_added': 'Appareil ajouté à l\'équipe', + 'team.toast.device_removed': 'Appareil retiré de l\'équipe', + 'team.toast.deleted': 'Équipe supprimée', + + // Activity + 'activity.title': 'Journal d\'activité', + 'activity.subtitle': 'Audit de toutes les actions', + 'activity.load_more': 'Charger plus', + 'activity.empty_title': 'Aucune activité', + 'activity.empty_desc': 'Les actions apparaîtront ici au fur et à mesure de votre utilisation.', + 'activity.system': 'Système', + 'activity.verb_created': 'a créé', + 'activity.verb_updated': 'a mis à jour', + 'activity.verb_deleted': 'a supprimé', + 'activity.action_paired_device': 'a apparié un appareil', + 'activity.action_added_remote_content': 'a ajouté du contenu distant', + 'activity.noun_content': 'contenu', + 'activity.noun_device': 'appareil', + 'activity.noun_playlist_assignment': 'attribution de liste', + 'activity.noun_assignment': 'attribution', + 'activity.noun_layout': 'mise en page', + 'activity.noun_widget': 'widget', + 'activity.noun_schedule': 'plage horaire', + 'activity.noun_video_wall': 'mur vidéo', + 'activity.alert_device_offline': 'alerte : appareil hors ligne', + + // Help + 'help.title': 'Centre d\'aide', + 'help.subtitle': 'Guides rapides et FAQ', + 'help.faq': 'Questions fréquentes', + 'help.shortcuts': 'Raccourcis clavier', + 'help.shortcut_esc': 'Réinitialiser le lecteur web (sur la page du lecteur)', + 'help.shortcut_f': 'Basculer le plein écran (lecteur web)', }; diff --git a/frontend/js/i18n/pt.js b/frontend/js/i18n/pt.js index 5492720..cec946c 100644 --- a/frontend/js/i18n/pt.js +++ b/frontend/js/i18n/pt.js @@ -983,4 +983,69 @@ export default { 'billing.toast.checkout_failed': 'Falha ao iniciar pagamento: {error}', 'billing.toast.portal_failed': 'Falha ao abrir portal de cobrança: {error}', 'billing.toast.payment_success': 'Pagamento bem-sucedido! Seu plano foi atualizado.', + + // Teams + 'team.title': 'Equipes', + 'team.subtitle': 'Gerencie equipes e acesso compartilhado', + 'team.help_tip': 'Crie equipes para compartilhar dispositivos com outros usuários. Proprietários gerenciam a equipe, editores podem alterar conteúdo/playlists, visualizadores apenas monitoram.', + 'team.new_team': 'Nova equipe', + 'team.prompt_name': 'Nome da equipe:', + 'team.empty_title': 'Sem equipes ainda', + 'team.empty_desc': 'Crie uma equipe para compartilhar dispositivos com outros usuários.', + 'team.your_role': 'Sua função: {role}', + 'team.member_count_one': '1 membro', + 'team.member_count_other': '{n} membros', + 'team.not_found': 'Equipe não encontrada', + 'team.back': 'Voltar para equipes', + 'team.delete_team': 'Excluir equipe', + 'team.members_count': 'Membros ({n})', + 'team.invite': '+ Convidar', + 'team.role_viewer': 'Visualizador', + 'team.role_editor': 'Editor', + 'team.role_owner': 'Proprietário', + 'team.remove': 'Remover', + 'team.remove_from_team': 'Remover da equipe', + 'team.no_members': 'Sem membros', + 'team.shared_devices': 'Dispositivos compartilhados ({n})', + 'team.add_device': '+ Adicionar dispositivo...', + 'team.no_devices': 'Sem dispositivos compartilhados com esta equipe', + 'team.prompt_email': 'E-mail para convidar:', + 'team.prompt_role': 'Função (viewer, editor ou owner):', + 'team.toast.invalid_role': 'Função inválida', + 'team.toast.invitation_sent': 'Convite enviado', + 'team.toast.role_updated': 'Função atualizada', + 'team.toast.member_removed': 'Membro removido', + 'team.toast.device_added': 'Dispositivo adicionado à equipe', + 'team.toast.device_removed': 'Dispositivo removido da equipe', + 'team.toast.deleted': 'Equipe excluída', + + // Activity + 'activity.title': 'Registro de atividades', + 'activity.subtitle': 'Trilha de auditoria de todas as ações', + 'activity.load_more': 'Carregar mais', + 'activity.empty_title': 'Sem atividade ainda', + 'activity.empty_desc': 'As ações aparecerão aqui conforme você usa o sistema.', + 'activity.system': 'Sistema', + 'activity.verb_created': 'criou', + 'activity.verb_updated': 'atualizou', + 'activity.verb_deleted': 'excluiu', + 'activity.action_paired_device': 'pareou um dispositivo', + 'activity.action_added_remote_content': 'adicionou conteúdo remoto', + 'activity.noun_content': 'conteúdo', + 'activity.noun_device': 'dispositivo', + 'activity.noun_playlist_assignment': 'atribuição de playlist', + 'activity.noun_assignment': 'atribuição', + 'activity.noun_layout': 'layout', + 'activity.noun_widget': 'widget', + 'activity.noun_schedule': 'agenda', + 'activity.noun_video_wall': 'parede de vídeo', + 'activity.alert_device_offline': 'alerta: dispositivo offline', + + // Help + 'help.title': 'Central de ajuda', + 'help.subtitle': 'Guias rápidos e perguntas frequentes', + 'help.faq': 'Perguntas frequentes', + 'help.shortcuts': 'Atalhos de teclado', + 'help.shortcut_esc': 'Reiniciar player web (na página do player)', + 'help.shortcut_f': 'Alternar tela cheia (player web)', }; diff --git a/frontend/js/views/activity.js b/frontend/js/views/activity.js index f9c9bc0..a4f9f8c 100644 --- a/frontend/js/views/activity.js +++ b/frontend/js/views/activity.js @@ -1,16 +1,17 @@ import { showToast } from '../components/toast.js'; import { esc } from '../utils.js'; +import { t } from '../i18n.js'; const API = (url) => fetch('/api' + url, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }}).then(r => r.json()); export async function render(container) { container.innerHTML = ` -

Loading...

+

${t('common.loading')}

- +
`; @@ -25,14 +26,14 @@ export async function render(container) { if (!append) list.innerHTML = ''; if (items.length === 0 && offset === 0) { - list.innerHTML = '

No activity yet

Actions will appear here as you use the system.

'; + list.innerHTML = `

${t('activity.empty_title')}

${t('activity.empty_desc')}

`; return; } const html = items.map(item => { const time = new Date(item.created_at * 1000); - const timeStr = time.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + ' ' + - time.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); + const timeStr = time.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) + ' ' + + time.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }); const icon = getActionIcon(item.action); return ` @@ -40,7 +41,7 @@ export async function render(container) {
${icon}
- ${esc(item.user_name || item.user_email || 'System')} + ${esc(item.user_name || item.user_email || t('activity.system'))} ${esc(formatAction(item.action))}
${item.details ? `
${esc(item.details)}
` : ''} @@ -81,22 +82,29 @@ function getActionIcon(action) { return '📄'; } +// Action verbs are user-visible; translate them through t() so they switch +// languages with the rest of the UI. The mapping below preserves the original +// verb-then-noun structure of the English version. function formatAction(action) { - return action - .replace('POST /api/', 'created ') - .replace('PUT /api/', 'updated ') - .replace('DELETE /api/', 'deleted ') - .replace('/provision/pair', 'paired a device') - .replace('/content/remote', 'added remote content') - .replace('/content', 'content') - .replace('/devices/:id', 'device') - .replace('/assignments/device/:deviceId', 'playlist assignment') - .replace('/assignments/:id', 'assignment') - .replace('/layouts', 'layout') - .replace('/widgets', 'widget') - .replace('/schedules', 'schedule') - .replace('/walls', 'video wall') - .replace('alert:device_offline', 'alert: device went offline'); + // Verbs + let s = action + .replace('POST /api/', t('activity.verb_created') + ' ') + .replace('PUT /api/', t('activity.verb_updated') + ' ') + .replace('DELETE /api/', t('activity.verb_deleted') + ' '); + // Specific endpoints + s = s + .replace('/provision/pair', t('activity.action_paired_device')) + .replace('/content/remote', t('activity.action_added_remote_content')) + .replace('/content', t('activity.noun_content')) + .replace('/devices/:id', t('activity.noun_device')) + .replace('/assignments/device/:deviceId', t('activity.noun_playlist_assignment')) + .replace('/assignments/:id', t('activity.noun_assignment')) + .replace('/layouts', t('activity.noun_layout')) + .replace('/widgets', t('activity.noun_widget')) + .replace('/schedules', t('activity.noun_schedule')) + .replace('/walls', t('activity.noun_video_wall')) + .replace('alert:device_offline', t('activity.alert_device_offline')); + return s; } export function cleanup() {} diff --git a/frontend/js/views/help.js b/frontend/js/views/help.js index b76c57c..83a52f5 100644 --- a/frontend/js/views/help.js +++ b/frontend/js/views/help.js @@ -1,7 +1,13 @@ +import { t } from '../i18n.js'; + +// Help guides + FAQ are documentation. Page chrome is translated; the body +// content is intentionally left in English because partial machine +// translation of multi-paragraph docs reads worse than a single source of +// truth. A native-language docs site is the right long-term answer. export function render(container) { container.innerHTML = `
@@ -25,7 +31,7 @@ export function render(container) {
-

Frequently Asked Questions

+

${t('help.faq')}

${[ { q: 'What devices are supported?', a: 'Android TV/tablets (APK), Raspberry Pi, Windows, ChromeOS, LG webOS, Samsung Tizen, Fire TV, and any device with a web browser.' }, { q: 'How does the free trial work?', a: 'New accounts get a 14-day free trial of the Pro plan (15 devices, all features). After 14 days, you\'re moved to the Free plan (1 device) unless you upgrade.' }, @@ -46,10 +52,10 @@ export function render(container) {
-

Keyboard Shortcuts

+

${t('help.shortcuts')}

- Esc Reset web player (on player page) - F Toggle fullscreen (web player) + Esc ${t('help.shortcut_esc')} + F ${t('help.shortcut_f')}
`; diff --git a/frontend/js/views/teams.js b/frontend/js/views/teams.js index 215f02b..5240ac1 100644 --- a/frontend/js/views/teams.js +++ b/frontend/js/views/teams.js @@ -1,5 +1,6 @@ import { api } from '../api.js'; import { showToast } from '../components/toast.js'; +import { t, tn } from '../i18n.js'; const API = (url, opts = {}) => fetch('/api' + url, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}`, ...opts.headers }, ...opts }).then(r => r.json()); @@ -15,17 +16,17 @@ export async function render(container) { async function renderList(container) { container.innerHTML = `
`; document.getElementById('newTeamBtn').onclick = async () => { - const name = prompt('Team name:'); + const name = prompt(t('team.prompt_name')); if (!name) return; const team = await API('/teams', { method: 'POST', body: JSON.stringify({ name }) }); window.location.hash = `#/team/${team.id}`; @@ -36,19 +37,19 @@ async function renderList(container) { const list = document.getElementById('teamsList'); if (!teams.length) { - list.innerHTML = '

No teams yet

Create a team to share devices with other users.

'; + list.innerHTML = `

${t('team.empty_title')}

${t('team.empty_desc')}

`; return; } list.innerHTML = `
- ${teams.map(t => ` -
+ ${teams.map(team => ` +
-
${t.name[0].toUpperCase()}
+
${team.name[0].toUpperCase()}
-
${t.name}
-
Your role: ${t.my_role} · ${t.member_count} member(s)
+
${team.name}
+
${t('team.your_role', { role: team.my_role })} · ${tn('team.member_count', team.member_count)}
@@ -66,28 +67,27 @@ async function renderTeamDetail(container, teamId) { API(`/teams/${teamId}/devices`), api.getDevices() ]); - } catch { container.innerHTML = '

Team not found

'; return; } + } catch { container.innerHTML = `

${t('team.not_found')}

`; return; } const unassignedDevices = allDevices.filter(d => !d.team_id || d.team_id !== teamId); container.innerHTML = ` - Back to Teams + ${t('team.back')}
-
-

Members (${team.members?.length || 0})

- +

${t('team.members_count', { n: team.members?.length || 0 })}

+
${(team.members || []).map(m => ` @@ -98,22 +98,21 @@ async function renderTeamDetail(container, teamId) {
${m.email}
- ${m.role !== 'owner' ? `` : ''} + ${m.role !== 'owner' ? `` : ''}
- `).join('') || '

No members yet

'} + `).join('') || `

${t('team.no_members')}

`}
-
-

Shared Devices (${devices.length})

+

${t('team.shared_devices', { n: devices.length })}

@@ -125,75 +124,69 @@ async function renderTeamDetail(container, teamId) {
${d.name}
${d.status}
- +
- `).join('') || '

No devices shared with this team

'} + `).join('') || `

${t('team.no_devices')}

`}
`; - // Invite member document.getElementById('inviteMemberBtn').onclick = async () => { - const email = prompt('Email address to invite:'); + const email = prompt(t('team.prompt_email')); if (!email) return; - const role = prompt('Role (viewer, editor, or owner):', 'editor'); - if (!['viewer', 'editor', 'owner'].includes(role)) { showToast('Invalid role', 'error'); return; } + const role = prompt(t('team.prompt_role'), 'editor'); + if (!['viewer', 'editor', 'owner'].includes(role)) { showToast(t('team.toast.invalid_role'), 'error'); return; } try { await API(`/teams/${teamId}/invite`, { method: 'POST', body: JSON.stringify({ email, role }) }); - showToast('Invitation sent', 'success'); + showToast(t('team.toast.invitation_sent'), 'success'); renderTeamDetail(container, teamId); } catch (err) { showToast(err.message, 'error'); } }; - // Change member role container.querySelectorAll('[data-member-id]').forEach(select => { select.onchange = async () => { try { await API(`/teams/${teamId}/members/${select.dataset.memberId}`, { method: 'PUT', body: JSON.stringify({ role: select.value }) }); - showToast('Role updated', 'success'); + showToast(t('team.toast.role_updated'), 'success'); } catch (err) { showToast(err.message, 'error'); } }; }); - // Remove member container.querySelectorAll('[data-remove-member]').forEach(btn => { btn.onclick = async () => { try { await API(`/teams/${teamId}/members/${btn.dataset.removeMember}`, { method: 'DELETE' }); - showToast('Member removed', 'success'); + showToast(t('team.toast.member_removed'), 'success'); renderTeamDetail(container, teamId); } catch (err) { showToast(err.message, 'error'); } }; }); - // Add device to team document.getElementById('addDeviceToTeam').onchange = async (e) => { const deviceId = e.target.value; if (!deviceId) return; try { await API(`/teams/${teamId}/devices`, { method: 'POST', body: JSON.stringify({ device_id: deviceId }) }); - showToast('Device added to team', 'success'); + showToast(t('team.toast.device_added'), 'success'); renderTeamDetail(container, teamId); } catch (err) { showToast(err.message, 'error'); } }; - // Remove device from team container.querySelectorAll('[data-remove-device]').forEach(btn => { btn.onclick = async () => { try { await API(`/teams/${teamId}/devices/${btn.dataset.removeDevice}`, { method: 'DELETE' }); - showToast('Device removed from team', 'success'); + showToast(t('team.toast.device_removed'), 'success'); renderTeamDetail(container, teamId); } catch (err) { showToast(err.message, 'error'); } }; }); - // Delete team document.getElementById('deleteTeamBtn').onclick = async () => { try { await API(`/teams/${teamId}`, { method: 'DELETE' }); - showToast('Team deleted', 'success'); + showToast(t('team.toast.deleted'), 'success'); window.location.hash = '#/teams'; } catch (err) { showToast(err.message, 'error'); } };