import i18next from "i18next"; import LanguageDetector from "i18next-browser-languagedetector"; import HttpApi from "i18next-http-backend"; import '@fortawesome/fontawesome-free/js/fontawesome' import '@fortawesome/fontawesome-free/js/solid' import '@fortawesome/fontawesome-free/js/regular' import '@fortawesome/fontawesome-free/js/brands' import './main.scss'; ///////////////// // TRANSLATION // ///////////////// async function initI18next() { await i18next .use(HttpApi) .use(LanguageDetector) .init({ supportedLngs: ["en", "de"], nonExplicitSupportedLngs: true, fallbackLng: "en", debug: true, backend: { loadPath: "/i18n/{{lng}}.json", }, }); } function translatePageElements() { // Translate content inside a tag const translatableElements = document.querySelectorAll( "[data-i18n]", ); translatableElements.forEach((el) => { const key = el.getAttribute("data-i18n"); el.innerHTML = i18next.t(key); }); // Translate alt texts const translatableAltTexts = document.querySelectorAll( "[alt-i18n-key]", ); translatableAltTexts.forEach((el) => { const translation_key = el.getAttribute("alt-i18n-key"); el.setAttribute("alt", i18next.t(translation_key)); }); } function bindLocaleSwitcher(initialValue) { const switcher = document.querySelector( "[data-i18n-switcher]", ); switcher.value = initialValue; switcher.onchange = (e) => { i18next .changeLanguage(e.target.value) .then(translatePageElements) .then(updateCageCheck); }; } (async function () { await initI18next(); translatePageElements(); bindLocaleSwitcher(i18next.resolvedLanguage); updateCageCheck(); })(); ////////// // TABS // ////////// function initTabs(tabs, tabContent, activationClass) { tabs.forEach((tab) => { tab.addEventListener('click', (e) => { let selectedTabID = tab.getAttribute('data-tab-id'); updateActiveTab(tabs, tab); updateActiveContent(tabContent, selectedTabID); }) }) } function updateActiveTab(tabs, selectedTabID) { tabs.forEach((tab) => { if (tab && tab.classList.contains(ACTIVATION_CLASS)) { tab.classList.remove(ACTIVATION_CLASS); } }); selectedTabID.classList.add(ACTIVATION_CLASS); } function updateActiveContent(tabsContent, selectedTabID) { tabsContent.forEach((item) => { if (item && item.classList.contains(ACTIVATION_CLASS)) { item.classList.remove(ACTIVATION_CLASS); } let data = item.getAttribute('data-content-id'); if (data === selectedTabID) { item.classList.add(ACTIVATION_CLASS); } }); } const primaryTabs = [...document.querySelectorAll('#primary-tabs li')]; const primaryTabsContent = [...document.querySelectorAll('#tab-content div')]; const ACTIVATION_CLASS = 'is-active'; initTabs(primaryTabs, primaryTabsContent, ACTIVATION_CLASS); //////////////// // CALCULATOR // //////////////// const MINIMUM_BASE_AREA = 0.5; const MINIMUM_AREA_THREE_RATS = 1.5; const AREA_PER_ADDITIONAL_RAT = 0.25; const MAXIMUM_FALL_HEIGHT = 0.5; const MINIMUM_LENGTH_LONG_SIDE = 0.8; const MINIMUM_LENGTH_SHORT_SIDE = 0.5; const MINIMUM_FLOOR_HEIGHT = 0.25; const FAILED_BASE_AREA = "failed_base_area"; const FAILED_OVERALL_AREA = "failed_overall_area"; const FAILED_FALL_HEIGHT = "failed_fall_height"; const FAILED_NUM_RATS = "failed_num_rats"; const FAILED_MINIMUM_LENGTH_LONG_SIDE = "failed_length_long_side"; const FAILED_MINIMUM_LENGTH_SHORT_SIDE = "failed_length_short_side"; const FAILED_FLOOR_HEIGHT = "failed_floor_height" const CRITERIA_BASE_AREA = "base_area"; const CRITERIA_FALL_HEIGHT = "fall_height"; const CRITERIA_MINIMUM_LENGTH_LONG_SIDE = "length_long_side"; const CRITERIA_MINIMUM_LENGTH_SHORT_SIDE = "length_short_side"; const CRITERIA_FLOOR_HEIGHT = "floor_height" const CRITERIA_OVERALL_AREA = "overall_area" class Validator { constructor() { this.FAIL_CRITERIA = { [FAILED_BASE_AREA]: i18next.t('failed-base-area', {"MINIMUM_BASE_AREA": MINIMUM_BASE_AREA}), [FAILED_OVERALL_AREA]: i18next.t("failed-overall-area"), [FAILED_FALL_HEIGHT]: i18next.t("failed-fall-height", {"maximum_fall_height": (MAXIMUM_FALL_HEIGHT * 100).toFixed(0)}), [FAILED_FLOOR_HEIGHT]: i18next.t("failed-floor-height", {"minimum_floor_height": (MINIMUM_FLOOR_HEIGHT * 100).toFixed(0)}), [FAILED_NUM_RATS]: i18next.t("failed-num-rats"), [FAILED_MINIMUM_LENGTH_LONG_SIDE]: i18next.t("failed-minimum-length-long-side", {"minimum_length_long_side": (MINIMUM_LENGTH_LONG_SIDE * 100).toFixed(0)}), [FAILED_MINIMUM_LENGTH_SHORT_SIDE]: i18next.t("failed-minimum-length-short-side", {"minimum_length_short_side": (MINIMUM_LENGTH_SHORT_SIDE * 100).toFixed(0)}), }; this.STATIC_CRITERIA = { [CRITERIA_BASE_AREA]: i18next.t('base-area', {"MINIMUM_BASE_AREA": MINIMUM_BASE_AREA}), [CRITERIA_FALL_HEIGHT]: i18next.t("fall-height", {"maximum_fall_height": (MAXIMUM_FALL_HEIGHT * 100).toFixed(0)}), [CRITERIA_FLOOR_HEIGHT]: i18next.t("floor-height", {"minimum_floor_height": (MINIMUM_FLOOR_HEIGHT * 100).toFixed(0)}), [CRITERIA_MINIMUM_LENGTH_LONG_SIDE]: i18next.t("minimum-length-long-side", {"minimum_length_long_side": (MINIMUM_LENGTH_LONG_SIDE * 100).toFixed(0)}), [CRITERIA_MINIMUM_LENGTH_SHORT_SIDE]: i18next.t("minimum-length-short-side", {"minimum_length_short_side": (MINIMUM_LENGTH_SHORT_SIDE * 100).toFixed(0)}), } } overallAreaNeeded(numOfRats) { if (numOfRats < 3 || numOfRats > 15) { throw new Error("This formula works only from 3 to 15 rats"); } return MINIMUM_AREA_THREE_RATS + (numOfRats - 3.0) * AREA_PER_ADDITIONAL_RAT; } allowedNumberOfRats(overallArea) { /* Calculates the number of rats that are allowed for a certain overall area. */ let result = 3.0 + (overallArea - MINIMUM_AREA_THREE_RATS) / AREA_PER_ADDITIONAL_RAT; if (result < 3) { throw new Error("Cages must be for three rats or more"); } return result; } failCageNumberIndependent(dimensions, numFullFloors) { /* Function that checks a cage independent of the number of rats. */ let failedCriteria = {}; const baseArea = dimensions.depth * dimensions.width; if (baseArea < MINIMUM_BASE_AREA) { failedCriteria[FAILED_BASE_AREA] = this.FAIL_CRITERIA[FAILED_BASE_AREA]; } if (dimensions.height / numFullFloors > MAXIMUM_FALL_HEIGHT) { failedCriteria[FAILED_FALL_HEIGHT] = this.FAIL_CRITERIA[FAILED_FALL_HEIGHT]; } if (dimensions.width < MINIMUM_LENGTH_LONG_SIDE && dimensions.depth < MINIMUM_LENGTH_LONG_SIDE) { failedCriteria[FAILED_MINIMUM_LENGTH_LONG_SIDE] = this.FAIL_CRITERIA[FAILED_MINIMUM_LENGTH_LONG_SIDE]; } if (dimensions.width < MINIMUM_LENGTH_SHORT_SIDE || dimensions.depth < MINIMUM_LENGTH_SHORT_SIDE) { failedCriteria[FAILED_MINIMUM_LENGTH_SHORT_SIDE] = this.FAIL_CRITERIA[FAILED_MINIMUM_LENGTH_SHORT_SIDE]; } if (dimensions.height / numFullFloors < MINIMUM_FLOOR_HEIGHT) { failedCriteria[FAILED_FLOOR_HEIGHT] = this.FAIL_CRITERIA[FAILED_FLOOR_HEIGHT]; } return failedCriteria; } getOverallArea(dimensions, numFullFloors) { const baseArea = dimensions.depth * dimensions.width; return baseArea * numFullFloors } cageCheck(dimensions, numRats, numFullFloors) { /* Function that checks a cage based on overall criteria and the number of rats. */ let failedCriteria = this.failCageNumberIndependent(dimensions, numFullFloors); if (numRats < 2 || numRats > 15) { failedCriteria[FAILED_NUM_RATS] = this.FAIL_CRITERIA[FAILED_NUM_RATS]; } const overallArea = this.getOverallArea(dimensions, numFullFloors); const overallAreaNeeded = this.overallAreaNeeded(numRats); if (overallArea < overallAreaNeeded) { failedCriteria[FAILED_OVERALL_AREA] = this.FAIL_CRITERIA[FAILED_OVERALL_AREA]; } return failedCriteria; } } class Dimensions { constructor(width, depth, height) { this.width = width; this.depth = depth; this.height = height; } toString() { return `${this.width}x${this.depth}x${this.height}`; } static fromDict(data) { const {width, depth, height} = data; return new Dimensions(width, depth, height); } } ////////////////////////// // DOCUMENT INTERACTION // ////////////////////////// /////// // 1 // /////// const numRatsCalculatorInputWidth = document.getElementById("num-rats-width"); numRatsCalculatorInputWidth.onchange = updateNumRatsCalculator; const numRatsCalculatorInputDepth = document.getElementById("num-rats-depth"); numRatsCalculatorInputDepth.onchange = updateNumRatsCalculator; const numRatsCalculatorInputHeight = document.getElementById("num-rats-height"); numRatsCalculatorInputHeight.onchange = updateNumRatsCalculator; let numRatsNumFullFloors = document.getElementById("num-rats-numFullFloors"); numRatsNumFullFloors.oninput = function () { updateNumRatsCalculator(); } /////// // 2 // /////// let cageCalcLabelNumRats = document.getElementById("cageCalcLabelNumRats"); let cageCalcRatSlider = document.getElementById("cageCalcNumRats"); cageCalcRatSlider.oninput = function () { updateCageCalc(); } /////// // 3 // /////// const inputWidth = document.getElementById("width"); inputWidth.onchange = updateCageCheck; const inputDepth = document.getElementById("depth"); inputDepth.onchange = updateCageCheck; const inputHeight = document.getElementById("height"); inputHeight.onchange = updateCageCheck; let fullFloorNum = document.getElementById("numFullFloors"); fullFloorNum.oninput = function () { updateCageCheck(); } let labelNumRats = document.getElementById("labelNumRats"); let ratSlider = document.getElementById("numRats"); ratSlider.oninput = function () { updateCageCheck(); } function getResultFromChecks(checks) { if (Object.keys(checks).length > 0) { const ul = document.createElement('ul'); for (const key in checks) { const li = document.createElement('li'); li.textContent = `❌ ` + checks[key]; ul.appendChild(li); } return ul; } else { const p = document.createElement('p'); p.innerHTML = "✅ " + i18next.t("cage-complies-with-all-criteria") return p; } } function formatCriteria(criteria) { const ul = document.createElement('ul'); for (const key in criteria) { const li = document.createElement('li'); li.textContent = `☑️ ` + criteria[key]; ul.appendChild(li); } return ul; } function updateCageCheck() { labelNumRats.innerHTML = i18next.t("cage-for-x-rats", {"num_rats": ratSlider.value}); const width = inputWidth.value const depth = inputDepth.value const height = inputHeight.value const dimensions = new Dimensions(width / 100, depth / 100, height / 100); const validator = new Validator(); const failed_checks = validator.cageCheck(dimensions, ratSlider.value, fullFloorNum.value); let resultsDiv = document.getElementById("resultsDiv"); const result = getResultFromChecks(failed_checks); resultsDiv.innerHTML = ""; resultsDiv.appendChild(result); } function updateCageCalc() { let numRats = cageCalcRatSlider.value cageCalcLabelNumRats.innerHTML = i18next.t("cage-for-x-rats", {"num_rats": numRats}); const validator = new Validator(); let criteria = validator.STATIC_CRITERIA; let minimumOverallArea = validator.overallAreaNeeded(cageCalcRatSlider.value); criteria[CRITERIA_OVERALL_AREA] = i18next.t('overall-area', {"numRats": numRats, "minimumOverallArea": minimumOverallArea}); let resultsDiv = document.getElementById("cageCalcResultsDiv"); const result = formatCriteria(criteria); resultsDiv.innerHTML = ""; resultsDiv.appendChild(result); } function updateNumRatsCalculator() { const width = numRatsCalculatorInputWidth.value const depth = numRatsCalculatorInputDepth.value const height = numRatsCalculatorInputHeight.value const dimensions = new Dimensions(width / 100, depth / 100, height / 100); const validator = new Validator(); const failed_checks = validator.failCageNumberIndependent(dimensions, fullFloorNum.value); let overallArea = validator.getOverallArea(dimensions, numRatsNumFullFloors.value); let allowedNumRats; try { allowedNumRats = validator.allowedNumberOfRats(overallArea); } catch (e) { console.log(e); failed_checks[FAILED_BASE_AREA] = validator.FAIL_CRITERIA[FAILED_OVERALL_AREA]; allowedNumRats = 0; } let resultsDiv = document.getElementById("num-rats-resultsDiv"); const result = getResultFromChecks(failed_checks); resultsDiv.innerHTML = ""; const p = document.createElement('p'); p.textContent = i18next.t("cage-for-x-rats", {"num_rats": allowedNumRats}); resultsDiv.appendChild(p); resultsDiv.appendChild(result); } const validator = new Validator();