@jjlmoya/utils-nature 1.20.0 → 1.22.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 (139) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +3 -2
  3. package/src/entries.ts +5 -1
  4. package/src/index.ts +4 -0
  5. package/src/pages/[locale]/[slug].astro +5 -4
  6. package/src/tests/diacritics_density.test.ts +1 -1
  7. package/src/tests/inverted_punctuation.test.ts +1 -1
  8. package/src/tests/script_density.test.ts +1 -1
  9. package/src/tests/seo_length.test.ts +1 -0
  10. package/src/tests/seo_translation_completeness.test.ts +69 -0
  11. package/src/tests/title_quality.test.ts +1 -1
  12. package/src/tests/tool_validation.test.ts +2 -3
  13. package/src/tests/translation_copy.test.ts +2 -2
  14. package/src/tool/compostBinVolumeRatioCalculator/bibliography.astro +18 -0
  15. package/src/tool/compostBinVolumeRatioCalculator/bibliography.ts +12 -0
  16. package/src/tool/compostBinVolumeRatioCalculator/component.astro +77 -0
  17. package/src/tool/compostBinVolumeRatioCalculator/compost-bin-volume-ratio-calculator.css +509 -0
  18. package/src/tool/compostBinVolumeRatioCalculator/controller.ts +189 -0
  19. package/src/tool/compostBinVolumeRatioCalculator/dom-views.ts +162 -0
  20. package/src/tool/compostBinVolumeRatioCalculator/entry.ts +28 -0
  21. package/src/tool/compostBinVolumeRatioCalculator/evaluator.ts +26 -0
  22. package/src/tool/compostBinVolumeRatioCalculator/i18n/de.ts +193 -0
  23. package/src/tool/compostBinVolumeRatioCalculator/i18n/en.ts +193 -0
  24. package/src/tool/compostBinVolumeRatioCalculator/i18n/es.ts +193 -0
  25. package/src/tool/compostBinVolumeRatioCalculator/i18n/fr.ts +193 -0
  26. package/src/tool/compostBinVolumeRatioCalculator/i18n/id.ts +185 -0
  27. package/src/tool/compostBinVolumeRatioCalculator/i18n/it.ts +193 -0
  28. package/src/tool/compostBinVolumeRatioCalculator/i18n/ja.ts +190 -0
  29. package/src/tool/compostBinVolumeRatioCalculator/i18n/ko.ts +190 -0
  30. package/src/tool/compostBinVolumeRatioCalculator/i18n/nl.ts +190 -0
  31. package/src/tool/compostBinVolumeRatioCalculator/i18n/pl.ts +190 -0
  32. package/src/tool/compostBinVolumeRatioCalculator/i18n/pt.ts +190 -0
  33. package/src/tool/compostBinVolumeRatioCalculator/i18n/ru.ts +190 -0
  34. package/src/tool/compostBinVolumeRatioCalculator/i18n/sv.ts +190 -0
  35. package/src/tool/compostBinVolumeRatioCalculator/i18n/tr.ts +190 -0
  36. package/src/tool/compostBinVolumeRatioCalculator/i18n/zh.ts +190 -0
  37. package/src/tool/compostBinVolumeRatioCalculator/index.ts +11 -0
  38. package/src/tool/compostBinVolumeRatioCalculator/logic.test.ts +55 -0
  39. package/src/tool/compostBinVolumeRatioCalculator/logic.ts +165 -0
  40. package/src/tool/compostBinVolumeRatioCalculator/seo.astro +23 -0
  41. package/src/tool/compostBinVolumeRatioCalculator/storage.ts +25 -0
  42. package/src/tool/compostBinVolumeRatioCalculator/ui.ts +54 -0
  43. package/src/tool/cricketThermometer/component.astro +1 -1
  44. package/src/tool/cricketThermometer/i18n/de.ts +2 -2
  45. package/src/tool/cricketThermometer/i18n/en.ts +2 -2
  46. package/src/tool/cricketThermometer/i18n/es.ts +2 -2
  47. package/src/tool/cricketThermometer/i18n/fr.ts +2 -2
  48. package/src/tool/cricketThermometer/i18n/id.ts +2 -2
  49. package/src/tool/cricketThermometer/i18n/it.ts +2 -2
  50. package/src/tool/cricketThermometer/i18n/ja.ts +2 -2
  51. package/src/tool/cricketThermometer/i18n/ko.ts +2 -2
  52. package/src/tool/cricketThermometer/i18n/nl.ts +2 -2
  53. package/src/tool/cricketThermometer/i18n/pl.ts +2 -2
  54. package/src/tool/cricketThermometer/i18n/pt.ts +2 -2
  55. package/src/tool/cricketThermometer/i18n/ru.ts +2 -2
  56. package/src/tool/cricketThermometer/i18n/sv.ts +2 -2
  57. package/src/tool/cricketThermometer/i18n/tr.ts +2 -2
  58. package/src/tool/cricketThermometer/i18n/zh.ts +2 -2
  59. package/src/tool/cricketThermometer/index.ts +1 -0
  60. package/src/tool/digitalCarbon/component.astro +3 -3
  61. package/src/tool/digitalCarbon/i18n/de.ts +2 -2
  62. package/src/tool/digitalCarbon/i18n/en.ts +2 -2
  63. package/src/tool/digitalCarbon/i18n/es.ts +2 -2
  64. package/src/tool/digitalCarbon/i18n/fr.ts +2 -2
  65. package/src/tool/digitalCarbon/i18n/id.ts +2 -2
  66. package/src/tool/digitalCarbon/i18n/it.ts +2 -2
  67. package/src/tool/digitalCarbon/i18n/ja.ts +2 -2
  68. package/src/tool/digitalCarbon/i18n/ko.ts +2 -2
  69. package/src/tool/digitalCarbon/i18n/nl.ts +2 -2
  70. package/src/tool/digitalCarbon/i18n/pl.ts +2 -2
  71. package/src/tool/digitalCarbon/i18n/pt.ts +2 -2
  72. package/src/tool/digitalCarbon/i18n/ru.ts +2 -2
  73. package/src/tool/digitalCarbon/i18n/sv.ts +2 -2
  74. package/src/tool/digitalCarbon/i18n/tr.ts +2 -2
  75. package/src/tool/digitalCarbon/i18n/zh.ts +2 -2
  76. package/src/tool/digitalCarbon/index.ts +1 -0
  77. package/src/tool/rainHarvester/i18n/de.ts +2 -2
  78. package/src/tool/rainHarvester/i18n/en.ts +2 -2
  79. package/src/tool/rainHarvester/i18n/es.ts +2 -2
  80. package/src/tool/rainHarvester/i18n/fr.ts +2 -2
  81. package/src/tool/rainHarvester/i18n/id.ts +2 -2
  82. package/src/tool/rainHarvester/i18n/it.ts +2 -2
  83. package/src/tool/rainHarvester/i18n/ja.ts +2 -2
  84. package/src/tool/rainHarvester/i18n/ko.ts +2 -2
  85. package/src/tool/rainHarvester/i18n/nl.ts +2 -2
  86. package/src/tool/rainHarvester/i18n/pl.ts +2 -2
  87. package/src/tool/rainHarvester/i18n/pt.ts +2 -2
  88. package/src/tool/rainHarvester/i18n/ru.ts +2 -2
  89. package/src/tool/rainHarvester/i18n/sv.ts +2 -2
  90. package/src/tool/rainHarvester/i18n/tr.ts +2 -2
  91. package/src/tool/rainHarvester/i18n/zh.ts +2 -2
  92. package/src/tool/rainHarvester/index.ts +1 -0
  93. package/src/tool/seedCalculator/i18n/de.ts +2 -2
  94. package/src/tool/seedCalculator/i18n/en.ts +2 -2
  95. package/src/tool/seedCalculator/i18n/es.ts +2 -2
  96. package/src/tool/seedCalculator/i18n/fr.ts +2 -2
  97. package/src/tool/seedCalculator/i18n/id.ts +2 -2
  98. package/src/tool/seedCalculator/i18n/it.ts +2 -2
  99. package/src/tool/seedCalculator/i18n/ja.ts +2 -2
  100. package/src/tool/seedCalculator/i18n/ko.ts +2 -2
  101. package/src/tool/seedCalculator/i18n/nl.ts +2 -2
  102. package/src/tool/seedCalculator/i18n/pl.ts +2 -2
  103. package/src/tool/seedCalculator/i18n/pt.ts +2 -2
  104. package/src/tool/seedCalculator/i18n/ru.ts +2 -2
  105. package/src/tool/seedCalculator/i18n/sv.ts +2 -2
  106. package/src/tool/seedCalculator/i18n/tr.ts +2 -2
  107. package/src/tool/seedCalculator/i18n/zh.ts +2 -2
  108. package/src/tool/seedCalculator/index.ts +1 -0
  109. package/src/tool/urbanGardenPlanner/bibliography.astro +9 -0
  110. package/src/tool/urbanGardenPlanner/bibliography.ts +16 -0
  111. package/src/tool/urbanGardenPlanner/component.astro +131 -0
  112. package/src/tool/urbanGardenPlanner/controller.ts +88 -0
  113. package/src/tool/urbanGardenPlanner/dom-views.ts +84 -0
  114. package/src/tool/urbanGardenPlanner/entry.ts +29 -0
  115. package/src/tool/urbanGardenPlanner/evaluator.ts +27 -0
  116. package/src/tool/urbanGardenPlanner/i18n/de.ts +50 -0
  117. package/src/tool/urbanGardenPlanner/i18n/en.ts +179 -0
  118. package/src/tool/urbanGardenPlanner/i18n/es.ts +50 -0
  119. package/src/tool/urbanGardenPlanner/i18n/fr.ts +50 -0
  120. package/src/tool/urbanGardenPlanner/i18n/id.ts +50 -0
  121. package/src/tool/urbanGardenPlanner/i18n/it.ts +45 -0
  122. package/src/tool/urbanGardenPlanner/i18n/ja.ts +39 -0
  123. package/src/tool/urbanGardenPlanner/i18n/ko.ts +39 -0
  124. package/src/tool/urbanGardenPlanner/i18n/nl.ts +39 -0
  125. package/src/tool/urbanGardenPlanner/i18n/pl.ts +39 -0
  126. package/src/tool/urbanGardenPlanner/i18n/pt.ts +39 -0
  127. package/src/tool/urbanGardenPlanner/i18n/ru.ts +39 -0
  128. package/src/tool/urbanGardenPlanner/i18n/sv.ts +39 -0
  129. package/src/tool/urbanGardenPlanner/i18n/tr.ts +104 -0
  130. package/src/tool/urbanGardenPlanner/i18n/zh.ts +104 -0
  131. package/src/tool/urbanGardenPlanner/index.ts +11 -0
  132. package/src/tool/urbanGardenPlanner/logic.test.ts +45 -0
  133. package/src/tool/urbanGardenPlanner/logic.ts +55 -0
  134. package/src/tool/urbanGardenPlanner/seo.astro +15 -0
  135. package/src/tool/urbanGardenPlanner/storage.ts +25 -0
  136. package/src/tool/urbanGardenPlanner/ui.ts +38 -0
  137. package/src/tool/urbanGardenPlanner/urban-garden-rainwater-and-soil-planner.css +531 -0
  138. package/src/tools.ts +5 -2
  139. package/src/types.ts +4 -4
