@jjlmoya/utils-audiovisual 1.17.0 → 1.18.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 (214) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -0
  3. package/src/entries.ts +4 -1
  4. package/src/index.ts +1 -0
  5. package/src/pages/[locale]/[slug].astro +28 -12
  6. package/src/tests/locale_completeness.test.ts +2 -36
  7. package/src/tests/shared-test-helpers.ts +56 -0
  8. package/src/tests/tool_exports.test.ts +34 -0
  9. package/src/tests/tool_validation.test.ts +2 -2
  10. package/src/tool/chromaticLens/bibliography.ts +12 -0
  11. package/src/tool/chromaticLens/entry.ts +2 -0
  12. package/src/tool/chromaticLens/i18n/de.ts +1 -15
  13. package/src/tool/chromaticLens/i18n/en.ts +1 -15
  14. package/src/tool/chromaticLens/i18n/es.ts +1 -13
  15. package/src/tool/chromaticLens/i18n/fr.ts +1 -13
  16. package/src/tool/chromaticLens/i18n/id.ts +1 -15
  17. package/src/tool/chromaticLens/i18n/it.ts +1 -15
  18. package/src/tool/chromaticLens/i18n/ja.ts +1 -15
  19. package/src/tool/chromaticLens/i18n/ko.ts +1 -15
  20. package/src/tool/chromaticLens/i18n/nl.ts +1 -15
  21. package/src/tool/chromaticLens/i18n/pl.ts +1 -15
  22. package/src/tool/chromaticLens/i18n/pt.ts +1 -15
  23. package/src/tool/chromaticLens/i18n/ru.ts +1 -15
  24. package/src/tool/chromaticLens/i18n/sv.ts +1 -15
  25. package/src/tool/chromaticLens/i18n/tr.ts +1 -15
  26. package/src/tool/chromaticLens/i18n/zh.ts +1 -15
  27. package/src/tool/chromaticLens/seo.astro +1 -1
  28. package/src/tool/collageMaker/bibliography.ts +8 -0
  29. package/src/tool/collageMaker/entry.ts +2 -0
  30. package/src/tool/collageMaker/i18n/de.ts +1 -11
  31. package/src/tool/collageMaker/i18n/en.ts +1 -11
  32. package/src/tool/collageMaker/i18n/es.ts +1 -9
  33. package/src/tool/collageMaker/i18n/fr.ts +1 -9
  34. package/src/tool/collageMaker/i18n/id.ts +1 -11
  35. package/src/tool/collageMaker/i18n/it.ts +1 -11
  36. package/src/tool/collageMaker/i18n/ja.ts +1 -11
  37. package/src/tool/collageMaker/i18n/ko.ts +1 -11
  38. package/src/tool/collageMaker/i18n/nl.ts +1 -11
  39. package/src/tool/collageMaker/i18n/pl.ts +1 -11
  40. package/src/tool/collageMaker/i18n/pt.ts +1 -11
  41. package/src/tool/collageMaker/i18n/ru.ts +1 -11
  42. package/src/tool/collageMaker/i18n/sv.ts +1 -11
  43. package/src/tool/collageMaker/i18n/tr.ts +1 -11
  44. package/src/tool/collageMaker/i18n/zh.ts +1 -11
  45. package/src/tool/collageMaker/seo.astro +1 -1
  46. package/src/tool/depthOfFieldCalculator/bibliography.astro +15 -0
  47. package/src/tool/depthOfFieldCalculator/bibliography.ts +20 -0
  48. package/src/tool/depthOfFieldCalculator/component.astro +341 -0
  49. package/src/tool/depthOfFieldCalculator/depth-of-field-calculator.css +417 -0
  50. package/src/tool/depthOfFieldCalculator/entry.ts +52 -0
  51. package/src/tool/depthOfFieldCalculator/i18n/de.ts +141 -0
  52. package/src/tool/depthOfFieldCalculator/i18n/en.ts +141 -0
  53. package/src/tool/depthOfFieldCalculator/i18n/es.ts +141 -0
  54. package/src/tool/depthOfFieldCalculator/i18n/fr.ts +141 -0
  55. package/src/tool/depthOfFieldCalculator/i18n/id.ts +141 -0
  56. package/src/tool/depthOfFieldCalculator/i18n/it.ts +141 -0
  57. package/src/tool/depthOfFieldCalculator/i18n/ja.ts +141 -0
  58. package/src/tool/depthOfFieldCalculator/i18n/ko.ts +141 -0
  59. package/src/tool/depthOfFieldCalculator/i18n/nl.ts +141 -0
  60. package/src/tool/depthOfFieldCalculator/i18n/pl.ts +141 -0
  61. package/src/tool/depthOfFieldCalculator/i18n/pt.ts +141 -0
  62. package/src/tool/depthOfFieldCalculator/i18n/ru.ts +141 -0
  63. package/src/tool/depthOfFieldCalculator/i18n/sv.ts +141 -0
  64. package/src/tool/depthOfFieldCalculator/i18n/tr.ts +141 -0
  65. package/src/tool/depthOfFieldCalculator/i18n/zh.ts +141 -0
  66. package/src/tool/depthOfFieldCalculator/index.ts +10 -0
  67. package/src/tool/depthOfFieldCalculator/logic.ts +91 -0
  68. package/src/tool/depthOfFieldCalculator/seo.astro +15 -0
  69. package/src/tool/exifCleaner/bibliography.ts +12 -0
  70. package/src/tool/exifCleaner/entry.ts +2 -0
  71. package/src/tool/exifCleaner/i18n/de.ts +1 -15
  72. package/src/tool/exifCleaner/i18n/en.ts +1 -15
  73. package/src/tool/exifCleaner/i18n/es.ts +1 -15
  74. package/src/tool/exifCleaner/i18n/fr.ts +1 -15
  75. package/src/tool/exifCleaner/i18n/id.ts +1 -15
  76. package/src/tool/exifCleaner/i18n/it.ts +1 -15
  77. package/src/tool/exifCleaner/i18n/ja.ts +1 -15
  78. package/src/tool/exifCleaner/i18n/ko.ts +1 -15
  79. package/src/tool/exifCleaner/i18n/nl.ts +1 -15
  80. package/src/tool/exifCleaner/i18n/pl.ts +1 -15
  81. package/src/tool/exifCleaner/i18n/pt.ts +1 -15
  82. package/src/tool/exifCleaner/i18n/ru.ts +1 -15
  83. package/src/tool/exifCleaner/i18n/sv.ts +1 -15
  84. package/src/tool/exifCleaner/i18n/tr.ts +1 -15
  85. package/src/tool/exifCleaner/i18n/zh.ts +1 -15
  86. package/src/tool/exifCleaner/seo.astro +2 -5
  87. package/src/tool/imageCompressor/bibliography.ts +12 -0
  88. package/src/tool/imageCompressor/entry.ts +2 -0
  89. package/src/tool/imageCompressor/i18n/de.ts +1 -15
  90. package/src/tool/imageCompressor/i18n/en.ts +1 -15
  91. package/src/tool/imageCompressor/i18n/es.ts +1 -13
  92. package/src/tool/imageCompressor/i18n/fr.ts +1 -13
  93. package/src/tool/imageCompressor/i18n/id.ts +1 -15
  94. package/src/tool/imageCompressor/i18n/it.ts +1 -15
  95. package/src/tool/imageCompressor/i18n/ja.ts +1 -15
  96. package/src/tool/imageCompressor/i18n/ko.ts +1 -15
  97. package/src/tool/imageCompressor/i18n/nl.ts +1 -15
  98. package/src/tool/imageCompressor/i18n/pl.ts +1 -15
  99. package/src/tool/imageCompressor/i18n/pt.ts +1 -15
  100. package/src/tool/imageCompressor/i18n/ru.ts +1 -15
  101. package/src/tool/imageCompressor/i18n/sv.ts +1 -15
  102. package/src/tool/imageCompressor/i18n/tr.ts +1 -15
  103. package/src/tool/imageCompressor/i18n/zh.ts +1 -15
  104. package/src/tool/imageCompressor/seo.astro +1 -1
  105. package/src/tool/printQualityCalculator/bibliography.ts +12 -0
  106. package/src/tool/printQualityCalculator/entry.ts +2 -0
  107. package/src/tool/printQualityCalculator/i18n/de.ts +1 -15
  108. package/src/tool/printQualityCalculator/i18n/en.ts +1 -15
  109. package/src/tool/printQualityCalculator/i18n/es.ts +1 -13
  110. package/src/tool/printQualityCalculator/i18n/fr.ts +1 -13
  111. package/src/tool/printQualityCalculator/i18n/id.ts +1 -15
  112. package/src/tool/printQualityCalculator/i18n/it.ts +1 -15
  113. package/src/tool/printQualityCalculator/i18n/ja.ts +1 -15
  114. package/src/tool/printQualityCalculator/i18n/ko.ts +1 -15
  115. package/src/tool/printQualityCalculator/i18n/nl.ts +1 -15
  116. package/src/tool/printQualityCalculator/i18n/pl.ts +1 -15
  117. package/src/tool/printQualityCalculator/i18n/pt.ts +1 -15
  118. package/src/tool/printQualityCalculator/i18n/ru.ts +1 -15
  119. package/src/tool/printQualityCalculator/i18n/sv.ts +1 -15
  120. package/src/tool/printQualityCalculator/i18n/tr.ts +1 -15
  121. package/src/tool/printQualityCalculator/i18n/zh.ts +1 -15
  122. package/src/tool/printQualityCalculator/seo.astro +2 -5
  123. package/src/tool/privacyBlur/bibliography.ts +8 -0
  124. package/src/tool/privacyBlur/entry.ts +2 -0
  125. package/src/tool/privacyBlur/i18n/de.ts +1 -11
  126. package/src/tool/privacyBlur/i18n/en.ts +1 -11
  127. package/src/tool/privacyBlur/i18n/es.ts +1 -9
  128. package/src/tool/privacyBlur/i18n/fr.ts +1 -9
  129. package/src/tool/privacyBlur/i18n/id.ts +1 -11
  130. package/src/tool/privacyBlur/i18n/it.ts +1 -11
  131. package/src/tool/privacyBlur/i18n/ja.ts +1 -11
  132. package/src/tool/privacyBlur/i18n/ko.ts +1 -11
  133. package/src/tool/privacyBlur/i18n/nl.ts +1 -11
  134. package/src/tool/privacyBlur/i18n/pl.ts +1 -11
  135. package/src/tool/privacyBlur/i18n/pt.ts +1 -11
  136. package/src/tool/privacyBlur/i18n/ru.ts +1 -11
  137. package/src/tool/privacyBlur/i18n/sv.ts +1 -11
  138. package/src/tool/privacyBlur/i18n/tr.ts +1 -11
  139. package/src/tool/privacyBlur/i18n/zh.ts +1 -11
  140. package/src/tool/privacyBlur/seo.astro +1 -1
  141. package/src/tool/subtitleSync/bibliography.ts +12 -0
  142. package/src/tool/subtitleSync/entry.ts +2 -0
  143. package/src/tool/subtitleSync/i18n/de.ts +1 -13
  144. package/src/tool/subtitleSync/i18n/en.ts +1 -13
  145. package/src/tool/subtitleSync/i18n/es.ts +1 -13
  146. package/src/tool/subtitleSync/i18n/fr.ts +1 -13
  147. package/src/tool/subtitleSync/i18n/id.ts +1 -13
  148. package/src/tool/subtitleSync/i18n/it.ts +1 -13
  149. package/src/tool/subtitleSync/i18n/ja.ts +1 -13
  150. package/src/tool/subtitleSync/i18n/ko.ts +1 -13
  151. package/src/tool/subtitleSync/i18n/nl.ts +1 -13
  152. package/src/tool/subtitleSync/i18n/pl.ts +1 -13
  153. package/src/tool/subtitleSync/i18n/pt.ts +1 -13
  154. package/src/tool/subtitleSync/i18n/ru.ts +1 -13
  155. package/src/tool/subtitleSync/i18n/sv.ts +1 -13
  156. package/src/tool/subtitleSync/i18n/tr.ts +1 -13
  157. package/src/tool/subtitleSync/i18n/zh.ts +1 -13
  158. package/src/tool/subtitleSync/seo.astro +1 -1
  159. package/src/tool/timelapseCalculator/bibliography.ts +20 -0
  160. package/src/tool/timelapseCalculator/entry.ts +2 -0
  161. package/src/tool/timelapseCalculator/i18n/de.ts +1 -21
  162. package/src/tool/timelapseCalculator/i18n/en.ts +1 -21
  163. package/src/tool/timelapseCalculator/i18n/es.ts +1 -21
  164. package/src/tool/timelapseCalculator/i18n/fr.ts +1 -21
  165. package/src/tool/timelapseCalculator/i18n/id.ts +1 -21
  166. package/src/tool/timelapseCalculator/i18n/it.ts +1 -21
  167. package/src/tool/timelapseCalculator/i18n/ja.ts +1 -21
  168. package/src/tool/timelapseCalculator/i18n/ko.ts +1 -21
  169. package/src/tool/timelapseCalculator/i18n/nl.ts +1 -21
  170. package/src/tool/timelapseCalculator/i18n/pl.ts +1 -21
  171. package/src/tool/timelapseCalculator/i18n/pt.ts +1 -21
  172. package/src/tool/timelapseCalculator/i18n/ru.ts +1 -21
  173. package/src/tool/timelapseCalculator/i18n/sv.ts +1 -21
  174. package/src/tool/timelapseCalculator/i18n/tr.ts +1 -21
  175. package/src/tool/timelapseCalculator/i18n/zh.ts +1 -21
  176. package/src/tool/timelapseCalculator/seo.astro +2 -5
  177. package/src/tool/tvDistance/bibliography.ts +12 -0
  178. package/src/tool/tvDistance/entry.ts +2 -0
  179. package/src/tool/tvDistance/i18n/de.ts +1 -13
  180. package/src/tool/tvDistance/i18n/en.ts +1 -13
  181. package/src/tool/tvDistance/i18n/es.ts +1 -13
  182. package/src/tool/tvDistance/i18n/fr.ts +1 -13
  183. package/src/tool/tvDistance/i18n/id.ts +1 -13
  184. package/src/tool/tvDistance/i18n/it.ts +1 -13
  185. package/src/tool/tvDistance/i18n/ja.ts +1 -13
  186. package/src/tool/tvDistance/i18n/ko.ts +1 -13
  187. package/src/tool/tvDistance/i18n/nl.ts +1 -13
  188. package/src/tool/tvDistance/i18n/pl.ts +1 -13
  189. package/src/tool/tvDistance/i18n/pt.ts +1 -13
  190. package/src/tool/tvDistance/i18n/ru.ts +1 -13
  191. package/src/tool/tvDistance/i18n/sv.ts +1 -13
  192. package/src/tool/tvDistance/i18n/tr.ts +1 -13
  193. package/src/tool/tvDistance/i18n/zh.ts +1 -13
  194. package/src/tool/tvDistance/seo.astro +1 -1
  195. package/src/tool/videoFrameExtractor/bibliography.ts +8 -0
  196. package/src/tool/videoFrameExtractor/entry.ts +2 -0
  197. package/src/tool/videoFrameExtractor/i18n/de.ts +1 -9
  198. package/src/tool/videoFrameExtractor/i18n/en.ts +1 -9
  199. package/src/tool/videoFrameExtractor/i18n/es.ts +1 -9
  200. package/src/tool/videoFrameExtractor/i18n/fr.ts +1 -8
  201. package/src/tool/videoFrameExtractor/i18n/id.ts +1 -9
  202. package/src/tool/videoFrameExtractor/i18n/it.ts +1 -8
  203. package/src/tool/videoFrameExtractor/i18n/ja.ts +1 -9
  204. package/src/tool/videoFrameExtractor/i18n/ko.ts +1 -9
  205. package/src/tool/videoFrameExtractor/i18n/nl.ts +1 -9
  206. package/src/tool/videoFrameExtractor/i18n/pl.ts +1 -9
  207. package/src/tool/videoFrameExtractor/i18n/pt.ts +1 -9
  208. package/src/tool/videoFrameExtractor/i18n/ru.ts +1 -9
  209. package/src/tool/videoFrameExtractor/i18n/sv.ts +1 -9
  210. package/src/tool/videoFrameExtractor/i18n/tr.ts +1 -9
  211. package/src/tool/videoFrameExtractor/i18n/zh.ts +1 -9
  212. package/src/tool/videoFrameExtractor/seo.astro +1 -1
  213. package/src/tools.ts +2 -0
  214. package/src/types.ts +0 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-audiovisual",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -9,6 +9,7 @@ import { tvDistance } from '../tool/tvDistance/index';
