@jjlmoya/utils-aquarium 1.65.0 → 1.67.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 (72) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +3 -1
  3. package/src/entries.ts +3 -1
  4. package/src/index.ts +2 -0
  5. package/src/layouts/ProductionCategoryPage.astro +184 -184
  6. package/src/mfe/category-ui.ts +34 -34
  7. package/src/tests/qa-test-helpers.ts +32 -0
  8. package/src/tests/qa_bibliography_links.test.ts +48 -0
  9. package/src/tests/qa_claim_evidence.test.ts +69 -0
  10. package/src/tests/qa_logic_reference_coverage.test.ts +46 -0
  11. package/src/tests/qa_runtime_i18n.test.ts +138 -0
  12. package/src/tests/tool_validation.test.ts +5 -3
  13. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/aquarium-calcium-alkalinity-dosing-calculator.css +611 -0
  14. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/bibliography.astro +7 -0
  15. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/bibliography.ts +5 -0
  16. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/component.astro +93 -0
  17. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/controller.ts +140 -0
  18. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/dom-views.ts +246 -0
  19. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/entry.ts +27 -0
  20. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/de.ts +2 -0
  21. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/en.ts +3 -0
  22. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/es.ts +2 -0
  23. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/fr.ts +2 -0
  24. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/id.ts +2 -0
  25. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/it.ts +2 -0
  26. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/ja.ts +2 -0
  27. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/ko.ts +2 -0
  28. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/nl.ts +2 -0
  29. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/pl.ts +2 -0
  30. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/pt.ts +2 -0
  31. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/ru.ts +2 -0
  32. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/shared.ts +63 -0
  33. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/sv.ts +2 -0
  34. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/tr.ts +2 -0
  35. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/translations.ts +155 -0
  36. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/zh.ts +2 -0
  37. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/index.ts +11 -0
  38. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/logic.test.ts +33 -0
  39. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/logic.ts +112 -0
  40. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/seo.astro +11 -0
  41. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/ui.ts +44 -0
  42. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/view-helpers.ts +68 -0
  43. package/src/tool/aquariumSaltDosingCalculator/aquarium-salt-dosing-calculator.css +503 -0
  44. package/src/tool/aquariumSaltDosingCalculator/bibliography.astro +6 -0
  45. package/src/tool/aquariumSaltDosingCalculator/bibliography.ts +5 -0
  46. package/src/tool/aquariumSaltDosingCalculator/component.astro +36 -0
  47. package/src/tool/aquariumSaltDosingCalculator/controller.ts +104 -0
  48. package/src/tool/aquariumSaltDosingCalculator/dom-views.ts +74 -0
  49. package/src/tool/aquariumSaltDosingCalculator/entry.ts +13 -0
  50. package/src/tool/aquariumSaltDosingCalculator/i18n/de.ts +2 -0
  51. package/src/tool/aquariumSaltDosingCalculator/i18n/en.ts +2 -0
  52. package/src/tool/aquariumSaltDosingCalculator/i18n/es.ts +2 -0
  53. package/src/tool/aquariumSaltDosingCalculator/i18n/fr.ts +2 -0
  54. package/src/tool/aquariumSaltDosingCalculator/i18n/id.ts +2 -0
  55. package/src/tool/aquariumSaltDosingCalculator/i18n/it.ts +2 -0
  56. package/src/tool/aquariumSaltDosingCalculator/i18n/ja.ts +2 -0
  57. package/src/tool/aquariumSaltDosingCalculator/i18n/ko.ts +2 -0
  58. package/src/tool/aquariumSaltDosingCalculator/i18n/nl.ts +2 -0
  59. package/src/tool/aquariumSaltDosingCalculator/i18n/pl.ts +2 -0
  60. package/src/tool/aquariumSaltDosingCalculator/i18n/pt.ts +2 -0
  61. package/src/tool/aquariumSaltDosingCalculator/i18n/ru.ts +2 -0
  62. package/src/tool/aquariumSaltDosingCalculator/i18n/sv.ts +2 -0
  63. package/src/tool/aquariumSaltDosingCalculator/i18n/tr.ts +2 -0
  64. package/src/tool/aquariumSaltDosingCalculator/i18n/zh.ts +2 -0
  65. package/src/tool/aquariumSaltDosingCalculator/index.ts +11 -0
  66. package/src/tool/aquariumSaltDosingCalculator/localized-content.ts +151 -0
  67. package/src/tool/aquariumSaltDosingCalculator/logic.test.ts +46 -0
  68. package/src/tool/aquariumSaltDosingCalculator/logic.ts +85 -0
  69. package/src/tool/aquariumSaltDosingCalculator/seo.astro +11 -0
  70. package/src/tool/aquariumSaltDosingCalculator/storage.ts +19 -0
  71. package/src/tool/aquariumSaltDosingCalculator/ui.ts +47 -0
  72. package/src/tools.ts +12 -0
