@sveltekit-i18n/parser-curly 3.0.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,17 +2,19 @@
2
2
 
3
3
  # @sveltekit-i18n/parser-curly
4
4
 
5
- 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.
5
+ The [Curly Message Format](https://curlymessage.dev) 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 requires the diagnostics channel to be stated. This README is a practical guide to the syntax — the full grammar and the resolution rules are in the specification.
6
6
 
7
7
  ## Installation
8
8
 
9
9
  ```bash
10
10
  npm install @sveltekit-i18n/parser-curly
11
+ # bun add @sveltekit-i18n/parser-curly
12
+ # deno add npm:@sveltekit-i18n/parser-curly
11
13
  ```
12
14
 
13
15
  This parser is included by default in [sveltekit-i18n](https://github.com/sveltekit-i18n/lib).
14
16
 
15
- **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.
17
+ **Requirements:** Node.js 22, Bun 1.2 or Deno 2, or newer. Version 3 is ESM-only and expects [`@sveltekit-i18n/base`](https://github.com/sveltekit-i18n/base) v3 as a peer dependency.
16
18
 
17
19
  ## Usage
18
20
 
@@ -208,7 +210,7 @@ i18n.t('notification', { count: 0 })
208
210
  // → "You have no messages."
209
211
  ```
210
212
 
211
- Nesting is resolved by interpolating the output again, so a payload value may carry a placeholder of its own; the [limits](#limits) bound that.
213
+ An option value is the one position where a placeholder holds another, and only the selected option's is resolved: over `{ count: 0 }` the comparison selects nothing, so no payload entry is read for the inner placeholders and no report they would have made is made. What a placeholder resolves to is nested in nothing — a value is data, so no payload reaches a branch the message did not select for it. How deep a message writes placeholders inside one another is what the [limits](#limits) bound.
212
214
 
213
215
  ### Escaping
214
216
 
@@ -224,7 +226,7 @@ A backslash cancels the structural meaning of the character after it: `:`, `;`,
224
226
  | `\d+` | `\d+` |
225
227
  | `\{{v}}` | `{{v}}`, text whatever the payload carries |
226
228
 
227
- 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.
229
+ Escape sequences are removed from the message and from nothing else: a payload value is data, so its backslashes and its braces reach the output as they stand and a value holding `\d+` renders `\d+`. 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.
228
230
 
229
231
  ## Payload
230
232
 
@@ -240,6 +242,8 @@ i18n.t('greeting', { name: { default: 'stranger' } })
240
242
 
241
243
  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.
242
244
 
245
+ A value is data throughout: no escape sequence is removed from it and no placeholder is found in it, so it reaches the output as it was passed. Where the payload carries data the caller did not write, pass `recognizeWrappers: false` — an entry of that shape would otherwise reconfigure every modifier its placeholder reaches without spelling any syntax at all.
246
+
243
247
  ## Options
244
248
 
245
249
  ```javascript
@@ -260,6 +264,12 @@ const config = {
260
264
  // Where diagnostics go. Required: a function, or `null` to state that
261
265
  // reports go nowhere.
262
266
  onReport: (report) => { /* ... */ },
267
+ // Whether a wrapper-shaped payload entry configures its value. On unless
268
+ // stated; `false` where the payload carries data the caller did not write.
269
+ recognizeWrappers: true,
270
+ // A migration aid: where a value holding what version 1 of the format read
271
+ // as syntax is announced. Nothing is looked for while this is unset.
272
+ onSuspectValue: null,
263
273
  }),
264
274
  };
265
275
  ```
@@ -330,18 +340,20 @@ A `Report` carries:
330
340
 
331
341
  | Field | Meaning |
332
342
  | --- | --- |
333
- | `code` | `unknown-modifier`, `failed-modifier`, `missing-options`, `unserializable-value`, `missing-locale`, `pass-limit` or `output-limit` |
343
+ | `code` | `unknown-modifier`, `failed-modifier`, `missing-options`, `unserializable-value`, `missing-locale`, `output-limit`, `read-limit` or `nesting-limit` |
334
344
  | `origin` | who fixes it: `message` (the message as written), `payload` (what the call passed) or `limit` (a bound this parser set) |
335
345
  | `message` | a self-contained English sentence carrying nothing from the payload |
336
346
  | `id` | the message's id, where the call passed one; base passes the translation key |
337
- | `limit` | the limit reached, for the two limit reports |
338
- | `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 |
347
+ | `limit` | the limit reached, for the three limit reports |
348
+ | `text` | the excerpt: the placeholder that named the trouble, or the message as it was passed where the read that refused is of the call's own structure — message text throughout and never a payload value, cut to 120 code units, with quotes, backslashes and line terminators escaped, so it can be written anywhere |
339
349
 
340
350
  A report never raises: the placeholder takes its fallback (the empty string for `missing-locale`) and the rest of the message resolves.
341
351
 
342
352
  ## Limits
343
353
 
344
- 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).
354
+ Three budgets bound a resolution. **Output** is what the output carries: 100 000 UTF-16 code units, and a placeholder whose result would carry it past that resolves to the empty string and reports `output-limit`, leaving the next placeholder to resolve. **Read** is what the payload is read for: 100 000 code units as well, spent by every character a placeholder takes from the payload whether or not any of it reaches the output, reported as `read-limit`. **Nesting** is how deep a message writes placeholders inside one another: the outermost is level 1, and one deeper than 8 levels is not resolved at all, taking its fallback chain and reporting `nesting-limit`. A fourth bound holds the conversion that feeds them — 100 000 nodes per value, past which the value is read as missing and reported as `unserializable-value`. A message's own text is the caller's and always reaches the output; what these bound is what the payload and the nesting add to it.
355
+
356
+ 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), and the tree cases against `cst`.
345
357
 