9
9
  import { imageCompressor } from '../tool/imageCompressor/index';
10
10
  import { collageMaker } from '../tool/collageMaker/index';
11
11
  import { videoFrameExtractor } from '../tool/videoFrameExtractor/index';
12
+ import { depthOfFieldCalculator } from '../tool/depthOfFieldCalculator/index';
12
13
 
13
14
  export const audiovisualCategory: AudiovisualCategoryEntry = {
14
15
  icon: 'mdi:camera-iris',
@@ -23,6 +24,7 @@ export const audiovisualCategory: AudiovisualCategoryEntry = {
23
24
  imageCompressor as AudiovisualToolEntry,
24
25
  collageMaker as AudiovisualToolEntry,
25
26
  videoFrameExtractor as AudiovisualToolEntry,
27
+ depthOfFieldCalculator as AudiovisualToolEntry,
26
28
  ],
27
29
  i18n: {
28
30
  es: async () => (await import('./i18n/es')).content,
package/src/entries.ts CHANGED
@@ -18,6 +18,8 @@ export { tvDistance } from './tool/tvDistance/entry';
18
18
  export type { TvDistanceUI, TvDistanceLocaleContent } from './tool/tvDistance/entry';
19
19
  export { videoFrameExtractor } from './tool/videoFrameExtractor/entry';
20
20
  export type { VideoFrameExtractorUI, VideoFrameExtractorLocaleContent } from './tool/videoFrameExtractor/entry';
21
+ export { depthOfFieldCalculator } from './tool/depthOfFieldCalculator/entry';
22
+ export type { DepthOfFieldUI, DepthOfFieldLocaleContent } from './tool/depthOfFieldCalculator/entry';
21
23
  export { audiovisualCategory, toolsCategory } from './category';
22
24
  import { chromaticLens } from './tool/chromaticLens/entry';
23
25
  import { collageMaker } from './tool/collageMaker/entry';
@@ -29,4 +31,5 @@ import { subtitleSync } from './tool/subtitleSync/entry';
29
31
  import { timelapseCalculator } from './tool/timelapseCalculator/entry';
30
32
  import { tvDistance } from './tool/tvDistance/entry';
31
33
  import { videoFrameExtractor } from './tool/videoFrameExtractor/entry';
32
- export const ALL_ENTRIES = [chromaticLens, collageMaker, exifCleaner, imageCompressor, printQualityCalculator, privacyBlur, subtitleSync, timelapseCalculator, tvDistance, videoFrameExtractor];
34
+ import { depthOfFieldCalculator } from './tool/depthOfFieldCalculator/entry';
35
+ export const ALL_ENTRIES = [chromaticLens, collageMaker, exifCleaner, imageCompressor, printQualityCalculator, privacyBlur, subtitleSync, timelapseCalculator, tvDistance, videoFrameExtractor, depthOfFieldCalculator];
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ export * from './tool/imageCompressor';
13
13
  export * from './tool/collageMaker';
14
14
  export * from './tool/videoFrameExtractor';
15
15
  export * from './tool/privacyBlur';
16
+ export * from './tool/depthOfFieldCalculator';
16
17
 
17
18
  export type {
18
19
  KnownLocale,
@@ -34,18 +34,28 @@ export async function getStaticPaths() {
34
34
  ]),
35
35
  ) as Partial<Record<KnownLocale, string>>;
36
36
 
37
+ const firstLoader = entry.i18n.en ?? Object.values(entry.i18n)[0];
38
+ const englishSlug = firstLoader ? (await firstLoader()).slug : entry.id;
39
+
37
40
  for (const { locale, content } of localeContents) {
38
- const allToolsNav = await Promise.all(
39
- ALL_TOOLS.map(async ({ entry: navEntry }) => ({
40
- id: navEntry.id,
41
- title: (await navEntry.i18n[locale]!()).title,
42
- href: `/${locale}/${(await navEntry.i18n[locale]!()).slug}`,
43
- isActive: navEntry.id === entry.id,
44
- })),
45
- );
41
+ const allToolsNav = (
42
+ await Promise.all(
43
+ ALL_TOOLS.map(async ({ entry: navEntry }) => {
44
+ const loader = navEntry.i18n[locale] ?? navEntry.i18n.en;
45
+ if (!loader) return null;
46
+ const navContent = await loader();
47
+ return {
48
+ id: navEntry.id,
49
+ title: navContent.title,
50
+ href: `/${locale}/${navContent.slug}`,
51
+ isActive: navEntry.id === entry.id,
52
+ };
53
+ }),
54
+ )
55
+ ).filter(Boolean) as NavItem[];
46
56
  paths.push({
47
57
  params: { locale, slug: content.slug },
48
- props: { Component, locale, content, localeUrls, allToolsNav },
58
+ props: { Component, locale, content, localeUrls, allToolsNav, englishSlug },
49
59
  });
50
60
  }
51
61
  }
