@jjlmoya/utils-books 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.github/workflows/ci.yml +79 -0
  2. package/.gitignore +1 -0
  3. package/astro.config.mjs +6 -1
  4. package/package.json +18 -5
  5. package/scripts/postbuild.mjs +17 -0
  6. package/scripts/postinstall.mjs +2 -4
  7. package/src/category/index.ts +2 -1
  8. package/src/components/ProductionBreadcrumb.astro +134 -0
  9. package/src/components/ProductionStructuredData.astro +46 -0
  10. package/src/components/ProductionWidget.astro +182 -0
  11. package/src/entries.ts +4 -1
  12. package/src/i18n/header-ui.ts +15 -0
  13. package/src/i18n/language-ui.ts +9 -0
  14. package/src/i18n/languages.ts +24 -0
  15. package/src/identity/brands.ts +5 -0
  16. package/src/index.ts +1 -0
  17. package/src/layouts/ProductionCategoryPage.astro +184 -0
  18. package/src/layouts/ProductionPage.astro +218 -0
  19. package/src/layouts/ProductionUtilityPage.astro +283 -0
  20. package/src/mfe/assets.ts +39 -0
  21. package/src/mfe/category-ui.ts +34 -0
  22. package/src/mfe/manifest.ts +45 -0
  23. package/src/mfe/routes.ts +50 -0
  24. package/src/pages/[locale]/[utilities]/[categories]/[category]/[slug]/manifest.json.ts +49 -0
  25. package/src/pages/[locale]/[utilities]/[categories]/[category]/[slug].astro +100 -0
  26. package/src/pages/[locale]/[utilities]/[categories]/[category].astro +44 -0
  27. package/src/pages/index.astro +1 -2
  28. package/src/pages/mfe-sitemaps/[locale]/[vertical]/sitemap.xml.ts +75 -0
  29. package/src/pages/utilidades/[slug]/manifest.json.ts +28 -0
  30. package/src/pages/utilidades/[slug].astro +90 -0
  31. package/src/pages/utilidades/categorias/[category].astro +38 -0
  32. package/src/tests/locale_completeness.test.ts +1 -1
  33. package/src/tests/mfe_cache_contract.test.ts +14 -0
  34. package/src/tests/mfe_manifest.test.ts +28 -0
  35. package/src/tests/registry_contract.test.ts +67 -0
  36. package/src/tests/tool_validation.test.ts +1 -1
  37. package/src/tool/book-index-page-budget-calculator/bibliography.astro +6 -0
  38. package/src/tool/book-index-page-budget-calculator/bibliography.ts +17 -0
  39. package/src/tool/book-index-page-budget-calculator/book-index-page-budget-calculator.css +478 -0
  40. package/src/tool/book-index-page-budget-calculator/component.astro +134 -0
  41. package/src/tool/book-index-page-budget-calculator/controller.ts +114 -0
  42. package/src/tool/book-index-page-budget-calculator/dom-views.ts +89 -0
  43. package/src/tool/book-index-page-budget-calculator/entry.ts +39 -0
  44. package/src/tool/book-index-page-budget-calculator/evaluator.ts +26 -0
  45. package/src/tool/book-index-page-budget-calculator/i18n/de.ts +35 -0
  46. package/src/tool/book-index-page-budget-calculator/i18n/en.ts +113 -0
  47. package/src/tool/book-index-page-budget-calculator/i18n/es.ts +35 -0
  48. package/src/tool/book-index-page-budget-calculator/i18n/fr.ts +8 -0
  49. package/src/tool/book-index-page-budget-calculator/i18n/id.ts +35 -0
  50. package/src/tool/book-index-page-budget-calculator/i18n/it.ts +8 -0
  51. package/src/tool/book-index-page-budget-calculator/i18n/ja.ts +8 -0
  52. package/src/tool/book-index-page-budget-calculator/i18n/ko.ts +8 -0
  53. package/src/tool/book-index-page-budget-calculator/i18n/nl.ts +8 -0
  54. package/src/tool/book-index-page-budget-calculator/i18n/pl.ts +8 -0
  55. package/src/tool/book-index-page-budget-calculator/i18n/pt.ts +8 -0
  56. package/src/tool/book-index-page-budget-calculator/i18n/ru.ts +8 -0
  57. package/src/tool/book-index-page-budget-calculator/i18n/sv.ts +8 -0
  58. package/src/tool/book-index-page-budget-calculator/i18n/tr.ts +8 -0
  59. package/src/tool/book-index-page-budget-calculator/i18n/zh.ts +8 -0
  60. package/src/tool/book-index-page-budget-calculator/index.ts +11 -0
  61. package/src/tool/book-index-page-budget-calculator/logic.test.ts +52 -0
  62. package/src/tool/book-index-page-budget-calculator/logic.ts +86 -0
  63. package/src/tool/book-index-page-budget-calculator/seo.astro +15 -0
  64. package/src/tool/book-index-page-budget-calculator/storage.ts +17 -0
  65. package/src/tool/book-index-page-budget-calculator/ui.ts +41 -0
  66. package/src/tools.ts +2 -1
  67. package/src/types.ts +4 -6
  68. package/src/worker.ts +27 -0
  69. package/tsconfig.json +14 -5
  70. package/src/pages/[locale]/[slug].astro +0 -164
  71. package/src/pages/[locale].astro +0 -251
