@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.
- package/README.md +60 -18
- 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
|
|
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
|
-
|
|
36
|
-
loader: async () => (await import(
|
|
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
|
-
|
|
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;
|
|
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
|
|
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](
|
|
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.
|
|
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.
|
|
56
|
+
"@curly-message/parser": "^3.1.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@curly-message/conformance": "^4.
|
|
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.
|
|
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",
|