@lingui/message-utils 5.1.1 → 5.2.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.
@@ -2,6 +2,305 @@
2
2
 
3
3
  const parser = require('@messageformat/parser');
4
4
 
5
+ /**
6
+ * Parent class for errors.
7
+ *
8
+ * @remarks
9
+ * Errors with `type: "warning"` do not necessarily indicate that the parser
10
+ * encountered an error. In addition to a human-friendly `message`, may also
11
+ * includes the `token` at which the error was encountered.
12
+ *
13
+ * @public
14
+ */
15
+ class DateFormatError extends Error {
16
+ /** @internal */
17
+ constructor(msg, token, type) {
18
+ super(msg);
19
+ this.token = token;
20
+ this.type = type || 'error';
21
+ }
22
+ }
23
+ const alpha = (width) => width < 4 ? 'short' : width === 4 ? 'long' : 'narrow';
24
+ const numeric = (width) => (width % 2 === 0 ? '2-digit' : 'numeric');
25
+ function yearOptions(token, onError) {
26
+ switch (token.char) {
27
+ case 'y':
28
+ return { year: numeric(token.width) };
29
+ case 'r':
30
+ return { calendar: 'gregory', year: 'numeric' };
31
+ case 'u':
32
+ case 'U':
33
+ case 'Y':
34
+ default:
35
+ onError(`${token.desc} is not supported; falling back to year:numeric`, DateFormatError.WARNING);
36
+ return { year: 'numeric' };
37
+ }
38
+ }
39
+ function monthStyle(token, onError) {
40
+ switch (token.width) {
41
+ case 1:
42
+ return 'numeric';
43
+ case 2:
44
+ return '2-digit';
45
+ case 3:
46
+ return 'short';
47
+ case 4:
48
+ return 'long';
49
+ case 5:
50
+ return 'narrow';
51
+ default:
52
+ onError(`${token.desc} is not supported with width ${token.width}`);
53
+ return undefined;
54
+ }
55
+ }
56
+ function dayStyle(token, onError) {
57
+ const { char, desc, width } = token;
58
+ if (char === 'd') {
59
+ return numeric(width);
60
+ }
61
+ else {
62
+ onError(`${desc} is not supported`);
63
+ return undefined;
64
+ }
65
+ }
66
+ function weekdayStyle(token, onError) {
67
+ const { char, desc, width } = token;
68
+ if ((char === 'c' || char === 'e') && width < 3) {
69
+ // ignoring stand-alone-ness
70
+ const msg = `Numeric value is not supported for ${desc}; falling back to weekday:short`;
71
+ onError(msg, DateFormatError.WARNING);
72
+ }
73
+ // merging narrow styles
74
+ return alpha(width);
75
+ }
76
+ function hourOptions(token) {
77
+ const hour = numeric(token.width);
78
+ let hourCycle;
79
+ switch (token.char) {
80
+ case 'h':
81
+ hourCycle = 'h12';
82
+ break;
83
+ case 'H':
84
+ hourCycle = 'h23';
85
+ break;
86
+ case 'k':
87
+ hourCycle = 'h24';
88
+ break;
89
+ case 'K':
90
+ hourCycle = 'h11';
91
+ break;
92
+ }
93
+ return hourCycle ? { hour, hourCycle } : { hour };
94
+ }
95
+ function timeZoneNameStyle(token, onError) {
96
+ // so much fallback behaviour here
97
+ const { char, desc, width } = token;
98
+ switch (char) {
99
+ case 'v':
100
+ case 'z':
101
+ return width === 4 ? 'long' : 'short';
102
+ case 'V':
103
+ if (width === 4)
104
+ return 'long';
105
+ onError(`${desc} is not supported with width ${width}`);
106
+ return undefined;
107
+ case 'X':
108
+ onError(`${desc} is not supported`);
109
+ return undefined;
110
+ }
111
+ return 'short';
112
+ }
113
+ function compileOptions(token, onError) {
114
+ switch (token.field) {
115
+ case 'era':
116
+ return { era: alpha(token.width) };
117
+ case 'year':
118
+ return yearOptions(token, onError);
119
+ case 'month':
120
+ return { month: monthStyle(token, onError) };
121
+ case 'day':
122
+ return { day: dayStyle(token, onError) };
123
+ case 'weekday':
124
+ return { weekday: weekdayStyle(token, onError) };
125
+ case 'period':
126
+ return undefined;
127
+ case 'hour':
128
+ return hourOptions(token);
129
+ case 'min':
130
+ return { minute: numeric(token.width) };
131
+ case 'sec':
132
+ return { second: numeric(token.width) };
133
+ case 'tz':
134
+ return { timeZoneName: timeZoneNameStyle(token, onError) };
135
+ case 'quarter':
136
+ case 'week':
137
+ case 'sec-frac':
138
+ case 'ms':
139
+ onError(`${token.desc} is not supported`);
140
+ }
141
+ return undefined;
142
+ }
143
+ function getDateFormatOptions(tokens, timeZone, onError = error => {
144
+ throw error;
145
+ }) {
146
+ const options = {
147
+ timeZone
148
+ };
149
+ const fields = [];
150
+ for (const token of tokens) {
151
+ const { error, field, str } = token;
152
+ if (error) {
153
+ const dte = new DateFormatError(error.message, token);
154
+ dte.stack = error.stack;
155
+ onError(dte);
156
+ }
157
+ if (str) {
158
+ const msg = `Ignoring string part: ${str}`;
159
+ onError(new DateFormatError(msg, token, DateFormatError.WARNING));
160
+ }
161
+ if (field) {
162
+ if (fields.indexOf(field) === -1)
163
+ fields.push(field);
164
+ else
165
+ onError(new DateFormatError(`Duplicate ${field} token`, token));
166
+ }
167
+ const opt = compileOptions(token, (msg, isWarning) => onError(new DateFormatError(msg, token, isWarning)));
168
+ if (opt)
169
+ Object.assign(options, opt);
170
+ }
171
+ return options;
172
+ }
173
+
174
+ const fields = {
175
+ G: { field: 'era', desc: 'Era' },
176
+ y: { field: 'year', desc: 'Year' },
177
+ Y: { field: 'year', desc: 'Year of "Week of Year"' },
178
+ u: { field: 'year', desc: 'Extended year' },
179
+ U: { field: 'year', desc: 'Cyclic year name' },
180
+ r: { field: 'year', desc: 'Related Gregorian year' },
181
+ Q: { field: 'quarter', desc: 'Quarter' },
182
+ q: { field: 'quarter', desc: 'Stand-alone quarter' },
183
+ M: { field: 'month', desc: 'Month in year' },
184
+ L: { field: 'month', desc: 'Stand-alone month in year' },
185
+ w: { field: 'week', desc: 'Week of year' },
186
+ W: { field: 'week', desc: 'Week of month' },
187
+ d: { field: 'day', desc: 'Day in month' },
188
+ D: { field: 'day', desc: 'Day of year' },
189
+ F: { field: 'day', desc: 'Day of week in month' },
190
+ g: { field: 'day', desc: 'Modified julian day' },
191
+ E: { field: 'weekday', desc: 'Day of week' },
192
+ e: { field: 'weekday', desc: 'Local day of week' },
193
+ c: { field: 'weekday', desc: 'Stand-alone local day of week' },
194
+ a: { field: 'period', desc: 'AM/PM marker' },
195
+ b: { field: 'period', desc: 'AM/PM/noon/midnight marker' },
196
+ B: { field: 'period', desc: 'Flexible day period' },
197
+ h: { field: 'hour', desc: 'Hour in AM/PM (1~12)' },
198
+ H: { field: 'hour', desc: 'Hour in day (0~23)' },
199
+ k: { field: 'hour', desc: 'Hour in day (1~24)' },
200
+ K: { field: 'hour', desc: 'Hour in AM/PM (0~11)' },
201
+ j: { field: 'hour', desc: 'Hour in preferred cycle' },
202
+ J: { field: 'hour', desc: 'Hour in preferred cycle without marker' },
203
+ C: { field: 'hour', desc: 'Hour in preferred cycle with flexible marker' },
204
+ m: { field: 'min', desc: 'Minute in hour' },
205
+ s: { field: 'sec', desc: 'Second in minute' },
206
+ S: { field: 'sec-frac', desc: 'Fractional second' },
207
+ A: { field: 'ms', desc: 'Milliseconds in day' },
208
+ z: { field: 'tz', desc: 'Time Zone: specific non-location' },
209
+ Z: { field: 'tz', desc: 'Time Zone' },
210
+ O: { field: 'tz', desc: 'Time Zone: localized' },
211
+ v: { field: 'tz', desc: 'Time Zone: generic non-location' },
212
+ V: { field: 'tz', desc: 'Time Zone: ID' },
213
+ X: { field: 'tz', desc: 'Time Zone: ISO8601 with Z' },
214
+ x: { field: 'tz', desc: 'Time Zone: ISO8601' }
215
+ };
216
+ const isLetter = (char) => (char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z');
217
+ function readFieldToken(src, pos) {
218
+ const char = src[pos];
219
+ let width = 1;
220
+ while (src[++pos] === char)
221
+ ++width;
222
+ const field = fields[char];
223
+ if (!field) {
224
+ const msg = `The letter ${char} is not a valid field identifier`;
225
+ return { char, error: new Error(msg), width };
226
+ }
227
+ return { char, field: field.field, desc: field.desc, width };
228
+ }
229
+ function readQuotedToken(src, pos) {
230
+ let str = src[++pos];
231
+ let width = 2;
232
+ if (str === "'")
233
+ return { char: "'", str, width };
234
+ while (true) {
235
+ const next = src[++pos];
236
+ ++width;
237
+ if (next === undefined) {
238
+ const msg = `Unterminated quoted literal in pattern: ${str || src}`;
239
+ return { char: "'", error: new Error(msg), str, width };
240
+ }
241
+ else if (next === "'") {
242
+ if (src[++pos] !== "'")
243
+ return { char: "'", str, width };
244
+ else
245
+ ++width;
246
+ }
247
+ str += next;
248
+ }
249
+ }
250
+ function readToken(src, pos) {
251
+ const char = src[pos];
252
+ if (!char)
253
+ return null;
254
+ if (isLetter(char))
255
+ return readFieldToken(src, pos);
256
+ if (char === "'")
257
+ return readQuotedToken(src, pos);
258
+ let str = char;
259
+ let width = 1;
260
+ while (true) {
261
+ const next = src[++pos];
262
+ if (!next || isLetter(next) || next === "'")
263
+ return { char, str, width };
264
+ str += next;
265
+ width += 1;
266
+ }
267
+ }
268
+ /**
269
+ * Parse an {@link http://userguide.icu-project.org/formatparse/datetime | ICU
270
+ * DateFormat skeleton} string into a {@link DateToken} array.
271
+ *
272
+ * @remarks
273
+ * Errors will not be thrown, but if encountered are included as the relevant
274
+ * token's `error` value.
275
+ *
276
+ * @public
277
+ * @param src - The skeleton string
278
+ *
279
+ * @example
280
+ * ```js
281
+ * import { parseDateTokens } from '@messageformat/date-skeleton'
282
+ *
283
+ * parseDateTokens('GrMMMdd', console.error)
284
+ * // [
285
+ * // { char: 'G', field: 'era', desc: 'Era', width: 1 },
286
+ * // { char: 'r', field: 'year', desc: 'Related Gregorian year', width: 1 },
287
+ * // { char: 'M', field: 'month', desc: 'Month in year', width: 3 },
288
+ * // { char: 'd', field: 'day', desc: 'Day in month', width: 2 }
289
+ * // ]
290
+ * ```
291
+ */
292
+ function parseDateTokens(src) {
293
+ const tokens = [];
294
+ let pos = 0;
295
+ while (true) {
296
+ const token = readToken(src, pos);
297
+ if (!token)
298
+ return tokens;
299
+ tokens.push(token);
300
+ pos += token.width;
301
+ }
302
+ }
303
+
5
304
  function processTokens(tokens, mapText) {
6
305
  if (!tokens.filter((token) => token.type !== "content").length) {
7
306
  return tokens.map((token) => mapText(token.value));
@@ -15,6 +314,12 @@ function processTokens(tokens, mapText) {
15
314
  return [token.arg];
16
315
  } else if (token.type === "function") {
17
316
  const _param = token?.param?.[0];
317
+ if (token.key === "date" && _param) {
318
+ const opts = compileDateExpression(_param.value.trim(), (e) => {
319
+ throw new Error(`Unable to compile date expression: ${e.message}`);
320
+ });
321
+ return [token.arg, token.key, opts];
322
+ }
18
323
  if (_param) {
19
324
  return [token.arg, token.key, _param.value.trim()];
20
325
  } else {
@@ -37,6 +342,13 @@ function processTokens(tokens, mapText) {
37
342
  ];
38
343
  });
39
344
  }
345
+ function compileDateExpression(format, onError) {
346
+ if (/^::/.test(format)) {
347
+ const tokens = parseDateTokens(format.substring(2));
348
+ return getDateFormatOptions(tokens, void 0, onError);
349
+ }
350
+ return format;
351
+ }
40
352
  function compileMessage(message, mapText = (v) => v) {
41
353
  try {
42
354
  return processTokens(parser.parse(message), mapText);
@@ -1,7 +1,11 @@
1
1
  type CompiledIcuChoices = Record<string, CompiledMessage> & {
2
2
  offset: number | undefined;
3
3
  };
4
- type CompiledMessageToken = string | [name: string, type?: string, format?: null | string | CompiledIcuChoices];
4
+ type CompiledMessageToken = string | [
5
+ name: string,
6
+ type?: string,
7
+ format?: null | string | unknown | CompiledIcuChoices
8
+ ];
5
9
  type CompiledMessage = CompiledMessageToken[];
6
10
  type MapTextFn = (value: string) => string;
7
11
  declare function compileMessage(message: string, mapText?: MapTextFn): CompiledMessage;
@@ -1,7 +1,11 @@
1
1
  type CompiledIcuChoices = Record<string, CompiledMessage> & {
2
2
  offset: number | undefined;
3
3
  };
4
- type CompiledMessageToken = string | [name: string, type?: string, format?: null | string | CompiledIcuChoices];
4
+ type CompiledMessageToken = string | [
5
+ name: string,
6
+ type?: string,
7
+ format?: null | string | unknown | CompiledIcuChoices
8
+ ];
5
9
  type CompiledMessage = CompiledMessageToken[];
6
10
  type MapTextFn = (value: string) => string;
7
11
  declare function compileMessage(message: string, mapText?: MapTextFn): CompiledMessage;
@@ -1,7 +1,11 @@
1
1
  type CompiledIcuChoices = Record<string, CompiledMessage> & {
2
2
  offset: number | undefined;
3
3
  };
4
- type CompiledMessageToken = string | [name: string, type?: string, format?: null | string | CompiledIcuChoices];
4
+ type CompiledMessageToken = string | [
5
+ name: string,
6
+ type?: string,
7
+ format?: null | string | unknown | CompiledIcuChoices
8
+ ];
5
9
  type CompiledMessage = CompiledMessageToken[];
6
10
  type MapTextFn = (value: string) => string;
7
11
  declare function compileMessage(message: string, mapText?: MapTextFn): CompiledMessage;
@@ -1,5 +1,304 @@
1
1
  import { parse } from '@messageformat/parser';
2
2
 
3
+ /**
4
+ * Parent class for errors.
5
+ *
6
+ * @remarks
7
+ * Errors with `type: "warning"` do not necessarily indicate that the parser
8
+ * encountered an error. In addition to a human-friendly `message`, may also
9
+ * includes the `token` at which the error was encountered.
10
+ *
11
+ * @public
12
+ */
13
+ class DateFormatError extends Error {
14
+ /** @internal */
15
+ constructor(msg, token, type) {
16
+ super(msg);
17
+ this.token = token;
18
+ this.type = type || 'error';
19
+ }
20
+ }
21
+ const alpha = (width) => width < 4 ? 'short' : width === 4 ? 'long' : 'narrow';
22
+ const numeric = (width) => (width % 2 === 0 ? '2-digit' : 'numeric');
23
+ function yearOptions(token, onError) {
24
+ switch (token.char) {
25
+ case 'y':
26
+ return { year: numeric(token.width) };
27
+ case 'r':
28
+ return { calendar: 'gregory', year: 'numeric' };
29
+ case 'u':
30
+ case 'U':
31
+ case 'Y':
32
+ default:
33
+ onError(`${token.desc} is not supported; falling back to year:numeric`, DateFormatError.WARNING);
34
+ return { year: 'numeric' };
35
+ }
36
+ }
37
+ function monthStyle(token, onError) {
38
+ switch (token.width) {
39
+ case 1:
40
+ return 'numeric';
41
+ case 2:
42
+ return '2-digit';
43
+ case 3:
44
+ return 'short';
45
+ case 4:
46
+ return 'long';
47
+ case 5:
48
+ return 'narrow';
49
+ default:
50
+ onError(`${token.desc} is not supported with width ${token.width}`);
51
+ return undefined;
52
+ }
53
+ }
54
+ function dayStyle(token, onError) {
55
+ const { char, desc, width } = token;
56
+ if (char === 'd') {
57
+ return numeric(width);
58
+ }
59
+ else {
60
+ onError(`${desc} is not supported`);
61
+ return undefined;
62
+ }
63
+ }
64
+ function weekdayStyle(token, onError) {
65
+ const { char, desc, width } = token;
66
+ if ((char === 'c' || char === 'e') && width < 3) {
67
+ // ignoring stand-alone-ness
68
+ const msg = `Numeric value is not supported for ${desc}; falling back to weekday:short`;
69
+ onError(msg, DateFormatError.WARNING);
70
+ }
71
+ // merging narrow styles
72
+ return alpha(width);
73
+ }
74
+ function hourOptions(token) {
75
+ const hour = numeric(token.width);
76
+ let hourCycle;
77
+ switch (token.char) {
78
+ case 'h':
79
+ hourCycle = 'h12';
80
+ break;
81
+ case 'H':
82
+ hourCycle = 'h23';
83
+ break;
84
+ case 'k':
85
+ hourCycle = 'h24';
86
+ break;
87
+ case 'K':
88
+ hourCycle = 'h11';
89
+ break;
90
+ }
91
+ return hourCycle ? { hour, hourCycle } : { hour };
92
+ }
93
+ function timeZoneNameStyle(token, onError) {
94
+ // so much fallback behaviour here
95
+ const { char, desc, width } = token;
96
+ switch (char) {
97
+ case 'v':
98
+ case 'z':
99
+ return width === 4 ? 'long' : 'short';
100
+ case 'V':
101
+ if (width === 4)
102
+ return 'long';
103
+ onError(`${desc} is not supported with width ${width}`);
104
+ return undefined;
105
+ case 'X':
106
+ onError(`${desc} is not supported`);
107
+ return undefined;
108
+ }
109
+ return 'short';
110
+ }
111
+ function compileOptions(token, onError) {
112
+ switch (token.field) {
113
+ case 'era':
114
+ return { era: alpha(token.width) };
115
+ case 'year':
116
+ return yearOptions(token, onError);
117
+ case 'month':
118
+ return { month: monthStyle(token, onError) };
119
+ case 'day':
120
+ return { day: dayStyle(token, onError) };
121
+ case 'weekday':
122
+ return { weekday: weekdayStyle(token, onError) };
123
+ case 'period':
124
+ return undefined;
125
+ case 'hour':
126
+ return hourOptions(token);
127
+ case 'min':
128
+ return { minute: numeric(token.width) };
129
+ case 'sec':
130
+ return { second: numeric(token.width) };
131
+ case 'tz':
132
+ return { timeZoneName: timeZoneNameStyle(token, onError) };
133
+ case 'quarter':
134
+ case 'week':
135
+ case 'sec-frac':
136
+ case 'ms':
137
+ onError(`${token.desc} is not supported`);
138
+ }
139
+ return undefined;
140
+ }
141
+ function getDateFormatOptions(tokens, timeZone, onError = error => {
142
+ throw error;
143
+ }) {
144
+ const options = {
145
+ timeZone
146
+ };
147
+ const fields = [];
148
+ for (const token of tokens) {
149
+ const { error, field, str } = token;
150
+ if (error) {
151
+ const dte = new DateFormatError(error.message, token);
152
+ dte.stack = error.stack;
153
+ onError(dte);
154
+ }
155
+ if (str) {
156
+ const msg = `Ignoring string part: ${str}`;
157
+ onError(new DateFormatError(msg, token, DateFormatError.WARNING));
158
+ }
159
+ if (field) {
160
+ if (fields.indexOf(field) === -1)
161
+ fields.push(field);
162
+ else
163
+ onError(new DateFormatError(`Duplicate ${field} token`, token));
164
+ }
165
+ const opt = compileOptions(token, (msg, isWarning) => onError(new DateFormatError(msg, token, isWarning)));
166
+ if (opt)
167
+ Object.assign(options, opt);
168
+ }
169
+ return options;
170
+ }
171
+
172
+ const fields = {
173
+ G: { field: 'era', desc: 'Era' },
174
+ y: { field: 'year', desc: 'Year' },
175
+ Y: { field: 'year', desc: 'Year of "Week of Year"' },
176
+ u: { field: 'year', desc: 'Extended year' },
177
+ U: { field: 'year', desc: 'Cyclic year name' },
178
+ r: { field: 'year', desc: 'Related Gregorian year' },
179
+ Q: { field: 'quarter', desc: 'Quarter' },
180
+ q: { field: 'quarter', desc: 'Stand-alone quarter' },
181
+ M: { field: 'month', desc: 'Month in year' },
182
+ L: { field: 'month', desc: 'Stand-alone month in year' },
183
+ w: { field: 'week', desc: 'Week of year' },
184
+ W: { field: 'week', desc: 'Week of month' },
185
+ d: { field: 'day', desc: 'Day in month' },
186
+ D: { field: 'day', desc: 'Day of year' },
187
+ F: { field: 'day', desc: 'Day of week in month' },
188
+ g: { field: 'day', desc: 'Modified julian day' },
189
+ E: { field: 'weekday', desc: 'Day of week' },
190
+ e: { field: 'weekday', desc: 'Local day of week' },
191
+ c: { field: 'weekday', desc: 'Stand-alone local day of week' },
192
+ a: { field: 'period', desc: 'AM/PM marker' },
193
+ b: { field: 'period', desc: 'AM/PM/noon/midnight marker' },
194
+ B: { field: 'period', desc: 'Flexible day period' },
195
+ h: { field: 'hour', desc: 'Hour in AM/PM (1~12)' },
196
+ H: { field: 'hour', desc: 'Hour in day (0~23)' },
197
+ k: { field: 'hour', desc: 'Hour in day (1~24)' },
198
+ K: { field: 'hour', desc: 'Hour in AM/PM (0~11)' },
199
+ j: { field: 'hour', desc: 'Hour in preferred cycle' },
200
+ J: { field: 'hour', desc: 'Hour in preferred cycle without marker' },
201
+ C: { field: 'hour', desc: 'Hour in preferred cycle with flexible marker' },
202
+ m: { field: 'min', desc: 'Minute in hour' },
203
+ s: { field: 'sec', desc: 'Second in minute' },
204
+ S: { field: 'sec-frac', desc: 'Fractional second' },
205
+ A: { field: 'ms', desc: 'Milliseconds in day' },
206
+ z: { field: 'tz', desc: 'Time Zone: specific non-location' },
207
+ Z: { field: 'tz', desc: 'Time Zone' },
208
+ O: { field: 'tz', desc: 'Time Zone: localized' },
209
+ v: { field: 'tz', desc: 'Time Zone: generic non-location' },
210
+ V: { field: 'tz', desc: 'Time Zone: ID' },
211
+ X: { field: 'tz', desc: 'Time Zone: ISO8601 with Z' },
212
+ x: { field: 'tz', desc: 'Time Zone: ISO8601' }
213
+ };
214
+ const isLetter = (char) => (char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z');
215
+ function readFieldToken(src, pos) {
216
+ const char = src[pos];
217
+ let width = 1;
218
+ while (src[++pos] === char)
219
+ ++width;
220
+ const field = fields[char];
221
+ if (!field) {
222
+ const msg = `The letter ${char} is not a valid field identifier`;
223
+ return { char, error: new Error(msg), width };
224
+ }
225
+ return { char, field: field.field, desc: field.desc, width };
226
+ }
227
+ function readQuotedToken(src, pos) {
228
+ let str = src[++pos];
229
+ let width = 2;
230
+ if (str === "'")
231
+ return { char: "'", str, width };
232
+ while (true) {
233
+ const next = src[++pos];
234
+ ++width;
235
+ if (next === undefined) {
236
+ const msg = `Unterminated quoted literal in pattern: ${str || src}`;
237
+ return { char: "'", error: new Error(msg), str, width };
238
+ }
239
+ else if (next === "'") {
240
+ if (src[++pos] !== "'")
241
+ return { char: "'", str, width };
242
+ else
243
+ ++width;
244
+ }
245
+ str += next;
246
+ }
247
+ }
248
+ function readToken(src, pos) {
249
+ const char = src[pos];
250
+ if (!char)
251
+ return null;
252
+ if (isLetter(char))
253
+ return readFieldToken(src, pos);
254
+ if (char === "'")
255
+ return readQuotedToken(src, pos);
256
+ let str = char;
257
+ let width = 1;
258
+ while (true) {
259
+ const next = src[++pos];
260
+ if (!next || isLetter(next) || next === "'")
261
+ return { char, str, width };
262
+ str += next;
263
+ width += 1;
264
+ }
265
+ }
266
+ /**
267
+ * Parse an {@link http://userguide.icu-project.org/formatparse/datetime | ICU
268
+ * DateFormat skeleton} string into a {@link DateToken} array.
269
+ *
270
+ * @remarks
271
+ * Errors will not be thrown, but if encountered are included as the relevant
272
+ * token's `error` value.
273
+ *
274
+ * @public
275
+ * @param src - The skeleton string
276
+ *
277
+ * @example
278
+ * ```js
279
+ * import { parseDateTokens } from '@messageformat/date-skeleton'
280
+ *
281
+ * parseDateTokens('GrMMMdd', console.error)
282
+ * // [
283
+ * // { char: 'G', field: 'era', desc: 'Era', width: 1 },
284
+ * // { char: 'r', field: 'year', desc: 'Related Gregorian year', width: 1 },
285
+ * // { char: 'M', field: 'month', desc: 'Month in year', width: 3 },
286
+ * // { char: 'd', field: 'day', desc: 'Day in month', width: 2 }
287
+ * // ]
288
+ * ```
289
+ */
290
+ function parseDateTokens(src) {
291
+ const tokens = [];
292
+ let pos = 0;
293
+ while (true) {
294
+ const token = readToken(src, pos);
295
+ if (!token)
296
+ return tokens;
297
+ tokens.push(token);
298
+ pos += token.width;
299
+ }
300
+ }
301
+
3
302
  function processTokens(tokens, mapText) {
4
303
  if (!tokens.filter((token) => token.type !== "content").length) {
5
304
  return tokens.map((token) => mapText(token.value));
@@ -13,6 +312,12 @@ function processTokens(tokens, mapText) {
13
312
  return [token.arg];
14
313
  } else if (token.type === "function") {
15
314
  const _param = token?.param?.[0];
315
+ if (token.key === "date" && _param) {
316
+ const opts = compileDateExpression(_param.value.trim(), (e) => {
317
+ throw new Error(`Unable to compile date expression: ${e.message}`);
318
+ });
319
+ return [token.arg, token.key, opts];
320
+ }
16
321
  if (_param) {
17
322
  return [token.arg, token.key, _param.value.trim()];
18
323
  } else {
@@ -35,6 +340,13 @@ function processTokens(tokens, mapText) {
35
340
  ];
36
341
  });
37
342
  }
343
+ function compileDateExpression(format, onError) {
344
+ if (/^::/.test(format)) {
345
+ const tokens = parseDateTokens(format.substring(2));
346
+ return getDateFormatOptions(tokens, void 0, onError);
347
+ }
348
+ return format;
349
+ }
38
350
  function compileMessage(message, mapText = (v) => v) {
39
351
  try {
40
352
  return processTokens(parse(message), mapText);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lingui/message-utils",
3
- "version": "5.1.1",
3
+ "version": "5.2.0",
4
4
  "license": "MIT",
5
5
  "keywords": [],
6
6
  "sideEffects": false,
@@ -52,7 +52,11 @@
52
52
  },
53
53
  "devDependencies": {
54
54
  "@lingui/jest-mocks": "^3.0.3",
55
+ "@messageformat/date-skeleton": "^1.1.0",
55
56
  "unbuild": "2.0.0"
56
57
  },
57
- "gitHead": "de0517677882f1900a052018e396b94f406963d6"
58
+ "bundledDependencies": [
59
+ "@messageformat/date-skeleton"
60
+ ],
61
+ "gitHead": "9c50b4877ca8b134d0d96c09a8055221ca70b095"
58
62
  }