refactor(i18n): i18n utils tools (#7251)

This commit is contained in:
EYHN
2024-06-19 07:38:20 +00:00
parent b379aa0a91
commit bcc66422fd
23 changed files with 848 additions and 363 deletions

View File

@@ -0,0 +1,399 @@
import { describe, expect, test } from 'vitest';
import { createI18n, getI18n } from '../../';
import { i18nTime } from '../time';
// Intl api is not available in github action, skip the test
describe.skip('humanTime', () => {
test('absolute', async () => {
createI18n();
expect(i18nTime('2024-10-10 13:30:28')).toBe('Oct 10, 2024, 1:30:28 PM');
expect(
i18nTime('2024-10-10 13:30:28', {
absolute: {
accuracy: 'minute',
},
})
).toBe('Oct 10, 2024, 1:30 PM');
expect(
i18nTime('2024-10-10 13:30:28', {
absolute: {
accuracy: 'day',
},
})
).toBe('Oct 10, 2024');
expect(
i18nTime('2024-10-10 13:30:28', {
absolute: {
accuracy: 'day',
noYear: true,
},
})
).toBe('Oct 10');
expect(
i18nTime('2024-10-10 13:30:28', {
absolute: {
accuracy: 'year',
},
})
).toBe('2024');
expect(
i18nTime('2024-10-10 13:30:28', {
absolute: {
noDate: true,
accuracy: 'minute',
},
})
).toBe('1:30 PM');
});
test('relative', async () => {
createI18n();
expect(
i18nTime('2024-10-10 13:30:28.005', {
now: '2024-10-10 13:30:30',
relative: true,
})
).toBe('1s ago');
expect(
i18nTime('2024-10-10 13:25:00', {
now: '2024-10-10 13:30:30',
relative: true,
})
).toBe('5m ago');
expect(
i18nTime('2024-10-10 12:59:00', {
now: '2024-10-10 13:30:30',
relative: true,
})
).toBe('31m ago');
expect(
i18nTime('2024-10-10 12:30:30', {
now: '2024-10-10 13:30:30',
relative: true,
})
).toBe('1h ago');
expect(
i18nTime('2024-10-9 13:30:00', {
now: '2024-10-10 13:30:30',
relative: true,
})
).toBe('yesterday');
expect(
i18nTime('2024-10-9 12:30:00', {
now: '2024-10-10 13:30:30',
relative: true,
})
).toBe('yesterday');
expect(
i18nTime('2024-10-8 23:59:00', {
now: '2024-10-10 13:30:30',
relative: true,
})
).toBe('2d ago');
expect(
i18nTime('2024-10-7 23:59:00', {
now: '2024-10-10 13:30:30',
relative: true,
})
).toBe('3d ago');
expect(
i18nTime('2024-10-4 00:00:00', {
now: '2024-10-10 13:30:30',
relative: true,
})
).toBe('6d ago');
expect(
i18nTime('2024-10-3 23:59:59', {
now: '2024-10-10 13:30:30',
relative: true,
})
).toBe('last wk.');
expect(
i18nTime('2024-9-29 23:59:59', {
now: '2024-10-10 13:30:30',
relative: true,
})
).toBe('last wk.');
expect(
i18nTime('2024-9-28 23:59:59', {
now: '2024-10-10 13:30:30',
relative: true,
})
).toBe('2w ago');
expect(
i18nTime('2024-9-15 00:00:00', {
now: '2024-10-10 13:30:30',
relative: true,
})
).toBe('last mo.');
expect(
i18nTime('2024-9-1 00:00:00', {
now: '2024-9-30 13:30:30',
relative: true,
})
).toBe('4w ago');
expect(
i18nTime('2024-9-10 13:30:30', {
now: '2024-10-10 13:30:30',
relative: true,
})
).toBe('last mo.');
expect(
i18nTime('2023-9-10 13:30:30', {
now: '2024-10-10 13:30:30',
relative: true,
})
).toBe('last yr.');
});
test('relative - accuracy', async () => {
createI18n();
expect(
i18nTime('2024-10-10 13:30:28.005', {
now: '2024-10-10 13:30:30',
relative: {
accuracy: 'minute',
},
})
).toBe('now');
expect(
i18nTime('2024-10-10 13:25:00', {
now: '2024-10-10 13:30:30',
relative: {
accuracy: 'minute',
},
})
).toBe('5m ago');
expect(
i18nTime('2024-10-10 12:59:00', {
now: '2024-10-10 13:30:30',
relative: {
accuracy: 'hour',
},
})
).toBe('now');
expect(
i18nTime('2024-10-10 12:30:30', {
now: '2024-10-10 13:30:30',
relative: {
accuracy: 'day',
},
})
).toBe('today');
expect(
i18nTime('2024-10-4 00:00:00', {
now: '2024-10-10 13:30:30',
relative: {
accuracy: 'week',
},
})
).toBe('last wk.');
expect(
i18nTime('2024-10-9 00:00:00', {
now: '2024-10-10 13:30:30',
relative: {
accuracy: 'week',
},
})
).toBe('this week');
expect(
i18nTime('2024-9-1 00:00:00', {
now: '2024-9-30 13:30:30',
relative: {
accuracy: 'month',
},
})
).toBe('this month');
expect(
i18nTime('2024-9-10 13:30:30', {
now: '2024-10-10 13:30:30',
relative: {
accuracy: 'year',
},
})
).toBe('this year');
expect(
i18nTime('2023-9-10 13:30:30', {
now: '2024-10-10 13:30:30',
relative: {
accuracy: 'year',
},
})
).toBe('last yr.');
});
test('relative - disable yesterdayAndTomorrow', async () => {
createI18n();
expect(
i18nTime('2024-10-9 13:30:30', {
now: '2024-10-10 13:30:30',
relative: {
yesterdayAndTomorrow: false,
},
})
).toBe('1d ago');
expect(
i18nTime('2024-10-11 13:30:30', {
now: '2024-10-10 13:30:30',
relative: {
yesterdayAndTomorrow: false,
},
})
).toBe('in 1d');
});
test('relative - weekday', async () => {
createI18n();
expect(
i18nTime('2024-10-9 13:30:30', {
now: '2024-10-10 13:30:30',
relative: {
weekday: true,
yesterdayAndTomorrow: false,
},
})
).toBe('Wednesday');
expect(
i18nTime('2024-10-4 13:30:30', {
now: '2024-10-10 13:30:30',
relative: {
weekday: true,
yesterdayAndTomorrow: false,
},
})
).toBe('Friday');
expect(
i18nTime('2024-10-3 13:30:30', {
now: '2024-10-10 13:30:30',
relative: {
weekday: true,
yesterdayAndTomorrow: false,
},
})
).toBe('1w ago');
expect(
i18nTime('2024-10-11 13:30:30', {
now: '2024-10-10 13:30:30',
relative: {
weekday: true,
yesterdayAndTomorrow: false,
},
})
).toBe('Friday');
expect(
i18nTime('2024-10-16 13:30:30', {
now: '2024-10-10 13:30:30',
relative: {
weekday: true,
yesterdayAndTomorrow: false,
},
})
).toBe('Wednesday');
expect(
i18nTime('2024-10-17 13:30:30', {
now: '2024-10-10 13:30:30',
relative: {
weekday: true,
yesterdayAndTomorrow: false,
},
})
).toBe('in 1w');
});
test('mix relative and absolute', async () => {
createI18n();
expect(
i18nTime('2024-10-9 14:30:30', {
now: '2024-10-10 13:30:30',
relative: {
max: [1, 'day'],
},
})
).toBe('23h ago');
expect(
i18nTime('2024-10-9 13:30:30', {
now: '2024-10-10 13:30:30',
relative: {
max: [1, 'day'],
},
absolute: {
accuracy: 'day',
},
})
).toBe('Oct 9, 2024');
expect(
i18nTime('2024-10-9 13:30:30', {
now: '2024-10-10 13:30:30',
relative: {
max: [2, 'day'],
},
absolute: {
accuracy: 'day',
},
})
).toBe('yesterday');
expect(
i18nTime('2024-10-8 13:30:30', {
now: '2024-10-10 13:30:30',
relative: {
max: [2, 'day'],
},
absolute: {
accuracy: 'day',
},
})
).toBe('Oct 8, 2024');
});
test('chinese', () => {
createI18n();
getI18n().changeLanguage('zh-Hans');
expect(i18nTime('2024-10-10 13:30:28.005')).toBe('2024年10月10日 13:30:28');
expect(
i18nTime('2024-10-10 13:30:28.005', {
absolute: {
accuracy: 'day',
},
})
).toBe('2024年10月10日');
expect(
i18nTime('2024-10-10 13:30:28.005', {
now: '2024-10-10 13:30:30',
relative: true,
})
).toBe('1秒前');
expect(
i18nTime('2024-10-9 13:30:30', {
now: '2024-10-10 13:30:30',
relative: true,
})
).toBe('昨天');
expect(
i18nTime('2024-10-8 13:30:30', {
now: '2024-10-10 13:30:30',
relative: {
weekday: true,
},
})
).toBe('星期二');
expect(
i18nTime('2024-10-8 13:30:30', {
now: '2024-10-10 13:30:30',
relative: {
accuracy: 'week',
},
})
).toBe('本周');
expect(
i18nTime('2024-10-8 13:30:30', {
now: '2024-10-10 13:30:30',
relative: {
accuracy: 'month',
},
})
).toBe('本月');
});
});

View File

@@ -0,0 +1 @@
export { i18nTime } from './time';

View File

@@ -0,0 +1,281 @@
import dayjs from 'dayjs';
import { I18n } from '../i18n';
export type TimeUnit =
| 'second'
| 'minute'
| 'hour'
| 'day'
| 'week'
| 'month'
| 'year';
const timeUnitCode = {
second: 1,
minute: 2,
hour: 3,
day: 4,
week: 5,
month: 6,
year: 7,
} satisfies Record<TimeUnit, number>;
/**
* ```ts
* // timestamp to string
* i18nTime(1728538228000) -> 'Oct 10, 2024, 1:30:28 PM'
*
* // absolute time string
* i18nTime('2024-10-10 13:30:28', { absolute: { accuracy: 'minute' } }) -> '2024-10-10 13:30 PM'
* i18nTime('2024-10-10 13:30:28', { absolute: { accuracy: 'minute', noDate: true } }) -> '13:30 PM'
* i18nTime('2024-10-10 13:30:28', { absolute: { accuracy: 'minute', noYear: true } }) -> 'Oct 10, 13:30 PM'
* i18nTime('2024-10-10 13:30:28', { absolute: { accuracy: 'day' } }) -> 'Oct 10, 2024'
*
* // relative time string
* i18nTime('2024-10-10 13:30:30', { relative: true }) -> 'now'
* i18nTime('2024-10-10 13:30:00', { relative: true }) -> '30s ago'
* i18nTime('2024-10-10 13:30:30', { relative: { accuracy: 'minute' } }) -> '2m ago'
*
* // show absolute time string if time is pass 1 day
* i18nTime('2024-10-9 14:30:30', { relative: { max: [1, 'day'] } }) -> '23h ago'
* i18nTime('2024-10-9 13:30:30', { relative: { max: [1, 'day'] } }) -> 'Oct 9, 2024, 1:30:30 PM'
* ```
*/
export function i18nTime(
time: dayjs.ConfigType,
options: {
// override i18n instance, default is global I18n instance
i18n?: I18n;
// override now time, default is current time
now?: dayjs.ConfigType;
relative?:
| {
// max time to show relative time, if time is pass this time, show absolute time
max?: [number, TimeUnit];
// show time with this accuracy
accuracy?: TimeUnit;
// show weekday, e.g. 'Monday', 'Tuesday', etc.
weekday?: boolean;
// show 'yesterday' or 'tomorrow' if time is
yesterdayAndTomorrow?: boolean;
}
| true; // use default relative option
absolute?: {
// show time with this accuracy
accuracy?: TimeUnit;
// hide year
noYear?: boolean;
// hide date (year, month, day)
noDate?: boolean;
};
} = {}
) {
const i18n = options.i18n ?? I18n;
time = dayjs(time);
const now = dayjs(options.now);
const defaultRelativeOption = {
max: [1000, 'year'],
accuracy: 'second',
weekday: false,
yesterdayAndTomorrow: true,
} satisfies typeof options.relative;
const relativeOption = options.relative
? options.relative === true
? defaultRelativeOption
: {
...defaultRelativeOption,
...options.relative,
}
: null;
const defaultAbsoluteOption = {
accuracy: 'second',
noYear: false,
noDate: false,
} satisfies typeof options.absolute;
const absoluteOption = {
...defaultAbsoluteOption,
...options.absolute,
};
if (relativeOption) {
// show relative
const formatter = new Intl.RelativeTimeFormat(i18n.language, {
style: 'narrow',
numeric: relativeOption.yesterdayAndTomorrow ? 'auto' : 'always',
});
const timeUnitProcessor = {
second: () => {
const diffSecond = time.diff(now) / 1000;
if (Math.abs(diffSecond) < 1) {
return i18n['com.affine.time.now']();
}
if (
relativeOption.max[1] === 'second' &&
Math.abs(diffSecond) >= relativeOption.max[0]
) {
return false;
}
if (Math.abs(diffSecond) < 60) {
return formatter.format(Math.trunc(diffSecond), 'second');
}
return null;
},
minute: () => {
const diffMinute = time.diff(now) / 1000 / 60;
if (Math.abs(diffMinute) < 1) {
return i18n['com.affine.time.now']();
}
if (
relativeOption.max[1] === 'minute' &&
Math.abs(diffMinute) >= relativeOption.max[0]
) {
return false;
}
if (Math.abs(diffMinute) < 60) {
return formatter.format(Math.trunc(diffMinute), 'minute');
}
return null;
},
hour: () => {
const diffHour = time.diff(now) / 1000 / 60 / 60;
if (Math.abs(diffHour) < 1) {
return i18n['com.affine.time.now']();
}
if (
relativeOption.max[1] === 'hour' &&
Math.abs(diffHour) >= relativeOption.max[0]
) {
return false;
}
if (Math.abs(diffHour) < 24) {
return formatter.format(Math.trunc(diffHour), 'hour');
}
return null;
},
day: () => {
const diffDay = time.startOf('day').diff(now.startOf('day'), 'day');
if (Math.abs(diffDay) < 1) {
return i18n['com.affine.time.today']();
}
if (
relativeOption.max[1] === 'day' &&
Math.abs(diffDay) >= relativeOption.max[0]
) {
return false;
}
if (relativeOption.yesterdayAndTomorrow && Math.abs(diffDay) < 2) {
return formatter.format(Math.trunc(diffDay), 'day');
} else if (relativeOption.weekday && Math.abs(diffDay) < 7) {
return new Intl.DateTimeFormat(i18n.language, {
weekday: 'long',
}).format(time.startOf('day').toDate());
} else if (Math.abs(diffDay) < 7) {
return formatter.format(Math.trunc(diffDay), 'day');
}
return null;
},
week: () => {
const inSameMonth = time.startOf('month').isSame(now.startOf('month'));
const diffWeek = time.startOf('week').diff(now.startOf('week'), 'week');
if (Math.abs(diffWeek) < 1) {
return i18n['com.affine.time.this-week']();
}
if (
relativeOption.max[1] === 'week' &&
Math.abs(diffWeek) >= relativeOption.max[0]
) {
return false;
}
if (inSameMonth || Math.abs(diffWeek) < 3) {
return formatter.format(Math.trunc(diffWeek), 'week');
}
return null;
},
month: () => {
const diffMonth = time
.startOf('month')
.diff(now.startOf('month'), 'month');
if (Math.abs(diffMonth) < 1) {
return i18n['com.affine.time.this-mouth']();
}
if (
relativeOption.max[1] === 'month' &&
Math.abs(diffMonth) >= relativeOption.max[0]
) {
return false;
}
if (Math.abs(diffMonth) < 12) {
return formatter.format(Math.trunc(diffMonth), 'month');
}
return null;
},
year: () => {
const diffYear = time.startOf('year').diff(now.startOf('year'), 'year');
if (Math.abs(diffYear) < 1) {
return i18n['com.affine.time.this-year']();
}
if (
relativeOption.max[1] === 'year' &&
Math.abs(diffYear) >= relativeOption.max[0]
) {
return false;
}
return formatter.format(Math.trunc(diffYear), 'year');
},
} as Record<TimeUnit, () => string | false | null>;
const processors = Object.entries(timeUnitProcessor).sort(
(a, b) => timeUnitCode[a[0] as TimeUnit] - timeUnitCode[b[0] as TimeUnit]
) as [TimeUnit, () => string | false | null][];
for (const [unit, processor] of processors) {
if (timeUnitCode[relativeOption.accuracy] > timeUnitCode[unit]) {
continue;
}
const result = processor();
if (result) {
return result;
}
if (result === false) {
break;
}
}
}
// show absolute
const formatter = new Intl.DateTimeFormat(i18n.language, {
year:
!absoluteOption.noYear && !absoluteOption.noDate ? 'numeric' : undefined,
month:
!absoluteOption.noDate &&
timeUnitCode[absoluteOption.accuracy] <= timeUnitCode['month']
? 'short'
: undefined,
day:
!absoluteOption.noDate &&
timeUnitCode[absoluteOption.accuracy] <= timeUnitCode['day']
? 'numeric'
: undefined,
hour:
timeUnitCode[absoluteOption.accuracy] <= timeUnitCode['hour']
? 'numeric'
: undefined,
minute:
timeUnitCode[absoluteOption.accuracy] <= timeUnitCode['minute']
? 'numeric'
: undefined,
second:
timeUnitCode[absoluteOption.accuracy] <= timeUnitCode['second']
? 'numeric'
: undefined,
});
return formatter.format(time.toDate());
}