Files
forkmessager/client-web/src/core/infrastructure/i18n.ts
Халимов Рустам a42007df2d Дизайн
2026-04-02 14:57:32 +03:00

592 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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: 'Имя пользователя',
chats: 'Чаты',
calls: 'Звонки',
contacts: 'Контакты',
archive: 'Архив',
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: 'Выберите чат для начала общения',
selectChatTitle: 'Выберите чат',
selectChatSubtext: 'Начните общение прямо сейчас. Все ваши сообщения защищены сквозным шифрованием.',
newMessage: 'Новое сообщение',
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: 'Очистить историю чата для себя? Собеседник сохранит свою историю.',
clearHistory: 'Очистить историю',
clearHistoryConfirm: 'Очистить историю?',
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 символа)',
searchResults: 'Результаты поиска',
noSearchResults: 'Пользователи не найдены',
minCharsHint: 'Введите минимум 3 символа после @',
selectContactToChat: 'Выберите контакт из списка, чтобы начать общение, или воспользуйтесь поиском для поиска новых людей.',
backToChats: 'Назад к чатам',
// 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: 'Непрочитанные сообщения',
attachmentDiscardConfirm: 'У вас есть прикрепленные вложения. Если вы перейдете в другой чат, они будут потеряны. Продолжить?',
},
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',
chats: 'Chats',
calls: 'Calls',
contacts: 'Contacts',
archive: 'Archive',
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',
selectChatTitle: 'Select a chat',
selectChatSubtext: 'Start communicating right now. All your messages are protected with end-to-end encryption.',
newMessage: 'New message',
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.',
clearHistory: 'Clear history',
clearHistoryConfirm: 'Clear 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)',
searchResults: 'Search Results',
noSearchResults: 'No users found',
minCharsHint: 'Enter at least 3 characters after @',
selectContactToChat: 'Select a contact from the list to start a conversation or use search to find new people.',
backToChats: 'Back to Chats',
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',
attachmentDiscardConfirm: 'You have attached files. If you switch to another chat, they will be lost. Continue?',
},
} 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>;
},
}));