Skip to content

String helpers ​

Seven helpers that count graphemes rather than UTF-16 units, and follow the reader's language rather than English.

Open in demo
Open in demo

Import ​

ts
import { capitalize, initials, maskString, slugify, titleCase, truncate } from '@shared/utils/string'
import { stripHtml } from '@shared/utils/html'

capitalize ​

ts
function capitalize(value: string, options?: CaseOptions): string

interface CaseOptions {
  locale?: string
  lowercaseRest?: boolean
}

Uppercases the first grapheme, locale-aware.

ts
capitalize('istanbul', { locale: 'tr' }) // 'İstanbul'
capitalize('istanbul', { locale: 'en' }) // 'Istanbul'
capitalize('iPhone') // 'IPhone'

lowercaseRest is off by default, because turning it on destroys iPhone.

locale goes straight to toLocaleUpperCase, which throws RangeError: Invalid language tag on anything malformed. See localeTag.

titleCase ​

ts
function titleCase(value: string, options?: CaseOptions): string
ts
titleCase('istanbul şehri', { locale: 'tr' }) // 'İstanbul Şehri'

Boundaries are whitespace and hyphens only. An apostrophe boundary would fix o'brien and break every contraction, which is the worse trade.

slugify ​

ts
function slugify(value: string, options?: SlugifyOptions): string

interface SlugifyOptions {
  separator?: string
  ascii?: boolean
  locale?: string
}
ts
slugify('Hello, World!') // 'hello-world'
slugify('مرحبا بالعالم') // 'مرحبا-بالعالم'
slugify('Crème Brûlée', { ascii: true }) // 'creme-brulee'

It keeps letters of any script. The character class is \p{L}\p{N}; the usual [^a-z0-9] would return an empty string for the Arabic example.

ascii: true reduces to [a-z0-9] and is Latin-only — Arabic, Chinese and Hebrew reduce to an empty slug, so leave it off for anything multilingual.

Lowercasing is locale-independent by default, so a slug cannot change because the reader switched language. Uncomposable combining marks are dropped, which is right for Arabic harakat and wrong for a script whose standalone marks carry vowels.

truncate ​

ts
function truncate(value: string, maxLength: number, options?: TruncateOptions): string
ts
truncate('The quick brown fox', 10) // 'The quick…'
truncate('Family 👨‍👩‍👧‍👦 time', 9) // 'Family 👨‍👩‍👧‍👦…'

Graphemes, not code units. A ZWJ family emoji is one character to a reader but eleven UTF-16 units, so .slice() prints a replacement box. The ellipsis is inside the budget, so the result is never longer than maxLength.

The suffix defaults to '…', one character.

initials ​

ts
function initials(value: string, options?: InitialsOptions): string

interface InitialsOptions {
  max?: number
  locale?: string
}
ts
initials('Ada Lovelace') // 'AL'
initials('محمد المعراج') // 'ما'
initials('ismail kaya', { locale: 'tr' }) // 'İK'

The first grapheme of each word, so an astral character yields a whole glyph rather than half a surrogate pair. max defaults to 2.

maskString ​

ts
function maskString(value: string, options?: MaskOptions): string

interface MaskOptions {
  keepStart?: number
  keepEnd?: number
  maskChar?: string
  locale?: string
}
ts
maskString('4242424242424242') // '••••••••••••4242'
maskString('ada@example.com', { keepStart: 2, keepEnd: 4 }) // 'ad•••••••••.com'

Defaults: keepStart 0, keepEnd 4, maskChar '•'.

It always masks at least one character. keepStart and keepEnd are clamped rather than allowed to no-op — which is how a card number ends up rendered in full.

stripHtml ​

ts
function stripHtml(value: string): string
ts
import { stripHtml } from '@shared/utils/html'

Removes tags, for a plain-text preview of rich-text content.