@@ -0,0 +1,190 @@
1
+ import type {
2
+ FAQPage,
3
+ HowTo,
4
+ SoftwareApplication,
5
+ WithContext,
6
+ } from "schema-dts";
7
+ import { bibliography } from "../bibliography";
8
+ import type { CompostBinVolumeRatioCalculatorLocaleContent } from "../entry";
9
+
10
+ const slug = "compost-bin-volume-and-browns-greens-ratio-calculator";
11
+ const title = "堆肥桶容积与棕绿物料比例计算器";
12
+ const description =
13
+ "计算矩形或圆柱形堆肥桶的内部容积,并检查棕色物料(碳源)、绿色物料(氮源)、湿度和通气性的合理平衡。";
14
+
15
+ const faq = [
16
+ {
17
+ question: "如何计算堆肥桶的容积?",
18
+ answer:
19
+ "测量桶的内部尺寸。对于矩形桶,将内宽、内深和内高相乘并将立方厘米转换为升;对于圆柱形桶,使用圆周率乘以半径的平方再乘以内高,同样换算为升或加仑。",
20
+ },
21
+ {
22
+ question: "堆肥中棕色物料与绿色物料的合适比例是多少?",
23
+ answer:
24
+ "实用建议是按体积计算,每1份富含氮的绿色物料搭配2至3份富含碳的棕色物料。根据实际湿度、气味和通气情况进行适当动态调整。",
25
+ },
26
+ {
27
+ question: "堆肥桶中可以放入多少堆肥材料?",
28
+ answer:
29
+ "将物料的总体积与堆肥桶的内部有效容积进行比较。顶部请务必保留一定空间以利于空气流通和后续翻堆操作,切勿过度压实物料。",
30
+ },
31
+ {
32
+ question: "拧干的湿海绵状态是指什么?",
33
+ answer:
34
+ "物料触感湿润,但用手用力握紧时不会滴水。堆肥过干会减缓微生物分解速度,过湿则会导致缺氧产生难闻异味,需要补充干棕色物料并翻堆。",
35
+ },
36
+ ];
37
+
38
+ const howTo = [
39
+ {
40
+ name: "选择堆肥桶形状",
41
+ text: "选择矩形或圆柱形,并以厘米或英寸为单位准确输入内部测量尺寸。",
42
+ },
43
+ {
44
+ name: "输入物料体积数据",
45
+ text: "分别估算富含碳的棕色物料与富含氮的绿色物料的实际体积。",
46
+ },
47
+ {
48
+ name: "设置堆肥湿度状态",
49
+ text: "选择干燥、拧干的海绵或过湿,以获取适合您堆肥桶的针对性维护建议。",
50
+ },
51
+ {
52
+ name: "查看评估结果与行动指南",
53
+ text: "利用总容量、填充率和物料比例,决定是否补充棕色物料、补水或进行翻堆。",
54
+ },
55
+ ];
56
+
57
+ const faqSchema: WithContext<FAQPage> = {
58
+ "@context": "https://schema.org",
59
+ "@type": "FAQPage",
60
+ mainEntity: faq.map((item) => ({
61
+ "@type": "Question",
62
+ name: item.question,
63
+ acceptedAnswer: { "@type": "Answer", text: item.answer },
64
+ })),
65
+ };
66
+
67
+ const howToSchema: WithContext<HowTo> = {
68
+ "@context": "https://schema.org",
69
+ "@type": "HowTo",
70
+ name: title,
71
+ description,
72
+ step: howTo.map((step, index) => ({
73
+ "@type": "HowToStep",
74
+ position: index + 1,
75
+ name: step.name,
76
+ text: step.text,
77
+ })),
78
+ };
79
+
80
+ const appSchema: WithContext<SoftwareApplication> = {
81
+ "@context": "https://schema.org",
82
+ "@type": "SoftwareApplication",
83
+ name: title,
84
+ description,
85
+ applicationCategory: "UtilityApplication",
86
+ operatingSystem: "All",
87
+ offers: { "@type": "Offer", price: "0", priceCurrency: "EUR" },
88
+ inLanguage: "zh",
89
+ };
90
+
91
+ export const content: CompostBinVolumeRatioCalculatorLocaleContent = {
92
+ slug,
93
+ title,
94
+ description,
95
+ ui: {
96
+ unitMetric: "公制",
97
+ unitImperial: "英制",
98
+ shapeTitle: "堆肥桶形状",
99
+ shapeRectangular: "矩形",
100
+ shapeCylindrical: "圆柱形",
101
+ widthLabel: "内宽",
102
+ depthLabel: "内深",
103
+ diameterLabel: "内径",
104
+ heightLabel: "内高",
105
+ brownLabel: "棕色物料",
106
+ greenLabel: "绿色物料",
107
+ moistureTitle: "湿度感官",
108
+ moistureDry: "干燥",
109
+ moistureSponge: "拧干的海绵",
110
+ moistureWet: "过湿",
111
+ moistureAdviceDry: "偏干:请适当补水并适当翻堆。",
112
+ moistureAdviceSponge: "良好:请保持拧干海绵般的湿度。",
113
+ moistureAdviceWet: "偏湿:请添加干燥棕色物料并翻堆通气。",
114
+ recipeTitle: "推荐物料组合",
115
+ recipeKitchen: "厨余垃圾",
116
+ recipeGarden: "园艺修剪物",
117
+ recipeLeafy: "落叶混合物",
118
+ sceneTitle: "堆肥剖面示意图",
119
+ resultCapacity: "堆肥桶内部容积",
120
+ resultMaterials: "已投入物料总体积",
121
+ resultRatio: "棕绿物料体积比",
122
+ resultFill: "已用容量比例",
123
+ brownTarget: "目标棕色物料量",
124
+ waterAction: "水分管理",
125
+ airAction: "空气与结构",
126
+ statusBalanced: "初始配比良好",
127
+ statusTooGreen: "建议添加更多棕色物料",
128
+ statusTooBrown: "建议添加更多绿色物料",
129
+ statusNoGreens: "请投入绿色物料",
130
+ statusOverfilled: "物料已超出堆肥桶容积",
131
+ statusDry: "堆肥偏干",
132
+ statusWet: "堆肥过湿",
133
+ statusGoodMoisture: "湿度处于理想范围",
134
+ statusNote: "请定期翻堆以增加内部孔隙与氧气。",
135
+ volumeUnitLiters: "升",
136
+ volumeUnitGallons: "加仑",
137
+ dimensionUnitCm: "厘米",
138
+ dimensionUnitIn: "英寸",
139
+ formulaTitle: "几何容积计算公式",
140
+ formulaText: "计算器根据内部尺寸确定实际有效容积。矩形桶使用 内宽 × 内深 × 内高;圆柱形桶使用 π × 半径² × 内高。物料体积统一换算后显示填充率。",
141
+ practicalTitle: "比例是实用起点",
142
+ practicalText: "目标是以绿色物料体积为基准,搭配2至3倍体积的棕色物料。保持如拧干海绵般的湿度并定期翻堆。",
143
+ localNote: "所有计算均在浏览器本地完成。家庭堆肥管理实用规划工具。",
144
+ resetLabel: "重置",
145
+ },
146
+ seo: [
147
+ {
148
+ type: "title",
149
+ text: "计算堆肥桶的真实有效容积",
150
+ level: 2,
151
+ },
152
+ {
153
+ type: "paragraph",
154
+ html: "堆肥桶容积计算器在基于桶内部的实际测量尺寸计算时最为精准,而不是仅仅依赖产品包装上的标称容积。请输入矩形堆肥桶的内宽、内深和内高,或者圆柱形堆肥桶的内径与内高,即可快速准确地计算出升或加仑单位的净有效容积,并将其与拟投入的堆肥物料总体积进行对比分析。",
155
+ },
156
+ { type: "title", text: "几何容积数学计算公式", level: 3 },
157
+ {
158
+ type: "paragraph",
159
+ html: "对于矩形堆肥桶,请使用几何公式 <code>内宽 × 内深 × 内高</code>。如果您使用厘米作为测量单位,将计算出的立方厘米结果除以 1,000 即可转换为升。对于圆柱形堆肥桶,请使用 <code>π × 半径² × 内高</code>,其中半径为内径的一半。测量时请仅测量桶内净空部分,不要算入桶壁厚度、底座或桶盖的高度。",
160
+ },
161
+ {
162
+ type: "list",
163
+ items: [
164
+ "<strong>矩形堆肥桶:</strong> 内宽 × 内深 × 内高",
165
+ "<strong>圆柱形堆肥桶:</strong> π × 半径² × 内高",
166
+ "<strong>单位换算关系:</strong> 1,000 立方厘米精准等于 1 升",
167
+ "<strong>容量填充率:</strong> 棕色与绿色物料总体积占桶净容积的百分比",
168
+ ],
169
+ },
170
+ { type: "title", text: "棕色物料与绿色物料的合理配比", level: 3 },
171
+ {
172
+ type: "paragraph",
173
+ html: "实践中推荐的经典混合比例为:按体积计算,每 1 份富含氮素的绿色物料(如厨余垃圾、新鲜草屑),搭配 2 至 3 份富含碳素的棕色物料(如干落叶、粉碎纸板、细树枝)。计算器会明确给出推荐的棕色物料目标范围,而适当的湿度与良好通气则是促进堆肥微生物高效发酵分解的关键因素。",
174
+ },
175
+ { type: "title", text: "堆肥湿度控制与定期翻堆通气", level: 3 },
176
+ {
177
+ type: "paragraph",
178
+ html: "请始终保持堆肥物料具有像充分拧干的海绵一样的湿润手感。堆肥过干会显著降低微生物的代谢分解活性,而堆肥过湿则会导致内部缺少氧气并可能产生难闻的厌氧发酵异味。通过定期翻堆搅拌,可以重新打通氧气通道并恢复良好的堆體结构。",
179
+ },
180
+ {
181
+ type: "tip",
182
+ title: "园艺爱好者实用建议",
183
+ html: "本计算工具得出的容积与比例结果作为家庭和花园堆肥管理的实操指南,并不能替代实验室专业的碳氮比(C:N)化学定量分析。",
184
+ },
185
+ ],
186
+ faq,
187
+ bibliography,
188
+ howTo,
189
+ schemas: [faqSchema, howToSchema, appSchema],
190
+ };
@@ -0,0 +1,11 @@
1
+ import type { ToolDefinition } from "../../types";
2
+ import { compostBinVolumeRatioCalculator } from "./entry";
3
+
4
+ export * from "./entry";
5
+
6
+ export const COMPOST_BIN_VOLUME_RATIO_CALCULATOR_TOOL: ToolDefinition = {
7
+ entry: compostBinVolumeRatioCalculator,
8
+ Component: () => import("./component.astro"),
9
+ SEOComponent: () => import("./seo.astro"),
10
+ BibliographyComponent: () => import("./bibliography.astro"),
11
+ };
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { applyPreset, calculateCompost, convertInputs, type CompostInputs } from './logic';
3
+
4
+ const base: CompostInputs = {
5
+ unitSystem: 'metric',
6
+ shape: 'rectangular',
7
+ width: 100,
8
+ depth: 50,
9
+ diameter: 60,
10
+ height: 80,
11
+ browns: 20,
12
+ greens: 10,
13
+ moisture: 'sponge',
14
+ };
15
+
16
+ describe('compost bin calculations', () => {
17
+ it('calculates a rectangular capacity and a balanced ratio', () => {
18
+ const result = calculateCompost(base);
19
+ expect(result.capacityLiters).toBe(400);
20
+ expect(result.materialsLiters).toBe(30);
21
+ expect(result.ratio).toBe(2);
22
+ expect(result.status).toBe('balanced');
23
+ });
24
+
25
+ it('calculates a cylindrical capacity', () => {
26
+ const result = calculateCompost({ ...base, shape: 'cylindrical', diameter: 100, height: 100 });
27
+ expect(result.capacityLiters).toBeCloseTo(785.398, 2);
28
+ });
29
+
30
+ it('identifies ratio and capacity warnings', () => {
31
+ expect(calculateCompost({ ...base, browns: 5 }).status).toBe('too-green');
32
+ expect(calculateCompost({ ...base, browns: 50 }).status).toBe('too-brown');
33
+ expect(calculateCompost({ ...base, greens: 0 }).status).toBe('no-greens');
34
+ expect(calculateCompost({ ...base, browns: 300, greens: 200 }).status).toBe('overfilled');
35
+ });
36
+
37
+ it('identifies moisture states', () => {
38
+ expect(calculateCompost({ ...base, moisture: 'dry' }).moistureStatus).toBe('dry');
39
+ expect(calculateCompost({ ...base, moisture: 'wet' }).moistureStatus).toBe('wet');
40
+ });
41
+
42
+ it('converts dimensions and volumes between systems', () => {
43
+ const imperial = convertInputs({ ...base, width: 25.4, depth: 25.4, height: 25.4, browns: 3.785411784, greens: 3.785411784 }, 'imperial');
44
+ expect(imperial.width).toBeCloseTo(10, 5);
45
+ expect(imperial.browns).toBeCloseTo(1, 5);
46
+ expect(convertInputs(imperial, 'metric').height).toBeCloseTo(25.4, 5);
47
+ });
48
+
49
+ it('returns independent preset states', () => {
50
+ const preset = applyPreset('leafy', 'imperial');
51
+ expect(preset.shape).toBe('cylindrical');
52
+ expect(preset.unitSystem).toBe('imperial');
53
+ expect(preset.moisture).toBe('dry');
54
+ });
55
+ });
@@ -0,0 +1,165 @@
1
+ export type UnitSystem = "metric" | "imperial";
2
+ export type BinShape = "rectangular" | "cylindrical";
3
+ export type MoistureLevel = "dry" | "sponge" | "wet";
4
+
5
+ export interface CompostInputs {
6
+ unitSystem: UnitSystem;
7
+ shape: BinShape;
8
+ width: number;
9
+ depth: number;
10
+ diameter: number;
11
+ height: number;
12
+ browns: number;
13
+ greens: number;
14
+ moisture: MoistureLevel;
15
+ }
16
+
17
+ export interface CompostResult {
18
+ capacityLiters: number;
19
+ materialsLiters: number;
20
+ ratio: number | null;
21
+ fillPercent: number;
22
+ targetBrownLowLiters: number;
23
+ targetBrownHighLiters: number;
24
+ status: "balanced" | "too-green" | "too-brown" | "no-greens" | "overfilled";
25
+ moistureStatus: "dry" | "good" | "wet";
26
+ }
27
+
28
+ const INCH_TO_CM = 2.54;
29
+ const GALLON_TO_LITERS = 3.785411784;
30
+ const PRESETS: Record<"kitchen" | "garden" | "leafy", CompostInputs> = {
31
+ kitchen: {
32
+ unitSystem: "metric",
33
+ shape: "rectangular",
34
+ width: 60,
35
+ depth: 45,
36
+ diameter: 55,
37
+ height: 70,
38
+ browns: 18,
39
+ greens: 8,
40
+ moisture: "sponge",
41
+ },
42
+ garden: {
43
+ unitSystem: "metric",
44
+ shape: "rectangular",
45
+ width: 100,
46
+ depth: 100,
47
+ diameter: 70,
48
+ height: 90,
49
+ browns: 90,
50
+ greens: 35,
51
+ moisture: "sponge",
52
+ },
53
+ leafy: {
54
+ unitSystem: "metric",
55
+ shape: "cylindrical",
56
+ width: 80,
57
+ depth: 80,
58
+ diameter: 120,
59
+ height: 100,
60
+ browns: 180,
61
+ greens: 45,
62
+ moisture: "dry",
63
+ },
64
+ };
65
+
66
+ function positive(value: number): number {
67
+ return Number.isFinite(value) && value > 0 ? value : 0;
68
+ }
69
+
70
+ function toMetric(value: number, unitSystem: UnitSystem): number {
71
+ return unitSystem === "imperial" ? value * INCH_TO_CM : value;
72
+ }
73
+
74
+ function toLiters(value: number, unitSystem: UnitSystem): number {
75
+ return unitSystem === "imperial" ? value * GALLON_TO_LITERS : value;
76
+ }
77
+
78
+ function fromMetric(value: number, unitSystem: UnitSystem): number {
79
+ return unitSystem === "imperial" ? value / INCH_TO_CM : value;
80
+ }
81
+
82
+ function fromLiters(value: number, unitSystem: UnitSystem): number {
83
+ return unitSystem === "imperial" ? value / GALLON_TO_LITERS : value;
84
+ }
85
+
86
+ function getStatus(
87
+ capacityLiters: number,
88
+ fillPercent: number,
89
+ ratio: number | null,
90
+ ): CompostResult["status"] {
91
+ if (capacityLiters > 0 && fillPercent > 100) return "overfilled";
92
+ if (ratio === null) return "no-greens";
93
+ if (ratio < 2) return "too-green";
94
+ if (ratio > 3) return "too-brown";
95
+ return "balanced";
96
+ }
97
+
98
+ function getMoistureStatus(
99
+ level: MoistureLevel,
100
+ ): CompostResult["moistureStatus"] {
101
+ if (level === "dry") return "dry";
102
+ if (level === "wet") return "wet";
103
+ return "good";
104
+ }
105
+
106
+ function capacityInLiters(inputs: CompostInputs): number {
107
+ const height = toMetric(positive(inputs.height), inputs.unitSystem);
108
+ if (inputs.shape === "cylindrical") {
109
+ const radius = toMetric(positive(inputs.diameter), inputs.unitSystem) / 2;
110
+ return (Math.PI * radius * radius * height) / 1000;
111
+ }
112
+ const width = toMetric(positive(inputs.width), inputs.unitSystem);
113
+ const depth = toMetric(positive(inputs.depth), inputs.unitSystem);
114
+ return (width * depth * height) / 1000;
115
+ }
116
+
117
+ export function calculateCompost(inputs: CompostInputs): CompostResult {
118
+ const capacityLiters = capacityInLiters(inputs);
119
+ const brownsLiters = toLiters(positive(inputs.browns), inputs.unitSystem);
120
+ const greensLiters = toLiters(positive(inputs.greens), inputs.unitSystem);
121
+ const materialsLiters = brownsLiters + greensLiters;
122
+ const ratio = greensLiters > 0 ? brownsLiters / greensLiters : null;
123
+ const fillPercent =
124
+ capacityLiters > 0 ? (materialsLiters / capacityLiters) * 100 : 0;
125
+ const status = getStatus(capacityLiters, fillPercent, ratio);
126
+ const moistureStatus = getMoistureStatus(inputs.moisture);
127
+ return {
128
+ capacityLiters,
129
+ materialsLiters,
130
+ ratio,
131
+ fillPercent,
132
+ targetBrownLowLiters: greensLiters * 2,
133
+ targetBrownHighLiters: greensLiters * 3,
134
+ status,
135
+ moistureStatus,
136
+ };
137
+ }
138
+
139
+ export function convertInputs(
140
+ inputs: CompostInputs,
141
+ unitSystem: UnitSystem,
142
+ ): CompostInputs {
143
+ if (inputs.unitSystem === unitSystem) return { ...inputs };
144
+ const convertDimension = (value: number): number =>
145
+ fromMetric(toMetric(value, inputs.unitSystem), unitSystem);
146
+ const convertVolume = (value: number): number =>
147
+ fromLiters(toLiters(value, inputs.unitSystem), unitSystem);
148
+ return {
149
+ ...inputs,
150
+ unitSystem,
151
+ width: convertDimension(inputs.width),
152
+ depth: convertDimension(inputs.depth),
153
+ diameter: convertDimension(inputs.diameter),
154
+ height: convertDimension(inputs.height),
155
+ browns: convertVolume(inputs.browns),
156
+ greens: convertVolume(inputs.greens),
157
+ };
158
+ }
159
+
160
+ export function applyPreset(
161
+ name: "kitchen" | "garden" | "leafy",
162
+ unitSystem: UnitSystem,
163
+ ): CompostInputs {
164
+ return convertInputs(PRESETS[name], unitSystem);
165
+ }
@@ -0,0 +1,23 @@
1
+ ---
2
+ import type { SEOSection } from '../../types';
3
+ import { compostBinVolumeRatioCalculator } from './entry';
4
+
5
+ interface Props {
6
+ locale?: string;
7
+ }
8
+
9
+ const { locale = 'en' } = Astro.props as Props;
10
+ const loader = compostBinVolumeRatioCalculator.i18n[locale as keyof typeof compostBinVolumeRatioCalculator.i18n] ?? compostBinVolumeRatioCalculator.i18n.en;
11
+ const content = loader ? await loader() : null;
12
+ const sections = (content?.seo ?? []) as SEOSection[];
13
+ ---
14
+
15
+ <div class="compost-seo" data-locale={locale}>
16
+ {sections.map((section) => {
17
+ if (section.type === 'title') return <h2>{section.text}</h2>;
18
+ if (section.type === 'paragraph') return <p set:html={section.html} />;
19
+ if (section.type === 'list') return <ul>{section.items.map((item) => <li set:html={item} />)}</ul>;
20
+ if (section.type === 'tip') return <aside><h3>{section.title}</h3><div set:html={section.html} /></aside>;
21
+ return null;
22
+ })}
23
+ </div>
@@ -0,0 +1,25 @@
1
+ import type { CompostInputs, UnitSystem } from "./logic";
2
+
3
+ const STORAGE_KEY = "jjlmoya-compost-bin-volume-ratio";
4
+
5
+ export function loadCompostState(): Partial<CompostInputs> | null {
6
+ try {
7
+ const raw = window.localStorage.getItem(STORAGE_KEY);
8
+ return raw ? (JSON.parse(raw) as Partial<CompostInputs>) : null;
9
+ } catch {
10
+ return null;
11
+ }
12
+ }
13
+
14
+ export function saveCompostState(state: CompostInputs): void {
15
+ try {
16
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
17
+ } catch {}
18
+ }
19
+
20
+ export function readStoredUnit(): UnitSystem | null {
21
+ const state = loadCompostState();
22
+ return state?.unitSystem === "metric" || state?.unitSystem === "imperial"
23
+ ? state.unitSystem
24
+ : null;
25
+ }
@@ -0,0 +1,54 @@
1
+ export interface CompostBinVolumeRatioCalculatorUI extends Record<
2
+ string,
3
+ string
4
+ > {
5
+ unitMetric: string;
6
+ unitImperial: string;
7
+ shapeTitle: string;
8
+ shapeRectangular: string;
9
+ shapeCylindrical: string;
10
+ widthLabel: string;
11
+ depthLabel: string;
12
+ diameterLabel: string;
13
+ heightLabel: string;
14
+ brownLabel: string;
15
+ greenLabel: string;
16
+ moistureTitle: string;
17
+ moistureDry: string;
18
+ moistureSponge: string;
19
+ moistureWet: string;
20
+ moistureAdviceDry: string;
21
+ moistureAdviceSponge: string;
22
+ moistureAdviceWet: string;
23
+ recipeTitle: string;
24
+ recipeKitchen: string;
25
+ recipeGarden: string;
26
+ recipeLeafy: string;
27
+ sceneTitle: string;
28
+ resultCapacity: string;
29
+ resultMaterials: string;
30
+ resultRatio: string;
31
+ resultFill: string;
32
+ brownTarget: string;
33
+ waterAction: string;
34
+ airAction: string;
35
+ statusBalanced: string;
36
+ statusTooGreen: string;
37
+ statusTooBrown: string;
38
+ statusNoGreens: string;
39
+ statusOverfilled: string;
40
+ statusDry: string;
41
+ statusWet: string;
42
+ statusGoodMoisture: string;
43
+ statusNote: string;
44
+ volumeUnitLiters: string;
45
+ volumeUnitGallons: string;
46
+ dimensionUnitCm: string;
47
+ dimensionUnitIn: string;
48
+ formulaTitle: string;
49
+ formulaText: string;
50
+ practicalTitle: string;
51
+ practicalText: string;
52
+ localNote: string;
53
+ resetLabel: string;
54
+ }
@@ -167,7 +167,7 @@ const { ui } = Astro.props;
167
167
 
