@dnax/ui 0.0.63 → 0.0.65
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.
- package/components/QDatePicker.vue +12 -5
- package/components/QTabs.vue +79 -2
- package/components/internal/QDateCalendar.vue +25 -23
- package/index.ts +16 -0
- package/lib/dateI18n.test.ts +91 -0
- package/lib/dateI18n.ts +85 -0
- package/lib/network.test.ts +199 -0
- package/lib/network.ts +147 -0
- package/lib/q.ts +6 -1
- package/lib/styleTokens.test.ts +32 -0
- package/lib/tabs.test.ts +35 -0
- package/lib/tabs.ts +38 -0
- package/lib/wakeLock.test.ts +219 -0
- package/lib/wakeLock.ts +202 -0
- package/package.json +1 -1
- package/runtime.ts +7 -0
- package/styles/main.css +143 -1
|
@@ -9,6 +9,8 @@ import { icons } from "../lib/icons"
|
|
|
9
9
|
import { cn } from "../lib/utils"
|
|
10
10
|
import { useOverlayBack } from "../lib/overlayBack"
|
|
11
11
|
import { placePopover, type PopoverPlacement } from "../lib/datePicker"
|
|
12
|
+
import { useConfigLang } from "../lib/useComponentProps"
|
|
13
|
+
import { dateLabels, dateLocale } from "../lib/dateI18n"
|
|
12
14
|
import QDateCalendar from "./internal/QDateCalendar.vue"
|
|
13
15
|
|
|
14
16
|
interface Props {
|
|
@@ -43,7 +45,7 @@ interface Props {
|
|
|
43
45
|
width?: string
|
|
44
46
|
/** Titre du panneau (modes à voile : sheet/modal/dialog) */
|
|
45
47
|
title?: string
|
|
46
|
-
/** Raccourci «
|
|
48
|
+
/** Raccourci « aujourd'hui » sous le calendrier (tous les modes) */
|
|
47
49
|
todayBtn?: boolean
|
|
48
50
|
/** En-tête : libellé cliquable ouvrant le choix du mois (tous les modes) */
|
|
49
51
|
monthDropdown?: boolean
|
|
@@ -75,6 +77,11 @@ const emit = defineEmits<{
|
|
|
75
77
|
clear: []
|
|
76
78
|
}>()
|
|
77
79
|
|
|
80
|
+
// Langue de l'app (`QConfigProvider lang`) : format d'affichage et libellés.
|
|
81
|
+
const configLang = useConfigLang()
|
|
82
|
+
const locale = computed(() => dateLocale(configLang.value))
|
|
83
|
+
const labels = computed(() => dateLabels(configLang.value))
|
|
84
|
+
|
|
78
85
|
/** Champ déclencheur (tout sauf `inline`) */
|
|
79
86
|
const isPanelMode = computed(() => props.mode !== "inline")
|
|
80
87
|
/** Panneau ancré au champ : ni voile, ni blocage du scroll, ni retour navigateur */
|
|
@@ -98,7 +105,7 @@ const formatFn = computed(
|
|
|
98
105
|
() =>
|
|
99
106
|
props.format ??
|
|
100
107
|
((d: Date) =>
|
|
101
|
-
new Intl.DateTimeFormat(
|
|
108
|
+
new Intl.DateTimeFormat(locale.value, { day: "2-digit", month: "short", year: "numeric" }).format(d)),
|
|
102
109
|
)
|
|
103
110
|
|
|
104
111
|
const displayText = computed(() => (props.modelValue ? formatFn.value(props.modelValue) : ""))
|
|
@@ -139,7 +146,7 @@ const closePopup = () => {
|
|
|
139
146
|
open.value = false
|
|
140
147
|
}
|
|
141
148
|
|
|
142
|
-
const sheetTitle = computed(() => props.title || props.label ||
|
|
149
|
+
const sheetTitle = computed(() => props.title || props.label || labels.value.selectDate)
|
|
143
150
|
|
|
144
151
|
const sheetStyle = computed<Record<string, string> | undefined>(() =>
|
|
145
152
|
props.width ? { width: props.width, maxWidth: props.width } : undefined,
|
|
@@ -263,7 +270,7 @@ onBeforeUnmount(() => {
|
|
|
263
270
|
v-if="clearable && modelValue && !disable && !readonly"
|
|
264
271
|
class="q-field__clear"
|
|
265
272
|
type="button"
|
|
266
|
-
aria-label="
|
|
273
|
+
:aria-label="labels.clear"
|
|
267
274
|
@click.stop="onClear"
|
|
268
275
|
>
|
|
269
276
|
<Icon :icon="icons.x" aria-hidden="true" />
|
|
@@ -304,7 +311,7 @@ onBeforeUnmount(() => {
|
|
|
304
311
|
<button
|
|
305
312
|
type="button"
|
|
306
313
|
class="q-date-picker__sheet-close"
|
|
307
|
-
:aria-label="
|
|
314
|
+
:aria-label="`${labels.close} ${sheetTitle}`"
|
|
308
315
|
@click="closePopup"
|
|
309
316
|
>
|
|
310
317
|
<Icon :icon="icons.x" aria-hidden="true" />
|
package/components/QTabs.vue
CHANGED
|
@@ -25,9 +25,12 @@ export const qTabsKey: InjectionKey<QTabContext> = Symbol("q-tabs")
|
|
|
25
25
|
|
|
26
26
|
<script setup lang="ts">
|
|
27
27
|
import { computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, provide, ref, shallowRef, watch } from "vue"
|
|
28
|
+
import { Icon } from "@iconify/vue"
|
|
28
29
|
import { cva } from "class-variance-authority"
|
|
29
30
|
import { cn } from "../lib/utils"
|
|
30
31
|
import { colorValue } from "../lib/colors"
|
|
32
|
+
import { icons } from "../lib/icons"
|
|
33
|
+
import { tabArrowState, tabArrowStep } from "../lib/tabs"
|
|
31
34
|
|
|
32
35
|
const tabsVariants = cva("q-tabs", {
|
|
33
36
|
variants: {
|
|
@@ -70,6 +73,12 @@ interface Props {
|
|
|
70
73
|
switchIndicatorPosition?: boolean | "top" | "bottom"
|
|
71
74
|
/** Tabs en colonne (verticales) */
|
|
72
75
|
vertical?: boolean
|
|
76
|
+
/**
|
|
77
|
+
* **Flèches de défilement** quand les tabs débordent (comportement Quasar, défaut `true`) :
|
|
78
|
+
* le contenu devient défilable et deux boutons apparaissent aux extrémités débordantes.
|
|
79
|
+
* `false` → aucun défilement (les tabs débordent, comme avant).
|
|
80
|
+
*/
|
|
81
|
+
showArrows?: boolean
|
|
73
82
|
/** Ne pas s'étendre (usage dans un QToolbar) */
|
|
74
83
|
shrink?: boolean
|
|
75
84
|
/** S'étendre sur l'axe croisé du parent flex */
|
|
@@ -106,6 +115,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
106
115
|
noCaps: false,
|
|
107
116
|
switchIndicatorPosition: undefined,
|
|
108
117
|
vertical: false,
|
|
118
|
+
showArrows: true,
|
|
109
119
|
shrink: false,
|
|
110
120
|
stretch: false,
|
|
111
121
|
contentClass: "",
|
|
@@ -148,10 +158,39 @@ const tabRegs = shallowRef<QTabRegistration[]>([])
|
|
|
148
158
|
const tick = ref(0)
|
|
149
159
|
const rootEl = ref<HTMLElement | null>(null)
|
|
150
160
|
|
|
151
|
-
//
|
|
161
|
+
// — Flèches de défilement (`show-arrows`, comportement Quasar) —
|
|
162
|
+
const contentEl = ref<HTMLElement | null>(null)
|
|
163
|
+
const canScrollStart = ref(false)
|
|
164
|
+
const canScrollEnd = ref(false)
|
|
165
|
+
let contentObserver: ResizeObserver | null = null
|
|
166
|
+
|
|
167
|
+
/** Met à jour la visibilité des flèches depuis les mesures de défilement (axe courant). */
|
|
168
|
+
const updateArrows = () => {
|
|
169
|
+
const el = contentEl.value
|
|
170
|
+
if (!el) return
|
|
171
|
+
const state = props.vertical
|
|
172
|
+
? tabArrowState(el.scrollTop, el.clientHeight, el.scrollHeight)
|
|
173
|
+
: tabArrowState(el.scrollLeft, el.clientWidth, el.scrollWidth)
|
|
174
|
+
canScrollStart.value = state.canStart
|
|
175
|
+
canScrollEnd.value = state.canEnd
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Fait défiler d'un cran (≈ 60 % de la zone visible) — fluide. */
|
|
179
|
+
const scrollArrows = (dir: -1 | 1) => {
|
|
180
|
+
const el = contentEl.value
|
|
181
|
+
if (!el) return
|
|
182
|
+
const delta = tabArrowStep(props.vertical ? el.clientHeight : el.clientWidth) * dir
|
|
183
|
+
el.scrollBy(props.vertical ? { top: delta, behavior: "smooth" } : { left: delta, behavior: "smooth" })
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Débordement réel → les deux flèches existent (celle du côté bloqué est **désactivée**). */
|
|
187
|
+
const scrollable = computed(() => canScrollStart.value || canScrollEnd.value)
|
|
188
|
+
|
|
189
|
+
// Recalcule la position de l'indicateur (et les flèches) après chaque changement de DOM
|
|
152
190
|
const measure = async () => {
|
|
153
191
|
await nextTick()
|
|
154
192
|
tick.value++
|
|
193
|
+
updateArrows()
|
|
155
194
|
}
|
|
156
195
|
|
|
157
196
|
// collapse-inactive : le label s'étend/retombe via une transition max-width
|
|
@@ -169,10 +208,18 @@ onMounted(() => {
|
|
|
169
208
|
measure()
|
|
170
209
|
if (typeof window !== "undefined") window.addEventListener("resize", measure)
|
|
171
210
|
rootEl.value?.addEventListener("transitionend", onCollapseEnd)
|
|
211
|
+
// Le conteneur peut changer de largeur sans resize fenêtre (sidebar, flex…) : les
|
|
212
|
+
// flèches doivent suivre le débordement réel.
|
|
213
|
+
if (typeof ResizeObserver !== "undefined" && contentEl.value) {
|
|
214
|
+
contentObserver = new ResizeObserver(updateArrows)
|
|
215
|
+
contentObserver.observe(contentEl.value)
|
|
216
|
+
}
|
|
172
217
|
})
|
|
173
218
|
onBeforeUnmount(() => {
|
|
174
219
|
if (typeof window !== "undefined") window.removeEventListener("resize", measure)
|
|
175
220
|
rootEl.value?.removeEventListener("transitionend", onCollapseEnd)
|
|
221
|
+
contentObserver?.disconnect()
|
|
222
|
+
contentObserver = null
|
|
176
223
|
})
|
|
177
224
|
|
|
178
225
|
const context: QTabContext = {
|
|
@@ -240,6 +287,11 @@ const tabsClasses = computed(() =>
|
|
|
240
287
|
props.animated && "q-tabs--animated",
|
|
241
288
|
props.animated && `q-tabs--anim-${props.transition}`,
|
|
242
289
|
props.collapseInactive && "q-tabs--collapse-inactive",
|
|
290
|
+
props.showArrows && "q-tabs--arrows",
|
|
291
|
+
// Fondu des bords : uniquement du côté où il reste du contenu à révéler (le CSS
|
|
292
|
+
// n'utilise qu'un masque alpha → rendu identique en thème clair et sombre).
|
|
293
|
+
props.showArrows && canScrollStart.value && "q-tabs--scroll-start",
|
|
294
|
+
props.showArrows && canScrollEnd.value && "q-tabs--scroll-end",
|
|
243
295
|
),
|
|
244
296
|
)
|
|
245
297
|
</script>
|
|
@@ -253,9 +305,34 @@ const tabsClasses = computed(() =>
|
|
|
253
305
|
role="tablist"
|
|
254
306
|
:aria-orientation="vertical ? 'vertical' : 'horizontal'"
|
|
255
307
|
>
|
|
256
|
-
<
|
|
308
|
+
<button
|
|
309
|
+
v-if="showArrows && scrollable"
|
|
310
|
+
type="button"
|
|
311
|
+
class="q-tabs__arrow q-tabs__arrow--start"
|
|
312
|
+
:disabled="!canScrollStart"
|
|
313
|
+
:aria-label="vertical ? 'Defiler les onglets vers le haut' : 'Defiler les onglets vers la gauche'"
|
|
314
|
+
@click="scrollArrows(-1)"
|
|
315
|
+
>
|
|
316
|
+
<Icon :icon="vertical ? icons.chevronUp : icons.chevronLeft" aria-hidden="true" />
|
|
317
|
+
</button>
|
|
318
|
+
<div
|
|
319
|
+
ref="contentEl"
|
|
320
|
+
class="q-tabs__content"
|
|
321
|
+
:class="contentClass"
|
|
322
|
+
@scroll="updateArrows"
|
|
323
|
+
>
|
|
257
324
|
<slot />
|
|
258
325
|
<div class="q-tabs__indicator" :style="indicatorStyle" aria-hidden="true" />
|
|
259
326
|
</div>
|
|
327
|
+
<button
|
|
328
|
+
v-if="showArrows && scrollable"
|
|
329
|
+
type="button"
|
|
330
|
+
class="q-tabs__arrow q-tabs__arrow--end"
|
|
331
|
+
:disabled="!canScrollEnd"
|
|
332
|
+
:aria-label="vertical ? 'Defiler les onglets vers le bas' : 'Defiler les onglets vers la droite'"
|
|
333
|
+
@click="scrollArrows(1)"
|
|
334
|
+
>
|
|
335
|
+
<Icon :icon="vertical ? icons.chevronDown : icons.chevronRight" aria-hidden="true" />
|
|
336
|
+
</button>
|
|
260
337
|
</div>
|
|
261
338
|
</template>
|
|
@@ -4,6 +4,14 @@ import { computed, ref, watch } from "vue"
|
|
|
4
4
|
import { Icon } from "@iconify/vue"
|
|
5
5
|
import { icons } from "../../lib/icons"
|
|
6
6
|
import { cn } from "../../lib/utils"
|
|
7
|
+
import { useConfigLang } from "../../lib/useComponentProps"
|
|
8
|
+
import {
|
|
9
|
+
dateLabels,
|
|
10
|
+
dateLocale,
|
|
11
|
+
monthShortNames,
|
|
12
|
+
monthYearLabel,
|
|
13
|
+
weekdayShortNames,
|
|
14
|
+
} from "../../lib/dateI18n"
|
|
7
15
|
|
|
8
16
|
interface Props {
|
|
9
17
|
modelValue?: Date | null
|
|
@@ -15,7 +23,7 @@ interface Props {
|
|
|
15
23
|
disabledDates?: (d: Date) => boolean
|
|
16
24
|
/** Premier jour de la semaine : 0 = dimanche, 1 = lundi (défaut) */
|
|
17
25
|
firstDayOfWeek?: number
|
|
18
|
-
/**
|
|
26
|
+
/** Libellé du raccourci « aujourd'hui » (texte selon `lang`) */
|
|
19
27
|
todayBtn?: boolean
|
|
20
28
|
/** En-tête : libellé cliquable ouvrant le choix du mois (et de l'année) */
|
|
21
29
|
monthDropdown?: boolean
|
|
@@ -30,6 +38,11 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
30
38
|
|
|
31
39
|
const emit = defineEmits<{ select: [date: Date] }>()
|
|
32
40
|
|
|
41
|
+
// Langue de l'app (`QConfigProvider lang`) : mois, jours et libellés suivent.
|
|
42
|
+
const configLang = useConfigLang()
|
|
43
|
+
const locale = computed(() => dateLocale(configLang.value))
|
|
44
|
+
const labels = computed(() => dateLabels(configLang.value))
|
|
45
|
+
|
|
33
46
|
const today = new Date()
|
|
34
47
|
const viewYear = ref(props.modelValue?.getFullYear() ?? today.getFullYear())
|
|
35
48
|
const viewMonth = ref(props.modelValue?.getMonth() ?? today.getMonth())
|
|
@@ -51,10 +64,7 @@ const startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDat
|
|
|
51
64
|
const sameDay = (a: Date, b: Date) =>
|
|
52
65
|
a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
|
|
53
66
|
|
|
54
|
-
const weekdayLabels = computed(() =>
|
|
55
|
-
const names = ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam"] as const
|
|
56
|
-
return Array.from({ length: 7 }, (_, i) => names[(props.firstDayOfWeek + i) % 7])
|
|
57
|
-
})
|
|
67
|
+
const weekdayLabels = computed(() => weekdayShortNames(locale.value, props.firstDayOfWeek))
|
|
58
68
|
|
|
59
69
|
const weeks = computed<(Date | null)[][]>(() => {
|
|
60
70
|
const y = viewYear.value
|
|
@@ -70,18 +80,10 @@ const weeks = computed<(Date | null)[][]>(() => {
|
|
|
70
80
|
return result
|
|
71
81
|
})
|
|
72
82
|
|
|
73
|
-
const monthLabel = computed(() =>
|
|
74
|
-
new Intl.DateTimeFormat(undefined, { month: "long", year: "numeric" }).format(
|
|
75
|
-
new Date(viewYear.value, viewMonth.value, 1),
|
|
76
|
-
),
|
|
77
|
-
)
|
|
83
|
+
const monthLabel = computed(() => monthYearLabel(locale.value, viewYear.value, viewMonth.value))
|
|
78
84
|
|
|
79
|
-
/** Noms courts des 12 mois, dans la langue
|
|
80
|
-
const monthNames = computed(() =>
|
|
81
|
-
Array.from({ length: 12 }, (_, i) =>
|
|
82
|
-
new Intl.DateTimeFormat(undefined, { month: "short" }).format(new Date(2000, i, 1)),
|
|
83
|
-
),
|
|
84
|
-
)
|
|
85
|
+
/** Noms courts des 12 mois, dans la langue de l'app (comme `monthLabel`) */
|
|
86
|
+
const monthNames = computed(() => monthShortNames(locale.value))
|
|
85
87
|
|
|
86
88
|
const prevMonth = () => {
|
|
87
89
|
if (viewMonth.value === 0) {
|
|
@@ -126,7 +128,7 @@ const onSelectDate = (d: Date) => {
|
|
|
126
128
|
emit("select", d)
|
|
127
129
|
}
|
|
128
130
|
|
|
129
|
-
/** Raccourci «
|
|
131
|
+
/** Raccourci « aujourd'hui » : désactivé quand aujourd'hui est hors bornes / désactivé */
|
|
130
132
|
const todaySelectable = computed(() => !isDisabled(startOfDay(new Date())))
|
|
131
133
|
|
|
132
134
|
const selectToday = () => {
|
|
@@ -141,7 +143,7 @@ const selectToday = () => {
|
|
|
141
143
|
<button
|
|
142
144
|
type="button"
|
|
143
145
|
class="q-date-calendar__nav-btn"
|
|
144
|
-
aria-label="
|
|
146
|
+
:aria-label="labels.prevMonth"
|
|
145
147
|
@click="prevMonth"
|
|
146
148
|
>
|
|
147
149
|
<Icon :icon="icons.chevronLeft" aria-hidden="true" />
|
|
@@ -152,7 +154,7 @@ const selectToday = () => {
|
|
|
152
154
|
class="q-date-calendar__nav-label q-date-calendar__nav-label--button"
|
|
153
155
|
:class="{ 'q-date-calendar__nav-label--open': monthPickerOpen }"
|
|
154
156
|
:aria-expanded="monthPickerOpen"
|
|
155
|
-
aria-label="
|
|
157
|
+
:aria-label="labels.chooseMonth"
|
|
156
158
|
@click="monthPickerOpen = !monthPickerOpen"
|
|
157
159
|
>
|
|
158
160
|
<span class="q-date-calendar__nav-text">{{ monthLabel }}</span>
|
|
@@ -162,7 +164,7 @@ const selectToday = () => {
|
|
|
162
164
|
<button
|
|
163
165
|
type="button"
|
|
164
166
|
class="q-date-calendar__nav-btn"
|
|
165
|
-
aria-label="
|
|
167
|
+
:aria-label="labels.nextMonth"
|
|
166
168
|
@click="nextMonth"
|
|
167
169
|
>
|
|
168
170
|
<Icon :icon="icons.chevronRight" aria-hidden="true" />
|
|
@@ -175,7 +177,7 @@ const selectToday = () => {
|
|
|
175
177
|
<button
|
|
176
178
|
type="button"
|
|
177
179
|
class="q-date-calendar__nav-btn"
|
|
178
|
-
aria-label="
|
|
180
|
+
:aria-label="labels.prevYear"
|
|
179
181
|
@click="prevYear"
|
|
180
182
|
>
|
|
181
183
|
<Icon :icon="icons.chevronLeft" aria-hidden="true" />
|
|
@@ -184,7 +186,7 @@ const selectToday = () => {
|
|
|
184
186
|
<button
|
|
185
187
|
type="button"
|
|
186
188
|
class="q-date-calendar__nav-btn"
|
|
187
|
-
aria-label="
|
|
189
|
+
:aria-label="labels.nextYear"
|
|
188
190
|
@click="nextYear"
|
|
189
191
|
>
|
|
190
192
|
<Icon :icon="icons.chevronRight" aria-hidden="true" />
|
|
@@ -239,7 +241,7 @@ const selectToday = () => {
|
|
|
239
241
|
:disabled="!todaySelectable"
|
|
240
242
|
@click="selectToday"
|
|
241
243
|
>
|
|
242
|
-
|
|
244
|
+
{{ labels.today }}
|
|
243
245
|
</button>
|
|
244
246
|
</div>
|
|
245
247
|
</div>
|
package/index.ts
CHANGED
|
@@ -164,6 +164,22 @@ export { screen } from "./lib/screen"
|
|
|
164
164
|
export type { QScreen } from "./lib/screen"
|
|
165
165
|
export { loading } from "./lib/loading"
|
|
166
166
|
export type { LoadingOptions, QLoadingPlugin } from "./lib/loading"
|
|
167
|
+
export { network, createNetwork, resolveOnline, onlineAfterEvent } from "./lib/network"
|
|
168
|
+
export type { QNetwork, NetworkEvent, NetworkOptions, NetworkEventTarget, NetworkNavigator } from "./lib/network"
|
|
169
|
+
export {
|
|
170
|
+
wakeLock,
|
|
171
|
+
createWakeLock,
|
|
172
|
+
isWakeLockCapable,
|
|
173
|
+
isDocumentVisible,
|
|
174
|
+
shouldReacquire,
|
|
175
|
+
} from "./lib/wakeLock"
|
|
176
|
+
export type {
|
|
177
|
+
QWakeLock,
|
|
178
|
+
WakeLockOptions,
|
|
179
|
+
WakeLockApiLike,
|
|
180
|
+
WakeLockSentinelLike,
|
|
181
|
+
WakeLockVisibilityLike,
|
|
182
|
+
} from "./lib/wakeLock"
|
|
167
183
|
export { localStorage, sessionStorage } from "./lib/storage"
|
|
168
184
|
export type { QWebStorage } from "./lib/storage"
|
|
169
185
|
export {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Libellés / locale des composants de date — pilotés par le `lang` du provider.
|
|
2
|
+
import { describe, expect, it } from "bun:test"
|
|
3
|
+
import {
|
|
4
|
+
dateLabels,
|
|
5
|
+
dateLocale,
|
|
6
|
+
monthShortNames,
|
|
7
|
+
monthYearLabel,
|
|
8
|
+
weekdayShortNames,
|
|
9
|
+
} from "./dateI18n"
|
|
10
|
+
|
|
11
|
+
describe("dateLocale", () => {
|
|
12
|
+
it("mappe la langue de l'app sur une locale BCP 47 déterministe", () => {
|
|
13
|
+
expect(dateLocale("fr")).toBe("fr-FR")
|
|
14
|
+
expect(dateLocale("en")).toBe("en-US")
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe("dateLabels", () => {
|
|
19
|
+
it("« Today » devient « Aujourd'hui » en français", () => {
|
|
20
|
+
expect(dateLabels("fr").today).toBe("Aujourd'hui")
|
|
21
|
+
expect(dateLabels("en").today).toBe("Today")
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it("couvre les libellés du panneau et de la navigation", () => {
|
|
25
|
+
expect(dateLabels("fr").selectDate).toBe("Sélectionner une date")
|
|
26
|
+
expect(dateLabels("fr").clear).toBe("Effacer")
|
|
27
|
+
expect(dateLabels("fr").close).toBe("Fermer")
|
|
28
|
+
expect(dateLabels("fr").prevMonth).toBe("Mois précédent")
|
|
29
|
+
expect(dateLabels("fr").nextMonth).toBe("Mois suivant")
|
|
30
|
+
expect(dateLabels("en").selectDate).toBe("Select a date")
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("retombe sur l'anglais pour une langue inconnue (JS non typé)", () => {
|
|
34
|
+
expect(dateLabels("de" as never).today).toBe("Today")
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe("weekdayShortNames", () => {
|
|
39
|
+
it("suit la locale demandée, pas celle du navigateur", () => {
|
|
40
|
+
expect(weekdayShortNames("fr-FR", 1)[0]).toMatch(/^lun/i)
|
|
41
|
+
expect(weekdayShortNames("en-US", 1)[0]).toMatch(/^mon/i)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("commence par le jour demandé (0 = dimanche)", () => {
|
|
45
|
+
const monday = weekdayShortNames("en-US", 1)
|
|
46
|
+
const sunday = weekdayShortNames("en-US", 0)
|
|
47
|
+
expect(sunday[0]).toMatch(/^sun/i)
|
|
48
|
+
expect(sunday[0]).toBe(monday[6])
|
|
49
|
+
expect(sunday[6]).toBe(monday[5])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("rend 7 jours sans point final ni minuscule initiale", () => {
|
|
53
|
+
const names = weekdayShortNames("fr-FR")
|
|
54
|
+
expect(names).toHaveLength(7)
|
|
55
|
+
for (const name of names) {
|
|
56
|
+
expect(name.endsWith(".")).toBe(false)
|
|
57
|
+
expect(name[0]).toBe(name[0]!.toUpperCase())
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe("monthShortNames", () => {
|
|
63
|
+
it("rend les 12 mois dans la langue demandée", () => {
|
|
64
|
+
const fr = monthShortNames("fr-FR")
|
|
65
|
+
const en = monthShortNames("en-US")
|
|
66
|
+
expect(fr).toHaveLength(12)
|
|
67
|
+
expect(en).toEqual([
|
|
68
|
+
"Jan",
|
|
69
|
+
"Feb",
|
|
70
|
+
"Mar",
|
|
71
|
+
"Apr",
|
|
72
|
+
"May",
|
|
73
|
+
"Jun",
|
|
74
|
+
"Jul",
|
|
75
|
+
"Aug",
|
|
76
|
+
"Sep",
|
|
77
|
+
"Oct",
|
|
78
|
+
"Nov",
|
|
79
|
+
"Dec",
|
|
80
|
+
])
|
|
81
|
+
expect(fr[0]).toMatch(/^janv/i)
|
|
82
|
+
expect(fr[11]).toMatch(/^déc/i)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe("monthYearLabel", () => {
|
|
87
|
+
it("en-tête long dans la langue demandée", () => {
|
|
88
|
+
expect(monthYearLabel("en-US", 2026, 0)).toBe("January 2026")
|
|
89
|
+
expect(monthYearLabel("fr-FR", 2026, 0)).toContain("janvier 2026")
|
|
90
|
+
})
|
|
91
|
+
})
|
package/lib/dateI18n.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Libellés et locale des composants de date (QDatePicker / QDateCalendar).
|
|
2
|
+
//
|
|
3
|
+
// La langue vient du `QConfigProvider` (`lang`, défaut "en") et arrive ici via
|
|
4
|
+
// `useConfigLang()`. On ne s'appuie **pas** sur la locale du navigateur : le rendu
|
|
5
|
+
// doit être déterministe (serveur ↔ client) et suivre la langue choisie dans l'app.
|
|
6
|
+
import type { QAppLang } from "./config"
|
|
7
|
+
|
|
8
|
+
/** Clés d'interface des composants de date. */
|
|
9
|
+
export type DateUiKey =
|
|
10
|
+
| "today"
|
|
11
|
+
| "prevMonth"
|
|
12
|
+
| "nextMonth"
|
|
13
|
+
| "chooseMonth"
|
|
14
|
+
| "prevYear"
|
|
15
|
+
| "nextYear"
|
|
16
|
+
| "selectDate"
|
|
17
|
+
| "clear"
|
|
18
|
+
| "close"
|
|
19
|
+
|
|
20
|
+
const DATE_UI: Record<QAppLang, Record<DateUiKey, string>> = {
|
|
21
|
+
en: {
|
|
22
|
+
today: "Today",
|
|
23
|
+
prevMonth: "Previous month",
|
|
24
|
+
nextMonth: "Next month",
|
|
25
|
+
chooseMonth: "Choose month",
|
|
26
|
+
prevYear: "Previous year",
|
|
27
|
+
nextYear: "Next year",
|
|
28
|
+
selectDate: "Select a date",
|
|
29
|
+
clear: "Clear",
|
|
30
|
+
close: "Close",
|
|
31
|
+
},
|
|
32
|
+
fr: {
|
|
33
|
+
today: "Aujourd'hui",
|
|
34
|
+
prevMonth: "Mois précédent",
|
|
35
|
+
nextMonth: "Mois suivant",
|
|
36
|
+
chooseMonth: "Choisir le mois",
|
|
37
|
+
prevYear: "Année précédente",
|
|
38
|
+
nextYear: "Année suivante",
|
|
39
|
+
selectDate: "Sélectionner une date",
|
|
40
|
+
clear: "Effacer",
|
|
41
|
+
close: "Fermer",
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Locale BCP 47 associée à la langue de l'app (déterministe, contrairement à `undefined`). */
|
|
46
|
+
export function dateLocale(lang: QAppLang): string {
|
|
47
|
+
return lang === "fr" ? "fr-FR" : "en-US"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Libellés d'interface des composants de date. */
|
|
51
|
+
export function dateLabels(lang: QAppLang): Record<DateUiKey, string> {
|
|
52
|
+
return DATE_UI[lang] ?? DATE_UI.en
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Abréviation : initiale en majuscule, sans point final (`dim.` → `Dim`). */
|
|
56
|
+
function shortName(value: string): string {
|
|
57
|
+
const clean = value.replace(/[.,]+$/, "").trim()
|
|
58
|
+
return clean.charAt(0).toUpperCase() + clean.slice(1)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Jours de la semaine abrégés, en commençant par `firstDayOfWeek`
|
|
63
|
+
* (0 = dimanche, 1 = lundi).
|
|
64
|
+
*/
|
|
65
|
+
export function weekdayShortNames(locale: string, firstDayOfWeek = 1): string[] {
|
|
66
|
+
const fmt = new Intl.DateTimeFormat(locale, { weekday: "short" })
|
|
67
|
+
// 2024-01-07 est un dimanche — référence stable, indépendante de la date du jour.
|
|
68
|
+
const names = Array.from({ length: 7 }, (_, i) =>
|
|
69
|
+
shortName(fmt.format(new Date(2024, 0, 7 + i))),
|
|
70
|
+
)
|
|
71
|
+
const start = ((firstDayOfWeek % 7) + 7) % 7
|
|
72
|
+
return Array.from({ length: 7 }, (_, i) => names[(start + i) % 7]!)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Mois abrégés, dans l'ordre de l'année (`Janv.` / `Jan`). */
|
|
76
|
+
export function monthShortNames(locale: string): string[] {
|
|
77
|
+
const fmt = new Intl.DateTimeFormat(locale, { month: "short" })
|
|
78
|
+
return Array.from({ length: 12 }, (_, i) => shortName(fmt.format(new Date(2024, i, 1))))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** En-tête du calendrier : « janvier 2026 » / « January 2026 ». */
|
|
82
|
+
export function monthYearLabel(locale: string, year: number, month: number): string {
|
|
83
|
+
return new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" })
|
|
84
|
+
.format(new Date(year, month, 1))
|
|
85
|
+
}
|