Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions docs/pages/formatters.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ formatPercentage(0.1234, { decimals: 1 }) // '12.3%'

---

### `formatDurationLabels(seconds: number, options?: { display?: 'short' | 'long', round?: boolean, decimals?: number }): string`
### `formatDurationLabels(seconds: number, options?: { display?: 'short' | 'long', round?: boolean, decimals?: number, locale?: string }): string`

Format time into a human-readable string.

Expand All @@ -166,6 +166,7 @@ Format time into a human-readable string.
- `unitDisplay` / `labels` ('short' | 'long', optional): Aliases for `display`
- `round` (boolean, optional): Round to largest unit
- `decimals` (number, optional): Decimal places for rounding
- `locale` (string, optional): BCP 47 locale for unit labels. Defaults to `'en-US'`

**Returns:** Human-readable duration string

Expand All @@ -176,6 +177,7 @@ import { formatDurationLabels } from 'usemods'
formatDurationLabels(3661) // '1 hour 1 minute 1 second'
formatDurationLabels(3661, { display: 'short' }) // '1 hr 1 min 1 sec'
formatDurationLabels(3661, { round: true }) // '1 hour'
formatDurationLabels(3661, { locale: 'de-DE' }) // '1 Stunde 1 Minute 1 Sekunde'
```

---
Expand Down Expand Up @@ -362,15 +364,16 @@ formatUnixTime(1609459200000) // '2021-01-01 00:00:00'

---

### `formatList(items: string | object | string[], options?: { limit?: number, conjunction?: string }): string`
### `formatList(items: string | object | string[], options?: { limit?: number, conjunction?: string, locale?: string }): string`

Create a string of comma-separated values from an array, object, or string with an optional limit and conjunction.
Create a string of comma-separated values from an array, object, or string with an optional limit and conjunction. Full lists use `Intl.ListFormat`; truncation (`limit` / "N more") stays custom.

**Parameters:**
- `items` (string | object | string[]): Items to format
- `options` (object, optional):
- `limit` (number, optional): Maximum items to show
- `conjunction` (string, optional): Conjunction word. Defaults to `'and'`
- `conjunction` (string, optional): Conjunction word. Defaults to `'and'` (`'or'` uses ListFormat disjunction)
- `locale` (string, optional): BCP 47 locale for ListFormat. Defaults to `'en-GB'` (no Oxford comma)

**Returns:** Formatted list string

Expand All @@ -381,6 +384,7 @@ import { formatList } from 'usemods'
formatList(['apple', 'banana', 'cherry']) // 'apple, banana and cherry'
formatList(['apple', 'banana', 'cherry'], { limit: 2 }) // 'apple, banana and 1 more'
formatList('apple, banana, cherry') // 'apple, banana and cherry'
formatList(['apple', 'banana', 'cherry'], { locale: 'en-US' }) // 'apple, banana, and cherry'
```

---
Expand Down
4 changes: 2 additions & 2 deletions nuxt-web/pages/docs/formatters.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
<PageFunction
name="formatDurationLabels"
description="Format time into a human-readable string"
params='[{"name":"seconds","type":"number"},{"name":"options?","type":"{\n display?: &#39;short&#39; | &#39;long&#39;\n round?: boolean\n decimals?: number\n }"}]'
params='[{"name":"seconds","type":"number"},{"name":"options?","type":"{\n display?: &#39;short&#39; | &#39;long&#39;\n round?: boolean\n decimals?: number\n locale?: string\n }"}]'
>
<FormatDurationLabels />
</PageFunction>
Expand Down Expand Up @@ -129,7 +129,7 @@
<PageFunction
name="formatList"
description="Create a string of comma-separated values from an array, object, or string with an optional limit and conjunction"
params='[{"name":"items","type":"string | object | string[]"},{"name":"options?","type":"{\n limit?: number\n conjunction?: string\n }"}]'
params='[{"name":"items","type":"string | object | string[]"},{"name":"options?","type":"{\n limit?: number\n conjunction?: string\n locale?: string\n }"}]'
>
<FormatList />
</PageFunction>
Expand Down
2 changes: 2 additions & 0 deletions src/formatters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ test('formatDurationLabels', () => {
expect(mod.formatDurationLabels(3600 * 400 + 60 + 1)).toBe('16 days 16 hours 1 minute 1 second')
expect(mod.formatDurationLabels(3600 * 400 + 60 + 1, { round: true })).toBe('16.7 days')
expect(mod.formatDurationLabels(9241233, { round: true })).toBe('107 days')
expect(mod.formatDurationLabels(3661, { locale: 'de-DE' })).toBe('1 Stunde 1 Minute 1 Sekunde')
})

