полная локализация проекта

This commit is contained in:
kazachilo 2025-04-14 15:00:53 +03:00
parent 2bf8cb05b2
commit 334d77a2ef
34 changed files with 1165 additions and 326 deletions

View File

@ -6,6 +6,7 @@ import { initializeUserId, getCurrentUserId } from './constants/user';
import { trackScreenView } from './services/analyticsService'; import { trackScreenView } from './services/analyticsService';
import customAnalyticsService from './services/customAnalyticsService'; import customAnalyticsService from './services/customAnalyticsService';
import { BalanceProvider } from './contexts/BalanceContext'; import { BalanceProvider } from './contexts/BalanceContext';
import { getTranslation } from './constants/translations';
// Ленивая загрузка компонентов // Ленивая загрузка компонентов
const OnboardingWelcome = lazy(() => import('./screens/onboarding/OnboardingWelcome')); const OnboardingWelcome = lazy(() => import('./screens/onboarding/OnboardingWelcome'));
@ -29,7 +30,7 @@ const LoadingScreen = () => (
alignItems: 'center', alignItems: 'center',
height: '100vh' height: '100vh'
}}> }}>
Загрузка... {getTranslation('loading')}
</div> </div>
); );

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import styles from './GenerateButton.module.css'; import styles from './GenerateButton.module.css';
import { getTranslation } from '../../constants/translations';
interface GenerateButtonProps { interface GenerateButtonProps {
onGenerate: () => void; onGenerate: () => void;
@ -13,8 +14,8 @@ const GenerateButton: React.FC<GenerateButtonProps> = ({ onGenerate, tokenCount
onClick={onGenerate} onClick={onGenerate}
> >
<span className={styles.generateButtonText}> <span className={styles.generateButtonText}>
Начать генерацию {getTranslation('start_generation')}
<span className={styles.tokenCount}>{tokenCount} токенов</span> <span className={styles.tokenCount}>{getTranslation('tokens_count', tokenCount)}</span>
</span> </span>
</button> </button>
); );

View File

@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { ButtonBlock } from '../../types/blocks'; import { ButtonBlock } from '../../types/blocks';
import SquareButton from './SquareButton'; import SquareButton from './SquareButton';
import styles from './GridButtonsBlock.module.css'; import styles from './GridButtonsBlock.module.css';
import { getTranslation } from '../../constants/translations';
interface GridButtonsBlockProps { interface GridButtonsBlockProps {
block: ButtonBlock; block: ButtonBlock;
@ -61,7 +62,7 @@ const GridButtonsBlock: React.FC<GridButtonsBlockProps> = ({ block, onAction, is
className={styles.showMoreButton} className={styles.showMoreButton}
onClick={() => setExpanded(!expanded)} onClick={() => setExpanded(!expanded)}
> >
{expanded ? 'Свернуть' : 'Показать больше'} {expanded ? getTranslation('collapse') : getTranslation('show_more')}
</button> </button>
)} )}
</div> </div>

View File

@ -1,6 +1,7 @@
import React, { useState, useRef } from 'react'; import React, { useState, useRef } from 'react';
import styles from './TextInputBlock.module.css'; import styles from './TextInputBlock.module.css';
import { TextInputBlock as TextInputBlockType } from '../../types/blocks'; import { TextInputBlock as TextInputBlockType } from '../../types/blocks';
import { getTranslation } from '../../constants/translations';
interface TextInputBlockProps { interface TextInputBlockProps {
block: TextInputBlockType; block: TextInputBlockType;
@ -40,7 +41,7 @@ const TextInputBlock: React.FC<TextInputBlockProps> = ({ block, visible, onTextC
<textarea <textarea
ref={textareaRef} ref={textareaRef}
className={styles.input} className={styles.input}
placeholder="Опишите, какой стикер вы хотите получить..." placeholder={getTranslation('sticker_description_placeholder')}
rows={3} rows={3}
value={text} value={text}
onChange={handleChange} onChange={handleChange}

View File

@ -1,6 +1,7 @@
import React, { useState, useRef } from 'react'; import React, { useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import styles from './UploadPhotoBlock.module.css'; import styles from './UploadPhotoBlock.module.css';
import { getTranslation } from '../../constants/translations';
interface UploadPhotoBlockProps { interface UploadPhotoBlockProps {
onPhotoSelect?: (file: File) => void; onPhotoSelect?: (file: File) => void;
@ -64,17 +65,17 @@ const UploadPhotoBlock: React.FC<UploadPhotoBlockProps> = ({ onPhotoSelect, prev
> >
{previewUrl ? ( {previewUrl ? (
<div className={styles.preview}> <div className={styles.preview}>
<img src={previewUrl} alt="Preview" className={styles.previewImage} /> <img src={previewUrl} alt={getTranslation('preview_alt')} className={styles.previewImage} />
<div className={styles.changeOverlay}> <div className={styles.changeOverlay}>
<span className={styles.changeText}>Изменить фото</span> <span className={styles.changeText}>{getTranslation('change_photo')}</span>
</div> </div>
</div> </div>
) : ( ) : (
<> <>
<div className={styles.icon}>📷</div> <div className={styles.icon}>📷</div>
<div className={styles.text}> <div className={styles.text}>
<span className={styles.title}>Загрузите фото</span> <span className={styles.title}>{getTranslation('upload_photo_block_title')}</span>
<span className={styles.subtitle}>Перетащите или нажмите для выбора</span> <span className={styles.subtitle}>{getTranslation('upload_photo_block_subtitle')}</span>
</div> </div>
</> </>
)} )}

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { useNavigate } from 'react-router-dom';
import BlockRenderer from '../blocks/BlockRenderer'; import BlockRenderer from '../blocks/BlockRenderer';
import { homeScreenConfig } from '../../config/homeScreen'; import { homeScreenConfig } from '../../config/homeScreen';
import customAnalyticsService from '../../services/customAnalyticsService'; import customAnalyticsService from '../../services/customAnalyticsService';
@ -16,6 +17,7 @@ const MainActions: React.FC<MainActionsProps> = ({
onSendFeedback, onSendFeedback,
onOpenTelegramBot onOpenTelegramBot
}) => { }) => {
const navigate = useNavigate();
// Находим блок верхних кнопок и разделитель в конфигурации // Находим блок верхних кнопок и разделитель в конфигурации
const actionsBlock = homeScreenConfig.homeScreen.blocks.find(block => block.id === 'mainActions'); const actionsBlock = homeScreenConfig.homeScreen.blocks.find(block => block.id === 'mainActions');
const dividerBlock = homeScreenConfig.homeScreen.blocks.find(block => block.id === 'mainDivider'); const dividerBlock = homeScreenConfig.homeScreen.blocks.find(block => block.id === 'mainDivider');
@ -45,6 +47,15 @@ const MainActions: React.FC<MainActionsProps> = ({
}); });
onOpenTelegramBot(); onOpenTelegramBot();
} }
} else if (actionType === 'route') {
// Отслеживаем событие нажатия на кнопку навигации
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'ui_interaction',
event_name: 'instruction_button_click',
unit: actionValue
});
navigate(actionValue);
} }
}; };

View File

@ -9,6 +9,7 @@ import { tokenPacks } from '../../constants/tokenPacks';
import NotificationModal from '../shared/NotificationModal'; import NotificationModal from '../shared/NotificationModal';
import { useBalance } from '../../contexts/BalanceContext'; import { useBalance } from '../../contexts/BalanceContext';
import customAnalyticsService from '../../services/customAnalyticsService'; import customAnalyticsService from '../../services/customAnalyticsService';
import { getTranslation } from '../../constants/translations';
import styles from './Header.module.css'; import styles from './Header.module.css';
const Header: React.FC = () => { const Header: React.FC = () => {
@ -55,7 +56,7 @@ const Header: React.FC = () => {
<div className={styles.avatar}> <div className={styles.avatar}>
<img <img
src={user.avatarUrl} src={user.avatarUrl}
alt="Avatar" alt={getTranslation('avatar_alt')}
className={styles.avatarImage} className={styles.avatarImage}
onError={(e) => { onError={(e) => {
e.currentTarget.onerror = null; e.currentTarget.onerror = null;
@ -80,10 +81,10 @@ const Header: React.FC = () => {
}); });
setShowTokensModal(true); setShowTokensModal(true);
}} }}
title="Нажмите чтобы пополнить баланс" title={getTranslation('add_balance_title')}
> >
<span className={styles.balanceIcon}> <span className={styles.balanceIcon}>
<img src={images.tokenIcon} alt="Токены" className={styles.tokenImage} /> <img src={images.tokenIcon} alt={getTranslation('tokens_alt')} className={styles.tokenImage} />
</span> </span>
<span className={styles.balanceValue}> <span className={styles.balanceValue}>
{balance} {balance}
@ -111,8 +112,8 @@ const Header: React.FC = () => {
updateBalance(); updateBalance();
// Показываем модальное окно с информацией об успешной оплате // Показываем модальное окно с информацией об успешной оплате
setNotificationTitle('Оплата успешна!'); setNotificationTitle(getTranslation('payment_success'));
setNotificationMessage(`Вы успешно приобрели ${pack.tokens + pack.bonusTokens} токенов.`); setNotificationMessage(getTranslation('tokens_purchased', pack.tokens + pack.bonusTokens));
setShowNotificationModal(true); setShowNotificationModal(true);
} else { } else {
// Если данные не получены, делаем запрос на получение данных пользователя // Если данные не получены, делаем запрос на получение данных пользователя
@ -124,8 +125,8 @@ const Header: React.FC = () => {
updateBalance(); updateBalance();
// Показываем модальное окно с информацией об успешной оплате // Показываем модальное окно с информацией об успешной оплате
setNotificationTitle('Оплата успешна!'); setNotificationTitle(getTranslation('payment_success'));
setNotificationMessage(`Вы успешно приобрели ${pack.tokens + pack.bonusTokens} токенов. Ваш текущий баланс: ${balance} токенов.`); setNotificationMessage(getTranslation('profile_tokens_purchased', pack.tokens + pack.bonusTokens, balance));
setShowNotificationModal(true); setShowNotificationModal(true);
} catch (error) { } catch (error) {
console.error('Ошибка при обновлении данных пользователя:', error); console.error('Ошибка при обновлении данных пользователя:', error);
@ -143,7 +144,7 @@ const Header: React.FC = () => {
onGalleryClick={() => setShowNotificationModal(false)} onGalleryClick={() => setShowNotificationModal(false)}
onContinueClick={() => setShowNotificationModal(false)} onContinueClick={() => setShowNotificationModal(false)}
showGalleryButton={false} showGalleryButton={false}
continueButtonText="Закрыть" continueButtonText={getTranslation('close')}
/> />
</header> </header>
); );

View File

@ -3,6 +3,7 @@ import { useNavigate, useLocation } from 'react-router-dom';
import styles from './Navigation.module.css'; import styles from './Navigation.module.css';
import customAnalyticsService from '../../services/customAnalyticsService'; import customAnalyticsService from '../../services/customAnalyticsService';
import { getCurrentUserId } from '../../constants/user'; import { getCurrentUserId } from '../../constants/user';
import { getTranslation } from '../../constants/translations';
const NavigationComponent: React.FC = () => { const NavigationComponent: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -32,7 +33,7 @@ const NavigationComponent: React.FC = () => {
<path d="M9 22V12h6v10" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/> <path d="M9 22V12h6v10" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg> </svg>
</span> </span>
<span className={styles.label}>Главная</span> <span className={styles.label}>{getTranslation('nav_home')}</span>
</button> </button>
<button <button
@ -54,7 +55,7 @@ const NavigationComponent: React.FC = () => {
<path d="M20 16l-4-4L6 20" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/> <path d="M20 16l-4-4L6 20" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg> </svg>
</span> </span>
<span className={styles.label}>Галерея</span> <span className={styles.label}>{getTranslation('nav_gallery')}</span>
</button> </button>
<button <button
@ -75,7 +76,7 @@ const NavigationComponent: React.FC = () => {
<path d="M4 8l8 4 8-4M12 12v8" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/> <path d="M4 8l8 4 8-4M12 12v8" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg> </svg>
</span> </span>
<span className={styles.label}>Стикерпаки</span> <span className={styles.label}>{getTranslation('nav_sticker_packs')}</span>
</button> </button>
<button <button
@ -96,7 +97,7 @@ const NavigationComponent: React.FC = () => {
<circle cx="12" cy="7" r="4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/> <circle cx="12" cy="7" r="4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg> </svg>
</span> </span>
<span className={styles.label}>Профиль</span> <span className={styles.label}>{getTranslation('nav_profile')}</span>
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,6 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import styles from './NotificationModal.module.css'; import styles from './NotificationModal.module.css';
import emojiStyles from './EmojiPickerModal.module.css'; import emojiStyles from './EmojiPickerModal.module.css';
import { getTranslation } from '../../constants/translations';
interface EmojiPickerModalProps { interface EmojiPickerModalProps {
isVisible: boolean; isVisible: boolean;
@ -63,7 +64,7 @@ const EmojiPickerModal: React.FC<EmojiPickerModalProps> = ({
<div className={styles.overlay}> <div className={styles.overlay}>
<div className={styles.modal}> <div className={styles.modal}>
<div className={styles.header}> <div className={styles.header}>
<div className={styles.title}>Выберите эмодзи</div> <div className={styles.title}>{getTranslation('emoji_picker_title')}</div>
</div> </div>
<div className={emojiStyles.emojiContainer}> <div className={emojiStyles.emojiContainer}>
@ -85,13 +86,13 @@ const EmojiPickerModal: React.FC<EmojiPickerModalProps> = ({
className={`${styles.button} ${styles.secondaryButton}`} className={`${styles.button} ${styles.secondaryButton}`}
onClick={onCancel} onClick={onCancel}
> >
Отмена {getTranslation('emoji_picker_cancel')}
</button> </button>
<button <button
className={`${styles.button} ${styles.primaryButton}`} className={`${styles.button} ${styles.primaryButton}`}
onClick={handleApply} onClick={handleApply}
> >
Применить {getTranslation('emoji_picker_apply')}
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,6 +1,7 @@
import React, { useState, useRef, useEffect, memo } from 'react'; import React, { useState, useRef, useEffect, memo } from 'react';
import styles from './NotificationModal.module.css'; // Используем существующие стили import styles from './NotificationModal.module.css'; // Используем существующие стили
import feedbackStyles from './FeedbackModal.module.css'; // Дополнительные стили для формы import feedbackStyles from './FeedbackModal.module.css'; // Дополнительные стили для формы
import { getTranslation } from '../../constants/translations';
export interface FeedbackData { export interface FeedbackData {
text: string; text: string;
@ -41,9 +42,9 @@ const ImagePreview = memo(({
return ( return (
<div className={feedbackStyles.imagePreview}> <div className={feedbackStyles.imagePreview}>
{url ? ( {url ? (
<img src={url} alt={`Preview ${index}`} /> <img src={url} alt={getTranslation('preview_image_alt', index)} />
) : ( ) : (
<div className={feedbackStyles.imageLoading}>Загрузка...</div> <div className={feedbackStyles.imageLoading}>{getTranslation('loading_image')}</div>
)} )}
<button <button
className={feedbackStyles.removeImageButton} className={feedbackStyles.removeImageButton}
@ -101,7 +102,7 @@ const FeedbackModal: React.FC<FeedbackModalProps> = ({ isVisible, onClose, onSub
setImages(prevImages => [...prevImages, file]); setImages(prevImages => [...prevImages, file]);
} else { } else {
console.error('Выбранный файл не является изображением'); console.error('Выбранный файл не является изображением');
setError('Пожалуйста, выберите файл изображения (JPEG, PNG и т.д.)'); setError(getTranslation('select_image_file'));
} }
}; };
@ -121,7 +122,7 @@ const FeedbackModal: React.FC<FeedbackModalProps> = ({ isVisible, onClose, onSub
} }
} catch (err) { } catch (err) {
console.error('Ошибка при обработке выбранного файла:', err); console.error('Ошибка при обработке выбранного файла:', err);
setError('Произошла ошибка при выборе файла. Пожалуйста, попробуйте еще раз.'); setError(getTranslation('file_selection_error'));
} }
}; };
@ -134,7 +135,7 @@ const FeedbackModal: React.FC<FeedbackModalProps> = ({ isVisible, onClose, onSub
const handleSubmit = async () => { const handleSubmit = async () => {
// Проверяем, что есть текст или изображения // Проверяем, что есть текст или изображения
if (!text.trim() && images.length === 0) { if (!text.trim() && images.length === 0) {
setError('Пожалуйста, введите текст или добавьте изображение'); setError(getTranslation('enter_text_or_add_image'));
return; return;
} }
@ -150,11 +151,11 @@ const FeedbackModal: React.FC<FeedbackModalProps> = ({ isVisible, onClose, onSub
if (result) { if (result) {
handleClose(); handleClose();
} else { } else {
setError('Не удалось отправить обратную связь. Пожалуйста, попробуйте позже.'); setError(getTranslation('failed_to_send_feedback'));
} }
} catch (err) { } catch (err) {
console.error('Ошибка при отправке обратной связи:', err); console.error('Ошибка при отправке обратной связи:', err);
setError('Произошла ошибка при отправке. Пожалуйста, попробуйте позже.'); setError(getTranslation('error_sending_feedback'));
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@ -168,16 +169,16 @@ const FeedbackModal: React.FC<FeedbackModalProps> = ({ isVisible, onClose, onSub
<div className={styles.header}> <div className={styles.header}>
<div className={styles.title}> <div className={styles.title}>
{isLoading && <span className={styles.spinner}></span>} {isLoading && <span className={styles.spinner}></span>}
Обратная связь {getTranslation('feedback_title')}
</div> </div>
<div className={styles.message}>Расскажите нам о проблеме или предложении</div> <div className={styles.message}>{getTranslation('feedback_subtitle')}</div>
</div> </div>
{/* Поле для ввода текста */} {/* Поле для ввода текста */}
<textarea <textarea
ref={textareaRef} ref={textareaRef}
className={feedbackStyles.textarea} className={feedbackStyles.textarea}
placeholder="Опишите вашу проблему или предложение..." placeholder={getTranslation('feedback_placeholder')}
value={text} value={text}
onChange={(e) => setText(e.target.value)} onChange={(e) => setText(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
@ -193,7 +194,7 @@ const FeedbackModal: React.FC<FeedbackModalProps> = ({ isVisible, onClose, onSub
type="button" type="button"
disabled={isLoading} disabled={isLoading}
> >
Добавить изображение {getTranslation('add_image')}
</button> </button>
<input <input
ref={fileInputRef} ref={fileInputRef}
@ -235,7 +236,7 @@ const FeedbackModal: React.FC<FeedbackModalProps> = ({ isVisible, onClose, onSub
type="button" type="button"
disabled={isLoading} disabled={isLoading}
> >
Отмена {getTranslation('cancel')}
</button> </button>
<button <button
className={`${styles.button} ${styles.primaryButton}`} className={`${styles.button} ${styles.primaryButton}`}
@ -243,7 +244,7 @@ const FeedbackModal: React.FC<FeedbackModalProps> = ({ isVisible, onClose, onSub
type="button" type="button"
disabled={isLoading} disabled={isLoading}
> >
{isLoading ? 'Отправка...' : 'Отправить'} {isLoading ? getTranslation('generation_sending') : getTranslation('accept')}
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { getTranslation } from '../../constants/translations';
import styles from './ImageViewer.module.css'; import styles from './ImageViewer.module.css';
interface ImageViewerProps { interface ImageViewerProps {
@ -96,7 +97,7 @@ const ImageViewer: React.FC<ImageViewerProps> = ({ imageUrl, onClose }) => {
</button> </button>
<img <img
src={imageUrl} src={imageUrl}
alt="Полноэкранный просмотр" alt={getTranslation('fullscreen_view_alt')}
className={styles.fullImage} className={styles.fullImage}
style={{ transform: `translateY(-${translateY}px)` }} style={{ transform: `translateY(-${translateY}px)` }}
/> />

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import styles from './NotificationModal.module.css'; import styles from './NotificationModal.module.css';
import { getTranslation } from '../../constants/translations';
interface NotificationModalProps { interface NotificationModalProps {
isVisible: boolean; isVisible: boolean;
@ -28,8 +29,8 @@ const NotificationModal: React.FC<NotificationModalProps> = ({
onContinueClick, onContinueClick,
showGalleryButton = true, // По умолчанию кнопка "В галерею" видима showGalleryButton = true, // По умолчанию кнопка "В галерею" видима
showButtons = true, // По умолчанию все кнопки видимы showButtons = true, // По умолчанию все кнопки видимы
continueButtonText = 'Продолжить', // По умолчанию текст кнопки "Продолжить" continueButtonText = getTranslation('continue'), // По умолчанию текст кнопки "Продолжить"
galleryButtonText = 'В галерею', // По умолчанию текст кнопки "В галерею" galleryButtonText = getTranslation('gallery'), // По умолчанию текст кнопки "В галерею"
isPrimaryGalleryButton = true, // По умолчанию кнопка "В галерею" синяя isPrimaryGalleryButton = true, // По умолчанию кнопка "В галерею" синяя
taskId, taskId,
generatedImageUrl, generatedImageUrl,
@ -52,13 +53,13 @@ const NotificationModal: React.FC<NotificationModalProps> = ({
{isGenerating && queuePosition !== undefined && queuePosition <= 2 && ( {isGenerating && queuePosition !== undefined && queuePosition <= 2 && (
<div className={styles.generationPreview}> <div className={styles.generationPreview}>
{generatedImageUrl ? ( {generatedImageUrl ? (
<img src={generatedImageUrl} alt="Сгенерированный стикер" className={styles.generatedImage} /> <img src={generatedImageUrl} alt={getTranslation('generated_sticker')} className={styles.generatedImage} />
) : ( ) : (
<> <>
<div className={styles.magicRipple}> <div className={styles.magicRipple}>
{/* Анимация магической ряби */} {/* Анимация магической ряби */}
</div> </div>
<div className={styles.generationText}>Генерация...</div> <div className={styles.generationText}>{getTranslation('generating')}</div>
</> </>
)} )}
</div> </div>

View File

@ -4,6 +4,7 @@ import { TokenPack } from '../../constants/tokenPacks';
import customAnalyticsService from '../../services/customAnalyticsService'; import customAnalyticsService from '../../services/customAnalyticsService';
import { getCurrentUserId } from '../../constants/user'; import { getCurrentUserId } from '../../constants/user';
import styles from './TokenPackCard.module.css'; import styles from './TokenPackCard.module.css';
import { getTranslation } from '../../constants/translations';
interface TokenPackCardProps extends TokenPack { interface TokenPackCardProps extends TokenPack {
onBuy: () => void; onBuy: () => void;
@ -13,6 +14,7 @@ interface TokenPackCardProps extends TokenPack {
} }
const TokenPackCard: React.FC<TokenPackCardProps> = ({ const TokenPackCard: React.FC<TokenPackCardProps> = ({
id,
title, title,
tokens, tokens,
bonusTokens, bonusTokens,
@ -43,38 +45,38 @@ const TokenPackCard: React.FC<TokenPackCardProps> = ({
> >
{isPopular && ( {isPopular && (
<div className={`${styles.badge} ${styles.popularBadge}`}> <div className={`${styles.badge} ${styles.popularBadge}`}>
Популярный выбор {getTranslation('token_pack_popular')}
</div> </div>
)} )}
{isBestValue && ( {isBestValue && (
<div className={`${styles.badge} ${styles.bestValueBadge}`}> <div className={`${styles.badge} ${styles.bestValueBadge}`}>
Максимальная выгода {getTranslation('token_pack_best_value')}
</div> </div>
)} )}
<h3 className={styles.title}>{title}</h3> <h3 className={styles.title}>{getTranslation(`token_pack_${id}_title`)}</h3>
<div className={styles.tokenInfo}> <div className={styles.tokenInfo}>
<img src={images.tokenIcon} alt="Токены" className={styles.tokenIcon} /> <img src={images.tokenIcon} alt={getTranslation('token_pack_tokens')} className={styles.tokenIcon} />
<div className={styles.tokenCount}> <div className={styles.tokenCount}>
<span className={styles.baseTokens}>{tokens}</span> <span className={styles.baseTokens}>{tokens}</span>
{bonusTokens > 0 && ( {bonusTokens > 0 && (
<span className={styles.bonusTokens}>+{bonusTokens} БОНУС</span> <span className={styles.bonusTokens}>{getTranslation('token_pack_bonus', bonusTokens)}</span>
)} )}
</div> </div>
</div> </div>
<div className={styles.stickersCount}> <div className={styles.stickersCount}>
{stickersCount} стикеров {getTranslation('token_pack_stickers_count', stickersCount)}
</div> </div>
{description && ( {description && (
<p className={styles.description}>{description}</p> <p className={styles.description}>{getTranslation(`token_pack_${id}_description`)}</p>
)} )}
<div className={styles.priceSection}> <div className={styles.priceSection}>
<div className={styles.price}> <div className={styles.price}>
{price} Stars {price} {getTranslation('token_pack_stars')}
</div> </div>
<button <button
className={styles.buyButton} className={styles.buyButton}
@ -90,7 +92,7 @@ const TokenPackCard: React.FC<TokenPackCardProps> = ({
onBuy(); onBuy();
}} }}
> >
КУПИТЬ {getTranslation('token_pack_buy')}
</button> </button>
</div> </div>
</div> </div>

View File

@ -2,6 +2,7 @@ import React, { useRef, useEffect } from 'react';
import { tokenPacks } from '../../constants/tokenPacks'; import { tokenPacks } from '../../constants/tokenPacks';
import TokenPackCard from './TokenPackCard'; import TokenPackCard from './TokenPackCard';
import styles from './TokenPacksList.module.css'; import styles from './TokenPacksList.module.css';
import { getTranslation } from '../../constants/translations';
interface TokenPacksListProps { interface TokenPacksListProps {
onBuyPack: (packId: string) => void; onBuyPack: (packId: string) => void;
@ -32,7 +33,7 @@ const TokenPacksList: React.FC<TokenPacksListProps> = ({
return ( return (
<div className={`${styles.container} ${className}`}> <div className={`${styles.container} ${className}`}>
<h2 className={styles.title}>Пакеты токенов</h2> <h2 className={styles.title}>{getTranslation('token_packs_title')}</h2>
<div <div
className={`${styles.list} ${compact ? styles.horizontalScroll : ''}`} className={`${styles.list} ${compact ? styles.horizontalScroll : ''}`}
ref={listRef} ref={listRef}

View File

@ -2,6 +2,7 @@ import React from 'react';
import { tokenPacks, TokenPack } from '../../constants/tokenPacks'; import { tokenPacks, TokenPack } from '../../constants/tokenPacks';
import styles from './TokenPacksModal.module.css'; import styles from './TokenPacksModal.module.css';
import TokenPacksList from './TokenPacksList'; import TokenPacksList from './TokenPacksList';
import { getTranslation } from '../../constants/translations';
interface TokenPacksModalProps { interface TokenPacksModalProps {
isVisible: boolean; isVisible: boolean;
@ -35,7 +36,7 @@ const TokenPacksModal: React.FC<TokenPacksModalProps> = ({
return ( return (
<div className={styles.overlay} onClick={onClose}> <div className={styles.overlay} onClick={onClose}>
<div className={styles.modal} onClick={e => e.stopPropagation()}> <div className={styles.modal} onClick={e => e.stopPropagation()}>
<h2 className={styles.title}>Недостаточно токенов для генерации</h2> <h2 className={styles.title}>{getTranslation('token_modal_title')}</h2>
<TokenPacksList <TokenPacksList
onBuyPack={onBuyPack} onBuyPack={onBuyPack}
@ -50,7 +51,7 @@ const TokenPacksModal: React.FC<TokenPacksModalProps> = ({
onShowAllPacks(); onShowAllPacks();
}} }}
> >
Показать все пакеты {getTranslation('token_modal_show_all')}
</button> </button>
<button <button

View File

@ -1,5 +1,6 @@
import { AppConfig } from '../types/blocks'; import { AppConfig } from '../types/blocks';
import { images } from '../assets'; import { images } from '../assets';
import { getTranslation } from '../constants/translations';
export const homeScreenConfig: AppConfig = { export const homeScreenConfig: AppConfig = {
homeScreen: { homeScreen: {
@ -15,7 +16,7 @@ export const homeScreenConfig: AppConfig = {
type: 'gradient', type: 'gradient',
colors: ['#FF69B4', '#FF1493'] colors: ['#FF69B4', '#FF1493']
}, },
title: 'Обратная связь', title: getTranslation('feedback'),
imageUrl: images.feedback, imageUrl: images.feedback,
action: { action: {
type: 'function', type: 'function',
@ -29,7 +30,7 @@ export const homeScreenConfig: AppConfig = {
type: 'gradient', type: 'gradient',
colors: ['#2B9CFF', '#1E88E5'] colors: ['#2B9CFF', '#1E88E5']
}, },
title: 'Инструкция', title: getTranslation('instruction'),
imageUrl: images.faq, imageUrl: images.faq,
action: { action: {
type: 'route', type: 'route',
@ -43,7 +44,7 @@ export const homeScreenConfig: AppConfig = {
type: 'gradient', type: 'gradient',
colors: ['#8A2BE2', '#9400D3'] colors: ['#8A2BE2', '#9400D3']
}, },
title: 'ShortsLoader', title: getTranslation('shortsLoader'),
imageUrl: images.shorts, imageUrl: images.shorts,
action: { action: {
type: 'function', type: 'function',
@ -68,7 +69,7 @@ export const homeScreenConfig: AppConfig = {
type: 'stepTitle', type: 'stepTitle',
id: 'step1', id: 'step1',
number: 1, number: 1,
text: 'Загрузи фото с лицом' text: getTranslation('uploadPhoto')
}, },
{ {
type: 'uploadPhoto', type: 'uploadPhoto',
@ -88,7 +89,7 @@ export const homeScreenConfig: AppConfig = {
type: 'color', type: 'color',
colors: ['#E0E0E0'] colors: ['#E0E0E0']
}, },
title: 'Авто', title: getTranslation('auto'),
action: { action: {
type: 'selectGenderDetection', type: 'selectGenderDetection',
value: 'auto' value: 'auto'
@ -101,7 +102,7 @@ export const homeScreenConfig: AppConfig = {
type: 'color', type: 'color',
colors: ['#E0E0E0'] colors: ['#E0E0E0']
}, },
title: 'Мужчина', title: getTranslation('man'),
action: { action: {
type: 'selectGender', type: 'selectGender',
value: 'man' value: 'man'
@ -114,7 +115,7 @@ export const homeScreenConfig: AppConfig = {
type: 'color', type: 'color',
colors: ['#E0E0E0'] colors: ['#E0E0E0']
}, },
title: 'Женщина', title: getTranslation('woman'),
action: { action: {
type: 'selectGender', type: 'selectGender',
value: 'woman' value: 'woman'
@ -132,7 +133,7 @@ export const homeScreenConfig: AppConfig = {
type: 'stepTitle', type: 'stepTitle',
id: 'step2', id: 'step2',
number: 2, number: 2,
text: 'Выбери стиль' text: getTranslation('chooseStyle')
}, },
{ {
type: 'scrollableButtons', type: 'scrollableButtons',
@ -145,7 +146,7 @@ export const homeScreenConfig: AppConfig = {
type: 'gradient', type: 'gradient',
colors: ['#2B9CFF', '#1E88E5'] colors: ['#2B9CFF', '#1E88E5']
}, },
title: 'Эмоции', title: getTranslation('emotions'),
imageUrl: images.emotions, imageUrl: images.emotions,
action: { action: {
type: 'selectStyle', type: 'selectStyle',
@ -159,7 +160,7 @@ export const homeScreenConfig: AppConfig = {
type: 'gradient', type: 'gradient',
colors: ['#FF69B4', '#FF1493'] colors: ['#FF69B4', '#FF1493']
}, },
title: 'Чиби', title: getTranslation('chibi'),
imageUrl: images.balerina, imageUrl: images.balerina,
action: { action: {
type: 'selectStyle', type: 'selectStyle',
@ -173,7 +174,7 @@ export const homeScreenConfig: AppConfig = {
type: 'gradient', type: 'gradient',
colors: ['#4CAF50', '#45A049'] colors: ['#4CAF50', '#45A049']
}, },
title: 'Реализм', title: getTranslation('realism'),
imageUrl: images.realism, imageUrl: images.realism,
action: { action: {
type: 'selectStyle', type: 'selectStyle',
@ -192,7 +193,7 @@ export const homeScreenConfig: AppConfig = {
type: 'stepTitle', type: 'stepTitle',
id: 'step3', id: 'step3',
number: 3, number: 3,
text: 'Выбери образ' text: getTranslation('chooseImage')
}, },
{ {
type: 'gridButtons', type: 'gridButtons',
@ -205,7 +206,7 @@ export const homeScreenConfig: AppConfig = {
type: 'gradient', type: 'gradient',
colors: ['#2B9CFF', '#1E88E5'] colors: ['#2B9CFF', '#1E88E5']
}, },
title: 'Коллекция', title: getTranslation('collection'),
action: { action: {
type: 'selectEmotionType', type: 'selectEmotionType',
value: 'prompts' value: 'prompts'
@ -218,7 +219,7 @@ export const homeScreenConfig: AppConfig = {
type: 'gradient', type: 'gradient',
colors: ['#FF69B4', '#FF1493'] colors: ['#FF69B4', '#FF1493']
}, },
title: 'Мемы', title: getTranslation('memes'),
action: { action: {
type: 'selectEmotionType', type: 'selectEmotionType',
value: 'memes' value: 'memes'
@ -243,7 +244,7 @@ export const homeScreenConfig: AppConfig = {
type: 'gradient', type: 'gradient',
colors: ['#FF69B4', '#FF1493'] colors: ['#FF69B4', '#FF1493']
}, },
title: 'Мем 1', title: getTranslation('meme1'),
imageUrl: images.meme1, imageUrl: images.meme1,
action: { action: {
type: 'selectMeme', type: 'selectMeme',
@ -257,7 +258,7 @@ export const homeScreenConfig: AppConfig = {
type: 'gradient', type: 'gradient',
colors: ['#2B9CFF', '#1E88E5'] colors: ['#2B9CFF', '#1E88E5']
}, },
title: 'Мем 2', title: getTranslation('meme2'),
imageUrl: images.meme2, imageUrl: images.meme2,
action: { action: {
type: 'selectMeme', type: 'selectMeme',
@ -271,7 +272,7 @@ export const homeScreenConfig: AppConfig = {
type: 'gradient', type: 'gradient',
colors: ['#4CAF50', '#45A049'] colors: ['#4CAF50', '#45A049']
}, },
title: 'Мем 3', title: getTranslation('meme3'),
imageUrl: images.meme3, imageUrl: images.meme3,
action: { action: {
type: 'selectMeme', type: 'selectMeme',
@ -285,7 +286,7 @@ export const homeScreenConfig: AppConfig = {
type: 'gradient', type: 'gradient',
colors: ['#9C27B0', '#7B1FA2'] colors: ['#9C27B0', '#7B1FA2']
}, },
title: 'Мем 4', title: getTranslation('meme4'),
imageUrl: images.meme4, imageUrl: images.meme4,
action: { action: {
type: 'selectMeme', type: 'selectMeme',
@ -299,7 +300,7 @@ export const homeScreenConfig: AppConfig = {
type: 'gradient', type: 'gradient',
colors: ['#FF5722', '#F4511E'] colors: ['#FF5722', '#F4511E']
}, },
title: 'Мем 5', title: getTranslation('meme5'),
imageUrl: images.meme5, imageUrl: images.meme5,
action: { action: {
type: 'selectMeme', type: 'selectMeme',
@ -313,7 +314,7 @@ export const homeScreenConfig: AppConfig = {
type: 'gradient', type: 'gradient',
colors: ['#FF9800', '#FB8C00'] colors: ['#FF9800', '#FB8C00']
}, },
title: 'Мем 6', title: getTranslation('meme6'),
imageUrl: images.meme6, imageUrl: images.meme6,
action: { action: {
type: 'selectMeme', type: 'selectMeme',
@ -327,7 +328,7 @@ export const homeScreenConfig: AppConfig = {
type: 'gradient', type: 'gradient',
colors: ['#607D8B', '#455A64'] colors: ['#607D8B', '#455A64']
}, },
title: 'Мем 7', title: getTranslation('meme7'),
imageUrl: images.meme7, imageUrl: images.meme7,
action: { action: {
type: 'selectMeme', type: 'selectMeme',
@ -341,7 +342,7 @@ export const homeScreenConfig: AppConfig = {
type: 'gradient', type: 'gradient',
colors: ['#795548', '#5D4037'] colors: ['#795548', '#5D4037']
}, },
title: 'Мем 8', title: getTranslation('meme8'),
imageUrl: images.meme8, imageUrl: images.meme8,
action: { action: {
type: 'selectMeme', type: 'selectMeme',
@ -355,7 +356,7 @@ export const homeScreenConfig: AppConfig = {
type: 'gradient', type: 'gradient',
colors: ['#009688', '#00796B'] colors: ['#009688', '#00796B']
}, },
title: 'Мем 9', title: getTranslation('meme9'),
imageUrl: images.meme9, imageUrl: images.meme9,
action: { action: {
type: 'selectMeme', type: 'selectMeme',
@ -369,7 +370,7 @@ export const homeScreenConfig: AppConfig = {
type: 'gradient', type: 'gradient',
colors: ['#673AB7', '#512DA8'] colors: ['#673AB7', '#512DA8']
}, },
title: 'Мем 10', title: getTranslation('meme10'),
imageUrl: images.meme10, imageUrl: images.meme10,
action: { action: {
type: 'selectMeme', type: 'selectMeme',

View File

@ -1,5 +1,6 @@
import { BlockButton } from '../types/blocks'; import { BlockButton } from '../types/blocks';
import { images } from '../assets'; import { images } from '../assets';
import { getTranslation } from '../constants/translations';
interface StylePresets { interface StylePresets {
[key: string]: { [key: string]: {
@ -17,7 +18,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#FF5722', '#F4511E'] colors: ['#FF5722', '#F4511E']
}, },
title: 'Спорткар', title: getTranslation('preset_sportscar'),
imageUrl: images.sportcar, imageUrl: images.sportcar,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -31,7 +32,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#4CAF50', '#45A049'] colors: ['#4CAF50', '#45A049']
}, },
title: 'Скейтборд', title: getTranslation('preset_skateboard'),
imageUrl: images.skateboard, imageUrl: images.skateboard,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -45,7 +46,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#795548', '#5D4037'] colors: ['#795548', '#5D4037']
}, },
title: 'Кофе', title: getTranslation('preset_coffee'),
imageUrl: images.coffee, imageUrl: images.coffee,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -59,7 +60,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#FF69B4', '#FF1493'] colors: ['#FF69B4', '#FF1493']
}, },
title: 'Цветы', title: getTranslation('preset_flowers'),
imageUrl: images.flowers, imageUrl: images.flowers,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -73,7 +74,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#2196F3', '#1976D2'] colors: ['#2196F3', '#1976D2']
}, },
title: 'Шарик', title: getTranslation('preset_balloon'),
imageUrl: images.balloon, imageUrl: images.balloon,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -87,7 +88,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#9C27B0', '#7B1FA2'] colors: ['#9C27B0', '#7B1FA2']
}, },
title: 'Книга', title: getTranslation('preset_book'),
imageUrl: images.book, imageUrl: images.book,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -101,7 +102,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#FF69B4', '#FF1493'] colors: ['#FF69B4', '#FF1493']
}, },
title: 'Мороженое', title: getTranslation('preset_icecream'),
imageUrl: images.icecream, imageUrl: images.icecream,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -115,7 +116,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#3F51B5', '#303F9F'] colors: ['#3F51B5', '#303F9F']
}, },
title: 'Зонт', title: getTranslation('preset_umbrella'),
imageUrl: images.umbrella, imageUrl: images.umbrella,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -129,7 +130,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#E91E63', '#C2185B'] colors: ['#E91E63', '#C2185B']
}, },
title: 'Коктейль', title: getTranslation('preset_cocktail'),
imageUrl: images.cocktail, imageUrl: images.cocktail,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -143,7 +144,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#FF69B4', '#FF1493'] colors: ['#FF69B4', '#FF1493']
}, },
title: 'Подарок', title: getTranslation('preset_gift'),
imageUrl: images.gift, imageUrl: images.gift,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -157,7 +158,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#795548', '#5D4037'] colors: ['#795548', '#5D4037']
}, },
title: 'Собака', title: getTranslation('preset_dog'),
imageUrl: images.dog, imageUrl: images.dog,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -171,7 +172,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#607D8B', '#455A64'] colors: ['#607D8B', '#455A64']
}, },
title: 'Газета', title: getTranslation('preset_newspaper'),
imageUrl: images.newspaper, imageUrl: images.newspaper,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -185,7 +186,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#4CAF50', '#45A049'] colors: ['#4CAF50', '#45A049']
}, },
title: 'Велосипед', title: getTranslation('preset_bicycle'),
imageUrl: images.bicycle, imageUrl: images.bicycle,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -199,7 +200,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#00BCD4', '#0097A7'] colors: ['#00BCD4', '#0097A7']
}, },
title: 'Серфер', title: getTranslation('preset_surfer'),
imageUrl: images.surfing, imageUrl: images.surfing,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -213,7 +214,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#9E9E9E', '#757575'] colors: ['#9E9E9E', '#757575']
}, },
title: 'Детектив', title: getTranslation('preset_detective'),
imageUrl: images.detective, imageUrl: images.detective,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -227,7 +228,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#212121', '#000000'] colors: ['#212121', '#000000']
}, },
title: 'Байкер', title: getTranslation('preset_biker'),
imageUrl: images.moto, imageUrl: images.moto,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -241,7 +242,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#FF69B4', '#FF1493'] colors: ['#FF69B4', '#FF1493']
}, },
title: 'Фея', title: getTranslation('preset_fairy'),
imageUrl: images.fairy, imageUrl: images.fairy,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -255,7 +256,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#3F51B5', '#303F9F'] colors: ['#3F51B5', '#303F9F']
}, },
title: 'Ученый', title: getTranslation('preset_scientist'),
imageUrl: images.science, imageUrl: images.science,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -269,7 +270,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#795548', '#5D4037'] colors: ['#795548', '#5D4037']
}, },
title: 'Ковбой', title: getTranslation('preset_cowboy'),
imageUrl: images.cowboy, imageUrl: images.cowboy,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -283,7 +284,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#607D8B', '#455A64'] colors: ['#607D8B', '#455A64']
}, },
title: 'Рыцарь', title: getTranslation('preset_knight'),
imageUrl: images.knight, imageUrl: images.knight,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -297,7 +298,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#FF69B4', '#FF1493'] colors: ['#FF69B4', '#FF1493']
}, },
title: 'Балерина', title: getTranslation('preset_ballerina'),
imageUrl: images.balerina, imageUrl: images.balerina,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -311,7 +312,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#FF5722', '#F4511E'] colors: ['#FF5722', '#F4511E']
}, },
title: 'Пожарный', title: getTranslation('preset_firefighter'),
imageUrl: images.fire, imageUrl: images.fire,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -325,7 +326,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#FF5722', '#F4511E'] colors: ['#FF5722', '#F4511E']
}, },
title: 'Шеф-повар', title: getTranslation('preset_chef'),
imageUrl: images.cook, imageUrl: images.cook,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -343,7 +344,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#2196F3', '#1976D2'] colors: ['#2196F3', '#1976D2']
}, },
title: 'Деловой', title: getTranslation('preset_business'),
icon: '💼', icon: '💼',
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -357,7 +358,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#FF5722', '#F4511E'] colors: ['#FF5722', '#F4511E']
}, },
title: 'Повседневный', title: getTranslation('preset_casual'),
icon: '👕', icon: '👕',
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -371,7 +372,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#4CAF50', '#45A049'] colors: ['#4CAF50', '#45A049']
}, },
title: 'Спортивный', title: getTranslation('preset_sport'),
icon: '⚽', icon: '⚽',
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -385,7 +386,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#9C27B0', '#7B1FA2'] colors: ['#9C27B0', '#7B1FA2']
}, },
title: 'Вечерний', title: getTranslation('preset_evening'),
icon: '🌙', icon: '🌙',
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -399,7 +400,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#00BCD4', '#0097A7'] colors: ['#00BCD4', '#0097A7']
}, },
title: 'Пляжный', title: getTranslation('preset_beach'),
icon: '🏖️', icon: '🏖️',
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -413,7 +414,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#90CAF9', '#64B5F6'] colors: ['#90CAF9', '#64B5F6']
}, },
title: 'Зимний', title: getTranslation('preset_winter'),
icon: '❄️', icon: '❄️',
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -427,7 +428,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#795548', '#5D4037'] colors: ['#795548', '#5D4037']
}, },
title: 'Ретро', title: getTranslation('preset_retro'),
icon: '🎸', icon: '🎸',
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -441,7 +442,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#E91E63', '#9C27B0'] colors: ['#E91E63', '#9C27B0']
}, },
title: 'Киберпанк', title: getTranslation('preset_cyberpunk'),
icon: '🤖', icon: '🤖',
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -455,7 +456,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#4CAF50', '#2E7D32'] colors: ['#4CAF50', '#2E7D32']
}, },
title: 'Военный', title: getTranslation('preset_military'),
icon: '🪖', icon: '🪖',
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -469,7 +470,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#795548', '#3E2723'] colors: ['#795548', '#3E2723']
}, },
title: 'Стимпанк', title: getTranslation('preset_steampunk'),
icon: '⚙️', icon: '⚙️',
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -488,7 +489,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#4CAF50', '#45A049'] colors: ['#4CAF50', '#45A049']
}, },
title: 'Привет', title: getTranslation('preset_greeting'),
imageUrl: images.emoGreeting, imageUrl: images.emoGreeting,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -502,7 +503,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#2196F3', '#1976D2'] colors: ['#2196F3', '#1976D2']
}, },
title: 'Класс', title: getTranslation('preset_thumbsup'),
imageUrl: images.emoThumbsUp, imageUrl: images.emoThumbsUp,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -516,7 +517,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#FF69B4', '#FF1493'] colors: ['#FF69B4', '#FF1493']
}, },
title: 'Сердце', title: getTranslation('preset_heart'),
imageUrl: images.emoHeart, imageUrl: images.emoHeart,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -530,7 +531,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#FFD700', '#FFA500'] colors: ['#FFD700', '#FFA500']
}, },
title: 'Смех', title: getTranslation('preset_laugh'),
imageUrl: images.emoLaugh, imageUrl: images.emoLaugh,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -544,7 +545,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#9C27B0', '#7B1FA2'] colors: ['#9C27B0', '#7B1FA2']
}, },
title: 'Шок', title: getTranslation('preset_shock'),
imageUrl: images.emoShock, imageUrl: images.emoShock,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -558,7 +559,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#FF5722', '#F4511E'] colors: ['#FF5722', '#F4511E']
}, },
title: 'Угроза', title: getTranslation('preset_threat'),
imageUrl: images.emoThreat, imageUrl: images.emoThreat,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -572,7 +573,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#4CAF50', '#2E7D32'] colors: ['#4CAF50', '#2E7D32']
}, },
title: 'Халк', title: getTranslation('preset_hulk'),
imageUrl: images.emoHulk, imageUrl: images.emoHulk,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -586,7 +587,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#FF5722', '#F4511E'] colors: ['#FF5722', '#F4511E']
}, },
title: 'Огонь', title: getTranslation('preset_fire'),
imageUrl: images.emoFire, imageUrl: images.emoFire,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -600,7 +601,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#795548', '#5D4037'] colors: ['#795548', '#5D4037']
}, },
title: 'Чай', title: getTranslation('preset_tea'),
imageUrl: images.emoTea, imageUrl: images.emoTea,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -614,7 +615,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#9575CD', '#7E57C2'] colors: ['#9575CD', '#7E57C2']
}, },
title: 'Танец', title: getTranslation('preset_dance'),
imageUrl: images.emoDance, imageUrl: images.emoDance,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -629,7 +630,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#FFD700', '#FFA500'] colors: ['#FFD700', '#FFA500']
}, },
title: 'Трофей', title: getTranslation('preset_trophy'),
imageUrl: images.emoTrophy, imageUrl: images.emoTrophy,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -643,7 +644,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#795548', '#5D4037'] colors: ['#795548', '#5D4037']
}, },
title: 'Мишка', title: getTranslation('preset_teddybear'),
imageUrl: images.emoTeddyBear, imageUrl: images.emoTeddyBear,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -657,7 +658,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#FF69B4', '#FF1493'] colors: ['#FF69B4', '#FF1493']
}, },
title: 'Роза', title: getTranslation('preset_rose'),
imageUrl: images.emoRose, imageUrl: images.emoRose,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -671,7 +672,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#FF69B4', '#FF1493'] colors: ['#FF69B4', '#FF1493']
}, },
title: 'Подарок', title: getTranslation('preset_gift'),
imageUrl: images.emoGift, imageUrl: images.emoGift,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -685,7 +686,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#4CAF50', '#45A049'] colors: ['#4CAF50', '#45A049']
}, },
title: 'Букет', title: getTranslation('preset_flowers_bouquet'),
imageUrl: images.emoFlowers, imageUrl: images.emoFlowers,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -699,7 +700,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#9C27B0', '#7B1FA2'] colors: ['#9C27B0', '#7B1FA2']
}, },
title: 'Караоке', title: getTranslation('preset_karaoke'),
imageUrl: images.emoKaraoke, imageUrl: images.emoKaraoke,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -713,7 +714,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#4CAF50', '#2E7D32'] colors: ['#4CAF50', '#2E7D32']
}, },
title: 'Зомби', title: getTranslation('preset_zombie'),
imageUrl: images.emoZombie, imageUrl: images.emoZombie,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -727,7 +728,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#607D8B', '#455A64'] colors: ['#607D8B', '#455A64']
}, },
title: 'Бездомный', title: getTranslation('preset_homeless'),
imageUrl: images.emoHomeless, imageUrl: images.emoHomeless,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -741,7 +742,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#FF9800', '#F57C00'] colors: ['#FF9800', '#F57C00']
}, },
title: 'Пиво', title: getTranslation('preset_beer'),
imageUrl: images.emoBeer, imageUrl: images.emoBeer,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -755,7 +756,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#FF5722', '#F4511E'] colors: ['#FF5722', '#F4511E']
}, },
title: 'Бокс', title: getTranslation('preset_boxing'),
imageUrl: images.emoBoxing, imageUrl: images.emoBoxing,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -769,7 +770,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#FF69B4', '#FF1493'] colors: ['#FF69B4', '#FF1493']
}, },
title: 'Котёнок', title: getTranslation('preset_kitten'),
imageUrl: images.emoKitten, imageUrl: images.emoKitten,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -783,7 +784,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#795548', '#5D4037'] colors: ['#795548', '#5D4037']
}, },
title: 'Щенок', title: getTranslation('preset_puppy'),
imageUrl: images.emoPuppy, imageUrl: images.emoPuppy,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -797,7 +798,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#3F51B5', '#303F9F'] colors: ['#3F51B5', '#303F9F']
}, },
title: 'Супергерой', title: getTranslation('preset_superhero'),
imageUrl: images.emoSuperhero, imageUrl: images.emoSuperhero,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -811,7 +812,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#00BCD4', '#0097A7'] colors: ['#00BCD4', '#0097A7']
}, },
title: 'Мороженое', title: getTranslation('preset_icecream'),
imageUrl: images.emoIceCream, imageUrl: images.emoIceCream,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -825,7 +826,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#9C27B0', '#7B1FA2'] colors: ['#9C27B0', '#7B1FA2']
}, },
title: 'Волшебник', title: getTranslation('preset_wizard'),
imageUrl: images.emoWizard, imageUrl: images.emoWizard,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -839,7 +840,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#FF5722', '#F4511E'] colors: ['#FF5722', '#F4511E']
}, },
title: 'Дракон', title: getTranslation('preset_dragon'),
imageUrl: images.emoDragon, imageUrl: images.emoDragon,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -853,7 +854,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#212121', '#000000'] colors: ['#212121', '#000000']
}, },
title: 'Киберпанк', title: getTranslation('preset_cyberpunk'),
imageUrl: images.emoCyberpunk, imageUrl: images.emoCyberpunk,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -867,7 +868,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#3F51B5', '#303F9F'] colors: ['#3F51B5', '#303F9F']
}, },
title: 'Маг', title: getTranslation('preset_mage'),
imageUrl: images.emoMage, imageUrl: images.emoMage,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -881,7 +882,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#795548', '#5D4037'] colors: ['#795548', '#5D4037']
}, },
title: 'Пират', title: getTranslation('preset_pirate'),
imageUrl: images.emoPirate, imageUrl: images.emoPirate,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -895,7 +896,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#212121', '#000000'] colors: ['#212121', '#000000']
}, },
title: 'Самурай', title: getTranslation('preset_samurai'),
imageUrl: images.emoSamurai, imageUrl: images.emoSamurai,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -909,7 +910,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#607D8B', '#455A64'] colors: ['#607D8B', '#455A64']
}, },
title: 'Учёный', title: getTranslation('preset_scientist'),
imageUrl: images.emoScientist, imageUrl: images.emoScientist,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -923,7 +924,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#9C27B0', '#7B1FA2'] colors: ['#9C27B0', '#7B1FA2']
}, },
title: 'Вино', title: getTranslation('preset_wine'),
imageUrl: images.emoWine, imageUrl: images.emoWine,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -937,7 +938,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#4CAF50', '#2E7D32'] colors: ['#4CAF50', '#2E7D32']
}, },
title: 'Слизь', title: getTranslation('preset_slime'),
imageUrl: images.emoSlime, imageUrl: images.emoSlime,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -951,7 +952,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#795548', '#5D4037'] colors: ['#795548', '#5D4037']
}, },
title: 'Всадник', title: getTranslation('preset_rider'),
imageUrl: images.emoRider, imageUrl: images.emoRider,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -965,7 +966,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#3F51B5', '#303F9F'] colors: ['#3F51B5', '#303F9F']
}, },
title: 'Доктор Стрэндж', title: getTranslation('preset_drstrange'),
imageUrl: images.emoDrStrange, imageUrl: images.emoDrStrange,
action: { action: {
type: 'selectPreset', type: 'selectPreset',
@ -979,7 +980,7 @@ export const stylePresets: StylePresets = {
type: 'gradient', type: 'gradient',
colors: ['#3F51B5', '#303F9F'] colors: ['#3F51B5', '#303F9F']
}, },
title: 'Капитан Америка', title: getTranslation('preset_captainamerica'),
imageUrl: images.emoCaptainAmerica, imageUrl: images.emoCaptainAmerica,
action: { action: {
type: 'selectPreset', type: 'selectPreset',

View File

@ -0,0 +1,797 @@
/**
* Файл с переводами и функциями для локализации
*/
// Импортируем функцию для получения информации о пользователе
import { getUserInfo } from './user';
// Типы для объектов переводов
type TranslationDictionary = {
[key: string]: string;
};
type Translations = {
[language: string]: TranslationDictionary;
};
// Объект с переводами
export const translations: Translations = {
ru: {
// Тексты из homeScreen.ts
'feedback': 'Обратная связь',
'instruction': 'Инструкция',
'shortsLoader': 'ShortsLoader',
'uploadPhoto': 'Загрузи фото с лицом',
'auto': 'Авто',
'man': 'Мужчина',
'woman': 'Женщина',
'chooseStyle': 'Выбери стиль',
'emotions': 'Эмоции',
'chibi': 'Чиби',
'realism': 'Реализм',
'chooseImage': 'Выбери образ',
'collection': 'Коллекция',
'memes': 'Мемы',
'loading': 'Загрузка...',
'generate': 'Сгенерировать',
'meme1': 'Мем 1',
'meme2': 'Мем 2',
'meme3': 'Мем 3',
'meme4': 'Мем 4',
'meme5': 'Мем 5',
'meme6': 'Мем 6',
'meme7': 'Мем 7',
'meme8': 'Мем 8',
'meme9': 'Мем 9',
'meme10': 'Мем 10',
// Тексты из OnboardingWelcome.tsx
'welcome_title': 'Добро пожаловать в Sticker Generator',
'welcome_description': 'Создавайте уникальные стикеры из ваших фотографий с помощью искусственного интеллекта',
'next': 'Далее',
'skip': 'Пропустить',
// Тексты из OnboardingHowTo.tsx
'how_to_title': 'Как создать стикер',
'upload_photo_title': 'Загрузите фото',
'upload_photo_description': 'Выберите фотографию и обрежьте её в квадрат',
'choose_style_title': 'Выберите стиль',
'choose_style_description': 'Подберите подходящий стиль для вашего стикера',
'create_sticker_title': 'Создайте стикер',
'create_sticker_description': 'Дождитесь генерации и сохраните результат',
// Тексты из OnboardingStickerPacks.tsx
'sticker_packs_title': 'Создавайте стикерпаки',
'start': 'Начать',
'select_stickers_title': 'Выберите стикеры',
'select_stickers_description': 'Отберите лучшие стикеры из вашей галереи',
'organize_pack_title': 'Организуйте набор',
'organize_pack_description': 'Расположите стикеры в нужном порядке',
'publish_title': 'Опубликуйте',
'publish_description': 'Создайте стикерпак в Telegram одним нажатием',
// Тексты из TermsAndConditions.tsx
'terms_title': 'Условия и Конфиденциальность',
'terms_description': 'Продолжая использовать это приложение, вы соглашаетесь с нашими Условиями использования и Политикой конфиденциальности.',
'terms_of_use': 'Условия использования',
'privacy_policy': 'Политика конфиденциальности',
'accept': 'Принять',
'decline': 'Отклонить',
// Тексты для NotificationModal
'continue': 'Продолжить',
'gallery': 'В галерею',
'generating': 'Генерация...',
'generated_sticker': 'Сгенерированный стикер',
// Тексты для useNotifications
'cancel': 'Отменить',
'close': 'Закрыть',
'connection_error': 'Возникли проблемы с подключением. Пожалуйста, проверьте интернет-соединение.\n\nЗапрос все еще обрабатывается, но это может занять больше времени, чем обычно.',
'feedback_thanks': 'Спасибо за обратную связь',
'feedback_sent': 'Ваше сообщение успешно отправлено',
'payment_success': 'Оплата успешна!',
'tokens_purchased': 'Вы успешно приобрели {0} токенов.',
// Тексты для UploadPhotoBlock
'change_photo': 'Изменить фото',
'upload_photo_block_title': 'Загрузите фото',
'upload_photo_block_subtitle': 'Перетащите или нажмите для выбора',
// Тексты для GridButtonsBlock
'show_more': 'Показать больше',
'collapse': 'Свернуть',
// Тексты для stylePresets (Чиби)
'preset_sportscar': 'Спорткар',
'preset_skateboard': 'Скейтборд',
'preset_coffee': 'Кофе',
'preset_flowers': 'Цветы',
'preset_balloon': 'Шарик',
'preset_book': 'Книга',
'preset_icecream': 'Мороженое',
'preset_umbrella': 'Зонт',
'preset_cocktail': 'Коктейль',
'preset_gift': 'Подарок',
'preset_dog': 'Собака',
'preset_newspaper': 'Газета',
'preset_bicycle': 'Велосипед',
'preset_surfer': 'Серфер',
'preset_detective': 'Детектив',
'preset_biker': 'Байкер',
'preset_fairy': 'Фея',
'preset_scientist': 'Ученый',
'preset_cowboy': 'Ковбой',
'preset_knight': 'Рыцарь',
'preset_ballerina': 'Балерина',
'preset_firefighter': 'Пожарный',
'preset_chef': 'Шеф-повар',
// Тексты для stylePresets (Реализм)
'preset_business': 'Деловой',
'preset_casual': 'Повседневный',
'preset_sport': 'Спортивный',
'preset_evening': 'Вечерний',
'preset_beach': 'Пляжный',
'preset_winter': 'Зимний',
'preset_retro': 'Ретро',
'preset_cyberpunk': 'Киберпанк',
'preset_military': 'Военный',
'preset_steampunk': 'Стимпанк',
// Тексты для stylePresets (Эмоции)
'preset_greeting': 'Привет',
'preset_thumbsup': 'Класс',
'preset_heart': 'Сердце',
'preset_laugh': 'Смех',
'preset_shock': 'Шок',
'preset_threat': 'Угроза',
'preset_hulk': 'Халк',
'preset_fire': 'Огонь',
'preset_tea': 'Чай',
'preset_dance': 'Танец',
'preset_trophy': 'Трофей',
'preset_teddybear': 'Мишка',
'preset_rose': 'Роза',
'preset_flowers_bouquet': 'Букет',
'preset_karaoke': 'Караоке',
'preset_zombie': 'Зомби',
'preset_homeless': 'Бездомный',
'preset_beer': 'Пиво',
'preset_boxing': 'Бокс',
'preset_kitten': 'Котёнок',
'preset_puppy': 'Щенок',
'preset_superhero': 'Супергерой',
'preset_wizard': 'Волшебник',
'preset_dragon': 'Дракон',
'preset_mage': 'Маг',
'preset_pirate': 'Пират',
'preset_samurai': 'Самурай',
'preset_wine': 'Вино',
'preset_slime': 'Слизь',
'preset_rider': 'Всадник',
'preset_drstrange': 'Доктор Стрэндж',
'preset_captainamerica': 'Капитан Америка',
// Тексты для GenerateButton
'start_generation': 'Начать генерацию',
'tokens_count': '{0} токенов',
// Тексты для Gallery
'gallery_title': 'Галерея стикеров',
'gallery_generating_section': 'В процессе генерации',
'gallery_loading': 'Загрузка изображений...',
'gallery_empty': 'У вас пока нет сгенерированных стикеров',
'gallery_refreshing': 'Обновление...',
'gallery_queue_position': 'В очереди: {0}',
'gallery_generating': 'Генерация...',
'gallery_time_left': 'Осталось: {0}',
'gallery_sticker_alt': 'Стикер {0}',
'gallery_create_pack': 'Создать стикерпак',
// Тексты для модального окна удаления
'gallery_delete_title': 'Удаление стикера',
'gallery_delete_message': 'Вы уверены, что хотите удалить этот стикер?',
'gallery_delete_cancel': 'Отмена',
'gallery_delete_confirm': 'Удалить',
// Тексты для расчета времени ожидания
'gallery_generation_started': 'Генерация началась',
'gallery_seconds': '{0} сек',
'gallery_minutes_seconds': '{0} мин {1} сек',
// Тексты для StickerPacks
'stickerpacks_title': 'Мои стикерпаки',
'stickerpacks_subtitle': 'Создавайте и публикуйте наборы стикеров в Telegram',
'stickerpacks_pack_title_prefix': 'Стикерпак',
'stickerpacks_pack_stats': '{0} / 49 стикеров',
'stickerpacks_loading': 'Загрузка стикерпаков...',
'stickerpacks_empty': 'У вас пока нет стикерпаков',
'stickerpacks_error': 'Не удалось загрузить список стикерпаков',
'stickerpacks_sticker_id_error': 'Не удалось определить ID стикера',
'stickerpacks_delete_sticker_error': 'Не удалось удалить стикер',
'stickerpacks_delete_pack_error': 'Не удалось удалить стикерпак',
'stickerpacks_create_button': 'Создать стикерпак',
'stickerpacks_retry_button': 'Повторить',
'stickerpacks_delete_button': 'Удалить',
'stickerpacks_back_button': '← Назад',
'stickerpacks_add_sticker_button': 'Добавить стикер',
'stickerpacks_open_telegram_button': 'Открыть в Telegram',
'stickerpacks_sticker_alt': 'Стикер {0}',
// Тексты для модальных окон удаления стикерпаков
'stickerpacks_delete_pack_title': 'Удаление стикерпака',
'stickerpacks_delete_pack_message': 'Вы уверены, что хотите удалить этот стикерпак?',
'stickerpacks_delete_sticker_title': 'Удаление стикера',
'stickerpacks_delete_sticker_message': 'Вы уверены, что хотите удалить этот стикер?',
'stickerpacks_cancel_button': 'Отмена',
'stickerpacks_confirm_delete_button': 'Удалить',
// Тексты для Profile
'profile_title': 'Профиль',
'profile_subtitle': 'Ваша статистика и настройки',
'profile_stickers_created': 'Стикеров создано',
'profile_sticker_packs': 'Стикерпаков',
'profile_tokens': 'Токенов',
'profile_payment_success': 'Оплата успешна!',
'profile_tokens_purchased': 'Вы успешно приобрели {0} токенов. Ваш текущий баланс: {1} токенов.',
'profile_close': 'Закрыть',
// Тексты для TokenPacksList
'token_packs_title': 'Пакеты токенов',
// Тексты для TokenPackCard
'token_pack_popular': 'Популярный выбор',
'token_pack_best_value': 'Максимальная выгода',
'token_pack_tokens': 'Токены',
'token_pack_bonus': '+{0} БОНУС',
'token_pack_stickers_count': '{0} стикеров',
'token_pack_buy': 'КУПИТЬ',
'token_pack_stars': 'Stars ⭐',
// Названия и описания пакетов токенов
'token_pack_basic_title': 'Стартовый набор стикеромана',
'token_pack_basic_description': 'Идеальный вариант для начала! Сгенерируйте 15 уникальных стикеров для вашего Telegram.',
'token_pack_optimal_title': 'Стикерный запас',
'token_pack_optimal_description': 'Создавайте стикеры для всех случаев жизни с оптимальным запасом токенов.',
'token_pack_advanced_title': 'Стикерный энтузиаст',
'token_pack_advanced_description': 'Расширьте свои возможности! Создавайте стикеры без ограничений.',
'token_pack_super_title': 'Стикерный магнат',
'token_pack_super_description': 'Для самых требовательных. Создавайте целые коллекции стикеров с максимальной выгодой.',
'token_pack_unlimited_title': 'Бог стикеров',
'token_pack_unlimited_description': 'Для профессионалов и настоящих ценителей. Неограниченные возможности для творчества с максимальной выгодой.',
// Для CropPhoto.tsx
'crop_photo_title': 'Обрезка фото',
'crop_photo_done': 'Готово',
'crop_photo_hint': 'Отрегулируйте масштаб и положение фото',
// Для TokenPacksModal.tsx
'token_modal_title': 'Недостаточно токенов для генерации',
'token_modal_show_all': 'Показать все пакеты',
// Для Navigation.tsx
'nav_home': 'Главная',
'nav_gallery': 'Галерея',
'nav_sticker_packs': 'Стикерпаки',
'nav_profile': 'Профиль',
// Для useGenerationState.ts
'generation_attention': 'Внимание',
'generation_duplicate_error': 'Нельзя отправить одну и ту же комбинацию изображения и образа подряд. Пожалуйста, измените изображение или выберите другой образ.',
'generation_title': 'Генерация стикера',
'generation_sending': 'Отправка запроса...',
'generation_connection_error': 'Возникли проблемы с подключением. Пожалуйста, проверьте интернет-соединение.',
'generation_invalid_prompt_title': 'Недопустимый промпт',
'generation_invalid_prompt_message': 'Промпт содержит недопустимый контент. Пожалуйста, используйте более нейтральные формулировки.',
'generation_started': 'Создание стикеров началось!',
'generation_queue_position': 'Позиция в очереди: {0}',
'generation_wait_time': 'Время ожидания: {0}',
'generation_result_available': 'Результат будет доступен в галерее после завершения генерации.',
'generation_error_title': 'Ошибка',
'generation_error_message': 'Не удалось начать генерацию',
// Для CreateSticker.tsx
'create_sticker_screen_title': 'Создание стикера',
'create_sticker_screen_subtitle': 'Загрузите фотографию для создания стикера',
'create_sticker_screen_upload_text': 'Нажмите чтобы выбрать фото',
'create_sticker_screen_upload_hint': 'или перетащите файл сюда',
'create_sticker_screen_cost': 'Стоимость: {0} токенов',
'create_sticker_screen_balance_error': 'Произошла ошибка при проверке баланса. Попробуйте позже.',
// Для форматирования времени
'time_minutes_short': 'мин',
'time_seconds_short': 'сек',
// Для компонентов с жестко закодированными строками
'create_first': 'Сначала создайте изображения в разделе "Создать стикер".',
'add_balance_title': 'Нажмите чтобы пополнить баланс',
'tokens_alt': 'Токены',
'feedback_placeholder': 'Опишите вашу проблему или предложение...',
'sticker_description_placeholder': 'Опишите, какой стикер вы хотите получить...',
'sticker_pack_name_label': 'Название стикерпака',
'fullscreen_view_alt': 'Полноэкранный просмотр',
'preview_alt': 'Предпросмотр',
'avatar_alt': 'Аватар',
// Для сообщений об ошибках в сервисах
'invalid_prompt': 'Недопустимый промпт',
'failed_to_generate': 'Не удалось сгенерировать изображение',
'failed_to_translate': 'Не удалось перевести текст',
// Для AddStickerToPackScreen.tsx
'back_button': '← Назад',
'add_stickers_to_pack_title': 'Добавление стикеров в "{0}"',
'loading_data': 'Загрузка данных...',
'select_images_for_stickers': 'Выберите изображения для стикеров',
'image_alt': 'Изображение {0}',
'stickers_not_selected': 'Стикеры не выбраны',
'select_at_least_one_image': 'Пожалуйста, выберите хотя бы одно изображение для добавления в стикерпак.',
'error': 'Ошибка',
'pack_name_not_specified': 'Не указано имя стикерпака.',
'failed_to_add_stickers': 'Не удалось добавить стикеры в стикерпак. Пожалуйста, попробуйте еще раз.',
'adding': 'Добавление...',
'add_stickers': 'Добавить стикеры',
'failed_to_load_data': 'Не удалось загрузить данные',
// Для FeedbackModal.tsx
'feedback_title': 'Обратная связь',
'feedback_subtitle': 'Расскажите нам о проблеме или предложении',
'add_image': 'Добавить изображение',
'loading_image': 'Загрузка...',
'preview_image_alt': 'Превью {0}',
'select_image_file': 'Пожалуйста, выберите файл изображения (JPEG, PNG и т.д.)',
'file_selection_error': 'Произошла ошибка при выборе файла. Пожалуйста, попробуйте еще раз.',
'enter_text_or_add_image': 'Пожалуйста, введите текст или добавьте изображение',
'failed_to_send_feedback': 'Не удалось отправить обратную связь. Пожалуйста, попробуйте позже.',
'error_sending_feedback': 'Произошла ошибка при отправке. Пожалуйста, попробуйте позже.',
// Для модальных окон валидации генерации
'select_style_warning_title': 'Внимание',
'select_style_warning_message': 'Выберите образ для генерации',
'upload_image_warning_title': 'Внимание',
'upload_image_warning_message': 'Сначала загрузите изображение',
'enter_prompt_warning_title': 'Внимание',
'enter_prompt_warning_message': 'Введите текст промпта',
// Для модального окна выбора эмодзи
'emoji_picker_title': 'Выберите эмодзи',
'emoji_picker_cancel': 'Отмена',
'emoji_picker_apply': 'Применить',
// Для экрана создания стикерпака
'create_sticker_pack_back': '← Назад',
'create_sticker_pack_title': 'Создание стикерпака',
'create_sticker_pack_select_images': 'Выберите изображения для стикеров',
'create_sticker_pack_loading': 'Загрузка изображений...',
'create_sticker_pack_image_alt': 'Изображение {0}',
'create_sticker_pack_creating': 'Создание...',
'create_sticker_pack_create': 'Создать стикерпак',
'create_sticker_pack_name_required_title': 'Название не указано',
'create_sticker_pack_name_required_message': 'Пожалуйста, введите название для вашего стикерпака.',
'create_sticker_pack_no_stickers_title': 'Стикеры не выбраны',
'create_sticker_pack_no_stickers_message': 'Пожалуйста, выберите хотя бы одно изображение для вашего стикерпака.',
'create_sticker_pack_name_occupied_title': 'Имя стикерпака уже занято',
'create_sticker_pack_name_occupied_message': 'Стикерпак с таким именем уже существует в Telegram. Пожалуйста, измените название и попробуйте снова.',
'create_sticker_pack_error_title': 'Ошибка при создании стикерпака',
'create_sticker_pack_error_message': 'Не удалось создать стикерпак. Пожалуйста, попробуйте еще раз с другим названием или изображениями.'
},
en: {
// Английские переводы
'feedback': 'Feedback',
'instruction': 'Instructions',
'shortsLoader': 'ShortsLoader',
'uploadPhoto': 'Upload a photo with a face',
'auto': 'Auto',
'man': 'Male',
'woman': 'Female',
'chooseStyle': 'Choose style',
'emotions': 'Emotions',
'chibi': 'Chibi',
'realism': 'Realism',
'chooseImage': 'Choose image',
'collection': 'Collection',
'memes': 'Memes',
'loading': 'Loading...',
'generate': 'Generate',
'meme1': 'Meme 1',
'meme2': 'Meme 2',
'meme3': 'Meme 3',
'meme4': 'Meme 4',
'meme5': 'Meme 5',
'meme6': 'Meme 6',
'meme7': 'Meme 7',
'meme8': 'Meme 8',
'meme9': 'Meme 9',
'meme10': 'Meme 10',
// Английские переводы для OnboardingWelcome
'welcome_title': 'Welcome to Sticker Generator',
'welcome_description': 'Create unique stickers from your photos using artificial intelligence',
'next': 'Next',
'skip': 'Skip',
// Английские переводы для OnboardingHowTo
'how_to_title': 'How to Create a Sticker',
'upload_photo_title': 'Upload a Photo',
'upload_photo_description': 'Choose a photo and crop it into a square',
'choose_style_title': 'Choose a Style',
'choose_style_description': 'Select a suitable style for your sticker',
'create_sticker_title': 'Create a Sticker',
'create_sticker_description': 'Wait for generation and save the result',
// Английские переводы для OnboardingStickerPacks
'sticker_packs_title': 'Create Sticker Packs',
'start': 'Start',
'select_stickers_title': 'Select Stickers',
'select_stickers_description': 'Choose the best stickers from your gallery',
'organize_pack_title': 'Organize Pack',
'organize_pack_description': 'Arrange stickers in the desired order',
'publish_title': 'Publish',
'publish_description': 'Create a Telegram sticker pack with one click',
// Английские переводы для TermsAndConditions
'terms_title': 'Terms and Privacy',
'terms_description': 'By continuing to use this application, you agree to our Terms of Use and Privacy Policy.',
'terms_of_use': 'Terms of Use',
'privacy_policy': 'Privacy Policy',
'accept': 'Accept',
'decline': 'Decline',
// Английские переводы для NotificationModal
'continue': 'Continue',
'gallery': 'To Gallery',
'generating': 'Generating...',
'generated_sticker': 'Generated sticker',
// Английские переводы для useNotifications
'cancel': 'Cancel',
'close': 'Close',
'connection_error': 'Connection problems detected. Please check your internet connection.\n\nThe request is still being processed, but it may take longer than usual.',
'feedback_thanks': 'Thank you for your feedback',
'feedback_sent': 'Your message has been successfully sent',
'payment_success': 'Payment successful!',
'tokens_purchased': 'You have successfully purchased {0} tokens.',
// Английские переводы для UploadPhotoBlock
'change_photo': 'Change Photo',
'upload_photo_block_title': 'Upload Photo',
'upload_photo_block_subtitle': 'Drag and drop or click to select',
// Английские переводы для GridButtonsBlock
'show_more': 'Show More',
'collapse': 'Collapse',
// Английские переводы для stylePresets (Чиби)
'preset_sportscar': 'Sports Car',
'preset_skateboard': 'Skateboard',
'preset_coffee': 'Coffee',
'preset_flowers': 'Flowers',
'preset_balloon': 'Balloon',
'preset_book': 'Book',
'preset_icecream': 'Ice Cream',
'preset_umbrella': 'Umbrella',
'preset_cocktail': 'Cocktail',
'preset_gift': 'Gift',
'preset_dog': 'Dog',
'preset_newspaper': 'Newspaper',
'preset_bicycle': 'Bicycle',
'preset_surfer': 'Surfer',
'preset_detective': 'Detective',
'preset_biker': 'Biker',
'preset_fairy': 'Fairy',
'preset_scientist': 'Scientist',
'preset_cowboy': 'Cowboy',
'preset_knight': 'Knight',
'preset_ballerina': 'Ballerina',
'preset_firefighter': 'Firefighter',
'preset_chef': 'Chef',
// Английские переводы для stylePresets (Реализм)
'preset_business': 'Business',
'preset_casual': 'Casual',
'preset_sport': 'Sport',
'preset_evening': 'Evening',
'preset_beach': 'Beach',
'preset_winter': 'Winter',
'preset_retro': 'Retro',
'preset_cyberpunk': 'Cyberpunk',
'preset_military': 'Military',
'preset_steampunk': 'Steampunk',
// Английские переводы для stylePresets (Эмоции)
'preset_greeting': 'Greeting',
'preset_thumbsup': 'Thumbs Up',
'preset_heart': 'Heart',
'preset_laugh': 'Laugh',
'preset_shock': 'Shock',
'preset_threat': 'Threat',
'preset_hulk': 'Hulk',
'preset_fire': 'Fire',
'preset_tea': 'Tea',
'preset_dance': 'Dance',
'preset_trophy': 'Trophy',
'preset_teddybear': 'Teddy Bear',
'preset_rose': 'Rose',
'preset_flowers_bouquet': 'Bouquet',
'preset_karaoke': 'Karaoke',
'preset_zombie': 'Zombie',
'preset_homeless': 'Homeless',
'preset_beer': 'Beer',
'preset_boxing': 'Boxing',
'preset_kitten': 'Kitten',
'preset_puppy': 'Puppy',
'preset_superhero': 'Superhero',
'preset_wizard': 'Wizard',
'preset_dragon': 'Dragon',
'preset_mage': 'Mage',
'preset_pirate': 'Pirate',
'preset_samurai': 'Samurai',
'preset_wine': 'Wine',
'preset_slime': 'Slime',
'preset_rider': 'Rider',
'preset_drstrange': 'Dr. Strange',
'preset_captainamerica': 'Captain America',
// Английские переводы для GenerateButton
'start_generation': 'Start Generation',
'tokens_count': '{0} tokens',
// Английские переводы для Gallery
'gallery_title': 'Sticker Gallery',
'gallery_generating_section': 'In Progress',
'gallery_loading': 'Loading images...',
'gallery_empty': 'You don\'t have any generated stickers yet',
'gallery_refreshing': 'Refreshing...',
'gallery_queue_position': 'In queue: {0}',
'gallery_generating': 'Generating...',
'gallery_time_left': 'Time left: {0}',
'gallery_sticker_alt': 'Sticker {0}',
'gallery_create_pack': 'Create Sticker Pack',
// Английские переводы для модального окна удаления
'gallery_delete_title': 'Delete Sticker',
'gallery_delete_message': 'Are you sure you want to delete this sticker?',
'gallery_delete_cancel': 'Cancel',
'gallery_delete_confirm': 'Delete',
// Английские переводы для расчета времени ожидания
'gallery_generation_started': 'Generation started',
'gallery_seconds': '{0} sec',
'gallery_minutes_seconds': '{0} min {1} sec',
// Английские переводы для StickerPacks
'stickerpacks_title': 'My Sticker Packs',
'stickerpacks_subtitle': 'Create and publish sticker sets in Telegram',
'stickerpacks_pack_title_prefix': 'Sticker Pack',
'stickerpacks_pack_stats': '{0} / 49 stickers',
'stickerpacks_loading': 'Loading sticker packs...',
'stickerpacks_empty': 'You don\'t have any sticker packs yet',
'stickerpacks_error': 'Failed to load sticker packs',
'stickerpacks_sticker_id_error': 'Failed to determine sticker ID',
'stickerpacks_delete_sticker_error': 'Failed to delete sticker',
'stickerpacks_delete_pack_error': 'Failed to delete sticker pack',
'stickerpacks_create_button': 'Create Sticker Pack',
'stickerpacks_retry_button': 'Retry',
'stickerpacks_delete_button': 'Delete',
'stickerpacks_back_button': '← Back',
'stickerpacks_add_sticker_button': 'Add Sticker',
'stickerpacks_open_telegram_button': 'Open in Telegram',
'stickerpacks_sticker_alt': 'Sticker {0}',
// Английские переводы для модальных окон удаления стикерпаков
'stickerpacks_delete_pack_title': 'Delete Sticker Pack',
'stickerpacks_delete_pack_message': 'Are you sure you want to delete this sticker pack?',
'stickerpacks_delete_sticker_title': 'Delete Sticker',
'stickerpacks_delete_sticker_message': 'Are you sure you want to delete this sticker?',
'stickerpacks_cancel_button': 'Cancel',
'stickerpacks_confirm_delete_button': 'Delete',
// Английские переводы для Profile
'profile_title': 'Profile',
'profile_subtitle': 'Your statistics and settings',
'profile_stickers_created': 'Stickers created',
'profile_sticker_packs': 'Sticker packs',
'profile_tokens': 'Tokens',
'profile_payment_success': 'Payment successful!',
'profile_tokens_purchased': 'You have successfully purchased {0} tokens. Your current balance: {1} tokens.',
'profile_close': 'Close',
// Английские переводы для TokenPacksList
'token_packs_title': 'Token Packages',
// Английские переводы для TokenPackCard
'token_pack_popular': 'Popular choice',
'token_pack_best_value': 'Best value',
'token_pack_tokens': 'Tokens',
'token_pack_bonus': '+{0} BONUS',
'token_pack_stickers_count': '{0} stickers',
'token_pack_buy': 'BUY',
'token_pack_stars': 'Stars ⭐',
// Названия и описания пакетов токенов (английские)
'token_pack_basic_title': 'Sticker Beginner Pack',
'token_pack_basic_description': 'Perfect for beginners! Generate 15 unique stickers for your Telegram.',
'token_pack_optimal_title': 'Sticker Supply',
'token_pack_optimal_description': 'Create stickers for all occasions with an optimal token supply.',
'token_pack_advanced_title': 'Sticker Enthusiast',
'token_pack_advanced_description': 'Expand your possibilities! Create stickers without limitations.',
'token_pack_super_title': 'Sticker Magnate',
'token_pack_super_description': 'For the most demanding users. Create entire collections of stickers with maximum value.',
'token_pack_unlimited_title': 'Sticker God',
'token_pack_unlimited_description': 'For professionals and true connoisseurs. Unlimited creative possibilities with maximum value.',
// Для CropPhoto.tsx (английские)
'crop_photo_title': 'Crop Photo',
'crop_photo_done': 'Done',
'crop_photo_hint': 'Adjust the scale and position of the photo',
// Для TokenPacksModal.tsx (английские)
'token_modal_title': 'Not enough tokens for generation',
'token_modal_show_all': 'Show all packages',
// Для Navigation.tsx (английские)
'nav_home': 'Home',
'nav_gallery': 'Gallery',
'nav_sticker_packs': 'Sticker Packs',
'nav_profile': 'Profile',
// Для useGenerationState.ts (английские)
'generation_attention': 'Attention',
'generation_duplicate_error': 'You cannot send the same combination of image and style in a row. Please change the image or select a different style.',
'generation_title': 'Sticker Generation',
'generation_sending': 'Sending request...',
'generation_connection_error': 'Connection problems detected. Please check your internet connection.',
'generation_invalid_prompt_title': 'Invalid Prompt',
'generation_invalid_prompt_message': 'The prompt contains inappropriate content. Please use more neutral wording.',
'generation_started': 'Sticker creation has started!',
'generation_queue_position': 'Queue position: {0}',
'generation_wait_time': 'Estimated wait time: {0}',
'generation_result_available': 'The result will be available in the gallery after generation is complete.',
'generation_error_title': 'Error',
'generation_error_message': 'Failed to start generation',
// Для CreateSticker.tsx (английские)
'create_sticker_screen_title': 'Create Sticker',
'create_sticker_screen_subtitle': 'Upload a photo to create a sticker',
'create_sticker_screen_upload_text': 'Click to select a photo',
'create_sticker_screen_upload_hint': 'or drag and drop a file here',
'create_sticker_screen_cost': 'Cost: {0} tokens',
'create_sticker_screen_balance_error': 'An error occurred while checking your balance. Please try again later.',
// Для форматирования времени (английские)
'time_minutes_short': 'min',
'time_seconds_short': 'sec',
// Для компонентов с жестко закодированными строками (английские)
'create_first': 'First create images in the "Create Sticker" section.',
'add_balance_title': 'Click to add balance',
'tokens_alt': 'Tokens',
'feedback_placeholder': 'Describe your issue or suggestion...',
'sticker_description_placeholder': 'Describe what kind of sticker you want to get...',
'sticker_pack_name_label': 'Sticker pack name',
'fullscreen_view_alt': 'Fullscreen view',
'preview_alt': 'Preview',
'avatar_alt': 'Avatar',
// Для сообщений об ошибках в сервисах (английские)
'invalid_prompt': 'Invalid prompt',
'failed_to_generate': 'Failed to generate image',
'failed_to_translate': 'Failed to translate text',
// Для AddStickerToPackScreen.tsx (английские)
'back_button': '← Back',
'add_stickers_to_pack_title': 'Add stickers to "{0}"',
'loading_data': 'Loading data...',
'select_images_for_stickers': 'Select images for stickers',
'image_alt': 'Image {0}',
'stickers_not_selected': 'No stickers selected',
'select_at_least_one_image': 'Please select at least one image to add to the sticker pack.',
'error': 'Error',
'pack_name_not_specified': 'Sticker pack name not specified.',
'failed_to_add_stickers': 'Failed to add stickers to the sticker pack. Please try again.',
'adding': 'Adding...',
'add_stickers': 'Add stickers',
'failed_to_load_data': 'Failed to load data',
// Для FeedbackModal.tsx (английские)
'feedback_title': 'Feedback',
'feedback_subtitle': 'Tell us about your issue or suggestion',
'add_image': 'Add image',
'loading_image': 'Loading...',
'preview_image_alt': 'Preview {0}',
'select_image_file': 'Please select an image file (JPEG, PNG, etc.)',
'file_selection_error': 'An error occurred while selecting the file. Please try again.',
'enter_text_or_add_image': 'Please enter text or add an image',
'failed_to_send_feedback': 'Failed to send feedback. Please try again later.',
'error_sending_feedback': 'An error occurred while sending. Please try again later.',
// Для модальных окон валидации генерации (английские)
'select_style_warning_title': 'Attention',
'select_style_warning_message': 'Please select a style for generation',
'upload_image_warning_title': 'Attention',
'upload_image_warning_message': 'Please upload an image first',
'enter_prompt_warning_title': 'Attention',
'enter_prompt_warning_message': 'Please enter prompt text',
// Для модального окна выбора эмодзи (английские)
'emoji_picker_title': 'Choose emoji',
'emoji_picker_cancel': 'Cancel',
'emoji_picker_apply': 'Apply',
// Для экрана создания стикерпака (английские)
'create_sticker_pack_back': '← Back',
'create_sticker_pack_title': 'Create Sticker Pack',
'create_sticker_pack_select_images': 'Select images for stickers',
'create_sticker_pack_loading': 'Loading images...',
'create_sticker_pack_image_alt': 'Image {0}',
'create_sticker_pack_creating': 'Creating...',
'create_sticker_pack_create': 'Create Sticker Pack',
'create_sticker_pack_name_required_title': 'Name Required',
'create_sticker_pack_name_required_message': 'Please enter a name for your sticker pack.',
'create_sticker_pack_no_stickers_title': 'No Stickers Selected',
'create_sticker_pack_no_stickers_message': 'Please select at least one image for your sticker pack.',
'create_sticker_pack_name_occupied_title': 'Sticker Pack Name Already Taken',
'create_sticker_pack_name_occupied_message': 'A sticker pack with this name already exists in Telegram. Please change the name and try again.',
'create_sticker_pack_error_title': 'Error Creating Sticker Pack',
'create_sticker_pack_error_message': 'Failed to create sticker pack. Please try again with a different name or images.'
}
};
/**
* Функция для форматирования строки с параметрами
* @param str Строка с плейсхолдерами {0}, {1}, ...
* @param args Аргументы для замены плейсхолдеров
* @returns Отформатированная строка
*/
export const formatString = (str: string, ...args: any[]): string => {
return str.replace(/{(\d+)}/g, (match, index) => {
return typeof args[index] !== 'undefined' ? args[index] : match;
});
};
/**
* Функция для получения языка пользователя
* @returns Код языка ('ru' или 'en')
*/
export const getUserLanguage = (): 'ru' | 'en' => {
try {
const userInfo = getUserInfo();
// Если язык пользователя русский, возвращаем 'ru', иначе 'en'
return userInfo.language_code === 'ru' ? 'ru' : 'en';
} catch (error) {
console.error('Ошибка при получении языка пользователя:', error);
// По умолчанию возвращаем русский язык
return 'en';
}
};
/**
* Функция для получения перевода по ключу
* @param key Ключ перевода
* @param args Аргументы для форматирования строки (опционально)
* @returns Переведенный текст
*/
export const getTranslation = (key: string, ...args: any[]): string => {
const language = getUserLanguage();
const translation = translations[language][key] || translations['ru'][key] || key;
if (args.length > 0) {
return formatString(translation, ...args);
}
return translation;
};
/**
* Хук для использования переводов в компонентах (для удобства)
* @returns Объект с функцией перевода и текущим языком
*/
export const useTranslation = () => {
const language = getUserLanguage();
return {
t: (key: string, ...args: any[]) => getTranslation(key, ...args),
language
};
};

View File

@ -7,6 +7,7 @@ import { useBalance } from '../contexts/BalanceContext';
import { WorkflowType } from '../constants/workflows'; import { WorkflowType } from '../constants/workflows';
import customAnalyticsService from '../services/customAnalyticsService'; import customAnalyticsService from '../services/customAnalyticsService';
import { getCurrentUserId } from '../constants/user'; import { getCurrentUserId } from '../constants/user';
import { getTranslation } from '../constants/translations';
/** /**
* Хук для управления состоянием генерации * Хук для управления состоянием генерации
@ -194,8 +195,8 @@ export const useGenerationState = (
if (isSame) { if (isSame) {
showNotification( showNotification(
'Внимание', getTranslation('generation_attention'),
'Нельзя отправить одну и ту же комбинацию изображения и образа подряд. Пожалуйста, измените изображение или выберите другой образ.', getTranslation('generation_duplicate_error'),
{ showGalleryButton: false } { showGalleryButton: false }
); );
return; return;
@ -219,8 +220,8 @@ export const useGenerationState = (
// Показываем уведомление о начале генерации // Показываем уведомление о начале генерации
showNotification( showNotification(
'Генерация стикера', getTranslation('generation_title'),
'Отправка запроса...', getTranslation('generation_sending'),
{ isLoading: true, showButtons: false } { isLoading: true, showButtons: false }
); );
@ -244,8 +245,8 @@ export const useGenerationState = (
// Вызываем handleRequestTimeout из useNotifications через showNotification // Вызываем handleRequestTimeout из useNotifications через showNotification
showNotification( showNotification(
'Генерация стикера', getTranslation('generation_title'),
'Возникли проблемы с подключением. Пожалуйста, проверьте интернет-соединение.', getTranslation('generation_connection_error'),
{ isLoading: true, showButtons: false } { isLoading: true, showButtons: false }
); );
}, 6000); }, 6000);
@ -310,8 +311,8 @@ export const useGenerationState = (
setIsGenerating(false); setIsGenerating(false);
showNotification( showNotification(
'Недопустимый промпт', getTranslation('generation_invalid_prompt_title'),
'Промпт содержит недопустимый контент. Пожалуйста, используйте более нейтральные формулировки.', getTranslation('generation_invalid_prompt_message'),
{ isLoading: false, showGalleryButton: false, showButtons: true } { isLoading: false, showGalleryButton: false, showButtons: true }
); );
@ -343,23 +344,23 @@ export const useGenerationState = (
const minutes = Math.floor(estimatedTime / 60); const minutes = Math.floor(estimatedTime / 60);
const seconds = estimatedTime % 60; const seconds = estimatedTime % 60;
const timeString = minutes > 0 const timeString = minutes > 0
? `${minutes} мин ${seconds} сек` ? `${minutes} ${getTranslation('time_minutes_short')} ${seconds} ${getTranslation('time_seconds_short')}`
: `${seconds} сек`; : `${seconds} ${getTranslation('time_seconds_short')}`;
// Форматируем сообщение // Форматируем сообщение
showNotification( showNotification(
'Генерация стикера', getTranslation('generation_title'),
`Создание стикеров началось!\n` + `${getTranslation('generation_started')}\n` +
`Позиция в очереди: ${result.queue_position}\n` + `${getTranslation('generation_queue_position', result.queue_position)}\n` +
`Время ожидания: ${timeString}\n\n` + `${getTranslation('generation_wait_time', timeString)}\n\n` +
`Результат будет доступен в галерее после завершения генерации.`, `${getTranslation('generation_result_available')}`,
{ isLoading: false, showButtons: true } { isLoading: false, showButtons: true }
); );
} else { } else {
showNotification( showNotification(
'Генерация стикера', getTranslation('generation_title'),
`Создание стикеров началось!\n\n` + `${getTranslation('generation_started')}\n\n` +
`Результат будет доступен в галерее после завершения генерации.`, `${getTranslation('generation_result_available')}`,
{ isLoading: false, showButtons: true } { isLoading: false, showButtons: true }
); );
} }
@ -388,8 +389,8 @@ export const useGenerationState = (
setIsGenerating(false); setIsGenerating(false);
showNotification( showNotification(
'Ошибка', getTranslation('generation_error_title'),
'Не удалось начать генерацию', getTranslation('generation_error_message'),
{ isLoading: false, showGalleryButton: false, showButtons: true } { isLoading: false, showGalleryButton: false, showButtons: true }
); );

View File

@ -1,6 +1,7 @@
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { NotificationState } from '../types/generation'; import { NotificationState } from '../types/generation';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { getTranslation } from '../constants/translations';
/** /**
* Хук для управления уведомлениями * Хук для управления уведомлениями
@ -15,7 +16,7 @@ export const useNotifications = () => {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [showGalleryButton, setShowGalleryButton] = useState(true); const [showGalleryButton, setShowGalleryButton] = useState(true);
const [showButtons, setShowButtons] = useState(true); const [showButtons, setShowButtons] = useState(true);
const [continueButtonText, setContinueButtonText] = useState('Продолжить'); const [continueButtonText, setContinueButtonText] = useState(getTranslation('continue'));
// Состояния для модального окна с токенами // Состояния для модального окна с токенами
const [showTokensModal, setShowTokensModal] = useState(false); const [showTokensModal, setShowTokensModal] = useState(false);
@ -26,13 +27,10 @@ export const useNotifications = () => {
* Функция для обработки таймаута запроса * Функция для обработки таймаута запроса
*/ */
const handleRequestTimeout = useCallback(() => { const handleRequestTimeout = useCallback(() => {
setNotificationMessage( setNotificationMessage(getTranslation('connection_error'));
'Возникли проблемы с подключением. Пожалуйста, проверьте интернет-соединение.\n\n' +
'Запрос все еще обрабатывается, но это может занять больше времени, чем обычно.'
);
// Добавляем кнопку отмены, чтобы пользователь мог прервать запрос // Добавляем кнопку отмены, чтобы пользователь мог прервать запрос
setShowButtons(true); setShowButtons(true);
setContinueButtonText('Отменить'); setContinueButtonText(getTranslation('cancel'));
}, []); }, []);
/** /**
@ -44,7 +42,7 @@ export const useNotifications = () => {
setIsLoading(false); setIsLoading(false);
setShowGalleryButton(false); setShowGalleryButton(false);
setShowButtons(true); setShowButtons(true);
setContinueButtonText('Закрыть'); setContinueButtonText(getTranslation('close'));
setIsNotificationVisible(true); setIsNotificationVisible(true);
}, []); }, []);
@ -62,7 +60,7 @@ export const useNotifications = () => {
setIsLoading(options?.isLoading ?? false); setIsLoading(options?.isLoading ?? false);
setShowGalleryButton(options?.showGalleryButton ?? true); setShowGalleryButton(options?.showGalleryButton ?? true);
setShowButtons(options?.showButtons ?? true); setShowButtons(options?.showButtons ?? true);
setContinueButtonText(options?.continueButtonText ?? 'Продолжить'); setContinueButtonText(options?.continueButtonText ?? getTranslation('continue'));
setIsNotificationVisible(true); setIsNotificationVisible(true);
}, []); }, []);
@ -95,13 +93,13 @@ export const useNotifications = () => {
*/ */
const showFeedbackSentNotification = useCallback(() => { const showFeedbackSentNotification = useCallback(() => {
showNotification( showNotification(
'Спасибо за обратную связь', getTranslation('feedback_thanks'),
'Ваше сообщение успешно отправлено', getTranslation('feedback_sent'),
{ {
isLoading: false, isLoading: false,
showGalleryButton: false, showGalleryButton: false,
showButtons: true, showButtons: true,
continueButtonText: 'Закрыть' continueButtonText: getTranslation('close')
} }
); );
}, [showNotification]); }, [showNotification]);
@ -111,13 +109,13 @@ export const useNotifications = () => {
*/ */
const showPaymentSuccessNotification = useCallback((tokens: number, bonusTokens: number) => { const showPaymentSuccessNotification = useCallback((tokens: number, bonusTokens: number) => {
showNotification( showNotification(
'Оплата успешна!', getTranslation('payment_success'),
`Вы успешно приобрели ${tokens + bonusTokens} токенов.`, getTranslation('tokens_purchased', tokens + bonusTokens),
{ {
isLoading: false, isLoading: false,
showGalleryButton: false, showGalleryButton: false,
showButtons: true, showButtons: true,
continueButtonText: 'Закрыть' continueButtonText: getTranslation('close')
} }
); );
}, [showNotification]); }, [showNotification]);

View File

@ -7,6 +7,7 @@ import { GeneratedImage } from '../types/api';
import { getCurrentUserId } from '../constants/user'; import { getCurrentUserId } from '../constants/user';
import EmojiPickerModal from '../components/shared/EmojiPickerModal'; import EmojiPickerModal from '../components/shared/EmojiPickerModal';
import ValidationModal from '../components/shared/ValidationModal'; import ValidationModal from '../components/shared/ValidationModal';
import { getTranslation } from '../constants/translations';
const AddStickerToPackScreen: React.FC = () => { const AddStickerToPackScreen: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -47,7 +48,7 @@ const AddStickerToPackScreen: React.FC = () => {
setError(null); setError(null);
} catch (err) { } catch (err) {
console.error('Ошибка при загрузке данных:', err); console.error('Ошибка при загрузке данных:', err);
setError('Не удалось загрузить данные'); setError(getTranslation('failed_to_load_data'));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -114,15 +115,15 @@ const AddStickerToPackScreen: React.FC = () => {
// Обработчик добавления стикеров // Обработчик добавления стикеров
const handleAddStickers = async () => { const handleAddStickers = async () => {
if (selectedImages.length === 0) { if (selectedImages.length === 0) {
setValidationTitle('Стикеры не выбраны'); setValidationTitle(getTranslation('stickers_not_selected'));
setValidationMessage('Пожалуйста, выберите хотя бы одно изображение для добавления в стикерпак.'); setValidationMessage(getTranslation('select_at_least_one_image'));
setIsValidationModalVisible(true); setIsValidationModalVisible(true);
return; return;
} }
if (!packName) { if (!packName) {
setValidationTitle('Ошибка'); setValidationTitle(getTranslation('error'));
setValidationMessage('Не указано имя стикерпака.'); setValidationMessage(getTranslation('pack_name_not_specified'));
setIsValidationModalVisible(true); setIsValidationModalVisible(true);
return; return;
} }
@ -156,8 +157,8 @@ const AddStickerToPackScreen: React.FC = () => {
navigate('/packs'); navigate('/packs');
} catch (err) { } catch (err) {
console.error('Ошибка при добавлении стикеров:', err); console.error('Ошибка при добавлении стикеров:', err);
setValidationTitle('Ошибка'); setValidationTitle(getTranslation('error'));
setValidationMessage('Не удалось добавить стикеры в стикерпак. Пожалуйста, попробуйте еще раз.'); setValidationMessage(getTranslation('failed_to_add_stickers'));
setIsValidationModalVisible(true); setIsValidationModalVisible(true);
} finally { } finally {
setAdding(false); setAdding(false);
@ -171,17 +172,17 @@ const AddStickerToPackScreen: React.FC = () => {
className={styles.backButton} className={styles.backButton}
onClick={() => navigate('/packs')} onClick={() => navigate('/packs')}
> >
Назад {getTranslation('back_button')}
</button> </button>
<h1 className={styles.title}> <h1 className={styles.title}>
Добавление стикеров в "{packTitle || packName}" {getTranslation('add_stickers_to_pack_title', packTitle || packName)}
</h1> </h1>
</div> </div>
<div className={styles.content}> <div className={styles.content}>
{loading && ( {loading && (
<div className={styles.loading}> <div className={styles.loading}>
<p>Загрузка данных...</p> <p>{getTranslation('loading_data')}</p>
</div> </div>
)} )}
@ -194,12 +195,11 @@ const AddStickerToPackScreen: React.FC = () => {
{!loading && ( {!loading && (
<> <>
<div className={styles.section}> <div className={styles.section}>
<h2 className={styles.sectionTitle}>Выберите изображения для стикеров</h2> <h2 className={styles.sectionTitle}>{getTranslation('select_images_for_stickers')}</h2>
{availableImages.length === 0 ? ( {availableImages.length === 0 ? (
<p className={styles.noImages}> <p className={styles.noImages}>
У вас пока нет сгенерированных изображений. {getTranslation('create_first')}
Сначала создайте изображения в разделе "Создать стикер".
</p> </p>
) : ( ) : (
<div className={styles.imagesGrid}> <div className={styles.imagesGrid}>
@ -215,7 +215,7 @@ const AddStickerToPackScreen: React.FC = () => {
> >
<img <img
src={image.url || ''} src={image.url || ''}
alt={`Изображение ${index + 1}`} alt={getTranslation('image_alt', index + 1)}
className={styles.image} className={styles.image}
/> />
@ -245,7 +245,7 @@ const AddStickerToPackScreen: React.FC = () => {
onClick={handleAddStickers} onClick={handleAddStickers}
disabled={adding} disabled={adding}
> >
{adding ? 'Добавление...' : 'Добавить стикеры'} {adding ? getTranslation('adding') : getTranslation('add_stickers')}
</button> </button>
</div> </div>

View File

@ -7,6 +7,7 @@ import { paymentService } from '../services/paymentService';
import apiService from '../services/api'; import apiService from '../services/api';
import { getCurrentUserId } from '../constants/user'; import { getCurrentUserId } from '../constants/user';
import NotificationModal from '../components/shared/NotificationModal'; import NotificationModal from '../components/shared/NotificationModal';
import { getTranslation } from '../constants/translations';
const TOKENS_PER_GENERATION = 10; const TOKENS_PER_GENERATION = 10;
@ -33,7 +34,7 @@ const CreateSticker: React.FC = () => {
return true; return true;
} catch (error) { } catch (error) {
console.error('Error checking balance:', error); console.error('Error checking balance:', error);
alert('Произошла ошибка при проверке баланса. Попробуйте позже.'); alert(getTranslation('create_sticker_screen_balance_error'));
return false; return false;
} }
}; };
@ -50,10 +51,10 @@ const CreateSticker: React.FC = () => {
<div className={styles.container}> <div className={styles.container}>
<div className={styles.header}> <div className={styles.header}>
<h1 className={styles.title}> <h1 className={styles.title}>
Создание стикера {getTranslation('create_sticker_screen_title')}
</h1> </h1>
<p className={styles.subtitle}> <p className={styles.subtitle}>
Загрузите фотографию для создания стикера {getTranslation('create_sticker_screen_subtitle')}
</p> </p>
</div> </div>
@ -64,13 +65,13 @@ const CreateSticker: React.FC = () => {
<div className={styles.uploadBox}> <div className={styles.uploadBox}>
<span className={styles.uploadIcon}>📷</span> <span className={styles.uploadIcon}>📷</span>
<span className={styles.uploadText}> <span className={styles.uploadText}>
Нажмите чтобы выбрать фото {getTranslation('create_sticker_screen_upload_text')}
</span> </span>
<span className={styles.uploadHint}> <span className={styles.uploadHint}>
или перетащите файл сюда {getTranslation('create_sticker_screen_upload_hint')}
</span> </span>
<span className={styles.tokenCost}> <span className={styles.tokenCost}>
Стоимость: {TOKENS_PER_GENERATION} токенов {getTranslation('create_sticker_screen_cost', TOKENS_PER_GENERATION)}
</span> </span>
</div> </div>
</div> </div>
@ -91,8 +92,8 @@ const CreateSticker: React.FC = () => {
if (userData) { if (userData) {
// Обновляем данные на основе полученной информации // Обновляем данные на основе полученной информации
// Показываем модальное окно с информацией об успешной оплате // Показываем модальное окно с информацией об успешной оплате
setNotificationTitle('Оплата успешна!'); setNotificationTitle(getTranslation('payment_success'));
setNotificationMessage(`Вы успешно приобрели ${pack.tokens + pack.bonusTokens} токенов.`); setNotificationMessage(getTranslation('tokens_purchased', pack.tokens + pack.bonusTokens));
setShowNotificationModal(true); setShowNotificationModal(true);
} else { } else {
// Если данные не получены, делаем запрос на получение данных пользователя // Если данные не получены, делаем запрос на получение данных пользователя
@ -101,8 +102,8 @@ const CreateSticker: React.FC = () => {
const balance = await apiService.getBalance(getCurrentUserId()); const balance = await apiService.getBalance(getCurrentUserId());
// Показываем модальное окно с информацией об успешной оплате // Показываем модальное окно с информацией об успешной оплате
setNotificationTitle('Оплата успешна!'); setNotificationTitle(getTranslation('payment_success'));
setNotificationMessage(`Вы успешно приобрели ${pack.tokens + pack.bonusTokens} токенов. Ваш текущий баланс: ${balance} токенов.`); setNotificationMessage(getTranslation('profile_tokens_purchased', pack.tokens + pack.bonusTokens, balance));
setShowNotificationModal(true); setShowNotificationModal(true);
} catch (error) { } catch (error) {
console.error('Ошибка при обновлении данных пользователя:', error); console.error('Ошибка при обновлении данных пользователя:', error);
@ -120,7 +121,7 @@ const CreateSticker: React.FC = () => {
onGalleryClick={() => setShowNotificationModal(false)} onGalleryClick={() => setShowNotificationModal(false)}
onContinueClick={() => setShowNotificationModal(false)} onContinueClick={() => setShowNotificationModal(false)}
showGalleryButton={false} showGalleryButton={false}
continueButtonText="Закрыть" continueButtonText={getTranslation('close')}
/> />
</div> </div>
); );

View File

@ -8,6 +8,7 @@ import { getCurrentUserId } from '../constants/user';
import EmojiPickerModal from '../components/shared/EmojiPickerModal'; import EmojiPickerModal from '../components/shared/EmojiPickerModal';
import ValidationModal from '../components/shared/ValidationModal'; import ValidationModal from '../components/shared/ValidationModal';
import customAnalyticsService from '../services/customAnalyticsService'; import customAnalyticsService from '../services/customAnalyticsService';
import { getTranslation } from '../constants/translations';
/** /**
* Транслитерирует кириллический текст в латиницу. * Транслитерирует кириллический текст в латиницу.
@ -160,15 +161,15 @@ const CreateStickerPack: React.FC = () => {
// Обработчик создания стикерпака // Обработчик создания стикерпака
const handleCreateStickerPack = async () => { const handleCreateStickerPack = async () => {
if (!title.trim()) { if (!title.trim()) {
setValidationTitle('Название не указано'); setValidationTitle(getTranslation('create_sticker_pack_name_required_title'));
setValidationMessage('Пожалуйста, введите название для вашего стикерпака.'); setValidationMessage(getTranslation('create_sticker_pack_name_required_message'));
setIsValidationModalVisible(true); setIsValidationModalVisible(true);
return; return;
} }
if (selectedImages.length === 0) { if (selectedImages.length === 0) {
setValidationTitle('Стикеры не выбраны'); setValidationTitle(getTranslation('create_sticker_pack_no_stickers_title'));
setValidationMessage('Пожалуйста, выберите хотя бы одно изображение для вашего стикерпака.'); setValidationMessage(getTranslation('create_sticker_pack_no_stickers_message'));
setIsValidationModalVisible(true); setIsValidationModalVisible(true);
return; return;
} }
@ -228,11 +229,11 @@ const CreateStickerPack: React.FC = () => {
// Проверяем, содержит ли сообщение об ошибке информацию о занятом имени // Проверяем, содержит ли сообщение об ошибке информацию о занятом имени
if (errorMessage.includes('sticker set name is already occupied')) { if (errorMessage.includes('sticker set name is already occupied')) {
setValidationTitle('Имя стикерпака уже занято'); setValidationTitle(getTranslation('create_sticker_pack_name_occupied_title'));
setValidationMessage('Стикерпак с таким именем уже существует в Telegram. Пожалуйста, измените название и попробуйте снова.'); setValidationMessage(getTranslation('create_sticker_pack_name_occupied_message'));
} else { } else {
setValidationTitle('Ошибка при создании стикерпака'); setValidationTitle(getTranslation('create_sticker_pack_error_title'));
setValidationMessage('Не удалось создать стикерпак. Пожалуйста, попробуйте еще раз с другим названием или изображениями.'); setValidationMessage(getTranslation('create_sticker_pack_error_message'));
} }
setIsValidationModalVisible(true); setIsValidationModalVisible(true);
@ -248,22 +249,22 @@ const CreateStickerPack: React.FC = () => {
className={styles.backButton} className={styles.backButton}
onClick={() => navigate('/packs')} onClick={() => navigate('/packs')}
> >
Назад {getTranslation('create_sticker_pack_back')}
</button> </button>
<h1 className={styles.title}>Создание стикерпака</h1> <h1 className={styles.title}>{getTranslation('create_sticker_pack_title')}</h1>
</div> </div>
<div className={styles.content}> <div className={styles.content}>
<div className={styles.form}> <div className={styles.form}>
<div className={styles.formGroup}> <div className={styles.formGroup}>
<label htmlFor="title" className={styles.label}>Название стикерпака</label> <label htmlFor="title" className={styles.label}>{getTranslation('sticker_pack_name_label')}</label>
<input <input
id="title" id="title"
type="text" type="text"
className={styles.input} className={styles.input}
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
placeholder="Введите название стикерпака" placeholder={getTranslation('sticker_pack_name_label')}
ref={titleInputRef} ref={titleInputRef}
onKeyDown={handleTitleKeyDown} onKeyDown={handleTitleKeyDown}
enterKeyHint="done" enterKeyHint="done"
@ -271,14 +272,13 @@ const CreateStickerPack: React.FC = () => {
</div> </div>
<div className={styles.formGroup}> <div className={styles.formGroup}>
<label className={styles.label}>Выберите изображения для стикеров</label> <label className={styles.label}>{getTranslation('create_sticker_pack_select_images')}</label>
{loading && <p className={styles.loading}>Загрузка изображений...</p>} {loading && <p className={styles.loading}>{getTranslation('create_sticker_pack_loading')}</p>}
{!loading && availableImages.length === 0 && ( {!loading && availableImages.length === 0 && (
<p className={styles.noImages}> <p className={styles.noImages}>
У вас пока нет сгенерированных изображений. {getTranslation('create_first')}
Сначала создайте изображения в разделе "Создать стикер".
</p> </p>
)} )}
@ -296,7 +296,7 @@ const CreateStickerPack: React.FC = () => {
> >
<img <img
src={image.url || ''} src={image.url || ''}
alt={`Изображение ${index + 1}`} alt={getTranslation('create_sticker_pack_image_alt', index + 1)}
className={styles.image} className={styles.image}
/> />
@ -327,7 +327,7 @@ const CreateStickerPack: React.FC = () => {
onClick={handleCreateStickerPack} onClick={handleCreateStickerPack}
disabled={creating} disabled={creating}
> >
{creating ? 'Создание...' : 'Создать стикерпак'} {creating ? getTranslation('create_sticker_pack_creating') : getTranslation('create_sticker_pack_create')}
</button> </button>
</div> </div>

View File

@ -1,6 +1,7 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import styles from './CropPhoto.module.css'; import styles from './CropPhoto.module.css';
import { getTranslation } from '../constants/translations';
const CropPhoto: React.FC = () => { const CropPhoto: React.FC = () => {
const location = useLocation(); const location = useLocation();
@ -283,9 +284,9 @@ const CropPhoto: React.FC = () => {
<button className={styles.backButton} onClick={() => navigate('/')}> <button className={styles.backButton} onClick={() => navigate('/')}>
</button> </button>
<h1 className={styles.title}>Обрезка фото</h1> <h1 className={styles.title}>{getTranslation('crop_photo_title')}</h1>
<button className={styles.confirmButton} onClick={handleConfirm}> <button className={styles.confirmButton} onClick={handleConfirm}>
Готово {getTranslation('crop_photo_done')}
</button> </button>
</div> </div>
@ -337,7 +338,7 @@ const CropPhoto: React.FC = () => {
className={styles.zoomSlider} className={styles.zoomSlider}
/> />
<div className={styles.hint}> <div className={styles.hint}>
Отрегулируйте масштаб и положение фото {getTranslation('crop_photo_hint')}
</div> </div>
</div> </div>
</div> </div>

View File

@ -8,6 +8,7 @@ import ImageWithFallback from '../components/shared/ImageWithFallback';
import NotificationModal from '../components/shared/NotificationModal'; import NotificationModal from '../components/shared/NotificationModal';
import customAnalyticsService from '../services/customAnalyticsService'; import customAnalyticsService from '../services/customAnalyticsService';
import { getCurrentUserId } from '../constants/user'; import { getCurrentUserId } from '../constants/user';
import { getTranslation } from '../constants/translations';
const GalleryScreen: React.FC = () => { const GalleryScreen: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -31,11 +32,11 @@ const GalleryScreen: React.FC = () => {
// Функция для расчета времени ожидания // Функция для расчета времени ожидания
const getEstimatedWaitTime = (queuePosition: number | null): string => { const getEstimatedWaitTime = (queuePosition: number | null): string => {
if (queuePosition === null) return 'Генерация началась'; if (queuePosition === null) return getTranslation('gallery_generation_started');
const seconds = apiService.calculateEstimatedWaitTime(queuePosition); const seconds = apiService.calculateEstimatedWaitTime(queuePosition);
if (seconds < 60) return `${seconds} сек`; if (seconds < 60) return getTranslation('gallery_seconds', seconds);
return `${Math.floor(seconds / 60)} мин ${seconds % 60} сек`; return getTranslation('gallery_minutes_seconds', Math.floor(seconds / 60), seconds % 60);
}; };
// Обработчики для режима удаления // Обработчики для режима удаления
@ -268,7 +269,7 @@ const GalleryScreen: React.FC = () => {
style={{ '--pull-distance': `${pullDistance}px` } as React.CSSProperties} style={{ '--pull-distance': `${pullDistance}px` } as React.CSSProperties}
> >
<div className={styles.refreshSpinner}></div> <div className={styles.refreshSpinner}></div>
{refreshing && <span>Обновление...</span>} {refreshing && <span>{getTranslation('gallery_refreshing')}</span>}
</div> </div>
<div <div
@ -281,13 +282,13 @@ const GalleryScreen: React.FC = () => {
> >
<div className={styles.header}> <div className={styles.header}>
<h1 className={styles.title}> <h1 className={styles.title}>
Галерея стикеров {getTranslation('gallery_title')}
</h1> </h1>
</div> </div>
{loading && ( {loading && (
<div className={styles.placeholder}> <div className={styles.placeholder}>
Загрузка изображений... {getTranslation('gallery_loading')}
</div> </div>
)} )}
@ -299,13 +300,13 @@ const GalleryScreen: React.FC = () => {
{!loading && !error && images.length === 0 && pendingTasks.length === 0 && ( {!loading && !error && images.length === 0 && pendingTasks.length === 0 && (
<div className={styles.placeholder}> <div className={styles.placeholder}>
У вас пока нет сгенерированных стикеров {getTranslation('gallery_empty')}
</div> </div>
)} )}
{!loading && !loadingPendingTasks && pendingTasks.length > 0 && ( {!loading && !loadingPendingTasks && pendingTasks.length > 0 && (
<div className={styles.pendingTasksSection}> <div className={styles.pendingTasksSection}>
<h2 className={styles.sectionTitle}>В процессе генерации</h2> <h2 className={styles.sectionTitle}>{getTranslation('gallery_generating_section')}</h2>
<div className={styles.pendingTasksGrid}> <div className={styles.pendingTasksGrid}>
{pendingTasks.map((task) => ( {pendingTasks.map((task) => (
<div key={task.task_id} className={styles.pendingTaskItem}> <div key={task.task_id} className={styles.pendingTaskItem}>
@ -315,11 +316,11 @@ const GalleryScreen: React.FC = () => {
<div className={styles.pendingTaskInfo}> <div className={styles.pendingTaskInfo}>
<p className={styles.pendingTaskStatus}> <p className={styles.pendingTaskStatus}>
{task.status === 'PENDING' {task.status === 'PENDING'
? `В очереди: ${task.queue_position}` ? getTranslation('gallery_queue_position', task.queue_position)
: 'Генерация...'} : getTranslation('gallery_generating')}
</p> </p>
<p className={styles.pendingTaskTime}> <p className={styles.pendingTaskTime}>
Осталось: {getEstimatedWaitTime(task.queue_position)} {getTranslation('gallery_time_left', getEstimatedWaitTime(task.queue_position))}
</p> </p>
</div> </div>
</div> </div>
@ -343,7 +344,7 @@ const GalleryScreen: React.FC = () => {
> >
<ImageWithFallback <ImageWithFallback
src={image.url || ''} src={image.url || ''}
alt={`Стикер ${index + 1}`} alt={getTranslation('gallery_sticker_alt', index + 1)}
className={styles.image} className={styles.image}
onClick={() => !isDeleteMode && image.url && setSelectedImage(image.url)} onClick={() => !isDeleteMode && image.url && setSelectedImage(image.url)}
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) => e.preventDefault()} onContextMenu={(e: React.MouseEvent<HTMLDivElement>) => e.preventDefault()}
@ -376,12 +377,12 @@ const GalleryScreen: React.FC = () => {
{/* Модальное окно подтверждения удаления */} {/* Модальное окно подтверждения удаления */}
<NotificationModal <NotificationModal
isVisible={!!selectedForDelete} isVisible={!!selectedForDelete}
title="Удаление стикера" title={getTranslation('gallery_delete_title')}
message="Вы уверены, что хотите удалить этот стикер?" message={getTranslation('gallery_delete_message')}
isLoading={isDeleting} isLoading={isDeleting}
showGalleryButton={true} showGalleryButton={true}
galleryButtonText="Отмена" galleryButtonText={getTranslation('gallery_delete_cancel')}
continueButtonText="Удалить" continueButtonText={getTranslation('gallery_delete_confirm')}
onContinueClick={handleConfirmDelete} onContinueClick={handleConfirmDelete}
onGalleryClick={() => setSelectedForDelete(null)} onGalleryClick={() => setSelectedForDelete(null)}
isPrimaryGalleryButton={false} isPrimaryGalleryButton={false}
@ -393,7 +394,7 @@ const GalleryScreen: React.FC = () => {
className={styles.createButtonFixed} className={styles.createButtonFixed}
onClick={handleCreateStickerPack} onClick={handleCreateStickerPack}
> >
Создать стикерпак {getTranslation('gallery_create_pack')}
</button> </button>
)} )}
</div> </div>

View File

@ -7,6 +7,7 @@ import TokenPacksList from '../components/tokens/TokenPacksList';
import { tokenPacks, TokenPack } from '../constants/tokenPacks'; import { tokenPacks, TokenPack } from '../constants/tokenPacks';
import { paymentService } from '../services/paymentService'; import { paymentService } from '../services/paymentService';
import NotificationModal from '../components/shared/NotificationModal'; import NotificationModal from '../components/shared/NotificationModal';
import { getTranslation } from '../constants/translations';
const Profile: React.FC = () => { const Profile: React.FC = () => {
const [stickersCount, setStickersCount] = useState<number>(0); const [stickersCount, setStickersCount] = useState<number>(0);
@ -83,22 +84,22 @@ const Profile: React.FC = () => {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.header}> <div className={styles.header}>
<h1 className={styles.title}>Профиль</h1> <h1 className={styles.title}>{getTranslation('profile_title')}</h1>
<p className={styles.subtitle}>Ваша статистика и настройки</p> <p className={styles.subtitle}>{getTranslation('profile_subtitle')}</p>
</div> </div>
<div className={styles.compactStats}> <div className={styles.compactStats}>
<div className={styles.statItem}> <div className={styles.statItem}>
<span className={styles.statValue}>{loading ? '...' : stickersCount}</span> <span className={styles.statValue}>{loading ? '...' : stickersCount}</span>
<span className={styles.statLabel}>Стикеров создано</span> <span className={styles.statLabel}>{getTranslation('profile_stickers_created')}</span>
</div> </div>
<div className={styles.statItem}> <div className={styles.statItem}>
<span className={styles.statValue}>{loading ? '...' : packsCount}</span> <span className={styles.statValue}>{loading ? '...' : packsCount}</span>
<span className={styles.statLabel}>Стикерпаков</span> <span className={styles.statLabel}>{getTranslation('profile_sticker_packs')}</span>
</div> </div>
<div className={styles.statItem}> <div className={styles.statItem}>
<span className={styles.statValue}>{loading ? '...' : userBalance}</span> <span className={styles.statValue}>{loading ? '...' : userBalance}</span>
<span className={styles.statLabel}>Токенов</span> <span className={styles.statLabel}>{getTranslation('profile_tokens')}</span>
</div> </div>
</div> </div>
@ -111,12 +112,14 @@ const Profile: React.FC = () => {
{/* Модальное окно успешной оплаты */} {/* Модальное окно успешной оплаты */}
<NotificationModal <NotificationModal
isVisible={showPaymentSuccessModal} isVisible={showPaymentSuccessModal}
title="Оплата успешна!" title={getTranslation('profile_payment_success')}
message={lastPurchasedPack ? `Вы успешно приобрели ${lastPurchasedPack.tokens + lastPurchasedPack.bonusTokens} токенов. Ваш текущий баланс: ${userBalance} токенов.` : "Оплата прошла успешно!"} message={lastPurchasedPack
? getTranslation('profile_tokens_purchased', lastPurchasedPack.tokens + lastPurchasedPack.bonusTokens, userBalance)
: getTranslation('profile_payment_success')}
onGalleryClick={handleClosePaymentSuccessModal} onGalleryClick={handleClosePaymentSuccessModal}
onContinueClick={handleClosePaymentSuccessModal} onContinueClick={handleClosePaymentSuccessModal}
showGalleryButton={false} showGalleryButton={false}
continueButtonText="Закрыть" continueButtonText={getTranslation('profile_close')}
/> />
</div> </div>
); );

View File

@ -5,6 +5,7 @@ import { stickerService } from '../services/stickerService';
import { getCurrentUserId } from '../constants/user'; import { getCurrentUserId } from '../constants/user';
import NotificationModal from '../components/shared/NotificationModal'; import NotificationModal from '../components/shared/NotificationModal';
import customAnalyticsService from '../services/customAnalyticsService'; import customAnalyticsService from '../services/customAnalyticsService';
import { getTranslation } from '../constants/translations';
// Функция для удаления дописанной части из названия стикерпака // Функция для удаления дописанной части из названия стикерпака
const cleanPackTitle = (title: string): string => { const cleanPackTitle = (title: string): string => {
@ -61,7 +62,7 @@ const StickerPacks: React.FC = () => {
setError(null); setError(null);
} catch (err) { } catch (err) {
console.error('Ошибка при загрузке стикерпаков:', err); console.error('Ошибка при загрузке стикерпаков:', err);
setError('Не удалось загрузить список стикерпаков'); setError(getTranslation('stickerpacks_error'));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -114,7 +115,7 @@ const StickerPacks: React.FC = () => {
setPackToDelete(null); setPackToDelete(null);
} catch (err) { } catch (err) {
console.error('Ошибка при удалении стикерпака:', err); console.error('Ошибка при удалении стикерпака:', err);
alert('Не удалось удалить стикерпак'); alert(getTranslation('stickerpacks_delete_pack_error'));
setIsDeleting(false); setIsDeleting(false);
} }
} }
@ -128,7 +129,7 @@ const StickerPacks: React.FC = () => {
const handleDeleteSticker = (fileId: string) => { const handleDeleteSticker = (fileId: string) => {
if (!fileId) { if (!fileId) {
alert('Не удалось определить ID стикера'); alert(getTranslation('stickerpacks_sticker_id_error'));
return; return;
} }
@ -158,7 +159,7 @@ const StickerPacks: React.FC = () => {
setStickerToDelete(null); setStickerToDelete(null);
} catch (err) { } catch (err) {
console.error('Ошибка при удалении стикера:', err); console.error('Ошибка при удалении стикера:', err);
alert('Не удалось удалить стикер'); alert(getTranslation('stickerpacks_delete_sticker_error'));
setIsDeletingSticker(false); setIsDeletingSticker(false);
} }
} }
@ -168,27 +169,27 @@ const StickerPacks: React.FC = () => {
<div className={styles.container}> <div className={styles.container}>
<div className={styles.header}> <div className={styles.header}>
<h1 className={styles.title}> <h1 className={styles.title}>
Мои стикерпаки {getTranslation('stickerpacks_title')}
</h1> </h1>
<p className={styles.subtitle}> <p className={styles.subtitle}>
Создавайте и публикуйте наборы стикеров в Telegram {getTranslation('stickerpacks_subtitle')}
</p> </p>
</div> </div>
{loading && ( {loading && (
<div className={styles.placeholder}> <div className={styles.placeholder}>
<p>Загрузка стикерпаков...</p> <p>{getTranslation('stickerpacks_loading')}</p>
</div> </div>
)} )}
{error && ( {error && (
<div className={styles.error}> <div className={styles.error}>
<p>{error}</p> <p>{getTranslation('stickerpacks_error')}</p>
<button <button
className={styles.retryButton} className={styles.retryButton}
onClick={() => window.location.reload()} onClick={() => window.location.reload()}
> >
Повторить {getTranslation('stickerpacks_retry_button')}
</button> </button>
</div> </div>
)} )}
@ -197,13 +198,13 @@ const StickerPacks: React.FC = () => {
<div className={styles.placeholder}> <div className={styles.placeholder}>
<span className={styles.placeholderIcon}>📦</span> <span className={styles.placeholderIcon}>📦</span>
<p className={styles.placeholderText}> <p className={styles.placeholderText}>
У вас пока нет стикерпаков {getTranslation('stickerpacks_empty')}
</p> </p>
<button <button
className={styles.createButton} className={styles.createButton}
onClick={handleCreateStickerPack} onClick={handleCreateStickerPack}
> >
Создать стикерпак {getTranslation('stickerpacks_create_button')}
</button> </button>
</div> </div>
)} )}
@ -219,10 +220,10 @@ const StickerPacks: React.FC = () => {
> >
<div className={styles.packHeader}> <div className={styles.packHeader}>
<h3 className={styles.packTitle}> <h3 className={styles.packTitle}>
Стикерпак: <span className={styles.packTitleName}>{cleanPackTitle(pack.title)}</span> {getTranslation('stickerpacks_pack_title_prefix')}: <span className={styles.packTitleName}>{cleanPackTitle(pack.title)}</span>
</h3> </h3>
<p className={styles.packStats}> <p className={styles.packStats}>
{pack.stickers ? pack.stickers.length : 0} / 49 стикеров {getTranslation('stickerpacks_pack_stats', pack.stickers ? pack.stickers.length : 0)}
</p> </p>
</div> </div>
<div className={styles.packStickers}> <div className={styles.packStickers}>
@ -231,7 +232,7 @@ const StickerPacks: React.FC = () => {
{sticker.file_url ? ( {sticker.file_url ? (
<img <img
src={sticker.file_url} src={sticker.file_url}
alt={`Стикер ${idx + 1}`} alt={getTranslation('stickerpacks_sticker_alt', idx + 1)}
className={styles.stickerImage} className={styles.stickerImage}
/> />
) : ( ) : (
@ -249,7 +250,7 @@ const StickerPacks: React.FC = () => {
handleDeletePack(pack.name); handleDeletePack(pack.name);
}} }}
> >
Удалить {getTranslation('stickerpacks_delete_button')}
</button> </button>
</div> </div>
))} ))}
@ -259,7 +260,7 @@ const StickerPacks: React.FC = () => {
className={styles.createButtonFixed} className={styles.createButtonFixed}
onClick={handleCreateStickerPack} onClick={handleCreateStickerPack}
> >
Создать стикерпак {getTranslation('stickerpacks_create_button')}
</button> </button>
</> </>
)} )}
@ -271,7 +272,7 @@ const StickerPacks: React.FC = () => {
className={styles.backButton} className={styles.backButton}
onClick={handleCloseDetails} onClick={handleCloseDetails}
> >
Назад {getTranslation('stickerpacks_back_button')}
</button> </button>
<h2 className={styles.detailsTitle}>{cleanPackTitle(selectedPack.title)}</h2> <h2 className={styles.detailsTitle}>{cleanPackTitle(selectedPack.title)}</h2>
</div> </div>
@ -282,14 +283,14 @@ const StickerPacks: React.FC = () => {
className={styles.addStickerButton} className={styles.addStickerButton}
onClick={() => navigate(`/add-sticker/${selectedPack.name}`)} onClick={() => navigate(`/add-sticker/${selectedPack.name}`)}
> >
Добавить стикер {getTranslation('stickerpacks_add_sticker_button')}
</button> </button>
<button <button
className={styles.openInTelegramButton} className={styles.openInTelegramButton}
onClick={handleOpenInTelegram} onClick={handleOpenInTelegram}
> >
Открыть в Telegram {getTranslation('stickerpacks_open_telegram_button')}
</button> </button>
</div> </div>
@ -299,7 +300,7 @@ const StickerPacks: React.FC = () => {
{sticker.file_url ? ( {sticker.file_url ? (
<img <img
src={sticker.file_url} src={sticker.file_url}
alt={`Стикер ${index + 1}`} alt={getTranslation('stickerpacks_sticker_alt', index + 1)}
className={styles.stickerImage} className={styles.stickerImage}
/> />
) : ( ) : (
@ -326,12 +327,12 @@ const StickerPacks: React.FC = () => {
{/* Модальное окно подтверждения удаления стикерпака */} {/* Модальное окно подтверждения удаления стикерпака */}
<NotificationModal <NotificationModal
isVisible={!!packToDelete} isVisible={!!packToDelete}
title="Удаление стикерпака" title={getTranslation('stickerpacks_delete_pack_title')}
message="Вы уверены, что хотите удалить этот стикерпак?" message={getTranslation('stickerpacks_delete_pack_message')}
isLoading={isDeleting} isLoading={isDeleting}
showGalleryButton={true} showGalleryButton={true}
galleryButtonText="Отмена" galleryButtonText={getTranslation('stickerpacks_cancel_button')}
continueButtonText="Удалить" continueButtonText={getTranslation('stickerpacks_confirm_delete_button')}
onContinueClick={handleConfirmDelete} onContinueClick={handleConfirmDelete}
onGalleryClick={() => setPackToDelete(null)} onGalleryClick={() => setPackToDelete(null)}
isPrimaryGalleryButton={false} isPrimaryGalleryButton={false}
@ -340,12 +341,12 @@ const StickerPacks: React.FC = () => {
{/* Модальное окно подтверждения удаления стикера */} {/* Модальное окно подтверждения удаления стикера */}
<NotificationModal <NotificationModal
isVisible={!!stickerToDelete} isVisible={!!stickerToDelete}
title="Удаление стикера" title={getTranslation('stickerpacks_delete_sticker_title')}
message="Вы уверены, что хотите удалить этот стикер?" message={getTranslation('stickerpacks_delete_sticker_message')}
isLoading={isDeletingSticker} isLoading={isDeletingSticker}
showGalleryButton={true} showGalleryButton={true}
galleryButtonText="Отмена" galleryButtonText={getTranslation('stickerpacks_cancel_button')}
continueButtonText="Удалить" continueButtonText={getTranslation('stickerpacks_confirm_delete_button')}
onContinueClick={handleConfirmDeleteSticker} onContinueClick={handleConfirmDeleteSticker}
onGalleryClick={() => setStickerToDelete(null)} onGalleryClick={() => setStickerToDelete(null)}
isPrimaryGalleryButton={false} isPrimaryGalleryButton={false}

View File

@ -4,6 +4,7 @@ import OnboardingLayout from '../../components/shared/OnboardingLayout';
import styles from './OnboardingHowTo.module.css'; import styles from './OnboardingHowTo.module.css';
import customAnalyticsService from '../../services/customAnalyticsService'; import customAnalyticsService from '../../services/customAnalyticsService';
import { getCurrentUserId } from '../../constants/user'; import { getCurrentUserId } from '../../constants/user';
import { getTranslation } from '../../constants/translations';
const OnboardingHowTo: React.FC = () => { const OnboardingHowTo: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -42,11 +43,11 @@ const OnboardingHowTo: React.FC = () => {
return ( return (
<OnboardingLayout <OnboardingLayout
title="Как создать стикер" title={getTranslation('how_to_title')}
currentStep={2} currentStep={2}
totalSteps={3} totalSteps={3}
primaryButtonText="Далее" primaryButtonText={getTranslation('next')}
secondaryButtonText="Пропустить" secondaryButtonText={getTranslation('skip')}
onPrimaryClick={handleNext} onPrimaryClick={handleNext}
onSecondaryClick={handleSkip} onSecondaryClick={handleSkip}
> >
@ -54,24 +55,24 @@ const OnboardingHowTo: React.FC = () => {
<div className={styles.step}> <div className={styles.step}>
<div className={styles.stepNumber}>1</div> <div className={styles.stepNumber}>1</div>
<div className={styles.stepContent}> <div className={styles.stepContent}>
<h3 className={styles.stepTitle}>Загрузите фото</h3> <h3 className={styles.stepTitle}>{getTranslation('upload_photo_title')}</h3>
<p className={styles.stepDescription}>Выберите фотографию и обрежьте её в квадрат</p> <p className={styles.stepDescription}>{getTranslation('upload_photo_description')}</p>
</div> </div>
</div> </div>
<div className={styles.step}> <div className={styles.step}>
<div className={styles.stepNumber}>2</div> <div className={styles.stepNumber}>2</div>
<div className={styles.stepContent}> <div className={styles.stepContent}>
<h3 className={styles.stepTitle}>Выберите стиль</h3> <h3 className={styles.stepTitle}>{getTranslation('choose_style_title')}</h3>
<p className={styles.stepDescription}>Подберите подходящий стиль для вашего стикера</p> <p className={styles.stepDescription}>{getTranslation('choose_style_description')}</p>
</div> </div>
</div> </div>
<div className={styles.step}> <div className={styles.step}>
<div className={styles.stepNumber}>3</div> <div className={styles.stepNumber}>3</div>
<div className={styles.stepContent}> <div className={styles.stepContent}>
<h3 className={styles.stepTitle}>Создайте стикер</h3> <h3 className={styles.stepTitle}>{getTranslation('create_sticker_title')}</h3>
<p className={styles.stepDescription}>Дождитесь генерации и сохраните результат</p> <p className={styles.stepDescription}>{getTranslation('create_sticker_description')}</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -4,6 +4,7 @@ import OnboardingLayout from '../../components/shared/OnboardingLayout';
import styles from './OnboardingStickerPacks.module.css'; import styles from './OnboardingStickerPacks.module.css';
import customAnalyticsService from '../../services/customAnalyticsService'; import customAnalyticsService from '../../services/customAnalyticsService';
import { getCurrentUserId } from '../../constants/user'; import { getCurrentUserId } from '../../constants/user';
import { getTranslation } from '../../constants/translations';
const OnboardingStickerPacks: React.FC = () => { const OnboardingStickerPacks: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -42,11 +43,11 @@ const OnboardingStickerPacks: React.FC = () => {
return ( return (
<OnboardingLayout <OnboardingLayout
title="Создавайте стикерпаки" title={getTranslation('sticker_packs_title')}
currentStep={3} currentStep={3}
totalSteps={3} totalSteps={3}
primaryButtonText="Начать" primaryButtonText={getTranslation('start')}
secondaryButtonText="Пропустить" secondaryButtonText={getTranslation('skip')}
onPrimaryClick={handleStart} onPrimaryClick={handleStart}
onSecondaryClick={handleStart} onSecondaryClick={handleStart}
> >
@ -54,24 +55,24 @@ const OnboardingStickerPacks: React.FC = () => {
<div className={styles.step}> <div className={styles.step}>
<div className={styles.stepNumber}>1</div> <div className={styles.stepNumber}>1</div>
<div className={styles.stepContent}> <div className={styles.stepContent}>
<h3 className={styles.stepTitle}>Выберите стикеры</h3> <h3 className={styles.stepTitle}>{getTranslation('select_stickers_title')}</h3>
<p className={styles.stepDescription}>Отберите лучшие стикеры из вашей галереи</p> <p className={styles.stepDescription}>{getTranslation('select_stickers_description')}</p>
</div> </div>
</div> </div>
<div className={styles.step}> <div className={styles.step}>
<div className={styles.stepNumber}>2</div> <div className={styles.stepNumber}>2</div>
<div className={styles.stepContent}> <div className={styles.stepContent}>
<h3 className={styles.stepTitle}>Организуйте набор</h3> <h3 className={styles.stepTitle}>{getTranslation('organize_pack_title')}</h3>
<p className={styles.stepDescription}>Расположите стикеры в нужном порядке</p> <p className={styles.stepDescription}>{getTranslation('organize_pack_description')}</p>
</div> </div>
</div> </div>
<div className={styles.step}> <div className={styles.step}>
<div className={styles.stepNumber}>3</div> <div className={styles.stepNumber}>3</div>
<div className={styles.stepContent}> <div className={styles.stepContent}>
<h3 className={styles.stepTitle}>Опубликуйте</h3> <h3 className={styles.stepTitle}>{getTranslation('publish_title')}</h3>
<p className={styles.stepDescription}>Создайте стикерпак в Telegram одним нажатием</p> <p className={styles.stepDescription}>{getTranslation('publish_description')}</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -4,6 +4,7 @@ import OnboardingLayout from '../../components/shared/OnboardingLayout';
import { images } from '../../assets'; import { images } from '../../assets';
import customAnalyticsService from '../../services/customAnalyticsService'; import customAnalyticsService from '../../services/customAnalyticsService';
import { getCurrentUserId } from '../../constants/user'; import { getCurrentUserId } from '../../constants/user';
import { getTranslation } from '../../constants/translations';
const OnboardingWelcome: React.FC = () => { const OnboardingWelcome: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -42,13 +43,13 @@ const OnboardingWelcome: React.FC = () => {
return ( return (
<OnboardingLayout <OnboardingLayout
title="Добро пожаловать в Sticker Generator" title={getTranslation('welcome_title')}
image={images.onboard1} image={images.onboard1}
description="Создавайте уникальные стикеры из ваших фотографий с помощью искусственного интеллекта" description={getTranslation('welcome_description')}
currentStep={1} currentStep={1}
totalSteps={3} totalSteps={3}
primaryButtonText="Далее" primaryButtonText={getTranslation('next')}
secondaryButtonText="Пропустить" secondaryButtonText={getTranslation('skip')}
onPrimaryClick={handleNext} onPrimaryClick={handleNext}
onSecondaryClick={handleSkip} onSecondaryClick={handleSkip}
/> />

View File

@ -4,6 +4,7 @@ import styles from './TermsAndConditions.module.css';
import { images } from '../../assets'; import { images } from '../../assets';
import customAnalyticsService from '../../services/customAnalyticsService'; import customAnalyticsService from '../../services/customAnalyticsService';
import { getCurrentUserId } from '../../constants/user'; import { getCurrentUserId } from '../../constants/user';
import { getTranslation } from '../../constants/translations';
const TermsAndConditions: React.FC = () => { const TermsAndConditions: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -63,10 +64,10 @@ const TermsAndConditions: React.FC = () => {
<img src={images.shieldIcon} alt="" className={styles.icon} /> <img src={images.shieldIcon} alt="" className={styles.icon} />
</div> </div>
<h1 className={styles.title}>Условия и Конфиденциальность</h1> <h1 className={styles.title}>{getTranslation('terms_title')}</h1>
<p className={styles.description}> <p className={styles.description}>
Продолжая использовать это приложение, вы соглашаетесь с нашими Условиями использования и Политикой конфиденциальности. {getTranslation('terms_description')}
</p> </p>
<div className={styles.links}> <div className={styles.links}>
@ -77,7 +78,7 @@ const TermsAndConditions: React.FC = () => {
rel="noopener noreferrer" rel="noopener noreferrer"
onClick={() => handlePolicyLinkClick("https://telegra.ph/Polzovatelskoe-soglashenie-03-19-13")} onClick={() => handlePolicyLinkClick("https://telegra.ph/Polzovatelskoe-soglashenie-03-19-13")}
> >
Условия использования {getTranslation('terms_of_use')}
</a> </a>
<a <a
href="https://telegra.ph/Politika-konfidencialnosti-03-19-10" href="https://telegra.ph/Politika-konfidencialnosti-03-19-10"
@ -86,7 +87,7 @@ const TermsAndConditions: React.FC = () => {
rel="noopener noreferrer" rel="noopener noreferrer"
onClick={() => handlePolicyLinkClick("https://telegra.ph/Politika-konfidencialnosti-03-19-10")} onClick={() => handlePolicyLinkClick("https://telegra.ph/Politika-konfidencialnosti-03-19-10")}
> >
Политика конфиденциальности {getTranslation('privacy_policy')}
</a> </a>
</div> </div>
@ -95,13 +96,13 @@ const TermsAndConditions: React.FC = () => {
className={styles.primaryButton} className={styles.primaryButton}
onClick={handleAccept} onClick={handleAccept}
> >
Принять {getTranslation('accept')}
</button> </button>
<button <button
className={styles.secondaryButton} className={styles.secondaryButton}
onClick={handleDecline} onClick={handleDecline}
> >
Отклонить {getTranslation('decline')}
</button> </button>
</div> </div>
</div> </div>

View File

@ -6,6 +6,7 @@ import translateService from './translateService';
import { getCurrentUserId, isTelegramWebAppAvailable } from '../constants/user'; import { getCurrentUserId, isTelegramWebAppAvailable } from '../constants/user';
import { trackStickerGeneration, trackRejectedPrompt } from './analyticsService'; import { trackStickerGeneration, trackRejectedPrompt } from './analyticsService';
import memeService from './memeService'; import memeService from './memeService';
import { getTranslation } from '../constants/translations';
const API_BASE_URL = 'https://stickerserver.gymnasticstuff.uk'; const API_BASE_URL = 'https://stickerserver.gymnasticstuff.uk';
@ -290,7 +291,7 @@ const apiService = {
// Не продолжаем генерацию, возвращаем ошибку с сообщением // Не продолжаем генерацию, возвращаем ошибку с сообщением
return { return {
translationFailed: true, translationFailed: true,
usedPrompt: 'Недопустимый промпт', // Сообщение для пользователя usedPrompt: getTranslation('invalid_prompt'), // Сообщение для пользователя
errorDetails: translationResult.text // Детали ошибки для отладки errorDetails: translationResult.text // Детали ошибки для отладки
}; };
} }
@ -519,7 +520,7 @@ const apiService = {
// Возвращаем ошибку // Возвращаем ошибку
return { return {
translationFailed: true, translationFailed: true,
usedPrompt: 'Недопустимый промпт', usedPrompt: getTranslation('invalid_prompt'),
errorDetails: translationResult.text errorDetails: translationResult.text
}; };
} }

View File

@ -1,6 +1,8 @@
// URL для LibreTranslate API (сохраняем, но не используем) // URL для LibreTranslate API (сохраняем, но не используем)
const TRANSLATE_API_URL = 'https://translate.maxdev.keenetic.pro/translate'; const TRANSLATE_API_URL = 'https://translate.maxdev.keenetic.pro/translate';
import { getTranslation } from '../constants/translations';
// URL и ключ для OpenRouter API // URL и ключ для OpenRouter API
const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions'; const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions';
const OPENROUTER_API_KEY = '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'; const OPENROUTER_API_KEY = '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';
@ -241,7 +243,7 @@ const translateService = {
// Если это основная модель, сразу возвращаем ошибку // Если это основная модель, сразу возвращаем ошибку
// и не пытаемся использовать резервную модель // и не пытаемся использовать резервную модель
if (model === TRANSLATION_MODELS.PRIMARY) { if (model === TRANSLATION_MODELS.PRIMARY) {
return { success: false, text: 'Недопустимый промпт' }; return { success: false, text: getTranslation('invalid_prompt') };
} else { } else {
throw new Error('Model refused to translate'); throw new Error('Model refused to translate');
} }
@ -268,7 +270,7 @@ const translateService = {
// Если это была последняя модель, прекращаем попытки // Если это была последняя модель, прекращаем попытки
if (i === models.length - 1) { if (i === models.length - 1) {
console.error('Все попытки перевода через LLM не удались'); console.error('Все попытки перевода через LLM не удались');
return { success: false, text: 'Недопустимый промпт' }; return { success: false, text: getTranslation('invalid_prompt') };
} }
// Иначе продолжаем со следующей моделью (только при технических ошибках) // Иначе продолжаем со следующей моделью (только при технических ошибках)
} }
@ -276,7 +278,7 @@ const translateService = {
// Этот код не должен выполниться, но оставляем для полноты // Этот код не должен выполниться, но оставляем для полноты
console.error('Все попытки перевода через LLM не удались'); console.error('Все попытки перевода через LLM не удались');
return { success: false, text: 'Недопустимый промпт' }; return { success: false, text: getTranslation('invalid_prompt') };
} }
}; };

View File

@ -1,5 +1,6 @@
import { WorkflowType } from '../constants/workflows'; import { WorkflowType } from '../constants/workflows';
import { GenerationOptions } from '../types/generation'; import { GenerationOptions } from '../types/generation';
import { getTranslation } from '../constants/translations';
/** /**
* Определяет тип воркфлоу в зависимости от выбранного стиля и типа эмоций * Определяет тип воркфлоу в зависимости от выбранного стиля и типа эмоций
@ -86,8 +87,8 @@ export const validateGenerationParams = (
if (!imageData) { if (!imageData) {
return { return {
isValid: false, isValid: false,
errorTitle: 'Внимание', errorTitle: getTranslation('upload_image_warning_title'),
errorMessage: 'Сначала загрузите изображение' errorMessage: getTranslation('upload_image_warning_message')
}; };
} }
@ -95,8 +96,8 @@ export const validateGenerationParams = (
if (!presetId) { if (!presetId) {
return { return {
isValid: false, isValid: false,
errorTitle: 'Внимание', errorTitle: getTranslation('select_style_warning_title'),
errorMessage: 'Выберите образ для генерации' errorMessage: getTranslation('select_style_warning_message')
}; };
} }
@ -104,8 +105,8 @@ export const validateGenerationParams = (
if (presetId === 'customPrompt' && (!customPrompt || !customPrompt.trim())) { if (presetId === 'customPrompt' && (!customPrompt || !customPrompt.trim())) {
return { return {
isValid: false, isValid: false,
errorTitle: 'Внимание', errorTitle: getTranslation('enter_prompt_warning_title'),
errorMessage: 'Введите текст промпта' errorMessage: getTranslation('enter_prompt_warning_message')
}; };
} }