@jjlmoya/utils-converters 1.15.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 (397) hide show
  1. package/package.json +8 -4
  2. package/scripts/clean_titles.py +95 -0
  3. package/scripts/postinstall.mjs +27 -0
  4. package/src/category/i18n/de.ts +1 -1
  5. package/src/category/i18n/fr.ts +9 -9
  6. package/src/category/i18n/ru.ts +1 -1
  7. package/src/category/i18n/sv.ts +1 -1
  8. package/src/data.ts +0 -21
  9. package/src/layouts/PreviewLayout.astro +2 -1
  10. package/src/pages/[locale]/[slug].astro +28 -12
  11. package/src/tests/diacritics_density.test.ts +118 -0
  12. package/src/tests/inverted_punctuation.test.ts +84 -0
  13. package/src/tests/no_en_dash.test.ts +70 -0
  14. package/src/tests/pagespeed_best_practices.test.ts +198 -0
  15. package/src/tests/script_density.test.ts +94 -0
  16. package/src/tests/shared-test-helpers.ts +56 -0
  17. package/src/tests/tool_exports.test.ts +34 -0
  18. package/src/tool/avifAJpg/bibliography.ts +12 -0
  19. package/src/tool/avifAJpg/entry.ts +2 -0
  20. package/src/tool/avifAJpg/i18n/de.ts +2 -12
  21. package/src/tool/avifAJpg/i18n/en.ts +2 -12
  22. package/src/tool/avifAJpg/i18n/es.ts +2 -12
  23. package/src/tool/avifAJpg/i18n/fr.ts +11 -21
  24. package/src/tool/avifAJpg/i18n/id.ts +2 -12
  25. package/src/tool/avifAJpg/i18n/it.ts +2 -12
  26. package/src/tool/avifAJpg/i18n/ja.ts +1 -11
  27. package/src/tool/avifAJpg/i18n/ko.ts +1 -11
  28. package/src/tool/avifAJpg/i18n/nl.ts +2 -12
  29. package/src/tool/avifAJpg/i18n/pl.ts +2 -12
  30. package/src/tool/avifAJpg/i18n/pt.ts +2 -12
  31. package/src/tool/avifAJpg/i18n/ru.ts +7 -17
  32. package/src/tool/avifAJpg/i18n/sv.ts +2 -12
  33. package/src/tool/avifAJpg/i18n/tr.ts +1 -11
  34. package/src/tool/avifAJpg/i18n/zh.ts +4 -14
  35. package/src/tool/avifAJpg/seo.astro +2 -1
  36. package/src/tool/avifAPng/bibliography.ts +12 -0
  37. package/src/tool/avifAPng/entry.ts +2 -0
  38. package/src/tool/avifAPng/i18n/de.ts +4 -14
  39. package/src/tool/avifAPng/i18n/en.ts +4 -14
  40. package/src/tool/avifAPng/i18n/es.ts +4 -14
  41. package/src/tool/avifAPng/i18n/fr.ts +10 -20
  42. package/src/tool/avifAPng/i18n/id.ts +4 -14
  43. package/src/tool/avifAPng/i18n/it.ts +1 -11
  44. package/src/tool/avifAPng/i18n/ja.ts +1 -11
  45. package/src/tool/avifAPng/i18n/ko.ts +1 -11
  46. package/src/tool/avifAPng/i18n/nl.ts +4 -14
  47. package/src/tool/avifAPng/i18n/pl.ts +4 -14
  48. package/src/tool/avifAPng/i18n/pt.ts +4 -14
  49. package/src/tool/avifAPng/i18n/ru.ts +5 -15
  50. package/src/tool/avifAPng/i18n/sv.ts +4 -14
  51. package/src/tool/avifAPng/i18n/tr.ts +1 -11
  52. package/src/tool/avifAPng/i18n/zh.ts +3 -13
  53. package/src/tool/avifAPng/seo.astro +2 -1
  54. package/src/tool/avifAWebp/bibliography.ts +12 -0
  55. package/src/tool/avifAWebp/entry.ts +2 -0
  56. package/src/tool/avifAWebp/i18n/de.ts +6 -16
  57. package/src/tool/avifAWebp/i18n/en.ts +6 -16
  58. package/src/tool/avifAWebp/i18n/es.ts +6 -16
  59. package/src/tool/avifAWebp/i18n/fr.ts +13 -23
  60. package/src/tool/avifAWebp/i18n/id.ts +6 -16
  61. package/src/tool/avifAWebp/i18n/it.ts +1 -11
  62. package/src/tool/avifAWebp/i18n/ja.ts +1 -11
  63. package/src/tool/avifAWebp/i18n/ko.ts +2 -12
  64. package/src/tool/avifAWebp/i18n/nl.ts +6 -16
  65. package/src/tool/avifAWebp/i18n/pl.ts +6 -16
  66. package/src/tool/avifAWebp/i18n/pt.ts +6 -16
  67. package/src/tool/avifAWebp/i18n/ru.ts +7 -17
  68. package/src/tool/avifAWebp/i18n/sv.ts +6 -16
  69. package/src/tool/avifAWebp/i18n/tr.ts +2 -12
  70. package/src/tool/avifAWebp/i18n/zh.ts +6 -16
  71. package/src/tool/avifAWebp/seo.astro +2 -1
  72. package/src/tool/bmpAJpg/bibliography.ts +12 -0
  73. package/src/tool/bmpAJpg/entry.ts +2 -0
  74. package/src/tool/bmpAJpg/i18n/de.ts +1 -11
  75. package/src/tool/bmpAJpg/i18n/en.ts +1 -11
  76. package/src/tool/bmpAJpg/i18n/es.ts +1 -11
  77. package/src/tool/bmpAJpg/i18n/fr.ts +9 -19
  78. package/src/tool/bmpAJpg/i18n/id.ts +1 -11
  79. package/src/tool/bmpAJpg/i18n/it.ts +1 -11
  80. package/src/tool/bmpAJpg/i18n/ja.ts +1 -11
  81. package/src/tool/bmpAJpg/i18n/ko.ts +1 -11
  82. package/src/tool/bmpAJpg/i18n/nl.ts +1 -11
  83. package/src/tool/bmpAJpg/i18n/pl.ts +1 -11
  84. package/src/tool/bmpAJpg/i18n/pt.ts +1 -11
  85. package/src/tool/bmpAJpg/i18n/ru.ts +5 -15
  86. package/src/tool/bmpAJpg/i18n/sv.ts +1 -11
  87. package/src/tool/bmpAJpg/i18n/tr.ts +1 -11
  88. package/src/tool/bmpAJpg/i18n/zh.ts +1 -11
  89. package/src/tool/bmpAJpg/seo.astro +2 -1
  90. package/src/tool/bmpAPng/bibliography.ts +12 -0
  91. package/src/tool/bmpAPng/entry.ts +2 -0
  92. package/src/tool/bmpAPng/i18n/de.ts +2 -12
  93. package/src/tool/bmpAPng/i18n/en.ts +3 -13
  94. package/src/tool/bmpAPng/i18n/es.ts +2 -12
  95. package/src/tool/bmpAPng/i18n/fr.ts +12 -22
  96. package/src/tool/bmpAPng/i18n/id.ts +2 -12
  97. package/src/tool/bmpAPng/i18n/it.ts +2 -12
  98. package/src/tool/bmpAPng/i18n/ja.ts +1 -11
  99. package/src/tool/bmpAPng/i18n/ko.ts +1 -11
  100. package/src/tool/bmpAPng/i18n/nl.ts +2 -12
  101. package/src/tool/bmpAPng/i18n/pl.ts +2 -12
  102. package/src/tool/bmpAPng/i18n/pt.ts +2 -12
  103. package/src/tool/bmpAPng/i18n/ru.ts +9 -19
  104. package/src/tool/bmpAPng/i18n/sv.ts +2 -12
  105. package/src/tool/bmpAPng/i18n/tr.ts +1 -11
  106. package/src/tool/bmpAPng/i18n/zh.ts +1 -11
  107. package/src/tool/bmpAPng/seo.astro +2 -1
  108. package/src/tool/bmpAWebp/bibliography.ts +12 -0
  109. package/src/tool/bmpAWebp/entry.ts +2 -0
  110. package/src/tool/bmpAWebp/i18n/de.ts +8 -18
  111. package/src/tool/bmpAWebp/i18n/en.ts +8 -18
  112. package/src/tool/bmpAWebp/i18n/es.ts +8 -18
  113. package/src/tool/bmpAWebp/i18n/fr.ts +13 -23
  114. package/src/tool/bmpAWebp/i18n/id.ts +8 -18
  115. package/src/tool/bmpAWebp/i18n/it.ts +8 -18
  116. package/src/tool/bmpAWebp/i18n/ja.ts +7 -17
  117. package/src/tool/bmpAWebp/i18n/ko.ts +7 -17
  118. package/src/tool/bmpAWebp/i18n/nl.ts +8 -18
  119. package/src/tool/bmpAWebp/i18n/pl.ts +8 -18
  120. package/src/tool/bmpAWebp/i18n/pt.ts +8 -18
  121. package/src/tool/bmpAWebp/i18n/ru.ts +10 -20
  122. package/src/tool/bmpAWebp/i18n/sv.ts +8 -18
  123. package/src/tool/bmpAWebp/i18n/tr.ts +8 -18
  124. package/src/tool/bmpAWebp/i18n/zh.ts +8 -18
  125. package/src/tool/bmpAWebp/seo.astro +2 -1
  126. package/src/tool/gifAJpg/bibliography.ts +12 -0
  127. package/src/tool/gifAJpg/entry.ts +2 -0
  128. package/src/tool/gifAJpg/i18n/de.ts +1 -11
  129. package/src/tool/gifAJpg/i18n/en.ts +1 -11
  130. package/src/tool/gifAJpg/i18n/es.ts +1 -11
  131. package/src/tool/gifAJpg/i18n/fr.ts +9 -19
  132. package/src/tool/gifAJpg/i18n/id.ts +1 -11
  133. package/src/tool/gifAJpg/i18n/it.ts +1 -11
  134. package/src/tool/gifAJpg/i18n/ja.ts +1 -11
  135. package/src/tool/gifAJpg/i18n/ko.ts +1 -11
  136. package/src/tool/gifAJpg/i18n/nl.ts +1 -11
  137. package/src/tool/gifAJpg/i18n/pl.ts +1 -11
  138. package/src/tool/gifAJpg/i18n/pt.ts +1 -11
  139. package/src/tool/gifAJpg/i18n/ru.ts +2 -12
  140. package/src/tool/gifAJpg/i18n/sv.ts +1 -11
  141. package/src/tool/gifAJpg/i18n/tr.ts +1 -11
  142. package/src/tool/gifAJpg/i18n/zh.ts +1 -11
  143. package/src/tool/gifAJpg/seo.astro +2 -1
  144. package/src/tool/gifAPng/bibliography.ts +12 -0
  145. package/src/tool/gifAPng/entry.ts +2 -0
  146. package/src/tool/gifAPng/i18n/de.ts +2 -12
  147. package/src/tool/gifAPng/i18n/en.ts +3 -13
  148. package/src/tool/gifAPng/i18n/es.ts +2 -12
  149. package/src/tool/gifAPng/i18n/fr.ts +11 -21
  150. package/src/tool/gifAPng/i18n/id.ts +2 -12
  151. package/src/tool/gifAPng/i18n/it.ts +2 -12
  152. package/src/tool/gifAPng/i18n/ja.ts +1 -11
  153. package/src/tool/gifAPng/i18n/ko.ts +1 -11
  154. package/src/tool/gifAPng/i18n/nl.ts +2 -12
  155. package/src/tool/gifAPng/i18n/pl.ts +3 -13
  156. package/src/tool/gifAPng/i18n/pt.ts +2 -12
  157. package/src/tool/gifAPng/i18n/ru.ts +3 -13
  158. package/src/tool/gifAPng/i18n/sv.ts +3 -13
  159. package/src/tool/gifAPng/i18n/tr.ts +1 -11
  160. package/src/tool/gifAPng/i18n/zh.ts +2 -12
  161. package/src/tool/gifAPng/seo.astro +2 -1
  162. package/src/tool/gifAWebp/bibliography.ts +12 -0
  163. package/src/tool/gifAWebp/entry.ts +2 -0
  164. package/src/tool/gifAWebp/i18n/de.ts +3 -13
  165. package/src/tool/gifAWebp/i18n/en.ts +3 -13
  166. package/src/tool/gifAWebp/i18n/es.ts +1 -11
  167. package/src/tool/gifAWebp/i18n/fr.ts +11 -21
  168. package/src/tool/gifAWebp/i18n/id.ts +3 -13
  169. package/src/tool/gifAWebp/i18n/it.ts +1 -11
  170. package/src/tool/gifAWebp/i18n/ja.ts +1 -11
  171. package/src/tool/gifAWebp/i18n/ko.ts +1 -11
  172. package/src/tool/gifAWebp/i18n/nl.ts +3 -13
  173. package/src/tool/gifAWebp/i18n/pl.ts +3 -13
  174. package/src/tool/gifAWebp/i18n/pt.ts +3 -13
  175. package/src/tool/gifAWebp/i18n/ru.ts +7 -17
  176. package/src/tool/gifAWebp/i18n/sv.ts +3 -13
  177. package/src/tool/gifAWebp/i18n/tr.ts +1 -11
  178. package/src/tool/gifAWebp/i18n/zh.ts +4 -14
  179. package/src/tool/gifAWebp/seo.astro +2 -1
  180. package/src/tool/imagenBase64/bibliography.ts +12 -0
  181. package/src/tool/imagenBase64/component.astro +0 -301
  182. package/src/tool/imagenBase64/entry.ts +2 -0
  183. package/src/tool/imagenBase64/i18n/de.ts +7 -17
  184. package/src/tool/imagenBase64/i18n/en.ts +7 -17
  185. package/src/tool/imagenBase64/i18n/es.ts +7 -17
  186. package/src/tool/imagenBase64/i18n/fr.ts +17 -27
  187. package/src/tool/imagenBase64/i18n/id.ts +7 -17
  188. package/src/tool/imagenBase64/i18n/it.ts +6 -16
  189. package/src/tool/imagenBase64/i18n/ja.ts +1 -11
  190. package/src/tool/imagenBase64/i18n/ko.ts +1 -11
  191. package/src/tool/imagenBase64/i18n/nl.ts +7 -17
  192. package/src/tool/imagenBase64/i18n/pl.ts +7 -17
  193. package/src/tool/imagenBase64/i18n/pt.ts +7 -17
  194. package/src/tool/imagenBase64/i18n/ru.ts +9 -19
  195. package/src/tool/imagenBase64/i18n/sv.ts +7 -17
  196. package/src/tool/imagenBase64/i18n/tr.ts +2 -12
  197. package/src/tool/imagenBase64/i18n/zh.ts +7 -17
  198. package/src/tool/imagenBase64/image-to-base64-converter.css +299 -0
  199. package/src/tool/imagenBase64/seo.astro +2 -1
  200. package/src/tool/jpgAIco/bibliography.ts +12 -0
  201. package/src/tool/jpgAIco/entry.ts +2 -0
  202. package/src/tool/jpgAIco/i18n/de.ts +9 -19
  203. package/src/tool/jpgAIco/i18n/en.ts +9 -19
  204. package/src/tool/jpgAIco/i18n/es.ts +7 -17
  205. package/src/tool/jpgAIco/i18n/fr.ts +13 -23
  206. package/src/tool/jpgAIco/i18n/id.ts +9 -19
  207. package/src/tool/jpgAIco/i18n/it.ts +3 -13
  208. package/src/tool/jpgAIco/i18n/ja.ts +2 -12
  209. package/src/tool/jpgAIco/i18n/ko.ts +2 -12
  210. package/src/tool/jpgAIco/i18n/nl.ts +9 -19
  211. package/src/tool/jpgAIco/i18n/pl.ts +9 -19
  212. package/src/tool/jpgAIco/i18n/pt.ts +9 -19
  213. package/src/tool/jpgAIco/i18n/ru.ts +11 -21
  214. package/src/tool/jpgAIco/i18n/sv.ts +9 -19
  215. package/src/tool/jpgAIco/i18n/tr.ts +3 -13
  216. package/src/tool/jpgAIco/i18n/zh.ts +9 -19
  217. package/src/tool/jpgAIco/seo.astro +2 -1
  218. package/src/tool/jpgAPng/bibliography.ts +12 -0
  219. package/src/tool/jpgAPng/entry.ts +2 -0
  220. package/src/tool/jpgAPng/i18n/de.ts +7 -17
  221. package/src/tool/jpgAPng/i18n/en.ts +9 -19
  222. package/src/tool/jpgAPng/i18n/es.ts +7 -17
  223. package/src/tool/jpgAPng/i18n/fr.ts +13 -23
  224. package/src/tool/jpgAPng/i18n/id.ts +7 -17
  225. package/src/tool/jpgAPng/i18n/it.ts +5 -15
  226. package/src/tool/jpgAPng/i18n/ja.ts +3 -13
  227. package/src/tool/jpgAPng/i18n/ko.ts +3 -13
  228. package/src/tool/jpgAPng/i18n/nl.ts +7 -17
  229. package/src/tool/jpgAPng/i18n/pl.ts +7 -17
  230. package/src/tool/jpgAPng/i18n/pt.ts +7 -17
  231. package/src/tool/jpgAPng/i18n/ru.ts +10 -20
  232. package/src/tool/jpgAPng/i18n/sv.ts +7 -17
  233. package/src/tool/jpgAPng/i18n/tr.ts +6 -16
  234. package/src/tool/jpgAPng/i18n/zh.ts +8 -18
  235. package/src/tool/jpgAPng/seo.astro +2 -1
  236. package/src/tool/jpgAWebp/bibliography.ts +12 -0
  237. package/src/tool/jpgAWebp/entry.ts +2 -0
  238. package/src/tool/jpgAWebp/i18n/de.ts +10 -20
  239. package/src/tool/jpgAWebp/i18n/en.ts +10 -20
  240. package/src/tool/jpgAWebp/i18n/es.ts +4 -14
  241. package/src/tool/jpgAWebp/i18n/fr.ts +16 -26
  242. package/src/tool/jpgAWebp/i18n/id.ts +10 -20
  243. package/src/tool/jpgAWebp/i18n/it.ts +6 -16
  244. package/src/tool/jpgAWebp/i18n/ja.ts +2 -12
  245. package/src/tool/jpgAWebp/i18n/ko.ts +3 -13
  246. package/src/tool/jpgAWebp/i18n/nl.ts +10 -20
  247. package/src/tool/jpgAWebp/i18n/pl.ts +10 -20
  248. package/src/tool/jpgAWebp/i18n/pt.ts +10 -20
  249. package/src/tool/jpgAWebp/i18n/ru.ts +10 -20
  250. package/src/tool/jpgAWebp/i18n/sv.ts +10 -20
  251. package/src/tool/jpgAWebp/i18n/tr.ts +5 -15
  252. package/src/tool/jpgAWebp/i18n/zh.ts +9 -19
  253. package/src/tool/jpgAWebp/seo.astro +2 -1
  254. package/src/tool/pngAIco/bibliography.ts +12 -0
  255. package/src/tool/pngAIco/entry.ts +2 -0
  256. package/src/tool/pngAIco/i18n/de.ts +8 -18
  257. package/src/tool/pngAIco/i18n/en.ts +8 -18
  258. package/src/tool/pngAIco/i18n/es.ts +8 -18
  259. package/src/tool/pngAIco/i18n/fr.ts +14 -24
  260. package/src/tool/pngAIco/i18n/id.ts +8 -18
  261. package/src/tool/pngAIco/i18n/it.ts +4 -14
  262. package/src/tool/pngAIco/i18n/ja.ts +2 -12
  263. package/src/tool/pngAIco/i18n/ko.ts +2 -12
  264. package/src/tool/pngAIco/i18n/nl.ts +8 -18
  265. package/src/tool/pngAIco/i18n/pl.ts +8 -18
  266. package/src/tool/pngAIco/i18n/pt.ts +8 -18
  267. package/src/tool/pngAIco/i18n/ru.ts +11 -21
  268. package/src/tool/pngAIco/i18n/sv.ts +8 -18
  269. package/src/tool/pngAIco/i18n/tr.ts +7 -17
  270. package/src/tool/pngAIco/i18n/zh.ts +8 -18
  271. package/src/tool/pngAIco/seo.astro +2 -1
  272. package/src/tool/pngAJpg/bibliography.ts +12 -0
  273. package/src/tool/pngAJpg/entry.ts +2 -0
  274. package/src/tool/pngAJpg/i18n/de.ts +1 -11
  275. package/src/tool/pngAJpg/i18n/en.ts +1 -11
  276. package/src/tool/pngAJpg/i18n/es.ts +1 -11
  277. package/src/tool/pngAJpg/i18n/fr.ts +3 -13
  278. package/src/tool/pngAJpg/i18n/id.ts +1 -11
  279. package/src/tool/pngAJpg/i18n/it.ts +1 -11
  280. package/src/tool/pngAJpg/i18n/ja.ts +1 -11
  281. package/src/tool/pngAJpg/i18n/ko.ts +1 -11
  282. package/src/tool/pngAJpg/i18n/nl.ts +1 -11
  283. package/src/tool/pngAJpg/i18n/pl.ts +7 -17
  284. package/src/tool/pngAJpg/i18n/pt.ts +1 -11
  285. package/src/tool/pngAJpg/i18n/ru.ts +9 -19
  286. package/src/tool/pngAJpg/i18n/sv.ts +1 -11
  287. package/src/tool/pngAJpg/i18n/tr.ts +1 -11
  288. package/src/tool/pngAJpg/i18n/zh.ts +6 -16
  289. package/src/tool/pngAJpg/seo.astro +2 -1
  290. package/src/tool/pngAWebp/bibliography.ts +16 -0
  291. package/src/tool/pngAWebp/entry.ts +2 -0
  292. package/src/tool/pngAWebp/i18n/de.ts +7 -21
  293. package/src/tool/pngAWebp/i18n/en.ts +7 -21
  294. package/src/tool/pngAWebp/i18n/es.ts +6 -20
  295. package/src/tool/pngAWebp/i18n/fr.ts +13 -27
  296. package/src/tool/pngAWebp/i18n/id.ts +7 -21
  297. package/src/tool/pngAWebp/i18n/it.ts +3 -17
  298. package/src/tool/pngAWebp/i18n/ja.ts +2 -16
  299. package/src/tool/pngAWebp/i18n/ko.ts +2 -16
  300. package/src/tool/pngAWebp/i18n/nl.ts +7 -21
  301. package/src/tool/pngAWebp/i18n/pl.ts +7 -21
  302. package/src/tool/pngAWebp/i18n/pt.ts +6 -20
  303. package/src/tool/pngAWebp/i18n/ru.ts +8 -22
  304. package/src/tool/pngAWebp/i18n/sv.ts +7 -21
  305. package/src/tool/pngAWebp/i18n/tr.ts +6 -20
  306. package/src/tool/pngAWebp/i18n/zh.ts +6 -20
  307. package/src/tool/pngAWebp/seo.astro +2 -1
  308. package/src/tool/svgAJpg/bibliography.ts +12 -0
  309. package/src/tool/svgAJpg/entry.ts +2 -0
  310. package/src/tool/svgAJpg/i18n/de.ts +2 -12
  311. package/src/tool/svgAJpg/i18n/en.ts +1 -11
  312. package/src/tool/svgAJpg/i18n/es.ts +1 -11
  313. package/src/tool/svgAJpg/i18n/fr.ts +6 -16
  314. package/src/tool/svgAJpg/i18n/id.ts +1 -11
  315. package/src/tool/svgAJpg/i18n/it.ts +1 -11
  316. package/src/tool/svgAJpg/i18n/ja.ts +1 -11
  317. package/src/tool/svgAJpg/i18n/ko.ts +1 -11
  318. package/src/tool/svgAJpg/i18n/nl.ts +1 -11
  319. package/src/tool/svgAJpg/i18n/pl.ts +1 -11
  320. package/src/tool/svgAJpg/i18n/pt.ts +1 -11
  321. package/src/tool/svgAJpg/i18n/ru.ts +3 -13
  322. package/src/tool/svgAJpg/i18n/sv.ts +1 -11
  323. package/src/tool/svgAJpg/i18n/tr.ts +1 -11
  324. package/src/tool/svgAJpg/i18n/zh.ts +1 -11
  325. package/src/tool/svgAJpg/seo.astro +2 -1
  326. package/src/tool/svgAPng/bibliography.ts +12 -0
  327. package/src/tool/svgAPng/entry.ts +2 -0
  328. package/src/tool/svgAPng/i18n/de.ts +3 -13
  329. package/src/tool/svgAPng/i18n/en.ts +3 -13
  330. package/src/tool/svgAPng/i18n/es.ts +1 -11
  331. package/src/tool/svgAPng/i18n/fr.ts +9 -19
  332. package/src/tool/svgAPng/i18n/id.ts +3 -13
  333. package/src/tool/svgAPng/i18n/it.ts +3 -13
  334. package/src/tool/svgAPng/i18n/ja.ts +1 -11
  335. package/src/tool/svgAPng/i18n/ko.ts +1 -11
  336. package/src/tool/svgAPng/i18n/nl.ts +3 -13
  337. package/src/tool/svgAPng/i18n/pl.ts +3 -13
  338. package/src/tool/svgAPng/i18n/pt.ts +3 -13
  339. package/src/tool/svgAPng/i18n/ru.ts +5 -15
  340. package/src/tool/svgAPng/i18n/sv.ts +3 -13
  341. package/src/tool/svgAPng/i18n/tr.ts +3 -13
  342. package/src/tool/svgAPng/i18n/zh.ts +3 -13
  343. package/src/tool/svgAPng/seo.astro +2 -1
  344. package/src/tool/webpAIco/bibliography.ts +12 -0
  345. package/src/tool/webpAIco/entry.ts +2 -0
  346. package/src/tool/webpAIco/i18n/de.ts +1 -11
  347. package/src/tool/webpAIco/i18n/en.ts +1 -11
  348. package/src/tool/webpAIco/i18n/es.ts +1 -11
  349. package/src/tool/webpAIco/i18n/fr.ts +5 -15
  350. package/src/tool/webpAIco/i18n/id.ts +1 -11
  351. package/src/tool/webpAIco/i18n/it.ts +1 -11
  352. package/src/tool/webpAIco/i18n/ja.ts +1 -11
  353. package/src/tool/webpAIco/i18n/ko.ts +1 -11
  354. package/src/tool/webpAIco/i18n/nl.ts +1 -11
  355. package/src/tool/webpAIco/i18n/pl.ts +1 -11
  356. package/src/tool/webpAIco/i18n/pt.ts +1 -11
  357. package/src/tool/webpAIco/i18n/ru.ts +3 -13
  358. package/src/tool/webpAIco/i18n/sv.ts +1 -11
  359. package/src/tool/webpAIco/i18n/tr.ts +1 -11
  360. package/src/tool/webpAIco/i18n/zh.ts +1 -12
  361. package/src/tool/webpAIco/seo.astro +2 -1
  362. package/src/tool/webpAJpg/bibliography.ts +16 -0
  363. package/src/tool/webpAJpg/entry.ts +2 -0
  364. package/src/tool/webpAJpg/i18n/de.ts +3 -17
  365. package/src/tool/webpAJpg/i18n/en.ts +3 -17
  366. package/src/tool/webpAJpg/i18n/es.ts +1 -15
  367. package/src/tool/webpAJpg/i18n/fr.ts +10 -24
  368. package/src/tool/webpAJpg/i18n/id.ts +3 -17
  369. package/src/tool/webpAJpg/i18n/it.ts +2 -16
  370. package/src/tool/webpAJpg/i18n/ja.ts +1 -15
  371. package/src/tool/webpAJpg/i18n/ko.ts +1 -15
  372. package/src/tool/webpAJpg/i18n/nl.ts +2 -16
  373. package/src/tool/webpAJpg/i18n/pl.ts +3 -17
  374. package/src/tool/webpAJpg/i18n/pt.ts +3 -17
  375. package/src/tool/webpAJpg/i18n/ru.ts +7 -21
  376. package/src/tool/webpAJpg/i18n/sv.ts +3 -17
  377. package/src/tool/webpAJpg/i18n/tr.ts +2 -16
  378. package/src/tool/webpAJpg/i18n/zh.ts +2 -16
  379. package/src/tool/webpAJpg/seo.astro +2 -1
  380. package/src/tool/webpAPng/bibliography.ts +16 -0
  381. package/src/tool/webpAPng/entry.ts +2 -0
  382. package/src/tool/webpAPng/i18n/de.ts +3 -17
  383. package/src/tool/webpAPng/i18n/en.ts +3 -17
  384. package/src/tool/webpAPng/i18n/es.ts +1 -15
  385. package/src/tool/webpAPng/i18n/fr.ts +9 -23
  386. package/src/tool/webpAPng/i18n/id.ts +3 -17
  387. package/src/tool/webpAPng/i18n/it.ts +3 -17
  388. package/src/tool/webpAPng/i18n/ja.ts +1 -15
  389. package/src/tool/webpAPng/i18n/ko.ts +1 -15
  390. package/src/tool/webpAPng/i18n/nl.ts +3 -17
  391. package/src/tool/webpAPng/i18n/pl.ts +3 -17
  392. package/src/tool/webpAPng/i18n/pt.ts +3 -17
  393. package/src/tool/webpAPng/i18n/ru.ts +4 -18
  394. package/src/tool/webpAPng/i18n/sv.ts +3 -17
  395. package/src/tool/webpAPng/i18n/tr.ts +2 -17
  396. package/src/tool/webpAPng/i18n/zh.ts +3 -17
  397. package/src/tool/webpAPng/seo.astro +2 -1
