@jjlmoya/utils-converters 1.23.0 → 1.25.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 (87) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +4 -0
  3. package/src/entries.ts +10 -1
  4. package/src/index.ts +3 -0
  5. package/src/shared/ImageToPdfConverter.astro +80 -0
  6. package/src/shared/image-to-pdf/controller.ts +173 -0
  7. package/src/shared/image-to-pdf/dom-views.ts +55 -0
  8. package/src/shared/image-to-pdf/evaluator.ts +18 -0
  9. package/src/shared/image-to-pdf/logic.test.ts +29 -0
  10. package/src/shared/image-to-pdf/logic.ts +214 -0
  11. package/src/shared/image-to-pdf/storage.ts +36 -0
  12. package/src/shared/image-to-pdf/ui.ts +39 -0
  13. package/src/shared/image-to-pdf.css +394 -0
  14. package/src/tests/i18n_coverage.test.ts +2 -3
  15. package/src/tests/locale_completeness.test.ts +2 -3
  16. package/src/tests/seo_translation_completeness.test.ts +69 -0
  17. package/src/tests/tool_validation.test.ts +2 -2
  18. package/src/tool/jpgToPdf/bibliography.astro +11 -0
  19. package/src/tool/jpgToPdf/bibliography.ts +12 -0
  20. package/src/tool/jpgToPdf/component.astro +10 -0
  21. package/src/tool/jpgToPdf/entry.ts +26 -0
  22. package/src/tool/jpgToPdf/i18n/de.ts +111 -0
  23. package/src/tool/jpgToPdf/i18n/en.ts +37 -0
  24. package/src/tool/jpgToPdf/i18n/es.ts +111 -0
  25. package/src/tool/jpgToPdf/i18n/fr.ts +111 -0
  26. package/src/tool/jpgToPdf/i18n/id.ts +111 -0
  27. package/src/tool/jpgToPdf/i18n/it.ts +111 -0
  28. package/src/tool/jpgToPdf/i18n/ja.ts +111 -0
  29. package/src/tool/jpgToPdf/i18n/ko.ts +111 -0
  30. package/src/tool/jpgToPdf/i18n/nl.ts +111 -0
  31. package/src/tool/jpgToPdf/i18n/pl.ts +111 -0
  32. package/src/tool/jpgToPdf/i18n/pt.ts +111 -0
  33. package/src/tool/jpgToPdf/i18n/ru.ts +111 -0
  34. package/src/tool/jpgToPdf/i18n/sv.ts +111 -0
  35. package/src/tool/jpgToPdf/i18n/tr.ts +111 -0
  36. package/src/tool/jpgToPdf/i18n/zh.ts +111 -0
  37. package/src/tool/jpgToPdf/index.ts +11 -0
  38. package/src/tool/jpgToPdf/jpg-to-pdf-converter.css +3 -0
  39. package/src/tool/jpgToPdf/seo.astro +12 -0
  40. package/src/tool/pngAJpg/i18n/ko.ts +1 -1
  41. package/src/tool/pngAJpg/i18n/ru.ts +1 -1
  42. package/src/tool/pngAJpg/i18n/zh.ts +1 -1
  43. package/src/tool/pngToPdf/bibliography.astro +11 -0
  44. package/src/tool/pngToPdf/bibliography.ts +12 -0
  45. package/src/tool/pngToPdf/component.astro +10 -0
  46. package/src/tool/pngToPdf/entry.ts +26 -0
  47. package/src/tool/pngToPdf/i18n/de.ts +111 -0
  48. package/src/tool/pngToPdf/i18n/en.ts +37 -0
  49. package/src/tool/pngToPdf/i18n/es.ts +111 -0
  50. package/src/tool/pngToPdf/i18n/fr.ts +111 -0
  51. package/src/tool/pngToPdf/i18n/id.ts +111 -0
  52. package/src/tool/pngToPdf/i18n/it.ts +111 -0
  53. package/src/tool/pngToPdf/i18n/ja.ts +111 -0
  54. package/src/tool/pngToPdf/i18n/ko.ts +111 -0
  55. package/src/tool/pngToPdf/i18n/nl.ts +111 -0
  56. package/src/tool/pngToPdf/i18n/pl.ts +111 -0
  57. package/src/tool/pngToPdf/i18n/pt.ts +111 -0
  58. package/src/tool/pngToPdf/i18n/ru.ts +111 -0
  59. package/src/tool/pngToPdf/i18n/sv.ts +111 -0
  60. package/src/tool/pngToPdf/i18n/tr.ts +111 -0
  61. package/src/tool/pngToPdf/i18n/zh.ts +111 -0
  62. package/src/tool/pngToPdf/index.ts +11 -0
  63. package/src/tool/pngToPdf/png-to-pdf-converter.css +3 -0
  64. package/src/tool/pngToPdf/seo.astro +12 -0
  65. package/src/tool/webpToPdf/bibliography.astro +11 -0
  66. package/src/tool/webpToPdf/bibliography.ts +12 -0
  67. package/src/tool/webpToPdf/component.astro +10 -0
  68. package/src/tool/webpToPdf/entry.ts +26 -0
  69. package/src/tool/webpToPdf/i18n/de.ts +111 -0
  70. package/src/tool/webpToPdf/i18n/en.ts +37 -0
  71. package/src/tool/webpToPdf/i18n/es.ts +111 -0
  72. package/src/tool/webpToPdf/i18n/fr.ts +111 -0
  73. package/src/tool/webpToPdf/i18n/id.ts +111 -0
  74. package/src/tool/webpToPdf/i18n/it.ts +111 -0
  75. package/src/tool/webpToPdf/i18n/ja.ts +111 -0
  76. package/src/tool/webpToPdf/i18n/ko.ts +111 -0
  77. package/src/tool/webpToPdf/i18n/nl.ts +111 -0
  78. package/src/tool/webpToPdf/i18n/pl.ts +111 -0
  79. package/src/tool/webpToPdf/i18n/pt.ts +111 -0
  80. package/src/tool/webpToPdf/i18n/ru.ts +111 -0
  81. package/src/tool/webpToPdf/i18n/sv.ts +111 -0
  82. package/src/tool/webpToPdf/i18n/tr.ts +111 -0
  83. package/src/tool/webpToPdf/i18n/zh.ts +111 -0
  84. package/src/tool/webpToPdf/index.ts +11 -0
  85. package/src/tool/webpToPdf/seo.astro +12 -0
  86. package/src/tool/webpToPdf/webp-to-pdf-converter.css +3 -0
  87. package/src/tools.ts +9 -1
