diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md
deleted file mode 100644
index c4bf532b..00000000
--- a/.github/ISSUE_TEMPLATE/bug-report.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-name: Bug report
-about: Create a report to help us improve our tools
-title: '[BUG] '
-labels: bug
-assignees: CorentinTh
----
-
-**Which tool is impacted?**
-Example: the token generator
-
-**To Reproduce**
-Steps to reproduce the behavior:
-
-1. Go to '...'
-2. Click on '....'
-3. Scroll down to '....'
-4. See error
-
-**Expected behavior**
-A clear and concise description of what you expected to happen.
-
-**Screenshots**
-If applicable, add screenshots to help explain your problem.
-
-**Configuration (please complete the following information):**
-
-- Device: [e.g. iPhone6, ]
-- OS: [e.g. iOS]
-- Browser [e.g. chrome, safari]
-- Version [e.g. 22]
-
-**Additional context**
-Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml
new file mode 100644
index 00000000..d338fa3f
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug-report.yml
@@ -0,0 +1,48 @@
+name: đ Bug Report
+description: File a bug report.
+labels: ['bug', 'triage']
+assignees:
+  - CorentinTh
+body:
+  - type: markdown
+    attributes:
+      value: |
+        Thanks for taking the time to fill out this bug report!
+
+  - type: textarea
+    id: bug-description
+    attributes:
+      label: Describe the bug
+      description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us in the description. Thanks!
+      placeholder: Bug description
+    validations:
+      required: true
+
+  - type: textarea
+    id: what-happened
+    attributes:
+      label: What happened?
+      description: Also tell us, what did you expect to happen? If you have a screenshot, you can paste it here.
+      placeholder: Tell us what you see!
+      value: 'A bug happened!'
+    validations:
+      required: true
+
+  - type: textarea
+    id: version
+    attributes:
+      label: System information
+      description: What is you environment? You can use the `npx envinfo --system --browsers` command to get this information.
+    validations:
+      required: true
+
+  - type: dropdown
+    id: app-type
+    attributes:
+      label: Where did you encounter the bug?
+      options:
+        - Public app (it-tools.tech)
+        - A self hosted
+        - Other (installations, docker, etc.)
+    validations:
+      required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..3ba13e0c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1 @@
+blank_issues_enabled: false
diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml
new file mode 100644
index 00000000..edae822e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature-request.yml
@@ -0,0 +1,56 @@
+name: đ New feature proposal
+description: Propose a new feature/enhancement or tool idea for IT-Tools
+labels: ['enhancement', 'triage']
+
+body:
+  - type: markdown
+    attributes:
+      value: |
+        Thanks for your interest in the project and taking the time to fill out this feature report!
+
+  - type: dropdown
+    id: request-type
+    attributes:
+      label: What type of request is this?
+      options:
+        - New tool idea
+        - New feature for an existing tool
+        - Deployment or CI/CD improvement
+        - Self-hosting improvement
+        - Other
+    validations:
+      required: true
+
+  - type: textarea
+    id: feature-description
+    attributes:
+      label: Clear and concise description of the feature you are proposing
+      description: A clear and concise description of what the feature is.
+      placeholder: 'Example: a token generator tool'
+    validations:
+      required: true
+
+  - type: textarea
+    id: alternative
+    attributes:
+      label: Is their example of this tool in the wild?
+      description: Provide link to already existing tool (like websites, apps, cli, ...) or npm packages that could be used or provide inspiration for the feature.
+
+  - type: textarea
+    id: additional-context
+    attributes:
+      label: Additional context
+      description: Any other context or screenshots about the feature request here.
+
+  - type: checkboxes
+    id: checkboxes
+    attributes:
+      label: Validations
+      description: Before submitting the issue, please make sure you do the following
+      options:
+        - label: Check the feature is not already implemented in the project.
+          required: true
+        - label: Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
+          required: true
+        - label: Check that the feature can be implemented in a client side only app (IT-Tools is client side only, no server).
+          required: true
diff --git a/.github/ISSUE_TEMPLATE/new-tool-request.md b/.github/ISSUE_TEMPLATE/new-tool-request.md
deleted file mode 100644
index a67a9cd0..00000000
--- a/.github/ISSUE_TEMPLATE/new-tool-request.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
-name: New tool request
-about: Suggest a new tool idea
-title: '[NEW TOOL]'
-labels: new tool
-assignees: CorentinTh
----
-
-**What tool do you want?**
-Example: a token generator
-
-**Describe the solution you'd like**
-A clear and concise description of what you want to happen.
-
-**Is their example of this tool in the wild?**
-Provide link to already existing tool or npm packages if any exists
-
-**Additional context**
-Add any other context about the feature request here.
diff --git a/.github/ISSUE_TEMPLATE/other-request.md b/.github/ISSUE_TEMPLATE/other-request.md
deleted file mode 100644
index b4901ed4..00000000
--- a/.github/ISSUE_TEMPLATE/other-request.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
-name: Other request
-about: Any request that does not concern a tool creation, a new feature request on a tool or a bug
-title: '[OTHER] '
-labels:
-assignees: CorentinTh
----
-
-**Describe the solution you'd like**
-A clear and concise description of what you want.
-
-**Additional context**
-Add any other context about the feature request here.
diff --git a/.github/ISSUE_TEMPLATE/tool-improvement.md b/.github/ISSUE_TEMPLATE/tool-improvement.md
deleted file mode 100644
index 8ff8bb6c..00000000
--- a/.github/ISSUE_TEMPLATE/tool-improvement.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
-name: Tool improvement
-about: Improvement on an existing tool
-title: '[TOOL IMPROVEMENT]'
-labels: enhancement
-assignees: CorentinTh
----
-
-**Describe the solution you'd like**
-A clear and concise description of what you want to happen.
-
-**Additional context**
-Add any other context about the feature request here.
diff --git a/.github/logo-dark.png b/.github/logo-dark.png
new file mode 100644
index 00000000..9bee8a9c
Binary files /dev/null and b/.github/logo-dark.png differ
diff --git a/.github/logo-white.png b/.github/logo-white.png
new file mode 100644
index 00000000..560a135b
Binary files /dev/null and b/.github/logo-white.png differ
diff --git a/.github/logo.png b/.github/logo.png
deleted file mode 100644
index 18bac950..00000000
Binary files a/.github/logo.png and /dev/null differ
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
deleted file mode 100644
index 6d11e825..00000000
--- a/.github/workflows/codeql-analysis.yml
+++ /dev/null
@@ -1,69 +0,0 @@
-# For most projects, this workflow file will not need changing; you simply need
-# to commit it to your repository.
-#
-# You may wish to alter this file to override the set of languages analyzed,
-# or to provide custom queries or build logic.
-#
-# ******** NOTE ********
-# We have attempted to detect the languages in your repository. Please check
-# the `language` matrix defined below to confirm you have the correct set of
-# supported CodeQL languages.
-#
-name: "CodeQL"
-
-on:
-  push:
-    branches: [ dev ]
-  pull_request:
-    # The branches below must be a subset of the branches above
-    branches: [ dev ]
-
-jobs:
-  analyze:
-    name: Analyze
-    runs-on: ubuntu-latest
-    permissions:
-      actions: read
-      contents: read
-      security-events: write
-
-    strategy:
-      fail-fast: false
-      matrix:
-        language: [ 'javascript' ]
-        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
-        # Learn more:
-        # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
-
-    steps:
-    - name: Checkout repository
-      uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
-
-    # Initializes the CodeQL tools for scanning.
-    - name: Initialize CodeQL
-      uses: github/codeql-action/init@v2
-      with:
-        languages: ${{ matrix.language }}
-        # If you wish to specify custom queries, you can do so here or in a config file.
-        # By default, queries listed here will override any specified in a config file.
-        # Prefix the list here with "+" to use these queries and those in the config file.
-        # queries: ./path/to/local/query, your-org/your-repo/queries@main
-
-    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
-    # If this step fails, then you should remove it and run the build manually (see below)
-    - name: Autobuild
-      uses: github/codeql-action/autobuild@v2
-
-    # âčïž Command-line programs to run using the OS shell.
-    # đ https://git.io/JvXDl
-
-    # âïž If the Autobuild fails above, remove it and uncomment the following three lines
-    #    and modify them (or add more) to build your code if your project
-    #    uses a compiled language
-
-    #- run: |
-    #   make bootstrap
-    #   make release
-
-    - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v2
diff --git a/.github/workflows/docker-nightly-release.yml b/.github/workflows/docker-nightly-release.yml
index 81a0898c..41dbb155 100644
--- a/.github/workflows/docker-nightly-release.yml
+++ b/.github/workflows/docker-nightly-release.yml
@@ -32,7 +32,7 @@ jobs:
       - run: corepack enable
       - uses: actions/setup-node@v3
         with:
-          node-version: 16
+          node-version: 20
           cache: 'pnpm'
 
       - name: Install dependencies
diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml
index 8ed4099d..d0d3febd 100644
--- a/.github/workflows/releases.yml
+++ b/.github/workflows/releases.yml
@@ -61,7 +61,7 @@ jobs:
 
       - uses: actions/setup-node@v3
         with:
-          node-version: 16
+          node-version: 20
           cache: 'pnpm'
 
       - name: Install dependencies
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1c12c11c..984fa58e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,9 +2,51 @@
 
 All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
 
