@sveltekit-i18n/parser-curly 3.0.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +425 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +1 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 sveltekit-i18n
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
[](https://badge.fury.io/js/@sveltekit-i18n%2Fparser-curly) [](https://github.com/sveltekit-i18n/parsers/actions/workflows/tests-parser-curly.yml)
|
|
2
|
+
[](https://app.netlify.com/sites/parser-default/deploys)
|
|
3
|
+
|
|
4
|
+
# @sveltekit-i18n/parser-curly
|
|
5
|
+
|
|
6
|
+
The [Curly Message Format](https://github.com/curly-message/spec) for [@sveltekit-i18n/base](https://github.com/sveltekit-i18n/base): placeholders, defaults, modifiers and comparisons written in double curly braces. Every message is resolved by [`@curly-message/parser`](https://github.com/curly-message/parsers), the format's reference implementation and this package's only dependency; the package itself unpacks the base library's calling convention and supplies a default diagnostics channel. This README is a practical guide to the syntax — the full grammar and the resolution rules are in the specification repository.
|
|
7
|
+
|
|
8
|
+
**[Live Demo](https://parser-default.netlify.app)** – See it in action
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @sveltekit-i18n/parser-curly
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
This parser is included by default in [sveltekit-i18n](https://github.com/sveltekit-i18n/lib).
|
|
17
|
+
|
|
18
|
+
**Requirements:** Node.js 22 or newer. Version 3 is ESM-only and expects [`@sveltekit-i18n/base`](https://github.com/sveltekit-i18n/base) v3 as a peer dependency.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### With @sveltekit-i18n/base
|
|
23
|
+
|
|
24
|
+
```javascript
|
|
25
|
+
import { I18n } from '@sveltekit-i18n/base';
|
|
26
|
+
import parser from '@sveltekit-i18n/parser-curly';
|
|
27
|
+
|
|
28
|
+
const config = {
|
|
29
|
+
parser: parser({
|
|
30
|
+
// Where diagnostics go; `null` states that they go nowhere.
|
|
31
|
+
onReport: null,
|
|
32
|
+
}),
|
|
33
|
+
loaders: [
|
|
34
|
+
{
|
|
35
|
+
locale: 'en',
|
|
36
|
+
key: 'common',
|
|
37
|
+
loader: async () => (await import('./en/common.json')).default,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const i18n = new I18n(config);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### With sveltekit-i18n
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
import { I18n } from 'sveltekit-i18n';
|
|
49
|
+
|
|
50
|
+
const config = {
|
|
51
|
+
// parser-curly is already included
|
|
52
|
+
loaders: [/* ... */],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const i18n = new I18n(config);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Either way, `i18n.t(key, payload?, props?)` takes the values the placeholders name and the per-call formatting options; the examples below use it.
|
|
59
|
+
|
|
60
|
+
## Syntax
|
|
61
|
+
|
|
62
|
+
A placeholder names a payload key and may carry a modifier, options and a default: `{{key:modifier; optionKey:value; default:fallback;}}`. Whitespace around the key, the modifier name, the option keys and the option values is not significant, so `{{value}}`, `{{ value }}` and `{{ value; }}` are the same placeholder.
|
|
63
|
+
|
|
64
|
+
### Placeholders
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"greeting": "Hello, {{name}}!",
|
|
69
|
+
"message": "You have {{count}} new messages."
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
i18n.t('greeting', { name: 'Alice' })
|
|
75
|
+
// → "Hello, Alice!"
|
|
76
|
+
|
|
77
|
+
i18n.t('message', { count: 5 })
|
|
78
|
+
// → "You have 5 new messages."
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Every value reaches the output as text: a plain object or an array becomes JSON, anything else becomes what `String()` makes of it.
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
i18n.t('greeting', { name: { first: 'Ann' } })
|
|
85
|
+
// → "Hello, {"first":"Ann"}!"
|
|
86
|
+
|
|
87
|
+
i18n.t('greeting', { name: ['Ann', 'Bob'] })
|
|
88
|
+
// → "Hello, ["Ann","Bob"]!"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Default Values
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"welcome": "Welcome, {{name; default:Guest;}}!"
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
i18n.t('welcome', { name: 'Bob' })
|
|
101
|
+
// → "Welcome, Bob!"
|
|
102
|
+
|
|
103
|
+
i18n.t('welcome', {})
|
|
104
|
+
// → "Welcome, Guest!"
|
|
105
|
+
|
|
106
|
+
i18n.t('welcome', { default: 'Anonymous' })
|
|
107
|
+
// → "Welcome, Anonymous!"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`default` is a reserved payload key: the fallback for every placeholder in the message whose value is absent, and `{{default}}` reads it. A placeholder with no value takes the first of these that yields text — the entry's own `default` (see [Payload](#payload)), the payload's `default`, the inline `default:`, the empty string — so the payload's `default` outranks the inline one. Only an absent value falls back: `0`, `false` and the empty string are values. A key that names no message resolves to the payload's `default` as well and, where the payload carries none, to the key itself, echoed verbatim and never read as a message.
|
|
111
|
+
|
|
112
|
+
### Modifiers
|
|
113
|
+
|
|
114
|
+
A modifier follows the key after a colon. The formatting modifiers delegate to `Intl` and read their options from the props argument, keyed by the modifier's name — `{ date: { dateStyle: 'full' } }`:
|
|
115
|
+
|
|
116
|
+
| Modifier | Reads the value as | Options |
|
|
117
|
+
| --- | --- | --- |
|
|
118
|
+
| `number` | a number | `Intl.NumberFormat` options; at most two fraction digits unless a layer names `maximumFractionDigits`, or a `minimumFractionDigits` above two widens that default |
|
|
119
|
+
| `date` | milliseconds since the epoch, or text `Date` parses | `Intl.DateTimeFormat` options |
|
|
120
|
+
| `ago` | a signed millisecond delta from now, negative for the past | `Intl.RelativeTimeFormat` options, plus `format`: a unit from `second` to `year`, or `auto` |
|
|
121
|
+
| `currency` | a number, multiplied by `ratio` (default `1`) | `Intl.NumberFormat` options in the currency style; `currency` names the code |
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"price": "Total: {{amount:number;}}",
|
|
126
|
+
"published": "Published: {{date:date;}}",
|
|
127
|
+
"time": "Time: {{timestamp:date;}}",
|
|
128
|
+
"updated": "Updated {{time:ago;}}",
|
|
129
|
+
"posted": "Posted {{timestamp:ago;}}",
|
|
130
|
+
"cost": "Cost: {{amount:currency;}}"
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
i18n.t('price', { amount: 1234.56 })
|
|
136
|
+
// → "Total: 1,234.56" (locale-dependent)
|
|
137
|
+
|
|
138
|
+
i18n.t('price', { amount: 1234.56 }, { number: { maximumFractionDigits: 1 } })
|
|
139
|
+
// → "Total: 1,234.6"
|
|
140
|
+
|
|
141
|
+
i18n.t('published', { date: new Date(2024, 0, 1) }, { date: { dateStyle: 'full' } })
|
|
142
|
+
// → "Published: Monday, January 1, 2024"
|
|
143
|
+
|
|
144
|
+
i18n.t('time', { timestamp: new Date(2024, 0, 1, 10, 30) }, { date: { timeStyle: 'short' } })
|
|
145
|
+
// → "Time: 10:30 AM"
|
|
146
|
+
|
|
147
|
+
i18n.t('updated', { time: -3600000 })
|
|
148
|
+
// → "Updated 1 hour ago"
|
|
149
|
+
|
|
150
|
+
i18n.t('posted', { timestamp: -86400000 })
|
|
151
|
+
// → "Posted yesterday"
|
|
152
|
+
|
|
153
|
+
i18n.t('posted', { timestamp: -172800000 }, { ago: { format: 'hour' } })
|
|
154
|
+
// → "Posted 48 hours ago"
|
|
155
|
+
|
|
156
|
+
i18n.t('cost', { amount: 99.99 }, { currency: { currency: 'USD' } })
|
|
157
|
+
// → "Cost: $99.99"
|
|
158
|
+
|
|
159
|
+
i18n.t('cost', { amount: 1999 }, { currency: { currency: 'USD', ratio: 0.01 } })
|
|
160
|
+
// → "Cost: $19.99"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
A value the modifier cannot read — text that is not a number, an empty string, a `Date` object under `number` — takes the fallback and is reported as `failed-modifier`; so does a `currency` placeholder with no currency code or an `ago` whose `format` names no unit. Nothing raises. A `Date` object does work under `date`, to the second, because it reaches the modifier as its `toString` text; pass a timestamp or an ISO string where a placeholder wants one. With no locale — none passed, or the empty string — a formatting modifier resolves to the empty string rather than to the fallback and reports `missing-locale`.
|
|
164
|
+
|
|
165
|
+
### Comparisons
|
|
166
|
+
|
|
167
|
+
`eq`, `ne`, `lt`, `lte`, `gt` and `gte` select among the options: the first option whose key satisfies the comparison against the value is the result, and none selected takes the fallback. `eq` and `ne` compare as text, case-insensitively; `lt` and `gt` compare numerically, considering the options in ascending or descending key order; `lte` and `gte` try equality first. A placeholder with options and no modifier compares with `eq`.
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
{
|
|
171
|
+
"status": "{{state; active:Online; inactive:Offline; default:Unknown;}}",
|
|
172
|
+
"items": "You have {{count}} {{count; 1:item; default:items;}}.",
|
|
173
|
+
"stock": "{{count:gt; 0:In stock ({{count}}); default:Out of stock;}}",
|
|
174
|
+
"age": "{{age:gte; 18:Adult; default:Minor;}}",
|
|
175
|
+
"temp": "{{degrees:lt; 0:Freezing; default:Above freezing;}}",
|
|
176
|
+
"health": "{{state:ne; ok:Problem; default:Fine;}}"
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
```javascript
|
|
181
|
+
i18n.t('status', { state: 'active' }) // → "Online"
|
|
182
|
+
i18n.t('status', { state: 'pending' }) // → "Unknown"
|
|
183
|
+
i18n.t('items', { count: 1 }) // → "You have 1 item."
|
|
184
|
+
i18n.t('items', { count: 5 }) // → "You have 5 items."
|
|
185
|
+
i18n.t('stock', { count: 5 }) // → "In stock (5)"
|
|
186
|
+
i18n.t('stock', { count: 0 }) // → "Out of stock"
|
|
187
|
+
i18n.t('age', { age: 25 }) // → "Adult"
|
|
188
|
+
i18n.t('temp', { degrees: -5 }) // → "Freezing"
|
|
189
|
+
i18n.t('health', { state: 'error' }) // → "Problem"
|
|
190
|
+
i18n.t('health', {}) // → "Fine"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
An absent value never reaches a comparison — it takes the fallback under every modifier, `ne` included. An option written as `key` alone stands for its own key; `key:` declares the empty string. An option value runs to the next unescaped semicolon, so `link:http://example.com` keeps its colons. A comparison with no options (`{{v:eq; default:D}}`) takes the fallback and reports `missing-options`; a placeholder naming a modifier nobody registered takes the fallback and reports `unknown-modifier` — it is never run as `eq`.
|
|
194
|
+
|
|
195
|
+
### Nested Placeholders
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
{
|
|
199
|
+
"notification": "You have {{count:gt; 0:{{count}} new {{count; 1:message; default:messages;}}!; default:no messages.;}}"
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
```javascript
|
|
204
|
+
i18n.t('notification', { count: 1 })
|
|
205
|
+
// → "You have 1 new message!"
|
|
206
|
+
|
|
207
|
+
i18n.t('notification', { count: 5 })
|
|
208
|
+
// → "You have 5 new messages!"
|
|
209
|
+
|
|
210
|
+
i18n.t('notification', { count: 0 })
|
|
211
|
+
// → "You have no messages."
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Nesting is resolved by interpolating the output again, so a payload value may carry a placeholder of its own; the [limits](#limits) bound that.
|
|
215
|
+
|
|
216
|
+
### Escaping
|
|
217
|
+
|
|
218
|
+
A backslash cancels the structural meaning of the character after it: `:`, `;`, `{`, `}`, whitespace and the backslash itself. Before any other character the backslash is text, so a regular expression or a Windows path survives as typed. In a JSON catalogue each backslash is written twice (`"\\{\\{"`).
|
|
219
|
+
|
|
220
|
+
| Message | Result |
|
|
221
|
+
| --- | --- |
|
|
222
|
+
| `Braces are written \{\{ like this \}\}` | `Braces are written {{ like this }}` |
|
|
223
|
+
| `Ratio\: 3\:1\;` | `Ratio: 3:1;` |
|
|
224
|
+
| `Hello, {{first\ name}}!` | reads the payload key `first name` |
|
|
225
|
+
| `{{count; 1:one\ ; default:none}}` | keeps the trailing space the trimming would take |
|
|
226
|
+
| `C:\\temp` | `C:\temp` |
|
|
227
|
+
| `\d+` | `\d+` |
|
|
228
|
+
| `\{{v}}` | `{{v}}`, text whatever the payload carries |
|
|
229
|
+
|
|
230
|
+
Escape sequences are removed once, from the finished text, and a payload value is read by the same rule: a value that must keep a backslash before a reserved character doubles it. A placeholder is written on one line — `{{` and `}}` with a line terminator between them are text — and `{{}}` is a placeholder naming no key, which resolves to the fallback.
|
|
231
|
+
|
|
232
|
+
## Payload
|
|
233
|
+
|
|
234
|
+
A payload entry may be a wrapper instead of the value: a plain object owning at least one of `value`, `default` and `props` and nothing else. Its `default` is tried before the payload's, and its `props` are the topmost formatting layer.
|
|
235
|
+
|
|
236
|
+
```javascript
|
|
237
|
+
i18n.t('price', { amount: { value: 1234.56, props: { number: { maximumFractionDigits: 1 } } } })
|
|
238
|
+
// → "Total: 1,234.6"
|
|
239
|
+
|
|
240
|
+
i18n.t('greeting', { name: { default: 'stranger' } })
|
|
241
|
+
// → "Hello, stranger!"
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
An entry owning any other key is data, wrapper-shaped or not: `{ value: 1, unit: 'kg' }` becomes JSON. Every entry is read as an own enumerable property — nothing on a prototype resolves.
|
|
245
|
+
|
|
246
|
+
## Options
|
|
247
|
+
|
|
248
|
+
```javascript
|
|
249
|
+
import parser from '@sveltekit-i18n/parser-curly';
|
|
250
|
+
|
|
251
|
+
const config = {
|
|
252
|
+
parser: parser({
|
|
253
|
+
// The bottom formatting layer, keyed by modifier name. The props a call
|
|
254
|
+
// passes and a wrapper's own props layer over it, property by property.
|
|
255
|
+
modifierDefaults: {
|
|
256
|
+
number: { minimumFractionDigits: 2, maximumFractionDigits: 2 },
|
|
257
|
+
date: { dateStyle: 'medium' },
|
|
258
|
+
ago: { numeric: 'auto' },
|
|
259
|
+
currency: { currency: 'USD' },
|
|
260
|
+
},
|
|
261
|
+
// Modifiers by name, over the built-in ones.
|
|
262
|
+
customModifiers: {},
|
|
263
|
+
// Where diagnostics go. Required: a function, or `null` to state that
|
|
264
|
+
// reports go nowhere.
|
|
265
|
+
onReport: (report) => { /* ... */ },
|
|
266
|
+
}),
|
|
267
|
+
};
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
The formatting layers compose per property, so a layer overrides only what it names:
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
modifierDefaults { number: { maximumFractionDigits: 4, useGrouping: false } }
|
|
274
|
+
call props { number: { useGrouping: true } }
|
|
275
|
+
wrapper props { number: { maximumFractionDigits: 1 } }
|
|
276
|
+
effective { maximumFractionDigits: 1, useGrouping: true } → "1,234.6"
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Custom Modifiers
|
|
280
|
+
|
|
281
|
+
A modifier is a function of `{ value, options, defaultValue, props, locale }`: the value as text, the options as `[{ key, value }]` in source order, the fallback chain behind `defaultValue` (resolved when read), the props composed under the modifier's own name (an empty object where nothing is configured) and the locale. What it returns becomes text. A modifier that returns nothing leaves the placeholder to its fallback, and so does one that throws, which is reported as `failed-modifier`. An absent value takes the fallback before any modifier runs. A modifier registered under a built-in name replaces it.
|
|
282
|
+
|
|
283
|
+
```javascript
|
|
284
|
+
const config = {
|
|
285
|
+
parser: parser({
|
|
286
|
+
customModifiers: {
|
|
287
|
+
// Absolute value equality
|
|
288
|
+
eqAbs: ({ value, options, defaultValue }) =>
|
|
289
|
+
options.find(({ key }) => Math.abs(+key) === Math.abs(+value))?.value ?? defaultValue,
|
|
290
|
+
|
|
291
|
+
// Uppercase transform
|
|
292
|
+
upper: ({ value }) => value.toUpperCase(),
|
|
293
|
+
|
|
294
|
+
// Truncate text; `props` is what the call passes under `truncate`
|
|
295
|
+
truncate: ({ value, props }) => {
|
|
296
|
+
const maxLength = props.maxLength ?? 50;
|
|
297
|
+
|
|
298
|
+
return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
}),
|
|
302
|
+
};
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
```json
|
|
306
|
+
{
|
|
307
|
+
"score": "{{value:eqAbs; 10:Perfect score!; default:Not quite.;}}",
|
|
308
|
+
"title": "{{text:upper;}}",
|
|
309
|
+
"description": "{{text:truncate;}}"
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
```javascript
|
|
314
|
+
i18n.t('score', { value: -10 })
|
|
315
|
+
// → "Perfect score!"
|
|
316
|
+
|
|
317
|
+
i18n.t('title', { text: 'hello world' })
|
|
318
|
+
// → "HELLO WORLD"
|
|
319
|
+
|
|
320
|
+
i18n.t('description', { text: 'This text is far too long to display' }, { truncate: { maxLength: 20 } })
|
|
321
|
+
// → "This text is far too..."
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Reports
|
|
325
|
+
|
|
326
|
+
The format prescribes no diagnostics channel, and neither does this package: `@curly-message/parser` writes nowhere by itself, and `parser()` adds no writer of its own. `onReport` is therefore a required option — a function, or `null` to state that reports go nowhere — so that silence is a decision, never an omission. A host with a logger routes reports to it:
|
|
327
|
+
|
|
328
|
+
```javascript
|
|
329
|
+
parser({ onReport: (report) => logger.warn(report.message, report) })
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
A `Report` carries:
|
|
333
|
+
|
|
334
|
+
| Field | Meaning |
|
|
335
|
+
| --- | --- |
|
|
336
|
+
| `code` | `unknown-modifier`, `failed-modifier`, `missing-options`, `unserializable-value`, `missing-locale`, `pass-limit` or `output-limit` |
|
|
337
|
+
| `origin` | who fixes it: `message` (the message as written), `payload` (what the call passed) or `limit` (a bound this parser set) |
|
|
338
|
+
| `message` | a self-contained English sentence carrying nothing from the payload |
|
|
339
|
+
| `key` | the message's key, where the call passed one |
|
|
340
|
+
| `limit` | the limit reached, for the two limit reports |
|
|
341
|
+
| `text` | the excerpt: the placeholder, or the output that would not settle — cut to 120 code units, with quotes, backslashes and line terminators escaped, so it can be written anywhere |
|
|
342
|
+
|
|
343
|
+
A report never raises: the placeholder takes its fallback (the empty string for `missing-locale`) and the rest of the message resolves.
|
|
344
|
+
|
|
345
|
+
## Limits
|
|
346
|
+
|
|
347
|
+
Resolution is bounded three ways: 10 interpolation passes (a value referencing its own placeholder stops with its placeholders unresolved, reported as `pass-limit`), 100 000 UTF-16 code units of output (a pass that would exceed it is discarded and the last output under the bound stands, reported as `output-limit`) and 100 000 nodes per value conversion (a value past it is read as missing, reported as `unserializable-value`). The specification's conformance set, `@curly-message/conformance`, runs against this package's public API in its tests, at every level the format defines (Core, Intl, Extensions).
|
|
348
|
+
|
|
349
|
+
## TypeScript
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
import { I18n } from '@sveltekit-i18n/base';
|
|
353
|
+
import parser from '@sveltekit-i18n/parser-curly';
|
|
354
|
+
import type { Config, Modifier, Report } from '@sveltekit-i18n/parser-curly';
|
|
355
|
+
|
|
356
|
+
type Payload = { applicationName: string };
|
|
357
|
+
type Props = { truncate?: { maxLength?: number } };
|
|
358
|
+
|
|
359
|
+
const truncate: Modifier.T<{ maxLength?: number }> = ({ value, props }) =>
|
|
360
|
+
value.length > (props.maxLength ?? 50) ? `${value.slice(0, props.maxLength ?? 50)}...` : value;
|
|
361
|
+
|
|
362
|
+
const config: Config<Payload, Props> = {
|
|
363
|
+
parser: parser({
|
|
364
|
+
customModifiers: { truncate },
|
|
365
|
+
onReport: (report: Report) => { console.warn(report.message); },
|
|
366
|
+
}),
|
|
367
|
+
loaders: [/* ... */],
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const i18n = new I18n(config);
|
|
371
|
+
|
|
372
|
+
i18n.t('common.welcome', { applicationName: 'My app' }, { truncate: { maxLength: 20 } })
|
|
373
|
+
// → ok
|
|
374
|
+
|
|
375
|
+
i18n.t('common.welcome', { aplicationName: 'My app' })
|
|
376
|
+
// → type error: typo caught
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
`Config<Payload, Props>` types the payload and the props `i18n.t` accepts; left bare, `Config` accepts any payload key and the built-in modifiers' props. A custom modifier types its own props through `Modifier.T<OwnProps>`. The factory's type arguments check the parser options the same way: with `Props` spelled as the second, `parser<Payload, Props>({ ... })`, a `modifierDefaults` entry for a custom modifier is checked; a modifier written inline reads typed `props` once the modifier names are spelled as the third argument too, `parser<Payload, Props, 'truncate'>({ ... })`. `Parser` holds the option and parameter types (`Parser.Options`, `Parser.OnReport`, `Parser.Params`, `Parser.Payload`), `Modifier` the modifier and wrapper types (`Modifier.T`, `Modifier.Wrapper`, `Modifier.Props`), and `Report` is the report.
|
|
380
|
+
|
|
381
|
+
## Examples
|
|
382
|
+
|
|
383
|
+
See the [parser-curly example](https://github.com/sveltekit-i18n/lib/tree/master/examples/parser-default) for a complete working application.
|
|
384
|
+
|
|
385
|
+
**[Live Demo](https://parser-default.netlify.app)** – Interactive examples
|
|
386
|
+
|
|
387
|
+
## Comparison with Other Parsers
|
|
388
|
+
|
|
389
|
+
### vs. ICU Message Format
|
|
390
|
+
|
|
391
|
+
**parser-curly:**
|
|
392
|
+
```json
|
|
393
|
+
{
|
|
394
|
+
"items": "You have {{count}} {{count; 1:item; default:items;}}."
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
**ICU:**
|
|
399
|
+
```json
|
|
400
|
+
{
|
|
401
|
+
"items": "You have {count} {count, plural, one {item} other {items}}."
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
- `parser-curly` has simpler syntax
|
|
406
|
+
- ICU has more advanced plural rules for complex languages
|
|
407
|
+
- `parser-curly` has one dependency, the format's reference implementation, and no others
|
|
408
|
+
- ICU is an industry standard
|
|
409
|
+
|
|
410
|
+
Choose `parser-curly` for simplicity, ICU for standards compliance.
|
|
411
|
+
|
|
412
|
+
## More Resources
|
|
413
|
+
|
|
414
|
+
- [All Parsers](https://github.com/sveltekit-i18n/parsers) – Parser overview
|
|
415
|
+
- [Curly Message Format](https://github.com/curly-message/spec) – The specification
|
|
416
|
+
- [Examples](https://github.com/sveltekit-i18n/lib/tree/master/examples) – Working examples
|
|
417
|
+
- [Changelog](./CHANGELOG.md) – Version history
|
|
418
|
+
|
|
419
|
+
## Issues
|
|
420
|
+
|
|
421
|
+
If you're facing issues with this parser, create a ticket [here](https://github.com/sveltekit-i18n/lib/issues).
|
|
422
|
+
|
|
423
|
+
## License
|
|
424
|
+
|
|
425
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Parser as Parser$2, Config as Config$1 } from '@sveltekit-i18n/base';
|
|
2
|
+
import { Modifier, Parser as Parser$1 } from '@curly-message/parser';
|
|
3
|
+
export { Modifier, Report } from '@curly-message/parser';
|
|
4
|
+
|
|
5
|
+
declare namespace Parser {
|
|
6
|
+
/**
|
|
7
|
+
* The options `parser()` takes, handed on to `@curly-message/parser`:
|
|
8
|
+
* `customModifiers`, `modifierDefaults` and `onReport`. `onReport` is
|
|
9
|
+
* required, `null` included: this package writes to no channel of its own,
|
|
10
|
+
* so where a report goes is stated by whoever builds the parser.
|
|
11
|
+
*/
|
|
12
|
+
type Options<Key extends string = Modifier.Key, Props = Modifier.DefaultProps> = Omit<Parser$1.Options<Key, Props>, 'onReport'> & {
|
|
13
|
+
onReport: OnReport | null | undefined;
|
|
14
|
+
};
|
|
15
|
+
type OnReport = Parser$1.OnReport;
|
|
16
|
+
type PayloadDefault = Parser$1.PayloadDefault;
|
|
17
|
+
type Payload<T = any, Props = Modifier.DefaultProps> = Parser$1.Payload<T, Props>;
|
|
18
|
+
/**
|
|
19
|
+
* The rest parameters of `t(key, payload?, props?)`: the values the message's
|
|
20
|
+
* placeholders name, then the per-call formatting options keyed by modifier
|
|
21
|
+
* name.
|
|
22
|
+
*/
|
|
23
|
+
type Params<P = PayloadDefault, M = Modifier.DefaultProps> = [payload?: Payload<P, M>, props?: Modifier.Props<M>];
|
|
24
|
+
type T<P extends Parser$2.Params = Params> = Parser$2.T<P, string>;
|
|
25
|
+
type Factory = <Payload = {}, Props = {}, Key extends string = Modifier.Key>(options: Options<Key, Props>) => T<Params<Payload & PayloadDefault, Props & Modifier.DefaultProps>>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The base config carrying this parser, typed by the payload the messages
|
|
29
|
+
* expect and by the formatting props a call may pass.
|
|
30
|
+
*/
|
|
31
|
+
type Config<P = Parser.PayloadDefault, M = Modifier.DefaultProps> = Config$1.T<Parser.Params<P, M>, string>;
|
|
32
|
+
|
|
33
|
+
declare const parser: Parser.Factory;
|
|
34
|
+
|
|
35
|
+
export { type Config, Parser, parser as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createParser as i}from"@curly-message/parser";var f=r=>{let{resolve:e}=i(r);return{parse:(o,[t,p],s,a)=>e(o,{payload:t,props:p,locale:s,key:a})}},c=f;export{c as default};
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sveltekit-i18n/parser-curly",
|
|
3
|
+
"version": "3.0.0-next.0",
|
|
4
|
+
"description": "Curly Message Format parser for the sveltekit-i18n library.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"dev": "tsup --watch",
|
|
20
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
21
|
+
"pretest": "npm run build && npm run typecheck",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"build": "tsup",
|
|
24
|
+
"prepublishOnly": "npm run build",
|
|
25
|
+
"lint": "eslint --fix .",
|
|
26
|
+
"prepare": "cd .. && simple-git-hooks parser-curly/.simple-git-hooks.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+ssh://git@github.com/sveltekit-i18n/parsers.git",
|
|
34
|
+
"directory": "parser-curly"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"parser",
|
|
38
|
+
"curly",
|
|
39
|
+
"curly-message",
|
|
40
|
+
"i18n",
|
|
41
|
+
"sveltekit-i18n"
|
|
42
|
+
],
|
|
43
|
+
"author": "Jarda Svoboda",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/sveltekit-i18n/lib/issues"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/sveltekit-i18n/parsers/tree/master/parser-curly#readme",
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"@sveltekit-i18n/base": "^3.0.0-next.0"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@curly-message/parser": "^1.0.0-next.2"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@curly-message/conformance": "^1.0.0-next.2",
|
|
57
|
+
"@eslint/js": "^10.0.1",
|
|
58
|
+
"@stylistic/eslint-plugin": "^5.10.0",
|
|
59
|
+
"@sveltekit-i18n/base": "^3.0.0-next.0",
|
|
60
|
+
"eslint": "^10.8.1",
|
|
61
|
+
"eslint-plugin-import-x": "^4.17.1",
|
|
62
|
+
"globals": "^17.11.0",
|
|
63
|
+
"simple-git-hooks": "^2.13.1",
|
|
64
|
+
"tsup": "^8.0.1",
|
|
65
|
+
"typescript": "^5.1.6",
|
|
66
|
+
"typescript-eslint": "^8.67.0",
|
|
67
|
+
"vitest": "^4.1.10"
|
|
68
|
+
}
|
|
69
|
+
}
|