feat: Complete localization system with i18n and DeepSeek AI translation
🌐 Interface Localization: - Added i18next for multi-language interface support - Created LocalizationService with language detection - Added translation files for Russian and English - Implemented language selection in user settings 🤖 AI Profile Translation (Premium feature): - Integrated DeepSeek API for profile translation - Added TranslationController for translation management - Premium-only access to profile translation feature - Support for 10 languages (ru, en, es, fr, de, it, pt, zh, ja, ko) �� Database & Models: - Added language field to users table with migration - Updated User model to support language preferences - Added language constraints and indexing 🎛️ User Interface: - Added language settings menu in bot settings - Implemented callback handlers for language selection - Added translate profile button for VIP users - Localized all interface strings 📚 Documentation: - Created comprehensive LOCALIZATION.md guide - Documented API usage and configuration - Added examples for extending language support
This commit is contained in:
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