@jjlmoya/utils-cooking 1.2.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 (130) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +24 -0
  3. package/src/category/i18n/es.ts +208 -0
  4. package/src/category/i18n/fr.ts +24 -0
  5. package/src/category/index.ts +37 -0
  6. package/src/category/seo.astro +15 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +11 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +32 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +19 -0
  17. package/src/tests/i18n-titles.test.ts +66 -0
  18. package/src/tests/locale_completeness.test.ts +42 -0
  19. package/src/tests/mocks/astro_mock.js +2 -0
  20. package/src/tests/no_h1_in_components.test.ts +48 -0
  21. package/src/tests/seo_length.test.ts +22 -0
  22. package/src/tests/tool_validation.test.ts +17 -0
  23. package/src/tool/american-kitchen-converter/AmericanKitchenEngine.ts +259 -0
  24. package/src/tool/american-kitchen-converter/bibliography.astro +6 -0
  25. package/src/tool/american-kitchen-converter/component.astro +838 -0
  26. package/src/tool/american-kitchen-converter/i18n/en.ts +282 -0
  27. package/src/tool/american-kitchen-converter/i18n/es.ts +281 -0
  28. package/src/tool/american-kitchen-converter/i18n/fr.ts +292 -0
  29. package/src/tool/american-kitchen-converter/index.ts +24 -0
  30. package/src/tool/american-kitchen-converter/seo.astro +8 -0
  31. package/src/tool/banana-ripeness/BananaCare.css +587 -0
  32. package/src/tool/banana-ripeness/BananaEngine.ts +79 -0
  33. package/src/tool/banana-ripeness/bibliography.astro +6 -0
  34. package/src/tool/banana-ripeness/component.astro +285 -0
  35. package/src/tool/banana-ripeness/i18n/en.ts +177 -0
  36. package/src/tool/banana-ripeness/i18n/es.ts +177 -0
  37. package/src/tool/banana-ripeness/i18n/fr.ts +177 -0
  38. package/src/tool/banana-ripeness/index.ts +24 -0
  39. package/src/tool/banana-ripeness/seo.astro +8 -0
  40. package/src/tool/brine/bibliography.astro +6 -0
  41. package/src/tool/brine/component.astro +884 -0
  42. package/src/tool/brine/i18n/en.ts +221 -0
  43. package/src/tool/brine/i18n/es.ts +222 -0
  44. package/src/tool/brine/i18n/fr.ts +221 -0
  45. package/src/tool/brine/index.ts +26 -0
  46. package/src/tool/brine/seo.astro +8 -0
  47. package/src/tool/cookware-guide/CookwareGuide.css +487 -0
  48. package/src/tool/cookware-guide/bibliography.astro +6 -0
  49. package/src/tool/cookware-guide/component.astro +164 -0
  50. package/src/tool/cookware-guide/i18n/en.ts +163 -0
  51. package/src/tool/cookware-guide/i18n/es.ts +163 -0
  52. package/src/tool/cookware-guide/i18n/fr.ts +164 -0
  53. package/src/tool/cookware-guide/index.ts +24 -0
  54. package/src/tool/cookware-guide/init.ts +174 -0
  55. package/src/tool/cookware-guide/seo.astro +8 -0
  56. package/src/tool/egg-timer/EggTimer.css +503 -0
  57. package/src/tool/egg-timer/bibliography.astro +14 -0
  58. package/src/tool/egg-timer/component.astro +281 -0
  59. package/src/tool/egg-timer/i18n/en.ts +230 -0
  60. package/src/tool/egg-timer/i18n/es.ts +222 -0
  61. package/src/tool/egg-timer/i18n/fr.ts +121 -0
  62. package/src/tool/egg-timer/index.ts +27 -0
  63. package/src/tool/egg-timer/seo.astro +39 -0
  64. package/src/tool/ingredient-rescaler/IngredientRescaler.css +308 -0
  65. package/src/tool/ingredient-rescaler/bibliography.astro +6 -0
  66. package/src/tool/ingredient-rescaler/component.astro +107 -0
  67. package/src/tool/ingredient-rescaler/i18n/en.ts +265 -0
  68. package/src/tool/ingredient-rescaler/i18n/es.ts +268 -0
  69. package/src/tool/ingredient-rescaler/i18n/fr.ts +207 -0
  70. package/src/tool/ingredient-rescaler/index.ts +24 -0
  71. package/src/tool/ingredient-rescaler/init.ts +200 -0
  72. package/src/tool/ingredient-rescaler/seo.astro +8 -0
  73. package/src/tool/kitchen-timer/KitchenTimer.css +325 -0
  74. package/src/tool/kitchen-timer/bibliography.astro +6 -0
  75. package/src/tool/kitchen-timer/component.astro +341 -0
  76. package/src/tool/kitchen-timer/i18n/en.ts +154 -0
  77. package/src/tool/kitchen-timer/i18n/es.ts +154 -0
  78. package/src/tool/kitchen-timer/i18n/fr.ts +154 -0
  79. package/src/tool/kitchen-timer/index.ts +26 -0
  80. package/src/tool/kitchen-timer/init.ts +55 -0
  81. package/src/tool/kitchen-timer/lib/AudioHelper.ts +27 -0
  82. package/src/tool/kitchen-timer/lib/DockManager.ts +97 -0
  83. package/src/tool/kitchen-timer/lib/KitchenTimer.ts +264 -0
  84. package/src/tool/kitchen-timer/seo.astro +8 -0
  85. package/src/tool/meringue-peak/MeringueCalculator.css +298 -0
  86. package/src/tool/meringue-peak/bibliography.astro +6 -0
  87. package/src/tool/meringue-peak/component.astro +169 -0
  88. package/src/tool/meringue-peak/i18n/en.ts +257 -0
  89. package/src/tool/meringue-peak/i18n/es.ts +234 -0
  90. package/src/tool/meringue-peak/i18n/fr.ts +234 -0
  91. package/src/tool/meringue-peak/index.ts +24 -0
  92. package/src/tool/meringue-peak/seo.astro +8 -0
  93. package/src/tool/mold-scaler/MoldScaler.css +406 -0
  94. package/src/tool/mold-scaler/bibliography.astro +6 -0
  95. package/src/tool/mold-scaler/component.astro +126 -0
  96. package/src/tool/mold-scaler/i18n/en.ts +268 -0
  97. package/src/tool/mold-scaler/i18n/es.ts +269 -0
  98. package/src/tool/mold-scaler/i18n/fr.ts +276 -0
  99. package/src/tool/mold-scaler/index.ts +26 -0
  100. package/src/tool/mold-scaler/init.ts +264 -0
  101. package/src/tool/mold-scaler/seo.astro +8 -0
  102. package/src/tool/pizza/Pizza.css +569 -0
  103. package/src/tool/pizza/bibliography.astro +6 -0
  104. package/src/tool/pizza/calculator.ts +143 -0
  105. package/src/tool/pizza/component.astro +237 -0
  106. package/src/tool/pizza/i18n/en.ts +288 -0
  107. package/src/tool/pizza/i18n/es.ts +289 -0
  108. package/src/tool/pizza/i18n/fr.ts +288 -0
  109. package/src/tool/pizza/index.ts +27 -0
  110. package/src/tool/pizza/seo.astro +8 -0
  111. package/src/tool/roux-guide/RouxGuide.css +483 -0
  112. package/src/tool/roux-guide/bibliography.astro +6 -0
  113. package/src/tool/roux-guide/component.astro +194 -0
  114. package/src/tool/roux-guide/i18n/en.ts +233 -0
  115. package/src/tool/roux-guide/i18n/es.ts +225 -0
  116. package/src/tool/roux-guide/i18n/fr.ts +225 -0
  117. package/src/tool/roux-guide/index.ts +24 -0
  118. package/src/tool/roux-guide/init.ts +187 -0
  119. package/src/tool/roux-guide/seo.astro +8 -0
  120. package/src/tool/sourdough-calculator/SourdoughCalculator.css +369 -0
  121. package/src/tool/sourdough-calculator/bibliography.astro +6 -0
  122. package/src/tool/sourdough-calculator/component.astro +198 -0
  123. package/src/tool/sourdough-calculator/i18n/en.ts +242 -0
  124. package/src/tool/sourdough-calculator/i18n/es.ts +243 -0
  125. package/src/tool/sourdough-calculator/i18n/fr.ts +248 -0
  126. package/src/tool/sourdough-calculator/index.ts +24 -0
  127. package/src/tool/sourdough-calculator/init.ts +131 -0
  128. package/src/tool/sourdough-calculator/seo.astro +8 -0
  129. package/src/tools.ts +29 -0
  130. package/src/types.ts +73 -0
