F8: Date formatting uses Intl per app language
formatDate special-cased only French, so Danish users saw US mm/dd/yyyy; formatDateTime mixed a manual French format with toLocaleString() (OS-locale dependent) for everyone else. Both now use Intl.DateTimeFormat with a fixed app-language -> BCP-47 map (en-US, fr-FR, da-DK — same mapping as useSpeech), 2-digit day/month so output stays deterministic per language: en 03/15/2026, fr 15/03/2026, da 15.03.2026. Adds a Danish format test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e3847edae1
commit
166cf87e95
|
|
@ -1,20 +1,24 @@
|
||||||
import i18next from 'i18next'
|
import i18next from 'i18next'
|
||||||
|
|
||||||
|
// App language → BCP-47 tag (same mapping as useSpeech.js). Fixed tags keep
|
||||||
|
// the output deterministic per app language regardless of the OS locale, so
|
||||||
|
// tests can assert it (en: 03/15/2026, fr: 15/03/2026, da: 15.03.2026).
|
||||||
|
const LANG_MAP = { en: 'en-US', fr: 'fr-FR', da: 'da-DK' }
|
||||||
|
|
||||||
|
function toLangTag(lang) {
|
||||||
|
return LANG_MAP[lang || i18next.language] || LANG_MAP.en
|
||||||
|
}
|
||||||
|
|
||||||
export function formatDate(date, lang) {
|
export function formatDate(date, lang) {
|
||||||
if (!date) return ''
|
if (!date) return ''
|
||||||
const d = date instanceof Date ? date : new Date(date)
|
const d = date instanceof Date ? date : new Date(date)
|
||||||
if (isNaN(d.getTime())) return ''
|
if (isNaN(d.getTime())) return ''
|
||||||
|
|
||||||
lang = lang || i18next.language || 'en'
|
return new Intl.DateTimeFormat(toLangTag(lang), {
|
||||||
const pad = (n) => String(n).padStart(2, '0')
|
day: '2-digit',
|
||||||
const dd = pad(d.getDate())
|
month: '2-digit',
|
||||||
const mm = pad(d.getMonth() + 1)
|
year: 'numeric',
|
||||||
const yyyy = d.getFullYear()
|
}).format(d)
|
||||||
|
|
||||||
if (lang === 'fr') {
|
|
||||||
return `${dd}/${mm}/${yyyy}`
|
|
||||||
}
|
|
||||||
return `${mm}/${dd}/${yyyy}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatItemDuration(ms) {
|
export function formatItemDuration(ms) {
|
||||||
|
|
@ -46,17 +50,12 @@ export function formatDateTime(date, lang) {
|
||||||
const d = date instanceof Date ? date : new Date(date)
|
const d = date instanceof Date ? date : new Date(date)
|
||||||
if (isNaN(d.getTime())) return ''
|
if (isNaN(d.getTime())) return ''
|
||||||
|
|
||||||
lang = lang || i18next.language || 'en'
|
return new Intl.DateTimeFormat(toLangTag(lang), {
|
||||||
const pad = (n) => String(n).padStart(2, '0')
|
day: '2-digit',
|
||||||
const dd = pad(d.getDate())
|
month: '2-digit',
|
||||||
const mm = pad(d.getMonth() + 1)
|
year: 'numeric',
|
||||||
const yyyy = d.getFullYear()
|
hour: '2-digit',
|
||||||
const HH = pad(d.getHours())
|
minute: '2-digit',
|
||||||
const min = pad(d.getMinutes())
|
second: '2-digit',
|
||||||
const ss = pad(d.getSeconds())
|
}).format(d)
|
||||||
|
|
||||||
if (lang === 'fr') {
|
|
||||||
return `${dd}/${mm}/${yyyy}, ${HH}:${min}:${ss}`
|
|
||||||
}
|
|
||||||
return d.toLocaleString()
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ Step 9 — Statistics
|
||||||
- test_stats_shows_empty_when_no_date: StatsView shows empty message when no creation date
|
- test_stats_shows_empty_when_no_date: StatsView shows empty message when no creation date
|
||||||
- test_stats_date_format_english: Date is formatted as MM/DD/YYYY in English
|
- test_stats_date_format_english: Date is formatted as MM/DD/YYYY in English
|
||||||
- test_stats_date_format_french: Date is formatted as DD/MM/YYYY in French
|
- test_stats_date_format_french: Date is formatted as DD/MM/YYYY in French
|
||||||
|
- test_stats_date_format_danish: Date is formatted as DD.MM.YYYY in Danish
|
||||||
- test_stats_date_format_updates_on_language_change: Switching language updates the date format reactively
|
- test_stats_date_format_updates_on_language_change: Switching language updates the date format reactively
|
||||||
- test_stats_date_range_tabs_visible: All four date range tabs are rendered
|
- test_stats_date_range_tabs_visible: All four date range tabs are rendered
|
||||||
- test_stats_summary_cards_zero_when_no_data: Summary cards show 0 when no execution logs exist
|
- test_stats_summary_cards_zero_when_no_data: Summary cards show 0 when no execution logs exist
|
||||||
|
|
@ -240,6 +241,28 @@ def test_stats_date_format_french(page: Page, app_url: str):
|
||||||
page.wait_for_timeout(300)
|
page.wait_for_timeout(300)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stats_date_format_danish(page: Page, app_url: str):
|
||||||
|
"""Date is formatted as DD.MM.YYYY in Danish (not the US fallback)."""
|
||||||
|
_set_date_and_reload(page, app_url, _KNOWN_DATE)
|
||||||
|
|
||||||
|
page.click('a[href="#/settings"]')
|
||||||
|
page.wait_for_url("**/#/settings", timeout=5000)
|
||||||
|
page.wait_for_selector('[data-testid="settings-language"]')
|
||||||
|
page.select_option('[data-testid="settings-language"]', 'da')
|
||||||
|
page.wait_for_timeout(500)
|
||||||
|
|
||||||
|
_go_to_stats(page)
|
||||||
|
|
||||||
|
text = page.locator('[data-testid="stats-using-since"]').text_content()
|
||||||
|
assert '15.03.2026' in text, f"Expected Danish date format, got: {text}"
|
||||||
|
|
||||||
|
page.click('a[href="#/settings"]')
|
||||||
|
page.wait_for_url("**/#/settings", timeout=5000)
|
||||||
|
page.wait_for_selector('[data-testid="settings-language"]')
|
||||||
|
page.select_option('[data-testid="settings-language"]', 'en')
|
||||||
|
page.wait_for_timeout(300)
|
||||||
|
|
||||||
|
|
||||||
def test_stats_date_format_updates_on_language_change(page: Page, app_url: str):
|
def test_stats_date_format_updates_on_language_change(page: Page, app_url: str):
|
||||||
"""Switching language updates the date format reactively without navigation."""
|
"""Switching language updates the date format reactively without navigation."""
|
||||||
_set_date_and_reload(page, app_url, _KNOWN_DATE)
|
_set_date_and_reload(page, app_url, _KNOWN_DATE)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue