@jjlmoya/utils-forensic-science 1.1.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 (316) hide show
  1. package/package.json +69 -0
  2. package/scripts/crystal-lattice-catalog-translations.mjs +459 -0
  3. package/scripts/postinstall.mjs +27 -0
  4. package/scripts/sync-crystal-lattice-i18n.mjs +50 -0
  5. package/src/category/i18n/de.ts +55 -0
  6. package/src/category/i18n/en.ts +55 -0
  7. package/src/category/i18n/es.ts +55 -0
  8. package/src/category/i18n/fr.ts +55 -0
  9. package/src/category/i18n/id.ts +55 -0
  10. package/src/category/i18n/it.ts +55 -0
  11. package/src/category/i18n/ja.ts +56 -0
  12. package/src/category/i18n/ko.ts +56 -0
  13. package/src/category/i18n/nl.ts +55 -0
  14. package/src/category/i18n/pl.ts +55 -0
  15. package/src/category/i18n/pt.ts +55 -0
  16. package/src/category/i18n/ru.ts +55 -0
  17. package/src/category/i18n/sv.ts +55 -0
  18. package/src/category/i18n/tr.ts +55 -0
  19. package/src/category/i18n/zh.ts +56 -0
  20. package/src/category/index.ts +48 -0
  21. package/src/category/seo.astro +15 -0
  22. package/src/components/PreviewNavSidebar.astro +116 -0
  23. package/src/components/PreviewToolbar.astro +143 -0
  24. package/src/data.ts +10 -0
  25. package/src/entries.ts +48 -0
  26. package/src/env.d.ts +5 -0
  27. package/src/index.ts +29 -0
  28. package/src/layouts/PreviewLayout.astro +117 -0
  29. package/src/pages/[locale]/[slug].astro +162 -0
  30. package/src/pages/[locale].astro +261 -0
  31. package/src/pages/index.astro +3 -0
  32. package/src/tests/diacritics_density.test.ts +140 -0
  33. package/src/tests/faq_count.test.ts +19 -0
  34. package/src/tests/i18n_coverage.test.ts +36 -0
  35. package/src/tests/inverted_punctuation.test.ts +84 -0
  36. package/src/tests/locale_completeness.test.ts +24 -0
  37. package/src/tests/mocks/astro_mock.js +2 -0
  38. package/src/tests/no_en_dash.test.ts +70 -0
  39. package/src/tests/no_h1_in_components.test.ts +48 -0
  40. package/src/tests/schemas_fulfillment.test.ts +23 -0
  41. package/src/tests/script_density.test.ts +94 -0
  42. package/src/tests/seo_length.test.ts +46 -0
  43. package/src/tests/shared-test-helpers.ts +56 -0
  44. package/src/tests/slug_language_code_format.test.ts +23 -0
  45. package/src/tests/slug_uniqueness.test.ts +81 -0
  46. package/src/tests/title_quality.test.ts +55 -0
  47. package/src/tests/tool_exports.test.ts +34 -0
  48. package/src/tests/tool_validation.test.ts +16 -0
  49. package/src/tests/widmark.test.ts +28 -0
  50. package/src/tool/forensic-age-estimator/bibliography.astro +14 -0
  51. package/src/tool/forensic-age-estimator/bibliography.ts +16 -0
  52. package/src/tool/forensic-age-estimator/component.astro +321 -0
  53. package/src/tool/forensic-age-estimator/dental-skeletal-third-molar-age-estimator.css +634 -0
  54. package/src/tool/forensic-age-estimator/entry.ts +32 -0
  55. package/src/tool/forensic-age-estimator/i18n/de.ts +245 -0
  56. package/src/tool/forensic-age-estimator/i18n/en.ts +245 -0
  57. package/src/tool/forensic-age-estimator/i18n/es.ts +245 -0
  58. package/src/tool/forensic-age-estimator/i18n/fr.ts +245 -0
  59. package/src/tool/forensic-age-estimator/i18n/id.ts +245 -0
  60. package/src/tool/forensic-age-estimator/i18n/it.ts +245 -0
  61. package/src/tool/forensic-age-estimator/i18n/ja.ts +245 -0
  62. package/src/tool/forensic-age-estimator/i18n/ko.ts +245 -0
  63. package/src/tool/forensic-age-estimator/i18n/nl.ts +245 -0
  64. package/src/tool/forensic-age-estimator/i18n/pl.ts +245 -0
  65. package/src/tool/forensic-age-estimator/i18n/pt.ts +245 -0
  66. package/src/tool/forensic-age-estimator/i18n/ru.ts +245 -0
  67. package/src/tool/forensic-age-estimator/i18n/sv.ts +245 -0
  68. package/src/tool/forensic-age-estimator/i18n/tr.ts +245 -0
  69. package/src/tool/forensic-age-estimator/i18n/zh.ts +246 -0
  70. package/src/tool/forensic-age-estimator/index.ts +11 -0
  71. package/src/tool/forensic-age-estimator/logic.ts +81 -0
  72. package/src/tool/forensic-age-estimator/seo.astro +15 -0
  73. package/src/tool/forensic-blood-test-simulator/bibliography.astro +6 -0
  74. package/src/tool/forensic-blood-test-simulator/bibliography.ts +12 -0
  75. package/src/tool/forensic-blood-test-simulator/component.astro +168 -0
  76. package/src/tool/forensic-blood-test-simulator/dom-utils.ts +40 -0
  77. package/src/tool/forensic-blood-test-simulator/entry.ts +32 -0
  78. package/src/tool/forensic-blood-test-simulator/forensic-presumptive-blood-testing-luminol-kastle-meyer-simulator.css +532 -0
  79. package/src/tool/forensic-blood-test-simulator/helpers.ts +16 -0
  80. package/src/tool/forensic-blood-test-simulator/i18n/de.ts +185 -0
  81. package/src/tool/forensic-blood-test-simulator/i18n/en.ts +185 -0
  82. package/src/tool/forensic-blood-test-simulator/i18n/es.ts +185 -0
  83. package/src/tool/forensic-blood-test-simulator/i18n/fr.ts +220 -0
  84. package/src/tool/forensic-blood-test-simulator/i18n/id.ts +220 -0
  85. package/src/tool/forensic-blood-test-simulator/i18n/it.ts +220 -0
  86. package/src/tool/forensic-blood-test-simulator/i18n/ja.ts +220 -0
  87. package/src/tool/forensic-blood-test-simulator/i18n/ko.ts +220 -0
  88. package/src/tool/forensic-blood-test-simulator/i18n/nl.ts +220 -0
  89. package/src/tool/forensic-blood-test-simulator/i18n/pl.ts +220 -0
  90. package/src/tool/forensic-blood-test-simulator/i18n/pt.ts +220 -0
  91. package/src/tool/forensic-blood-test-simulator/i18n/ru.ts +220 -0
  92. package/src/tool/forensic-blood-test-simulator/i18n/sv.ts +220 -0
  93. package/src/tool/forensic-blood-test-simulator/i18n/tr.ts +220 -0
  94. package/src/tool/forensic-blood-test-simulator/i18n/zh.ts +220 -0
  95. package/src/tool/forensic-blood-test-simulator/index.ts +11 -0
  96. package/src/tool/forensic-blood-test-simulator/logic.ts +174 -0
  97. package/src/tool/forensic-blood-test-simulator/script.ts +192 -0
  98. package/src/tool/forensic-blood-test-simulator/seo.astro +15 -0
  99. package/src/tool/forensic-fiber-comparison-microscope/bibliography.astro +6 -0
  100. package/src/tool/forensic-fiber-comparison-microscope/bibliography.ts +16 -0
  101. package/src/tool/forensic-fiber-comparison-microscope/component.astro +146 -0
  102. package/src/tool/forensic-fiber-comparison-microscope/entry.ts +32 -0
  103. package/src/tool/forensic-fiber-comparison-microscope/forensic-fiber-comparison-microscope.css +629 -0
  104. package/src/tool/forensic-fiber-comparison-microscope/i18n/de.ts +250 -0
  105. package/src/tool/forensic-fiber-comparison-microscope/i18n/en.ts +246 -0
  106. package/src/tool/forensic-fiber-comparison-microscope/i18n/es.ts +246 -0
  107. package/src/tool/forensic-fiber-comparison-microscope/i18n/fr.ts +246 -0
  108. package/src/tool/forensic-fiber-comparison-microscope/i18n/id.ts +250 -0
  109. package/src/tool/forensic-fiber-comparison-microscope/i18n/it.ts +246 -0
  110. package/src/tool/forensic-fiber-comparison-microscope/i18n/ja.ts +250 -0
  111. package/src/tool/forensic-fiber-comparison-microscope/i18n/ko.ts +246 -0
  112. package/src/tool/forensic-fiber-comparison-microscope/i18n/nl.ts +246 -0
  113. package/src/tool/forensic-fiber-comparison-microscope/i18n/pl.ts +250 -0
  114. package/src/tool/forensic-fiber-comparison-microscope/i18n/pt.ts +250 -0
  115. package/src/tool/forensic-fiber-comparison-microscope/i18n/ru.ts +250 -0
  116. package/src/tool/forensic-fiber-comparison-microscope/i18n/sv.ts +250 -0
  117. package/src/tool/forensic-fiber-comparison-microscope/i18n/tr.ts +246 -0
  118. package/src/tool/forensic-fiber-comparison-microscope/i18n/zh.ts +250 -0
  119. package/src/tool/forensic-fiber-comparison-microscope/index.ts +11 -0
  120. package/src/tool/forensic-fiber-comparison-microscope/logic.ts +244 -0
  121. package/src/tool/forensic-fiber-comparison-microscope/render.ts +265 -0
  122. package/src/tool/forensic-fiber-comparison-microscope/seo.astro +15 -0
  123. package/src/tool/forensic-fiber-comparison-microscope/view.ts +267 -0
  124. package/src/tool/forensic-glass-becke-line-simulator/bibliography.astro +6 -0
  125. package/src/tool/forensic-glass-becke-line-simulator/bibliography.ts +16 -0
  126. package/src/tool/forensic-glass-becke-line-simulator/component.astro +81 -0
  127. package/src/tool/forensic-glass-becke-line-simulator/entry.ts +32 -0
  128. package/src/tool/forensic-glass-becke-line-simulator/forensic-glass-becke-line-simulator.css +392 -0
  129. package/src/tool/forensic-glass-becke-line-simulator/i18n/de.ts +231 -0
  130. package/src/tool/forensic-glass-becke-line-simulator/i18n/en.ts +231 -0
  131. package/src/tool/forensic-glass-becke-line-simulator/i18n/es.ts +231 -0
  132. package/src/tool/forensic-glass-becke-line-simulator/i18n/fr.ts +231 -0
  133. package/src/tool/forensic-glass-becke-line-simulator/i18n/id.ts +231 -0
  134. package/src/tool/forensic-glass-becke-line-simulator/i18n/it.ts +231 -0
  135. package/src/tool/forensic-glass-becke-line-simulator/i18n/ja.ts +231 -0
  136. package/src/tool/forensic-glass-becke-line-simulator/i18n/ko.ts +231 -0
  137. package/src/tool/forensic-glass-becke-line-simulator/i18n/nl.ts +231 -0
  138. package/src/tool/forensic-glass-becke-line-simulator/i18n/pl.ts +231 -0
  139. package/src/tool/forensic-glass-becke-line-simulator/i18n/pt.ts +231 -0
  140. package/src/tool/forensic-glass-becke-line-simulator/i18n/ru.ts +231 -0
  141. package/src/tool/forensic-glass-becke-line-simulator/i18n/sv.ts +231 -0
  142. package/src/tool/forensic-glass-becke-line-simulator/i18n/tr.ts +231 -0
  143. package/src/tool/forensic-glass-becke-line-simulator/i18n/zh.ts +231 -0
  144. package/src/tool/forensic-glass-becke-line-simulator/index.ts +11 -0
  145. package/src/tool/forensic-glass-becke-line-simulator/logic.ts +100 -0
  146. package/src/tool/forensic-glass-becke-line-simulator/seo.astro +15 -0
  147. package/src/tool/forensic-glass-becke-line-simulator/view.ts +281 -0
  148. package/src/tool/forensic-image-authenticity-analyzer/bibliography.astro +9 -0
  149. package/src/tool/forensic-image-authenticity-analyzer/bibliography.ts +7 -0
  150. package/src/tool/forensic-image-authenticity-analyzer/component.astro +250 -0
  151. package/src/tool/forensic-image-authenticity-analyzer/entry.ts +29 -0
  152. package/src/tool/forensic-image-authenticity-analyzer/forensic-image-metadata-authenticity-analyzer.css +679 -0
  153. package/src/tool/forensic-image-authenticity-analyzer/i18n/de.ts +105 -0
  154. package/src/tool/forensic-image-authenticity-analyzer/i18n/en.ts +105 -0
  155. package/src/tool/forensic-image-authenticity-analyzer/i18n/es.ts +105 -0
  156. package/src/tool/forensic-image-authenticity-analyzer/i18n/fr.ts +105 -0
  157. package/src/tool/forensic-image-authenticity-analyzer/i18n/id.ts +76 -0
  158. package/src/tool/forensic-image-authenticity-analyzer/i18n/it.ts +105 -0
  159. package/src/tool/forensic-image-authenticity-analyzer/i18n/ja.ts +72 -0
  160. package/src/tool/forensic-image-authenticity-analyzer/i18n/ko.ts +72 -0
  161. package/src/tool/forensic-image-authenticity-analyzer/i18n/nl.ts +75 -0
  162. package/src/tool/forensic-image-authenticity-analyzer/i18n/pl.ts +75 -0
  163. package/src/tool/forensic-image-authenticity-analyzer/i18n/pt.ts +105 -0
  164. package/src/tool/forensic-image-authenticity-analyzer/i18n/ru.ts +75 -0
  165. package/src/tool/forensic-image-authenticity-analyzer/i18n/sv.ts +76 -0
  166. package/src/tool/forensic-image-authenticity-analyzer/i18n/tr.ts +76 -0
  167. package/src/tool/forensic-image-authenticity-analyzer/i18n/zh.ts +71 -0
  168. package/src/tool/forensic-image-authenticity-analyzer/index.ts +11 -0
  169. package/src/tool/forensic-image-authenticity-analyzer/logic.ts +283 -0
  170. package/src/tool/forensic-image-authenticity-analyzer/seo.astro +10 -0
  171. package/src/tool/forensic-microcrystal-drug-simulator/bibliography.astro +6 -0
  172. package/src/tool/forensic-microcrystal-drug-simulator/bibliography.ts +12 -0
  173. package/src/tool/forensic-microcrystal-drug-simulator/component.astro +240 -0
  174. package/src/tool/forensic-microcrystal-drug-simulator/entry.ts +32 -0
  175. package/src/tool/forensic-microcrystal-drug-simulator/forensic-microcrystal-drug-simulator.css +430 -0
  176. package/src/tool/forensic-microcrystal-drug-simulator/i18n/de.ts +244 -0
  177. package/src/tool/forensic-microcrystal-drug-simulator/i18n/en.ts +244 -0
  178. package/src/tool/forensic-microcrystal-drug-simulator/i18n/es.ts +244 -0
  179. package/src/tool/forensic-microcrystal-drug-simulator/i18n/fr.ts +244 -0
  180. package/src/tool/forensic-microcrystal-drug-simulator/i18n/id.ts +244 -0
  181. package/src/tool/forensic-microcrystal-drug-simulator/i18n/it.ts +244 -0
  182. package/src/tool/forensic-microcrystal-drug-simulator/i18n/ja.ts +244 -0
  183. package/src/tool/forensic-microcrystal-drug-simulator/i18n/ko.ts +244 -0
  184. package/src/tool/forensic-microcrystal-drug-simulator/i18n/nl.ts +244 -0
  185. package/src/tool/forensic-microcrystal-drug-simulator/i18n/pl.ts +244 -0
  186. package/src/tool/forensic-microcrystal-drug-simulator/i18n/pt.ts +244 -0
  187. package/src/tool/forensic-microcrystal-drug-simulator/i18n/ru.ts +244 -0
  188. package/src/tool/forensic-microcrystal-drug-simulator/i18n/sv.ts +244 -0
  189. package/src/tool/forensic-microcrystal-drug-simulator/i18n/tr.ts +244 -0
  190. package/src/tool/forensic-microcrystal-drug-simulator/i18n/zh.ts +244 -0
  191. package/src/tool/forensic-microcrystal-drug-simulator/index.ts +11 -0
  192. package/src/tool/forensic-microcrystal-drug-simulator/logic.ts +189 -0
  193. package/src/tool/forensic-microcrystal-drug-simulator/seo.astro +15 -0
  194. package/src/tool/forensic-sex-determinator/bibliography.astro +14 -0
  195. package/src/tool/forensic-sex-determinator/bibliography.ts +12 -0
  196. package/src/tool/forensic-sex-determinator/component.astro +463 -0
  197. package/src/tool/forensic-sex-determinator/entry.ts +32 -0
  198. package/src/tool/forensic-sex-determinator/forensic-sex-determinator.css +413 -0
  199. package/src/tool/forensic-sex-determinator/i18n/de.ts +211 -0
  200. package/src/tool/forensic-sex-determinator/i18n/en.ts +211 -0
  201. package/src/tool/forensic-sex-determinator/i18n/es.ts +211 -0
  202. package/src/tool/forensic-sex-determinator/i18n/fr.ts +211 -0
  203. package/src/tool/forensic-sex-determinator/i18n/id.ts +211 -0
  204. package/src/tool/forensic-sex-determinator/i18n/it.ts +211 -0
  205. package/src/tool/forensic-sex-determinator/i18n/ja.ts +211 -0
  206. package/src/tool/forensic-sex-determinator/i18n/ko.ts +211 -0
  207. package/src/tool/forensic-sex-determinator/i18n/nl.ts +211 -0
  208. package/src/tool/forensic-sex-determinator/i18n/pl.ts +211 -0
  209. package/src/tool/forensic-sex-determinator/i18n/pt.ts +211 -0
  210. package/src/tool/forensic-sex-determinator/i18n/ru.ts +211 -0
  211. package/src/tool/forensic-sex-determinator/i18n/sv.ts +211 -0
  212. package/src/tool/forensic-sex-determinator/i18n/tr.ts +211 -0
  213. package/src/tool/forensic-sex-determinator/i18n/zh.ts +211 -0
  214. package/src/tool/forensic-sex-determinator/index.ts +11 -0
  215. package/src/tool/forensic-sex-determinator/logic.ts +89 -0
  216. package/src/tool/forensic-sex-determinator/seo.astro +15 -0
  217. package/src/tool/forensic-stature-estimator/bibliography.astro +14 -0
  218. package/src/tool/forensic-stature-estimator/bibliography.ts +12 -0
  219. package/src/tool/forensic-stature-estimator/component.astro +49 -0
  220. package/src/tool/forensic-stature-estimator/components/EstimationPanel.astro +65 -0
  221. package/src/tool/forensic-stature-estimator/components/OsteometricBoard.astro +39 -0
  222. package/src/tool/forensic-stature-estimator/components/OsteometricSelector.astro +109 -0
  223. package/src/tool/forensic-stature-estimator/dom-utils.ts +71 -0
  224. package/src/tool/forensic-stature-estimator/entry.ts +32 -0
  225. package/src/tool/forensic-stature-estimator/forensic-stature-estimator.css +689 -0
  226. package/src/tool/forensic-stature-estimator/helpers.ts +51 -0
  227. package/src/tool/forensic-stature-estimator/i18n/de.ts +196 -0
  228. package/src/tool/forensic-stature-estimator/i18n/en.ts +196 -0
  229. package/src/tool/forensic-stature-estimator/i18n/es.ts +196 -0
  230. package/src/tool/forensic-stature-estimator/i18n/fr.ts +196 -0
  231. package/src/tool/forensic-stature-estimator/i18n/id.ts +196 -0
  232. package/src/tool/forensic-stature-estimator/i18n/it.ts +196 -0
  233. package/src/tool/forensic-stature-estimator/i18n/ja.ts +196 -0
  234. package/src/tool/forensic-stature-estimator/i18n/ko.ts +196 -0
  235. package/src/tool/forensic-stature-estimator/i18n/nl.ts +196 -0
  236. package/src/tool/forensic-stature-estimator/i18n/pl.ts +196 -0
  237. package/src/tool/forensic-stature-estimator/i18n/pt.ts +196 -0
  238. package/src/tool/forensic-stature-estimator/i18n/ru.ts +196 -0
  239. package/src/tool/forensic-stature-estimator/i18n/sv.ts +196 -0
  240. package/src/tool/forensic-stature-estimator/i18n/tr.ts +196 -0
  241. package/src/tool/forensic-stature-estimator/i18n/zh.ts +196 -0
  242. package/src/tool/forensic-stature-estimator/index.ts +11 -0
  243. package/src/tool/forensic-stature-estimator/logic.ts +119 -0
  244. package/src/tool/forensic-stature-estimator/script.ts +288 -0
  245. package/src/tool/forensic-stature-estimator/seo.astro +15 -0
  246. package/src/tool/forensic-tlc-ink-simulator/bibliography.astro +6 -0
  247. package/src/tool/forensic-tlc-ink-simulator/bibliography.ts +16 -0
  248. package/src/tool/forensic-tlc-ink-simulator/component.astro +245 -0
  249. package/src/tool/forensic-tlc-ink-simulator/entry.ts +32 -0
  250. package/src/tool/forensic-tlc-ink-simulator/forensic-tlc-ink-simulator.css +462 -0
  251. package/src/tool/forensic-tlc-ink-simulator/i18n/de.ts +243 -0
  252. package/src/tool/forensic-tlc-ink-simulator/i18n/en.ts +243 -0
  253. package/src/tool/forensic-tlc-ink-simulator/i18n/es.ts +243 -0
  254. package/src/tool/forensic-tlc-ink-simulator/i18n/fr.ts +243 -0
  255. package/src/tool/forensic-tlc-ink-simulator/i18n/id.ts +243 -0
  256. package/src/tool/forensic-tlc-ink-simulator/i18n/it.ts +243 -0
  257. package/src/tool/forensic-tlc-ink-simulator/i18n/ja.ts +235 -0
  258. package/src/tool/forensic-tlc-ink-simulator/i18n/ko.ts +235 -0
  259. package/src/tool/forensic-tlc-ink-simulator/i18n/nl.ts +243 -0
  260. package/src/tool/forensic-tlc-ink-simulator/i18n/pl.ts +243 -0
  261. package/src/tool/forensic-tlc-ink-simulator/i18n/pt.ts +243 -0
  262. package/src/tool/forensic-tlc-ink-simulator/i18n/ru.ts +243 -0
  263. package/src/tool/forensic-tlc-ink-simulator/i18n/sv.ts +243 -0
  264. package/src/tool/forensic-tlc-ink-simulator/i18n/tr.ts +243 -0
  265. package/src/tool/forensic-tlc-ink-simulator/i18n/zh.ts +235 -0
  266. package/src/tool/forensic-tlc-ink-simulator/index.ts +11 -0
  267. package/src/tool/forensic-tlc-ink-simulator/logic.ts +152 -0
  268. package/src/tool/forensic-tlc-ink-simulator/seo.astro +15 -0
  269. package/src/tool/gsr-dispersion-calculator/bibliography.astro +6 -0
  270. package/src/tool/gsr-dispersion-calculator/bibliography.ts +16 -0
  271. package/src/tool/gsr-dispersion-calculator/component.astro +294 -0
  272. package/src/tool/gsr-dispersion-calculator/entry.ts +32 -0
  273. package/src/tool/gsr-dispersion-calculator/gsr-dispersion-calculator.css +305 -0
  274. package/src/tool/gsr-dispersion-calculator/i18n/de.ts +272 -0
  275. package/src/tool/gsr-dispersion-calculator/i18n/en.ts +272 -0
  276. package/src/tool/gsr-dispersion-calculator/i18n/es.ts +272 -0
  277. package/src/tool/gsr-dispersion-calculator/i18n/fr.ts +272 -0
  278. package/src/tool/gsr-dispersion-calculator/i18n/id.ts +272 -0
  279. package/src/tool/gsr-dispersion-calculator/i18n/it.ts +272 -0
  280. package/src/tool/gsr-dispersion-calculator/i18n/ja.ts +272 -0
  281. package/src/tool/gsr-dispersion-calculator/i18n/ko.ts +272 -0
  282. package/src/tool/gsr-dispersion-calculator/i18n/nl.ts +272 -0
  283. package/src/tool/gsr-dispersion-calculator/i18n/pl.ts +272 -0
  284. package/src/tool/gsr-dispersion-calculator/i18n/pt.ts +272 -0
  285. package/src/tool/gsr-dispersion-calculator/i18n/ru.ts +272 -0
  286. package/src/tool/gsr-dispersion-calculator/i18n/sv.ts +272 -0
  287. package/src/tool/gsr-dispersion-calculator/i18n/tr.ts +272 -0
  288. package/src/tool/gsr-dispersion-calculator/i18n/zh.ts +272 -0
  289. package/src/tool/gsr-dispersion-calculator/index.ts +11 -0
  290. package/src/tool/gsr-dispersion-calculator/logic.ts +148 -0
  291. package/src/tool/gsr-dispersion-calculator/seo.astro +15 -0
  292. package/src/tool/widmark-alcohol-simulator/bibliography.astro +14 -0
  293. package/src/tool/widmark-alcohol-simulator/bibliography.ts +12 -0
  294. package/src/tool/widmark-alcohol-simulator/component.astro +453 -0
  295. package/src/tool/widmark-alcohol-simulator/entry.ts +32 -0
  296. package/src/tool/widmark-alcohol-simulator/i18n/de.ts +193 -0
  297. package/src/tool/widmark-alcohol-simulator/i18n/en.ts +206 -0
  298. package/src/tool/widmark-alcohol-simulator/i18n/es.ts +193 -0
  299. package/src/tool/widmark-alcohol-simulator/i18n/fr.ts +193 -0
  300. package/src/tool/widmark-alcohol-simulator/i18n/id.ts +193 -0
  301. package/src/tool/widmark-alcohol-simulator/i18n/it.ts +193 -0
  302. package/src/tool/widmark-alcohol-simulator/i18n/ja.ts +193 -0
  303. package/src/tool/widmark-alcohol-simulator/i18n/ko.ts +193 -0
  304. package/src/tool/widmark-alcohol-simulator/i18n/nl.ts +193 -0
  305. package/src/tool/widmark-alcohol-simulator/i18n/pl.ts +193 -0
  306. package/src/tool/widmark-alcohol-simulator/i18n/pt.ts +193 -0
  307. package/src/tool/widmark-alcohol-simulator/i18n/ru.ts +193 -0
  308. package/src/tool/widmark-alcohol-simulator/i18n/sv.ts +193 -0
  309. package/src/tool/widmark-alcohol-simulator/i18n/tr.ts +193 -0
  310. package/src/tool/widmark-alcohol-simulator/i18n/zh.ts +193 -0
  311. package/src/tool/widmark-alcohol-simulator/index.ts +11 -0
  312. package/src/tool/widmark-alcohol-simulator/logic.ts +97 -0
  313. package/src/tool/widmark-alcohol-simulator/seo.astro +15 -0
  314. package/src/tool/widmark-alcohol-simulator/widmark-alcohol-simulator.css +386 -0
  315. package/src/tools.ts +27 -0
  316. package/src/types.ts +70 -0