package/package.json CHANGED
@@ -1,16 +1,19 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-converters",
3
- "version": "1.15.0",
3
+ "version": "1.18.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
7
7
  "exports": {
8
8
  ".": "./src/index.ts",
9
9
  "./data": "./src/data.ts",
10
- "./entries": "./src/entries.ts"
10
+ "./entries": "./src/entries.ts",
11
+ "./runtime/*": "./src/tool/*/index.ts",
12
+ "./category-seo": "./src/category/seo.astro"
11
13
  },
12
14
  "files": [
13
- "src"
15
+ "src",
16
+ "scripts"
14
17
  ],
15
18
  "publishConfig": {
16
19
  "access": "public"
@@ -29,7 +32,8 @@
29
32
  "postversion": "git push && git push --tags",
30
33
  "patch": "npm version patch",
31
34
  "minor": "npm version minor",
32
- "major": "npm version major"
35
+ "major": "npm version major",
36
+ "postinstall": "node scripts/postinstall.mjs"
33
37
  },
34
38
  "lint-staged": {
35
39
  "*.{ts,tsx,astro}": [
@@ -0,0 +1,95 @@
1
+ import os
2
+ import re
3
+
4
+ class FileDiscovery:
5
+ def __init__(self, root_path, extension):
6
+ self.root_path = root_path
7
+ self.extension = extension
8
+
9
+ def get_files(self):
10
+ matched_files = []
11
+ for root, _, files in os.walk(self.root_path):
12
+ for file in files:
13
+ if file.endswith(self.extension):
14
+ matched_files.append(os.path.join(root, file))
15
+ return matched_files
16
+
17
+ class ContentProcessor:
18
+ def __init__(self, separators):
19
+ self.separators = separators
20
+ self.target_keys = ["title", "text", "name", "faqTitle", "bibliographyTitle"]
21
+
22
+ def _clean_text(self, text):
23
+ cleaned = text
24
+ for sep in self.separators:
25
+ if sep in cleaned:
26
+ cleaned = cleaned.split(sep)[0].strip()
27
+ return cleaned
28
+
29
+ def _replace_callback(self, match):
30
+ key = match.group(1)
31
+ quote = match.group(2)
32
+ text = match.group(3)
33
+
34
+ cleaned_text = self._clean_text(text)
35
+ return f"{key}: {quote}{cleaned_text}{quote}"
36
+
37
+ def _replace_const_callback(self, match):
38
+ prefix = match.group(1)
39
+ quote = match.group(2)
40
+ text = match.group(3)
41
+
42
+ cleaned_text = self._clean_text(text)
43
+ return f"{prefix}{quote}{cleaned_text}{quote}"
44
+
45
+ def _replace_seo_callback(self, match):
46
+ prefix = match.group(1)
47
+ quote = match.group(2)
48
+ text = match.group(3)
49
+
50
+ cleaned_text = self._clean_text(text)
51
+ return f"{prefix}{quote}{cleaned_text}{quote}"
52
+
53
+ def process(self, content):
54
+ title_keys = ["title", "faqTitle", "bibliographyTitle"]
55
+ keys_pattern = "|".join(title_keys)
56
+
57
+ pattern = rf"(?<!slug)(?<!\w)({keys_pattern})\s*:\s*(['\"`])(.*?)\2"
58
+ content = re.sub(pattern, self._replace_callback, content)
59
+
60
+ const_pattern = r"(const\s+title\s*=\s*)(['\"`])(.*?)\2"
61
+ content = re.sub(const_pattern, self._replace_const_callback, content)
62
+
63
+ seo_pattern = r"(type:\s*['\"]title['\"]\s*,\s*text:\s*)(['\"`])(.*?)\2"
64
+ content = re.sub(seo_pattern, self._replace_seo_callback, content)
65
+
66
+ return content
67
+
68
+ class FileStorage:
69
+ @staticmethod
70
+ def read(file_path):
71
+ with open(file_path, "r", encoding="utf-8") as f:
72
+ return f.read()
73
+
74
+ @staticmethod
75
+ def write(file_path, content):
76
+ with open(file_path, "w", encoding="utf-8") as f:
77
+ f.write(content)
78
+
79
+ class TitleCleaner:
80
+ def __init__(self):
81
+ self.discovery = FileDiscovery("src/tool", ".ts")
82
+ self.processor = ContentProcessor(["|", "-"])
83
+ self.storage = FileStorage()
84
+
85
+ def run(self):
86
+ files = self.discovery.get_files()
87
+ for file_path in files:
88
+ content = self.storage.read(file_path)
89
+ new_content = self.processor.process(content)
90
+ if content != new_content:
91
+ self.storage.write(file_path, new_content)
92
+
93
+ if __name__ == "__main__":
94
+ cleaner = TitleCleaner()
95
+ cleaner.run()
@@ -0,0 +1,27 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const libDir = dirname(fileURLToPath(import.meta.url));
6
+ const toolsDir = join(libDir, '../src/tool');
7
+
8
+ const inNodeModules = libDir.includes('node_modules');
9
+ if (!inNodeModules) process.exit(0);
10
+
11
+ const projectRoot = join(libDir, '../../../..');
12
+ const categoryKey = JSON.parse(readFileSync(join(libDir, '../package.json'), 'utf8')).name.replace('@jjlmoya/utils-', '');
13
+ const destDir = join(projectRoot, `public/styles/lib/${categoryKey}`);
14
+
15
+ mkdirSync(destDir, { recursive: true });
16
+
17
+ const tools = readdirSync(toolsDir, { withFileTypes: true }).filter(d => d.isDirectory());
18
+ for (const tool of tools) {
19
+ const toolDir = join(toolsDir, tool.name);
20
+ let files;
21
+ try { files = readdirSync(toolDir).filter(f => f.endsWith('.css')); }
22
+ catch { continue; }
23
+ for (const file of files) {
24
+ writeFileSync(join(destDir, file), readFileSync(join(toolDir, file)));
25
+ console.log(`[@jjlmoya/utils-${categoryKey}] copied ${file}`);
26
+ }
27
+ }
@@ -22,7 +22,7 @@ export const content: CategoryLocaleContent = {
22
22
  },
23
23
  {
24
24
  type: 'paragraph',
25
- html: 'Die Wahl des richtigen Bildformats hat direkten Einfluss auf die Web-Performance, die Gerätekompatibilität und die wahrgenommene visuelle Qualität. Unsere <strong>Online-Bild-Konverter</strong> decken die von Designern, Webentwicklern und Fotografen am häufigsten benötigten Konvertierungswege ab ganz ohne externe Server, die die Vertraulichkeit Ihrer Dateien gefährden könnten.',
25
+ html: 'Die Wahl des richtigen Bildformats hat direkten Einfluss auf die Web-Performance, die Gerätekompatibilität und die wahrgenommene visuelle Qualität. Unsere <strong>Online-Bild-Konverter</strong> decken die von Designern, Webentwicklern und Fotografen am häufigsten benötigten Konvertierungswege ab - ganz ohne externe Server, die die Vertraulichkeit Ihrer Dateien gefährden könnten.',
26
26
  },
27
27
  {
28
28
  type: 'title',
@@ -9,15 +9,15 @@ export const content: CategoryLocaleContent = {
9
9
  type: 'summary',
10
10
  title: 'Pourquoi utiliser nos convertisseurs',
11
11
  items: [
12
- 'Conversion 100% locale : vos images ne quittent jamais votre appareil.',
13
- 'Support de 21 combinaisons de formats : PNG, JPG, WebP, SVG, AVIF, GIF, BMP et ICO.',
12
+ 'Conversion 100% locale: vos images ne quittent jamais votre appareil.',
13
+ 'Support de 21 combinaisons de formats: PNG, JPG, WebP, SVG, AVIF, GIF, BMP et ICO.',
14
14
  'Traitement par lots avec téléchargement ZIP.',
15
15
  'Sans inscription, sans limites, sans publicité intrusive.',
16
16
  ],
17
17
  },
18
18
  {
19
19
  type: 'title',
20
- text: 'Conversion d\'Images : Guide Complet des Formats',
20
+ text: 'Conversion d\'Images: Guide Complet des Formats',
21
21
  level: 2,
22
22
  },
23
23
  {
@@ -26,12 +26,12 @@ export const content: CategoryLocaleContent = {
26
26
  },
27
27
  {
28
28
  type: 'title',
29
- text: 'PNG, JPG et WebP : Les Trois Piliers du Web Moderne',
29
+ text: 'PNG, JPG et WebP: Les Trois Piliers du Web Moderne',
30
30
  level: 2,
31
31
  },
32
32
  {
33
33
  type: 'paragraph',
34
- html: 'Le <strong>format PNG</strong> se distingue par sa compression sans perte et son support de la transparence (canal alpha), étant le standard pour les logos, icônes et graphiques aux bords nets. Le <strong>format JPG</strong> offre une compatibilité universelle maximale et des tailles réduites pour les photos, mais sacrifie la qualité à chaque sauvegarde. Le <strong>format WebP</strong>, développé par Google, combine le meilleur des deux : une compression 30-40% supérieure à PNG/JPG avec support de la transparence et des animations.',
34
+ html: 'Le <strong>format PNG</strong> se distingue par sa compression sans perte et son support de la transparence (canal alpha), étant le standard pour les logos, icônes et graphiques aux bords nets. Le <strong>format JPG</strong> offre une compatibilité universelle maximale et des tailles réduites pour les photos, mais sacrifie la qualité à chaque sauvegarde. Le <strong>format WebP</strong>, développé par Google, combine le meilleur des deux: une compression 30-40% supérieure à PNG/JPG avec support de la transparence et des animations.',
35
35
  },
36
36
  {
37
37
  type: 'table',
@@ -47,7 +47,7 @@ export const content: CategoryLocaleContent = {
47
47
  },
48
48
  {
49
49
  type: 'title',
50
- text: 'Formats Nouvelle Génération : AVIF et WebP',
50
+ text: 'Formats Nouvelle Génération: AVIF et WebP',
51
51
  level: 2,
52
52
  },
53
53
  {
@@ -61,7 +61,7 @@ export const content: CategoryLocaleContent = {
61
61
  },
62
62
  {
63
63
  type: 'title',
64
- text: 'SVG, BMP et ICO : Cas d\'Usage Spéciaux',
64
+ text: 'SVG, BMP et ICO: Cas d\'Usage Spéciaux',
65
65
  level: 2,
66
66
  },
67
67
  {
@@ -79,12 +79,12 @@ export const content: CategoryLocaleContent = {
79
79
  },
80
80
  {
81
81
  type: 'title',
82
- text: 'Confidentialité Totale : Conversion sans Serveurs',
82
+ text: 'Confidentialité Totale: Conversion sans Serveurs',
83
83
  level: 2,
84
84
  },
85
85
  {
86
86
  type: 'paragraph',
87
- html: 'Contrairement à d\'autres outils en ligne qui téléchargent vos images sur des serveurs distants, nos convertisseurs utilisent exclusivement les APIs natives du navigateur : <strong>Canvas 2D API</strong> pour la rastérisation, <strong>FileReader API</strong> pour lire les fichiers localement et <strong>Blob/URL.createObjectURL</strong> pour les téléchargements. Vous pouvez ainsi convertir des images confidentielles, des logos d\'entreprise ou des documents privés en toute sécurité.',
87
+ html: 'Contrairement à d\'autres outils en ligne qui téléchargent vos images sur des serveurs distants, nos convertisseurs utilisent exclusivement les APIs natives du navigateur: <strong>Canvas 2D API</strong> pour la rastérisation, <strong>FileReader API</strong> pour lire les fichiers localement et <strong>Blob/URL.createObjectURL</strong> pour les téléchargements. Vous pouvez ainsi convertir des images confidentielles, des logos d\'entreprise ou des documents privés en toute sécurité.',
88
88
  },
89
89
  ],
90
90
  };
@@ -66,7 +66,7 @@ export const content: CategoryLocaleContent = {
66
66
  },
67
67
  {
68
68
  type: 'paragraph',
69
- html: '<strong>Файлы SVG</strong> это математические векторы, которые масштабируются без потерь до любого разрешения, что идеально подходит для логотипов и графического дизайна. Однако многие платформы и приложения для редактирования не поддерживают их напрямую, поэтому для распространения их необходимо растрировать в PNG или JPG. <strong>Битмап-файлы (BMP)</strong>, хотя и устарели для использования в Интернете, по-прежнему часто встречаются в среде Windows и устаревшем программном обеспечении для редактирования. Наконец, <strong>формат ICO</strong> является стандартом для веб-фавиконов и настройки значков Windows, требуя определенной структуры двоичного заголовка, которую наш конвертер генерирует нативно.',
69
+ html: '<strong>Файлы SVG</strong> - это математические векторы, которые масштабируются без потерь до любого разрешения, что идеально подходит для логотипов и графического дизайна. Однако многие платформы и приложения для редактирования не поддерживают их напрямую, поэтому для распространения их необходимо растрировать в PNG или JPG. <strong>Битмап-файлы (BMP)</strong>, хотя и устарели для использования в Интернете, по-прежнему часто встречаются в среде Windows и устаревшем программном обеспечении для редактирования. Наконец, <strong>формат ICO</strong> является стандартом для веб-фавиконов и настройки значков Windows, требуя определенной структуры двоичного заголовка, которую наш конвертер генерирует нативно.',
70
70
  },
71
71
  {
72
72
  type: 'stats',
@@ -31,7 +31,7 @@ export const content: CategoryLocaleContent = {
31
31
  },
32
32
  {
33
33
  type: 'paragraph',
34
- html: '<strong>PNG-formatet</strong> utmärker sig för sin förlustfria komprimering och stöd för transparens (alfakanal), och är standarden för logotyper, ikoner och grafik med skarpa kanter. <strong>JPG-formatet</strong> erbjuder maximal universell kompatibilitet och reducerade storlekar för foton, men offrar kvalitet vid varje sparande. <strong>WebP-formatet</strong>, utvecklat av Google, kombinerar det bästa av båda: 3040 % bättre komprimering än PNG/JPG med stöd för transparens och animering, och är nu det rekommenderade formatet av Core Web Vitals för att förbättra LCP.',
34
+ html: '<strong>PNG-formatet</strong> utmärker sig för sin förlustfria komprimering och stöd för transparens (alfakanal), och är standarden för logotyper, ikoner och grafik med skarpa kanter. <strong>JPG-formatet</strong> erbjuder maximal universell kompatibilitet och reducerade storlekar för foton, men offrar kvalitet vid varje sparande. <strong>WebP-formatet</strong>, utvecklat av Google, kombinerar det bästa av båda: 30-40 % bättre komprimering än PNG/JPG med stöd för transparens och animering, och är nu det rekommenderade formatet av Core Web Vitals för att förbättra LCP.',
35
35
  },
36
36
  {
37
37
  type: 'table',
package/src/data.ts CHANGED
@@ -1,24 +1,3 @@
1
- export { pngAJpg } from './tool/pngAJpg';
2
- export { jpgAPng } from './tool/jpgAPng';
3
- export { webpAPng } from './tool/webpAPng';
4
- export { webpAJpg } from './tool/webpAJpg';
5
- export { pngAWebp } from './tool/pngAWebp';
6
- export { jpgAWebp } from './tool/jpgAWebp';
7
- export { svgAPng } from './tool/svgAPng';
8
- export { svgAJpg } from './tool/svgAJpg';
9
- export { imagenBase64 } from './tool/imagenBase64';
10
- export { bmpAJpg } from './tool/bmpAJpg';
11
- export { bmpAPng } from './tool/bmpAPng';
12
- export { bmpAWebp } from './tool/bmpAWebp';
13
- export { avifAJpg } from './tool/avifAJpg';
14
- export { avifAPng } from './tool/avifAPng';
15
- export { avifAWebp } from './tool/avifAWebp';
16
- export { gifAJpg } from './tool/gifAJpg';
17
- export { gifAPng } from './tool/gifAPng';
18
- export { gifAWebp } from './tool/gifAWebp';
19
- export { pngAIco } from './tool/pngAIco';
20
- export { jpgAIco } from './tool/jpgAIco';
21
- export { webpAIco } from './tool/webpAIco';
22
1
  export { convertersCategory } from './category';
23
2
 
24
3
  export type { ImageConverterUI } from './shared/ImageConverter.astro';
@@ -1,4 +1,4 @@
1
- ---
1
+ ---
2
2
  import "@jjlmoya/utils-shared/theme.css";
3
3
  import PreviewToolbar from "../components/PreviewToolbar.astro";
4
4
  import type { KnownLocale } from "../types";
@@ -78,6 +78,7 @@ const { title, currentLocale = "es", localeUrls = {}, hasSidebar = false } = Ast
78
78
  transition:
79
79
  background-color 0.3s ease,
80
80
  color 0.3s ease;
81
+ font-family: Inter, sans-serif;
81
82
  }
82
83
 
83
84
  main {
@@ -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
+ {toolCss ? <Fragment set:html={`<style is:inline>${toolCss}</style>`} /> : null}
92
108
  {
93
- content.schemas.map((schema) => (
109
+ ( content.schemas ?? []).map((schema) => (
94
110
  <script
95
111
  is:inline
96
112
  type="application/ld+json"
@@ -0,0 +1,118 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+
4
+ type LocaleWithDiacritics = keyof typeof DIACRITIC_RULES;
5
+
6
+ const DIACRITIC_RULES = {
7
+ de: {
8
+ language: 'German',
9
+ expectedCharacters: 'ä ö ü ß',
10
+ characters: /[äöüÄÖÜß]/g,
11
+ minPerThousandLetters: 0.1,
12
+ },
13
+ es: {
14
+ language: 'Spanish',
15
+ expectedCharacters: 'á é í ó ú ü ñ',
16
+ characters: /[áéíóúüñÁÉÍÓÚÜÑ]/g,
17
+ minPerThousandLetters: 0.1,
18
+ },
19
+ fr: {
20
+ language: 'French',
21
+ expectedCharacters: 'à â æ ç é è ê ë î ï ô œ ù û ü ÿ',
22
+ characters: /[àâæçéèêëîïôœùûüÿÀÂÆÇÉÈÊËÎÏÔŒÙÛÜŸ]/g,
23
+ minPerThousandLetters: 0.1,
24
+ },
25
+ it: {
26
+ language: 'Italian',
27
+ expectedCharacters: 'à è é ì í î ò ó ù ú',
28
+ characters: /[àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ]/g,
29
+ minPerThousandLetters: 0.1,
30
+ },
31
+ pl: {
32
+ language: 'Polish',
33
+ expectedCharacters: 'ą ć ę ł ń ó ś ź ż',
34
+ characters: /[ąćęłńóśźżĄĆĘŁŃÓŚŹŻ]/g,
35
+ minPerThousandLetters: 0.1,
36
+ },
37
+ pt: {
38
+ language: 'Portuguese',
39
+ expectedCharacters: 'á â ã à ç é ê í ó ô õ ú ü',
40
+ characters: /[áâãàçéêíóôõúüÁÂÃÀÇÉÊÍÓÔÕÚÜ]/g,
41
+ minPerThousandLetters: 0.1,
42
+ },
43
+ sv: {
44
+ language: 'Swedish',
45
+ expectedCharacters: 'å ä ö',
46
+ characters: /[åäöÅÄÖ]/g,
47
+ minPerThousandLetters: 0.1,
48
+ },
49
+ tr: {
50
+ language: 'Turkish',
51
+ expectedCharacters: 'ç ğ ı İ ö ş ü',
52
+ characters: /[çğıöşüÇĞİÖŞÜ]/g,
53
+ minPerThousandLetters: 0.1,
54
+ },
55
+ } as const;
56
+
57
+ const LETTERS = /\p{L}/gu;
58
+ const TRANSLATABLE_KEYS = ['title', 'description', 'ui', 'seo', 'faq', 'howTo'] as const;
59
+
60
+ function collectStrings(value: unknown): string[] {
61
+ if (typeof value === 'string') return [value];
62
+ if (!value || typeof value !== 'object') return [];
63
+ if (Array.isArray(value)) return value.flatMap(collectStrings);
64
+ return Object.values(value).flatMap(collectStrings);
65
+ }
66
+
67
+ function normalizeText(value: unknown): string {
68
+ return collectStrings(value).join(' ').normalize('NFC');
69
+ }
70
+
71
+ function translatableContent(content: Record<string, unknown>) {
72
+ return TRANSLATABLE_KEYS.map((key) => content[key]);
73
+ }
74
+
75
+ function letterCount(text: string): number {
76
+ return text.match(LETTERS)?.length ?? 0;
77
+ }
78
+
79
+ function diacriticCount(text: string, locale: LocaleWithDiacritics): number {
80
+ return text.match(DIACRITIC_RULES[locale].characters)?.length ?? 0;
81
+ }
82
+
83
+ function diacriticsPerThousandLetters(text: string, locale: LocaleWithDiacritics): number {
84
+ const letters = letterCount(text);
85
+ if (letters === 0) return 0;
86
+ return diacriticCount(text, locale) / letters * 1000;
87
+ }
88
+
89
+ describe('Diacritics density validation', () => {
90
+ ALL_TOOLS.forEach((tool) => {
91
+ describe(`Tool: ${tool.entry.id}`, () => {
92
+ Object.keys(DIACRITIC_RULES).forEach((locale) => {
93
+ it(`${locale} keeps the expected accent and special-letter set`, async () => {
94
+ const typedLocale = locale as LocaleWithDiacritics;
95
+ const loader = tool.entry.i18n[typedLocale];
96
+ if (!loader) return;
97
+
98
+ const content = await loader();
99
+ const text = normalizeText(translatableContent(content as Record<string, unknown>));
100
+ const rule = DIACRITIC_RULES[typedLocale];
101
+ const letters = letterCount(text);
102
+ const matches = diacriticCount(text, typedLocale);
103
+ const density = diacriticsPerThousandLetters(text, typedLocale);
104
+
105
+ expect(
106
+ density,
107
+ [
108
+ `Possible spelling or encoding issue detected in ${tool.entry.id}/${typedLocale} (${rule.language}).`,
109
+ `The text has ${matches} special characters (${density.toFixed(2)} per 1000 letters, ${letters} letters analyzed).`,
110
+ `This locale should include some of these characters: ${rule.expectedCharacters}.`,
111
+ 'If the count is 0 or near 0, accents, tildes, or special letters were probably stripped by encoding or normalization.',
112
+ ].join(' '),
113
+ ).toBeGreaterThanOrEqual(rule.minPerThousandLetters);
114
+ });
115
+ });
116
+ });
117
+ });
118
+ });
@@ -0,0 +1,84 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+
4
+ const INVERTED_PUNCTUATION_LOCALES = {
5
+ es: {
6
+ language: 'Spanish',
7
+ questionStart: '¿',
8
+ questionEnd: '?',
9
+ exclamationStart: '¡',
10
+ exclamationEnd: '!',
11
+ },
12
+ } as const;
13
+
14
+ type InvertedPunctuationLocale = keyof typeof INVERTED_PUNCTUATION_LOCALES;
15
+
16
+ const TRANSLATABLE_KEYS = ['title', 'description', 'ui', 'seo', 'faq', 'howTo'] as const;
17
+
18
+ function collectStrings(value: unknown): string[] {
19
+ if (typeof value === 'string') return [value];
20
+ if (!value || typeof value !== 'object') return [];
21
+ if (Array.isArray(value)) return value.flatMap(collectStrings);
22
+ return Object.values(value).flatMap(collectStrings);
23
+ }
24
+
25
+ function translatableStrings(content: Record<string, unknown>): string[] {
26
+ return TRANSLATABLE_KEYS.flatMap((key) => collectStrings(content[key]));
27
+ }
28
+
29
+ function sentenceStart(text: string, endIndex: number): string {
30
+ const beforeMark = text.slice(0, endIndex).trimEnd();
31
+ const boundary = Math.max(
32
+ beforeMark.lastIndexOf('.'),
33
+ beforeMark.lastIndexOf(':'),
34
+ beforeMark.lastIndexOf(';'),
35
+ beforeMark.lastIndexOf('\n'),
36
+ );
37
+
38
+ return beforeMark.slice(boundary + 1).trimStart();
39
+ }
40
+
41
+ function findMissingInvertedMarks(
42
+ text: string,
43
+ startMark: string,
44
+ endMark: string,
45
+ ): string[] {
46
+ return [...text.matchAll(new RegExp(`\\${endMark}`, 'g'))]
47
+ .map((match) => sentenceStart(text, match.index ?? 0))
48
+ .filter((segment) => segment.length > 0 && !segment.includes(startMark))
49
+ .map((segment) => `${segment}${endMark}`);
50
+ }
51
+
52
+ describe('Inverted punctuation validation', () => {
53
+ ALL_TOOLS.forEach((tool) => {
54
+ describe(`Tool: ${tool.entry.id}`, () => {
55
+ Object.keys(INVERTED_PUNCTUATION_LOCALES).forEach((locale) => {
56
+ it(`${locale} uses opening punctuation marks for questions and exclamations`, async () => {
57
+ const typedLocale = locale as InvertedPunctuationLocale;
58
+ const loader = tool.entry.i18n[typedLocale];
59
+ if (!loader) return;
60
+
61
+ const rule = INVERTED_PUNCTUATION_LOCALES[typedLocale];
62
+ const content = await loader();
63
+ const strings = translatableStrings(content as Record<string, unknown>);
64
+ const missingQuestions = strings.flatMap((text) =>
65
+ findMissingInvertedMarks(text, rule.questionStart, rule.questionEnd)
66
+ );
67
+ const missingExclamations = strings.flatMap((text) =>
68
+ findMissingInvertedMarks(text, rule.exclamationStart, rule.exclamationEnd)
69
+ );
70
+ const failures = [...missingQuestions, ...missingExclamations];
71
+
72
+ expect(
73
+ failures,
74
+ [
75
+ `Missing opening punctuation marks in ${tool.entry.id}/${typedLocale} (${rule.language}).`,
76
+ `Questions must use ${rule.questionStart}...${rule.questionEnd} and exclamations must use ${rule.exclamationStart}...${rule.exclamationEnd}.`,
77
+ `Examples: ${failures.slice(0, 5).join(' | ')}`,
78
+ ].join(' '),
79
+ ).toHaveLength(0);
80
+ });
81
+ });
82
+ });
83
+ });
84
+ });
@@ -0,0 +1,70 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ function getFiles(dir: string): string[] {
6
+ const results: string[] = [];
7
+ if (!fs.existsSync(dir)) {
8
+ return results;
9
+ }
10
+ const list = fs.readdirSync(dir);
11
+ for (const file of list) {
12
+ const fullPath = path.join(dir, file);
13
+ const stat = fs.statSync(fullPath);
14
+ if (stat && stat.isDirectory()) {
15
+ if (file !== 'tests' && file !== 'node_modules' && file !== '.astro') {
16
+ results.push(...getFiles(fullPath));
17
+ }
18
+ } else {
19
+ results.push(fullPath);
20
+ }
21
+ }
22
+ return results;
23
+ }
24
+
25
+ function isContentFile(filePath: string): boolean {
26
+ return /\\i18n\\/.test(filePath) || filePath.endsWith('bibliography.ts');
27
+ }
28
+
29
+ const srcDir = path.join(process.cwd(), 'src');
30
+ const scriptsDir = path.join(process.cwd(), 'scripts');
31
+ const filesToTest = [
32
+ ...getFiles(srcDir).filter(isContentFile),
33
+ ...getFiles(scriptsDir).filter(isContentFile),
34
+ ];
35
+
36
+ const aiTypographyGarbage = [
37
+ '\u2013',
38
+ '\u2014',
39
+ '\u2026',
40
+ '\u201C',
41
+ '\u201D',
42
+ '\u2018',
43
+ '\u2019',
44
+ '\u00AB',
45
+ '\u00BB',
46
+ '\u200B',
47
+ '\u201E',
48
+ ];
49
+
50
+ describe('Typography Garbage Character Validation', () => {
51
+ filesToTest.forEach((filePath) => {
52
+ const relativePath = path.relative(process.cwd(), filePath);
53
+ it(`should not contain typography garbage characters in ${relativePath}`, () => {
54
+ const content = fs.readFileSync(filePath, 'utf-8');
55
+ const hasAiPatterns = aiTypographyGarbage.some(char => content.includes(char));
56
+ expect(hasAiPatterns).toBe(false);
57
+ });
58
+
59
+ it(`should not contain space before colon in ${relativePath}`, () => {
60
+ const content = fs.readFileSync(filePath, 'utf-8');
61
+ const spaceBeforeColon = / : /.test(content);
62
+ expect(spaceBeforeColon).toBe(false);
63
+ });
64
+
65
+ it(`should not contain double hyphen in ${relativePath}`, () => {
66
+ const content = fs.readFileSync(filePath, 'utf-8');
67
+ expect(content).not.toContain('--');
68
+ });
69
+ });
70
+ });