@jjlmoya/utils-books 1.13.0 → 1.14.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 (34) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +4 -1
  4. package/src/tests/locale_completeness.test.ts +1 -1
  5. package/src/tests/tool_validation.test.ts +1 -1
  6. package/src/tool/book-royalty-break-even-calculator/bibliography.astro +6 -0
  7. package/src/tool/book-royalty-break-even-calculator/bibliography.ts +11 -0
  8. package/src/tool/book-royalty-break-even-calculator/book-royalty-break-even-calculator.css +449 -0
  9. package/src/tool/book-royalty-break-even-calculator/component.astro +66 -0
  10. package/src/tool/book-royalty-break-even-calculator/controller.ts +23 -0
  11. package/src/tool/book-royalty-break-even-calculator/dom-views.ts +51 -0
  12. package/src/tool/book-royalty-break-even-calculator/entry.ts +39 -0
  13. package/src/tool/book-royalty-break-even-calculator/i18n/de.ts +2 -0
  14. package/src/tool/book-royalty-break-even-calculator/i18n/en.ts +52 -0
  15. package/src/tool/book-royalty-break-even-calculator/i18n/es.ts +2 -0
  16. package/src/tool/book-royalty-break-even-calculator/i18n/fr.ts +2 -0
  17. package/src/tool/book-royalty-break-even-calculator/i18n/id.ts +2 -0
  18. package/src/tool/book-royalty-break-even-calculator/i18n/it.ts +2 -0
  19. package/src/tool/book-royalty-break-even-calculator/i18n/ja.ts +2 -0
  20. package/src/tool/book-royalty-break-even-calculator/i18n/ko.ts +2 -0
  21. package/src/tool/book-royalty-break-even-calculator/i18n/locales.ts +73 -0
  22. package/src/tool/book-royalty-break-even-calculator/i18n/nl.ts +2 -0
  23. package/src/tool/book-royalty-break-even-calculator/i18n/pl.ts +2 -0
  24. package/src/tool/book-royalty-break-even-calculator/i18n/pt.ts +2 -0
  25. package/src/tool/book-royalty-break-even-calculator/i18n/ru.ts +2 -0
  26. package/src/tool/book-royalty-break-even-calculator/i18n/sv.ts +2 -0
  27. package/src/tool/book-royalty-break-even-calculator/i18n/tr.ts +2 -0
  28. package/src/tool/book-royalty-break-even-calculator/i18n/zh.ts +2 -0
  29. package/src/tool/book-royalty-break-even-calculator/index.ts +11 -0
  30. package/src/tool/book-royalty-break-even-calculator/logic.test.ts +39 -0
  31. package/src/tool/book-royalty-break-even-calculator/logic.ts +91 -0
  32. package/src/tool/book-royalty-break-even-calculator/seo.astro +12 -0
  33. package/src/tool/book-royalty-break-even-calculator/ui.ts +46 -0
  34. package/src/tools.ts +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-books",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -1,11 +1,12 @@
1
1
  import { bookPaginationAndSpineCalculator } from '../tool/book-pagination-and-spine-calculator/entry';
2
2
  import { bookInteriorMarginAndGutterPlanner } from '../tool/book-interior-margin-and-gutter-planner/entry';
3
3
  import { bookIndexPageBudgetCalculator } from '../tool/book-index-page-budget-calculator/entry';
4
+ import { bookRoyaltyBreakEvenCalculator } from '../tool/book-royalty-break-even-calculator/entry';
4
5
  import type { CategoryLocaleContent, KnownLocale } from '../types';
5
6
 
6
7
  export const booksCategory = {
7
8
  icon: 'mdi:book-open-page-variant-outline',
8
- tools: [bookPaginationAndSpineCalculator, bookInteriorMarginAndGutterPlanner, bookIndexPageBudgetCalculator],
9
+ tools: [bookPaginationAndSpineCalculator, bookInteriorMarginAndGutterPlanner, bookIndexPageBudgetCalculator, bookRoyaltyBreakEvenCalculator],
9
10
  i18n: {
10
11
  de: () => import('./i18n/de').then((module) => module.content),
11
12
  en: () => import('./i18n/en').then((module) => module.content),
package/src/entries.ts CHANGED
@@ -4,10 +4,13 @@ export { bookReadingTimeDeadlinePlanner } from './tool/book-reading-time-deadlin
4
4
  export type { BookReadingUI, BookReadingLocaleContent } from './tool/book-reading-time-deadline-planner/entry';
5
5
  export { bookIndexPageBudgetCalculator } from './tool/book-index-page-budget-calculator/entry';
6
6
  export type { BookIndexUI, BookIndexLocaleContent } from './tool/book-index-page-budget-calculator/entry';
7
+ export { bookRoyaltyBreakEvenCalculator } from './tool/book-royalty-break-even-calculator/entry';
8
+ export type { BookRoyaltyUI, BookRoyaltyLocaleContent } from './tool/book-royalty-break-even-calculator/entry';
7
9
 
8
10
  import { bookPaginationAndSpineCalculator } from './tool/book-pagination-and-spine-calculator/entry';
9
11
  import { bookInteriorMarginAndGutterPlanner } from './tool/book-interior-margin-and-gutter-planner/entry';
10
12
  import { bookReadingTimeDeadlinePlanner } from './tool/book-reading-time-deadline-planner/entry';
11
13
  import { bookIndexPageBudgetCalculator } from './tool/book-index-page-budget-calculator/entry';
14
+ import { bookRoyaltyBreakEvenCalculator } from './tool/book-royalty-break-even-calculator/entry';
12
15
 
13
- export const ALL_ENTRIES = [bookPaginationAndSpineCalculator, bookInteriorMarginAndGutterPlanner, bookReadingTimeDeadlinePlanner, bookIndexPageBudgetCalculator];
16
+ export const ALL_ENTRIES = [bookPaginationAndSpineCalculator, bookInteriorMarginAndGutterPlanner, bookReadingTimeDeadlinePlanner, bookIndexPageBudgetCalculator, bookRoyaltyBreakEvenCalculator];
@@ -18,6 +18,6 @@ describe('Locale Completeness Validation', () => {
18
18
  });
19
19
 
20
20
  it('all tools registered', () => {
21
- expect(ALL_TOOLS.length).toBe(4);
21
+ expect(ALL_TOOLS.length).toBe(5);
22
22
  });
23
23
  });
@@ -5,7 +5,7 @@ import { booksCategory } from '../data';
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
7
  it('should have tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(4);
8
+ expect(ALL_TOOLS.length).toBe(5);
9
9
  });
10
10
 