168
168
  let totalInterval = 0;
169
169
  for (let i = 1; i < this.taps.length; i++) {
170
- totalInterval += this.taps[i] - this.taps[i - 1];
170
+ totalInterval += (this.taps[i] ?? 0) - (this.taps[i - 1] ?? 0);
171
171
  }
172
172
  const avgInterval = totalInterval / (this.taps.length - 1);
173
173
  const bpm = 60000 / avgInterval;
@@ -1,4 +1,4 @@
1
- import type { WithContext, FAQPage, HowToThing, SoftwareApplication } from 'schema-dts';
1
+ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
2
  import { bibliography } from '../bibliography';
3
3
  import type { ToolLocaleContent } from '../../../types';
4
4
  import type { CricketThermometerUI } from '../ui';
@@ -60,7 +60,7 @@ const faqSchema: WithContext<FAQPage> = {
60
60
  })),
61
61
  };
62
62
 
63
- const howToSchema: WithContext<HowToThing> = {
63
+ const howToSchema: WithContext<HowTo> = {
64
64
  '@context': 'https://schema.org',
65
65
  '@type': 'HowTo',
66
66
  name: title,
@@ -1,4 +1,4 @@
1
- import type { WithContext, FAQPage, HowToThing, SoftwareApplication } from 'schema-dts';
1
+ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
2
  import { bibliography } from '../bibliography';
3
3
  import type { ToolLocaleContent } from '../../../types';
4
4
  import type { CricketThermometerUI } from '../ui';
@@ -60,7 +60,7 @@ const faqSchema: WithContext<FAQPage> = {
60
60
  })),
