Skip to content

Commit d4851e4

Browse files
authored
fix(date): valid date-fns locale + formats type; useDate() docs (#318, #313, #323) (#374)
1 parent c1f5698 commit d4851e4

6 files changed

Lines changed: 186 additions & 8 deletions

File tree

docs/guide/features/date.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,37 @@ import { adapter, dateConfiguration, i18n } from 'virtual:vuetify-date-configura
2727
```
2828

2929
Check out [vuetify-date](https://github.com/vuetifyjs/nuxt-module/blob/main/src/runtime/plugins/vuetify-date.ts) plugin and the [date module](https://github.com/vuetifyjs/nuxt-module/blob/main/src/runtime/plugins/date.ts) for an example of a custom date adapter and how to access to the configuration.
30+
31+
## Troubleshooting
32+
33+
### `useDate()` — call methods on the instance, don't destructure them
34+
35+
`useDate()` returns a Vuetify date adapter instance whose methods rely on `this`.
36+
Destructuring them detaches `this`, which throws during SSR (e.g.
37+
`Cannot read properties of undefined (reading 'locale')`). In dev the error is
38+
swallowed and the page falls back to client rendering, so it only surfaces after
39+
`nuxt build`/`nuxt generate`.
40+
41+
```ts
42+
// ❌ loses `this` — crashes on the server
43+
const { date, format } = useDate()
44+
format(date(value), 'keyboardDate')
45+
46+
// ✅ keep the instance, call methods on it
47+
const adapter = useDate()
48+
adapter.format(adapter.date(value), 'keyboardDate')
49+
```
50+
51+
### date-fns base locale
52+
53+
With the `date-fns` adapter the locale is resolved from
54+
`vuetifyOptions.locale.locale` (mapped to the matching `date-fns/locale` export)
55+
at build time. If it is unset or unknown the module falls back to `enUS` and logs
56+
a warning. Set `vuetifyOptions.locale.locale` to a supported Vuetify locale to
57+
choose the base locale.
58+
59+
### Function-valued date formats are not supported
60+
61+
`vuetifyOptions.date.formats` only accepts serializable
62+
`Intl.DateTimeFormatOptions`. The date configuration is statically serialized, so
63+
function formats cannot be expressed. Use a `custom` date adapter if you need them.

packages/vuetify-nuxt-module/src/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,13 @@ export interface DateOptions {
2121
adapter?: DateAdapter
2222
/**
2323
* Formats.
24+
*
25+
* Only serializable `Intl.DateTimeFormatOptions` values are supported here:
26+
* the date configuration is statically serialized to a virtual module, so
27+
* function-valued formats cannot be expressed (see #313, #331). Use a custom
28+
* date adapter if you need function formats.
2429
*/
25-
formats?: Record<string, string>
30+
formats?: Record<string, Intl.DateTimeFormatOptions>
2631
/**
2732
* Locales.
2833
*
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* Maps Vuetify locale codes to date-fns `locale` export names.
3+
*
4+
* Only divergent codes are listed; codes whose Vuetify name already equals the
5+
* date-fns export name (e.g. `pt`, `de`, `srLatn`) pass through unchanged.
6+
* date-fns has no bare `en` export — see #318.
7+
*/
8+
const VUETIFY_TO_DATE_FNS: Record<string, string> = {
9+
en: 'enUS',
10+
fa: 'faIR',
11+
no: 'nb',
12+
srCyrl: 'sr',
13+
zhHans: 'zhCN',
14+
zhHant: 'zhTW',
15+
}
16+
17+
/**
18+
* date-fns export names reachable from Vuetify's supported locale codes.
19+
* Used to validate the resolved name so we never emit an import for a
20+
* non-existent export (which would crash the build).
21+
*/
22+
const DATE_FNS_SUPPORTED = new Set<string>([
23+
'af', 'ar', 'az', 'bg', 'ca', 'ckb', 'cs', 'da', 'de', 'el', 'enUS', 'es',
24+
'et', 'faIR', 'fi', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'km', 'ko',
25+
'lt', 'lv', 'nb', 'nl', 'pl', 'pt', 'ro', 'ru', 'sk', 'sl', 'sr', 'srLatn',
26+
'sv', 'th', 'tr', 'uk', 'vi', 'zhCN', 'zhTW',
27+
])
28+
29+
export interface ResolvedDateFnsLocale {
30+
/** A date-fns `locale` export name guaranteed to exist. */
31+
name: string
32+
/** True when the input could not be resolved and `enUS` was substituted. */
33+
fallback: boolean
34+
}
35+
36+
export function resolveDateFnsLocaleName (code: string | undefined): ResolvedDateFnsLocale {
37+
if (!code) {
38+
return { name: 'enUS', fallback: true }
39+
}
40+
41+
const candidate = VUETIFY_TO_DATE_FNS[code] ?? code
42+
if (DATE_FNS_SUPPORTED.has(candidate)) {
43+
return { name: candidate, fallback: false }
44+
}
45+
46+
return { name: 'enUS', fallback: true }
47+
}

packages/vuetify-nuxt-module/src/vite/vuetify-date-configuration-plugin.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Plugin } from 'vite'
22
import type { VuetifyNuxtContext } from '../utils/config'
3+
import { resolveDateFnsLocaleName } from '../utils/date-fns-locale'
34
import { RESOLVED_VIRTUAL_VUETIFY_DATE_CONFIGURATION, VIRTUAL_VUETIFY_DATE_CONFIGURATION } from './constants'
45

56
export function vuetifyDateConfigurationPlugin (ctx: VuetifyNuxtContext) {
@@ -27,22 +28,31 @@ export function dateConfiguration() {
2728

2829
const { adapter: _adapter, ...newDateOptions } = ctx.vuetifyOptions.date ?? {}
2930

30-
return `${buildImports()}
31+
let dateFnsLocale: string | undefined
32+
if (ctx.dateAdapter === 'date-fns') {
33+
const resolved = resolveDateFnsLocaleName(ctx.vuetifyOptions.locale?.locale)
34+
dateFnsLocale = resolved.name
35+
if (resolved.fallback) {
36+
ctx.logger.warn(`[vuetify-nuxt-module] date-fns locale for "${ctx.vuetifyOptions.locale?.locale ?? '(unset)'}" not found, falling back to "enUS". Set "vuetifyOptions.locale.locale" to a supported locale.`)
37+
}
38+
}
39+
40+
return `${buildImports(dateFnsLocale)}
3141
export const enabled = true
3242
export const isDev = ${ctx.isDev}
3343
export const i18n = ${ctx.i18n}
3444
export const adapter = '${ctx.dateAdapter}'
3545
export function dateConfiguration() {
3646
const options = JSON.parse('${JSON.stringify(newDateOptions)}')
37-
${buildAdapter()}
47+
${buildAdapter(dateFnsLocale)}
3848
return options
3949
}
4050
`
4151
}
4252
},
4353
}
4454