@@ -0,0 +1,134 @@
1
+ ---
2
+ import { calculateIndexBudget, DEFAULT_INDEX_INPUTS } from './logic';
3
+ import { evaluateBudget } from './evaluator';
4
+ import type { BookIndexUI } from './ui';
5
+
6
+ interface Props {
7
+ ui: BookIndexUI;
8
+ }
9
+
10
+ const { ui } = Astro.props as Props;
11
+ const initialBudget = calculateIndexBudget(DEFAULT_INDEX_INPUTS);
12
+ const initialEvaluation = evaluateBudget(initialBudget);
13
+ function initialStatusCopy(status: ReturnType<typeof evaluateBudget>['status'], content: BookIndexUI): { text: string; hint: string } {
14
+ if (status === 'tight') return { text: content.statusTight, hint: content.statusTightHint };
15
+ if (status === 'roomy') return { text: content.statusRoomy, hint: content.statusRoomyHint };
16
+ return { text: content.statusBalanced, hint: content.statusBalancedHint };
17
+ }
18
+
19
+ function initialGapCopy(gap: number, content: BookIndexUI): string {
20
+ if (gap > 0) return `${gap} ${content.summaryPagesOver}`;
21
+ if (gap < 0) return `${Math.abs(gap)} ${content.summaryPagesSpare}`;
22
+ return content.summaryOnTarget;
23
+ }
24
+
25
+ const initialStatusCopyValue = initialStatusCopy(initialEvaluation.status, ui);
26
+ const initialGap = initialBudget.estimatedIndexPages - initialBudget.targetIndexPages;
27
+ const initialGapLabel = initialGapCopy(initialGap, ui);
28
+ const initialBodyShare = (initialBudget.textPages / initialBudget.totalPages) * 100;
29
+ const initialIndexShare = 100 - initialBodyShare;
30
+ ---
31
+
32
+ <div class="book-index-tool" data-book-index-tool data-ui={JSON.stringify(ui)} data-status={initialEvaluation.status} data-units="imperial">
33
+ <form class="index-workbench" data-form>
34
+ <section class="budget-summary" aria-label={ui.summaryLabel}>
35
+ <div class="summary-topline">
36
+ <span>{ui.summaryLabel}</span>
37
+ <strong data-output="summary-gap">{initialGapLabel}</strong>
38
+ </div>
39
+ <div class="summary-grid">
40
+ <div class="summary-total">
41
+ <span>{ui.totalPagesLabel}</span>
42
+ <strong data-output="total-pages">{initialBudget.totalPages}</strong>
43
+ <small>{ui.targetTotalPagesLabel} <b data-output="target-total-pages">{initialBudget.targetTotalPages}</b></small>
44
+ </div>
45
+ <div class="budget-strip" data-scene aria-label={ui.sceneLabel}>
46
+ <div class="strip-labels">
47
+ <span>{ui.sceneBodyLabel} <b data-output="scene-body-count">{initialBudget.textPages}</b></span>
48
+ <span>{ui.sceneIndexLabel} <b data-output="scene-index-count">{initialBudget.estimatedIndexPages}</b></span>
49
+ </div>
50
+ <div class="strip-track">
51
+ <span class="strip-body" data-paper="body" style={`--paper-share: ${initialBodyShare}%`}></span>
52
+ <span class="strip-index" data-paper="index" style={`--paper-share: ${initialIndexShare}%`}></span>
53
+ </div>
54
+ <div class="strip-target"><span>{ui.targetIndexPagesLabel}</span><strong data-output="summary-target">{initialBudget.targetIndexPages}</strong></div>
55
+ </div>
56
+ <div class="summary-brief" data-status-line>
57
+ <span class="status-dot"></span>
58
+ <strong data-output="status">{initialStatusCopyValue.text}</strong>
59
+ <p data-output="status-hint">{initialStatusCopyValue.hint}</p>
60
+ </div>
61
+ </div>
62
+ </section>
63
+
64
+ <div class="workbench-controls">
65
+ <div class="control-block">
66
+ <div class="field">
67
+ <label for="manuscript-words">{ui.wordsLabel}</label>
68
+ <input id="manuscript-words" data-input="manuscript-words" type="number" min="100" max="500000" step="100" value={DEFAULT_INDEX_INPUTS.manuscriptWords} inputmode="numeric" />
69
+ <span class="field-hint">{ui.wordsHint}</span>
70
+ </div>
71
+ <div class="field">
72
+ <label for="index-entries">{ui.indexEntriesLabel}</label>
73
+ <input id="index-entries" data-input="index-entries" type="number" min="10" max="10000" step="10" value={DEFAULT_INDEX_INPUTS.indexEntries} inputmode="numeric" />
74
+ <span class="field-hint">{ui.indexEntriesHint}</span>
75
+ </div>
76
+ </div>
77
+
78
+ <div class="control-block">
79
+ <div class="dimension-field">
80
+ <div class="field-row">
81
+ <label for="trim-width">{ui.trimWidthLabel}</label>
82
+ <label for="trim-height">{ui.trimHeightLabel}</label>
83
+ </div>
84
+ <div class="input-row">
85
+ <input id="trim-width" data-input="trim-width" type="number" min="3" max="15" step="0.01" value={DEFAULT_INDEX_INPUTS.trimWidthInches} inputmode="decimal" />
86
+ <span class="times">×</span>
87
+ <input id="trim-height" data-input="trim-height" type="number" min="3" max="15" step="0.01" value={DEFAULT_INDEX_INPUTS.trimHeightInches} inputmode="decimal" />
88
+ </div>
89
+ <span class="field-hint">{ui.trimHint}</span>
90
+ </div>
91
+ <fieldset class="unit-field">
92
+ <legend>{ui.unitLabel}</legend>
93
+ <div class="unit-switch">
94
+ <button type="button" data-unit="imperial" aria-pressed="true">{ui.imperialButtonLabel}</button>
95
+ <button type="button" data-unit="metric" aria-pressed="false">{ui.metricButtonLabel}</button>
96
+ </div>
97
+ </fieldset>
98
+ </div>
99
+
100
+ <div class="control-block">
101
+ <fieldset class="density-field">
102
+ <legend>{ui.densityLabel}</legend>
103
+ <span class="field-hint">{ui.densityHint}</span>
104
+ <div class="density-options">
105
+ <button type="button" data-density="airy" aria-pressed="false"><span class="density-mark airy-mark"></span>{ui.airyLabel}</button>
106
+ <button type="button" data-density="standard" aria-pressed="true"><span class="density-mark standard-mark"></span>{ui.standardLabel}</button>
107
+ <button type="button" data-density="compact" aria-pressed="false"><span class="density-mark compact-mark"></span>{ui.compactLabel}</button>
108
+ </div>
109
+ </fieldset>
110
+ <div class="field">
111
+ <label for="target-index-pages">{ui.targetIndexPagesLabel}</label>
112
+ <input id="target-index-pages" data-input="target-index-pages" type="number" min="1" max="200" step="1" value={DEFAULT_INDEX_INPUTS.targetIndexPages} inputmode="numeric" />
113
+ <span class="field-hint">{ui.targetIndexPagesHint}</span>
114
+ </div>
115
+ </div>
116
+ </div>
117
+
118
+ <div class="reading-details">
119
+ <div class="reading-item"><span>{ui.textPagesLabel}</span><strong data-output="text-pages">{initialBudget.textPages}</strong></div>
120
+ <div class="reading-item"><span>{ui.estimatedIndexPagesLabel}</span><strong data-output="index-pages">{initialBudget.estimatedIndexPages}</strong></div>
121
+ <div class="reading-item"><span>{ui.entriesPerPageLabel}</span><strong data-output="entries-per-page">{initialBudget.targetEntriesPerPage.toFixed(1)}</strong></div>
122
+ <div class="reading-item"><span>{ui.indexShareLabel}</span><strong data-output="index-share">{initialBudget.indexSharePercent.toFixed(1)}%</strong></div>
123
+ <div class="reading-item"><span>{ui.densityLabel}</span><strong data-output="words-per-page">{initialBudget.wordsPerPage} {ui.wordsLabel.toLowerCase()}</strong></div>
124
+ </div>
125
+ <p class="source-note">{ui.sourceNote}</p>
126
+ </form>
127
+ </div>
128
+
129
+ <script>
130
+ import { initBookIndexTool } from './controller';
131
+
132
+ const root = document.querySelector<HTMLElement>('[data-book-index-tool]');
133
+ if (root) initBookIndexTool(root);
134
+ </script>
@@ -0,0 +1,114 @@
1
+ import {
2
+ calculateIndexBudget,
3
+ clampNumber,
4
+ centimetersToInches,
5
+ DEFAULT_INDEX_INPUTS,
6
+ inchesToCentimeters,
7
+ type BookIndexInputs,
8
+ type Density,
9
+ } from './logic';
10
+ import { renderBudget } from './dom-views';
11
+ import { loadUnitSystem, saveUnitSystem, type UnitSystem } from './storage';
12
+ import type { BookIndexUI } from './ui';
13
+
14
+ const DISPLAY_LIMITS = {
15
+ manuscriptWords: [100, 500000],
16
+ indexEntries: [10, 10000],
17
+ targetIndexPages: [1, 200],
18
+ } as const;
19
+
20
+ function input(root: HTMLElement, name: string): HTMLInputElement | null {
21
+ return root.querySelector<HTMLInputElement>(`[data-input="${name}"]`);
22
+ }
23
+
24
+ function parseInput(root: HTMLElement, name: string, fallback: number): number {
25
+ const field = input(root, name);
26
+ return field ? Number(field.value) || fallback : fallback;
27
+ }
28
+
29
+ function displayedDimension(inputs: BookIndexInputs, unitSystem: UnitSystem, dimension: 'width' | 'height'): number {
30
+ const value = dimension === 'width' ? inputs.trimWidthInches : inputs.trimHeightInches;
31
+ return unitSystem === 'metric' ? inchesToCentimeters(value) : value;
32
+ }
33
+
34
+ function readInputs(root: HTMLElement, previous: BookIndexInputs, unitSystem: UnitSystem): BookIndexInputs {
35
+ const width = parseInput(root, 'trim-width', displayedDimension(previous, unitSystem, 'width'));
36
+ const height = parseInput(root, 'trim-height', displayedDimension(previous, unitSystem, 'height'));
37
+ const toInches = (value: number): number => unitSystem === 'metric' ? centimetersToInches(value) : value;
38
+
39
+ return {
40
+ manuscriptWords: clampNumber(parseInput(root, 'manuscript-words', previous.manuscriptWords), ...DISPLAY_LIMITS.manuscriptWords, previous.manuscriptWords),
41
+ trimWidthInches: clampNumber(toInches(width), 3, 15, previous.trimWidthInches),
42
+ trimHeightInches: clampNumber(toInches(height), 3, 15, previous.trimHeightInches),
43
+ density: previous.density,
44
+ indexEntries: clampNumber(parseInput(root, 'index-entries', previous.indexEntries), ...DISPLAY_LIMITS.indexEntries, previous.indexEntries),
45
+ targetIndexPages: clampNumber(parseInput(root, 'target-index-pages', previous.targetIndexPages), ...DISPLAY_LIMITS.targetIndexPages, previous.targetIndexPages),
46
+ };
47
+ }
48
+
49
+ function setDimensionFields(root: HTMLElement, inputs: BookIndexInputs, unitSystem: UnitSystem): void {
50
+ const width = input(root, 'trim-width');
51
+ const height = input(root, 'trim-height');
52
+ if (width) width.value = displayedDimension(inputs, unitSystem, 'width').toFixed(unitSystem === 'metric' ? 1 : 2);
53
+ if (height) height.value = displayedDimension(inputs, unitSystem, 'height').toFixed(unitSystem === 'metric' ? 1 : 2);
54
+ }
55
+
56
+ function setUnitButtons(root: HTMLElement, unitSystem: UnitSystem): void {
57
+ root.querySelectorAll<HTMLButtonElement>('[data-unit]').forEach((button) => {
58
+ button.setAttribute('aria-pressed', String(button.dataset.unit === unitSystem));
59
+ });
60
+ root.dataset.units = unitSystem;
61
+ }
62
+
63
+ function setDensityButtons(root: HTMLElement, density: Density): void {
64
+ root.querySelectorAll<HTMLButtonElement>('[data-density]').forEach((button) => {
65
+ button.setAttribute('aria-pressed', String(button.dataset.density === density));
66
+ });
67
+ }
68
+
69
+ function render(root: HTMLElement, ui: BookIndexUI, inputs: BookIndexInputs, unitSystem: UnitSystem): void {
70
+ setUnitButtons(root, unitSystem);
71
+ setDensityButtons(root, inputs.density);
72
+ renderBudget({ root, budget: calculateIndexBudget(inputs), inputs, ui, unitSystem });
73
+ }
74
+
75
+ function bindNumberFields(root: HTMLElement, ui: BookIndexUI, state: { inputs: BookIndexInputs; unitSystem: UnitSystem }): void {
76
+ root.querySelectorAll<HTMLInputElement>('[data-input]').forEach((field) => {
77
+ field.addEventListener('input', () => {
78
+ state.inputs = readInputs(root, state.inputs, state.unitSystem);
79
+ render(root, ui, state.inputs, state.unitSystem);
80
+ });
81
+ });
82
+ }
83
+
84
+ function bindDensity(root: HTMLElement, ui: BookIndexUI, state: { inputs: BookIndexInputs; unitSystem: UnitSystem }): void {
85
+ root.querySelectorAll<HTMLButtonElement>('[data-density]').forEach((button) => {
86
+ button.addEventListener('click', () => {
87
+ const density = button.dataset.density as Density;
88
+ state.inputs = { ...readInputs(root, state.inputs, state.unitSystem), density };
89
+ render(root, ui, state.inputs, state.unitSystem);
90
+ });
91
+ });
92
+ }
93
+
94
+ function bindUnits(root: HTMLElement, ui: BookIndexUI, state: { inputs: BookIndexInputs; unitSystem: UnitSystem }): void {
95
+ root.querySelectorAll<HTMLButtonElement>('[data-unit]').forEach((button) => {
96
+ button.addEventListener('click', () => {
97
+ state.inputs = readInputs(root, state.inputs, state.unitSystem);
98
+ state.unitSystem = button.dataset.unit === 'metric' ? 'metric' : 'imperial';
99
+ saveUnitSystem(state.unitSystem);
100
+ setDimensionFields(root, state.inputs, state.unitSystem);
101
+ render(root, ui, state.inputs, state.unitSystem);
102
+ });
103
+ });
104
+ }
105
+
106
+ export function initBookIndexTool(root: HTMLElement): void {
107
+ const ui = JSON.parse(root.dataset.ui ?? '{}') as BookIndexUI;
108
+ const state = { inputs: DEFAULT_INDEX_INPUTS, unitSystem: loadUnitSystem() };
109
+ bindNumberFields(root, ui, state);
110
+ bindDensity(root, ui, state);
111
+ bindUnits(root, ui, state);
112
+ setDimensionFields(root, state.inputs, state.unitSystem);
113
+ render(root, ui, state.inputs, state.unitSystem);
114
+ }
@@ -0,0 +1,89 @@
1
+ import type { BookIndexBudget, BookIndexInputs } from './logic';
2
+ import { evaluateBudget } from './evaluator';
3
+ import type { BookIndexUI } from './ui';
4
+ import type { UnitSystem } from './storage';
5
+
6
+ interface RenderArgs {
7
+ root: HTMLElement;
8
+ budget: BookIndexBudget;
9
+ inputs: BookIndexInputs;
10
+ ui: BookIndexUI;
11
+ unitSystem: UnitSystem;
12
+ }
13
+
14
+ interface SceneArgs {
15
+ root: HTMLElement;
16
+ budget: BookIndexBudget;
17
+ inputs: BookIndexInputs;
18
+ ui: BookIndexUI;
19
+ unitSystem: UnitSystem;
20
+ }
21
+
22
+ function number(value: number, digits = 0): string {
23
+ return new Intl.NumberFormat('en-US', { maximumFractionDigits: digits }).format(value);
24
+ }
25
+
26
+ function setOutput(root: HTMLElement, name: string, value: string): void {
27
+ const output = root.querySelector<HTMLElement>(`[data-output="${name}"]`);
28
+ if (output) output.textContent = value;
29
+ }
30
+
31
+ function renderOutputs(root: HTMLElement, budget: BookIndexBudget, ui: BookIndexUI): void {
32
+ setOutput(root, 'text-pages', number(budget.textPages));
33
+ setOutput(root, 'index-pages', number(budget.estimatedIndexPages));
34
+ setOutput(root, 'total-pages', number(budget.totalPages));
35
+ setOutput(root, 'target-total-pages', number(budget.targetTotalPages));
36
+ setOutput(root, 'entries-per-page', number(budget.targetEntriesPerPage, 1));
37
+ setOutput(root, 'index-share', `${number(budget.indexSharePercent, 1)}%`);
38
+ setOutput(root, 'words-per-page', `${number(budget.wordsPerPage)} ${ui.wordsLabel.toLowerCase()}`);
39
+ setOutput(root, 'summary-target', number(budget.targetIndexPages));
40
+ }
41
+
42
+ function summaryGapCopy(gap: number, ui: BookIndexUI): string {
43
+ if (gap > 0) return `${number(gap)} ${ui.summaryPagesOver}`;
44
+ if (gap < 0) return `${number(Math.abs(gap))} ${ui.summaryPagesSpare}`;
45
+ return ui.summaryOnTarget;
46
+ }
47
+
48
+ function renderSummary(root: HTMLElement, budget: BookIndexBudget, ui: BookIndexUI): void {
49
+ const gap = budget.estimatedIndexPages - budget.targetIndexPages;
50
+ const gapLabel = summaryGapCopy(gap, ui);
51
+ setOutput(root, 'summary-gap', gapLabel);
52
+ setOutput(root, 'scene-body-count', number(budget.textPages));
53
+ setOutput(root, 'scene-index-count', number(budget.estimatedIndexPages));
54
+ }
55
+
56
+ function statusCopy(status: ReturnType<typeof evaluateBudget>['status'], ui: BookIndexUI): { text: string; hint: string } {
57
+ if (status === 'tight') return { text: ui.statusTight, hint: ui.statusTightHint };
58
+ if (status === 'roomy') return { text: ui.statusRoomy, hint: ui.statusRoomyHint };
59
+ return { text: ui.statusBalanced, hint: ui.statusBalancedHint };
60
+ }
61
+
62
+ function renderStatus(root: HTMLElement, budget: BookIndexBudget, ui: BookIndexUI): void {
63
+ const evaluation = evaluateBudget(budget);
64
+ const copy = statusCopy(evaluation.status, ui);
65
+ root.dataset.status = evaluation.status;
66
+ setOutput(root, 'status', copy.text);
67
+ setOutput(root, 'status-hint', copy.hint);
68
+ }
69
+
70
+ function renderScene(args: SceneArgs): void {
71
+ const scene = args.root.querySelector<HTMLElement>('[data-scene]');
72
+ if (!scene) return;
73
+ const bodyShare = (args.budget.textPages / args.budget.totalPages) * 100;
74
+ const indexShare = 100 - bodyShare;
75
+ scene.style.setProperty('--body-share', `${bodyShare}%`);
76
+ scene.style.setProperty('--index-share', `${indexShare}%`);
77
+ scene.dataset.density = args.inputs.density;
78
+ scene.dataset.units = args.unitSystem;
79
+ setOutput(args.root, 'scene-body-label', `${number(args.budget.textPages)} ${args.ui.sceneBodyLabel}`);
80
+ setOutput(args.root, 'scene-index-label', `${number(args.budget.estimatedIndexPages)} ${args.ui.sceneIndexLabel}`);
81
+ setOutput(args.root, 'scene-caption', `${args.ui.sceneCaption} ${number(args.budget.totalPages)}. ${args.ui.sceneTargetCaption} ${number(args.budget.targetTotalPages)}.`);
82
+ }
83
+
84
+ export function renderBudget(args: RenderArgs): void {
85
+ renderOutputs(args.root, args.budget, args.ui);
86
+ renderSummary(args.root, args.budget, args.ui);
87
+ renderStatus(args.root, args.budget, args.ui);
88
+ renderScene(args);
89
+ }
@@ -0,0 +1,39 @@
1
+ import type { BooksToolEntry, SEOSection, ToolLocaleContent } from '../../types';
2
+ import type { BookIndexUI } from './ui';
3
+
4
+ export type { BookIndexUI } from './ui';
5
+ export type BookIndexLocaleContent = ToolLocaleContent<BookIndexUI>;
6
+
7
+ function withLocalizedSchemas(content: BookIndexLocaleContent): BookIndexLocaleContent {
8
+ const seo: SEOSection[] = content.seo.length >= 10
9
+ ? content.seo
10
+ : [...content.seo, { type: 'title', text: content.ui.sceneLabel, level: 2 }, { type: 'paragraph', html: content.ui.sourceNote }];
11
+ const schemas: Record<string, unknown>[] = [
12
+ { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: content.title, applicationCategory: 'DesignApplication', operatingSystem: 'Any', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' } },
13
+ { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: content.faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) },
14
+ { '@context': 'https://schema.org', '@type': 'HowTo', name: content.title, step: content.howTo.map((item) => ({ '@type': 'HowToStep', name: item.name, text: item.text })) },
15
+ ];
16
+ return { ...content, seo, schemas };
17
+ }
18
+
19
+ export const bookIndexPageBudgetCalculator: BooksToolEntry<BookIndexUI> = {
20
+ id: 'book-index-page-budget-calculator',
21
+ icons: { bg: 'mdi:book-open-page-variant-outline', fg: 'mdi:format-list-numbered' },
22
+ i18n: {
23
+ de: () => import('./i18n/de').then((module) => withLocalizedSchemas(module.content as BookIndexLocaleContent)),
24
+ en: () => import('./i18n/en').then((module) => module.content),
25
+ es: () => import('./i18n/es').then((module) => withLocalizedSchemas(module.content as BookIndexLocaleContent)),
26
+ fr: () => import('./i18n/fr').then((module) => withLocalizedSchemas(module.content as BookIndexLocaleContent)),
27
+ id: () => import('./i18n/id').then((module) => withLocalizedSchemas(module.content as BookIndexLocaleContent)),
28
+ it: () => import('./i18n/it').then((module) => withLocalizedSchemas(module.content as BookIndexLocaleContent)),
29
+ ja: () => import('./i18n/ja').then((module) => withLocalizedSchemas(module.content as BookIndexLocaleContent)),
30
+ ko: () => import('./i18n/ko').then((module) => withLocalizedSchemas(module.content as BookIndexLocaleContent)),
31
+ nl: () => import('./i18n/nl').then((module) => withLocalizedSchemas(module.content as BookIndexLocaleContent)),
32
+ pl: () => import('./i18n/pl').then((module) => withLocalizedSchemas(module.content as BookIndexLocaleContent)),
33
+ pt: () => import('./i18n/pt').then((module) => withLocalizedSchemas(module.content as BookIndexLocaleContent)),
34
+ ru: () => import('./i18n/ru').then((module) => withLocalizedSchemas(module.content as BookIndexLocaleContent)),
35
+ sv: () => import('./i18n/sv').then((module) => withLocalizedSchemas(module.content as BookIndexLocaleContent)),
36
+ tr: () => import('./i18n/tr').then((module) => withLocalizedSchemas(module.content as BookIndexLocaleContent)),
37
+ zh: () => import('./i18n/zh').then((module) => withLocalizedSchemas(module.content as BookIndexLocaleContent)),
38
+ },
39
+ };
@@ -0,0 +1,26 @@
1
+ import type { BookIndexBudget } from './logic';
2
+
3
+ export type BudgetStatus = 'balanced' | 'tight' | 'roomy';
4
+
5
+ export interface BudgetEvaluation {
6
+ status: BudgetStatus;
7
+ gapPages: number;
8
+ targetIsTooDense: boolean;
9
+ }
10
+
11
+ function statusForGap(gapPages: number): BudgetStatus {
12
+ if (gapPages < 0) return 'tight';
13
+ if (gapPages > 0) return 'roomy';
14
+ return 'balanced';
15
+ }
16
+
17
+ export function evaluateBudget(budget: BookIndexBudget): BudgetEvaluation {
18
+ const gapPages = budget.targetIndexPages - budget.estimatedIndexPages;
19
+ const status = statusForGap(gapPages);
20
+
21
+ return {
22
+ status,
23
+ gapPages,
24
+ targetIsTooDense: budget.targetEntriesPerPage > budget.entriesPerPage,
25
+ };
26
+ }
@@ -0,0 +1,35 @@
1
+ import type { BookIndexUI } from '../ui';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+ import { content as base } from './en';
4
+
5
+ const ui: BookIndexUI = {
6
+ ...base.ui,
7
+ wordsLabel: 'Wörter im Manuskript', wordsHint: 'Textkörper vor dem Anhang', trimWidthLabel: 'Satzbreite', trimHeightLabel: 'Satzhöhe', trimHint: 'Endformat der Buchseite', unitLabel: 'Seiteneinheiten', metricButtonLabel: 'Metrisch cm', imperialButtonLabel: 'Imperial Zoll', densityLabel: 'Typografische Dichte', densityHint: 'Steuert Wörter und Indexeinträge pro Seite', airyLabel: 'Locker', standardLabel: 'Standard', compactLabel: 'Kompakt', indexEntriesLabel: 'Erwartete Indexeinträge', indexEntriesHint: 'Themen, Namen, Orte und Querverweise', targetIndexPagesLabel: 'Geplante Indexseiten', targetIndexPagesHint: 'Dein Produktionsbudget für den Index', textPagesLabel: 'Textseiten', estimatedIndexPagesLabel: 'Geschätzter Index', totalPagesLabel: 'Geschätzter Buchumfang', targetTotalPagesLabel: 'Mit geplantem Index', entriesPerPageLabel: 'Einträge pro Zielseite', indexShareLabel: 'Indexanteil am Buch', statusBalanced: 'Ziel entspricht der Schätzung', statusTight: 'Ziel ist zu knapp', statusRoomy: 'Ziel lässt Spielraum', statusBalancedHint: 'Dein Indexbudget entspricht dem Dichtemodell.', statusTightHint: 'Plane mehr Seiten ein oder reduziere Einträge vor dem Satz.', statusRoomyHint: 'Das großzügige Budget kann spätere redaktionelle Ergänzungen aufnehmen.', sceneLabel: 'Das Seitenbudget', sceneBodyLabel: 'Textseiten', sceneIndexLabel: 'Indexseiten', sceneCaption: 'Der geschätzte Buchumfang beträgt', sceneTargetCaption: 'Mit deinem Zielbudget ergibt sich', sourceNote: 'Dies ist ein redaktionelles Planungsmodell. Der endgültige Umfang hängt von Satz, Korrekturen, Indexstil sowie Verlag und Druckerei ab.', summaryLabel: 'Budgetsignal', summaryPagesOver: 'Seiten über dem Ziel', summaryPagesSpare: 'Seiten frei', summaryOnTarget: 'Im Zielbereich',
8
+ };
9
+
10
+ const faq = [
11
+ { question: 'Was soll ich als Indexeinträge eingeben?', answer: 'Gib Themen, Namen, Orte, Untereinträge und Querverweise ein, die im fertigen Index erscheinen sollen.' },
12
+ { question: 'Warum werden Text- und Indexseiten getrennt berechnet?', answer: 'Der Textumfang hängt von Wörtern und Seitengestaltung ab, der Indexumfang von Einträgen und Indexdichte.' },
13
+ { question: 'Was bedeutet ein zu knappes Ziel?', answer: 'Das Ziel würde mehr Einträge pro Seite verlangen, als das gewählte Dichtemodell sinnvoll trägt.' },
14
+ { question: 'Ersetzt das Ergebnis den finalen Indexabzug?', answer: 'Nein. Es ist eine Planungsschätzung. Seitenverweise und Satz müssen am fertigen Korrekturabzug geprüft werden.' },
15
+ ];
16
+
17
+ const howTo = [
18
+ { name: 'Manuskriptumfang eingeben', text: 'Trage die Wörter des Manuskripts ohne Rückentext ein.' },
19
+ { name: 'Seitengestaltung anpassen', text: 'Wähle Endformat und typografische Dichte.' },
20
+ { name: 'Index beschreiben', text: 'Schätze Einträge und reserviere ein Seitenbudget.' },
21
+ { name: 'Vergleich auswerten', text: 'Erhöhe das Budget oder reduziere Einträge, wenn das Ziel zu knapp ist.' },
22
+ ];
23
+
24
+ const seo: ToolLocaleContent<BookIndexUI>['seo'] = [
25
+ { type: 'title', text: 'Indexseiten für ein Buch frühzeitig budgetieren', level: 2 },
26
+ { type: 'paragraph', html: 'Ein Sachindex fügt einem gedruckten Buch echte Rückseiten hinzu. Dieses Werkzeug verbindet Manuskriptumfang, Seitenformat, typografische Dichte, erwartete Einträge und dein Produktionsbudget zu einer ersten Planung.' },
27
+ { type: 'title', text: 'So liest du die Schätzung', level: 2 },
28
+ { type: 'paragraph', html: 'Textseiten entstehen aus Wörtern und der erwarteten Wortdichte. Indexseiten entstehen aus Einträgen und der Eintragsdichte. Vergleiche den geschätzten Index mit dem reservierten Ziel, bevor der Satz beginnt.' },
29
+ { type: 'list', items: ['Zähle erwartete Einträge statt aller Wörter im Manuskript.', 'Halte Format und Dichte nahe am geplanten Buchdesign.', 'Reserviere zusätzliche Seiten, wenn Querverweise oder Untereinträge wachsen.', 'Prüfe den Wert erneut am stabilen Korrekturabzug.'] },
30
+ { type: 'title', text: 'Ein knappes Budget ist ein redaktionelles Signal', level: 2 },
31
+ { type: 'paragraph', html: 'Ein zu knappes Ziel kann zu schwer lesbarer Typografie oder einem überfüllten Index führen. Oft ist es sinnvoller, Verweise zu bereinigen oder mehr Seiten zu reservieren, statt die Dichte immer weiter zu erhöhen.' },
32
+ { type: 'tip', title: 'Die Schätzung ist kein Druckfreigabewert', html: 'Schrift, Satz, Korrekturen und Indexstil verändern den endgültigen Umfang. Bestätige das Ergebnis mit Verlag, Druckerei oder professionellem Indexer.' },
33
+ ];
34
+
35
+ export const content = { ...base, slug: 'buch-index-seitenbudget-rechner', title: 'Budgetrechner für Indexseiten von Büchern', description: 'Schätze die zusätzlichen Indexseiten eines Buches und vergleiche sie mit deinem redaktionellen Seitenbudget.', ui, seo, faq, howTo };
@@ -0,0 +1,113 @@
1
+ import type { ToolLocaleContent } from '../../../types';
2
+ import { BOOK_INDEX_BIBLIOGRAPHY } from '../bibliography';
3
+ import type { BookIndexUI } from '../ui';
4
+ import type { FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
5
+
6
+ const ui: BookIndexUI = {
7
+ wordsLabel: 'Manuscript words',
8
+ wordsHint: 'The body text before back matter',
9
+ trimWidthLabel: 'Trim width',
10
+ trimHeightLabel: 'Trim height',
11
+ trimHint: 'Physical page size',
12
+ unitLabel: 'Page units',
13
+ metricButtonLabel: 'Metric cm',
14
+ imperialButtonLabel: 'Imperial in',
15
+ densityLabel: 'Typography density',
16
+ densityHint: 'Controls body words per page and index entries per page',
17
+ airyLabel: 'Airy',
18
+ standardLabel: 'Standard',
19
+ compactLabel: 'Compact',
20
+ indexEntriesLabel: 'Expected index entries',
21
+ indexEntriesHint: 'Topics, names, places, and cross references',
22
+ targetIndexPagesLabel: 'Target index pages',
23
+ targetIndexPagesHint: 'Your production allowance for the index',
24
+ textPagesLabel: 'Body pages',
25
+ estimatedIndexPagesLabel: 'Estimated index',
26
+ totalPagesLabel: 'Estimated book total',
27
+ targetTotalPagesLabel: 'With target index',
28
+ entriesPerPageLabel: 'Entries per target page',
29
+ indexShareLabel: 'Index share of book',
30
+ statusBalanced: 'Target matches the estimate',
31
+ statusTight: 'Target is too tight',
32
+ statusRoomy: 'Target leaves room',
33
+ statusBalancedHint: 'Your planned index allowance matches the density model.',
34
+ statusTightHint: 'Add pages or reduce entries before typesetting the final index.',
35
+ statusRoomyHint: 'The allowance is generous and may absorb late editorial additions.',
36
+ sceneLabel: 'The page budget',
37
+ sceneBodyLabel: 'body pages',
38
+ sceneIndexLabel: 'index pages',
39
+ sceneCaption: 'The estimated book occupies',
40
+ sceneTargetCaption: 'Your target allowance produces',
41
+ sourceNote: 'This is an editorial planning model. Final pagination depends on typesetting, proof corrections, index style, and the publisher or printer.',
42
+ summaryLabel: 'Budget signal',
43
+ summaryPagesOver: 'pages over target',
44
+ summaryPagesSpare: 'pages spare',
45
+ summaryOnTarget: 'On target',
46
+ };
47
+
48
+ const seo: ToolLocaleContent<BookIndexUI>['seo'] = [
49
+ { type: 'title', text: 'Budget Index Pages Before Your Book Is Typeset', level: 2 },
50
+ { type: 'paragraph', html: 'An analytical index is back matter that adds real pages to a printed book. This calculator turns a manuscript word count, trim size, typography density, expected index entries, and a page allowance into a practical editorial budget. It keeps the body estimate and the index estimate visible as separate parts of the same book.' },
51
+ { type: 'title', text: 'What the Estimate Does', level: 2 },
52
+ { type: 'paragraph', html: 'Body pages are estimated from manuscript words divided by a density-adjusted words-per-page value. Index pages are estimated from the expected number of entries divided by the entries-per-page capacity of the chosen typography profile, then rounded up to a whole page. The book total adds the body and estimated index pages; the target total adds the body to the page allowance you plan to reserve.' },
53
+ { type: 'list', items: ['Use expected entries rather than the number of unique words in the manuscript.', 'Compare the estimated index pages with the target allowance before layout begins.', 'Treat a high entries-per-target-page value as a signal to simplify, split, or expand the index budget.', 'Keep the trim size and density close to the intended interior design when comparing scenarios.'] },
54
+ { type: 'title', text: 'How to Read a Tight Budget', level: 2 },
55
+ { type: 'paragraph', html: 'A tight target means the planned allowance would require more entries per page than the selected profile supports. That can lead to cramped typography, weaker hierarchy, or an index that is difficult to scan. The useful editorial response is not always to squeeze harder: remove passing references, merge duplicate concepts, clarify cross references, or reserve more pages for the reader-facing navigation tool.' },
56
+ { type: 'title', text: 'Plan Early and Recheck at Proof', level: 2 },
57
+ { type: 'paragraph', html: 'Index locators depend on the typeset page sequence, so a late change in the body can move page references and force another index pass. Use this calculator while planning the book, then revisit the budget when the proof has stabilized. The result does not replace publisher instructions or a professional indexer review.' },
58
+ { type: 'tip', title: 'The index is not a word count', html: 'Two books with the same number of words can need very different indexes. Names, subentries, cross references, and the amount of useful material per locator change the final footprint. Count the entries you expect a reader to use, not every mention in the manuscript.' },
59
+ ];
60
+
61
+ const applicationSchema = ({
62
+ '@context': 'https://schema.org',
63
+ '@type': 'SoftwareApplication',
64
+ name: 'Book Index Page Budget Calculator',
65
+ applicationCategory: 'DesignApplication',
66
+ operatingSystem: 'Any',
67
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
68
+ } as unknown) as SoftwareApplication & Record<string, unknown>;
69
+
70
+ const faqSchema = ({
71
+ '@context': 'https://schema.org',
72
+ '@type': 'FAQPage',
73
+ mainEntity: [
74
+ { '@type': 'Question', name: 'What should I enter as index entries?', acceptedAnswer: { '@type': 'Answer', text: 'Enter the topics, names, places, subentries, and cross references you expect to include in the finished index.' } },
75
+ { '@type': 'Question', name: 'Why are body and index pages calculated separately?', acceptedAnswer: { '@type': 'Answer', text: 'The body is driven by words and page design, while the index is driven by entries and index density.' } },
76
+ { '@type': 'Question', name: 'What does a tight target mean?', acceptedAnswer: { '@type': 'Answer', text: 'It means the target pages would require more entries per page than the selected profile is designed to hold.' } },
77
+ { '@type': 'Question', name: 'Can this replace the final index proof?', acceptedAnswer: { '@type': 'Answer', text: 'No. It is a planning estimate. Final locators and pagination must be checked against typeset proofs.' } },
78
+ ],
79
+ } as unknown) as FAQPage & Record<string, unknown>;
80
+
81
+ const howToSchema = ({
82
+ '@context': 'https://schema.org',
83
+ '@type': 'HowTo',
84
+ name: 'Build a preliminary book index page budget',
85
+ step: [
86
+ { '@type': 'HowToStep', name: 'Enter the manuscript size', text: 'Enter the manuscript word count before back matter.' },
87
+ { '@type': 'HowToStep', name: 'Match the page design', text: 'Set the trim dimensions and choose airy, standard, or compact typography.' },
88
+ { '@type': 'HowToStep', name: 'Describe the index', text: 'Enter the expected number of index entries and the page allowance you want to reserve.' },
89
+ { '@type': 'HowToStep', name: 'Act on the comparison', text: 'Use the estimated index pages and status to adjust the editorial plan before typesetting.' },
90
+ ],
91
+ } as unknown) as HowTo & Record<string, unknown>;
92
+
93
+ export const content: ToolLocaleContent<BookIndexUI> = {
94
+ slug: 'book-index-page-budget-calculator',
95
+ title: 'Book Index Page Budget Calculator',
96
+ description: 'Estimate the pages an analytical index will add to your book and compare that footprint with your editorial page allowance.',
97
+ ui,
98
+ seo,
99
+ faq: [
100
+ { question: 'What should I enter as index entries?', answer: 'Enter the topics, names, places, subentries, and cross references you expect to include in the finished index.' },
101
+ { question: 'Why are body and index pages calculated separately?', answer: 'The body is driven by words and page design, while the index is driven by entries and index density.' },
102
+ { question: 'What does a tight target mean?', answer: 'It means the target pages would require more entries per page than the selected profile is designed to hold.' },
103
+ { question: 'Can this replace the final index proof?', answer: 'No. It is a planning estimate. Final locators and pagination must be checked against typeset proofs.' },
104
+ ],
105
+ bibliography: BOOK_INDEX_BIBLIOGRAPHY,
106
+ howTo: [
107
+ { name: 'Enter the manuscript size', text: 'Enter the manuscript word count before back matter.' },
108
+ { name: 'Match the page design', text: 'Set the trim dimensions and choose a typography density.' },
109
+ { name: 'Describe the index', text: 'Enter expected entries and the index page allowance.' },
110
+ { name: 'Act on the comparison', text: 'Adjust the editorial plan when the allowance is tight.' },
111
+ ],
112
+ schemas: [applicationSchema, faqSchema, howToSchema],
113
+ };
@@ -0,0 +1,35 @@
1
+ import { content as base } from './en';
2
+ import type { BookIndexUI } from '../ui';
3
+ import type { ToolLocaleContent } from '../../../types';
4
+
5
+ const ui: BookIndexUI = {
6
+ ...base.ui,
7
+ wordsLabel: 'Palabras del manuscrito', wordsHint: 'Texto principal antes de los anexos', trimWidthLabel: 'Ancho de corte', trimHeightLabel: 'Alto de corte', trimHint: 'Tamaño final de la página', unitLabel: 'Unidades de página', metricButtonLabel: 'Métrico cm', imperialButtonLabel: 'Imperial pulgadas', densityLabel: 'Densidad tipográfica', densityHint: 'Controla palabras y entradas por página', airyLabel: 'Aireada', standardLabel: 'Estándar', compactLabel: 'Compacta', indexEntriesLabel: 'Entradas de índice previstas', indexEntriesHint: 'Temas, nombres, lugares y referencias cruzadas', targetIndexPagesLabel: 'Páginas de índice reservadas', targetIndexPagesHint: 'Tu margen de producción para el índice', textPagesLabel: 'Páginas de texto', estimatedIndexPagesLabel: 'Índice estimado', totalPagesLabel: 'Total estimado del libro', targetTotalPagesLabel: 'Con el índice reservado', entriesPerPageLabel: 'Entradas por página objetivo', indexShareLabel: 'Parte del índice en el libro', statusBalanced: 'El objetivo coincide con la estimación', statusTight: 'El objetivo es demasiado ajustado', statusRoomy: 'El objetivo deja margen', statusBalancedHint: 'La reserva prevista coincide con el modelo de densidad.', statusTightHint: 'Reserva más páginas o reduce entradas antes de maquetar el índice.', statusRoomyHint: 'El margen puede absorber incorporaciones editoriales de última hora.', sceneLabel: 'El presupuesto de páginas', sceneBodyLabel: 'páginas de texto', sceneIndexLabel: 'páginas de índice', sceneCaption: 'El libro estimado ocupa', sceneTargetCaption: 'Con tu reserva el total sería', sourceNote: 'Es un modelo de planificación editorial. La paginación final depende de la maquetación, las correcciones, el estilo del índice y las indicaciones de la editorial o imprenta.', summaryLabel: 'Señal del presupuesto', summaryPagesOver: 'páginas por encima', summaryPagesSpare: 'páginas de margen', summaryOnTarget: 'En objetivo',
8
+ };
9
+
10
+ const faq = [
11
+ { question: '¿Qué debo introducir como entradas de índice?', answer: 'Introduce los temas, nombres, lugares, subentradas y referencias cruzadas que esperas incluir en el índice final.' },
12
+ { question: '¿Por qué se calculan por separado el texto y el índice?', answer: 'El texto depende de las palabras y del diseño de página; el índice depende de sus entradas y de su densidad.' },
13
+ { question: '¿Qué significa un objetivo demasiado ajustado?', answer: 'Significa que necesitarías colocar más entradas por página de las que admite razonablemente el perfil elegido.' },
14
+ { question: '¿Sustituye esto a la prueba final del índice?', answer: 'No. Es una estimación de planificación. Los localizadores y la paginación deben revisarse sobre las pruebas maquetadas.' },
15
+ ];
16
+
17
+ const howTo = [
18
+ { name: 'Introduce el manuscrito', text: 'Indica las palabras del texto principal antes de los anexos.' },
19
+ { name: 'Ajusta el diseño', text: 'Elige el tamaño final y la densidad tipográfica prevista.' },
20
+ { name: 'Describe el índice', text: 'Estima las entradas y reserva un número de páginas.' },
21
+ { name: 'Actúa sobre la comparación', text: 'Amplía la reserva o reduce entradas cuando el objetivo quede corto.' },
22
+ ];
23
+
24
+ const seo: ToolLocaleContent<BookIndexUI>['seo'] = [
25
+ { type: 'title', text: 'Calcula pronto cuántas páginas añadirá el índice de tu libro', level: 2 },
26
+ { type: 'paragraph', html: 'Un índice analítico añade páginas reales a la parte final de un libro impreso. Esta herramienta combina palabras, formato, densidad tipográfica, entradas previstas y reserva editorial en una primera previsión útil.' },
27
+ { type: 'title', text: 'Cómo interpretar el resultado', level: 2 },
28
+ { type: 'paragraph', html: 'Las páginas de texto se estiman a partir de las palabras y la densidad. Las páginas de índice se estiman a partir de las entradas y la capacidad por página. Compara ambas cifras antes de cerrar la maquetación.' },
29
+ { type: 'list', items: ['Cuenta entradas previstas, no todas las palabras del manuscrito.', 'Usa el formato y la densidad que tendrá el interior real.', 'Amplía la reserva si habrá muchos subapartados o referencias cruzadas.', 'Vuelve a calcular cuando la prueba de páginas sea estable.'] },
30
+ { type: 'title', text: 'Un objetivo ajustado requiere una decisión editorial', level: 2 },
31
+ { type: 'paragraph', html: 'Forzar demasiadas entradas en pocas páginas puede reducir la legibilidad. Antes de apretar la tipografía, depura referencias pasajeras, une conceptos duplicados o reserva más espacio para la herramienta de navegación del lector.' },
32
+ { type: 'tip', title: 'No es una cifra lista para imprenta', html: 'La fuente, la composición, las correcciones y el estilo del índice cambian el resultado final. Confirma siempre la cifra con la editorial, la imprenta o un profesional del indexado.' },
33
+ ];
34
+
35
+ export const content = { ...base, slug: 'calculadora-presupuesto-paginas-indice-libro', title: 'Calculadora de presupuesto de páginas de índice', description: 'Estima las páginas que añadirá un índice analítico y compáralas con la reserva editorial de tu libro.', ui, seo, faq, howTo };
@@ -0,0 +1,8 @@
1
+ import { content as base } from './en';
2
+ import type { BookIndexUI } from '../ui';
3
+
4
+ const ui: BookIndexUI = { ...base.ui, wordsLabel: 'Mots du manuscrit', wordsHint: 'Texte principal avant les annexes', trimWidthLabel: 'Largeur de coupe', trimHeightLabel: 'Hauteur de coupe', trimHint: 'Format fini de la page', unitLabel: 'Unites de page', metricButtonLabel: 'Metrique cm', imperialButtonLabel: 'Imperial pouces', densityLabel: 'Densite typographique', densityHint: 'Regle les mots et les entrees par page', airyLabel: 'Aeree', standardLabel: 'Standard', compactLabel: 'Compacte', indexEntriesLabel: 'Entrees d index prevues', indexEntriesHint: 'Themes, noms, lieux et renvois', targetIndexPagesLabel: 'Pages d index prevues', targetIndexPagesHint: 'Votre reserve de production pour l index', textPagesLabel: 'Pages de texte', estimatedIndexPagesLabel: 'Index estime', totalPagesLabel: 'Total estime du livre', targetTotalPagesLabel: 'Avec la reserve d index', entriesPerPageLabel: 'Entrees par page cible', indexShareLabel: 'Part de l index dans le livre', statusBalanced: 'La cible correspond a l estimation', statusTight: 'La cible est trop serree', statusRoomy: 'La cible laisse de la marge', statusBalancedHint: 'La reserve prevue correspond au modele de densite.', statusTightHint: 'Ajoutez des pages ou reduisez les entrees avant la composition.', statusRoomyHint: 'La marge peut absorber des ajouts editoriaux tardifs.', sceneLabel: 'Le budget de pages', sceneBodyLabel: 'pages de texte', sceneIndexLabel: 'pages d index', sceneCaption: 'Le livre estime occupe', sceneTargetCaption: 'Avec votre reserve, le total serait', sourceNote: 'Il s agit d un modele de planification editoriale. La pagination finale depend de la composition, des corrections, du style d index et des consignes de l editeur ou de l imprimeur.', summaryLabel: 'Signal du budget', summaryPagesOver: 'pages au dessus de la cible', summaryPagesSpare: 'pages de marge', summaryOnTarget: 'Dans la cible' };
5
+ const faq = [{ question: 'Que dois je saisir comme entrees d index ?', answer: 'Saisissez les themes, noms, lieux, sous entrees et renvois prevus dans l index final.' }, { question: 'Pourquoi separer les pages de texte et d index ?', answer: 'Le texte depend des mots et de la mise en page, tandis que l index depend de ses entrees et de sa densite.' }, { question: 'Que signifie une cible trop serree ?', answer: 'La reserve demanderait plus d entrees par page que le profil choisi ne peut en accueillir confortablement.' }, { question: 'Le resultat remplace t il l epreuve finale ?', answer: 'Non. C est une estimation de planification. Les renvois et la pagination doivent etre verifies sur l epreuve composee.' }];
6
+ const howTo = [{ name: 'Saisir le manuscrit', text: 'Indiquez le nombre de mots du texte principal.' }, { name: 'Adapter la maquette', text: 'Choisissez le format fini et la densite typographique.' }, { name: 'Decrire l index', text: 'Estimez les entrees et reservez un nombre de pages.' }, { name: 'Agir sur l ecart', text: 'Augmentez la reserve ou reduisez les entrees si la cible est trop serree.' }];
7
+ const seo = [{ type: 'title', text: 'Prevoir le budget de pages d index du livre', level: 2 }, { type: 'paragraph', html: 'Un index analytique ajoute des pages reelles a la fin d un livre imprime. Cet outil combine les mots du manuscrit, le format, la densite typographique, les entrees prevues et la reserve editoriale.' }, { type: 'title', text: 'Lire l estimation', level: 2 }, { type: 'paragraph', html: 'Les pages de texte dependent des mots et de la densite. Les pages d index dependent du nombre d entrees et de la capacite par page. Comparez l estimation a la reserve avant de finaliser la composition.' }, { type: 'list', items: ['Comptez les entrees prevues plutot que chaque mot du manuscrit.', 'Utilisez le format et la densite de l interieur prevu.', 'Reservez davantage si les renvois et sous entrees sont nombreux.', 'Recalculez lorsque l epreuve de pages est stabilisee.'] }, { type: 'title', text: 'Une cible serree appelle une decision editoriale', level: 2 }, { type: 'paragraph', html: 'Entasser trop d entrees peut rendre l index difficile a parcourir. Nettoyez les renvois peu utiles ou augmentez la reserve avant de reduire davantage la taille du texte.' }, { type: 'tip', title: 'Ce n est pas une valeur d impression definitive', html: 'La police, la composition, les corrections et le style d index changent le resultat final. Confirmez le avec l editeur, l imprimeur ou un indexeur professionnel.' }];
8
+ export const content = { ...base, slug: 'calculateur-budget-pages-index-livre', title: 'Calculateur du budget de pages indexées', description: 'Estimez les pages ajoutées par un index analytique et comparez-les à la réserve éditoriale de votre livre.', ui, seo, faq, howTo };