346
358
  ## TypeScript
347
359
 
@@ -373,7 +385,7 @@ i18n.t('common.welcome', { aplicationName: 'My app' })
373
385
  // → type error: typo caught
374
386
  ```
375
387
 
376
- `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.
388
+ `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`), `Cst` the [tree](#describing-a-message) node types, and `Report` is the report.
377
389
 
378
390
  ## Extracting Parameters
379
391
 
@@ -405,9 +417,28 @@ A parameter several placeholders name accepts what all of them say together, and
405
417
 
406
418
  `optional` reports what the message says rather than what resolution tolerates. Every placeholder renders without its value, an absent one taking the fallback chain, so a placeholder declaring an inline `default` is the message saying the value may be missing, and one declaring none is the message saying it is expected.
407
419
 
408
- Build the extractor from the same options `parser()` is built from: a custom modifier registered under a name the format defines changes what a message naming it says about its value. `onReport` is not required here, and neither it nor `modifierDefaults` reaches anything — extraction formats nothing and reports nothing.
420
+ Build the extractor from the same options `parser()` is built from: a custom modifier registered under a name the format defines changes what a message naming it says about its value. `onReport` is not required here, and neither it nor `modifierDefaults`, `recognizeWrappers` or `onSuspectValue` reaches anything — extraction formats nothing, reports nothing and reads no payload.
421
+
422
+ Only the text of a message is scanned. A translation leaf that is not text names no parameters rather than throwing, and a placeholder a payload value carries is not one the message itself names — a value is data, so nothing reads it as source. A placeholder the message writes inside another is named beside the one holding it, in the order the message writes them.
409
423
 
410
- Only the text of a message is scanned. A translation leaf that is not text names no parameters rather than throwing, and a placeholder a payload value carries into a later interpolation pass is not one the message itself names.
424
+ ## Describing a Message
425
+
426
+ `cst` describes a message as the parts it is written from: where its placeholders are, what each is made of, and where every escape sequence falls. It is the format's own describer, a named export for the same reason `extractParamsFactory` is one — resolution never calls it, so a bundle that never reaches it drops it.
427
+
428
+ ```typescript
429
+ import { cst } from '@sveltekit-i18n/parser-curly';
430
+
431
+ cst('Hello, {{name; default:Guest;}}!');
432
+ // → { type: 'message', start: 0, end: 32, nodes: [
433
+ // { type: 'text', start: 0, end: 7 },
434
+ // { type: 'placeholder', start: 7, end: 31, nodes: [/* ... */] },
435
+ // { type: 'text', start: 31, end: 32 },
436
+ // ] }
437
+ ```
438
+
439
+ The tree is concrete: every node carries its `[start, end)` span in UTF-16 code units, the leaves come in the order the message writes them, and concatenating them spells the message back — which is what an editor, a linter or a syntax highlighter needs. The placeholders are the ones resolution finds, because the same scan finds them: one written in an option value is described there, and a construct the grammar derives nothing from is text. A name — a key, a modifier name, an option key — carries `name`, the span unescaped, beside `nodes`, how the message spells it; an escape carries `cancels`, which of its two readings the backslash takes.
440
+
441
+ It reads no options: a name is a name whether or not a modifier answers to it, so nothing a host registers changes the text. The node types are the `Cst` namespace (`Cst.Message`, `Cst.Placeholder`, `Cst.Node`, ...).
411
442
 
412
443
  ## Comparison with Other Parsers
413
444
 
@@ -436,8 +467,9 @@ Choose `parser-curly` for simplicity, ICU for standards compliance.
436
467
 
437
468
  ## More Resources
438
469
 
470
+ - [sveltekit-i18n.github.io](https://sveltekit-i18n.github.io) – The documentation site, with a live playground
439
471
  - [All Parsers](https://github.com/sveltekit-i18n/parsers) – Parser overview
440
- - [Curly Message Format](https://github.com/curly-message/spec) – The specification
472
+ - [Curly Message Format](https://curlymessage.dev) – The specification
441
473
  - [Examples](https://github.com/sveltekit-i18n/lib/tree/master/examples) – Working examples
442
474
  - [Changelog](./CHANGELOG.md) – Version history
443
475
 
@@ -445,6 +477,11 @@ Choose `parser-curly` for simplicity, ICU for standards compliance.
445
477
 
446
478
  If you're facing issues with this parser, create a ticket [here](https://github.com/sveltekit-i18n/lib/issues).
447
479
 
480
+ ## Sponsor
481
+
482
+ You can support the maintenance of this package through
483
+ [GitHub Sponsors](https://github.com/sponsors/sveltekit-i18n).
484
+
448
485
  ## License
449
486
 
450
487
  MIT
package/dist/index.d.ts CHANGED
@@ -1,18 +1,32 @@
1
1
  import { Parser as Parser$2, Config as Config$1 } from '@sveltekit-i18n/base';
2
2
  import { Modifier, Parser as Parser$1 } from '@curly-message/parser';
3
- export { Modifier, Report } from '@curly-message/parser';
3
+ export { Cst, Modifier, Report, cst } from '@curly-message/parser';
4
4
 
5
5
  declare namespace Parser {
6
6
  /**
7
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.
8
+ * `customModifiers`, `modifierDefaults`, `onReport`, `recognizeWrappers` and
9
+ * `onSuspectValue`. `onReport` is required, `null` included: this package
10
+ * writes to no channel of its own, so where a report goes is stated by
11
+ * whoever builds the parser.
11
12
  */
12
13
  type Options<Key extends string = Modifier.Key, Props = Modifier.DefaultProps> = Omit<Parser$1.Options<Key, Props>, 'onReport'> & {
13
14
  onReport: OnReport | null | undefined;
14
15
  };
15
16
  type OnReport = Parser$1.OnReport;
17
+ /**
18
+ * What a value holds that version 1 of the format would have read as syntax:
19
+ * `placeholder` where it holds `{{`, `escape` where it holds a backslash.
20
+ */
21
+ type SuspectKind = Parser$1.SuspectKind;
22
+ /**
23
+ * A value a placeholder read that version 1 of the format would have read as
24
+ * syntax. It is not a report — the placeholder resolved to exactly the text
25
+ * the payload holds — but a migration aid for a catalogue that composed
26
+ * messages through its payload.
27
+ */
28
+ type Suspect = Parser$1.Suspect;
29
+ type OnSuspectValue = Parser$1.OnSuspectValue;
16
30
  type PayloadDefault = Parser$1.PayloadDefault;
17
31
  type Payload<T = any, Props = Modifier.DefaultProps> = Parser$1.Payload<T, Props>;
18
32
  /**
@@ -28,8 +42,9 @@ declare namespace Parser {
28
42
  * so an extractor is built the way the app builds its parser — a custom
29
43
  * modifier registered under a name the format defines changes what a message
30
44
  * naming it says about its value. `onReport` is not required here, and
31
- * neither it nor `modifierDefaults` reaches anything: extraction formats
32
- * nothing and reports nothing.
45
+ * neither it nor `modifierDefaults`, `recognizeWrappers` or `onSuspectValue`
46
+ * reaches anything: extraction formats nothing, reports nothing and reads no
47
+ * payload.
33
48
  */
34
49
  type ExtractOptions<Key extends string = Modifier.Key, Props = Modifier.DefaultProps> = Parser$1.Options<Key, Props>;
35
50
  /**
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{createParser as f}from"@curly-message/parser";import{createExtractor as i}from"@curly-message/parser";var c=i;var m=r=>{let{resolve:e}=f(r);return{parse:(t,[o,a],p,s)=>e(t,{payload:o,props:a,locale:p,id:s})}},n=m;export{n as default,c as extractParamsFactory};
1
+ import{createParser as f}from"@curly-message/parser";import{createExtractor as c}from"@curly-message/parser";var i=c;import{cst as M}from"@curly-message/parser";var m=r=>{let{resolve:t}=f(r);return{parse:(e,[o,a],p,s)=>t(e,{payload:o,props:a,locale:p,id:s})}},n=m;export{M as cst,n as default,i as extractParamsFactory};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltekit-i18n/parser-curly",
3
- "version": "3.0.0",
3
+ "version": "3.1.1",
4
4
  "description": "Curly Message Format parser for the sveltekit-i18n library.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -13,7 +13,9 @@
13
13
  "./package.json": "./package.json"
14
14
  },
15
15
  "engines": {
16
- "node": ">=22"
16
+ "node": ">=22",
17
+ "bun": ">=1.2",
18
+ "deno": ">=2"
17
19
  },
18
20
  "scripts": {
19
21
  "dev": "tsup --watch",
@@ -45,15 +47,16 @@
45
47
  "bugs": {
46
48
  "url": "https://github.com/sveltekit-i18n/lib/issues"
47
49
  },
48
- "homepage": "https://github.com/sveltekit-i18n/parsers/tree/master/parser-curly#readme",
50
+ "homepage": "https://sveltekit-i18n.github.io",
51
+ "funding": "https://github.com/sponsors/sveltekit-i18n",
49
52
  "peerDependencies": {
50
- "@sveltekit-i18n/base": "^3.0.0"
53
+ "@sveltekit-i18n/base": "^3.0.0 || ^3.1.0-next.0"
51
54
  },
52
55
  "dependencies": {
53
- "@curly-message/parser": "^1.0.1"
56
+ "@curly-message/parser": "^3.0.0"
54
57
  },
55
58
  "devDependencies": {
56
- "@curly-message/conformance": "^1.0.1",
59
+ "@curly-message/conformance": "^4.0.0",
57
60
  "@eslint/js": "^10.0.1",
58
61
  "@stylistic/eslint-plugin": "^5.10.0",
59
62
  "@sveltekit-i18n/base": "^3.0.0",
@@ -65,5 +68,8 @@
65
68
  "typescript": "^5.1.6",
66
69
  "typescript-eslint": "^8.67.0",
67
70
  "vitest": "^4.1.10"
71
+ },
72
+ "overrides": {
73
+ "esbuild": "^0.28.1"
68
74
  }
69
75
  }