test('formatDurationNumbers', () => {
Expand Down Expand Up @@ -111,6 +112,7 @@ test('formatList', () => {
expect(mod.formatList(['Apple', 'Oranges', 'Bananas', 'Pears', 'Grapes'], { limit: 2 })).toBe('Apple, Oranges and 3 more')
expect(mod.formatList(['Apple', 'Oranges', 'Bananas', 'Pears', 'Grapes'], { limit: 2, conjunction: 'or' })).toBe('Apple, Oranges or 3 more')
expect(mod.formatList([], { limit: 2, conjunction: 'or' })).toBe('')
expect(mod.formatList(['Apple', 'Oranges', 'Bananas'], { locale: 'en-US' })).toBe('Apple, Oranges, and Bananas')
})

test('formatTitle', () => {
Expand Down
40 changes: 29 additions & 11 deletions src/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,11 +190,13 @@ export function formatDurationLabels(
labels?: DisplayLength
round?: boolean
decimals?: number
locale?: string
},
): string {
const display = resolveDisplay(options, 'long')
const locale = options?.locale ?? 'en-US'

if (seconds <= 0) return formatUnit(0, { unit: 'second', decimals: 0, display })
if (seconds <= 0) return formatUnit(0, { unit: 'second', decimals: 0, display, locale })

const units = [
{ unit: 'year', value: 31536000 },
Expand All @@ -211,7 +213,7 @@ export function formatDurationLabels(
const unitValue = seconds / value
const hasDecimal = unitValue % 1 !== 0
const decimals = hasDecimal && unitValue.toFixed(1).endsWith('.0') ? 0 : hasDecimal ? 1 : 0
return formatUnit(unitValue, { unit, decimals, display })
return formatUnit(unitValue, { unit, decimals, display, locale })
}
}
}
Expand All @@ -221,13 +223,13 @@ export function formatDurationLabels(
for (const { unit, value } of units) {
const unitValue = Math.floor(seconds / value)
if (unitValue > 0) {
results.push(formatUnit(unitValue, { unit, decimals: 0, display }))
results.push(formatUnit(unitValue, { unit, decimals: 0, display, locale }))
seconds %= value
}
}

const milliseconds = Math.floor((seconds % 1) * 1000)
if (milliseconds > 0) results.push(formatUnit(milliseconds, { unit: 'millisecond', decimals: 0, display }))
if (milliseconds > 0) results.push(formatUnit(milliseconds, { unit: 'millisecond', decimals: 0, display, locale }))
return results.join(' ')
}

Expand Down Expand Up @@ -459,27 +461,43 @@ export function formatUnixTime(timestamp?: number): string {
}

/**
* Create a string of comma-separated values from an array, object, or string with an optional limit and conjunction
* Create a string of comma-separated values from an array, object, or string with an optional limit and conjunction.
* Uses Intl.ListFormat for locale-aware joining; truncation (`limit` / "N more") stays custom.
Comment on lines +464 to +465
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat
*/
export function formatList(
items: string | object | string[],
options?: {
limit?: number
conjunction?: string
locale?: string
},
): string {
if (typeof items === 'string') items = items.split(',').map(item => item.trim())
if (typeof items === 'object' && !Array.isArray(items)) items = Object.values(items)
if (!Array.isArray(items) || items.length === 0) return ''

// Default en-GB: no Oxford comma, matching historical English output.
const locale = options?.locale ?? 'en-GB'
const conj = options?.conjunction ?? 'and'
if (items.length <= 2) return items.join(items.length === 2 ? ` ${conj} ` : '')
const list = items.map(String)
const effectiveLimit = options?.limit ?? list.length

// ListFormat has no "N more" equivalent — keep truncation English-literal.
if (list.length > effectiveLimit) {
const listedItems = list.slice(0, effectiveLimit).join(', ')
const remaining = list.length - effectiveLimit
return `${listedItems} ${conj} ${remaining} more`
}
Comment on lines +486 to +491

const effectiveLimit = options?.limit ?? items.length
if (items.length <= effectiveLimit) return `${items.slice(0, -1).join(', ')} ${conj} ${items.at(-1)}`
// Custom conjunctions beyond and/or cannot go through ListFormat type.
if (conj !== 'and' && conj !== 'or') {
if (list.length <= 2) return list.join(list.length === 2 ? ` ${conj} ` : '')
return `${list.slice(0, -1).join(', ')} ${conj} ${list.at(-1)}`
}

const listedItems = items.slice(0, effectiveLimit).join(', ')
const remaining = items.length - effectiveLimit
return `${listedItems} ${conj} ${remaining} more`
const type: Intl.ListFormatType = conj === 'or' ? 'disjunction' : 'conjunction'
return new Intl.ListFormat(locale, { type, style: 'long' }).format(list)
}

/**
Expand Down
Loading