feat: initial daily-timer implementation
This commit is contained in:
318
frontend/src/lib/i18n.js
Normal file
318
frontend/src/lib/i18n.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user