175 lines
6.9 KiB
TypeScript
175 lines
6.9 KiB
TypeScript
// 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,
|
||
};
|
||
});
|
||
|
||
// Hanzi–syllable 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,
|
||
};
|
||
});
|
||
}
|