@datagouv/components-next 1.0.2-dev.192 → 1.0.2-dev.194

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/{Datafair.client-fxOwwaND.js → Datafair.client-Drn-ygd-.js} +1 -1
  2. package/dist/{ImagePreview.client-XSR9THWR.js → ImagePreview.client-DCzPQ-xK.js} +1 -1
  3. package/dist/{JsonPreview.client-CU1FWFIy.js → JsonPreview.client-Dqioe7mC.js} +2 -2
  4. package/dist/{MapContainer.client-D0-Dn7jq.js → MapContainer.client-CV5ZRvvr.js} +2 -2
  5. package/dist/{PdfPreview.client-B7eDgq8y.js → PdfPreview.client-XFmjFK8r.js} +2 -2
  6. package/dist/{Pmtiles.client-CCpvggkZ.js → Pmtiles.client-DAp930rV.js} +1 -1
  7. package/dist/{PreviewWrapper.vue_vue_type_script_setup_true_lang-BzhNjYoY.js → PreviewWrapper.vue_vue_type_script_setup_true_lang-BkPS3zke.js} +1 -1
  8. package/dist/{XmlPreview.client-CyjpNh5i.js → XmlPreview.client-zetBGS4F.js} +3 -3
  9. package/dist/components-next.css +1 -1
  10. package/dist/components-next.js +164 -163
  11. package/dist/{index-BBnxH-Nj.js → index-DWuJwKzS.js} +1 -1
  12. package/dist/{leaflet-src-Be0Ohp2k.js → leaflet-src-DiG78UoJ.js} +1 -1
  13. package/dist/{main-C2kCZkPD.js → main-dOC1WJIR.js} +12194 -12006
  14. package/dist/{vue3-xml-viewer.common-__BZsi8i.js → vue3-xml-viewer.common-D1KbuT3v.js} +1 -1
  15. package/package.json +1 -1
  16. package/src/components/ActivityList/ActivityList.vue +21 -12
  17. package/src/components/DataserviceCard.vue +15 -5
  18. package/src/components/DatasetCard.vue +15 -5
  19. package/src/components/DatasetInformation/DatasetTemporalitySection.vue +7 -4
  20. package/src/components/DiscussionMessageCard.vue +14 -15
  21. package/src/components/FormattedDate.vue +81 -0
  22. package/src/components/PostCard.vue +13 -15
  23. package/src/components/ResourceAccordion/Downloads.vue +19 -7
  24. package/src/components/ResourceAccordion/Metadata.vue +3 -4
  25. package/src/components/ResourceAccordion/Preview.vue +8 -4
  26. package/src/components/ResourceAccordion/ResourceAccordion.vue +13 -3
  27. package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +10 -5
  28. package/src/components/ResourceExplorer/ResourceViewerHeader.vue +10 -3
  29. package/src/components/ResourceListItem.vue +10 -3
  30. package/src/components/ReuseCard.vue +14 -3
  31. package/src/components/SmallChart.vue +11 -10
  32. package/src/components/StatBox.vue +13 -5
  33. package/src/components/TabularExplorer/types.ts +15 -12
  34. package/src/components/TabularExplorer/useColumnMetadata.ts +1 -1
  35. package/src/components/TopicCard.vue +13 -14
  36. package/src/functions/charts.ts +2 -2
  37. package/src/functions/dates.ts +3 -5
  38. package/src/main.ts +2 -0
@@ -1,4 +1,4 @@
1
- import { g as Ke } from "./main-C2kCZkPD.js";
1
+ import { g as Ke } from "./main-dOC1WJIR.js";
2
2
  import We from "vue";
