@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/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. This module is internal to
12
- * `@jarenjs/locales` - packs import it, consumers never see it.
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';