@@ -0,0 +1,36 @@
1
+ import type { Orientation, PageSize } from './logic';
2
+
3
+ export interface StoredPdfOptions {
4
+ pageSize: PageSize;
5
+ orientation: Orientation;
6
+ marginMm: number;
7
+ }
8
+
9
+ const STORAGE_KEY = 'jjlmoya-image-to-pdf-options';
10
+
11
+ const fallback: StoredPdfOptions = {
12
+ pageSize: 'a4',
13
+ orientation: 'portrait',
14
+ marginMm: 12,
15
+ };
16
+
17
+ export function loadPdfOptions(): StoredPdfOptions {
18
+ try {
19
+ const saved = localStorage.getItem(STORAGE_KEY);
20
+ if (!saved) return fallback;
21
+ const parsed = JSON.parse(saved) as Partial<StoredPdfOptions>;
22
+ return {
23
+ pageSize: parsed.pageSize === 'letter' ? 'letter' : 'a4',
24
+ orientation: parsed.orientation === 'landscape' ? 'landscape' : 'portrait',
25
+ marginMm: typeof parsed.marginMm === 'number' ? Math.min(32, Math.max(0, parsed.marginMm)) : fallback.marginMm,
26
+ };
27
+ } catch {
28
+ return fallback;
29
+ }
30
+ }
31
+
32
+ export function savePdfOptions(options: StoredPdfOptions): void {
33
+ try {
34
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(options));
35
+ } catch {}
36
+ }
@@ -0,0 +1,39 @@
1
+ export interface ImageToPdfUI {
2
+ [key: string]: string;
3
+ formatLabel: string;
4
+ dropTitle: string;
5
+ dropText: string;
6
+ selectFiles: string;
7
+ privacyNote: string;
8
+ optionsTitle: string;
9
+ settingsTitle: string;
10
+ pageSizeLabel: string;
11
+ a4: string;
12
+ letter: string;
13
+ orientationLabel: string;
14
+ portrait: string;
15
+ landscape: string;
16
+ marginLabel: string;
17
+ pagesTitle: string;
18
+ reorderHint: string;
19
+ page: string;
20
+ pageSingular: string;
21
+ pagePlural: string;
22
+ moveUp: string;
23
+ moveDown: string;
24
+ remove: string;
25
+ emptyPages: string;
26
+ createPdf: string;
27
+ reset: string;
28
+ ready: string;
29
+ processing: string;
30
+ error: string;
31
+ downloadPdf: string;
32
+ building: string;
33
+ pdfReady: string;
34
+ invalidFile: string;
35
+ bibliographyTitle: string;
36
+ faqTitle: string;
37
+ }
38
+
39
+ export type SourceImageFormat = 'jpg' | 'png' | 'webp';
@@ -0,0 +1,394 @@
1
+ .pdf-converter {
2
+ --n-surface: #fff;
3
+ --n-surface-muted: #f4f4f5;
4
+ --n-surface-raised: #fafafa;
5
+ --n-border: #d4d4d8;
6
+ --n-text: #18181b;
7
+ --n-muted: #52525b;
8
+ --n-accent: #2563eb;
9
+ --n-accent-soft: #dbeafe;
10
+ --n-accent-strong: #1d4ed8;
11
+ --n-success: #15803d;
12
+ --n-error: #b91c1c;
13
+ --n-white: #fff;
14
+ --n-ring: 0 0 0 0.35rem var(--n-accent-soft);
15
+
16
+ color: var(--n-text);
17
+ display: grid;
18
+ gap: 1rem;
19
+ margin: 0 auto;
20
+ max-width: 1040px;
21
+ }
22
+
23
+ .theme-dark .pdf-converter {
24
+ --n-surface: #18181b;
25
+ --n-surface-muted: #27272a;
26
+ --n-surface-raised: #202023;
27
+ --n-border: #52525b;
28
+ --n-text: #fafafa;
29
+ --n-muted: #d4d4d8;
30
+ --n-accent-soft: #1e3a8a;
31
+ }
32
+
33
+ .pdf-converter-jpg {
34
+ --n-accent: #b45309;
35
+ --n-accent-strong: #92400e;
36
+ --n-accent-soft: #fef3c7;
37
+ }
38
+
39
+ .theme-dark .pdf-converter-jpg {
40
+ --n-accent-soft: #78350f;
41
+ }
42
+
43
+ .pdf-converter-png {
44
+ --n-accent: #047857;
45
+ --n-accent-strong: #065f46;
46
+ --n-accent-soft: #d1fae5;
47
+ }
48
+
49
+ .theme-dark .pdf-converter-png {
50
+ --n-accent-soft: #064e3b;
51
+ }
52
+
53
+ .pdf-converter-webp {
54
+ --n-accent: #7c3aed;
55
+ --n-accent-strong: #6d28d9;
56
+ --n-accent-soft: #ede9fe;
57
+ }
58
+
59
+ .theme-dark .pdf-converter-webp {
60
+ --n-accent-soft: #4c1d95;
61
+ }
62
+
63
+ .pdf-converter [hidden] {
64
+ display: none;
65
+ }
66
+
67
+ .pdf-converter-stage,
68
+ .pdf-converter-workspace,
69
+ .pdf-converter-options,
70
+ .pdf-converter-pages {
71
+ background: var(--n-surface);
72
+ border: 1px solid var(--n-border);
73
+ border-radius: 1.25rem;
74
+ }
75
+
76
+ .pdf-converter-stage {
77
+ display: grid;
78
+ gap: 0.8rem;
79
+ justify-items: center;
80
+ padding: 2.3rem 1.5rem;
81
+ text-align: center;
82
+ }
83
+
84
+ .pdf-converter-stage.is-dragging {
85
+ background: var(--n-accent-soft);
86
+ border-color: var(--n-accent);
87
+ }
88
+
89
+ .pdf-converter-mark {
90
+ align-items: center;
91
+ background: var(--n-accent-soft);
92
+ border: 1px solid var(--n-accent);
93
+ border-radius: 50%;
94
+ color: var(--n-accent-strong);
95
+ display: flex;
96
+ font-size: 2rem;
97
+ height: 4.5rem;
98
+ justify-content: center;
99
+ box-shadow: var(--n-ring);
100
+ width: 4.5rem;
101
+ }
102
+
103
+ .pdf-converter-stage h2,
104
+ .pdf-converter-stage p {
105
+ margin: 0;
106
+ }
107
+
108
+ .pdf-converter-stage h2 {
109
+ font-size: clamp(1.1rem, 2vw, 1.5rem);
110
+ }
111
+
112
+ .pdf-converter-stage p {
113
+ color: var(--n-muted);
114
+ max-width: 38rem;
115
+ }
116
+
117
+ .pdf-converter-privacy {
118
+ color: var(--n-success);
119
+ font-size: 0.82rem;
120
+ }
121
+
122
+ .pdf-converter-select,
123
+ .pdf-converter-primary,
124
+ .pdf-converter-secondary,
125
+ .pdf-converter-download,
126
+ .pdf-page-action,
127
+ .pdf-converter-choice {
128
+ border: 1px solid var(--n-border);
129
+ border-radius: 0.65rem;
130
+ cursor: pointer;
131
+ font: inherit;
132
+ }
133
+
134
+ .pdf-converter-select,
135
+ .pdf-converter-primary,
136
+ .pdf-converter-secondary,
137
+ .pdf-converter-download {
138
+ padding: 0.7rem 1rem;
139
+ }
140
+
141
+ .pdf-converter-select,
142
+ .pdf-converter-primary,
143
+ .pdf-converter-download {
144
+ background: var(--n-accent);
145
+ color: var(--n-white);
146
+ font-weight: 700;
147
+ }
148
+
149
+ .pdf-converter-select:hover,
150
+ .pdf-converter-primary:hover,
151
+ .pdf-converter-download:hover {
152
+ background: var(--n-accent-strong);
153
+ }
154
+
155
+ .pdf-converter-secondary {
156
+ background: var(--n-surface-muted);
157
+ color: var(--n-text);
158
+ }
159
+
160
+ .pdf-converter-workspace {
161
+ display: grid;
162
+ gap: 1rem;
163
+ padding: 1rem;
164
+ }
165
+
166
+ .pdf-converter-toolbar,
167
+ .pdf-converter-footer,
168
+ .pdf-converter-pages-header,
169
+ .pdf-converter-option-line {
170
+ align-items: center;
171
+ display: flex;
172
+ gap: 0.75rem;
173
+ justify-content: space-between;
174
+ }
175
+
176
+ .pdf-converter-toolbar {
177
+ border-bottom: 1px solid var(--n-border);
178
+ padding: 0.25rem 0 0.9rem;
179
+ }
180
+
181
+ .pdf-converter-toolbar h2,
182
+ .pdf-converter-pages-header h3 {
183
+ font-size: 1rem;
184
+ margin: 0;
185
+ }
186
+
187
+ .pdf-converter-toolbar p,
188
+ .pdf-converter-pages-header p {
189
+ color: var(--n-muted);
190
+ font-size: 0.82rem;
191
+ margin: 0.2rem 0 0;
192
+ }
193
+
194
+ .pdf-converter-content {
195
+ display: grid;
196
+ gap: 1rem;
197
+ grid-template-columns: minmax(14rem, 0.8fr) minmax(0, 1.5fr);
198
+ }
199
+
200
+ .pdf-converter-options,
201
+ .pdf-converter-pages {
202
+ padding: 1rem;
203
+ }
204
+
205
+ .pdf-converter-options h3 {
206
+ font-size: 0.95rem;
207
+ margin: 0 0 1rem;
208
+ }
209
+
210
+ .pdf-converter-option-line {
211
+ align-items: flex-start;
212
+ border-top: 1px solid var(--n-border);
213
+ flex-direction: column;
214
+ padding: 0.9rem 0;
215
+ }
216
+
217
+ .pdf-converter-option-line:first-of-type {
218
+ border-top: 0;
219
+ padding-top: 0;
220
+ }
221
+
222
+ .pdf-converter-option-label {
223
+ color: var(--n-muted);
224
+ font-size: 0.78rem;
225
+ font-weight: 700;
226
+ text-transform: uppercase;
227
+ }
228
+
229
+ .pdf-converter-choices {
230
+ display: flex;
231
+ flex-wrap: wrap;
232
+ gap: 0.5rem;
233
+ width: 100%;
234
+ }
235
+
236
+ .pdf-converter-choice {
237
+ background: var(--n-surface-muted);
238
+ color: var(--n-text);
239
+ padding: 0.55rem 0.75rem;
240
+ }
241
+
242
+ .pdf-converter-choice.is-active {
243
+ background: var(--n-accent-soft);
244
+ border-color: var(--n-accent);
245
+ color: var(--n-accent-strong);
246
+ font-weight: 700;
247
+ }
248
+
249
+ .pdf-converter-range {
250
+ accent-color: var(--n-accent);
251
+ width: 100%;
252
+ }
253
+
254
+ .pdf-converter-value {
255
+ color: var(--n-text);
256
+ font-weight: 700;
257
+ }
258
+
259
+ .pdf-pages {
260
+ display: grid;
261
+ gap: 0.55rem;
262
+ list-style: none;
263
+ margin: 1rem 0 0;
264
+ padding: 0;
265
+ }
266
+
267
+ .pdf-page {
268
+ align-items: center;
269
+ background: var(--n-surface-raised);
270
+ border: 1px solid var(--n-border);
271
+ border-radius: 0.8rem;
272
+ display: grid;
273
+ gap: 0.7rem;
274
+ grid-template-columns: 1.7rem 3.4rem minmax(0, 1fr) auto;
275
+ padding: 0.55rem;
276
+ }
277
+
278
+ .pdf-page-number {
279
+ color: var(--n-muted);
280
+ font-size: 0.85rem;
281
+ text-align: center;
282
+ }
283
+
284
+ .pdf-page-thumb {
285
+ background: var(--n-white);
286
+ border: 1px solid var(--n-border);
287
+ border-radius: 0.35rem;
288
+ height: 3.6rem;
289
+ object-fit: contain;
290
+ width: 3.4rem;
291
+ }
292
+
293
+ .pdf-page-meta {
294
+ display: grid;
295
+ gap: 0.25rem;
296
+ min-width: 0;
297
+ }
298
+
299
+ .pdf-page-meta strong {
300
+ overflow: hidden;
301
+ text-overflow: ellipsis;
302
+ white-space: nowrap;
303
+ }
304
+
305
+ .pdf-page-meta span {
306
+ color: var(--n-muted);
307
+ font-size: 0.78rem;
308
+ }
309
+
310
+ .pdf-page-actions {
311
+ display: flex;
312
+ flex-wrap: wrap;
313
+ gap: 0.35rem;
314
+ justify-content: flex-end;
315
+ }
316
+
317
+ .pdf-page-action {
318
+ background: var(--n-surface-muted);
319
+ color: var(--n-text);
320
+ font-size: 0.72rem;
321
+ padding: 0.4rem 0.5rem;
322
+ }
323
+
324
+ .pdf-page-action:disabled {
325
+ cursor: not-allowed;
326
+ opacity: 0.4;
327
+ }
328
+
329
+ .pdf-page-error {
330
+ color: var(--n-error);
331
+ }
332
+
333
+ .pdf-pages-empty {
334
+ color: var(--n-muted);
335
+ padding: 1rem;
336
+ text-align: center;
337
+ }
338
+
339
+ .pdf-converter-footer {
340
+ border-top: 1px solid var(--n-border);
341
+ padding-top: 1rem;
342
+ }
343
+
344
+ .pdf-converter-status {
345
+ color: var(--n-muted);
346
+ font-size: 0.82rem;
347
+ }
348
+
349
+ .pdf-converter-status[data-tone="ready"] {
350
+ color: var(--n-success);
351
+ font-weight: 700;
352
+ }
353
+
354
+ .pdf-converter-status[data-tone="error"] {
355
+ color: var(--n-error);
356
+ }
357
+
358
+ .pdf-converter-actions {
359
+ align-items: center;
360
+ display: flex;
361
+ flex-wrap: wrap;
362
+ gap: 0.55rem;
363
+ justify-content: flex-end;
364
+ }
365
+
366
+ .pdf-converter-primary:disabled {
367
+ cursor: not-allowed;
368
+ opacity: 0.5;
369
+ }
370
+
371
+ .pdf-converter-download {
372
+ display: inline-block;
373
+ text-decoration: none;
374
+ }
375
+
376
+ @media (max-width: 760px) {
377
+ .pdf-converter-content {
378
+ grid-template-columns: 1fr;
379
+ }
380
+
381
+ .pdf-page {
382
+ grid-template-columns: 1.5rem 3.2rem minmax(0, 1fr);
383
+ }
384
+
385
+ .pdf-page-actions {
386
+ grid-column: 3;
387
+ justify-content: flex-start;
388
+ }
389
+
390
+ .pdf-converter-footer {
391
+ align-items: flex-start;
392
+ flex-direction: column;
393
+ }
394
+ }
@@ -24,10 +24,9 @@ describe('I18n Coverage Validation', () => {
24
24
 
25
25
  it('all locale loaders should be functions', () => {
26
26
  EXPECTED_LOCALES.forEach((locale) => {
27
- const loader = entry.i18n[locale as keyof typeof entry.i18n];
28
27
  expect(
29
- typeof loader,
30
- `Tool "${entry.id}" locale "${locale}" loader is not a function`,
28
+ typeof entry.i18n[locale],
29
+ `Locale "${locale}" loader for "${entry.id}" is not a function`,
31
30
  ).toBe('function');
32
31
  });
33
32
  });
@@ -35,8 +35,7 @@ describe('Locale Completeness Validation', () => {
35
35
  });
36
36
  });
