diff --git a/docs/pages/formatters.md b/docs/pages/formatters.md
index 4eeef5a..f29befe 100644
--- a/docs/pages/formatters.md
+++ b/docs/pages/formatters.md
@@ -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.
@@ -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
@@ -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'
```
---
@@ -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
@@ -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'
```
---
diff --git a/nuxt-web/pages/docs/formatters.vue b/nuxt-web/pages/docs/formatters.vue
index bbbc9f6..2e13ccf 100644
--- a/nuxt-web/pages/docs/formatters.vue
+++ b/nuxt-web/pages/docs/formatters.vue
@@ -56,7 +56,7 @@
@@ -129,7 +129,7 @@
diff --git a/src/formatters.test.ts b/src/formatters.test.ts
index a771e88..22f285a 100644
--- a/src/formatters.test.ts
+++ b/src/formatters.test.ts
@@ -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', () => {
@@ -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', () => {
diff --git a/src/formatters.ts b/src/formatters.ts
index a86163c..94d66bc 100644
--- a/src/formatters.ts
+++ b/src/formatters.ts
@@ -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 },
@@ -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 })
}
}
}
@@ -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(' ')
}
@@ -459,27 +461,44 @@ 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 locale-aware list string from an array, object, or string with an optional limit and conjunction.
+ * Uses Intl.ListFormat for full lists; truncation (`limit` / "N more") stays custom.
+ * 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 listed = list.slice(0, Math.max(0, effectiveLimit))
+ const remaining = list.length - listed.length
+ if (listed.length === 0) return `${remaining} more`
+ return `${listed.join(', ')} ${conj} ${remaining} more`
+ }
- 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)
}
/**