@@ -0,0 +1,117 @@
1
+ ---
2
+ import "@jjlmoya/utils-shared/theme.css";
3
+ import PreviewToolbar from "../components/PreviewToolbar.astro";
4
+ import type { KnownLocale } from "../types";
5
+
6
+ interface Props {
7
+ title: string;
8
+ currentLocale?: KnownLocale;
9
+ localeUrls?: Partial<Record<KnownLocale, string>>;
10
+ hasSidebar?: boolean;
11
+ }
12
+
13
+ const { title, currentLocale = "es", localeUrls = {}, hasSidebar = false } = Astro.props;
14
+ ---
15
+
16
+ <!doctype html>
17
+ <html lang={currentLocale}>
18
+ <head>
19
+ <meta charset="UTF-8" />
20
+ <meta name="viewport" content="width=device-width" />
21
+ <title>{title} · preview</title>
22
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
23
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
24
+ <link
25
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap"
26
+ rel="stylesheet"
27
+ />
28
+
29
+ <script is:inline>
30
+ (function () {
31
+ const saved = localStorage.getItem("theme") || "theme-dark";
32
+ document.documentElement.classList.add(saved);
33
+ })();
34
+ </script>
35
+ <slot name="head" />
36
+ </head>
37
+ <body>
38
+ <PreviewToolbar currentLocale={currentLocale} localeUrls={localeUrls} />
39
+ <div class:list={["page-wrapper", { "with-sidebar": hasSidebar }]}>
40
+ {
41
+ hasSidebar && (
42
+ <aside class="sidebar-area">
43
+ <slot name="sidebar" />
44
+ </aside>
45
+ )
46
+ }
47
+ <main>
48
+ <slot />
49
+ </main>
50
+ </div>
51
+ </body>
52
+ </html>
53
+
54
+ <style is:global>
55
+ :root {
56
+ --accent: #f43f5e;
57
+ --primary-base: #9f1239;
58
+ --cyan: #06b6d4;
59
+ }
60
+
61
+ .theme-dark,
62
+ .theme-light {
63
+ --text-main: var(--text-base);
64
+ --border-color: var(--border-base);
65
+ }
66
+
67
+ *,
68
+ *::before,
69
+ *::after {
70
+ box-sizing: border-box;
71
+ }
72
+
73
+ body {
74
+ background-color: var(--bg-page);
75
+ color: var(--text-base);
76
+ margin: 0;
77
+ min-height: 100vh;
78
+ transition:
79
+ background-color 0.3s ease,
80
+ color 0.3s ease;
81
+ }
82
+
83
+ main {
84
+ padding: 0 2rem;
85
+ }
86
+
87
+ .page-wrapper {
88
+ display: flex;
89
+ flex-direction: column;
90
+ }
91
+
92
+ .page-wrapper.with-sidebar {
93
+ display: grid;
94
+ grid-template-columns: 240px 1fr;
95
+ min-height: 100vh;
96
+ }
97
+
98
+ .sidebar-area {
99
+ position: sticky;
100
+ top: 0;
101
+ height: 100vh;
102
+ overflow-y: auto;
103
+ border-right: 1px solid var(--border-color);
104
+ background: var(--bg-page);
105
+ }
106
+
107
+ @media (max-width: 768px) {
108
+ .page-wrapper.with-sidebar {
109
+ grid-template-columns: 1fr;
110
+ }
111
+
112
+ .sidebar-area {
113
+ display: none;
114
+ }
115
+ }
116
+ </style>
117
+
@@ -0,0 +1,162 @@
1
+ ---
2
+ import PreviewLayout from "../../layouts/PreviewLayout.astro";
3
+ import PreviewNavSidebar from "../../components/PreviewNavSidebar.astro";
4
+ import { ALL_TOOLS } from "../../index";
5
+ import {
6
+ UtilityHeader,
7
+ FAQSection,
8
+ Bibliography,
9
+ SEORenderer,
10
+ } from "@jjlmoya/utils-shared";
11
+ import type { KnownLocale, ToolLocaleContent } from "../../types";
12
+ import type { UtilitySEOContent } from "@jjlmoya/utils-shared";
13
+
14
+ export async function getStaticPaths() {
15
+ const paths = [];
16
+
17
+ for (const { entry, Component: lazyComp } of ALL_TOOLS) {
18
+ const { default: Component } = await (lazyComp as () => Promise<{ default: unknown }>)();
19
+ const localeEntries = Object.entries(entry.i18n) as [
20
+ KnownLocale,
21
+ () => Promise<ToolLocaleContent>,
22
+ ][];
23
+ const localeContents = await Promise.all(
24
+ localeEntries.map(async ([locale, loader]) => ({
25
+ locale,
26
+ content: await loader(),
27
+ })),
28
+ );
29
+
30
+ const localeUrls = Object.fromEntries(
31
+ localeContents.map(({ locale, content }) => [
32
+ locale,
33
+ `/${locale}/${content.slug}`,
34
+ ]),
35
+ ) as Partial<Record<KnownLocale, string>>;
36
+
37
+ const firstLoader = entry.i18n.en ?? Object.values(entry.i18n)[0];
38
+ const englishSlug = firstLoader ? (await firstLoader()).slug : entry.id;
39
+
40
+ for (const { locale, content } of localeContents) {
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[];
56
+ paths.push({
57
+ params: { locale, slug: content.slug },
58
+ props: { Component, locale, content, localeUrls, allToolsNav, englishSlug },
59
+ });
60
+ }
61
+ }
62
+
63
+ return paths;
64
+ }
65
+
66
+ interface NavItem {
67
+ id: string;
68
+ title: string;
69
+ href: string;
70
+ isActive?: boolean;
71
+ }
72
+
73
+ interface Props {
74
+ Component: unknown;
75
+ locale: KnownLocale;
76
+ content: ToolLocaleContent;
77
+ localeUrls: Partial<Record<KnownLocale, string>>;
78
+ allToolsNav: NavItem[];
79
+ englishSlug: string;
80
+ }
81
+
82
+ const { Component, locale, content, localeUrls, allToolsNav, englishSlug } = Astro.props as 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 cssLoader = cssKey ? cssFiles[cssKey] : null;
87
+ const toolCss = cssLoader ? await cssLoader() as string : "";
88
+
89
+ const seoContent: UtilitySEOContent = { locale, sections: content.seo ?? [] };
90
+
91
+ const words = content.title.split(" ");
92
+ const titleHighlight = words[0] || "";
93
+ const titleBase = words.slice(1).join(" ") || "";
94
+ ---
95
+
96
+ <PreviewLayout
97
+ title={content.title}
98
+ currentLocale={locale}
99
+ localeUrls={localeUrls}
100
+ hasSidebar={true}
101
+ >
102
+ <PreviewNavSidebar
103
+ slot="sidebar"
104
+ categoryTitle="Tools"
105
+ tools={allToolsNav}
106
+ />
107
+ <Fragment slot="head">
108
+ {toolCss ? <Fragment set:html={`<style is:inline>${toolCss}</style>`} /> : null}
109
+ {
110
+ ( content.schemas ?? []).map((schema: unknown) => (
111
+ <script
112
+ is:inline
113
+ type="application/ld+json"
114
+ set:html={JSON.stringify(schema)}
115
+ />
116
+ ))
117
+ }
118
+ </Fragment>
119
+
120
+ <div class="tool-page">
121
+ <UtilityHeader
122
+ titleHighlight={titleHighlight}
123
+ titleBase={titleBase}
124
+ description={content.description}
125
+ />
126
+
127
+ <section class="section-tool">
128
+ <Component ui={content.ui} />
129
+ </section>
130
+
131
+ <section class="section-seo">
132
+ <SEORenderer content={seoContent} />
133
+ </section>
134
+
135
+ <section class="section-faq">
136
+ <FAQSection items={content.faq} />
137
+ </section>
138
+
139
+ <section class="section-bibliography">
140
+ <Bibliography links={content.bibliography} />
141
+ </section>
142
+ </div>
143
+ </PreviewLayout>
144
+
145
+ <style>
146
+ .tool-page {
147
+ display: flex;
148
+ flex-direction: column;
149
+ gap: 2rem;
150
+ }
151
+ .section-tool {
152
+ max-width: 1200px;
153
+ margin: 0 auto;
154
+ width: 100%;
155
+ }
156
+ .section-seo,
157
+ .section-faq,
158
+ .section-bibliography {
159
+ padding-top: 2rem;
160
+ border-top: 1px solid var(--border-color);
161
+ }
162
+ </style>
@@ -0,0 +1,261 @@
1
+ ---
2
+ import PreviewLayout from '../layouts/PreviewLayout.astro';
3
+ import PreviewNavSidebar from '../components/PreviewNavSidebar.astro';
4
+ import { templateCategory, ALL_TOOLS } from '../index';
5
+ import { Icon } from 'astro-icon/components';
6
+ import type { KnownLocale, ToolLocaleContent } from '../types';
7
+
8
+ export async function getStaticPaths() {
9
+ const locales = Object.keys(templateCategory.i18n) as KnownLocale[];
10
+ return locales.map((locale) => ({ params: { locale } }));
11
+ }
12
+
13
+ const categoryLocales = Object.keys(templateCategory.i18n) as KnownLocale[];
14
+
15
+ const { locale: requestedLocale } = Astro.params as { locale: KnownLocale };
16
+ const currentLocale = categoryLocales.includes(requestedLocale) ? requestedLocale : 'en';
17
+ const categoryLoader = templateCategory.i18n[currentLocale] ?? templateCategory.i18n.en;
18
+
19
+ if (!categoryLoader) {
20
+ throw new Error('No category locale loader is registered.');
21
+ }
22
+
23
+ const categoryContent = await categoryLoader();
24
+ const tools = ALL_TOOLS || [];
25
+
26
+ const toolsWithContent = tools.length > 0
27
+ ? await Promise.all(
28
+ tools.map(async ({ entry, Component }) => {
29
+ const languages = Object.keys(entry.i18n) as KnownLocale[];
30
+ const localeEntries = await Promise.all(
31
+ languages.map(async (l) => {
32
+ const loader = entry.i18n[l];
33
+ if (!loader) return null;
34
+ const content = await loader();
35
+ return [l, content] as const;
36
+ })
37
+ );
38
+
39
+ const localeContents = Object.fromEntries(localeEntries.filter(Boolean)) as Record<string, ToolLocaleContent<Record<string, string>>>;
40
+ const currentLocaleContent = localeContents[currentLocale] || localeContents.en;
41
+ const availableLocales: Record<string, string> = {};
42
+
43
+ for (const l of languages) {
44
+ const lCont = localeContents[l];
45
+ if (lCont) {
46
+ availableLocales[l] = `/${l}/${lCont.slug}`;
47
+ }
48
+ }
49
+
50
+ return { entry, Component, locale: currentLocaleContent, availableLocales };
51
+ })
52
+ )
53
+ : [];
54
+ ---
55
+
56
+ <PreviewLayout
57
+ title={categoryContent.title}
58
+ currentLocale={currentLocale}
59
+ hasSidebar={true}
60
+ >
61
+ <PreviewNavSidebar
62
+ slot="sidebar"
63
+ categoryTitle={categoryContent.title}
64
+ tools={toolsWithContent.map(({ entry, locale, availableLocales }) => {
65
+ const href = availableLocales[currentLocale] || (locale ? `/${currentLocale}/${locale.slug}` : '#');
66
+ return {
67
+ id: entry.id,
68
+ title: locale?.title || entry.id,
69
+ href: href,
70
+ };
71
+ })}
72
+ />
73
+ <div class="dashboard">
74
+ <header class="preview-header">
75
+ <span class="badge">preview - @jjlmoya/utils-forensic-science</span>
76
+ <h1>{categoryContent.title}</h1>
77
+ <p>{categoryContent.description}</p>
78
+ </header>
79
+
80
+ <div class="tool-list">
81
+ {toolsWithContent?.map(({ entry, locale, availableLocales }) => (
82
+ <article class="tool-card">
83
+ <a href={availableLocales?.[currentLocale] || (locale ? `/${currentLocale}/${locale.slug}` : '#')} class="tool-card-link">
84
+ <div class="tool-icons">
85
+ <div class="icon-wrapper bg">
86
+ <Icon name={entry.icons.bg} />
87
+ </div>
88
+ <div class="icon-wrapper fg">
89
+ <Icon name={entry.icons.fg} />
90
+ </div>
91
+ </div>
92
+ <div class="tool-card-content">
93
+ <h2 class="tool-title">{locale?.title}</h2>
94
+ <p class="tool-description">{locale?.description}</p>
95
+ </div>
96
+ <div class="tool-card-meta">
97
+ <span class="tool-id">{entry.id}</span>
98
+ </div>
99
+ </a>
100
+
101
+ {availableLocales && Object.keys(availableLocales).length > 1 && (
102
+ <div class="tool-locales">
103
+ {Object.entries(availableLocales).map(([l, url]) => (
104
+ <a href={url} class="locale-badge" title={`View in ${l.toUpperCase()}`} class:list={{ active: l === currentLocale }}>
105
+ {l.toUpperCase()}
106
+ </a>
107
+ ))}
108
+ </div>
109
+ )}
110
+ </article>
111
+ ))}
112
+ </div>
113
+ </div>
114
+ </PreviewLayout>
115
+
116
+ <style>
117
+ .dashboard {
118
+ display: flex;
119
+ flex-direction: column;
120
+ gap: 5rem;
121
+ }
122
+ .preview-header {
123
+ text-align: center;
124
+ padding-bottom: 3rem;
125
+ border-bottom: 1px solid var(--border-color);
126
+ }
127
+ .badge {
128
+ display: inline-block;
129
+ padding: 0.25rem 0.75rem;
130
+ background: var(--accent);
131
+ border-radius: 99px;
132
+ font-size: 0.7rem;
133
+ font-weight: 800;
134
+ margin-bottom: 1.5rem;
135
+ color: var(--text-base);
136
+ letter-spacing: 0.05em;
137
+ }
138
+ h1 {
139
+ font-size: clamp(2rem, 6vw, 3.5rem);
140
+ font-weight: 900;
141
+ margin: 0 0 1rem;
142
+ background: linear-gradient(to bottom, var(--text-base), var(--text-muted));
143
+ -webkit-background-clip: text;
144
+ -webkit-text-fill-color: transparent;
145
+ background-clip: text;
146
+ }
147
+ .preview-header p {
148
+ color: var(--text-muted);
149
+ font-size: 1.1rem;
150
+ margin: 0;
151
+ }
152
+ .tool-list {
153
+ display: grid;
154
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
155
+ gap: 2rem;
156
+ }
157
+ .tool-card {
158
+ display: flex;
159
+ flex-direction: column;
160
+ gap: 1rem;
161
+ }
162
+ .tool-card-link {
163
+ flex: 1;
164
+ display: flex;
165
+ flex-direction: column;
166
+ padding: 1.5rem;
167
+ background: var(--bg-surface);
168
+ border: 1px solid var(--border-color);
169
+ border-radius: 0.75rem;
170
+ text-decoration: none;
171
+ transition: all 0.2s ease;
172
+ }
173
+ .tool-card-link:hover {
174
+ border-color: var(--accent);
175
+ background: rgba(244, 63, 94, 0.05);
176
+ transform: translateY(-2px);
177
+ }
178
+ .tool-icons {
179
+ display: flex;
180
+ align-items: center;
181
+ gap: 1rem;
182
+ margin-bottom: 1.25rem;
183
+ }
184
+ .icon-wrapper {
185
+ display: flex;
186
+ align-items: center;
187
+ justify-content: center;
188
+ width: 3rem;
189
+ height: 3rem;
190
+ border-radius: 0.5rem;
191
+ font-size: 1.5rem;
192
+ }
193
+ .icon-wrapper.bg {
194
+ background: var(--accent);
195
+ color: var(--text-base);
196
+ }
197
+ .icon-wrapper.fg {
198
+ background: var(--bg-page);
199
+ border: 1px solid var(--border-color);
200
+ color: var(--accent);
201
+ }
202
+ .tool-card-content {
203
+ flex: 1;
204
+ display: flex;
205
+ flex-direction: column;
206
+ }
207
+ .tool-title {
208
+ font-size: 1.25rem;
209
+ font-weight: 700;
210
+ margin: 0 0 0.5rem;
211
+ color: var(--text-base);
212
+ }
213
+ .tool-description {
214
+ font-size: 0.9375rem;
215
+ color: var(--text-muted);
216
+ line-height: 1.5;
217
+ margin: 0;
218
+ }
219
+ .tool-card-meta {
220
+ padding-top: 1rem;
221
+ border-top: 1px solid var(--border-color);
222
+ margin-top: 1.5rem;
223
+ }
224
+ .tool-id {
225
+ display: inline-block;
226
+ font-size: 0.7rem;
227
+ background: var(--bg-page);
228
+ border: 1px solid var(--border-color);
229
+ padding: 0.35rem 0.75rem;
230
+ border-radius: 0.4rem;
231
+ color: var(--accent);
232
+ font-weight: 600;
233
+ }
234
+ .tool-locales {
235
+ display: flex;
236
+ gap: 0.5rem;
237
+ flex-wrap: wrap;
238
+ }
239
+ .locale-badge {
240
+ display: inline-block;
241
+ padding: 0.4rem 0.85rem;
242
+ background: var(--bg-page);
243
+ border: 1px solid var(--border-color);
244
+ border-radius: 0.4rem;
245
+ color: var(--text-muted);
246
+ text-decoration: none;
247
+ font-size: 0.75rem;
248
+ font-weight: 600;
249
+ text-transform: uppercase;
250
+ letter-spacing: 0.05em;
251
+ transition: all 0.15s ease;
252
+ }
253
+ .locale-badge:hover,
254
+ .locale-badge.active {
255
+ color: var(--accent);
256
+ border-color: var(--accent);
257
+ background: rgba(244, 63, 94, 0.1);
258
+ }
259
+ </style>
260
+
261
+
@@ -0,0 +1,3 @@
1
+ ---
2
+ ---
3
+ <meta http-equiv="refresh" content="0;url=/en" />
@@ -0,0 +1,140 @@
1
+ import { describe, expect, it, afterAll } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+
4
+ const failures: Map<string, string[]> = new Map();
5
+
6
+ type LocaleWithDiacritics = keyof typeof DIACRITIC_RULES;
7
+
8
+ const DIACRITIC_RULES = {
9
+ de: {
10
+ language: 'German',
11
+ expectedCharacters: 'ä ö ü ß',
12
+ characters: /[äöüÄÖÜß]/g,
13
+ minPerThousandLetters: 0.1,
14
+ },
15
+ es: {
16
+ language: 'Spanish',
17
+ expectedCharacters: 'á é í ó ú ü ñ',
18
+ characters: /[áéíóúüñÁÉÍÓÚÜÑ]/g,
19
+ minPerThousandLetters: 0.1,
20
+ },
21
+ fr: {
22
+ language: 'French',
23
+ expectedCharacters: 'à â æ ç é è ê ë î ï ô œ ù û ü ÿ',
24
+ characters: /[àâæçéèêëîïôœùûüÿÀÂÆÇÉÈÊËÎÏÔŒÙÛÜŸ]/g,
25
+ minPerThousandLetters: 0.1,
26
+ },
27
+ it: {
28
+ language: 'Italian',
29
+ expectedCharacters: 'à è é ì í î ò ó ù ú',
30
+ characters: /[àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ]/g,
31
+ minPerThousandLetters: 0.1,
32
+ },
33
+ pl: {
34
+ language: 'Polish',
35
+ expectedCharacters: 'ą ć ę ł ń ó ś ź ż',
36
+ characters: /[ąćęłńóśźżĄĆĘŁŃÓŚŹŻ]/g,
37
+ minPerThousandLetters: 0.1,
38
+ },
39
+ pt: {
40
+ language: 'Portuguese',
41
+ expectedCharacters: 'á â ã à ç é ê í ó ô õ ú ü',
42
+ characters: /[áâãàçéêíóôõúüÁÂÃÀÇÉÊÍÓÔÕÚÜ]/g,
43
+ minPerThousandLetters: 0.1,
44
+ },
45
+ sv: {
46
+ language: 'Swedish',
47
+ expectedCharacters: 'å ä ö',
48
+ characters: /[åäöÅÄÖ]/g,
49
+ minPerThousandLetters: 0.1,
50
+ },
51
+ tr: {
52
+ language: 'Turkish',
53
+ expectedCharacters: 'ç ğ ı İ ö ş ü',
54
+ characters: /[çğıöşüÇĞİÖŞÜ]/g,
55
+ minPerThousandLetters: 0.1,
56
+ },
57
+ } as const;
58
+
59
+ const LETTERS = /\p{L}/gu;
60
+ const TRANSLATABLE_KEYS = ['title', 'description', 'ui', 'seo', 'faq', 'howTo'] as const;
61
+
62
+ function collectStrings(value: unknown): string[] {
63
+ if (typeof value === 'string') return [value];
64
+ if (!value || typeof value !== 'object') return [];
65
+ if (Array.isArray(value)) return value.flatMap(collectStrings);
66
+ return Object.values(value).flatMap(collectStrings);
67
+ }
68
+
69
+ function normalizeText(value: unknown): string {
70
+ return collectStrings(value).join(' ').normalize('NFC');
71
+ }
72
+
73
+ function translatableContent(content: Record<string, unknown>) {
74
+ return TRANSLATABLE_KEYS.map((key) => content[key]);
75
+ }
76
+
77
+ function letterCount(text: string): number {
78
+ return text.match(LETTERS)?.length ?? 0;
79
+ }
80
+
81
+ function diacriticCount(text: string, locale: LocaleWithDiacritics): number {
82
+ return text.match(DIACRITIC_RULES[locale].characters)?.length ?? 0;
83
+ }
84
+
85
+ function diacriticsPerThousandLetters(text: string, locale: LocaleWithDiacritics): number {
86
+ const letters = letterCount(text);
87
+ if (letters === 0) return 0;
88
+ return diacriticCount(text, locale) / letters * 1000;
89
+ }
90
+
91
+ describe('Diacritics density validation', () => {
92
+ ALL_TOOLS.forEach((tool) => {
93
+ describe(`Tool: ${tool.entry.id}`, () => {
94
+ Object.keys(DIACRITIC_RULES).forEach((locale) => {
95
+ it(`${locale} keeps the expected accent and special-letter set`, async () => {
96
+ const typedLocale = locale as LocaleWithDiacritics;
97
+ const loader = tool.entry.i18n[typedLocale];
98
+ if (!loader) return;
99
+
100
+ const content = await loader();
101
+ const text = normalizeText(translatableContent(content as Record<string, unknown>));
102
+ const rule = DIACRITIC_RULES[typedLocale];
103
+ const letters = letterCount(text);
104
+ const matches = diacriticCount(text, typedLocale);
105
+ const density = diacriticsPerThousandLetters(text, typedLocale);
106
+
107
+ expect(
108
+ density,
109
+ [
110
+ `Possible spelling or encoding issue detected in ${tool.entry.id}/${typedLocale} (${rule.language}).`,
111
+ `The text has ${matches} special characters (${density.toFixed(2)} per 1000 letters, ${letters} letters analyzed).`,
112
+ `This locale should include some of these characters: ${rule.expectedCharacters}.`,
113
+ 'If the count is 0 or near 0, accents, tildes, or special letters were probably stripped by encoding or normalization.',
114
+ ].join(' '),
115
+ ).toBeGreaterThanOrEqual(rule.minPerThousandLetters);
116
+
117
+ if (density < rule.minPerThousandLetters) {
118
+ const existing = failures.get(tool.entry.id) ?? [];
119
+ existing.push(typedLocale);
120
+ failures.set(tool.entry.id, existing);
121
+ }
122
+ });
123
+ });
124
+ });
125
+ });
126
+ });
127
+
128
+ afterAll(() => {
129
+ if (failures.size > 0) {
130
+ const sorted = [...failures.entries()].sort(([a], [b]) => a.localeCompare(b));
131
+ console.log('\n=== DIACRITICS DENSITY FAILURES (grouped by tool) ===');
132
+ let total = 0;
133
+ for (const [tool, locales] of sorted) {
134
+ locales.sort();
135
+ console.log(` ${tool}: ${locales.join(', ')}`);
136
+ total += locales.length;
137
+ }
138
+ console.log(` Total: ${total} failures across ${failures.size} tools\n`);
139
+ }
140
+ });
@@ -0,0 +1,19 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type * as DATA from '../data';
3
+
4
+ const TOOLS: typeof DATA.scienceCategory[] = [];
5
+
6
+ describe('FAQ Content Validation', () => {
7
+ TOOLS.forEach((entry) => {
8
+ describe(`Tool: ${entry.icon}`, () => {
9
+ it('placeholder', () => {
10
+ expect(true).toBe(true);
11
+ });
12
+ });
13
+ });
14
+
15
+ it('no tools registered yet', () => {
16
+ expect(TOOLS.length).toBe(0);
17
+ });
18
+ });
19
+