37
37
 
38
- it('all 21 tools registered', () => {
39
- expect(ALL_TOOLS.length).toBe(21);
38
+ it('all 24 tools registered', () => {
39
+ expect(ALL_TOOLS.length).toBe(24);
40
40
  });
41
41
  });
42
-
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+
4
+ const ASIAN_LOCALES = ['ja', 'ko', 'zh'];
5
+
6
+ function getSimpleSectionLength(section: any): number {
7
+ if (section.type === 'title') return section.text ? section.text.length : 0;
8
+ if (section.type === 'paragraph') return section.html ? section.html.replace(/<[^>]*>/g, '').length : 0;
9
+ if (section.type === 'list') return section.items ? section.items.join('').length : 0;
10
+ return 0;
11
+ }
12
+
13
+ function getComplexSectionLength(section: any): number {
14
+ if (section.type === 'diagnostic' || section.type === 'tip') {
15
+ return (section.title ? section.title.length : 0) + (section.html ? section.html.replace(/<[^>]*>/g, '').length : 0);
16
+ }
17
+ if (section.type === 'table') {
18
+ return ((section.headers || []).join('').length) + ((section.rows || []).flat().join('').length);
19
+ }
20
+ return 0;
21
+ }
22
+
23
+ function getSectionLength(section: any): number {
24
+ return getSimpleSectionLength(section) || getComplexSectionLength(section);
25
+ }
26
+
27
+ function calculateSeoTextLength(seoSections: any[]): number {
28
+ return seoSections.reduce((acc, section) => acc + getSectionLength(section), 0);
29
+ }
30
+
31
+ function checkLocaleSeoLength(toolId: string, locale: string, localeLen: number, enLen: number): void {
32
+ const isAsian = ASIAN_LOCALES.includes(locale);
33
+ const minLength = Math.floor(enLen * (isAsian ? 0.25 : 0.70));
34
+ const msgType = isAsian ? 'suspiciously short' : 'truncated/lazy';
35
+
36
+ expect(
37
+ localeLen,
38
+ `[LAZY SEO TRANSLATION] Tool "${toolId}" locale "${locale}" SEO text is ${msgType} (${localeLen} chars vs EN ${enLen} chars, expected min ${minLength})`,
39
+ ).toBeGreaterThanOrEqual(minLength);
40
+ }
41
+
42
+ async function auditSingleLocale(toolId: string, locale: string, loader: any, enSeoLength: number): Promise<void> {
43
+ if (locale === 'en' || !loader) return;
44
+ const content = await loader();
45
+ if (!content.seo || !Array.isArray(content.seo)) return;
46
+
47
+ checkLocaleSeoLength(toolId, locale, calculateSeoTextLength(content.seo), enSeoLength);
48
+ }
49
+
50
+ describe('SEO Translation Completeness & Laziness Audit', () => {
51
+ ALL_TOOLS.forEach(({ entry }) => {
52
+ describe(`Tool: ${entry.id}`, () => {
53
+ it('should not have lazy or truncated SEO content compared to English reference', async () => {
54
+ const enLoader = entry.i18n['en' as keyof typeof entry.i18n];
55
+ if (!enLoader) return;
56
+
57
+ const enContent = await enLoader();
58
+ if (!enContent.seo || !Array.isArray(enContent.seo)) return;
59
+
60
+ const enSeoLength = calculateSeoTextLength(enContent.seo);
61
+
62
+ for (const [locale, loader] of Object.entries(entry.i18n)) {
63
+ await auditSingleLocale(entry.id, locale, loader, enSeoLength);
64
+ }
65
+ });
66
+ });
67
+ });
68
+ });
69
+
@@ -4,8 +4,8 @@ import { convertersCategory } from '../data';
4
4
 
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
- it('should have 21 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(21);
7
+ it('should have 24 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(24);
9
9
  });