@@ -66,11 +76,16 @@ interface Props {
66
76
  content: ToolLocaleContent;
67
77
  localeUrls: Partial<Record<KnownLocale, string>>;
68
78
  allToolsNav: NavItem[];
79
+ englishSlug: string;
69
80
  }
70
81
 
71
- const { Component, locale, content, localeUrls, allToolsNav } = Astro.props;
82
+ const { Component, locale, content, localeUrls, allToolsNav, englishSlug } = Astro.props;
83
+
84
+ const cssFiles = import.meta.glob("../../tool/*/*.css", { query: "?raw", import: "default" });
85
+ const cssKey = Object.keys(cssFiles).find((k) => k.endsWith(`/${englishSlug}.css`));
86
+ const toolCss = cssKey ? await cssFiles[cssKey]() as string : "";
72
87
 
73
- const seoContent: UtilitySEOContent = { locale, sections: content.seo };
88
+ const seoContent: UtilitySEOContent = { locale, sections: content.seo ?? [] };
74
89
 
75
90
  const words = content.title.split(" ");
76
91
  const titleHighlight = words[0] || "";
@@ -89,8 +104,9 @@ const titleBase = words.slice(1).join(" ") || "";
89
104
  tools={allToolsNav}
90
105
  />
91
106
  <Fragment slot="head">