61
61
  };
62
62
 
63
- const howToSchema: WithContext<HowToThing> = {
63
+ const howToSchema: WithContext<HowTo> = {
64
64
  '@context': 'https://schema.org',
65
65
  '@type': 'HowTo',
66
66
  name: title,
@@ -1,4 +1,4 @@
1
- import type { WithContext, FAQPage, HowToThing, SoftwareApplication } from 'schema-dts';
1
+ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
2
  import { bibliography } from '../bibliography';
3
3
  import type { ToolLocaleContent } from '../../../types';
4
4
  import type { CricketThermometerUI } from '../ui';
@@ -60,7 +60,7 @@ const faqSchema: WithContext<FAQPage> = {
60
60
  })),
61
61
  };
62
62
 
63
- const howToSchema: WithContext<HowToThing> = {
63
+ const howToSchema: WithContext<HowTo> = {
64
64
  '@context': 'https://schema.org',
65
65
  '@type': 'HowTo',
66
66
  name: title,
@@ -1,4 +1,4 @@
1
- import type { WithContext, FAQPage, HowToThing, SoftwareApplication } from 'schema-dts';
1
+ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
2
  import { bibliography } from '../bibliography';
3
3
  import type { ToolLocaleContent } from '../../../types';
4
4
  import type { CricketThermometerUI } from '../ui';
@@ -60,7 +60,7 @@ const faqSchema: WithContext<FAQPage> = {
60
60
  })),
