@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.
- package/LICENSE.md +165 -0
- package/package.json +29 -0
- package/src/catalog.ts +92 -0
- package/src/catalogs/en.json +2846 -0
- package/src/catalogs/index.ts +12 -0
- package/src/index.ts +38 -0
- package/src/locale.ts +116 -0
- package/src/message.ts +450 -0
- package/src/msg.ts +23 -0
- package/src/timestamp.ts +97 -0
- package/src/translator.ts +88 -0
package/src/timestamp.ts
ADDED
|
@@ -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
|
+
}
|