diff --git a/Documentation/API.md b/Documentation/API.md index ee8166c..c3f9aa8 100644 --- a/Documentation/API.md +++ b/Documentation/API.md @@ -171,6 +171,32 @@ Movements are filtered to those with at least one artist active in the requested --- +## Locale (`?locale=ru`) + +Public catalog endpoints accept optional **`locale`** query param (`en` default, `ru` supported) or `Accept-Language: ru`. + +Affected routes: `/api/catalog/bootstrap`, `/api/timeline`, `/api/search`, `/api/artists`, `/api/artists/:id`, `/api/paintings/:id`, `/api/movements/:id/gallery`, `/api/movements/:id/artists`, `/api/artists/:id/navigation`. + +Responses include `"locale": "ru"` when resolved. Display field names are unchanged; values come from `entity_translations` when `status = published`, else canonical English. + +Full guide: [i18n-russian.md](i18n-russian.md). + +--- + +## Translations (curator) + +Requires curator session. Base path: `/api/translations`. + +| Method | Path | Purpose | +|--------|------|---------| +| `GET` | `/api/translations/coverage?locale=ru` | Coverage stats | +| `GET` | `/api/translations/worklist/:entityType?locale=ru` | Artists/paintings/movements with translation status | +| `GET` | `/api/translations/:entityType/:id` | Canonical + all translation rows | +| `PUT` | `/api/translations/:entityType/:id` | Upsert fields `{ locale, fields, status }` | +| `POST` | `/api/translations/:entityType/:id/publish` | Publish all draft/reviewed rows for locale | + +--- + ## `GET /api/search` Public catalog search over **artists**, **paintings**, and **art movements**. Used by the timeline header search bar (`CatalogSearchBar.tsx`). @@ -182,6 +208,7 @@ Public catalog search over **artists**, **paintings**, and **art movements**. Us | `q` | string | — | Search text (min **2** characters after trim; shorter returns `{ q, results: [] }`) | | `limit` | int | 20 | Max results total (capped at **50**) | | `types` | string | all | Optional comma list: `artist`, `painting`, `movement` | +| `locale` | string | `en` | `ru` — search and return published Russian aliases when available | **Matching (case-insensitive `ILIKE`):** @@ -190,6 +217,7 @@ Public catalog search over **artists**, **paintings**, and **art movements**. Us | Artist | `name`, `wikipedia_title`, movement name | | Movement | movement `name`, era name | | Painting | `title`, `wikipedia_title`, `year` (as text), artist name, movement name | +| All (when `locale=ru`) | Published rows in `entity_translations` for `name` / `title` | Prefix matches on primary labels (`name` / `title`) rank before substring matches. diff --git a/Documentation/Plans.md b/Documentation/Plans.md index 9565cfc..9f46ba4 100644 --- a/Documentation/Plans.md +++ b/Documentation/Plans.md @@ -1,6 +1,6 @@ this file contains draft for future releases and features -1. Multy language support, russian version at least, search for best implementation, preferably story in db and easily expandable. tool to check and correct translation +1. ~~Multi language support, russian version at least~~ — done: UI i18n (EN/RU) + `entity_translations` DB + curator Translations tool — [i18n-russian.md](i18n-russian.md) 2. tool to manage links (influence/influenced by ) import csv's ( define format), edit ,add, delete, visualize, map to pictures/ entities 3. tool to monitor/manage (plan actions) of curator actions, markers to check painting/text ? 4. ~~tool to sync prod /env resources (both ways), db structure, db data, images, users etc~~ — done for catalog DB + images: `npm run harmonize` (schema dev→prod only; users/audit excluded) — [harmonize-dev-prod.md](harmonize-dev-prod.md) diff --git a/Documentation/harmonize-dev-prod.md b/Documentation/harmonize-dev-prod.md index 1408a92..b11e9eb 100644 --- a/Documentation/harmonize-dev-prod.md +++ b/Documentation/harmonize-dev-prod.md @@ -107,7 +107,7 @@ Reports are written to `db/SyncReports/harmonize_db_*.json` and `harmonize_image Processed in FK order: -`historical_eras` → `art_movements` → `artists` → `artist_periods` → `paintings` → `painting_influences` → `painting_influence_sources` → `painting_annotations` +`historical_eras` → `art_movements` → `artists` → `artist_periods` → `paintings` → `painting_influences` → `painting_influence_sources` → `painting_annotations` → **`entity_translations`** --- diff --git a/Documentation/i18n-russian.md b/Documentation/i18n-russian.md new file mode 100644 index 0000000..1eac0f9 --- /dev/null +++ b/Documentation/i18n-russian.md @@ -0,0 +1,105 @@ +# Russian localization (i18n) + +Full Russian support: **UI chrome** via `react-i18next`, **catalog text** via PostgreSQL `entity_translations`, with Cyrillic display aliases where available and English fallback. + +--- + +## Architecture + +| Layer | English | Russian | +|-------|---------|---------| +| UI labels, buttons | `client/src/locales/en/*.json` | `client/src/locales/ru/*.json` | +| Names / titles (display) | canonical DB columns | `entity_translations` (`name`, `title`) | +| Bios, notes, descriptions | canonical DB columns | `entity_translations` (`bio_full`, `body`, `notes`, …) | + +English remains canonical in main tables. Russian rows use `status`: `draft` → `reviewed` → `published`. Public API returns only **`published`** (unless curator preview). + +--- + +## User-facing locale switch + +Timeline header: **EN | RU** toggle (`LocaleSwitcher`). + +- Persists `gallery_locale` in `localStorage` +- Sets `document.documentElement.lang` +- Passes `?locale=ru` on catalog API requests +- Refetches bootstrap catalog when locale changes + +--- + +## Setup (dev) + +```powershell +npm run dev:migrate # includes migrate-i18n.sql +npm run dev:fetch-artist-bios-ru # draft bios + Cyrillic names from ru.wikipedia +# Curator: Translations page → review → Publish +npm run dev:import-translations -- --file path/to/translations.json --publish +``` + +Optional manual import JSON: + +```json +[ + { "entity_type": "painting", "entity_id": 42, "field_name": "title", "value": "Джоконда", "status": "published", "source": "manual" } +] +``` + +CSV: `entity_type,entity_id,field_name,value,status,source` + +--- + +## Curator translation review + +1. Sign in as curator +2. Header → **Translations** (or **Переводы** in RU UI) +3. Pick entity type (artist / painting / movement) +4. Select row → edit Russian fields side-by-side with English canonical +5. **Publish** saves and marks rows `published` + +Coverage stats show artists with `bio_full`, paintings with `title` alias, draft vs published counts. + +API (curator-only): see [API.md](API.md#translations-curator). + +--- + +## Translatable fields (v1) + +| entity_type | fields | +|-------------|--------| +| `era`, `movement` | `name`, `description` | +| `artist` | `name`, `bio_short`, `bio_full` | +| `artist_period` | `name`, `description` | +| `painting` | `title`, `description` | +| `annotation` | `label`, `body` | +| `influence_source` | `notes`, `aspects`, `quote`, `period_note` | + +--- + +## API locale + +Public endpoints accept `?locale=ru` or `Accept-Language: ru`. Responses include `"locale": "ru"` on catalog payloads; field names unchanged — values are already resolved. + +Search matches canonical text **or** published Russian aliases. + +--- + +## Prod rollout + +1. `npm run dev:migrate` on dev; prod schema: `npm run harmonize:schema` (dev → prod only) +2. Seed Russian on dev: `npm run dev:fetch-artist-bios-ru` +3. Curator review + publish +4. `npm run harmonize` to sync `entity_translations` to prod (or full promote if preferred) + +`entity_translations` is included in [`harmonize-db.js`](../scripts/harmonize-db.js) catalog sync. + +--- + +## npm scripts + +| Script | Purpose | +|--------|---------| +| `dev:migrate:i18n` | Apply `entity_translations` table only | +| `dev:fetch-artist-bios-ru` | Fetch ru.wikipedia bios into translations (draft) | +| `dev:import-translations` | Import JSON/CSV translation rows | + +See also [setup.md](setup.md), [environments.md](environments.md), [harmonize-dev-prod.md](harmonize-dev-prod.md). diff --git a/Documentation/setup.md b/Documentation/setup.md index 0168764..8a6f000 100644 --- a/Documentation/setup.md +++ b/Documentation/setup.md @@ -89,6 +89,9 @@ npm run dev:update-influences # painting influence graph for detail vie npm run dev:migrate:checkup-flags # optional: review/fixed flags for Checkup page (paintings) npm run dev:migrate:artist-checkup-flags # optional: same flags for artist portraits (bio debug) npm run dev:migrate:search # optional on very old DBs — also applied by dev:migrate / prod Step 4 +npm run dev:migrate:i18n # optional — entity_translations (also in dev:migrate) +npm run dev:fetch-artist-bios-ru # draft Russian bios + Cyrillic names from ru.wikipedia +npm run dev:import-translations -- --file path/to/file.json # bulk translation import npm run dev:migrate:painting-annotations # optional: art-history notes table npm run dev:update-painting-annotations # optional: load curated notes (+ --wikipedia for Wikipedia intros) npm run dev:fetch-images -- --limit=50 # random sample; 10s max per painting (default) @@ -117,7 +120,7 @@ Image fetch can take hours if you run it for the entire catalog. The first line | `npm run dev:server` | API with nodemon reload (local HMR workflow) | | `npm run dev:client` | Vite dev server on :5173 | -See [environments.md](environments.md) for dev/prod URLs, database split, Docker deploy, and sync commands. For incremental dev ↔ prod merge (both sides edited), see [harmonize-dev-prod.md](harmonize-dev-prod.md). Quick reference: [FAC.md](FAC.md). +See [environments.md](environments.md) for dev/prod URLs, database split, Docker deploy, and sync commands. For Russian UI + catalog text, see [i18n-russian.md](i18n-russian.md). Quick reference: [FAC.md](FAC.md). **Production frontend:** build the client, then start the server: diff --git a/client/index.html b/client/index.html index 3269aca..10f67d2 100644 --- a/client/index.html +++ b/client/index.html @@ -4,7 +4,7 @@ -
{error}
} {!error && !loading && flatResults.length === 0 && ( -No matches found.
+{t('noMatches')}
)} {grouped.map((group) => ({TYPE_LABELS[group.type]}
+{typeLabels[group.type]}
Debug tools and catalog edits require a curator account.
diff --git a/client/src/components/LocaleSwitcher.css b/client/src/components/LocaleSwitcher.css new file mode 100644 index 0000000..4cea03c --- /dev/null +++ b/client/src/components/LocaleSwitcher.css @@ -0,0 +1,26 @@ +.locale-switcher { + display: inline-flex; + border: 1px solid rgba(255, 255, 255, 0.25); + border-radius: 6px; + overflow: hidden; +} + +.locale-switcher-btn { + background: transparent; + color: inherit; + border: none; + padding: 0.25rem 0.55rem; + font-size: 0.75rem; + font-weight: 600; + cursor: pointer; + opacity: 0.75; +} + +.locale-switcher-btn-active { + background: rgba(255, 255, 255, 0.15); + opacity: 1; +} + +.locale-switcher-btn:hover { + opacity: 1; +} diff --git a/client/src/components/LocaleSwitcher.tsx b/client/src/components/LocaleSwitcher.tsx new file mode 100644 index 0000000..f2bb34f --- /dev/null +++ b/client/src/components/LocaleSwitcher.tsx @@ -0,0 +1,43 @@ +import { useTranslation } from 'react-i18next'; +import i18n from '../i18n'; +import { setApiLocale } from '../api/client'; +import { writeStoredLocale, type AppLocale } from '../utils/localeStorage'; +import './LocaleSwitcher.css'; + +interface Props { + onLocaleChange?: (locale: AppLocale) => void; +} + +export default function LocaleSwitcher({ onLocaleChange }: Props) { + const { t } = useTranslation('common'); + const current = (i18n.language === 'ru' ? 'ru' : 'en') as AppLocale; + + const setLocale = (locale: AppLocale) => { + if (locale === current) return; + void i18n.changeLanguage(locale); + writeStoredLocale(locale); + setApiLocale(locale); + onLocaleChange?.(locale); + }; + + return ( +