61
61
  };
62
62
 
63
- const howToSchema: WithContext<HowToThing> = {
63
+ const howToSchema: WithContext<HowTo> = {
64
64
  '@context': 'https://schema.org',
65
65
  '@type': 'HowTo',
66
66
  name: title,
@@ -1,4 +1,4 @@
1
- import type { WithContext, FAQPage, HowToThing, SoftwareApplication } from 'schema-dts';
1
+ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
2
  import { bibliography } from '../bibliography';
3
3
  import type { ToolLocaleContent } from '../../../types';
4
4
  import type { CricketThermometerUI } from '../ui';
@@ -60,7 +60,7 @@ const faqSchema: WithContext<FAQPage> = {
60
60
  })),
61
61
  };
62
62
 
63
- const howToSchema: WithContext<HowToThing> = {
63
+ const howToSchema: WithContext<HowTo> = {
64
64
  '@context': 'https://schema.org',
65
65
  '@type': 'HowTo',
66
66
  name: title,
@@ -1,4 +1,4 @@
1
- import type { WithContext, FAQPage, HowToThing, SoftwareApplication } from 'schema-dts';
1
+ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
2
  import { bibliography } from '../bibliography';
3
3
  import type { ToolLocaleContent } from '../../../types';
4
4
  import type { CricketThermometerUI } from '../ui';
@@ -60,7 +60,7 @@ const faqSchema: WithContext<FAQPage> = {
60
60
  })),
