From 3c677bf8466cdc2f024d6734f8775382b9001a33 Mon Sep 17 00:00:00 2001 From: Bhavesh Kshatriya Date: Tue, 3 Mar 2026 01:30:30 +0530 Subject: [PATCH 1/7] feat: add slug generator tool - Implement slug generation logic in service.ts - Create UI component using shared ToolContent - Add unit tests for various edge cases - Add translations in string.json - Register the tool in the string category --- public/locales/en/string.json | 6 +++ src/pages/tools/string/index.ts | 4 +- .../tools/string/slug-generator/index.tsx | 49 +++++++++++++++++++ src/pages/tools/string/slug-generator/meta.ts | 15 ++++++ .../tools/string/slug-generator/service.ts | 10 ++++ .../slug-generator.service.test.ts | 28 +++++++++++ .../tools/string/slug-generator/types.ts | 1 + 7 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 src/pages/tools/string/slug-generator/index.tsx create mode 100644 src/pages/tools/string/slug-generator/meta.ts create mode 100644 src/pages/tools/string/slug-generator/service.ts create mode 100644 src/pages/tools/string/slug-generator/slug-generator.service.test.ts create mode 100644 src/pages/tools/string/slug-generator/types.ts diff --git a/public/locales/en/string.json b/public/locales/en/string.json index deb649a..e9ba540 100644 --- a/public/locales/en/string.json +++ b/public/locales/en/string.json @@ -323,5 +323,11 @@ "title": "Unicode Encoder / Decoder", "description": "This tool lets you convert plain text into Unicode escape sequences (e.g., \\uXXXX) and decode Unicode escape sequences back into standard text. You can also choose whether the hexadecimal output is formatted in uppercase or lowercase when encoding." } + }, + "slugGenerator": { + "title": "Slug generator", + "description": "Convert any text into a URL-friendly slug.", + "shortDescription": "Generate URL-friendly slugs from text.", + "longDescription": "This tool helps you create SEO-friendly slugs for URLs by converting text to lowercase, removing special characters, and replacing spaces with dashes. It's perfect for blog post titles, product names, or any other content that needs a clean, readable URL." } } diff --git a/src/pages/tools/string/index.ts b/src/pages/tools/string/index.ts index ac067c9..56ed239 100644 --- a/src/pages/tools/string/index.ts +++ b/src/pages/tools/string/index.ts @@ -1,3 +1,4 @@ +import { tool as stringSlugGenerator } from './slug-generator/meta'; import { tool as stringHiddenCharacterDetector } from './hidden-character-detector/meta'; import { tool as stringRemoveDuplicateLines } from './remove-duplicate-lines/meta'; import { tool as stringRotate } from './rotate/meta'; @@ -47,5 +48,6 @@ export const stringTools = [ stringEncodeUrl, StringDecodeUrl, stringUnicode, - stringHiddenCharacterDetector + stringHiddenCharacterDetector, + stringSlugGenerator ]; diff --git a/src/pages/tools/string/slug-generator/index.tsx b/src/pages/tools/string/slug-generator/index.tsx new file mode 100644 index 0000000..8d72165 --- /dev/null +++ b/src/pages/tools/string/slug-generator/index.tsx @@ -0,0 +1,49 @@ +import { Box } from '@mui/material'; +import React, { useState } from 'react'; +import ToolContent from '@components/ToolContent'; +import { ToolComponentProps } from '@tools/defineTool'; +import ToolTextInput from '@components/input/ToolTextInput'; +import ToolTextResult from '@components/result/ToolTextResult'; +import { GetGroupsType } from '@components/options/ToolOptions'; +import { CardExampleType } from '@components/examples/ToolExamples'; +import { main } from './service'; +import { InitialValuesType } from './types'; + +const initialValues: InitialValuesType = {}; + +const exampleCards: CardExampleType[] = [ + { + title: 'Generate a Slug', + description: 'Convert a title into a URL-friendly slug', + sampleText: 'Hello World! This is a test.', + sampleResult: 'hello-world-this-is-a-test', + sampleOptions: {} + } +]; +export default function SlugGenerator({ + title, + longDescription +}: ToolComponentProps) { + const [input, setInput] = useState(''); + const [result, setResult] = useState(''); + + const compute = (values: InitialValuesType, input: string) => { + setResult(main(input, values)); + }; + + const getGroups: GetGroupsType | null = null; + return ( + } + resultComponent={} + initialValues={initialValues} + exampleCards={exampleCards} + getGroups={getGroups} + setInput={setInput} + compute={compute} + toolInfo={{ title: `What is a ${title}?`, description: longDescription }} + /> + ); +} diff --git a/src/pages/tools/string/slug-generator/meta.ts b/src/pages/tools/string/slug-generator/meta.ts new file mode 100644 index 0000000..b9f3ed0 --- /dev/null +++ b/src/pages/tools/string/slug-generator/meta.ts @@ -0,0 +1,15 @@ +import { defineTool } from '@tools/defineTool'; +import { lazy } from 'react'; + +export const tool = defineTool('string', { + i18n: { + name: 'string:slugGenerator.title', + description: 'string:slugGenerator.description', + shortDescription: 'string:slugGenerator.shortDescription', + longDescription: 'string:slugGenerator.longDescription' + }, + path: 'slug-generator', + icon: 'solar:link-bold-duotone', + keywords: ['slug', 'generator', 'url', 'seo'], + component: lazy(() => import('./index')) +}); diff --git a/src/pages/tools/string/slug-generator/service.ts b/src/pages/tools/string/slug-generator/service.ts new file mode 100644 index 0000000..0e7a009 --- /dev/null +++ b/src/pages/tools/string/slug-generator/service.ts @@ -0,0 +1,10 @@ +import { InitialValuesType } from './types'; + +export function main(input: string, options: InitialValuesType): string { + return input + .toLowerCase() + .trim() + .replace(/[^\w\s-]/g, '') + .replace(/[\s_-]+/g, '-') + .replace(/^-+|-+$/g, ''); +} diff --git a/src/pages/tools/string/slug-generator/slug-generator.service.test.ts b/src/pages/tools/string/slug-generator/slug-generator.service.test.ts new file mode 100644 index 0000000..aaa3606 --- /dev/null +++ b/src/pages/tools/string/slug-generator/slug-generator.service.test.ts @@ -0,0 +1,28 @@ +import { expect, describe, it } from 'vitest'; +import { main } from './service'; + +describe('slug-generator', () => { + it('should convert to lowercase', () => { + expect(main('Hello World', {})).toBe('hello-world'); + }); + + it('should replace spaces with dashes', () => { + expect(main('hello world', {})).toBe('hello-world'); + }); + + it('should remove special characters', () => { + expect(main('hello world!', {})).toBe('hello-world'); + }); + + it('should handle multiple spaces and dashes', () => { + expect(main('hello world--test', {})).toBe('hello-world-test'); + }); + + it('should trim dashes from ends', () => { + expect(main(' hello world ', {})).toBe('hello-world'); + }); + + it('should handle non-alphanumeric characters', () => { + expect(main('Hello World @2023!', {})).toBe('hello-world-2023'); + }); +}); diff --git a/src/pages/tools/string/slug-generator/types.ts b/src/pages/tools/string/slug-generator/types.ts new file mode 100644 index 0000000..9325310 --- /dev/null +++ b/src/pages/tools/string/slug-generator/types.ts @@ -0,0 +1 @@ +export type InitialValuesType = Record; From 53a7a7202995b48cd3bc4c95c8b83f4c04e48f3f Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Tue, 7 Apr 2026 13:45:39 +0200 Subject: [PATCH 2/7] chore: case handling added to types.ts file --- src/pages/tools/string/slug-generator/types.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/tools/string/slug-generator/types.ts b/src/pages/tools/string/slug-generator/types.ts index 9325310..e0d1d31 100644 --- a/src/pages/tools/string/slug-generator/types.ts +++ b/src/pages/tools/string/slug-generator/types.ts @@ -1 +1,3 @@ -export type InitialValuesType = Record; +export type InitialValuesType = { + caseSensitive: boolean; +}; From 34d154108d6473fc3fe65b145be5edffabe92310 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Tue, 7 Apr 2026 13:47:52 +0200 Subject: [PATCH 3/7] chore: userTypes added to meta.ts --- src/pages/tools/string/slug-generator/meta.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/tools/string/slug-generator/meta.ts b/src/pages/tools/string/slug-generator/meta.ts index b9f3ed0..368840d 100644 --- a/src/pages/tools/string/slug-generator/meta.ts +++ b/src/pages/tools/string/slug-generator/meta.ts @@ -6,7 +6,8 @@ export const tool = defineTool('string', { name: 'string:slugGenerator.title', description: 'string:slugGenerator.description', shortDescription: 'string:slugGenerator.shortDescription', - longDescription: 'string:slugGenerator.longDescription' + longDescription: 'string:slugGenerator.longDescription', + userTypes: ['developers', 'generalUsers'] }, path: 'slug-generator', icon: 'solar:link-bold-duotone', From c8892b917dac3e2900f50607994045daedcc3a15 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Tue, 7 Apr 2026 13:50:51 +0200 Subject: [PATCH 4/7] chore: update service - add multi-line processing - add accented characters handling --- .../tools/string/slug-generator/service.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/pages/tools/string/slug-generator/service.ts b/src/pages/tools/string/slug-generator/service.ts index 0e7a009..8106cc0 100644 --- a/src/pages/tools/string/slug-generator/service.ts +++ b/src/pages/tools/string/slug-generator/service.ts @@ -1,10 +1,21 @@ import { InitialValuesType } from './types'; -export function main(input: string, options: InitialValuesType): string { - return input - .toLowerCase() +function compute(input: string, options: InitialValuesType): string { + return (!options.caseSensitive ? input.toLowerCase() : input) + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') .trim() .replace(/[^\w\s-]/g, '') .replace(/[\s_-]+/g, '-') .replace(/^-+|-+$/g, ''); } + +export function slugGenerator( + input: string, + options: InitialValuesType +): string { + return input + .split('\n') + .map((line) => (line.trim() === '' ? '' : compute(line, options))) + .join('\n'); +} From 527d2f46613c727a9883b33667c35e49977eaf1b Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Tue, 7 Apr 2026 13:51:38 +0200 Subject: [PATCH 5/7] chore: rewritten test case (service improvements) --- .../slug-generator.service.test.ts | 62 ++++++++++++++++--- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/src/pages/tools/string/slug-generator/slug-generator.service.test.ts b/src/pages/tools/string/slug-generator/slug-generator.service.test.ts index aaa3606..9c0e49b 100644 --- a/src/pages/tools/string/slug-generator/slug-generator.service.test.ts +++ b/src/pages/tools/string/slug-generator/slug-generator.service.test.ts @@ -1,28 +1,70 @@ import { expect, describe, it } from 'vitest'; -import { main } from './service'; +import { slugGenerator } from './service'; describe('slug-generator', () => { + const defaultOptions = { caseSensitive: false }; + const caseSensitiveOptions = { caseSensitive: true }; + + // Basic conversion it('should convert to lowercase', () => { - expect(main('Hello World', {})).toBe('hello-world'); + expect(slugGenerator('Hello World', defaultOptions)).toBe('hello-world'); }); + it('should preserve casing when caseSensitive is true', () => { + expect(slugGenerator('Hello World', caseSensitiveOptions)).toBe( + 'Hello-World' + ); + }); + + // Spaces and dashes it('should replace spaces with dashes', () => { - expect(main('hello world', {})).toBe('hello-world'); - }); - - it('should remove special characters', () => { - expect(main('hello world!', {})).toBe('hello-world'); + expect(slugGenerator('hello world', defaultOptions)).toBe('hello-world'); }); it('should handle multiple spaces and dashes', () => { - expect(main('hello world--test', {})).toBe('hello-world-test'); + expect(slugGenerator('hello world--test', defaultOptions)).toBe( + 'hello-world-test' + ); }); it('should trim dashes from ends', () => { - expect(main(' hello world ', {})).toBe('hello-world'); + expect(slugGenerator(' hello world ', defaultOptions)).toBe( + 'hello-world' + ); + }); + + // Special characters + it('should remove special characters', () => { + expect(slugGenerator('hello world!', defaultOptions)).toBe('hello-world'); }); it('should handle non-alphanumeric characters', () => { - expect(main('Hello World @2023!', {})).toBe('hello-world-2023'); + expect(slugGenerator('Hello World @2023!', defaultOptions)).toBe( + 'hello-world-2023' + ); + }); + + // Accents + it('should normalize accented characters', () => { + expect(slugGenerator('Héllo Wörld', defaultOptions)).toBe('hello-world'); + }); + + it('should normalize accents while preserving case when caseSensitive is true', () => { + expect(slugGenerator('Héllo Wörld', caseSensitiveOptions)).toBe( + 'Hello-World' + ); + }); + + // Multi-line + it('should handle multiple lines', () => { + expect(slugGenerator('Hello World\nFoo Bar', defaultOptions)).toBe( + 'hello-world\nfoo-bar' + ); + }); + + it('should preserve empty lines', () => { + expect(slugGenerator('Hello World\n\nFoo Bar', defaultOptions)).toBe( + 'hello-world\n\nfoo-bar' + ); }); }); From 3938f2d5255a97f7271a80d06be910bc437bf1bb Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Tue, 7 Apr 2026 14:05:45 +0200 Subject: [PATCH 6/7] chore: index file updated - full i8n support - Checkbox for case handling - examples updated --- .../tools/string/slug-generator/index.tsx | 67 ++++++++++++++++--- 1 file changed, 58 insertions(+), 9 deletions(-) diff --git a/src/pages/tools/string/slug-generator/index.tsx b/src/pages/tools/string/slug-generator/index.tsx index 8d72165..96f01b0 100644 --- a/src/pages/tools/string/slug-generator/index.tsx +++ b/src/pages/tools/string/slug-generator/index.tsx @@ -1,43 +1,92 @@ -import { Box } from '@mui/material'; import React, { useState } from 'react'; import ToolContent from '@components/ToolContent'; import { ToolComponentProps } from '@tools/defineTool'; import ToolTextInput from '@components/input/ToolTextInput'; import ToolTextResult from '@components/result/ToolTextResult'; +import CheckboxWithDesc from '@components/options/CheckboxWithDesc'; import { GetGroupsType } from '@components/options/ToolOptions'; import { CardExampleType } from '@components/examples/ToolExamples'; -import { main } from './service'; +import { slugGenerator } from './service'; import { InitialValuesType } from './types'; +import { useTranslation } from 'react-i18next'; -const initialValues: InitialValuesType = {}; +const initialValues: InitialValuesType = { + caseSensitive: false +}; const exampleCards: CardExampleType[] = [ { - title: 'Generate a Slug', + title: 'Slug (lowercase)', description: 'Convert a title into a URL-friendly slug', sampleText: 'Hello World! This is a test.', sampleResult: 'hello-world-this-is-a-test', - sampleOptions: {} + sampleOptions: { + caseSensitive: false + } + }, + { + title: 'Slug (case preserved)', + description: + 'Convert a title into a URL-friendly slug without any case change', + sampleText: 'Hello World! This is a test.', + sampleResult: 'Hello-World-This-is-a-test', + sampleOptions: { + caseSensitive: true + } + }, + { + title: 'Slugs (multi lines processing)', + description: 'Convert titles on each lines into a URL-friendly slugs', + sampleText: 'Hello World! This is a test.\nMultiples lines are processed', + sampleResult: 'Hello-World-This-is-a-test\nMultiples-lines-are-processed', + sampleOptions: { + caseSensitive: true + } } ]; export default function SlugGenerator({ title, longDescription }: ToolComponentProps) { + const { t } = useTranslation('string'); const [input, setInput] = useState(''); const [result, setResult] = useState(''); const compute = (values: InitialValuesType, input: string) => { - setResult(main(input, values)); + setResult(slugGenerator(input, values)); }; - const getGroups: GetGroupsType | null = null; + const getGroups: GetGroupsType = ({ + values, + updateField + }) => [ + { + title: t('slugGenerator.options.title'), + component: [ + updateField('caseSensitive', val)} + /> + ] + } + ]; return ( } - resultComponent={} + inputComponent={ + + } + resultComponent={ + + } initialValues={initialValues} exampleCards={exampleCards} getGroups={getGroups} From 0880226d4db4910853ce83278f2d139bbde94dcf Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Tue, 7 Apr 2026 14:10:22 +0200 Subject: [PATCH 7/7] locales: full i8n support --- public/locales/en/string.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/public/locales/en/string.json b/public/locales/en/string.json index e9ba540..464f25f 100644 --- a/public/locales/en/string.json +++ b/public/locales/en/string.json @@ -325,9 +325,16 @@ } }, "slugGenerator": { - "title": "Slug generator", + "title": "Slug Generator", "description": "Convert any text into a URL-friendly slug.", "shortDescription": "Generate URL-friendly slugs from text.", - "longDescription": "This tool helps you create SEO-friendly slugs for URLs by converting text to lowercase, removing special characters, and replacing spaces with dashes. It's perfect for blog post titles, product names, or any other content that needs a clean, readable URL." + "longDescription": "Convert any text into clean, SEO-friendly slugs for use in URLs. The tool handles spaces, special characters, accented letters, and multiple lines at once — making it easy to batch-process blog post titles, product names, or page headings into consistent, readable URL segments.", + "inputTitle": "Input Text", + "resultTitle": "URL Slugs", + "options": { + "title": "Slug Options", + "caseLabel": "Case Sensitive", + "caseDescription": "Preserve the original casing of the input instead of converting to lowercase" + } } }