@lingui/message-utils 5.1.2 → 5.3.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/dist/compileMessage.cjs +317 -1
- package/dist/compileMessage.d.cts +7 -2
- package/dist/compileMessage.d.mts +7 -2
- package/dist/compileMessage.d.ts +7 -2
- package/dist/compileMessage.mjs +317 -2
- package/package.json +6 -2
package/dist/compileMessage.cjs
CHANGED
|
@@ -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,9 +342,19 @@ 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
|
+
}
|
|
352
|
+
function compileMessageOrThrow(message, mapText = (v) => v) {
|
|
353
|
+
return processTokens(parser.parse(message), mapText);
|
|
354
|
+
}
|
|
40
355
|
function compileMessage(message, mapText = (v) => v) {
|
|
41
356
|
try {
|
|
42
|
-
return
|
|
357
|
+
return compileMessageOrThrow(message, mapText);
|
|
43
358
|
} catch (e) {
|
|
44
359
|
console.error(`${e.message}
|
|
45
360
|
|
|
@@ -49,3 +364,4 @@ Message: ${message}`);
|
|
|
49
364
|
}
|
|
50
365
|
|
|
51
366
|
exports.compileMessage = compileMessage;
|
|
367
|
+
exports.compileMessageOrThrow = compileMessageOrThrow;
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
type CompiledIcuChoices = Record<string, CompiledMessage> & {
|
|
2
2
|
offset: number | undefined;
|
|
3
3
|
};
|
|
4
|
-
type CompiledMessageToken = string | [
|
|
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;
|
|
11
|
+
declare function compileMessageOrThrow(message: string, mapText?: MapTextFn): CompiledMessage;
|
|
7
12
|
declare function compileMessage(message: string, mapText?: MapTextFn): CompiledMessage;
|
|
8
13
|
|
|
9
|
-
export { type CompiledIcuChoices, type CompiledMessage, type CompiledMessageToken, compileMessage };
|
|
14
|
+
export { type CompiledIcuChoices, type CompiledMessage, type CompiledMessageToken, compileMessage, compileMessageOrThrow };
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
type CompiledIcuChoices = Record<string, CompiledMessage> & {
|
|
2
2
|
offset: number | undefined;
|
|
3
3
|
};
|
|
4
|
-
type CompiledMessageToken = string | [
|
|
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;
|
|
11
|
+
declare function compileMessageOrThrow(message: string, mapText?: MapTextFn): CompiledMessage;
|
|
7
12
|
declare function compileMessage(message: string, mapText?: MapTextFn): CompiledMessage;
|
|
8
13
|
|
|
9
|
-
export { type CompiledIcuChoices, type CompiledMessage, type CompiledMessageToken, compileMessage };
|
|
14
|
+
export { type CompiledIcuChoices, type CompiledMessage, type CompiledMessageToken, compileMessage, compileMessageOrThrow };
|
package/dist/compileMessage.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
type CompiledIcuChoices = Record<string, CompiledMessage> & {
|
|
2
2
|
offset: number | undefined;
|
|
3
3
|
};
|
|
4
|
-
type CompiledMessageToken = string | [
|
|
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;
|
|
11
|
+
declare function compileMessageOrThrow(message: string, mapText?: MapTextFn): CompiledMessage;
|
|
7
12
|
declare function compileMessage(message: string, mapText?: MapTextFn): CompiledMessage;
|
|
8
13
|
|
|
9
|
-
export { type CompiledIcuChoices, type CompiledMessage, type CompiledMessageToken, compileMessage };
|
|
14
|
+
export { type CompiledIcuChoices, type CompiledMessage, type CompiledMessageToken, compileMessage, compileMessageOrThrow };
|
package/dist/compileMessage.mjs
CHANGED
|
@@ -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,9 +340,19 @@ 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
|
+
}
|
|
350
|
+
function compileMessageOrThrow(message, mapText = (v) => v) {
|
|
351
|
+
return processTokens(parse(message), mapText);
|
|
352
|
+
}
|
|
38
353
|
function compileMessage(message, mapText = (v) => v) {
|
|
39
354
|
try {
|
|
40
|
-
return
|
|
355
|
+
return compileMessageOrThrow(message, mapText);
|
|
41
356
|
} catch (e) {
|
|
42
357
|
console.error(`${e.message}
|
|
43
358
|
|
|
@@ -46,4 +361,4 @@ Message: ${message}`);
|
|
|
46
361
|
}
|
|
47
362
|
}
|
|
48
363
|
|
|
49
|
-
export { compileMessage };
|
|
364
|
+
export { compileMessage, compileMessageOrThrow };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lingui/message-utils",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.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
|
-
"
|
|
58
|
+
"bundledDependencies": [
|
|
59
|
+
"@messageformat/date-skeleton"
|
|
60
|
+
],
|
|
61
|
+
"gitHead": "ed491cce7e5209378922327e0e7b802fb7b5873d"
|
|
58
62
|
}
|