@@ -0,0 +1,140 @@
1
+ import {
2
+ DEFAULT_DOSING_INPUT,
3
+ type AquariumParameter,
4
+ type DoseUnit,
5
+ } from "./logic";
6
+ import type { AquariumCalciumAlkalinityDosingCalculatorUI } from "./ui";
7
+ import {
8
+ readInput,
9
+ render,
10
+ updateLabels,
11
+ writeInput,
12
+ type DisplayState,
13
+ type VolumeUnit,
14
+ } from "./dom-views";
15
+
16
+ interface ControllerOptions {
17
+ root: HTMLElement;
18
+ ui: AquariumCalciumAlkalinityDosingCalculatorUI;
19
+ }
20
+
21
+ const ALKALINITY_DEFAULTS = {
22
+ currentValue: 7,
23
+ targetValue: 8.5,
24
+ productStrengthPerDose: 0.5,
25
+ };
26
+
27
+ function bindParameterEvents(
28
+ root: HTMLElement,
29
+ setState: (state: DisplayState) => void,
30
+ getState: () => DisplayState,
31
+ update: () => void,
32
+ ): void {
33
+ root
34
+ .querySelectorAll<HTMLButtonElement>("[data-parameter-choice]")
35
+ .forEach((button) =>
36
+ button.addEventListener("click", () => {
37
+ const parameter = button.dataset.parameterChoice as AquariumParameter;
38
+ setState({ ...getState(), parameter });
39
+ const nextInput =
40
+ parameter === "alkalinity"
41
+ ? {
42
+ ...DEFAULT_DOSING_INPUT,
43
+ ...ALKALINITY_DEFAULTS,
44
+ parameter,
45
+ doseUnit: getState().doseUnit,
46
+ }
47
+ : {
48
+ ...DEFAULT_DOSING_INPUT,
49
+ parameter,
50
+ doseUnit: getState().doseUnit,
51
+ };
52
+ writeInput(root, nextInput, getState().volumeUnit);
53
+ update();
54
+ }),
55
+ );
56
+ }
57
+ function bindDoseUnitEvents(
58
+ root: HTMLElement,
59
+ setState: (state: DisplayState) => void,
60
+ getState: () => DisplayState,
61
+ update: () => void,
62
+ ): void {
63
+ root
64
+ .querySelectorAll<HTMLButtonElement>("[data-dose-unit-choice]")
65
+ .forEach((button) =>
66
+ button.addEventListener("click", () => {
67
+ setState({
68
+ ...getState(),
69
+ doseUnit: button.dataset.doseUnitChoice as DoseUnit,
70
+ });
71
+ update();
72
+ }),
73
+ );
74
+ }
75
+ function bindVolumeUnitEvents(
76
+ root: HTMLElement,
77
+ setState: (state: DisplayState) => void,
78
+ getState: () => DisplayState,
79
+ update: () => void,
80
+ ): void {
81
+ root
82
+ .querySelectorAll<HTMLButtonElement>("[data-volume-unit-choice]")
83
+ .forEach((button) =>
84
+ button.addEventListener("click", () => {
85
+ const state = getState();
86
+ const nextUnit = button.dataset.volumeUnitChoice as VolumeUnit;
87
+ const input = readInput(root, state);
88
+ setState({ ...state, volumeUnit: nextUnit });
89
+ writeInput(root, input, nextUnit);
90
+ update();
91
+ }),
92
+ );
93
+ }
94
+ function bindInputEvents(root: HTMLElement, update: () => void): void {
95
+ root
96
+ .querySelectorAll<HTMLInputElement>("input")
97
+ .forEach((input) => input.addEventListener("input", update));
98
+ }
99
+ function bindResetEvent(
100
+ root: HTMLElement,
101
+ setState: (state: DisplayState) => void,
102
+ update: () => void,
103
+ ): void {
104
+ root.querySelector('[data-action="reset"]')?.addEventListener("click", () => {
105
+ const state: DisplayState = {
106
+ parameter: "calcium",
107
+ doseUnit: "ml",
108
+ volumeUnit: "litres",
109
+ };
110
+ setState(state);
111
+ writeInput(root, DEFAULT_DOSING_INPUT, state.volumeUnit);
112
+ update();
113
+ });
114
+ }
115
+
116
+ export function startCalciumAlkalinityDosingController({
117
+ root,
118
+ ui,
119
+ }: ControllerOptions): void {
120
+ let state: DisplayState = {
121
+ parameter: DEFAULT_DOSING_INPUT.parameter,
122
+ doseUnit: DEFAULT_DOSING_INPUT.doseUnit,
123
+ volumeUnit: "litres",
124
+ };
125
+ const getState = (): DisplayState => state;
126
+ const setState = (nextState: DisplayState): void => {
127
+ state = nextState;
128
+ };
129
+ const update = (): void => {
130
+ updateLabels(root, ui, state);
131
+ render(root, ui, state);
132
+ };
133
+ writeInput(root, DEFAULT_DOSING_INPUT, state.volumeUnit);
134
+ update();
135
+ bindParameterEvents(root, setState, getState, update);
136
+ bindDoseUnitEvents(root, setState, getState, update);
137
+ bindVolumeUnitEvents(root, setState, getState, update);
138
+ bindInputEvents(root, update);
139
+ bindResetEvent(root, setState, update);
140
+ }
@@ -0,0 +1,246 @@
1
+ import {
2
+ calculateCalciumAlkalinityDosing,
3
+ type AquariumParameter,
4
+ type CalciumAlkalinityDosingInput,
5
+ type DoseUnit,
6
+ type DosingAction,
7
+ } from "./logic";
8
+ import type { AquariumCalciumAlkalinityDosingCalculatorUI } from "./ui";
9
+ import {
10
+ actionText,
11
+ field,
12
+ format,
13
+ numberValue,
14
+ parameterUnit,
15
+ renderValuePath,
16
+ sceneText,
17
+ setText,
18
+ summaryText,
19
+ } from "./view-helpers";
20
+ export type VolumeUnit = "litres" | "gallons";
21
+ export interface DisplayState {
22
+ parameter: AquariumParameter;
23
+ doseUnit: DoseUnit;
24
+ volumeUnit: VolumeUnit;
25
+ }
26
+ const US_GALLON_LITRES = 3.785411784;
27
+ export function readInput(
28
+ root: HTMLElement,
29
+ state: DisplayState,
30
+ ): CalciumAlkalinityDosingInput {
31
+ const enteredVolume = numberValue(root, "volume");
32
+ return {
33
+ parameter: state.parameter,
34
+ volumeLitres:
35
+ state.volumeUnit === "litres"
36
+ ? enteredVolume
37
+ : enteredVolume * US_GALLON_LITRES,
38
+ currentValue: numberValue(root, "current"),
39
+ targetValue: numberValue(root, "target"),
40
+ productStrengthPerDose: numberValue(root, "strength"),
41
+ doseUnit: state.doseUnit,
42
+ };
43
+ }
44
+ export function writeInput(
45
+ root: HTMLElement,
46
+ input: CalciumAlkalinityDosingInput,
47
+ volumeUnit: VolumeUnit,
48
+ ): void {
49
+ const volume =
50
+ volumeUnit === "litres"
51
+ ? input.volumeLitres
52
+ : input.volumeLitres / US_GALLON_LITRES;
53
+ field(root, "volume").value = String(Number(volume.toFixed(2)));
54
+ field(root, "current").value = String(input.currentValue);
55
+ field(root, "target").value = String(input.targetValue);
56
+ field(root, "strength").value = String(input.productStrengthPerDose);
57
+ }
58
+ function updateChoiceButtons(
59
+ root: HTMLElement,
60
+ selector: string,
61
+ attribute: string,
62
+ active: string,
63
+ ): void {
64
+ root.querySelectorAll<HTMLButtonElement>(selector).forEach((button) => {
65
+ const selected = button.dataset[attribute] === active;
66
+ button.classList.toggle("is-active", selected);
67
+ button.setAttribute("aria-pressed", String(selected));
68
+ });
69
+ }
70
+ export function updateLabels(
71
+ root: HTMLElement,
72
+ ui: AquariumCalciumAlkalinityDosingCalculatorUI,
73
+ state: DisplayState,
74
+ ): void {
75
+ updateLabelText(root, ui, state);
76
+ updateSelections(root, state);
77
+ updateValueRanges(root, state);
78
+ }
79
+ function updateLabelText(
80
+ root: HTMLElement,
81
+ ui: AquariumCalciumAlkalinityDosingCalculatorUI,
82
+ state: DisplayState,
83
+ ): void {
84
+ const unit = parameterUnit(ui, state.parameter);
85
+ const volumeLabel =
86
+ state.volumeUnit === "litres" ? ui.litresLabel : ui.gallonsLabel;
87
+ const doseLabel =
88
+ state.doseUnit === "ml" ? ui.millilitresLabel : ui.gramsLabel;
89
+ const parameterShort =
90
+ state.parameter === "calcium"
91
+ ? ui.calciumShortLabel
92
+ : ui.alkalinityShortLabel;
93
+ setText(root, '[data-output="value-unit"]', unit);
94
+ setText(root, '[data-output="value-unit-target"]', unit);
95
+ setText(root, '[data-output="volume-unit"]', volumeLabel);
96
+ setText(root, '[data-output="parameter-short"]', parameterShort);
97
+ setText(root, '[data-output="parameter-unit"]', unit);
98
+ setText(
99
+ root,
100
+ '[data-output="strength-unit"]',
101
+ `${unit} / ${doseLabel} / 100 L`,
102
+ );
103
+ }
104
+ function updateSelections(root: HTMLElement, state: DisplayState): void {
105
+ updateChoiceButtons(
106
+ root,
107
+ "[data-parameter-choice]",
108
+ "parameterChoice",
109
+ state.parameter,
110
+ );
111
+ updateChoiceButtons(
112
+ root,
113
+ "[data-dose-unit-choice]",
114
+ "doseUnitChoice",
115
+ state.doseUnit,
116
+ );
117
+ updateChoiceButtons(
118
+ root,
119
+ "[data-volume-unit-choice]",
120
+ "volumeUnitChoice",
121
+ state.volumeUnit,
122
+ );
123
+ }
124
+ function updateValueRanges(root: HTMLElement, state: DisplayState): void {
125
+ const maximum = state.parameter === "calcium" ? "1000" : "40";
126
+ field(root, "current").max = maximum;
127
+ field(root, "target").max = maximum;
128
+ }
129
+ function amountText(
130
+ ui: AquariumCalciumAlkalinityDosingCalculatorUI,
131
+ state: DisplayState,
132
+ output: {
133
+ action: DosingAction;
134
+ doseAmount: number;
135
+ estimatedWaterChangePercent: number;
136
+ },
137
+ ): string {
138
+ if (output.action === "reduce")
139
+ return `${format(output.estimatedWaterChangePercent, 1)} ${ui.unitPercent}`;
140
+ const unit = state.doseUnit === "ml" ? ui.millilitresLabel : ui.gramsLabel;
141
+ return `${format(output.doseAmount)} ${unit}`;
142
+ }
143
+ function renderInvalid(
144
+ root: HTMLElement,
145
+ ui: AquariumCalciumAlkalinityDosingCalculatorUI,
146
+ ): void {
147
+ root.querySelector<HTMLElement>("[data-error]")!.hidden = false;
148
+ setText(root, '[data-output="action"]', ui.invalidState);
149
+ setText(root, '[data-output="action-detail"]', "—");
150
+ setText(root, '[data-output="dose"]', "—");
151
+ setText(root, '[data-output="dose-chip"]', "—");
152
+ setText(root, '[data-output="current-value"]', "—");
153
+ setText(root, '[data-output="target-value"]', "—");
154
+ setText(root, '[data-output="summary"]', ui.invalidState);
155
+ setText(root, '[data-output="current-target"]', "—");
156
+ setText(root, '[data-output="difference"]', "—");
157
+ setText(root, '[data-output="water-change"]', "—");
158
+ }
159
+ function renderPrimary(
160
+ root: HTMLElement,
161
+ ui: AquariumCalciumAlkalinityDosingCalculatorUI,
162
+ state: DisplayState,
163
+ output: {
164
+ action: DosingAction;
165
+ doseAmount: number;
166
+ estimatedWaterChangePercent: number;
167
+ },
168
+ ): void {
169
+ const action = actionText(ui, output.action);
170
+ const label = output.action === "reduce" ? ui.waterChangeLabel : ui.doseLabel;
171
+ setText(root, '[data-output="action"]', action);
172
+ setText(root, '[data-output="action-detail"]', action);
173
+ setText(root, '[data-output="dose-label"]', label);
174
+ setText(root, '[data-output="dose-label-chip"]', label);
175
+ setText(root, '[data-output="dose-label-answer"]', label);
176
+ setText(root, '[data-output="dose"]', amountText(ui, state, output));
177
+ setText(root, '[data-output="dose-chip"]', amountText(ui, state, output));
178
+ setText(root, '[data-output="summary"]', summaryText(ui, output.action));
179
+ }
180
+ function renderStats(
181
+ root: HTMLElement,
182
+ ui: AquariumCalciumAlkalinityDosingCalculatorUI,
183
+ state: DisplayState,
184
+ output: {
185
+ action: DosingAction;
186
+ estimatedWaterChangePercent: number;
187
+ difference: number;
188
+ input: CalciumAlkalinityDosingInput;
189
+ },
190
+ ): void {
191
+ const unit = parameterUnit(ui, state.parameter);
192
+ const sign = output.difference > 0 ? "+" : "";
193
+ const waterChange =
194
+ output.action === "reduce"
195
+ ? `${format(output.estimatedWaterChangePercent, 1)} ${ui.unitPercent}`
196
+ : "—";
197
+ renderValuePath(root, output.input, unit);
198
+ setText(
199
+ root,
200
+ '[data-output="current-target"]',
201
+ `${format(output.input.currentValue)} → ${format(output.input.targetValue)} ${unit}`,
202
+ );
203
+ setText(
204
+ root,
205
+ '[data-output="difference"]',
206
+ `${sign}${format(output.difference)} ${unit}`,
207
+ );
208
+ setText(root, '[data-output="water-change"]', waterChange);
209
+ }
210
+ function renderScene(
211
+ root: HTMLElement,
212
+ ui: AquariumCalciumAlkalinityDosingCalculatorUI,
213
+ action: DosingAction,
214
+ ): void {
215
+ const scene = root.querySelector<HTMLElement>('[data-output="scene"]');
216
+ scene?.classList.remove("is-add", "is-reduce", "is-target");
217
+ scene?.classList.add(`is-${action}`);
218
+ scene?.setAttribute("aria-label", sceneText(ui, action));
219
+ }
220
+ function renderValid(
221
+ root: HTMLElement,
222
+ ui: AquariumCalciumAlkalinityDosingCalculatorUI,
223
+ state: DisplayState,
224
+ result: Extract<
225
+ ReturnType<typeof calculateCalciumAlkalinityDosing>,
226
+ { ok: true }
227
+ >,
228
+ ): void {
229
+ const output = result.value;
230
+ root.querySelector<HTMLElement>("[data-error]")!.hidden = true;
231
+ renderPrimary(root, ui, state, output);
232
+ renderStats(root, ui, state, output);
233
+ renderScene(root, ui, output.action);
234
+ }
235
+ export function render(
236
+ root: HTMLElement,
237
+ ui: AquariumCalciumAlkalinityDosingCalculatorUI,
238
+ state: DisplayState,
239
+ ): void {
240
+ const result = calculateCalciumAlkalinityDosing(readInput(root, state));
241
+ if (!result.ok) {
242
+ renderInvalid(root, ui);
243
+ return;
244
+ }
245
+ renderValid(root, ui, state, result);
246
+ }
@@ -0,0 +1,27 @@
1
+ import type { AquariumToolEntry, ToolLocaleContent } from '../../types';
2
+ import type { AquariumCalciumAlkalinityDosingCalculatorUI } from './ui';
3
+
4
+ export type { AquariumCalciumAlkalinityDosingCalculatorUI };
5
+ export type AquariumCalciumAlkalinityDosingCalculatorLocaleContent = ToolLocaleContent<AquariumCalciumAlkalinityDosingCalculatorUI>;
6
+
7
+ export const aquariumCalciumAlkalinityDosingCalculator: AquariumToolEntry<AquariumCalciumAlkalinityDosingCalculatorUI> = {
8
+ id: 'aquarium-calcium-alkalinity-dosing-calculator',
9
+ icons: { bg: 'mdi:fishbowl-outline', fg: 'mdi:eyedropper-variant' },
10
+ i18n: {
11
+ de: () => import('./i18n/de').then((m) => m.content),
12
+ en: () => import('./i18n/en').then((m) => m.content),
13
+ es: () => import('./i18n/es').then((m) => m.content),
14
+ fr: () => import('./i18n/fr').then((m) => m.content),
15
+ id: () => import('./i18n/id').then((m) => m.content),
16
+ it: () => import('./i18n/it').then((m) => m.content),
17
+ ja: () => import('./i18n/ja').then((m) => m.content),
18
+ ko: () => import('./i18n/ko').then((m) => m.content),
19
+ nl: () => import('./i18n/nl').then((m) => m.content),
20
+ pl: () => import('./i18n/pl').then((m) => m.content),
21
+ pt: () => import('./i18n/pt').then((m) => m.content),
22
+ ru: () => import('./i18n/ru').then((m) => m.content),
23
+ sv: () => import('./i18n/sv').then((m) => m.content),
24
+ tr: () => import('./i18n/tr').then((m) => m.content),
25
+ zh: () => import('./i18n/zh').then((m) => m.content),
26
+ },
27
+ };
@@ -0,0 +1,2 @@
1
+ import { localizedContents } from './translations';
2
+ export const content = localizedContents.de;
@@ -0,0 +1,3 @@
1
+ import { localizedContents } from './translations';
2
+
3
+ export const content = localizedContents.en;
@@ -0,0 +1,2 @@
1
+ import { localizedContents } from './translations';
2
+ export const content = localizedContents.es;
@@ -0,0 +1,2 @@
1
+ import { localizedContents } from './translations';
2
+ export const content = localizedContents.fr;
@@ -0,0 +1,2 @@
1
+ import { localizedContents } from './translations';
2
+ export const content = localizedContents.id;
@@ -0,0 +1,2 @@
1
+ import { localizedContents } from './translations';
2
+ export const content = localizedContents.it;
@@ -0,0 +1,2 @@
1
+ import { localizedContents } from './translations';
2
+ export const content = localizedContents.ja;
@@ -0,0 +1,2 @@
1
+ import { localizedContents } from './translations';
2
+ export const content = localizedContents.ko;
@@ -0,0 +1,2 @@
1
+ import { localizedContents } from './translations';
2
+ export const content = localizedContents.nl;
@@ -0,0 +1,2 @@
1
+ import { localizedContents } from './translations';
2
+ export const content = localizedContents.pl;
@@ -0,0 +1,2 @@
1
+ import { localizedContents } from './translations';
2
+ export const content = localizedContents.pt;
@@ -0,0 +1,2 @@
1
+ import { localizedContents } from './translations';
2
+ export const content = localizedContents.ru;
@@ -0,0 +1,63 @@
1
+ import type { FAQPage, HowTo, SoftwareApplication, WithContext } from 'schema-dts';
2
+ import type { SEOSection } from '../../../types';
3
+ import type { BibliographyEntry, HowToStep, ToolLocaleContent } from '../../../types';
4
+ import type { AquariumCalciumAlkalinityDosingCalculatorUI } from '../ui';
5
+
6
+ export interface LocalizedCopy {
7
+ language: string;
8
+ slug: string;
9
+ title: string;
10
+ description: string;
11
+ ui: AquariumCalciumAlkalinityDosingCalculatorUI;
12
+ seo: SEOSection[];
13
+ faq: { question: string; answer: string }[];
14
+ howTo: HowToStep[];
15
+ bibliography: BibliographyEntry[];
16
+ }
17
+
18
+ export function buildContent(copy: LocalizedCopy): ToolLocaleContent<AquariumCalciumAlkalinityDosingCalculatorUI> {
19
+ const faqSchema: WithContext<FAQPage> = {
20
+ '@context': 'https://schema.org',
21
+ '@type': 'FAQPage',
22
+ inLanguage: copy.language,
23
+ mainEntity: copy.faq.map((item) => ({
24
+ '@type': 'Question',
25
+ name: item.question,
26
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
27
+ })),
28
+ };
29
+ const howToSchema: WithContext<HowTo> = {
30
+ '@context': 'https://schema.org',
31
+ '@type': 'HowTo',
32
+ inLanguage: copy.language,
33
+ name: copy.title,
34
+ description: copy.description,
35
+ step: copy.howTo.map((step, index) => ({
36
+ '@type': 'HowToStep',
37
+ position: index + 1,
38
+ name: step.name,
39
+ text: step.text,
40
+ })),
41
+ };
42
+ const appSchema: WithContext<SoftwareApplication> = {
43
+ '@context': 'https://schema.org',
44
+ '@type': 'SoftwareApplication',
45
+ name: copy.title,
46
+ description: copy.description,
47
+ applicationCategory: 'UtilityApplication',
48
+ operatingSystem: 'Web',
49
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
50
+ inLanguage: copy.language,
51
+ };
52
+ return {
53
+ slug: copy.slug,
54
+ title: copy.title,
55
+ description: copy.description,
56
+ ui: copy.ui,
57
+ faq: copy.faq,
58
+ howTo: copy.howTo,
59
+ bibliography: copy.bibliography,
60
+ seo: copy.seo,
61
+ schemas: [faqSchema, howToSchema, appSchema],
62
+ };
63
+ }
@@ -0,0 +1,2 @@
1
+ import { localizedContents } from './translations';
2
+ export const content = localizedContents.sv;
@@ -0,0 +1,2 @@
1
+ import { localizedContents } from './translations';
2
+ export const content = localizedContents.tr;