61
61
  };
62
62
 
63
- const howToSchema: WithContext<HowToThing> = {
63
+ const howToSchema: WithContext<HowTo> = {
64
64
  '@context': 'https://schema.org',
65
65
  '@type': 'HowTo',
66
66
  name: title,
@@ -1,4 +1,4 @@
1
- import type { WithContext, FAQPage, HowToThing, SoftwareApplication } from 'schema-dts';
1
+ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
2
  import { bibliography } from '../bibliography';
3
3
  import type { ToolLocaleContent } from '../../../types';
4
4
  import type { CricketThermometerUI } from '../ui';
@@ -60,7 +60,7 @@ const faqSchema: WithContext<FAQPage> = {
60
60
  })),
61
61
  };
62
62
 
63
- const howToSchema: WithContext<HowToThing> = {
63
+ const howToSchema: WithContext<HowTo> = {
64
64
  '@context': 'https://schema.org',
65
65
  '@type': 'HowTo',
66
66
  name: title,
@@ -1,4 +1,4 @@
1
- import type { WithContext, FAQPage, HowToThing, SoftwareApplication } from 'schema-dts';
1
+ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
2
  import { bibliography } from '../bibliography';
3
3
  import type { ToolLocaleContent } from '../../../types';
4
4
  import type { CricketThermometerUI } from '../ui';
@@ -60,7 +60,7 @@ const faqSchema: WithContext<FAQPage> = {
60
60
  })),
61
61
  };
62
62
 
63
- const howToSchema: WithContext<HowToThing> = {
63
+ const howToSchema: WithContext<HowTo> = {
64
64
  '@context': 'https://schema.org',
65
65
  '@type': 'HowTo',
66
66
  name: title,