diff --git a/.gitignore b/.gitignore
index d027e22..98e8302 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,5 +14,6 @@ node_modules/
*.ai
# Temp
+static/resources/DecimalJs*
static/resources/SortableJS
static/resources/Standalone
diff --git a/data/applets/vat-calculator.yml b/data/applets/vat-calculator.yml
new file mode 100644
index 0000000..1ea6dee
--- /dev/null
+++ b/data/applets/vat-calculator.yml
@@ -0,0 +1,7 @@
+applets:
+ - id: "vat-calculator"
+ resources:
+ scripts:
+ - "applet://vat-calculator.mjs"
+ stylesheets:
+ - "applet://vat-calculator.css"
diff --git a/data/sitemap.yml b/data/sitemap.yml
index 92ffc3a..5545a56 100644
--- a/data/sitemap.yml
+++ b/data/sitemap.yml
@@ -14,3 +14,4 @@
- "/tools/iban-generator/"
- "/tools/excel-password-remover/"
- "/tools/uuid-generator/"
+- "/tools/vat-calculator/"
diff --git a/data/strings/en/commons.yml b/data/strings/en/commons.yml
index fa671f5..cda46b0 100644
--- a/data/strings/en/commons.yml
+++ b/data/strings/en/commons.yml
@@ -91,3 +91,19 @@ format.json: "JSON"
format.yaml: "YAML"
action.generate: "Generate"
+
+country.afghanistan: "Afghanistan"
+country.albania: "Albania"
+country.algeria: "Algeria"
+country.andorra: "Andorra"
+country.angola: "Angola"
+country.anguilla: "Anguilla"
+country.argentina: "Argentina"
+country.australia: "Australia"
+country.austria: "Austria"
+country.azerbaijan: "Azerbaijan"
+country.bahamas: "Bahamas"
+
+country.belgium: "Belgium"
+
+country.luxembourg: "Luxembourg"
diff --git a/data/strings/en/vat-calculator.yml b/data/strings/en/vat-calculator.yml
new file mode 100644
index 0000000..97cc112
--- /dev/null
+++ b/data/strings/en/vat-calculator.yml
@@ -0,0 +1,50 @@
+# EN - VAT Calculator
+
+meta.title: "VAT Calculator"
+meta.description: "Simple VAT calculator with a selection of common rates for over X countries."
+
+preset.label: "Official rates"
+
+option.detailed: "Show VAT rate types"
+
+radio.rate: "VAT rate: "
+radio.untaxed: "Excl. VAT: "
+radio.taxed: "Incl. VAT: "
+
+text.radio.explanation: "The selected radio input indicates the automatically calculated field."
+
+rate.option.custom: "Custom rate"
+
+rate.type.standard: "Standard"
+rate.type.intermediate: "Intermediate"
+rate.type.reduced: "Reduced"
+rate.type.reduced.super: "Super reduced"
+
+option.decimal-places: "Decimal places count"
+option.trim-zeroes: "Trim trailing zeroes"
+
+rounding.mode.label: "Rounding mode"
+
+rounding.mode.group.regular: "Regular rounding"
+rounding.mode.group.half: "Half rounding (Towards nearest neighbour)"
+
+rounding.mode.up: "Away from zero (Up)"
+rounding.mode.down: "Towards zero (Down)"
+rounding.mode.ceil: "Towards infinity (Ceil)"
+rounding.mode.floor: "Towards negative infinity (Floor)"
+
+rounding.mode.up.half: "Away from zero if equidistant (Half up)"
+rounding.mode.down.half: "Towards zero if equidistant (Half down)"
+rounding.mode.even.half: "Towards even neighbour if equidistant (Half even)"
+rounding.mode.ceil.half: "Towards infinity if equidistant (Half ceil)"
+rounding.mode.floor.half: "Towards negative infinity if equidistant (Half floor)"
+
+#rounding.mode.up.half: "Towards nearest neighbour, away from zero if equidistant (Half up)"
+#rounding.mode.down.half: "Towards nearest neighbour, towards zero if equidistant (Half down)"
+#rounding.mode.even.half: "Towards nearest neighbour, towards even neighbour if equidistant (Half even)"
+#rounding.mode.ceil.half: "Towards nearest neighbour, towards infinity if equidistant (Half ceil)"
+#rounding.mode.floor.half: "Towards nearest neighbour, towards negative infinity if equidistant (Half floor)"
+
+license.text.1: "This tool uses the decimal.js-light
+library, which is licensed under the MIT license."
+license.text.2: "The rest of this tool's code is released in the public domain."
diff --git a/data/strings/fr/commons.yml b/data/strings/fr/commons.yml
index f043abe..bceecc6 100644
--- a/data/strings/fr/commons.yml
+++ b/data/strings/fr/commons.yml
@@ -91,3 +91,7 @@ format.json: "JSON"
format.yaml: "YAML"
action.generate: "Générer"
+
+country.belgium: "Belgique"
+
+country.luxembourg: "Luxembourg"
diff --git a/data/strings/fr/vat-calculator.yml b/data/strings/fr/vat-calculator.yml
new file mode 100644
index 0000000..498be49
--- /dev/null
+++ b/data/strings/fr/vat-calculator.yml
@@ -0,0 +1,50 @@
+# FR - VAT Calculator
+
+meta.title: "Calculateur de TVA"
+meta.description: "Simple calculateur de TVA avec une selection de taux communs pour plus de X pays."
+
+preset.label: "Taux officiel"
+
+option.detailed: "Afficher le type de TVA"
+
+radio.rate: "Taux: "
+radio.untaxed: "HTVA: "
+radio.taxed: "TTC: "
+
+text.radio.explanation: "Le cercle en vert indique la valeur automatiquement calculée."
+
+rate.option.custom: "Taux personalisé"
+
+rate.type.standard: "Standard"
+rate.type.intermediate: "Intermédiaire"
+rate.type.reduced: "Réduit"
+rate.type.reduced.super: "Super réduit"
+
+option.decimal-places: "Nombre de décimales"
+option.trim-zeroes: "Supprimer les zéros de fin"
+
+rounding.mode.label: "Type d'arrondi"
+
+rounding.mode.group.regular: "Arrondis classiques"
+rounding.mode.group.half: "Demi-arrondis (Vers le plus proche)"
+
+rounding.mode.up: "Loin de zéro (Arrondi au supérieur)"
+rounding.mode.down: "Vers zéro (Arrondi à l'inférieur)"
+rounding.mode.ceil: "Vers l'infini (Par plafond)"
+rounding.mode.floor: "Vers moins l'infini (Par plancher)"
+
+rounding.mode.up.half: "Loin de zéro si équidistant (Demi supérieur)"
+rounding.mode.down.half: "Vers zéro si équidistant (Demi inférieur)"
+rounding.mode.even.half: "Vers le pair si équidistant (Demi pair)"
+rounding.mode.ceil.half: "Vers l'infini (Demi plafond)"
+rounding.mode.floor.half: "Vers moins l'infini (Demi plancher)"
+
+#rounding.mode.up.half: "Vers le plus proche, loin de zéro si équidistant (Demi supérieur)"
+#rounding.mode.down.half: "Vers le plus proche, vers zéro si équidistant (Demi inférieur)"
+#rounding.mode.even.half: "Vers le plus proche, vers le pair si équidistant (Demi pair)"
+#rounding.mode.ceil.half: "Vers le plus proche, vers l'infini (Demi plafond)"
+#rounding.mode.floor.half: "Vers le plus proche, vers moins l'infini (Demi plancher)"
+
+license.text.1: "Cet outil utilise la bibliothèque decimal.js-light,
+qui est distribuée sous licence MIT."
+license.text.2: "Le reste du code de cet outil est placé dans le domaine public."
diff --git a/data/tools/vat-calculator.yml b/data/tools/vat-calculator.yml
new file mode 100644
index 0000000..34a6822
--- /dev/null
+++ b/data/tools/vat-calculator.yml
@@ -0,0 +1,31 @@
+
+tools:
+ - id: "vat-calculator"
+ applet_id: "vat-calculator"
+ metadata:
+ head:
+ title_key: "meta.title"
+ description_key: "meta.description"
+ opengraph:
+ title_key: "meta.title"
+ description_key: "meta.description"
+ type: null
+ url: null
+ image_url: "/resources/NibblePoker/images/tools/vat-calculator/main-quiet.png"
+ image_type: null
+ twitter:
+ title_key: "meta.title"
+ description_key: "meta.description"
+ index:
+ priority: 500
+ enable: true
+ title_key: "meta.title"
+ preamble_key: "meta.description"
+ image_url: "/resources/NibblePoker/images/tools/vat-calculator/main-quiet.png"
+ image_alt_key: ""
+ general:
+ icon: "fab fa-python"
+ title_key: "meta.title"
+ subtitle_key: "article.subtitle"
+ tags:
+ - "calculator"
diff --git a/scripts/compile-js-site.cmd b/scripts/compile-js-site.cmd
index edd0fda..0ce3a85 100644
--- a/scripts/compile-js-site.cmd
+++ b/scripts/compile-js-site.cmd
@@ -93,6 +93,16 @@ call "%~dp0node_modules\.bin\rollup" png-analyser.mjs --file png-analyser.js
call "%~dp0node_modules\.bin\terser" png-analyser.js -c -m -o png-analyser.min.js
popd
+
+:js-vatcalculator-minify
+echo Minifying VAT Calculator
+pushd %CD%
+cd %~dp0\..\static\resources\NibblePoker\applets\vat-calculator\
+echo ^> static\resources\NibblePoker\applets\vat-calculator\vat-calculator.mjs
+call "%~dp0node_modules\.bin\rollup" vat-calculator.mjs --file vat-calculator.js
+call "%~dp0node_modules\.bin\terser" vat-calculator.js -c -m -o vat-calculator.min.js
+popd
+
:js-nibblepoker-end
:end
diff --git a/scripts/package.json b/scripts/package.json
index c9976ba..539f4b7 100644
--- a/scripts/package.json
+++ b/scripts/package.json
@@ -1,10 +1,11 @@
{
"devDependencies": {
"minify": "^10.2.0",
- "rollup": "^3.27.2",
+ "rollup": "^4.48.1",
"sass": "^1.63.6",
"terser": "^5.19.0",
"typescript": "^5.1.6",
- "html-minifier-terser": "^7.2.0"
+ "html-minifier-terser": "^7.2.0",
+ "browserify": "^17.0.1"
}
}
diff --git a/static/resources/NibblePoker/applets/vat-calculator/vat-calculator.css b/static/resources/NibblePoker/applets/vat-calculator/vat-calculator.css
new file mode 100644
index 0000000..e69de29
diff --git a/static/resources/NibblePoker/applets/vat-calculator/vat-calculator.mjs b/static/resources/NibblePoker/applets/vat-calculator/vat-calculator.mjs
new file mode 100644
index 0000000..47375c2
--- /dev/null
+++ b/static/resources/NibblePoker/applets/vat-calculator/vat-calculator.mjs
@@ -0,0 +1,248 @@
+import {initCore} from "../../js/nibblepoker-core.mjs";
+//import {Decimal} from "../../../DecimalJs/10.6.0/decimal.mjs";
+import {Decimal} from "../../../DecimalJs-Light/2.5.1/decimal.mjs";
+import {getInputCount, getInputNumber} from "../../libs/input-utils.mjs";
+
+// Tool-centric stuff
+{
+ initCore();
+
+ const classesReadonly = ["bkgd-gray"];
+
+ const calcRadioGroupName = "vat_calc_target";
+
+ /** @type {HTMLLabelElement} */
+ const ePresetShortLabel = document.querySelector("label[for=vat-calculator-preset-short]");
+ /** @type {HTMLSelectElement} */
+ const ePresetShortSelect = document.getElementById("vat-calculator-preset-short");
+
+ /** @type {HTMLLabelElement} */
+ const ePresetDetailedLabel = document.querySelector("label[for=vat-calculator-preset-detailed]");
+ /** @type {HTMLSelectElement} */
+ const ePresetDetailedSelect = document.getElementById("vat-calculator-preset-detailed");
+
+ /** @type {HTMLInputElement} */
+ const eCheckboxDetailedPreset = document.getElementById("vat-calculator-detailed-presets");
+
+ /** @type {HTMLSpanElement} */
+ const ePresetEchoedCountry = document.getElementById("vat-calculator-preset-country-echo");
+
+ /** @type {HTMLButtonElement} */
+ const eButtonDecimalPlacesMinus = document.getElementById("vat-calculator-decimal-places-minus");
+ /** @type {HTMLInputElement} */
+ const eInputDecimalPlaces = document.getElementById("vat-calculator-option-decimal-places");
+ /** @type {HTMLButtonElement} */
+ const eButtonDecimalPlacesPlus = document.getElementById("vat-calculator-decimal-places-plus");
+
+ /* #vat-calculator-detailed-trim-zeroes */
+
+ /** @type {HTMLInputElement} */
+ const eCalcRateRadio = document.getElementById("vat-calculator-radio-rate");
+ /** @type {HTMLInputElement} */
+ const eCalcRateInput = document.getElementById("vat-calculator-input-rate");
+
+ /** @type {HTMLInputElement} */
+ const eCalcUntaxedRadio = document.getElementById("vat-calculator-radio-untaxed");
+ /** @type {HTMLInputElement} */
+ const eCalcUntaxedInput = document.getElementById("vat-calculator-input-untaxed");
+
+ /** @type {HTMLInputElement} */
+ const eCalcTaxedRadio = document.getElementById("vat-calculator-radio-taxed");
+ /** @type {HTMLInputElement} */
+ const eCalcTaxedInput = document.getElementById("vat-calculator-input-taxed");
+
+ /** @type {HTMLSelectElement} */
+ const eRoundingModeSelect = document.getElementById("vat-calculator-rounding-mode");
+
+ /**
+ * Handles the switch between the short and detailed standard rates selects
+ */
+ function handlePresetDetailLevelChange() {
+ ePresetShortLabel.hidden = eCheckboxDetailedPreset.checked;
+ ePresetShortSelect.hidden = eCheckboxDetailedPreset.checked;
+ ePresetDetailedLabel.hidden = !eCheckboxDetailedPreset.checked;
+ ePresetDetailedSelect.hidden = !eCheckboxDetailedPreset.checked;
+ }
+
+ /** @returns {number} */
+ function getDecimalPlaces() {
+ return getInputCount(eInputDecimalPlaces, 0, 99);
+ }
+
+ function changeDecimalPlacesDesiredCount(difference = 0) {
+ if (difference !== 0) {
+ eInputDecimalPlaces.value = getInputCount(eInputDecimalPlaces, 0, 99) + difference;
+ }
+ eInputDecimalPlaces.value = getInputCount(eInputDecimalPlaces, 0, 99);
+ }
+
+ /**
+ * Handles the locking and unlocking of the calculator input fields.
+ * @param eInput {HTMLInputElement}
+ * @param isLocked {boolean}
+ */
+ function setCalcFieldLockStatus(eInput, isLocked) {
+ eInput.readOnly = isLocked;
+ classesReadonly.forEach((roClass) => {
+ eInput.classList.remove(roClass);
+ if(isLocked) {
+ eInput.classList.add(roClass);
+ }
+ });
+ }
+
+ function handleCalcValueChange() {
+ let vatRate = getInputNumber(eCalcRateInput);
+ let untaxedValue = getInputNumber(eCalcUntaxedInput);
+ let taxedValue = getInputNumber(eCalcTaxedInput);
+
+ if(eCalcRateRadio.checked) {
+ if(untaxedValue === null || taxedValue === null || isNaN(untaxedValue) || isNaN(taxedValue)) {
+ return;
+ }
+ untaxedValue = new Decimal(eCalcUntaxedInput.value);
+ taxedValue = new Decimal(eCalcTaxedInput.value);
+
+ eCalcRateInput.value = taxedValue
+ .minus(untaxedValue)
+ .div(untaxedValue)
+ .times(100)
+ .toDecimalPlaces(getDecimalPlaces());
+ } else if(eCalcUntaxedRadio.checked) {
+ if(vatRate === null || taxedValue === null || isNaN(vatRate) || isNaN(taxedValue)) {
+ return;
+ }
+ vatRate = new Decimal(eCalcRateInput.value).dividedBy(100).plus(1);
+ taxedValue = new Decimal(eCalcTaxedInput.value);
+
+ eCalcUntaxedInput.value = taxedValue
+ .dividedBy(vatRate)
+ .toDecimalPlaces(getDecimalPlaces());
+ } else if(eCalcTaxedRadio.checked) {
+ if(vatRate === null || untaxedValue === null || isNaN(vatRate) || isNaN(untaxedValue)) {
+ return;
+ }
+ vatRate = new Decimal(eCalcRateInput.value).dividedBy(100).plus(1);
+ untaxedValue = new Decimal(eCalcUntaxedInput.value);
+
+ eCalcTaxedInput.value = untaxedValue
+ .times(vatRate)
+ .toDecimalPlaces(getDecimalPlaces());
+ }
+ }
+
+ function handleDecimalConfigChange() {
+ Decimal.set({
+ rounding: getInputCount(eRoundingModeSelect, 0, 8),
+ precision: 99,
+ defaults: true,
+ });
+ }
+
+ function handlePresetChange() {
+ if(ePresetShortSelect.value.length > 0) {
+ let eSelectedOption = ePresetShortSelect.querySelector('option:checked');
+ if(eSelectedOption === null) {
+ return;
+ }
+
+ let eSelectedOptionGroup = eSelectedOption.closest('optgroup');
+ if(eSelectedOptionGroup === null) {
+ return;
+ }
+
+ ePresetEchoedCountry.innerHTML = eSelectedOptionGroup.label;
+ } else {
+ ePresetEchoedCountry.innerHTML = "";
+ }
+ }
+
+ window.onload = function () {
+ // Handling the detailed rate toggle
+ eCheckboxDetailedPreset.addEventListener("click", function () {
+ handlePresetDetailLevelChange();
+ });
+
+ // Handling the rate select input
+ ePresetShortSelect.addEventListener("change", function () {
+ ePresetDetailedSelect.selectedIndex = ePresetShortSelect.selectedIndex;
+ eCalcRateInput.value = ePresetShortSelect.value;
+ handlePresetChange();
+ handleCalcValueChange();
+ });
+ ePresetDetailedSelect.addEventListener("change", function () {
+ ePresetShortSelect.selectedIndex = ePresetDetailedSelect.selectedIndex;
+ eCalcRateInput.value = ePresetDetailedSelect.value;
+ handlePresetChange();
+ handleCalcValueChange();
+ });
+
+ // Handling calc radio input change
+ document.addEventListener("change", (e) => {
+ if (e.target.type === "radio" && e.target.name === calcRadioGroupName) {
+ setCalcFieldLockStatus(eCalcRateInput, e.target.value === "0");
+ setCalcFieldLockStatus(eCalcUntaxedInput, e.target.value === "1");
+ setCalcFieldLockStatus(eCalcTaxedInput, e.target.value === "2");
+
+ ePresetDetailedSelect.disabled = e.target === eCalcRateRadio;
+ ePresetShortSelect.disabled = e.target === eCalcRateRadio;
+ }
+ });
+ eCalcRateRadio.addEventListener("change", function () {
+ ePresetShortSelect.selectedIndex = 0;
+ ePresetDetailedSelect.selectedIndex = 0;
+ handlePresetChange();
+ });
+
+ // Handling decimal places options
+ eButtonDecimalPlacesMinus.addEventListener("click", function () {
+ changeDecimalPlacesDesiredCount(-1);
+ handleDecimalConfigChange();
+ handleCalcValueChange();
+ });
+ eButtonDecimalPlacesPlus.addEventListener("click", function () {
+ changeDecimalPlacesDesiredCount(1);
+ handleDecimalConfigChange();
+ handleCalcValueChange();
+ });
+ eInputDecimalPlaces.addEventListener("change", function() {
+ changeDecimalPlacesDesiredCount(0);
+ handleDecimalConfigChange();
+ handleCalcValueChange();
+ });
+ eInputDecimalPlaces.addEventListener("mousewheel", function(e) {
+ // Handling wheel scroll on count field.
+ if(e.wheelDelta < 0) {
+ changeDecimalPlacesDesiredCount(-1);
+ } else {
+ changeDecimalPlacesDesiredCount(1);
+ }
+ handleDecimalConfigChange();
+ handleCalcValueChange();
+ });
+
+ // Handling other DecimalJs config fields
+ eRoundingModeSelect.addEventListener("change", function() {
+ handleDecimalConfigChange();
+ handleCalcValueChange();
+ });
+
+ // Handling the calculator field changes
+ eCalcRateInput.addEventListener("change", function() {
+ ePresetShortSelect.selectedIndex = 0;
+ ePresetDetailedSelect.selectedIndex = 0;
+ handleCalcValueChange();
+ handlePresetChange();
+ });
+ eCalcUntaxedInput.addEventListener("change", function() {
+ handleCalcValueChange();
+ });
+ eCalcTaxedInput.addEventListener("change", function() {
+ handleCalcValueChange();
+ });
+
+ handlePresetDetailLevelChange();
+ handleDecimalConfigChange();
+ handlePresetChange();
+ }
+}
\ No newline at end of file
diff --git a/static/resources/NibblePoker/images/content/docker-mini-cctv-nvr/main.png b/static/resources/NibblePoker/images/content/docker-mini-cctv-nvr/main.png
index 3783fa5..bf4fb8e 100644
Binary files a/static/resources/NibblePoker/images/content/docker-mini-cctv-nvr/main.png and b/static/resources/NibblePoker/images/content/docker-mini-cctv-nvr/main.png differ
diff --git a/static/resources/NibblePoker/images/tools/vat-calculator/main-noisy.png b/static/resources/NibblePoker/images/tools/vat-calculator/main-noisy.png
new file mode 100644
index 0000000..1abc967
Binary files /dev/null and b/static/resources/NibblePoker/images/tools/vat-calculator/main-noisy.png differ
diff --git a/static/resources/NibblePoker/images/tools/vat-calculator/main-quiet.png b/static/resources/NibblePoker/images/tools/vat-calculator/main-quiet.png
new file mode 100644
index 0000000..3b2815e
Binary files /dev/null and b/static/resources/NibblePoker/images/tools/vat-calculator/main-quiet.png differ
diff --git a/static/resources/NibblePoker/libs/input-utils.mjs b/static/resources/NibblePoker/libs/input-utils.mjs
index 5e50e2f..5cd3a26 100644
--- a/static/resources/NibblePoker/libs/input-utils.mjs
+++ b/static/resources/NibblePoker/libs/input-utils.mjs
@@ -2,21 +2,7 @@
// Author: Herwin Bozet (@NibblePoker)
// License: Public Domain (This code)
-/**
- * Retrieves the number from an `HTMLInputElement`
- * @param eInput {HTMLInputElement} The `HTMLInputElement` from which the value will be retrieved.
- * @param min {number|null} If given, sets a minimum the value can have when returned.
- * @param max {number|null} If given, sets a maximum the value can have when returned.
- * @returns {number} The value from the given `HTMLInputElement`, or `1` if no valid one was given.
- */
-export function getInputCount(eInput, min = null, max = null) {
- let desiredCount = null;
- try {
- desiredCount = parseInt(eInput.value);
- } catch (e) {
- console.error(e);
- }
-
+function postProcessNumber(desiredCount, min = null, max = null) {
if (desiredCount === null) {
desiredCount = 1;
}
@@ -34,3 +20,37 @@ export function getInputCount(eInput, min = null, max = null) {
return desiredCount;
}
+
+/**
+ * Retrieves the integer number from an `HTMLInputElement`
+ * @param eInput {HTMLInputElement|HTMLSelectElement} The `HTMLInputElement` from which the value will be retrieved.
+ * @param min {number|null} If given, sets a minimum the value can have when returned.
+ * @param max {number|null} If given, sets a maximum the value can have when returned.
+ * @returns {number|NaN} The value from the given `HTMLInputElement`, or `1` if no valid one was given.
+ */
+export function getInputCount(eInput, min = null, max = null) {
+ let desiredCount = null;
+ try {
+ desiredCount = parseInt(eInput.value);
+ } catch (e) {
+ console.error(e);
+ }
+ return postProcessNumber(desiredCount, min, max);
+}
+
+/**
+ * Retrieves the float number from an `HTMLInputElement`
+ * @param eInput {HTMLInputElement|HTMLSelectElement} The `HTMLInputElement` from which the value will be retrieved.
+ * @param min {number|null} If given, sets a minimum the value can have when returned.
+ * @param max {number|null} If given, sets a maximum the value can have when returned.
+ * @returns {number|NaN} The value from the given `HTMLInputElement`, or `1` if no valid one was given.
+ */
+export function getInputNumber(eInput, min = null, max = null) {
+ let desiredCount = null;
+ try {
+ desiredCount = parseFloat(eInput.value);
+ } catch (e) {
+ console.error(e);
+ }
+ return postProcessNumber(desiredCount, min, max);
+}
diff --git a/templates/applets/vat-calculator.jinja b/templates/applets/vat-calculator.jinja
new file mode 100644
index 0000000..db782c1
--- /dev/null
+++ b/templates/applets/vat-calculator.jinja
@@ -0,0 +1,170 @@
+
+{%
+ set all_vat_data = [
+ ["afghanistan", [[10, "standard"]],
+ "https://ard.gov.af/file_download/432/FAQs+of+VAT+English.pdf"],
+ ["belgium", [[6, "reduced"],[12, "intermediate"],[21, "standard"]],
+ "https://finance.belgium.be/en/enterprises/vat/vat-obligation/rates-and-calculation/vat-rates"],
+ ["luxembourg", [[3, "reduced.super"],[8, "reduced"],[14, "intermediate"],[17, "standard"]],
+ "https://logistics.public.lu/en/formalities-procedures/taxes/value-added-tax/national-operations.html"],
+ ]
+%}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + | ++ + + {% if not is_standalone %}{% else %}%{% endif %} + | +
+ + + | ++ + + {% if not is_standalone %}{% endif %} + | +
+ + + | ++ + + {% if not is_standalone %}{% endif %} + | +
{{ l10n("text.radio.explanation", "vat-calculator", user_lang) }}
+ + +
+ {{ l10n("license.text.1", "vat-calculator", user_lang) }}
+ {{ l10n("license.text.2", "vat-calculator", user_lang) }}
+