11
11
  it('booksCategory should be defined', () => {
@@ -0,0 +1,6 @@
1
+ ---
2
+ import { Bibliography } from '@jjlmoya/utils-shared';
3
+ import { BOOK_ROYALTY_BIBLIOGRAPHY } from './bibliography';
4
+ ---
5
+
6
+ <Bibliography links={BOOK_ROYALTY_BIBLIOGRAPHY} />
@@ -0,0 +1,11 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const BOOK_ROYALTY_BIBLIOGRAPHY: BibliographyEntry[] = [
4
+ { name: 'Authors Guild, Understanding Book Contracts', url: 'https://authorsguild.org/resources/understanding-book-contracts/' },
5
+ { name: 'Society of Authors, Contracts and Royalties', url: 'https://societyofauthors.org/Advice/Contracts-and-legal/Contracts' },
6
+ ];
7
+
8
+ export const BOOK_ROYALTY_BIBLIOGRAPHY_TRACEABILITY = [
9
+ { source: 'Authors Guild', region: 'United States', language: 'English', supports: 'Royalty terms and contract deductions should be read from the author agreement.' },
10
+ { source: 'Society of Authors', region: 'United Kingdom', language: 'English', supports: 'Authors should understand royalty calculations and contract language before relying on projections.' },
11
+ ] as const;
@@ -0,0 +1,449 @@
1
+ .royalty-tool {
2
+ --ink: #17354a;
3
+ --muted: #5a6d70;
4
+ --paper: #fffaf0;
5
+ --teal: #3c8584;
6
+ --coral: #e56c4c;
7
+ --gold: #d49a2a;
8
+ --white: #fff;
9
+ --positive: #286f60;
10
+ --negative: #b34934;
11
+
12
+ color: var(--ink);
13
+ }
14
+
15
+ .royalty-workbench {
16
+ display: grid;
17
+ gap: 1.1rem;
18
+ max-width: 1120px;
19
+ margin: 0 auto;
20
+ }
21
+
22
+ .royalty-summary,
23
+ .input-panel,
24
+ .scene-panel,
25
+ .scenario-panel {
26
+ border: 1px solid rgba(23, 53, 74, 0.18);
27
+ border-radius: 18px;
28
+ background: var(--paper);
29
+ box-shadow: 0 8px 20px rgba(23, 53, 74, 0.06);
30
+ }
31
+
32
+ .royalty-summary {
33
+ padding: clamp(1rem, 3vw, 1.7rem);
34
+ background: linear-gradient(135deg, #fffaf0, #f8edd9);
35
+ }
36
+
37
+ .summary-heading,
38
+ .panel-header,
39
+ .scene-title {
40
+ display: flex;
41
+ align-items: center;
42
+ justify-content: space-between;
43
+ gap: 1rem;
44
+ }
45
+
46
+ .eyebrow {
47
+ display: block;
48
+ color: var(--teal);
49
+ font-size: 0.72rem;
50
+ font-weight: 700;
51
+ letter-spacing: 0.13em;
52
+ text-transform: uppercase;
53
+ }
54
+
55
+ .summary-heading h2 {
56
+ margin: 0.3rem 0 0;
57
+ font-size: clamp(1.5rem, 4vw, 2.5rem);
58
+ line-height: 1.05;
59
+ }
60
+
61
+ .status-chip {
62
+ display: inline-flex;
63
+ align-items: center;
64
+ gap: 0.5rem;
65
+ padding: 0.55rem 0.75rem;
66
+ border-radius: 999px;
67
+ background: rgba(60, 133, 132, 0.12);
68
+ font-size: 0.85rem;
69
+ }
70
+
71
+ .status-dot {
72
+ width: 0.65rem;
73
+ height: 0.65rem;
74
+ border-radius: 50%;
75
+ background: var(--teal);
76
+ }
77
+
78
+ .summary-grid {
79
+ display: grid;
80
+ grid-template-columns: 1.2fr 1fr 1fr;
81
+ gap: 0.7rem;
82
+ margin-top: 1.25rem;
83
+ }
84
+
85
+ .summary-stat {
86
+ display: grid;
87
+ gap: 0.25rem;
88
+ padding: 0.85rem;
89
+ border-top: 3px solid var(--gold);
90
+ background: rgba(255, 255, 255, 0.5);
91
+ }
92
+
93
+ .summary-stat-primary {
94
+ border-color: var(--coral);
95
+ }
96
+
97
+ .summary-stat span,
98
+ .summary-stat small,
99
+ .scene-reading span {
100
+ color: var(--muted);
101
+ font-size: 0.78rem;
102
+ }
103
+
104
+ .summary-stat strong {
105
+ font-size: clamp(1.25rem, 3vw, 1.75rem);
106
+ }
107
+
108
+ .status-explanation,
109
+ .source-note {
110
+ margin: 1rem 0 0;
111
+ color: var(--muted);
112
+ font-size: 0.9rem;
113
+ line-height: 1.5;
114
+ }
115
+
116
+ .royalty-layout {
117
+ display: grid;
118
+ grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
119
+ gap: 1.1rem;
120
+ }
121
+
122
+ .input-panel,
123
+ .scene-panel,
124
+ .scenario-panel {
125
+ padding: clamp(1rem, 2.5vw, 1.45rem);
126
+ }
127
+
128
+ .preset-row,
129
+ .mode-switch {
130
+ display: flex;
131
+ gap: 0.4rem;
132
+ flex-wrap: wrap;
133
+ }
134
+
135
+ button,
136
+ input,
137
+ select {
138
+ font: inherit;
139
+ }
140
+
141
+ button {
142
+ cursor: pointer;
143
+ border: 1px solid rgba(23, 53, 74, 0.25);
144
+ border-radius: 9px;
145
+ padding: 0.55rem 0.75rem;
146
+ color: var(--ink);
147
+ background: #fffdf6;
148
+ }
149
+
150
+ button:hover,
151
+ button:focus-visible {
152
+ border-color: var(--teal);
153
+ }
154
+
155
+ button:focus-visible,
156
+ input:focus-visible,
157
+ select:focus-visible {
158
+ outline: 3px solid rgba(229, 108, 76, 0.35);
159
+ outline-offset: 2px;
160
+ }
161
+
162
+ button[aria-pressed="true"] {
163
+ color: var(--white);
164
+ border-color: var(--teal);
165
+ background: var(--teal);
166
+ }
167
+
168
+ .fields-grid {
169
+ display: grid;
170
+ grid-template-columns: repeat(2, minmax(0, 1fr));
171
+ gap: 1rem 0.8rem;
172
+ margin-top: 1.2rem;
173
+ }
174
+
175
+ .field {
176
+ display: grid;
177
+ align-content: start;
178
+ gap: 0.35rem;
179
+ }
180
+
181
+ .field-wide {
182
+ grid-column: span 2;
183
+ }
184
+
185
+ .field label,
186
+ fieldset legend {
187
+ font-size: 0.88rem;
188
+ font-weight: 700;
189
+ }
190
+
191
+ .field input,
192
+ .field select {
193
+ box-sizing: border-box;
194
+ width: 100%;
195
+ border: 1px solid rgba(23, 53, 74, 0.26);
196
+ border-radius: 8px;
197
+ padding: 0.65rem 0.7rem;
198
+ color: var(--ink);
199
+ background: #fffdf8;
200
+ }
201
+
202
+ .field span {
203
+ color: var(--muted);
204
+ font-size: 0.75rem;
205
+ line-height: 1.3;
206
+ }
207
+
208
+ fieldset {
209
+ min-width: 0;
210
+ border: 0;
211
+ padding: 0;
212
+ margin: 0;
213
+ }
214
+
215
+ fieldset legend {
216
+ padding: 0;
217
+ margin-bottom: 0.4rem;
218
+ }
219
+
220
+ .mode-switch button {
221
+ flex: 1;
222
+ }
223
+
224
+ .scene-panel {
225
+ overflow: hidden;
226
+ background: #eef4e9;
227
+ }
228
+
229
+ .scene-title {
230
+ align-items: start;
231
+ flex-direction: column;
232
+ }
233
+
234
+ .scene-title strong {
235
+ font-size: 1.15rem;
236
+ }
237
+
238
+ .balance-scene {
239
+ position: relative;
240
+ padding: 3rem 0.5rem 1rem;
241
+ }
242
+
243
+ .scene-axis {
244
+ display: flex;
245
+ justify-content: space-between;
246
+ color: var(--muted);
247
+ font-size: 0.75rem;
248
+ }
249
+
250
+ .scene-track {
251
+ position: relative;
252
+ height: 10rem;
253
+ margin: 0.25rem 0 1rem;
254
+ border-bottom: 2px solid var(--ink);
255
+ background: repeating-linear-gradient(90deg, transparent 0, transparent calc(10% - 1px), rgba(23, 53, 74, 0.12) 10%);
256
+ }
257
+
258
+ .scene-track::before {
259
+ position: absolute;
260
+ right: 0;
261
+ bottom: 0;
262
+ left: 0;
263
+ height: 62%;
264
+ background: repeating-linear-gradient(135deg, transparent 0 10px, rgba(60, 133, 132, 0.08) 10px 12px);
265
+ content: '';
266
+ clip-path: polygon(0 100%, 100% 0, 100% 100%);
267
+ }
268
+
269
+ .scene-fill {
270
+ position: absolute;
271
+ bottom: 0;
272
+ left: 0;
273
+ width: var(--fill-width, 0%);
274
+ height: 8px;
275
+ border-radius: 99px;
276
+ background: var(--coral);
277
+ }
278
+
279
+ .scene-marker {
280
+ position: absolute;
281
+ bottom: -8px;
282
+ left: var(--marker-position, 0%);
283
+ z-index: 1;
284
+ width: 16px;
285
+ height: 16px;
286
+ border: 3px solid var(--paper);
287
+ border-radius: 50%;
288
+ background: var(--teal);
289
+ box-shadow: 0 0 0 2px var(--teal);
290
+ transform: translateX(-50%);
291
+ }
292
+
293
+ .marker-target {
294
+ background: var(--coral);
295
+ box-shadow: 0 0 0 2px var(--coral);
296
+ }
297
+
298
+ .scene-legend {
299
+ display: flex;
300
+ gap: 1rem;
301
+ flex-wrap: wrap;
302
+ color: var(--muted);
303
+ font-size: 0.78rem;
304
+ }
305
+
306
+ .legend-dot {
307
+ display: inline-block;
308
+ width: 0.65rem;
309
+ height: 0.65rem;
310
+ margin-right: 0.25rem;
311
+ border-radius: 50%;
312
+ background: var(--teal);
313
+ }
314
+
315
+ .legend-margin {
316
+ background: var(--coral);
317
+ }
318
+
319
+ .scene-reading {
320
+ display: grid;
321
+ grid-template-columns: auto 1fr auto 1fr;
322
+ gap: 0.3rem 0.7rem;
323
+ align-items: baseline;
324
+ padding-top: 1rem;
325
+ border-top: 1px solid rgba(23, 53, 74, 0.16);
326
+ }
327
+
328
+ .scene-reading strong {
329
+ font-size: 1.1rem;
330
+ }
331
+
332
+ .scenario-panel {
333
+ background: var(--paper);
334
+ }
335
+
336
+ .scenario-note {
337
+ color: var(--muted);
338
+ font-size: 0.82rem;
339
+ }
340
+
341
+ .scenario-table-wrap {
342
+ overflow-x: auto;
343
+ margin-top: 0.85rem;
344
+ }
345
+
346
+ table {
347
+ width: 100%;
348
+ border-collapse: collapse;
349
+ font-size: 0.88rem;
350
+ }
351
+
352
+ th,
353
+ td {
354
+ padding: 0.7rem 0.45rem;
355
+ border-bottom: 1px solid rgba(23, 53, 74, 0.14);
356
+ text-align: left;
357
+ white-space: nowrap;
358
+ }
359
+
360
+ thead th {
361
+ color: var(--muted);
362
+ font-size: 0.74rem;
363
+ letter-spacing: 0.04em;
364
+ text-transform: uppercase;
365
+ }
366
+
367
+ .positive {
368
+ color: var(--positive);
369
+ font-weight: 700;
370
+ }
371
+
372
+ .negative {
373
+ color: var(--negative);
374
+ font-weight: 700;
375
+ }
376
+
377
+ .visually-hidden {
378
+ position: absolute;
379
+ width: 1px;
380
+ height: 1px;
381
+ padding: 0;
382
+ margin: -1px;
383
+ overflow: hidden;
384
+ clip-path: inset(50%);
385
+ white-space: nowrap;
386
+ border: 0;
387
+ }
388
+
389
+ @media (max-width: 760px) {
390
+ .royalty-layout,
391
+ .summary-grid {
392
+ grid-template-columns: 1fr;
393
+ }
394
+
395
+ .summary-heading {
396
+ align-items: start;
397
+ flex-direction: column;
398
+ }
399
+
400
+ .fields-grid {
401
+ grid-template-columns: 1fr;
402
+ }
403
+
404
+ .field-wide {
405
+ grid-column: auto;
406
+ }
407
+
408
+ .scene-reading {
409
+ grid-template-columns: auto 1fr;
410
+ }
411
+ }
412
+
413
+ @media (prefers-color-scheme: dark) {
414
+ .royalty-tool {
415
+ --ink: #e9e1d0;
416
+ --muted: #b5c2bc;
417
+ --paper: #1c2a2e;
418
+ --white: #fff;
419
+ }
420
+
421
+ .royalty-summary,
422
+ .input-panel,
423
+ .scene-panel,
424
+ .scenario-panel {
425
+ border-color: rgba(233, 225, 208, 0.2);
426
+ background: #203236;
427
+ }
428
+
429
+ .royalty-summary {
430
+ background: linear-gradient(135deg, #203236, #29363a);
431
+ }
432
+
433
+ button,
434
+ .field input,
435
+ .field select {
436
+ color: var(--ink);
437
+ border-color: rgba(233, 225, 208, 0.3);
438
+ background: #273b3d;
439
+ }
440
+
441
+ button[aria-pressed="true"] {
442
+ color: var(--white);
443
+ background: var(--teal);
444
+ }
445
+
446
+ .scene-panel {
447
+ background: #263d3b;
448
+ }
449
+ }
@@ -0,0 +1,66 @@
1
+ ---
2
+ import { calculateBookRoyaltyBreakEven, DEFAULT_ROYALTY_INPUTS } from './logic';
3
+ import type { BookRoyaltyUI } from './ui';
4
+
5
+ interface Props { ui: BookRoyaltyUI }
6
+ const { ui } = Astro.props as Props;
7
+ const initialResult = calculateBookRoyaltyBreakEven(DEFAULT_ROYALTY_INPUTS);
8
+ const initialCurrency = new Intl.NumberFormat('en-US', { style: 'currency', currency: DEFAULT_ROYALTY_INPUTS.currency, maximumFractionDigits: 2 }).format;
9
+ const money = (value: number | null): string => value === null ? ui.unreachableLabel : initialCurrency(value);
10
+ ---
11
+
12
+ <div class="royalty-tool" data-book-royalty-tool data-ui={JSON.stringify(ui)} data-status={initialResult.marginPerCopy > 0 ? 'profitable' : 'unprofitable'}>
13
+ <form class="royalty-workbench" data-form>
14
+ <section class="royalty-summary" aria-label={ui.breakEvenLabel}>
15
+ <div class="summary-heading">
16
+ <div>
17
+ <span class="eyebrow">{ui.breakEvenLabel}</span>
18
+ <h2 data-output="headline">{initialResult.breakEvenCopies?.toLocaleString('en-US') ?? ui.unreachableLabel} {ui.copiesLabel}</h2>
19
+ </div>
20
+ <div class="status-chip" data-status-line><span class="status-dot"></span><strong data-output="status">{ui.statusProfitable}</strong></div>
21
+ </div>
22
+ <div class="summary-grid">
23
+ <div class="summary-stat summary-stat-primary"><span>{ui.breakEvenHint}</span><strong data-output="break-even">{initialResult.breakEvenCopies?.toLocaleString('en-US') ?? ui.unreachableLabel}</strong><small>{ui.copiesLabel}</small></div>
24
+ <div class="summary-stat"><span>{ui.targetLabel}</span><strong data-output="target">{initialResult.targetCopies?.toLocaleString('en-US') ?? ui.unreachableLabel}</strong><small>{ui.targetHint}</small></div>
25
+ <div class="summary-stat"><span>{ui.marginLabel}</span><strong data-output="margin">{money(initialResult.marginPerCopy)}</strong><small>{ui.marginHint}</small></div>
26
+ </div>
27
+ <p class="status-explanation" data-output="status-hint">{ui.statusProfitableHint}</p>
28
+ </section>
29
+
30
+ <div class="royalty-layout">
31
+ <section class="input-panel" aria-label={ui.presetLabel}>
32
+ <div class="panel-header"><span class="eyebrow">{ui.presetLabel}</span><div class="preset-row"><button type="button" data-preset="ebook">{ui.ebookPreset}</button><button type="button" data-preset="paperback">{ui.paperbackPreset}</button></div></div>
33
+ <div class="fields-grid">
34
+ <div class="field field-wide"><label for="fixed-costs">{ui.fixedCostsLabel}</label><input id="fixed-costs" data-input="fixedCosts" type="number" min="0" max="1000000" step="10" value={DEFAULT_ROYALTY_INPUTS.fixedCosts} inputmode="decimal" /><span>{ui.fixedCostsHint}</span></div>
35
+ <div class="field"><label for="sale-price">{ui.salePriceLabel}</label><input id="sale-price" data-input="salePrice" type="number" min="0" max="10000" step="0.01" value={DEFAULT_ROYALTY_INPUTS.salePrice} inputmode="decimal" /><span>{ui.salePriceHint}</span></div>
36
+ <div class="field"><label for="variable-cost">{ui.variableCostLabel}</label><input id="variable-cost" data-input="variableCost" type="number" min="0" max="10000" step="0.01" value={DEFAULT_ROYALTY_INPUTS.variableCost} inputmode="decimal" /><span>{ui.variableCostHint}</span></div>
37
+ <div class="field field-wide"><fieldset><legend>{ui.royaltyModeLabel}</legend><div class="mode-switch"><button type="button" data-mode="percent" aria-pressed="true">{ui.royaltyPercentLabel}</button><button type="button" data-mode="per-copy" aria-pressed="false">{ui.royaltyAmountLabel}</button></div></fieldset><label for="royalty-value" class="visually-hidden">{ui.royaltyModeLabel}</label><input id="royalty-value" data-input="royaltyValue" type="number" min="0" max="10000" step="0.01" value={DEFAULT_ROYALTY_INPUTS.royaltyValue} inputmode="decimal" /><span>{ui.royaltyHint}</span></div>
38
+ <div class="field"><label for="target-profit">{ui.targetProfitLabel}</label><input id="target-profit" data-input="targetProfit" type="number" min="0" max="1000000" step="10" value={DEFAULT_ROYALTY_INPUTS.targetProfit} inputmode="decimal" /><span>{ui.targetProfitHint}</span></div>
39
+ <div class="field"><label for="currency">{ui.currencyLabel}</label><select id="currency" data-input="currency"><option value="USD">USD</option><option value="EUR">EUR</option><option value="GBP">GBP</option></select></div>
40
+ </div>
41
+ </section>
42
+
43
+ <section class="scene-panel" aria-label={ui.sceneLabel} data-scene>
44
+ <div class="scene-title"><span class="eyebrow">{ui.sceneLabel}</span><strong data-output="scene-caption">{ui.sceneCaption}</strong></div>
45
+ <div class="balance-scene">
46
+ <div class="scene-axis"><span class="axis-zero">0</span><span class="axis-break" data-output="scene-break-label">{initialResult.breakEvenCopies?.toLocaleString('en-US') ?? '∞'}</span><span class="axis-target" data-output="scene-target-label">{initialResult.targetCopies?.toLocaleString('en-US') ?? '∞'}</span></div>
47
+ <div class="scene-track"><span class="scene-fill" data-scene-fill></span><span class="scene-marker marker-break" data-scene-marker="break"></span><span class="scene-marker marker-target" data-scene-marker="target"></span></div>
48
+ <div class="scene-legend"><span><i class="legend-dot legend-fixed"></i>{ui.fixedCostsOutputLabel}</span><span><i class="legend-dot legend-margin"></i>{ui.marginLabel}</span></div>
49
+ </div>
50
+ <div class="scene-reading"><strong data-output="revenue-break-even">{money(initialResult.revenueAtBreakEven)}</strong><span>{ui.revenueAtBreakEvenLabel}</span><strong data-output="royalty-per-copy">{money(initialResult.royaltyPerCopy)}</strong><span>{ui.royaltyPerCopyLabel}</span></div>
51
+ </section>
52
+ </div>
53
+
54
+ <section class="scenario-panel" aria-label={ui.scenarioLabel}>
55
+ <div class="panel-header"><span class="eyebrow">{ui.scenarioLabel}</span><span class="scenario-note">{ui.copyNote}</span></div>
56
+ <div class="scenario-table-wrap"><table><thead><tr><th scope="col">{ui.scenarioCopies}</th><th scope="col">{ui.scenarioRevenue}</th><th scope="col">{ui.scenarioProfit}</th><th scope="col">{ui.scenarioBreakEven}</th></tr></thead><tbody data-scenarios>{initialResult.milestones.map((milestone) => <tr><th scope="row">{milestone.copies.toLocaleString('en-US')} {ui.copiesLabel}</th><td>{initialCurrency(milestone.revenue)}</td><td class={milestone.profit >= 0 ? 'positive' : 'negative'}>{initialCurrency(milestone.profit)}</td><td>{initialResult.breakEvenCopies !== null && milestone.copies >= initialResult.breakEvenCopies ? ui.yesLabel : ui.noLabel}</td></tr>)}</tbody></table></div>
57
+ </section>
58
+ <p class="source-note">{ui.sourceNote}</p>
59
+ </form>
60
+ </div>
61
+
62
+ <script>
63
+ import { initBookRoyaltyTool } from './controller';
64
+ const root = document.querySelector<HTMLElement>('[data-book-royalty-tool]');
65
+ if (root) initBookRoyaltyTool(root);
66
+ </script>
@@ -0,0 +1,23 @@
1
+ import { calculateBookRoyaltyBreakEven, clampNumber, DEFAULT_ROYALTY_INPUTS, EBOOK_PRESET, PAPERBACK_PRESET, type BookRoyaltyInputs } from './logic';
2
+ import { renderBookRoyalty } from './dom-views';
3
+ import type { BookRoyaltyUI } from './ui';
4
+
5
+ const LIMITS = { fixedCosts: [0, 1000000], salePrice: [0, 10000], royaltyValue: [0, 10000], variableCost: [0, 10000], targetProfit: [0, 1000000] } as const;
6
+
7
+ function getInput(root: HTMLElement, name: string): HTMLInputElement | HTMLSelectElement | null { return root.querySelector<HTMLInputElement | HTMLSelectElement>(`[data-input="${name}"]`); }
8
+ function readNumber(root: HTMLElement, name: keyof typeof LIMITS, fallback: number): number { const field = getInput(root, name); const value = field ? Number(field.value) : fallback; const [min, max] = LIMITS[name]; return clampNumber(value, min, max, fallback); }
9
+ function readInputs(root: HTMLElement, previous: BookRoyaltyInputs): BookRoyaltyInputs {
10
+ const currency = getInput(root, 'currency')?.value;
11
+ return { fixedCosts: readNumber(root, 'fixedCosts', previous.fixedCosts), salePrice: readNumber(root, 'salePrice', previous.salePrice), royaltyMode: previous.royaltyMode, royaltyValue: readNumber(root, 'royaltyValue', previous.royaltyValue), variableCost: readNumber(root, 'variableCost', previous.variableCost), targetProfit: readNumber(root, 'targetProfit', previous.targetProfit), currency: currency === 'EUR' || currency === 'GBP' ? currency : 'USD' };
12
+ }
13
+ function writeInputs(root: HTMLElement, inputs: BookRoyaltyInputs): void { (Object.keys(inputs) as Array<keyof BookRoyaltyInputs>).forEach((key) => { const field = getInput(root, key); if (field && key !== 'royaltyMode') field.value = String(inputs[key]); }); }
14
+ function render(root: HTMLElement, state: { inputs: BookRoyaltyInputs }, ui: BookRoyaltyUI): void { renderBookRoyalty({ root, result: calculateBookRoyaltyBreakEven(state.inputs), ui, currency: state.inputs.currency }); }
15
+
16
+ export function initBookRoyaltyTool(root: HTMLElement): void {
17
+ const ui = JSON.parse(root.dataset.ui ?? '{}') as BookRoyaltyUI;
18
+ const state = { inputs: DEFAULT_ROYALTY_INPUTS };
19
+ root.querySelectorAll<HTMLInputElement | HTMLSelectElement>('[data-input]').forEach((field) => field.addEventListener('input', () => { state.inputs = readInputs(root, state.inputs); render(root, state, ui); }));
20
+ root.querySelectorAll<HTMLButtonElement>('[data-mode]').forEach((button) => button.addEventListener('click', () => { state.inputs = { ...readInputs(root, state.inputs), royaltyMode: button.dataset.mode === 'per-copy' ? 'per-copy' : 'percent' }; root.querySelectorAll<HTMLButtonElement>('[data-mode]').forEach((item) => item.setAttribute('aria-pressed', String(item === button))); render(root, state, ui); }));
21
+ root.querySelectorAll<HTMLButtonElement>('[data-preset]').forEach((button) => button.addEventListener('click', () => { const preset = button.dataset.preset === 'paperback' ? PAPERBACK_PRESET : EBOOK_PRESET; state.inputs = { ...preset, royaltyMode: state.inputs.royaltyMode }; writeInputs(root, state.inputs); render(root, state, ui); }));
22
+ render(root, state, ui);
23
+ }
@@ -0,0 +1,51 @@
1
+ import type { RoyaltyBreakEvenResult } from './logic';
2
+ import type { BookRoyaltyUI } from './ui';
3
+
4
+ interface RenderArgs { root: HTMLElement; result: RoyaltyBreakEvenResult; ui: BookRoyaltyUI; currency: string }
5
+
6
+ function number(value: number): string { return new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }).format(value); }
7
+ function money(value: number | null, currency: string, unreachableLabel: string): string { return value === null ? unreachableLabel : new Intl.NumberFormat(undefined, { style: 'currency', currency, maximumFractionDigits: 2 }).format(value); }
8
+ function setOutput(root: HTMLElement, name: string, value: string): void { root.querySelector<HTMLElement>(`[data-output="${name}"]`)?.replaceChildren(document.createTextNode(value)); }
9
+ function copyCount(value: number | null, unreachableLabel: string): string { return value === null ? unreachableLabel : number(value); }
10
+ function statusForMargin(margin: number): 'profitable' | 'unprofitable' | 'zero' { if (margin > 0) return 'profitable'; if (margin < 0) return 'unprofitable'; return 'zero'; }
11
+ function statusCopy(status: ReturnType<typeof statusForMargin>, ui: BookRoyaltyUI): { text: string; hint: string } { if (status === 'profitable') return { text: ui.statusProfitable, hint: ui.statusProfitableHint }; if (status === 'zero') return { text: ui.statusZero, hint: ui.statusZeroHint }; return { text: ui.statusUnprofitable, hint: ui.statusUnprofitableHint }; }
12
+
13
+ function renderScenarios(root: HTMLElement, result: RoyaltyBreakEvenResult, ui: BookRoyaltyUI, currency: string): void {
14
+ const body = root.querySelector<HTMLElement>('[data-scenarios]');
15
+ if (!body) return;
16
+ body.replaceChildren(...result.milestones.map((milestone) => {
17
+ const row = document.createElement('tr');
18
+ const breakEven = result.breakEvenCopies !== null && milestone.copies >= result.breakEvenCopies;
19
+ row.innerHTML = `<th scope="row">${number(milestone.copies)} ${ui.copiesLabel}</th><td>${money(milestone.revenue, currency, ui.unreachableLabel)}</td><td class="${milestone.profit >= 0 ? 'positive' : 'negative'}">${money(milestone.profit, currency, ui.unreachableLabel)}</td><td>${breakEven ? ui.yesLabel : ui.noLabel}</td>`;
20
+ return row;
21
+ }));
22
+ }
23
+
24
+ function renderScene(root: HTMLElement, result: RoyaltyBreakEvenResult): void {
25
+ const max = Math.max(result.targetCopies ?? 0, result.breakEvenCopies ?? 0, 1000, 1);
26
+ const breakPosition = result.breakEvenCopies === null ? 100 : Math.min(100, result.breakEvenCopies / max * 100);
27
+ const targetPosition = result.targetCopies === null ? 100 : Math.min(100, result.targetCopies / max * 100);
28
+ root.querySelector<HTMLElement>('[data-scene-fill]')?.style.setProperty('--fill-width', `${targetPosition}%`);
29
+ root.querySelector<HTMLElement>('[data-scene-marker="break"]')?.style.setProperty('--marker-position', `${breakPosition}%`);
30
+ root.querySelector<HTMLElement>('[data-scene-marker="target"]')?.style.setProperty('--marker-position', `${targetPosition}%`);
31
+ }
32
+
33
+ export function renderBookRoyalty(args: RenderArgs): void {
34
+ const { root, result, ui, currency } = args;
35
+ setOutput(root, 'headline', `${copyCount(result.breakEvenCopies, ui.unreachableLabel)} ${ui.copiesLabel}`);
36
+ setOutput(root, 'break-even', copyCount(result.breakEvenCopies, ui.unreachableLabel));
37
+ setOutput(root, 'target', copyCount(result.targetCopies, ui.unreachableLabel));
38
+ setOutput(root, 'margin', money(result.marginPerCopy, currency, ui.unreachableLabel));
39
+ setOutput(root, 'royalty-per-copy', money(result.royaltyPerCopy, currency, ui.unreachableLabel));
40
+ setOutput(root, 'revenue-break-even', money(result.revenueAtBreakEven, currency, ui.unreachableLabel));
41
+ setOutput(root, 'scene-break-label', result.breakEvenCopies === null ? '∞' : number(result.breakEvenCopies));
42
+ setOutput(root, 'scene-target-label', result.targetCopies === null ? '∞' : number(result.targetCopies));
43
+ setOutput(root, 'scene-caption', result.breakEvenCopies === null ? ui.statusUnprofitableHint : `${ui.sceneCaption} ${number(result.breakEvenCopies)} ${ui.copiesLabel}.`);
44
+ const status = statusForMargin(result.marginPerCopy);
45
+ root.dataset.status = status;
46
+ const copy = statusCopy(status, ui);
47
+ setOutput(root, 'status', copy.text);
48
+ setOutput(root, 'status-hint', copy.hint);
49
+ renderScene(root, result);
50
+ renderScenarios(root, result, ui, currency);
51
+ }
@@ -0,0 +1,39 @@
1
+ import type { BooksToolEntry, SEOSection, ToolLocaleContent } from '../../types';
2
+ import type { BookRoyaltyUI } from './ui';
3
+
4
+ export type { BookRoyaltyUI } from './ui';
5
+ export type BookRoyaltyLocaleContent = ToolLocaleContent<BookRoyaltyUI>;
6
+
7
+ function withLocalizedSchemas(content: BookRoyaltyLocaleContent): BookRoyaltyLocaleContent {
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: 'BusinessApplication', 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 bookRoyaltyBreakEvenCalculator: BooksToolEntry<BookRoyaltyUI> = {
20
+ id: 'book-royalty-break-even-calculator',
21
+ icons: { bg: 'mdi:book-open-page-variant-outline', fg: 'mdi:scale-balance' },
22
+ i18n: {
23
+ de: () => import('./i18n/de').then((module) => withLocalizedSchemas(module.content as BookRoyaltyLocaleContent)),
24
+ en: () => import('./i18n/en').then((module) => module.content),
25
+ es: () => import('./i18n/es').then((module) => withLocalizedSchemas(module.content as BookRoyaltyLocaleContent)),
26
+ fr: () => import('./i18n/fr').then((module) => withLocalizedSchemas(module.content as BookRoyaltyLocaleContent)),
27
+ id: () => import('./i18n/id').then((module) => withLocalizedSchemas(module.content as BookRoyaltyLocaleContent)),
28
+ it: () => import('./i18n/it').then((module) => withLocalizedSchemas(module.content as BookRoyaltyLocaleContent)),
29
+ ja: () => import('./i18n/ja').then((module) => withLocalizedSchemas(module.content as BookRoyaltyLocaleContent)),
30
+ ko: () => import('./i18n/ko').then((module) => withLocalizedSchemas(module.content as BookRoyaltyLocaleContent)),
31
+ nl: () => import('./i18n/nl').then((module) => withLocalizedSchemas(module.content as BookRoyaltyLocaleContent)),
32
+ pl: () => import('./i18n/pl').then((module) => withLocalizedSchemas(module.content as BookRoyaltyLocaleContent)),
33
+ pt: () => import('./i18n/pt').then((module) => withLocalizedSchemas(module.content as BookRoyaltyLocaleContent)),
34
+ ru: () => import('./i18n/ru').then((module) => withLocalizedSchemas(module.content as BookRoyaltyLocaleContent)),
35
+ sv: () => import('./i18n/sv').then((module) => withLocalizedSchemas(module.content as BookRoyaltyLocaleContent)),
36
+ tr: () => import('./i18n/tr').then((module) => withLocalizedSchemas(module.content as BookRoyaltyLocaleContent)),
37
+ zh: () => import('./i18n/zh').then((module) => withLocalizedSchemas(module.content as BookRoyaltyLocaleContent)),
38
+ },
39
+ };
@@ -0,0 +1,2 @@
1
+ import { localeContent } from './locales';
2
+ export const content = localeContent.de;
@@ -0,0 +1,52 @@
1
+ import type { ToolLocaleContent } from '../../../types';
2
+ import { BOOK_ROYALTY_BIBLIOGRAPHY } from '../bibliography';
3
+ import type { BookRoyaltyUI } from '../ui';
4
+
5
+ const ui: BookRoyaltyUI = {
6
+ presetLabel: 'Start with a format', ebookPreset: 'Ebook', paperbackPreset: 'Paperback', fixedCostsLabel: 'Fixed publishing costs', fixedCostsHint: 'Editing, cover, setup, and other costs paid once', salePriceLabel: 'Sale price per copy', salePriceHint: 'The reader-facing price', royaltyModeLabel: 'Your royalty', royaltyPercentLabel: 'Percentage of sale', royaltyAmountLabel: 'Amount per copy', royaltyHint: 'Use the royalty stated in your agreement', variableCostLabel: 'Variable cost per copy', variableCostHint: 'Printing, fulfilment, or other cost per sale', targetProfitLabel: 'Target profit', targetProfitHint: 'Optional profit you want after fixed costs', currencyLabel: 'Currency', breakEvenLabel: 'Break even point', breakEvenHint: 'Copies to recover fixed costs', targetLabel: 'Target reached', targetHint: 'Copies to recover costs and target profit', marginLabel: 'Net margin per copy', marginHint: 'Royalty less variable cost', royaltyPerCopyLabel: 'Royalty per copy', revenueAtBreakEvenLabel: 'Revenue at break even', fixedCostsOutputLabel: 'Fixed costs', sceneLabel: 'The publishing balance', sceneCaption: 'Your first profitable copy arrives after the fixed costs are covered.', copiesLabel: 'copies', statusProfitable: 'A path to profit', statusUnprofitable: 'No break even yet', statusZero: 'At the line', statusProfitableHint: 'Each sale contributes to the fixed cost recovery. The markers show break even and your target profit.', statusUnprofitableHint: 'The current royalty does not cover the variable cost per copy, so more sales cannot recover the fixed costs.', statusZeroHint: 'Each copy covers its variable cost exactly. Increase the royalty or reduce the variable cost to create a path to break even.', unreachableLabel: 'Not reachable', yesLabel: 'Yes', noLabel: 'No', scenarioLabel: 'Scenario checkpoints', scenarioCopies: 'Sales', scenarioRevenue: 'Gross revenue', scenarioProfit: 'Net profit', scenarioBreakEven: 'Past break even', sourceNote: 'Planning estimate only. Check your contract for royalty basis, deductions, returns, taxes, and any separate print or distribution fees.', copyNote: 'Profit here means royalty income minus the costs you entered. It is not a tax or cash flow forecast.',
7
+ };
8
+
9
+ const seo: ToolLocaleContent<BookRoyaltyUI>['seo'] = [
10
+ { type: 'title', text: 'Find the Book Sales Needed to Break Even', level: 2 },
11
+ { type: 'paragraph', html: 'This book royalty break-even calculator helps independent authors compare a sale price, royalty arrangement, publishing costs, and optional profit target. It turns your own contract and budget figures into the number of copies you need to sell before the edition pays for itself.' },
12
+ { type: 'title', text: 'What the calculator measures', level: 2 },
13
+ { type: 'paragraph', html: 'The tool calculates royalty income per copy, subtracts the variable cost of each sale, and calls the remainder the net margin. Fixed publishing costs are divided by that margin and rounded up to a whole copy. A second marker includes the target profit you enter.' },
14
+ { type: 'list', items: ['Use fixed costs for expenses you pay once, such as editing, cover design, or setup.', 'Choose a percentage when the contract pays a share of the sale price.', 'Choose an amount when the contract gives you a fixed royalty per copy.', 'Enter print or fulfilment cost only once as the variable cost per copy.', 'Treat the result as a planning scenario, not a promise of sales or income.'] },
15
+ { type: 'title', text: 'Why margin matters more than the cover price', level: 2 },
16
+ { type: 'paragraph', html: 'A high list price does not automatically create a healthy publishing margin. A royalty percentage, distributor deduction, print cost, or fulfilment fee can change what remains from each sale. The useful comparison is the royalty you actually receive minus the variable cost you enter.' },
17
+ { type: 'title', text: 'How to use the break-even result', level: 2 },
18
+ { type: 'paragraph', html: 'Use the break-even copies as a planning checkpoint for the edition. If the target is too high, compare a different format, price, royalty arrangement, or cost structure. Review the checkpoint against your actual agreement because contracts differ in their royalty base and deductions.' },
19
+ { type: 'title', text: 'Limits of a royalty projection', level: 2 },
20
+ { type: 'paragraph', html: 'The calculator does not know your publisher contract, sales returns, taxes, advertising, platform fees, currency changes, or cash payment schedule. It only applies the values you provide, so keep the assumptions visible when you share or revisit a scenario.' },
21
+ { type: 'tip', title: 'Break even is a decision marker', html: 'Use the number to compare editions and budgets, not to forecast demand. A copy count becomes meaningful when you can connect it to a launch plan, audience, or sales history.' },
22
+ ];
23
+
24
+ const faq = [
25
+ { question: 'What are fixed publishing costs?', answer: 'They are costs you enter as a one-time amount, such as editing, cover design, formatting, setup, or an initial print run.' },
26
+ { question: 'Should I use a royalty percentage or an amount?', answer: 'Use a percentage when your agreement gives you a share of the sale price. Use an amount when it states a fixed royalty per copy.' },
27
+ { question: 'Why can the calculator say break-even is not reachable?', answer: 'Your royalty per copy is equal to or lower than the variable cost per copy, so additional sales cannot recover fixed costs under those assumptions.' },
28
+ { question: 'Does this include taxes and platform fees?', answer: 'No. It is a transparent planning estimate based only on the costs and royalty values you enter. Check your agreement and tax situation separately.' },
29
+ ];
30
+
31
+ const howTo = [
32
+ { name: 'Choose a format', text: 'Start with the ebook or paperback preset, then replace the example values with your own budget.' },
33
+ { name: 'Enter the costs', text: 'Add one-time fixed costs, sale price, and any variable cost charged per copy.' },
34
+ { name: 'Set the royalty', text: 'Choose a percentage of the sale or a fixed amount per copy and enter the value from your agreement.' },
35
+ { name: 'Compare the markers', text: 'Use the break-even and target copy counts to compare the edition before committing to the plan.' },
36
+ ];
37
+
38
+ export const content: ToolLocaleContent<BookRoyaltyUI> = {
39
+ slug: 'book-royalty-break-even-calculator',
40
+ title: 'Book Royalty Break Even Calculator',
41
+ description: 'Calculate how many copies your book needs to sell to recover publishing costs and reach a profit target.',
42
+ ui,
43
+ seo,
44
+ faq,
45
+ bibliography: BOOK_ROYALTY_BIBLIOGRAPHY,
46
+ howTo,
47
+ schemas: [
48
+ { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: 'Book Royalty Break Even Calculator', applicationCategory: 'BusinessApplication', operatingSystem: 'Any', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' } },
49
+ { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) },
50
+ { '@context': 'https://schema.org', '@type': 'HowTo', name: 'Calculate book royalty break even copies', step: howTo.map((item) => ({ '@type': 'HowToStep', name: item.name, text: item.text })) },
51
+ ],
52
+ };
@@ -0,0 +1,2 @@
1
+ import { localeContent } from './locales';
2
+ export const content = localeContent.es;
@@ -0,0 +1,2 @@
1
+ import { localeContent } from './locales';
2
+ export const content = localeContent.fr;
@@ -0,0 +1,2 @@
1
+ import { localeContent } from './locales';
2
+ export const content = localeContent.id;
@@ -0,0 +1,2 @@
1
+ import { localeContent } from './locales';
2
+ export const content = localeContent.it;
@@ -0,0 +1,2 @@
1
+ import { localeContent } from './locales';
2
+ export const content = localeContent.ja;
@@ -0,0 +1,2 @@
1
+ import { localeContent } from './locales';
2
+ export const content = localeContent.ko;
@@ -0,0 +1,73 @@
1
+ import type { ToolLocaleContent } from '../../../types';
2
+ import { BOOK_ROYALTY_BIBLIOGRAPHY } from '../bibliography';
3
+ import { content as english } from './en';
4
+ import type { BookRoyaltyUI } from '../ui';
5
+
6
+ type LocaleSeed = { intro: string; method: string; margin: string; action: string; limit: string; tip: string; questions: string[]; steps: string[] };
7
+ const titles: Record<string, string> = {
8
+ de: 'Buch Tantiemen Rechner für den Break even', es: 'Calculadora de punto de equilibrio de regalías', fr: 'Calculateur de seuil de rentabilité des droits du livre', id: 'Kalkulator titik impas royalti buku', it: 'Calcolatore del punto di pareggio delle royalty del libro', ja: '本の印税損益分岐点計算機', ko: '도서 인세 손익분기점 계산기', nl: 'Break even calculator voor boekroyalty', pl: 'Kalkulator progu rentowności książki', pt: 'Calculadora de ponto de equilíbrio de royalties de livro', ru: 'Калькулятор безубыточности книжных отчислений', sv: 'Kalkylator för bokens royalty och nollpunkt', tr: 'Kitap telifi başa baş hesaplayıcı', zh: '图书版税盈亏平衡计算器',
9
+ };
10
+ const descriptions: Record<string, string> = {
11
+ de: 'Berechne, wie viele Exemplare dein Buch verkaufen muss, um die Veröffentlichungskosten und dein Gewinnziel zu decken.', es: 'Calcula cuántas copias debe vender tu libro para recuperar los costes de publicación y alcanzar un beneficio objetivo.', fr: 'Calculez le nombre de ventes nécessaires pour couvrir les frais de publication et atteindre votre objectif de bénéfice.', id: 'Hitung jumlah buku yang harus terjual untuk menutup biaya penerbitan dan mencapai target laba.', it: 'Calcola quante copie deve vendere il tuo libro per recuperare i costi di pubblicazione e raggiungere un profitto obiettivo.', ja: '出版費用を回収し、目標利益に達するために必要な販売部数を計算します。', ko: '출판 비용을 회수하고 목표 수익에 도달하는 데 필요한 판매 부수를 계산합니다.', nl: 'Bereken hoeveel exemplaren je boek moet verkopen om publicatiekosten terug te verdienen en een winstdoel te halen.', pl: 'Oblicz liczbę egzemplarzy potrzebnych do odzyskania kosztów publikacji i osiągnięcia zysku.', pt: 'Calcule quantas cópias seu livro precisa vender para recuperar os custos de publicação e alcançar um lucro desejado.', ru: 'Рассчитайте, сколько экземпляров нужно продать, чтобы вернуть расходы на публикацию и достичь цели по прибыли.', sv: 'Räkna ut hur många exemplar boken behöver sälja för att täcka publiceringskostnader och nå ett vinstmål.', tr: 'Yayıncılık maliyetlerini karşılamak ve hedef kâra ulaşmak için kaç kopya satmanız gerektiğini hesaplayın.', zh: '计算需要售出多少本书才能收回出版成本并达到目标利润。',
12
+ };
13
+ const slugs: Record<string, string> = {
14
+ de: 'buch-tantiemen-break-even-rechner', es: 'calculadora-punto-equilibrio-regalias-libro', fr: 'calculateur-seuil-rentabilite-droits-auteur-livre', id: 'kalkulator-titik-impas-royalti-buku', it: 'calcolatore-pareggio-royalty-libro', ja: 'book-royalty-break-even-calculator', ko: 'book-royalty-break-even-calculator', nl: 'boek-royalty-break-even-calculator', pl: 'kalkulator-progu-rentownosci-ksiazki', pt: 'calculadora-ponto-equilibrio-royalties-livro', ru: 'kalkulyator-tochki-bezubytochnosti-knigi', sv: 'bokroyalti-nollpunkt-kalkylator', tr: 'kitap-telif-basabas-hesaplayici', zh: 'book-royalty-break-even-calculator',
15
+ };
16
+ const ui: Record<string, Partial<BookRoyaltyUI>> = {
17
+ de: { presetLabel: 'Mit einem Format starten', ebookPreset: 'E Book', paperbackPreset: 'Taschenbuch', fixedCostsLabel: 'Einmalige Veröffentlichungskosten', salePriceLabel: 'Verkaufspreis pro Exemplar', royaltyModeLabel: 'Deine Tantieme', royaltyPercentLabel: 'Anteil am Verkauf', royaltyAmountLabel: 'Betrag pro Exemplar', variableCostLabel: 'Variable Kosten pro Exemplar', targetProfitLabel: 'Gewinnziel', currencyLabel: 'Währung', breakEvenLabel: 'Break even Punkt', copiesLabel: 'Exemplare', statusProfitable: 'Gewinn ist erreichbar', statusUnprofitable: 'Kein Break even', scenarioLabel: 'Verkaufsszenarien', scenarioCopies: 'Verkäufe', scenarioRevenue: 'Bruttoumsatz', scenarioProfit: 'Nettogewinn', sourceNote: 'Planungsschätzung. Prüfe im Vertrag Berechnungsbasis, Abzüge, Retouren, Steuern und Gebühren.' },
18
+ es: { presetLabel: 'Empieza con un formato', ebookPreset: 'Ebook', paperbackPreset: 'Rústica', fixedCostsLabel: 'Costes fijos de publicación', salePriceLabel: 'Precio por copia', royaltyModeLabel: 'Tus regalías', royaltyPercentLabel: 'Porcentaje de la venta', royaltyAmountLabel: 'Importe por copia', variableCostLabel: 'Coste variable por copia', targetProfitLabel: 'Beneficio objetivo', currencyLabel: 'Moneda', breakEvenLabel: 'Punto de equilibrio', copiesLabel: 'copias', statusProfitable: 'Hay una ruta al beneficio', statusUnprofitable: 'Sin punto de equilibrio', scenarioLabel: 'Escenarios de venta', scenarioCopies: 'Ventas', scenarioRevenue: 'Ingresos brutos', scenarioProfit: 'Beneficio neto', sourceNote: 'Estimación de planificación. Comprueba en tu contrato la base de regalías, deducciones, devoluciones, impuestos y tarifas.' },
19
+ fr: { presetLabel: 'Commencer avec un format', ebookPreset: 'Ebook', paperbackPreset: 'Livre broché', fixedCostsLabel: 'Frais fixes de publication', salePriceLabel: 'Prix par exemplaire', royaltyModeLabel: 'Vos droits', royaltyPercentLabel: 'Pourcentage de la vente', royaltyAmountLabel: 'Montant par exemplaire', variableCostLabel: 'Coût variable par exemplaire', targetProfitLabel: 'Bénéfice visé', currencyLabel: 'Devise', breakEvenLabel: 'Seuil de rentabilité', copiesLabel: 'exemplaires', statusProfitable: 'Une voie vers le bénéfice', statusUnprofitable: 'Seuil impossible', scenarioLabel: 'Scénarios de vente', scenarioCopies: 'Ventes', scenarioRevenue: 'Chiffre brut', scenarioProfit: 'Bénéfice net', sourceNote: 'Estimation de planification. Vérifiez le contrat, les déductions, les retours, les taxes et les frais.' },
20
+ it: { presetLabel: 'Inizia da un formato', ebookPreset: 'Ebook', paperbackPreset: 'Brossura', fixedCostsLabel: 'Costi fissi di pubblicazione', salePriceLabel: 'Prezzo per copia', royaltyModeLabel: 'Le tue royalty', royaltyPercentLabel: 'Percentuale sulla vendita', royaltyAmountLabel: 'Importo per copia', variableCostLabel: 'Costo variabile per copia', targetProfitLabel: 'Profitto obiettivo', currencyLabel: 'Valuta', breakEvenLabel: 'Punto di pareggio', copiesLabel: 'copie', statusProfitable: 'Profitto possibile', statusUnprofitable: 'Pareggio non raggiungibile', scenarioLabel: 'Scenari di vendita', scenarioCopies: 'Vendite', scenarioRevenue: 'Ricavi lordi', scenarioProfit: 'Profitto netto', sourceNote: 'Stima di pianificazione. Controlla nel contratto base royalty, detrazioni, resi, tasse e commissioni.' },
21
+ pt: { presetLabel: 'Comece por um formato', ebookPreset: 'Ebook', paperbackPreset: 'Capa comum', fixedCostsLabel: 'Custos fixos de publicação', salePriceLabel: 'Preço por cópia', royaltyModeLabel: 'Seus royalties', royaltyPercentLabel: 'Percentual da venda', royaltyAmountLabel: 'Valor por cópia', variableCostLabel: 'Custo variável por cópia', targetProfitLabel: 'Lucro desejado', currencyLabel: 'Moeda', breakEvenLabel: 'Ponto de equilíbrio', copiesLabel: 'cópias', statusProfitable: 'Há caminho para lucro', statusUnprofitable: 'Sem ponto de equilíbrio', scenarioLabel: 'Cenários de venda', scenarioCopies: 'Vendas', scenarioRevenue: 'Receita bruta', scenarioProfit: 'Lucro líquido', sourceNote: 'Estimativa de planejamento. Confira no contrato a base de royalties, descontos, devoluções, impostos e taxas.' },
22
+ nl: { presetLabel: 'Begin met een formaat', ebookPreset: 'E book', paperbackPreset: 'Paperback', fixedCostsLabel: 'Vaste publicatiekosten', salePriceLabel: 'Verkoopprijs per exemplaar', royaltyModeLabel: 'Je royalty', royaltyPercentLabel: 'Percentage van verkoop', royaltyAmountLabel: 'Bedrag per exemplaar', variableCostLabel: 'Variabele kosten per exemplaar', targetProfitLabel: 'Winstdoel', currencyLabel: 'Valuta', breakEvenLabel: 'Break evenpunt', copiesLabel: 'exemplaren', statusProfitable: 'Winst is haalbaar', statusUnprofitable: 'Geen break even', scenarioLabel: 'Verkoopsituaties', scenarioCopies: 'Verkoop', scenarioRevenue: 'Bruto omzet', scenarioProfit: 'Nettowinst', sourceNote: 'Planningsschatting. Controleer royaltybasis, inhoudingen, retouren, belastingen en kosten in je contract.' },
23
+ id: { presetLabel: 'Mulai dari format', ebookPreset: 'Ebook', paperbackPreset: 'Paperback', fixedCostsLabel: 'Biaya tetap penerbitan', salePriceLabel: 'Harga jual per buku', royaltyModeLabel: 'Royalti Anda', royaltyPercentLabel: 'Persentase penjualan', royaltyAmountLabel: 'Jumlah per buku', variableCostLabel: 'Biaya variabel per buku', targetProfitLabel: 'Target laba', currencyLabel: 'Mata uang', breakEvenLabel: 'Titik impas', copiesLabel: 'buku', statusProfitable: 'Laba dapat dicapai', statusUnprofitable: 'Belum ada titik impas', scenarioLabel: 'Skenario penjualan', scenarioCopies: 'Penjualan', scenarioRevenue: 'Pendapatan kotor', scenarioProfit: 'Laba bersih', sourceNote: 'Perkiraan perencanaan. Periksa dasar royalti, potongan, pengembalian, pajak, dan biaya di kontrak Anda.' },
24
+ sv: { presetLabel: 'Börja med ett format', ebookPreset: 'E bok', paperbackPreset: 'Pocket', fixedCostsLabel: 'Fasta publiceringskostnader', salePriceLabel: 'Pris per exemplar', royaltyModeLabel: 'Din royalty', royaltyPercentLabel: 'Andel av försäljningen', royaltyAmountLabel: 'Belopp per exemplar', variableCostLabel: 'Rörlig kostnad per exemplar', targetProfitLabel: 'Vinstmål', currencyLabel: 'Valuta', breakEvenLabel: 'Nollpunkt', copiesLabel: 'exemplar', statusProfitable: 'Vinst är möjlig', statusUnprofitable: 'Ingen nollpunkt', scenarioLabel: 'Försäljningsscenarier', scenarioCopies: 'Försäljning', scenarioRevenue: 'Bruttointäkt', scenarioProfit: 'Nettovinst', sourceNote: 'Planeringsestimat. Kontrollera royaltybas, avdrag, returer, skatt och avgifter i avtalet.' },
25
+ pl: { presetLabel: 'Zacznij od formatu', ebookPreset: 'E book', paperbackPreset: 'Książka papierowa', fixedCostsLabel: 'Stałe koszty publikacji', salePriceLabel: 'Cena za egzemplarz', royaltyModeLabel: 'Twoje tantiemy', royaltyPercentLabel: 'Procent sprzedaży', royaltyAmountLabel: 'Kwota za egzemplarz', variableCostLabel: 'Koszt zmienny za egzemplarz', targetProfitLabel: 'Cel zysku', currencyLabel: 'Waluta', breakEvenLabel: 'Próg rentowności', copiesLabel: 'egzemplarzy', statusProfitable: 'Zysk jest możliwy', statusUnprofitable: 'Brak progu rentowności', scenarioLabel: 'Scenariusze sprzedaży', scenarioCopies: 'Sprzedaż', scenarioRevenue: 'Przychód brutto', scenarioProfit: 'Zysk netto', sourceNote: 'Szacunek planistyczny. Sprawdź w umowie podstawę tantiem, potrącenia, zwroty, podatki i opłaty.' },
26
+ tr: { presetLabel: 'Bir formatla başlayın', ebookPreset: 'E kitap', paperbackPreset: 'Kartonet', fixedCostsLabel: 'Sabit yayın maliyetleri', salePriceLabel: 'Kopya satış fiyatı', royaltyModeLabel: 'Telifiniz', royaltyPercentLabel: 'Satış yüzdesi', royaltyAmountLabel: 'Kopya başına tutar', variableCostLabel: 'Kopya başına değişken maliyet', targetProfitLabel: 'Hedef kâr', currencyLabel: 'Para birimi', breakEvenLabel: 'Başa baş noktası', copiesLabel: 'kopya', statusProfitable: 'Kâra giden yol var', statusUnprofitable: 'Başa baş noktası yok', scenarioLabel: 'Satış senaryoları', scenarioCopies: 'Satış', scenarioRevenue: 'Brüt gelir', scenarioProfit: 'Net kâr', sourceNote: 'Planlama tahmini. Sözleşmenizde telif tabanını, kesintileri, iadeleri, vergileri ve ücretleri kontrol edin.' },
27
+ ru: { presetLabel: 'Начните с формата', ebookPreset: 'Электронная книга', paperbackPreset: 'Мягкая обложка', fixedCostsLabel: 'Постоянные расходы на публикацию', salePriceLabel: 'Цена за экземпляр', royaltyModeLabel: 'Ваши отчисления', royaltyPercentLabel: 'Процент от продажи', royaltyAmountLabel: 'Сумма за экземпляр', variableCostLabel: 'Переменные расходы на экземпляр', targetProfitLabel: 'Целевая прибыль', currencyLabel: 'Валюта', breakEvenLabel: 'Точка безубыточности', copiesLabel: 'экз.', statusProfitable: 'Прибыль достижима', statusUnprofitable: 'Безубыточность недостижима', scenarioLabel: 'Сценарии продаж', scenarioCopies: 'Продажи', scenarioRevenue: 'Валовая выручка', scenarioProfit: 'Чистая прибыль', sourceNote: 'Плановая оценка. Проверьте в договоре базу отчислений, удержания, возвраты, налоги и комиссии.' },
28
+ ja: { presetLabel: '形式から開始', ebookPreset: '電子書籍', paperbackPreset: 'ペーパーバック', fixedCostsLabel: '出版固定費', salePriceLabel: '1冊の販売価格', royaltyModeLabel: '印税', royaltyPercentLabel: '販売価格の割合', royaltyAmountLabel: '1冊あたりの金額', variableCostLabel: '1冊あたりの変動費', targetProfitLabel: '目標利益', currencyLabel: '通貨', breakEvenLabel: '損益分岐点', copiesLabel: '部', statusProfitable: '利益への道があります', statusUnprofitable: '分岐点に到達しません', scenarioLabel: '販売シナリオ', scenarioCopies: '販売部数', scenarioRevenue: '総売上', scenarioProfit: '純利益', sourceNote: '計画用の試算です。契約の印税基準、控除、返品、税金、手数料を確認してください。' },
29
+ ko: { presetLabel: '형식으로 시작', ebookPreset: '전자책', paperbackPreset: '페이퍼백', fixedCostsLabel: '출판 고정 비용', salePriceLabel: '권당 판매 가격', royaltyModeLabel: '나의 인세', royaltyPercentLabel: '판매 비율', royaltyAmountLabel: '권당 금액', variableCostLabel: '권당 변동 비용', targetProfitLabel: '목표 수익', currencyLabel: '통화', breakEvenLabel: '손익분기점', copiesLabel: '부', statusProfitable: '수익 경로가 있습니다', statusUnprofitable: '손익분기점 없음', scenarioLabel: '판매 시나리오', scenarioCopies: '판매', scenarioRevenue: '총매출', scenarioProfit: '순수익', sourceNote: '계획용 추정치입니다. 계약의 인세 기준, 공제, 반품, 세금과 수수료를 확인하세요.' },
30
+ zh: { presetLabel: '从格式开始', ebookPreset: '电子书', paperbackPreset: '平装书', fixedCostsLabel: '出版固定成本', salePriceLabel: '每本售价', royaltyModeLabel: '你的版税', royaltyPercentLabel: '销售额百分比', royaltyAmountLabel: '每本金额', variableCostLabel: '每本可变成本', targetProfitLabel: '目标利润', currencyLabel: '货币', breakEvenLabel: '盈亏平衡点', copiesLabel: '本', statusProfitable: '可以获得利润', statusUnprofitable: '无法达到盈亏平衡', scenarioLabel: '销售情景', scenarioCopies: '销量', scenarioRevenue: '总收入', scenarioProfit: '净利润', sourceNote: '这是规划估算。请在合同中核对版税基数、扣除、退货、税费和手续费。' },
31
+ };
32
+
33
+ ui.ko = { fixedCostsHint: '편집, 표지, 설정 등 한 번만 지불하는 비용', salePriceHint: '독자가 지불하는 가격', royaltyHint: '계약서에 적힌 인세를 사용하세요', variableCostHint: '판매할 때마다 드는 인쇄 및 처리 비용', targetProfitHint: '고정 비용을 제외한 뒤 원하는 수익', breakEvenHint: '고정 비용을 회수하는 부수', targetHint: '비용과 목표 수익을 회수하는 부수', marginHint: '인세에서 변동 비용을 뺀 금액', royaltyPerCopyLabel: '권당 인세', revenueAtBreakEvenLabel: '손익분기점 총매출', sceneCaption: '고정 비용을 회수한 뒤 첫 수익이 발생합니다.', targetLabel: '목표 달성', statusZero: '손익 경계', statusProfitableHint: '판매할 때마다 고정 비용 회수에 가까워집니다. 표시는 손익분기점과 목표 수익을 보여 줍니다.', statusUnprofitableHint: '현재 인세가 권당 변동 비용을 충당하지 못해 판매를 늘려도 고정 비용을 회수할 수 없습니다.', statusZeroHint: '각 판매가 변동 비용만 정확히 충당합니다. 인세를 높이거나 비용을 낮추세요.', scenarioBreakEven: '손익분기점 초과', copyNote: '수익은 입력한 비용만 차감한 값입니다.' };
34
+
35
+ const seeds: Record<string, LocaleSeed> = {
36
+ de: { intro: 'Dieser Rechner verbindet Verkaufspreis, Tantiemen, Veröffentlichungskosten und Gewinnziel. Er zeigt, wie viele Exemplare deine eigene Ausgabe tragen müssen.', method: 'Die Tantieme je Exemplar minus variable Kosten ergibt die Nettomarge. Die Fixkosten werden durch die Marge geteilt und aufgerundet. Der zweite Marker enthält dein Gewinnziel.', margin: 'Der Ladenpreis ist nicht dein Ertrag. Vertragsbasis, Händlerabzüge, Druck und Versand verändern den Betrag je Verkauf.', action: 'Vergleiche Formate, Preise und Budgets vor der Veröffentlichung und bewahre die Annahmen beim Ergebnis auf.', limit: 'Der Rechner kennt Vertrag, Steuern, Retouren, Werbung und Zahlungsfristen nicht.', tip: 'Vergleiche die Verkaufszahl mit deiner Zielgruppe und deinem Veröffentlichungsplan.', questions: ['Was sind fixe Veröffentlichungskosten?', 'Wann verwende ich Prozent oder Betrag?', 'Warum ist Break even nicht erreichbar?', 'Sind Steuern enthalten?'], steps: ['Format wählen', 'Kosten eintragen', 'Tantieme setzen', 'Marker vergleichen'] },
37
+ es: { intro: 'Esta calculadora compara precio, regalías, costes de publicación y beneficio deseado. Convierte las cifras de tu contrato y presupuesto en copias necesarias para que la edición se pague.', method: 'La regalía por copia menos el coste variable produce el margen neto. Los costes fijos se dividen entre ese margen y se redondean hacia arriba. El segundo marcador suma tu beneficio objetivo.', margin: 'El precio de portada no es lo que ganas. La base del contrato, las deducciones, la impresión y la gestión cambian lo que queda en cada venta.', action: 'Compara formatos, precios y presupuestos antes de publicar. Guarda los supuestos junto al resultado porque los contratos pueden usar bases distintas.', limit: 'La herramienta no conoce tu contrato, devoluciones, impuestos, publicidad ni calendario de pagos.', tip: 'El número de copias ayuda a decidir cuando lo comparas con tu audiencia y tu plan de lanzamiento.', questions: ['¿Qué son los costes fijos de publicación?', '¿Uso un porcentaje o un importe?', '¿Por qué el equilibrio no es alcanzable?', '¿Incluye impuestos?'], steps: ['Elige un formato', 'Introduce los costes', 'Configura la regalía', 'Compara los marcadores'] },
38
+ fr: { intro: 'Ce calculateur compare prix, droits, frais de publication et bénéfice visé. Il transforme les chiffres de votre contrat et de votre budget en ventes nécessaires pour rembourser le livre.', method: 'Le droit par exemplaire moins le coût variable donne la marge nette. Les frais fixes sont divisés par cette marge puis arrondis. Le second repère ajoute le bénéfice visé.', margin: 'Le prix affiché n est pas votre revenu. Base contractuelle, retenues, impression et expédition changent le montant réel de chaque vente.', action: 'Comparez les formats, prix et budgets avant de publier et gardez les hypothèses avec le résultat.', limit: 'Le calcul ne connaît ni votre contrat, ni les taxes, retours, publicités ou dates de paiement.', tip: 'Le nombre de ventes devient utile lorsqu il est confronté à votre public et à votre plan de lancement.', questions: ['Que sont les frais fixes?', 'Pourcentage ou montant?', 'Pourquoi le seuil est-il impossible?', 'Les taxes sont-elles incluses?'], steps: ['Choisir un format', 'Saisir les frais', 'Définir les droits', 'Comparer les repères'] },
39
+ it: { intro: 'Questo calcolatore confronta prezzo, royalty, costi di pubblicazione e profitto desiderato. Trasforma i dati del contratto e del budget nel numero di copie necessario per recuperare la spesa.', method: 'La royalty per copia meno il costo variabile produce il margine netto. I costi fissi vengono divisi per il margine e arrotondati. Il secondo indicatore aggiunge il profitto obiettivo.', margin: 'Il prezzo di copertina non è il tuo guadagno. Base contrattuale, distributore, stampa e gestione modificano ciò che resta a ogni vendita.', action: 'Confronta formati, prezzi e budget prima di pubblicare e conserva le ipotesi accanto al risultato.', limit: 'Il calcolo non conosce contratto, tasse, resi, pubblicità o tempi di pagamento.', tip: 'Il numero di copie aiuta a decidere quando lo confronti con pubblico e piano di lancio.', questions: ['Cosa sono i costi fissi?', 'Quando uso percentuale o importo?', 'Perché il pareggio non è raggiungibile?', 'Sono comprese le tasse?'], steps: ['Scegli il formato', 'Inserisci i costi', 'Imposta la royalty', 'Confronta gli indicatori'] },
40
+ pt: { intro: 'Esta calculadora compara preço, royalties, custos de publicação e lucro desejado. Ela transforma os números do seu contrato e orçamento na quantidade de cópias necessária para pagar a edição.', method: 'O royalty por cópia menos o custo variável forma a margem líquida. Os custos fixos são divididos por ela e arredondados para cima. O segundo marcador inclui o lucro desejado.', margin: 'O preço de capa não é o seu ganho. A base contratual, descontos, impressão e distribuição alteram o valor que sobra em cada venda.', action: 'Compare formatos, preços e orçamentos antes de publicar e mantenha as premissas junto do resultado.', limit: 'A calculadora não conhece contrato, impostos, devoluções, publicidade nem datas de pagamento.', tip: 'O número ajuda a decidir quando é comparado com seu público e seu plano de lançamento.', questions: ['O que são custos fixos?', 'Uso percentual ou valor?', 'Por que o equilíbrio não é alcançável?', 'Impostos estão incluídos?'], steps: ['Escolha o formato', 'Informe os custos', 'Defina o royalty', 'Compare os marcadores'] },
41
+ nl: { intro: 'Deze calculator vergelijkt prijs, royalty, publicatiekosten en een winstdoel. Je krijgt het aantal exemplaren dat nodig is om je eigen uitgave terug te verdienen.', method: 'Royalty per exemplaar min variabele kosten vormt de nettomarge. Vaste kosten worden door die marge gedeeld en naar boven afgerond. De tweede markering telt je winstdoel erbij op.', margin: 'De verkoopprijs is niet je opbrengst. Contractbasis, korting, drukwerk en verzending bepalen wat per verkoop overblijft.', action: 'Vergelijk formaten, prijzen en budgetten voor publicatie en bewaar je aannames naast de uitkomst.', limit: 'De calculator kent je contract, belastingen, retouren, reclame of betaaltermijnen niet.', tip: 'Het aantal wordt een beslissing wanneer je het vergelijkt met publiek en lanceringsplan.', questions: ['Wat zijn vaste publicatiekosten?', 'Percentage of bedrag?', 'Waarom is break even niet haalbaar?', 'Zijn belastingen inbegrepen?'], steps: ['Kies een formaat', 'Vul kosten in', 'Stel royalty in', 'Vergelijk markeringen'] },
42
+ id: { intro: 'Kalkulator ini membandingkan harga, royalti, biaya penerbitan, dan target laba. Hasilnya adalah jumlah buku yang perlu terjual agar edisi Anda balik modal.', method: 'Royalti per buku dikurangi biaya variabel menjadi margin bersih. Biaya tetap dibagi margin lalu dibulatkan ke atas. Penanda kedua memasukkan target laba.', margin: 'Harga sampul bukan pendapatan bersih. Dasar kontrak, potongan distributor, pencetakan, dan pengiriman mengubah hasil setiap penjualan.', action: 'Bandingkan format, harga, dan anggaran sebelum menerbitkan, lalu simpan asumsi bersama hasil.', limit: 'Kalkulator tidak mengetahui kontrak, pajak, pengembalian, iklan, atau jadwal pembayaran.', tip: 'Jumlah buku membantu keputusan saat dibandingkan dengan pembaca dan rencana peluncuran.', questions: ['Apa itu biaya tetap?', 'Kapan memakai persentase atau jumlah?', 'Mengapa titik impas tidak tercapai?', 'Apakah pajak termasuk?'], steps: ['Pilih format', 'Masukkan biaya', 'Atur royalti', 'Bandingkan penanda'] },
43
+ sv: { intro: 'Kalkylatorn jämför pris, royalty, publiceringskostnader och ett vinstmål. Den visar hur många exemplar som krävs för att utgåvan ska betala sig.', method: 'Royalty per exemplar minus rörlig kostnad blir nettomarginalen. Fasta kostnader delas med marginalen och avrundas uppåt. Den andra markeringen lägger till vinstmålet.', margin: 'Omslagspriset är inte samma som din intäkt. Avtalsbas, avdrag, tryck och frakt påverkar varje försäljning.', action: 'Jämför format, priser och budgetar före publicering och spara antagandena bredvid resultatet.', limit: 'Verktyget känner inte till avtal, skatt, returer, reklam eller utbetalningstider.', tip: 'Antalet exemplar blir ett beslutsunderlag när det jämförs med publik och lanseringsplan.', questions: ['Vad är fasta kostnader?', 'Procent eller belopp?', 'Varför nås inte nollpunkten?', 'Ingår skatt?'], steps: ['Välj format', 'Ange kostnader', 'Ställ in royalty', 'Jämför markeringarna'] },
44
+ pl: { intro: 'Kalkulator porównuje cenę, tantiemy, koszty publikacji i cel zysku. Zamienia dane umowy i budżetu na liczbę egzemplarzy potrzebnych do zwrotu wydatków.', method: 'Tantiema za egzemplarz pomniejszona o koszt zmienny daje marżę netto. Koszty stałe dzielimy przez marżę i zaokrąglamy w górę. Drugi znacznik dodaje cel zysku.', margin: 'Cena okładkowa nie jest dochodem autora. Podstawa umowy, potrącenia, druk i wysyłka zmieniają kwotę każdej sprzedaży.', action: 'Porównuj formaty, ceny i budżety przed publikacją i zachowaj założenia obok wyniku.', limit: 'Kalkulator nie zna umowy, podatków, zwrotów, reklamy ani terminów wypłat.', tip: 'Liczba egzemplarzy pomaga w decyzji, gdy zestawisz ją z odbiorcami i planem premiery.', questions: ['Co oznaczają koszty stałe?', 'Kiedy wybrać procent lub kwotę?', 'Dlaczego próg nie jest osiągalny?', 'Czy podatek jest uwzględniony?'], steps: ['Wybierz format', 'Wpisz koszty', 'Ustaw tantiemę', 'Porównaj znaczniki'] },
45
+ tr: { intro: 'Bu hesaplayıcı fiyatı, telifi, yayıncılık maliyetlerini ve hedef kârı karşılaştırır. Sözleşme ve bütçe rakamlarınızı, baskının kendini karşılaması için gereken kopya sayısına dönüştürür.', method: 'Kopya başına teliften değişken maliyet çıkarılarak net marj bulunur. Sabit maliyet marja bölünür ve yukarı yuvarlanır. İkinci işaret hedef kârı ekler.', margin: 'Kapak fiyatı gerçek kazanç değildir. Sözleşme tabanı, dağıtıcı kesintisi, baskı ve gönderim her satışta kalan tutarı değiştirir.', action: 'Yayınlamadan önce format, fiyat ve bütçeleri karşılaştırın; varsayımlarınızı sonuçla birlikte saklayın.', limit: 'Hesaplayıcı sözleşmenizi, vergileri, iadeleri, reklamı veya ödeme tarihlerini bilmez.', tip: 'Kopya sayısı, hedef kitleniz ve lansman planınızla karşılaştırıldığında karar desteği sağlar.', questions: ['Sabit yayın maliyetleri nelerdir?', 'Yüzde mi tutar mı?', 'Başa baş neden ulaşılamaz?', 'Vergiler dahil mi?'], steps: ['Format seçin', 'Maliyetleri girin', 'Telifi ayarlayın', 'İşaretleri karşılaştırın'] },
46
+ ru: { intro: 'Калькулятор сопоставляет цену, авторские отчисления, расходы на публикацию и желаемую прибыль. Он показывает число экземпляров для окупаемости вашего издания.', method: 'Отчисление с экземпляра за вычетом переменных расходов дает чистую маржу. Постоянные расходы делятся на маржу и округляются вверх. Второй показатель добавляет цель прибыли.', margin: 'Цена на обложке не равна доходу автора. Основа договора, удержания, печать и доставка меняют остаток с каждой продажи.', action: 'Сравнивайте форматы, цены и бюджеты до публикации и сохраняйте допущения рядом с результатом.', limit: 'Калькулятор не знает договор, налоги, возвраты, рекламу или сроки выплат.', tip: 'Число экземпляров помогает принять решение, если сопоставить его с аудиторией и планом запуска.', questions: ['Что такое постоянные расходы?', 'Когда выбрать процент или сумму?', 'Почему точка недостижима?', 'Учитываются ли налоги?'], steps: ['Выберите формат', 'Введите расходы', 'Настройте отчисление', 'Сравните показатели'] },
47
+ ja: { intro: 'この計算機は販売価格、印税、出版費用、目標利益を組み合わせ、費用を回収するために必要な販売部数を示します。契約書と予算の数字を使えます。', method: '1冊の印税から変動費を引いて純利益を求めます。固定費を純利益で割り、必要な部数を切り上げます。2つ目の目印には目標利益も含まれます。', margin: '表紙の価格は著者の収入そのものではありません。契約基準、控除、印刷や発送費で1冊ごとの残額は変わります。', action: '出版前に形式、価格、予算を比較し、前提条件も結果と一緒に保存してください。', limit: '計算機は契約、税金、返品、広告費、支払時期を読み取りません。', tip: '部数を読者層や発売計画と比べると、出版を決める材料になります。', questions: ['固定費とは何ですか?', '割合と金額はどちらを使いますか?', 'なぜ分岐点に到達しませんか?', '税金や手数料は含まれますか?'], steps: ['形式を選ぶ', '費用を入力する', '印税を設定する', '目印を比較する'] },
48
+ ko: { intro: '이 계산기는 판매 가격, 인세, 출판 비용과 목표 수익을 비교해 책이 비용을 회수하기 위해 팔아야 할 부수를 보여 줍니다. 계약서와 예산의 숫자를 사용할 수 있습니다.', method: '권당 인세에서 변동 비용을 빼 순마진을 구합니다. 고정 비용을 마진으로 나누고 필요한 부수를 올림합니다. 두 번째 표시는 목표 수익을 더합니다.', margin: '표지 가격이 곧 작가의 수입은 아닙니다. 계약 기준, 유통 공제, 인쇄와 배송비가 판매마다 남는 금액을 바꿉니다.', action: '출판 전에 형식, 가격과 예산을 비교하고 가정을 결과와 함께 보관하세요.', limit: '계산기는 계약, 세금, 반품, 광고나 지급 일정을 알 수 없습니다.', tip: '부수를 독자층과 출시 계획에 비교하면 출판 결정을 돕습니다.', questions: ['고정 비용은 무엇인가요?', '비율과 금액 중 무엇을 사용하나요?', '왜 손익분기점에 도달하지 못하나요?', '세금과 수수료가 포함되나요?'], steps: ['형식 선택', '비용 입력', '인세 설정', '표시값 비교'] },
49
+ zh: { intro: '这款计算器把售价、版税、出版成本和目标利润放在一起比较,得到收回这本书成本所需的销量。可以直接使用合同和预算中的数字。', method: '每本版税减去可变成本后得到净利润。固定成本除以净利润并向上取整。第二个标记还会加入目标利润。', margin: '封面价格不等于作者收入。合同基数、发行商扣除、印刷和配送会改变每次销售留下的金额。', action: '出版前比较格式、价格和预算,并把输入假设与结果一起保存。', limit: '计算器不了解你的合同、税费、退货、广告或付款时间。', tip: '把销量与读者群和发布计划比较后,这个数字可以帮助你做出出版决定。', questions: ['什么是固定出版成本?', '应该选择百分比还是金额?', '为什么无法达到盈亏平衡?', '是否包含税费?'], steps: ['选择格式', '输入成本', '设置版税', '比较标记'] },
50
+ };
51
+
52
+ function buildSeo(seed: LocaleSeed): ToolLocaleContent<BookRoyaltyUI>['seo'] {
53
+ return [
54
+ { type: 'title', text: seed.intro.split('.')[0], level: 2 }, { type: 'paragraph', html: seed.intro },
55
+ { type: 'title', text: seed.method.split('.')[0], level: 2 }, { type: 'paragraph', html: seed.method },
56
+ { type: 'list', items: seed.steps },
57
+ { type: 'title', text: seed.margin.split('.')[0], level: 2 }, { type: 'paragraph', html: seed.margin },
58
+ { type: 'title', text: seed.action.split('.')[0], level: 2 }, { type: 'paragraph', html: seed.action },
59
+ { type: 'tip', title: seed.tip.split('.')[0], html: `${seed.limit} ${seed.tip}` },
60
+ { type: 'title', text: seed.tip.split('.')[0], level: 2 }, { type: 'paragraph', html: seed.tip },
61
+ ] as ToolLocaleContent<BookRoyaltyUI>['seo'];
62
+ }
63
+
64
+ function makeContent(locale: string): ToolLocaleContent<BookRoyaltyUI> {
65
+ const seed = seeds[locale];
66
+ if (!seed || !titles[locale] || !descriptions[locale] || !slugs[locale]) throw new Error(`Missing locale bundle: ${locale}`);
67
+ const questions = seed.questions.map((question, index) => ({ question, answer: [seed.limit, seed.method, seed.margin, seed.action][index] ?? seed.limit }));
68
+ const howTo = seed.steps.map((step, index) => ({ name: step, text: [seed.intro, seed.method, seed.action, seed.tip][index] ?? seed.action }));
69
+ const localizedUi = Object.assign({}, english.ui, ui[locale]) as BookRoyaltyUI;
70
+ return { ...english, title: titles[locale], description: descriptions[locale], slug: slugs[locale], ui: localizedUi, seo: buildSeo(seed), faq: questions, bibliography: BOOK_ROYALTY_BIBLIOGRAPHY, howTo, schemas: [] };
71
+ }
72
+
73
+ export const localeContent: Record<string, ToolLocaleContent<BookRoyaltyUI>> = Object.fromEntries(Object.keys(titles).map((locale) => [locale, makeContent(locale)]));
@@ -0,0 +1,2 @@
1
+ import { localeContent } from './locales';
2
+ export const content = localeContent.nl;
@@ -0,0 +1,2 @@
1
+ import { localeContent } from './locales';
2
+ export const content = localeContent.pl;
@@ -0,0 +1,2 @@
1
+ import { localeContent } from './locales';
2
+ export const content = localeContent.pt;
@@ -0,0 +1,2 @@
1
+ import { localeContent } from './locales';
2
+ export const content = localeContent.ru;
@@ -0,0 +1,2 @@
1
+ import { localeContent } from './locales';
2
+ export const content = localeContent.sv;
@@ -0,0 +1,2 @@
1
+ import { localeContent } from './locales';
2
+ export const content = localeContent.tr;
@@ -0,0 +1,2 @@
1
+ import { localeContent } from './locales';
2
+ export const content = localeContent.zh;
@@ -0,0 +1,11 @@
1
+ import type { ToolDefinition } from '../../types';
2
+ import { bookRoyaltyBreakEvenCalculator } from './entry';
3
+
4
+ export * from './entry';
5
+
6
+ export const BOOK_ROYALTY_BREAK_EVEN_CALCULATOR_TOOL: ToolDefinition = {
7
+ entry: bookRoyaltyBreakEvenCalculator,
8
+ Component: () => import('./component.astro'),
9
+ SEOComponent: () => import('./seo.astro'),
10
+ BibliographyComponent: () => import('./bibliography.astro'),
11
+ };
@@ -0,0 +1,39 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { calculateBookRoyaltyBreakEven, DEFAULT_ROYALTY_INPUTS, normalizeInputs } from './logic';
3
+
4
+ describe('book royalty break-even logic', () => {
5
+ it('calculates percentage royalties and target copies', () => {
6
+ const result = calculateBookRoyaltyBreakEven(DEFAULT_ROYALTY_INPUTS);
7
+
8
+ expect(result.royaltyPerCopy).toBeCloseTo(10.493);
9
+ expect(result.marginPerCopy).toBeCloseTo(10.493);
10
+ expect(result.breakEvenCopies).toBe(115);
11
+ expect(result.targetCopies).toBe(210);
12
+ expect(result.profitAtBreakEven).toBeGreaterThanOrEqual(0);
13
+ });
14
+
15
+ it('subtracts variable print cost from a fixed per-copy royalty', () => {
16
+ const result = calculateBookRoyaltyBreakEven({ ...DEFAULT_ROYALTY_INPUTS, royaltyMode: 'per-copy', royaltyValue: 8, variableCost: 2.5 });
17
+
18
+ expect(result.royaltyPerCopy).toBe(8);
19
+ expect(result.marginPerCopy).toBe(5.5);
20
+ expect(result.breakEvenCopies).toBe(219);
21
+ });
22
+
23
+ it('returns no break-even when every copy loses money', () => {
24
+ const result = calculateBookRoyaltyBreakEven({ ...DEFAULT_ROYALTY_INPUTS, royaltyMode: 'per-copy', royaltyValue: 2, variableCost: 3 });
25
+
26
+ expect(result.breakEvenCopies).toBeNull();
27
+ expect(result.targetCopies).toBeNull();
28
+ expect(result.revenueAtBreakEven).toBeNull();
29
+ });
30
+
31
+ it('normalizes invalid and negative inputs safely', () => {
32
+ const safe = normalizeInputs({ ...DEFAULT_ROYALTY_INPUTS, fixedCosts: -1, salePrice: Number.NaN, targetProfit: -8, currency: 'AUD' as 'USD' });
33
+
34
+ expect(safe.fixedCosts).toBe(0);
35
+ expect(safe.salePrice).toBe(0);
36
+ expect(safe.targetProfit).toBe(0);
37
+ expect(safe.currency).toBe('USD');
38
+ });
39
+ });
@@ -0,0 +1,91 @@
1
+ export type RoyaltyMode = 'percent' | 'per-copy';
2
+ export type Currency = 'USD' | 'EUR' | 'GBP';
3
+
4
+ export interface BookRoyaltyInputs {
5
+ fixedCosts: number;
6
+ salePrice: number;
7
+ royaltyMode: RoyaltyMode;
8
+ royaltyValue: number;
9
+ variableCost: number;
10
+ targetProfit: number;
11
+ currency: Currency;
12
+ }
13
+
14
+ export interface RoyaltyBreakEvenResult {
15
+ royaltyPerCopy: number;
16
+ marginPerCopy: number;
17
+ breakEvenCopies: number | null;
18
+ targetCopies: number | null;
19
+ revenueAtBreakEven: number | null;
20
+ profitAtBreakEven: number | null;
21
+ targetProfit: number;
22
+ milestones: Array<{ copies: number; revenue: number; profit: number }>;
23
+ }
24
+
25
+ export const DEFAULT_ROYALTY_INPUTS: BookRoyaltyInputs = {
26
+ fixedCosts: 1200,
27
+ salePrice: 14.99,
28
+ royaltyMode: 'percent',
29
+ royaltyValue: 70,
30
+ variableCost: 0,
31
+ targetProfit: 1000,
32
+ currency: 'USD',
33
+ };
34
+
35
+ export const EBOOK_PRESET: BookRoyaltyInputs = { ...DEFAULT_ROYALTY_INPUTS, fixedCosts: 900, salePrice: 4.99, royaltyValue: 70, variableCost: 0, targetProfit: 1200 };
36
+ export const PAPERBACK_PRESET: BookRoyaltyInputs = { ...DEFAULT_ROYALTY_INPUTS, fixedCosts: 1800, salePrice: 16.99, royaltyValue: 60, variableCost: 5.2, targetProfit: 1800 };
37
+
38
+ function positive(value: number): number {
39
+ return Number.isFinite(value) && value > 0 ? value : 0;
40
+ }
41
+
42
+ function nonNegative(value: number): number {
43
+ return Number.isFinite(value) && value >= 0 ? value : 0;
44
+ }
45
+
46
+ function wholeCopies(value: number): number | null {
47
+ return Number.isFinite(value) && value >= 0 ? Math.ceil(value) : null;
48
+ }
49
+
50
+ export function normalizeInputs(inputs: BookRoyaltyInputs): BookRoyaltyInputs {
51
+ return {
52
+ fixedCosts: nonNegative(inputs.fixedCosts),
53
+ salePrice: positive(inputs.salePrice),
54
+ royaltyMode: inputs.royaltyMode === 'per-copy' ? 'per-copy' : 'percent',
55
+ royaltyValue: nonNegative(inputs.royaltyValue),
56
+ variableCost: nonNegative(inputs.variableCost),
57
+ targetProfit: nonNegative(inputs.targetProfit),
58
+ currency: inputs.currency === 'EUR' || inputs.currency === 'GBP' ? inputs.currency : 'USD',
59
+ };
60
+ }
61
+
62
+ export function calculateBookRoyaltyBreakEven(inputs: BookRoyaltyInputs): RoyaltyBreakEvenResult {
63
+ const safe = normalizeInputs(inputs);
64
+ const royaltyPerCopy = safe.royaltyMode === 'percent' ? safe.salePrice * safe.royaltyValue / 100 : safe.royaltyValue;
65
+ const marginPerCopy = royaltyPerCopy - safe.variableCost;
66
+ const breakEvenCopies = marginPerCopy > 0 ? wholeCopies(safe.fixedCosts / marginPerCopy) : null;
67
+ const targetCopies = marginPerCopy > 0 ? wholeCopies((safe.fixedCosts + safe.targetProfit) / marginPerCopy) : null;
68
+ const revenueAtBreakEven = breakEvenCopies === null ? null : breakEvenCopies * safe.salePrice;
69
+ const profitAtBreakEven = breakEvenCopies === null ? null : breakEvenCopies * marginPerCopy - safe.fixedCosts;
70
+ const milestones = [100, 500, 1000].map((copies) => ({
71
+ copies,
72
+ revenue: copies * safe.salePrice,
73
+ profit: copies * marginPerCopy - safe.fixedCosts,
74
+ }));
75
+
76
+ return {
77
+ royaltyPerCopy,
78
+ marginPerCopy,
79
+ breakEvenCopies,
80
+ targetCopies,
81
+ revenueAtBreakEven,
82
+ profitAtBreakEven,
83
+ targetProfit: safe.targetProfit,
84
+ milestones,
85
+ };
86
+ }
87
+
88
+ export function clampNumber(value: number, min: number, max: number, fallback: number): number {
89
+ const safeValue = Number.isFinite(value) ? value : fallback;
90
+ return Math.min(max, Math.max(min, safeValue));
91
+ }
@@ -0,0 +1,12 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { bookRoyaltyBreakEvenCalculator } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props { locale?: KnownLocale }
7
+ const { locale = 'en' } = Astro.props as Props;
8
+ const loader = bookRoyaltyBreakEvenCalculator.i18n[locale] || bookRoyaltyBreakEvenCalculator.i18n.en;
9
+ const content = await loader?.();
10
+ ---
11
+
12
+ {content ? <SEORenderer content={{ locale, sections: content.seo }} /> : null}
@@ -0,0 +1,46 @@
1
+ export type BookRoyaltyUI = Record<string, string> & {
2
+ presetLabel: string;
3
+ ebookPreset: string;
4
+ paperbackPreset: string;
5
+ fixedCostsLabel: string;
6
+ fixedCostsHint: string;
7
+ salePriceLabel: string;
8
+ salePriceHint: string;
9
+ royaltyModeLabel: string;
10
+ royaltyPercentLabel: string;
11
+ royaltyAmountLabel: string;
12
+ royaltyHint: string;
13
+ variableCostLabel: string;
14
+ variableCostHint: string;
15
+ targetProfitLabel: string;
16
+ targetProfitHint: string;
17
+ currencyLabel: string;
18
+ breakEvenLabel: string;
19
+ breakEvenHint: string;
20
+ targetLabel: string;
21
+ targetHint: string;
22
+ marginLabel: string;
23
+ marginHint: string;
24
+ royaltyPerCopyLabel: string;
25
+ fixedCostsOutputLabel: string;
26
+ revenueAtBreakEvenLabel: string;
27
+ sceneLabel: string;
28
+ sceneCaption: string;
29
+ copiesLabel: string;
30
+ statusProfitable: string;
31
+ statusUnprofitable: string;
32
+ statusZero: string;
33
+ statusProfitableHint: string;
34
+ statusUnprofitableHint: string;
35
+ statusZeroHint: string;
36
+ unreachableLabel: string;
37
+ yesLabel: string;
38
+ noLabel: string;
39
+ scenarioLabel: string;
40
+ scenarioCopies: string;
41
+ scenarioRevenue: string;
42
+ scenarioProfit: string;
43
+ scenarioBreakEven: string;
44
+ sourceNote: string;
45
+ copyNote: string;
46
+ };
package/src/tools.ts CHANGED
@@ -4,5 +4,6 @@ import { BOOK_PAGINATION_AND_SPINE_CALCULATOR_TOOL } from './tool/book-paginatio
4
4
  import { BOOK_INTERIOR_MARGIN_AND_GUTTER_PLANNER_TOOL } from './tool/book-interior-margin-and-gutter-planner';
5
5
  import { BOOK_READING_TIME_DEADLINE_PLANNER_TOOL } from './tool/book-reading-time-deadline-planner';
6
6
  import { BOOK_INDEX_PAGE_BUDGET_CALCULATOR_TOOL } from './tool/book-index-page-budget-calculator';
7
+ import { BOOK_ROYALTY_BREAK_EVEN_CALCULATOR_TOOL } from './tool/book-royalty-break-even-calculator';
7
8
 
8
- export const ALL_TOOLS: ToolDefinition[] = [BOOK_PAGINATION_AND_SPINE_CALCULATOR_TOOL, BOOK_INTERIOR_MARGIN_AND_GUTTER_PLANNER_TOOL, BOOK_READING_TIME_DEADLINE_PLANNER_TOOL, BOOK_INDEX_PAGE_BUDGET_CALCULATOR_TOOL];
9
+ export const ALL_TOOLS: ToolDefinition[] = [BOOK_PAGINATION_AND_SPINE_CALCULATOR_TOOL, BOOK_INTERIOR_MARGIN_AND_GUTTER_PLANNER_TOOL, BOOK_READING_TIME_DEADLINE_PLANNER_TOOL, BOOK_INDEX_PAGE_BUDGET_CALCULATOR_TOOL, BOOK_ROYALTY_BREAK_EVEN_CALCULATOR_TOOL];