feat: Add basic translation framework
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
10
i18next-parser.config.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
module.exports = {
|
||||||
|
defaultNamespace: 'translation',
|
||||||
|
lexers: {
|
||||||
|
js: ['JsxLexer'], // we're writing jsx inside .js files
|
||||||
|
default: ['JavascriptLexer'],
|
||||||
|
},
|
||||||
|
locales: ['en', 'de'],
|
||||||
|
output: 'public/lang/$LOCALE.json',
|
||||||
|
input: [ 'src/*.js', 'public/*.html', ],
|
||||||
|
}
|
5269
package-lock.json
generated
Normal file
30
package.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "kaefigrechner",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Ein Rechner für die minimale Größe eines Rattenkäfigs nach Standard des VdRD e.V.",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "webpack-dev-server --config webpack.config.js",
|
||||||
|
"ex-trans": "i18next -c i18next-parser.config.js"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.hyteck.de/moanos/RattenheimRechner"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"animal",
|
||||||
|
"welfare"
|
||||||
|
],
|
||||||
|
"author": "Julian-Samuel Gebühr",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"devDependencies": {
|
||||||
|
"webpack": "^5.93.0",
|
||||||
|
"webpack-cli": "^5.1.4",
|
||||||
|
"webpack-dev-server": "^5.0.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"i18next": "^23.12.2",
|
||||||
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
|
"i18next-http-backend": "^2.5.2"
|
||||||
|
}
|
||||||
|
}
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 432 B After Width: | Height: | Size: 432 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
1
public/assets/img/info.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg class="text-grey-dark" width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor"><path d="M9.00026 12.6C9.00026 12.6 9.00026 12.1224 9.00026 11.5333V8.86666C9.00026 8.57211 8.76148 8.33333 8.46693 8.33333H7.93359" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M8.73346 5.26666C8.58619 5.26666 8.4668 5.38605 8.4668 5.53333C8.4668 5.68061 8.58619 5.8 8.73346 5.8C8.88074 5.8 9.00013 5.68061 9.00013 5.53333C9.00013 5.38605 8.88074 5.26666 8.73346 5.26666V5.26666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M9 17C13.4183 17 17 13.4183 17 9C17 4.58172 13.4183 1 9 1C4.58172 1 1 4.58172 1 9C1 13.4183 4.58172 17 9 17Z" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
After Width: | Height: | Size: 834 B |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 546 B After Width: | Height: | Size: 546 B |
Before Width: | Height: | Size: 3.2 MiB After Width: | Height: | Size: 3.2 MiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 639 B After Width: | Height: | Size: 639 B |
@ -18,6 +18,12 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<img src="img/translation-icon@2x.png" class="translation-icon" / >
|
||||||
|
<select data-i18n-switcher class="locale-switcher">
|
||||||
|
<option value="en">English</option>
|
||||||
|
<option value="de">Deutsch</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h1 data-i18n-key="app-name">Käfigrechner</h1>
|
<h1 data-i18n-key="app-name">Käfigrechner</h1>
|
||||||
<div class="container-form">
|
<div class="container-form">
|
||||||
@ -89,9 +95,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="ncontainer">
|
<div class="ncontainer">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<button onclick="decreaseFloorNum()">-</button>
|
<button id="decreaseFloorNum">-</button>
|
||||||
<input type="text" id="numFullFloors" value="3" readonly>
|
<input type="text" id="numFullFloors" value="3" readonly>
|
||||||
<button onclick="increaseFloorNum()">+</button>
|
<button id="increaseFloorNum">+</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -108,145 +114,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
|
||||||
|
|
||||||
const inputWidth = document.getElementById("width");
|
<script src="./bundle.js"></script>
|
||||||
inputWidth.onchange = updateViaManualMeasurements;
|
|
||||||
const inputDepth = document.getElementById("depth");
|
|
||||||
inputDepth.onchange = updateViaManualMeasurements;
|
|
||||||
const inputHeight = document.getElementById("height");
|
|
||||||
inputHeight.onchange = updateViaManualMeasurements;
|
|
||||||
|
|
||||||
|
|
||||||
const selectSavicSuiteRoyaleXL = document.getElementById("SavicSuiteRoyaleXL");
|
|
||||||
const selectSavicSuiteRoyale95Double = document.getElementById("SavicSuiteRoyale95Double");
|
|
||||||
const selectTiakiKleintierkäfigEtagere = document.getElementById("TiakiKleintierkäfigEtagere");
|
|
||||||
|
|
||||||
const cardSavicSuiteRoyaleXL = document.getElementById("card-SavicSuiteRoyaleXL");
|
|
||||||
const cardSavicSuiteRoyale95Double = document.getElementById("card-SavicSuiteRoyale95Double");
|
|
||||||
const cardTiakiKleintierkäfigEtagere = document.getElementById("card-TiakiKleintierkäfigEtagere");
|
|
||||||
|
|
||||||
|
|
||||||
function markActiveCage(cageName) {
|
|
||||||
|
|
||||||
cardSavicSuiteRoyaleXL.classList.remove("card-active");
|
|
||||||
cardSavicSuiteRoyale95Double.classList.remove("card-active");
|
|
||||||
cardTiakiKleintierkäfigEtagere.classList.remove("card-active");
|
|
||||||
if (cageName != "") {
|
|
||||||
const activeCage = document.getElementById("card-" + cageName);
|
|
||||||
activeCage.classList.add("card-active")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCage(event) {
|
|
||||||
selectSavicSuiteRoyaleXL.checked = false;
|
|
||||||
selectSavicSuiteRoyale95Double.checked = false;
|
|
||||||
selectTiakiKleintierkäfigEtagere.checked = false;
|
|
||||||
const selectedCage = event.currentTarget
|
|
||||||
selectedCage.checked = true;
|
|
||||||
const cageName = selectedCage.id;
|
|
||||||
|
|
||||||
console.log(cageName);
|
|
||||||
var dim = getCageDimensions(cageName);
|
|
||||||
inputWidth.value = dim.width;
|
|
||||||
inputDepth.value = dim.depth;
|
|
||||||
inputHeight.value = dim.height;
|
|
||||||
|
|
||||||
markActiveCage(cageName);
|
|
||||||
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
selectSavicSuiteRoyaleXL.onchange = updateCage;
|
|
||||||
selectSavicSuiteRoyale95Double.onchange = updateCage;
|
|
||||||
selectTiakiKleintierkäfigEtagere.onchange = updateCage;
|
|
||||||
|
|
||||||
|
|
||||||
var labelNumRats = document.getElementById("labelNumRats");
|
|
||||||
|
|
||||||
var ratSlider = document.getElementById("numRats");
|
|
||||||
|
|
||||||
labelNumRats.innerHTML = `Anzahl an Ratten: ` + ratSlider.value;
|
|
||||||
|
|
||||||
ratSlider.oninput = function () {
|
|
||||||
labelNumRats.innerHTML = `Anzahl an Ratten: ` + this.value;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Full floor functions
|
|
||||||
var fullFloorNum = document.getElementById("numFullFloors");
|
|
||||||
|
|
||||||
function getCageDimensions(cageName) {
|
|
||||||
console.log(cageName);
|
|
||||||
if (cageName == "SavicSuiteRoyaleXL") {
|
|
||||||
return new Dimensions(115, 67.5, 153);
|
|
||||||
}
|
|
||||||
if (cageName == "SavicSuiteRoyale95Double") {
|
|
||||||
return new Dimensions(95, 63, 120);
|
|
||||||
}
|
|
||||||
if (cageName == "TiakiKleintierkäfigEtagere") {
|
|
||||||
return new Dimensions(93.5, 63, 141.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getResultFromChecks(checks) {
|
|
||||||
console.log(checks.length)
|
|
||||||
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 = "✅ Der Käfig erfüllt alle Kriterien!"
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateViaManualMeasurements() {
|
|
||||||
markActiveCage("ManualMeasurements");
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
function update() {
|
|
||||||
const width = inputWidth.value
|
|
||||||
const depth = inputDepth.value
|
|
||||||
const height = inputHeight.value
|
|
||||||
const dimensions = new Dimensions(width / 100, depth / 100, height / 100);
|
|
||||||
const failed_checks = cageCheck(dimensions, ratSlider.value, fullFloorNum.value);
|
|
||||||
console.log(failed_checks);
|
|
||||||
let resultsDiv = document.getElementById("resultsDiv");
|
|
||||||
resultsDiv.innerHTML = `<strong>Ergebnis</strong>`;
|
|
||||||
|
|
||||||
const result = getResultFromChecks(failed_checks);
|
|
||||||
|
|
||||||
resultsDiv.appendChild(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function decreaseFloorNum() {
|
|
||||||
var input = document.getElementById('numFullFloors');
|
|
||||||
var value = parseInt(input.value);
|
|
||||||
if (value > 0) {
|
|
||||||
input.value = value - 1;
|
|
||||||
}
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
function increaseFloorNum() {
|
|
||||||
var input = document.getElementById('numFullFloors');
|
|
||||||
var value = parseInt(input.value);
|
|
||||||
input.value = value + 1;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
update();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
10
public/lang/de.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"back-to-home": "Zurück zur Homepage",
|
||||||
|
"app-name": "Käfigrechner",
|
||||||
|
"number-of-rats": "Anzahl an Ratten",
|
||||||
|
"full-floors": "Vollebenen",
|
||||||
|
"width-cm": "Breite (cm)",
|
||||||
|
"depth-cm": "Tiefe (cm)",
|
||||||
|
"height-cm": "Höhe (cm)",
|
||||||
|
"full-floors-tooltip": "Als Vollebenen zählen alle Ebenen die größer als 0.5m² sind, inklusive des Käfigbodens."
|
||||||
|
}
|
10
public/lang/en.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"back-to-home": "Back to home",
|
||||||
|
"app-name": "Cage Calculator",
|
||||||
|
"number-of-rats": "Number of Rats",
|
||||||
|
"full-floors": "Full floors",
|
||||||
|
"width-cm": "Width (cm)",
|
||||||
|
"depth-cm": "Depth (cm)",
|
||||||
|
"height-cm": "Height (cm)",
|
||||||
|
"full-floors-tooltip": "A full floor is each floor with a area greater than 0.5m², including the bottom of the cage."
|
||||||
|
}
|
196
src/index.js
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
const inputDecreaseFloorNum = document.getElementById("decreaseFloorNum");
|
||||||
|
inputDecreaseFloorNum.onclick = decreaseFloorNum;
|
||||||
|
|
||||||
|
const inputIncreaseFloorNum = document.getElementById("increaseFloorNum");
|
||||||
|
inputIncreaseFloorNum.onclick = increaseFloorNum;
|
||||||
|
|
||||||
|
|
||||||
|
const inputWidth = document.getElementById("width");
|
||||||
|
inputWidth.onchange = updateViaManualMeasurements;
|
||||||
|
const inputDepth = document.getElementById("depth");
|
||||||
|
inputDepth.onchange = updateViaManualMeasurements;
|
||||||
|
const inputHeight = document.getElementById("height");
|
||||||
|
inputHeight.onchange = updateViaManualMeasurements;
|
||||||
|
|
||||||
|
|
||||||
|
const selectSavicSuiteRoyaleXL = document.getElementById("SavicSuiteRoyaleXL");
|
||||||
|
const selectSavicSuiteRoyale95Double = document.getElementById("SavicSuiteRoyale95Double");
|
||||||
|
const selectTiakiKleintierkäfigEtagere = document.getElementById("TiakiKleintierkäfigEtagere");
|
||||||
|
|
||||||
|
const cardSavicSuiteRoyaleXL = document.getElementById("card-SavicSuiteRoyaleXL");
|
||||||
|
const cardSavicSuiteRoyale95Double = document.getElementById("card-SavicSuiteRoyale95Double");
|
||||||
|
const cardTiakiKleintierkäfigEtagere = document.getElementById("card-TiakiKleintierkäfigEtagere");
|
||||||
|
|
||||||
|
|
||||||
|
function markActiveCage(cageName) {
|
||||||
|
|
||||||
|
cardSavicSuiteRoyaleXL.classList.remove("card-active");
|
||||||
|
cardSavicSuiteRoyale95Double.classList.remove("card-active");
|
||||||
|
cardTiakiKleintierkäfigEtagere.classList.remove("card-active");
|
||||||
|
if (cageName != "") {
|
||||||
|
const activeCage = document.getElementById("card-" + cageName);
|
||||||
|
activeCage.classList.add("card-active")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCage(event) {
|
||||||
|
selectSavicSuiteRoyaleXL.checked = false;
|
||||||
|
selectSavicSuiteRoyale95Double.checked = false;
|
||||||
|
selectTiakiKleintierkäfigEtagere.checked = false;
|
||||||
|
const selectedCage = event.currentTarget
|
||||||
|
selectedCage.checked = true;
|
||||||
|
const cageName = selectedCage.id;
|
||||||
|
|
||||||
|
console.log(cageName);
|
||||||
|
var dim = getCageDimensions(cageName);
|
||||||
|
inputWidth.value = dim.width;
|
||||||
|
inputDepth.value = dim.depth;
|
||||||
|
inputHeight.value = dim.height;
|
||||||
|
|
||||||
|
markActiveCage(cageName);
|
||||||
|
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
selectSavicSuiteRoyaleXL.onchange = updateCage;
|
||||||
|
selectSavicSuiteRoyale95Double.onchange = updateCage;
|
||||||
|
selectTiakiKleintierkäfigEtagere.onchange = updateCage;
|
||||||
|
|
||||||
|
|
||||||
|
var labelNumRats = document.getElementById("labelNumRats");
|
||||||
|
|
||||||
|
var ratSlider = document.getElementById("numRats");
|
||||||
|
|
||||||
|
labelNumRats.innerHTML = `Anzahl an Ratten: ` + ratSlider.value;
|
||||||
|
|
||||||
|
ratSlider.oninput = function () {
|
||||||
|
labelNumRats.innerHTML = `Anzahl an Ratten: ` + this.value;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full floor functions
|
||||||
|
var fullFloorNum = document.getElementById("numFullFloors");
|
||||||
|
|
||||||
|
function getCageDimensions(cageName) {
|
||||||
|
console.log(cageName);
|
||||||
|
if (cageName == "SavicSuiteRoyaleXL") {
|
||||||
|
return new Dimensions(115, 67.5, 153);
|
||||||
|
}
|
||||||
|
if (cageName == "SavicSuiteRoyale95Double") {
|
||||||
|
return new Dimensions(95, 63, 120);
|
||||||
|
}
|
||||||
|
if (cageName == "TiakiKleintierkäfigEtagere") {
|
||||||
|
return new Dimensions(93.5, 63, 141.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getResultFromChecks(checks) {
|
||||||
|
console.log(checks.length)
|
||||||
|
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 = "✅ Der Käfig erfüllt alle Kriterien!"
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateViaManualMeasurements() {
|
||||||
|
markActiveCage("ManualMeasurements");
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
const width = inputWidth.value
|
||||||
|
const depth = inputDepth.value
|
||||||
|
const height = inputHeight.value
|
||||||
|
const dimensions = new Dimensions(width / 100, depth / 100, height / 100);
|
||||||
|
const failed_checks = cageCheck(dimensions, ratSlider.value, fullFloorNum.value);
|
||||||
|
console.log(failed_checks);
|
||||||
|
let resultsDiv = document.getElementById("resultsDiv");
|
||||||
|
resultsDiv.innerHTML = `<strong>Ergebnis</strong>`;
|
||||||
|
|
||||||
|
const result = getResultFromChecks(failed_checks);
|
||||||
|
|
||||||
|
resultsDiv.appendChild(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function decreaseFloorNum() {
|
||||||
|
var input = document.getElementById('numFullFloors');
|
||||||
|
var value = parseInt(input.value);
|
||||||
|
if (value > 0) {
|
||||||
|
input.value = value - 1;
|
||||||
|
}
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
function increaseFloorNum() {
|
||||||
|
var input = document.getElementById('numFullFloors');
|
||||||
|
var value = parseInt(input.value);
|
||||||
|
input.value = value + 1;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
update();
|
||||||
|
|
||||||
|
|
||||||
|
import i18next from "i18next";
|
||||||
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
|
import HttpApi from "i18next-http-backend";
|
||||||
|
|
||||||
|
async function initI18next() {
|
||||||
|
// We use() the backend and await it to load
|
||||||
|
// the translations from the network
|
||||||
|
await i18next
|
||||||
|
.use(HttpApi)
|
||||||
|
.use(LanguageDetector)
|
||||||
|
.init({
|
||||||
|
lng: "en",
|
||||||
|
supportedLngs: ["en", "de"],
|
||||||
|
nonExplicitSupportedLngs: true,
|
||||||
|
fallbackLng: "en",
|
||||||
|
debug: true,
|
||||||
|
backend: {
|
||||||
|
loadPath: "/lang/{{lng}}.json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function translatePageElements() {
|
||||||
|
const translatableElements = document.querySelectorAll(
|
||||||
|
"[data-i18n-key]",
|
||||||
|
);
|
||||||
|
translatableElements.forEach((el) => {
|
||||||
|
const key = el.getAttribute("data-i18n-key");
|
||||||
|
el.innerHTML = i18next.t(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
|
||||||
|
function bindLocaleSwitcher(initialValue) {
|
||||||
|
const switcher = document.querySelector(
|
||||||
|
"[data-i18n-switcher]",
|
||||||
|
);
|
||||||
|
switcher.value = initialValue;
|
||||||
|
switcher.onchange = (e) => {
|
||||||
|
i18next
|
||||||
|
.changeLanguage(e.target.value)
|
||||||
|
.then(translatePageElements);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
(async function () {
|
||||||
|
await initI18next();
|
||||||
|
translatePageElements();
|
||||||
|
bindLocaleSwitcher(i18next.resolvedLanguage);
|
||||||
|
})();
|
25
webpack.config.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const port = 3000;
|
||||||
|
const openBrowser = true;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: {
|
||||||
|
app: ["./src/index.js"],
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
filename: "bundle.js",
|
||||||
|
path: path.resolve(__dirname, "public"),
|
||||||
|
publicPath: "/",
|
||||||
|
},
|
||||||
|
mode: "development",
|
||||||
|
devtool: "source-map",
|
||||||
|
devServer: {
|
||||||
|
port: port,
|
||||||
|
open: openBrowser,
|
||||||
|
historyApiFallback: {
|
||||||
|
index: "index.html",
|
||||||
|
},
|
||||||
|
static: "public",
|
||||||
|
},
|
||||||
|
};
|