@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,74 @@
1
+ import type { AquariumSaltDosingCalculatorUI } from './ui';
2
+ import type { SaltDosingResult, UnitSystem } from './logic';
3
+ import { gramsPerLitreToOuncesPerGallon, litresToGallons } from './logic';
4
+
5
+ function setText(root: HTMLElement, selector: string, text: string): void {
6
+ const element = root.querySelector<HTMLElement>(selector);
7
+ if (element) element.textContent = text;
8
+ }
9
+
10
+ function format(value: number, locale: string, digits = 1): string {
11
+ return new Intl.NumberFormat(locale, { maximumFractionDigits: digits }).format(value);
12
+ }
13
+
14
+ function volume(value: number, unit: UnitSystem, locale: string, ui: AquariumSaltDosingCalculatorUI): string {
15
+ return format(unit === 'metric' ? value : litresToGallons(value), locale) + ' ' + (unit === 'metric' ? ui.litresUnit : ui.gallonsUnit);
16
+ }
17
+
18
+ function salt(value: number, unit: UnitSystem, locale: string, ui: AquariumSaltDosingCalculatorUI): string {
19
+ return format(unit === 'metric' ? value : value * 0.0352739619, locale) + ' ' + (unit === 'metric' ? ui.gramsUnit : ui.ouncesUnit);
20
+ }
21
+
22
+ interface RenderOptions {
23
+ result: SaltDosingResult;
24
+ unit: UnitSystem;
25
+ locale: string;
26
+ ui: AquariumSaltDosingCalculatorUI;
27
+ }
28
+
29
+ function actionText(action: SaltDosingResult['action'], ui: AquariumSaltDosingCalculatorUI): string {
30
+ if (action === 'prepare') return ui.prepareAction;
31
+ if (action === 'raise') return ui.raiseAction;
32
+ if (action === 'lower') return ui.lowerAction;
33
+ return ui.targetAction;
34
+ }
35
+
36
+ function amountText(result: SaltDosingResult, options: RenderOptions): string {
37
+ if (result.action === 'lower') return volume(result.amountLitres, options.unit, options.locale, options.ui);
38
+ if (result.action === 'target') return options.ui.noChangeLabel;
39
+ return salt(result.amountGrams, options.unit, options.locale, options.ui);
40
+ }
41
+
42
+ function amountLabel(result: SaltDosingResult, ui: AquariumSaltDosingCalculatorUI): string {
43
+ if (result.action === 'lower') return ui.waterAmountLabel;
44
+ if (result.action === 'target') return ui.amountLabel;
45
+ return ui.saltAmountLabel;
46
+ }
47
+
48
+ function saltFill(result: SaltDosingResult): number {
49
+ if (result.action === 'lower') return 12;
50
+ if (result.action === 'target') return 0;
51
+ return Math.min(100, Math.max(12, (result.amountGrams / Math.max(1, result.volumeLitres * result.productRateGramsPerLitre)) * 100));
52
+ }
53
+
54
+ export function renderResults(root: HTMLElement, options: RenderOptions): void {
55
+ const { result, locale, ui } = options;
56
+ setText(root, '[data-output="action"]', actionText(result.action, ui));
57
+ setText(root, '[data-output="amount-label"]', amountLabel(result, ui));
58
+ setText(root, '[data-output="amount"]', amountText(result, options));
59
+ setText(root, '[data-output="summary"]', ui.currentTargetLabel + ': ' + format(result.currentSalinityPpt, locale) + ' → ' + format(result.targetSalinityPpt, locale) + ' ' + ui.salinityUnit);
60
+ const saltFillValue = saltFill(result);
61
+ root.style.setProperty('--salt-fill', saltFillValue + '%');
62
+ root.dataset.action = result.action;
63
+ root.classList.add('has-results');
64
+ root.classList.remove('has-error');
65
+ }
66
+
67
+ export function renderError(root: HTMLElement, message: string): void {
68
+ setText(root, '[data-output="amount"]', '—');
69
+ setText(root, '[data-output="action"]', message);
70
+ root.classList.remove('has-results');
71
+ root.classList.add('has-error');
72
+ }
73
+
74
+ export function formatRateForDisplay(rate: number, unit: UnitSystem): number { return unit === 'metric' ? rate : gramsPerLitreToOuncesPerGallon(rate); }
@@ -0,0 +1,13 @@
1
+ import type { AquariumToolEntry, ToolLocaleContent } from '../../types';
2
+ import type { AquariumSaltDosingCalculatorUI } from './ui';
3
+
4
+ export type { AquariumSaltDosingCalculatorUI };
5
+ export type AquariumSaltDosingCalculatorLocaleContent = ToolLocaleContent<AquariumSaltDosingCalculatorUI>;
6
+
7
+ export const aquariumSaltDosingCalculator: AquariumToolEntry<AquariumSaltDosingCalculatorUI> = {
8
+ id: 'aquarium-salt-dosing-calculator',
9
+ icons: { bg: 'mdi:fishbowl-outline', fg: 'mdi:shaker-outline' },
10
+ i18n: {
11
+ de: () => import('./i18n/de').then((m) => m.content), en: () => import('./i18n/en').then((m) => m.content), es: () => import('./i18n/es').then((m) => m.content), fr: () => import('./i18n/fr').then((m) => m.content), id: () => import('./i18n/id').then((m) => m.content), it: () => import('./i18n/it').then((m) => m.content), ja: () => import('./i18n/ja').then((m) => m.content), ko: () => import('./i18n/ko').then((m) => m.content), nl: () => import('./i18n/nl').then((m) => m.content), pl: () => import('./i18n/pl').then((m) => m.content), pt: () => import('./i18n/pt').then((m) => m.content), ru: () => import('./i18n/ru').then((m) => m.content), sv: () => import('./i18n/sv').then((m) => m.content), tr: () => import('./i18n/tr').then((m) => m.content), zh: () => import('./i18n/zh').then((m) => m.content),
12
+ },
13
+ };
@@ -0,0 +1,2 @@
1
+ import { createSaltDosingContent } from '../localized-content';
2
+ export const content = createSaltDosingContent('de');
@@ -0,0 +1,2 @@
1
+ import { createSaltDosingContent } from '../localized-content';
2
+ export const content = createSaltDosingContent('en');
@@ -0,0 +1,2 @@
1
+ import { createSaltDosingContent } from '../localized-content';
2
+ export const content = createSaltDosingContent('es');
@@ -0,0 +1,2 @@
1
+ import { createSaltDosingContent } from '../localized-content';
2
+ export const content = createSaltDosingContent('fr');
@@ -0,0 +1,2 @@
1
+ import { createSaltDosingContent } from '../localized-content';
2
+ export const content = createSaltDosingContent('id');
@@ -0,0 +1,2 @@
1
+ import { createSaltDosingContent } from '../localized-content';
2
+ export const content = createSaltDosingContent('it');
@@ -0,0 +1,2 @@
1
+ import { createSaltDosingContent } from '../localized-content';
2
+ export const content = createSaltDosingContent('ja');
@@ -0,0 +1,2 @@
1
+ import { createSaltDosingContent } from '../localized-content';
2
+ export const content = createSaltDosingContent('ko');
@@ -0,0 +1,2 @@
1
+ import { createSaltDosingContent } from '../localized-content';
2
+ export const content = createSaltDosingContent('nl');
@@ -0,0 +1,2 @@
1
+ import { createSaltDosingContent } from '../localized-content';
2
+ export const content = createSaltDosingContent('pl');
@@ -0,0 +1,2 @@
1
+ import { createSaltDosingContent } from '../localized-content';
2
+ export const content = createSaltDosingContent('pt');
@@ -0,0 +1,2 @@
1
+ import { createSaltDosingContent } from '../localized-content';
2
+ export const content = createSaltDosingContent('ru');
@@ -0,0 +1,2 @@
1
+ import { createSaltDosingContent } from '../localized-content';
2
+ export const content = createSaltDosingContent('sv');
@@ -0,0 +1,2 @@
1
+ import { createSaltDosingContent } from '../localized-content';
2
+ export const content = createSaltDosingContent('tr');
@@ -0,0 +1,2 @@
1
+ import { createSaltDosingContent } from '../localized-content';
2
+ export const content = createSaltDosingContent('zh');
@@ -0,0 +1,11 @@
1
+ import { aquariumSaltDosingCalculator } from './entry';
2
+ import type { ToolDefinition } from '../../types';
3
+
4
+ export * from './entry';
5
+
6
+ export const AQUARIUM_SALT_DOSING_CALCULATOR_TOOL: ToolDefinition = {
7
+ entry: aquariumSaltDosingCalculator,
8
+ Component: () => import('./component.astro'),
9
+ SEOComponent: () => import('./seo.astro'),
10
+ BibliographyComponent: () => import('./bibliography.astro'),
11
+ };
@@ -0,0 +1,151 @@
1
+ import { bibliography } from './bibliography';
2
+ import type { FAQPage, HowTo, SoftwareApplication, WithContext } from 'schema-dts';
3
+ import type { ToolLocaleContent } from '../../types';
4
+ import type { AquariumSaltDosingCalculatorUI } from './ui';
5
+
6
+ type LocaleCopy = {
7
+ title: string;
8
+ description: string;
9
+ introTitle: string;
10
+ intro: string;
11
+ methodTitle: string;
12
+ method: string;
13
+ resultTitle: string;
14
+ result: string;
15
+ tipTitle: string;
16
+ tip: string;
17
+ faq: [string, string][];
18
+ howTo: [string, string][];
19
+ ui: Partial<AquariumSaltDosingCalculatorUI>;
20
+ };
21
+
22
+ const baseUi: AquariumSaltDosingCalculatorUI = {
23
+ toolKicker: 'Salt, measured calmly', resultTitle: 'Your next salt decision', configureTitle: 'Start with your product label', modeLabel: 'Calculation mode', prepareMode: 'Prepare new water', correctMode: 'Correct tank', unitsLabel: 'Units', metricLabel: 'Metric', imperialLabel: 'Imperial', volumeLabel: 'Water volume', currentSalinityLabel: 'Current salinity', targetSalinityLabel: 'Target salinity', productRateLabel: 'Product rate on label', salinityUnit: 'ppt', metricVolumeUnit: 'L', imperialVolumeUnit: 'gal', metricRateUnit: 'g/L', imperialRateUnit: 'oz/gal', presetLabel: 'Quick target', brackishPreset: 'Brackish 10', marinePreset: 'Marine 35', customPreset: 'Fresh start', prepareHint: 'For a new batch, enter the water you are mixing and copy the product rate from its label.', correctHint: 'For an existing tank, the result changes direction: add salt to raise salinity or replace water to lower it.', productRateHint: 'Use the label rate for your exact salt mix. There is no universal grams per litre dose.', outputActionLabel: 'Recommended action', prepareAction: 'Prepare this batch', raiseAction: 'Add salt gradually', lowerAction: 'Replace water gradually', targetAction: 'No correction needed', amountLabel: 'Amount', saltAmountLabel: 'Salt to weigh', waterAmountLabel: 'Water to replace', noChangeLabel: '0', currentTargetLabel: 'Current to target', verificationNote: 'Estimate only. Dissolve and mix fully, then check salinity with your refractometer or hydrometer before adding it to livestock.', gradualNote: 'Large corrections should be split into smaller steps and checked between changes.', emptyState: 'Enter your own tank and product values to begin.', invalidState: 'Check the volume, salinity and product rate. Use finite values within the displayed ranges.', resetLabel: 'Reset example', gramsUnit: 'g', ouncesUnit: 'oz', litresUnit: 'L', gallonsUnit: 'gal',
24
+ };
25
+
26
+ const localizedUiDefaults: Record<string, Partial<AquariumSaltDosingCalculatorUI>> = {
27
+ es: { unitsLabel: 'Unidades', metricLabel: 'Métrico', currentTargetLabel: 'Actual → objetivo', outputActionLabel: 'Acción recomendada', invalidState: 'Revisa el volumen, la salinidad y la proporción del producto. Usa valores finitos dentro de los rangos mostrados.' },
28
+ de: { unitsLabel: 'Einheiten', metricLabel: 'Metrisch', currentTargetLabel: 'Aktuell → Ziel', outputActionLabel: 'Empfohlene Handlung', invalidState: 'Prüfe Wasservolumen, Salinität und Produktdosierung. Verwende endliche Werte innerhalb der angezeigten Bereiche.' },
29
+ fr: { unitsLabel: 'Unités', metricLabel: 'Métrique', currentTargetLabel: 'Actuelle → cible', outputActionLabel: 'Action recommandée', invalidState: 'Vérifiez le volume, la salinité et le dosage du produit. Utilisez des valeurs finies dans les plages affichées.' },
30
+ it: { unitsLabel: 'Unità', metricLabel: 'Metrico', currentTargetLabel: 'Attuale → obiettivo', outputActionLabel: 'Azione consigliata', invalidState: 'Controlla volume, salinità e dosaggio del prodotto. Usa valori finiti negli intervalli mostrati.' },
31
+ pt: { unitsLabel: 'Unidades', metricLabel: 'Métrico', currentTargetLabel: 'Atual → objetivo', outputActionLabel: 'Ação recomendada', invalidState: 'Verifique o volume, a salinidade e a dosagem do produto. Use valores finitos dentro dos intervalos apresentados.' },
32
+ nl: { unitsLabel: 'Eenheden', metricLabel: 'Metrisch', currentTargetLabel: 'Huidig → doel', outputActionLabel: 'Aanbevolen actie', invalidState: 'Controleer het volume, het zoutgehalte en de productdosering. Gebruik eindige waarden binnen de getoonde bereiken.' },
33
+ sv: { unitsLabel: 'Enheter', metricLabel: 'Metriskt', currentTargetLabel: 'Aktuell → mål', outputActionLabel: 'Rekommenderad åtgärd', invalidState: 'Kontrollera volym, salthalt och produktdosering. Använd ändliga värden inom de visade intervallen.' },
34
+ pl: { unitsLabel: 'Jednostki', metricLabel: 'Metryczne', currentTargetLabel: 'Bieżące → cel', outputActionLabel: 'Zalecane działanie', invalidState: 'Sprawdź objętość, zasolenie i dawkę produktu. Używaj skończonych wartości w pokazanych zakresach.' },
35
+ id: { unitsLabel: 'Satuan', metricLabel: 'Metrik', currentTargetLabel: 'Saat ini → target', outputActionLabel: 'Tindakan yang disarankan', invalidState: 'Periksa volume, salinitas, dan takaran produk. Gunakan nilai yang valid dalam rentang yang ditampilkan.' },
36
+ tr: { unitsLabel: 'Birimler', metricLabel: 'Metrik', currentTargetLabel: 'Mevcut → hedef', outputActionLabel: 'Önerilen işlem', invalidState: 'Hacmi, tuzluluğu ve ürün dozajını kontrol edin. Gösterilen aralıklar içinde sonlu değerler kullanın.' },
37
+ ru: { unitsLabel: 'Единицы', metricLabel: 'Метрические', currentTargetLabel: 'Текущая → цель', outputActionLabel: 'Рекомендуемое действие', invalidState: 'Проверьте объём, солёность и дозировку продукта. Используйте конечные значения в указанных диапазонах.' },
38
+ ja: { unitsLabel: '単位', metricLabel: 'メートル法', currentTargetLabel: '現在 → 目標', outputActionLabel: '推奨アクション', invalidState: '水量、塩分、製品量を確認してください。表示された範囲内の有限な値を入力してください。' },
39
+ ko: { unitsLabel: '단위', metricLabel: '미터법', currentTargetLabel: '현재 → 목표', outputActionLabel: '권장 작업', invalidState: '물의 양, 염도, 제품 비율을 확인하세요. 표시된 범위 안의 유한한 값을 입력하세요.' },
40
+ zh: { unitsLabel: '单位', metricLabel: '公制', currentTargetLabel: '当前 → 目标', outputActionLabel: '建议操作', invalidState: '请检查水量、盐度和产品比例。请输入显示范围内的有限数值。' },
41
+ };
42
+
43
+ const copies: Record<string, LocaleCopy> = {
44
+ en: {
45
+ title: 'Aquarium Salt Dosing Calculator', description: 'Calculate how much aquarium salt mix to weigh for a new batch of water, or estimate a gradual correction for an existing tank using your own product label rate.', introTitle: 'Measure the mix, not a universal dose', intro: 'Salt products vary. This calculator starts with the rate printed on your own product label, then turns your water volume and target salinity into a practical amount to weigh. It works locally in the browser with no catalogue or account.', methodTitle: 'Two jobs, two safer outputs', method: 'Choose Prepare new water when mixing a replacement batch. Choose Correct tank when a measured tank is below or above its target. Raising salinity returns an estimated salt mass; lowering salinity returns the matching volume to remove and replace with prepared lower salinity water.', resultTitle: 'Read the action before the number', result: 'The number is useful because it is attached to an action. Weigh the estimated salt for a new batch or a small upward correction. For a downward correction, remove the calculated volume and replace it with water prepared at the intended target. Keep checking with your own instrument.', tipTitle: 'Product labels and measurements win', tip: '<p>Use the exact salt mix rate printed on the package. The calculation is an estimate, not a livestock safety guarantee or a replacement for a refractometer, hydrometer, gradual acclimation and species-specific care.</p>', faq: [['Can I use a universal aquarium salt dose?', 'No. Enter the grams per litre or ounces per gallon printed for your exact salt mix. Different products and target salinities are not interchangeable.'], ['What happens when I lower salinity?', 'The calculator estimates how much tank water to remove and replace with lower salinity water, using a simple salt-balance model. Make the change gradually and recheck.'], ['Does this calculate freshwater aquarium salt?', 'It is designed for preparing brackish or marine water and for measured salinity corrections. It does not decide whether a freshwater animal should receive salt.'], ['Why does raising salinity use the product rate?', 'The entered label rate is treated as the mass of that product needed per litre at the target salinity. The estimate scales it by the salinity difference.'], ['Does the calculation leave my browser?', 'No network request is required. The last form values may be stored locally in this browser for convenience.']], howTo: [['Choose a mode', 'Select Prepare new water for a batch or Correct tank for a measured tank adjustment.'], ['Copy the product rate', 'Read the rate from your product label and enter it in the displayed unit.'], ['Enter salinity and volume', 'Use the water volume you will actually mix or the current working volume of the tank.'], ['Verify before use', 'Mix fully, measure with your instrument and make any correction in small steps.']], ui: {},
46
+ },
47
+ es: {
48
+ title: 'Calculadora de dosis de sal para acuarios', description: 'Calcula cuánta mezcla de sal pesar para preparar agua nueva o estima una corrección gradual de un acuario usando la proporción de tu propio producto.', introTitle: 'Mide la mezcla, no una dosis universal', intro: 'Los productos de sal no son iguales. Esta calculadora parte de la proporción impresa en tu envase y transforma el volumen y la salinidad objetivo en una cantidad práctica. Funciona localmente en el navegador, sin catálogos ni cuentas.', methodTitle: 'Dos tareas, dos resultados claros', method: 'Elige Preparar agua nueva para mezclar un lote de reposición. Elige Corregir acuario si la medición está por debajo o por encima del objetivo. Para subir devuelve una masa estimada de sal; para bajar devuelve el volumen que conviene retirar y sustituir por agua preparada.', resultTitle: 'Lee la acción antes que la cifra', result: 'La cifra sirve porque está unida a una acción. Pesa la sal estimada para el lote o para una corrección pequeña al alza. Para bajar, retira el volumen calculado y sustitúyelo por agua con la salinidad prevista. Comprueba siempre con tu instrumento.', tipTitle: 'Gana la etiqueta y gana la medición', tip: '<p>Usa la proporción exacta de tu mezcla. Es una estimación, no una garantía de seguridad para los animales ni un sustituto del refractómetro, el hidrómetro o los cambios graduales.</p>', faq: [['¿Puedo usar una dosis universal?', 'No. Introduce los gramos por litro que indique tu mezcla. Los productos y las salinidades objetivo no son intercambiables.'], ['¿Qué ocurre al bajar la salinidad?', 'Estima cuánto agua del acuario retirar y sustituir por agua de menor salinidad mediante un balance sencillo de sal. Hazlo poco a poco y vuelve a medir.'], ['¿Sirve para acuarios de agua dulce?', 'Está pensada para preparar agua salobre o marina y para corregir una salinidad medida. No decide si un animal de agua dulce debe recibir sal.'], ['¿Por qué usa la proporción del producto al subir?', 'Trata la proporción indicada como la masa de producto necesaria por litro en la salinidad objetivo y la ajusta a la diferencia medida.'], ['¿Salen mis datos del navegador?', 'No hace falta ninguna petición de red. Los últimos valores pueden guardarse localmente en este navegador.']], howTo: [['Elige el modo', 'Selecciona Preparar agua nueva para un lote o Corregir acuario para un tanque medido.'], ['Copia la proporción', 'Lee la proporción de la etiqueta e introdúcela en la unidad mostrada.'], ['Escribe volumen y salinidad', 'Usa el volumen real que vas a mezclar o el volumen de trabajo actual del acuario.'], ['Comprueba antes de usar', 'Mezcla bien, mide con tu instrumento y corrige en pasos pequeños.']], ui: { toolKicker: 'Sal medida con calma', resultTitle: 'Tu próxima decisión con la sal', configureTitle: 'Empieza por la etiqueta', prepareMode: 'Preparar agua nueva', correctMode: 'Corregir acuario', modeLabel: 'Modo de cálculo', volumeLabel: 'Volumen de agua', currentSalinityLabel: 'Salinidad actual', targetSalinityLabel: 'Salinidad objetivo', productRateLabel: 'Proporción de la etiqueta', presetLabel: 'Objetivo rápido', brackishPreset: 'Salobre 10', marinePreset: 'Marina 35', customPreset: 'Inicio propio', prepareHint: 'Para un lote nuevo, indica el agua que vas a mezclar y copia la proporción del envase.', correctHint: 'En un tanque existente, subir implica añadir sal y bajar implica cambiar agua.', productRateHint: 'Usa la proporción de tu mezcla exacta. No existe una dosis universal.', prepareAction: 'Prepara este lote', raiseAction: 'Añade sal poco a poco', lowerAction: 'Cambia agua poco a poco', targetAction: 'No hace falta corregir', amountLabel: 'Cantidad', saltAmountLabel: 'Sal a pesar', waterAmountLabel: 'Agua a sustituir', verificationNote: 'Es una estimación. Disuelve y mezcla bien; comprueba la salinidad con refractómetro o hidrómetro antes de usarla.', resetLabel: 'Restablecer ejemplo' },
49
+ },
50
+ de: {
51
+ title: 'Aquarium Salz Dosierungsrechner', description: 'Berechne die Salzmenge für neues Wasser oder eine schrittweise Korrektur eines bestehenden Aquariums mit der Dosierung deines eigenen Produkts.', introTitle: 'Die Mischung messen, keine Universalmenge', intro: 'Salzprodukte unterscheiden sich. Der Rechner verwendet zuerst die Angabe auf deiner Packung und übersetzt Wassermenge und Zielsalinität in eine abwieg­bare Menge. Er läuft lokal im Browser.', methodTitle: 'Zwei Aufgaben, zwei klare Ergebnisse', method: 'Wähle Neues Wasser vorbereiten für eine neue Mischung. Wähle Aquarium korrigieren für einen gemessenen Wert unter oder über dem Ziel. Zum Erhöhen wird eine Salzmasse geschätzt, zum Senken das zu ersetzende Wasservolumen.', resultTitle: 'Erst die Handlung, dann die Zahl', result: 'Wiege die geschätzte Salzmenge für die neue Mischung oder eine kleine Erhöhung ab. Beim Senken entfernst du das berechnete Volumen und ersetzt es durch Wasser mit der geplanten Salinität. Danach immer selbst messen.', tipTitle: 'Etikett und Messgerät sind entscheidend', tip: '<p>Verwende die genaue Angabe deiner Salzmarke. Das Ergebnis ist eine Schätzung und keine Sicherheitsgarantie für Tiere oder Ersatz für Refraktometer, Hydrometer und langsame Änderungen.</p>', faq: [['Kann ich eine Universal-Dosis verwenden?', 'Nein. Trage die Angabe deiner eigenen Mischung ein. Produkte und Zielsalinitäten sind nicht beliebig austauschbar.'], ['Was passiert beim Senken der Salinität?', 'Der Rechner schätzt mit einer einfachen Salzbilanz, wie viel Wasser entfernt und durch Wasser mit niedrigerer Salinität ersetzt wird. Danach erneut messen.'], ['Ist der Rechner für Süßwasser gedacht?', 'Er dient zum Mischen von Brack- oder Meerwasser und zur Korrektur einer gemessenen Salinität. Er entscheidet nicht über Salz für Süßwassertiere.'], ['Warum wird beim Erhöhen die Produktangabe gebraucht?', 'Die Etikettangabe wird als Produktmasse pro Liter bei der Zielsalinität behandelt und auf die gemessene Differenz skaliert.'], ['Verlassen meine Daten den Browser?', 'Nein. Für die Berechnung ist keine Netzwerkanfrage nötig. Letzte Werte können lokal gespeichert werden.']], howTo: [['Modus wählen', 'Wähle Neues Wasser vorbereiten oder Aquarium korrigieren.'], ['Produktangabe übernehmen', 'Lies die Dosierung vom Etikett ab und trage sie in der angezeigten Einheit ein.'], ['Volumen und Salinität eingeben', 'Verwende das tatsächliche Mischvolumen oder das aktuelle Arbeitsvolumen des Aquariums.'], ['Vor der Verwendung prüfen', 'Vollständig mischen, messen und in kleinen Schritten korrigieren.']], ui: { toolKicker: 'Salz ruhig abgemessen', resultTitle: 'Deine nächste Salzentscheidung', configureTitle: 'Mit dem Produktetikett beginnen', modeLabel: 'Berechnungsmodus', prepareMode: 'Neues Wasser vorbereiten', correctMode: 'Aquarium korrigieren', volumeLabel: 'Wasservolumen', currentSalinityLabel: 'Aktuelle Salinität', targetSalinityLabel: 'Zielsalinität', productRateLabel: 'Produktangabe auf dem Etikett', presetLabel: 'Schnellziel', brackishPreset: 'Brackwasser 10', marinePreset: 'Meerwasser 35', customPreset: 'Eigener Start', prepareHint: 'Für eine neue Mischung das tatsächliche Wasser und die Dosierung vom Etikett eingeben.', correctHint: 'Beim bestehenden Aquarium erhöht Salz den Wert; zum Senken wird Wasser ersetzt.', productRateHint: 'Die genaue Angabe deiner Mischung verwenden. Es gibt keine Universaldosis.', prepareAction: 'Diese Mischung vorbereiten', raiseAction: 'Salz schrittweise zugeben', lowerAction: 'Wasser schrittweise ersetzen', targetAction: 'Keine Korrektur nötig', amountLabel: 'Menge', saltAmountLabel: 'Abzuwiegendes Salz', waterAmountLabel: 'Zu ersetzendes Wasser', verificationNote: 'Nur eine Schätzung. Vollständig lösen, mischen und vor dem Einsatz mit Refraktometer oder Hydrometer prüfen.', resetLabel: 'Beispiel zurücksetzen' },
52
+ },
53
+ fr: {
54
+ title: 'Calculateur de dosage de sel pour aquarium', description: 'Calculez la quantité de mélange de sel pour préparer une nouvelle eau ou estimer une correction progressive avec le dosage de votre produit.', introTitle: 'Mesurer le mélange, pas une dose universelle', intro: 'Les mélanges de sel varient. Cet outil part du dosage imprimé sur votre emballage et transforme votre volume et votre salinité cible en quantité à peser. Le calcul reste local dans le navigateur.', methodTitle: 'Deux usages, deux actions', method: 'Choisissez Préparer une eau neuve pour un nouveau lot. Choisissez Corriger le bac pour une mesure actuelle au-dessus ou au-dessous de la cible. Pour augmenter, le résultat donne une masse de sel; pour diminuer, il donne le volume d’eau à remplacer.', resultTitle: 'Lire l’action avant le nombre', result: 'Pesez le sel estimé pour le lot ou une petite hausse. Pour baisser la salinité, retirez le volume calculé et remplacez-le par une eau préparée. Mesurez toujours avec votre propre instrument.', tipTitle: 'L’étiquette et la mesure priment', tip: '<p>Utilisez le dosage exact de votre mélange. Le résultat est une estimation, pas une garantie de sécurité pour les animaux ni un remplacement du réfractomètre et des changements progressifs.</p>', faq: [['Puis-je utiliser une dose universelle?', 'Non. Saisissez le dosage de votre mélange. Les produits et les salinités cibles ne sont pas interchangeables.'], ['Que se passe-t-il si je baisse la salinité?', 'L’outil estime le volume à retirer et à remplacer avec une eau moins salée selon un bilan simple. Mesurez à nouveau après le changement.'], ['Est-ce prévu pour l’eau douce?', 'Il sert à préparer de l’eau saumâtre ou marine et à corriger une salinité mesurée. Il ne décide pas si un animal d’eau douce doit recevoir du sel.'], ['Pourquoi le dosage du produit est-il nécessaire pour augmenter?', 'Le dosage est traité comme la masse de produit par litre à la salinité cible, puis ajusté à l’écart mesuré.'], ['Mes données quittent-elles le navigateur?', 'Non. Le calcul ne demande aucune requête réseau. Les dernières valeurs peuvent être conservées localement.']], howTo: [['Choisir un mode', 'Sélectionnez Préparer une eau neuve ou Corriger le bac.'], ['Lire le produit', 'Copiez le dosage de l’étiquette dans l’unité affichée.'], ['Saisir volume et salinité', 'Utilisez le volume réellement mélangé ou le volume utile du bac.'], ['Vérifier avant usage', 'Mélangez complètement, mesurez et corrigez par petites étapes.']], ui: { toolKicker: 'Sel mesuré sereinement', resultTitle: 'Votre prochaine décision de sel', configureTitle: 'Commencez par l’étiquette', modeLabel: 'Mode de calcul', prepareMode: 'Préparer une eau neuve', correctMode: 'Corriger le bac', volumeLabel: 'Volume d’eau', currentSalinityLabel: 'Salinité actuelle', targetSalinityLabel: 'Salinité cible', productRateLabel: 'Dosage sur l’étiquette', presetLabel: 'Cible rapide', brackishPreset: 'Saumâtre 10', marinePreset: 'Marine 35', customPreset: 'Départ personnalisé', prepareHint: 'Pour un nouveau lot, saisissez le volume à mélanger et le dosage du produit.', correctHint: 'Dans un bac existant, augmenter ajoute du sel; diminuer remplace de l’eau.', productRateHint: 'Utilisez le dosage exact du mélange. Il n’existe pas de dose universelle.', prepareAction: 'Préparer ce lot', raiseAction: 'Ajouter le sel progressivement', lowerAction: 'Remplacer l’eau progressivement', targetAction: 'Aucune correction nécessaire', amountLabel: 'Quantité', saltAmountLabel: 'Sel à peser', waterAmountLabel: 'Eau à remplacer', verificationNote: 'Estimation uniquement. Dissolvez, mélangez puis vérifiez avec un réfractomètre ou un hydromètre.', resetLabel: 'Réinitialiser l’exemple' },
55
+ },
56
+ it: {
57
+ title: 'Calcolatore del dosaggio di sale per acquario', description: 'Calcola quanto sale pesare per preparare nuova acqua o stimare una correzione graduale usando il dosaggio del tuo prodotto.', introTitle: 'Misura la miscela, non una dose universale', intro: 'Le miscele di sale cambiano da prodotto a prodotto. Il calcolatore usa il dosaggio stampato sulla confezione e trasforma volume e salinità obiettivo in una quantità pratica. Funziona localmente nel browser.', methodTitle: 'Due lavori, due azioni', method: 'Scegli Prepara nuova acqua per una nuova miscela. Scegli Correggi la vasca per una misura sopra o sotto l’obiettivo. Per aumentare ottieni una massa di sale stimata; per diminuire ottieni il volume d’acqua da sostituire.', resultTitle: 'Leggi l’azione prima del numero', result: 'Pesa il sale stimato per la miscela o per un piccolo aumento. Per diminuire, rimuovi il volume indicato e sostituiscilo con acqua preparata. Controlla sempre con il tuo strumento.', tipTitle: 'Etichetta e misurazione prima di tutto', tip: '<p>Usa il dosaggio esatto della tua miscela. È una stima, non una garanzia per gli animali e non sostituisce rifrattometro, idrometro e correzioni graduali.</p>', faq: [['Posso usare una dose universale?', 'No. Inserisci il dosaggio della tua miscela. Prodotti e salinità obiettivo non sono intercambiabili.'], ['Cosa succede quando abbasso la salinità?', 'Stima l’acqua da rimuovere e sostituire con acqua meno salata usando un semplice bilancio del sale. Misura di nuovo.'], ['È per acquari d’acqua dolce?', 'Serve per preparare acqua salmastra o marina e correggere una salinità misurata. Non decide se aggiungere sale a un animale d’acqua dolce.'], ['Perché serve il dosaggio per aumentare?', 'Il dosaggio viene trattato come massa di prodotto per litro alla salinità obiettivo e ridimensionato sulla differenza misurata.'], ['I miei dati escono dal browser?', 'No. Il calcolo non richiede rete. Gli ultimi valori possono essere salvati localmente.']], howTo: [['Scegli la modalità', 'Seleziona Prepara nuova acqua oppure Correggi la vasca.'], ['Copia il dosaggio', 'Leggi l’etichetta e inserisci il valore nell’unità mostrata.'], ['Inserisci volume e salinità', 'Usa il volume reale da miscelare o il volume di lavoro della vasca.'], ['Verifica prima dell’uso', 'Sciogli bene, misura e correggi a piccoli passi.']], ui: { toolKicker: 'Sale misurato con calma', resultTitle: 'La tua prossima decisione sul sale', configureTitle: 'Parti dall’etichetta', modeLabel: 'Modalità di calcolo', prepareMode: 'Prepara nuova acqua', correctMode: 'Correggi la vasca', volumeLabel: 'Volume d’acqua', currentSalinityLabel: 'Salinità attuale', targetSalinityLabel: 'Salinità obiettivo', productRateLabel: 'Dosaggio sull’etichetta', presetLabel: 'Obiettivo rapido', brackishPreset: 'Salmastra 10', marinePreset: 'Marina 35', customPreset: 'Inizio personalizzato', prepareHint: 'Per una nuova miscela inserisci il volume reale e copia il dosaggio dalla confezione.', correctHint: 'Nella vasca esistente, aumentare aggiunge sale; diminuire sostituisce acqua.', productRateHint: 'Usa il dosaggio della tua miscela. Non esiste una dose universale.', prepareAction: 'Prepara questa miscela', raiseAction: 'Aggiungi sale gradualmente', lowerAction: 'Sostituisci acqua gradualmente', targetAction: 'Nessuna correzione necessaria', amountLabel: 'Quantità', saltAmountLabel: 'Sale da pesare', waterAmountLabel: 'Acqua da sostituire', verificationNote: 'Solo una stima. Sciogli, mescola e controlla con rifrattometro o idrometro.', resetLabel: 'Ripristina esempio' },
58
+ },
59
+ id: {
60
+ title: 'Kalkulator dosis garam akuarium', description: 'Hitung jumlah campuran garam untuk air baru atau perkirakan koreksi bertahap dengan takaran produk yang Anda gunakan.', introTitle: 'Ukur campuran, bukan dosis universal', intro: 'Produk garam berbeda-beda. Kalkulator ini memakai takaran pada label produk Anda, lalu mengubah volume air dan salinitas target menjadi jumlah yang dapat ditimbang. Semua perhitungan berjalan lokal di browser.', methodTitle: 'Dua pekerjaan, dua tindakan', method: 'Pilih Siapkan air baru untuk membuat campuran baru. Pilih Koreksi akuarium untuk nilai yang terukur di atas atau di bawah target. Untuk menaikkan, hasilnya adalah perkiraan massa garam; untuk menurunkan, hasilnya adalah volume air yang perlu diganti.', resultTitle: 'Baca tindakannya sebelum angkanya', result: 'Timbang garam untuk campuran atau kenaikan kecil. Untuk menurunkan salinitas, keluarkan volume yang dihitung dan ganti dengan air yang sudah disiapkan. Periksa kembali dengan alat ukur Anda.', tipTitle: 'Label produk dan pengukuran adalah acuan', tip: '<p>Gunakan takaran tepat dari campuran Anda. Ini hanya perkiraan, bukan jaminan keselamatan hewan dan bukan pengganti refraktometer, hidrometer, atau perubahan bertahap.</p>', faq: [['Bolehkah memakai dosis garam universal?', 'Tidak. Masukkan takaran dari campuran Anda sendiri. Produk dan target salinitas tidak dapat dipertukarkan begitu saja.'], ['Apa yang terjadi saat salinitas diturunkan?', 'Kalkulator memperkirakan air yang harus dikeluarkan dan diganti dengan air yang lebih rendah salinitasnya menggunakan keseimbangan garam sederhana.'], ['Apakah ini untuk akuarium air tawar?', 'Ini untuk menyiapkan air payau atau laut dan memperbaiki salinitas yang terukur. Kalkulator tidak menentukan penggunaan garam untuk hewan air tawar.'], ['Mengapa takaran produk dibutuhkan saat menaikkan salinitas?', 'Takaran dianggap sebagai massa produk per liter pada salinitas target, lalu disesuaikan dengan selisih pengukuran.'], ['Apakah data saya keluar dari browser?', 'Tidak. Perhitungan tidak memerlukan jaringan. Nilai terakhir dapat disimpan secara lokal.']], howTo: [['Pilih mode', 'Pilih Siapkan air baru atau Koreksi akuarium.'], ['Salin takaran produk', 'Baca label dan masukkan nilai pada satuan yang ditampilkan.'], ['Masukkan volume dan salinitas', 'Gunakan volume air yang benar-benar dicampur atau volume kerja akuarium.'], ['Periksa sebelum digunakan', 'Larutkan, campur, ukur, lalu lakukan koreksi sedikit demi sedikit.']], ui: { toolKicker: 'Garam diukur dengan tenang', resultTitle: 'Keputusan garam Anda berikutnya', configureTitle: 'Mulai dari label produk', modeLabel: 'Mode perhitungan', prepareMode: 'Siapkan air baru', correctMode: 'Koreksi akuarium', volumeLabel: 'Volume air', currentSalinityLabel: 'Salinitas saat ini', targetSalinityLabel: 'Salinitas target', productRateLabel: 'Takaran pada label', presetLabel: 'Target cepat', brackishPreset: 'Payau 10', marinePreset: 'Laut 35', customPreset: 'Mulai sendiri', prepareHint: 'Untuk campuran baru, masukkan volume air dan takaran dari label produk.', correctHint: 'Pada akuarium yang ada, menaikkan menambah garam; menurunkan mengganti air.', productRateHint: 'Gunakan takaran campuran yang tepat. Tidak ada dosis universal.', prepareAction: 'Siapkan campuran ini', raiseAction: 'Tambahkan garam bertahap', lowerAction: 'Ganti air bertahap', targetAction: 'Tidak perlu koreksi', amountLabel: 'Jumlah', saltAmountLabel: 'Garam yang ditimbang', waterAmountLabel: 'Air yang diganti', verificationNote: 'Hanya perkiraan. Larutkan dan campur sepenuhnya, lalu periksa dengan refraktometer atau hidrometer.', resetLabel: 'Atur ulang contoh' },
61
+ },
62
+ ja: {
63
+ title: '水槽の塩分量計算機', description: '使用する製品の表示量をもとに、新しい水に必要な塩の量や、既存水槽の塩分を段階的に調整する量を計算します。', introTitle: '万能量ではなく製品を測る', intro: '塩製品の配合はそれぞれ異なります。この計算機は製品ラベルの量と実際の水量、目標塩分を使い、量って使える結果に変換します。計算はブラウザ内で完了します。', methodTitle: '目的に合わせた二つの計算', method: '新しい水を準備する場合は新しい水を準備を選びます。水槽を測定して目標より高いか低い場合は水槽を調整を選びます。上げる場合は塩の質量、下げる場合は交換する水量を示します。', resultTitle: '数字より先に行動を見る', result: '新しい水や少し上げる調整では塩を量ります。下げる調整では計算された水量を取り出し、準備した水に交換します。使用する測定器で必ず確認してください。', tipTitle: 'ラベルと実測値を優先する', tip: '<p>使用する製品の表示量を入力してください。結果は推定値であり、生体の安全を保証するものでも、屈折計や比重計、段階的な変更の代わりでもありません。</p>', faq: [['万能の塩量を使えますか?', 'いいえ。使用する製品のラベルにある量を入力してください。製品や目標塩分は同じではありません。'], ['塩分を下げるときはどうなりますか?', '簡単な塩分収支から、取り出して低い塩分の水に交換する量を推定します。交換後に再測定してください。'], ['淡水水槽用ですか?', '汽水や海水の準備、測定した塩分の調整を想定しています。淡水の生体に塩を入れる判断はしません。'], ['上げるときに製品量が必要なのはなぜですか?', 'ラベルの量を目標塩分での1リットルあたりの製品量として扱い、差分に合わせて計算します。'], ['データはブラウザの外に送られますか?', 'いいえ。計算に通信は不要です。最後の入力値はこのブラウザに保存できます。']], howTo: [['モードを選ぶ', '新しい水を準備または水槽を調整を選びます。'], ['製品量を入力する', 'ラベルを読み、表示された単位で入力します。'], ['水量と塩分を入力する', '実際に混ぜる水量、または水槽の現在の水量を使います。'], ['使用前に確認する', '完全に溶かして混ぜ、測定し、少しずつ調整します。']], ui: { toolKicker: '塩を落ち着いて測る', resultTitle: '次に行う塩分調整', configureTitle: '製品ラベルから開始', modeLabel: '計算モード', prepareMode: '新しい水を準備', correctMode: '水槽を調整', volumeLabel: '水量', currentSalinityLabel: '現在の塩分', targetSalinityLabel: '目標塩分', productRateLabel: '製品ラベルの量', presetLabel: '目標プリセット', brackishPreset: '汽水 10', marinePreset: '海水 35', customPreset: '自分で入力', prepareHint: '新しい水には、混ぜる水量と製品ラベルの量を入力します。', correctHint: '既存水槽では、上げる場合は塩を加え、下げる場合は水を交換します。', productRateHint: '使用する製品の表示量を入力してください。万能の量はありません。', prepareAction: 'この水を準備', raiseAction: '塩を少しずつ加える', lowerAction: '水を少しずつ交換', targetAction: '調整は不要', amountLabel: '量', saltAmountLabel: '量る塩', waterAmountLabel: '交換する水', verificationNote: '推定値です。完全に溶かして混ぜ、屈折計または比重計で確認してください。', resetLabel: '例に戻す' },
64
+ },
65
+ ko: {
66
+ title: '수족관 소금 투입량 계산기', description: '사용 중인 제품의 표시 비율로 새 물에 넣을 소금 양과 기존 수조의 염도를 단계적으로 조정할 양을 계산합니다.', introTitle: '보편적인 양이 아니라 제품을 측정하세요', intro: '소금 제품은 서로 다릅니다. 이 계산기는 제품 라벨의 비율과 실제 물의 양, 목표 염도를 사용해 저울로 잴 수 있는 양을 계산합니다. 계산은 브라우저에서 로컬로 처리됩니다.', methodTitle: '목적에 맞는 두 가지 결과', method: '새 물을 준비할 때는 새 물 준비를 선택합니다. 측정한 수조가 목표보다 높거나 낮을 때는 수조 보정을 선택합니다. 염도를 올릴 때는 소금 질량을, 낮출 때는 교체할 물의 양을 보여줍니다.', resultTitle: '숫자보다 행동을 먼저 확인하세요', result: '새 물이나 작은 상승 보정에는 계산된 소금을 계량합니다. 하강 보정에는 계산된 물을 빼고 준비한 물로 교체합니다. 사용 중인 측정기로 항상 확인하세요.', tipTitle: '라벨과 측정값을 우선하세요', tip: '<p>사용하는 제품의 정확한 표시 비율을 입력하세요. 결과는 추정치이며 생물의 안전을 보장하거나 굴절계, 비중계, 점진적인 조정을 대신하지 않습니다.</p>', faq: [['보편적인 소금 양을 사용할 수 있나요?', '아니요. 사용하는 제품 라벨의 비율을 입력하세요. 제품과 목표 염도는 서로 대체할 수 없습니다.'], ['염도를 낮추면 어떻게 되나요?', '간단한 소금 수지로 빼고 낮은 염도의 물로 교체할 양을 추정합니다. 교체 후 다시 측정하세요.'], ['담수 수조용인가요?', '기수나 해수 준비와 측정된 염도 보정을 위한 도구입니다. 담수 생물에게 소금을 넣을지는 판단하지 않습니다.'], ['올릴 때 제품 비율이 필요한 이유는 무엇인가요?', '라벨 비율을 목표 염도에서 1리터에 필요한 제품 질량으로 보고 차이에 맞춰 계산합니다.'], ['데이터가 브라우저 밖으로 전송되나요?', '아니요. 계산에 네트워크가 필요하지 않습니다. 마지막 입력값은 브라우저에 로컬 저장될 수 있습니다.']], howTo: [['모드 선택', '새 물 준비 또는 수조 보정을 선택합니다.'], ['제품 비율 입력', '라벨을 읽고 표시된 단위로 입력합니다.'], ['물의 양과 염도 입력', '실제로 섞을 물의 양 또는 현재 수조의 작업 물량을 사용합니다.'], ['사용 전 확인', '완전히 녹이고 섞은 뒤 측정하며 조금씩 조정합니다.']], ui: { toolKicker: '차분하게 계량하는 소금', resultTitle: '다음 소금 조정 결정', configureTitle: '제품 라벨부터 시작', modeLabel: '계산 모드', prepareMode: '새 물 준비', correctMode: '수조 보정', volumeLabel: '물의 양', currentSalinityLabel: '현재 염도', targetSalinityLabel: '목표 염도', productRateLabel: '제품 라벨 비율', presetLabel: '빠른 목표', brackishPreset: '기수 10', marinePreset: '해수 35', customPreset: '직접 입력', prepareHint: '새 물을 만들 때 실제 물의 양과 제품 라벨의 비율을 입력합니다.', correctHint: '기존 수조는 올릴 때 소금을 넣고 낮출 때 물을 교체합니다.', productRateHint: '정확한 제품 비율을 사용하세요. 보편적인 투입량은 없습니다.', prepareAction: '이 물을 준비', raiseAction: '소금을 조금씩 추가', lowerAction: '물을 조금씩 교체', targetAction: '보정 불필요', amountLabel: '양', saltAmountLabel: '계량할 소금', waterAmountLabel: '교체할 물', verificationNote: '추정치입니다. 완전히 녹이고 섞은 뒤 굴절계나 비중계로 확인하세요.', resetLabel: '예시로 재설정' },
67
+ },
68
+ nl: {
69
+ title: 'Zoutdosering calculator voor aquaria', description: 'Bereken hoeveel zout je moet afwegen voor nieuw aquariumwater of schat een geleidelijke correctie met de dosering van jouw product.', introTitle: 'Meet je mengsel, niet een universele dosis', intro: 'Zoutmengsels verschillen. Deze calculator gebruikt de dosering op jouw verpakking en zet watervolume en doelzoutgehalte om in een praktische hoeveelheid. De berekening blijft lokaal in de browser.', methodTitle: 'Twee taken, twee acties', method: 'Kies Nieuw water bereiden voor een nieuwe batch. Kies Aquarium corrigeren wanneer een meting boven of onder het doel ligt. Voor verhogen krijg je een geschatte zoutmassa; voor verlagen het watervolume dat je moet vervangen.', resultTitle: 'Lees eerst de actie', result: 'Weeg de geschatte hoeveelheid voor een nieuwe batch of kleine verhoging. Voor verlagen haal je het berekende volume weg en vervang je het door voorbereid water. Controleer altijd opnieuw met je eigen meetinstrument.', tipTitle: 'Etiket en meting gaan voor', tip: '<p>Gebruik de exacte dosering van je eigen mengsel. Dit is een schatting, geen veiligheidsgarantie en geen vervanging voor een refractometer, hydrometer of geleidelijke correctie.</p>', faq: [['Kan ik een universele zoutdosis gebruiken?', 'Nee. Vul de dosering van jouw mengsel in. Producten en doelwaarden zijn niet zomaar uitwisselbaar.'], ['Wat gebeurt er als ik het zoutgehalte verlaag?', 'De calculator schat met een eenvoudige zoutbalans hoeveel water je vervangt door water met een lagere saliniteit. Meet daarna opnieuw.'], ['Is dit voor zoetwateraquaria?', 'De tool is bedoeld voor brak of zeewater en gemeten saliniteitscorrecties. Hij bepaalt niet of een zoetwaterdier zout nodig heeft.'], ['Waarom is de productdosering nodig bij verhogen?', 'De etiketwaarde wordt gezien als productmassa per liter bij de doelwaarde en aangepast aan het gemeten verschil.'], ['Gaan mijn gegevens uit de browser?', 'Nee. De berekening heeft geen netwerk nodig. De laatste waarden kunnen lokaal worden bewaard.']], howTo: [['Kies een modus', 'Selecteer Nieuw water bereiden of Aquarium corrigeren.'], ['Lees het etiket', 'Vul de productdosering in de getoonde eenheid in.'], ['Vul volume en saliniteit in', 'Gebruik het werkelijke mengvolume of het huidige watervolume.'], ['Controleer voor gebruik', 'Los volledig op, meet en corrigeer in kleine stappen.']], ui: { toolKicker: 'Zout rustig afgemeten', resultTitle: 'Je volgende zoutbeslissing', configureTitle: 'Begin met het productetiket', modeLabel: 'Rekenmodus', prepareMode: 'Nieuw water bereiden', correctMode: 'Aquarium corrigeren', volumeLabel: 'Watervolume', currentSalinityLabel: 'Huidige saliniteit', targetSalinityLabel: 'Doelsaliniteit', productRateLabel: 'Dosering op etiket', presetLabel: 'Snel doel', brackishPreset: 'Brak 10', marinePreset: 'Zee 35', customPreset: 'Eigen start', prepareHint: 'Voer voor een nieuwe batch het mengvolume en de dosering van het etiket in.', correctHint: 'In een bestaand aquarium verhoog je met zout en verlaag je door water te vervangen.', productRateHint: 'Gebruik de exacte productdosering. Er is geen universele dosis.', prepareAction: 'Deze batch bereiden', raiseAction: 'Zout geleidelijk toevoegen', lowerAction: 'Water geleidelijk vervangen', targetAction: 'Geen correctie nodig', amountLabel: 'Hoeveelheid', saltAmountLabel: 'Af te wegen zout', waterAmountLabel: 'Te vervangen water', verificationNote: 'Slechts een schatting. Los op, meng en controleer met refractometer of hydrometer.', resetLabel: 'Voorbeeld herstellen' },
70
+ },
71
+ pl: {
72
+ title: 'Kalkulator dawkowania soli do akwarium', description: 'Oblicz ilość mieszanki soli do przygotowania nowej wody albo oszacuj stopniową korektę na podstawie dawki własnego produktu.', introTitle: 'Mierz własną mieszankę, nie uniwersalną dawkę', intro: 'Mieszanki soli różnią się składem. Kalkulator korzysta z dawki podanej na Twoim opakowaniu i zamienia objętość wody oraz zasolenie docelowe na ilość do odważenia. Działa lokalnie w przeglądarce.', methodTitle: 'Dwa zadania, dwa działania', method: 'Wybierz Przygotuj nową wodę dla nowej partii. Wybierz Skoryguj akwarium, gdy zmierzone zasolenie jest wyższe lub niższe od celu. Przy podnoszeniu otrzymasz masę soli, a przy obniżaniu objętość wody do wymiany.', resultTitle: 'Najpierw przeczytaj działanie', result: 'Odważ sól dla nowej partii lub małej korekty w górę. Przy obniżaniu usuń podaną objętość i zastąp ją przygotowaną wodą. Zawsze sprawdź wynik własnym przyrządem.', tipTitle: 'Etykieta i pomiar są najważniejsze', tip: '<p>Użyj dokładnej dawki swojej mieszanki. To szacunek, a nie gwarancja bezpieczeństwa zwierząt ani zamiennik refraktometru, areometru i stopniowych zmian.</p>', faq: [['Czy mogę użyć uniwersalnej dawki soli?', 'Nie. Wpisz dawkę swojej mieszanki. Produkty i zasolenia docelowe nie są zamienne.'], ['Co dzieje się przy obniżaniu zasolenia?', 'Kalkulator szacuje ilość wody do usunięcia i zastąpienia wodą o niższym zasoleniu na podstawie prostego bilansu soli.'], ['Czy narzędzie służy do akwariów słodkowodnych?', 'Służy do wody słonawej lub morskiej oraz do korekty zmierzonego zasolenia. Nie ocenia potrzeby soli u zwierząt słodkowodnych.'], ['Dlaczego przy podnoszeniu potrzebna jest dawka produktu?', 'Dawka z etykiety jest traktowana jako masa produktu na litr przy zasoleniu docelowym i skalowana do różnicy pomiaru.'], ['Czy dane opuszczają przeglądarkę?', 'Nie. Obliczenia nie wymagają sieci, a ostatnie wartości mogą zostać zapisane lokalnie.']], howTo: [['Wybierz tryb', 'Wybierz Przygotuj nową wodę albo Skoryguj akwarium.'], ['Przepisz dawkę', 'Odczytaj etykietę i wpisz wartość w pokazanej jednostce.'], ['Wpisz objętość i zasolenie', 'Użyj rzeczywistej objętości mieszania lub objętości roboczej akwarium.'], ['Sprawdź przed użyciem', 'Rozpuść, wymieszaj, zmierz i koryguj małymi krokami.']], ui: { toolKicker: 'Sól odmierzona spokojnie', resultTitle: 'Następna decyzja o soli', configureTitle: 'Zacznij od etykiety produktu', modeLabel: 'Tryb obliczeń', prepareMode: 'Przygotuj nową wodę', correctMode: 'Skoryguj akwarium', volumeLabel: 'Objętość wody', currentSalinityLabel: 'Obecne zasolenie', targetSalinityLabel: 'Zasolenie docelowe', productRateLabel: 'Dawka z etykiety', presetLabel: 'Szybki cel', brackishPreset: 'Słonawa 10', marinePreset: 'Morska 35', customPreset: 'Własny start', prepareHint: 'Dla nowej partii wpisz objętość wody i dawkę z opakowania.', correctHint: 'W istniejącym akwarium podnoszenie dodaje sól, a obniżanie wymaga wymiany wody.', productRateHint: 'Użyj dawki swojej mieszanki. Nie ma jednej dawki uniwersalnej.', prepareAction: 'Przygotuj tę partię', raiseAction: 'Dodaj sól stopniowo', lowerAction: 'Wymieniaj wodę stopniowo', targetAction: 'Korekta niepotrzebna', amountLabel: 'Ilość', saltAmountLabel: 'Sól do odważenia', waterAmountLabel: 'Woda do wymiany', verificationNote: 'To tylko szacunek. Rozpuść, wymieszaj i sprawdź refraktometrem lub areometrem.', resetLabel: 'Przywróć przykład' },
73
+ },
74
+ pt: {
75
+ title: 'Calculadora de dosagem de sal para aquário', description: 'Calcule quanto de mistura de sal pesar para preparar água nova ou estime uma correção gradual usando a dosagem do seu produto.', introTitle: 'Meça a sua mistura, não uma dose universal', intro: 'As misturas de sal variam. A calculadora usa a dosagem indicada na sua embalagem e transforma o volume de água e a salinidade alvo numa quantidade prática. O cálculo é feito localmente no navegador.', methodTitle: 'Duas tarefas, duas ações', method: 'Escolha Preparar água nova para um lote novo. Escolha Corrigir aquário quando a medição estiver acima ou abaixo do alvo. Para subir, recebe uma massa de sal; para baixar, o volume de água a substituir.', resultTitle: 'Leia a ação antes do número', result: 'Pese o sal estimado para o lote ou para uma pequena subida. Para baixar, retire o volume indicado e substitua por água preparada. Confirme sempre com o seu instrumento.', tipTitle: 'A etiqueta e a medição prevalecem', tip: '<p>Use a dosagem exata da sua mistura. É uma estimativa, não uma garantia de segurança nem substitui refratómetro, densímetro e alterações graduais.</p>', faq: [['Posso usar uma dose universal?', 'Não. Introduza a dosagem da sua mistura. Produtos e salinidades alvo não são intercambiáveis.'], ['O que acontece ao baixar a salinidade?', 'A ferramenta estima a água a retirar e substituir por água menos salgada através de um balanço simples. Meça novamente depois.'], ['Serve para aquários de água doce?', 'Destina-se a água salobra ou marinha e a correções de salinidade medida. Não decide se um animal de água doce deve receber sal.'], ['Por que é necessária a dosagem ao subir?', 'A dosagem da etiqueta é tratada como massa por litro na salinidade alvo e ajustada à diferença medida.'], ['Os meus dados saem do navegador?', 'Não. O cálculo não precisa de rede. Os últimos valores podem ser guardados localmente.']], howTo: [['Escolha o modo', 'Selecione Preparar água nova ou Corrigir aquário.'], ['Leia a embalagem', 'Introduza a dosagem na unidade apresentada.'], ['Indique volume e salinidade', 'Use o volume real a misturar ou o volume de trabalho do aquário.'], ['Confirme antes de usar', 'Dissolva, misture, meça e corrija em passos pequenos.']], ui: { toolKicker: 'Sal medido com calma', resultTitle: 'A sua próxima decisão sobre sal', configureTitle: 'Comece pela etiqueta', modeLabel: 'Modo de cálculo', prepareMode: 'Preparar água nova', correctMode: 'Corrigir aquário', volumeLabel: 'Volume de água', currentSalinityLabel: 'Salinidade atual', targetSalinityLabel: 'Salinidade alvo', productRateLabel: 'Dosagem da etiqueta', presetLabel: 'Alvo rápido', brackishPreset: 'Salobra 10', marinePreset: 'Marinha 35', customPreset: 'Início próprio', prepareHint: 'Para um lote novo, indique o volume e copie a dosagem da embalagem.', correctHint: 'Num aquário existente, subir adiciona sal e baixar exige substituir água.', productRateHint: 'Use a dosagem da sua mistura. Não existe uma dose universal.', prepareAction: 'Preparar este lote', raiseAction: 'Adicionar sal gradualmente', lowerAction: 'Substituir água gradualmente', targetAction: 'Não é necessária correção', amountLabel: 'Quantidade', saltAmountLabel: 'Sal a pesar', waterAmountLabel: 'Água a substituir', verificationNote: 'Apenas uma estimativa. Dissolva, misture e confirme com refratómetro ou densímetro.', resetLabel: 'Repor exemplo' },
76
+ },
77
+ ru: {
78
+ title: 'Калькулятор дозировки соли для аквариума', description: 'Рассчитайте количество солевой смеси для новой воды или постепенной коррекции аквариума по дозировке вашего продукта.', introTitle: 'Измеряйте свою смесь, а не универсальную дозу', intro: 'Солевые смеси отличаются. Калькулятор использует дозировку с вашей упаковки и переводит объём воды и целевую солёность в количество для взвешивания. Расчёт выполняется локально в браузере.', methodTitle: 'Две задачи, два действия', method: 'Выберите Приготовить новую воду для новой порции. Выберите Скорректировать аквариум, если измеренная солёность выше или ниже цели. Для повышения будет рассчитана масса соли, для снижения объём воды на замену.', resultTitle: 'Сначала смотрите на действие', result: 'Взвесьте соль для новой порции или небольшого повышения. Для снижения удалите рассчитанный объём и замените его подготовленной водой. Всегда проверяйте результат своим прибором.', tipTitle: 'Главные ориентиры: этикетка и измерение', tip: '<p>Используйте точную дозировку своей смеси. Это оценка, а не гарантия безопасности животных и не замена рефрактометру, ареометру или постепенным изменениям.</p>', faq: [['Можно ли использовать универсальную дозу соли?', 'Нет. Введите дозировку именно вашей смеси. Продукты и целевая солёность не взаимозаменяемы.'], ['Что происходит при снижении солёности?', 'Калькулятор оценивает объём воды для удаления и замены водой с меньшей солёностью по простому балансу соли.'], ['Подходит ли инструмент для пресной воды?', 'Он предназначен для солоноватой или морской воды и коррекции измеренной солёности. Он не решает, нужна ли соль пресноводному животному.'], ['Зачем нужна дозировка продукта при повышении?', 'Дозировка рассматривается как масса продукта на литр при целевой солёности и масштабируется по разнице измерений.'], ['Уходят ли мои данные из браузера?', 'Нет. Для расчёта сеть не нужна. Последние значения могут сохраняться локально.']], howTo: [['Выберите режим', 'Выберите Приготовить новую воду или Скорректировать аквариум.'], ['Перенесите дозировку', 'Считайте значение с этикетки и введите его в показанной единице.'], ['Введите объём и солёность', 'Используйте реальный объём смеси или рабочий объём аквариума.'], ['Проверьте перед использованием', 'Растворите, перемешайте, измерьте и корректируйте малыми шагами.']], ui: { toolKicker: 'Соль измерена спокойно', resultTitle: 'Следующее решение по соли', configureTitle: 'Начните с этикетки продукта', modeLabel: 'Режим расчёта', prepareMode: 'Приготовить новую воду', correctMode: 'Скорректировать аквариум', volumeLabel: 'Объём воды', currentSalinityLabel: 'Текущая солёность', targetSalinityLabel: 'Целевая солёность', productRateLabel: 'Дозировка на этикетке', presetLabel: 'Быстрая цель', brackishPreset: 'Солоноватая 10', marinePreset: 'Морская 35', customPreset: 'Свой вариант', prepareHint: 'Для новой порции введите объём воды и дозировку с упаковки.', correctHint: 'В существующем аквариуме повышение добавляет соль, а снижение требует замены воды.', productRateHint: 'Используйте дозировку своей смеси. Универсальной дозы нет.', prepareAction: 'Приготовить эту порцию', raiseAction: 'Добавлять соль постепенно', lowerAction: 'Постепенно менять воду', targetAction: 'Коррекция не нужна', amountLabel: 'Количество', saltAmountLabel: 'Соли взвесить', waterAmountLabel: 'Воды заменить', verificationNote: 'Только оценка. Растворите, перемешайте и проверьте рефрактометром или ареометром.', resetLabel: 'Сбросить пример' },
79
+ },
80
+ sv: {
81
+ title: 'Kalkylator för akvariesalt', description: 'Beräkna hur mycket saltblandning du ska väga för nytt vatten eller uppskatta en gradvis korrigering med doseringen på din produkt.', introTitle: 'Mät din blandning, inte en universell dos', intro: 'Saltblandningar skiljer sig åt. Kalkylatorn använder mängden på din egen förpackning och omvandlar vattenvolym och målsalthalt till en praktisk mängd. Beräkningen sker lokalt i webbläsaren.', methodTitle: 'Två uppgifter, två åtgärder', method: 'Välj Förbered nytt vatten för en ny sats. Välj Korrigera akvarium när den uppmätta salthalten ligger över eller under målet. För att höja får du en uppskattad saltmassa; för att sänka får du volymen vatten som ska bytas.', resultTitle: 'Läs åtgärden före siffran', result: 'Väg saltet för den nya satsen eller en liten höjning. För att sänka tar du bort den beräknade volymen och ersätter den med förberett vatten. Kontrollera alltid med ditt eget mätinstrument.', tipTitle: 'Etikett och mätning gäller', tip: '<p>Använd den exakta doseringen för din blandning. Detta är en uppskattning, inte en säkerhetsgaranti och inte en ersättning för refraktometer, hydrometer eller gradvisa ändringar.</p>', faq: [['Kan jag använda en universell saltdos?', 'Nej. Ange doseringen för din egen blandning. Produkter och mål kan inte bytas mot varandra.'], ['Vad händer när salthalten sänks?', 'Kalkylatorn uppskattar vilket vatten som ska tas bort och ersättas med vatten med lägre salthalt genom en enkel saltbalans.'], ['Är den till för sötvatten?', 'Den är avsedd för bräckt eller marint vatten och uppmätta salthaltskorrigeringar. Den avgör inte om ett sötvattensdjur ska få salt.'], ['Varför behövs produktdosen när salthalten höjs?', 'Etikettens dos behandlas som produktmassa per liter vid målsalthalten och skalas efter den uppmätta skillnaden.'], ['Lämnar mina data webbläsaren?', 'Nej. Beräkningen kräver inget nätverk. De senaste värdena kan sparas lokalt.']], howTo: [['Välj läge', 'Välj Förbered nytt vatten eller Korrigera akvarium.'], ['Läs av etiketten', 'Ange doseringen i den visade enheten.'], ['Ange volym och salthalt', 'Använd den verkliga blandningsvolymen eller akvariets arbetsvolym.'], ['Kontrollera före användning', 'Lös upp, blanda, mät och korrigera i små steg.']], ui: { toolKicker: 'Salt lugnt uppmätt', resultTitle: 'Ditt nästa saltbeslut', configureTitle: 'Börja med produktetiketten', modeLabel: 'Beräkningsläge', prepareMode: 'Förbered nytt vatten', correctMode: 'Korrigera akvarium', volumeLabel: 'Vattenvolym', currentSalinityLabel: 'Aktuell salthalt', targetSalinityLabel: 'Målsalthalt', productRateLabel: 'Dosering på etiketten', presetLabel: 'Snabbt mål', brackishPreset: 'Bräckt 10', marinePreset: 'Marint 35', customPreset: 'Eget värde', prepareHint: 'För en ny sats anger du volymen och doseringen från förpackningen.', correctHint: 'I ett befintligt akvarium höjer du med salt och sänker genom vattenbyte.', productRateHint: 'Använd den exakta doseringen. Det finns ingen universell dos.', prepareAction: 'Förbered denna sats', raiseAction: 'Tillsätt salt gradvis', lowerAction: 'Byt vatten gradvis', targetAction: 'Ingen korrigering behövs', amountLabel: 'Mängd', saltAmountLabel: 'Salt att väga', waterAmountLabel: 'Vatten att byta', verificationNote: 'Endast en uppskattning. Lös upp, blanda och kontrollera med refraktometer eller hydrometer.', resetLabel: 'Återställ exempel' },
82
+ },
83
+ tr: {
84
+ title: 'Akvaryum tuzu dozaj hesaplayıcı', description: 'Yeni su hazırlamak için tartılacak tuz karışımını veya kendi ürün etiketinizdeki oranla mevcut tank için kademeli düzeltmeyi hesaplayın.', introTitle: 'Evrensel dozu değil, kendi karışımınızı ölçün', intro: 'Tuz karışımları birbirinden farklıdır. Bu hesaplayıcı ürün etiketinizdeki oranı kullanır ve su hacmi ile hedef tuzluluğu tartılabilir bir miktara dönüştürür. Hesaplama tarayıcıda yerel olarak yapılır.', methodTitle: 'İki iş, iki eylem', method: 'Yeni bir karışım için Yeni su hazırla seçeneğini kullanın. Ölçülen değer hedefin üstünde veya altındaysa Tankı düzelt seçeneğini seçin. Yükseltmek için tuz kütlesi, düşürmek için değiştirilecek su hacmi hesaplanır.', resultTitle: 'Önce eylemi okuyun', result: 'Yeni karışım veya küçük bir yükseltme için tuzu tartın. Düşürmek için hesaplanan suyu çıkarıp hazırlanmış suyla değiştirin. Sonucu kendi ölçüm cihazınızla mutlaka kontrol edin.', tipTitle: 'Etiket ve ölçüm önceliklidir', tip: '<p>Kendi karışımınızın doğru oranını kullanın. Bu bir tahmindir; canlı güvenliği garantisi değildir ve refraktometre, hidrometre veya kademeli değişimin yerine geçmez.</p>', faq: [['Evrensel bir tuz dozu kullanabilir miyim?', 'Hayır. Kullandığınız karışımın oranını girin. Ürünler ve hedef tuzluluklar birbirinin yerine kullanılamaz.'], ['Tuzluluğu düşürünce ne olur?', 'Basit bir tuz dengesiyle çıkarılıp daha düşük tuzluluktaki suyla değiştirilecek hacim tahmin edilir. Sonra yeniden ölçün.'], ['Tatlı su akvaryumları için mi?', 'Acı veya deniz suyu hazırlama ve ölçülmüş tuzluluk düzeltmeleri içindir. Tatlı su canlısına tuz gerekip gerekmediğine karar vermez.'], ['Yükseltme için ürün oranı neden gerekli?', 'Etiket oranı hedef tuzlulukta litre başına ürün kütlesi kabul edilir ve ölçülen farka göre ölçeklenir.'], ['Verilerim tarayıcıdan çıkar mı?', 'Hayır. Hesaplama için ağ gerekmez. Son değerler tarayıcıda yerel olarak saklanabilir.']], howTo: [['Mod seçin', 'Yeni su hazırla veya Tankı düzelt seçeneğini seçin.'], ['Ürün oranını okuyun', 'Etiketteki değeri gösterilen birimde girin.'], ['Hacim ve tuzluluğu girin', 'Gerçekte karıştıracağınız hacmi veya tankın çalışma hacmini kullanın.'], ['Kullanmadan önce doğrulayın', 'Tamamen çözün, karıştırın, ölçün ve küçük adımlarla düzeltin.']], ui: { toolKicker: 'Tuz sakin biçimde ölçülür', resultTitle: 'Sıradaki tuz kararınız', configureTitle: 'Ürün etiketiyle başlayın', modeLabel: 'Hesaplama modu', prepareMode: 'Yeni su hazırla', correctMode: 'Tankı düzelt', volumeLabel: 'Su hacmi', currentSalinityLabel: 'Mevcut tuzluluk', targetSalinityLabel: 'Hedef tuzluluk', productRateLabel: 'Etiketteki ürün oranı', presetLabel: 'Hızlı hedef', brackishPreset: 'Acı su 10', marinePreset: 'Deniz 35', customPreset: 'Özel başlangıç', prepareHint: 'Yeni karışım için su hacmini ve ürün etiketindeki oranı girin.', correctHint: 'Mevcut tankta yükseltmek tuz ekler, düşürmek su değişimi gerektirir.', productRateHint: 'Tam ürün oranını kullanın. Evrensel doz yoktur.', prepareAction: 'Bu karışımı hazırla', raiseAction: 'Tuzu kademeli ekle', lowerAction: 'Suyu kademeli değiştir', targetAction: 'Düzeltme gerekmiyor', amountLabel: 'Miktar', saltAmountLabel: 'Tartılacak tuz', waterAmountLabel: 'Değiştirilecek su', verificationNote: 'Yalnızca tahmindir. Çözün, karıştırın ve refraktometre veya hidrometreyle kontrol edin.', resetLabel: 'Örneği sıfırla' },
85
+ },
86
+ zh: {
87
+ title: '水族箱海水盐量计算工具', description: '根据你使用的产品标签比例,计算新水需要称取的盐量,或估算现有水族箱的渐进式盐度调整。', introTitle: '测量你的盐混合物,而不是套用通用剂量', intro: '不同盐产品的配方并不相同。本工具使用你手上产品的标签比例,把实际水量和目标盐度转换成可以称量的数量。计算在浏览器本地完成。', methodTitle: '两种任务,两种动作', method: '准备新水时选择准备新水。已有水族箱的测量值高于或低于目标时选择调整水族箱。提高盐度会给出盐的质量,降低盐度会给出需要更换的水量。', resultTitle: '先看动作,再看数字', result: '准备新水或小幅提高盐度时,称取计算出的盐。降低盐度时,取出计算出的水量并换入已经准备好的水。使用自己的测量仪器再次确认。', tipTitle: '以标签和实际测量为准', tip: '<p>请输入你使用的盐产品的准确比例。这只是估算值,不是生物安全保证,也不能替代折射仪、比重计和渐进式调整。</p>', faq: [['可以使用通用盐量吗?', '不可以。请输入你使用的盐产品标签比例。不同产品和目标盐度不能直接互换。'], ['降低盐度时会发生什么?', '工具根据简单的盐平衡估算需要取出并换成低盐度水的体积。更换后请重新测量。'], ['适合淡水水族箱吗?', '它用于准备汽水或海水,以及调整已经测量的盐度。不判断淡水生物是否需要加盐。'], ['为什么提高盐度需要产品比例?', '工具把标签比例视为目标盐度下每升所需的产品质量,再根据测量差值进行缩放。'], ['数据会离开浏览器吗?', '不会。计算不需要网络。最近一次输入可以保存在本地浏览器中。']], howTo: [['选择模式', '选择准备新水或调整水族箱。'], ['读取产品比例', '查看标签,并按显示的单位输入数值。'], ['输入水量和盐度', '使用实际混合水量或水族箱当前的工作水量。'], ['使用前确认', '充分溶解并混合,测量后再分小步调整。']], ui: { toolKicker: '平静地称量盐', resultTitle: '下一步盐度决定', configureTitle: '先看产品标签', modeLabel: '计算模式', prepareMode: '准备新水', correctMode: '调整水族箱', volumeLabel: '水量', currentSalinityLabel: '当前盐度', targetSalinityLabel: '目标盐度', productRateLabel: '产品标签比例', presetLabel: '快速目标', brackishPreset: '汽水 10', marinePreset: '海水 35', customPreset: '自定义起点', prepareHint: '准备新水时输入实际水量,并复制产品标签上的比例。', correctHint: '现有水族箱提高盐度要加盐,降低盐度要换水。', productRateHint: '使用准确的产品比例。没有通用剂量。', prepareAction: '准备这批水', raiseAction: '逐步添加盐', lowerAction: '逐步更换水', targetAction: '无需调整', amountLabel: '数量', saltAmountLabel: '需要称取的盐', waterAmountLabel: '需要更换的水', verificationNote: '仅为估算值。完全溶解混合后,请用折射仪或比重计确认。', resetLabel: '重置示例' },
88
+ },
89
+ };
90
+
91
+ const fallbackCopy: LocaleCopy = copies.en!;
92
+
93
+ const spanishSeoOverrides: Partial<LocaleCopy> = {
94
+ description: 'Calcula cuánta sal pesa tu mezcla para preparar agua nueva y descubre cuánto añadir o sustituir al corregir la salinidad de un acuario.',
95
+ introTitle: 'Calcula con la etiqueta de tu mezcla',
96
+ intro: 'No existe una cucharada universal de sal: cada producto concentra sus sales de forma distinta. Introduce los gramos por litro que aparecen en tu envase, el volumen real de agua y la salinidad que quieres conseguir. Así obtienes una cifra trazable a tu producto, no una recomendación genérica copiada de otro acuario.',
97
+ methodTitle: 'Tres escenarios, tres decisiones',
98
+ method: 'Para preparar agua nueva, multiplica el volumen por la proporción de la etiqueta: 100 litros a 36 g/L son 3.600 g de mezcla. Para subir un acuario de 30 a 35 ppt, calcula una cantidad menor y añádela por etapas. Para bajar de 30 a 25 ppt, no añadas sal: retira aproximadamente el 16,7 % del agua y reemplázala con agua preparada a menor salinidad. El volumen útil debe ser el real, descontando decoración, sustrato y espacios que no contienen agua.',
99
+ resultTitle: 'Lee la acción antes que la cifra',
100
+ result: 'La cifra solo es útil si sabes qué hacer con ella. “Sal a pesar” significa disolver y mezclar antes de introducir el agua; “añade sal poco a poco” significa dividir la corrección y volver a medir entre pasos; “agua a sustituir” significa retirar ese volumen y reponerlo con agua ya preparada. Si el resultado es cero, la medición actual coincide con tu objetivo.',
101
+ tipTitle: 'La comprobación final también forma parte del cálculo',
102
+ tip: '<p>Usa el volumen de trabajo real y calibra tu refractómetro o hidrómetro según las instrucciones del fabricante. Disuelve la mezcla por completo, deja que el agua alcance una temperatura estable y mide antes de usarla. En un acuario con animales, evita los saltos bruscos: divide las correcciones grandes y observa la respuesta entre cambios.</p><p>Esta herramienta hace una estimación matemática a partir de tu etiqueta y tu medición; no sustituye la ficha del producto, el control de calidad del agua ni el criterio específico para cada especie.</p>',
103
+ howTo: [['Mide el volumen útil', 'Cuenta solo el agua que realmente vas a mezclar o el volumen de trabajo del acuario; el sustrato y la decoración no cuentan como agua.'], ['Copia la proporción del envase', 'Introduce los gramos por litro u onzas por galón de tu mezcla exacta. Si la etiqueta da otra referencia, conviértela antes.'], ['Elige la acción correcta', 'Preparar devuelve sal para un lote nuevo; corregir al alza devuelve sal para añadir por etapas; corregir a la baja devuelve agua para retirar y sustituir.'], ['Disuelve, mide y repite', 'Mezcla por completo, comprueba con tu instrumento y divide cualquier corrección grande en pasos pequeños.']],
104
+ };
105
+
106
+ const localizedSlugs: Record<string, string> = {
107
+ de: 'aquarium-salz-dosierungsrechner',
108
+ es: 'calculadora-dosis-sal-acuario',
109
+ fr: 'calculateur-dosage-sel-aquarium',
110
+ id: 'kalkulator-dosis-garam-akuarium',
111
+ it: 'calcolatore-dosaggio-sale-acquario',
112
+ nl: 'zoutdosering-calculator-aquarium',
113
+ pl: 'kalkulator-dawkowania-soli-akwarium',
114
+ pt: 'calculadora-dosagem-sal-aquario',
115
+ ru: 'kalkulyator-dozirovki-soli-akvarium',
116
+ sv: 'kalkylator-akvariesalt',
117
+ tr: 'akvaryum-tuzu-dozaj-hesaplayici',
118
+ };
119
+
120
+ export function createSaltDosingContent(locale: string): ToolLocaleContent<AquariumSaltDosingCalculatorUI> {
121
+ const copy = { ...(copies[locale] ?? fallbackCopy), ...(locale === 'es' ? spanishSeoOverrides : {}) };
122
+ const title = locale === 'ja' ? copy.title + 'ツール' : copy.title;
123
+ const faq = copy.faq.map(([question, answer]) => ({ question, answer }));
124
+ const howTo = copy.howTo.map(([name, text]) => ({ name, text }));
125
+ const faqSchema: WithContext<FAQPage> = { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) };
126
+ const howToSchema: WithContext<HowTo> = { '@context': 'https://schema.org', '@type': 'HowTo', name: title, description: copy.description, step: howTo.map((step, index) => ({ '@type': 'HowToStep', position: index + 1, name: step.name, text: step.text })) };
127
+ const appSchema: WithContext<SoftwareApplication> = { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: title, description: copy.description, applicationCategory: 'UtilityApplication', operatingSystem: 'All', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' }, inLanguage: locale };
128
+ const ui = { ...baseUi, ...localizedUiDefaults[locale], ...copy.ui } as AquariumSaltDosingCalculatorUI;
129
+ return {
130
+ slug: localizedSlugs[locale] ?? 'aquarium-salt-dosing-calculator',
131
+ title,
132
+ description: copy.description,
133
+ faq,
134
+ bibliography,
135
+ howTo,
136
+ schemas: [faqSchema, howToSchema, appSchema],
137
+ seo: [
138
+ { type: 'title', text: copy.introTitle, level: 2 },
139
+ { type: 'paragraph', html: copy.intro },
140
+ { type: 'title', text: copy.methodTitle, level: 2 },
141
+ { type: 'paragraph', html: copy.method },
142
+ { type: 'title', text: copy.resultTitle, level: 2 },
143
+ { type: 'paragraph', html: copy.result },
144
+ { type: 'tip', title: copy.tipTitle, html: copy.tip },
145
+ { type: 'table', headers: [ui.amountLabel, ui.outputActionLabel], rows: [[ui.prepareAction, ui.saltAmountLabel], [ui.raiseAction, ui.saltAmountLabel], [ui.lowerAction, ui.waterAmountLabel], [ui.targetAction, ui.noChangeLabel]] },
146
+ ],
147
+ ui,
148
+ };
149
+ }
150
+
151
+ export const localizedContents = Object.fromEntries(Object.keys(copies).map((locale) => [locale, createSaltDosingContent(locale)])) as Record<string, ToolLocaleContent<AquariumSaltDosingCalculatorUI>>;
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ DEFAULT_SALT_DOSING_INPUT,
4
+ calculateSaltDosing,
5
+ gallonsToLitres,
6
+ gramsPerLitreToOuncesPerGallon,
7
+ litresToGallons,
8
+ ouncesPerGallonToGramsPerLitre,
9
+ } from './logic';
10
+
11
+ describe('aquarium salt dosing logic', () => {
12
+ it('calculates product mass for preparing new water', () => {
13
+ const result = calculateSaltDosing(DEFAULT_SALT_DOSING_INPUT);
14
+ expect(result).toEqual({ ok: true, value: expect.objectContaining({ action: 'prepare', amountLitres: 20, amountGrams: 720 }) });
15
+ });
16
+
17
+ it('estimates the salt needed to raise an existing tank', () => {
18
+ const result = calculateSaltDosing({ ...DEFAULT_SALT_DOSING_INPUT, mode: 'correct', volumeLitres: 100, currentSalinityPpt: 30, targetSalinityPpt: 35 });
19
+ expect(result.ok).toBe(true);
20
+ if (result.ok) {
21
+ expect(result.value.action).toBe('raise');
22
+ expect(result.value.amountGrams).toBeCloseTo(514.2857142857143, 8);
23
+ }
24
+ });
25
+
26
+ it('calculates the matching water replacement to lower salinity', () => {
27
+ const result = calculateSaltDosing({ ...DEFAULT_SALT_DOSING_INPUT, mode: 'correct', volumeLitres: 100, currentSalinityPpt: 40, targetSalinityPpt: 35 });
28
+ expect(result).toEqual({ ok: true, value: expect.objectContaining({ action: 'lower', amountLitres: 12.5 }) });
29
+ });
30
+
31
+ it('returns no action when the target already matches', () => {
32
+ const result = calculateSaltDosing({ ...DEFAULT_SALT_DOSING_INPUT, mode: 'correct', currentSalinityPpt: 35 });
33
+ expect(result).toEqual({ ok: true, value: expect.objectContaining({ action: 'target', amountLitres: 0, amountGrams: 0 }) });
34
+ });
35
+
36
+ it('rejects invalid and non-finite values', () => {
37
+ expect(calculateSaltDosing({ ...DEFAULT_SALT_DOSING_INPUT, volumeLitres: 0 })).toEqual({ ok: false, error: 'volume' });
38
+ expect(calculateSaltDosing({ ...DEFAULT_SALT_DOSING_INPUT, targetSalinityPpt: Number.NaN })).toEqual({ ok: false, error: 'salinity' });
39
+ expect(calculateSaltDosing({ ...DEFAULT_SALT_DOSING_INPUT, productRateGramsPerLitre: 201 })).toEqual({ ok: false, error: 'productRate' });
40
+ });
41
+
42
+ it('keeps imperial conversions reversible', () => {
43
+ expect(gallonsToLitres(litresToGallons(20))).toBeCloseTo(20, 8);
44
+ expect(ouncesPerGallonToGramsPerLitre(gramsPerLitreToOuncesPerGallon(36))).toBeCloseTo(36, 8);
45
+ });
46
+ });
@@ -0,0 +1,85 @@
1
+ export type SaltDosingMode = 'prepare' | 'correct';
2
+ export type UnitSystem = 'metric' | 'imperial';
3
+ export type SaltDosingAction = 'prepare' | 'raise' | 'lower' | 'target';
4
+
5
+ export interface SaltDosingInput {
6
+ mode: SaltDosingMode;
7
+ volumeLitres: number;
8
+ currentSalinityPpt: number;
9
+ targetSalinityPpt: number;
10
+ productRateGramsPerLitre: number;
11
+ }
12
+
13
+ export interface SaltDosingResult {
14
+ action: SaltDosingAction;
15
+ amountLitres: number;
16
+ amountGrams: number;
17
+ volumeLitres: number;
18
+ currentSalinityPpt: number;
19
+ targetSalinityPpt: number;
20
+ productRateGramsPerLitre: number;
21
+ }
22
+
23
+ export type SaltDosingError = 'volume' | 'salinity' | 'productRate';
24
+ export type CalculationResult = { ok: true; value: SaltDosingResult } | { ok: false; error: SaltDosingError };
25
+
26
+ export const DEFAULT_SALT_DOSING_INPUT: SaltDosingInput = {
27
+ mode: 'prepare',
28
+ volumeLitres: 20,
29
+ currentSalinityPpt: 0,
30
+ targetSalinityPpt: 35,
31
+ productRateGramsPerLitre: 36,
32
+ };
33
+
34
+ const MAX_VOLUME_LITRES = 100_000;
35
+ const MAX_SALINITY_PPT = 60;
36
+ const MAX_PRODUCT_RATE_GRAMS_PER_LITRE = 200;
37
+
38
+ function isFiniteInRange(value: number, minimum: number, maximum: number): boolean {
39
+ return Number.isFinite(value) && value >= minimum && value <= maximum;
40
+ }
41
+
42
+ function validateInput(input: SaltDosingInput): SaltDosingError | null {
43
+ if (!isFiniteInRange(input.volumeLitres, 0.1, MAX_VOLUME_LITRES)) return 'volume';
44
+ if (!isFiniteInRange(input.targetSalinityPpt, 0, MAX_SALINITY_PPT) || !isFiniteInRange(input.currentSalinityPpt, 0, MAX_SALINITY_PPT)) return 'salinity';
45
+ if (input.mode === 'prepare' && input.targetSalinityPpt <= 0) return 'salinity';
46
+ if (!isFiniteInRange(input.productRateGramsPerLitre, 0.01, MAX_PRODUCT_RATE_GRAMS_PER_LITRE)) return 'productRate';
47
+ return null;
48
+ }
49
+
50
+ export function calculateSaltDosing(input: SaltDosingInput): CalculationResult {
51
+ const error = validateInput(input);
52
+ if (error !== null) return { ok: false, error };
53
+
54
+ if (input.mode === 'prepare') {
55
+ return {
56
+ ok: true,
57
+ value: {
58
+ action: 'prepare',
59
+ amountLitres: input.volumeLitres,
60
+ amountGrams: input.volumeLitres * input.productRateGramsPerLitre,
61
+ volumeLitres: input.volumeLitres,
62
+ currentSalinityPpt: 0,
63
+ targetSalinityPpt: input.targetSalinityPpt,
64
+ productRateGramsPerLitre: input.productRateGramsPerLitre,
65
+ },
66
+ };
67
+ }
68
+
69
+ const difference = input.targetSalinityPpt - input.currentSalinityPpt;
70
+ if (difference === 0) {
71
+ return { ok: true, value: { action: 'target', amountLitres: 0, amountGrams: 0, ...input } };
72
+ }
73
+ if (difference > 0) {
74
+ const amountGrams = input.volumeLitres * (difference / input.targetSalinityPpt) * input.productRateGramsPerLitre;
75
+ return { ok: true, value: { action: 'raise', amountLitres: 0, amountGrams, ...input } };
76
+ }
77
+
78
+ const amountLitres = input.volumeLitres * (Math.abs(difference) / input.currentSalinityPpt);
79
+ return { ok: true, value: { action: 'lower', amountLitres, amountGrams: 0, ...input } };
80
+ }
81
+
82
+ export function litresToGallons(litres: number): number { return litres / 3.785411784; }
83
+ export function gallonsToLitres(gallons: number): number { return gallons * 3.785411784; }
84
+ export function gramsPerLitreToOuncesPerGallon(gramsPerLitre: number): number { return gramsPerLitre * 0.133526471; }
85
+ export function ouncesPerGallonToGramsPerLitre(ouncesPerGallon: number): number { return ouncesPerGallon / 0.133526471; }
@@ -0,0 +1,11 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { aquariumSaltDosingCalculator } from './entry';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ const { locale = 'en' } = Astro.props as { locale?: KnownLocale };
7
+ const loader = aquariumSaltDosingCalculator.i18n[locale] ?? aquariumSaltDosingCalculator.i18n.en;
8
+ const content = loader === undefined ? undefined : await loader();
9
+ ---
10
+
11
+ {content && <SEORenderer content={{ locale, sections: content.seo }} />}