107
+ <Fragment set:html={toolCss ? `<style>${toolCss}</style>` : ''} />
92
108
  {
93
- content.schemas.map((schema: unknown) => (
109
+ ( content.schemas ?? []).map((schema: unknown) => (
94
110
  <script
95
111
  is:inline
96
112
  type="application/ld+json"
@@ -1,42 +1,8 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { ALL_TOOLS } from '../tools';
3
- import type { ToolLocaleContent } from '../types';
4
3
 
5
4
  describe('Locale Completeness Validation', () => {
6
- ALL_TOOLS.forEach((tool) => {
7
- describe(`Tool: ${tool.entry.id}`, () => {
8
- Object.keys(tool.entry.i18n).forEach((locale) => {
9
- describe(`Locale: ${locale}`, () => {
10
- it('faqTitle should be defined when faq items exist', async () => {
11
- const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
12
- const content = (await loader?.()) as ToolLocaleContent;
13
-
14
- if (content.faq.length > 0) {
15
- expect(
16
- content.faqTitle,
17
- `Tool "${tool.entry.id}" locale "${locale}" has ${content.faq.length} FAQ items but is missing faqTitle`,
18
- ).toBeTruthy();
19
- }
20
- });
21
-
22
- it('bibliographyTitle should be defined when bibliography items exist', async () => {
23
- const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
24
- const content = (await loader?.()) as ToolLocaleContent;
25
-
26
- if (content.bibliography.length > 0) {
27
- expect(
28
- content.bibliographyTitle,
29
- `Tool "${tool.entry.id}" locale "${locale}" has ${content.bibliography.length} bibliography items but is missing bibliographyTitle`,
30
- ).toBeTruthy();
31
- }
32
- });
33
- });
34
- });
35
- });
36
- });
37
-
38
- it('all 10 tools registered', () => {
39
- expect(ALL_TOOLS.length).toBe(10);
5
+ it('all 11 tools registered', () => {
6
+ expect(ALL_TOOLS.length).toBe(11);
40
7
  });
41
8
  });
42
-
@@ -0,0 +1,56 @@
1
+ import type { ToolDefinition } from '../types';
2
+
3
+ export interface ToolExportValidationResult {
4
+ passed: boolean;
5
+ failures: string[];
6
+ }
7
+
8
+ function validateComponentType(
9
+ toolId: string,
10
+ componentName: string,
11
+ component: unknown,
12
+ failures: string[],
13
+ ): void {
14
+ if (typeof component !== 'function') {
15
+ failures.push(`${toolId}: ${componentName} is not a function (${typeof component})`);
16
+ }
17
+ }
18
+
19
+ async function validateComponentExecution(
20
+ toolId: string,
21
+ componentName: string,
22
+ fn: () => Promise<unknown>,
23
+ failures: string[],
24
+ ): Promise<void> {
25
+ try {
26
+ const result = await fn();
27
+ if (!result || typeof result !== 'object') {
28
+ failures.push(`${toolId}: ${componentName} import returned invalid result`);
29
+ }
30
+ } catch (error) {
31
+ failures.push(`${toolId}: ${componentName} execution error - ${error instanceof Error ? error.message : 'unknown'}`);
32
+ }
33
+ }
34
+
35
+ export async function validateToolExports(tools: ToolDefinition[]): Promise<ToolExportValidationResult> {
36
+ const failures: string[] = [];
37
+
38
+ for (const tool of tools) {
39
+ validateComponentType(tool.entry.id, 'Component', tool.Component, failures);
40
+ validateComponentType(tool.entry.id, 'SEOComponent', tool.SEOComponent, failures);
41
+ validateComponentType(tool.entry.id, 'BibliographyComponent', tool.BibliographyComponent, failures);
42
+
43
+ const componentFn = tool.Component as () => Promise<unknown>;
44
+ const seoFn = tool.SEOComponent as () => Promise<unknown>;
45
+ const bibFn = tool.BibliographyComponent as () => Promise<unknown>;
46
+
47
+ await validateComponentExecution(tool.entry.id, 'Component', componentFn, failures);
48
+ await validateComponentExecution(tool.entry.id, 'SEOComponent', seoFn, failures);
49
+ await validateComponentExecution(tool.entry.id, 'BibliographyComponent', bibFn, failures);
50
+ }
51
+
52
+ return {
53
+ passed: failures.length === 0,
54
+ failures,
55
+ };
56
+ }
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+ import { validateToolExports } from './shared-test-helpers';
4
+
5
+ describe('Tool Exports Pattern Validation', () => {
6
+ describe('Component Exports Format', () => {
7
+ ALL_TOOLS.forEach((tool) => {
8
+ it(`${tool.entry.id}: Component should be a lazy-loaded function`, () => {
9
+ expect(typeof tool.Component).toBe('function');
10
+ expect(tool.Component).toBeInstanceOf(Function);
11
+ });
12
+
13
+ it(`${tool.entry.id}: SEOComponent should be a lazy-loaded function`, () => {
14
+ expect(typeof tool.SEOComponent).toBe('function');
15
+ expect(tool.SEOComponent).toBeInstanceOf(Function);
16
+ });
17
+
18
+ it(`${tool.entry.id}: BibliographyComponent should be a lazy-loaded function`, () => {
19
+ expect(typeof tool.BibliographyComponent).toBe('function');
20
+ expect(tool.BibliographyComponent).toBeInstanceOf(Function);
21
+ });
22
+ });
23
+ });
24
+
25
+ describe('Dynamic Import Validation', () => {
26
+ it('all tools must have functional dynamic imports', async () => {
27
+ const result = await validateToolExports(ALL_TOOLS);
28
+ if (!result.passed) {
29
+ throw new Error(`Tool export validation failed:\n${result.failures.join('\n')}`);
30
+ }
31
+ expect(result.passed).toBe(true);
32
+ });
33
+ });
34
+ });
@@ -4,8 +4,8 @@ import { audiovisualCategory } from '../data';
4
4
 
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
- it('should have 10 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(10);
7
+ it('should have 11 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(11);
9
9
  });
10
10
 
11
11
  it('audiovisualCategory should be defined', () => {
@@ -0,0 +1,12 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: "Median Cut Algorithm - Wikipedia",
6
+ url: "https://en.wikipedia.org/wiki/Median_cut",
7
+ },
8
+ {
9
+ name: "Farblehre für Designer",
10
+ url: "https://www.smashingmagazine.com/2010/01/color-theory-for-designers-part-1-the-meaning-of-color/",
11
+ },
12
+ ];
@@ -37,3 +37,5 @@ export const chromaticLens: AudiovisualToolEntry<ChromaticLensUI> = {
37
37
  zh: async () => (await import('./i18n/zh')).content as unknown as ChromaticLensLocaleContent,
38
38
  },
39
39
  };
40
+
41
+ export { bibliography } from './bibliography';
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography';
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ChromaticLensUI, ChromaticLensLocaleContent } from '../index';
3
4
 
@@ -14,8 +15,6 @@ const ui: ChromaticLensUI = {
14
15
  copiedLabel: "Kopiert!",
15
16
  colorCountLabel: "Anzahl der Farben",
16
17
  changeImage: "Bild ändern",
17
- faqTitle: "Häufig gestellte Fragen zur Farbetraktion",
18
- bibliographyTitle: "Ressourcen und technische Dokumentation"
19
18
  };
20
19
 
21
20
  const faq: ChromaticLensLocaleContent['faq'] = [
@@ -52,17 +51,6 @@ const howTo: ChromaticLensLocaleContent['howTo'] = [
52
51
  },
53
52
  ];
54
53
 
55
- const bibliography: ChromaticLensLocaleContent['bibliography'] = [
56
- {
57
- name: "Median Cut Algorithm - Wikipedia",
58
- url: "https://en.wikipedia.org/wiki/Median_cut",
59
- },
60
- {
61
- name: "Farblehre für Designer",
62
- url: "https://www.smashingmagazine.com/2010/01/color-theory-for-designers-part-1-the-meaning-of-color/",
63
- },
64
- ];
65
-
66
54
  const seo: ChromaticLensLocaleContent['seo'] = [
67
55
  {
68
56
  type: 'summary',
@@ -237,9 +225,7 @@ export const content: ChromaticLensLocaleContent = {
237
225
  description,
238
226
  ui,
239
227
  seo,
240
- faqTitle: "Häufig gestellte Fragen",
241
228
  faq,
242
- bibliographyTitle: "Referenzen",
243
229
  bibliography,
244
230
  howTo,
245
231
  schemas: [faqSchema as any, howToSchema as any, appSchema],
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography';
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ChromaticLensUI, ChromaticLensLocaleContent } from '../index';
3
4
 
@@ -14,8 +15,6 @@ const ui: ChromaticLensUI = {
14
15
  copiedLabel: "Copied!",
15
16
  colorCountLabel: "Number of colors",
16
17
  changeImage: "Change image",
17
- faqTitle: "Frequently asked questions about color extraction",
18
- bibliographyTitle: "Resources and technical documentation"
19
18
  };
20
19
 
21
20
  const faq: ChromaticLensLocaleContent['faq'] = [
@@ -52,17 +51,6 @@ const howTo: ChromaticLensLocaleContent['howTo'] = [
52
51
  },
53
52
  ];
54
53
 
55
- const bibliography: ChromaticLensLocaleContent['bibliography'] = [
56
- {
57
- name: "Median Cut Algorithm - Wikipedia",
58
- url: "https://en.wikipedia.org/wiki/Median_cut",
59
- },
60
- {
61
- name: "Color Theory for Designers",
62
- url: "https://www.smashingmagazine.com/2010/01/color-theory-for-designers-part-1-the-meaning-of-color/",
63
- },
64
- ];
65
-
66
54
  const seo: ChromaticLensLocaleContent['seo'] = [
67
55
  {
68
56
  type: 'summary',
@@ -237,9 +225,7 @@ export const content: ChromaticLensLocaleContent = {
237
225
  description,
238
226
  ui,
239
227
  seo,
240
- faqTitle: "Frequently Asked Questions",
241
228
  faq,
242
- bibliographyTitle: "References",
243
229
  bibliography,
244
230
  howTo,
245
231
  schemas: [faqSchema as any, howToSchema as any, appSchema],
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography';
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ChromaticLensUI, ChromaticLensLocaleContent } from '../index';
3
4
 
@@ -50,17 +51,6 @@ const howTo: ChromaticLensLocaleContent['howTo'] = [
50
51
  },
51
52
  ];
52
53
 
53
- const bibliography: ChromaticLensLocaleContent['bibliography'] = [
54
- {
55
- name: "Median Cut Algorithm - Wikipedia",
56
- url: "https://en.wikipedia.org/wiki/Median_cut",
57
- },
58
- {
59
- name: "Color Theory for Designers",
60
- url: "https://www.smashingmagazine.com/2010/01/color-theory-for-designers-part-1-the-meaning-of-color/",
61
- },
62
- ];
63
-
64
54
  const seo: ChromaticLensLocaleContent['seo'] = [
65
55
  {
66
56
  type: 'summary',
@@ -236,9 +226,7 @@ export const content: ChromaticLensLocaleContent = {
236
226
  ui,
237
227
  seo,
238
228
  faq,
239
- faqTitle: 'Preguntas frecuentes sobre paletas de color',
240
229
  bibliography,
241
- bibliographyTitle: 'Recursos técnicos sobre teoría del color',
242
230
  howTo,
243
231
  schemas: [faqSchema as any, howToSchema as any, appSchema],
244
232
  };
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography';
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ChromaticLensUI, ChromaticLensLocaleContent } from '../index';
3
4
 
@@ -50,17 +51,6 @@ const howTo: ChromaticLensLocaleContent['howTo'] = [
50
51
  },
51
52
  ];
52
53
 
53
- const bibliography: ChromaticLensLocaleContent['bibliography'] = [
54
- {
55
- name: "Median Cut Algorithm - Wikipédia",
56
- url: "https://fr.wikipedia.org/wiki/Coupure_m%C3%A9diane",
57
- },
58
- {
59
- name: "Théorie des couleurs pour les designers",
60
- url: "https://www.smashingmagazine.com/2010/01/color-theory-for-designers-part-1-the-meaning-of-color/",
61
- },
62
- ];
63
-
64
54
  const seo: ChromaticLensLocaleContent['seo'] = [
65
55
  {
66
56
  type: 'summary',
@@ -235,10 +225,8 @@ export const content: ChromaticLensLocaleContent = {
235
225
  description,
236
226
  ui,
237
227
  seo,
238
- faqTitle: "Frequently Asked Questions",
239
228
  faq,
240
229
  bibliography,
241
- bibliographyTitle: "References",
242
230
  howTo,
243
231
  schemas: [faqSchema as any, howToSchema as any, appSchema],
244
232
  };
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography';
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ChromaticLensUI, ChromaticLensLocaleContent } from '../index';
3
4
 
@@ -14,8 +15,6 @@ const ui: ChromaticLensUI = {
14
15
  copiedLabel: "Tersalin!",
15
16
  colorCountLabel: "Jumlah warna",
16
17
  changeImage: "Ganti gambar",
17
- faqTitle: "Pertanyaan umum tentang ekstraksi warna",
18
- bibliographyTitle: "Sumber daya dan dokumentasi teknis"
19
18
  };
20
19
 
21
20
  const faq: ChromaticLensLocaleContent['faq'] = [
@@ -52,17 +51,6 @@ const howTo: ChromaticLensLocaleContent['howTo'] = [
52
51
  },
53
52
  ];
54
53
 
55
- const bibliography: ChromaticLensLocaleContent['bibliography'] = [
56
- {
57
- name: "Median Cut Algorithm - Wikipedia",
58
- url: "https://en.wikipedia.org/wiki/Median_cut",
59
- },
60
- {
61
- name: "Teori Warna untuk Desainer",
62
- url: "https://www.smashingmagazine.com/2010/01/color-theory-for-designers-part-1-the-meaning-of-color/",
63
- },
64
- ];
65
-
66
54
  const seo: ChromaticLensLocaleContent['seo'] = [
67
55
  {
68
56
  type: 'summary',
@@ -237,9 +225,7 @@ export const content: ChromaticLensLocaleContent = {
237
225
  description,
238
226
  ui,
239
227
  seo,
240
- faqTitle: "Pertanyaan yang Sering Diajukan",
241
228
  faq,
242
- bibliographyTitle: "Referensi",
243
229
  bibliography,
244
230
  howTo,
245
231
  schemas: [faqSchema as any, howToSchema as any, appSchema],
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography';
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ChromaticLensUI, ChromaticLensLocaleContent } from '../index';
3
4
 
@@ -14,8 +15,6 @@ const ui: ChromaticLensUI = {
14
15
  copiedLabel: "Copiato!",
15
16
  colorCountLabel: "Numero di colori",
16
17
  changeImage: "Cambia immagine",
17
- faqTitle: "Domande frequenti sull'estrazione del colore",
18
- bibliographyTitle: "Risorse e documentazione tecnica"
19
18
  };
20
19
 
21
20
  const faq: ChromaticLensLocaleContent['faq'] = [
@@ -52,17 +51,6 @@ const howTo: ChromaticLensLocaleContent['howTo'] = [
52
51
  },
53
52
  ];
54
53
 
55
- const bibliography: ChromaticLensLocaleContent['bibliography'] = [
56
- {
57
- name: "Median Cut Algorithm - Wikipedia",
58
- url: "https://en.wikipedia.org/wiki/Median_cut",
59
- },
60
- {
61
- name: "Teoria del Colore per Designer",
62
- url: "https://www.smashingmagazine.com/2010/01/color-theory-for-designers-part-1-the-meaning-of-color/",
63
- },
64
- ];
65
-
66
54
  const seo: ChromaticLensLocaleContent['seo'] = [
67
55
  {
68
56
  type: 'summary',
@@ -237,9 +225,7 @@ export const content: ChromaticLensLocaleContent = {
237
225
  description,
238
226
  ui,
239
227
  seo,
240
- faqTitle: "Domande Frequenti",
241
228
  faq,
242
- bibliographyTitle: "Riferimenti",
243
229
  bibliography,
244
230
  howTo,
245
231
  schemas: [faqSchema as any, howToSchema as any, appSchema],
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography';
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ChromaticLensUI, ChromaticLensLocaleContent } from '../index';
3
4
 
@@ -14,8 +15,6 @@ const ui: ChromaticLensUI = {
14
15
  copiedLabel: "コピーしました!",
15
16
  colorCountLabel: "色の数",
16
17
  changeImage: "画像を変更",
17
- faqTitle: "カラー抽出に関するよくある質問",
18
- bibliographyTitle: "リソースと技術ドキュメント"
19
18
  };
20
19
 
21
20
  const faq: ChromaticLensLocaleContent['faq'] = [
@@ -52,17 +51,6 @@ const howTo: ChromaticLensLocaleContent['howTo'] = [
52
51
  },
53
52
  ];
54
53
 
55
- const bibliography: ChromaticLensLocaleContent['bibliography'] = [
56
- {
57
- name: "メディアンカット・アルゴリズム - Wikipedia",
58
- url: "https://ja.wikipedia.org/wiki/%E3%83%A1%E3%83%87%E3%82%A3%E3%82%A2%E3%83%B3%E3%82%AB%E3%83%83%E3%83%88",
59
- },
60
- {
61
- name: "デザイナーのための色彩理論",
62
- url: "https://www.smashingmagazine.com/2010/01/color-theory-for-designers-part-1-the-meaning-of-color/",
63
- },
64
- ];
65
-
66
54
  const seo: ChromaticLensLocaleContent['seo'] = [
67
55
  {
68
56
  type: 'summary',
@@ -237,9 +225,7 @@ export const content: ChromaticLensLocaleContent = {
237
225
  description,
238
226
  ui,
239
227
  seo,
240
- faqTitle: "よくある質問",
241
228
  faq,
242
- bibliographyTitle: "リファレンス",
243
229
  bibliography,
244
230
  howTo,
245
231
  schemas: [faqSchema as any, howToSchema as any, appSchema],
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography';
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ChromaticLensUI, ChromaticLensLocaleContent } from '../index';
3
4
 
@@ -14,8 +15,6 @@ const ui: ChromaticLensUI = {
14
15
  copiedLabel: "복사 완료!",
15
16
  colorCountLabel: "색상 수",
16
17
  changeImage: "이미지 변경",
17
- faqTitle: "색상 추출에 관한 자주 묻는 질문",
18
- bibliographyTitle: "리소스 및 기술 문서"
19
18
  };
20
19
 
21
20
  const faq: ChromaticLensLocaleContent['faq'] = [
@@ -52,17 +51,6 @@ const howTo: ChromaticLensLocaleContent['howTo'] = [
52
51
  },
53
52
  ];
54
53
 
55
- const bibliography: ChromaticLensLocaleContent['bibliography'] = [
56
- {
57
- name: "메디안 컷 알고리즘 - 위키백과",
58
- url: "https://ko.wikipedia.org/wiki/%EB%A9%94%EB%94%94%EC%95%88_%EC%BB%B7_%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98",
59
- },
60
- {
61
- name: "디자이너를 위한 색채 이론",
62
- url: "https://www.smashingmagazine.com/2010/01/color-theory-for-designers-part-1-the-meaning-of-color/",
63
- },
64
- ];
65
-
66
54
  const seo: ChromaticLensLocaleContent['seo'] = [
67
55
  {
68
56
  type: 'summary',
@@ -237,9 +225,7 @@ export const content: ChromaticLensLocaleContent = {
237
225
  description,
238
226
  ui,
239
227
  seo,
240
- faqTitle: "자주 묻는 질문",
241
228
  faq,
242
- bibliographyTitle: "참고 문헌",
243
229
  bibliography,
244
230
  howTo,
245
231
  schemas: [faqSchema as any, howToSchema as any, appSchema],
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography';
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ChromaticLensUI, ChromaticLensLocaleContent } from '../index';
3
4
 
@@ -14,8 +15,6 @@ const ui: ChromaticLensUI = {
14
15
  copiedLabel: "Gekopieerd!",
15
16
  colorCountLabel: "Aantal kleuren",
16
17
  changeImage: "Afbeelding wijzigen",
17
- faqTitle: "Veelgestelde vragen over kleurextractie",
18
- bibliographyTitle: "Hulpmiddelen en technische documentatie"
19
18
  };
20
19
 
21
20
  const faq: ChromaticLensLocaleContent['faq'] = [
@@ -52,17 +51,6 @@ const howTo: ChromaticLensLocaleContent['howTo'] = [
52
51
  },
53
52
  ];
54
53
 
55
- const bibliography: ChromaticLensLocaleContent['bibliography'] = [
56
- {
57
- name: "Median Cut Algorithm - Wikipedia",
58
- url: "https://en.wikipedia.org/wiki/Median_cut",
59
- },
60
- {
61
- name: "Kleurentheorie voor Ontwerpers",
62
- url: "https://www.smashingmagazine.com/2010/01/color-theory-for-designers-part-1-the-meaning-of-color/",
63
- },
64
- ];
65
-
66
54
  const seo: ChromaticLensLocaleContent['seo'] = [
67
55
  {
68
56
  type: 'summary',
@@ -237,9 +225,7 @@ export const content: ChromaticLensLocaleContent = {
237
225
  description,
238
226
  ui,
239
227
  seo,
240
- faqTitle: "Veelgestelde Vragen",
241
228
  faq,
242
- bibliographyTitle: "Referenties",
243
229
  bibliography,
244
230
  howTo,
245
231
  schemas: [faqSchema as any, howToSchema as any, appSchema],