localization #2
160
LOCALIZATION.md
Normal file
160
LOCALIZATION.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Система локализации Telegram Tinder Bot
|
||||
|
||||
## Обзор
|
||||
|
||||
Система локализации обеспечивает многоязычную поддержку бота с использованием i18next для интерфейса и DeepSeek AI для перевода анкет пользователей.
|
||||
|
||||
## Архитектура
|
||||
|
||||
### Компоненты системы
|
||||
|
||||
1. **LocalizationService** - основной сервис локализации интерфейса
|
||||
2. **DeepSeekTranslationService** - сервис для перевода анкет с помощью AI
|
||||
3. **TranslationController** - контроллер для управления переводами
|
||||
4. **Файлы переводов** - JSON файлы с переводами для каждого языка
|
||||
|
||||
### Поддерживаемые языки
|
||||
|
||||
- 🇷🇺 Русский (ru) - по умолчанию
|
||||
- 🇺🇸 Английский (en)
|
||||
- 🇪🇸 Испанский (es)
|
||||
- 🇫🇷 Французский (fr)
|
||||
- 🇩🇪 Немецкий (de)
|
||||
- 🇮🇹 Итальянский (it)
|
||||
- 🇵🇹 Португальский (pt)
|
||||
- 🇨🇳 Китайский (zh)
|
||||
- 🇯🇵 Японский (ja)
|
||||
- 🇰🇷 Корейский (ko)
|
||||
|
||||
## Использование
|
||||
|
||||
### Локализация интерфейса
|
||||
|
||||
```typescript
|
||||
import { t } from '../services/localizationService';
|
||||
|
||||
// Простой перевод
|
||||
const message = t('welcome.greeting');
|
||||
|
||||
// Перевод с параметрами
|
||||
const message = t('profile.ageRange', { min: 18, max: 65 });
|
||||
|
||||
// Установка языка пользователя
|
||||
localizationService.setLanguage('en');
|
||||
```
|
||||
|
||||
### Структура файлов переводов
|
||||
|
||||
```json
|
||||
{
|
||||
"welcome": {
|
||||
"greeting": "Добро пожаловать в Telegram Tinder Bot! 💕",
|
||||
"description": "Найди свою вторую половинку прямо здесь!"
|
||||
},
|
||||
"profile": {
|
||||
"name": "Имя",
|
||||
"age": "Возраст",
|
||||
"bio": "О себе"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Перевод анкет (Premium функция)
|
||||
|
||||
```typescript
|
||||
import DeepSeekTranslationService from '../services/deepSeekTranslationService';
|
||||
|
||||
const translationService = DeepSeekTranslationService.getInstance();
|
||||
|
||||
// Перевод текста анкеты
|
||||
const result = await translationService.translateProfile({
|
||||
text: "Привет! Я люблю путешествовать и читать книги.",
|
||||
targetLanguage: 'en',
|
||||
sourceLanguage: 'ru'
|
||||
});
|
||||
```
|
||||
|
||||
## Настройка
|
||||
|
||||
### Переменные окружения
|
||||
|
||||
```env
|
||||
# DeepSeek API для перевода анкет
|
||||
DEEPSEEK_API_KEY=your_deepseek_api_key_here
|
||||
```
|
||||
|
||||
### База данных
|
||||
|
||||
Таблица `users` содержит поле `language` для хранения предпочитаемого языка пользователя:
|
||||
|
||||
```sql
|
||||
ALTER TABLE users
|
||||
ADD COLUMN language VARCHAR(5) DEFAULT 'ru';
|
||||
```
|
||||
|
||||
## Функции
|
||||
|
||||
### Автоматическое определение языка
|
||||
|
||||
- При регистрации пользователя язык определяется по `language_code` из Telegram
|
||||
- Пользователь может изменить язык в настройках
|
||||
- Поддерживается определение языка текста для перевода
|
||||
|
||||
### Премиум функции перевода
|
||||
|
||||
- **Перевод анкет** - доступен только для премиум пользователей
|
||||
- **AI-перевод** - используется DeepSeek API для качественного перевода
|
||||
- **Контекстный перевод** - сохраняется тон и стиль исходного текста
|
||||
|
||||
### Клавиатуры и меню
|
||||
|
||||
Все кнопки и меню автоматически локализуются на основе языка пользователя:
|
||||
|
||||
```typescript
|
||||
// Пример создания локализованной клавиатуры
|
||||
public getLanguageSelectionKeyboard() {
|
||||
return {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '🇷🇺 Русский', callback_data: 'set_language_ru' },
|
||||
{ text: '🇺🇸 English', callback_data: 'set_language_en' }
|
||||
]
|
||||
]
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Расширение
|
||||
|
||||
### Добавление нового языка
|
||||
|
||||
1. Создать файл перевода `src/locales/{language_code}.json`
|
||||
2. Добавить язык в массив поддерживаемых языков в `LocalizationService`
|
||||
3. Обновить ограничение в базе данных
|
||||
4. Добавить кнопку в меню выбора языка
|
||||
|
||||
### Добавление новых переводов
|
||||
|
||||
1. Добавить ключи в основной файл перевода (`ru.json`)
|
||||
2. Перевести на все поддерживаемые языки
|
||||
3. Использовать в коде через функцию `t()`
|
||||
|
||||
## Безопасность
|
||||
|
||||
- API ключ DeepSeek хранится в переменных окружения
|
||||
- Проверка премиум статуса перед доступом к переводу
|
||||
- Ограничение по количеству запросов к API
|
||||
- Таймауты для предотвращения зависания
|
||||
|
||||
## Мониторинг
|
||||
|
||||
- Логирование ошибок перевода
|
||||
- Отслеживание использования API
|
||||
- Статистика по языкам пользователей
|
||||
|
||||
## Производительность
|
||||
|
||||
- Кэширование переводов интерфейса
|
||||
- Ленивая загрузка файлов переводов
|
||||
- Асинхронная обработка запросов к DeepSeek API
|
||||
- Индексы в базе данных для быстрого поиска по языку
|
||||
49
package-lock.json
generated
49
package-lock.json
generated
@@ -10,8 +10,9 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node-telegram-bot-api": "^0.64.11",
|
||||
"axios": "^1.6.2",
|
||||
"axios": "^1.12.1",
|
||||
"dotenv": "^16.6.1",
|
||||
"i18next": "^25.5.2",
|
||||
"node-telegram-bot-api": "^0.64.0",
|
||||
"pg": "^8.11.3",
|
||||
"sharp": "^0.32.6",
|
||||
@@ -438,6 +439,14 @@
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
||||
@@ -1349,9 +1358,9 @@
|
||||
"integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz",
|
||||
"integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==",
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.1.tgz",
|
||||
"integrity": "sha512-Kn4kbSXpkFHCGE6rBFNwIv0GQs4AvDT80jlveJDKFxjbTYMUeB4QtsdPCv6H8Cm19Je7IU6VFtRl2zWZI0rudQ==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
@@ -2993,6 +3002,36 @@
|
||||
"node": ">=10.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "25.5.2",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.5.2.tgz",
|
||||
"integrity": "sha512-lW8Zeh37i/o0zVr+NoCHfNnfvVw+M6FQbRp36ZZ/NyHDJ3NJVpp2HhAUyU9WafL5AssymNoOjMRB48mmx2P6Hw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com/i18next.html"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
@@ -6255,7 +6294,7 @@
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
||||
@@ -12,8 +12,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node-telegram-bot-api": "^0.64.11",
|
||||
"axios": "^1.6.2",
|
||||
"axios": "^1.12.1",
|
||||
"dotenv": "^16.6.1",
|
||||
"i18next": "^25.5.2",
|
||||
"node-telegram-bot-api": "^0.64.0",
|
||||
"pg": "^8.11.3",
|
||||
"sharp": "^0.32.6",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { testConnection, query } from './database/connection';
|
||||
import { ProfileService } from './services/profileService';
|
||||
import { MatchingService } from './services/matchingService';
|
||||
import { NotificationService } from './services/notificationService';
|
||||
import LocalizationService from './services/localizationService';
|
||||
import { CommandHandlers } from './handlers/commandHandlers';
|
||||
import { CallbackHandlers } from './handlers/callbackHandlers';
|
||||
import { MessageHandlers } from './handlers/messageHandlers';
|
||||
@@ -13,6 +14,7 @@ class TelegramTinderBot {
|
||||
private profileService: ProfileService;
|
||||
private matchingService: MatchingService;
|
||||
private notificationService: NotificationService;
|
||||
private localizationService: LocalizationService;
|
||||
private commandHandlers: CommandHandlers;
|
||||
private callbackHandlers: CallbackHandlers;
|
||||
private messageHandlers: MessageHandlers;
|
||||
@@ -27,6 +29,7 @@ class TelegramTinderBot {
|
||||
this.profileService = new ProfileService();
|
||||
this.matchingService = new MatchingService();
|
||||
this.notificationService = new NotificationService(this.bot);
|
||||
this.localizationService = LocalizationService.getInstance();
|
||||
|
||||
this.commandHandlers = new CommandHandlers(this.bot);
|
||||
this.messageHandlers = new MessageHandlers(this.bot);
|
||||
@@ -41,6 +44,9 @@ class TelegramTinderBot {
|
||||
try {
|
||||
console.log('🚀 Initializing Telegram Tinder Bot...');
|
||||
|
||||
// Инициализация сервиса локализации
|
||||
await this.localizationService.initialize();
|
||||
|
||||
// Проверка подключения к базе данных
|
||||
const dbConnected = await testConnection();
|
||||
if (!dbConnected) {
|
||||
|
||||
196
src/controllers/translationController.ts
Normal file
196
src/controllers/translationController.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import LocalizationService, { t } from '../services/localizationService';
|
||||
import DeepSeekTranslationService from '../services/deepSeekTranslationService';
|
||||
import { VipService } from '../services/vipService';
|
||||
|
||||
export class TranslationController {
|
||||
private localizationService: LocalizationService;
|
||||
private translationService: DeepSeekTranslationService;
|
||||
private vipService: VipService;
|
||||
|
||||
constructor() {
|
||||
this.localizationService = LocalizationService.getInstance();
|
||||
this.translationService = DeepSeekTranslationService.getInstance();
|
||||
this.vipService = new VipService();
|
||||
}
|
||||
|
||||
// Показать меню выбора языка
|
||||
public getLanguageSelectionKeyboard() {
|
||||
return {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '🇷🇺 Русский', callback_data: 'set_language_ru' },
|
||||
{ text: '🇺🇸 English', callback_data: 'set_language_en' }
|
||||
],
|
||||
[
|
||||
{ text: '🇪🇸 Español', callback_data: 'set_language_es' },
|
||||
{ text: '🇫🇷 Français', callback_data: 'set_language_fr' }
|
||||
],
|
||||
[
|
||||
{ text: '🇩🇪 Deutsch', callback_data: 'set_language_de' },
|
||||
{ text: '🇮🇹 Italiano', callback_data: 'set_language_it' }
|
||||
],
|
||||
[{ text: t('buttons.back'), callback_data: 'back_to_settings' }]
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// Обработать установку языка
|
||||
public async handleLanguageSelection(telegramId: number, languageCode: string): Promise<string> {
|
||||
try {
|
||||
// Здесь должно быть обновление в базе данных
|
||||
// await userService.updateUserLanguage(telegramId, languageCode);
|
||||
|
||||
this.localizationService.setLanguage(languageCode);
|
||||
|
||||
const languageNames: { [key: string]: string } = {
|
||||
'ru': '🇷🇺 Русский',
|
||||
'en': '🇺🇸 English',
|
||||
'es': '🇪🇸 Español',
|
||||
'fr': '🇫🇷 Français',
|
||||
'de': '🇩🇪 Deutsch',
|
||||
'it': '🇮🇹 Italiano'
|
||||
};
|
||||
|
||||
return `✅ Язык интерфейса изменен на ${languageNames[languageCode] || languageCode}`;
|
||||
} catch (error) {
|
||||
console.error('Error setting language:', error);
|
||||
return t('errors.serverError');
|
||||
}
|
||||
}
|
||||
|
||||
// Получить кнопку перевода анкеты
|
||||
public getTranslateProfileButton(telegramId: number, profileUserId: number) {
|
||||
return {
|
||||
inline_keyboard: [
|
||||
[{ text: t('vip.translateProfile'), callback_data: `translate_profile_${profileUserId}` }]
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// Обработать запрос на перевод анкеты
|
||||
public async handleProfileTranslation(
|
||||
telegramId: number,
|
||||
profileUserId: number,
|
||||
targetLanguage: string
|
||||
): Promise<{ success: boolean; message: string; translatedProfile?: any }> {
|
||||
try {
|
||||
// Проверяем премиум статус
|
||||
const isPremium = await this.vipService.checkPremiumStatus(telegramId.toString());
|
||||
if (!isPremium) {
|
||||
return {
|
||||
success: false,
|
||||
message: t('translation.premiumOnly')
|
||||
};
|
||||
}
|
||||
|
||||
// Получаем профиль для перевода
|
||||
const profile = await this.getProfileForTranslation(profileUserId);
|
||||
if (!profile) {
|
||||
return {
|
||||
success: false,
|
||||
message: t('errors.profileNotFound')
|
||||
};
|
||||
}
|
||||
|
||||
// Переводим профиль
|
||||
const translatedProfile = await this.translateProfileData(profile, targetLanguage);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: t('translation.translated'),
|
||||
translatedProfile
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Profile translation error:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: t('translation.error')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Получить профиль для перевода (заглушка - нужна реализация)
|
||||
private async getProfileForTranslation(userId: number): Promise<any> {
|
||||
// TODO: Реализовать получение профиля из базы данных
|
||||
// Это должно быть интегрировано с существующим ProfileService
|
||||
return {
|
||||
name: 'Sample Name',
|
||||
bio: 'Sample bio text',
|
||||
city: 'Sample City',
|
||||
hobbies: 'Sample hobbies',
|
||||
datingGoal: 'relationship'
|
||||
};
|
||||
}
|
||||
|
||||
// Перевести данные профиля
|
||||
private async translateProfileData(profile: any, targetLanguage: string): Promise<any> {
|
||||
const fieldsToTranslate = ['bio', 'hobbies'];
|
||||
const translatedProfile = { ...profile };
|
||||
|
||||
for (const field of fieldsToTranslate) {
|
||||
if (profile[field] && typeof profile[field] === 'string') {
|
||||
try {
|
||||
const sourceLanguage = this.translationService.detectLanguage(profile[field]);
|
||||
|
||||
// Пропускаем перевод, если исходный и целевой языки совпадают
|
||||
if (sourceLanguage === targetLanguage) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const translation = await this.translationService.translateProfile({
|
||||
text: profile[field],
|
||||
targetLanguage,
|
||||
sourceLanguage
|
||||
});
|
||||
|
||||
translatedProfile[field] = translation.translatedText;
|
||||
} catch (error) {
|
||||
console.error(`Error translating field ${field}:`, error);
|
||||
// Оставляем оригинальный текст при ошибке
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return translatedProfile;
|
||||
}
|
||||
|
||||
// Форматировать переведенный профиль для отображения
|
||||
public formatTranslatedProfile(profile: any, originalLanguage: string, targetLanguage: string): string {
|
||||
const languageNames: { [key: string]: string } = {
|
||||
'ru': '🇷🇺 Русский',
|
||||
'en': '🇺🇸 English',
|
||||
'es': '🇪🇸 Español',
|
||||
'fr': '🇫🇷 Français',
|
||||
'de': '🇩🇪 Deutsch',
|
||||
'it': '🇮🇹 Italiano'
|
||||
};
|
||||
|
||||
let text = `🌐 ${t('translation.translated')}\n`;
|
||||
text += `📝 ${originalLanguage} → ${targetLanguage}\n\n`;
|
||||
|
||||
text += `👤 ${t('profile.name')}: ${profile.name}\n`;
|
||||
text += `📍 ${t('profile.city')}: ${profile.city}\n\n`;
|
||||
|
||||
if (profile.bio) {
|
||||
text += `💭 ${t('profile.bio')}:\n${profile.bio}\n\n`;
|
||||
}
|
||||
|
||||
if (profile.hobbies) {
|
||||
text += `🎯 ${t('profile.hobbies')}:\n${profile.hobbies}\n\n`;
|
||||
}
|
||||
|
||||
if (profile.datingGoal) {
|
||||
text += `💕 ${t('profile.datingGoal')}: ${t(`profile.${profile.datingGoal}`)}\n`;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
// Проверить доступность сервиса перевода
|
||||
public async checkTranslationServiceStatus(): Promise<boolean> {
|
||||
return await this.translationService.checkServiceAvailability();
|
||||
}
|
||||
}
|
||||
|
||||
export default TranslationController;
|
||||
14
src/database/migrations/add_language_support.sql
Normal file
14
src/database/migrations/add_language_support.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
-- Добавляем поле языка пользователя в таблицу users
|
||||
ALTER TABLE users
|
||||
ADD COLUMN language VARCHAR(5) DEFAULT 'ru';
|
||||
|
||||
-- Создаем индекс для оптимизации запросов по языку
|
||||
CREATE INDEX idx_users_language ON users(language);
|
||||
|
||||
-- Добавляем ограничение на поддерживаемые языки
|
||||
ALTER TABLE users
|
||||
ADD CONSTRAINT check_users_language
|
||||
CHECK (language IN ('ru', 'en', 'es', 'fr', 'de', 'it', 'pt', 'zh', 'ja', 'ko'));
|
||||
|
||||
-- Обновляем существующих пользователей
|
||||
UPDATE users SET language = 'ru' WHERE language IS NULL;
|
||||
@@ -8,6 +8,8 @@ import { ProfileEditController } from '../controllers/profileEditController';
|
||||
import { EnhancedChatHandlers } from './enhancedChatHandlers';
|
||||
import { VipController } from '../controllers/vipController';
|
||||
import { VipService } from '../services/vipService';
|
||||
import { TranslationController } from '../controllers/translationController';
|
||||
import { t } from '../services/localizationService';
|
||||
|
||||
export class CallbackHandlers {
|
||||
private bot: TelegramBot;
|
||||
@@ -19,6 +21,7 @@ export class CallbackHandlers {
|
||||
private enhancedChatHandlers: EnhancedChatHandlers;
|
||||
private vipController: VipController;
|
||||
private vipService: VipService;
|
||||
private translationController: TranslationController;
|
||||
|
||||
constructor(bot: TelegramBot, messageHandlers: MessageHandlers) {
|
||||
this.bot = bot;
|
||||
@@ -30,6 +33,7 @@ export class CallbackHandlers {
|
||||
this.enhancedChatHandlers = new EnhancedChatHandlers(bot);
|
||||
this.vipController = new VipController(bot);
|
||||
this.vipService = new VipService();
|
||||
this.translationController = new TranslationController();
|
||||
}
|
||||
|
||||
register(): void {
|
||||
@@ -243,6 +247,19 @@ export class CallbackHandlers {
|
||||
await this.handleVipDislike(chatId, telegramId, targetTelegramId);
|
||||
}
|
||||
|
||||
// Настройки языка и переводы
|
||||
else if (data === 'language_settings') {
|
||||
await this.handleLanguageSettings(chatId, telegramId);
|
||||
} else if (data.startsWith('set_language_')) {
|
||||
const languageCode = data.replace('set_language_', '');
|
||||
await this.handleSetLanguage(chatId, telegramId, languageCode);
|
||||
} else if (data.startsWith('translate_profile_')) {
|
||||
const profileUserId = parseInt(data.replace('translate_profile_', ''));
|
||||
await this.handleTranslateProfile(chatId, telegramId, profileUserId);
|
||||
} else if (data === 'back_to_settings') {
|
||||
await this.handleSettings(chatId, telegramId);
|
||||
}
|
||||
|
||||
else {
|
||||
await this.bot.answerCallbackQuery(query.id, {
|
||||
text: 'Функция в разработке!',
|
||||
@@ -721,8 +738,8 @@ export class CallbackHandlers {
|
||||
{ text: '🔔 Уведомления', callback_data: 'notification_settings' }
|
||||
],
|
||||
[
|
||||
{ text: '<EFBFBD> Статистика', callback_data: 'view_stats' },
|
||||
{ text: '👀 Кто смотрел', callback_data: 'view_profile_viewers' }
|
||||
{ text: '🌐 Язык интерфейса', callback_data: 'language_settings' },
|
||||
{ text: '📊 Статистика', callback_data: 'view_stats' }
|
||||
],
|
||||
[
|
||||
{ text: '<27>🚫 Скрыть профиль', callback_data: 'hide_profile' },
|
||||
@@ -2016,4 +2033,64 @@ export class CallbackHandlers {
|
||||
console.error('VIP Dislike error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Обработчики языковых настроек
|
||||
async handleLanguageSettings(chatId: number, telegramId: string): Promise<void> {
|
||||
try {
|
||||
const keyboard = this.translationController.getLanguageSelectionKeyboard();
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
`🌐 ${t('commands.settings')} - Выбор языка\n\nВыберите язык интерфейса:`,
|
||||
{ reply_markup: keyboard }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Language settings error:', error);
|
||||
await this.bot.sendMessage(chatId, t('errors.serverError'));
|
||||
}
|
||||
}
|
||||
|
||||
async handleSetLanguage(chatId: number, telegramId: string, languageCode: string): Promise<void> {
|
||||
try {
|
||||
const result = await this.translationController.handleLanguageSelection(parseInt(telegramId), languageCode);
|
||||
await this.bot.sendMessage(chatId, result);
|
||||
|
||||
// Показать обновленное меню настроек
|
||||
setTimeout(() => {
|
||||
this.handleSettings(chatId, telegramId);
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.error('Set language error:', error);
|
||||
await this.bot.sendMessage(chatId, t('errors.serverError'));
|
||||
}
|
||||
}
|
||||
|
||||
async handleTranslateProfile(chatId: number, telegramId: string, profileUserId: number): Promise<void> {
|
||||
try {
|
||||
// Показать индикатор загрузки
|
||||
await this.bot.sendMessage(chatId, t('translation.translating'));
|
||||
|
||||
// Получить текущий язык пользователя
|
||||
const userLanguage = 'ru'; // TODO: получить из базы данных
|
||||
|
||||
const result = await this.translationController.handleProfileTranslation(
|
||||
parseInt(telegramId),
|
||||
profileUserId,
|
||||
userLanguage
|
||||
);
|
||||
|
||||
if (result.success && result.translatedProfile) {
|
||||
const formattedProfile = this.translationController.formatTranslatedProfile(
|
||||
result.translatedProfile,
|
||||
'auto',
|
||||
userLanguage
|
||||
);
|
||||
await this.bot.sendMessage(chatId, formattedProfile);
|
||||
} else {
|
||||
await this.bot.sendMessage(chatId, result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Translate profile error:', error);
|
||||
await this.bot.sendMessage(chatId, t('translation.error'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
101
src/locales/en.json
Normal file
101
src/locales/en.json
Normal file
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"welcome": {
|
||||
"greeting": "Welcome to Telegram Tinder Bot! 💕",
|
||||
"description": "Find your soulmate right here!",
|
||||
"getStarted": "Get Started"
|
||||
},
|
||||
"profile": {
|
||||
"create": "Create Profile",
|
||||
"edit": "Edit Profile",
|
||||
"view": "View Profile",
|
||||
"name": "Name",
|
||||
"age": "Age",
|
||||
"city": "City",
|
||||
"bio": "About",
|
||||
"photos": "Photos",
|
||||
"gender": "Gender",
|
||||
"lookingFor": "Looking for",
|
||||
"datingGoal": "Dating Goal",
|
||||
"hobbies": "Hobbies",
|
||||
"lifestyle": "Lifestyle",
|
||||
"male": "Male",
|
||||
"female": "Female",
|
||||
"both": "Both",
|
||||
"relationship": "Relationship",
|
||||
"friendship": "Friendship",
|
||||
"dating": "Dating",
|
||||
"hookup": "Hookup",
|
||||
"marriage": "Marriage",
|
||||
"networking": "Networking",
|
||||
"travel": "Travel",
|
||||
"business": "Business",
|
||||
"other": "Other"
|
||||
},
|
||||
"search": {
|
||||
"title": "Browse Profiles",
|
||||
"noProfiles": "No more profiles! Try again later.",
|
||||
"like": "❤️ Like",
|
||||
"dislike": "👎 Pass",
|
||||
"superLike": "⭐ Super Like",
|
||||
"match": "It's a match! 🎉"
|
||||
},
|
||||
"vip": {
|
||||
"title": "VIP Search",
|
||||
"premiumRequired": "This feature is available for premium users only",
|
||||
"filters": "Filters",
|
||||
"ageRange": "Age Range",
|
||||
"cityFilter": "City",
|
||||
"datingGoalFilter": "Dating Goal",
|
||||
"hobbiesFilter": "Hobbies",
|
||||
"lifestyleFilter": "Lifestyle",
|
||||
"applyFilters": "Apply Filters",
|
||||
"clearFilters": "Clear Filters",
|
||||
"noResults": "No profiles found with your filters",
|
||||
"translateProfile": "🌐 Translate Profile"
|
||||
},
|
||||
"premium": {
|
||||
"title": "Premium Subscription",
|
||||
"features": "Premium features:",
|
||||
"vipSearch": "• VIP search with filters",
|
||||
"profileTranslation": "• Profile translation to your language",
|
||||
"unlimitedLikes": "• Unlimited likes",
|
||||
"superLikes": "• Extra super likes",
|
||||
"price": "Price: $4.99/month",
|
||||
"activate": "Activate Premium"
|
||||
},
|
||||
"translation": {
|
||||
"translating": "Translating profile...",
|
||||
"translated": "Profile translated:",
|
||||
"error": "Translation error. Please try again later.",
|
||||
"premiumOnly": "Translation is available for premium users only"
|
||||
},
|
||||
"commands": {
|
||||
"start": "Main Menu",
|
||||
"profile": "My Profile",
|
||||
"search": "Browse",
|
||||
"vip": "VIP Search",
|
||||
"matches": "Matches",
|
||||
"premium": "Premium",
|
||||
"settings": "Settings",
|
||||
"help": "Help"
|
||||
},
|
||||
"buttons": {
|
||||
"back": "« Back",
|
||||
"next": "Next »",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"yes": "Yes",
|
||||
"no": "No"
|
||||
},
|
||||
"errors": {
|
||||
"profileNotFound": "Profile not found",
|
||||
"profileIncomplete": "Please complete your profile",
|
||||
"ageInvalid": "Please enter a valid age (18-100)",
|
||||
"photoRequired": "Please add at least one photo",
|
||||
"networkError": "Network error. Please try again later.",
|
||||
"serverError": "Server error. Please try again later."
|
||||
}
|
||||
}
|
||||
101
src/locales/ru.json
Normal file
101
src/locales/ru.json
Normal file
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"welcome": {
|
||||
"greeting": "Добро пожаловать в Telegram Tinder Bot! 💕",
|
||||
"description": "Найди свою вторую половинку прямо здесь!",
|
||||
"getStarted": "Начать знакомство"
|
||||
},
|
||||
"profile": {
|
||||
"create": "Создать анкету",
|
||||
"edit": "Редактировать анкету",
|
||||
"view": "Посмотреть анкету",
|
||||
"name": "Имя",
|
||||
"age": "Возраст",
|
||||
"city": "Город",
|
||||
"bio": "О себе",
|
||||
"photos": "Фотографии",
|
||||
"gender": "Пол",
|
||||
"lookingFor": "Ищу",
|
||||
"datingGoal": "Цель знакомства",
|
||||
"hobbies": "Хобби",
|
||||
"lifestyle": "Образ жизни",
|
||||
"male": "Мужской",
|
||||
"female": "Женский",
|
||||
"both": "Не важно",
|
||||
"relationship": "Серьезные отношения",
|
||||
"friendship": "Дружба",
|
||||
"dating": "Свидания",
|
||||
"hookup": "Интрижка",
|
||||
"marriage": "Брак",
|
||||
"networking": "Общение",
|
||||
"travel": "Путешествия",
|
||||
"business": "Бизнес",
|
||||
"other": "Другое"
|
||||
},
|
||||
"search": {
|
||||
"title": "Поиск анкет",
|
||||
"noProfiles": "Анкеты закончились! Попробуйте позже.",
|
||||
"like": "❤️ Нравится",
|
||||
"dislike": "👎 Не нравится",
|
||||
"superLike": "⭐ Супер лайк",
|
||||
"match": "Это взаимность! 🎉"
|
||||
},
|
||||
"vip": {
|
||||
"title": "VIP Поиск",
|
||||
"premiumRequired": "Функция доступна только для премиум пользователей",
|
||||
"filters": "Фильтры",
|
||||
"ageRange": "Возрастной диапазон",
|
||||
"cityFilter": "Город",
|
||||
"datingGoalFilter": "Цель знакомства",
|
||||
"hobbiesFilter": "Хобби",
|
||||
"lifestyleFilter": "Образ жизни",
|
||||
"applyFilters": "Применить фильтры",
|
||||
"clearFilters": "Очистить фильтры",
|
||||
"noResults": "По вашим фильтрам никого не найдено",
|
||||
"translateProfile": "🌐 Перевести анкету"
|
||||
},
|
||||
"premium": {
|
||||
"title": "Премиум подписка",
|
||||
"features": "Возможности премиум:",
|
||||
"vipSearch": "• VIP поиск с фильтрами",
|
||||
"profileTranslation": "• Перевод анкет на ваш язык",
|
||||
"unlimitedLikes": "• Безлимитные лайки",
|
||||
"superLikes": "• Дополнительные супер-лайки",
|
||||
"price": "Стоимость: 299₽/месяц",
|
||||
"activate": "Активировать премиум"
|
||||
},
|
||||
"translation": {
|
||||
"translating": "Переводим анкету...",
|
||||
"translated": "Анкета переведена:",
|
||||
"error": "Ошибка перевода. Попробуйте позже.",
|
||||
"premiumOnly": "Перевод доступен только для премиум пользователей"
|
||||
},
|
||||
"commands": {
|
||||
"start": "Главное меню",
|
||||
"profile": "Моя анкета",
|
||||
"search": "Поиск",
|
||||
"vip": "VIP поиск",
|
||||
"matches": "Взаимности",
|
||||
"premium": "Премиум",
|
||||
"settings": "Настройки",
|
||||
"help": "Помощь"
|
||||
},
|
||||
"buttons": {
|
||||
"back": "« Назад",
|
||||
"next": "Далее »",
|
||||
"save": "Сохранить",
|
||||
"cancel": "Отмена",
|
||||
"confirm": "Подтвердить",
|
||||
"edit": "Редактировать",
|
||||
"delete": "Удалить",
|
||||
"yes": "Да",
|
||||
"no": "Нет"
|
||||
},
|
||||
"errors": {
|
||||
"profileNotFound": "Анкета не найдена",
|
||||
"profileIncomplete": "Заполните анкету полностью",
|
||||
"ageInvalid": "Введите корректный возраст (18-100)",
|
||||
"photoRequired": "Добавьте хотя бы одну фотографию",
|
||||
"networkError": "Ошибка сети. Попробуйте позже.",
|
||||
"serverError": "Ошибка сервера. Попробуйте позже."
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ export interface UserData {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
languageCode?: string;
|
||||
language?: string; // Предпочитаемый язык интерфейса
|
||||
isActive: boolean;
|
||||
createdAt: Date;
|
||||
lastActiveAt: Date;
|
||||
@@ -17,6 +18,7 @@ export class User {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
languageCode?: string;
|
||||
language: string; // Предпочитаемый язык интерфейса
|
||||
isActive: boolean;
|
||||
createdAt: Date;
|
||||
lastActiveAt: Date;
|
||||
@@ -28,6 +30,7 @@ export class User {
|
||||
this.firstName = data.firstName;
|
||||
this.lastName = data.lastName;
|
||||
this.languageCode = data.languageCode || 'en';
|
||||
this.language = data.language || 'ru'; // Язык интерфейса по умолчанию
|
||||
this.isActive = data.isActive;
|
||||
this.createdAt = data.createdAt;
|
||||
this.lastActiveAt = data.lastActiveAt;
|
||||
@@ -67,4 +70,14 @@ export class User {
|
||||
this.isActive = true;
|
||||
this.updateLastActive();
|
||||
}
|
||||
|
||||
// Установить язык интерфейса
|
||||
setLanguage(language: string): void {
|
||||
this.language = language;
|
||||
}
|
||||
|
||||
// Получить язык интерфейса
|
||||
getLanguage(): string {
|
||||
return this.language;
|
||||
}
|
||||
}
|
||||
171
src/services/deepSeekTranslationService.ts
Normal file
171
src/services/deepSeekTranslationService.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export interface TranslationRequest {
|
||||
text: string;
|
||||
targetLanguage: string;
|
||||
sourceLanguage?: string;
|
||||
}
|
||||
|
||||
export interface TranslationResponse {
|
||||
translatedText: string;
|
||||
sourceLanguage: string;
|
||||
targetLanguage: string;
|
||||
}
|
||||
|
||||
export class DeepSeekTranslationService {
|
||||
private static instance: DeepSeekTranslationService;
|
||||
private apiKey: string;
|
||||
private apiUrl: string = 'https://api.deepseek.com/v1/chat/completions';
|
||||
|
||||
private constructor() {
|
||||
this.apiKey = process.env.DEEPSEEK_API_KEY || '';
|
||||
if (!this.apiKey) {
|
||||
console.warn('⚠️ DEEPSEEK_API_KEY not found in environment variables');
|
||||
}
|
||||
}
|
||||
|
||||
public static getInstance(): DeepSeekTranslationService {
|
||||
if (!DeepSeekTranslationService.instance) {
|
||||
DeepSeekTranslationService.instance = new DeepSeekTranslationService();
|
||||
}
|
||||
return DeepSeekTranslationService.instance;
|
||||
}
|
||||
|
||||
public async translateProfile(request: TranslationRequest): Promise<TranslationResponse> {
|
||||
if (!this.apiKey) {
|
||||
throw new Error('DeepSeek API key is not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
const prompt = this.createTranslationPrompt(request);
|
||||
|
||||
const response = await axios.post(this.apiUrl, {
|
||||
model: 'deepseek-chat',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: 'You are a professional translator specializing in dating profiles. Translate the given text naturally, preserving the tone and personality. Respond only with the translated text, no additional comments.'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt
|
||||
}
|
||||
],
|
||||
max_tokens: 1000,
|
||||
temperature: 0.3
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 30000 // 30 секунд таймаут
|
||||
});
|
||||
|
||||
if (response.data?.choices?.[0]?.message?.content) {
|
||||
const translatedText = response.data.choices[0].message.content.trim();
|
||||
|
||||
return {
|
||||
translatedText,
|
||||
sourceLanguage: request.sourceLanguage || 'auto',
|
||||
targetLanguage: request.targetLanguage
|
||||
};
|
||||
} else {
|
||||
throw new Error('Invalid response from DeepSeek API');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ DeepSeek translation error:', error);
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response?.status === 401) {
|
||||
throw new Error('Invalid DeepSeek API key');
|
||||
} else if (error.response?.status === 429) {
|
||||
throw new Error('Translation rate limit exceeded. Please try again later.');
|
||||
} else if (error.code === 'ECONNABORTED') {
|
||||
throw new Error('Translation request timed out. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Translation service temporarily unavailable');
|
||||
}
|
||||
}
|
||||
|
||||
private createTranslationPrompt(request: TranslationRequest): string {
|
||||
const languageMap: { [key: string]: string } = {
|
||||
'en': 'English',
|
||||
'ru': 'Russian',
|
||||
'es': 'Spanish',
|
||||
'fr': 'French',
|
||||
'de': 'German',
|
||||
'it': 'Italian',
|
||||
'pt': 'Portuguese',
|
||||
'zh': 'Chinese',
|
||||
'ja': 'Japanese',
|
||||
'ko': 'Korean'
|
||||
};
|
||||
|
||||
const targetLanguageName = languageMap[request.targetLanguage] || request.targetLanguage;
|
||||
|
||||
let prompt = `Translate the following dating profile text to ${targetLanguageName}. `;
|
||||
|
||||
if (request.sourceLanguage && request.sourceLanguage !== 'auto') {
|
||||
const sourceLanguageName = languageMap[request.sourceLanguage] || request.sourceLanguage;
|
||||
prompt += `The source language is ${sourceLanguageName}. `;
|
||||
}
|
||||
|
||||
prompt += `Keep the tone natural and personal, as if the person is describing themselves:\n\n${request.text}`;
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
// Определить язык текста (базовая логика)
|
||||
public detectLanguage(text: string): string {
|
||||
// Простая эвристика для определения языка
|
||||
const cyrillicPattern = /[а-яё]/i;
|
||||
const latinPattern = /[a-z]/i;
|
||||
|
||||
const cyrillicCount = (text.match(cyrillicPattern) || []).length;
|
||||
const latinCount = (text.match(latinPattern) || []).length;
|
||||
|
||||
if (cyrillicCount > latinCount) {
|
||||
return 'ru';
|
||||
} else if (latinCount > 0) {
|
||||
return 'en';
|
||||
}
|
||||
|
||||
return 'auto';
|
||||
}
|
||||
|
||||
// Проверить доступность сервиса
|
||||
public async checkServiceAvailability(): Promise<boolean> {
|
||||
if (!this.apiKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(this.apiUrl, {
|
||||
model: 'deepseek-chat',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Test'
|
||||
}
|
||||
],
|
||||
max_tokens: 5
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
return response.status === 200;
|
||||
} catch (error) {
|
||||
console.error('DeepSeek service availability check failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DeepSeekTranslationService;
|
||||
105
src/services/localizationService.ts
Normal file
105
src/services/localizationService.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import i18next from 'i18next';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export class LocalizationService {
|
||||
private static instance: LocalizationService;
|
||||
private initialized = false;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): LocalizationService {
|
||||
if (!LocalizationService.instance) {
|
||||
LocalizationService.instance = new LocalizationService();
|
||||
}
|
||||
return LocalizationService.instance;
|
||||
}
|
||||
|
||||
public async initialize(): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
|
||||
try {
|
||||
// Загружаем файлы переводов
|
||||
const localesPath = path.join(__dirname, '..', 'locales');
|
||||
const ruTranslations = JSON.parse(fs.readFileSync(path.join(localesPath, 'ru.json'), 'utf8'));
|
||||
const enTranslations = JSON.parse(fs.readFileSync(path.join(localesPath, 'en.json'), 'utf8'));
|
||||
|
||||
await i18next.init({
|
||||
lng: 'ru', // Язык по умолчанию
|
||||
fallbackLng: 'ru',
|
||||
debug: false,
|
||||
resources: {
|
||||
ru: {
|
||||
translation: ruTranslations
|
||||
},
|
||||
en: {
|
||||
translation: enTranslations
|
||||
}
|
||||
},
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
}
|
||||
});
|
||||
|
||||
this.initialized = true;
|
||||
console.log('✅ Localization service initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to initialize localization service:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public t(key: string, options?: any): string {
|
||||
return i18next.t(key, options) as string;
|
||||
}
|
||||
|
||||
public setLanguage(language: string): void {
|
||||
i18next.changeLanguage(language);
|
||||
}
|
||||
|
||||
public getCurrentLanguage(): string {
|
||||
return i18next.language;
|
||||
}
|
||||
|
||||
public getSupportedLanguages(): string[] {
|
||||
return ['ru', 'en'];
|
||||
}
|
||||
|
||||
// Получить перевод для определенного языка без изменения текущего
|
||||
public getTranslation(key: string, language: string, options?: any): string {
|
||||
const currentLang = i18next.language;
|
||||
i18next.changeLanguage(language);
|
||||
const translation = i18next.t(key, options) as string;
|
||||
i18next.changeLanguage(currentLang);
|
||||
return translation;
|
||||
}
|
||||
|
||||
// Определить язык пользователя по его настройкам Telegram
|
||||
public detectUserLanguage(telegramLanguageCode?: string): string {
|
||||
if (!telegramLanguageCode) return 'ru';
|
||||
|
||||
// Поддерживаемые языки
|
||||
const supportedLanguages = this.getSupportedLanguages();
|
||||
|
||||
// Проверяем точное совпадение
|
||||
if (supportedLanguages.includes(telegramLanguageCode)) {
|
||||
return telegramLanguageCode;
|
||||
}
|
||||
|
||||
// Проверяем по первым двум символам (например, en-US -> en)
|
||||
const languagePrefix = telegramLanguageCode.substring(0, 2);
|
||||
if (supportedLanguages.includes(languagePrefix)) {
|
||||
return languagePrefix;
|
||||
}
|
||||
|
||||
// По умолчанию русский
|
||||
return 'ru';
|
||||
}
|
||||
}
|
||||
|
||||
// Функция-хелпер для быстрого доступа к переводам
|
||||
export const t = (key: string, options?: any): string => {
|
||||
return LocalizationService.getInstance().t(key, options);
|
||||
};
|
||||
|
||||
export default LocalizationService;
|
||||
Reference in New Issue
Block a user