diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 9752f3f..9a15818 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,119 +4,12 @@
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
@@ -487,7 +380,7 @@
-
+
@@ -617,15 +510,7 @@
-
-
-
-
- 1743470832619
-
-
-
- 1743470832619
+
@@ -1011,7 +896,15 @@
1752505593881
-
+
+
+ 1752512678963
+
+
+
+ 1752512678963
+
+
@@ -1058,8 +951,6 @@
-
-
@@ -1083,7 +974,9 @@
-
+
+
+
false
diff --git a/src/pages/tools/string/index.ts b/src/pages/tools/string/index.ts
index e4979fc..0ded96d 100644
--- a/src/pages/tools/string/index.ts
+++ b/src/pages/tools/string/index.ts
@@ -17,6 +17,7 @@ import { tool as stringTruncate } from './truncate/meta';
import { tool as stringBase64 } from './base64/meta';
import { tool as stringStatistic } from './statistic/meta';
import { tool as stringCensor } from './censor/meta';
+import { tool as stringPasswordGenerator } from './password-generator/meta';
export const stringTools = [
stringSplit,
@@ -37,5 +38,6 @@ export const stringTools = [
stringRot13,
stringBase64,
stringStatistic,
- stringCensor
+ stringCensor,
+ stringPasswordGenerator
];
diff --git a/src/pages/tools/string/password-generator/index.tsx b/src/pages/tools/string/password-generator/index.tsx
new file mode 100644
index 0000000..9d7f62d
--- /dev/null
+++ b/src/pages/tools/string/password-generator/index.tsx
@@ -0,0 +1,165 @@
+import React, { useState } from 'react';
+import { Box, Checkbox, FormControlLabel, FormGroup } from '@mui/material';
+import { generatePassword } from './service';
+import { initialValues, InitialValuesType } from './initialValues';
+import ToolContent from '@components/ToolContent';
+import ToolTextResult from '@components/result/ToolTextResult';
+import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
+import { ToolComponentProps } from '@tools/defineTool';
+import { GetGroupsType } from '@components/options/ToolOptions';
+import { CardExampleType } from '@components/examples/ToolExamples';
+import { useTranslation } from 'react-i18next';
+
+const exampleCards: CardExampleType[] = [
+ {
+ title: 'Strong Password (12 characters)',
+ description:
+ 'Generate a secure password with all character types including symbols.',
+ sampleText: '',
+ sampleResult: 'A7#mK9$pL2@x',
+ sampleOptions: {
+ length: '12',
+ includeLowercase: true,
+ includeUppercase: true,
+ includeNumbers: true,
+ includeSymbols: true,
+ avoidAmbiguous: false
+ }
+ },
+ {
+ title: 'Simple Password (8 characters)',
+ description: 'Generate a basic password with letters and numbers only.',
+ sampleText: '',
+ sampleResult: 'Ab3mK9pL',
+ sampleOptions: {
+ length: '8',
+ includeLowercase: true,
+ includeUppercase: true,
+ includeNumbers: true,
+ includeSymbols: false,
+ avoidAmbiguous: false
+ }
+ },
+ {
+ title: 'Clear Password (No ambiguous)',
+ description:
+ 'Generate a password without ambiguous characters (i, I, l, 0, O).',
+ sampleText: '',
+ sampleResult: 'A7#mK9$pL2@x',
+ sampleOptions: {
+ length: '12',
+ includeLowercase: true,
+ includeUppercase: true,
+ includeNumbers: true,
+ includeSymbols: true,
+ avoidAmbiguous: true
+ }
+ }
+];
+
+export default function PasswordGenerator({ title }: ToolComponentProps) {
+ const { t } = useTranslation('string');
+ const [result, setResult] = useState('');
+
+ function compute(values: InitialValuesType) {
+ setResult(generatePassword(values));
+ }
+
+ const getGroups: GetGroupsType = ({
+ values,
+ updateField
+ }) => [
+ {
+ title: t('passwordGenerator.optionsTitle'),
+ component: (
+
+ updateField('length', val)}
+ type="number"
+ />
+
+
+
+ updateField('includeLowercase', e.target.checked)
+ }
+ />
+ }
+ label={t('passwordGenerator.includeLowercase')}
+ />
+
+ updateField('includeUppercase', e.target.checked)
+ }
+ />
+ }
+ label={t('passwordGenerator.includeUppercase')}
+ />
+
+ updateField('includeNumbers', e.target.checked)
+ }
+ />
+ }
+ label={t('passwordGenerator.includeNumbers')}
+ />
+
+ updateField('includeSymbols', e.target.checked)
+ }
+ />
+ }
+ label={t('passwordGenerator.includeSymbols')}
+ />
+
+ updateField('avoidAmbiguous', e.target.checked)
+ }
+ />
+ }
+ label={t('passwordGenerator.avoidAmbiguous')}
+ />
+
+
+ )
+ }
+ ];
+
+ return (
+
+ }
+ toolInfo={{
+ title: t('passwordGenerator.toolInfo.title'),
+ description: t('passwordGenerator.toolInfo.description')
+ }}
+ exampleCards={exampleCards}
+ />
+ );
+}
diff --git a/src/pages/tools/string/password-generator/initialValues.ts b/src/pages/tools/string/password-generator/initialValues.ts
new file mode 100644
index 0000000..a96cc1b
--- /dev/null
+++ b/src/pages/tools/string/password-generator/initialValues.ts
@@ -0,0 +1,17 @@
+export type InitialValuesType = {
+ length: string; // user enters a number here
+ includeLowercase: boolean;
+ includeUppercase: boolean;
+ includeNumbers: boolean;
+ includeSymbols: boolean;
+ avoidAmbiguous: boolean;
+};
+
+export const initialValues: InitialValuesType = {
+ length: '12',
+ includeLowercase: true,
+ includeUppercase: true,
+ includeNumbers: true,
+ includeSymbols: true,
+ avoidAmbiguous: false
+};
diff --git a/src/pages/tools/string/password-generator/meta.ts b/src/pages/tools/string/password-generator/meta.ts
new file mode 100644
index 0000000..f24ffc2
--- /dev/null
+++ b/src/pages/tools/string/password-generator/meta.ts
@@ -0,0 +1,14 @@
+import { defineTool } from '@tools/defineTool';
+import { lazy } from 'react';
+
+export const tool = defineTool('string', {
+ path: 'password-generator',
+ icon: 'material-symbols:key',
+ keywords: ['password', 'generator', 'random', 'secure'],
+ component: lazy(() => import('./index')),
+ i18n: {
+ name: 'string:passwordGenerator.title',
+ description: 'string:passwordGenerator.description',
+ shortDescription: 'string:passwordGenerator.shortDescription'
+ }
+});
diff --git a/src/pages/tools/string/password-generator/password-generator.service.test.ts b/src/pages/tools/string/password-generator/password-generator.service.test.ts
new file mode 100644
index 0000000..9907452
--- /dev/null
+++ b/src/pages/tools/string/password-generator/password-generator.service.test.ts
@@ -0,0 +1,143 @@
+import { describe, expect, it } from 'vitest';
+import { generatePassword } from './service';
+import { initialValues } from './initialValues';
+
+describe('generatePassword', () => {
+ it('should generate a password with the specified length', () => {
+ const options = { ...initialValues, length: '10' };
+ const result = generatePassword(options);
+ expect(result).toHaveLength(10);
+ });
+
+ it('should return empty string for invalid length', () => {
+ const options = { ...initialValues, length: '0' };
+ const result = generatePassword(options);
+ expect(result).toBe('');
+ });
+
+ it('should return empty string for non-numeric length', () => {
+ const options = { ...initialValues, length: 'abc' };
+ const result = generatePassword(options);
+ expect(result).toBe('');
+ });
+
+ it('should return empty string when no character types are selected', () => {
+ const options = {
+ ...initialValues,
+ includeLowercase: false,
+ includeUppercase: false,
+ includeNumbers: false,
+ includeSymbols: false
+ };
+ const result = generatePassword(options);
+ expect(result).toBe('');
+ });
+
+ it('should only include lowercase letters when only lowercase is selected', () => {
+ const options = {
+ ...initialValues,
+ length: '20',
+ includeLowercase: true,
+ includeUppercase: false,
+ includeNumbers: false,
+ includeSymbols: false
+ };
+ const result = generatePassword(options);
+ expect(result).toMatch(/^[a-z]+$/);
+ expect(result).toHaveLength(20);
+ });
+
+ it('should only include uppercase letters when only uppercase is selected', () => {
+ const options = {
+ ...initialValues,
+ length: '15',
+ includeLowercase: false,
+ includeUppercase: true,
+ includeNumbers: false,
+ includeSymbols: false
+ };
+ const result = generatePassword(options);
+ expect(result).toMatch(/^[A-Z]+$/);
+ expect(result).toHaveLength(15);
+ });
+
+ it('should only include numbers when only numbers is selected', () => {
+ const options = {
+ ...initialValues,
+ length: '8',
+ includeLowercase: false,
+ includeUppercase: false,
+ includeNumbers: true,
+ includeSymbols: false
+ };
+ const result = generatePassword(options);
+ expect(result).toMatch(/^[0-9]+$/);
+ expect(result).toHaveLength(8);
+ });
+
+ it('should include mixed character types when multiple are selected', () => {
+ const options = {
+ ...initialValues,
+ length: '100', // larger sample for better testing
+ includeLowercase: true,
+ includeUppercase: true,
+ includeNumbers: true,
+ includeSymbols: false
+ };
+ const result = generatePassword(options);
+ expect(result).toMatch(/^[a-zA-Z0-9]+$/);
+ expect(result).toHaveLength(100);
+ });
+
+ it('should exclude ambiguous characters when avoidAmbiguous is true', () => {
+ const options = {
+ ...initialValues,
+ length: '50',
+ avoidAmbiguous: true
+ };
+ const result = generatePassword(options);
+ expect(result).not.toMatch(/[iIl0O]/);
+ expect(result).toHaveLength(50);
+ });
+
+ it('should include symbols when includeSymbols is true', () => {
+ const options = {
+ ...initialValues,
+ length: '30',
+ includeLowercase: false,
+ includeUppercase: false,
+ includeNumbers: false,
+ includeSymbols: true
+ };
+ const result = generatePassword(options);
+ expect(result).toMatch(/^[!@#$%^&*()_+~`|}{[\]:;?><,./-=]+$/);
+ expect(result).toHaveLength(30);
+ });
+
+ it('should exclude ambiguous characters from symbols too', () => {
+ const options = {
+ ...initialValues,
+ length: '50',
+ includeLowercase: false,
+ includeUppercase: false,
+ includeNumbers: true,
+ includeSymbols: true,
+ avoidAmbiguous: true
+ };
+ const result = generatePassword(options);
+ expect(result).not.toMatch(/[iIl0O]/);
+ expect(result).toHaveLength(50);
+ });
+
+ it('should handle edge case with very short length', () => {
+ const options = { ...initialValues, length: '1' };
+ const result = generatePassword(options);
+ expect(result).toHaveLength(1);
+ });
+
+ it('should handle negative length', () => {
+ const options = { ...initialValues, length: '-5' };
+ const result = generatePassword(options);
+ expect(result).toBe('');
+ });
+});
diff --git a/src/pages/tools/string/password-generator/service.ts b/src/pages/tools/string/password-generator/service.ts
new file mode 100644
index 0000000..fe20a6b
--- /dev/null
+++ b/src/pages/tools/string/password-generator/service.ts
@@ -0,0 +1,38 @@
+import type { InitialValuesType } from './initialValues';
+
+export function generatePassword(options: InitialValuesType): string {
+ const length = parseInt(options.length || '', 10);
+ if (isNaN(length) || length <= 0) {
+ return '';
+ }
+
+ let charset = '';
+ const lower = 'abcdefghijklmnopqrstuvwxyz';
+ const upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+ const numbers = '0123456789';
+ const symbols = '!@#$%^&*()_+~`|}{[]:;?><,./-=';
+
+ if (options.includeLowercase) charset += lower;
+ if (options.includeUppercase) charset += upper;
+ if (options.includeNumbers) charset += numbers;
+ if (options.includeSymbols) charset += symbols;
+
+ if (options.avoidAmbiguous) {
+ // ambiguous set = i, I, l, 0, O
+ const ambig = new Set(['i', 'I', 'l', '0', 'O']);
+ charset = Array.from(charset)
+ .filter((c) => !ambig.has(c))
+ .join('');
+ }
+
+ if (!charset) {
+ return ''; // nothing to pick from
+ }
+
+ let pwd = '';
+ for (let i = 0; i < length; i++) {
+ const idx = Math.floor(Math.random() * charset.length);
+ pwd += charset[idx];
+ }
+ return pwd;
+}
diff --git a/src/tools/defineTool.tsx b/src/tools/defineTool.tsx
index b1e36a0..48692e1 100644
--- a/src/tools/defineTool.tsx
+++ b/src/tools/defineTool.tsx
@@ -2,7 +2,7 @@ import ToolLayout from '../components/ToolLayout';
import React, { JSXElementConstructor, LazyExoticComponent } from 'react';
import { IconifyIcon } from '@iconify/react';
import { FullI18nKey } from '../i18n';
-import { ParseKeys } from 'i18next';
+import { useTranslation } from 'react-i18next';
export interface ToolMeta {
path: string;
@@ -62,10 +62,16 @@ export const defineTool = (
description: i18n.description,
shortDescription: i18n.shortDescription,
keywords,
- component: () => {
+ component: function ToolComponent() {
+ const { t } = useTranslation();
return (
-
+
);
}