feat: initial daily-timer implementation

This commit is contained in:
Mikhail Kiselev
2026-02-08 05:17:37 +03:00
parent 537f72eb51
commit ef23291bdd
37 changed files with 7779 additions and 0 deletions

318
frontend/src/lib/i18n.js Normal file
View File

@@ -0,0 +1,318 @@
import { writable, derived } from 'svelte/store';
export const locale = writable('ru');
export const translations = {
ru: {
// Navigation
nav: {
timer: 'Таймер',
setup: 'Участники',
history: 'История',
settings: 'Настройки',
},
// Setup page
setup: {
title: 'Название собрания',
participants: 'Участники',
addParticipant: 'Добавить участника',
namePlaceholder: 'Имя участника',
noParticipants: 'Добавьте участников для начала собрания',
selectAll: 'Выбрать всех',
deselectAll: 'Снять выбор',
startMeeting: 'Начать собрание',
speakerTime: 'Время на спикера',
totalTime: 'Общее время',
minutes: 'мин',
unlimited: 'Без ограничения',
dragHint: 'перетащите для изменения порядка, ✓/✗ присутствие',
},
// Timer page
timer: {
currentSpeaker: 'Текущий спикер',
speakerTime: 'Время спикера',
totalTime: 'Общее время',
remaining: 'Осталось',
queue: 'Очередь',
participants: 'Участники',
finished: 'Выступили',
noSpeaker: 'Нет спикера',
noActiveMeeting: 'Собрание не запущено',
goToParticipants: 'Перейдите в раздел Участники, чтобы добавить участников',
readyToStart: 'Всё готово к началу',
editParticipants: 'Редактировать участников',
noParticipants: 'Участники не добавлены',
registeredParticipants: 'Зарегистрированные участники',
},
// Controls
controls: {
next: 'Следующий',
skip: 'Пропустить',
pause: 'Пауза',
resume: 'Продолжить',
stop: 'Завершить',
},
// History page
history: {
title: 'История собраний',
noHistory: 'История пуста',
date: 'Дата',
duration: 'Длительность',
participants: 'Участники',
avgTime: 'Среднее время',
export: 'Экспорт',
exportJSON: 'Экспорт JSON',
exportCSV: 'Экспорт CSV',
delete: 'Удалить',
deleteAll: 'Удалить историю',
deleteSession: 'Удалить запись',
confirmDelete: 'Удалить эту запись?',
confirmDeleteTitle: 'Подтверждение удаления',
confirmDeleteSession: 'Вы уверены, что хотите удалить эту запись? Действие необратимо.',
confirmDeleteAllTitle: 'Удалить всю историю?',
confirmDeleteAll: 'Вы уверены, что хотите удалить ВСЮ историю собраний? Это действие необратимо!',
overtimeRate: 'Процент превышения',
avgAttendance: 'Средняя явка',
recentSessions: 'Последние собрания',
noSessions: 'Собраний пока нет',
participantBreakdown: 'Статистика по участникам',
name: 'Имя',
sessions: 'Собрания',
overtime: 'Превышение',
attendance: 'Явка',
},
// Settings page
settings: {
title: 'Настройки собрания',
language: 'Язык',
sound: 'Звуковые уведомления',
soundEnabled: 'Включены',
soundDisabled: 'Выключены',
warningTime: 'Предупреждение за',
seconds: 'сек',
defaultSpeakerTime: 'Время на спикера по умолчанию',
defaultTotalTime: 'Общее время собрания (мин)',
theme: 'Тема оформления',
themeDark: 'Тёмная',
themeLight: 'Светлая',
save: 'Сохранить',
saved: 'Сохранено!',
windowWidth: 'Настройка окна',
windowWidthHint: 'Ширина окна (мин. 480 пикселей)',
windowFullHeight: 'Окно на всю высоту экрана',
},
// Participant management
participants: {
title: 'Управление участниками',
add: 'Добавить',
edit: 'Редактировать',
delete: 'Удалить',
name: 'Имя',
stats: 'Статистика',
avgSpeakTime: 'Среднее время выступления',
totalMeetings: 'Всего собраний',
confirmDelete: 'Удалить участника?',
},
// Common
common: {
cancel: 'Отмена',
confirm: 'Подтвердить',
save: 'Сохранить',
close: 'Закрыть',
delete: 'Удалить',
yes: 'Да',
no: 'Нет',
loading: 'Загрузка...',
error: 'Ошибка',
},
// Time formats
time: {
hours: 'ч',
minutes: 'мин',
seconds: 'сек',
},
},
en: {
// Navigation
nav: {
timer: 'Timer',
setup: 'Participants',
history: 'History',
settings: 'Settings',
},
// Setup page
setup: {
title: 'New Meeting',
participants: 'Participants',
addParticipant: 'Add Participant',
namePlaceholder: 'Participant name',
noParticipants: 'Add participants to start a meeting',
selectAll: 'Select All',
deselectAll: 'Deselect All',
startMeeting: 'Start Meeting',
speakerTime: 'Speaker Time',
totalTime: 'Total Time',
minutes: 'min',
unlimited: 'Unlimited',
dragHint: 'drag to reorder, ✓/✗ attendance',
},
// Timer page
timer: {
currentSpeaker: 'Current Speaker',
speakerTime: 'Speaker Time',
totalTime: 'Total Time',
remaining: 'Remaining',
queue: 'Queue',
participants: 'Participants',
finished: 'Finished',
noSpeaker: 'No speaker',
noActiveMeeting: 'No active meeting',
goToParticipants: 'Go to Participants to add participants',
readyToStart: 'Ready to start',
editParticipants: 'Edit participants',
noParticipants: 'No participants added',
registeredParticipants: 'Registered participants',
},
// Controls
controls: {
next: 'Next',
skip: 'Skip',
pause: 'Pause',
resume: 'Resume',
stop: 'Stop',
},
// History page
history: {
title: 'Meeting History',
noHistory: 'No history yet',
date: 'Date',
duration: 'Duration',
participants: 'Participants',
avgTime: 'Avg Time',
export: 'Export',
exportJSON: 'Export JSON',
exportCSV: 'Export CSV',
delete: 'Delete',
deleteAll: 'Delete History',
deleteSession: 'Delete session',
confirmDelete: 'Delete this record?',
confirmDeleteTitle: 'Confirm Deletion',
confirmDeleteSession: 'Are you sure you want to delete this session? This action cannot be undone.',
confirmDeleteAllTitle: 'Delete All History?',
confirmDeleteAll: 'Are you sure you want to delete ALL meeting history? This action cannot be undone!',
overtimeRate: 'Overtime Rate',
avgAttendance: 'Avg. Attendance',
recentSessions: 'Recent Sessions',
noSessions: 'No sessions yet',
participantBreakdown: 'Participant Breakdown',
name: 'Name',
sessions: 'Sessions',
overtime: 'Overtime',
attendance: 'Attendance',
},
// Settings page
settings: {
title: 'Meeting Settings',
language: 'Language',
sound: 'Sound Notifications',
soundEnabled: 'Enabled',
soundDisabled: 'Disabled',
warningTime: 'Warning before',
seconds: 'sec',
defaultSpeakerTime: 'Default Speaker Time',
defaultTotalTime: 'Total meeting time (min)',
theme: 'Theme',
themeDark: 'Dark',
themeLight: 'Light',
save: 'Save',
saved: 'Saved!',
windowWidth: 'Window Settings',
windowWidthHint: 'Window width (min. 480 pixels)',
windowFullHeight: 'Full screen height window',
},
// Participant management
participants: {
title: 'Manage Participants',
add: 'Add',
edit: 'Edit',
delete: 'Delete',
name: 'Name',
stats: 'Statistics',
avgSpeakTime: 'Avg Speaking Time',
totalMeetings: 'Total Meetings',
confirmDelete: 'Delete participant?',
},
// Common
common: {
cancel: 'Cancel',
confirm: 'Confirm',
save: 'Save',
close: 'Close',
delete: 'Delete',
yes: 'Yes',
no: 'No',
loading: 'Loading...',
error: 'Error',
},
// Time formats
time: {
hours: 'h',
minutes: 'min',
seconds: 'sec',
},
},
};
export const t = derived(locale, ($locale) => {
return (key) => {
const keys = key.split('.');
let value = translations[$locale];
for (const k of keys) {
if (value && typeof value === 'object' && k in value) {
value = value[k];
} else {
console.warn(`Translation missing: ${key} for locale ${$locale}`);
return key;
}
}
return value;
};
});
export function setLocale(lang) {
if (translations[lang]) {
locale.set(lang);
localStorage.setItem('daily-timer-locale', lang);
}
}
export function initLocale() {
const saved = localStorage.getItem('daily-timer-locale');
if (saved && translations[saved]) {
locale.set(saved);
} else {
const browserLang = navigator.language.split('-')[0];
if (translations[browserLang]) {
locale.set(browserLang);
}
}
}