@jjlmoya/utils-converters 1.22.0 → 1.24.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 (88) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +4 -0
  3. package/src/entries.ts +4 -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/spanish_leakage.test.ts +5 -1
  18. package/src/tests/tool_validation.test.ts +2 -2
  19. package/src/tool/jpgToPdf/bibliography.astro +11 -0
  20. package/src/tool/jpgToPdf/bibliography.ts +12 -0
  21. package/src/tool/jpgToPdf/component.astro +10 -0
  22. package/src/tool/jpgToPdf/entry.ts +26 -0
  23. package/src/tool/jpgToPdf/i18n/de.ts +111 -0
  24. package/src/tool/jpgToPdf/i18n/en.ts +37 -0
  25. package/src/tool/jpgToPdf/i18n/es.ts +111 -0
  26. package/src/tool/jpgToPdf/i18n/fr.ts +111 -0
  27. package/src/tool/jpgToPdf/i18n/id.ts +111 -0
  28. package/src/tool/jpgToPdf/i18n/it.ts +111 -0
  29. package/src/tool/jpgToPdf/i18n/ja.ts +111 -0
  30. package/src/tool/jpgToPdf/i18n/ko.ts +111 -0
  31. package/src/tool/jpgToPdf/i18n/nl.ts +111 -0
  32. package/src/tool/jpgToPdf/i18n/pl.ts +111 -0
  33. package/src/tool/jpgToPdf/i18n/pt.ts +111 -0
  34. package/src/tool/jpgToPdf/i18n/ru.ts +111 -0
  35. package/src/tool/jpgToPdf/i18n/sv.ts +111 -0
  36. package/src/tool/jpgToPdf/i18n/tr.ts +111 -0
  37. package/src/tool/jpgToPdf/i18n/zh.ts +111 -0
  38. package/src/tool/jpgToPdf/index.ts +11 -0
  39. package/src/tool/jpgToPdf/jpg-to-pdf-converter.css +3 -0
  40. package/src/tool/jpgToPdf/seo.astro +12 -0
  41. package/src/tool/pngAJpg/i18n/ko.ts +1 -1
  42. package/src/tool/pngAJpg/i18n/ru.ts +1 -1
  43. package/src/tool/pngAJpg/i18n/zh.ts +1 -1
  44. package/src/tool/pngToPdf/bibliography.astro +11 -0
  45. package/src/tool/pngToPdf/bibliography.ts +12 -0
  46. package/src/tool/pngToPdf/component.astro +10 -0
  47. package/src/tool/pngToPdf/entry.ts +26 -0
  48. package/src/tool/pngToPdf/i18n/de.ts +111 -0
  49. package/src/tool/pngToPdf/i18n/en.ts +37 -0
  50. package/src/tool/pngToPdf/i18n/es.ts +111 -0
  51. package/src/tool/pngToPdf/i18n/fr.ts +111 -0
  52. package/src/tool/pngToPdf/i18n/id.ts +111 -0
  53. package/src/tool/pngToPdf/i18n/it.ts +111 -0
  54. package/src/tool/pngToPdf/i18n/ja.ts +111 -0
  55. package/src/tool/pngToPdf/i18n/ko.ts +111 -0
  56. package/src/tool/pngToPdf/i18n/nl.ts +111 -0
  57. package/src/tool/pngToPdf/i18n/pl.ts +111 -0
  58. package/src/tool/pngToPdf/i18n/pt.ts +111 -0
  59. package/src/tool/pngToPdf/i18n/ru.ts +111 -0
  60. package/src/tool/pngToPdf/i18n/sv.ts +111 -0
  61. package/src/tool/pngToPdf/i18n/tr.ts +111 -0
  62. package/src/tool/pngToPdf/i18n/zh.ts +111 -0
  63. package/src/tool/pngToPdf/index.ts +11 -0
  64. package/src/tool/pngToPdf/png-to-pdf-converter.css +3 -0
  65. package/src/tool/pngToPdf/seo.astro +12 -0
  66. package/src/tool/webpToPdf/bibliography.astro +11 -0
  67. package/src/tool/webpToPdf/bibliography.ts +12 -0
  68. package/src/tool/webpToPdf/component.astro +10 -0
  69. package/src/tool/webpToPdf/entry.ts +26 -0
  70. package/src/tool/webpToPdf/i18n/de.ts +111 -0
  71. package/src/tool/webpToPdf/i18n/en.ts +37 -0
  72. package/src/tool/webpToPdf/i18n/es.ts +111 -0
  73. package/src/tool/webpToPdf/i18n/fr.ts +111 -0
  74. package/src/tool/webpToPdf/i18n/id.ts +111 -0
  75. package/src/tool/webpToPdf/i18n/it.ts +111 -0
  76. package/src/tool/webpToPdf/i18n/ja.ts +111 -0
  77. package/src/tool/webpToPdf/i18n/ko.ts +111 -0
  78. package/src/tool/webpToPdf/i18n/nl.ts +111 -0
  79. package/src/tool/webpToPdf/i18n/pl.ts +111 -0
  80. package/src/tool/webpToPdf/i18n/pt.ts +111 -0
  81. package/src/tool/webpToPdf/i18n/ru.ts +111 -0
  82. package/src/tool/webpToPdf/i18n/sv.ts +111 -0
  83. package/src/tool/webpToPdf/i18n/tr.ts +111 -0
  84. package/src/tool/webpToPdf/i18n/zh.ts +111 -0
  85. package/src/tool/webpToPdf/index.ts +11 -0
  86. package/src/tool/webpToPdf/seo.astro +12 -0
  87. package/src/tool/webpToPdf/webp-to-pdf-converter.css +3 -0
  88. package/src/tools.ts +9 -1
@@ -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
+
@@ -67,12 +67,16 @@ function normalize(value: string): string {
67
67
  }
68
68
 
69
69
  function isTechnicalInvariant(text: string): boolean {
70
+ const ledUnitMatches = text.match(/\b\d+(?:-\d+)?\s*(?:w|lm)\b/g) ?? [];
70
71
  return [
71
72
  'data:image/svg+xml;base64',
72
73
  'background-image: url',
73
74
  '.layout-playground {',
74
75
  'const samplerate =',
75
- ].some((pattern) => text.includes(pattern)) || (text.includes('presets') && text.includes('hz'));
76
+ ].some((pattern) => text.includes(pattern)) ||
77
+ (text.includes('presets') && text.includes('hz')) ||
78
+ (text.includes('t_rectal') && text.includes('exp(-k * t)')) ||
79
+ (text.includes('led') && ledUnitMatches.length >= 3);
76
80
  }
77
81
 
78
82
  function collectString(value: string, output: string[]): void {
@@ -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
+ };