mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-06 23:32:08 +00:00
Merge pull request #158 from IliesAllali/fix/dates-region
Dates : ordre et format de la région du navigateur, mois en lettres (#152)
This commit is contained in:
6 files changed
+123
-15
No files matched your search
@@ -36,7 +36,7 @@ export default function DateField({ id, value, onChange, placeholder, className
|
||||
else setText(dateToInputText(parsed))
|
||||
}}
|
||||
/>
|
||||
{touched && invalid && <span className="pz-date-hint" role="status">{t('Une année (1950) ou une date (12/03/1950)')}</span>}
|
||||
{touched && invalid && <span className="pz-date-hint" role="status">{t('Une année (1950) ou une date ({exemple})', { exemple: dateToInputText('1950-03-12') })}</span>}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -34,7 +34,7 @@ function formatDate(value) {
|
||||
const options = day ? { day: 'numeric', month: 'long', year: 'numeric' } : { month: 'long', year: 'numeric' }
|
||||
const text = date.toLocaleDateString(dateLocale, { ...options, timeZone: 'UTC' })
|
||||
// « 1er janvier », pas « 1 janvier »
|
||||
return dateLocale === 'fr-FR' && day === 1 ? text.replace(/^1 /, '1er ') : text
|
||||
return dateLocale.startsWith('fr') && day === 1 ? text.replace(/^1 /, '1er ') : text
|
||||
}
|
||||
|
||||
// « Né le 24 août 1898 à Monségur (40) », accordé au sexe quand la source le donne
|
||||
|
||||
@@ -468,7 +468,7 @@ export default {
|
||||
'Merci, c’est envoyé. Le moment apparaîtra dans sa vie quand la personne qui gère l’arbre l’aura accepté.': 'Thanks, it has been sent. The moment will show in their life once the person who manages the tree has accepted it.',
|
||||
// Dates : année seule, décès sans date (issue #77)
|
||||
'Année ou date': 'Year or date',
|
||||
'Une année (1950) ou une date (12/03/1950)': 'A year (1950) or a date (12/03/1950)',
|
||||
'Une année (1950) ou une date ({exemple})': 'A year (1950) or a date ({exemple})',
|
||||
'Date inconnue': 'Date unknown',
|
||||
'date inconnue': 'date unknown',
|
||||
'Décédé(e), date inconnue': 'Deceased, date unknown',
|
||||
|
||||
@@ -32,8 +32,26 @@ function detectLocale() {
|
||||
|
||||
export const locale = typeof window === 'undefined' ? 'fr' : detectLocale()
|
||||
|
||||
// Pour toLocaleDateString et consorts.
|
||||
export const dateLocale = locale === 'fr' ? 'fr-FR' : 'en-GB'
|
||||
// Pour toLocaleDateString et consorts : la région du navigateur dans la langue de l'interface (en-US, fr-CA…),
|
||||
// sinon fr-FR ou en-GB. Elle décide de l'ordre jour, mois, année et du séparateur (issue #152).
|
||||
function regionalDateLocale() {
|
||||
const fallback = locale === 'fr' ? 'fr-FR' : 'en-GB'
|
||||
const languages = typeof navigator === 'undefined' ? [] : (navigator.languages?.length ? navigator.languages : [navigator.language])
|
||||
const regional = languages.find((lang) => new RegExp(`^${locale}-[a-z]{2}$`, 'i').test(String(lang)))
|
||||
if (!regional) return fallback
|
||||
try { return Intl.DateTimeFormat.supportedLocalesOf(regional)[0] || fallback } catch { return fallback }
|
||||
}
|
||||
export const dateLocale = regionalDateLocale()
|
||||
|
||||
// Une date en chiffres dans cette région : ordre ('dmy', 'mdy' ou 'ymd') et séparateur (« / », « - », « . »)
|
||||
export const dateFormat = (() => {
|
||||
try {
|
||||
const parts = new Intl.DateTimeFormat(dateLocale, { day: '2-digit', month: '2-digit', year: 'numeric' }).formatToParts(new Date(Date.UTC(2026, 9, 25)))
|
||||
const order = parts.filter((p) => ['day', 'month', 'year'].includes(p.type)).map((p) => p.type[0]).join('')
|
||||
const sep = parts.find((p) => p.type === 'literal')?.value.trim() || '/'
|
||||
return ['dmy', 'mdy', 'ymd'].includes(order) ? { order, sep } : { order: 'dmy', sep: '/' }
|
||||
} catch { return { order: 'dmy', sep: '/' } }
|
||||
})()
|
||||
|
||||
if (typeof document !== 'undefined') document.documentElement.lang = locale
|
||||
|
||||
|
||||
@@ -1,37 +1,75 @@
|
||||
// Saisie d'une date dans la fiche : une année seule (« 1950 ») ou une date complète (« 12/03/1950 »).
|
||||
// L'année seule se range au 1er janvier, la convention de toute l'appli (issue #77).
|
||||
// L'ordre et le séparateur suivent la région du navigateur : « 03/12/1950 » en en-US, « 1950-03-12 » en fr-CA
|
||||
// (issue #152). Le mois s'écrit aussi en lettres, en français ou en anglais : « 12 mars 1950 », « March 12, 1950 ».
|
||||
import { dateFormat } from '../i18n/index.js'
|
||||
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
|
||||
// Valeur stockée (« 1950-01-01 », « 1950 », « 1950-03-12 ») vers le texte du champ
|
||||
export function dateToInputText(value) {
|
||||
export function dateToInputText(value, { order, sep } = dateFormat) {
|
||||
const raw = String(value ?? '').trim()
|
||||
if (!raw) return ''
|
||||
const full = raw.match(/^(\d{4})-(\d{2})-(\d{2})/)
|
||||
if (full) return full[2] === '01' && full[3] === '01' ? full[1] : `${full[3]}/${full[2]}/${full[1]}`
|
||||
if (full) {
|
||||
const [, y, m, d] = full
|
||||
if (m === '01' && d === '01') return y
|
||||
const parts = { d, m, y }
|
||||
return order.split('').map((k) => parts[k]).join(sep)
|
||||
}
|
||||
const year = raw.match(/^(\d{4})$/)
|
||||
return year ? year[1] : raw
|
||||
}
|
||||
|
||||
// Chiffres seuls au-delà d'une année (5 à 8 chiffres) : on pose les barres, « 12031950 » → « 12/03/1950 »
|
||||
export function formatDigitsAsDate(text) {
|
||||
// Chiffres seuls au-delà d'une année (5 à 8 chiffres) : on pose les séparateurs, « 12031950 » → « 12/03/1950 »
|
||||
export function formatDigitsAsDate(text, { order, sep } = dateFormat) {
|
||||
const raw = String(text ?? '')
|
||||
if (!/^\d{5,8}$/.test(raw)) return raw
|
||||
return `${raw.slice(0, 2)}/${raw.slice(2, 4)}/${raw.slice(4)}`
|
||||
if (order === 'ymd') return [raw.slice(0, 4), raw.slice(4, 6), raw.slice(6)].filter(Boolean).join(sep)
|
||||
return `${raw.slice(0, 2)}${sep}${raw.slice(2, 4)}${sep}${raw.slice(4)}`
|
||||
}
|
||||
|
||||
// Noms des mois sans accents, en minuscules, dans les deux langues de l'appli
|
||||
const MONTHS = [
|
||||
['janvier', 'january'], ['fevrier', 'february'], ['mars', 'march'], ['avril', 'april'], ['mai', 'may'], ['juin', 'june'],
|
||||
['juillet', 'july'], ['aout', 'august'], ['septembre', 'september'], ['octobre', 'october'], ['novembre', 'november'], ['decembre', 'december'],
|
||||
]
|
||||
const normalize = (s) => s.normalize('NFD').replace(/\p{M}/gu, '').toLowerCase()
|
||||
|
||||
// « juin », « June », « sept. », « févr » : un mois si le mot (3 lettres au moins) n'en désigne qu'un
|
||||
function monthFromWord(word) {
|
||||
const w = normalize(word).replace(/\.$/, '')
|
||||
if (w.length < 3 || !/^[a-z]+$/.test(w)) return undefined
|
||||
const found = new Set(MONTHS.flatMap((names, i) => (names.some((n) => n.startsWith(w)) ? [i + 1] : [])))
|
||||
return found.size === 1 ? [...found][0] : undefined
|
||||
}
|
||||
|
||||
// « 12 juin 1950 », « 1er juin 1950 », « June 12, 1950 », « 12 June 1950 »
|
||||
function parseWritten(raw) {
|
||||
const words = raw.replace(/,/g, ' ').replace(/\b(\d{1,2})(er|st|nd|rd|th)\b/gi, '$1').split(/\s+/).filter(Boolean)
|
||||
if (words.length !== 3 || !/^\d{4}$/.test(words[2])) return undefined
|
||||
const [a, b, year] = words
|
||||
if (/^\d{1,2}$/.test(a) && monthFromWord(b)) return { day: a, month: monthFromWord(b), year }
|
||||
if (monthFromWord(a) && /^\d{1,2}$/.test(b)) return { day: b, month: monthFromWord(a), year }
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Texte du champ vers la valeur stockée : '' si vide, « AAAA-MM-JJ » si valable, undefined si on ne comprend pas
|
||||
export function parseDateInput(text) {
|
||||
export function parseDateInput(text, { order } = dateFormat) {
|
||||
const raw = String(text ?? '').trim()
|
||||
if (!raw) return ''
|
||||
if (/^\d{4}$/.test(raw)) return `${raw}-01-01`
|
||||
let day
|
||||
let month
|
||||
let year
|
||||
const dmy = raw.match(/^(\d{1,2})[/.\-\s](\d{1,2})[/.\-\s](\d{4})$/)
|
||||
const iso = raw.match(/^(\d{4})-(\d{2})-(\d{2})$/)
|
||||
if (dmy) [, day, month, year] = dmy
|
||||
else if (iso) [, year, month, day] = iso
|
||||
const yearLast = raw.match(/^(\d{1,2})[/.\-\s](\d{1,2})[/.\-\s](\d{4})$/)
|
||||
const yearFirst = raw.match(/^(\d{4})[/.\-\s](\d{1,2})[/.\-\s](\d{1,2})$/)
|
||||
const written = yearLast || yearFirst ? undefined : parseWritten(raw)
|
||||
// Année en dernier : le mois d'abord seulement dans une région qui l'écrit ainsi (en-US)
|
||||
if (yearLast && order === 'mdy') [, month, day, year] = yearLast
|
||||
else if (yearLast) [, day, month, year] = yearLast
|
||||
else if (yearFirst) [, year, month, day] = yearFirst
|
||||
else if (written) ({ day, month, year } = written)
|
||||
else return undefined
|
||||
const d = Number(day)
|
||||
const m = Number(month)
|
||||
|
||||
@@ -38,3 +38,55 @@ describe('formatDigitsAsDate', () => {
|
||||
expect(formatDigitsAsDate('1/3/1950')).toBe('1/3/1950')
|
||||
})
|
||||
})
|
||||
|
||||
const FR = { order: 'dmy', sep: '/' }
|
||||
const US = { order: 'mdy', sep: '/' }
|
||||
const CA = { order: 'ymd', sep: '-' }
|
||||
|
||||
describe('région du navigateur (issue #152)', () => {
|
||||
it('en-US : le mois avant le jour', () => {
|
||||
expect(dateToInputText('1950-03-12', US)).toBe('03/12/1950')
|
||||
expect(parseDateInput('03/12/1950', US)).toBe('1950-03-12')
|
||||
expect(parseDateInput('12/25/1950', US)).toBe('1950-12-25')
|
||||
expect(parseDateInput('25/12/1950', US)).toBeUndefined()
|
||||
expect(formatDigitsAsDate('03121950', US)).toBe('03/12/1950')
|
||||
})
|
||||
it('fr-CA : l année d abord, avec des tirets', () => {
|
||||
expect(dateToInputText('1950-03-12', CA)).toBe('1950-03-12')
|
||||
expect(parseDateInput('1950-3-12', CA)).toBe('1950-03-12')
|
||||
expect(formatDigitsAsDate('19500312', CA)).toBe('1950-03-12')
|
||||
expect(formatDigitsAsDate('19503', CA)).toBe('1950-3')
|
||||
})
|
||||
it('une année seule ne change nulle part', () => {
|
||||
for (const f of [FR, US, CA]) {
|
||||
expect(dateToInputText('1950-01-01', f)).toBe('1950')
|
||||
expect(dateToInputText('1950', f)).toBe('1950')
|
||||
expect(parseDateInput('1950', f)).toBe('1950-01-01')
|
||||
}
|
||||
})
|
||||
it('l année en premier se lit partout', () => {
|
||||
expect(parseDateInput('1950-03-12', FR)).toBe('1950-03-12')
|
||||
expect(parseDateInput('1950/03/12', US)).toBe('1950-03-12')
|
||||
})
|
||||
})
|
||||
|
||||
describe('mois en lettres, français ou anglais', () => {
|
||||
it('jour, mois, année', () => {
|
||||
expect(parseDateInput('12 juin 1924', FR)).toBe('1924-06-12')
|
||||
expect(parseDateInput('1er août 1950', FR)).toBe('1950-08-01')
|
||||
expect(parseDateInput('12 June 1924', US)).toBe('1924-06-12')
|
||||
expect(parseDateInput('3 sept. 1901', FR)).toBe('1901-09-03')
|
||||
expect(parseDateInput('3 févr 1901', FR)).toBe('1901-02-03')
|
||||
expect(parseDateInput('3 FEVRIER 1901', FR)).toBe('1901-02-03')
|
||||
})
|
||||
it('mois, jour, année', () => {
|
||||
expect(parseDateInput('June 12, 1924', US)).toBe('1924-06-12')
|
||||
expect(parseDateInput('Aug 1st 1950', FR)).toBe('1950-08-01')
|
||||
})
|
||||
it('refuse un mois ambigu ou une date impossible', () => {
|
||||
expect(parseDateInput('12 ju 1924', FR)).toBeUndefined()
|
||||
expect(parseDateInput('12 jui 1924', FR)).toBeUndefined()
|
||||
expect(parseDateInput('31 juin 1924', FR)).toBeUndefined()
|
||||
expect(parseDateInput('juin 1924', FR)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user