Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/api/generate/scene-content/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export async function POST(req: NextRequest) {
// Ensure outline has language from stageInfo (fallback for older outlines)
const outline: SceneOutline = {
...rawOutline,
language: rawOutline.language || (stageInfo?.language as 'zh-CN' | 'en-US') || 'zh-CN',
language: rawOutline.language || (stageInfo?.language as 'zh-CN' | 'en-US' | 'ru-RU') || 'zh-CN',
};

// ── Model resolution from request headers ──
Expand Down
23 changes: 18 additions & 5 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const RECENT_OPEN_STORAGE_KEY = 'recentClassroomsOpen';
interface FormState {
pdfFile: File | null;
requirement: string;
language: 'zh-CN' | 'en-US';
language: 'zh-CN' | 'en-US' | 'ru-RU';
webSearch: boolean;
}

Expand Down Expand Up @@ -101,10 +101,10 @@ function HomePage() {
const savedLanguage = localStorage.getItem(LANGUAGE_STORAGE_KEY);
const updates: Partial<FormState> = {};
if (savedWebSearch === 'true') updates.webSearch = true;
if (savedLanguage === 'zh-CN' || savedLanguage === 'en-US') {
if (savedLanguage === 'zh-CN' || savedLanguage === 'en-US' || savedLanguage === 'ru-RU') {
updates.language = savedLanguage;
} else {
const detected = navigator.language?.startsWith('zh') ? 'zh-CN' : 'en-US';
const detected = navigator.language?.startsWith('zh') ? 'zh-CN' : navigator.language?.startsWith('ru') ? 'ru-RU' : 'en-US';
updates.language = detected;
}
if (Object.keys(updates).length > 0) {
Expand Down Expand Up @@ -338,7 +338,7 @@ function HomePage() {
}}
className="flex items-center gap-1 px-3 py-1.5 rounded-full text-xs font-bold text-gray-500 dark:text-gray-400 hover:bg-white dark:hover:bg-gray-700 hover:text-gray-800 dark:hover:text-gray-200 hover:shadow-sm transition-all"
>
{locale === 'zh-CN' ? 'CN' : 'EN'}
{locale === 'zh-CN' ? 'CN' : locale === 'ru-RU' ? 'RU' : 'EN'}
</button>
{languageOpen && (
<div className="absolute top-full mt-2 right-0 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg overflow-hidden z-50 min-w-[120px]">
Expand Down Expand Up @@ -366,7 +366,20 @@ function HomePage() {
'bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400',
)}
>
English
English
</button>
<button
onClick={() => {
setLocale('ru-RU');
setLanguageOpen(false);
}}
className={cn(
'w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors',
locale === 'ru-RU' &&
'bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400',
)}
>
Русский
</button>
</div>
)}
Expand Down
11 changes: 7 additions & 4 deletions components/generation/generation-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ const MAX_PDF_SIZE_BYTES = MAX_PDF_SIZE_MB * 1024 * 1024;

// ─── Types ───────────────────────────────────────────────────
export interface GenerationToolbarProps {
language: 'zh-CN' | 'en-US';
onLanguageChange: (lang: 'zh-CN' | 'en-US') => void;
language: 'zh-CN' | 'en-US' | 'ru-RU';
onLanguageChange: (lang: 'zh-CN' | 'en-US' | 'ru-RU') => void;
webSearch: boolean;
onWebSearchChange: (v: boolean) => void;
onSettingsOpen: (section?: SettingsSection) => void;
Expand Down Expand Up @@ -361,11 +361,14 @@ export function GenerationToolbar({
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => onLanguageChange(language === 'zh-CN' ? 'en-US' : 'zh-CN')}
onClick={() => {
const next = language === 'zh-CN' ? 'en-US' : language === 'en-US' ? 'ru-RU' : 'zh-CN';
onLanguageChange(next);
}}
className={pillMuted}
>
<Globe className="size-3.5" />
<span>{language === 'zh-CN' ? '中文' : 'EN'}</span>
<span>{language === 'zh-CN' ? '中文' : language === 'ru-RU' ? 'RU' : 'EN'}</span>
</button>
</TooltipTrigger>
<TooltipContent>{t('toolbar.languageHint')}</TooltipContent>
Expand Down
2 changes: 1 addition & 1 deletion lib/generation/scene-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -735,7 +735,7 @@ function normalizeQuizAnswer(question: Record<string, unknown>): string[] | unde
async function generateInteractiveContent(
outline: SceneOutline,
aiCall: AICallFn,
language: 'zh-CN' | 'en-US' = 'zh-CN',
language: 'zh-CN' | 'en-US' | 'ru-RU' = 'zh-CN',
): Promise<GeneratedInteractiveContent | null> {
const config = outline.interactiveConfig!;

Expand Down
4 changes: 2 additions & 2 deletions lib/hooks/use-i18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type I18nContextType = {
};

const LOCALE_STORAGE_KEY = 'locale';
const VALID_LOCALES: Locale[] = ['zh-CN', 'en-US'];
const VALID_LOCALES: Locale[] = ['zh-CN', 'en-US', 'ru-RU'];

const I18nContext = createContext<I18nContextType | undefined>(undefined);

Expand All @@ -26,7 +26,7 @@ export function I18nProvider({ children }: { children: ReactNode }) {
setLocaleState(stored as Locale);
return;
}
const detected = navigator.language?.startsWith('zh') ? 'zh-CN' : 'en-US';
const detected = navigator.language?.startsWith('zh') ? 'zh-CN' : navigator.language?.startsWith('ru') ? 'ru-RU' : 'en-US';
localStorage.setItem(LOCALE_STORAGE_KEY, detected);
setLocaleState(detected);
} catch {
Expand Down
74 changes: 74 additions & 0 deletions lib/i18n/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,77 @@ export const chatEnUS = {
stopListening: 'Stop recording',
},
} as const;

export const chatRuRU = {
chat: {
lecture: 'Лекция',
noConversations: 'Нет диалогов',
startConversation: 'Введите сообщение, чтобы начать диалог',
noMessages: 'Пока нет сообщений',
ended: 'завершено',
unknown: 'Неизвестно',
stopDiscussion: 'Завершить обсуждение',
endQA: 'Завершить вопросы и ответы',
tabs: {
lecture: 'Заметки',
chat: 'Чат',
},
lectureNotes: {
empty: 'Заметки появятся здесь после воспроизведения лекции',
emptyHint: 'Нажмите воспроизведение для начала лекции',
pageLabel: 'Страница {n}',
currentPage: 'Текущая',
},
badge: {
qa: 'Вопросы',
discussion: 'ДИСКУС',
lecture: 'ЛЕКЦ',
},
},
actions: {
names: {
spotlight: 'Прожектор',
laser: 'Лазерная указка',
wb_open: 'Открыть доску',
wb_draw_text: 'Текст на доске',
wb_draw_shape: 'Фигура на доске',
wb_draw_chart: 'График на доске',
wb_draw_latex: 'Формула на доске',
wb_draw_table: 'Таблица на доске',
wb_draw_line: 'Линия на доске',
wb_clear: 'Очистить доску',
wb_delete: 'Удалить элемент',
wb_close: 'Закрыть доску',
discussion: 'Обсуждение',
},
status: {
inputStreaming: 'Ожидание',
inputAvailable: 'Выполняется',
outputAvailable: 'Завершено',
outputError: 'Ошибка',
outputDenied: 'Отклонено',
running: 'Выполняется',
result: 'Завершено',
error: 'Ошибка',
},
},
agentBar: {
readyToLearn: 'Готовы учиться вместе?',
expandedTitle: 'Настройка ролей в классе',
configTooltip: 'Нажмите для настройки ролей в классе',
voiceLabel: 'Голос',
voiceLoading: 'Загрузка...',
voiceAutoAssign: 'Голоса будут назначены автоматически',
},
proactiveCard: {
discussion: 'Обсуждение',
join: 'Присоединиться',
skip: 'Пропустить',
pause: 'Пауза',
resume: 'Продолжить',
},
voice: {
startListening: 'Голосовой ввод',
stopListening: 'Остановить запись',
},
} as const;
41 changes: 41 additions & 0 deletions lib/i18n/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,44 @@ export const commonEnUS = {
exportFailed: 'Export failed',
},
} as const;

export const commonRuRU = {
common: {
you: 'Вы',
confirm: 'Подтвердить',
cancel: 'Отмена',
loading: 'Загрузка...',
},
home: {
slogan: 'Generative Learning in Multi-Agent Interactive Classroom',
greeting: 'Привет, ',
},
toolbar: {
languageHint: 'Курс будет сгенерирован на этом языке',
pdfParser: 'Парсер',
pdfUpload: 'Загрузить PDF',
removePdf: 'Удалить файл',
webSearchOn: 'Включено',
webSearchOff: 'Нажмите для включения',
webSearchDesc: 'Поиск актуальной информации в интернете перед генерацией',
webSearchProvider: 'Поисковый движок',
webSearchNoProvider: 'Настройте API-ключ поиска в Настройках',
selectProvider: 'Выбрать провайдера',
configureProvider: 'Настроить модель',
configureProviderHint: 'Настройте хотя бы одного провайдера моделей для генерации курсов',
enterClassroom: 'Войти в класс',
advancedSettings: 'Расширенные настройки',
ttsTitle: 'Синтез речи',
ttsHint: 'Выберите голос для AI-учителя',
ttsPreview: 'Прослушать',
ttsPreviewing: 'Воспроизведение...',
},
export: {
pptx: 'Экспорт PPTX',
resourcePack: 'Экспорт ресурсного пакета',
resourcePackDesc: 'PPTX + интерактивные страницы',
exporting: 'Экспорт...',
exportSuccess: 'Экспорт успешен',
exportFailed: 'Ошибка экспорта',
},
} as const;
62 changes: 62 additions & 0 deletions lib/i18n/generation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,65 @@ export const generationEnUS = {
webSearchFailed: 'Web search failed',
},
} as const;

export const generationRuRU = {
upload: {
pdfSizeLimit: 'PDF до 50 МБ',
fileTooLarge: 'Файл слишком большой (макс. 50 МБ)',
requirementPlaceholder: 'Опишите что вы хотите изучить, и мы создадим для вас интерактивный курс...',
requirementRequired: 'Пожалуйста, укажите требования к курсу',
generateFailed: 'Ошибка генерации, попробуйте снова',
},
generation: {
generating: 'Генерация курса...',
step1: 'Этап 1: Анализ требований',
step2: 'Этап 2: Генерация структуры сцен',
step3: 'Этап 3: Генерация содержимого',
progress: 'Прогресс: {progress}%',
scenesGenerated: 'Сгенерировано {count} сцен',
complete: 'Генерация завершена!',
failed: 'Ошибка генерации',
retry: 'Повторить',
},
preview: {
title: 'Предпросмотр',
sceneCount: '{count} сцен',
startClassroom: 'Войти в класс',
regenerate: 'Сгенерировать заново',
editing: 'Редактирование',
sceneTitle: 'Сцена {index}: {title}',
slideType: 'Слайд',
quizType: 'Тест',
interactiveType: 'Интерактив',
pblType: 'Проект',
editOutlines: 'Изменить структуру',
applyOutlines: 'Применить изменения',
cancelOutlines: 'Отменить',
addOutline: 'Добавить сцену',
moveUp: 'Выше',
moveDown: 'Ниже',
deleteOutline: 'Удалить',
outlinesChanged: 'Структура изменена',
keyPoints: 'Ключевые моменты',
description: 'Описание',
autoSaved: 'Автосохранено',
lastEdited: 'Последнее редактирование',
},
classroom: {
today: 'Сегодня',
yesterday: 'Вчера',
daysAgo: 'дн. назад',
recentClassrooms: 'Недавние классы',
untitled: 'Без названия',
deleteConfirm: 'Удалить этот класс?',
deleteHint: 'Это действие нельзя отменить',
delete: 'Удалить',
},
controls: {
play: 'Воспроизвести / Пауза',
prev: 'Предыдущая',
next: 'Следующая',
fullscreen: 'Полный экран',
exitFullscreen: 'Развернуть',
},
} as const;
20 changes: 14 additions & 6 deletions lib/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { defaultLocale, type Locale } from './types';
export { type Locale, defaultLocale } from './types';
import { commonZhCN, commonEnUS } from './common';
import { stageZhCN, stageEnUS } from './stage';
import { chatZhCN, chatEnUS } from './chat';
import { generationZhCN, generationEnUS } from './generation';
import { settingsZhCN, settingsEnUS } from './settings';
import { commonZhCN, commonEnUS, commonRuRU } from './common';
import { stageZhCN, stageEnUS, stageRuRU } from './stage';
import { chatZhCN, chatEnUS, chatRuRU } from './chat';
import { generationZhCN, generationEnUS, generationRuRU } from './generation';
import { settingsZhCN, settingsEnUS, settingsRuRU } from './settings';

export const translations = {
'zh-CN': {
Expand All @@ -21,6 +21,13 @@ export const translations = {
...generationEnUS,
...settingsEnUS,
},
'ru-RU': {
...commonRuRU,
...stageRuRU,
...chatRuRU,
...generationRuRU,
...settingsRuRU,
},
} as const;

export type TranslationKey = keyof (typeof translations)[typeof defaultLocale];
Expand All @@ -40,7 +47,7 @@ export function getClientTranslation(key: string): string {
if (typeof window !== 'undefined') {
try {
const storedLocale = localStorage.getItem('locale');
if (storedLocale === 'zh-CN' || storedLocale === 'en-US') {
if (storedLocale === 'zh-CN' || storedLocale === 'en-US' || storedLocale === 'ru-RU') {
locale = storedLocale;
}
} catch {
Expand All @@ -50,3 +57,4 @@ export function getClientTranslation(key: string): string {

return translate(locale, key);
}

Loading
Loading