@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.
- package/.github/workflows/ci.yml +79 -0
- package/.gitignore +1 -0
- package/astro.config.mjs +6 -1
- package/package.json +18 -5
- package/scripts/postbuild.mjs +17 -0
- package/scripts/postinstall.mjs +2 -4
- package/src/category/index.ts +2 -1
- package/src/components/ProductionBreadcrumb.astro +134 -0
- package/src/components/ProductionStructuredData.astro +46 -0
- package/src/components/ProductionWidget.astro +182 -0
- package/src/entries.ts +4 -1
- package/src/i18n/header-ui.ts +15 -0
- package/src/i18n/language-ui.ts +9 -0
- package/src/i18n/languages.ts +24 -0
- package/src/identity/brands.ts +5 -0
- package/src/index.ts +1 -0
- package/src/layouts/ProductionCategoryPage.astro +184 -0
- package/src/layouts/ProductionPage.astro +218 -0
- package/src/layouts/ProductionUtilityPage.astro +283 -0
- package/src/mfe/assets.ts +39 -0
- package/src/mfe/category-ui.ts +34 -0
- package/src/mfe/manifest.ts +45 -0
- package/src/mfe/routes.ts +50 -0
- package/src/pages/[locale]/[utilities]/[categories]/[category]/[slug]/manifest.json.ts +49 -0
- package/src/pages/[locale]/[utilities]/[categories]/[category]/[slug].astro +100 -0
- package/src/pages/[locale]/[utilities]/[categories]/[category].astro +44 -0
- package/src/pages/index.astro +1 -2
- package/src/pages/mfe-sitemaps/[locale]/[vertical]/sitemap.xml.ts +75 -0
- package/src/pages/utilidades/[slug]/manifest.json.ts +28 -0
- package/src/pages/utilidades/[slug].astro +90 -0
- package/src/pages/utilidades/categorias/[category].astro +38 -0
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/mfe_cache_contract.test.ts +14 -0
- package/src/tests/mfe_manifest.test.ts +28 -0
- package/src/tests/registry_contract.test.ts +67 -0
- package/src/tests/tool_validation.test.ts +1 -1
- package/src/tool/book-index-page-budget-calculator/bibliography.astro +6 -0
- package/src/tool/book-index-page-budget-calculator/bibliography.ts +17 -0
- package/src/tool/book-index-page-budget-calculator/book-index-page-budget-calculator.css +478 -0
- package/src/tool/book-index-page-budget-calculator/component.astro +134 -0
- package/src/tool/book-index-page-budget-calculator/controller.ts +114 -0
- package/src/tool/book-index-page-budget-calculator/dom-views.ts +89 -0
- package/src/tool/book-index-page-budget-calculator/entry.ts +39 -0
- package/src/tool/book-index-page-budget-calculator/evaluator.ts +26 -0
- package/src/tool/book-index-page-budget-calculator/i18n/de.ts +35 -0
- package/src/tool/book-index-page-budget-calculator/i18n/en.ts +113 -0
- package/src/tool/book-index-page-budget-calculator/i18n/es.ts +35 -0
- package/src/tool/book-index-page-budget-calculator/i18n/fr.ts +8 -0
- package/src/tool/book-index-page-budget-calculator/i18n/id.ts +35 -0
- package/src/tool/book-index-page-budget-calculator/i18n/it.ts +8 -0
- package/src/tool/book-index-page-budget-calculator/i18n/ja.ts +8 -0
- package/src/tool/book-index-page-budget-calculator/i18n/ko.ts +8 -0
- package/src/tool/book-index-page-budget-calculator/i18n/nl.ts +8 -0
- package/src/tool/book-index-page-budget-calculator/i18n/pl.ts +8 -0
- package/src/tool/book-index-page-budget-calculator/i18n/pt.ts +8 -0
- package/src/tool/book-index-page-budget-calculator/i18n/ru.ts +8 -0
- package/src/tool/book-index-page-budget-calculator/i18n/sv.ts +8 -0
- package/src/tool/book-index-page-budget-calculator/i18n/tr.ts +8 -0
- package/src/tool/book-index-page-budget-calculator/i18n/zh.ts +8 -0
- package/src/tool/book-index-page-budget-calculator/index.ts +11 -0
- package/src/tool/book-index-page-budget-calculator/logic.test.ts +52 -0
- package/src/tool/book-index-page-budget-calculator/logic.ts +86 -0
- package/src/tool/book-index-page-budget-calculator/seo.astro +15 -0
- package/src/tool/book-index-page-budget-calculator/storage.ts +17 -0
- package/src/tool/book-index-page-budget-calculator/ui.ts +41 -0
- package/src/tools.ts +2 -1
- package/src/types.ts +4 -6
- package/src/worker.ts +27 -0
- package/tsconfig.json +14 -5
- package/src/pages/[locale]/[slug].astro +0 -164
- package/src/pages/[locale].astro +0 -251
|
@@ -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: 'Kata dalam manuskrip', wordsHint: 'Teks utama sebelum bagian belakang', trimWidthLabel: 'Lebar potong', trimHeightLabel: 'Tinggi potong', trimHint: 'Ukuran halaman jadi', unitLabel: 'Satuan halaman', metricButtonLabel: 'Metrik cm', imperialButtonLabel: 'Imperial inci', densityLabel: 'Kepadatan tipografi', densityHint: 'Mengatur kata dan entri indeks per halaman', airyLabel: 'Renggang', standardLabel: 'Standar', compactLabel: 'Padat', indexEntriesLabel: 'Perkiraan entri indeks', indexEntriesHint: 'Topik, nama, tempat, dan rujuk silang', targetIndexPagesLabel: 'Halaman indeks yang ditargetkan', targetIndexPagesHint: 'Alokasi produksi untuk indeks', textPagesLabel: 'Halaman isi', estimatedIndexPagesLabel: 'Perkiraan indeks', totalPagesLabel: 'Total buku yang diperkirakan', targetTotalPagesLabel: 'Dengan target indeks', entriesPerPageLabel: 'Entri per halaman target', indexShareLabel: 'Porsi indeks dalam buku', statusBalanced: 'Target sesuai perkiraan', statusTight: 'Target terlalu ketat', statusRoomy: 'Target masih longgar', statusBalancedHint: 'Alokasi indeks sesuai dengan model kepadatan.', statusTightHint: 'Tambahkan halaman atau kurangi entri sebelum penyusunan akhir.', statusRoomyHint: 'Kelonggaran ini dapat menampung tambahan editorial di akhir.', sceneLabel: 'Anggaran halaman', sceneBodyLabel: 'halaman isi', sceneIndexLabel: 'halaman indeks', sceneCaption: 'Perkiraan buku menggunakan', sceneTargetCaption: 'Dengan alokasi target, totalnya menjadi', sourceNote: 'Ini adalah model perencanaan editorial. Pagination akhir bergantung pada tata letak, koreksi, gaya indeks, serta arahan penerbit atau percetakan.', summaryLabel: 'Sinyal anggaran', summaryPagesOver: 'halaman di atas target', summaryPagesSpare: 'halaman tersisa', summaryOnTarget: 'Sesuai target',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const faq = [
|
|
11
|
+
{ question: 'Apa yang harus dimasukkan sebagai entri indeks?', answer: 'Masukkan topik, nama, tempat, subentri, dan rujuk silang yang akan muncul dalam indeks akhir.' },
|
|
12
|
+
{ question: 'Mengapa halaman isi dan indeks dihitung terpisah?', answer: 'Isi dipengaruhi jumlah kata dan desain halaman, sedangkan indeks dipengaruhi jumlah entri dan kepadatannya.' },
|
|
13
|
+
{ question: 'Apa arti target yang terlalu ketat?', answer: 'Alokasi tersebut menuntut lebih banyak entri per halaman daripada yang wajar untuk profil yang dipilih.' },
|
|
14
|
+
{ question: 'Apakah hasil ini menggantikan pemeriksaan indeks akhir?', answer: 'Tidak. Ini adalah perkiraan perencanaan. Rujukan halaman harus diperiksa pada proof yang sudah ditata.' },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const howTo = [
|
|
18
|
+
{ name: 'Masukkan manuskrip', text: 'Masukkan jumlah kata teks utama sebelum bagian belakang.' },
|
|
19
|
+
{ name: 'Sesuaikan desain halaman', text: 'Pilih ukuran jadi dan kepadatan tipografi.' },
|
|
20
|
+
{ name: 'Jelaskan indeks', text: 'Perkirakan entri dan alokasikan jumlah halaman.' },
|
|
21
|
+
{ name: 'Tindak lanjuti hasil', text: 'Tambah alokasi atau kurangi entri jika target terlalu ketat.' },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const seo: ToolLocaleContent<BookIndexUI>['seo'] = [
|
|
25
|
+
{ type: 'title', text: 'Rencanakan anggaran halaman indeks buku sejak awal', level: 2 },
|
|
26
|
+
{ type: 'paragraph', html: 'Indeks analitis menambah halaman nyata pada bagian belakang buku cetak. Alat ini menggabungkan kata manuskrip, ukuran halaman, kepadatan tipografi, entri yang diperkirakan, dan alokasi editorial.' },
|
|
27
|
+
{ type: 'title', text: 'Cara membaca perkiraan', level: 2 },
|
|
28
|
+
{ type: 'paragraph', html: 'Halaman isi diperkirakan dari jumlah kata dan kepadatan. Halaman indeks diperkirakan dari jumlah entri dan kapasitas per halaman. Bandingkan perkiraan dengan alokasi sebelum tata letak selesai.' },
|
|
29
|
+
{ type: 'list', items: ['Hitung entri yang diperkirakan, bukan semua kata dalam manuskrip.', 'Gunakan ukuran dan kepadatan yang mendekati desain buku sebenarnya.', 'Sediakan halaman tambahan bila rujuk silang dan subentri banyak.', 'Hitung ulang setelah proof halaman stabil.'] },
|
|
30
|
+
{ type: 'title', text: 'Target ketat adalah sinyal editorial', level: 2 },
|
|
31
|
+
{ type: 'paragraph', html: 'Memaksa terlalu banyak entri dalam sedikit halaman dapat menurunkan keterbacaan. Bersihkan rujukan yang kurang berguna atau tambah ruang sebelum mempersempit tipografi.' },
|
|
32
|
+
{ type: 'tip', title: 'Bukan angka final untuk percetakan', html: 'Font, tata letak, koreksi, dan gaya indeks memengaruhi hasil akhir. Konfirmasikan dengan penerbit, percetakan, atau pengindeks profesional.' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export const content = { ...base, slug: 'kalkulator-anggaran-halaman-indeks-buku', title: 'Kalkulator Anggaran Halaman Indeks Buku', description: 'Perkirakan halaman tambahan dari indeks analitis dan bandingkan dengan alokasi editorial buku Anda.', 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: 'Parole del manoscritto', wordsHint: 'Testo principale prima degli allegati', trimWidthLabel: 'Larghezza rifilata', trimHeightLabel: 'Altezza rifilata', trimHint: 'Formato finito della pagina', unitLabel: 'Unita della pagina', metricButtonLabel: 'Metrico cm', imperialButtonLabel: 'Imperiale pollici', densityLabel: 'Densita tipografica', densityHint: 'Controlla parole e voci indice per pagina', airyLabel: 'Aria', standardLabel: 'Standard', compactLabel: 'Compatta', indexEntriesLabel: 'Voci indice previste', indexEntriesHint: 'Argomenti, nomi, luoghi e rinvii', targetIndexPagesLabel: 'Pagine indice previste', targetIndexPagesHint: 'Margine di produzione per l indice', textPagesLabel: 'Pagine di testo', estimatedIndexPagesLabel: 'Indice stimato', totalPagesLabel: 'Totale stimato del libro', targetTotalPagesLabel: 'Con l indice previsto', entriesPerPageLabel: 'Voci per pagina obiettivo', indexShareLabel: 'Quota dell indice nel libro', statusBalanced: 'L obiettivo corrisponde alla stima', statusTight: 'L obiettivo e troppo stretto', statusRoomy: 'L obiettivo lascia margine', statusBalancedHint: 'La riserva prevista corrisponde al modello di densita.', statusTightHint: 'Aggiungi pagine o riduci le voci prima della composizione.', statusRoomyHint: 'Il margine puo assorbire aggiunte editoriali tardive.', sceneLabel: 'Il budget di pagine', sceneBodyLabel: 'pagine di testo', sceneIndexLabel: 'pagine indice', sceneCaption: 'Il libro stimato occupa', sceneTargetCaption: 'Con la tua riserva il totale sarebbe', sourceNote: 'E un modello di pianificazione editoriale. La paginazione finale dipende da composizione, correzioni, stile dell indice e indicazioni dell editore o dello stampatore.', summaryLabel: 'Segnale del budget', summaryPagesOver: 'pagine oltre l obiettivo', summaryPagesSpare: 'pagine disponibili', summaryOnTarget: 'In linea' };
|
|
5
|
+
const faq = [{ question: 'Cosa devo inserire come voci indice?', answer: 'Inserisci argomenti, nomi, luoghi, sottovoci e rinvii previsti nell indice finale.' }, { question: 'Perche testo e indice sono calcolati separatamente?', answer: 'Il testo dipende dalle parole e dal progetto della pagina, mentre l indice dipende dalle voci e dalla densita.' }, { question: 'Cosa significa un obiettivo troppo stretto?', answer: 'La riserva richiederebbe piu voci per pagina di quante il profilo scelto possa contenere comodamente.' }, { question: 'Sostituisce la prova finale dell indice?', answer: 'No. E una stima di pianificazione. Rinvii e paginazione vanno verificati sulla prova impaginata.' }];
|
|
6
|
+
const howTo = [{ name: 'Inserisci il manoscritto', text: 'Indica il numero di parole del testo principale.' }, { name: 'Adatta il progetto', text: 'Scegli formato finito e densita tipografica.' }, { name: 'Descrivi l indice', text: 'Stima le voci e riserva un numero di pagine.' }, { name: 'Agisci sul confronto', text: 'Aumenta la riserva o riduci le voci quando l obiettivo e troppo stretto.' }];
|
|
7
|
+
const seo = [{ type: 'title', text: 'Prevedere il budget di pagine dell indice del libro', level: 2 }, { type: 'paragraph', html: 'Un indice analitico aggiunge pagine reali alla parte finale di un libro stampato. Questo strumento combina parole, formato, densita tipografica, voci previste e riserva editoriale.' }, { type: 'title', text: 'Come leggere la stima', level: 2 }, { type: 'paragraph', html: 'Le pagine di testo dipendono dalle parole e dalla densita. Le pagine indice dipendono dalle voci e dalla capacita per pagina. Confronta la stima con la riserva prima di chiudere l impaginazione.' }, { type: 'list', items: ['Conta le voci previste, non tutte le parole del manoscritto.', 'Usa il formato e la densita del progetto reale.', 'Riserva spazio extra se ci sono molti rinvii e sottovoci.', 'Ricalcola quando la prova di pagina e stabile.'] }, { type: 'title', text: 'Un obiettivo stretto richiede una scelta editoriale', level: 2 }, { type: 'paragraph', html: 'Concentrare troppe voci in poche pagine puo ridurre la leggibilita. Pulisci i rinvii poco utili o aumenta lo spazio prima di comprimere ulteriormente la tipografia.' }, { type: 'tip', title: 'Non e un valore definitivo per la stampa', html: 'Font, composizione, correzioni e stile dell indice cambiano il risultato finale. Confermalo con editore, stampatore o indicizzatore professionale.' }];
|
|
8
|
+
export const content = { ...base, slug: 'calcolatore-budget-pagine-indice-libro', title: 'Calcolatore del budget di pagine dell indice', description: 'Stima le pagine aggiunte da un indice analitico e confrontale con la riserva editoriale più adatta del tuo 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: '原稿の語数', wordsHint: '後付けを除く本文', trimWidthLabel: '仕上がり幅', trimHeightLabel: '仕上がり高さ', trimHint: '完成時のページサイズ', unitLabel: 'ページ単位', metricButtonLabel: 'メートル cm', imperialButtonLabel: 'インチ', densityLabel: '組版の密度', densityHint: '1ページあたりの本文語数と索引項目数を調整', airyLabel: 'ゆったり', standardLabel: '標準', compactLabel: '詰める', indexEntriesLabel: '想定する索引項目数', indexEntriesHint: 'テーマ、名前、場所、参照項目', targetIndexPagesLabel: '確保する索引ページ', targetIndexPagesHint: '索引の制作に割り当てるページ数', textPagesLabel: '本文ページ', estimatedIndexPagesLabel: '索引の見積もり', totalPagesLabel: '本の総ページ数', targetTotalPagesLabel: '目標ページを使った場合', entriesPerPageLabel: '目標ページあたりの項目数', indexShareLabel: '本に占める索引の割合', statusBalanced: '目標は見積もりどおり', statusTight: '目標が厳しすぎます', statusRoomy: '目標に余裕があります', statusBalancedHint: '確保したページ数は密度モデルと一致しています。', statusTightHint: '組版前にページを増やすか項目を減らしてください。', statusRoomyHint: '余裕があるため、後からの編集追加にも対応できます。', sceneLabel: 'ページ予算', sceneBodyLabel: '本文ページ', sceneIndexLabel: '索引ページ', sceneCaption: '見積もり上の本は', sceneTargetCaption: '目標ページを使うと', sourceNote: 'これは編集段階の計画モデルです。最終ページ数は組版、校正、索引の形式、出版社や印刷所の指定で変わります。', summaryLabel: '予算の状態', summaryPagesOver: 'ページ超過', summaryPagesSpare: 'ページの余裕', summaryOnTarget: '目標どおり', };
|
|
5
|
+
const faq = [{ question: '索引項目には何を入力しますか?', answer: '完成した索引に載せるテーマ、名前、場所、細目、参照項目を入力します。' }, { question: '本文と索引を分けて計算する理由は?', answer: '本文は語数とページ設計、索引は項目数と索引の密度によってページ数が決まるためです。' }, { question: '目標が厳しいとはどういう意味ですか?', answer: '選択した密度で無理なく収まる項目数を超えて、1ページに詰める必要があるという意味です。' }, { question: '最終校正の代わりになりますか?', answer: 'いいえ。計画用の見積もりです。ページ参照とページ数は組版後の校正で確認してください。' }];
|
|
6
|
+
const howTo = [{ name: '原稿量を入力する', text: '本文の語数を入力します。' }, { name: 'ページ設計を合わせる', text: '仕上がりサイズと組版の密度を選びます。' }, { name: '索引を見積もる', text: '項目数と確保するページ数を入力します。' }, { name: '差を確認する', text: '目標が厳しければページを増やすか項目を整理します。' }];
|
|
7
|
+
const seo = [{ type: 'title', text: '本の索引ページ数を早めに見積もる', level: 2 }, { type: 'paragraph', html: '索引は印刷された本の後半に実際のページを追加します。このツールは原稿の語数、仕上がりサイズ、組版密度、想定項目数、編集上の予算を一つの計画にまとめます。' }, { type: 'title', text: '結果の読み方', level: 2 }, { type: 'paragraph', html: '本文ページは語数と密度から、索引ページは項目数と1ページの収容力から見積もります。組版を確定する前に、見積もりと確保したページを比較してください。' }, { type: 'list', items: ['原稿の全語数ではなく、想定する索引項目を数える。', '実際の本に近いサイズと密度を使う。', '参照項目や細目が多い場合は余分なページを確保する。', '校正用のページが安定したら再計算する。'] }, { type: 'title', text: '厳しい目標は編集上の判断材料', level: 2 }, { type: 'paragraph', html: '少ないページに項目を詰め込むと、索引の読みやすさが下がります。文字をさらに圧縮する前に、不要な参照を整理するかページを増やしてください。' }, { type: 'tip', title: '印刷用の確定値ではありません', html: '書体、組版、校正、索引形式で最終結果は変わります。出版社、印刷所、専門の索引作成者に確認してください。' }];
|
|
8
|
+
export const content = { ...base, slug: 'book-index-page-budget-calculator', title: '本の索引ページ予算計算機', description: '索引が追加するページ数を見積もり、本の編集上のページ予算と比較します。', 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: '원고 단어 수', wordsHint: '부록을 제외한 본문', trimWidthLabel: '재단 너비', trimHeightLabel: '재단 높이', trimHint: '완성 페이지 크기', unitLabel: '페이지 단위', metricButtonLabel: '미터법 cm', imperialButtonLabel: '인치', densityLabel: '조판 밀도', densityHint: '페이지당 본문 단어와 색인 항목 수를 조정합니다', airyLabel: '여유 있게', standardLabel: '표준', compactLabel: '촘촘하게', indexEntriesLabel: '예상 색인 항목', indexEntriesHint: '주제, 이름, 장소 및 상호 참조', targetIndexPagesLabel: '예약할 색인 페이지', targetIndexPagesHint: '색인 제작에 배정할 페이지 수', textPagesLabel: '본문 페이지', estimatedIndexPagesLabel: '예상 색인', totalPagesLabel: '예상 책 전체', targetTotalPagesLabel: '목표 색인 포함', entriesPerPageLabel: '목표 페이지당 항목', indexShareLabel: '책에서 색인이 차지하는 비율', statusBalanced: '목표가 예상치와 일치합니다', statusTight: '목표가 너무 촘촘합니다', statusRoomy: '목표에 여유가 있습니다', statusBalancedHint: '예약한 색인 분량이 밀도 모델과 일치합니다.', statusTightHint: '최종 조판 전에 페이지를 늘리거나 항목을 줄이세요.', statusRoomyHint: '남은 공간이 늦은 편집 추가를 흡수할 수 있습니다.', sceneLabel: '페이지 예산', sceneBodyLabel: '본문 페이지', sceneIndexLabel: '색인 페이지', sceneCaption: '예상 책은', sceneTargetCaption: '목표 분량을 사용하면', sourceNote: '편집 계획을 위한 모델입니다. 최종 페이지 수는 조판, 교정, 색인 방식, 출판사와 인쇄소의 지침에 따라 달라집니다.', summaryLabel: '예산 신호', summaryPagesOver: '페이지 초과', summaryPagesSpare: '페이지 여유', summaryOnTarget: '목표에 맞음', };
|
|
5
|
+
const faq = [{ question: '색인 항목에는 무엇을 입력하나요?', answer: '완성된 색인에 넣을 주제, 이름, 장소, 하위 항목과 상호 참조를 입력합니다.' }, { question: '본문과 색인 페이지를 따로 계산하는 이유는 무엇인가요?', answer: '본문은 단어 수와 페이지 디자인, 색인은 항목 수와 색인 밀도에 따라 달라지기 때문입니다.' }, { question: '목표가 너무 촘촘하다는 뜻은 무엇인가요?', answer: '선택한 밀도에서 편안하게 담을 수 있는 것보다 많은 항목을 한 페이지에 넣어야 한다는 뜻입니다.' }, { question: '최종 색인 교정을 대신할 수 있나요?', answer: '아니요. 계획용 추정치입니다. 페이지 참조와 페이지 수는 조판 교정본에서 확인해야 합니다.' }];
|
|
6
|
+
const howTo = [{ name: '원고 분량 입력', text: '부록을 제외한 본문 단어 수를 입력합니다.' }, { name: '페이지 디자인 맞추기', text: '완성 크기와 조판 밀도를 선택합니다.' }, { name: '색인 설명하기', text: '항목 수와 예약할 페이지를 추정합니다.' }, { name: '차이 확인하기', text: '목표가 촘촘하면 페이지를 늘리거나 항목을 정리합니다.' }];
|
|
7
|
+
const seo = [{ type: 'title', text: '책 색인 페이지 예산을 미리 계획하세요', level: 2 }, { type: 'paragraph', html: '분석 색인은 인쇄 도서 뒷부분에 실제 페이지를 추가합니다. 이 도구는 원고 단어 수, 판형, 조판 밀도, 예상 항목과 편집 예산을 하나의 계획으로 묶습니다.' }, { type: 'title', text: '결과 읽는 법', level: 2 }, { type: 'paragraph', html: '본문 페이지는 단어 수와 밀도로, 색인 페이지는 항목 수와 페이지당 수용량으로 추정합니다. 조판을 확정하기 전에 추정치와 예약 분량을 비교하세요.' }, { type: 'list', items: ['원고의 모든 단어가 아니라 예상 색인 항목을 셉니다.', '실제 책 디자인에 가까운 크기와 밀도를 사용합니다.', '상호 참조와 하위 항목이 많다면 여분의 페이지를 예약합니다.', '페이지 교정본이 안정되면 다시 계산합니다.'] }, { type: 'title', text: '촘촘한 목표는 편집 판단의 신호입니다', level: 2 }, { type: 'paragraph', html: '적은 페이지에 너무 많은 항목을 넣으면 색인을 읽기 어려워집니다. 글자를 더 줄이기 전에 불필요한 참조를 정리하거나 공간을 늘리세요.' }, { type: 'tip', title: '인쇄용 확정값이 아닙니다', html: '서체, 조판, 교정과 색인 방식에 따라 최종 결과가 달라집니다. 출판사, 인쇄소 또는 전문 색인 작성자에게 확인하세요.' }];
|
|
8
|
+
export const content = { ...base, slug: 'book-index-page-budget-calculator', title: '책 색인 페이지 예산 계산기', description: '분석 색인이 추가할 페이지를 추정하고 책의 편집 페이지 예산과 비교합니다.', 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: 'Woorden in manuscript', wordsHint: 'Hoofdtekst voor het nawerk', trimWidthLabel: 'Snijbreedte', trimHeightLabel: 'Snijhoogte', trimHint: 'Afgewerkt paginaformaat', unitLabel: 'Pagina eenheden', metricButtonLabel: 'Metrisch cm', imperialButtonLabel: 'Imperiaal inch', densityLabel: 'Typografische dichtheid', densityHint: 'Regelt woorden en indexitems per pagina', airyLabel: 'Ruim', standardLabel: 'Standaard', compactLabel: 'Compact', indexEntriesLabel: 'Verwachte indexitems', indexEntriesHint: 'Onderwerpen, namen, plaatsen en kruisverwijzingen', targetIndexPagesLabel: 'Gereserveerde indexpaginas', targetIndexPagesHint: 'Productieruimte voor de index', textPagesLabel: 'Tekstpaginas', estimatedIndexPagesLabel: 'Geschatte index', totalPagesLabel: 'Geschat boektotaal', targetTotalPagesLabel: 'Met indexdoel', entriesPerPageLabel: 'Items per doelpagina', indexShareLabel: 'Aandeel index in boek', statusBalanced: 'Doel komt overeen met schatting', statusTight: 'Doel is te krap', statusRoomy: 'Doel laat ruimte', statusBalancedHint: 'De geplande reserve past bij het dichtheidsmodel.', statusTightHint: 'Reserveer meer paginas of verminder items voor het zetten.', statusRoomyHint: 'De ruimte kan late redactionele toevoegingen opvangen.', sceneLabel: 'Het paginabudget', sceneBodyLabel: 'tekstpaginas', sceneIndexLabel: 'indexpaginas', sceneCaption: 'Het geschatte boek beslaat', sceneTargetCaption: 'Met je doelreserve wordt dat', sourceNote: 'Dit is een redactioneel planningsmodel. De definitieve paginering hangt af van zetwerk, correcties, indexstijl en de uitgever of drukker.', summaryLabel: 'Budgetsignaal', summaryPagesOver: 'paginas boven doel', summaryPagesSpare: 'paginas over', summaryOnTarget: 'Op doel' };
|
|
5
|
+
const faq = [{ question: 'Wat vul ik in als indexitems?', answer: 'Vul onderwerpen, namen, plaatsen, subitems en kruisverwijzingen in die in de uiteindelijke index komen.' }, { question: 'Waarom worden tekst en index apart berekend?', answer: 'De tekst hangt af van woorden en paginadesign, de index van items en indexdichtheid.' }, { question: 'Wat betekent een te krap doel?', answer: 'De reserve zou meer items per pagina vereisen dan het gekozen profiel comfortabel kan dragen.' }, { question: 'Vervangt dit de laatste indexproef?', answer: 'Nee. Het is een planningsschatting. Verwijzingen en paginering moeten in de gezette proef worden gecontroleerd.' }];
|
|
6
|
+
const howTo = [{ name: 'Manuscript invoeren', text: 'Geef het aantal woorden van de hoofdtekst op.' }, { name: 'Paginaontwerp afstemmen', text: 'Kies het afgewerkte formaat en de typografische dichtheid.' }, { name: 'Index beschrijven', text: 'Schat de items en reserveer paginas.' }, { name: 'Het verschil beoordelen', text: 'Vergroot de reserve of verminder items als het doel te krap is.' }];
|
|
7
|
+
const seo = [{ type: 'title', text: 'Plan het paginabudget van een boekindex vroeg', level: 2 }, { type: 'paragraph', html: 'Een analytische index voegt echte paginas toe aan het nawerk van een gedrukt boek. Deze tool combineert manuscriptwoorden, formaat, typografische dichtheid, verwachte items en redactionele reserve.' }, { type: 'title', text: 'De schatting lezen', level: 2 }, { type: 'paragraph', html: 'Tekstpaginas worden geschat vanuit woorden en dichtheid. Indexpaginas komen uit items en capaciteit per pagina. Vergelijk de schatting met de reserve voordat het zetwerk wordt gesloten.' }, { type: 'list', items: ['Tel verwachte indexitems, niet alle woorden in het manuscript.', 'Gebruik formaat en dichtheid van het echte boekontwerp.', 'Reserveer extra ruimte bij veel kruisverwijzingen en subitems.', 'Bereken opnieuw zodra de paginaproef stabiel is.'] }, { type: 'title', text: 'Een krap doel vraagt om een redactionele keuze', level: 2 }, { type: 'paragraph', html: 'Te veel items op weinig paginas maken een index moeilijk te doorzoeken. Ruim zwakke verwijzingen op of vergroot de ruimte voordat je de typografie verder samenperst.' }, { type: 'tip', title: 'Geen definitief drukkersgetal', html: 'Lettertype, zetwerk, correcties en indexstijl veranderen het eindresultaat. Controleer het bij uitgever, drukker of professionele indexmaker.' }];
|
|
8
|
+
export const content = { ...base, slug: 'calculator-index-paginabudget-boek', title: 'Calculator voor het paginabudget van een boekindex', description: 'Schat de extra paginas van een analytische index en vergelijk ze met je redactionele paginabudget.', 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: 'Liczba słów rękopisu', wordsHint: 'Tekst główny przed dodatkami', trimWidthLabel: 'Szerokość po obcięciu', trimHeightLabel: 'Wysokość po obcięciu', trimHint: 'Gotowy format strony', unitLabel: 'Jednostki strony', metricButtonLabel: 'Metryczne cm', imperialButtonLabel: 'Calowe', densityLabel: 'Gęstość typografii', densityHint: 'Steruje liczbą słów i haseł indeksu na stronie', airyLabel: 'Luźna', standardLabel: 'Standardowa', compactLabel: 'Zwarta', indexEntriesLabel: 'Przewidywane hasła indeksu', indexEntriesHint: 'Tematy, nazwiska, miejsca i odsyłacze', targetIndexPagesLabel: 'Zarezerwowane strony indeksu', targetIndexPagesHint: 'Limit produkcyjny na indeks', textPagesLabel: 'Strony tekstu', estimatedIndexPagesLabel: 'Szacowany indeks', totalPagesLabel: 'Szacowana liczba stron książki', targetTotalPagesLabel: 'Z docelowym indeksem', entriesPerPageLabel: 'Haseł na stronę docelową', indexShareLabel: 'Udział indeksu w książce', statusBalanced: 'Cel odpowiada szacunkom', statusTight: 'Cel jest zbyt ciasny', statusRoomy: 'Cel zostawia zapas', statusBalancedHint: 'Zaplanowany limit odpowiada modelowi gęstości.', statusTightHint: 'Dodaj strony lub zmniejsz liczbę haseł przed składem.', statusRoomyHint: 'Zapas może pomieścić późniejsze dodatki redakcyjne.', sceneLabel: 'Budżet stron', sceneBodyLabel: 'strony tekstu', sceneIndexLabel: 'strony indeksu', sceneCaption: 'Szacowana książka zajmuje', sceneTargetCaption: 'Przy zaplanowanym limicie będzie mieć', sourceNote: 'To model planowania redakcyjnego. Ostateczna paginacja zależy od składu, korekt, stylu indeksu oraz wytycznych wydawcy lub drukarni.', summaryLabel: 'Sygnał budżetu', summaryPagesOver: 'stron ponad cel', summaryPagesSpare: 'stron zapasu', summaryOnTarget: 'Zgodnie z celem', };
|
|
5
|
+
const faq = [{ question: 'Co wpisać jako hasła indeksu?', answer: 'Wpisz tematy, nazwiska, miejsca, podhasła i odsyłacze, które mają znaleźć się w gotowym indeksie.' }, { question: 'Dlaczego strony tekstu i indeksu liczą się osobno?', answer: 'Tekst zależy od liczby słów i projektu strony, a indeks od liczby haseł i jego gęstości.' }, { question: 'Co oznacza zbyt ciasny cel?', answer: 'Limit wymagałby upchnięcia na stronie większej liczby haseł, niż wygodnie mieści wybrany profil.' }, { question: 'Czy wynik zastępuje końcową korektę indeksu?', answer: 'Nie. To szacunek do planowania. Odsyłacze i paginację trzeba sprawdzić na złożonej korekcie.' }];
|
|
6
|
+
const howTo = [{ name: 'Wpisz objętość rękopisu', text: 'Podaj liczbę słów tekstu głównego.' }, { name: 'Dopasuj projekt strony', text: 'Wybierz format końcowy i gęstość typografii.' }, { name: 'Opisz indeks', text: 'Oszacuj hasła i zarezerwuj strony.' }, { name: 'Zareaguj na różnicę', text: 'Zwiększ limit lub uporządkuj hasła, gdy cel jest zbyt ciasny.' }];
|
|
7
|
+
const seo = [{ type: 'title', text: 'Wcześnie zaplanuj budżet stron indeksu książki', level: 2 }, { type: 'paragraph', html: 'Indeks analityczny dodaje rzeczywiste strony do końcowej części drukowanej książki. To narzędzie łączy słowa rękopisu, format, gęstość typografii, przewidywane hasła i rezerwę redakcyjną.' }, { type: 'title', text: 'Jak czytać szacunek', level: 2 }, { type: 'paragraph', html: 'Strony tekstu wynikają ze słów i gęstości. Strony indeksu wynikają z liczby haseł i pojemności strony. Porównaj szacunek z rezerwą przed zamknięciem składu.' }, { type: 'list', items: ['Licz przewidywane hasła, a nie wszystkie słowa rękopisu.', 'Używaj formatu i gęstości planowanego wnętrza.', 'Zarezerwuj więcej miejsca przy licznych odsyłaczach i podhasłach.', 'Przelicz ponownie, gdy korekta stron będzie stabilna.'] }, { type: 'title', text: 'Ciasny cel jest sygnałem redakcyjnym', level: 2 }, { type: 'paragraph', html: 'Zbyt wiele haseł na kilku stronach obniża czytelność indeksu. Uporządkuj słabe odsyłacze lub zwiększ przestrzeń, zanim jeszcze bardziej ściśniesz typografię.' }, { type: 'tip', title: 'To nie jest ostateczna wartość dla drukarni', html: 'Krój pisma, skład, korekty i styl indeksu zmienią końcowy wynik. Potwierdź go z wydawcą, drukarnią lub profesjonalnym indeksatorem.' }];
|
|
8
|
+
export const content = { ...base, slug: 'kalkulator-budzetu-stron-indeksu-ksiazki', title: 'Kalkulator budżetu stron indeksu książki', description: 'Oszacuj strony dodane przez indeks analityczny i porównaj je z redakcyjnym budżetem książki.', 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: 'Palavras do manuscrito', wordsHint: 'Texto principal antes dos anexos', trimWidthLabel: 'Largura de corte', trimHeightLabel: 'Altura de corte', trimHint: 'Formato final da página', unitLabel: 'Unidades da página', metricButtonLabel: 'Métrico cm', imperialButtonLabel: 'Imperial polegadas', densityLabel: 'Densidade tipográfica', densityHint: 'Controla palavras e entradas por página', airyLabel: 'Arejada', standardLabel: 'Padrão', compactLabel: 'Compacta', indexEntriesLabel: 'Entradas de índice previstas', indexEntriesHint: 'Temas, nomes, lugares e referências cruzadas', targetIndexPagesLabel: 'Páginas de índice reservadas', targetIndexPagesHint: 'A sua margem de produção para o índice', textPagesLabel: 'Páginas de texto', estimatedIndexPagesLabel: 'Índice estimado', totalPagesLabel: 'Total estimado do livro', targetTotalPagesLabel: 'Com o índice reservado', entriesPerPageLabel: 'Entradas por página-alvo', indexShareLabel: 'Parte do índice no livro', statusBalanced: 'O alvo corresponde à estimativa', statusTight: 'O alvo é apertado demais', statusRoomy: 'O alvo deixa margem', statusBalancedHint: 'A reserva prevista corresponde ao modelo de densidade.', statusTightHint: 'Reserve mais páginas ou reduza entradas antes da composição.', statusRoomyHint: 'A margem pode absorver acréscimos editoriais tardios.', sceneLabel: 'O orçamento de páginas', sceneBodyLabel: 'páginas de texto', sceneIndexLabel: 'páginas de índice', sceneCaption: 'O livro estimado ocupa', sceneTargetCaption: 'Com a sua reserva, o total seria', sourceNote: 'Este é um modelo de planeamento editorial. A paginação final depende da composição, das correções, do estilo do índice e das instruções da editora ou gráfica.', summaryLabel: 'Sinal do orçamento', summaryPagesOver: 'páginas acima do alvo', summaryPagesSpare: 'páginas de margem', summaryOnTarget: 'Dentro do alvo', };
|
|
5
|
+
const faq = [{ question: 'O que devo inserir como entradas do índice?', answer: 'Insira os temas, nomes, lugares, subentradas e referências cruzadas que espera incluir no índice final.' }, { question: 'Por que o texto e o índice são calculados separadamente?', answer: 'O texto depende das palavras e do design da página; o índice depende das entradas e da sua densidade.' }, { question: 'O que significa um alvo apertado demais?', answer: 'A reserva exigiria mais entradas por página do que o perfil escolhido consegue acomodar confortavelmente.' }, { question: 'Isto substitui a prova final do índice?', answer: 'Não. É uma estimativa de planeamento. As referências e a paginação devem ser conferidas na prova composta.' }];
|
|
6
|
+
const howTo = [{ name: 'Introduza o manuscrito', text: 'Indique as palavras do texto principal.' }, { name: 'Ajuste o design da página', text: 'Escolha o formato final e a densidade tipográfica.' }, { name: 'Descreva o índice', text: 'Estime as entradas e reserve páginas.' }, { name: 'Aja sobre a comparação', text: 'Aumente a reserva ou reduza entradas quando o alvo for apertado.' }];
|
|
7
|
+
const seo = [{ type: 'title', text: 'Planeie cedo o orçamento de páginas do índice', level: 2 }, { type: 'paragraph', html: 'Um índice analítico acrescenta páginas reais à parte final de um livro impresso. Esta ferramenta combina palavras, formato, densidade tipográfica, entradas previstas e reserva editorial.' }, { type: 'title', text: 'Como ler a estimativa', level: 2 }, { type: 'paragraph', html: 'As páginas de texto dependem das palavras e da densidade. As páginas de índice dependem das entradas e da capacidade por página. Compare a estimativa com a reserva antes de fechar a composição.' }, { type: 'list', items: ['Conte entradas previstas, não todas as palavras do manuscrito.', 'Use o formato e a densidade do design real.', 'Reserve espaço extra quando houver muitas referências cruzadas e subentradas.', 'Volte a calcular quando a prova de páginas estiver estável.'] }, { type: 'title', text: 'Um alvo apertado exige uma decisão editorial', level: 2 }, { type: 'paragraph', html: 'Colocar entradas demais em poucas páginas dificulta a consulta. Limpe referências fracas ou aumente o espaço antes de comprimir ainda mais a tipografia.' }, { type: 'tip', title: 'Não é um valor final para a gráfica', html: 'Fonte, composição, correções e estilo do índice alteram o resultado final. Confirme-o com a editora, a gráfica ou um indexador profissional.' }];
|
|
8
|
+
export const content = { ...base, slug: 'calculadora-orcamento-paginas-indice-livro', title: 'Calculadora do orçamento de páginas do índice', description: 'Estime as páginas acrescentadas por um índice analítico e compare-as com a reserva editorial do livro.', 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: 'Слов в рукописи', wordsHint: 'Основной текст до приложений', trimWidthLabel: 'Обрезная ширина', trimHeightLabel: 'Обрезная высота', trimHint: 'Готовый размер страницы', unitLabel: 'Единицы страницы', metricButtonLabel: 'Метрические см', imperialButtonLabel: 'Дюймы', densityLabel: 'Типографская плотность', densityHint: 'Настраивает число слов и записей указателя на странице', airyLabel: 'Свободная', standardLabel: 'Стандартная', compactLabel: 'Плотная', indexEntriesLabel: 'Ожидаемые записи указателя', indexEntriesHint: 'Темы, имена, места и перекрёстные ссылки', targetIndexPagesLabel: 'Зарезервированные страницы указателя', targetIndexPagesHint: 'Производственный лимит указателя', textPagesLabel: 'Страницы текста', estimatedIndexPagesLabel: 'Расчёт указателя', totalPagesLabel: 'Расчётный объём книги', targetTotalPagesLabel: 'С целевым указателем', entriesPerPageLabel: 'Записей на целевую страницу', indexShareLabel: 'Доля указателя в книге', statusBalanced: 'Цель соответствует расчёту', statusTight: 'Цель слишком мала', statusRoomy: 'Цель оставляет запас', statusBalancedHint: 'Запланированный объём соответствует модели плотности.', statusTightHint: 'Добавьте страницы или сократите записи до начала вёрстки.', statusRoomyHint: 'Запас может вместить поздние редакторские добавления.', sceneLabel: 'Бюджет страниц', sceneBodyLabel: 'страницы текста', sceneIndexLabel: 'страницы указателя', sceneCaption: 'Расчётный объём книги -', sceneTargetCaption: 'С целевым объёмом указателя -', sourceNote: 'Это редакционная модель планирования. Итоговая пагинация зависит от вёрстки, корректур, стиля указателя и требований издателя или типографии.', summaryLabel: 'Сигнал бюджета', summaryPagesOver: 'стр. сверх цели', summaryPagesSpare: 'стр. запаса', summaryOnTarget: 'В пределах цели', };
|
|
5
|
+
const faq = [{ question: 'Что вводить в поле записей указателя?', answer: 'Введите темы, имена, места, подзаписи и перекрёстные ссылки, которые войдут в готовый указатель.' }, { question: 'Почему текст и указатель считаются отдельно?', answer: 'Текст зависит от числа слов и дизайна страницы, а указатель - от записей и плотности указателя.' }, { question: 'Что означает слишком маленькая цель?', answer: 'Чтобы уложиться в резерв, пришлось бы разместить на странице больше записей, чем позволяет выбранный профиль.' }, { question: 'Заменяет ли результат финальную корректуру?', answer: 'Нет. Это планировочная оценка. Ссылки и пагинацию нужно проверить на свёрстанной корректуре.' }];
|
|
6
|
+
const howTo = [{ name: 'Введите объём рукописи', text: 'Укажите число слов основного текста.' }, { name: 'Настройте дизайн страницы', text: 'Выберите готовый размер и типографскую плотность.' }, { name: 'Опишите указатель', text: 'Оцените записи и зарезервируйте страницы.' }, { name: 'Используйте сравнение', text: 'Увеличьте резерв или сократите записи, если цель слишком мала.' }];
|
|
7
|
+
const seo = [{ type: 'title', text: 'Заранее рассчитайте бюджет страниц указателя книги', level: 2 }, { type: 'paragraph', html: 'Предметный указатель добавляет реальные страницы в конец печатной книги. Инструмент объединяет объём рукописи, формат, типографскую плотность, ожидаемые записи и редакционный резерв.' }, { type: 'title', text: 'Как читать расчёт', level: 2 }, { type: 'paragraph', html: 'Страницы текста зависят от слов и плотности. Страницы указателя зависят от числа записей и вместимости страницы. Сравните расчёт с резервом до завершения вёрстки.' }, { type: 'list', items: ['Считайте записи, а не все слова рукописи.', 'Используйте формат и плотность настоящего макета.', 'Зарезервируйте больше места при большом числе ссылок и подзаписей.', 'Пересчитайте результат после стабилизации корректуры.'] }, { type: 'title', text: 'Маленькая цель требует редакторского решения', level: 2 }, { type: 'paragraph', html: 'Слишком плотный указатель сложнее просматривать. Сначала уберите слабые ссылки или увеличьте резерв, а уже потом уплотняйте типографику.' }, { type: 'tip', title: 'Это не финальная цифра для типографии', html: 'Шрифт, вёрстка, корректуры и стиль указателя меняют результат. Подтвердите его с издателем, типографией или профессиональным составителем указателя.' }];
|
|
8
|
+
export const content = { ...base, slug: 'raschet-byudzheta-stranits-ukazatelya-knigi', title: 'Калькулятор бюджета страниц указателя книги', description: 'Рассчитайте дополнительные страницы предметного указателя и сравните их с редакционным резервом книги.', 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: 'Ord i manuskriptet', wordsHint: 'Brödtext före eftertext', trimWidthLabel: 'Skärbredd', trimHeightLabel: 'Skärhöjd', trimHint: 'Färdigt sidformat', unitLabel: 'Sidenheter', metricButtonLabel: 'Metriskt cm', imperialButtonLabel: 'Tum', densityLabel: 'Typografisk täthet', densityHint: 'Styr ord och indexposter per sida', airyLabel: 'Luftig', standardLabel: 'Standard', compactLabel: 'Tät', indexEntriesLabel: 'Förväntade indexposter', indexEntriesHint: 'Ämnen, namn, platser och korshänvisningar', targetIndexPagesLabel: 'Reserverade indexsidor', targetIndexPagesHint: 'Produktionsutrymme för indexet', textPagesLabel: 'Textsidor', estimatedIndexPagesLabel: 'Beräknat index', totalPagesLabel: 'Beräknat antal boksidor', targetTotalPagesLabel: 'Med indexmålet', entriesPerPageLabel: 'Poster per målsida', indexShareLabel: 'Indexets andel av boken', statusBalanced: 'Målet motsvarar uppskattningen', statusTight: 'Målet är för snävt', statusRoomy: 'Målet lämnar utrymme', statusBalancedHint: 'Den planerade reserven stämmer med täthetsmodellen.', statusTightHint: 'Lägg till sidor eller minska poster före sättningen.', statusRoomyHint: 'Utrymmet kan ta emot sena redaktionella tillägg.', sceneLabel: 'Sidbudgeten', sceneBodyLabel: 'textsidor', sceneIndexLabel: 'indexsidor', sceneCaption: 'Den beräknade boken omfattar', sceneTargetCaption: 'Med din reserv blir totalen', sourceNote: 'Detta är en redaktionell planeringsmodell. Den slutliga pagineringen beror på sättning, korrektur, indexstil och instruktioner från förlag eller tryckeri.', summaryLabel: 'Budgetsignal', summaryPagesOver: 'sidor över målet', summaryPagesSpare: 'sidor tillgodo', summaryOnTarget: 'På målet', };
|
|
5
|
+
const faq = [{ question: 'Vad ska jag ange som indexposter?', answer: 'Ange ämnen, namn, platser, underposter och korshänvisningar som ska finnas i det färdiga indexet.' }, { question: 'Varför beräknas text och index separat?', answer: 'Texten styrs av ord och sidlayout, medan indexet styrs av poster och indextäthet.' }, { question: 'Vad betyder ett för snävt mål?', answer: 'Reserven skulle kräva fler poster per sida än den valda profilen rimligen rymmer.' }, { question: 'Ersätter detta den slutliga indexkorrekturen?', answer: 'Nej. Det är en planeringsuppskattning. Hänvisningar och paginering måste kontrolleras i det satta korrekturet.' }];
|
|
6
|
+
const howTo = [{ name: 'Ange manuskriptet', text: 'Skriv in antalet ord i brödtexten.' }, { name: 'Anpassa sidlayouten', text: 'Välj färdigt format och typografisk täthet.' }, { name: 'Beskriv indexet', text: 'Uppskatta poster och reservera sidor.' }, { name: 'Agera på skillnaden', text: 'Öka reserven eller minska poster när målet är för snävt.' }];
|
|
7
|
+
const seo = [{ type: 'title', text: 'Planera bokens indexbudget tidigt', level: 2 }, { type: 'paragraph', html: 'Ett analytiskt index lägger till verkliga sidor i slutet av en tryckt bok. Verktyget kombinerar manuskriptord, format, typografisk täthet, förväntade poster och redaktionell reserv.' }, { type: 'title', text: 'Så läser du uppskattningen', level: 2 }, { type: 'paragraph', html: 'Textsidor beräknas från ord och täthet. Indexsidor beräknas från poster och kapacitet per sida. Jämför uppskattningen med reserven innan sättningen låses.' }, { type: 'list', items: ['Räkna förväntade poster, inte alla manuskriptord.', 'Använd format och täthet från den riktiga inredningen.', 'Reservera extra utrymme vid många korshänvisningar och underposter.', 'Räkna om när sidkorrekturet har stabiliserats.'] }, { type: 'title', text: 'Ett snävt mål är en redaktionell signal', level: 2 }, { type: 'paragraph', html: 'För många poster på få sidor gör indexet svårare att använda. Rensa svaga hänvisningar eller öka utrymmet innan du pressar ihop typografin ytterligare.' }, { type: 'tip', title: 'Inte ett slutligt tryckvärde', html: 'Typsnitt, sättning, korrektur och indexstil ändrar slutresultatet. Bekräfta det med förlag, tryckeri eller professionell indexerare.' }];
|
|
8
|
+
export const content = { ...base, slug: 'budget-for-bokens-indexsidor', title: 'Kalkylator för bokens indexbudget', description: 'Uppskatta indexets extra sidor och jämför dem med bokens redaktionella sidbudget.', 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: 'El yazması kelimeleri', wordsHint: 'Eklerden önceki ana metin', trimWidthLabel: 'Kesim genişliği', trimHeightLabel: 'Kesim yüksekliği', trimHint: 'Bitmiş sayfa boyutu', unitLabel: 'Sayfa birimleri', metricButtonLabel: 'Metrik cm', imperialButtonLabel: 'İnç', densityLabel: 'Tipografik yoğunluk', densityHint: 'Sayfa başına kelime ve dizin maddelerini ayarlar', airyLabel: 'Seyrek', standardLabel: 'Standart', compactLabel: 'Sıkı', indexEntriesLabel: 'Beklenen dizin maddeleri', indexEntriesHint: 'Konular, adlar, yerler ve çapraz göndermeler', targetIndexPagesLabel: 'Ayrılan dizin sayfaları', targetIndexPagesHint: 'Dizin için üretim payı', textPagesLabel: 'Metin sayfaları', estimatedIndexPagesLabel: 'Tahmini dizin', totalPagesLabel: 'Tahmini kitap toplamı', targetTotalPagesLabel: 'Hedef dizinle', entriesPerPageLabel: 'Hedef sayfa başına madde', indexShareLabel: 'Dizinin kitaptaki payı', statusBalanced: 'Hedef tahminle uyumlu', statusTight: 'Hedef çok sıkı', statusRoomy: 'Hedef alan bırakıyor', statusBalancedHint: 'Planlanan pay yoğunluk modeliyle uyumlu.', statusTightHint: 'Dizgi öncesinde sayfa ekleyin veya madde sayısını azaltın.', statusRoomyHint: 'Pay, geç eklenen editoryal içerikleri karşılayabilir.', sceneLabel: 'Sayfa bütçesi', sceneBodyLabel: 'metin sayfası', sceneIndexLabel: 'dizin sayfası', sceneCaption: 'Tahmini kitap', sceneTargetCaption: 'Hedef payla toplam', sourceNote: 'Bu bir editoryal planlama modelidir. Son sayfa sayısı dizgiye, düzeltmelere, dizin stiline ve yayınevi ya da matbaa talimatlarına bağlıdır.', summaryLabel: 'Bütçe sinyali', summaryPagesOver: 'sayfa hedef üstü', summaryPagesSpare: 'sayfa boşluk', summaryOnTarget: 'Hedefte', };
|
|
5
|
+
const faq = [{ question: 'Dizin maddesi olarak ne girmeliyim?', answer: 'Son dizinde yer alacak konu, ad, yer, alt madde ve çapraz göndermeleri girin.' }, { question: 'Metin ve dizin sayfaları neden ayrı hesaplanır?', answer: 'Metin kelime sayısı ve sayfa tasarımına, dizin ise madde sayısı ve yoğunluğuna bağlıdır.' }, { question: 'Hedefin çok sıkı olması ne demektir?', answer: 'Seçilen profilin rahatça taşıyabileceğinden daha fazla maddeyi bir sayfaya sığdırmanız gerekir.' }, { question: 'Bu sonuç son dizin provasının yerini tutar mı?', answer: 'Hayır. Bu bir planlama tahminidir. Göndermeler ve sayfalar dizgili prova üzerinde kontrol edilmelidir.' }];
|
|
6
|
+
const howTo = [{ name: 'El yazmasını girin', text: 'Ana metnin kelime sayısını yazın.' }, { name: 'Sayfa tasarımını eşleştirin', text: 'Bitmiş boyutu ve tipografik yoğunluğu seçin.' }, { name: 'Dizini tanımlayın', text: 'Madde sayısını tahmin edip sayfa ayırın.' }, { name: 'Karşılaştırmayı kullanın', text: 'Hedef sıkıysa payı artırın veya maddeleri azaltın.' }];
|
|
7
|
+
const seo = [{ type: 'title', text: 'Kitap dizininin sayfa bütçesini erkenden planlayın', level: 2 }, { type: 'paragraph', html: 'Analitik dizin, basılı kitabın sonunda gerçek sayfalar oluşturur. Bu araç el yazması kelimelerini, boyutu, tipografik yoğunluğu, beklenen maddeleri ve editoryal payı tek bir planlamada birleştirir.' }, { type: 'title', text: 'Tahmini nasıl okuyabilirsiniz?', level: 2 }, { type: 'paragraph', html: 'Metin sayfaları kelime sayısı ve yoğunluktan, dizin sayfaları madde sayısı ve sayfa kapasitesinden tahmin edilir. Dizgiyi tamamlamadan önce tahmini ayrılan sayfalarla karşılaştırın.' }, { type: 'list', items: ['El yazmasının tüm kelimelerini değil, beklenen dizin maddelerini sayın.', 'Gerçek iç tasarıma yakın boyut ve yoğunluğu kullanın.', 'Çapraz gönderme ve alt maddeler fazlaysa ek sayfa ayırın.', 'Sayfa provası sabitlendiğinde yeniden hesaplayın.'] }, { type: 'title', text: 'Sıkı hedef editoryal bir sinyaldir', level: 2 }, { type: 'paragraph', html: 'Az sayfaya çok madde sığdırmak dizinin taranabilirliğini azaltır. Tipografiyi daha da sıkıştırmadan önce zayıf göndermeleri temizleyin veya alanı artırın.' }, { type: 'tip', title: 'Matbaa için kesin değer değildir', html: 'Yazı tipi, dizgi, düzeltmeler ve dizin stili sonucu değiştirir. Yayınevi, matbaa veya profesyonel dizin hazırlayıcısıyla doğrulayın.' }];
|
|
8
|
+
export const content = { ...base, slug: 'kitap-dizin-sayfa-butcesi-hesaplayici', title: 'Kitap dizini sayfa bütçesi hesaplayıcısı', description: 'Analitik dizinin ekleyeceği sayfaları tahmin edin ve kitabın editoryal sayfa payıyla karşılaştırın.', 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: '稿件字数', wordsHint: '正文,不含书后部分', trimWidthLabel: '成品宽度', trimHeightLabel: '成品高度', trimHint: '页面成品尺寸', unitLabel: '页面单位', metricButtonLabel: '公制 cm', imperialButtonLabel: '英制 英寸', densityLabel: '排版密度', densityHint: '调整每页正文词数和索引条目数', airyLabel: '宽松', standardLabel: '标准', compactLabel: '紧凑', indexEntriesLabel: '预计索引条目', indexEntriesHint: '主题、姓名、地点和交叉引用', targetIndexPagesLabel: '预留索引页数', targetIndexPagesHint: '为索引预留的制作空间', textPagesLabel: '正文页', estimatedIndexPagesLabel: '预计索引页', totalPagesLabel: '预计全书页数', targetTotalPagesLabel: '按目标索引计算', entriesPerPageLabel: '每个目标页的条目数', indexShareLabel: '索引占全书比例', statusBalanced: '目标与估算一致', statusTight: '目标过于紧张', statusRoomy: '目标留有余量', statusBalancedHint: '预留页数与当前密度模型一致。', statusTightHint: '排版前增加页数或减少索引条目。', statusRoomyHint: '余量可以容纳后期的编辑增补。', sceneLabel: '页面预算', sceneBodyLabel: '正文页', sceneIndexLabel: '索引页', sceneCaption: '预计全书为', sceneTargetCaption: '按目标预留则为', sourceNote: '这是编辑计划模型。最终页数会受排版、校样修改、索引样式以及出版社或印刷厂要求影响。', summaryLabel: '预算信号', summaryPagesOver: '页超出目标', summaryPagesSpare: '页余量', summaryOnTarget: '符合目标', };
|
|
5
|
+
const faq = [{ question: '索引条目应该填什么?', answer: '填写预计出现在最终索引中的主题、姓名、地点、子条目和交叉引用。' }, { question: '为什么正文页和索引页要分开计算?', answer: '正文取决于字数和页面设计,索引则取决于条目数量和索引密度。' }, { question: '目标过于紧张是什么意思?', answer: '这表示需要在每页放入超过所选密度模型合理承载量的条目。' }, { question: '它能替代最终索引校样吗?', answer: '不能。这是计划阶段的估算,页码引用和分页必须在排版校样中核对。' }];
|
|
6
|
+
const howTo = [{ name: '输入稿件规模', text: '输入不含书后部分的正文词数。' }, { name: '匹配页面设计', text: '选择成品尺寸和排版密度。' }, { name: '描述索引', text: '估计条目数量并预留页数。' }, { name: '根据差额行动', text: '目标紧张时增加页数或整理条目。' }];
|
|
7
|
+
const seo = [{ type: 'title', text: '尽早规划书籍索引的页面预算', level: 2 }, { type: 'paragraph', html: '分析索引会为印刷书籍的后半部分增加实际页面。本工具将稿件字数、成品尺寸、排版密度、预计条目和编辑预算放在同一份计划中。' }, { type: 'title', text: '如何阅读估算结果', level: 2 }, { type: 'paragraph', html: '正文页根据字数和密度估算,索引页根据条目数和每页容量估算。在确定排版前,将预计页数与预留页数进行比较。' }, { type: 'list', items: ['统计预计索引条目,而不是稿件中的全部字词。', '使用接近实际内页设计的尺寸和密度。', '交叉引用和子条目较多时预留额外页面。', '页面校样稳定后重新计算。'] }, { type: 'title', text: '紧张的目标是编辑决策信号', level: 2 }, { type: 'paragraph', html: '把过多条目塞进少量页面会降低索引的可读性。在进一步压缩排版前,先清理低价值引用或增加页面空间。' }, { type: 'tip', title: '不是印刷最终值', html: '字体、排版、校对和索引样式都会改变最终结果。请与出版社、印刷厂或专业索引编制人员确认。' }];
|
|
8
|
+
export const content = { ...base, slug: 'book-index-page-budget-calculator', title: '书籍索引页面预算计算器', description: '估算分析索引会增加的页面,并与书籍的编辑页面预算进行比较。', ui, seo, faq, howTo };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../types';
|
|
2
|
+
import { bookIndexPageBudgetCalculator } from './entry';
|
|
3
|
+
|
|
4
|
+
export * from './entry';
|
|
5
|
+
|
|
6
|
+
export const BOOK_INDEX_PAGE_BUDGET_CALCULATOR_TOOL: ToolDefinition = {
|
|
7
|
+
entry: bookIndexPageBudgetCalculator,
|
|
8
|
+
Component: () => import('./component.astro'),
|
|
9
|
+
SEOComponent: () => import('./seo.astro'),
|
|
10
|
+
BibliographyComponent: () => import('./bibliography.astro'),
|
|
11
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
calculateIndexBudget,
|
|
4
|
+
centimetersToInches,
|
|
5
|
+
clampNumber,
|
|
6
|
+
inchesToCentimeters,
|
|
7
|
+
DEFAULT_INDEX_INPUTS,
|
|
8
|
+
} from './logic';
|
|
9
|
+
|
|
10
|
+
describe('book index page budget logic', () => {
|
|
11
|
+
it('estimates body pages, index pages, and totals for the default brief', () => {
|
|
12
|
+
const result = calculateIndexBudget(DEFAULT_INDEX_INPUTS);
|
|
13
|
+
|
|
14
|
+
expect(result.textPages).toBe(188);
|
|
15
|
+
expect(result.estimatedIndexPages).toBe(18);
|
|
16
|
+
expect(result.totalPages).toBe(206);
|
|
17
|
+
expect(result.targetTotalPages).toBe(197);
|
|
18
|
+
expect(result.entriesPerPage).toBe(46);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('responds to airy, compact, and large trim profiles', () => {
|
|
22
|
+
const airy = calculateIndexBudget({ ...DEFAULT_INDEX_INPUTS, density: 'airy' });
|
|
23
|
+
const compact = calculateIndexBudget({ ...DEFAULT_INDEX_INPUTS, density: 'compact' });
|
|
24
|
+
const large = calculateIndexBudget({ ...DEFAULT_INDEX_INPUTS, trimWidthInches: 8, trimHeightInches: 10 });
|
|
25
|
+
|
|
26
|
+
expect(airy.wordsPerPage).toBe(320);
|
|
27
|
+
expect(compact.entriesPerPage).toBe(60);
|
|
28
|
+
expect(large.textPages).toBeLessThan(DEFAULT_INDEX_INPUTS.manuscriptWords / 400);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('protects calculations from empty and invalid values', () => {
|
|
32
|
+
const result = calculateIndexBudget({
|
|
33
|
+
...DEFAULT_INDEX_INPUTS,
|
|
34
|
+
manuscriptWords: 0,
|
|
35
|
+
trimWidthInches: Number.NaN,
|
|
36
|
+
density: 'unknown' as 'standard',
|
|
37
|
+
indexEntries: 0,
|
|
38
|
+
targetIndexPages: 0,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(result.textPages).toBe(1);
|
|
42
|
+
expect(result.estimatedIndexPages).toBe(1);
|
|
43
|
+
expect(result.targetIndexPages).toBe(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('clamps numeric input and converts dimensions', () => {
|
|
47
|
+
expect(clampNumber(Number.NaN, 1, 10, 5)).toBe(5);
|
|
48
|
+
expect(clampNumber(20, 1, 10, 5)).toBe(10);
|
|
49
|
+
expect(clampNumber(-2, 1, 10, 5)).toBe(1);
|
|
50
|
+
expect(centimetersToInches(inchesToCentimeters(6))).toBeCloseTo(6);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export type Density = 'airy' | 'standard' | 'compact';
|
|
2
|
+
|
|
3
|
+
export interface BookIndexInputs {
|
|
4
|
+
manuscriptWords: number;
|
|
5
|
+
trimWidthInches: number;
|
|
6
|
+
trimHeightInches: number;
|
|
7
|
+
density: Density;
|
|
8
|
+
indexEntries: number;
|
|
9
|
+
targetIndexPages: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface BookIndexBudget {
|
|
13
|
+
textPages: number;
|
|
14
|
+
estimatedIndexPages: number;
|
|
15
|
+
targetIndexPages: number;
|
|
16
|
+
totalPages: number;
|
|
17
|
+
targetTotalPages: number;
|
|
18
|
+
wordsPerPage: number;
|
|
19
|
+
entriesPerPage: number;
|
|
20
|
+
targetEntriesPerPage: number;
|
|
21
|
+
indexSharePercent: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_INDEX_INPUTS: BookIndexInputs = {
|
|
25
|
+
manuscriptWords: 75000,
|
|
26
|
+
trimWidthInches: 6,
|
|
27
|
+
trimHeightInches: 9,
|
|
28
|
+
density: 'standard',
|
|
29
|
+
indexEntries: 800,
|
|
30
|
+
targetIndexPages: 9,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const DENSITY_PROFILES: Record<Density, { wordsPerPage: number; entriesPerPage: number }> = {
|
|
34
|
+
airy: { wordsPerPage: 320, entriesPerPage: 34 },
|
|
35
|
+
standard: { wordsPerPage: 400, entriesPerPage: 46 },
|
|
36
|
+
compact: { wordsPerPage: 480, entriesPerPage: 60 },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function positive(value: number, fallback: number): number {
|
|
40
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function areaFactor(inputs: BookIndexInputs): number {
|
|
44
|
+
const area = positive(inputs.trimWidthInches, 6) * positive(inputs.trimHeightInches, 9);
|
|
45
|
+
return Math.min(1.6, Math.max(0.65, area / 54));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function roundPages(value: number): number {
|
|
49
|
+
return Math.max(1, Math.ceil(value));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function calculateIndexBudget(inputs: BookIndexInputs): BookIndexBudget {
|
|
53
|
+
const profile = DENSITY_PROFILES[inputs.density] ?? DENSITY_PROFILES.standard;
|
|
54
|
+
const wordsPerPage = Math.round(profile.wordsPerPage * areaFactor(inputs));
|
|
55
|
+
const textPages = roundPages(positive(inputs.manuscriptWords, 1) / wordsPerPage);
|
|
56
|
+
const indexEntries = positive(inputs.indexEntries, 1);
|
|
57
|
+
const estimatedIndexPages = roundPages(indexEntries / profile.entriesPerPage);
|
|
58
|
+
const targetIndexPages = roundPages(inputs.targetIndexPages);
|
|
59
|
+
const totalPages = textPages + estimatedIndexPages;
|
|
60
|
+
const targetTotalPages = textPages + targetIndexPages;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
textPages,
|
|
64
|
+
estimatedIndexPages,
|
|
65
|
+
targetIndexPages,
|
|
66
|
+
totalPages,
|
|
67
|
+
targetTotalPages,
|
|
68
|
+
wordsPerPage,
|
|
69
|
+
entriesPerPage: profile.entriesPerPage,
|
|
70
|
+
targetEntriesPerPage: indexEntries / targetIndexPages,
|
|
71
|
+
indexSharePercent: (estimatedIndexPages / totalPages) * 100,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function clampNumber(value: number, min: number, max: number, fallback: number): number {
|
|
76
|
+
const safeValue = Number.isFinite(value) ? value : fallback;
|
|
77
|
+
return Math.min(max, Math.max(min, safeValue));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function inchesToCentimeters(value: number): number {
|
|
81
|
+
return value * 2.54;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function centimetersToInches(value: number): number {
|
|
85
|
+
return value / 2.54;
|
|
86
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { bookIndexPageBudgetCalculator } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en' } = Astro.props as Props;
|
|
11
|
+
const loader = bookIndexPageBudgetCalculator.i18n[locale] || bookIndexPageBudgetCalculator.i18n.en;
|
|
12
|
+
const content = await loader?.();
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
{content ? <SEORenderer content={{ locale, sections: content.seo }} /> : null}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type UnitSystem = 'metric' | 'imperial';
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEY = 'jjlmoya-books-index-budget-unit';
|
|
4
|
+
|
|
5
|
+
export function loadUnitSystem(): UnitSystem {
|
|
6
|
+
try {
|
|
7
|
+
return localStorage.getItem(STORAGE_KEY) === 'metric' ? 'metric' : 'imperial';
|
|
8
|
+
} catch {
|
|
9
|
+
return 'imperial';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function saveUnitSystem(unitSystem: UnitSystem): void {
|
|
14
|
+
try {
|
|
15
|
+
localStorage.setItem(STORAGE_KEY, unitSystem);
|
|
16
|
+
} catch {}
|
|
17
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface BookIndexUI extends Record<string, string> {
|
|
2
|
+
wordsLabel: string;
|
|
3
|
+
wordsHint: string;
|
|
4
|
+
trimWidthLabel: string;
|
|
5
|
+
trimHeightLabel: string;
|
|
6
|
+
trimHint: string;
|
|
7
|
+
unitLabel: string;
|
|
8
|
+
metricButtonLabel: string;
|
|
9
|
+
imperialButtonLabel: string;
|
|
10
|
+
densityLabel: string;
|
|
11
|
+
densityHint: string;
|
|
12
|
+
airyLabel: string;
|
|
13
|
+
standardLabel: string;
|
|
14
|
+
compactLabel: string;
|
|
15
|
+
indexEntriesLabel: string;
|
|
16
|
+
indexEntriesHint: string;
|
|
17
|
+
targetIndexPagesLabel: string;
|
|
18
|
+
targetIndexPagesHint: string;
|
|
19
|
+
textPagesLabel: string;
|
|
20
|
+
estimatedIndexPagesLabel: string;
|
|
21
|
+
totalPagesLabel: string;
|
|
22
|
+
targetTotalPagesLabel: string;
|
|
23
|
+
entriesPerPageLabel: string;
|
|
24
|
+
indexShareLabel: string;
|
|
25
|
+
statusBalanced: string;
|
|
26
|
+
statusTight: string;
|
|
27
|
+
statusRoomy: string;
|
|
28
|
+
statusBalancedHint: string;
|
|
29
|
+
statusTightHint: string;
|
|
30
|
+
statusRoomyHint: string;
|
|
31
|
+
sceneLabel: string;
|
|
32
|
+
sceneBodyLabel: string;
|
|
33
|
+
sceneIndexLabel: string;
|
|
34
|
+
sceneCaption: string;
|
|
35
|
+
sceneTargetCaption: string;
|
|
36
|
+
sourceNote: string;
|
|
37
|
+
summaryLabel: string;
|
|
38
|
+
summaryPagesOver: string;
|
|
39
|
+
summaryPagesSpare: string;
|
|
40
|
+
summaryOnTarget: string;
|
|
41
|
+
}
|
package/src/tools.ts
CHANGED
|
@@ -3,5 +3,6 @@ import type { ToolDefinition } from './types';
|
|
|
3
3
|
import { BOOK_PAGINATION_AND_SPINE_CALCULATOR_TOOL } from './tool/book-pagination-and-spine-calculator';
|
|
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
|
+
import { BOOK_INDEX_PAGE_BUDGET_CALCULATOR_TOOL } from './tool/book-index-page-budget-calculator';
|
|
6
7
|
|
|
7
|
-
export const ALL_TOOLS: ToolDefinition[] = [BOOK_PAGINATION_AND_SPINE_CALCULATOR_TOOL, BOOK_INTERIOR_MARGIN_AND_GUTTER_PLANNER_TOOL, BOOK_READING_TIME_DEADLINE_PLANNER_TOOL];
|
|
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];
|
package/src/types.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import type { SEOSection } from '@jjlmoya/utils-shared';
|
|
2
|
+
import type { UtilityLocale } from '@jjlmoya/utils-shared/routing';
|
|
2
3
|
|
|
3
4
|
export type { SEOSection };
|
|
4
5
|
|
|
5
|
-
export type KnownLocale =
|
|
6
|
-
| 'ar' | 'da' | 'de' | 'en' | 'es' | 'fi'
|
|
7
|
-
| 'fr' | 'id' | 'it' | 'ja' | 'ko' | 'nb' | 'nl'
|
|
8
|
-
| 'pl' | 'pt' | 'ru' | 'sv' | 'tr' | 'zh';
|
|
6
|
+
export type KnownLocale = UtilityLocale;
|
|
9
7
|
|
|
10
8
|
export interface FAQItem {
|
|
11
9
|
question: string;
|
|
@@ -22,7 +20,7 @@ export interface HowToStep {
|
|
|
22
20
|
text: string;
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
export interface ToolLocaleContent<TUI extends
|
|
23
|
+
export interface ToolLocaleContent<TUI extends object = Record<string, string>> {
|
|
26
24
|
slug: string;
|
|
27
25
|
title: string;
|
|
28
26
|
description: string;
|
|
@@ -45,7 +43,7 @@ export type LocaleLoader<T> = () => Promise<T>;
|
|
|
45
43
|
|
|
46
44
|
export type LocaleMap<T> = Partial<Record<KnownLocale, LocaleLoader<T>>>;
|
|
47
45
|
|
|
48
|
-
export interface BooksToolEntry<TUI extends
|
|
46
|
+
export interface BooksToolEntry<TUI extends object = Record<string, string>> {
|
|
49
47
|
id: string;
|
|
50
48
|
icons: {
|
|
51
49
|
bg: string;
|
package/src/worker.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
interface UtilityMfeEnvironment {
|
|
2
|
+
ASSETS: { fetch(request: Request): Promise<Response> };
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export const LONG_LIVED_ASSET_CACHE = "public, max-age=31536000, immutable";
|
|
6
|
+
export const SITEMAP_CACHE = "public, max-age=3600, s-maxage=3600, must-revalidate";
|
|
7
|
+
|
|
8
|
+
export const getCacheControl = (pathname: string): string | undefined => {
|
|
9
|
+
if (pathname.endsWith("/sitemap.xml")) return SITEMAP_CACHE;
|
|
10
|
+
if (pathname.endsWith("/manifest.json")) return LONG_LIVED_ASSET_CACHE;
|
|
11
|
+
if (pathname.startsWith("/_utilities/")) {
|
|
12
|
+
return LONG_LIVED_ASSET_CACHE;
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
async fetch(request: Request, environment: UtilityMfeEnvironment): Promise<Response> {
|
|
19
|
+
const response = await environment.ASSETS.fetch(request);
|
|
20
|
+
const cacheControl = getCacheControl(new URL(request.url).pathname);
|
|
21
|
+
if (!cacheControl) return response;
|
|
22
|
+
|
|
23
|
+
const headers = new Headers(response.headers);
|
|
24
|
+
headers.set("Cache-Control", cacheControl);
|
|
25
|
+
return new Response(response.body, { status: response.status, headers });
|
|
26
|
+
},
|
|
27
|
+
};
|
package/tsconfig.json
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"extends": "astro/tsconfigs/strictest",
|
|
3
|
-
"include": [
|
|
3
|
+
"include": [
|
|
4
|
+
"src/**/*",
|
|
5
|
+
".astro/types.d.ts"
|
|
6
|
+
],
|
|
4
7
|
"compilerOptions": {
|
|
5
8
|
"baseUrl": ".",
|
|
6
9
|
"strictNullChecks": true,
|
|
7
10
|
"noImplicitAny": true,
|
|
8
11
|
"strict": true,
|
|
9
|
-
"types": [
|
|
12
|
+
"types": [
|
|
13
|
+
"astro/client",
|
|
14
|
+
"astro/jsx-runtime"
|
|
15
|
+
],
|
|
10
16
|
"jsx": "preserve",
|
|
11
|
-
"jsxImportSource": "astro"
|
|
17
|
+
"jsxImportSource": "astro",
|
|
18
|
+
"resolveJsonModule": true
|
|
12
19
|
},
|
|
13
|
-
"exclude": [
|
|
20
|
+
"exclude": [
|
|
21
|
+
"node_modules",
|
|
22
|
+
"dist"
|
|
23
|
+
]
|
|
14
24
|
}
|
|
15
|
-
|