Dernière activité 1746986465

Калькулятор RAID'ов большинства используемых типов ( https://blacktower.wintersky.ru/public/raid_calc.html )

Révision f3e3596e0c01fae2e6242f09400e74bddaea50ba

gistfile1.txt Brut
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <meta name="description" content="RAID Capacity Calculator - calculate storage efficiency for RAID 0, 1, 5, 6, 10 and ZFS configurations">
7 <meta name="keywords" content="RAID calculator, storage calculator, ZFS calculator, disk array, RAID capacity">
8 <meta name="author" content="Karel Wintersky & Deepseek">
9
10 <!-- Favicon SVG -->
11 <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'>
12 <rect width='100' height='100' fill='%233367D6'/>
13 <rect x='20' y='30' width='60' height='40' fill='%23fff' rx='5'/>
14 <rect x='25' y='35' width='50' height='10' fill='%233367D6'/>
15 <circle cx='40' cy='60' r='5' fill='%233367D6'/>
16 <circle cx='60' cy='60' r='5' fill='%233367D6'/>
17 <path d='M30 75 L70 75 L80 85 L20 85 Z' fill='%23fff'/>
18 </svg>" type="image/svg+xml">
19
20 <!-- Open Graph / Facebook -->
21 <meta property="og:type" content="website">
22 <meta property="og:url" content="https://blacktower.wintersky.ru/public/raid_calc.html">
23 <meta property="og:title" content="RAID Capacity Calculator">
24 <meta property="og:description" content="Calculate storage efficiency for various RAID configurations">
25 <meta property="og:image" content="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 630'>
26 <rect width='1200' height='630' fill='%233367D6'/>
27 <rect x='100' y='150' width='1000' height='330' fill='%23fff' rx='20'/>
28 <rect x='150' y='200' width='900' height='80' fill='%233367D6' rx='10'/>
29 <text x='150' y='300' font-family='Arial' font-size='60' fill='%23000'>RAID 0: <tspan fill='%233367D6'>4×1TB = 4TB</tspan></text>
30 <text x='150' y='380' font-family='Arial' font-size='60' fill='%23000'>RAID 5: <tspan fill='%233367D6'>4×1TB = 3TB</tspan></text>
31 <text x='150' y='460' font-family='Arial' font-size='60' fill='%23000'>RAID 10: <tspan fill='%233367D6'>4×1TB = 2TB</tspan></text>
32 <text x='600' y='550' font-family='Arial' font-size='40' fill='%23fff'>RAID Capacity Calculator</text>
33 </svg>">
34 <meta property="og:image:width" content="1200">
35 <meta property="og:image:height" content="630">
36
37 <!-- Twitter -->
38 <meta name="twitter:card" content="summary_large_image">
39 <meta name="twitter:title" content="RAID Capacity Calculator">
40 <meta name="twitter:description" content="Professional tool for RAID storage calculations">
41 <meta name="twitter:image" content="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 630'>
42 <rect width='1200' height='630' fill='%233367D6'/>
43 <rect x='100' y='150' width='1000' height='330' fill='%23fff' rx='20'/>
44 <rect x='150' y='200' width='900' height='80' fill='%233367D6' rx='10'/>
45 <text x='150' y='300' font-family='Arial' font-size='60' fill='%23000'>RAID 0: <tspan fill='%233367D6'>4×1TB = 4TB</tspan></text>
46 <text x='150' y='380' font-family='Arial' font-size='60' fill='%23000'>RAID 5: <tspan fill='%233367D6'>4×1TB = 3TB</tspan></text>
47 <text x='150' y='460' font-family='Arial' font-size='60' fill='%23000'>RAID 10: <tspan fill='%233367D6'>4×1TB = 2TB</tspan></text>
48 <text x='600' y='550' font-family='Arial' font-size='40' fill='%23fff'>RAID Capacity Calculator</text>
49 </svg>">
50
51 <title>RAID Capacity Calculator</title>
52 <style>
53 body {
54 font-family: Arial, sans-serif;
55 max-width: 1000px;
56 margin: 0 auto;
57 padding: 20px;
58 line-height: 1.6;
59 }
60 .language-switcher {
61 text-align: right;
62 margin-bottom: 20px;
63 }
64 .language-switcher button {
65 background: none;
66 border: 1px solid #ccc;
67 padding: 5px 10px;
68 cursor: pointer;
69 margin-left: 5px;
70 }
71 .language-switcher button.active {
72 background: #eee;
73 }
74 .calculator-container {
75 display: flex;
76 gap: 20px;
77 margin-bottom: 30px;
78 }
79 .input-section {
80 flex: 1;
81 padding: 15px;
82 border: 1px solid #ddd;
83 border-radius: 5px;
84 }
85 .results-section {
86 flex: 1;
87 padding: 15px;
88 border: 1px solid #ddd;
89 border-radius: 5px;
90 background-color: #f5f5f5;
91 }
92 .input-group {
93 margin-bottom: 15px;
94 }
95 label {
96 display: block;
97 margin-bottom: 5px;
98 font-weight: bold;
99 }
100 input, select {
101 padding: 8px;
102 width: 100%;
103 box-sizing: border-box;
104 }
105 .result-item {
106 margin-bottom: 10px;
107 padding: 10px;
108 background-color: white;
109 border-radius: 3px;
110 }
111 .result-label {
112 font-weight: bold;
113 color: #555;
114 }
115 .hint-section {
116 padding: 20px;
117 border: 1px solid #ddd;
118 border-radius: 5px;
119 background-color: #f9f9f9;
120 margin-top: 20px;
121 }
122 .hint-title {
123 font-weight: bold;
124 margin-bottom: 10px;
125 color: #333;
126 }
127 .hint-content {
128 line-height: 1.5;
129 }
130
131 /* */
132 .language-switcher {
133 text-align: right;
134 margin-bottom: 20px;
135 }
136 .language-switcher button {
137 background: none;
138 border: 1px solid #ccc;
139 padding: 5px 10px;
140 cursor: pointer;
141 margin-left: 5px;
142 display: inline-flex;
143 align-items: center;
144 gap: 5px;
145 }
146 .language-switcher button:hover {
147 background-color: #f0f0f0;
148 }
149 .language-switcher button.active {
150 background: #eee;
151 font-weight: bold;
152 }
153 .language-switcher svg {
154 border: 1px solid #ddd;
155 }
156
157 </style>
158</head>
159<body>
160 <!--
161 <div class="language-switcher">
162 <button id="lang-en">English</button>
163 <button id="lang-ru">Русский</button>
164 </div>
165-->
166 <div class="language-switcher">
167 <button id="lang-en" title="English">
168 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 30" width="20" height="10">
169 <clipPath id="s">
170 <path d="M0,0 v30 h60 v-30 z"/>
171 </clipPath>
172 <clipPath id="t">
173 <path d="M30,15 h30 v15 z v15 h-30 z h-30 v-15 z v-15 h30 z"/>
174 </clipPath>
175 <g clip-path="url(#s)">
176 <path d="M0,0 v30 h60 v-30 z" fill="#012169"/>
177 <path d="M0,0 L60,30 M60,0 L0,30" stroke="#fff" stroke-width="6"/>
178 <path d="M0,0 L60,30 M60,0 L0,30" stroke="#C8102E" stroke-width="4"/>
179 <path d="M30,0 v30 M0,15 h60" stroke="#fff" stroke-width="10"/>
180 <path d="M30,0 v30 M0,15 h60" stroke="#C8102E" stroke-width="6"/>
181 </g>
182 <g clip-path="url(#t)">
183 <path d="M0,0 v30 h60 v-30 z" fill="#012169"/>
184 <path d="M0,0 L60,30 M60,0 L0,30" stroke="#fff" stroke-width="6"/>
185 <path d="M0,0 L60,30 M60,0 L0,30" stroke="#C8102E" stroke-width="4"/>
186 <path d="M30,0 v30 M0,15 h60" stroke="#fff" stroke-width="10"/>
187 <path d="M30,0 v30 M0,15 h60" stroke="#C8102E" stroke-width="6"/>
188 </g>
189 </svg>
190 <span>English</span>
191 </button>
192 <button id="lang-ru" title="Русский">
193 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 30" width="20" height="10">
194 <clipPath id="r">
195 <path d="M0,0 v30 h60 v-30 z"/>
196 </clipPath>
197 <g clip-path="url(#r)">
198 <path d="M0,0 h60 v10 h-60 z" fill="#fff"/>
199 <path d="M0,10 h60 v10 h-60 z" fill="#0039A6"/>
200 <path d="M0,20 h60 v10 h-60 z" fill="#D52B1E"/>
201 </g>
202 </svg>
203 <span>Русский</span>
204 </button>
205</div>
206
207 <h1 id="title">RAID Capacity Calculator</h1>
208
209 <div class="calculator-container">
210 <div class="input-section">
211 <div class="input-group">
212 <label for="disk-count" id="disk-count-label">Number of disks:</label>
213 <input type="number" id="disk-count" min="1" value="4">
214 </div>
215
216 <div class="input-group">
217 <label for="disk-size" id="disk-size-label">Disk size (GB):</label>
218 <input type="number" id="disk-size" min="1" value="1000">
219 </div>
220
221 <div class="input-group">
222 <label for="raid-type" id="raid-type-label">RAID type:</label>
223 <select id="raid-type">
224 <optgroup label="Standard RAID">
225 <option value="raid0">RAID 0</option>
226 <option value="raid1">RAID 1</option>
227 <option value="raid3">RAID 3</option>
228 <option value="raid5">RAID 5</option>
229 <option value="raid6">RAID 6</option>
230 <option value="raid10">RAID 10</option>
231 </optgroup>
232 <optgroup label="ZFS">
233 <option value="zfsStripe">Stripe set</option>
234 <option value="zfsMirror">Mirror</option>
235 <option value="zfsZ1">RAID-Z1</option>
236 <option value="zfsZ2">RAID-Z2</option>
237 <option value="zfsZ3">RAID-Z3</option>
238 </optgroup>
239 </select>
240 </div>
241 </div>
242
243 <div class="results-section">
244 <h2 id="results-title">Results</h2>
245 <div class="result-item">
246 <div class="result-label" id="total-space-label">Total space:</div>
247 <div id="total-space">0</div>
248 </div>
249 <div class="result-item">
250 <div class="result-label" id="effective-space-label">Effective space:</div>
251 <div id="effective-space">0</div>
252 </div>
253 <div class="result-item">
254 <div class="result-label" id="efficiency-label">Efficiency:</div>
255 <div id="efficiency">0</div>
256 </div>
257 <div class="result-item">
258 <div class="result-label" id="fault-tolerance-label">Fault tolerance:</div>
259 <div id="fault-tolerance">None</div>
260 </div>
261 </div>
262 </div>
263
264 <div class="hint-section">
265 <div class="hint-title" id="hint-title">About selected RAID type:</div>
266 <div class="hint-content" id="hint-content"></div>
267 </div>
268
269 <script>
270 // Language data
271 const translations = {
272 en: {
273 title: "RAID Capacity Calculator",
274 diskCountLabel: "Number of disks:",
275 diskSizeLabel: "Disk size (GB):",
276 raidTypeLabel: "RAID type:",
277 resultsTitle: "Results",
278 totalSpaceLabel: "Total space:",
279 effectiveSpaceLabel: "Effective space:",
280 efficiencyLabel: "Efficiency:",
281 faultToleranceLabel: "Fault tolerance:",
282 hintTitle: "About selected RAID type:",
283 raidTypes: {
284 standard: "Standard RAID",
285 zfs: "ZFS",
286 raid0: "RAID 0",
287 raid1: "RAID 1",
288 raid3: "RAID 3",
289 raid5: "RAID 5",
290 raid6: "RAID 6",
291 raid10: "RAID 10",
292 zfsStripe: "Stripe set",
293 zfsMirror: "Mirror",
294 zfsZ1: "RAID-Z1",
295 zfsZ2: "RAID-Z2",
296 zfsZ3: "RAID-Z3"
297 },
298 faultTolerance: {
299 none: "None",
300 one: "1 disk failure",
301 two: "2 disk failures",
302 three: "3 disk failures",
303 half: "Up to half of disks (in mirrored pairs)"
304 },
305 hints: {
306 raid0: "RAID 0 (striping) combines multiple disks into one logical unit with data distributed across all disks. Provides improved performance but no redundancy - failure of any disk results in total data loss.",
307 raid1: "RAID 1 (mirroring) creates an exact copy of data on two or more disks. Provides fault tolerance but at the cost of reduced storage capacity.",
308 raid3: "RAID 3 uses byte-level striping with a dedicated parity disk. Good for sequential data access but poor for random access. Can tolerate one disk failure.",
309 raid5: "RAID 5 uses block-level striping with distributed parity. Provides good balance between performance, capacity and fault tolerance. Can tolerate one disk failure.",
310 raid6: "RAID 6 is similar to RAID 5 but with double distributed parity. Can tolerate two disk failures, making it more reliable for large arrays.",
311 raid10: "RAID 10 (1+0) combines mirroring and striping. Requires even number of disks (minimum 4). Provides good performance and can tolerate multiple disk failures (one per mirrored pair).",
312 zfsStripe: "ZFS stripe is similar to RAID 0 - no redundancy but maximum capacity and performance. Data is distributed across all disks in the pool.",
313 zfsMirror: "ZFS mirror is similar to RAID 1 - data is duplicated on two or more disks. Provides redundancy at the cost of storage capacity.",
314 zfsZ1: "RAID-Z1 is similar to RAID 5 - single parity with distributed striping. Can tolerate one disk failure while providing good capacity and performance.",
315 zfsZ2: "RAID-Z2 is similar to RAID 6 - double parity with distributed striping. Can tolerate two disk failures, recommended for larger pools.",
316 zfsZ3: "RAID-Z3 provides triple parity protection, able to withstand three disk failures. Recommended for very large pools where rebuild times are long."
317 }
318 },
319 ru: {
320 title: "Калькулятор ёмкости RAID",
321 diskCountLabel: "Число дисков:",
322 diskSizeLabel: "Размер диска (ГБ):",
323 raidTypeLabel: "Тип RAID:",
324 resultsTitle: "Результаты",
325 totalSpaceLabel: "Общий объем:",
326 effectiveSpaceLabel: "Эффективный объем:",
327 efficiencyLabel: "Эффективность использования:",
328 faultToleranceLabel: "Отказоустойчивость:",
329 hintTitle: "О выбранном типе RAID:",
330 raidTypes: {
331 standard: "Обычный RAID",
332 zfs: "ZFS",
333 raid0: "RAID 0",
334 raid1: "RAID 1",
335 raid3: "RAID 3",
336 raid5: "RAID 5",
337 raid6: "RAID 6",
338 raid10: "RAID 10",
339 zfsStripe: "Stripe set",
340 zfsMirror: "Mirror",
341 zfsZ1: "RAID-Z1",
342 zfsZ2: "RAID-Z2",
343 zfsZ3: "RAID-Z3"
344 },
345 faultTolerance: {
346 none: "Нет",
347 one: "1 отказ диска",
348 two: "2 отказа диска",
349 three: "3 отказа диска",
350 half: "До половины дисков (в зеркальных парах)"
351 },
352 hints: {
353 raid0: "RAID 0 (чередование) объединяет несколько дисков в один логический блок с распределением данных по всем дискам. Обеспечивает повышенную производительность, но не имеет избыточности - отказ любого диска приводит к полной потере данных.",
354 raid1: "RAID 1 (зеркалирование) создает точную копию данных на двух или более дисках. Обеспечивает отказоустойчивость, но за счет уменьшения полезного объема хранилища.",
355 raid3: "RAID 3 использует чередование на уровне байтов с выделенным диском четности. Хорошо подходит для последовательного доступа к данным, но плохо для случайного. Может выдержать отказ одного диска.",
356 raid5: "RAID 5 использует чередование на уровне блоков с распределенной четностью. Обеспечивает хороший баланс между производительностью, емкостью и отказоустойчивостью. Может выдержать отказ одного диска.",
357 raid6: "RAID 6 аналогичен RAID 5, но с двойной распределенной четностью. Может выдержать отказ двух дисков, что делает его более надежным для больших массивов.",
358 raid10: "RAID 10 (1+0) сочетает зеркалирование и чередование. Требует четного количества дисков (минимум 4). Обеспечивает хорошую производительность и может выдержать отказ нескольких дисков (по одному в каждой зеркальной паре).",
359 zfsStripe: "ZFS stripe аналогичен RAID 0 - нет избыточности, но максимальная емкость и производительность. Данные распределяются по всем дискам в пуле.",
360 zfsMirror: "ZFS mirror аналогичен RAID 1 - данные дублируются на двух или более дисках. Обеспечивает избыточность за счет полезного объема хранилища.",
361 zfsZ1: "RAID-Z1 аналогичен RAID 5 - одинарная четность с распределенным чередованием. Может выдержать отказ одного диска при хорошей емкости и производительности.",
362 zfsZ2: "RAID-Z2 аналогичен RAID 6 - двойная четность с распределенным чередованием. Может выдержать отказ двух дисков, рекомендуется для больших пулов.",
363 zfsZ3: "RAID-Z3 обеспечивает тройную защиту четностью, может выдержать отказ трех дисков. Рекомендуется для очень больших пулов, где время восстановления велико."
364 }
365 }
366 };
367
368 // Current language
369 let currentLang = 'en';
370
371 // DOM elements
372 const elements = {
373 title: document.getElementById('title'),
374 diskCountLabel: document.getElementById('disk-count-label'),
375 diskSizeLabel: document.getElementById('disk-size-label'),
376 raidTypeLabel: document.getElementById('raid-type-label'),
377 resultsTitle: document.getElementById('results-title'),
378 totalSpaceLabel: document.getElementById('total-space-label'),
379 effectiveSpaceLabel: document.getElementById('effective-space-label'),
380 efficiencyLabel: document.getElementById('efficiency-label'),
381 faultToleranceLabel: document.getElementById('fault-tolerance-label'),
382 hintTitle: document.getElementById('hint-title'),
383 hintContent: document.getElementById('hint-content'),
384 raidTypeSelect: document.getElementById('raid-type'),
385 diskCountInput: document.getElementById('disk-count'),
386 diskSizeInput: document.getElementById('disk-size'),
387 raidTypeOptgroups: document.querySelectorAll('#raid-type optgroup'),
388 raidTypeOptions: document.querySelectorAll('#raid-type option')
389 };
390
391 // Save state to URL hash
392 function saveStateToHash() {
393 const params = new URLSearchParams();
394 params.set('lang', currentLang);
395 params.set('count', elements.diskCountInput.value);
396 params.set('size', elements.diskSizeInput.value);
397 params.set('raid', elements.raidTypeSelect.value);
398
399 window.location.hash = params.toString();
400 }
401
402
403 // Load state from URL hash
404 function loadStateFromHash() {
405 if (window.location.hash) {
406 try {
407 const hash = window.location.hash.substring(1);
408 const params = new URLSearchParams(hash);
409
410 if (params.has('lang') && translations[params.get('lang')]) {
411 currentLang = params.get('lang');
412 }
413 if (params.has('count')) {
414 elements.diskCountInput.value = params.get('count');
415 }
416 if (params.has('size')) {
417 elements.diskSizeInput.value = params.get('size');
418 }
419 if (params.has('raid')) {
420 elements.raidTypeSelect.value = params.get('raid');
421 }
422
423 return true;
424 } catch (e) {
425 console.error("Error parsing hash:", e);
426 }
427 }
428 return false;
429 }
430
431 // Update UI language
432 function updateLanguage(lang) {
433 currentLang = lang;
434 const t = translations[lang];
435
436 // Update labels
437 elements.title.textContent = t.title;
438 elements.diskCountLabel.textContent = t.diskCountLabel;
439 elements.diskSizeLabel.textContent = t.diskSizeLabel;
440 elements.raidTypeLabel.textContent = t.raidTypeLabel;
441 elements.resultsTitle.textContent = t.resultsTitle;
442 elements.totalSpaceLabel.textContent = t.totalSpaceLabel;
443 elements.effectiveSpaceLabel.textContent = t.effectiveSpaceLabel;
444 elements.efficiencyLabel.textContent = t.efficiencyLabel;
445 elements.faultToleranceLabel.textContent = t.faultToleranceLabel;
446 elements.hintTitle.textContent = t.hintTitle;
447
448 // Update RAID type options
449 elements.raidTypeOptgroups[0].label = t.raidTypes.standard;
450 elements.raidTypeOptgroups[1].label = t.raidTypes.zfs;
451
452 const raidTypeOptions = [
453 t.raidTypes.raid0, t.raidTypes.raid1, t.raidTypes.raid3,
454 t.raidTypes.raid5, t.raidTypes.raid6, t.raidTypes.raid10,
455 t.raidTypes.zfsStripe, t.raidTypes.zfsMirror,
456 t.raidTypes.zfsZ1, t.raidTypes.zfsZ2, t.raidTypes.zfsZ3
457 ];
458
459 elements.raidTypeOptions.forEach((option, index) => {
460 option.textContent = raidTypeOptions[index];
461 });
462
463 // Recalculate to update fault tolerance text and hint
464 calculate();
465 }
466
467 // Update hint based on selected RAID type
468 function updateHint(raidType) {
469 elements.hintContent.textContent = translations[currentLang].hints[raidType] || '';
470 }
471
472 // Calculate RAID parameters
473 function calculate() {
474 const diskCount = parseInt(elements.diskCountInput.value) || 0;
475 const diskSize = parseInt(elements.diskSizeInput.value) || 0;
476 const raidType = elements.raidTypeSelect.value;
477
478 let totalSpace, effectiveSpace, efficiency, faultTolerance;
479 const t = translations[currentLang].faultTolerance;
480
481 switch(raidType) {
482 case 'raid0':
483 case 'zfsStripe':
484 totalSpace = diskCount * diskSize;
485 effectiveSpace = totalSpace;
486 efficiency = 100;
487 faultTolerance = t.none;
488 break;
489
490 case 'raid1':
491 case 'zfsMirror':
492 totalSpace = diskCount * diskSize;
493 effectiveSpace = diskSize;
494 efficiency = (100 / diskCount).toFixed(2);
495 faultTolerance = diskCount > 2 ? t.half : t.one;
496 break;
497
498 case 'raid3':
499 case 'raid5':
500 case 'zfsZ1':
501 totalSpace = diskCount * diskSize;
502 effectiveSpace = (diskCount - 1) * diskSize;
503 efficiency = ((diskCount - 1) / diskCount * 100).toFixed(2);
504 faultTolerance = t.one;
505 break;
506
507 case 'raid6':
508 case 'zfsZ2':
509 totalSpace = diskCount * diskSize;
510 effectiveSpace = (diskCount - 2) * diskSize;
511 efficiency = ((diskCount - 2) / diskCount * 100).toFixed(2);
512 faultTolerance = t.two;
513 break;
514
515 case 'zfsZ3':
516 totalSpace = diskCount * diskSize;
517 effectiveSpace = (diskCount - 3) * diskSize;
518 efficiency = ((diskCount - 3) / diskCount * 100).toFixed(2);
519 faultTolerance = t.three;
520 break;
521
522 case 'raid10':
523 if (diskCount < 2 || diskCount % 2 !== 0) {
524 effectiveSpace = 0;
525 efficiency = 0;
526 faultTolerance = t.none;
527 } else {
528 totalSpace = diskCount * diskSize;
529 effectiveSpace = (diskCount / 2) * diskSize;
530 efficiency = 50;
531 faultTolerance = t.half;
532 }
533 break;
534
535 default:
536 totalSpace = effectiveSpace = efficiency = 0;
537 faultTolerance = t.none;
538 }
539
540 // Update results
541 document.getElementById('total-space').textContent = totalSpace.toLocaleString() + ' Gb';
542 document.getElementById('effective-space').textContent = effectiveSpace.toLocaleString() + ' Gb';
543 document.getElementById('efficiency').textContent = efficiency + ' %';
544 document.getElementById('fault-tolerance').textContent = faultTolerance;
545
546 // Update hint
547 updateHint(raidType);
548
549 // Save state
550 saveStateToHash();
551 }
552
553 // Event listeners
554 function setupEventListeners() {
555 elements.diskCountInput.addEventListener('input', calculate);
556 elements.diskSizeInput.addEventListener('input', calculate);
557 elements.raidTypeSelect.addEventListener('change', calculate);
558
559 document.getElementById('lang-en').addEventListener('click', () => {
560 currentLang = 'en';
561 updateLanguage('en');
562 saveStateToHash();
563 });
564 document.getElementById('lang-ru').addEventListener('click', () => {
565 currentLang = 'ru';
566 updateLanguage('ru');
567 saveStateToHash();
568 });
569 }
570
571 // Initialize
572 function init() {
573 // Try to load state from hash
574 const stateLoaded = loadStateFromHash();
575
576 // Setup event listeners
577 setupEventListeners();
578
579 // Update UI with current or default values
580 updateLanguage(currentLang);
581
582 // If no state was loaded, calculate with defaults
583 if (!stateLoaded) {
584 calculate();
585 }
586 }
587
588 // Start the application
589 init();
590 </script>
591</body>
592</html>