@@ -0,0 +1,268 @@
1
+ import type { ToolLocaleContent } from '../../../types';
2
+
3
+ export const content: ToolLocaleContent = {
4
+ slug: 'escalador-ingredientes',
5
+ title: 'Escalador de Ingredientes Ajuste de Recetas',
6
+ description: 'Escala recetas automáticamente según el número de raciones. Calcula las cantidades exactas de ingredientes multiplicando o reduciendo tu receta sin complicaciones.',
7
+ ui: {
8
+ servings: 'Raciones',
9
+ original: 'Original',
10
+ desired: 'Deseado',
11
+ multiplyingFactor: 'Factor Multiplicador',
12
+ ingredientsList: 'Lista de Ingredientes',
13
+ pasteHere: 'Pega aquí tu lista.',
14
+ exampleLine1: 'Ej:',
15
+ exampleLine2: '500g Harina',
16
+ exampleLine3: '300ml Agua',
17
+ exampleLine4: '10g Sal',
18
+ result: 'Resultado',
19
+ copy: 'Copiar',
20
+ copied: 'Copiado!',
21
+ emptyState: 'Tus ingredientes ajustados aparecerán aquí...',
22
+ defaultIngredient1: '200g Harina',
23
+ defaultIngredient2: '100ml Leche',
24
+ defaultIngredient3: '2 Huevos',
25
+ },
26
+ faqTitle: 'Preguntas Frecuentes',
27
+ faq: [
28
+ {
29
+ question: '¿Por qué mi factor de conversión incluye decimales?',
30
+ answer: 'Porque las proporciones culinarias no siempre son redondas. Si escalas una receta para 4 personas a 7, el factor es 1.75 exactamente. Los ingredientes se multiplican por ese número, aunque encuentres fracciones. Es más exacto que redondear arbitrariamente.',
31
+ },
32
+ {
33
+ question: '¿Qué pasa si ingreso "1/2 cucharadita de sal"?',
34
+ answer: 'El parser busca el número primero. Reconocerá "1" como cantidad, luego "/2" como parte de la unidad. El resultado será "0.5", y al escalar se multiplicará correctamente. Para fracciones como "1/2", ingresa "0.5" directamente (más claro) o la herramienta las interpreta como división.',
35
+ },
36
+ {
37
+ question: '¿Debo redondear los resultados finales?',
38
+ answer: 'Depende del ingrediente. Para harinas, sí. Para levaduras o especias, la precisión importa menos (escala al 75% de lo indicado). Para huevos: si obtienes 2.3, usa 2 completos + parte de un tercero (pesada), o redondea a 2 si el plato lo permite.',
39
+ },
40
+ {
41
+ question: '¿Por qué no cambia el resultado cuando cambio las raciones?',
42
+ answer: 'Asegúrate de que el campo de ingredientes tenga contenido. Si está vacío, no hay nada que escalar. También verifica que los números en tus ingredientes sean reconocibles (ej: "500g", "1/2 cucharadita").',
43
+ },
44
+ {
45
+ question: '¿Funciona con medidas imperiales (onzas, tazas)?',
46
+ answer: 'Técnicamente sí, la herramienta lee números y escala. Pero la precisión es limitada con tazas (volumen inconsistente). Se recomienda convertir a gramos antes de escalar.',
47
+ },
48
+ ],
49
+ bibliographyTitle: 'Bibliografía',
50
+ bibliography: [
51
+ {
52
+ name: 'Harold McGee - On Food and Cooking: The Science and Lore of the Kitchen',
53
+ url: 'https://en.wikipedia.org/wiki/Harold_McGee',
54
+ },
55
+ {
56
+ name: 'The Flavor Bible by Karen Page and Andrew Dornenburg',
57
+ url: 'https://www.flavorprints.com/',
58
+ },
59
+ {
60
+ name: 'Modernist Cuisine - Técnicas de escalado científico',
61
+ url: 'https://www.modernistcuisine.com/',
62
+ },
63
+ ],
64
+ howTo: [
65
+ {
66
+ name: 'Ingresa tus raciones',
67
+ text: 'En el campo "Original", coloca el número de personas para el que la receta está diseñada. En "Deseado", coloca el número de personas que vas a cocinar.',
68
+ },
69
+ {
70
+ name: 'Pega tu lista de ingredientes',
71
+ text: 'Copia y pega tu lista tal cual. Cada ingrediente en una línea. La herramienta reconoce números al inicio (500g, 1/2, 2.5) y escala automáticamente.',
72
+ },
73
+ {
74
+ name: 'Ajusta según contexto',
75
+ text: 'Los resultados son matemáticamente exactos, pero la cocina es arte. Especias: escala al 75%. Levaduras: menos de lo teórico en grandes cantidades. Tiempos: no se escalan nunca.',
76
+ },
77
+ ],
78
+ seo: [
79
+ {
80
+ type: 'title',
81
+ text: 'Guía Maestro para el Escalado y Conversión de Recetas',
82
+ level: 2,
83
+ },
84
+ {
85
+ type: 'paragraph',
86
+ html: 'El <strong>escalado de ingredientes</strong> es una de las tareas más críticas y propensas a errores en la cocina profesional. No se trata simplemente de multiplicar números, sino de entender cómo la química y la física de los alimentos reaccionan ante cambios de volumen y superficie.',
87
+ },
88
+ {
89
+ type: 'stats',
90
+ columns: 4,
91
+ items: [
92
+ {
93
+ value: 'x1.75',
94
+ label: 'Factor de 4 a 7 per.',
95
+ icon: 'mdi:calculator',
96
+ },
97
+ {
98
+ value: '75%',
99
+ label: 'Ajuste de Especias',
100
+ icon: 'mdi:shaker-outline',
101
+ },
102
+ {
103
+ value: '0%',
104
+ label: 'Escalado del Tiempo',
105
+ icon: 'mdi:timer-off',
106
+ },
107
+ {
108
+ value: '100g',
109
+ label: 'Base de Referencia',
110
+ icon: 'mdi:weight-gram',
111
+ },
112
+ ],
113
+ },
114
+ {
115
+ type: 'title',
116
+ text: 'Diferencias entre Escalado Lineal y Contextual',
117
+ level: 3,
118
+ },
119
+ {
120
+ type: 'comparative',
121
+ columns: 2,
122
+ items: [
123
+ {
124
+ title: 'Escalado Lineal Matemático',
125
+ icon: 'mdi:math-log',
126
+ description: 'Multiplicación directa de todos los valores por el factor de conversión obtenido.',
127
+ points: [
128
+ 'Perfecto para harinas y líquidos base',
129
+ 'Ideal para repostería de precisión fría',
130
+ 'Funciona bien en cambios pequeños (x2, x0.5)',
131
+ 'Fácil de calcular automáticamente',
132
+ ],
133
+ },
134
+ {
135
+ title: 'Escalado Contextual de Cocina',
136
+ icon: 'mdi:chef-hat',
137
+ description: 'Ajuste de proporciones basado en la saturación de sabor y evaporación.',
138
+ highlight: true,
139
+ points: [
140
+ 'Evita el exceso de sal y especias',
141
+ 'Considera la superficie de evaporación',
142
+ 'Ajusta levaduras según fricción',
143
+ 'Requiere experiencia y criterio del chef',
144
+ ],
145
+ },
146
+ ],
147
+ },
148
+ {
149
+ type: 'title',
150
+ text: 'Factores de Conversión por Número de Comensales',
151
+ level: 3,
152
+ },
153
+ {
154
+ type: 'table',
155
+ headers: ['De Raciones', 'A Raciones', 'Factor (FC)', 'Dificultad de Ajuste'],
156
+ rows: [
157
+ ['2 personas', '4 personas', 'x 2.0', 'Baja (Lineal)'],
158
+ ['4 personas', '6 personas', 'x 1.5', 'Baja (Lineal)'],
159
+ ['4 personas', '10 personas', 'x 2.5', 'Media (Ojo con especias)'],
160
+ ['4 personas', '25 personas', 'x 6.25', 'Alta (Contextual)'],
161
+ ],
162
+ },
163
+ {
164
+ type: 'diagnostic',
165
+ variant: 'warning',
166
+ title: 'Problemas Comunes al Doblar una Receta',
167
+ html: 'Si notas que el sabor es demasiado intenso tras escalar (especialmente con picantes o sal), reduce el escalado de estos ingredientes específicos al 75%. Nunca dupliques el tiempo de cocción; la temperatura interna se alcanza según el grosor de la pieza, no según el peso total de la masa.',
168
+ },
169
+ {
170
+ type: 'title',
171
+ text: 'Glosario de Terminología de Estandarización',
172
+ level: 3,
173
+ },
174
+ {
175
+ type: 'glossary',
176
+ items: [
177
+ {
178
+ term: 'Factor de Conversión',
179
+ definition: 'Número por el que se multiplican todos los ingredientes: Cantidad Deseada / Cantidad Original.',
180
+ },
181
+ {
182
+ term: 'Merma de Cocción',
183
+ definition: 'Pérdida de peso de un ingrediente por evaporación o pérdida de grasa. No escala de forma lineal.',
184
+ },
185
+ {
186
+ term: 'Peso Bruto vs Neto',
187
+ definition: 'El peso antes y después de limpiar el ingrediente. Escalamos siempre sobre el peso de la receta original.',
188
+ },
189
+ {
190
+ term: 'Rendimiento',
191
+ definition: 'Cantidad total de producto terminado tras el escalado y la cocción.',
192
+ },
193
+ ],
194
+ },
195
+ {
196
+ type: 'tip',
197
+ title: 'El truco del Huevo Batido',
198
+ html: 'Si la calculadora indica 2.3 huevos, bate un huevo entero, pésalo y utiliza solo el porcentaje correspondiente (0.3). Es la única forma de mantener la precisión en repostería fina.',
199
+ },
200
+ {
201
+ type: 'paragraph',
202
+ html: 'Nuestra herramienta simplifica la matemática para que puedas dedicar tu energía a lo que realmente importa: la creatividad y el sabor.',
203
+ },
204
+ ],
205
+
206
+ schemas: [
207
+ {
208
+ '@context': 'https://schema.org',
209
+ '@type': 'WebApplication',
210
+ name: 'Escalador de Ingredientes',
211
+ description: 'Escala recetas automáticamente según el número de raciones. Calcula las cantidades exactas de ingredientes multiplicando o reduciendo tu receta sin complicaciones.',
212
+ url: 'https://jjlmoya.es/utilidades/escalador-ingredientes',
213
+ applicationCategory: 'Utilities',
214
+ inLanguage: 'es-ES',
215
+ offers: {
216
+ '@type': 'Offer',
217
+ price: '0',
218
+ priceCurrency: 'EUR',
219
+ },
220
+ },
221
+ {
222
+ '@context': 'https://schema.org',
223
+ '@type': 'Article',
224
+ headline: 'Escalador de Ingredientes - Ajusta tus Recetas Fácilmente',
225
+ description: 'Escala recetas automáticamente según el número de raciones. Calcula las cantidades exactas de ingredientes multiplicando o reduciendo tu receta sin complicaciones.',
226
+ author: {
227
+ '@type': 'Person',
228
+ name: 'jjlmoya',
229
+ },
230
+ inLanguage: 'es-ES',
231
+ isPartOf: {
232
+ '@type': 'WebSite',
233
+ name: 'Utilidades jjlmoya',
234
+ url: 'https://jjlmoya.es',
235
+ },
236
+ },
237
+ {
238
+ '@context': 'https://schema.org',
239
+ '@type': 'BreadcrumbList',
240
+ itemListElement: [
241
+ {
242
+ '@type': 'ListItem',
243
+ position: 1,
244
+ name: 'Inicio',
245
+ item: 'https://jjlmoya.es',
246
+ },
247
+ {
248
+ '@type': 'ListItem',
249
+ position: 2,
250
+ name: 'Utilidades',
251
+ item: 'https://jjlmoya.es/utilidades',
252
+ },
253
+ {
254
+ '@type': 'ListItem',
255
+ position: 3,
256
+ name: 'Cocina',
257
+ item: 'https://jjlmoya.es/utilidades/cocina',
258
+ },
259
+ {
260
+ '@type': 'ListItem',
261
+ position: 4,
262
+ name: 'Escalador de Ingredientes',
263
+ item: 'https://jjlmoya.es/utilidades/escalador-ingredientes',
264
+ },
265
+ ],
266
+ },
267
+ ],
268
+ };
@@ -0,0 +1,207 @@
1
+ import type { ToolLocaleContent } from "../../../types";
2
+
3
+ export const content: ToolLocaleContent = {
4
+ slug: "calculateur-mise-echelle-recette-ingredients-portions",
5
+ title: "Convertisseur de Portions et Ingrédients Professionnel",
6
+ description:
7
+ "Mettez à l'échelle vos recettes automatiquement selon le nombre de portions. Calculez les quantités exactes d'ingrédients avec une précision professionnelle.",
8
+ ui: {
9
+ servings: "Portions",
10
+ original: "Original",
11
+ desired: "Souhaité",
12
+ multiplyingFactor: "Facteur de Conversion",
13
+ ingredientsList: "Liste des Ingrédients",
14
+ pasteHere: "Collez votre liste d'ingrédients ici.",
15
+ exampleLine1: "Ex :",
16
+ exampleLine2: "500g Farine",
17
+ exampleLine3: "300ml Eau",
18
+ exampleLine4: "10g Sel",
19
+ result: "Résultat",
20
+ copy: "Copier",
21
+ copied: "Copié !",
22
+ emptyState: "Vos ingrédients ajustés apparaîtront ici...",
23
+ defaultIngredient1: "200g Farine",
24
+ defaultIngredient2: "100ml Lait",
25
+ defaultIngredient3: "2 Œufs",
26
+ },
27
+ faqTitle: "Questions Fréquentes sur l'Échelle des Recettes",
28
+ faq: [
29
+ {
30
+ question: "Pourquoi mon facteur de conversion inclut-il des décimales ?",
31
+ answer:
32
+ "Parce que les proportions culinaires ne tombent pas toujours sur des chiffres ronds. Si vous passez d'une recette pour 4 à 7 personnes, le facteur est exactement 1,75. Multiplier par ce ratio précis est plus sûr que d'arrondir arbitrairement.",
33
+ },
34
+ {
35
+ question: "Comment gérer les fractions comme '1/2 cac de sel' ?",
36
+ answer:
37
+ "Notre outil reconnaît automatiquement les chiffres. Il lira '1/2' comme '0,5' et l'adaptera. Pour plus de clarté, vous pouvez aussi saisir directement des décimales.",
38
+ },
39
+ {
40
+ question: "Dois-je arrondir les résultats finaux ?",
41
+ answer:
42
+ "Cela dépend de l'ingrédient. Pour les farines et liquides, oui. Pour les épices fortes ou la levure, nous recommandons de ne monter qu'à 75% du total calculé pour éviter de saturer le plat.",
43
+ },
44
+ {
45
+ question: "L'outil fonctionne-t-il avec les onces ou les tasses ?",
46
+ answer:
47
+ "Oui, l'outil multiplie n'importe quelle valeur numérique. Cependant, nous conseillons vivement de convertir en grammes pour une précision professionnelle sur de gros volumes.",
48
+ },
49
+ ],
50
+ howTo: [
51
+ {
52
+ name: "Entrez le nombre de portions",
53
+ text: "Définissez le nombre de personnes prévu initialement dans la recette, et le nombre pour lequel vous souhaitez cuisiner.",
54
+ },
55
+ {
56
+ name: "Collez votre liste d'ingrédients",
57
+ text: "Copiez et collez l'intégralité de votre liste. L'outil reconnaît les chiffres en début de ligne (ex: 500g, 1/2, 2.5) et les convertit instantanément.",
58
+ },
59
+ {
60
+ name: "Appliquez les ajustements de chef",
61
+ text: "Le calcul est exact, mais la cuisine est intuitive. Réduisez les épices à 75%. Ne multipliez jamais les temps de cuisson par le même facteur.",
62
+ },
63
+ ],
64
+ bibliographyTitle: "Bibliographie et Ressources Professionnelles",
65
+ bibliography: [
66
+ {
67
+ name: "Harold McGee - On Food and Cooking",
68
+ url: "https://en.wikipedia.org/wiki/Harold_McGee",
69
+ },
70
+ {
71
+ name: "The Flavor Bible - Page & Dornenburg",
72
+ url: "https://www.flavorprints.com/",
73
+ },
74
+ {
75
+ name: "Modernist Cuisine - Techniques de mise à l'échelle",
76
+ url: "https://www.modernistcuisine.com/",
77
+ },
78
+ ],
79
+ seo: [
80
+ {
81
+ type: "title",
82
+ text: "Guide Maître pour la Mise à l'Échelle et la Conversion de Recettes",
83
+ level: 2,
84
+ },
85
+ {
86
+ type: "paragraph",
87
+ html: "Le <strong>calcul des proportions</strong> est l'une des tâches les plus critiques en cuisine professionnelle. Il ne s'agit pas seulement de mathématiques, mais de comprendre comment la chimie des aliments réagit aux changements de volume.",
88
+ },
89
+ {
90
+ type: "stats",
91
+ columns: 4,
92
+ items: [
93
+ {
94
+ value: "x1.75",
95
+ label: "Facteur de 4 à 7 pers.",
96
+ icon: "mdi:calculator",
97
+ },
98
+ {
99
+ value: "75%",
100
+ label: "Ajustement Épices",
101
+ icon: "mdi:shaker-outline",
102
+ },
103
+ {
104
+ value: "0%",
105
+ label: "Échelle de Temps",
106
+ icon: "mdi:timer-off",
107
+ },
108
+ {
109
+ value: "100g",
110
+ label: "Base de Référence",
111
+ icon: "mdi:weight-gram",
112
+ },
113
+ ],
114
+ },
115
+ {
116
+ type: "title",
117
+ text: "Différences entre Échelle Linéaire et Contextuelle",
118
+ level: 3,
119
+ },
120
+ {
121
+ type: "comparative",
122
+ columns: 2,
123
+ items: [
124
+ {
125
+ title: "Mise à l'Échelle Linéaire",
126
+ icon: "mdi:math-log",
127
+ description: "Multiplication directe de toutes les valeurs par le facteur de conversion calculé.",
128
+ points: [
129
+ "Parfait pour la farine et les liquides de base",
130
+ "Idéal pour la pâtisserie froide de précision",
131
+ "Fonctionne bien pour les petits changements (x2, x0.5)",
132
+ "Facile à calculer automatiquement",
133
+ ],
134
+ },
135
+ {
136
+ title: "Mise à l'Échelle Contextuelle",
137
+ icon: "mdi:chef-hat",
138
+ description: "Ajustement des proportions basé sur la saturation des saveurs et l'évaporation.",
139
+ highlight: true,
140
+ points: [
141
+ "Évite l'excès de sel et d'épices",
142
+ "Prend en compte la surface d'évaporation",
143
+ "Ajuste la levure selon la chaleur de friction",
144
+ "Demande un jugement culinaire professionnel",
145
+ ],
146
+ },
147
+ ],
148
+ },
149
+ {
150
+ type: "title",
151
+ text: "Facteurs de Conversion Courants par Portion",
152
+ level: 3,
153
+ },
154
+ {
155
+ type: "table",
156
+ headers: ["Portions Origine", "Portions Cibles", "Facteur (FC)", "Difficulté"],
157
+ rows: [
158
+ ["2 personnes", "4 personnes", "x 2.0", "Faible (Linéaire)"],
159
+ ["4 personnes", "6 personnes", "x 1.5", "Faible (Linéaire)"],
160
+ ["4 personnes", "10 personnes", "x 2.5", "Moyenne (Vigilance épices)"],
161
+ ["4 personnes", "25 personnes", "x 6.25", "Haute (Contextuelle)"],
162
+ ],
163
+ },
164
+ {
165
+ type: "diagnostic",
166
+ variant: "warning",
167
+ title: "Erreurs Classiques en Doublant une Recette",
168
+ html: "Si vous remarquez que le goût est trop intense après conversion—surtout pour le piment ou le sel—réduisez l'échelle de ces ingrédients spécifiques à 75%. Ne doublez jamais le temps de cuisson ; la température interne dépend de l'épaisseur de la pièce.",
169
+ },
170
+ {
171
+ type: "title",
172
+ text: "Glossaire de la Standardisation Culinaire",
173
+ level: 3,
174
+ },
175
+ {
176
+ type: "glossary",
177
+ items: [
178
+ {
179
+ term: "Facteur de Conversion",
180
+ definition: "Chiffre multiplicateur : Quantité Souhaitée / Quantité Originale.",
181
+ },
182
+ {
183
+ term: "Ratio de Perte",
184
+ definition: "Perte de poids liée à l'évaporation ou au gras. Ne suit pas une courbe linéaire.",
185
+ },
186
+ {
187
+ term: "Poids Brut vs Net",
188
+ definition: "Poids avant et après parage. On convertit toujours sur la base du poids net de la recette.",
189
+ },
190
+ {
191
+ term: "Rendement de Production",
192
+ definition: "Quantité totale de produit fini après mise à l'échelle et cuisson.",
193
+ },
194
+ ],
195
+ },
196
+ {
197
+ type: "tip",
198
+ title: "L'Astuce de l'Œuf Battu",
199
+ html: "Si le calculateur demande 2,3 œufs, battez un œuf entier, pesez-le et n'utilisez que 30% du poids. C'est la seule façon de rester précis en pâtisserie fine.",
200
+ },
201
+ {
202
+ type: "paragraph",
203
+ html: "Notre outil simplifie les calculs pour vous permettre de vous concentrer sur l'essentiel : la créativité et le goût.",
204
+ },
205
+ ],
206
+ schemas: [],
207
+ };
@@ -0,0 +1,24 @@
1
+ import type { CookingToolEntry, ToolDefinition } from '../../types';
2
+ import IngredientRescalerComponent from './component.astro';
3
+ import IngredientRescalerSEO from './seo.astro';
4
+ import IngredientRescalerBibliography from './bibliography.astro';
5
+
6
+ export const ingredientRescaler: CookingToolEntry = {
7
+ id: 'ingredient-rescaler',
8
+ icons: {
9
+ bg: 'mdi:pot-steam',
10
+ fg: 'mdi:chef-hat',
11
+ },
12
+ i18n: {
13
+ es: () => import('./i18n/es').then((m) => m.content),
14
+ en: () => import('./i18n/en').then((m) => m.content),
15
+ fr: () => import('./i18n/fr').then((m) => m.content),
16
+ },
17
+ };
18
+
19
+ export const INGREDIENT_RESCALER_TOOL: ToolDefinition = {
20
+ entry: ingredientRescaler,
21
+ Component: IngredientRescalerComponent,
22
+ SEOComponent: IngredientRescalerSEO,
23
+ BibliographyComponent: IngredientRescalerBibliography,
24
+ };
@@ -0,0 +1,200 @@
1
+ type ParsedLine = {
2
+ original: string;
3
+ amount: number | null;
4
+ unit: string | null;
5
+ ingredient: string;
6
+ prefix: string;
7
+ };
8
+
9
+ interface IngredientRescalerUI {
10
+ copy: string;
11
+ copied: string;
12
+ emptyState: string;
13
+ }
14
+
15
+ const NUMBER_REGEX = /(\d+[\.,]\d+|\d+\/\d+|\d+)/;
16
+
17
+ function parseLine(line: string): ParsedLine {
18
+ const cleanLine = line.trim();
19
+ if (!cleanLine) return { original: line, amount: null, unit: null, ingredient: '', prefix: '' };
20
+
21
+ const match = cleanLine.match(NUMBER_REGEX);
22
+
23
+ if (!match) {
24
+ return { original: line, amount: null, unit: null, ingredient: cleanLine, prefix: '' };
25
+ }
26
+
27
+ const numberStr = match[0];
28
+ const index = match.index || 0;
29
+
30
+ const prefix = cleanLine.substring(0, index);
31
+ const rest = cleanLine.substring(index + numberStr.length);
32
+
33
+ let amount = 0;
34
+ if (numberStr.includes('/')) {
35
+ const [num, den] = numberStr.split('/');
36
+ if (parseFloat(den) !== 0) {
37
+ amount = parseFloat(num) / parseFloat(den);
38
+ } else {
39
+ amount = parseFloat(num);
40
+ }
41
+ } else {
42
+ amount = parseFloat(numberStr.replace(',', '.'));
43
+ }
44
+
45
+ return {
46
+ original: line,
47
+ amount,
48
+ unit: null,
49
+ ingredient: rest,
50
+ prefix,
51
+ };
52
+ }
53
+
54
+ function formatAmount(amount: number): string {
55
+ if (Number.isInteger(amount)) return amount.toString();
56
+ const rounded = Math.round(amount * 100) / 100;
57
+ return rounded.toString().replace('.', ',');
58
+ }
59
+
60
+ function renderEmptyState(resultsContainer: HTMLDivElement, emptyStateText: string): void {
61
+ resultsContainer.innerHTML = `<div class="ir-empty-state"><span class="ir-empty-icon"></span><p>${emptyStateText}</p></div>`;
62
+ }
63
+
64
+ function renderResult(resultsContainer: HTMLDivElement, lines: string[], ratio: number): void {
65
+ lines.forEach((line) => {
66
+ const parsed = parseLine(line);
67
+
68
+ if (parsed.amount !== null) {
69
+ const newAmount = parsed.amount * ratio;
70
+ const formatted = formatAmount(newAmount);
71
+
72
+ const row = document.createElement('div');
73
+ row.className = 'ir-result-row';
74
+ row.innerHTML = `
75
+ <span class="ir-original-amount">${parsed.original}</span>
76
+ <span class="ir-new-amount">
77
+ ${parsed.prefix}<span class="ir-new-value">${formatted}</span>${parsed.ingredient}
78
+ </span>
79
+ `;
80
+ resultsContainer.appendChild(row);
81
+ } else if (line.trim()) {
82
+ const row = document.createElement('div');
83
+ row.className = 'ir-no-amount-row';
84
+ row.textContent = line;
85
+ resultsContainer.appendChild(row);
86
+ }
87
+ });
88
+ }
89
+
90
+ interface RescalerInputs {
91
+ originalInput: HTMLInputElement;
92
+ targetInput: HTMLInputElement;
93
+ ingredientsInput: HTMLTextAreaElement;
94
+ }
95
+
96
+ function getCopyLines(inputs: RescalerInputs): string[] {
97
+ const lines: string[] = [];
98
+ const original = parseFloat(inputs.originalInput.value) || 1;
99
+ const target = parseFloat(inputs.targetInput.value) || 1;
100
+ const ratio = original > 0 ? target / original : 1;
101
+
102
+ inputs.ingredientsInput.value.split('\n').forEach((line) => {
103
+ const parsed = parseLine(line);
104
+ if (parsed.amount !== null) {
105
+ const newAmount = parsed.amount * ratio;
106
+ lines.push(`${parsed.prefix}${formatAmount(newAmount)}${parsed.ingredient}`);
107
+ } else {
108
+ lines.push(line);
109
+ }
110
+ });
111
+
112
+ return lines;
113
+ }
114
+
115
+ function updateButtonState(copyBtn: HTMLButtonElement, copied: boolean, copiedText: string): void {
116
+ if (copied) {
117
+ copyBtn.textContent = copiedText;
118
+ copyBtn.classList.add('ir-copied');
119
+ copyBtn.classList.remove('ir-default');
120
+ } else {
121
+ copyBtn.classList.remove('ir-copied');
122
+ copyBtn.classList.add('ir-default');
123
+ }
124
+ }
125
+
126
+ function setupCopyButton(copyBtn: HTMLButtonElement, inputs: RescalerInputs, copiedText: string): void {
127
+ copyBtn.addEventListener('click', () => {
128
+ const lines = getCopyLines(inputs);
129
+ const originalText = copyBtn.innerHTML;
130
+
131
+ navigator.clipboard.writeText(lines.join('\n')).then(() => {
132
+ updateButtonState(copyBtn, true, copiedText);
133
+ setTimeout(() => {
134
+ copyBtn.innerHTML = originalText;
135
+ updateButtonState(copyBtn, false, copiedText);
136
+ }, 2000);
137
+ });
138
+ });
139
+ }
140
+
141
+ interface UpdateContext {
142
+ originalInput: HTMLInputElement;
143
+ targetInput: HTMLInputElement;
144
+ ingredientsInput: HTMLTextAreaElement;
145
+ multiplierDisplay: HTMLSpanElement;
146
+ resultsContainer: HTMLDivElement;
147
+ emptyStateText: string;
148
+ }
149
+
150
+ function createUpdateFunction(ctx: UpdateContext): () => void {
151
+ return () => {
152
+ const original = parseFloat(ctx.originalInput.value) || 1;
153
+ const target = parseFloat(ctx.targetInput.value) || 1;
154
+ const ratio = original > 0 ? target / original : 1;
155
+
156
+ ctx.multiplierDisplay.textContent = `${ratio.toFixed(2).replace('.', ',')}x`;
157
+ ctx.resultsContainer.innerHTML = '';
158
+
159
+ const text = ctx.ingredientsInput.value;
160
+ if (!text.trim()) {
161
+ renderEmptyState(ctx.resultsContainer, ctx.emptyStateText);
162
+ return;
163
+ }
164
+
165
+ renderResult(ctx.resultsContainer, text.split('\n'), ratio);
166
+ };
167
+ }
168
+
169
+ export function initIngredientRescaler(ui: IngredientRescalerUI & Record<string, string>): void {
170
+ const originalInput = document.getElementById('original-servings') as HTMLInputElement | null;
171
+ const targetInput = document.getElementById('target-servings') as HTMLInputElement | null;
172
+ const ingredientsInput = document.getElementById('ingredients-input') as HTMLTextAreaElement | null;
173
+ const multiplierDisplay = document.getElementById('multiplier-display') as HTMLSpanElement | null;
174
+ const resultsContainer = document.getElementById('results-container') as HTMLDivElement | null;
175
+ const copyBtn = document.getElementById('copy-btn') as HTMLButtonElement | null;
176
+
177
+ if (!originalInput || !targetInput || !ingredientsInput || !multiplierDisplay || !resultsContainer || !copyBtn) {
178
+ return;
179
+ }
180
+
181
+ const update = createUpdateFunction({
182
+ originalInput,
183
+ targetInput,
184
+ ingredientsInput,
185
+ multiplierDisplay,
186
+ resultsContainer,
187
+ emptyStateText: ui.emptyState,
188
+ });
189
+
190
+ originalInput.addEventListener('input', update);
191
+ targetInput.addEventListener('input', update);
192
+ ingredientsInput.addEventListener('input', update);
193
+
194
+ if (!ingredientsInput.value.trim()) {
195
+ ingredientsInput.value = `${ui.defaultIngredient1}\n${ui.defaultIngredient2}\n${ui.defaultIngredient3}`;
196
+ }
197
+
198
+ update();
199
+ setupCopyButton(copyBtn, { originalInput, targetInput, ingredientsInput }, ui.copied);
200
+ }