Add Russian i18n with DB translations, locale API, and curator review UI.

UI chrome via react-i18next, catalog text in entity_translations with ru.wikipedia seeding, locale-aware search, and Translations page for publish workflow.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila Khodjaef
2026-07-15 11:51:46 +03:00
co-authored by Cursor
parent f247b418d8
commit ca58c43648
51 changed files with 2252 additions and 101 deletions
+61 -10
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import Timeline from '../components/Timeline';
import TimelineEventGuides from '../components/TimelineEventGuides';
import MovementBands from '../components/MovementBands';
@@ -6,13 +7,17 @@ import VirtualGallery from '../components/VirtualGallery';
import PaintingDetailView from '../components/PaintingDetail';
import ArtistBio from '../components/ArtistBio';
import CheckupPage from '../pages/CheckupPage';
import TranslationsPage from '../pages/TranslationsPage';
import CuratorLoginModal from '../components/CuratorLoginModal';
import CatalogSearchBar from '../components/CatalogSearchBar';
import LocaleSwitcher from '../components/LocaleSwitcher';
import GalleryLoadingMarker from '../components/GalleryLoadingMarker';
import '../components/CatalogSearchBar.css';
import '../components/CuratorLoginModal.css';
import '../components/LocaleSwitcher.css';
import '../pages/TranslationsPage.css';
import { useAuth } from '../context/AuthContext';
import { api, type FixPaintingImageResult, type FixArtistPortraitResult } from '../api/client';
import { api, setApiLocale, type FixPaintingImageResult, type FixArtistPortraitResult } from '../api/client';
import type { TimelineData, Artist, ArtistDetail, Painting, PaintingDetail, MovementGalleryDetail } from '../types';
import { createViewChangeScheduler } from '../utils/timelineView';
import { sortArtistPaintingsChronological } from '../utils/paintingUtils';
@@ -22,6 +27,7 @@ import './HomePage.css';
type View =
| { type: 'timeline' }
| { type: 'checkup' }
| { type: 'translations' }
| { type: 'gallery'; artistId: number; data: ArtistDetail }
| { type: 'movement-gallery'; movementId: number; data: MovementGalleryDetail }
| { type: 'painting'; paintingId: number; data: PaintingDetail; returnTo: View }
@@ -102,6 +108,7 @@ function catalogNavigateTarget(
}
export default function HomePage() {
const { t } = useTranslation('home');
const { isCurator, username, login, logout } = useAuth();
const [view, setView] = useState<View>({ type: 'timeline' });
const [gallerySession, setGallerySession] = useState<GallerySession | null>(null);
@@ -117,10 +124,11 @@ export default function HomePage() {
const [detailArtistPaintings, setDetailArtistPaintings] = useState<Painting[]>([]);
const [imageRevisions, setImageRevisions] = useState<Record<number, number>>({});
const [portraitRevisions, setPortraitRevisions] = useState<Record<number, number>>({});
const [localeVersion, setLocaleVersion] = useState(0);
const [debugMode, setDebugMode] = useState(readDebugMode);
const [debugShowMore, setDebugShowMore] = useState(readDebugShowMore);
const [loginOpen, setLoginOpen] = useState(false);
const [loginRedirect, setLoginRedirect] = useState<'checkup' | null>(null);
const [loginRedirect, setLoginRedirect] = useState<'checkup' | 'translations' | null>(null);
const effectiveDebugMode = debugMode && isCurator;
const [galleryRevision, setGalleryRevision] = useState(0);
const viewRef = useRef(view);
@@ -170,6 +178,11 @@ export default function HomePage() {
return () => {
cancelled = true;
};
}, [localeVersion]);
const handleLocaleChange = useCallback((locale: 'en' | 'ru') => {
setApiLocale(locale);
setLocaleVersion((v) => v + 1);
}, []);
const viewChangeScheduler = useRef(
@@ -209,7 +222,7 @@ export default function HomePage() {
writeDebugShowMore(enabled);
};
const openCuratorLogin = (redirect: 'checkup' | null = null) => {
const openCuratorLogin = (redirect: 'checkup' | 'translations' | null = null) => {
setLoginRedirect(redirect);
setLoginOpen(true);
};
@@ -219,6 +232,8 @@ export default function HomePage() {
setLoginOpen(false);
if (loginRedirect === 'checkup') {
setView({ type: 'checkup' });
} else if (loginRedirect === 'translations') {
setView({ type: 'translations' });
}
setLoginRedirect(null);
};
@@ -227,7 +242,7 @@ export default function HomePage() {
await logout();
writeDebugMode(false);
setDebugMode(false);
if (view.type === 'checkup') {
if (view.type === 'checkup' || view.type === 'translations') {
goToTimelineHome();
}
};
@@ -240,6 +255,14 @@ export default function HomePage() {
setView({ type: 'checkup' });
};
const openTranslations = () => {
if (!isCurator) {
openCuratorLogin('translations');
return;
}
setView({ type: 'translations' });
};
const handlePaintingImageFixed = useCallback(async (paintingId: number, fixResult: FixPaintingImageResult) => {
const data = await api.getPainting(paintingId);
const patch: Partial<Painting> = {
@@ -663,7 +686,7 @@ export default function HomePage() {
key={view.paintingId}
data={view.data}
artistPaintings={sortedDetailArtistPaintings}
backLabel={view.returnTo.type === 'timeline' ? '← Back to Timeline' : '← Back to Gallery'}
backLabel={view.returnTo.type === 'timeline' ? t('backToTimeline') : t('backToGallery')}
onBack={() => {
if (view.returnTo.type === 'timeline') {
goToTimelineHome();
@@ -723,6 +746,25 @@ export default function HomePage() {
</div>
)}
{view.type === 'translations' && (
isCurator ? (
<TranslationsPage onBack={goToTimelineHome} />
) : (
<div className="curator-login-gate">
<h2>{t('curatorRequiredTitle')}</h2>
<p>{t('curatorRequiredBody')}</p>
<div className="curator-login-gate-actions">
<button type="button" className="checkup-link-btn" onClick={() => openCuratorLogin('translations')}>
{t('curatorLogin')}
</button>
<button type="button" className="debug-mode-toggle" onClick={goToTimelineHome}>
{t('backToGalleryBtn')}
</button>
</div>
</div>
)
)}
{view.type === 'checkup' && (
isCurator ? (
<CheckupPage
@@ -782,13 +824,21 @@ export default function HomePage() {
/>
Show more
</label>
<button
type="button"
className="checkup-link-btn"
onClick={openTranslations}
title="Review and publish Russian translations"
>
{t('translations')}
</button>
<button
type="button"
className="checkup-link-btn"
onClick={openCheckup}
title="Open painting image checkup table"
>
Checkup
{t('checkup')}
</button>
<button
type="button"
@@ -796,7 +846,7 @@ export default function HomePage() {
onClick={handleCuratorLogout}
title="Sign out curator session"
>
Logout
{t('curatorLogout')}
</button>
</>
) : (
@@ -806,12 +856,13 @@ export default function HomePage() {
onClick={() => openCuratorLogin()}
title="Sign in as curator to use debug tools"
>
Curator login
{t('curatorLogin')}
</button>
)}
<LocaleSwitcher onLocaleChange={handleLocaleChange} />
</div>
<h1>Virtual Art Gallery</h1>
<p className="site-subtitle">Watch art movements branch forward through time each flowing from what came before</p>
<h1>{t('title')}</h1>
<p className="site-subtitle">{t('subtitle')}</p>
<CatalogSearchBar
onSelectArtist={handleArtistClick}
onSelectMovement={handleMovementClick}