@meith/i18n 0.7.0

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.
@@ -0,0 +1,97 @@
1
+ import type { Translator } from './translator'
2
+
3
+ export interface TimestampLabel {
4
+ readonly iso: string
5
+ readonly label: string
6
+ }
7
+
8
+ interface CalendarParts {
9
+ readonly year: number
10
+ readonly month: number
11
+ readonly day: number
12
+ }
13
+
14
+ const DAY_IN_MS = 24 * 60 * 60 * 1000
15
+
16
+ export function formatTimestamp(at: Date, now: Date, translator: Translator): TimestampLabel {
17
+ const iso = at.toISOString()
18
+ const when = calendarParts(at, translator)
19
+ const today = calendarParts(now, translator)
20
+ const time = clock(at, translator)
21
+
22
+ if (sameDay(when, today)) {
23
+ return { iso, label: translator.t('time.today', { time }) }
24
+ }
25
+
26
+ if (sameDay(when, calendarParts(new Date(now.getTime() - DAY_IN_MS), translator))) {
27
+ return { iso, label: translator.t('time.yesterday', { time }) }
28
+ }
29
+
30
+ const day = number(when.day, translator)
31
+ const month = monthName(at, translator)
32
+
33
+ if (when.year === today.year) {
34
+ return { iso, label: translator.t('time.thisYear', { day, month, time }) }
35
+ }
36
+
37
+ return {
38
+ iso,
39
+ label: translator.t('time.past', { day, month, year: number(when.year, translator) }),
40
+ }
41
+ }
42
+
43
+ export function formatDay(at: Date, translator: Translator): TimestampLabel {
44
+ const when = calendarParts(at, translator)
45
+
46
+ return {
47
+ iso: at.toISOString(),
48
+ label: translator.t('time.day', {
49
+ day: number(when.day, translator),
50
+ month: monthName(at, translator),
51
+ year: number(when.year, translator),
52
+ }),
53
+ }
54
+ }
55
+
56
+ function calendarParts(at: Date, translator: Translator): CalendarParts {
57
+ const parts = new Map(
58
+ new Intl.DateTimeFormat('en-GB', {
59
+ timeZone: translator.timeZone,
60
+ year: 'numeric',
61
+ month: 'numeric',
62
+ day: 'numeric',
63
+ })
64
+ .formatToParts(at)
65
+ .map((part) => [part.type, part.value]),
66
+ )
67
+
68
+ return {
69
+ year: Number(parts.get('year')),
70
+ month: Number(parts.get('month')),
71
+ day: Number(parts.get('day')),
72
+ }
73
+ }
74
+
75
+ function sameDay(a: CalendarParts, b: CalendarParts): boolean {
76
+ return a.year === b.year && a.month === b.month && a.day === b.day
77
+ }
78
+
79
+ function clock(at: Date, translator: Translator): string {
80
+ const cycle = translator.t('time.hourCycle')
81
+
82
+ return translator.parts(at, {
83
+ hour: '2-digit',
84
+ minute: '2-digit',
85
+ ...(cycle === 'h11' || cycle === 'h12' || cycle === 'h23' || cycle === 'h24'
86
+ ? { hourCycle: cycle }
87
+ : {}),
88
+ })
89
+ }
90
+
91
+ function monthName(at: Date, translator: Translator): string {
92
+ return translator.parts(at, { month: 'short' })
93
+ }
94
+
95
+ function number(value: number, translator: Translator): string {
96
+ return translator.number(value, { useGrouping: false })
97
+ }
@@ -0,0 +1,88 @@
1
+ import type { MessageCatalog } from './catalog'
2
+ import { type Locale, SOURCE_LOCALE } from './locale'
3
+ import { formatMessage, type MessageArgs, pluralCategory } from './message'
4
+
5
+ export const DEFAULT_TIME_ZONE = 'UTC'
6
+
7
+ export interface Translator {
8
+ readonly locale: Locale
9
+ readonly timeZone: string
10
+ t(key: string, args?: MessageArgs): string
11
+ has(key: string): boolean
12
+ pattern(key: string): string | undefined
13
+ number(value: number, options?: Intl.NumberFormatOptions): string
14
+ list(items: readonly string[], options?: Intl.ListFormatOptions): string
15
+ parts(value: Date, options: Intl.DateTimeFormatOptions): string
16
+ plural(value: number, ordinal?: boolean): string
17
+ }
18
+
19
+ export interface TranslatorInput {
20
+ readonly locale: Locale
21
+ readonly catalog: MessageCatalog
22
+ readonly timeZone?: string
23
+ }
24
+
25
+ export function createTranslator(input: TranslatorInput): Translator {
26
+ const locale = input.locale
27
+ const timeZone = usableTimeZone(input.timeZone)
28
+ const context = { locale, timeZone }
29
+
30
+ return {
31
+ locale,
32
+ timeZone,
33
+
34
+ t(key, args = {}) {
35
+ const pattern = input.catalog[key]
36
+ if (pattern === undefined) return key
37
+
38
+ try {
39
+ return formatMessage(pattern, args, context)
40
+ } catch {
41
+ return pattern
42
+ }
43
+ },
44
+
45
+ has(key) {
46
+ return input.catalog[key] !== undefined
47
+ },
48
+
49
+ pattern(key) {
50
+ return input.catalog[key]
51
+ },
52
+
53
+ number(value, options) {
54
+ return new Intl.NumberFormat(locale, options).format(value)
55
+ },
56
+
57
+ list(items, options) {
58
+ return new Intl.ListFormat(locale, options).format(items)
59
+ },
60
+
61
+ parts(value, options) {
62
+ return new Intl.DateTimeFormat(locale, { timeZone, ...options }).format(value)
63
+ },
64
+
65
+ plural(value, ordinal = false) {
66
+ return pluralCategory(locale, value, ordinal)
67
+ },
68
+ }
69
+ }
70
+
71
+ export function usableTimeZone(timeZone: string | null | undefined): string {
72
+ if (typeof timeZone !== 'string' || timeZone.trim() === '') return DEFAULT_TIME_ZONE
73
+
74
+ try {
75
+ new Intl.DateTimeFormat('en', { timeZone }).format(0)
76
+ return timeZone
77
+ } catch {
78
+ return DEFAULT_TIME_ZONE
79
+ }
80
+ }
81
+
82
+ export function sourceTranslator(catalog: MessageCatalog, timeZone?: string): Translator {
83
+ return createTranslator({
84
+ locale: SOURCE_LOCALE,
85
+ catalog,
86
+ ...(timeZone === undefined ? {} : { timeZone }),
87
+ })
88
+ }