@sveltekit-i18n/parser-curly 3.0.0-next.3 → 3.1.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/README.md CHANGED
@@ -1,21 +1,20 @@
1
1
  [![npm version](https://badge.fury.io/js/@sveltekit-i18n%2Fparser-curly.svg)](https://badge.fury.io/js/@sveltekit-i18n%2Fparser-curly) [![Tests](https://github.com/sveltekit-i18n/parsers/actions/workflows/tests-parser-curly.yml/badge.svg)](https://github.com/sveltekit-i18n/parsers/actions/workflows/tests-parser-curly.yml)
2
- [![Netlify Status](https://api.netlify.com/api/v1/badges/61a65082-1dc8-4c2a-94f2-0334c005dad0/deploy-status)](https://app.netlify.com/sites/parser-default/deploys)
3
2
 
4
3
  # @sveltekit-i18n/parser-curly
5
4
 
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
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.
9
6
 
10
7
  ## Installation
11
8
 
12
9
  ```bash
13
10
  npm install @sveltekit-i18n/parser-curly
11
+ # bun add @sveltekit-i18n/parser-curly
12
+ # deno add npm:@sveltekit-i18n/parser-curly
14
13
  ```
15
14
 
16
15
  This parser is included by default in [sveltekit-i18n](https://github.com/sveltekit-i18n/lib).
17
16
 
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.
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.
19
18
 
20
19
  ## Usage
21
20
 
@@ -211,7 +210,7 @@ i18n.t('notification', { count: 0 })
211
210
  // → "You have no messages."
212
211
  ```
213
212
 
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.
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.
215
214
 
216
215
  ### Escaping
217
216
 
@@ -227,7 +226,7 @@ A backslash cancels the structural meaning of the character after it: `:`, `;`,
227
226
  | `\d+` | `\d+` |
228
227
  | `\{{v}}` | `{{v}}`, text whatever the payload carries |
229
228
 
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.
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.
231
230
 
232
231
  ## Payload
233
232
 
@@ -243,6 +242,8 @@ i18n.t('greeting', { name: { default: 'stranger' } })
243
242
 
244
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.
245
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
+
246
247
  ## Options
247
248
 
248
249
  ```javascript
@@ -263,6 +264,12 @@ const config = {
263
264
  // Where diagnostics go. Required: a function, or `null` to state that
264
265
  // reports go nowhere.
265
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,
266
273
  }),
267
274
  };
268
275
  ```
@@ -333,18 +340,20 @@ A `Report` carries:
333
340
 
334
341
  | Field | Meaning |
335
342
  | --- | --- |
336
- | `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` |
337
344
  | `origin` | who fixes it: `message` (the message as written), `payload` (what the call passed) or `limit` (a bound this parser set) |
338
345
  | `message` | a self-contained English sentence carrying nothing from the payload |
339
346
  | `id` | the message's id, where the call passed one; base passes the translation key |
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 |
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 |
342
349
 
343
350
  A report never raises: the placeholder takes its fallback (the empty string for `missing-locale`) and the rest of the message resolves.
344
351
 
345
352
  ## Limits
346
353
 
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).
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`.
348
357
 
349
358
  ## TypeScript
350
359
 
@@ -376,7 +385,7 @@ i18n.t('common.welcome', { aplicationName: 'My app' })
376
385
  // → type error: typo caught
377
386
  ```
378
387
 
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.
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.
380
389
 
381
390
  ## Extracting Parameters
382
391
 
@@ -408,15 +417,28 @@ A parameter several placeholders name accepts what all of them say together, and
408
417
 
409
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.
410
419
 
411
- 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.
412
421
 
413
- 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.
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.
414
423
 
415
- ## Examples
424
+ ## Describing a Message
416
425
 
417
- See the [parser-curly example](https://github.com/sveltekit-i18n/lib/tree/master/examples/parser-default) for a complete working application.
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.
418
427
 
419
- **[Live Demo](https://parser-default.netlify.app)** – Interactive examples
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`, ...).
420
442
 
421
443
  ## Comparison with Other Parsers
422
444
 
@@ -445,8 +467,9 @@ Choose `parser-curly` for simplicity, ICU for standards compliance.
445
467
 
446
468
  ## More Resources
447
469
 
470
+ - [sveltekit-i18n.github.io](https://sveltekit-i18n.github.io) – The documentation site, with a live playground
448
471
  - [All Parsers](https://github.com/sveltekit-i18n/parsers) – Parser overview
449
- - [Curly Message Format](https://github.com/curly-message/spec) – The specification
472
+ - [Curly Message Format](https://curlymessage.dev) – The specification
450
473
  - [Examples](https://github.com/sveltekit-i18n/lib/tree/master/examples) – Working examples
451
474
  - [Changelog](./CHANGELOG.md) – Version history
452
475
 
@@ -454,6 +477,11 @@ Choose `parser-curly` for simplicity, ICU for standards compliance.
454
477
 
455
478
  If you're facing issues with this parser, create a ticket [here](https://github.com/sveltekit-i18n/lib/issues).
456
479
 
480
+ ## Sponsor
481
+
482
+ You can support the maintenance of this package through
483
+ [GitHub Sponsors](https://github.com/sponsors/sveltekit-i18n).
484
+
457
485
  ## License
458
486
 
459
487
  MIT
package/dist/index.d.ts CHANGED
@@ -1,18 +1,27 @@
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
+ * A value a placeholder read that version 1 of the format would have read as
19
+ * syntax. It is not a report — the placeholder resolved to exactly the text
20
+ * the payload holds — but a migration aid for a catalogue that composed
21
+ * messages through its payload.
22
+ */
23
+ type Suspect = Parser$1.Suspect;
24
+ type OnSuspectValue = Parser$1.OnSuspectValue;
16
25
  type PayloadDefault = Parser$1.PayloadDefault;
17
26
  type Payload<T = any, Props = Modifier.DefaultProps> = Parser$1.Payload<T, Props>;
18
27
  /**
@@ -28,8 +37,9 @@ declare namespace Parser {
28
37
  * so an extractor is built the way the app builds its parser — a custom
29
38
  * modifier registered under a name the format defines changes what a message
30
39
  * 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.
40
+ * neither it nor `modifierDefaults`, `recognizeWrappers` or `onSuspectValue`
41
+ * reaches anything: extraction formats nothing, reports nothing and reads no
42
+ * payload.
33
43
  */
34
44
  type ExtractOptions<Key extends string = Modifier.Key, Props = Modifier.DefaultProps> = Parser$1.Options<Key, Props>;
35
45
  /**
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-next.3",
3
+ "version": "3.1.0",
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,18 +47,19 @@
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-next.0"
53
+ "@sveltekit-i18n/base": "^3.0.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
- "@sveltekit-i18n/base": "^3.0.0-next.0",
62
+ "@sveltekit-i18n/base": "^3.0.0",
60
63
  "eslint": "^10.8.1",
61
64
  "eslint-plugin-import-x": "^4.17.1",
62
65
  "globals": "^17.11.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
  }