+## Version 2024.05.13-a0bc346
+
+### Features
+- **i18n**: added German translation (#1038) (2c2fb21)
+- **new tool**: Outlook Safelink Decoder (#911) (d3b32cc)
+- **new tool**: ascii art generator (#886) (fe349ad)
+- **i18n**: get locales on build (#880) (dc04615)
+- **i18n**: added vi tools translations (#876) (079aa21)
+- **i18n**: added zh tools translations (#874) (9c6b122)
+- **i18n**: added missing locale files in tools (#863) (7f5fa00)
+- **i18n**: added vietnamese language (#859) (1334bff)
+- **i18n**: added spanish language (#854) (85b50bb)
+- **i18n**: added portuguese language (#813) (c65ffb6)
+- **i18n**: added ukrainian language (#827) (693f362)
+- **new-tool**: yaml formater (#779) (fc06f01)
+- **new-tool**: added unicode conversion utilities (#858) (c46207f)
+
+### Bug fixes
+- **language**: English language cleanup (#1036) (221ddfa)
+- **url-encoder, validation**: typo in validation of url-encoder.vue #1024 (cb5b462)
+- **integer base converter**: support bigint (#872) (9eac9cb)
+- **bcrypt tool**: allow salt rounds up to 100 (#987) (23f82d9)
+
+### Refactoring
+- **lint**: removed extra semi (33e5294)
+- **auto-imports**: regen auto imports (1242842)
+- **home**: lightened tool cards (#882) (a07806c)
+- **home**: removed n-grid to prevent layout shift (#881) (10e56b3)
+- **i18n**: added locales per tool (#861) (95698cb)
+
+### Chores
+- **issues**: prevent empty issues (#1078) (a0bc346)
+- **issues**: removed old issue templates (#1077) (5a7b0f9)
+- **node**: upgraded node version in CI workflows (b59942a)
+- **version**: release 2024.05.10-33e5294 (38d5687)
+- **issues**: improved issues template (2852c30)
+- **issues**: improved bug issue template (#1046) (a799234)
+
+### Documentation
+- **changelog**: update changelog for 2024.05.10-33e5294 (9dfd347)
+
 ## Version 2023.12.21-5ed3693
 
 ### Features
+
 - **i18n**: improve chinese i18n (#757) (2e56641)
 - **i18n**: add tooltip and favoriteButton i18n (#756) (a1037cf)
 - **i18n**: add Chinese translation base (#718) (8f99eb6)
@@ -12,6 +54,7 @@ All notable changes to this project will be documented in this file. See [standa
 - **new tool**: numeronym generator (#729) (e07e2ae)
 
 ### Bug fixes
+
 - **jwt-parser**: jwt claim array support (#799) (5ed3693)
 - **camera-recorder**: stop camera on navigation (#782) (80e46c9)
 - **doc**: updated create new tool command in readme (#762) (7a70dbb)
@@ -20,6 +63,7 @@ All notable changes to this project will be documented in this file. See [standa
 - **eta**: corrected example (#737) (821cbea)
 
 ### Refactoring
+
 - **about, i18n**: improved i18n dx with markdown (#753) (bd3edcb)
 - **token, i18n**: complete fr translation (#752) (de1ee69)
 - **uuid generator**: uuid version picker (#751) (38586ca)
@@ -29,6 +73,7 @@ All notable changes to this project will be documented in this file. See [standa
 - **bcrypt**: fix input label align (#721) (093ff31)
 
 ### Chores
+
 - **deps**: switched from oui to oui-data for mac address lookup (#693) (0fe9a20)
 - **deps**: update unocss monorepo to ^0.57.0 (#638) (2e396d8)
 - **docker**: added armv7 plateform for docker releases (#722) (fe1de8c)
@@ -36,19 +81,23 @@ All notable changes to this project will be documented in this file. See [standa
 ## Version 2023.11.02-7d94e11
 
 ### Features
+
 - **i18n**: language selector (#710) (e86fd96)
 
 ### Bug fixes
+
 - **dockerfile**: revert replacement of nginx image with non-privileged one (#716) (7d94e11)
 - **encryption**: alert on decryption error (#711) (02b0d0d)
 
 ### Refactoring
+
 - **math-evaluator**: improved description (e87f4b1)
 - **math-evaluator**: improved search and UX (#713) (58de897)
 
 ## Version 2023.11.01-e164afb
 
 ### Features
+
 - **command-palette**: clear prompt on palette close (#708) (d013696)
 - **command-palette**: added about page in command palette (99b1eb9)
 - **new tool**: random MAC address generator (#657) (cc3425d)
@@ -67,11 +116,13 @@ All notable changes to this project will be documented in this file. See [standa
 - **new tool**: text diff and comparator (#588) (81bfe57)
 
 ### Bug fixes
+
 - **deps**: fix issue on slugify (#593) (#673) (720201a)
 - **deps**: update dependency monaco-editor to ^0.43.0 (#620) (e371ef7)
 - **deps**: update dependency sql-formatter to v13 (#606) (c7d4562)
 
 ### Refactoring
+
 - **ui**: better ui demo preview menu (#664) (015c673)
 - **color-converter**: improved color-converter UX (#701) (abb8335)
 - **docker**: improved docker config (#700) (020e9cb)
@@ -88,6 +139,7 @@ All notable changes to this project will be documented in this file. See [standa
 - **bcrypt**: fix typo (#604) (e18bae1)
 
 ### Chores
+
 - **deps**: clean unused dependencies (#709) (e164afb)
 - **deps**: update docker/setup-qemu-action action to v3 (#627) (4365226)
 - **deps**: update docker/setup-buildx-action action to v3 (#626) (57ecda1)
@@ -102,19 +154,23 @@ All notable changes to this project will be documented in this file. See [standa
 - **deps**: update dependency typescript to ~5.2.0 (#587) (f3e14fc)
 
 ### Doc
+
 - **readme**: added contributors list (#622) (557b304)
 - **hosting**: added cloudron in the other hosting solutions section (#589) (06c3547)
 
 ## Version 2023.08.21-6f93cba
 
 ### Features
+
 - **copy**: support legacy copy to clipboard for older browser (#581) (6f93cba)
 - **new tool**: string obfuscator (#575) (c58d6e3)
 
 ### Bug fixes
+
 - **deps**: update dependency sql-formatter to v12 (#520) (2bcb77a)
 
 ### Chores
+
 - **deps**: switched to fucking typescript v5 (#501) (76b2761)
 - **deps**: update dependency @antfu/eslint-config to ^0.40.0 (#552) (6ff9a01)
 - **deps**: update dependency prettier to v3 (#564) (a2b9b15)
@@ -124,6 +180,7 @@ All notable changes to this project will be documented in this file. See [standa
 ## Version 2023.08.16-9bd4ad4
 
 ### Features
+
 - **Case Converter**: Add lowercase and uppercase (#534) (7b6232a)
 - **new tool**: emoji picker (#551) (93f7cf0)
 - **ui**: added c-select in the ui lib (#550) (dfa1ba8)
@@ -144,6 +201,7 @@ All notable changes to this project will be documented in this file. See [standa
 - **base64-string-converter**: switch to encode and decode url safe base64 strings (#392) (0b20f1c)
 
 ### Bug fixes
+
 - **deps**: update dependency uuid to v9 (#566) (5e12991)
 - **deps**: update dependency mathjs to v11 (#519) (7924456)
 - **deps**: update dependency @vueuse/router to v10 (#516) (ea0f27c)
@@ -163,6 +221,7 @@ All notable changes to this project will be documented in this file. See [standa
 - **ipv4-converter**: removed readonly on input (7aed9c5)
 
 ### Refactoring
+
 - **navbar**: consistent spacing in navbar buttons (#507) (30f88fc)
 - **ui**: remove n-text (#506) (72c98a3)
 - **ui**: replaced some n-input to c-input (#505) (05ea545)
@@ -175,6 +234,7 @@ All notable changes to this project will be documented in this file. See [standa
 - **ui**: replaced some n-input with c-input-text (f7fc779)
 
 ### Chores
+
 - **deps**: update dependency vitest to ^0.34.0 (#562) (9bd4ad4)
 - **deps**: update dependency node to v18.17.1 (#560) (65a9474)
 - **deps**: update dependency unocss to ^0.55.0 (#561) (85cc7a8)
@@ -215,47 +275,58 @@ All notable changes to this project will be documented in this file. See [standa
 - **lint**: switched to a better lint config (33c9b66)
 
 ### Refacor
+
 - **transformers**: use monospace font for JSON and SQL text areas (#476) (ba4876d)
 
 ### Documentation
+
 - **ide**: updated vscode extensions settings (#472) (847323c)
 
 ### Chors
+
 - **deps**: updated vueuse dependency version (8515c24)
 
 ## Version 2023.05.14-77f2efc
 
 ### Features
+
 - **list-converter**: a small converter who deals with column based data and do some stuff with it (#387) (83a7b3b)
 - **new tool**: phone parser and normalizer (ce3150c)
 
 ### Bug fixes
+
 - **phone-parser**: use default country code (a43c546)
 - **home**: prevent weird blue border on card (3f6c8f0)
 
 ### Refactoring
+
 - **ui**: replaced some n-input with c-input-text (77f2efc)
 
 ### Chores
+
 - **issues**: updated new tool request issue template (edae4c6)
 
 ### Ui-lib
+
 - **new-component**: added text input component in the c-lib (aad8d84)
 - **button**: size variants (401f13f)
 
 ## Version 2023.04.23-92bd835
 
 ### Features
+
 - **ui-lib**: demo pages for c-lib components (92bd835)
 - **new-tool**: diff of two json objects (362f2fa)
 - **ipv4-range-expander**: expands a given IPv4 start and end address to a valid IPv4 subnet (#366) (df989e2)
 - **date converter**: auto focus main input (6d22025)
 
 ### Bug fixes
+
 - **ts**: cleaned legacy typechecking warning (e88c1d5)
 - **mac-address-lookup**: added copy handler on button click (c311e38)
 
 ### Refactoring
+
 - **ui-lib**: prevent c-button to shrink (61ece23)
 - **ui**: replaced naive ui cards with custom ones (f080933)
 - **clean**: removed unused lodash import (bb32513)
@@ -265,48 +336,60 @@ All notable changes to this project will be documented in this file. See [standa
 ## Version 2023.04.14-dbad773
 
 ### Features
+
 - **new-tool**: http status codes (8355bd2)
 
 ### Refactoring
+
 - **uuid-generator**: prevent NaN in quantity (6fb4994)
 
 ### Chores
+
 - **release**: create a github release on new version (dbad773)
 - **version**: reset CHANGELOG content to support new format (85cb0ff)
 
 ## Version 2023.04.14-f9b77b7
 
 ### Features
+
 - **new-tool**: http status codes (8355bd2)
 
 ### Refactoring
+
 - **uuid-generator**: prevent NaN in quantity (6fb4994)
 
 ### Chores
+
 - **release**: create a github release on new version (f9b77b7)
 - **version**: reset CHANGELOG content to support new format (85cb0ff)
 
 ## Version 2023.04.14-2f0d239
 
 ### Features
+
 - **new-tool**: http status codes (8355bd2)
 
 ### Refactoring
+
 - **uuid-generator**: prevent NaN in quantity (6fb4994)
 
 ### Chores
+
 - **release**: create a github release on new version (2f0d239)
 - **version**: reset CHANGELOG content to support new format (85cb0ff)
 
 ## Version 2023.04.14-474cae4
 
 ### Features
+
 - **new-tool**: http status codes (8355bd2)
 
 ### Refactoring
+
 - **uuid-generator**: prevent NaN in quantity (6fb4994)
 
 ### Chores
+
 - **release**: create a github release on new version (474cae4)
 - **version**: reset CHANGELOG content to support new format (85cb0ff)
 
diff --git a/README.md b/README.md
index a51f9c53..b9726864 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,8 @@
-
+
+    
+    
+       
 
 Useful tools for developer and people working in IT. [Have a look !](https://it-tools.tech).
 
diff --git a/components.d.ts b/components.d.ts
index f2c3146f..3e65c3cc 100644
--- a/components.d.ts
+++ b/components.d.ts
@@ -72,6 +72,7 @@ declare module '@vue/runtime-core' {
     DockerRunToDockerComposeConverter: typeof import('./src/tools/docker-run-to-docker-compose-converter/docker-run-to-docker-compose-converter.vue')['default']
     DynamicValues: typeof import('./src/tools/benchmark-builder/dynamic-values.vue')['default']
     Editor: typeof import('./src/tools/html-wysiwyg-editor/editor/editor.vue')['default']
+    EmailNormalizer: typeof import('./src/tools/email-normalizer/email-normalizer.vue')['default']
     EmojiCard: typeof import('./src/tools/emoji-picker/emoji-card.vue')['default']
     EmojiGrid: typeof import('./src/tools/emoji-picker/emoji-grid.vue')['default']
     EmojiPicker: typeof import('./src/tools/emoji-picker/emoji-picker.vue')['default']
@@ -110,6 +111,7 @@ declare module '@vue/runtime-core' {
     JsonMinify: typeof import('./src/tools/json-minify/json-minify.vue')['default']
     JsonToCsv: typeof import('./src/tools/json-to-csv/json-to-csv.vue')['default']
     JsonToToml: typeof import('./src/tools/json-to-toml/json-to-toml.vue')['default']
+    JsonToXml: typeof import('./src/tools/json-to-xml/json-to-xml.vue')['default']
     JsonToYaml: typeof import('./src/tools/json-to-yaml-converter/json-to-yaml.vue')['default']
     JsonViewer: typeof import('./src/tools/json-viewer/json-viewer.vue')['default']
     JwtParser: typeof import('./src/tools/jwt-parser/jwt-parser.vue')['default']
@@ -119,6 +121,7 @@ declare module '@vue/runtime-core' {
     LoremIpsumGenerator: typeof import('./src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue')['default']
     MacAddressGenerator: typeof import('./src/tools/mac-address-generator/mac-address-generator.vue')['default']
     MacAddressLookup: typeof import('./src/tools/mac-address-lookup/mac-address-lookup.vue')['default']
+    MarkdownToHtml: typeof import('./src/tools/markdown-to-html/markdown-to-html.vue')['default']
     MathEvaluator: typeof import('./src/tools/math-evaluator/math-evaluator.vue')['default']
     MenuBar: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar.vue')['default']
     MenuBarItem: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar-item.vue')['default']
@@ -127,24 +130,19 @@ declare module '@vue/runtime-core' {
     MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default']
     MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
     NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
-    NCode: typeof import('naive-ui')['NCode']
+    NCheckbox: typeof import('naive-ui')['NCheckbox']
     NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
     NConfigProvider: typeof import('naive-ui')['NConfigProvider']
     NDivider: typeof import('naive-ui')['NDivider']
     NEllipsis: typeof import('naive-ui')['NEllipsis']
-    NFormItem: typeof import('naive-ui')['NFormItem']
-    NGi: typeof import('naive-ui')['NGi']
-    NGrid: typeof import('naive-ui')['NGrid']
     NH1: typeof import('naive-ui')['NH1']
     NH3: typeof import('naive-ui')['NH3']
     NIcon: typeof import('naive-ui')['NIcon']
-    NInputNumber: typeof import('naive-ui')['NInputNumber']
-    NLabel: typeof import('naive-ui')['NLabel']
     NLayout: typeof import('naive-ui')['NLayout']
     NLayoutSider: typeof import('naive-ui')['NLayoutSider']
     NMenu: typeof import('naive-ui')['NMenu']
-    NScrollbar: typeof import('naive-ui')['NScrollbar']
-    NSpin: typeof import('naive-ui')['NSpin']
+    NSpace: typeof import('naive-ui')['NSpace']
+    NTable: typeof import('naive-ui')['NTable']
     NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default']
     OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
     PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default']
@@ -154,6 +152,9 @@ declare module '@vue/runtime-core' {
     PhoneParserAndFormatter: typeof import('./src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue')['default']
     QrCodeGenerator: typeof import('./src/tools/qr-code-generator/qr-code-generator.vue')['default']
     RandomPortGenerator: typeof import('./src/tools/random-port-generator/random-port-generator.vue')['default']
+    RegexMemo: typeof import('./src/tools/regex-memo/regex-memo.vue')['default']
+    'RegexMemo.content': typeof import('./src/tools/regex-memo/regex-memo.content.md')['default']
+    RegexTester: typeof import('./src/tools/regex-tester/regex-tester.vue')['default']
     ResultRow: typeof import('./src/tools/ipv4-range-expander/result-row.vue')['default']
     RomanNumeralConverter: typeof import('./src/tools/roman-numeral-converter/roman-numeral-converter.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
@@ -186,6 +187,7 @@ declare module '@vue/runtime-core' {
     UuidGenerator: typeof import('./src/tools/uuid-generator/uuid-generator.vue')['default']
     WifiQrCodeGenerator: typeof import('./src/tools/wifi-qr-code-generator/wifi-qr-code-generator.vue')['default']
     XmlFormatter: typeof import('./src/tools/xml-formatter/xml-formatter.vue')['default']
+    XmlToJson: typeof import('./src/tools/xml-to-json/xml-to-json.vue')['default']
     YamlToJson: typeof import('./src/tools/yaml-to-json-converter/yaml-to-json.vue')['default']
     YamlToToml: typeof import('./src/tools/yaml-to-toml/yaml-to-toml.vue')['default']
     YamlViewer: typeof import('./src/tools/yaml-viewer/yaml-viewer.vue')['default']
diff --git a/locales/de.yml b/locales/de.yml
new file mode 100644
index 00000000..5a47c85d
--- /dev/null
+++ b/locales/de.yml
@@ -0,0 +1,455 @@
+'404':
+  notFound: 404 Nicht gefunden
+  sorry: Entschuldigung, diese Seite scheint nicht zu existieren
+  maybe: >-
+    Vielleicht macht der Cache etwas Seltsames. Mit einem erzwungenen Neuladen
+    versuchen?
+  backHome: ZurĂŒck zur Startseite
+home:
+  categories:
+    newestTools: Neueste Tools
+    favoriteTools: Deine Lieblingstools
+    allTools: Alle Tools
+  subtitle: Praktische Tools fĂŒr Entwickler
+  toggleMenu: MenĂŒ umschalten
+  home: Startseite
+  uiLib: UI-Bibliothek
+  support: UnterstĂŒtze die Entwicklung von IT-Tools
+  buyMeACoffee: Kauf mir einen Kaffee
+  follow:
+    title: Magst du IT-Tools?
+    p1: Gib uns einen Stern auf
+    githubRepository: IT-Tools GitHub-Repository
+    p2: oder folge uns auf
+    twitterAccount: IT-Tools Twitter-Konto
+    thankYou: Vielen Dank!
+  nav:
+    github: GitHub-Repository
+    githubRepository: IT-Tools GitHub-Repository
+    twitter: Twitter-Konto
+    twitterAccount: IT-Tools Twitter-Konto
+    about: Ăber IT-Tools
+    aboutLabel: Ăber
+    darkMode: Dunkelmodus
+    lightMode: Hellmodus
+    mode: Wechseln zwischen dunklem/hellem Modus
+about:
+  content: >
+    # Ăber IT-Tools
+
+    Diese wunderbare Website, erstellt mit †von [Corentin
+    Thomasset](https://github.com/CorentinTh), sammelt nĂŒtzliche Tools fĂŒr
+    Entwickler und Menschen, die in der IT arbeiten. Wenn du sie nĂŒtzlich
+    findest, teile sie gerne mit Personen, von denen du denkst, dass sie sie
+    ebenfalls nĂŒtzlich finden könnten, und vergiss nicht, sie in deiner
+    Lesezeichenleiste zu speichern!
+
+    IT-Tools ist Open Source (unter der MIT-Lizenz) und kostenlos und wird es
+    immer sein, aber es kostet mich Geld, die Website zu hosten und den
+    Domainnamen zu erneuern. Wenn du meine Arbeit unterstĂŒtzen möchtest und mich
+    ermutigen möchtest, mehr Tools hinzuzufĂŒgen, ĂŒberlege bitte, mich durch
+    [Sponsoring](https://www.buymeacoffee.com/cthmsst) zu unterstĂŒtzen.
+
+    ## Technologien
+
+    IT-Tools wurde mit Vue.js (Vue 3) und der Naive UI-Komponentenbibliothek
+    erstellt und wird von Vercel gehostet und kontinuierlich bereitgestellt. In
+    einigen Tools werden Drittanbieter-Open-Source-Bibliotheken verwendet. Du
+    findest die vollstÀndige Liste in der
+    [package.json](https://github.com/CorentinTh/it-tools/blob/main/package.json)-Datei
+    des Repositorys.
+
+    ## Einen Fehler gefunden? Ein Tool fehlt?
+
+    Wenn du ein Tool benötigst, das hier noch nicht vorhanden ist, und du
+    denkst, dass es nĂŒtzlich sein könnte, bist du herzlich eingeladen, einen
+    Feature-Request im
+    [Issues-Bereich](https://github.com/CorentinTh/it-tools/issues/new/choose)
+    im GitHub-Repository einzureichen.
+
+    Und wenn du einen Fehler gefunden hast oder etwas nicht wie erwartet
+    funktioniert, melde bitte einen Fehler im
+    [Issues-Bereich](https://github.com/CorentinTh/it-tools/issues/new/choose)
+    im GitHub-Repository.
+favoriteButton:
+  remove: Aus Favoriten entfernen
+  add: Zu Favoriten hinzufĂŒgen
+toolCard:
+  new: Neu
+search:
+  label: Suche
+tools:
+  categories:
+    favorite-tools: Deine Lieblingstools
+    crypto: Krypto
+    converter: Konverter
+    web: Web
+    images and videos: Bilder & Videos
+    development: Entwicklung
+    network: Netzwerk
+    math: Mathematik
+    measurement: Messung
+    text: Text
+    data: Daten
+  password-strength-analyser:
+    title: PasswortstÀrken-Analysator
+    description: >-
+      Ermittle die StÀrke deines Passworts mit diesem Client-seitigen
+      PasswortstÀrken-Analysator und Tool zur SchÀtzung der Knackzeit.
+  chronometer:
+    title: Chronometer
+    description: >-
+      Ăberwache die Dauer einer Sache. Im Grunde ein Chronometer mit einfachen
+      Chronometerfunktionen.
+  token-generator:
+    title: Token-Generator
+    description: >-
+      Generiere eine zufĂ€llige Zeichenfolge mit den von dir gewĂŒnschten Zeichen,
+      GroĂ- oder Kleinbuchstaben, Zahlen und/oder Symbolen.
+    uppercase: GroĂbuchstaben (ABC...)
+    lowercase: Kleinbuchstaben (abc...)
+    numbers: Zahlen (123...)
+    symbols: Symbole (!-;...)
+    length: LĂ€nge
+    tokenPlaceholder: Der Token ...
+    copied: Token in die Zwischenablage kopiert
+    button:
+      copy: Kopieren
+      refresh: Aktualisieren
+  percentage-calculator:
+    title: Prozentrechner
+    description: >-
+      Berechne einfach ProzentsÀtze von einem Wert zu einem anderen Wert oder
+      von einem Prozentsatz zu einem Wert.
+  svg-placeholder-generator:
+    title: SVG-Platzhalter-Generator
+    description: >-
+      Generiere SVG-Bilder, die als Platzhalter in deinen Anwendungen verwendet
+      werden können.
+  json-to-csv:
+    title: JSON zu CSV
+    description: Konvertiere JSON mit automatischer Headererkennung in CSV.
+  camera-recorder:
+    title: Kamera-Rekorder
+    description: Mache ein Foto oder nimm ein Video von deiner Webcam oder Kamera auf.
+  keycode-info:
+    title: Keycode-Info
+    description: >-
+      Finde den JavaScript-Keycode, den Code, den Standort und die Modifikatoren
+      einer beliebigen gedrĂŒckten Taste.
+  emoji-picker:
+    title: Emoji-Picker
+    description: >-
+      Einfaches Kopieren und EinfĂŒgen von Emojis. Erhalte auĂerdem den Unicode-
+      und Codepunkt-Wert jedes Emojis.
+  color-converter:
+    title: Farbkonverter
+    description: >-
+      Konvertiere Farben zwischen den verschiedenen Formaten (Hex, RGB, HSL und
+      CSS-Name).
+  bcrypt:
+    title: Bcrypt
+    description: >-
+      Hashen und Vergleichen von Strings mit bcrypt. Bcrypt ist eine auf der
+      Blowfish-Chiffre basierende Hash-Funktion.
+  crontab-generator:
+    title: Crontab-Generator
+    description: >-
+      ĂberprĂŒfe und generiere Crontab und erhalte die menschenlesbare
+      Beschreibung des Cron-Zeitplans.
+  http-status-codes:
+    title: HTTP-Statuscodes
+    description: Liste aller HTTP-Statuscodes, ihrer Namen und ihrer Bedeutung.
+  sql-prettify:
+    title: SQL verschönern und formatieren
+    description: >-
+      Formatiere und verschönere deine SQL-Abfragen online (unterstĂŒtzt
+      verschiedene SQL-Dialekte).
+  benchmark-builder:
+    title: Benchmark-Builder
+    description: >-
+      Vergleiche ganz einfach die AusfĂŒhrungszeit von Aufgaben mit diesem sehr
+      einfachen Online-Benchmark-Builder.
+  git-memo:
+    title: Git-Spickzettel
+    description: >-
+      Git ist eine dezentrale Versionsverwaltungssoftware. Mit diesem
+      Spickzettel hast du schnellen Zugriff auf die gÀngigsten Git-Befehle.
+  slugify-string:
+    title: Slugify String
+    description: Mache einen String URL-, Dateinamen- und ID-sicher.
+  encryption:
+    title: Text verschlĂŒsseln / entschlĂŒsseln
+    description: >-
+      VerschlĂŒssele und entschlĂŒssele Klartext mithilfe von Kryptoalgorithmen
+      wie AES, TripleDES, Rabbit oder RC4.
+  random-port-generator:
+    title: ZufÀlliger Port-Generator
+    description: >-
+      Generiere zufĂ€llige Portnummern auĂerhalb des Bereichs der "bekannten"
+      Ports (0-1023).
+  yaml-prettify:
+    title: YAML verschönern und formatieren
+    description: Verschönere deinen YAML-String in ein menschenlesbares Format.
+  eta-calculator:
+    title: ETA-Rechner
+    description: >-
+      Ein ETA (Estimated Time of Arrival)-Rechner, um die ungefÀhre Endzeit
+      einer Aufgabe zu erfahren, z. B. den Zeitpunkt des Endes eines Downloads.
+  roman-numeral-converter:
+    title: Römische Zahlen Konverter
+    description: >-
+      Konvertiere römische Zahlen in Dezimalzahlen und Dezimalzahlen in römische
+      Zahlen.
+  hmac-generator:
+    title: HMAC-Generator
+    description: >-
+      Berechnet einen hashbasierten Nachrichtenauthentifizierungscode (HMAC)
+      unter Verwendung eines geheimen SchlĂŒssels und deiner bevorzugten
+      Hash-Funktion.
+  bip39-generator:
+    title: BIP39-Passphrasengenerator
+    description: >-
+      Generiere BIP39-Passphrasen aus vorhandener oder zufÀlliger Mnemonik oder
+      erhalte die Mnemonik aus der Passphrase.
+  base64-file-converter:
+    title: Base64-Dateikonverter
+    description: Konvertiere Strings, Dateien oder Bilder in ihre Base64-ReprÀsentation.
+  list-converter:
+    title: Listenkonverter
+    description: >-
+      Dieses Tool kann spaltenbasierte Daten verarbeiten und verschiedene
+      Ănderungen (transponieren, PrĂ€fix und Suffix hinzufĂŒgen, Liste umkehren,
+      Liste sortieren, Werte in Kleinbuchstaben umwandeln, Werte abschneiden)
+      auf jede Zeile anwenden.
+  base64-string-converter:
+    title: Base64-String-Encoder/Decoder
+    description: Codiere und decodiere Strings einfach in ihre Base64-ReprÀsentation.
+  toml-to-yaml:
+    title: TOML zu YAML
+    description: Parse und konvertiere TOML zu YAML.
+  math-evaluator:
+    title: Mathematischer Auswerter
+    description: >-
+      Ein Taschenrechner zum Auswerten mathematischer AusdrĂŒcke. Du kannst
+      Funktionen wie sqrt, cos, sin, abs usw. verwenden.
+  json-to-yaml-converter:
+    title: JSON zu YAML
+    description: Konvertiere JSON einfach in YAML mit diesem Live-Online-Konverter.
+  url-parser:
+    title: URL-Parser
+    description: >-
+      Parse eine URL-Zeichenfolge, um alle verschiedenen Teile (Protokoll,
+      Ursprung, Parameter, Port, Benutzername-Passwort usw.) zu erhalten.
+  iban-validator-and-parser:
+    title: IBAN-Validator und -Parser
+    description: >-
+      Validiere und parse IBAN-Nummern. ĂberprĂŒfe, ob die IBAN gĂŒltig ist, und
+      erhalte das Land, BBAN, ob es sich um eine QR-IBAN handelt und das
+      IBAN-freundliche Format.
+  user-agent-parser:
+    title: User-Agent-Parser
+    description: >-
+      Erkenne und parse Browser, Engine, Betriebssystem, CPU und
+      GerÀtetyp/-modell aus einer User-Agent-Zeichenfolge.
+  numeronym-generator:
+    title: Numeronym-Generator
+    description: >-
+      Ein Numeronym ist ein Wort, bei dem eine Zahl verwendet wird, um eine
+      AbkĂŒrzung zu bilden. Zum Beispiel ist "i18n" ein Numeronym fĂŒr
+      "internationalization", wobei 18 fĂŒr die Anzahl der Buchstaben zwischen
+      dem ersten "i" und dem letzten "n" im Wort steht.
+  case-converter:
+    title: Fall-Konverter
+    description: >-
+      Ăndere den Fall eines Strings und wĂ€hle zwischen verschiedenen Formaten
+      aus.
+  html-entities:
+    title: HTML-Entity-Escape
+    description: >-
+      Escape oder unescape HTML-EntitÀten (ersetze <, >, &, " und ' durch ihre
+      HTML-Version).
+  json-prettify:
+    title: JSON verschönern und formatieren
+    description: Verschönere deinen JSON-String in ein menschenlesbares Format.
+  docker-run-to-docker-compose-converter:
+    title: Docker run zu Docker compose Konverter
+    description: Wandle docker run-Befehle in docker-compose-Dateien um!
+  mac-address-lookup:
+    title: MAC-Adressensuche
+    description: Finde den Anbieter und Hersteller eines GerÀts anhand seiner MAC-Adresse.
+  mime-types:
+    title: MIME-Typen
+    description: Konvertiere MIME-Typen in Erweiterungen und umgekehrt.
+  toml-to-json:
+    title: TOML zu JSON
+    description: Parse und konvertiere TOML zu JSON.
+  lorem-ipsum-generator:
+    title: Lorem Ipsum Generator
+    description: >-
+      Lorem Ipsum ist ein Platzhaltertext, der hÀufig verwendet wird, um die
+      visuelle Form eines Dokuments oder einer Schriftart ohne Verwendung von
+      bedeutendem Inhalt zu demonstrieren.
+  qrcode-generator:
+    title: QR-Code-Generator
+    description: >-
+      Generiere und downloade QR-Codes fĂŒr eine URL oder einfach einen Text und
+      passe die Hintergrund- und Vordergrundfarben an.
+  wifi-qrcode-generator:
+    title: WLAN-QR-Code-Generator
+    description: >-
+      Generiere und lade QR-Codes fĂŒr schnelle Verbindungen zu WLAN-Netzwerken
+      herunter.
+  xml-formatter:
+    title: XML-Formatter
+    description: Verschönere deinen XML-String in ein menschenlesbares Format.
+  temperature-converter:
+    title: Temperaturkonverter
+    description: >-
+      Temperaturgradumrechnungen fĂŒr Kelvin, Celsius, Fahrenheit, Rankine,
+      Delisle, Newton, Réaumur und RÞmer.
+  chmod-calculator:
+    title: Chmod-Rechner
+    description: >-
+      Berechne deine Chmod-Berechtigungen und -Befehle mit diesem
+      Online-Chmod-Rechner.
+  rsa-key-pair-generator:
+    title: RSA-SchlĂŒsselpaar-Generator
+    description: Generiere neue zufÀllige RSA-Private- und Public-Key-PEM-Zertifikate.
+  html-wysiwyg-editor:
+    title: HTML-WYSIWYG-Editor
+    description: >-
+      Online-HTML-Editor mit funktionsreichem WYSIWYG-Editor, erhalte sofort den
+      Quellcode des Inhalts.
+  yaml-to-toml:
+    title: YAML zu TOML
+    description: Parse und konvertiere YAML zu TOML.
+  mac-address-generator:
+    title: MAC-Adressen-Generator
+    description: >-
+      Gebe die Menge und das PrÀfix ein. MAC-Adressen werden in deiner gewÀhlten
+      Schreibweise (GroĂ- oder Kleinbuchstaben) generiert.
+  json-diff:
+    title: JSON-Unterschied
+    description: Vergleiche zwei JSON-Objekte und erhalte die Unterschiede zwischen ihnen.
+  jwt-parser:
+    title: JWT-Parser
+    description: >-
+      Parse und decodiere deinen JSON-Web-Token (JWT) und zeige dessen Inhalt
+      an.
+  date-converter:
+    title: Datum-Uhrzeit-Konverter
+    description: Konvertiere Datum und Uhrzeit in verschiedene Formate.
+  phone-parser-and-formatter:
+    title: Telefonnummer-Parser und -Formatter
+    description: >-
+      Parse, validiere und formatiere Telefonnummern. Erhalte Informationen zur
+      Telefonnummer, wie z. B. die Landesvorwahl, den Typ usw.
+  ipv4-subnet-calculator:
+    title: IPv4-Subnetzrechner
+    description: >-
+      Parse deine IPv4-CIDR-Blöcke und erhalte alle Informationen, die du ĂŒber
+      dein Subnetz benötigst.
+  og-meta-generator:
+    title: Open Graph Meta-Generator
+    description: Generiere Open Graph- und Social-HTML-Metatags fĂŒr deine Website.
+  ipv6-ula-generator:
+    title: IPv6-ULA-Generator
+    description: >-
+      Generiere deine eigenen lokalen, nicht routbaren IP-Adressen in deinem
+      Netzwerk gemÀà RFC4193.
+  hash-text:
+    title: Text hashen
+    description: >-
+      Hashe einen Text-String mit der von dir benötigten Funktion: MD5, SHA1,
+      SHA256, SHA224, SHA512, SHA384, SHA3 oder RIPEMD160
+  json-to-toml:
+    title: JSON zu TOML
+    description: Parse und konvertiere JSON zu TOML.
+  device-information:
+    title: GerÀteinformationen
+    description: >-
+      Informationen zu deinem aktuellen GerĂ€t (BildschirmgröĂe, PixelverhĂ€ltnis,
+      Benutzeragent, ...) erhalten.
+  pdf-signature-checker:
+    title: PDF-SignaturprĂŒfer
+    description: >-
+      ĂberprĂŒfe die Signaturen einer PDF-Datei. Eine signierte PDF-Datei enthĂ€lt
+      eine oder mehrere Signaturen, die verwendet werden können, um
+      festzustellen, ob der Inhalt der Datei seit dem Zeitpunkt der Signierung
+      geÀndert wurde.
+  json-minify:
+    title: JSON minifizieren
+    description: >-
+      Minifiziere und komprimiere dein JSON, indem unnötige Leerzeichen entfernt
+      werden.
+  ulid-generator:
+    title: ULID-Generator
+    description: >-
+      Generiere zufÀllige Universally Unique Lexicographically Sortable
+      Identifier (ULID).
+  string-obfuscator:
+    title: String-Verschleierer
+    description: >-
+      Verschleiere einen String (wie ein Secret, eine IBAN oder ein Token), um
+      ihn weitergeben zu können und identifizierbar zu machen, ohne seinen
+      Inhalt preiszugeben.
+  base-converter:
+    title: Ganzzahl-Basiskonverter
+    description: >-
+      Konvertiere Zahlen zwischen verschiedenen Basen (Dezimal, Hexadezimal,
+      BinÀr, Oktal, Base64, ...).
+  yaml-to-json-converter:
+    title: YAML zu JSON
+    description: Konvertiere YAML einfach in JSON mit diesem Live-Online-Konverter.
+  uuid-generator:
+    title: UUID-Generator
+    description: >-
+      Ein Universally Unique Identifier (UUID) ist eine 128-Bit-Nummer, die zur
+      Identifizierung von Informationen in Computersystemen verwendet wird. Die
+      Anzahl der möglichen UUIDs betrÀgt 16^32, was 2^128 oder etwa 3,4x10^38
+      entspricht (was ziemlich viel ist!).
+  ipv4-address-converter:
+    title: IPv4-Adresskonverter
+    description: >-
+      Konvertiere eine IP-Adresse in Dezimal, BinÀr, Hexadezimal oder sogar in
+      IPv6.
+  text-statistics:
+    title: Textstatistiken
+    description: >-
+      Informationen zu einem Text erhalten, wie die Anzahl der Zeichen, die
+      Anzahl der Wörter, die GröĂe usw.
+  text-to-nato-alphabet:
+    title: Text zu NATO-Alphabet
+    description: >-
+      Wandle Text in das NATO-Phonetik-Alphabet fĂŒr die mĂŒndliche Ăbermittlung
+      um.
+  basic-auth-generator:
+    title: Basic-Auth-Generator
+    description: >-
+      Generiere einen Base64-Basic-Auth-Header aus einem Benutzernamen und einem
+      Passwort.
+  text-to-unicode:
+    title: Text zu Unicode
+    description: Parse und konvertiere Text in Unicode und umgekehrt.
+  ipv4-range-expander:
+    title: IPv4-Bereichserweiterer
+    description: >-
+      Bei Angabe einer Start- und End-IPv4-Adresse berechnet dieses Tool ein
+      gĂŒltiges IPv4-Netzwerk mit seiner CIDR-Notation.
+  text-diff:
+    title: Textunterschied
+    description: Vergleiche zwei Texte und sieh die Unterschiede zwischen ihnen.
+  otp-generator:
+    title: OTP-Code-Generator
+    description: >-
+      Generiere und validiere zeitbasierte OTPs (Einmalpasswörter) fĂŒr
+      Multi-Faktor-Authentifizierung.
+  url-encoder:
+    title: Kodieren/Decodieren von URL-formatierten Zeichenfolgen
+    description: >-
+      Kodiere zum URL-kodierten Format (auch als "prozentkodiert" bekannt) oder
+      decodiere es.
+  text-to-binary:
+    title: Text zu ASCII-BinÀr
+    description: Konvertiere Text in seine ASCII-BinÀrreprÀsentation und umgekehrt.
diff --git a/locales/en.yml b/locales/en.yml
index 50d48af9..d09d435a 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -7,7 +7,7 @@ home:
   toggleMenu: 'Toggle menu'
   home: Home
   uiLib: 'UI Lib'
-  support: 'Support IT Tools development'
+  support: 'Support IT-Tools development'
   buyMeACoffee: 'Buy me a coffee'
   follow:
     title: 'You like it-tools?'
@@ -15,7 +15,7 @@ home:
     githubRepository: 'IT-Tools GitHub repository'
     p2: 'or follow us on'
     twitterAccount: 'IT-Tools Twitter account'
-    thankYou: 'Thank you !'
+    thankYou: 'Thank you!'
   nav:
     github: 'GitHub repository'
     githubRepository: 'IT-Tools GitHub repository'
@@ -72,7 +72,7 @@ tools:
 
   password-strength-analyser:
     title: Password strength analyser
-    description: Discover the strength of your password with this client side only password strength analyser and crack time estimation tool.
+    description: Discover the strength of your password with this client-side-only password strength analyser and crack time estimation tool.
 
   chronometer:
     title: Chronometer
@@ -98,7 +98,7 @@ tools:
 
   svg-placeholder-generator:
     title: SVG placeholder generator
-    description: Generate svg images to use as placeholder in your applications.
+    description: Generate svg images to use as a placeholder in your applications.
 
   json-to-csv:
     title: JSON to CSV
@@ -126,11 +126,11 @@ tools:
 
   crontab-generator:
     title: Crontab generator
-    description: Validate and generate crontab and get the human readable description of the cron schedule.
+    description: Validate and generate crontab and get the human-readable description of the cron schedule.
 
   http-status-codes:
     title: HTTP status codes
-    description: The list of all HTTP status codes their name and their meaning.
+    description: The list of all HTTP status codes, their name, and their meaning.
 
   sql-prettify:
     title: SQL prettify and format
@@ -142,7 +142,7 @@ tools:
 
   git-memo:
     title: Git cheatsheet
-    description: Git is a decentralized version management software. With this cheatsheet you will have a quick access to the most common git commands.
+    description: Git is a decentralized version management software. With this cheatsheet, you will have quick access to the most common git commands.
 
   slugify-string:
     title: Slugify string
@@ -150,7 +150,7 @@ tools:
 
   encryption:
     title: Encrypt / decrypt text
-    description: Encrypt and decrypt text clear text using crypto algorithm like AES, TripleDES, Rabbit or RC4.
+    description: Encrypt clear text and decrypt ciphertext using crypto algorithms like AES, TripleDES, Rabbit or RC4.
 
   random-port-generator:
     title: Random port generator
@@ -158,11 +158,11 @@ tools:
 
   yaml-prettify:
     title: YAML prettify and format
-    description: Prettify your YAML string to a human friendly readable format.
+    description: Prettify your YAML string into a friendly, human-readable format.
 
   eta-calculator:
     title: ETA calculator
-    description: An ETA (Estimated Time of Arrival) calculator to know the approximate end time of a task, for example the moment of ending of a download.
+    description: An ETA (Estimated Time of Arrival) calculator to determine the approximate end time of a task, for example, the end time and duration of a file download.
 
   roman-numeral-converter:
     title: Roman numeral converter
@@ -174,11 +174,11 @@ tools:
 
   bip39-generator:
     title: BIP39 passphrase generator
-    description: Generate BIP39 passphrase from existing or random mnemonic, or get the mnemonic from the passphrase.
+    description: Generate a BIP39 passphrase from an existing or random mnemonic, or get the mnemonic from the passphrase.
 
   base64-file-converter:
     title: Base64 file converter
-    description: Convert string, files or images into a it\'s base64 representation.
+    description: Convert a string, file, or image into its base64 representation.
 
   list-converter:
     title: List converter
@@ -186,7 +186,7 @@ tools:
 
   base64-string-converter:
     title: Base64 string encoder/decoder
-    description: Simply encode and decode string into a their base64 representation.
+    description: Simply encode and decode strings into their base64 representation.
 
   toml-to-yaml:
     title: TOML to YAML
@@ -198,15 +198,15 @@ tools:
 
   json-to-yaml-converter:
     title: JSON to YAML converter
-    description: Simply convert JSON to YAML with this live online converter.
+    description: Simply convert JSON to YAML with this online live converter.
 
   url-parser:
-    title: Url parser
-    description: Parse an url string to get all the different parts (protocol, origin, params, port, username-password, ...)
+    title: URL parser
+    description: Parse a URL into its separate constituent parts (protocol, origin, params, port, username-password, ...)
 
   iban-validator-and-parser:
     title: IBAN validator and parser
-    description: Validate and parse IBAN numbers. Check if IBAN is valid and get the country, BBAN, if it is a QR-IBAN and the IBAN friendly format.
+    description: Validate and parse IBAN numbers. Check if an IBAN is valid and get the country, BBAN, if it is a QR-IBAN and the IBAN friendly format.
 
   user-agent-parser:
     title: User-agent parser
@@ -218,27 +218,27 @@ tools:
 
   case-converter:
     title: Case converter
-    description: Change the case of a string and chose between different formats
+    description: Transform the case of a string and choose between different formats
 
   html-entities:
-    title: Escape html entities
-    description: Escape or unescape html entities (replace <,>, &, " and \' to their html version)
+    title: Escape HTML entities
+    description: Escape or unescape HTML entities (replace characters like <,>, &, " and \' with their HTML version)
 
   json-prettify:
     title: JSON prettify and format
-    description: Prettify your JSON string to a human friendly readable format.
+    description: Prettify your JSON string into a friendly, human-readable format.
 
   docker-run-to-docker-compose-converter:
     title: Docker run to Docker compose converter
-    description: Turns docker run commands into docker-compose files!
+    description: Transforms "docker run" commands into docker-compose files!
 
   mac-address-lookup:
     title: MAC address lookup
     description: Find the vendor and manufacturer of a device by its MAC address.
 
   mime-types:
-    title: Mime types
-    description: Convert mime types to extensions and vice-versa.
+    title: MIME types
+    description: Convert MIME types to file extensions and vice-versa.
 
   toml-to-json:
     title: TOML to JSON
@@ -250,19 +250,19 @@ tools:
 
   qrcode-generator:
     title: QR Code generator
-    description: Generate and download QR-code for an url or just a text and customize the background and foreground colors.
+    description: Generate and download a QR code for a URL (or just plain text), and customize the background and foreground colors.
 
   wifi-qrcode-generator:
     title: WiFi QR Code generator
-    description: Generate and download QR-codes for quick connections to WiFi networks.
+    description: Generate and download QR codes for quick connections to WiFi networks.
 
   xml-formatter:
     title: XML formatter
-    description: Prettify your XML string to a human friendly readable format.
+    description: Prettify your XML string into a friendly, human-readable format.
 
   temperature-converter:
     title: Temperature converter
-    description: Temperature degrees conversions for Kelvin, Celsius, Fahrenheit, Rankine, Delisle, Newton, Réaumur and RÞmer.
+    description: Degrees temperature conversions for Kelvin, Celsius, Fahrenheit, Rankine, Delisle, Newton, Réaumur, and RÞmer.
 
   chmod-calculator:
     title: Chmod calculator
@@ -270,11 +270,11 @@ tools:
 
   rsa-key-pair-generator:
     title: RSA key pair generator
-    description: Generate new random RSA private and public key pem certificates.
+    description: Generate a new random RSA private and public pem certificate key pair.
 
   html-wysiwyg-editor:
     title: HTML WYSIWYG editor
-    description: Online HTML editor with feature-rich WYSIWYG editor, get the source code of the content immediately.
+    description: Online, feature-rich WYSIWYG HTML editor which generates the source code of the content immediately.
 
   yaml-to-toml:
     title: YAML to TOML
@@ -302,15 +302,15 @@ tools:
 
   ipv4-subnet-calculator:
     title: IPv4 subnet calculator
-    description: Parse your IPv4 CIDR blocks and get all the info you need about your sub network.
+    description: Parse your IPv4 CIDR blocks and get all the info you need about your subnet.
 
   og-meta-generator:
     title: Open graph meta generator
-    description: Generate open-graph and socials html meta tags for your website.
+    description: Generate open-graph and socials HTML meta tags for your website.
 
   ipv6-ula-generator:
     title: IPv6 ULA generator
-    description: Generate your own local, non-routable IP addresses on your network according to RFC4193.
+    description: Generate your own local, non-routable IP addresses for your network according to RFC4193.
 
   hash-text:
     title: Hash text
@@ -330,7 +330,7 @@ tools:
 
   json-minify:
     title: JSON minify
-    description: Minify and compress your JSON by removing unnecessary white spaces.
+    description: Minify and compress your JSON by removing unnecessary whitespace.
 
   ulid-generator:
     title: ULID generator
@@ -342,31 +342,31 @@ tools:
 
   base-converter:
     title: Integer base converter
-    description: Convert number between different bases (decimal, hexadecimal, binary, octal, base64, ...)
+    description: Convert a number between different bases (decimal, hexadecimal, binary, octal, base64, ...)
 
   yaml-to-json-converter:
     title: YAML to JSON converter
-    description: Simply convert YAML to JSON with this live online converter.
+    description: Simply convert YAML to JSON with this online live converter.
 
   uuid-generator:
     title: UUIDs generator
     description: A Universally Unique Identifier (UUID) is a 128-bit number used to identify information in computer systems. The number of possible UUIDs is 16^32, which is 2^128 or about 3.4x10^38 (which is a lot!).
 
   ipv4-address-converter:
-    title: Ipv4 address converter
-    description: Convert an ip address into decimal, binary, hexadecimal or event in ipv6
+    title: IPv4 address converter
+    description: Convert an IP address into decimal, binary, hexadecimal, or even an IPv6 representation of it.
 
   text-statistics:
     title: Text statistics
-    description: Get information about a text, the amount of characters, the amount of words, it\'s size, ...
+    description: Get information about a text, the number of characters, the number of words, its size in bytes, ...
 
   text-to-nato-alphabet:
     title: Text to NATO alphabet
-    description: Transform text into NATO phonetic alphabet for oral transmission.
+    description: Transform text into the NATO phonetic alphabet for oral transmission.
 
   basic-auth-generator:
     title: Basic auth generator
-    description: Generate a base64 basic auth header from an username and a password.
+    description: Generate a base64 basic auth header from a username and password.
 
   text-to-unicode:
     title: Text to Unicode
@@ -374,7 +374,7 @@ tools:
 
   ipv4-range-expander:
     title: IPv4 range expander
-    description: Given a start and an end IPv4 address this tool calculates a valid IPv4 network with its CIDR notation.
+    description: Given a start and an end IPv4 address, this tool calculates a valid IPv4 subnet along with its CIDR notation.
 
   text-diff:
     title: Text diff
@@ -385,9 +385,9 @@ tools:
     description: Generate and validate time-based OTP (one time password) for multi-factor authentication.
 
   url-encoder:
-    title: Encode/decode url formatted strings
-    description: Encode to url-encoded format (also known as "percent-encoded") or decode from it.
+    title: Encode/decode URL-formatted strings
+    description: Encode text to URL-encoded format (also known as "percent-encoded"), or decode from it.
 
   text-to-binary:
     title: Text to ASCII binary
-    description: Convert text to its ASCII binary representation and vice versa.
+    description: Convert text to its ASCII binary representation and vice-versa.
diff --git a/package.json b/package.json
index 2bdfb7b4..8d037498 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "it-tools",
-  "version": "2023.12.21-5ed3693",
+  "version": "2024.5.13-a0bc346",
   "description": "Collection of handy online tools for developers, with great UX. ",
   "keywords": [
     "productivity",
@@ -37,11 +37,13 @@
   "dependencies": {
     "@it-tools/bip39": "^0.0.4",
     "@it-tools/oggen": "^1.3.0",
+    "@regexper/render": "^1.0.0",
     "@sindresorhus/slugify": "^2.2.1",
     "@tiptap/pm": "2.1.6",
     "@tiptap/starter-kit": "2.1.6",
     "@tiptap/vue-3": "2.0.3",
     "@types/figlet": "^1.5.8",
+    "@types/markdown-it": "^13.0.7",
     "@vicons/material": "^0.12.0",
     "@vicons/tabler": "^0.12.0",
     "@vueuse/core": "^10.3.0",
@@ -57,6 +59,7 @@
     "crypto-js": "^4.1.1",
     "date-fns": "^2.29.3",
     "dompurify": "^3.0.6",
+    "email-normalizer": "^1.0.0",
     "emojilib": "^3.0.10",
     "figlet": "^1.7.0",
     "figue": "^1.2.0",
@@ -64,11 +67,13 @@
     "highlight.js": "^11.7.0",
     "iarna-toml-esm": "^3.0.5",
     "ibantools": "^4.3.3",
+    "js-base64": "^3.7.6",
     "json5": "^2.2.3",
     "jwt-decode": "^3.1.2",
     "korean-unpacker": "^1.0.3",
     "libphonenumber-js": "^1.10.28",
     "lodash": "^4.17.21",
+    "markdown-it": "^14.0.0",
     "marked": "^10.0.0",
     "mathjs": "^11.9.1",
     "mime-types": "^2.1.35",
@@ -81,6 +86,7 @@
     "pinia": "^2.0.34",
     "plausible-tracker": "^0.3.8",
     "qrcode": "^1.5.1",
+    "randexp": "^0.5.3",
     "sql-formatter": "^13.0.0",
     "ua-parser-js": "^1.0.35",
     "ulid": "^2.3.0",
@@ -90,8 +96,10 @@
     "vue": "^3.3.4",
     "vue-i18n": "^9.9.1",
     "vue-router": "^4.1.6",
+    "vue-shadow-dom": "^4.2.0",
     "vue-tsc": "^1.8.1",
     "xml-formatter": "^3.3.2",
+    "xml-js": "^1.6.11",
     "yaml": "^2.2.1"
   },
   "devDependencies": {
@@ -138,5 +146,6 @@
     "vitest": "^0.34.0",
     "workbox-window": "^7.0.0",
     "zx": "^7.2.1"
-  }
+  },
+  "packageManager": "pnpm@8.15.3"
 }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3d013aee..c66685bc 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -11,6 +11,9 @@ dependencies:
   '@it-tools/oggen':
     specifier: ^1.3.0
     version: 1.3.0
+  '@regexper/render':
+    specifier: ^1.0.0
+    version: 1.0.0
   '@sindresorhus/slugify':
     specifier: ^2.2.1
     version: 2.2.1
@@ -26,6 +29,9 @@ dependencies:
   '@types/figlet':
     specifier: ^1.5.8
     version: 1.5.8
+  '@types/markdown-it':
+    specifier: ^13.0.7
+    version: 13.0.9
   '@vicons/material':
     specifier: ^0.12.0
     version: 0.12.0
@@ -71,6 +77,9 @@ dependencies:
   dompurify:
     specifier: ^3.0.6
     version: 3.0.6
+  email-normalizer:
+    specifier: ^1.0.0
+    version: 1.0.0
   emojilib:
     specifier: ^3.0.10
     version: 3.0.10
@@ -92,6 +101,9 @@ dependencies:
   ibantools:
     specifier: ^4.3.3
     version: 4.3.3
+  js-base64:
+    specifier: ^3.7.6
+    version: 3.7.7
   json5:
     specifier: ^2.2.3
     version: 2.2.3
@@ -107,6 +119,9 @@ dependencies:
   lodash:
     specifier: ^4.17.21
     version: 4.17.21
+  markdown-it:
+    specifier: ^14.0.0
+    version: 14.1.0
   marked:
     specifier: ^10.0.0
     version: 10.0.0
@@ -143,6 +158,9 @@ dependencies:
   qrcode:
     specifier: ^1.5.1
     version: 1.5.1
+  randexp:
+    specifier: ^0.5.3
+    version: 0.5.3
   sql-formatter:
     specifier: ^13.0.0
     version: 13.0.0
@@ -170,12 +188,18 @@ dependencies:
   vue-router:
     specifier: ^4.1.6
     version: 4.1.6(vue@3.3.4)
+  vue-shadow-dom:
+    specifier: ^4.2.0
+    version: 4.2.0
   vue-tsc:
     specifier: ^1.8.1
     version: 1.8.1(typescript@5.2.2)
   xml-formatter:
     specifier: ^3.3.2
     version: 3.3.2
+  xml-js:
+    specifier: ^1.6.11
+    version: 1.6.11
   yaml:
     specifier: ^2.2.1
     version: 2.2.1
@@ -2468,6 +2492,17 @@ packages:
     resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
     dev: false
 
+  /@regexper/parser@1.0.0:
+    resolution: {integrity: sha512-S8AWIGpCNdl9PNHdbhI6TpXZsPk6FDU/RTZI+6UFF4rVFqDQKjCIbZSgFu7NihoEZKq57wKFPbbT1EzrjVvPHA==}
+    dev: false
+
+  /@regexper/render@1.0.0:
+    resolution: {integrity: sha512-xYm9RUgnhhZotTtf8UZpK1PG2CcTRXQ3JPwfTlYUZsy2J+UcTVc7BaO/MJadpMoVuT8jrIyptH4Y0HLzqhI3hQ==}
+    dependencies:
+      '@regexper/parser': 1.0.0
+      '@svgdotjs/svg.js': 3.2.4
+    dev: false
+
   /@remirror/core-constants@2.0.1:
     resolution: {integrity: sha512-ZR4aihtnnT9lMbhh5DEbsriJRlukRXmLZe7HmM+6ufJNNUDoazc75UX26xbgQlNUqgAqMcUdGFAnPc1JwgAdLQ==}
     dependencies:
@@ -2617,6 +2652,10 @@ packages:
       string.prototype.matchall: 4.0.10
     dev: true
 
+  /@svgdotjs/svg.js@3.2.4:
+    resolution: {integrity: sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==}
+    dev: false
+
   /@tiptap/core@2.1.12(@tiptap/pm@2.1.6):
     resolution: {integrity: sha512-ZGc3xrBJA9KY8kln5AYTj8y+GDrKxi7u95xIl2eccrqTY5CQeRu6HRNM1yT4mAjuSaG9jmazyjGRlQuhyxCKxQ==}
     peerDependencies:
@@ -2946,7 +2985,6 @@ packages:
 
   /@types/linkify-it@3.0.2:
     resolution: {integrity: sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==}
-    dev: true
 
   /@types/lodash-es@4.17.10:
     resolution: {integrity: sha512-YJP+w/2khSBwbUSFdGsSqmDvmnN3cCKoPOL7Zjle6s30ZtemkkqhjVfFqGwPN7ASil5VyjE2GtyU/yqYY6mC0A==}
@@ -2968,6 +3006,13 @@ packages:
       '@types/mdurl': 1.0.2
     dev: true
 
+  /@types/markdown-it@13.0.9:
+    resolution: {integrity: sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==}
+    dependencies:
+      '@types/linkify-it': 3.0.2
+      '@types/mdurl': 1.0.2
+    dev: false
+
   /@types/mdast@3.0.11:
     resolution: {integrity: sha512-Y/uImid8aAwrEA24/1tcRZwpxX3pIFTSilcNDKSPn+Y2iDywSEachzRuvgAYYLR3wpGXAsMbv5lvKLDZLeYPAw==}
     dependencies:
@@ -2976,7 +3021,6 @@ packages:
 
   /@types/mdurl@1.0.2:
     resolution: {integrity: sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==}
-    dev: true
 
   /@types/mime-types@2.1.1:
     resolution: {integrity: sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==}
@@ -4945,6 +4989,11 @@ packages:
       tslib: 2.5.0
     dev: false
 
+  /drange@1.1.1:
+    resolution: {integrity: sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==}
+    engines: {node: '>=4'}
+    dev: false
+
   /duplexer@0.1.2:
     resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
     dev: true
@@ -4973,6 +5022,12 @@ packages:
   /electron-to-chromium@1.4.572:
     resolution: {integrity: sha512-RlFobl4D3ieetbnR+2EpxdzFl9h0RAJkPK3pfiwMug2nhBin2ZCsGIAJWdpNniLz43sgXam/CgipOmvTA+rUiA==}
 
+  /email-normalizer@1.0.0:
+    resolution: {integrity: sha512-wZYuuMtL4kUOmg/TPtCrf9hAZjbFq+FcjWA85Z5nr2lGllRnWJPxCJw3gy4Cx+adMoyVw4VJfGGvt/OHgIW+qg==}
+    dependencies:
+      typescript: 5.5.4
+    dev: false
+
   /emitter-component@1.1.1:
     resolution: {integrity: sha512-G+mpdiAySMuB7kesVRLuyvYRqDmshB7ReKEVuyBPkzQlmiDiLrt7hHHIy4Aff552bgknVN7B2/d3lzhGO5dvpQ==}
     dev: false
@@ -5004,7 +5059,6 @@ packages:
   /entities@4.5.0:
     resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
     engines: {node: '>=0.12'}
-    dev: true
 
   /errno@0.1.8:
     resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==}
@@ -6475,6 +6529,10 @@ packages:
     hasBin: true
     dev: true
 
+  /js-base64@3.7.7:
+    resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==}
+    dev: false
+
   /js-beautify@1.14.6:
     resolution: {integrity: sha512-GfofQY5zDp+cuHc+gsEXKPpNw2KbPddreEo35O6jT6i0RVK6LhsoYBhq5TvK4/n74wnA0QbK8gGd+jUZwTMKJw==}
     engines: {node: '>=10'}
@@ -6676,6 +6734,12 @@ packages:
     dependencies:
       uc.micro: 1.0.6
 
+  /linkify-it@5.0.0:
+    resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
+    dependencies:
+      uc.micro: 2.1.0
+    dev: false
+
   /local-pkg@0.4.3:
     resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==}
     engines: {node: '>=14'}
@@ -6813,6 +6877,18 @@ packages:
       mdurl: 1.0.1
       uc.micro: 1.0.6
 
+  /markdown-it@14.1.0:
+    resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
+    hasBin: true
+    dependencies:
+      argparse: 2.0.1
+      entities: 4.5.0
+      linkify-it: 5.0.0
+      mdurl: 2.0.0
+      punycode.js: 2.3.1
+      uc.micro: 2.1.0
+    dev: false
+
   /marked@10.0.0:
     resolution: {integrity: sha512-YiGcYcWj50YrwBgNzFoYhQ1hT6GmQbFG8SksnYJX1z4BXTHSOrz1GB5/Jm2yQvMg4nN1FHP4M6r03R10KrVUiA==}
     engines: {node: '>= 18'}
@@ -6861,6 +6937,10 @@ packages:
   /mdurl@1.0.1:
     resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==}
 
+  /mdurl@2.0.0:
+    resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
+    dev: false
+
   /merge-stream@2.0.0:
     resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
     dev: true
@@ -7670,6 +7750,11 @@ packages:
     resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==}
     dev: true
 
+  /punycode.js@2.3.1:
+    resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
+    engines: {node: '>=6'}
+    dev: false
+
   /punycode@2.3.0:
     resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
     engines: {node: '>=6'}
@@ -7709,6 +7794,14 @@ packages:
       ret: 0.1.15
     dev: false
 
+  /randexp@0.5.3:
+    resolution: {integrity: sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==}
+    engines: {node: '>=4'}
+    dependencies:
+      drange: 1.1.1
+      ret: 0.2.2
+    dev: false
+
   /randombytes@2.1.0:
     resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
     dependencies:
@@ -7879,6 +7972,11 @@ packages:
     engines: {node: '>=0.12'}
     dev: false
 
+  /ret@0.2.2:
+    resolution: {integrity: sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==}
+    engines: {node: '>=4'}
+    dev: false
+
   /reusify@1.0.4:
     resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
     engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
@@ -7959,8 +8057,6 @@ packages:
   /sax@1.2.4:
     resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
     requiresBuild: true
-    dev: true
-    optional: true
 
   /saxes@6.0.0:
     resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
@@ -8603,6 +8699,12 @@ packages:
     engines: {node: '>=14.17'}
     hasBin: true
 
+  /typescript@5.5.4:
+    resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==}
+    engines: {node: '>=14.17'}
+    hasBin: true
+    dev: false
+
   /ua-parser-js@1.0.35:
     resolution: {integrity: sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==}
     dev: false
@@ -8610,6 +8712,10 @@ packages:
   /uc.micro@1.0.6:
     resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==}
 
+  /uc.micro@2.1.0:
+    resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
+    dev: false
+
   /ufo@1.1.2:
     resolution: {integrity: sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==}
 
@@ -9143,8 +9249,8 @@ packages:
       vue: 3.3.4
     dev: false
 
-  /vue-demi@0.14.5(vue@3.3.4):
-    resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==}
+  /vue-demi@0.14.10(vue@3.3.4):
+    resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
     engines: {node: '>=12'}
     hasBin: true
     requiresBuild: true
@@ -9211,6 +9317,10 @@ packages:
       vue: 3.3.4
     dev: false
 
+  /vue-shadow-dom@4.2.0:
+    resolution: {integrity: sha512-lguI064rT2HT/dxqSmXtz860KOvCq+W3nU1jMqroTmX3K1H46q22BMR4emh/Ld3ozy35XJKOaNGcr6mkJ/t/yg==}
+    dev: false
+
   /vue-template-compiler@2.7.14:
     resolution: {integrity: sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==}
     dependencies:
@@ -9550,6 +9660,13 @@ packages:
       xml-parser-xo: 4.0.5
     dev: false
 
+  /xml-js@1.6.11:
+    resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
+    hasBin: true
+    dependencies:
+      sax: 1.2.4
+    dev: false
+
   /xml-name-validator@4.0.0:
     resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
     engines: {node: '>=12'}
diff --git a/src/components/FormatTransformer.vue b/src/components/FormatTransformer.vue
index 677fc25a..bcfcdb58 100644
--- a/src/components/FormatTransformer.vue
+++ b/src/components/FormatTransformer.vue
@@ -48,7 +48,7 @@ const output = computed(() => transformer.value(input.value));
     monospace
   />
 
-  
+  
     
       {{ outputLabel }}
     
diff --git a/src/components/TextareaCopyable.vue b/src/components/TextareaCopyable.vue
index 8b0aae61..9585177d 100644
--- a/src/components/TextareaCopyable.vue
+++ b/src/components/TextareaCopyable.vue
@@ -7,6 +7,7 @@ import sqlHljs from 'highlight.js/lib/languages/sql';
 import xmlHljs from 'highlight.js/lib/languages/xml';
 import yamlHljs from 'highlight.js/lib/languages/yaml';
 import iniHljs from 'highlight.js/lib/languages/ini';
+import markdownHljs from 'highlight.js/lib/languages/markdown';
 import { useCopy } from '@/composable/copy';
 
 const props = withDefaults(
@@ -30,6 +31,7 @@ hljs.registerLanguage('html', xmlHljs);
 hljs.registerLanguage('xml', xmlHljs);
 hljs.registerLanguage('yaml', yamlHljs);
 hljs.registerLanguage('toml', iniHljs);
+hljs.registerLanguage('markdown', markdownHljs);
 
 const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props);
 const { height } = followHeightOf.value ? useElementSize(followHeightOf) : { height: ref(null) };
diff --git a/src/composable/debouncedref.ts b/src/composable/debouncedref.ts
new file mode 100644
index 00000000..09dd10e9
--- /dev/null
+++ b/src/composable/debouncedref.ts
@@ -0,0 +1,21 @@
+import _ from 'lodash';
+
+function useDebouncedRef
(initialValue: T, delay: number, immediate: boolean = false) {
+  const state = ref(initialValue);
+  const debouncedRef = customRef((track, trigger) => ({
+    get() {
+      track();
+      return state.value;
+    },
+    set: _.debounce(
+      (value) => {
+        state.value = value;
+        trigger();
+      },
+      delay,
+      { leading: immediate },
+    ),
+  }));
+  return debouncedRef;
+}
+export default useDebouncedRef;
diff --git a/src/composable/downloadBase64.ts b/src/composable/downloadBase64.ts
index 37b0428d..3bc20226 100644
--- a/src/composable/downloadBase64.ts
+++ b/src/composable/downloadBase64.ts
@@ -1,8 +1,13 @@
-import { extension as getExtensionFromMime } from 'mime-types';
+import { extension as getExtensionFromMimeType, extension as getMimeTypeFromExtension } from 'mime-types';
 import type { Ref } from 'vue';
 import _ from 'lodash';
 
-export { getMimeTypeFromBase64, useDownloadFileFromBase64 };
+export {
+  getMimeTypeFromBase64,
+  getMimeTypeFromExtension, getExtensionFromMimeType,
+  useDownloadFileFromBase64, useDownloadFileFromBase64Refs,
+  previewImageFromBase64,
+};
 
 const commonMimeTypesSignatures = {
   'JVBERi0': 'application/pdf',
@@ -36,30 +41,78 @@ function getFileExtensionFromMimeType({
   defaultExtension?: string
 }) {
   if (mimeType) {
-    return getExtensionFromMime(mimeType) ?? defaultExtension;
+    return getExtensionFromMimeType(mimeType) ?? defaultExtension;
   }
 
   return defaultExtension;
 }
 
-function useDownloadFileFromBase64({ source, filename }: { source: Ref; filename?: string }) {
+function downloadFromBase64({ sourceValue, filename, extension, fileMimeType }:
+{ sourceValue: string; filename?: string; extension?: string; fileMimeType?: string }) {
+  if (sourceValue === '') {
+    throw new Error('Base64 string is empty');
+  }
+
+  const defaultExtension = extension ?? 'txt';
+  const { mimeType } = getMimeTypeFromBase64({ base64String: sourceValue });
+  let base64String = sourceValue;
+  if (!mimeType) {
+    const targetMimeType = fileMimeType ?? getMimeTypeFromExtension(defaultExtension);
+    base64String = `data:${targetMimeType};base64,${sourceValue}`;
+  }
+
+  const cleanExtension = extension ?? getFileExtensionFromMimeType(
+    { mimeType, defaultExtension });
+  let cleanFileName = filename ?? `file.${cleanExtension}`;
+  if (extension && !cleanFileName.endsWith(`.${extension}`)) {
+    cleanFileName = `${cleanFileName}.${cleanExtension}`;
+  }
+
+  const a = document.createElement('a');
+  a.href = base64String;
+  a.download = cleanFileName;
+  a.click();
+}
+
+function useDownloadFileFromBase64(
+  { source, filename, extension, fileMimeType }:
+  { source: Ref; filename?: string; extension?: string; fileMimeType?: string }) {
   return {
     download() {
-      if (source.value === '') {
-        throw new Error('Base64 string is empty');
-      }
-
-      const { mimeType } = getMimeTypeFromBase64({ base64String: source.value });
-      const base64String = mimeType
-        ? source.value
-        : `data:text/plain;base64,${source.value}`;
-
-      const cleanFileName = filename ?? `file.${getFileExtensionFromMimeType({ mimeType })}`;
-
-      const a = document.createElement('a');
-      a.href = base64String;
-      a.download = cleanFileName;
-      a.click();
+      downloadFromBase64({ sourceValue: source.value, filename, extension, fileMimeType });
     },
   };
 }
+
+function useDownloadFileFromBase64Refs(
+  { source, filename, extension }:
+  { source: Ref; filename?: Ref; extension?: Ref }) {
+  return {
+    download() {
+      downloadFromBase64({ sourceValue: source.value, filename: filename?.value, extension: extension?.value });
+    },
+  };
+}
+
+function previewImageFromBase64(base64String: string): HTMLImageElement {
+  if (base64String === '') {
+    throw new Error('Base64 string is empty');
+  }
+
+  const img = document.createElement('img');
+  img.src = base64String;
+
+  const container = document.createElement('div');
+  container.appendChild(img);
+
+  const previewContainer = document.getElementById('previewContainer');
+  if (previewContainer) {
+    previewContainer.innerHTML = '';
+    previewContainer.appendChild(container);
+  }
+  else {
+    throw new Error('Preview container element not found');
+  }
+
+  return img;
+}
diff --git a/src/composable/queryParams.ts b/src/composable/queryParams.ts
index 9699abbc..7cc8cc0d 100644
--- a/src/composable/queryParams.ts
+++ b/src/composable/queryParams.ts
@@ -1,7 +1,8 @@
 import { useRouteQuery } from '@vueuse/router';
 import { computed } from 'vue';
+import { useStorage } from '@vueuse/core';
 
-export { useQueryParam };
+export { useQueryParam, useQueryParamOrStorage };
 
 const transformers = {
   number: {
@@ -16,6 +17,12 @@ const transformers = {
     fromQuery: (value: string) => value.toLowerCase() === 'true',
     toQuery: (value: boolean) => (value ? 'true' : 'false'),
   },
+  object: {
+    fromQuery: (value: string) => {
+      return JSON.parse(value);
+    },
+    toQuery: (value: object) => JSON.stringify(value),
+  },
 };
 
 function useQueryParam({ name, defaultValue }: { name: string; defaultValue: T }) {
@@ -33,3 +40,27 @@ function useQueryParam({ name, defaultValue }: { name: string; defaultValue:
     },
   });
 }
+
+function useQueryParamOrStorage({ name, storageName, defaultValue }: { name: string; storageName: string; defaultValue: T }) {
+  const type = typeof defaultValue;
+  const transformer = transformers[type as keyof typeof transformers] ?? transformers.string;
+
+  const storageRef = useStorage(storageName, defaultValue);
+  const proxyDefaultValue = transformer.toQuery(defaultValue as never);
+  const proxy = useRouteQuery(name, proxyDefaultValue);
+
+  const r = ref(defaultValue);
+
+  watch(r,
+    (value) => {
+      proxy.value = transformer.toQuery(value as never);
+      storageRef.value = value as never;
+    },
+    { deep: true });
+
+  r.value = (proxy.value && proxy.value !== proxyDefaultValue
+    ? transformer.fromQuery(proxy.value) as unknown as T
+    : storageRef.value as T) as never;
+
+  return r;
+}
diff --git a/src/composable/validation.ts b/src/composable/validation.ts
index 472ca4b2..33a6a7b2 100644
--- a/src/composable/validation.ts
+++ b/src/composable/validation.ts
@@ -3,9 +3,11 @@ import _ from 'lodash';
 import { type Ref, reactive, watch } from 'vue';
 
 type ValidatorReturnType = unknown;
+type GetErrorMessageReturnType = string;
 
 export interface UseValidationRule {
   validator: (value: T) => ValidatorReturnType
+  getErrorMessage?: (value: T) => GetErrorMessageReturnType
   message: string
 }
 
@@ -24,6 +26,15 @@ export function isFalsyOrHasThrown(cb: () => ValidatorReturnType): boolean {
   }
 }
 
+export function getErrorMessageOrThrown(cb: () => GetErrorMessageReturnType): string {
+  try {
+    return cb() || '';
+  }
+  catch (e: any) {
+    return e.toString();
+  }
+}
+
 export interface ValidationAttrs {
   feedback: string
   validationStatus: string | undefined
@@ -61,7 +72,13 @@ export function useValidation({
 
       for (const rule of get(rules)) {
         if (isFalsyOrHasThrown(() => rule.validator(source.value))) {
-          state.message = rule.message;
+          if (rule.getErrorMessage) {
+            const getErrorMessage = rule.getErrorMessage;
+            state.message = rule.message.replace('{0}', getErrorMessageOrThrown(() => getErrorMessage(source.value)));
+          }
+          else {
+            state.message = rule.message;
+          }
           state.status = 'error';
         }
       }
diff --git a/src/main.ts b/src/main.ts
index 36ba3b7f..19d28bf2 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -3,6 +3,7 @@ import { createPinia } from 'pinia';
 import { createHead } from '@vueuse/head';
 
 import { registerSW } from 'virtual:pwa-register';
+import shadow from 'vue-shadow-dom';
 import { plausible } from './plugins/plausible.plugin';
 
 import 'virtual:uno.css';
@@ -23,5 +24,6 @@ app.use(i18nPlugin);
 app.use(router);
 app.use(naive);
 app.use(plausible);
+app.use(shadow);
 
 app.mount('#app');
diff --git a/src/modules/i18n/components/locale-selector.vue b/src/modules/i18n/components/locale-selector.vue
index 3f0c461c..45732bf9 100644
--- a/src/modules/i18n/components/locale-selector.vue
+++ b/src/modules/i18n/components/locale-selector.vue
@@ -3,6 +3,7 @@ const { availableLocales, locale } = useI18n();
 
 const localesLong: Record = {
   en: 'English',
+  de: 'Deutsch',
   es: 'Español',
   fr: 'Français',
   pt: 'PortuguĂȘs',
diff --git a/src/tools/base64-file-converter/base64-file-converter.vue b/src/tools/base64-file-converter/base64-file-converter.vue
index 377625bd..a489f9a1 100644
--- a/src/tools/base64-file-converter/base64-file-converter.vue
+++ b/src/tools/base64-file-converter/base64-file-converter.vue
@@ -2,12 +2,19 @@
 import { useBase64 } from '@vueuse/core';
 import type { Ref } from 'vue';
 import { useCopy } from '@/composable/copy';
-import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
+import { getExtensionFromMimeType, getMimeTypeFromBase64, previewImageFromBase64, useDownloadFileFromBase64Refs } from '@/composable/downloadBase64';
 import { useValidation } from '@/composable/validation';
 import { isValidBase64 } from '@/utils/base64';
 
+const fileName = ref('file');
+const fileExtension = ref('');
 const base64Input = ref('');
-const { download } = useDownloadFileFromBase64({ source: base64Input });
+const { download } = useDownloadFileFromBase64Refs(
+  {
+    source: base64Input,
+    filename: fileName,
+    extension: fileExtension,
+  });
 const base64InputValidation = useValidation({
   source: base64Input,
   rules: [
@@ -18,6 +25,35 @@ const base64InputValidation = useValidation({
   ],
 });
 
+watch(
+  base64Input,
+  (newValue, _) => {
+    const { mimeType } = getMimeTypeFromBase64({ base64String: newValue });
+    if (mimeType) {
+      fileExtension.value = getExtensionFromMimeType(mimeType) || fileExtension.value;
+    }
+  },
+);
+
+function previewImage() {
+  if (!base64InputValidation.isValid) {
+    return;
+  }
+  try {
+    const image = previewImageFromBase64(base64Input.value);
+    image.style.maxWidth = '100%';
+    image.style.maxHeight = '400px';
+    const previewContainer = document.getElementById('previewContainer');
+    if (previewContainer) {
+      previewContainer.innerHTML = '';
+      previewContainer.appendChild(image);
+    }
+  }
+  catch (_) {
+    //
+  }
+}
+
 function downloadFile() {
   if (!base64InputValidation.isValid) {
     return;
@@ -44,6 +80,24 @@ async function onUpload(file: File) {
 
 
   
+    
+      
+         
+      
+         
+     
     
+    
+
+    
+      
+        Preview image
+       
       
         Download file
        
diff --git a/src/tools/bcrypt/bcrypt.vue b/src/tools/bcrypt/bcrypt.vue
index c28c20bf..d4881299 100644
--- a/src/tools/bcrypt/bcrypt.vue
+++ b/src/tools/bcrypt/bcrypt.vue
@@ -28,7 +28,7 @@ const compareMatch = computed(() => compareSync(compareString.value, compareHash
       mb-2
     />
     
-       
 
     
diff --git a/src/tools/email-normalizer/email-normalizer.vue b/src/tools/email-normalizer/email-normalizer.vue
new file mode 100644
index 00000000..eae97c4e
--- /dev/null
+++ b/src/tools/email-normalizer/email-normalizer.vue
@@ -0,0 +1,65 @@
+
+
+
+  
+    
+      Raw emails to normalize:
+    
+    
+
+    
+      Normalized emails:
+    
+    
+    
+      
+        Clear emails
+       
+      
+        Copy normalized emails
+       
+    
+  
 
diff --git a/src/tools/email-normalizer/index.ts b/src/tools/email-normalizer/index.ts
new file mode 100644
index 00000000..299a30f7
--- /dev/null
+++ b/src/tools/email-normalizer/index.ts
@@ -0,0 +1,12 @@
+import { Mail } from '@vicons/tabler';
+import { defineTool } from '../tool';
+
+export const tool = defineTool({
+  name: 'Email normalizer',
+  path: '/email-normalizer',
+  description: 'Normalize email addresses to a standard format for easier comparison. Useful for deduplication and data cleaning.',
+  keywords: ['email', 'normalizer'],
+  component: () => import('./email-normalizer.vue'),
+  icon: Mail,
+  createdAt: new Date('2024-08-15'),
+});
diff --git a/src/tools/emoji-picker/emoji-picker.vue b/src/tools/emoji-picker/emoji-picker.vue
index 750695f5..a12b10c2 100644
--- a/src/tools/emoji-picker/emoji-picker.vue
+++ b/src/tools/emoji-picker/emoji-picker.vue
@@ -4,6 +4,7 @@ import emojiKeywords from 'emojilib';
 import _ from 'lodash';
 import type { EmojiInfo } from './emoji.types';
 import { useFuzzySearch } from '@/composable/fuzzySearch';
+import useDebouncedRef from '@/composable/debouncedref';
 
 const escapeUnicode = ({ emoji }: { emoji: string }) => emoji.split('').map(unit => `\\u${unit.charCodeAt(0).toString(16).padStart(4, '0')}`).join('');
 const getEmojiCodePoints = ({ emoji }: { emoji: string }) => emoji.codePointAt(0) ? `0x${emoji.codePointAt(0)?.toString(16)}` : undefined;
@@ -23,7 +24,7 @@ const emojisGroups: { emojiInfos: EmojiInfo[]; group: string }[] = _
   .map((emojiInfos, group) => ({ group, emojiInfos }))
   .value();
 
-const searchQuery = ref('');
+const searchQuery = useDebouncedRef('', 500);
 
 const { searchResult } = useFuzzySearch({
   search: searchQuery,
diff --git a/src/tools/index.ts b/src/tools/index.ts
index aa861c93..388cfaf4 100644
--- a/src/tools/index.ts
+++ b/src/tools/index.ts
@@ -1,11 +1,17 @@
 import { tool as base64FileConverter } from './base64-file-converter';
 import { tool as base64StringConverter } from './base64-string-converter';
 import { tool as basicAuthGenerator } from './basic-auth-generator';
+import { tool as emailNormalizer } from './email-normalizer';
 
 import { tool as asciiTextDrawer } from './ascii-text-drawer';
 
 import { tool as textToUnicode } from './text-to-unicode';
 import { tool as safelinkDecoder } from './safelink-decoder';
+import { tool as xmlToJson } from './xml-to-json';
+import { tool as jsonToXml } from './json-to-xml';
+import { tool as regexTester } from './regex-tester';
+import { tool as regexMemo } from './regex-memo';
+import { tool as markdownToHtml } from './markdown-to-html';
 import { tool as pdfSignatureChecker } from './pdf-signature-checker';
 import { tool as numeronymGenerator } from './numeronym-generator';
 import { tool as macAddressGenerator } from './mac-address-generator';
@@ -107,6 +113,9 @@ export const toolsByCategory: ToolCategory[] = [
       listConverter,
       tomlToJson,
       tomlToYaml,
+      xmlToJson,
+      jsonToXml,
+      markdownToHtml,
     ],
   },
   {
@@ -148,6 +157,9 @@ export const toolsByCategory: ToolCategory[] = [
       dockerRunToDockerComposeConverter,
       xmlFormatter,
       yamlViewer,
+      emailNormalizer,
+      regexTester,
+      regexMemo,
     ],
   },
   {
diff --git a/src/tools/integer-base-converter/integer-base-converter.model.test.ts b/src/tools/integer-base-converter/integer-base-converter.model.test.ts
index d0387b64..c7d7db79 100644
--- a/src/tools/integer-base-converter/integer-base-converter.model.test.ts
+++ b/src/tools/integer-base-converter/integer-base-converter.model.test.ts
@@ -11,6 +11,9 @@ describe('integer-base-converter', () => {
         expect(convertBase({ value: '10100101', fromBase: 2, toBase: 16 })).toEqual('a5');
         expect(convertBase({ value: '192654', fromBase: 10, toBase: 8 })).toEqual('570216');
         expect(convertBase({ value: 'zz', fromBase: 64, toBase: 10 })).toEqual('2275');
+        expect(convertBase({ value: '42540766411283223938465490632011909384', fromBase: 10, toBase: 10 })).toEqual('42540766411283223938465490632011909384');
+        expect(convertBase({ value: '42540766411283223938465490632011909384', fromBase: 10, toBase: 16 })).toEqual('20010db8000085a300000000ac1f8908');
+        expect(convertBase({ value: '20010db8000085a300000000ac1f8908', fromBase: 16, toBase: 10 })).toEqual('42540766411283223938465490632011909384');
       });
     });
   });
diff --git a/src/tools/integer-base-converter/integer-base-converter.model.ts b/src/tools/integer-base-converter/integer-base-converter.model.ts
index b4470e57..da0fe77f 100644
--- a/src/tools/integer-base-converter/integer-base-converter.model.ts
+++ b/src/tools/integer-base-converter/integer-base-converter.model.ts
@@ -5,16 +5,16 @@ export function convertBase({ value, fromBase, toBase }: { value: string; fromBa
   let decValue = value
     .split('')
     .reverse()
-    .reduce((carry: number, digit: string, index: number) => {
+    .reduce((carry: bigint, digit: string, index: number) => {
       if (!fromRange.includes(digit)) {
         throw new Error(`Invalid digit "${digit}" for base ${fromBase}.`);
       }
-      return (carry += fromRange.indexOf(digit) * fromBase ** index);
-    }, 0);
+      return (carry += BigInt(fromRange.indexOf(digit)) * BigInt(fromBase) ** BigInt(index));
+    }, 0n);
   let newValue = '';
   while (decValue > 0) {
-    newValue = toRange[decValue % toBase] + newValue;
-    decValue = (decValue - (decValue % toBase)) / toBase;
+    newValue = toRange[Number(decValue % BigInt(toBase))] + newValue;
+    decValue = (decValue - (decValue % BigInt(toBase))) / BigInt(toBase);
   }
   return newValue || '0';
 }
diff --git a/src/tools/json-to-xml/index.ts b/src/tools/json-to-xml/index.ts
new file mode 100644
index 00000000..c35ace2b
--- /dev/null
+++ b/src/tools/json-to-xml/index.ts
@@ -0,0 +1,12 @@
+import { Braces } from '@vicons/tabler';
+import { defineTool } from '../tool';
+
+export const tool = defineTool({
+  name: 'JSON to XML',
+  path: '/json-to-xml',
+  description: 'Convert JSON to XML',
+  keywords: ['json', 'xml'],
+  component: () => import('./json-to-xml.vue'),
+  icon: Braces,
+  createdAt: new Date('2024-08-09'),
+});
diff --git a/src/tools/json-to-xml/json-to-xml.vue b/src/tools/json-to-xml/json-to-xml.vue
new file mode 100644
index 00000000..96a7cf16
--- /dev/null
+++ b/src/tools/json-to-xml/json-to-xml.vue
@@ -0,0 +1,32 @@
+
+
+
+   
diff --git a/src/tools/jwt-parser/jwt-parser.vue b/src/tools/jwt-parser/jwt-parser.vue
index 6b30fc0c..a26064d7 100644
--- a/src/tools/jwt-parser/jwt-parser.vue
+++ b/src/tools/jwt-parser/jwt-parser.vue
@@ -39,7 +39,7 @@ const validation = useValidation({
             {{ section.title }}
           
           
-            
+             
               
                 {{ claim }}
                
@@ -47,7 +47,7 @@ const validation = useValidation({
                 ({{ claimDescription }})
               
              
-            
+             
               {{ value }} 
               
                 ({{ friendlyValue }})
diff --git a/src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue b/src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue
index 9085725f..ccd8b519 100644
--- a/src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue
+++ b/src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue
@@ -2,6 +2,7 @@
 import { generateLoremIpsum } from './lorem-ipsum-generator.service';
 import { useCopy } from '@/composable/copy';
 import { randIntFromInterval } from '@/utils/random';
+import { computedRefreshable } from '@/composable/computedRefreshable';
 
 const paragraphs = ref(1);
 const sentences = ref([3, 8]);
@@ -9,7 +10,7 @@ const words = ref([8, 15]);
 const startWithLoremIpsum = ref(true);
 const asHTML = ref(false);
 
-const loremIpsumText = computed(() =>
+const [loremIpsumText, refreshLoremIpsum] = computedRefreshable(() =>
   generateLoremIpsum({
     paragraphCount: paragraphs.value,
     asHTML: asHTML.value,
@@ -18,6 +19,7 @@ const loremIpsumText = computed(() =>
     startWithLoremIpsum: startWithLoremIpsum.value,
   }),
 );
+
 const { copy } = useCopy({ source: loremIpsumText, text: 'Lorem ipsum copied to the clipboard' });
 
 
@@ -41,10 +43,13 @@ const { copy } = useCopy({ source: loremIpsumText, text: 'Lorem ipsum copied to
 
     
+    
       
         Copy
        
+      
+        Refresh
+       
     
   
 
diff --git a/src/tools/markdown-to-html/index.ts b/src/tools/markdown-to-html/index.ts
new file mode 100644
index 00000000..73a6cfb3
--- /dev/null
+++ b/src/tools/markdown-to-html/index.ts
@@ -0,0 +1,12 @@
+import { Markdown } from '@vicons/tabler';
+import { defineTool } from '../tool';
+
+export const tool = defineTool({
+  name: 'Markdown to HTML',
+  path: '/markdown-to-html',
+  description: 'Convert Markdown to Html and allow to print (as PDF)',
+  keywords: ['markdown', 'html', 'converter', 'pdf'],
+  component: () => import('./markdown-to-html.vue'),
+  icon: Markdown,
+  createdAt: new Date('2024-08-25'),
+});
diff --git a/src/tools/markdown-to-html/markdown-to-html.vue b/src/tools/markdown-to-html/markdown-to-html.vue
new file mode 100644
index 00000000..c84d44ec
--- /dev/null
+++ b/src/tools/markdown-to-html/markdown-to-html.vue
@@ -0,0 +1,44 @@
+
+
+
+  
+    
+
+    
+
+    
+       
+
+    
+      
+        Print as PDF
+       
+    
+  
 
diff --git a/src/tools/regex-memo/index.ts b/src/tools/regex-memo/index.ts
new file mode 100644
index 00000000..f1f56489
--- /dev/null
+++ b/src/tools/regex-memo/index.ts
@@ -0,0 +1,12 @@
+import { BrandJavascript } from '@vicons/tabler';
+import { defineTool } from '../tool';
+
+export const tool = defineTool({
+  name: 'Regex cheatsheet',
+  path: '/regex-memo',
+  description: 'Javascript Regex/Regular Expression cheatsheet',
+  keywords: ['regex', 'regular', 'expression', 'javascript', 'memo', 'cheatsheet'],
+  component: () => import('./regex-memo.vue'),
+  icon: BrandJavascript,
+  createdAt: new Date('2024-09-20'),
+});
diff --git a/src/tools/regex-memo/regex-memo.content.md b/src/tools/regex-memo/regex-memo.content.md
new file mode 100644
index 00000000..0f779401
--- /dev/null
+++ b/src/tools/regex-memo/regex-memo.content.md
@@ -0,0 +1,121 @@
+### Normal characters
+
+Expression | Description
+:--|:--
+`.` or `[^\n\r]` | any character *excluding* a newline or carriage return
+`[A-Za-z]` | alphabet
+`[a-z]` | lowercase alphabet
+`[A-Z]` | uppercase alphabet
+`\d` or `[0-9]` | digit
+`\D` or `[^0-9]` | non-digit
+`_` | underscore
+`\w` or `[A-Za-z0-9_]` | alphabet, digit or underscore
+`\W` or `[^A-Za-z0-9_]` | inverse of `\w`
+`\S` | inverse of `\s`
+
+### Whitespace characters
+
+Expression | Description
+:--|:--
+` ` | space
+`\t` | tab
+`\n` | newline
+`\r` | carriage return
+`\s` | space, tab, newline or carriage return
+
+### Character set
+
+Expression | Description
+:--|:--
+`[xyz]` | either `x`, `y` or `z`
+`[^xyz]` | neither `x`, `y` nor `z`
+`[1-3]` | either `1`, `2` or `3`
+`[^1-3]` | neither `1`, `2` nor `3`
+
+- Think of a character set as an `OR` operation on the single characters that are enclosed between the square brackets.
+- Use `^` after the opening `[` to ânegateâ the character set.
+- Within a character set, `.` means a literal period.
+
+### Characters that require escaping
+
+#### Outside a character set
+
+Expression | Description
+:--|:--
+`\.` | period
+`\^` | caret
+`\$` | dollar sign
+`\|` | pipe
+`\\` | back slash
+`\/` | forward slash
+`\(` | opening bracket
+`\)` | closing bracket
+`\[` | opening square bracket
+`\]` | closing square bracket
+`\{` | opening curly bracket
+`\}` | closing curly bracket
+
+#### Inside a character set
+
+Expression | Description
+:--|:--
+`\\` | back slash
+`\]` | closing square bracket
+
+- A `^` must be escaped only if it occurs immediately after the opening `[` of the character set.
+- A `-` must be escaped only if it occurs between two alphabets or two digits.
+
+### Quantifiers
+
+Expression | Description
+:--|:--
+`{2}` | exactly 2
+`{2,}` | at least 2
+`{2,7}` | at least 2 but no more than 7
+`*` | 0 or more
+`+` | 1 or more
+`?` | exactly 0 or 1
+
+- The quantifier goes *after* the expression to be quantified.
+
+### Boundaries
+
+Expression | Description
+:--|:--
+`^` | start of string
+`$` | end of string
+`\b` | word boundary
+
+- How word boundary matching works:
+    - At the beginning of the string if the first character is `\w`.
+    - Between two adjacent characters within the string, if the first character is `\w` and the second character is `\W`.
+    - At the end of the string if the last character is `\w`.
+
+### Matching
+
+Expression | Description
+:--|:--
+`foo\|bar` | match either `foo` or `bar`
+`foo(?=bar)` | match `foo` if itâs before `bar`
+`foo(?!bar)` | match `foo` if itâs *not* before `bar`
+`(?<=bar)foo` | match `foo` if itâs after `bar`
+`(?
+import { useThemeVars } from 'naive-ui';
+import Memo from './regex-memo.content.md';
+
+const themeVars = useThemeVars();
+
+
+
+  
+    
+ 
+
+
diff --git a/src/tools/regex-tester/index.ts b/src/tools/regex-tester/index.ts
new file mode 100644
index 00000000..9cc542f0
--- /dev/null
+++ b/src/tools/regex-tester/index.ts
@@ -0,0 +1,12 @@
+import { Language } from '@vicons/tabler';
+import { defineTool } from '../tool';
+
+export const tool = defineTool({
+  name: 'Regex Tester',
+  path: '/regex-tester',
+  description: 'Regex Tester',
+  keywords: ['regex', 'tester', 'sample', 'expression'],
+  component: () => import('./regex-tester.vue'),
+  icon: Language,
+  createdAt: new Date('2024-09-20'),
+});
diff --git a/src/tools/regex-tester/regex-tester.service.test.ts b/src/tools/regex-tester/regex-tester.service.test.ts
new file mode 100644
index 00000000..bd4efbbc
--- /dev/null
+++ b/src/tools/regex-tester/regex-tester.service.test.ts
@@ -0,0 +1,106 @@
+import { describe, expect, it } from 'vitest';
+import { matchRegex } from './regex-tester.service';
+
+const regexesData = [
+  {
+    regex: '',
+    text: '',
+    flags: '',
+    result: [],
+  },
+  {
+    regex: '.*',
+    text: '',
+    flags: '',
+    result: [],
+  },
+  {
+    regex: '',
+    text: 'aaa',
+    flags: '',
+    result: [],
+  },
+  {
+    regex: 'a',
+    text: 'baaa',
+    flags: '',
+    result: [
+      {
+        captures: [],
+        groups: [],
+        index: 1,
+        value: 'a',
+      },
+    ],
+  },
+  {
+    regex: '(.)(?
r)',
+    text: 'azertyr',
+    flags: 'g',
+    result: [
+      {
+        captures: [
+          {
+            end: 3,
+            name: '1',
+            start: 2,
+            value: 'e',
+          },
+          {
+            end: 4,
+            name: '2',
+            start: 3,
+            value: 'r',
+          },
+        ],
+        groups: [
+          {
+            end: 4,
+            name: 'g',
+            start: 3,
+            value: 'r',
+          },
+        ],
+        index: 2,
+        value: 'er',
+      },
+      {
+        captures: [
+          {
+            end: 6,
+            name: '1',
+            start: 5,
+            value: 'y',
+          },
+          {
+            end: 7,
+            name: '2',
+            start: 6,
+            value: 'r',
+          },
+        ],
+        groups: [
+          {
+            end: 7,
+            name: 'g',
+            start: 6,
+            value: 'r',
+          },
+        ],
+        index: 5,
+        value: 'yr',
+      },
+    ],
+  },
+];
+
+describe('regex-tester', () => {
+  for (const reg of regexesData) {
+    const { regex, text, flags, result: expected_result } = reg;
+    it(`Should matchRegex("${regex}","${text}","${flags}") return correct result`, async () => {
+      const result = matchRegex(regex, text, `${flags}d`);
+
+      expect(result).to.deep.equal(expected_result);
+    });
+  }
+});
diff --git a/src/tools/regex-tester/regex-tester.service.ts b/src/tools/regex-tester/regex-tester.service.ts
new file mode 100644
index 00000000..ec8682c5
--- /dev/null
+++ b/src/tools/regex-tester/regex-tester.service.ts
@@ -0,0 +1,61 @@
+interface RegExpGroupIndices {
+  [name: string]: [number, number]
+}
+interface RegExpIndices extends Array<[number, number]> {
+  groups: RegExpGroupIndices
+}
+interface RegExpExecArrayWithIndices extends RegExpExecArray {
+  indices: RegExpIndices
+}
+interface GroupCapture {
+  name: string
+  value: string
+  start: number
+  end: number
+};
+
+export function matchRegex(regex: string, text: string, flags: string) {
+  // if (regex === '' || text === '') {
+  //   return [];
+  // }
+
+  let lastIndex = -1;
+  const re = new RegExp(regex, flags);
+  const results = [];
+  let match = re.exec(text) as RegExpExecArrayWithIndices;
+  while (match !== null) {
+    if (re.lastIndex === lastIndex || match[0] === '') {
+      break;
+    }
+    const indices = match.indices;
+    const captures: Array = [];
+    Object.entries(match).forEach(([captureName, captureValue]) => {
+      if (captureName !== '0' && captureName.match(/\d+/)) {
+        captures.push({
+          name: captureName,
+          value: captureValue,
+          start: indices[Number(captureName)][0],
+          end: indices[Number(captureName)][1],
+        });
+      }
+    });
+    const groups: Array = [];
+    Object.entries(match.groups || {}).forEach(([groupName, groupValue]) => {
+      groups.push({
+        name: groupName,
+        value: groupValue,
+        start: indices.groups[groupName][0],
+        end: indices.groups[groupName][1],
+      });
+    });
+    results.push({
+      index: match.index,
+      value: match[0],
+      captures,
+      groups,
+    });
+    lastIndex = re.lastIndex;
+    match = re.exec(text) as RegExpExecArrayWithIndices;
+  }
+  return results;
+}
diff --git a/src/tools/regex-tester/regex-tester.vue b/src/tools/regex-tester/regex-tester.vue
new file mode 100644
index 00000000..a1fa7958
--- /dev/null
+++ b/src/tools/regex-tester/regex-tester.vue
@@ -0,0 +1,193 @@
+
+
+
+  
+    
+      
+        See Regular Expression Cheatsheet
+       
+      
+        
+          Global search. (g) 
+         
+        
+          Case-insensitive search. (i) 
+         
+        
+          Multiline(m) 
+         
+        
+          Singleline(s) 
+         
+        
+          Unicode(u) 
+         
+        
+          Unicode Sets (v) 
+         
+       
+
+       
+
+    
+      
+        
+          
+            
+              Index in text
+             
+            
+              Value
+             
+            
+              Captures
+             
+            
+              Groups
+             
+           
+         
+        
+          
+            {{ match.index }} 
+            {{ match.value }} 
+            
+              
+                
+                  "{{ capture.name }}" = {{ capture.value }} [{{ capture.start }} - {{ capture.end }}]
+                 
+               
+             
+            
+              
+                
+                  "{{ group.name }}" = {{ group.value }} [{{ group.start }} - {{ group.end }}]
+                 
+               
+             
+           
+         
+       
+      
+        No match
+       
+     
+
+    
+      {{ sample }} 
+     
+
+    
+      
+ 
+       
+     
+  
 
diff --git a/src/tools/token-generator/token-generator.service.ts b/src/tools/token-generator/token-generator.service.ts
index 3733a884..f928a415 100644
--- a/src/tools/token-generator/token-generator.service.ts
+++ b/src/tools/token-generator/token-generator.service.ts
@@ -20,7 +20,7 @@ export function createToken({
     withLowercase ? 'abcdefghijklmopqrstuvwxyz' : '',
     withNumbers ? '0123456789' : '',
     withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : '',
-  ].join(''); ;
+  ].join('');
 
   return shuffleString(allAlphabet.repeat(length)).substring(0, length);
 }
diff --git a/src/tools/url-encoder/url-encoder.vue b/src/tools/url-encoder/url-encoder.vue
index c43f8193..19025190 100644
--- a/src/tools/url-encoder/url-encoder.vue
+++ b/src/tools/url-encoder/url-encoder.vue
@@ -23,7 +23,7 @@ const decodeInput = ref('Hello%20world%20%3A)');
 const decodeOutput = computed(() => withDefaultOnError(() => decodeURIComponent(decodeInput.value), ''));
 
 const decodeValidation = useValidation({
-  source: encodeInput,
+  source: decodeInput,
   rules: [
     {
       validator: value => isNotThrowing(() => decodeURIComponent(value)),
diff --git a/src/tools/xml-to-json/index.ts b/src/tools/xml-to-json/index.ts
new file mode 100644
index 00000000..8d83f4fe
--- /dev/null
+++ b/src/tools/xml-to-json/index.ts
@@ -0,0 +1,12 @@
+import { Braces } from '@vicons/tabler';
+import { defineTool } from '../tool';
+
+export const tool = defineTool({
+  name: 'XML to JSON',
+  path: '/xml-to-json',
+  description: 'Convert XML to JSON',
+  keywords: ['xml', 'json'],
+  component: () => import('./xml-to-json.vue'),
+  icon: Braces,
+  createdAt: new Date('2024-08-09'),
+});
diff --git a/src/tools/xml-to-json/xml-to-json.vue b/src/tools/xml-to-json/xml-to-json.vue
new file mode 100644
index 00000000..e1e5a477
--- /dev/null
+++ b/src/tools/xml-to-json/xml-to-json.vue
@@ -0,0 +1,32 @@
+
+
+
+   
diff --git a/src/utils/base64.test.ts b/src/utils/base64.test.ts
index 994f1b1b..51d15239 100644
--- a/src/utils/base64.test.ts
+++ b/src/utils/base64.test.ts
@@ -38,7 +38,8 @@ describe('base64 utils', () => {
 
     it('should throw for incorrect base64 string', () => {
       expect(() => base64ToText('a')).to.throw('Incorrect base64 string');
-      expect(() => base64ToText(' ')).to.throw('Incorrect base64 string');
+      // should not really be false because trimming of space is now implied
+      // expect(() => base64ToText(' ')).to.throw('Incorrect base64 string');
       expect(() => base64ToText('Ă©')).to.throw('Incorrect base64 string');
       // missing final '='
       expect(() => base64ToText('bG9yZW0gaXBzdW0')).to.throw('Incorrect base64 string');
@@ -56,17 +57,17 @@ describe('base64 utils', () => {
 
     it('should return false for incorrect base64 string', () => {
       expect(isValidBase64('a')).to.eql(false);
-      expect(isValidBase64(' ')).to.eql(false);
       expect(isValidBase64('Ă©')).to.eql(false);
       expect(isValidBase64('data:text/plain;notbase64,YQ==')).to.eql(false);
       // missing final '='
       expect(isValidBase64('bG9yZW0gaXBzdW0')).to.eql(false);
     });
 
-    it('should return false for untrimmed correct base64 string', () => {
-      expect(isValidBase64('bG9yZW0gaXBzdW0= ')).to.eql(false);
-      expect(isValidBase64(' LTE=')).to.eql(false);
-      expect(isValidBase64(' YQ== ')).to.eql(false);
+    it('should return true for untrimmed correct base64 string', () => {
+      expect(isValidBase64('bG9yZW0gaXBzdW0= ')).to.eql(true);
+      expect(isValidBase64(' LTE=')).to.eql(true);
+      expect(isValidBase64(' YQ== ')).to.eql(true);
+      expect(isValidBase64(' ')).to.eql(true);
     });
   });
 
diff --git a/src/utils/base64.ts b/src/utils/base64.ts
index 16912ee3..44e59f41 100644
--- a/src/utils/base64.ts
+++ b/src/utils/base64.ts
@@ -1,7 +1,9 @@
+import { Base64 } from 'js-base64';
+
 export { textToBase64, base64ToText, isValidBase64, removePotentialDataAndMimePrefix };
 
 function textToBase64(str: string, { makeUrlSafe = false }: { makeUrlSafe?: boolean } = {}) {
-  const encoded = window.btoa(str);
+  const encoded = Base64.encode(str);
   return makeUrlSafe ? makeUriSafe(encoded) : encoded;
 }
 
@@ -16,7 +18,7 @@ function base64ToText(str: string, { makeUrlSafe = false }: { makeUrlSafe?: bool
   }
 
   try {
-    return window.atob(cleanStr);
+    return Base64.decode(cleanStr);
   }
   catch (_) {
     throw new Error('Incorrect base64 string');
@@ -34,10 +36,11 @@ function isValidBase64(str: string, { makeUrlSafe = false }: { makeUrlSafe?: boo
   }
 
   try {
+    const reEncodedBase64 = Base64.fromUint8Array(Base64.toUint8Array(cleanStr));
     if (makeUrlSafe) {
-      return removePotentialPadding(window.btoa(window.atob(cleanStr))) === cleanStr;
+      return removePotentialPadding(reEncodedBase64) === cleanStr;
     }
-    return window.btoa(window.atob(cleanStr)) === cleanStr;
+    return reEncodedBase64 === cleanStr.replace(/\s/g, '');
   }
   catch (err) {
     return false;