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:
co-authored by
Cursor
parent
f247b418d8
commit
ca58c43648
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { CatalogSearchResult } from '../types';
|
||||
import { api, imageUrl, portraitThumbUrl } from '../api/client';
|
||||
import './CatalogSearchBar.css';
|
||||
@@ -10,11 +11,6 @@ interface Props {
|
||||
}
|
||||
|
||||
const TYPE_ORDER: CatalogSearchResult['type'][] = ['artist', 'movement', 'painting'];
|
||||
const TYPE_LABELS: Record<CatalogSearchResult['type'], string> = {
|
||||
artist: 'Artists',
|
||||
movement: 'Movements',
|
||||
painting: 'Paintings',
|
||||
};
|
||||
|
||||
function resultKey(item: CatalogSearchResult): string {
|
||||
return `${item.type}-${item.id}`;
|
||||
@@ -30,6 +26,12 @@ export default function CatalogSearchBar({
|
||||
onSelectMovement,
|
||||
onSelectPainting,
|
||||
}: Props) {
|
||||
const { t } = useTranslation('search');
|
||||
const typeLabels: Record<CatalogSearchResult['type'], string> = {
|
||||
artist: t('groupArtist'),
|
||||
movement: t('groupMovement'),
|
||||
painting: t('groupPainting'),
|
||||
};
|
||||
const listboxId = useId();
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -91,7 +93,7 @@ export default function CatalogSearchBar({
|
||||
.catch(() => {
|
||||
if (cancelled) return;
|
||||
setResults([]);
|
||||
setError('Search failed.');
|
||||
setError(t('searchFailed'));
|
||||
setOpen(true);
|
||||
setActiveIndex(-1);
|
||||
})
|
||||
@@ -141,7 +143,7 @@ export default function CatalogSearchBar({
|
||||
return (
|
||||
<div className="catalog-search" ref={rootRef}>
|
||||
<label className="catalog-search-label" htmlFor={`${listboxId}-input`}>
|
||||
Search
|
||||
{t('label')}
|
||||
</label>
|
||||
<div className="catalog-search-field">
|
||||
<input
|
||||
@@ -156,7 +158,7 @@ export default function CatalogSearchBar({
|
||||
aria-activedescendant={
|
||||
showPanel && activeIndex >= 0 ? `${listboxId}-opt-${activeIndex}` : undefined
|
||||
}
|
||||
placeholder="Search artists, paintings, movements…"
|
||||
placeholder={t('placeholder')}
|
||||
value={query}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
@@ -177,15 +179,15 @@ export default function CatalogSearchBar({
|
||||
id={`${listboxId}-listbox`}
|
||||
className="catalog-search-panel"
|
||||
role="listbox"
|
||||
aria-label="Search results"
|
||||
aria-label={t('resultsLabel')}
|
||||
>
|
||||
{error && <p className="catalog-search-message catalog-search-error">{error}</p>}
|
||||
{!error && !loading && flatResults.length === 0 && (
|
||||
<p className="catalog-search-message">No matches found.</p>
|
||||
<p className="catalog-search-message">{t('noMatches')}</p>
|
||||
)}
|
||||
{grouped.map((group) => (
|
||||
<div key={group.type} className="catalog-search-group">
|
||||
<p className="catalog-search-group-label">{TYPE_LABELS[group.type]}</p>
|
||||
<p className="catalog-search-group-label">{typeLabels[group.type]}</p>
|
||||
<ul className="catalog-search-list">
|
||||
{group.items.map((item) => {
|
||||
const flatIndex = flatResults.findIndex((r) => resultKey(r) === resultKey(item));
|
||||
|
||||
Reference in New Issue
Block a user