@jarenjs/locales 0.46.4 → 0.49.2
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/README.md +97 -4
- package/dist/types/dates.d.ts +68 -0
- package/dist/types/helpers.d.ts +63 -2
- package/dist/types/index.d.ts +11 -0
- package/dist/types/intl-dates.d.ts +20 -0
- package/package.json +10 -2
- package/src/ar.js +76 -1
- package/src/dates.js +263 -0
- package/src/de.js +49 -0
- package/src/es.js +47 -0
- package/src/fr.js +49 -0
- package/src/helpers.js +116 -2
- package/src/index.js +12 -0
- package/src/intl-dates.js +160 -0
- package/src/ja.js +49 -0
- package/src/ko.js +49 -0
- package/src/nl.js +49 -0
- package/src/pt.js +47 -0
- package/src/ru.js +63 -4
- package/src/tr.js +49 -0
- package/src/zh-tw.js +49 -0
package/src/dates.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Calendar language: month, weekday and meridiem names, signed
|
|
5
|
+
* relative-time phrases and date-format display names, on the same flat
|
|
6
|
+
* msgid machinery every other message in the suite travels on.
|
|
7
|
+
*
|
|
8
|
+
* `@jarenjs/core/dates` is locale-free on purpose - its `MMMM`, `EEE`
|
|
9
|
+
* and `a` tokens refuse to render without a names provider rather than
|
|
10
|
+
* inventing an English default. This module is that provider, and it is
|
|
11
|
+
* repository DATA rather than an `Intl` lookup, because the site is
|
|
12
|
+
* server-rendered under byte-comparison tests and ICU output drifts
|
|
13
|
+
* between Node versions. A host that wants a hundred locales and does
|
|
14
|
+
* not need byte-stable output constructs `createIntlDateLocale`
|
|
15
|
+
* explicitly (`./intl-dates.js`); nothing here reaches for `Intl`.
|
|
16
|
+
*
|
|
17
|
+
* The key set is closed and mechanical:
|
|
18
|
+
*
|
|
19
|
+
* date/month/01..12/wide|short the twelve months, January first
|
|
20
|
+
* date/weekday/0..6/wide|short the seven days, Sunday = 0
|
|
21
|
+
* date/meridiem/am|pm the two day-period markers
|
|
22
|
+
* date/relative/<unit>/past|future the seven units, both directions
|
|
23
|
+
* date/relative/now|yesterday|today|tomorrow
|
|
24
|
+
* format/name/<format> the date/time format display names
|
|
25
|
+
*
|
|
26
|
+
* Name entries take no parameters. A relative entry receives the
|
|
27
|
+
* ABSOLUTE amount as `{ value }` and owns its language's plural and case
|
|
28
|
+
* grammar; the direction is already chosen by the key.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { compileMessageCatalog } from '@jarenjs/core/message';
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
RELATIVE_UNITS,
|
|
35
|
+
checkRelativeArguments,
|
|
36
|
+
dateNameEntries,
|
|
37
|
+
} from './helpers.js';
|
|
38
|
+
|
|
39
|
+
export { RELATIVE_UNITS };
|
|
40
|
+
|
|
41
|
+
//#region The English defaults
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The English date catalog: the canonical key set every pack covers,
|
|
45
|
+
* and the entries `compileDateLocale` falls back to for a partial
|
|
46
|
+
* caller-supplied catalog.
|
|
47
|
+
* @type {Record<string, string | ((params: any, error?: object) => string)>}
|
|
48
|
+
*/
|
|
49
|
+
export const dateMessagesEn = {
|
|
50
|
+
...dateNameEntries({
|
|
51
|
+
months: [
|
|
52
|
+
'January', 'February', 'March', 'April', 'May', 'June',
|
|
53
|
+
'July', 'August', 'September', 'October', 'November', 'December',
|
|
54
|
+
],
|
|
55
|
+
monthsShort: [
|
|
56
|
+
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
|
57
|
+
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',
|
|
58
|
+
],
|
|
59
|
+
weekdays: [
|
|
60
|
+
'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday',
|
|
61
|
+
],
|
|
62
|
+
weekdaysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
|
63
|
+
meridiem: ['AM', 'PM'],
|
|
64
|
+
}),
|
|
65
|
+
|
|
66
|
+
'date/relative/second/past': (p) => `${p.value} second${p.value === 1 ? '' : 's'} ago`,
|
|
67
|
+
'date/relative/second/future': (p) => `in ${p.value} second${p.value === 1 ? '' : 's'}`,
|
|
68
|
+
'date/relative/minute/past': (p) => `${p.value} minute${p.value === 1 ? '' : 's'} ago`,
|
|
69
|
+
'date/relative/minute/future': (p) => `in ${p.value} minute${p.value === 1 ? '' : 's'}`,
|
|
70
|
+
'date/relative/hour/past': (p) => `${p.value} hour${p.value === 1 ? '' : 's'} ago`,
|
|
71
|
+
'date/relative/hour/future': (p) => `in ${p.value} hour${p.value === 1 ? '' : 's'}`,
|
|
72
|
+
'date/relative/day/past': (p) => `${p.value} day${p.value === 1 ? '' : 's'} ago`,
|
|
73
|
+
'date/relative/day/future': (p) => `in ${p.value} day${p.value === 1 ? '' : 's'}`,
|
|
74
|
+
'date/relative/week/past': (p) => `${p.value} week${p.value === 1 ? '' : 's'} ago`,
|
|
75
|
+
'date/relative/week/future': (p) => `in ${p.value} week${p.value === 1 ? '' : 's'}`,
|
|
76
|
+
'date/relative/month/past': (p) => `${p.value} month${p.value === 1 ? '' : 's'} ago`,
|
|
77
|
+
'date/relative/month/future': (p) => `in ${p.value} month${p.value === 1 ? '' : 's'}`,
|
|
78
|
+
'date/relative/year/past': (p) => `${p.value} year${p.value === 1 ? '' : 's'} ago`,
|
|
79
|
+
'date/relative/year/future': (p) => `in ${p.value} year${p.value === 1 ? '' : 's'}`,
|
|
80
|
+
'date/relative/now': 'now',
|
|
81
|
+
'date/relative/yesterday': 'yesterday',
|
|
82
|
+
'date/relative/today': 'today',
|
|
83
|
+
'date/relative/tomorrow': 'tomorrow',
|
|
84
|
+
|
|
85
|
+
'format/name/date': 'date',
|
|
86
|
+
'format/name/time': 'time',
|
|
87
|
+
'format/name/date-time': 'date and time',
|
|
88
|
+
'format/name/iso-date': 'ISO date',
|
|
89
|
+
'format/name/iso-time': 'ISO time',
|
|
90
|
+
'format/name/iso-date-time': 'ISO date and time',
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
//#endregion
|
|
94
|
+
|
|
95
|
+
//#region Compilation
|
|
96
|
+
|
|
97
|
+
/** Every key a pack has to carry, in catalog order. */
|
|
98
|
+
const DATE_KEYS = Object.freeze(Object.keys(dateMessagesEn));
|
|
99
|
+
|
|
100
|
+
/** The `format/name/` prefix, and the formats this family closes over. */
|
|
101
|
+
const FORMAT_PREFIX = 'format/name/';
|
|
102
|
+
const FORMAT_NAMES = Object.freeze(DATE_KEYS
|
|
103
|
+
.filter((key) => key.startsWith(FORMAT_PREFIX))
|
|
104
|
+
.map((key) => key.slice(FORMAT_PREFIX.length)));
|
|
105
|
+
|
|
106
|
+
/** The compiled English defaults, once (allocation discipline). */
|
|
107
|
+
const EN = compileMessageCatalog(dateMessagesEn);
|
|
108
|
+
|
|
109
|
+
/** Shared empty params for the entries that interpolate nothing. */
|
|
110
|
+
const NO_PARAMS = Object.freeze({});
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The calendar names a date pattern needs - the shape
|
|
114
|
+
* `compileDateFormat`'s `names` argument takes.
|
|
115
|
+
* @typedef {Object} DateLocaleNames
|
|
116
|
+
* @property {string[]} months - 12 wide month names, January first
|
|
117
|
+
* @property {string[]} monthsShort - 12 abbreviated month names
|
|
118
|
+
* @property {string[]} weekdays - 7 wide weekday names, Sunday first
|
|
119
|
+
* @property {string[]} weekdaysShort - 7 abbreviated weekday names
|
|
120
|
+
* @property {[string, string]} meridiem - the AM and PM markers
|
|
121
|
+
*/
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A compiled calendar language.
|
|
125
|
+
* @typedef {Object} DateLocale
|
|
126
|
+
* @property {DateLocaleNames} names - the arrays `compileDateFormat` reads
|
|
127
|
+
* @property {(amount: number, unit: string, options?: {numeric?: string}) => string} relative - a signed relative-time phrase
|
|
128
|
+
* @property {(format: string) => string|undefined} formatName - a date-format display name, or undefined when the catalog has none
|
|
129
|
+
*/
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Read one entry and insist it rendered readable text: a caller catalog
|
|
133
|
+
* may hold a closure, and a closure can return anything.
|
|
134
|
+
* @param {Record<string, (params: object, error?: object) => string>} compiled - The compiled date entries
|
|
135
|
+
* @param {string} key - The msgid
|
|
136
|
+
* @returns {string}
|
|
137
|
+
*/
|
|
138
|
+
function readName(compiled, key) {
|
|
139
|
+
const render = compiled[key] ?? EN[key];
|
|
140
|
+
const text = render(NO_PARAMS);
|
|
141
|
+
if (typeof text !== 'string' || text === '')
|
|
142
|
+
throw new TypeError(`the date catalog entry '${key}' must render a non-empty string`);
|
|
143
|
+
return text;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Compile a catalog into the calendar language a formatter and a UI
|
|
148
|
+
* need. Names are read once and frozen, so a formatter compiled against
|
|
149
|
+
* them and a label rendered from them cost no lookup per date.
|
|
150
|
+
*
|
|
151
|
+
* The argument is a catalog-like - a locale pack, an already-compiled
|
|
152
|
+
* catalog, or a partial object of overrides; every key it leaves out
|
|
153
|
+
* falls back to English. The eleven shipped packs cover the whole key
|
|
154
|
+
* set, so that fallback exists for a caller's own catalog, not for them.
|
|
155
|
+
*
|
|
156
|
+
* @param {Record<string, any>} [catalogLike] - A catalog to read, or undefined for English
|
|
157
|
+
* @returns {Readonly<DateLocale>} The frozen calendar language
|
|
158
|
+
* @throws {TypeError} when an entry does not render a non-empty string
|
|
159
|
+
* @example
|
|
160
|
+
* import { compileDateFormat } from '@jarenjs/core/dates';
|
|
161
|
+
* const dates = compileDateLocale(nl);
|
|
162
|
+
* const long = compileDateFormat('EEEE d MMMM yyyy', dates.names);
|
|
163
|
+
* dates.relative(-3, 'day'); // '3 dagen geleden'
|
|
164
|
+
* dates.relative(-1, 'day', { numeric: 'auto' }); // 'gisteren'
|
|
165
|
+
*/
|
|
166
|
+
export function compileDateLocale(catalogLike = undefined) {
|
|
167
|
+
/** @type {Record<string, any>} */
|
|
168
|
+
const picked = {};
|
|
169
|
+
if (catalogLike != null) {
|
|
170
|
+
for (let i = 0; i < DATE_KEYS.length; ++i) {
|
|
171
|
+
const key = DATE_KEYS[i];
|
|
172
|
+
const entry = catalogLike[key];
|
|
173
|
+
if (entry !== undefined) picked[key] = entry;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const compiled = compileMessageCatalog(picked);
|
|
177
|
+
|
|
178
|
+
const months = [];
|
|
179
|
+
const monthsShort = [];
|
|
180
|
+
for (let i = 1; i <= 12; ++i) {
|
|
181
|
+
const number = i < 10 ? `0${i}` : `${i}`;
|
|
182
|
+
months.push(readName(compiled, `date/month/${number}/wide`));
|
|
183
|
+
monthsShort.push(readName(compiled, `date/month/${number}/short`));
|
|
184
|
+
}
|
|
185
|
+
const weekdays = [];
|
|
186
|
+
const weekdaysShort = [];
|
|
187
|
+
for (let i = 0; i < 7; ++i) {
|
|
188
|
+
weekdays.push(readName(compiled, `date/weekday/${i}/wide`));
|
|
189
|
+
weekdaysShort.push(readName(compiled, `date/weekday/${i}/short`));
|
|
190
|
+
}
|
|
191
|
+
const names = Object.freeze({
|
|
192
|
+
months: Object.freeze(months),
|
|
193
|
+
monthsShort: Object.freeze(monthsShort),
|
|
194
|
+
weekdays: Object.freeze(weekdays),
|
|
195
|
+
weekdaysShort: Object.freeze(weekdaysShort),
|
|
196
|
+
meridiem: /** @type {[string, string]} */ (/** @type {unknown} */ (Object.freeze([
|
|
197
|
+
readName(compiled, 'date/meridiem/am'),
|
|
198
|
+
readName(compiled, 'date/meridiem/pm'),
|
|
199
|
+
]))),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const phrases = Object.create(null);
|
|
203
|
+
for (const unit of RELATIVE_UNITS) {
|
|
204
|
+
phrases[unit] = {
|
|
205
|
+
past: compiled[`date/relative/${unit}/past`] ?? EN[`date/relative/${unit}/past`],
|
|
206
|
+
future: compiled[`date/relative/${unit}/future`] ?? EN[`date/relative/${unit}/future`],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const now = readName(compiled, 'date/relative/now');
|
|
210
|
+
const yesterday = readName(compiled, 'date/relative/yesterday');
|
|
211
|
+
const today = readName(compiled, 'date/relative/today');
|
|
212
|
+
const tomorrow = readName(compiled, 'date/relative/tomorrow');
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Phrase a signed offset. The amount is the caller's: negative is
|
|
216
|
+
* past, positive is future, and nothing here reads a clock.
|
|
217
|
+
* @param {number} amount - Whole units, signed
|
|
218
|
+
* @param {string} unit - One of second|minute|hour|day|week|month|year
|
|
219
|
+
* @param {{numeric?: string}} [options] - `numeric: 'auto'` prefers the four named days
|
|
220
|
+
* @returns {string}
|
|
221
|
+
*/
|
|
222
|
+
function relative(amount, unit, options = undefined) {
|
|
223
|
+
const numeric = checkRelativeArguments(amount, unit, options);
|
|
224
|
+
// 'auto' reaches for a named day only where it is EXACT: a whole day
|
|
225
|
+
// away is yesterday/today/tomorrow and a zero-second offset is now,
|
|
226
|
+
// while '0 minutes' could be anything within the minute and stays
|
|
227
|
+
// numeric.
|
|
228
|
+
if (numeric === 'auto') {
|
|
229
|
+
if (unit === 'day') {
|
|
230
|
+
if (amount === -1) return yesterday;
|
|
231
|
+
if (amount === 0) return today;
|
|
232
|
+
if (amount === 1) return tomorrow;
|
|
233
|
+
}
|
|
234
|
+
else if (unit === 'second' && amount === 0) return now;
|
|
235
|
+
}
|
|
236
|
+
const pair = phrases[unit];
|
|
237
|
+
// Math.abs also folds -0 back to 0, so a signed zero cannot reach a
|
|
238
|
+
// pack's number renderer and come back with a minus in front of it
|
|
239
|
+
const value = Math.abs(amount);
|
|
240
|
+
return amount < 0 ? pair.past({ value }) : pair.future({ value });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// read at compile time like the names, so a label costs a lookup
|
|
244
|
+
const formatNames = Object.create(null);
|
|
245
|
+
for (const format of FORMAT_NAMES)
|
|
246
|
+
formatNames[format] = readName(compiled, FORMAT_PREFIX + format);
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The display name of a date/time format, for a message that would
|
|
250
|
+
* otherwise show the format's wire name. The family is closed at the
|
|
251
|
+
* six date and time formats; anything else has no name here and the
|
|
252
|
+
* caller keeps its own.
|
|
253
|
+
* @param {string} format - A format name (`'date-time'`)
|
|
254
|
+
* @returns {string|undefined} The display name, or undefined when this family has none
|
|
255
|
+
*/
|
|
256
|
+
function formatName(format) {
|
|
257
|
+
return typeof format === 'string' ? formatNames[format] : undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return Object.freeze({ names, relative, formatName });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
//#endregion
|
package/src/de.js
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
makeNumberRenderer,
|
|
25
25
|
makePluralPicker,
|
|
26
26
|
makeTypeNamer,
|
|
27
|
+
dateNameEntries,
|
|
27
28
|
} from './helpers.js';
|
|
28
29
|
|
|
29
30
|
//#region Intl singletons
|
|
@@ -167,4 +168,52 @@ export const de = {
|
|
|
167
168
|
'contract/stream-error': 'der Stream der Operation {op} endete mit einem Serverfehler ({code})',
|
|
168
169
|
'contract/heartbeat-missed': 'der Stream der Operation {op} blieb {ms} ms lang still',
|
|
169
170
|
//#endregion
|
|
171
|
+
|
|
172
|
+
//#region calendar language (the date names, relative phrasing and
|
|
173
|
+
// format display names of @jarenjs/locales' date adapter)
|
|
174
|
+
// Both 'vor' and 'in' govern the dative, so the counted nouns are the
|
|
175
|
+
// dative forms ('vor 2 Tagen', not 'vor 2 Tage').
|
|
176
|
+
...dateNameEntries({
|
|
177
|
+
months: [
|
|
178
|
+
'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
|
|
179
|
+
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember',
|
|
180
|
+
],
|
|
181
|
+
monthsShort: [
|
|
182
|
+
'Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun',
|
|
183
|
+
'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez',
|
|
184
|
+
],
|
|
185
|
+
weekdays: [
|
|
186
|
+
'Sonntag', 'Montag', 'Dienstag', 'Mittwoch',
|
|
187
|
+
'Donnerstag', 'Freitag', 'Samstag',
|
|
188
|
+
],
|
|
189
|
+
weekdaysShort: [
|
|
190
|
+
'So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa',
|
|
191
|
+
],
|
|
192
|
+
meridiem: ['AM', 'PM'],
|
|
193
|
+
}),
|
|
194
|
+
'date/relative/second/past': (p) => `vor ${num(p.value)} ${plural(p.value, 'Sekunde', 'Sekunden')}`,
|
|
195
|
+
'date/relative/second/future': (p) => `in ${num(p.value)} ${plural(p.value, 'Sekunde', 'Sekunden')}`,
|
|
196
|
+
'date/relative/minute/past': (p) => `vor ${num(p.value)} ${plural(p.value, 'Minute', 'Minuten')}`,
|
|
197
|
+
'date/relative/minute/future': (p) => `in ${num(p.value)} ${plural(p.value, 'Minute', 'Minuten')}`,
|
|
198
|
+
'date/relative/hour/past': (p) => `vor ${num(p.value)} ${plural(p.value, 'Stunde', 'Stunden')}`,
|
|
199
|
+
'date/relative/hour/future': (p) => `in ${num(p.value)} ${plural(p.value, 'Stunde', 'Stunden')}`,
|
|
200
|
+
'date/relative/day/past': (p) => `vor ${num(p.value)} ${plural(p.value, 'Tag', 'Tagen')}`,
|
|
201
|
+
'date/relative/day/future': (p) => `in ${num(p.value)} ${plural(p.value, 'Tag', 'Tagen')}`,
|
|
202
|
+
'date/relative/week/past': (p) => `vor ${num(p.value)} ${plural(p.value, 'Woche', 'Wochen')}`,
|
|
203
|
+
'date/relative/week/future': (p) => `in ${num(p.value)} ${plural(p.value, 'Woche', 'Wochen')}`,
|
|
204
|
+
'date/relative/month/past': (p) => `vor ${num(p.value)} ${plural(p.value, 'Monat', 'Monaten')}`,
|
|
205
|
+
'date/relative/month/future': (p) => `in ${num(p.value)} ${plural(p.value, 'Monat', 'Monaten')}`,
|
|
206
|
+
'date/relative/year/past': (p) => `vor ${num(p.value)} ${plural(p.value, 'Jahr', 'Jahren')}`,
|
|
207
|
+
'date/relative/year/future': (p) => `in ${num(p.value)} ${plural(p.value, 'Jahr', 'Jahren')}`,
|
|
208
|
+
'date/relative/now': 'jetzt',
|
|
209
|
+
'date/relative/yesterday': 'gestern',
|
|
210
|
+
'date/relative/today': 'heute',
|
|
211
|
+
'date/relative/tomorrow': 'morgen',
|
|
212
|
+
'format/name/date': 'Datum',
|
|
213
|
+
'format/name/time': 'Uhrzeit',
|
|
214
|
+
'format/name/date-time': 'Datum und Uhrzeit',
|
|
215
|
+
'format/name/iso-date': 'ISO-Datum',
|
|
216
|
+
'format/name/iso-time': 'ISO-Uhrzeit',
|
|
217
|
+
'format/name/iso-date-time': 'ISO-Datum und -Uhrzeit',
|
|
218
|
+
//#endregion
|
|
170
219
|
};
|
package/src/es.js
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
makeNumberRenderer,
|
|
25
25
|
makePluralPicker,
|
|
26
26
|
makeTypeNamer,
|
|
27
|
+
dateNameEntries,
|
|
27
28
|
} from './helpers.js';
|
|
28
29
|
|
|
29
30
|
//#region Intl singletons
|
|
@@ -167,4 +168,50 @@ export const es = {
|
|
|
167
168
|
'contract/stream-error': 'el flujo de la operación {op} terminó con un error del servidor ({code})',
|
|
168
169
|
'contract/heartbeat-missed': 'el flujo de la operación {op} quedó en silencio durante {ms} ms',
|
|
169
170
|
//#endregion
|
|
171
|
+
|
|
172
|
+
//#region calendar language (the date names, relative phrasing and
|
|
173
|
+
// format display names of @jarenjs/locales' date adapter)
|
|
174
|
+
...dateNameEntries({
|
|
175
|
+
months: [
|
|
176
|
+
'enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio',
|
|
177
|
+
'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre',
|
|
178
|
+
],
|
|
179
|
+
monthsShort: [
|
|
180
|
+
'ene', 'feb', 'mar', 'abr', 'may', 'jun',
|
|
181
|
+
'jul', 'ago', 'sept', 'oct', 'nov', 'dic',
|
|
182
|
+
],
|
|
183
|
+
weekdays: [
|
|
184
|
+
'domingo', 'lunes', 'martes', 'miércoles',
|
|
185
|
+
'jueves', 'viernes', 'sábado',
|
|
186
|
+
],
|
|
187
|
+
weekdaysShort: [
|
|
188
|
+
'dom', 'lun', 'mar', 'mié', 'jue', 'vie', 'sáb',
|
|
189
|
+
],
|
|
190
|
+
meridiem: ['a. m.', 'p. m.'],
|
|
191
|
+
}),
|
|
192
|
+
'date/relative/second/past': (p) => `hace ${num(p.value)} ${plural(p.value, 'segundo', 'segundos')}`,
|
|
193
|
+
'date/relative/second/future': (p) => `dentro de ${num(p.value)} ${plural(p.value, 'segundo', 'segundos')}`,
|
|
194
|
+
'date/relative/minute/past': (p) => `hace ${num(p.value)} ${plural(p.value, 'minuto', 'minutos')}`,
|
|
195
|
+
'date/relative/minute/future': (p) => `dentro de ${num(p.value)} ${plural(p.value, 'minuto', 'minutos')}`,
|
|
196
|
+
'date/relative/hour/past': (p) => `hace ${num(p.value)} ${plural(p.value, 'hora', 'horas')}`,
|
|
197
|
+
'date/relative/hour/future': (p) => `dentro de ${num(p.value)} ${plural(p.value, 'hora', 'horas')}`,
|
|
198
|
+
'date/relative/day/past': (p) => `hace ${num(p.value)} ${plural(p.value, 'día', 'días')}`,
|
|
199
|
+
'date/relative/day/future': (p) => `dentro de ${num(p.value)} ${plural(p.value, 'día', 'días')}`,
|
|
200
|
+
'date/relative/week/past': (p) => `hace ${num(p.value)} ${plural(p.value, 'semana', 'semanas')}`,
|
|
201
|
+
'date/relative/week/future': (p) => `dentro de ${num(p.value)} ${plural(p.value, 'semana', 'semanas')}`,
|
|
202
|
+
'date/relative/month/past': (p) => `hace ${num(p.value)} ${plural(p.value, 'mes', 'meses')}`,
|
|
203
|
+
'date/relative/month/future': (p) => `dentro de ${num(p.value)} ${plural(p.value, 'mes', 'meses')}`,
|
|
204
|
+
'date/relative/year/past': (p) => `hace ${num(p.value)} ${plural(p.value, 'año', 'años')}`,
|
|
205
|
+
'date/relative/year/future': (p) => `dentro de ${num(p.value)} ${plural(p.value, 'año', 'años')}`,
|
|
206
|
+
'date/relative/now': 'ahora',
|
|
207
|
+
'date/relative/yesterday': 'ayer',
|
|
208
|
+
'date/relative/today': 'hoy',
|
|
209
|
+
'date/relative/tomorrow': 'mañana',
|
|
210
|
+
'format/name/date': 'fecha',
|
|
211
|
+
'format/name/time': 'hora',
|
|
212
|
+
'format/name/date-time': 'fecha y hora',
|
|
213
|
+
'format/name/iso-date': 'fecha ISO',
|
|
214
|
+
'format/name/iso-time': 'hora ISO',
|
|
215
|
+
'format/name/iso-date-time': 'fecha y hora ISO',
|
|
216
|
+
//#endregion
|
|
170
217
|
};
|
package/src/fr.js
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
makeNumberRenderer,
|
|
25
25
|
makePluralPicker,
|
|
26
26
|
makeTypeNamer,
|
|
27
|
+
dateNameEntries,
|
|
27
28
|
} from './helpers.js';
|
|
28
29
|
|
|
29
30
|
//#region Intl singletons
|
|
@@ -167,4 +168,52 @@ export const fr = {
|
|
|
167
168
|
'contract/stream-error': "le flux de l'opération {op} s'est terminé par une erreur serveur ({code})",
|
|
168
169
|
'contract/heartbeat-missed': "le flux de l'opération {op} est resté silencieux pendant {ms} ms",
|
|
169
170
|
//#endregion
|
|
171
|
+
|
|
172
|
+
//#region calendar language (the date names, relative phrasing and
|
|
173
|
+
// format display names of @jarenjs/locales' date adapter)
|
|
174
|
+
// French keeps the singular at zero as well as at one, which is what
|
|
175
|
+
// the pack's CLDR plural rules already say; 'mois' has no plural mark.
|
|
176
|
+
...dateNameEntries({
|
|
177
|
+
months: [
|
|
178
|
+
'janvier', 'février', 'mars', 'avril', 'mai', 'juin',
|
|
179
|
+
'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre',
|
|
180
|
+
],
|
|
181
|
+
monthsShort: [
|
|
182
|
+
'janv.', 'févr.', 'mars', 'avr.', 'mai', 'juin',
|
|
183
|
+
'juil.', 'août', 'sept.', 'oct.', 'nov.', 'déc.',
|
|
184
|
+
],
|
|
185
|
+
weekdays: [
|
|
186
|
+
'dimanche', 'lundi', 'mardi', 'mercredi',
|
|
187
|
+
'jeudi', 'vendredi', 'samedi',
|
|
188
|
+
],
|
|
189
|
+
weekdaysShort: [
|
|
190
|
+
'dim.', 'lun.', 'mar.', 'mer.', 'jeu.', 'ven.', 'sam.',
|
|
191
|
+
],
|
|
192
|
+
meridiem: ['AM', 'PM'],
|
|
193
|
+
}),
|
|
194
|
+
'date/relative/second/past': (p) => `il y a ${num(p.value)} ${plural(p.value, 'seconde', 'secondes')}`,
|
|
195
|
+
'date/relative/second/future': (p) => `dans ${num(p.value)} ${plural(p.value, 'seconde', 'secondes')}`,
|
|
196
|
+
'date/relative/minute/past': (p) => `il y a ${num(p.value)} ${plural(p.value, 'minute', 'minutes')}`,
|
|
197
|
+
'date/relative/minute/future': (p) => `dans ${num(p.value)} ${plural(p.value, 'minute', 'minutes')}`,
|
|
198
|
+
'date/relative/hour/past': (p) => `il y a ${num(p.value)} ${plural(p.value, 'heure', 'heures')}`,
|
|
199
|
+
'date/relative/hour/future': (p) => `dans ${num(p.value)} ${plural(p.value, 'heure', 'heures')}`,
|
|
200
|
+
'date/relative/day/past': (p) => `il y a ${num(p.value)} ${plural(p.value, 'jour', 'jours')}`,
|
|
201
|
+
'date/relative/day/future': (p) => `dans ${num(p.value)} ${plural(p.value, 'jour', 'jours')}`,
|
|
202
|
+
'date/relative/week/past': (p) => `il y a ${num(p.value)} ${plural(p.value, 'semaine', 'semaines')}`,
|
|
203
|
+
'date/relative/week/future': (p) => `dans ${num(p.value)} ${plural(p.value, 'semaine', 'semaines')}`,
|
|
204
|
+
'date/relative/month/past': (p) => `il y a ${num(p.value)} ${plural(p.value, 'mois', 'mois')}`,
|
|
205
|
+
'date/relative/month/future': (p) => `dans ${num(p.value)} ${plural(p.value, 'mois', 'mois')}`,
|
|
206
|
+
'date/relative/year/past': (p) => `il y a ${num(p.value)} ${plural(p.value, 'an', 'ans')}`,
|
|
207
|
+
'date/relative/year/future': (p) => `dans ${num(p.value)} ${plural(p.value, 'an', 'ans')}`,
|
|
208
|
+
'date/relative/now': 'maintenant',
|
|
209
|
+
'date/relative/yesterday': 'hier',
|
|
210
|
+
'date/relative/today': 'aujourd\'hui',
|
|
211
|
+
'date/relative/tomorrow': 'demain',
|
|
212
|
+
'format/name/date': 'date',
|
|
213
|
+
'format/name/time': 'heure',
|
|
214
|
+
'format/name/date-time': 'date et heure',
|
|
215
|
+
'format/name/iso-date': 'date ISO',
|
|
216
|
+
'format/name/iso-time': 'heure ISO',
|
|
217
|
+
'format/name/iso-date-time': 'date et heure ISO',
|
|
218
|
+
//#endregion
|
|
170
219
|
};
|
package/src/helpers.js
CHANGED
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
* Each factory takes a pack's own `Intl` singleton or translated table
|
|
9
9
|
* and returns the render closure the catalog entries call. Building the
|
|
10
10
|
* closure once at module load keeps the packs' allocation discipline:
|
|
11
|
-
* nothing is constructed per message.
|
|
12
|
-
*
|
|
11
|
+
* nothing is constructed per message. The calendar half is here for the
|
|
12
|
+
* same reason: the date msgids are mechanical, and expanding them from
|
|
13
|
+
* arrays is what keeps forty keys per pack from being forty chances to
|
|
14
|
+
* mistype one. This module is internal to `@jarenjs/locales` - packs and
|
|
15
|
+
* the date adapters import it, consumers never see it.
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
18
|
export { formatMessageValue } from '@jarenjs/core/message';
|
|
@@ -57,3 +60,114 @@ export function makeTypeNamer(typeNames) {
|
|
|
57
60
|
return typeNames[type] ?? type;
|
|
58
61
|
};
|
|
59
62
|
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build a multi-form noun picker over a pack's plural rules: the
|
|
66
|
+
* count's CLDR category selects a member of the `forms` record, and
|
|
67
|
+
* `other` covers every category the record leaves out. For languages
|
|
68
|
+
* whose counted messages need more than the two forms
|
|
69
|
+
* {@link makePluralPicker} covers - Russian's one/few/many, Arabic's
|
|
70
|
+
* six.
|
|
71
|
+
*
|
|
72
|
+
* @param {Intl.PluralRules} pluralRules - The pack's plural rules
|
|
73
|
+
* @returns {(count: number, forms: Record<string, string>) => string} The form picker
|
|
74
|
+
* @example
|
|
75
|
+
* plural(21, { one: 'секунду', few: 'секунды', other: 'секунд' }); // 'секунду'
|
|
76
|
+
*/
|
|
77
|
+
export function makePluralForms(pluralRules) {
|
|
78
|
+
return function pickPluralForms(count, forms) {
|
|
79
|
+
return forms[pluralRules.select(count)] ?? forms.other;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The relative-time units, in the order a duration shrinks. A unit
|
|
85
|
+
* outside this list is refused rather than approximated.
|
|
86
|
+
*/
|
|
87
|
+
export const RELATIVE_UNITS = Object.freeze([
|
|
88
|
+
'second', 'minute', 'hour', 'day', 'week', 'month', 'year',
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check a relative-time call and resolve its numeric mode. Both date
|
|
93
|
+
* locale providers - the repository one and the `Intl` one - run this,
|
|
94
|
+
* so the opt-in provider refuses exactly what the default provider
|
|
95
|
+
* refuses instead of quietly answering where the other raises.
|
|
96
|
+
*
|
|
97
|
+
* @param {number} amount - Whole units, signed: negative past, positive future
|
|
98
|
+
* @param {string} unit - One of {@link RELATIVE_UNITS}
|
|
99
|
+
* @param {{numeric?: string}} [options] - The caller's options
|
|
100
|
+
* @returns {string} The resolved numeric mode, `'always'` or `'auto'`
|
|
101
|
+
* @throws {TypeError} on a fractional amount, an unsupported unit or an unknown mode
|
|
102
|
+
*/
|
|
103
|
+
export function checkRelativeArguments(amount, unit, options) {
|
|
104
|
+
if (!Number.isInteger(amount))
|
|
105
|
+
throw new TypeError(`a relative amount must be a whole number of units, got ${amount}`);
|
|
106
|
+
if (!RELATIVE_UNITS.includes(unit))
|
|
107
|
+
throw new TypeError(`unsupported relative unit '${unit}'`);
|
|
108
|
+
const numeric = options == null || options.numeric === undefined ? 'always' : options.numeric;
|
|
109
|
+
if (numeric !== 'always' && numeric !== 'auto')
|
|
110
|
+
throw new TypeError(`unknown relative numeric mode '${numeric}'`);
|
|
111
|
+
return numeric;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Two-digit month numbers, so a msgid sorts the way a calendar reads. */
|
|
115
|
+
const MONTH_NUMBERS = Object.freeze([
|
|
116
|
+
'01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12',
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check one name array: exact length, every member a non-empty string.
|
|
121
|
+
* @param {unknown} values - The candidate array
|
|
122
|
+
* @param {number} count - The exact length required
|
|
123
|
+
* @param {string} what - The member name, for the refusal
|
|
124
|
+
* @returns {string[]}
|
|
125
|
+
*/
|
|
126
|
+
function requireNames(values, count, what) {
|
|
127
|
+
if (!Array.isArray(values) || values.length !== count)
|
|
128
|
+
throw new TypeError(`date names '${what}' must be an array of exactly ${count} strings`);
|
|
129
|
+
for (let i = 0; i < count; ++i) {
|
|
130
|
+
if (typeof values[i] !== 'string' || values[i] === '')
|
|
131
|
+
throw new TypeError(`date names '${what}[${i}]' must be a non-empty string`);
|
|
132
|
+
}
|
|
133
|
+
return values;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Expand a pack's five calendar-name arrays into the flat `date/...`
|
|
138
|
+
* msgid entries every catalog carries. The arrays are what a translator
|
|
139
|
+
* wants to read and the flat keys are what the catalog contract needs,
|
|
140
|
+
* so the expansion - and the length/order check that goes with it -
|
|
141
|
+
* happens once here rather than as forty hand-typed keys per pack.
|
|
142
|
+
*
|
|
143
|
+
* Weekdays start at Sunday, matching the index
|
|
144
|
+
* `@jarenjs/core/dates`' `EEEE`/`EEE` tokens look names up by.
|
|
145
|
+
*
|
|
146
|
+
* @param {{months: string[], monthsShort: string[], weekdays: string[], weekdaysShort: string[], meridiem: string[]}} names - The pack's calendar names
|
|
147
|
+
* @returns {Record<string, string>} The `date/month|weekday|meridiem/...` entries
|
|
148
|
+
* @throws {TypeError} when an array has the wrong length or a non-string member
|
|
149
|
+
* @example
|
|
150
|
+
* dateNameEntries({ months, monthsShort, weekdays, weekdaysShort, meridiem })
|
|
151
|
+
* // { 'date/month/01/wide': 'januari', ..., 'date/meridiem/pm': 'p.m.' }
|
|
152
|
+
*/
|
|
153
|
+
export function dateNameEntries({ months, monthsShort, weekdays, weekdaysShort, meridiem }) {
|
|
154
|
+
requireNames(months, 12, 'months');
|
|
155
|
+
requireNames(monthsShort, 12, 'monthsShort');
|
|
156
|
+
requireNames(weekdays, 7, 'weekdays');
|
|
157
|
+
requireNames(weekdaysShort, 7, 'weekdaysShort');
|
|
158
|
+
requireNames(meridiem, 2, 'meridiem');
|
|
159
|
+
|
|
160
|
+
/** @type {Record<string, string>} */
|
|
161
|
+
const entries = {};
|
|
162
|
+
for (let i = 0; i < 12; ++i) {
|
|
163
|
+
entries[`date/month/${MONTH_NUMBERS[i]}/wide`] = months[i];
|
|
164
|
+
entries[`date/month/${MONTH_NUMBERS[i]}/short`] = monthsShort[i];
|
|
165
|
+
}
|
|
166
|
+
for (let i = 0; i < 7; ++i) {
|
|
167
|
+
entries[`date/weekday/${i}/wide`] = weekdays[i];
|
|
168
|
+
entries[`date/weekday/${i}/short`] = weekdaysShort[i];
|
|
169
|
+
}
|
|
170
|
+
entries['date/meridiem/am'] = meridiem[0];
|
|
171
|
+
entries['date/meridiem/pm'] = meridiem[1];
|
|
172
|
+
return entries;
|
|
173
|
+
}
|
package/src/index.js
CHANGED
|
@@ -11,8 +11,20 @@
|
|
|
11
11
|
* own translations and `Intl` singletons, and imports nothing but the
|
|
12
12
|
* rendering helpers of `./helpers.js` - never a consumer package, so
|
|
13
13
|
* either consumer can serve any pack.
|
|
14
|
+
*
|
|
15
|
+
* Beside the error messages, every pack carries the calendar language
|
|
16
|
+
* `@jarenjs/core/dates` refuses to invent: month, weekday and meridiem
|
|
17
|
+
* names, relative-time phrases and date-format display names.
|
|
18
|
+
* `compileDateLocale` (`./dates`) turns a pack into the frozen record a
|
|
19
|
+
* formatter and a UI read; importing that subpath directly costs no
|
|
20
|
+
* `Intl` construction at all, which is what keeps server-rendered output
|
|
21
|
+
* byte-stable. `createIntlDateLocale` (`./intl-dates`) is the opt-in
|
|
22
|
+
* provider for hosts that want the platform's locales instead.
|
|
14
23
|
*/
|
|
15
24
|
|
|
25
|
+
export { dateMessagesEn, compileDateLocale, RELATIVE_UNITS } from './dates.js';
|
|
26
|
+
export { createIntlDateLocale } from './intl-dates.js';
|
|
27
|
+
|
|
16
28
|
export { ar } from './ar.js';
|
|
17
29
|
export { de } from './de.js';
|
|
18
30
|
export { es } from './es.js';
|