3
3
  function Fe($, K) {
4
4
  for (var V = 0; V < K.length; V++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datagouv/components-next",
3
- "version": "1.0.2-dev.192",
3
+ "version": "1.0.2-dev.194",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -12,7 +12,10 @@
12
12
  class="my-1"
13
13
  >
14
14
  <p class="pl-3 mb-1 text-xs text-gray-medium">
15
- {{ month }}
15
+ <FormattedDate
16
+ :date="month"
17
+ :options="{ dateStyle: undefined, year: 'numeric', month: 'long', day: undefined }"
18
+ />
16
19
  </p>
17
20
  <ul
18
21
  class="space-y-2 p-0 m-0"
@@ -82,9 +85,15 @@
82
85
  </template>
83
86
  </slot>
84
87
  </p>
85
- <p class="m-0 flex-none text-xs text-gray-medium">
86
- {{ t('le {date}', { date: formatDate(activity.created_at) }) }}
87
- </p>
88
+ <TranslationT
89
+ tag="p"
90
+ class="m-0 flex-none text-xs text-gray-medium"
91
+ keypath="le {date}"
92
+ >
93
+ <template #date>
94
+ <FormattedDate :date="activity.created_at" />
95
+ </template>
96
+ </TranslationT>
88
97
  </div>
89
98
  </li>
90
99
  </ul>
@@ -118,13 +127,14 @@ import { useRoute } from 'vue-router'
118
127
  import { useTranslation } from '../../composables/useTranslation'
119
128
  import { getActivityTranslation } from '../../functions/activities'
120
129
  import { useFetch } from '../../functions/api'
121
- import { useFormatDate } from '../../functions/dates'
122
130
  import type { PaginatedArray } from '../../types/api'
123
131
  import type { Activity } from '../../types/activity'
124
132
  import Avatar from '../Avatar.vue'
125
133
  import LoadingBlock from '../LoadingBlock.vue'
126
134
  import Pagination from '../Pagination.vue'
127
135
  import PaddedContainer from '../PaddedContainer.vue'
136
+ import FormattedDate from '../FormattedDate.vue'
137
+ import TranslationT from '../TranslationT.vue'
128
138
  import listSrc from '../../../assets/illustrations/list.svg?url'
129
139
 
130
140
  const props = defineProps<{
@@ -133,7 +143,6 @@ const props = defineProps<{
133
143
  }>()
134
144
 
135
145
  const route = useRoute()
136
- const { formatDate } = useFormatDate()
137
146
  const { t } = useTranslation()
138
147
 
139
148
  const page = ref(parseInt(route.query.page as string | undefined ?? '1', 10))
@@ -146,13 +155,13 @@ const { data: activities, status } = await useFetch<PaginatedArray<Activity>>('/
146
155
  },
147
156
  })
148
157
 
158
+ // Grouping on a month read in the reader's timezone would change both the number of
159
+ // groups and their keys between the server render and the browser's — a mismatch on
160
+ // the children themselves, which no `data-allow-mismatch` covers. The `YYYY-MM` the
161
+ // timestamp starts with is the same string for everyone; the month is worded from it
162
+ // at render time.
149
163
  const groupedActivities = computed(() => activities.value?.data.reduce((grouped, activity) => {
150
- const activityMonth = formatDate(activity.created_at, {
151
- dateStyle: undefined,
152
- day: undefined,
153
- month: 'long',
154
- year: 'numeric',
155
- })
164
+ const activityMonth = activity.created_at.slice(0, 7)
156
165
  if (!grouped[activityMonth]) {
157
166
  grouped[activityMonth] = []
158
167
  }
@@ -71,9 +71,19 @@
71
71
  class="size-4 flex-none fill-gray-medium"
72
72
  />
73
73
  <!-- https://github.com/datagouv/cdata/issues/653 -->
74
- <p class="text-sm whitespace-nowrap mb-0 text-gray-medium">
75
- {{ t('Mis à jour {date}', { date: formatRelativeIfRecentDate(dataservice.metadata_modified_at, { dateStyle: 'medium' }) }) }}
76
- </p>
74
+ <TranslationT
75
+ tag="p"
76
+ class="text-sm whitespace-nowrap mb-0 text-gray-medium"
77
+ keypath="Mis à jour {date}"
78
+ >
79
+ <template #date>
80
+ <FormattedDate
81
+ :date="dataservice.metadata_modified_at"
82
+ format="relative"
83
+ :options="{ dateStyle: 'medium' }"
84
+ />
85
+ </template>
86
+ </TranslationT>
77
87
  <RiSubtractLine
78
88
  aria-hidden="true"
79
89
  class="size-4 flex-none fill-gray-medium"
@@ -118,7 +128,6 @@ import { computed } from 'vue'
118
128
  import type { RouteLocationRaw } from 'vue-router'
119
129
  import { RiArchiveLine, RiEyeLine, RiLockLine, RiPassValidLine, RiSparklingLine, RiStarLine, RiSubtractLine, RiTerminalLine } from '@remixicon/vue'
120
130
  import { useComponentsConfig } from '../config'
121
- import { useFormatDate } from '../functions/dates'
122
131
  import { summarize } from '../functions/helpers'
123
132
  import type { Dataservice } from '../types/dataservices'
124
133
  import type { TitleTag } from '../types/ui'
@@ -132,6 +141,8 @@ import ObjectCardHeader from './ObjectCardHeader.vue'
132
141
  import ObjectCardOwner from './ObjectCardOwner.vue'
133
142
  import ObjectCardShortDescription from './ObjectCardShortDescription.vue'
134
143
  import DataserviceQualityInline from './DataserviceQualityInline.vue'
144
+ import FormattedDate from './FormattedDate.vue'
145
+ import TranslationT from './TranslationT.vue'
135
146
 
136
147
  type Props = {
137
148
  dataservice: Dataservice
@@ -146,7 +157,6 @@ const props = withDefaults(defineProps<Props>(), {
146
157
  })
147
158
 
148
159
  const { t } = useTranslation()
149
- const { formatRelativeIfRecentDate } = useFormatDate()
150
160
 
151
161
  const config = useComponentsConfig()
152
162
  const isTabularApi = computed(() => {
@@ -56,9 +56,19 @@
56
56
  :organization-url="organizationUrl"
57
57
  />
58
58
  <RiSubtractLine class="hidden md:block size-4 flex-none fill-gray-medium" />
59
- <div class="w-full md:w-auto text-gray-medium whitespace-nowrap">
60
- {{ t('Mis à jour {date}', { date: formatRelativeIfRecentDate(dataset.last_update, { dateStyle: 'medium' }) }) }}
61
- </div>
59
+ <TranslationT
60
+ tag="div"
61
+ class="w-full md:w-auto text-gray-medium whitespace-nowrap"
62
+ keypath="Mis à jour {date}"
63
+ >
64
+ <template #date>
65
+ <FormattedDate
66
+ :date="dataset.last_update"
67
+ format="relative"
68
+ :options="{ dateStyle: 'medium' }"
69
+ />
70
+ </template>
71
+ </TranslationT>
62
72
  </div>
63
73
  <div class="mx-0 -mb-1 flex flex-wrap items-center text-sm text-gray-medium">
64
74
  <div class="hidden sm:flex text-gray-medium">
@@ -122,7 +132,6 @@ import { RiArchiveLine, RiDatabase2Line, RiDownloadLine, RiEyeLine, RiLineChartL
122
132
  import type { Dataset, DatasetV2 } from '../types/datasets'
123
133
  import type { TitleTag } from '../types/ui'
124
134
  import { summarize } from '../functions/helpers'
125
- import { useFormatDate } from '../functions/dates'
126
135
  import { getDescriptionShort } from '../functions/description'
127
136
  import { useTranslation } from '../composables/useTranslation'
128
137
  import DatasetQualityInline from './DatasetQualityInline.vue'
@@ -134,6 +143,8 @@ import ObjectCard from './ObjectCard.vue'
134
143
  import ObjectCardBadge from './ObjectCardBadge.vue'
135
144
  import ObjectCardHeader from './ObjectCardHeader.vue'
136
145
  import ObjectCardShortDescription from './ObjectCardShortDescription.vue'
146
+ import FormattedDate from './FormattedDate.vue'
147
+ import TranslationT from './TranslationT.vue'
137
148
 
138
149
  type Props = {
139
150
  dataset: Dataset | DatasetV2
@@ -149,5 +160,4 @@ const props = withDefaults(defineProps<Props>(), {
149
160
  })
150
161
 
151
162
  const { t } = useTranslation()
152
- const { formatRelativeIfRecentDate } = useFormatDate()
153
163
  </script>
@@ -6,7 +6,9 @@
6
6
  <DescriptionList>
7
7
  <div>
8
8
  <DescriptionListTerm>{{ t('Création') }}</DescriptionListTerm>
9
- <DescriptionListDetails>{{ formatDate(dataset.created_at) }}</DescriptionListDetails>
9
+ <DescriptionListDetails>
10
+ <FormattedDate :date="dataset.created_at" />
11
+ </DescriptionListDetails>
10
12
  </div>
11
13
  <div v-if="dataset.frequency">
12
14
  <DescriptionListTerm>{{ t('Fréquence') }}</DescriptionListTerm>
@@ -20,7 +22,9 @@
20
22
  </div>
21
23
  <div>
22
24
  <DescriptionListTerm>{{ t('Dernière mise à jour') }}</DescriptionListTerm>
23
- <DescriptionListDetails>{{ formatDate(dataset.last_update) }}</DescriptionListDetails>
25
+ <DescriptionListDetails>
26
+ <FormattedDate :date="dataset.last_update" />
27
+ </DescriptionListDetails>
24
28
  </div>
25
29
  <slot />
26
30
  </DescriptionList>
@@ -29,17 +33,16 @@
29
33
 
30
34
  <script setup lang="ts">
31
35
  import { useTranslation } from '../../composables/useTranslation'
32
- import { useFormatDate } from '../../functions/dates'
33
36
  import type { DatasetV2WithFullObject } from '../../types/datasets'
34
37
  import DescriptionList from '../DescriptionList.vue'
35
38
  import DescriptionListTerm from '../DescriptionListTerm.vue'
36
39
  import DescriptionListDetails from '../DescriptionListDetails.vue'
37
40
  import DateRangeDetails from '../DateRangeDetails.vue'
41
+ import FormattedDate from '../FormattedDate.vue'
38
42
 
39
43
  defineProps<{
40
44
  dataset: DatasetV2WithFullObject
41
45
  }>()
42
46
 
43
47
  const { t } = useTranslation()
44
- const { formatDate } = useFormatDate()
45
48
  </script>
@@ -20,9 +20,18 @@
20
20
  aria-hidden="true"
21
21
  class="size-4 flex-none fill-gray-medium"
22
22
  />
23
- <span class="text-gray-medium whitespace-nowrap">
24
- {{ t('Posté {date}', { date: formatDate(discussion.created) }) }}
25
- </span>
23
+ <TranslationT
24
+ class="text-gray-medium whitespace-nowrap"
25
+ keypath="Posté {date}"
26
+ >
27
+ <template #date>
28
+ <FormattedDate
29
+ :date="discussion.created"
30
+ format="relative"
31
+ :options="{ dateStyle: 'long', timeStyle: 'short' }"
32
+ />
33
+ </template>
34
+ </TranslationT>
26
35
  </div>
27
36
 
28
37
  <ObjectCardShortDescription :text="firstMessageContent" />
@@ -35,13 +44,13 @@
35
44
  import { RiChat3Line, RiSubtractLine } from '@remixicon/vue'
36
45
  import { computed } from 'vue'
37
46
  import type { RouteLocationRaw } from 'vue-router'
38
- import { useFormatDate } from '../functions/dates'
39
- import { useTranslation } from '../composables/useTranslation'
40
47
  import type { Thread } from '../types/discussions'
41
48
  import ObjectCard from './ObjectCard.vue'
42
49
  import ObjectCardHeader from './ObjectCardHeader.vue'
43
50
  import ObjectCardOwner from './ObjectCardOwner.vue'
44
51
  import ObjectCardShortDescription from './ObjectCardShortDescription.vue'
52
+ import FormattedDate from './FormattedDate.vue'
53
+ import TranslationT from './TranslationT.vue'
45
54
 
46
55
  const props = defineProps<{
47
56
  discussion: Thread
@@ -49,15 +58,5 @@ const props = defineProps<{
49
58
  organizationUrl?: RouteLocationRaw
50
59
  }>()
51
60
 
52
- const { t } = useTranslation()
53
- const { formatRelativeIfRecentDate } = useFormatDate()
54
-
55
61
  const firstMessageContent = computed(() => props.discussion.discussion?.[0]?.content)
56
-
57
- const formatDate = (dateString: string) => {
58
- return formatRelativeIfRecentDate(dateString, {
59
- dateStyle: 'long',
60
- timeStyle: 'short',
61
- })
62
- }
63
62
  </script>
@@ -0,0 +1,81 @@
1
+ <template>
2
+ <!-- An instant is formatted in the server's timezone at render time and in the
3
+ reader's at read time, so its text and `title` legitimately differ and Vue is
4
+ told to accept it. A plain date carries no time, so nothing about it varies —
5
+ exempting it would hide a real bug instead of a legitimate difference. Vue only
6
+ honours `data-allow-mismatch` on an element whose children are plain text,
7
+ never on a text node coming from a slot: hence two static elements instead of a
8
+ `<component :is>`, which would pass the text through a slot. -->
9
+ <time
10
+ v-if="datetime"
11
+ :datetime
12
+ :title
13
+ :data-allow-mismatch="allowMismatch"
14
+ >{{ formatted }}</time>
15
+ <!-- No date to format: nothing here depends on the timezone or on "now". -->
16
+ <span v-else>{{ formatted }}</span>
17
+ </template>
18
+
19
+ <script setup lang="ts">
20
+ import { computed } from 'vue'
21
+ import { useFormatDate } from '../functions/dates'
22
+
23
+ const props = withDefaults(defineProps<{
24
+ date: Date | string | null | undefined
25
+ /**
26
+ * `date`: the full date. `relative`: "3 days ago" while the date is less than a
27
+ * month old, the full date beyond. `from-now`: always relative, however old.
28
+ */
29
+ format?: 'date' | 'relative' | 'from-now'
30
+ options?: Intl.DateTimeFormatOptions
31
+ }>(), {
32
+ format: 'date',
33
+ options: () => ({}),
34
+ })
35
+
36
+ const { formatDate, formatFromNow, formatRelativeIfRecentDate } = useFormatDate()
37
+
38
+ // `2026`, `2026-04` and `2026-04-24` carry no time of day. `new Date()` reads them as
39
+ // midnight UTC, an instant they never claimed, and then converts it: a reader west of
40
+ // UTC gets the day before — and with it the month, and sometimes the year.
41
+ const PLAIN_DATE = /^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?$/
42
+
43
+ const plainDate = computed(() => typeof props.date === 'string' ? props.date.match(PLAIN_DATE) : null)
44
+
45
+ const parsed = computed(() => {
46
+ if (!props.date) return null
47
+ const plain = plainDate.value
48
+ const date = plain
49
+ ? new Date(Number(plain[1]), Number(plain[2] ?? 1) - 1, Number(plain[3] ?? 1))
50
+ : new Date(props.date)
51
+ return isNaN(date.getTime()) ? null : date
52
+ })
53
+
54
+ // Announce the precision the value carries, never more: a month stays a month.
55
+ const datetime = computed(() => {
56
+ if (!parsed.value) return undefined
57
+ return plainDate.value ? plainDate.value[0] : parsed.value.toISOString()
58
+ })
59
+
60
+ // Only what genuinely differs between server and browser is exempted: an instant, and
61
+ // the relative forms, which are measured against "now".
62
+ const allowMismatch = computed(() => !plainDate.value || props.format !== 'date'
63
+ ? 'text,attribute'
64
+ : undefined)
65
+
66
+ const formatted = computed(() => {
67
+ if (props.format === 'relative') return formatRelativeIfRecentDate(parsed.value, props.options)
68
+ if (props.format === 'from-now') return formatFromNow(parsed.value)
69
+ return formatDate(parsed.value, props.options)
70
+ })
71
+
72
+ // Hovering an instant gives the exact moment, which the displayed form usually omits:
73
+ // the relative forms drop it entirely, and most styles drop the time. A plain date has
74
+ // no moment to reveal — a `title` there would be one we made up, and a form that already
75
+ // spells the moment out needs no tooltip repeating it word for word.
76
+ const title = computed(() => {
77
+ if (!parsed.value || plainDate.value) return undefined
78
+ const exact = formatDate(parsed.value, { dateStyle: 'long', timeStyle: 'short' })
79
+ return exact === formatted.value ? undefined : exact
80
+ })
81
+ </script>
@@ -23,12 +23,20 @@
23
23
 
24
24
  <ObjectCardShortDescription :text="post.headline || post.content" />
25
25
 
26
- <div
26
+ <TranslationT
27
27
  v-if="post.published || post.created_at"
28
+ tag="div"
28
29
  class="text-sm text-gray-medium mt-1"
30
+ keypath="Publié {date}"
29
31
  >
30
- {{ t('Publié {date}', { date: formatDate(post.published || post.created_at) }) }}
31
- </div>
32
+ <template #date>
33
+ <FormattedDate
34
+ :date="post.published || post.created_at"
35
+ format="relative"
36
+ :options="{ dateStyle: 'long', timeStyle: 'short' }"
37
+ />
38
+ </template>
39
+ </TranslationT>
32
40
 
33
41
  <slot />
34
42
  </ObjectCard>
@@ -37,26 +45,16 @@
37
45
  <script setup lang="ts">
38
46
  import { RiArticleLine } from '@remixicon/vue'
39
47
  import type { RouteLocationRaw } from 'vue-router'
40
- import { useFormatDate } from '../functions/dates'
41
- import { useTranslation } from '../composables/useTranslation'
42
48
  import type { Post } from '../types/posts'
43
49
  import Placeholder from './Placeholder.vue'
44
50
  import ObjectCard from './ObjectCard.vue'
45
51
  import ObjectCardHeader from './ObjectCardHeader.vue'
46
52
  import ObjectCardShortDescription from './ObjectCardShortDescription.vue'
53
+ import FormattedDate from './FormattedDate.vue'
54
+ import TranslationT from './TranslationT.vue'
47
55
 
48
56
  defineProps<{
49
57
  post: Post
50
58
  postUrl?: RouteLocationRaw
51
59
  }>()
52
-
53
- const { t } = useTranslation()
54
- const { formatRelativeIfRecentDate } = useFormatDate()
55
-
56
- const formatDate = (dateString: string) => {
57
- return formatRelativeIfRecentDate(dateString, {
58
- dateStyle: 'long',
59
- timeStyle: 'short',
60
- })
61
- }
62
60
  </script>
@@ -47,9 +47,21 @@
47
47
  />
48
48
  </dd>
49
49
  <template v-if="generatedFormats.length">
50
- <dt class="font-bold fr-text--sm fr-mb-0">
51
- {{ t('Formats générés automatiquement par {platform} (dernière mise à jour {date})', { platform: config.name, date: conversionsLastUpdate }) }}
52
- </dt>
50
+ <TranslationT
51
+ tag="dt"
52
+ class="font-bold fr-text--sm fr-mb-0"
53
+ keypath="Formats générés automatiquement par {platform} (dernière mise à jour {date})"
54
+ >
55
+ <template #platform>
56
+ {{ config.name }}
57
+ </template>
58
+ <template #date>
59
+ <FormattedDate
60
+ :date="conversionsFinishedAt"
61
+ format="relative"
62
+ />
63
+ </template>
64
+ </TranslationT>
53
65
  <dd
54
66
  v-for="generatedFormat in generatedFormats"
55
67
  :key="generatedFormat.format"
@@ -132,7 +144,8 @@ import { filesize } from '../../functions/helpers'
132
144
  import { getResourceFilesize } from '../../functions/resources'
133
145
  import { trackEvent } from '../../functions/matomo'
134
146
  import { useComponentsConfig } from '../../config'
135
- import { useFormatDate } from '../../functions/dates'
147
+ import FormattedDate from '../FormattedDate.vue'
148
+ import TranslationT from '../TranslationT.vue'
136
149
  import { useTranslation } from '../../composables/useTranslation'
137
150
  import { useResourceCapabilities } from '../../composables/useResourceCapabilities'
138
151
  import type { Resource } from '../../types/resources'
@@ -145,7 +158,6 @@ const props = defineProps<{
145
158
 
146
159
  const { t } = useTranslation()
147
160
  const config = useComponentsConfig()
148
- const { formatRelativeIfRecentDate } = useFormatDate()
149
161
 
150
162
  const { generatedFormats, wfsFormats, defaultWfsProjection } = useResourceCapabilities(
151
163
  () => props.resource,
@@ -154,7 +166,7 @@ const { generatedFormats, wfsFormats, defaultWfsProjection } = useResourceCapabi
154
166
 
155
167
  const resourceFilesize = computed(() => getResourceFilesize(props.resource))
156
168
 
157
- const conversionsLastUpdate = computed(() =>
158
- formatRelativeIfRecentDate(props.resource.extras['analysis:parsing:finished_at'] as string | undefined),
169
+ const conversionsFinishedAt = computed(() =>
170
+ props.resource.extras['analysis:parsing:finished_at'] as string | undefined,
159
171
  )
160
172
  </script>
@@ -4,7 +4,7 @@ import type { Resource } from '../../types/resources'
4
4
  import CopyButton from '../CopyButton.vue'
5
5
  import DescriptionDetails from '../DescriptionDetails.vue'
6
6
  import DescriptionTerm from '../DescriptionTerm.vue'
7
- import { useFormatDate } from '../../functions/dates'
7
+ import FormattedDate from '../FormattedDate.vue'
8
8
  import { filesize } from '../../functions/helpers'
9
9
  import ExtraAccordion from '../ExtraAccordion.vue'
10
10
  import { getResourceTitleId, getResourceLabel, getResourceFilesize } from '../../functions/resources'
@@ -19,7 +19,6 @@ const resourceTitleId = computed(() => getResourceTitleId(props.resource))
19
19
  const resourceFilesize = computed(() => getResourceFilesize(props.resource))
20
20
 
21
21
  const { t } = useTranslation()
22
- const { formatDate } = useFormatDate()
23
22
  </script>
24
23
 
25
24
  <template>
@@ -94,11 +93,11 @@ const { formatDate } = useFormatDate()
94
93
  >
95
94
  <DescriptionTerm>{{ t('Créée le') }}</DescriptionTerm>
96
95
  <DescriptionDetails>
97
- {{ formatDate(resource.created_at) }}
96
+ <FormattedDate :date="resource.created_at" />
98
97
  </DescriptionDetails>
99
98
  <DescriptionTerm>{{ t('Modifiée le') }}</DescriptionTerm>
100
99
  <DescriptionDetails>
101
- {{ formatDate(resource.last_modified) }}
100
+ <FormattedDate :date="resource.last_modified" />
102
101
  </DescriptionDetails>
103
102
  </dl>
104
103
  <dl
@@ -90,7 +90,11 @@
90
90
  @change="changePage"
91
91
  />
92
92
  <div class="fr-px-5v">
93
- {{ t("Dernière mise à jour de la prévisualisation : {date}", { date: lastUpdate }) }} —
93
+ <TranslationT keypath="Dernière mise à jour de la prévisualisation : {date}">
94
+ <template #date>
95
+ <FormattedDate :date="parsingFinishedAt" />
96
+ </template>
97
+ </TranslationT> —
94
98
  {{ t('{count} colonnes', columns.length) }} —
95
99
  {{ t('Lignes {count}', rowCount) }}
96
100
  </div>
@@ -103,7 +107,8 @@ import { computed, onMounted, ref } from 'vue'
103
107
  import { RiArrowDownLine, RiArrowUpLine, RiExternalLinkFill } from '@remixicon/vue'
104
108
  import Pagination from '../Pagination.vue'
105
109
  import { getData, type SortConfig } from '../../functions/tabularApi'
106
- import { useFormatDate } from '../../functions/dates'
110
+ import FormattedDate from '../FormattedDate.vue'
111
+ import TranslationT from '../TranslationT.vue'
107
112
  import { trackEvent } from '../../functions/matomo'
108
113
  import type { Resource } from '../../types/resources'
109
114
  import { useComponentsConfig } from '../../config'
@@ -116,7 +121,6 @@ import PreviewLoader from './PreviewLoader.vue'
116
121
  const props = defineProps<{ resource: Resource }>()
117
122
 
118
123
  const { t } = useTranslation()
119
- const { formatDate } = useFormatDate()
120
124
 
121
125
  const rows = ref<Array<Record<string, unknown>>>([])
122
126
  const columns = ref<Array<string>>([])
@@ -196,7 +200,7 @@ function sortByField(col: string) {
196
200
  getTableInfos(currentPage.value, sortConfig.value)
197
201
  };
198
202
 
199
- const lastUpdate = computed(() => formatDate(props.resource.extras['analysis:parsing:finished_at'] as string | undefined))
203
+ const parsingFinishedAt = computed(() => props.resource.extras['analysis:parsing:finished_at'] as string | undefined)
200
204
 
201
205
  onMounted(() => {
202
206
  getTableInfos(currentPage.value)
@@ -60,7 +60,17 @@
60
60
  aria-hidden="true"
61
61
  class="size-3 fill-gray-medium"
62
62
  />
63
- <span class="text-xs mb-0">{{ t('Mis à jour {date}', { date: formatRelativeIfRecentDate(lastUpdate) }) }}</span>
63
+ <TranslationT
64
+ class="text-xs mb-0"
65
+ keypath="Mis à jour {date}"
66
+ >
67
+ <template #date>
68
+ <FormattedDate
69
+ :date="lastUpdate"
70
+ format="relative"
71
+ />
72
+ </template>
73
+ </TranslationT>
64
74
  <RiSubtractLine
65
75
  aria-hidden="true"
66
76
  class="size-3 fill-gray-medium"
@@ -303,7 +313,8 @@ import { ref, computed, defineAsyncComponent } from 'vue'
303
313
  import { RiDownloadLine, RiFileCopyLine, RiFileWarningLine, RiSubtractLine } from '@remixicon/vue'
304
314
  import OrganizationNameWithCertificate from '../OrganizationNameWithCertificate.vue'
305
315
  import { filesize, summarize } from '../../functions/helpers'
306
- import { useFormatDate } from '../../functions/dates'
316
+ import FormattedDate from '../FormattedDate.vue'
317
+ import TranslationT from '../TranslationT.vue'
307
318
  import MarkdownViewer from '../MarkdownViewer.vue'
308
319
  import type { CommunityResource, Resource } from '../../types/resources'
309
320
  import type { Dataset, DatasetV2 } from '../../types/datasets'
@@ -355,7 +366,6 @@ const ImagePreview = defineAsyncComponent(() => import('./ImagePreview.client.vu
355
366
  const DatafairPreview = defineAsyncComponent(() => import('./Datafair.client.vue'))
356
367
 
357
368
  const { t } = useTranslation()
358
- const { formatRelativeIfRecentDate } = useFormatDate()
359
369
  const checkTabularData = useHasTabularData()
360
370
 
361
371
  const hasPreview = computed(() => {
@@ -18,9 +18,14 @@
18
18
  <span class="min-w-0 truncate font-bold text-gray-title">{{ dataset.title }}</span>
19
19
  <template v-if="dataset.last_update">
20
20
  <span class="hidden shrink-0 text-gray-medium sm:inline">·</span>
21
- <span class="hidden truncate text-gray-medium sm:inline">
22
- {{ t('mis à jour le {date}', { date: formatDate(dataset.last_update) }) }}
23
- </span>
21
+ <TranslationT
22
+ class="hidden truncate text-gray-medium sm:inline"
23
+ keypath="mis à jour le {date}"
24
+ >
25
+ <template #date>
26
+ <FormattedDate :date="dataset.last_update" />
27
+ </template>
28
+ </TranslationT>
24
29
  </template>
25
30
  </div>
26
31
  </div>
@@ -53,9 +58,10 @@ import Avatar from '../Avatar.vue'
53
58
  import BrandedButton from '../BrandedButton.vue'
54
59
  import OrganizationLogo from '../OrganizationLogo.vue'
55
60
  import ResourceMainAction from './ResourceMainAction.vue'
61
+ import FormattedDate from '../FormattedDate.vue'
62
+ import TranslationT from '../TranslationT.vue'
56
63
  import { getOwnerName } from '../../functions/owned'
57
64
  import { useTranslation } from '../../composables/useTranslation'
58
- import { useFormatDate } from '../../functions/dates'
59
65
  import type { Dataset, DatasetV2 } from '../../types/datasets'
60
66
  import type { Resource } from '../../types/resources'
61
67
 
@@ -66,7 +72,6 @@ const props = defineProps<{
66
72
  }>()
67
73
 
68
74
  const { t } = useTranslation()
69
- const { formatDate } = useFormatDate()
70
75
 
71
76
  // Plain producer name, matching the prototype: dark text, no link, no certificate
72
77
  // badge — unlike ObjectCardOwner which renders a styled link.
@@ -24,7 +24,14 @@
24
24
  carried by the separators' margins. -->
25
25
  <div class="min-w-0 truncate [flex-shrink:9999]">
26
26
  <span class="mr-1.5">·</span>
27
- <span :title="formatDate(resource.last_modified)">{{ t('mis à jour {date}', { date: formatRelativeIfRecentDate(resource.last_modified) }) }}</span>
27
+ <TranslationT keypath="mis à jour {date}">
28
+ <template #date>
29
+ <FormattedDate
30
+ :date="resource.last_modified"
31
+ format="relative"
32
+ />
33
+ </template>
34
+ </TranslationT>
28
35
  <template v-if="resourceFilesize">
29
36
  <span class="mx-1.5">·</span>
30
37
  <span>{{ filesize(resourceFilesize) }}</span>
@@ -84,10 +91,11 @@ import ResourceIcon from '../ResourceAccordion/ResourceIcon.vue'
84
91
  import SchemaBadge from '../ResourceAccordion/SchemaBadge.vue'
85
92
  import ResourceSelector from './ResourceSelector.vue'
86
93
  import ResourceMainAction from './ResourceMainAction.vue'
94
+ import FormattedDate from '../FormattedDate.vue'
95
+ import TranslationT from '../TranslationT.vue'
87
96
  import { filesize, summarize } from '../../functions/helpers'
88
97
  import { getResourceExternalUrl, getResourceFilesize } from '../../functions/resources'
89
98
  import { trackEvent } from '../../functions/matomo'
90
- import { useFormatDate } from '../../functions/dates'
91
99
  import { useTranslation } from '../../composables/useTranslation'
92
100
  import type { RouteLocationRaw } from 'vue-router'
93
101
  import type { Resource } from '../../types/resources'
@@ -108,7 +116,6 @@ const props = defineProps<{
108
116
  }>()
109
117
 
110
118
  const { t } = useTranslation()
111
- const { formatRelativeIfRecentDate, formatDate } = useFormatDate()
112
119
 
113
120
  const resourceFilesize = computed(() => getResourceFilesize(props.resource))
114
121
  const resourceExternalUrl = computed(() => getResourceExternalUrl(props.dataset, props.resource))