45-
function buildAdapter () {
55+
function buildAdapter (dateFnsLocale?: string) {
4656
if (ctx.dateAdapter === 'custom' || (ctx.dateAdapter === 'vuetify' && ctx.vuetifyGte('3.4.0'))) {
4757
return ''
4858
}
@@ -51,15 +61,14 @@ export function dateConfiguration() {
5161
return 'options.adapter = VuetifyDateAdapter'
5262
}
5363

54-
const locale = ctx.vuetifyOptions.locale?.locale ?? 'en'
5564
if (ctx.dateAdapter === 'date-fns') {
56-
return `options.adapter = new Adapter({ locale: ${locale} })`
65+
return `options.adapter = new Adapter({ locale: ${dateFnsLocale} })`
5766
}
5867

5968
return 'options.adapter = Adapter'
6069
}
6170

62-
function buildImports () {
71+
function buildImports (dateFnsLocale?: string) {
6372
if (ctx.dateAdapter === 'custom' || (ctx.dateAdapter === 'vuetify' && ctx.vuetifyGte('3.4.0'))) {
6473
return ''
6574
}
@@ -70,7 +79,7 @@ export function dateConfiguration() {
7079

7180
const imports = [`import Adapter from '@date-io/${ctx.dateAdapter}'`]
7281
if (ctx.dateAdapter === 'date-fns') {
73-
imports.push(`import { ${ctx.vuetifyOptions.locale?.locale ?? 'en'} } from 'date-fns/locale'`)
82+
imports.push(`import { ${dateFnsLocale} } from 'date-fns/locale'`)
7483
}
7584

7685
return imports.join('\n')
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { describe, expect, it, vi } from 'vitest'
2+
import { RESOLVED_VIRTUAL_VUETIFY_DATE_CONFIGURATION } from '../src/vite/constants'
3+
import { vuetifyDateConfigurationPlugin } from '../src/vite/vuetify-date-configuration-plugin'
4+
5+
function makeCtx (localeCode: string | undefined) {
6+
return {
7+
dateAdapter: 'date-fns',
8+
isDev: false,
9+
i18n: false,
10+
vuetifyGte: () => true,
11+
logger: { warn: vi.fn() },
12+
vuetifyOptions: {
13+
date: { adapter: 'date-fns' },
14+
locale: localeCode === undefined ? {} : { locale: localeCode },
15+
},
16+
} as any
17+
}
18+
19+
async function loadModule (ctx: any) {
20+
const plugin = vuetifyDateConfigurationPlugin(ctx) as any
21+
return (await plugin.load(RESOLVED_VIRTUAL_VUETIFY_DATE_CONFIGURATION)) as string
22+
}
23+
24+
describe('vuetifyDateConfigurationPlugin date-fns locale', () => {
25+
it('imports a valid date-fns export for a Vuetify code', async () => {
26+
const ctx = makeCtx('en')
27+
const code = await loadModule(ctx)
28+
expect(code).toContain('import { enUS } from \'date-fns/locale\'')
29+
expect(code).toContain('new Adapter({ locale: enUS })')
30+
expect(code).not.toContain('import { en } from \'date-fns/locale\'')
31+
expect(ctx.logger.warn).not.toHaveBeenCalled()
32+
})
33+
34+
it('maps divergent codes (zhHans -> zhCN)', async () => {
35+
const code = await loadModule(makeCtx('zhHans'))
36+
expect(code).toContain('import { zhCN } from \'date-fns/locale\'')
37+
expect(code).toContain('new Adapter({ locale: zhCN })')
38+
})
39+
40+
it('passes through a locale that matches a date-fns export (de)', async () => {
41+
const ctx = makeCtx('de')
42+
const code = await loadModule(ctx)
43+
expect(code).toContain('import { de } from \'date-fns/locale\'')
44+
expect(code).toContain('new Adapter({ locale: de })')
45+
expect(ctx.logger.warn).not.toHaveBeenCalled()
46+
})
47+
48+
it('falls back to enUS and warns for an unset locale', async () => {
49+
const ctx = makeCtx(undefined)
50+
const code = await loadModule(ctx)
51+
expect(code).toContain('import { enUS } from \'date-fns/locale\'')
52+
expect(code).toContain('new Adapter({ locale: enUS })')
53+
expect(ctx.logger.warn).toHaveBeenCalledExactlyOnceWith(expect.stringContaining('(unset)'))
54+
})
55+
})
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { resolveDateFnsLocaleName } from '../src/utils/date-fns-locale'
3+
4+
describe('resolveDateFnsLocaleName', () => {
5+
it('maps divergent Vuetify codes to date-fns export names', () => {
6+
expect(resolveDateFnsLocaleName('en')).toEqual({ name: 'enUS', fallback: false })
7+
expect(resolveDateFnsLocaleName('zhHans')).toEqual({ name: 'zhCN', fallback: false })
8+
expect(resolveDateFnsLocaleName('zhHant')).toEqual({ name: 'zhTW', fallback: false })
9+
expect(resolveDateFnsLocaleName('srCyrl')).toEqual({ name: 'sr', fallback: false })
10+
expect(resolveDateFnsLocaleName('fa')).toEqual({ name: 'faIR', fallback: false })
11+
expect(resolveDateFnsLocaleName('no')).toEqual({ name: 'nb', fallback: false })
12+
})
13+
14+
it('passes through codes that already match a date-fns export', () => {
15+
expect(resolveDateFnsLocaleName('pt')).toEqual({ name: 'pt', fallback: false })
16+
expect(resolveDateFnsLocaleName('de')).toEqual({ name: 'de', fallback: false })
17+
expect(resolveDateFnsLocaleName('srLatn')).toEqual({ name: 'srLatn', fallback: false })
18+
expect(resolveDateFnsLocaleName('ar')).toEqual({ name: 'ar', fallback: false })
19+
expect(resolveDateFnsLocaleName('da')).toEqual({ name: 'da', fallback: false })
20+
expect(resolveDateFnsLocaleName('km')).toEqual({ name: 'km', fallback: false })
21+
})
22+
23+
it('falls back to enUS for undefined or unknown codes', () => {
24+
expect(resolveDateFnsLocaleName(undefined)).toEqual({ name: 'enUS', fallback: true })
25+
expect(resolveDateFnsLocaleName('')).toEqual({ name: 'enUS', fallback: true })
26+
expect(resolveDateFnsLocaleName('klingon')).toEqual({ name: 'enUS', fallback: true })
27+
})
28+
})

0 commit comments

Comments
 (0)