@pinooxhq/slug
v0.2.0
Published
Persian-to-Finglish transliteration and URL slugs for Vue, React, Svelte, and vanilla JS.
Readme
English | فارسی
@pinooxhq/slug
Persian → Finglish, then a URL-safe slug. Works in vanilla JS, Vue, React, and Svelte. Zero runtime dependencies.
Unknown Persian words are converted with a heja (syllable) engine. Common CMS and tech terms are mapped first (محصولات → product, لپتاپ → laptop). The dictionary can be turned off per call.
import { slugify, toFinglish } from '@pinooxhq/slug'
toFinglish('سلام دنیا') // 'salam donya'
slugify('سلام دنیا') // 'salam-donya'
slugify('محصولات جدید') // 'product-jadid'
slugify('محصولات جدید', { dictionary: false }) // heja only
slugify('لپتاپ گیمینگ') // 'laptop-gaming'Contents
Install
npm i @pinooxhq/slugPeer dependencies vue, react, and svelte are optional. Install only the one you use, and import from @pinooxhq/slug/vue, /react, or /svelte.
How it works
Final shape:
{prefix}-{body}-{suffix}-{hash}Pipeline:
- Normalize Persian letters (
ي/ك/ة, diacritics, ZWNJ) - Optional
customReplacementsand symbol maps (&→and) - Tokenize on spaces and punctuation (
سلام، دنیا!) - Dictionary lookup (longest token first) — skip if
dictionary: false - Heja transliteration for anything left
- Kebab-case / strict URL filter
- Prefix, suffix, then hash
maxLengthtruncates the body and reserves room for affixes and hash
Matching is token-based. Short keys do not match inside longer words: موسسه is not mouse, شاپور is not shop.
Quick start
import { slugify, createSlugify } from '@pinooxhq/slug'
slugify('کتابخانه') // 'ketabkhane'
slugify('سلام دنیا', { replacement: '_' }) // 'salam_donya'
slugify('محصولات جدید', {
prefix: 'shop',
suffix: 'fa',
hash: 42, // stable seed (id / sku) — prefer this over hash: true
hashLength: 6,
})
// 'shop-product-jadid-fa-xxxxxx'
const shopSlug = createSlugify({ prefix: 'shop', hashLength: 8 })
shopSlug('محصولات جدید', { hash: productId })Vanilla JS
ESM:
import { toFinglish, slugify, sanitizeSlug, createSlugify } from '@pinooxhq/slug'
slugify('کتابخانه') // 'ketabkhane'
sanitizeSlug('Lap--Top!') // 'laptop'CommonJS:
const { slugify } = require('@pinooxhq/slug')Script tag (IIFE):
<script src="https://unpkg.com/@pinooxhq/slug/dist/pinoox-slug.global.js"></script>
<script>
PinooxSlug.slugify('سلام دنیا')
</script>CDN aliases: unpkg and jsdelivr both point at dist/pinoox-slug.global.js.
Vue
import { reactive } from 'vue'
import { useSlugField } from '@pinooxhq/slug/vue'
const form = reactive({ title: '', slug: '', slugManual: false })
const {
onTitleInput,
onSlugInput,
resolveSlug,
resetSlugManual,
enableSlugManual,
} = useSlugField(form, {
slugify: { prefix: 'shop' },
})
onTitleInput(form.title) // fills form.slug unless the user edited it
onSlugInput(rawValue) // sanitizes while typing
const slug = resolveSlug(form.title)| Helper | Role |
| --- | --- |
| onTitleInput(value) | Updates form.slug from the title while not in manual mode |
| onSlugInput(value) | Marks the field manual and runs sanitizeSlug |
| resolveSlug(title) | form.slug or a generated slug |
| enableSlugManual() / resetSlugManual() | Lock / unlock auto-fill |
| slugTouched | Vue ref — true after the user typed in the slug |
Pass any slugify options through useSlugField(form, { slugify: { … } }).
React
import { useState } from 'react'
import { useSlugField } from '@pinooxhq/slug/react'
function ProductForm() {
const [title, setTitle] = useState('')
const { slug, onSlugInput, resetManual, manual } = useSlugField(title, {
slugify: { prefix: 'shop', preserveTrailingDash: true },
})
return (
<>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
<input dir="ltr" value={slug} onChange={(e) => onSlugInput(e.target.value)} />
{manual && <button type="button" onClick={resetManual}>Auto slug</button>}
</>
)
}| Return | Role |
| --- | --- |
| slug / setSlug | Current slug string |
| manual | User has edited the slug |
| onSlugInput(value) | Sanitize + switch to manual |
| resetManual() / enableManual() | Follow the title again / lock |
Svelte
import { writable } from 'svelte/store'
import { slugField, derivedSlug } from '@pinooxhq/slug/svelte'
const title = writable('')
const { slug, onSlugInput, resetManual, manual } = slugField(title, {
slugify: { prefix: 'shop' },
})
// one-way: always generated, no manual editing
const auto = derivedSlug(title, { slugify: { dictionary: false } })slug and manual are Svelte writable stores.
API
slugify(text, options?)
Runs toFinglish, then builds a URL slug.
slugify('محصولات جدید', { dictionary: false })
slugify('سلام، دنیا!') // 'salam-donya'
slugify('') // ''
slugify(null) // ''Options
| Option | Default | Meaning |
| --- | --- | --- |
| dictionary | true | true = CMS + tech maps; false = heja only; object = extra entries merged for this call |
| prefix | — | Sanitized prefix (Shop → shop) |
| suffix / postfix | — | Sanitized suffix (literal extra such as v2 or fa) |
| hash | — | true hashes the title (changes if the title changes); string/number is a stable seed |
| hashLength | 6 | Hash length, [a-z0-9], minimum 2 |
| replacement / separator | '-' | Word separator (separator is an alias) |
| lower | true | Lowercase the result |
| strict | true | Keep [a-z0-9] plus the separator. Persian letters stay if transliterate: false |
| trim | true | Strip leading/trailing separators |
| maxLength | — | Truncate on a separator; prefix/suffix/hash are reserved and not cut |
| stopwords | — | true drops و از به در را که با; or pass a custom list |
| symbols | true | & → and, % → percent, + → plus, ♥/❤ → love |
| decamelize | true | fooBar → foo-bar |
| transliterate | true | false keeps Persian letters in the slug |
| preserveTrailingDash | false | Keep a trailing separator while typing |
| customReplacements | — | [['@', ' at ']] applied before conversion |
| remove | — | Extra RegExp stripped before separators |
slugify('fooBar') // 'foo-bar'
slugify('fooBar', { decamelize: false }) // 'foobar'
slugify('Dogs & Cats') // 'dogs-and-cats'
slugify('سلام و دنیا', { stopwords: true }) // 'salam-donya'
slugify('سلام دنیا', { maxLength: 8 }) // 'salam'
slugify('سلام دنیا', { transliterate: false }) // 'سلام-دنیا'
slugify('Foo@site', { customReplacements: [['@', ' at ']] })
// 'foo-at-site'Prefix, suffix, hash
Order is always {prefix}-{body}-{suffix}-{hash}.
slugify('محصولات جدید', { prefix: 'shop', suffix: 'fa' })
// 'shop-product-jadid-fa'
slugify('محصولات', { hash: 42, hashLength: 6 })
// 'product-xxxxxx' (same seed → same hash)
slugify('سلام دنیا', { hash: true, hashLength: 8 })
// changes if the title changes — not ideal for CMS permalinksUse hash: product.id (or SKU) so the URL stays stable when the title is edited. Use suffix when you already have a literal tail (v2, fa). postfix is an alias of suffix.
createSlugify(defaults)
Returns a function with frozen defaults. Per-call options override them. Dictionary objects are merged. Nothing is stored globally — safer for SSR and multiple apps on one page.
const shopSlug = createSlugify({
prefix: 'shop',
hashLength: 8,
dictionary: { پینوکس: 'pinoox' },
})
shopSlug('پینوکس محصولات', { hash: productId })
shopSlug('محصولات جدید', { dictionary: false })slugifyWithCounter(defaults?)
Repeating the same output appends -2, -3, … (heading ids). Call .reset() to start over.
import { slugifyWithCounter } from '@pinooxhq/slug'
const unique = slugifyWithCounter()
unique('Example') // 'example'
unique('Example') // 'example-2'
unique.reset()
unique('Example') // 'example'toFinglish(text, options?) / toPinglish(text)
Romanize Persian to Finglish (spaces, not dashes). Latin is left as-is. Accepts dictionary, stopwords, symbols, decamelize, customReplacements, and transliterate. toPinglish is an alias.
toFinglish('سلام دنیا') // 'salam donya'
toFinglish('محصولات', { dictionary: false }) // heja, not 'product'sanitizeSlug(text, options?)
For a slug input the user types by hand. Spaces and punctuation are dropped, not turned into dashes.
sanitizeSlug('Laptop Gamer!') // 'laptopgamer'
sanitizeSlug('lap--top') // 'lap-top'
sanitizeSlug('lap-top-', { preserveTrailingDash: true }) // 'lap-top-'Dictionary
Three layers run before heja:
| Layer | Role | Example |
| --- | --- | --- |
| CMS | Semantic URL words | محصولات → product |
| Tech loanwords | English written in Persian | لپتاپ → laptop |
| extendWords | Finglish spelling override only | تهران → tehran |
Keys are matched as whole tokens after normalizing ی/ک and stripping ZWNJ. Multi-word keys (سبد خرید, ثبت نام) win over shorter ones.
Import the maps if you need to inspect or copy them:
import { DEFAULT_CMS_DICTIONARY, DEFAULT_LOANWORDS, PERSIAN_STOPWORDS } from '@pinooxhq/slug'Disable or extend
slugify('محصولات جدید', { dictionary: false })
slugify('پینوکس شاپ', { dictionary: { پینوکس: 'pinoox' } }) // merge for this callGlobal helpers (shared by every caller — prefer createSlugify in apps):
import {
extendDictionary,
resetDictionary,
extendLoanwords,
resetLoanwords,
extendWords,
resetWords,
} from '@pinooxhq/slug'
extendDictionary({ 'برند ویژه': 'label' })
extendLoanwords({ پینوکس: 'pinoox' })
extendWords({ تهران: 'tehran' })extendLoanwords is kept for compatibility with 0.1.x. New code should use dictionary on createSlugify / slugify.
CMS map
| Persian | Slug |
| --- | --- |
| محصول، محصولات | product |
| دسته، دسته بندی، دستهبندی | category |
| مقاله، مقالات | article |
| نوشته، پست | post |
| برگه، صفحه | page |
| فروشگاه | shop |
| کاربر، کاربران | user |
| سفارش | order |
| پرداخت | payment |
| سبد، سبد خرید | cart |
| تخفیف | discount |
| بلاگ، وبلاگ | blog |
| خبر، اخبار | news |
| ورود | login |
| ثبت نام، ثبتنام | register |
| پروفایل | profile |
| تنظیمات | settings |
| جستجو | search |
| تماس | contact |
| خانه، صفحه اصلی، صفحه نخست | home |
| درباره ما | about |
| برند | brand |
| قیمت | price |
| گالری | gallery |
| ویدیو، فیلم | video |
| تصویر، عکس | image |
| فایل | file |
| دانلود | download |
| ادمین، مدیریت | admin |
Tech loanwords
| Persian | Slug |
| --- | --- |
| لپتاپ، لپ تاپ، لپتاپ | laptop |
| موبایل | mobile |
| تبلت | tablet |
| هدفون | headphone |
| هندزفری | handsfree |
| شارژر | charger |
| کیبورد | keyboard |
| ماوس، موس | mouse |
| مانیتور | monitor |
| اسپیکر | speaker |
| پرینتر | printer |
| اسکنر | scanner |
| گیمینگ | gaming |
| شاپ | shop |
| کنسول | console |
| ایرپاد | airpod |
| سامسونگ | samsung |
| شیائومی | xiaomi |
| پلی استیشن، پلیاستیشن | playstation |
| ایکس باکس، ایکسباکس | xbox |
| بلوتوث | bluetooth |
| وای فای، وایفای، وایفای | wifi |
| کامپیوتر | computer |
TypeScript
Types ship in the package:
import type {
SlugifyOptions,
SlugifyFn,
DictionaryMap,
DictionaryOption,
WordMap,
LoanwordMap,
SanitizeSlugOptions,
} from '@pinooxhq/slug'Framework entry points export their own option/return types (UseSlugFieldOptions, SlugFieldStores, …).
Notes
- Empty,
null, andundefinedinputs become''. hash: trueis derived from the current title. For permalinks, pass a stable id.- Global
extend*maps are process-wide. UsecreateSlugify({ dictionary })when more than one app or test file shares the process. - Heja still guesses short vowels for words that are not in the dictionary; that is expected.
See CHANGELOG.md for 0.2.0.
License
MIT
