Reorganize web folder structurally

This commit is contained in:
Халимов Рустам
2026-03-19 22:24:21 +03:00
parent 11ebbc853b
commit 4384233aa5
62 changed files with 0 additions and 0 deletions
+565
View File
@@ -0,0 +1,565 @@
import { create } from 'zustand';
export type Lang = 'ru' | 'en';
const LANG_KEY = 'knot_language';
const translations = {
ru: {
// Side menu
myProfile: 'Мой профиль',
settings: 'Настройки',
aboutApp: 'О приложении',
logout: 'Выйти из аккаунта',
// Profile
name: 'Имя',
username: 'Имя пользователя',
aboutMe: 'О себе',
birthday: 'Дата рождения',
enterName: 'Введите имя',
tellAboutYourself: 'Расскажите о себе',
removePhoto: 'Удалить фото',
// Settings
language: 'Язык / Language',
interfaceLang: 'Язык интерфейса',
theme: 'Оформление',
russian: 'Русский',
english: 'English',
about: 'О приложении',
version: 'Версия',
modernMessenger: 'Современный мессенджер с фокусом',
onPrivacy: 'на приватность и удобство',
modernMessengerShort: 'Современный мессенджер',
// Chat
chat: 'Чат',
group: 'Группа',
searchChats: 'Поиск чатов...',
noChats: 'Нет чатов — создайте первый!',
nothingFound: 'Ничего не найдено',
selectChat: 'Выберите чат для начала общения',
noMessages: 'Нет сообщений — напишите первое!',
typing: 'печатает...',
online: 'в сети',
wasRecently: 'был(а) недавно',
members: 'участников',
files: 'файлов',
clearAll: 'Очистить всё',
messagePlaceholder: 'Сообщение...',
message: 'Сообщение...',
addCaption: 'Добавьте подпись...',
searchMessages: 'Поиск сообщений...',
searchMessagesBtn: 'Поиск сообщений',
userProfile: 'Профиль пользователя',
enableSound: 'Включить звук',
disableSound: 'Выключить звук',
deleteChat: 'Удалить чат',
// Messages
messageDeleted: 'Сообщение удалено',
reply: 'Ответить',
copy: 'Копировать',
edit: 'Редактировать',
delete: 'Удалить',
deleteForMe: 'Удалить у меня',
deleteForAll: 'Удалить у всех',
deleteAlsoFor: 'Удалить также для',
editing: 'Редактирование',
replyTo: 'Ответ',
media: 'Медиа',
download: 'Скачать',
edited: 'ред.',
selected: 'выбрано',
fileLabel: 'Файл',
kb: 'КБ',
voice: '🎤 Голосовое',
photo: '🖼 Фото',
video: '🎬 Видео',
file: 'Файл',
// Call
call: 'Звонок',
videoCall: 'Видеозвонок',
incomingCall: 'Входящий звонок',
incomingVideoCall: 'Входящий видеозвонок',
calling: 'Вызов...',
accept: 'Принять',
decline: 'Отклонить',
endCall: 'Завершить',
callEnded: 'Звонок завершён',
callDeclined: 'Звонок отклонён',
// Photo/video
photoVideo: 'Фото / видео',
fileBtn: 'Файл',
sendError: 'Ошибка отправки',
// New chat
newChat: 'Новый чат',
menu: 'Меню',
// Date picker
months: ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'],
weekDays: ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'],
clear: 'Очистить',
today: 'Сегодня',
// New chat
newChatTitle: 'Новый чат',
selectMembers: 'Выберите участников',
newGroup: 'Новая группа',
groupNamePlaceholder: 'Название группы...',
membersCount: 'Участники',
createGroup: 'Создать группу',
upTo200: 'До 200 участников',
findUser: 'Найти пользователя по имени или @username...',
addMembers: 'Добавить участников...',
usersNotFound: 'Пользователи не найдены',
enterNameOrUsername: 'Введите имя или @username',
next: 'Далее',
pinMessage: 'Закрепить',
unpinMessage: 'Открепить',
pinnedMessage: 'Закреплённое сообщение',
forwardMessage: 'Переслать сообщение',
forward: 'Переслать',
forwardedFrom: 'Переслано от',
replyWithQuote: 'Ответить с цитатой',
select: 'Выбрать',
sending: 'Отправка...',
scheduleMessage: 'Запланировать',
scheduleSend: 'Отправить по расписанию',
scheduled: 'Запланировано',
myStory: 'Моя история',
newStory: 'Новая история',
textStory: 'Текст',
imageStory: 'Фото',
mediaStory: 'Медиа',
typeYourStory: 'Напишите историю...',
publishStory: 'Опубликовать',
stories: 'Истории',
clearChat: 'Очистить чат',
clearChatConfirm: 'Очистить историю чата для себя? Собеседник сохранит свою историю.',
deleteChatConfirm: 'Удалить чат? Это действие нельзя отменить.',
pinChat: 'Закрепить чат',
unpinChat: 'Открепить чат',
chatCleared: 'Чат очищен',
groupSettings: 'Настройки группы',
editGroupName: 'Изменить название',
addMember: 'Добавить участника',
removeMember: 'Удалить из группы',
leaveGroup: 'Покинуть группу',
adminBadge: 'Админ',
groupDescription: 'Описание',
noDescription: 'Нет описания',
memberBadge: 'Участник',
screenShare: 'Демонстрация экрана',
stopScreenShare: 'Остановить демонстрацию',
switchCamera: 'Выбрать камеру',
showInChat: 'Показать в чате',
minimize: 'Свернуть',
volume: 'Громкость',
mute: 'Выключить микрофон',
unmute: 'Включить микрофон',
noiseSuppressionOn: 'Шумоподавление включено',
noiseSuppressionOff: 'Шумоподавление выключено',
selectMicrophone: 'Выбрать микрофон',
microphone: 'Микрофон',
rightClickVolume: 'ПКМ для регулировки громкости',
participants: 'участников',
joinCall: 'Присоединиться к звонку',
activeCall: 'Активный звонок',
groupNameRequired: 'Введите название группы',
confirmRemoveMember: 'Удалить этого участника из группы?',
yes: 'Да',
no: 'Нет',
you: 'вы',
// User profile
mediaTab: 'Медиа',
gifs: 'GIF',
filesTab: 'Файлы',
linksTab: 'Ссылки',
profileTitle: 'Профиль',
removeAvatar: 'Удалить аватар',
namePlaceholder: 'Имя',
notSpecified: 'Не указано',
onKnotSince: 'На Knot с',
cancel: 'Отмена',
confirm: 'Подтвердить',
save: 'Сохранить',
sharedPhotos: 'Общие фото и видео будут здесь',
sharedFiles: 'Общие файлы будут здесь',
sharedLinks: 'Общие ссылки будут здесь',
profileNotFound: 'Профиль не найден',
storiesTab: 'Истории',
publicationsTab: 'Публикации',
noStories: 'Публикаций пока нет',
goToMessage: 'Перейти к сообщению',
story: 'История',
commonGroups: 'Общие группы',
more: 'Ещё',
// Typing
typingText: 'печатает',
// Emoji
emojiFrequent: 'Часто',
emojiGestures: 'Жесты',
emojiEmotions: 'Эмоции',
emojiObjects: 'Предметы',
emojiFood: 'Еда',
emojiNature: 'Природа',
// Auth
login: 'Вход',
register: 'Регистрация',
latinOnly: '(латиница, нельзя изменить)',
displayNameLabel: 'Отображаемое имя',
displayNamePlaceholder: 'Ваше имя (любой язык)',
password: 'Пароль',
passwordPlaceholder: 'Введите пароль',
bioPlaceholder: 'Расскажите о себе (необязательно)',
loginBtn: 'Войти',
createAccount: 'Создать аккаунт',
testAccounts: 'Тестовые аккаунты: evgeniy, anastasia, artem, polina',
passwordForAll: 'Пароль для всех:',
dontHaveAccount: 'Нет аккаунта?',
alreadyHaveAccount: 'Уже есть аккаунт?',
passwordRequirements: 'Минимум 8 символов, буквы и цифры',
registerNow: 'Зарегистрироваться',
loginNow: 'Войти',
// File/upload
fileTooLarge: 'Файл слишком большой (макс. 200МБ)',
dropFileHere: 'Отпустите файл здесь',
uploading: 'Загрузка...',
uploadError: 'Ошибка загрузки файла',
changePhoto: 'Сменить фото',
remove: 'Удалить',
// Formatting
formatBold: 'Жирный',
formatBoldHint: '**текст**',
formatItalic: 'Курсив',
formatItalicHint: '_текст_',
formatStrike: 'Зачёркнут',
formatStrikeHint: '~текст~',
formatMono: 'Моноширинный',
formatMonoHint: '`текст`',
// Draft
draft: 'Черновик:',
// Date
datePlaceholder: 'дд.мм.гггг',
// GIF / Emoji
tenorKeyRequired: 'Для GIF нужен Tenor API ключ',
openConsoleRun: 'Откройте консоль и выполните:',
searchGifs: 'Поиск GIF...',
trending: 'Популярные',
// Misc
error: 'Ошибка',
// Friends
friends: 'Контакты',
friendRequests: 'Заявки в контакты',
friendsList: 'Список контактов',
noFriends: 'Пока нет контактов',
addFriend: 'Добавить в контакты',
removeFriend: 'Удалить из контактов',
requestSent: 'Заявка отправлена',
searchFriends: 'Поиск по @username (мин. 3 символа)',
noSearchResults: 'Пользователи не найдены',
minCharsHint: 'Введите минимум 3 символа после @',
// Story viewers
storyViewers: 'Кто просмотрел',
noViewers: 'Пока никто не посмотрел',
replyToStory: 'Ответить на историю...',
send: 'Отправить',
// Favorites
favorites: 'Избранное',
favoritesDescription: 'Сохраняйте важные сообщения здесь',
// Privacy
privacy: 'Приватность',
hideStoryViews: 'Скрывать просмотры историй',
hideStoryViewsDesc: 'Другие не увидят, что вы смотрели их историю',
// Scheduled
scheduledFor: 'Запланировано на',
messageScheduled: 'Сообщение запланировано',
scheduledDelivered: 'Сообщение отправлено для',
scheduledDeliveredAt: 'в',
scheduleIn1h: 'Через 1 час',
scheduleIn3h: 'Через 3 часа',
scheduleTomorrow: 'Завтра в 9:00',
scheduleCustom: 'Выбрать дату и время',
scheduleTime: 'Время',
scheduleDate: 'Дата',
// Last seen
lastSeenAt: 'был(а)',
justNow: 'только что',
loadStoriesError: 'Ошибка загрузки историй',
loadChatsError: 'Ошибка загрузки чатов',
loadMessagesError: 'Ошибка загрузки сообщений',
unreadMessages: 'Непрочитанные сообщения',
},
en: {
myProfile: 'My Profile',
settings: 'Settings',
aboutApp: 'About',
logout: 'Log Out',
name: 'Name',
username: 'Username',
aboutMe: 'About me',
birthday: 'Birthday',
enterName: 'Enter name',
tellAboutYourself: 'Tell about yourself',
removePhoto: 'Remove photo',
language: 'Language',
interfaceLang: 'Interface language',
theme: 'Theme',
russian: 'Русский',
english: 'English',
about: 'About',
version: 'Version',
modernMessenger: 'A modern messenger focused',
onPrivacy: 'on privacy and convenience',
modernMessengerShort: 'Knot Messenger',
searchChats: 'Search chats...',
chat: 'Chat',
group: 'Group',
noChats: 'No chats — create one!',
nothingFound: 'Nothing found',
selectChat: 'Select a chat to start messaging',
noMessages: 'No messages — write the first one!',
typing: 'typing...',
online: 'online',
wasRecently: 'was recently',
members: 'members',
files: 'files',
clearAll: 'Clear all',
messagePlaceholder: 'Message...',
message: 'Message...',
addCaption: 'Add a caption...',
searchMessages: 'Search messages...',
searchMessagesBtn: 'Search messages',
userProfile: 'User profile',
enableSound: 'Enable sound',
disableSound: 'Mute',
deleteChat: 'Delete chat',
messageDeleted: 'Message deleted',
reply: 'Reply',
copy: 'Copy',
edit: 'Edit',
delete: 'Delete',
deleteForMe: 'Delete for me',
deleteForAll: 'Delete for everyone',
deleteAlsoFor: 'Delete also for',
editing: 'Editing',
replyTo: 'Reply',
media: 'Media',
download: 'Download',
edited: 'edit.',
selected: 'selected',
fileLabel: 'File',
kb: 'KB',
voice: '🎤 Voice',
photo: '🖼 Photo',
video: '🎬 Video',
file: 'File',
call: 'Call',
videoCall: 'Video call',
incomingCall: 'Incoming call',
incomingVideoCall: 'Incoming video call',
calling: 'Calling...',
accept: 'Accept',
decline: 'Decline',
endCall: 'End call',
callEnded: 'Call ended',
callDeclined: 'Call declined',
photoVideo: 'Photo / video',
fileBtn: 'File',
sendError: 'Send error',
newChat: 'New chat',
menu: 'Menu',
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
weekDays: ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
clear: 'Clear',
today: 'Today',
newChatTitle: 'New Chat',
selectMembers: 'Select members',
newGroup: 'New Group',
groupNamePlaceholder: 'Group name...',
membersCount: 'Members',
createGroup: 'Create Group',
upTo200: 'Up to 200 members',
findUser: 'Find user by name or @username...',
addMembers: 'Add members...',
usersNotFound: 'Users not found',
enterNameOrUsername: 'Enter name or @username',
next: 'Next',
pinMessage: 'Pin',
unpinMessage: 'Unpin',
pinnedMessage: 'Pinned message',
forwardMessage: 'Forward message',
forward: 'Forward',
forwardedFrom: 'Forwarded from',
replyWithQuote: 'Reply with quote',
select: 'Select',
sending: 'Sending...',
scheduleMessage: 'Schedule',
scheduleSend: 'Send scheduled',
scheduled: 'Scheduled',
myStory: 'My story',
newStory: 'New story',
textStory: 'Text',
imageStory: 'Photo',
mediaStory: 'Media',
typeYourStory: 'Write your story...',
publishStory: 'Publish',
stories: 'Stories',
clearChat: 'Clear chat',
clearChatConfirm: 'Clear chat history for yourself? The other person will keep their history.',
deleteChatConfirm: 'Delete this chat? This action cannot be undone.',
pinChat: 'Pin chat',
unpinChat: 'Unpin chat',
chatCleared: 'Chat cleared',
groupSettings: 'Group settings',
editGroupName: 'Edit name',
addMember: 'Add member',
removeMember: 'Remove from group',
leaveGroup: 'Leave group',
adminBadge: 'Admin',
groupDescription: 'Description',
noDescription: 'No description',
memberBadge: 'Member',
screenShare: 'Screen share',
stopScreenShare: 'Stop sharing',
switchCamera: 'Switch camera',
showInChat: 'Show in chat',
minimize: 'Minimize',
volume: 'Volume',
mute: 'Mute',
unmute: 'Unmute',
noiseSuppressionOn: 'Noise suppression on',
noiseSuppressionOff: 'Noise suppression off',
selectMicrophone: 'Select microphone',
microphone: 'Microphone',
rightClickVolume: 'Right-click to adjust volume',
participants: 'participants',
joinCall: 'Join call',
activeCall: 'Active call',
groupNameRequired: 'Enter a group name',
confirmRemoveMember: 'Remove this member from the group?',
yes: 'Yes',
no: 'No',
you: 'you',
mediaTab: 'Media',
gifs: 'GIF',
filesTab: 'Files',
linksTab: 'Links',
profileTitle: 'Profile',
removeAvatar: 'Remove avatar',
namePlaceholder: 'Name',
notSpecified: 'Not specified',
onKnotSince: 'On Knot since',
cancel: 'Cancel',
confirm: 'Confirm',
save: 'Save',
sharedPhotos: 'Shared photos and videos will appear here',
sharedFiles: 'Shared files will appear here',
sharedLinks: 'Shared links will appear here',
profileNotFound: 'Profile not found',
storiesTab: 'Stories',
publicationsTab: 'Publications',
noStories: 'No stories yet',
goToMessage: 'Go to message',
story: 'Story',
commonGroups: 'Common groups',
more: 'More',
typingText: 'typing',
emojiFrequent: 'Frequent',
emojiGestures: 'Gestures',
emojiEmotions: 'Emotions',
emojiObjects: 'Objects',
emojiFood: 'Food',
emojiNature: 'Nature',
login: 'Login',
register: 'Register',
latinOnly: '(latin only, cannot be changed)',
displayNameLabel: 'Display name',
displayNamePlaceholder: 'Your name (any language)',
password: 'Password',
passwordPlaceholder: 'Enter password',
bioPlaceholder: 'Tell about yourself (optional)',
loginBtn: 'Login',
createAccount: 'Create account',
testAccounts: 'Test accounts: evgeniy, anastasia, artem, polina',
passwordForAll: 'Password for all:',
dontHaveAccount: "Don't have an account?",
alreadyHaveAccount: 'Already have an account?',
passwordRequirements: 'At least 8 characters, letters and numbers',
registerNow: 'Register',
loginNow: 'Login',
fileTooLarge: 'File too large (max 200MB)',
dropFileHere: 'Drop file here',
uploading: 'Uploading...',
uploadError: 'File upload error',
changePhoto: 'Change photo',
remove: 'Remove',
formatBold: 'Bold',
formatBoldHint: '**text**',
formatItalic: 'Italic',
formatItalicHint: '_text_',
formatStrike: 'Strikethrough',
formatStrikeHint: '~text~',
formatMono: 'Monospace',
formatMonoHint: '`text`',
draft: 'Draft:',
datePlaceholder: 'dd.mm.yyyy',
tenorKeyRequired: 'Tenor API key required for GIFs',
openConsoleRun: 'Open console and run:',
searchGifs: 'Search GIFs...',
trending: 'Trending',
error: 'Error',
friends: 'Contacts',
friendRequests: 'Contact requests',
friendsList: 'Contacts list',
noFriends: 'No contacts yet',
addFriend: 'Add to contacts',
removeFriend: 'Remove from contacts',
requestSent: 'Request sent',
searchFriends: 'Search by @username (min. 3 chars)',
noSearchResults: 'No users found',
minCharsHint: 'Enter at least 3 characters after @',
storyViewers: 'Who viewed',
noViewers: 'No one viewed yet',
replyToStory: 'Reply to story...',
send: 'Send',
favorites: 'Favorites',
favoritesDescription: 'Save important messages here',
privacy: 'Privacy',
hideStoryViews: 'Hide story views',
hideStoryViewsDesc: 'Others won\'t see that you viewed their story',
scheduledFor: 'Scheduled for',
messageScheduled: 'Message scheduled',
scheduledDelivered: 'Message sent to',
scheduledDeliveredAt: 'at',
scheduleIn1h: 'In 1 hour',
scheduleIn3h: 'In 3 hours',
scheduleTomorrow: 'Tomorrow at 9:00',
scheduleCustom: 'Choose date & time',
scheduleTime: 'Time',
scheduleDate: 'Date',
lastSeenAt: 'was',
justNow: 'just now',
loadStoriesError: 'Error loading stories',
loadChatsError: 'Error loading chats',
loadMessagesError: 'Error loading messages',
unreadMessages: 'Unread messages',
},
} as const;
export type TranslationKey = keyof typeof translations.ru;
type TranslationValue<K extends TranslationKey> = (typeof translations.ru)[K];
interface LangState {
lang: Lang;
setLang: (lang: Lang) => void;
t: <K extends TranslationKey>(key: K) => TranslationValue<K>;
}
export const useLang = create<LangState>((set, get) => ({
lang: (localStorage.getItem(LANG_KEY) as Lang) || 'ru',
setLang: (lang) => {
localStorage.setItem(LANG_KEY, lang);
set({ lang });
},
t: (key) => {
const { lang } = get();
return (translations[lang][key] ?? translations.ru[key] ?? key) as TranslationValue<typeof key>;
},
}));