diff --git a/web/.gitignore b/web/.gitignore index 4998cb543..d451ff16c 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -3,4 +3,3 @@ node_modules dist dist-ssr *.local -src/assets/dayjsLocales diff --git a/web/.prettierignore b/web/.prettierignore index 203898907..2087370c5 100644 --- a/web/.prettierignore +++ b/web/.prettierignore @@ -4,5 +4,4 @@ coverage/ LICENSE components.d.ts src/assets/locales/*.json -src/assets/dayjsLocales/ !src/assets/locales/en.json diff --git a/web/eslint.config.js b/web/eslint.config.js index 1b704b719..ac3bf0827 100644 --- a/web/eslint.config.js +++ b/web/eslint.config.js @@ -106,7 +106,6 @@ export default antfu( 'tsconfig.json', 'src/assets/locales/**/*', '!src/assets/locales/en.json', - 'src/assets/dayjsLocales/', 'components.d.ts', ], }, diff --git a/web/package.json b/web/package.json index 83ff5fb20..8cd959870 100644 --- a/web/package.json +++ b/web/package.json @@ -23,7 +23,6 @@ "@mdi/js": "^7.4.47", "@vueuse/core": "^12.0.0", "ansi_up": "^6.0.2", - "dayjs": "^1.11.12", "dompurify": "^3.2.0", "fuse.js": "^7.0.0", "js-base64": "^3.7.7", @@ -57,7 +56,6 @@ "eslint-plugin-vue-scoped-css": "^2.8.1", "jsdom": "^25.0.0", "prettier": "^3.3.3", - "replace-in-file": "^8.1.0", "tinycolor2": "^1.6.0", "typescript": "5.6.3", "vite": "^5.4.1", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index de6da3438..369877a2c 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -26,9 +26,6 @@ importers: ansi_up: specifier: ^6.0.2 version: 6.0.2 - dayjs: - specifier: ^1.11.12 - version: 1.11.13 dompurify: specifier: ^3.2.0 version: 3.2.1 @@ -123,9 +120,6 @@ importers: prettier: specifier: ^3.3.3 version: 3.3.3 - replace-in-file: - specifier: ^8.1.0 - version: 8.2.0 tinycolor2: specifier: ^1.6.0 version: 1.6.0 @@ -1107,10 +1101,6 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chalk@5.3.0: - resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -1214,9 +1204,6 @@ packages: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} - dayjs@1.11.13: - resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} - de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} @@ -2295,11 +2282,6 @@ packages: resolution: {integrity: sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==} hasBin: true - replace-in-file@8.2.0: - resolution: {integrity: sha512-hMsQtdYHwWviQT5ZbNsgfu0WuCiNlcUSnnD+aHAL081kbU9dPkPocDaHlDvAHKydTWWpx1apfcEcmvIyQk3CpQ==} - engines: {node: '>=18'} - hasBin: true - require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -3798,8 +3780,6 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - chalk@5.3.0: {} - char-regex@1.0.2: {} character-entities@2.0.2: {} @@ -3898,8 +3878,6 @@ snapshots: whatwg-mimetype: 4.0.0 whatwg-url: 14.0.0 - dayjs@1.11.13: {} - de-indent@1.0.2: {} debug@3.2.7: @@ -5205,12 +5183,6 @@ snapshots: dependencies: jsesc: 0.5.0 - replace-in-file@8.2.0: - dependencies: - chalk: 5.3.0 - glob: 10.4.5 - yargs: 17.7.2 - require-directory@2.1.1: {} resolve-from@4.0.0: {} diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json index cba931e00..f02638fea 100644 --- a/web/src/assets/locales/en.json +++ b/web/src/assets/locales/en.json @@ -24,8 +24,8 @@ "not_found": "Server could not find requested object" }, "time": { - "template": "MMM D, YYYY, HH:mm z", - "not_started": "not started yet" + "not_started": "not started yet", + "just_now": "just now" }, "repo": { "manual_pipeline": { diff --git a/web/src/compositions/useDate.ts b/web/src/compositions/useDate.ts index c467a244e..0c1a75821 100644 --- a/web/src/compositions/useDate.ts +++ b/web/src/compositions/useDate.ts @@ -1,44 +1,94 @@ -import dayjs from 'dayjs'; -import advancedFormat from 'dayjs/plugin/advancedFormat'; -import duration from 'dayjs/plugin/duration'; -import relativeTime from 'dayjs/plugin/relativeTime'; -import timezone from 'dayjs/plugin/timezone'; -import utc from 'dayjs/plugin/utc'; import { useI18n } from 'vue-i18n'; -dayjs.extend(timezone); -dayjs.extend(utc); -dayjs.extend(advancedFormat); -dayjs.extend(relativeTime); -dayjs.extend(duration); +let currentLocale = 'en'; -function toLocaleString(date: Date) { - return dayjs(date).format(useI18n().t('time.template')); +function splitDuration(durationMs: number) { + const totalSeconds = durationMs / 1000; + const totalMinutes = totalSeconds / 60; + const totalHours = totalMinutes / 60; + + const seconds = Math.floor(totalSeconds) % 60; + const minutes = Math.floor(totalMinutes) % 60; + const hours = Math.floor(totalHours) % 24; + + return { + seconds, + minutes, + hours, + totalHours, + totalMinutes, + totalSeconds, + }; } -function timeAgo(date: Date | string | number) { - return dayjs().to(dayjs(date)); +function toLocaleString(date: Date) { + return date.toLocaleString(currentLocale, { + dateStyle: 'short', + timeStyle: 'short', + }); +} + +function timeAgo(date: number) { + const seconds = Math.floor((new Date().getTime() - date) / 1000); + + const formatter = new Intl.RelativeTimeFormat(currentLocale); + + let interval = seconds / 31536000; + if (interval > 1) { + return formatter.format(-Math.round(interval), 'year'); + } + interval = seconds / 2592000; + if (interval > 1) { + return formatter.format(-Math.round(interval), 'month'); + } + interval = seconds / 86400; + if (interval > 1) { + return formatter.format(-Math.round(interval), 'day'); + } + interval = seconds / 3600; + if (interval > 1) { + return formatter.format(-Math.round(interval), 'hour'); + } + interval = seconds / 60; + if (interval > 0.5) { + return formatter.format(-Math.round(interval), 'minute'); + } + return useI18n().t('time.just_now'); } function prettyDuration(durationMs: number) { - return dayjs.duration(durationMs).humanize(); + const t = splitDuration(durationMs); + + if (t.totalHours > 1) { + return Intl.NumberFormat(currentLocale, { style: 'unit', unit: 'hour', unitDisplay: 'long' }).format( + Math.round(t.totalHours), + ); + } + if (t.totalMinutes > 1) { + return Intl.NumberFormat(currentLocale, { style: 'unit', unit: 'minute', unitDisplay: 'long' }).format( + Math.round(t.totalMinutes), + ); + } + return Intl.NumberFormat(currentLocale, { style: 'unit', unit: 'second', unitDisplay: 'long' }).format( + Math.round(t.totalSeconds), + ); } function durationAsNumber(durationMs: number): string { - const dur = dayjs.duration(durationMs); - return dur.format(dur.hours() > 1 ? 'HH:mm:ss' : 'mm:ss'); + const { seconds, minutes, hours } = splitDuration(durationMs); + + const minSecFormat = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + + if (hours > 0) { + return `${hours.toString().padStart(2, '0')}:${minSecFormat}`; + } + + return minSecFormat; } export function useDate() { - const addedLocales = ['en']; - async function setDayjsLocale(locale: string) { - if (!addedLocales.includes(locale)) { - const l = (await import(`~/assets/dayjsLocales/${locale}.js`)) as { default: string }; - dayjs.locale(l.default); - } else { - dayjs.locale(locale); - } + currentLocale = locale; } return { diff --git a/web/vite.config.ts b/web/vite.config.ts index 45fe7ba40..1b71b4bf9 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -1,9 +1,8 @@ -import { copyFile, existsSync, mkdirSync, readdirSync } from 'node:fs'; +import { readdirSync } from 'node:fs'; import path from 'node:path'; import process from 'node:process'; import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'; import vue from '@vitejs/plugin-vue'; -import { replaceInFileSync } from 'replace-in-file'; import type { Plugin } from 'vite'; import prismjs from 'vite-plugin-prismjs'; import WindiCSS from 'vite-plugin-windicss'; @@ -54,44 +53,6 @@ export default defineConfig({ const filenames = readdirSync('src/assets/locales/').map((filename) => filename.replace('.json', '')); - if (!existsSync('src/assets/dayjsLocales')) { - mkdirSync('src/assets/dayjsLocales'); - } - - filenames.forEach((name) => { - // English is always directly loaded (compiled by Vite) and thus not copied - if (name === 'en') { - return; - } - let langName = name; - - // copy dayjs language - if (name === 'zh-Hans') { - // zh-Hans is called zh in dayjs - langName = 'zh'; - } else if (name === 'zh-Hant') { - // zh-Hant is called zh-cn in dayjs - langName = 'zh-cn'; - } - - copyFile( - `node_modules/dayjs/esm/locale/${langName}.js`, - `src/assets/dayjsLocales/${name}.js`, - // eslint-disable-next-line promise/prefer-await-to-callbacks - (err) => { - if (err) { - throw err; - } - }, - ); - }); - replaceInFileSync({ - files: 'src/assets/dayjsLocales/*.js', - // remove any dayjs import and any dayjs.locale call - from: /(?:import dayjs.*'|dayjs\.locale.*);/g, - to: '', - }); - return { name: 'vue-i18n-supported-locales',