10
10
 
11
11
  it('convertersCategory should be defined', () => {
@@ -0,0 +1,11 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { jpgToPdf } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props { locale?: KnownLocale }
7
+ const { locale = 'en' } = Astro.props;
8
+ const content = await jpgToPdf.i18n[locale]?.();
9
+ ---
10
+
11
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,12 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Adobe PDF Reference 1.7',
6
+ url: 'https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfs/pdf_reference_1-7.pdf',
7
+ },
8
+ {
9
+ name: 'JPEG File Interchange Format Version 1.02',
10
+ url: 'https://www.w3.org/Graphics/JPEG/jfif3.pdf',
11
+ },
12
+ ];
@@ -0,0 +1,10 @@
1
+ ---
2
+ import '../../shared/image-to-pdf.css';
3
+ import ImageToPdfConverter from '../../shared/ImageToPdfConverter.astro';
4
+ import type { ImageToPdfUI } from '../../shared/image-to-pdf/ui';
5
+
6
+ interface Props { ui: ImageToPdfUI }
7
+ const { ui } = Astro.props;
8
+ ---
9
+
10
+ <ImageToPdfConverter ui={ui} sourceFormat="jpg" />
@@ -0,0 +1,26 @@
1
+ import type { ConvertersToolEntry, ToolLocaleContent } from '../../types';
2
+ import type { ImageToPdfUI } from '../../shared/image-to-pdf/ui';
3
+
4
+ export type JpgToPdfLocaleContent = ToolLocaleContent<ImageToPdfUI>;
5
+
6
+ export const jpgToPdf: ConvertersToolEntry<ImageToPdfUI> = {
7
+ id: 'jpg-to-pdf',
8
+ icons: { bg: 'mdi:file-image', fg: 'mdi:file-pdf-box' },
9
+ i18n: {
10
+ de: () => import('./i18n/de').then((module) => module.content),
11
+ en: () => import('./i18n/en').then((module) => module.content),
12
+ es: () => import('./i18n/es').then((module) => module.content),
13
+ fr: () => import('./i18n/fr').then((module) => module.content),
14
+ id: () => import('./i18n/id').then((module) => module.content),
15
+ it: () => import('./i18n/it').then((module) => module.content),
16
+ ja: () => import('./i18n/ja').then((module) => module.content),
17
+ ko: () => import('./i18n/ko').then((module) => module.content),
18
+ nl: () => import('./i18n/nl').then((module) => module.content),
19
+ pl: () => import('./i18n/pl').then((module) => module.content),
20
+ pt: () => import('./i18n/pt').then((module) => module.content),
21
+ ru: () => import('./i18n/ru').then((module) => module.content),
22
+ sv: () => import('./i18n/sv').then((module) => module.content),
23
+ tr: () => import('./i18n/tr').then((module) => module.content),
24
+ zh: () => import('./i18n/zh').then((module) => module.content),
25
+ },
26
+ };