textmachine/frontend/src/mock/scale.ts

175 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Load fixture: the real scale of the stand's book — 2284 chapters and 4276 edit units (measured,
// D39.84). Not decoration: the layout is drawn for a dozen chapters, and the lists had to earn
// their behaviour at 10³ items before any screen existed (BACKLOG Ф-12). Since S3 it also carries
// the LONG TAIL of the data layer: the collections are served in pages, so following `next_cursor`
// to the end is exercised in a real browser and not only in a test.
//
// The text here is not a quotation, unlike src/mock/book.ts: chapter labels and terms are built
// from genre-typical words and transliteration syllables. The proportions of the two scripts are
// real — a hanzi is shorter than Cyrillic, and without that the columns drift.
import type { components } from '../api/schema';
type Schemas = components['schemas'];
const pick = (pool: readonly string[], index: number) => pool[index % pool.length] ?? '';
const chapterNames = [
'Нет раскаяния',
'Прозрение пятисот лет',
'Церемония открытия',
'Класс А',
'Деревня Гуюэ',
'Первый гу',
'Аптека',
'Кровь на снегу',
'Утраченный свиток',
'Горная тропа',
'Ночь перед испытанием',
'Скрытая пещера',
'Договор на крови',
'Возвращение старейшины',
'Три чаши вина',
'Сломанный клинок',
'Лисий морок',
'Заброшенный храм',
'Рынок диковин',
'Осенний смотр',
];
/** The draft wave has passed the first hundred-odd chapters: the hard case, again. */
const draftFront = 110;
export const scaleChapters: Schemas['Chapter'][] = Array.from({ length: 2284 }, (_, index) => {
const number = index + 1;
const unitsTotal = 1 + (number % 3);
return {
id: `ch_${String(number)}`,
number,
// Every twelfth chapter carries no label: a book without headings is legal, and a tree of 2284
// rows is where a client would be most tempted to paper over the gap with "Chapter N".
heading: number % 12 === 0 ? null : pick(chapterNames, index),
units_total: unitsTotal,
units_done: number < draftFront ? unitsTotal : number === draftFront ? 1 : 0,
note_count: number % 17 === 0 ? 1 + (number % 3) : 0,
};
});
// Hanzisyllable pairs: folded in twos and threes they give terms of realistic length and shape
// without repeating a single term of the live bank.
const syllables = [
['方', 'Фан'],
['白', 'Бай'],
['花', 'Хуа'],
['沈', 'Шэнь'],
['古', 'Гу'],
['月', 'Юэ'],
['山', 'Шань'],
['春', 'Чунь'],
['秋', 'Цю'],
['金', 'Цзинь'],
['光', 'Гуан'],
['虫', 'Чун'],
['蛊', 'Гоу'],
['师', 'Ши'],
['学', 'Сюэ'],
['堂', 'Тан'],
['开', 'Кай'],
['窍', 'Цяо'],
['冰', 'Бин'],
['酒', 'Цзю'],
] as const;
const kinds: Schemas['TermKind'][] = ['name', 'nickname', 'title', 'place', 'term'];
const statuses: Schemas['TermStatus'][] = ['approved', 'approved', 'draft', 'auto'];
const origins: Schemas['TermOrigin'][] = ['seed', 'ruby', 'mined'];
const senses = [
'спутник главного героя',
'наставник из первой части',
'место последней битвы',
'титул старейшины',
'гу времени',
'клан северных земель',
];
export const scaleTerms: Schemas['BankTerm'][] = Array.from({ length: 1200 }, (_, index) => {
const first = syllables[index % syllables.length] ?? syllables[0];
const second = syllables[Math.floor(index / syllables.length) % syllables.length] ?? syllables[0];
const tail = Math.floor(index / (syllables.length * syllables.length)) % 3;
const third = tail === 0 ? undefined : syllables[(index + tail) % syllables.length];
const status = statuses[index % statuses.length] ?? 'auto';
return {
id: `t_${String(index)}`,
src: first[0] + second[0] + (third?.[0] ?? ''),
dst:
status === 'auto' && index % 8 === 0
? ''
: first[1] + second[1].toLowerCase() + (third?.[1].toLowerCase() ?? ''),
// Every ninth row has no kind: the engine leaves ruby candidates of other classes untyped.
kind: index % 9 === 0 ? null : (kinds[index % kinds.length] ?? 'term'),
status,
origin: origins[index % origins.length] ?? 'mined',
// Most rows have a description, and that is not fixture cosmetics: the spoiler column on the
// long tail would otherwise be empty from beginning to end, that is, the spoiler mechanism at
// a thousand rows is checked by nothing (found by an adversarial review).
sense: index % 5 === 0 ? '' : (senses[index % senses.length] ?? ''),
// An applicability window on every seventh row: in a real book's bank it is rare, but not
// absent.
since_chapter: index % 7 === 0 ? (index % 400) + 40 : 0,
until_chapter: 0,
};
});
export const scaleBook: Schemas['Book'] = {
id: 'bk_scale',
title: 'Гу Чжэньжэнь',
source_lang: 'zh',
target_lang: 'ru',
genre: 'сянься',
chapter_count: scaleChapters.length,
character_count: 7_780_000,
added_at: '2026-07-28T09:14:00Z',
status: 'translating',
progress: {
draft: { done: 214, total: 4276 },
edit: { done: 0, total: 4276 },
eta_seconds: 9240,
},
note_count: scaleChapters.reduce((sum, chapter) => sum + chapter.note_count, 0),
};
export const scaleNotes: Schemas['Note'][] = scaleChapters
.filter((chapter) => chapter.note_count > 0)
.slice(0, 120)
.map((chapter) => ({
severity: chapter.note_count > 2 ? ('attention' as const) : ('glance' as const),
message: 'Служебная разметка вычищена автоматически',
chapter_id: chapter.id,
}));
// Units for a chapter of the scale book, generated from the SAME numbers the tree shows. Serving
// another book's hand-written units here made the reader contradict the row it was opened from:
// the tree said "1 of 2 done", the reader showed four blocks of a different chapter.
const lines = [
'青茅山下,古月家的少年们排成一列,等待开窍。',
'风从北面吹来,带着雪的气味。',
'祠堂前的石阶上落满了霜。',
];
export function scaleUnitsOf(chapterId: string): Schemas['Unit'][] {
const chapter = scaleChapters.find((row) => row.id === chapterId);
if (!chapter) return [];
return Array.from({ length: chapter.units_total }, (_, index) => {
const done = index < chapter.units_done;
return {
id: `${chapterId}_u${String(index)}`,
source: lines[index % lines.length] ?? '',
target: done
? 'У подножия Цинмао юноши рода Гуюэ стояли в ряд и ждали открытия апертуры.'
: '',
state: done ? ('translated' as const) : ('pending' as const),
note: null,
};
});
}