pre=deploy
This commit is contained in:
430
docs/LOCALIZATION_MIGRATION_PLAN.md
Normal file
430
docs/LOCALIZATION_MIGRATION_PLAN.md
Normal file
@@ -0,0 +1,430 @@
|
||||
# План замены хардкод-текстов на локализационные ключи
|
||||
|
||||
## Текущее состояние
|
||||
|
||||
✅ **Реализовано:**
|
||||
- Система локализации с i18next
|
||||
- Выбор языка при первом запуске
|
||||
- 10 поддерживаемых языков
|
||||
- Сохранение языка в БД
|
||||
|
||||
⚠️ **Требуется:**
|
||||
- Извлечение и замена ~500+ хардкод-текстов в коде
|
||||
- Дополнение языковых файлов
|
||||
|
||||
## Стратегия замены
|
||||
|
||||
### Фаза 1: Критически важные пользовательские тексты (СНАЧАЛА)
|
||||
|
||||
#### Приоритет: HIGH
|
||||
Файлы с наибольшим количеством пользовательских сообщений:
|
||||
|
||||
1. **src/handlers/messageHandlers.ts** (~150 текстов)
|
||||
- Создание профиля
|
||||
- Ввод данных (имя, возраст, город, био)
|
||||
- Валидация ввода
|
||||
- Сообщения об ошибках
|
||||
|
||||
2. **src/handlers/callbackHandlers.ts** (~200 текстов)
|
||||
- Кнопки меню
|
||||
- Просмотр профилей
|
||||
- Лайки/дислайки
|
||||
- Настройки профиля
|
||||
- VIP функции
|
||||
|
||||
3. **src/handlers/commandHandlers.ts** (~50 текстов)
|
||||
- Команды бота
|
||||
- Главное меню
|
||||
- Справка
|
||||
|
||||
### Фаза 2: Второстепенные тексты
|
||||
|
||||
#### Приоритет: MEDIUM
|
||||
|
||||
4. **src/services/notificationService.ts** (~30 текстов)
|
||||
- Уведомления о лайках
|
||||
- Уведомления о матчах
|
||||
- Уведомления о сообщениях
|
||||
|
||||
5. **src/handlers/notificationHandlers.ts** (~20 текстов)
|
||||
- Настройки уведомлений
|
||||
|
||||
### Фаза 3: Служебные тексты
|
||||
|
||||
#### Приоритет: LOW
|
||||
|
||||
6. **src/services/profileService.ts** (~10 текстов)
|
||||
- Сообщения об ошибках валидации
|
||||
|
||||
7. **src/services/matchingService.ts** (~5 текстов)
|
||||
- Логирование и отладка
|
||||
|
||||
## Процесс замены (пошаговый)
|
||||
|
||||
### Шаг 1: Анализ файла
|
||||
|
||||
```bash
|
||||
# Найти все хардкод-тексты
|
||||
grep -n "'[А-Яа-яЁё]" src/handlers/messageHandlers.ts
|
||||
grep -n '"[А-Яа-яЁё]' src/handlers/messageHandlers.ts
|
||||
```
|
||||
|
||||
### Шаг 2: Создание ключей локализации
|
||||
|
||||
Для каждого найденного текста:
|
||||
|
||||
1. **Определить категорию:**
|
||||
- `profile.*` - профиль
|
||||
- `buttons.*` - кнопки
|
||||
- `errors.*` - ошибки
|
||||
- `messages.*` - сообщения
|
||||
- `commands.*` - команды
|
||||
- `search.*` - поиск
|
||||
- `matches.*` - матчи
|
||||
- `settings.*` - настройки
|
||||
- `notifications.*` - уведомления
|
||||
|
||||
2. **Создать понятный ключ:**
|
||||
```
|
||||
Плохо: "text1", "msg2"
|
||||
Хорошо: "profile.namePrompt", "errors.invalidAge"
|
||||
```
|
||||
|
||||
3. **Добавить в ru.json:**
|
||||
```json
|
||||
{
|
||||
"profile": {
|
||||
"namePrompt": "👤 Введите ваше имя:",
|
||||
"agePrompt": "🎂 Сколько вам лет?",
|
||||
"cityPrompt": "🌍 В каком городе вы находитесь?"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Шаг 3: Замена в коде
|
||||
|
||||
#### Было:
|
||||
```typescript
|
||||
await bot.sendMessage(chatId, '👤 Введите ваше имя:');
|
||||
```
|
||||
|
||||
#### Стало:
|
||||
```typescript
|
||||
const userId = msg.from?.id.toString();
|
||||
const lang = await profileService.getUserLanguage(userId);
|
||||
localizationService.setLanguage(lang);
|
||||
|
||||
await bot.sendMessage(chatId, localizationService.t('profile.namePrompt'));
|
||||
```
|
||||
|
||||
#### Оптимизация (для методов класса):
|
||||
|
||||
```typescript
|
||||
// В начале метода
|
||||
private async sendLocalizedMessage(
|
||||
chatId: number,
|
||||
userId: string,
|
||||
key: string,
|
||||
options?: any
|
||||
): Promise<void> {
|
||||
const lang = await this.profileService.getUserLanguage(userId);
|
||||
this.localizationService.setLanguage(lang);
|
||||
const text = this.localizationService.t(key, options);
|
||||
await this.bot.sendMessage(chatId, text);
|
||||
}
|
||||
|
||||
// Использование
|
||||
await this.sendLocalizedMessage(chatId, userId, 'profile.namePrompt');
|
||||
```
|
||||
|
||||
### Шаг 4: Перевод на другие языки
|
||||
|
||||
После добавления ключа в `ru.json`, добавить переводы:
|
||||
|
||||
**en.json:**
|
||||
```json
|
||||
{
|
||||
"profile": {
|
||||
"namePrompt": "👤 Enter your name:",
|
||||
"agePrompt": "🎂 How old are you?",
|
||||
"cityPrompt": "🌍 What city are you in?"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**ko.json:**
|
||||
```json
|
||||
{
|
||||
"profile": {
|
||||
"namePrompt": "👤 이름을 입력하세요:",
|
||||
"agePrompt": "🎂 나이가 어떻게 되세요?",
|
||||
"cityPrompt": "🌍 어느 도시에 계세요?"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
И так для всех 10 языков.
|
||||
|
||||
## Примеры типичных замен
|
||||
|
||||
### 1. Простое сообщение
|
||||
|
||||
**Было:**
|
||||
```typescript
|
||||
await bot.sendMessage(chatId, 'Анкеты закончились! Попробуйте позже.');
|
||||
```
|
||||
|
||||
**Стало:**
|
||||
```typescript
|
||||
await bot.sendMessage(chatId, localizationService.t('search.noProfiles'));
|
||||
```
|
||||
|
||||
### 2. Сообщение с параметрами
|
||||
|
||||
**Было:**
|
||||
```typescript
|
||||
await bot.sendMessage(chatId, `Привет, ${name}! С возвращением!`);
|
||||
```
|
||||
|
||||
**Стало:**
|
||||
```json
|
||||
// ru.json
|
||||
{
|
||||
"welcome": {
|
||||
"greeting": "Привет, {{name}}! С возвращением!"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
await bot.sendMessage(
|
||||
chatId,
|
||||
localizationService.t('welcome.greeting', { name })
|
||||
);
|
||||
```
|
||||
|
||||
### 3. Кнопки
|
||||
|
||||
**Было:**
|
||||
```typescript
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[{ text: '❤️ Нравится', callback_data: 'like' }],
|
||||
[{ text: '👎 Не нравится', callback_data: 'dislike' }]
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
**Стало:**
|
||||
```json
|
||||
// ru.json
|
||||
{
|
||||
"buttons": {
|
||||
"like": "❤️ Нравится",
|
||||
"dislike": "👎 Не нравится"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[{
|
||||
text: localizationService.t('buttons.like'),
|
||||
callback_data: 'like'
|
||||
}],
|
||||
[{
|
||||
text: localizationService.t('buttons.dislike'),
|
||||
callback_data: 'dislike'
|
||||
}]
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
### 4. Множественное число (плюрализация)
|
||||
|
||||
**Было:**
|
||||
```typescript
|
||||
const text = count === 1
|
||||
? `У вас ${count} новый матч`
|
||||
: `У вас ${count} новых матчей`;
|
||||
```
|
||||
|
||||
**Стало:**
|
||||
```json
|
||||
// ru.json
|
||||
{
|
||||
"matches": {
|
||||
"newCount_one": "У вас {{count}} новый матч",
|
||||
"newCount_few": "У вас {{count}} новых матча",
|
||||
"newCount_many": "У вас {{count}} новых матчей"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
await bot.sendMessage(
|
||||
chatId,
|
||||
localizationService.t('matches.newCount', { count })
|
||||
);
|
||||
```
|
||||
|
||||
## Инструменты для автоматизации
|
||||
|
||||
### Скрипт поиска хардкод-текстов
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# find_hardcoded_texts.sh
|
||||
|
||||
echo "Поиск русских текстов в кавычках..."
|
||||
grep -rn "'[А-Яа-яЁё]" src/ --include="*.ts" | wc -l
|
||||
grep -rn '"[А-Яа-яЁё]' src/ --include="*.ts" | wc -l
|
||||
|
||||
echo "Топ-10 файлов с наибольшим количеством хардкода:"
|
||||
grep -rn "'[А-Яа-яЁё]\|\"[А-Яа-яЁё]" src/ --include="*.ts" | \
|
||||
cut -d: -f1 | \
|
||||
sort | \
|
||||
uniq -c | \
|
||||
sort -rn | \
|
||||
head -10
|
||||
```
|
||||
|
||||
### Скрипт проверки покрытия переводами
|
||||
|
||||
```typescript
|
||||
// scripts/check-translations.ts
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const localesPath = path.join(__dirname, '..', 'src', 'locales');
|
||||
const ruFile = JSON.parse(fs.readFileSync(path.join(localesPath, 'ru.json'), 'utf8'));
|
||||
|
||||
function getAllKeys(obj: any, prefix = ''): string[] {
|
||||
let keys: string[] = [];
|
||||
for (const key in obj) {
|
||||
const fullKey = prefix ? `${prefix}.${key}` : key;
|
||||
if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
|
||||
keys = keys.concat(getAllKeys(obj[key], fullKey));
|
||||
} else {
|
||||
keys.push(fullKey);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
const ruKeys = getAllKeys(ruFile);
|
||||
const languages = ['en', 'es', 'fr', 'de', 'it', 'pt', 'zh', 'ja', 'ko'];
|
||||
|
||||
languages.forEach(lang => {
|
||||
const langFile = JSON.parse(fs.readFileSync(path.join(localesPath, `${lang}.json`), 'utf8'));
|
||||
const langKeys = getAllKeys(langFile);
|
||||
|
||||
const missing = ruKeys.filter(key => !langKeys.includes(key));
|
||||
|
||||
console.log(`\n${lang}.json:`);
|
||||
console.log(` Всего ключей: ${langKeys.length}/${ruKeys.length}`);
|
||||
if (missing.length > 0) {
|
||||
console.log(` Отсутствуют: ${missing.length}`);
|
||||
console.log(` Пример: ${missing.slice(0, 5).join(', ')}`);
|
||||
} else {
|
||||
console.log(` ✅ Все ключи присутствуют`);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Контрольный список (Checklist)
|
||||
|
||||
### Перед началом замены файла:
|
||||
|
||||
- [ ] Сделать backup файла или создать новую ветку в Git
|
||||
- [ ] Прочитать весь файл, понять структуру
|
||||
- [ ] Составить список всех текстов для замены
|
||||
|
||||
### В процессе замены:
|
||||
|
||||
- [ ] Заменять по 10-20 текстов за раз
|
||||
- [ ] Тестировать после каждой замены
|
||||
- [ ] Проверять TypeScript ошибки: `npm run build`
|
||||
- [ ] Коммитить изменения: `git commit -m "localize: messageHandlers profile section"`
|
||||
|
||||
### После замены файла:
|
||||
|
||||
- [ ] Убедиться, что нет TypeScript ошибок
|
||||
- [ ] Протестировать все функции файла в боте
|
||||
- [ ] Обновить переводы для всех 10 языков
|
||||
- [ ] Запустить скрипт проверки покрытия
|
||||
- [ ] Создать Pull Request для review
|
||||
|
||||
## Рекомендации
|
||||
|
||||
1. **Начинайте с самого используемого функционала:**
|
||||
- Регистрация (messageHandlers.ts - createProfile)
|
||||
- Просмотр анкет (callbackHandlers.ts - showNextCandidate)
|
||||
- Главное меню (commandHandlers.ts - handleStart)
|
||||
|
||||
2. **Группируйте ключи логически:**
|
||||
```json
|
||||
{
|
||||
"profile": {
|
||||
"prompts": {
|
||||
"name": "...",
|
||||
"age": "...",
|
||||
"city": "..."
|
||||
},
|
||||
"validation": {
|
||||
"nameLength": "...",
|
||||
"ageRange": "...",
|
||||
"cityRequired": "..."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Используйте консистентную нотацию:**
|
||||
- Всегда camelCase для ключей
|
||||
- Всегда точки для разделения уровней
|
||||
- Prefix для категории (profile, button, error)
|
||||
|
||||
4. **Не переводите:**
|
||||
- Технические логи
|
||||
- Callback_data значения
|
||||
- Имена переменных и функций
|
||||
|
||||
5. **Делайте переводы качественными:**
|
||||
- Нанимайте native speakers для перевода
|
||||
- Используйте контекст культуры (эмодзи, формальность)
|
||||
- Учитывайте длину текста (для кнопок)
|
||||
|
||||
## Оценка объема работ
|
||||
|
||||
### Время на замену (приблизительно):
|
||||
|
||||
- **messageHandlers.ts**: 4-6 часов
|
||||
- **callbackHandlers.ts**: 6-8 часов
|
||||
- **commandHandlers.ts**: 2-3 часа
|
||||
- **notificationService.ts**: 1-2 часа
|
||||
- **Прочие файлы**: 2-3 часа
|
||||
|
||||
**Итого на замену:** ~15-22 часа
|
||||
|
||||
### Время на переводы:
|
||||
|
||||
- **1 язык (native speaker)**: 2-3 часа
|
||||
- **9 языков**: 18-27 часов
|
||||
|
||||
**ОБЩИЙ ОБЪЕМ:** ~33-49 часов
|
||||
|
||||
## Следующий шаг
|
||||
|
||||
Начните с файла **src/handlers/messageHandlers.ts**, секция создания профиля:
|
||||
|
||||
```bash
|
||||
# Создайте ветку для работы
|
||||
git checkout -b localization-phase1-message-handlers
|
||||
|
||||
# Начните замену
|
||||
code src/handlers/messageHandlers.ts
|
||||
```
|
||||
|
||||
Удачи! 🚀
|
||||
Reference in New Issue
Block a user