-            | + | {{ claim }}
               
@@ -49,7 +49,7 @@ const validation = useValidation({
                 ({{ claimDescription }})- | + | {{ value }}
               
                 ({{ friendlyValue }})
diff --git a/src/tools/lorem-ipsum-generator/locales/en.yml b/src/tools/lorem-ipsum-generator/locales/en.yml
index 2a8c762a..255b73cd 100644
--- a/src/tools/lorem-ipsum-generator/locales/en.yml
+++ b/src/tools/lorem-ipsum-generator/locales/en.yml
@@ -12,3 +12,4 @@ tools:
 
     copyBtn: Copy
     copied: Lorem ipsum copied to the clipboard
+    refresh: Refresh
diff --git a/src/tools/lorem-ipsum-generator/locales/fr.yml b/src/tools/lorem-ipsum-generator/locales/fr.yml
index e591c413..6f222efd 100644
--- a/src/tools/lorem-ipsum-generator/locales/fr.yml
+++ b/src/tools/lorem-ipsum-generator/locales/fr.yml
@@ -12,3 +12,4 @@ tools:
 
     copyBtn: Copier
     copied: Lorem ipsum copié dans le presse-papiers
+    refresh: Rafraîchir
diff --git a/src/tools/lorem-ipsum-generator/locales/zh.yml b/src/tools/lorem-ipsum-generator/locales/zh.yml
index 3951709f..0f033dbf 100644
--- a/src/tools/lorem-ipsum-generator/locales/zh.yml
+++ b/src/tools/lorem-ipsum-generator/locales/zh.yml
@@ -12,3 +12,4 @@ tools:
 
     copyBtn: 复制
     copied: Lorem Ipsum 已复制到剪贴板
+    refresh: 刷新
diff --git a/src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue b/src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue
index de9096bb..560a7941 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 { t } = useI18n();
 const paragraphs = ref(1);
@@ -10,7 +11,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,
@@ -42,10 +43,13 @@ const { copy } = useCopy({ source: loremIpsumText, text: t('tools.lorem-ipsum-ge
 
     
 
- 
+     
       
         {{ t('tools.lorem-ipsum-generator.copyBtn') }}
       
+      
+        {{ t('tools.lorem-ipsum-generator.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..0160efd8
--- /dev/null
+++ b/src/tools/markdown-to-html/index.ts
@@ -0,0 +1,13 @@
+import { Markdown } from '@vicons/tabler';
+import { defineTool } from '../tool';
+import { translate as t } from '@/plugins/i18n.plugin';
+
+export const tool = defineTool({
+  name: t('tools.markdown-to-html.title'),
+  path: '/markdown-to-html',
+  description: t('tools.markdown-to-html.description'),
+  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/locales/en.yml b/src/tools/markdown-to-html/locales/en.yml
new file mode 100644
index 00000000..a8d8070d
--- /dev/null
+++ b/src/tools/markdown-to-html/locales/en.yml
@@ -0,0 +1,10 @@
+tools:
+  markdown-to-html:
+    title: Markdown to HTML
+    description: Convert Markdown to Html and allow to print (as PDF).
+
+    inputLabel: 'Your Markdown to convert:'
+    inputPlaceholder: 'Your Markdown content...'
+    outputLabel: 'Output HTML:'
+
+    printAsPDF: 'Print as PDF'
diff --git a/src/tools/markdown-to-html/locales/fr.yml b/src/tools/markdown-to-html/locales/fr.yml
new file mode 100644
index 00000000..df5410db
--- /dev/null
+++ b/src/tools/markdown-to-html/locales/fr.yml
@@ -0,0 +1,10 @@
+tools:
+  markdown-to-html:
+    title: Markdown vers HTML
+    description: Convertir Markdown en Html et permettre l'impression (en PDF).
+
+    inputLabel: 'Votre Markdown à convertir:'
+    inputPlaceholder: 'Votre contenu Markdown...'
+    outputLabel: 'HTML de sortie:'
+
+    printAsPDF: 'Imprimer en PDF'
diff --git a/src/tools/markdown-to-html/locales/zh.yml b/src/tools/markdown-to-html/locales/zh.yml
new file mode 100644
index 00000000..fc4fb8a8
--- /dev/null
+++ b/src/tools/markdown-to-html/locales/zh.yml
@@ -0,0 +1,10 @@
+tools:
+  markdown-to-html:
+    title: Markdown 转 HTML
+    description: '将 Markdown 转换为 Html 并允许打印(作为 PDF)。'
+
+    inputLabel: '要转换的 Markdown:'
+    inputPlaceholder: '您的 Markdown 内容...'
+    outputLabel: '输出 HTML:'
+
+    printAsPDF: '作为 PDF 打印'
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..9f22185c
--- /dev/null
+++ b/src/tools/markdown-to-html/markdown-to-html.vue
@@ -0,0 +1,45 @@
+
+
++ +
+     
+
+     
+
+    +      
+ 
+
+     
+      
+        {{ t('tools.markdown-to-html.printAsPDF') }}
+      
+     
+  
diff --git a/src/tools/regex-memo/index.ts b/src/tools/regex-memo/index.ts
new file mode 100644
index 00000000..b9e74e46
--- /dev/null
+++ b/src/tools/regex-memo/index.ts
@@ -0,0 +1,13 @@
+import { BrandJavascript } from '@vicons/tabler';
+import { defineTool } from '../tool';
+import { translate as t } from '@/plugins/i18n.plugin';
+
+export const tool = defineTool({
+  name: t('tools.regex-memo.title'),
+  path: '/regex-memo',
+  description: t('tools.regex-memo.description'),
+  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/locales/en.yml b/src/tools/regex-memo/locales/en.yml
new file mode 100644
index 00000000..ca44c7f8
--- /dev/null
+++ b/src/tools/regex-memo/locales/en.yml
@@ -0,0 +1,4 @@
+tools:
+  regex-memo:
+    title: Regex cheatsheet
+    description: Javascript Regex/Regular Expression cheatsheet.
diff --git a/src/tools/regex-memo/locales/fr.yml b/src/tools/regex-memo/locales/fr.yml
new file mode 100644
index 00000000..5407e639
--- /dev/null
+++ b/src/tools/regex-memo/locales/fr.yml
@@ -0,0 +1,4 @@
+tools:
+  regex-memo:
+    title: Feuille de triche Regex
+    description: Feuille de triche Javascript Regex/Expression régulière.
diff --git a/src/tools/regex-memo/locales/zh.yml b/src/tools/regex-memo/locales/zh.yml
new file mode 100644
index 00000000..a839d041
--- /dev/null
+++ b/src/tools/regex-memo/locales/zh.yml
@@ -0,0 +1,4 @@
+tools:
+  regex-memo:
+    title: 正则表达式速查表
+    description: Javascript 正则表达式速查表。
diff --git a/src/tools/regex-memo/regex-memo.content.fr.md b/src/tools/regex-memo/regex-memo.content.fr.md
new file mode 100644
index 00000000..591cc4f5
--- /dev/null
+++ b/src/tools/regex-memo/regex-memo.content.fr.md
@@ -0,0 +1,121 @@
+### Caractères normaux
+
+Expression | Description
+:--|:--
+`.` ou `[^\n\r]` | tout caractère *sauf* un saut de ligne ou retour chariot
+`[A-Za-z]` | alphabet
+`[a-z]` | alphabet minuscule
+`[A-Z]` | alphabet majuscule
+`\d` ou `[0-9]` | chiffre
+`\D` ou `[^0-9]` | non-chiffre
+`_` | soulignement
+`\w` ou `[A-Za-z0-9_]` | alphabet, chiffre ou soulignement
+`\W` ou `[^A-Za-z0-9_]` | inverse de `\w`
+`\S` | inverse de `\s`
+
+### Caractères d'espace
+
+Expression | Description
+:--|:--
+` ` | espace
+`\t` | tabulation
+`\n` | saut de ligne
+`\r` | retour chariot
+`\s` | espace, tabulation, saut de ligne ou retour chariot
+
+### Ensemble de caractères
+
+Expression | Description
+:--|:--
+`[xyz]` | soit `x`, `y` ou `z`
+`[^xyz]` | ni `x`, ni `y`, ni `z`
+`[1-3]` | soit `1`, `2` ou `3`
+`[^1-3]` | ni `1`, ni `2`, ni `3`
+
+- Pensez à un ensemble de caractères comme une opération `OU` sur les caractères simples qui sont enfermés entre crochets.
+- Utilisez `^` après le `[` d'ouverture pour "négation" de l'ensemble de caractères.
+- Dans un ensemble de caractères, `.` signifie un point littéral.
+
+### Caractères nécessitant un échappement
+
+#### En dehors d'un ensemble de caractères
+
+Expression | Description
+:--|:--
+`\.` | point
+`\^` | accent circonflexe
+`\$` | signe dollar
+`\|` | barre verticale
+`\\` | barre oblique inverse
+`\/` | barre oblique
+`\(` | parenthèse ouvrante
+`\)` | parenthèse fermante
+`\[` | crochet ouvrant
+`\]` | crochet fermant
+`\{` | accolade ouvrante
+`\}` | accolade fermante
+
+#### À l'intérieur d'un ensemble de caractères
+
+Expression | Description
+:--|:--
+`\\` | barre oblique inverse
+`\]` | crochet fermant
+
+- Un `^` doit être échappé uniquement s'il se produit immédiatement après le `[` d'ouverture de l'ensemble de caractères.
+- Un `-` doit être échappé uniquement s'il se produit entre deux alphabets ou deux chiffres.
+
+### Quantificateurs
+
+Expression | Description
+:--|:--
+`{2}` | exactement 2
+`{2,}` | au moins 2
+`{2,7}` | au moins 2 mais pas plus de 7
+`*` | 0 ou plus
+`+` | 1 ou plus
+`?` | exactement 0 ou 1
+
+- Le quantificateur va *après* l'expression à quantifier.
+
+### Limites
+
+Expression | Description
+:--|:--
+`^` | début de chaîne
+`$` | fin de chaîne
+`\b` | limite de mot
+
+- Comment fonctionne la correspondance des limites de mots :
+    - Au début de la chaîne si le premier caractère est `\w`.
+    - Entre deux caractères adjacents dans la chaîne, si le premier caractère est `\w` et le deuxième caractère est `\W`.
+    - À la fin de la chaîne si le dernier caractère est `\w`.
+
+### Correspondance
+
+Expression | Description
+:--|:--
+`foo\|bar` | correspond soit à `foo` soit à `bar`
+`foo(?=bar)` | correspond à `foo` s'il est avant `bar`
+`foo(?!bar)` | correspond à `foo` s'il n'est *pas* avant `bar`
+`(?<=bar)foo` | correspond à `foo` s'il est après `bar`
+`(?
+import { useThemeVars } from 'naive-ui';
+import Memo from './regex-memo.content.md';
+import MemoZH from './regex-memo.content.zh.md';
+import MemoFR from './regex-memo.content.fr.md';
+
+const themeVars = useThemeVars();
+const { locale } = useI18n();
+
+
++ 
+    
+    
+    
+  + 
+
+
diff --git a/src/tools/regex-tester/index.ts b/src/tools/regex-tester/index.ts
new file mode 100644
index 00000000..a8861e21
--- /dev/null
+++ b/src/tools/regex-tester/index.ts
@@ -0,0 +1,13 @@
+import { Language } from '@vicons/tabler';
+import { defineTool } from '../tool';
+import { translate as t } from '@/plugins/i18n.plugin';
+
+export const tool = defineTool({
+  name: t('tools.regex-tester.title'),
+  path: '/regex-tester',
+  description: t('tools.regex-tester.description'),
+  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/locales/en.yml b/src/tools/regex-tester/locales/en.yml
new file mode 100644
index 00000000..b0532efa
--- /dev/null
+++ b/src/tools/regex-tester/locales/en.yml
@@ -0,0 +1,36 @@
+tools:
+  regex-tester:
+    title: Regex Tester
+    description: Test your regular expressions with sample text.
+
+    inputTitle: 'Regex'
+    inputLabel: 'Regex to test:'
+    inputPlaceholder: 'Put the regex to test'
+    outputLabel: 'Text to match:'
+    outputPlaceholder: 'Put the text to match'
+
+    helpTip: 'See Regular Expression Cheatsheet'
+    globalTitle: 'Global search'
+    globalLabel: 'Global search.'
+    ignoreCaseTitle: 'Case-insensitive search'
+    ignoreCaseLabel: 'Case-insensitive search.'
+    multilineTitle: 'Allows ^ and $ to match next to newline characters.'
+    multilineLabel: 'Multiline'
+    dotAllTitle: 'Allows . to match newline characters.'
+    dotAllLabel: 'Singleline'
+    unicodeTitle: 'Unicode; treat a pattern as a sequence of Unicode code points.'
+    unicodeLabel: 'Unicode'
+    unicodeSetsTitle: 'An upgrade to the u mode with more Unicode features.'
+    unicodeSetsLabel: 'Unicode Sets'
+
+    matches: 'Matches'
+    index: 'Index in text'
+    value: 'Value'
+    captures: 'Captures'
+    groups: 'Groups'
+    noMatch: 'No match'
+
+    sampleMatchingText: 'Sample matching text'
+    regexDiagram: 'Regex Diagram'
+
+    invalidRegex: 'Invalid regex: {0}'
diff --git a/src/tools/regex-tester/locales/fr.yml b/src/tools/regex-tester/locales/fr.yml
new file mode 100644
index 00000000..683cce88
--- /dev/null
+++ b/src/tools/regex-tester/locales/fr.yml
@@ -0,0 +1,36 @@
+tools:
+  regex-tester:
+    title: Testeur Regex
+    description: Testez vos expressions régulières avec un texte d'exemple.
+
+    inputTitle: 'Regex'
+    inputLabel: 'Regex à tester:'
+    inputPlaceholder: 'Mettez le regex à tester'
+    outputLabel: 'Texte à faire correspondre:'
+    outputPlaceholder: 'Mettez le texte à faire correspondre'
+
+    helpTip: 'Voir la feuille de triche des expressions régulières'
+    globalTitle: 'Recherche globale'
+    globalLabel: 'Recherche globale.'
+    ignoreCaseTitle: 'Recherche insensible à la casse'
+    ignoreCaseLabel: 'Recherche insensible à la casse.'
+    multilineTitle: 'Permet à ^ et $ de correspondre aux caractères de nouvelle ligne.'
+    multilineLabel: 'Multiligne'
+    dotAllTitle: 'Permet à . de correspondre aux caractères de nouvelle ligne.'
+    dotAllLabel: 'Monoligne'
+    unicodeTitle: 'Unicode ; traiter un modèle comme une séquence de points de code Unicode.'
+    unicodeLabel: 'Unicode'
+    unicodeSetsTitle: 'Une mise à niveau du mode u avec plus de fonctionnalités Unicode.'
+    unicodeSetsLabel: 'Ensembles Unicode'
+
+    matches: 'Correspondances'
+    index: 'Index dans le texte'
+    value: 'Valeur'
+    captures: 'Captures'
+    groups: 'Groupes'
+    noMatch: 'Aucune correspondance'
+
+    sampleMatchingText: "Texte de correspondance d'exemple"
+    regexDiagram: 'Schéma Regex'
+
+    invalidRegex: 'Regex invalide: {0}'
diff --git a/src/tools/regex-tester/locales/zh.yml b/src/tools/regex-tester/locales/zh.yml
new file mode 100644
index 00000000..3a908909
--- /dev/null
+++ b/src/tools/regex-tester/locales/zh.yml
@@ -0,0 +1,36 @@
+tools:
+  regex-tester:
+    title: 正则表达式测试工具
+    description: 使用示例文本测试您的正则表达式。
+
+    inputTitle: '正则表达式'
+    inputLabel: '要测试的正则表达式:'
+    inputPlaceholder: '输入要测试的正则表达式'
+    outputLabel: '匹配的文本:'
+    outputPlaceholder: '输入要匹配的文本'
+
+    helpTip: '查看正则表达式速查表'
+    globalTitle: '全局搜索'
+    globalLabel: '全局搜索。'
+    ignoreCaseTitle: '不区分大小写搜索'
+    ignoreCaseLabel: '不区分大小写搜索。'
+    multilineTitle: '允许^和$匹配换行符旁边的字符。'
+    multilineLabel: '多行'
+    dotAllTitle: '允许.匹配换行符。'
+    dotAllLabel: '单行'
+    unicodeTitle: 'Unicode; 将模式视为 Unicode 代码点序列。'
+    unicodeLabel: 'Unicode'
+    unicodeSetsTitle: '具有更多 Unicode 功能的 u 模式升级版。'
+    unicodeSetsLabel: 'Unicode 集'
+
+    matches: '匹配项'
+    index: '文本中的索引'
+    value: '值'
+    captures: '捕获'
+    groups: '分组'
+    noMatch: '无匹配项'
+
+    sampleMatchingText: '示例匹配文本'
+    regexDiagram: '正则表达式图解'
+
+    invalidRegex: '无效的正则表达式:{0}'
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..f6dee686
--- /dev/null
+++ b/src/tools/regex-tester/regex-tester.vue
@@ -0,0 +1,194 @@
+
+
+
+ +
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 caa117a5..6c3a0523 100644
--- a/src/tools/url-encoder/url-encoder.vue
+++ b/src/tools/url-encoder/url-encoder.vue
@@ -25,7 +25,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..a037b703
--- /dev/null
+++ b/src/tools/xml-to-json/index.ts
@@ -0,0 +1,13 @@
+import { Braces } from '@vicons/tabler';
+import { defineTool } from '../tool';
+import { translate as t } from '@/plugins/i18n.plugin';
+
+export const tool = defineTool({
+  name: t('tools.xml-to-json.title'),
+  path: '/xml-to-json',
+  description: t('tools.xml-to-json.description'),
+  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/locales/en.yml b/src/tools/xml-to-json/locales/en.yml
new file mode 100644
index 00000000..d84d8213
--- /dev/null
+++ b/src/tools/xml-to-json/locales/en.yml
@@ -0,0 +1,10 @@
+tools:
+  xml-to-json:
+    title: XML to JSON
+    description: Convert XML to JSON.
+
+    inputLabel: Your XML content
+    inputPlaceholder: Paste your XML content here...
+    outputLabel: Converted JSON
+
+    invalidMessage: Provided XML is not valid.
diff --git a/src/tools/xml-to-json/locales/fr.yml b/src/tools/xml-to-json/locales/fr.yml
new file mode 100644
index 00000000..502490b6
--- /dev/null
+++ b/src/tools/xml-to-json/locales/fr.yml
@@ -0,0 +1,10 @@
+tools:
+  xml-to-json:
+    title: XML vers JSON
+    description: Convertir XML en JSON.
+
+    inputLabel: Votre contenu XML
+    inputPlaceholder: Collez votre contenu XML ici...
+    outputLabel: JSON converti
+
+    invalidMessage: Le XML fourni n'est pas valide.
diff --git a/src/tools/xml-to-json/locales/zh.yml b/src/tools/xml-to-json/locales/zh.yml
new file mode 100644
index 00000000..51412cd2
--- /dev/null
+++ b/src/tools/xml-to-json/locales/zh.yml
@@ -0,0 +1,10 @@
+tools:
+  xml-to-json:
+    title: XML 转 JSON
+    description: 将 XML 转换为 JSON。
+
+    inputLabel: 您的 XML 内容
+    inputPlaceholder: 在此粘贴您的 XML 内容...
+    outputLabel: 转换后的 JSON
+
+    invalidMessage: 提供的 XML 不是有效的。
\ No newline at end of file
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..9c148595
--- /dev/null
+++ b/src/tools/xml-to-json/xml-to-json.vue
@@ -0,0 +1,33 @@
+
+
+
+  
+
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;
+    +      
+      
+        {{ t('tools.regex-tester.helpTip') }}
+      
+      
+        
+          {{ t('tools.regex-tester.globalLabel') }} ( g)
+        
+        
+          {{ t('tools.regex-tester.ignoreCaseLabel') }} (i)
+        
+        
+          {{ t('tools.regex-tester.multilineLabel') }}(m)
+        
+        
+          {{ t('tools.regex-tester.dotAllLabel') }}(s)
+        
+        
+          {{ t('tools.regex-tester.unicodeLabel') }}(u)
+        
+        
+          {{ t('tools.regex-tester.unicodeSetsLabel') }} (v)
+        
+      
+
+      
+
+      
+
+
+    +      
+        
+ 
+            +        
+        
+| +              {{ t('tools.regex-tester.index') }}
++ | +              {{ t('tools.regex-tester.value') }}
++ | +              {{ t('tools.regex-tester.captures') }}
++ | +              {{ t('tools.regex-tester.groups') }}
++ |  
+            +        
+      
+      
+        {{ t('tools.regex-tester.noMatch') }}
+      
+| {{ match.index }}+ | {{ match.value }}+ | + +
+                +
+                  "{{ capture.name }}" = {{ capture.value }} [{{ capture.start }} - {{ capture.end }}]
+                + | + +
+                +
+                  "{{ group.name }}" = {{ group.value }} [{{ group.start }} - {{ group.end }}]
+                + |  
+
+    + {{ sample }}+
+
+    +      
+ 
+      
+ 
+   |