@sveltekit-i18n/parser-curly 3.1.1 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +60 -18
  2. package/package.json +4 -4
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # @sveltekit-i18n/parser-curly
4
4
 
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.
5
+ The [Curly Message Format](https://curlymessage.dev) for [@sveltekit-i18n/base](https://github.com/sveltekit-i18n/base): placeholders, defaults, modifiers, comparisons and plural selection 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
 
@@ -14,7 +14,7 @@ npm install @sveltekit-i18n/parser-curly
14
14
 
15
15
  This parser is included by default in [sveltekit-i18n](https://github.com/sveltekit-i18n/lib).
16
16
 
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.
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. The examples use base 3.1's loader spelling — `namespace`, and one descriptor listing several locales; on base 3.0 write `key` and one descriptor per locale, whose loader names its own file, since 3.0 passes it no `namespace`: `{ locale: 'en', key: 'common', loader: async () => (await import('./en/common.json')).default }`.
18
18
 
19
19
  ## Usage
20
20
 
@@ -24,16 +24,16 @@ This parser is included by default in [sveltekit-i18n](https://github.com/svelte
24
24
  import { I18n } from '@sveltekit-i18n/base';
25
25
  import parser from '@sveltekit-i18n/parser-curly';
26
26
 
27
- const config = {
27
+ export const config = {
28
28
  parser: parser({
29
29
  // Where diagnostics go; `null` states that they go nowhere.
30
30
  onReport: null,
31
31
  }),
32
32
  loaders: [
33
33
  {
34
- locale: 'en',
35
- key: 'common',
36
- loader: async () => (await import('./en/common.json')).default,
34
+ locale: ['en', 'cs'],
35
+ namespace: 'common',
36
+ loader: async ({ locale, namespace }) => (await import(`./${locale}/${namespace}.json`)).default,
37
37
  },
38
38
  ],
39
39
  };
@@ -46,7 +46,7 @@ export const i18n = new I18n(config);
46
46
  ```javascript
47
47
  import { I18n } from 'sveltekit-i18n';
48
48
 
49
- const config = {
49
+ export const config = {
50
50
  // parser-curly is already included
51
51
  loaders: [/* ... */],
52
52
  };
@@ -54,7 +54,11 @@ const config = {
54
54
  export const i18n = new I18n(config);
55
55
  ```
56
56
 
57
- Either way, `i18n.t(key, payload?, props?)` takes the values the placeholders name and the per-call formatting options; the examples below use it.
57
+ ### In a SvelteKit app
58
+
59
+ A SvelteKit app wires its config through the `/kit` subpath, new in 3.1, rather than through an instance of its own: `defineI18n(config, { preferredLocale })` returns `handle` for `hooks.server.js`, one `load` for both root layout files, `use()` for the root layout and `get()` for every component below it. The server builds an instance per request, so no visitor sees another visitor's locale. `sveltekit-i18n` users import it from `sveltekit-i18n/kit`, which fills the parser in; a base app imports it from `@sveltekit-i18n/base/kit` and hands it the config above. The core's [SvelteKit guide](https://github.com/sveltekit-i18n/base/blob/master/docs/README.md#sveltekit) walks through the setup.
60
+
61
+ However the instance is built, `i18n.t(key, payload?, props?)` takes the values the placeholders name and the per-call formatting options; the examples below use it.
58
62
 
59
63
  ## Syntax
60
64
 
@@ -159,7 +163,7 @@ i18n.t('cost', { amount: 1999 }, { currency: { currency: 'USD', ratio: 0.01 } })
159
163
  // → "Cost: $19.99"
160
164
  ```
161
165
 
162
- 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`.
166
+ 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 or a [plural selection](#plural-selection) resolves to the empty string rather than to the fallback and reports `missing-locale`.
163
167
 
164
168
  ### Comparisons
165
169
 
@@ -168,7 +172,6 @@ A value the modifier cannot read — text that is not a number, an empty string,
168
172
  ```json
169
173
  {
170
174
  "status": "{{state; active:Online; inactive:Offline; default:Unknown;}}",
171
- "items": "You have {{count}} {{count; 1:item; default:items;}}.",
172
175
  "stock": "{{count:gt; 0:In stock ({{count}}); default:Out of stock;}}",
173
176
  "age": "{{age:gte; 18:Adult; default:Minor;}}",
174
177
  "temp": "{{degrees:lt; 0:Freezing; default:Above freezing;}}",
@@ -179,8 +182,6 @@ A value the modifier cannot read — text that is not a number, an empty string,
179
182
  ```javascript
180
183
  i18n.t('status', { state: 'active' }) // → "Online"
181
184
  i18n.t('status', { state: 'pending' }) // → "Unknown"
182
- i18n.t('items', { count: 1 }) // → "You have 1 item."
183
- i18n.t('items', { count: 5 }) // → "You have 5 items."
184
185
  i18n.t('stock', { count: 5 }) // → "In stock (5)"
185
186
  i18n.t('stock', { count: 0 }) // → "Out of stock"
186
187
  i18n.t('age', { age: 25 }) // → "Adult"
@@ -189,7 +190,32 @@ i18n.t('health', { state: 'error' }) // → "Problem"
189
190
  i18n.t('health', {}) // → "Fine"
190
191
  ```
191
192
 
192
- 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`.
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 or a plural selection 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
+ ### Plural Selection
196
+
197
+ `plural` and `ordinal` select an option by the category the locale's plural rules put a number in — `Intl.PluralRules`, cardinal and ordinal. The categories are CLDR's `zero`, `one`, `two`, `few`, `many` and `other`, and each locale uses its own subset: in Russian 1, 21 and 101 are `one`, which a comparison cannot express.
198
+
199
+ ```json
200
+ {
201
+ "items": "You have {{count}} {{count:plural; one:item; other:items;}}.",
202
+ "inbox": "{{count:plural; 0:No messages; one:{{count}} message; other:{{count}} messages;}}",
203
+ "files": "{{count}} {{count:plural; one:файл; few:файла; many:файлов; other:файла;}}",
204
+ "place": "{{n}}{{n:ordinal; one:st; two:nd; few:rd; other:th;}}"
205
+ }
206
+ ```
207
+
208
+ ```javascript
209
+ i18n.t('items', { count: 1 }) // → "You have 1 item."
210
+ i18n.t('items', { count: 5 }) // → "You have 5 items."
211
+ i18n.t('inbox', { count: 0 }) // → "No messages"
212
+ i18n.t('inbox', { count: 21 }) // → "21 messages"
213
+ i18n.t('files', { count: 21 }) // → "21 файл" (in ru)
214
+ i18n.t('files', { count: 5 }) // → "5 файлов" (in ru)
215
+ i18n.t('place', { n: 22 }) // → "22nd"
216
+ ```
217
+
218
+ A numeric key matches the value exactly and wins over a category, as `0:` does above. A category the placeholder writes no option for takes the fallback — no option catches it as ICU's `other` does — so a message writes every category its locale uses. How `default` and the rest of the fallback chain answer, which props each modifier reads, and why `plural` follows the digits `number` shows are set out in [the reference implementation's README](https://github.com/curly-message/parsers/tree/js-v3.1.0/js#plural-selection).
193
219
 
194
220
  ### Nested Placeholders
195
221
 
@@ -385,7 +411,9 @@ i18n.t('common.welcome', { aplicationName: 'My app' })
385
411
  // → type error: typo caught
386
412
  ```
387
413
 
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.
414
+ `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`) and the migration aid's (`Parser.OnSuspectValue`, `Parser.Suspect`, `Parser.SuspectKind`), `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.
415
+
416
+ `onSuspectValue` receives a `Parser.Suspect` for every value a placeholder read that version 1 of the format would have read as syntax: `found`, the `Parser.SuspectKind`s the value holds (`placeholder` where it holds `{{`, `escape` where it holds a backslash); `placeholder`, the placeholder that read it, as the message spells it; `id`, the message's id, where the call passed one; and `text`, the value truncated with its line terminators escaped. Unlike a report's `text`, that is payload text, so a host writing it somewhere writes what its payload holds.
389
417
 
390
418
  ## Extracting Parameters
391
419
 
@@ -410,10 +438,11 @@ Each parameter is reported once, in the order the message first names it, and sa
410
438
  | `number`, `currency` | `'number'` |
411
439
  | `ago` | `'number'` — a signed millisecond delta relative to now, not a point in time |
412
440
  | `date` | `['date', 'string']` — milliseconds since the epoch, and failing that text the host reads as a date |
441
+ | `plural`, `ordinal` | `'number'` — `ordinal` takes an integer, which the kinds have no word for |
413
442
 
414
443
  A parameter several placeholders name accepts what all of them say together, and `unknown` is the top of that lattice rather than a member of it: it is what a parameter accepts while nothing has narrowed it, and it drops out the moment something does. So `{{count}}` alone reports `unknown`, and the example above — where a second placeholder formats the same key with `number` — reports `'number'` rather than `['unknown', 'number']`.
415
444
 
416
- `values` lists the option keys of an `eq` selection, which is the one comparison whose keys are values of the parameter: `ne`'s are what the value must differ from, and an inequality's are thresholds it is ordered against. It is a hint and never a closed set — a value none of them matches takes the fallback chain rather than failing.
445
+ `values` lists the option keys of an `eq` selection, which is the one comparison whose keys are values of the parameter: `ne`'s are what the value must differ from, and an inequality's are thresholds it is ordered against. A plural selection lists its keys that are numbers — of `ordinal`'s, the integers — which are values the parameter takes; its categories (`one`, `few`, …) are not. It is a hint and never a closed set — a value none of them matches takes the fallback chain rather than failing.
417
446
 
418
447
  `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.
419
448
 
@@ -421,6 +450,18 @@ Build the extractor from the same options `parser()` is built from: a custom mod
421
450
 
422
451
  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.
423
452
 
453
+ This is what types `t` and `l` by key and payload: [`@sveltekit-i18n/typegen`](https://github.com/sveltekit-i18n/typegen), a Vite plugin, runs the config the module it is pointed at exports as `config`, reads every message through the extractor and writes the schema, which it registers for every config that states no [`schema`](https://github.com/sveltekit-i18n/base/blob/master/docs/README.md#schema) of its own, so on base 3.1 there is nothing to wire ([an older core](https://github.com/sveltekit-i18n/typegen#3-nothing-to-wire-on-a-31-core) takes a cast). `sveltekit-i18n` re-exports this extractor, so its users name that package; a base app names this one:
454
+
455
+ ```javascript
456
+ // vite.config.js, with sveltekit-i18n
457
+ typegen({ config: 'src/lib/i18n.js', extractParams: { from: 'sveltekit-i18n' } })
458
+
459
+ // vite.config.js, with @sveltekit-i18n/base
460
+ typegen({ config: 'src/lib/i18n.js', extractParams: { from: '@sveltekit-i18n/parser-curly' } })
461
+ ```
462
+
463
+ The plugin hands `extractParams.options` to the factory as JSON, so no function reaches the extractor. `customModifiers` — the one option extraction reads — cannot be passed that way, and a custom modifier registered under a built-in name that changes the `kind` a parameter is reported with is not reflected: the schema types the parameter by the built-in modifier.
464
+
424
465
  ## Describing a Message
425
466
 
426
467
  `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.
@@ -447,7 +488,7 @@ It reads no options: a name is a name whether or not a modifier answers to it, s
447
488
  **parser-curly:**
448
489
  ```json
449
490
  {
450
- "items": "You have {{count}} {{count; 1:item; default:items;}}."
491
+ "items": "You have {{count}} {{count:plural; one:item; other:items;}}."
451
492
  }
452
493
  ```
453
494
 
@@ -459,7 +500,8 @@ It reads no options: a name is a name whether or not a modifier answers to it, s
459
500
  ```
460
501
 
461
502
  - `parser-curly` has simpler syntax
462
- - ICU has more advanced plural rules for complex languages
503
+ - both select plural forms by the locale's CLDR categories; ICU's `other` catches every category a message does not write, while in `parser-curly` such a category takes the fallback, so a message for a locale writes the categories it uses
504
+ - ICU also has `offset` and the `#` shorthand
463
505
  - `parser-curly` has one dependency, the format's reference implementation, and no others
464
506
  - ICU is an industry standard
465
507
 
@@ -471,7 +513,7 @@ Choose `parser-curly` for simplicity, ICU for standards compliance.
471
513
  - [All Parsers](https://github.com/sveltekit-i18n/parsers) – Parser overview
472
514
  - [Curly Message Format](https://curlymessage.dev) – The specification
473
515
  - [Examples](https://github.com/sveltekit-i18n/lib/tree/master/examples) – Working examples
474
- - [Changelog](./CHANGELOG.md) – Version history
516
+ - [Changelog](https://github.com/sveltekit-i18n/parsers/blob/parser-curly@3.2.0/parser-curly/CHANGELOG.md) – Version history
475
517
 
476
518
  ## Issues
477
519
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltekit-i18n/parser-curly",
3
- "version": "3.1.1",
3
+ "version": "3.2.0",
4
4
  "description": "Curly Message Format parser for the sveltekit-i18n library.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -53,13 +53,13 @@
53
53
  "@sveltekit-i18n/base": "^3.0.0 || ^3.1.0-next.0"
54
54
  },
55
55
  "dependencies": {
56
- "@curly-message/parser": "^3.0.0"
56
+ "@curly-message/parser": "^3.1.0"
57
57
  },
58
58
  "devDependencies": {
59
- "@curly-message/conformance": "^4.0.0",
59
+ "@curly-message/conformance": "^4.1.0",
60
60
  "@eslint/js": "^10.0.1",
61
61
  "@stylistic/eslint-plugin": "^5.10.0",
62
- "@sveltekit-i18n/base": "^3.0.0",
62
+ "@sveltekit-i18n/base": "^3.1.2",
63
63
  "eslint": "^10.8.1",
64
64
  "eslint-plugin-import-x": "^4.17.1",
65
65
  "globals": "^17.11.0",