prosebook 0.2.0 → 0.4.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 (115) hide show
  1. package/README.md +345 -29
  2. package/dist/bun.d.ts +34 -0
  3. package/dist/bun.d.ts.map +1 -0
  4. package/dist/bun.js +98 -0
  5. package/dist/bun.js.map +1 -0
  6. package/dist/bundle-CpUIWb9j.d.ts +6 -0
  7. package/dist/bundle-CpUIWb9j.d.ts.map +1 -0
  8. package/dist/bundle-DboItAre.js +111 -0
  9. package/dist/bundle-DboItAre.js.map +1 -0
  10. package/dist/cli.js +51 -56
  11. package/dist/cli.js.map +1 -1
  12. package/dist/compile-ClK9htKr.d.ts +16 -0
  13. package/dist/compile-ClK9htKr.d.ts.map +1 -0
  14. package/dist/{config-_4HfnJQu.d.ts → config-CzaJJQ60.d.ts} +75 -2
  15. package/dist/config-CzaJJQ60.d.ts.map +1 -0
  16. package/dist/{config-D--RITk2.js → config-nWZf9QmF.js} +42 -212
  17. package/dist/config-nWZf9QmF.js.map +1 -0
  18. package/dist/config.d.ts +2 -2
  19. package/dist/config.js +1 -1
  20. package/dist/{copy-DXnzXsJa.d.ts → copy-B3V_QRkT.d.ts} +20 -7
  21. package/dist/copy-B3V_QRkT.d.ts.map +1 -0
  22. package/dist/copy-Bo_FIoLy.js +154 -0
  23. package/dist/copy-Bo_FIoLy.js.map +1 -0
  24. package/dist/endpoints-BgG_5Vpm.js +96 -0
  25. package/dist/endpoints-BgG_5Vpm.js.map +1 -0
  26. package/dist/generate-BW3N8EGb.js +5488 -0
  27. package/dist/generate-BW3N8EGb.js.map +1 -0
  28. package/dist/helpers-BWGcD0kS.js +96 -0
  29. package/dist/helpers-BWGcD0kS.js.map +1 -0
  30. package/dist/icu-Dej0mNsF.js +218 -0
  31. package/dist/icu-Dej0mNsF.js.map +1 -0
  32. package/dist/index.d.ts +12 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +4 -188
  35. package/dist/inspector.d.ts +4 -1
  36. package/dist/inspector.d.ts.map +1 -1
  37. package/dist/inspector.js +7 -2
  38. package/dist/inspector.js.map +1 -1
  39. package/dist/locale-DpQA1pqH.js +70 -0
  40. package/dist/locale-DpQA1pqH.js.map +1 -0
  41. package/dist/next-loader.d.ts +11 -0
  42. package/dist/next-loader.d.ts.map +1 -0
  43. package/dist/next-loader.js +17 -0
  44. package/dist/next-loader.js.map +1 -0
  45. package/dist/next.d.ts +30 -0
  46. package/dist/next.d.ts.map +1 -0
  47. package/dist/next.js +160 -0
  48. package/dist/next.js.map +1 -0
  49. package/dist/react/index.d.ts +3 -3
  50. package/dist/react/index.d.ts.map +1 -1
  51. package/dist/react/index.js.map +1 -1
  52. package/dist/react/server.d.ts +20 -0
  53. package/dist/react/server.d.ts.map +1 -0
  54. package/dist/react/server.js +34 -0
  55. package/dist/react/server.js.map +1 -0
  56. package/dist/{sarif-CI8HOokm.js → sarif-B5nrMqrT.js} +103 -87
  57. package/dist/sarif-B5nrMqrT.js.map +1 -0
  58. package/dist/scan-Bx0_yDSi.js +387 -0
  59. package/dist/scan-Bx0_yDSi.js.map +1 -0
  60. package/dist/tools.d.ts +83 -90
  61. package/dist/tools.d.ts.map +1 -1
  62. package/dist/tools.js +5 -4
  63. package/dist/vite.d.ts +3 -2
  64. package/dist/vite.d.ts.map +1 -1
  65. package/dist/vite.js +29 -109
  66. package/dist/vite.js.map +1 -1
  67. package/package.json +37 -4
  68. package/src/bun.ts +132 -0
  69. package/src/cli.ts +60 -58
  70. package/src/index.ts +1 -1
  71. package/src/inspector.ts +8 -3
  72. package/src/next-loader.ts +16 -0
  73. package/src/next.ts +188 -0
  74. package/src/react/index.tsx +6 -6
  75. package/src/react/server.ts +37 -0
  76. package/src/runtime/copy.ts +22 -3
  77. package/src/runtime/helpers.ts +54 -18
  78. package/src/runtime/locale.ts +19 -0
  79. package/src/runtime/types.ts +21 -6
  80. package/src/tools/bundle.ts +131 -0
  81. package/src/tools/catalog.ts +2 -2
  82. package/src/tools/compile.ts +60 -23
  83. package/src/tools/config.ts +57 -0
  84. package/src/tools/endpoints.ts +121 -0
  85. package/src/tools/entry.ts +77 -0
  86. package/src/tools/generate.ts +2 -0
  87. package/src/tools/icu.ts +63 -7
  88. package/src/tools/index.ts +6 -3
  89. package/src/tools/lock.ts +7 -0
  90. package/src/tools/mf1.ts +21 -7
  91. package/src/tools/mf2.ts +249 -57
  92. package/src/tools/migrate.ts +3 -2
  93. package/src/tools/native/android.ts +201 -0
  94. package/src/tools/native/apple.ts +139 -0
  95. package/src/tools/native/emit.ts +87 -0
  96. package/src/tools/native/index.ts +48 -0
  97. package/src/tools/native/kotlin.ts +269 -0
  98. package/src/tools/native/model.ts +292 -0
  99. package/src/tools/native/number.ts +210 -0
  100. package/src/tools/native/plurals.ts +103 -0
  101. package/src/tools/native/printf.ts +95 -0
  102. package/src/tools/native/rust.ts +367 -0
  103. package/src/tools/native/swift.ts +326 -0
  104. package/src/tools/syntax.ts +3 -3
  105. package/src/tools/todo.ts +118 -0
  106. package/src/vite.ts +28 -114
  107. package/dist/config-D--RITk2.js.map +0 -1
  108. package/dist/config-_4HfnJQu.d.ts.map +0 -1
  109. package/dist/copy-DXnzXsJa.d.ts.map +0 -1
  110. package/dist/helpers-BPuPGjQP.js +0 -61
  111. package/dist/helpers-BPuPGjQP.js.map +0 -1
  112. package/dist/index.js.map +0 -1
  113. package/dist/sarif-CI8HOokm.js.map +0 -1
  114. package/dist/scan-Ch2BfIUD.js +0 -2561
  115. package/dist/scan-Ch2BfIUD.js.map +0 -1
package/README.md CHANGED
@@ -32,11 +32,13 @@ const { t } = useCopy();
32
32
  2.5 KB gzipped.
33
33
  - **Checked.** Placeholders that drift between the source and a translation fail the build. A
34
34
  translation whose source has changed since is marked stale, automatically.
35
- - **Pruned.** A Vite build ships only the messages the code names, and fails on a read whose key
36
- it can't see rather than show a key where a message was.
35
+ - **Pruned.** A Vite, Next.js or Bun build ships only the messages the code names, and fails on a
36
+ read whose key it can't see rather than show a key where a message was.
37
37
  - **Written well.** An optional lint holds the words to a writing guide: word budgets per kind of
38
38
  string, periods, sentence case, banned filler, and `{count} files` outside a plural. Plugins add your
39
39
  own rules, scoped by kind, surface and locale, and translations are held to their source.
40
+ - **Native too.** The same catalog generates Rust, Swift and Kotlin for a tray, a menu bar or a CLI,
41
+ with plurals and number formats that say what the browser says, and the string resources Android and Apple read.
40
42
 
41
43
  ## Quick start
42
44
 
@@ -68,6 +70,63 @@ export default defineConfig({ plugins: [react(), prosebook()] });
68
70
  Several apps that share one catalog each point the plugin at it: `prosebook({ root: '../../packages/copy' })`
69
71
  reads the config there and resolves its paths from there, so every app generates the same files.
70
72
 
73
+ ### With Next.js
74
+
75
+ ```ts
76
+ // next.config.ts
77
+ import { withProsebook } from 'prosebook/next';
78
+
79
+ export default withProsebook({/* the rest of your Next config */});
80
+ ```
81
+
82
+ `next dev` and `next build` generate first, and dev regenerates on every catalog edit: server
83
+ components read the new text on their next render, and in the browser the messages change in
84
+ place. A build ships only the messages the code names. It works with Turbopack, Next's default
85
+ bundler, which doesn't resolve `./en.js` to `en.ts`, so set `importExtension: ''` in the prosebook
86
+ config. In dev the [inspector](#finding-a-strings-entry) comes with it, with nothing to add to the app.
87
+
88
+ ### With Bun
89
+
90
+ `Bun.serve`'s dev server bundles a page with the plugins bunfig.toml names, and regenerates and
91
+ swaps the messages in place with this one:
92
+
93
+ ```toml
94
+ # bunfig.toml
95
+ [serve.static]
96
+ plugins = ["prosebook/bun"]
97
+ ```
98
+
99
+ In dev the page gets the [inspector](#finding-a-strings-entry) too. Its endpoints are routes for
100
+ `Bun.serve` to spread into its own, imported only in dev:
101
+
102
+ ```ts
103
+ // src/server.ts
104
+ const dev = process.env.NODE_ENV !== 'production';
105
+
106
+ Bun.serve({
107
+ routes: { ...(dev ? (await import('prosebook/bun')).inspector() : {}), '/': index },
108
+ development: dev && { hmr: true },
109
+ });
110
+ ```
111
+
112
+ The `bun build` command reads no plugins, so a build that prunes calls `Bun.build`, with
113
+ `prosebook/bun` left `external` so the dev-only routes stay out of the bundle:
114
+
115
+ ```ts
116
+ // build.ts
117
+ import { prosebook } from 'prosebook/bun';
118
+
119
+ await Bun.build({
120
+ entrypoints: ['./src/server.ts'],
121
+ target: 'bun',
122
+ outdir: './dist',
123
+ external: ['prosebook/bun'],
124
+ plugins: [prosebook()],
125
+ });
126
+ ```
127
+
128
+ TanStack Start, React Router and other frameworks built on Vite take the Vite plugin as it is.
129
+
71
130
  ### In React
72
131
 
73
132
  ```tsx
@@ -108,8 +167,204 @@ document.title = copy.t('app.title');
108
167
  copy.subscribe(() => render()); // after a locale switch, and in dev after a catalog edit
109
168
  ```
110
169
 
111
- Set `language: 'js'` in the config to generate `.js` modules with a `.d.ts` beside the index. On a
112
- server, create one `copy` per request.
170
+ Set `language: 'js'` in the config to generate `.js` modules with a `.d.ts` beside the index, or
171
+ `importExtension: '.ts'` to run the generated TypeScript in Node as it is.
172
+
173
+ A message never throws. A placeholder with no value, or with one `Intl` refuses (an invalid date,
174
+ `currency: 'euro'`), shows as `{name}` (`{$name}` in MF2), and the rest of the message renders. The
175
+ problem goes to `console.error`, or to `onError` if you pass one:
176
+
177
+ ```js
178
+ createCopy(catalog, { onError: (error, key, locale) => report(error, { key, locale }) });
179
+ ```
180
+
181
+ ### On a server
182
+
183
+ Create one `copy` per request, in the locale it asks for, and wait for its messages.
184
+ `acceptedLocales` reads an `Accept-Language` header into the locales it asks for, by weight, so
185
+ `de;q=0.1, en;q=0.9` asks for English first:
186
+
187
+ ```ts
188
+ import { acceptedLocales, createCopy } from 'prosebook';
189
+
190
+ const copy = createCopy(catalog, { locale: acceptedLocales(request.headers.get('accept-language')) });
191
+ await copy.ready;
192
+ ```
193
+
194
+ **React Server Components** can't read context, so `prosebook/react/server` hands each request one
195
+ copy that every server component in it shares. Your function is asked for the locale once per
196
+ request, and React's `cache` keeps one request's copy from reaching another's:
197
+
198
+ ```ts
199
+ // server-copy.ts, here with Next.js
200
+ import { cookies, headers } from 'next/headers';
201
+ import { acceptedLocales } from 'prosebook';
202
+ import { createServerCopy } from 'prosebook/react/server';
203
+
204
+ export const serverCopy = createServerCopy(catalog, async () => {
205
+ const picked = (await cookies()).get('locale')?.value;
206
+ const asked = acceptedLocales((await headers()).get('accept-language'));
207
+ return picked === undefined ? asked : [picked, ...asked];
208
+ });
209
+ ```
210
+
211
+ ```tsx
212
+ export default async function Page() {
213
+ const { t, locale } = await serverCopy(); // ready, in the locale negotiated for the request
214
+ return <h1 lang={locale}>{t('app.title')}</h1>;
215
+ }
216
+ ```
217
+
218
+ It's its own entry because `prosebook/react` creates a context when it loads, which the server
219
+ build of React refuses. It needs React 19, where `cache` is.
220
+
221
+ A page rendered on the server must hydrate in the same locale, so the browser's copy waits for its
222
+ messages too. Create it above a `<Suspense>` boundary and wait below it: a component that suspends
223
+ before it first mounts loses its state, copy included.
224
+
225
+ ```tsx
226
+ function Providers({ locale, children }: { locale: Locale; children: ReactNode }) {
227
+ const [copy] = useState(() => createCopy(catalog, { locale }));
228
+ return (
229
+ <Suspense>
230
+ <Ready copy={copy}>{children}</Ready>
231
+ </Suspense>
232
+ );
233
+ }
234
+
235
+ function Ready({ copy, children }: { copy: Copy; children: ReactNode }) {
236
+ use(copy.ready);
237
+ return <CopyProvider copy={copy}>{children}</CopyProvider>;
238
+ }
239
+ ```
240
+
241
+ ### Outside the browser: Rust, Swift, Kotlin, Android and Apple
242
+
243
+ The parts of an app the browser doesn't draw, a tray, a menu bar, a CLI, an Android shell, can read
244
+ the same catalog. `generate` writes one file per target in `native`, holding every locale:
245
+
246
+ ```ts
247
+ // prosebook.config.ts
248
+ export default defineConfig({
249
+ native: [
250
+ { language: 'rust', out: 'src-tauri/src/copy.rs', keys: ['tray', 'menu'] },
251
+ { language: 'swift', out: 'mac/Sources/Copy.swift' },
252
+ { language: 'kotlin', out: 'android/app/src/main/java/shop/Copy.kt', package: 'shop' },
253
+ ],
254
+ });
255
+ ```
256
+
257
+ Each is an enum of the locales with a member per message, named for its key (`tray.recordings` is
258
+ `tray_recordings` in Rust, `trayRecordings` in Swift and Kotlin). `keys` takes only the keys under
259
+ those prefixes and names them without the prefix, so `tray.quit` above is `quit`:
260
+
261
+ ```rust
262
+ mod copy;
263
+ use copy::Locale;
264
+
265
+ let copy = Locale::negotiate(&["de-AT", "en"]); // de-AT, then de, then the source
266
+ let quit: &'static str = copy.quit();
267
+ let count = copy.recordings(3); // "3 Aufnahmen": any integer or float
268
+ ```
269
+
270
+ ```swift
271
+ let copy = Copy.negotiate(Locale.preferredLanguages)
272
+ menu.title = copy.trayQuit
273
+ label.stringValue = copy.trayRecordings(count: 3)
274
+ ```
275
+
276
+ ```kotlin
277
+ val copy = Copy.negotiate(listOf(Locale.getDefault().toLanguageTag()))
278
+ item.title = copy.trayQuit
279
+ label.text = copy.trayRecordings(3)
280
+ ```
281
+
282
+ A message with no placeholders is a `&'static str` in Rust and a property in Swift and Kotlin; one
283
+ with placeholders is a function taking them in order, a string or a number of any integer or float type. Each member's doc comment
284
+ is the source text, its context and what each argument holds.
285
+
286
+ The files need nothing beside them: each brings the plural rules of its locales, compiled from CLDR's
287
+ (the data `Intl.PluralRules` is built on), and its number formats, taken apart from `Intl` when it is
288
+ generated: the digits, the separators, how integers group, the text around a currency or a percent.
289
+ Each format is checked against `Intl` before any code is written, so native code writes what the
290
+ browser does, Arabic digits and Indian grouping included. A key with no translation falls back
291
+ through its parents to the source as the runtime's do, resolved when the file is written.
292
+
293
+ What can't be written that way fails `generate`, with the fix: a date (pass it formatted, as a plain
294
+ `{when}`), a tag, compact notation, significant digits, a unit or currency name that changes with the
295
+ count (`unitDisplay: 'long'`), and an MF2 option read from an argument. Leave such keys out of the
296
+ target's `keys`.
297
+
298
+ #### Android resources
299
+
300
+ An Android app needs string resources even with the Kotlin target: its manifest, launcher shortcuts,
301
+ an accessibility service's description and every other XML resource name strings as `@string/…`, and
302
+ only a resource answers that. The `android` target writes them, with `out` naming the `res` folder:
303
+
304
+ ```ts
305
+ native: [{ language: 'android', out: 'android/app/src/main/res', keys: ['android'] }],
306
+ ```
307
+
308
+ `values/strings.xml` holds the source locale, and each other locale gets a folder named as Android
309
+ names it (`values-de`, `values-pt-rBR`, `values-b+sr+Latn` for a script), holding what that locale
310
+ shows, every fallback resolved. `name` renames the file, `strings` by default, so it can sit beside
311
+ strings you write yourself. A resource is named for its key, less the prefix, with `.` and `-` as
312
+ `_`: `android.sign-in.denied` is `R.string.sign_in_denied`. Above each is a comment with its context
313
+ and what each argument holds.
314
+
315
+ ```xml
316
+ <!-- Counts the cart. count: how many items the cart holds. -->
317
+ <plurals name="cart_items">
318
+ <item quantity="one">%1$d item in the cart.</item>
319
+ <item quantity="other">%1$d items in the cart.</item>
320
+ </plurals>
321
+ <!-- Greets the person signed in. firstName: the person's first name. -->
322
+ <string name="hello">Hello, %1$s!</string>
323
+ ```
324
+
325
+ Android fills placeholders in with `String.format` and picks a plural's arm itself, so a resource
326
+ holds less than the other targets: text, plain `{name}` placeholders, written `%1$s` by their place in
327
+ the source so a translation can reorder them, and one plural, which becomes `<plurals>` with its count
328
+ as `%1$d` (the text around it goes into each arm). Call it as Android does,
329
+ `resources.getQuantityString(R.plurals.cart_items, count, count)`. `String.format` isn't `Intl`, so
330
+ nothing here is checked against it: `%d` writes 1234 without grouping. Android picks the arm by the
331
+ device's language, so a plural a locale doesn't translate picks among the source's arms by that
332
+ language's rules. Escaping is done for you: quotes, apostrophes, `\`, `&`, `<`, a newline, a leading
333
+ `@` or `?`, `%` where the string is formatted, and spaces Android would collapse or trim.
334
+
335
+ What a resource can't hold fails `generate`, with the fix: a select, a date, a formatted number, a
336
+ second plural, an ordinal, an offset, an exact arm such as `=0` (Android's `zero` is a category, which
337
+ English never picks), a selector other than a bare `:number` or `:integer`, and a tag.
338
+
339
+ #### Apple string tables
340
+
341
+ An Apple app has the same gap: macOS and iOS read the permission prompts, the app's name and other
342
+ Info.plist strings from the bundle, never from Swift. The `apple` target writes `.strings` tables, a
343
+ `<locale>.lproj/<name>.strings` per locale under `out`, each holding what that locale shows. They are
344
+ plain `.strings` files, not a String Catalog, which only Xcode compiles: an app bundled by Tauri or a
345
+ script copies the `.lproj` folders into `Contents/Resources` as they are.
346
+
347
+ With `plist`, the table is `InfoPlist` and translates that Info.plist. A catalog key can't be written
348
+ `NSCameraUsageDescription`, so its words spell the plist key: `macos.ns-camera-usage-description` is
349
+ `NSCameraUsageDescription`. The key must be in the plist, since the system reads only the keys there,
350
+ and the plist's own text must be the source's, since it shows where no `.lproj` matches. Either way
351
+ round, `generate` fails with the fix:
352
+
353
+ ```ts
354
+ native: [{ language: 'apple', out: 'src-tauri/Resources', keys: ['macos'], plist: 'src-tauri/Info.plist' }],
355
+ ```
356
+
357
+ ```c
358
+ /* Asks for the camera. */
359
+ "NSCameraUsageDescription" = "Corner Shop scans a product's barcode with the camera.";
360
+ ```
361
+
362
+ Without `plist`, the table is `Localizable`, for what the system looks up by key, such as a push
363
+ notification's `loc-key`. An entry is named for its key less the prefix, as written (`tray.quit` is
364
+ `"quit"` under `['tray']`), and placeholders are written `%1$@` by their place in the source, filled
365
+ in by `String(format:)`, not `Intl`. A table holds text and plain `{name}` placeholders; a plural (it
366
+ would take a `.stringsdict`), a select, a date, a formatted number and a tag fail `generate`, and an
367
+ Info.plist string takes no placeholders at all. Show those from the Swift target.
113
368
 
114
369
  ## The catalog
115
370
 
@@ -199,14 +454,40 @@ en: |-
199
454
 
200
455
  A message with `.match` holds whole sentences per variant, so a translator never stitches a sentence
201
456
  from an arm and the text around it. Several selectors pick the variant MF2's rules pick: the best
202
- match on the first, then the next, an exact number beating its plural category. `:offset` covers
203
- MF1's plural offset.
457
+ match on the first, then the next, an exact number beating its plural category. A number is matched
458
+ as it's shown: under `.input {$n :number minimumFractionDigits=1}`, 1 shows as "1.0", which English
459
+ counts as `other`. `:integer` matches the value rounded, and `:percent` matches it times 100.
460
+ `:offset` covers MF1's plural offset.
204
461
 
205
462
  The functions are MF2's defaults: `:string`, `:number`, `:integer`, `:percent`, `:currency`, `:unit`,
206
463
  `:offset`, and the date and time functions `:date`, `:time` and `:datetime`, which are still a draft
207
- in the spec and follow it as the `messageformat` package does. Option values are literals, since
208
- formatting is settled at build time; `currency=$code` is refused with that reason. Placeholders
209
- aren't wrapped in bidi isolation marks.
464
+ in the spec and follow it as the `messageformat` package does. `.match` selects with `:number`,
465
+ `:integer`, `:percent` or `:string`.
466
+
467
+ **Text direction.** In a locale written right to left, each placeholder is wrapped in Unicode
468
+ isolation marks, so a value written the other way can't reorder the text around it. Without them, a
469
+ Latin name in a Hebrew sentence can pull the punctuation after it to the wrong side. `u:dir` on an
470
+ expression picks its direction (`{$name :string u:dir=ltr}`) and asks for isolation in any locale.
471
+ `isolate: 'always'` in the config also isolates every string placeholder in left-to-right locales,
472
+ as the spec's default does. `isolate: false` turns it off.
473
+
474
+ **Options from arguments.** An option can take its value from an argument, so one message formats
475
+ whatever currency the order is in:
476
+
477
+ ```yaml
478
+ en: 'Total: {$price :currency currency=$code}' # t('cart.total', { price: 9, code: 'USD' }): $9.00
479
+ en: 'Due {$when :time timeZone=$tz}'
480
+ ```
481
+
482
+ The argument is typed with the rest (`code: string`; `currencyDisplay=$d` takes only the values the
483
+ option does), and a translation must use it too. These options can come from an argument: `currency`,
484
+ `currencyDisplay`, `currencySign`, `unit`, `unitDisplay`, `signDisplay`, `fractionDigits` and the
485
+ other digit counts, `timeZone` and `calendar`. The rest, such as a date's `fields` or a number's
486
+ `select`, decide what the formatter is, so they're written in the message and the message says so.
487
+ Literal options are still settled at build time. The ones from arguments are merged in as the
488
+ message renders, and each locale keeps one formatter per distinct value. A value `Intl` refuses,
489
+ such as `code: 'euro'`, throws a `RangeError` there. MF1 has no way to say this, so `migrate 1`
490
+ stops at such a message.
210
491
 
211
492
  **Switching.** `prosebook migrate 2` rewrites the catalog and every translation in MF2, and
212
493
  `prosebook migrate 1` goes back. It's a dry run unless `--write`; then set `messageFormat` in the
@@ -236,7 +517,8 @@ still hold:
236
517
 
237
518
  ```bash
238
519
  prosebook status # coverage and stale counts per locale
239
- prosebook todo de # missing and stale entries, with context, ready to translate
520
+ prosebook todo de > de.yaml # missing and stale entries, with context, ready to translate
521
+ prosebook import de de.yaml # each filled-in entry into its file, checked first
240
522
  prosebook accept de cart.items
241
523
  ```
242
524
 
@@ -251,6 +533,7 @@ de 13/14 92% 1 2
251
533
 
252
534
  $ prosebook todo de
253
535
  # The button on a product that adds one to the cart.
536
+ # kind: action, surface: action
254
537
  # en: Add one to cart
255
538
  # stale, the source changed: Add {+one +}to cart
256
539
  cart.add: In den Warenkorb
@@ -258,6 +541,23 @@ cart.add: In den Warenkorb
258
541
 
259
542
  Stale translations still show by default; `stale: 'fallback'` shows the source instead.
260
543
 
544
+ **Translating with a model.** prosebook calls no model itself. `prosebook todo de --prompt` prints
545
+ the same entries as a prompt for whichever model you use: how to reply, the plural forms German
546
+ takes, what each kind of string and surface asks for, and how to treat a stale entry. Hand it to the
547
+ model and give its reply to `import`, as a file or on stdin:
548
+
549
+ ```bash
550
+ prosebook todo de --prompt | pbcopy # paste into a chat, or hand it to an agent
551
+ prosebook import de reply.md # the YAML block is read out of the reply
552
+ ```
553
+
554
+ `import` checks every entry as the whole catalog before writing it. One whose placeholders, tags
555
+ or syntax don't hold is left out with its fix, which you can give back to the model, and the rest
556
+ are written. A stale translation given back unchanged is accepted as still right. The imported
557
+ translations are then linted, as a person's would be. An agent working in the repository needs none
558
+ of this: it can read `prosebook todo de`, write the translation files itself, and run
559
+ `prosebook check`.
560
+
261
561
  **Pseudo-locale.** `pseudo: 'en-XA'` generates `⟦Ýóóúúŕ çááŕţ⟧` from the source. Switch to it and
262
562
  any plain text on screen is hardcoded, a missing bracket is clipped text, and brackets inside
263
563
  brackets are a sentence spliced from fragments.
@@ -496,7 +796,8 @@ the singular is the plural less its `s`, `es` or `ies`.
496
796
  ## Pruning
497
797
 
498
798
  The Vite plugin replaces each locale module in a build with only the messages some module
499
- references. A reference is any string literal equal to a key, so keys kept in arrays or maps count;
799
+ references. Next's Turbopack and Bun load a locale before the code that reads it, so their
800
+ integrations read the source under `scan.dirs` instead, and every key a file there names is kept. A reference is any string literal equal to a key, so keys kept in arrays or maps count;
500
801
  ``t(`status.${state}`)`` and `t('status.' + state)` keep every key under `status.`. For keys built
501
802
  some other way, list them: `prosebook({ keep: ['plan.*'] })`.
502
803
 
@@ -506,10 +807,11 @@ or, where every key it reads is written elsewhere (in an array, say) or listed i
506
807
  line with `prosebook-ignore`. Reads are known by name: `t` and `rich` alone or as methods, the names
507
808
  they're destructured to (`const { rich: tRich } = copy`) in any module, and `<Text>` from
508
809
  `prosebook/react`. `prune: 'loose'` prunes without the check, and `prune: false` ships everything.
810
+ `prune` and `keep` are options of each integration.
509
811
 
510
812
  ## Finding a string's entry
511
813
 
512
- In dev, hold Alt (Option on a Mac) and point at text on the page: the Vite plugin outlines the
814
+ In dev, hold Alt (Option on a Mac) and point at text on the page: the inspector outlines the
513
815
  string and names its key and the locale it came from. Alt-click opens its entry in your editor: the
514
816
  translation for a translated string, the catalog entry for one that fell back. Alt-Shift-click opens
515
817
  the catalog entry either way. Save, and the string changes on the page, which keeps its locale and
@@ -539,10 +841,17 @@ Nothing is added to the page to make this work. In dev the runtime reports each
539
841
  and the inspector matches the text under the pointer back to it, so `t()`, `<Text>`, a sentence with
540
842
  a link inside it and readable attributes (`placeholder`, `title`, `aria-label`, `alt`) all work. Text
541
843
  the code changes after reading it (`t('…').toUpperCase()`) can't be traced. Where two keys read the
542
- same text, the label says so and the click opens the one read last.
543
-
544
- The editor is the one Vite's `/__open-in-editor` launches: one it finds running, or the command in
545
- `LAUNCH_EDITOR`. Turn the inspector off with `prosebook({ inspect: false })`.
844
+ same text, the label says so and the click opens the one read last. Under Next.js, only text read in
845
+ the browser is found: a server component's reads happen where the inspector can't see them.
846
+
847
+ The inspector comes with each integration, in dev only, and opens the editor its dev server already
848
+ launches. Under Vite, and so in any framework built on it, TanStack Start among them, that is the one
849
+ `/__open-in-editor` launches: one it finds running, or the command in `LAUNCH_EDITOR`. Under Next.js
850
+ it is the one `/__nextjs_launch-editor` launches, from `REACT_EDITOR` or one it finds running; the
851
+ endpoints are served on a local port of their own, which a rewrite of `/__prosebook/*` sends to.
852
+ Under Bun it is the one `Bun.openInEditor` finds, from `VISUAL`, `EDITOR` or bunfig.toml's
853
+ `[debug] editor`, and the endpoints are the `inspector()` routes [above](#with-bun). Turn it off with
854
+ `inspect: false` in the options of the Vite plugin, `withProsebook` or the Bun plugin.
546
855
 
547
856
  ## Config
548
857
 
@@ -557,6 +866,7 @@ export default defineConfig({
557
866
  lock: 'copy/lock.json',
558
867
  source: 'en',
559
868
  messageFormat: 1, // or 2, for Unicode MessageFormat 2
869
+ isolate: 'rtl', // MF2: isolate placeholders in right-to-left locales; 'always', or false
560
870
  bundled: [], // locales shipped in the main bundle; the rest load on first use
561
871
  pseudo: false, // or 'en-XA'
562
872
  stale: 'use', // or 'fallback'
@@ -576,29 +886,35 @@ export default defineConfig({
576
886
  properNouns: [],
577
887
  pathSeparator: '›',
578
888
  },
889
+ native: [], // Rust, Swift or Kotlin files, Android's res or Apple's .lproj: { language: 'rust', out: 'src/copy.rs', keys: ['tray'] }
579
890
  scan: { dirs: ['src'] },
580
891
  });
581
892
  ```
582
893
 
583
- ## Example
894
+ ## Examples
895
+
896
+ Each is a small shop in English, German and the pseudo-locale, and uses prosebook as an app would,
897
+ from the package. CI generates, lints, typechecks and builds each one, then checks what its build
898
+ serves.
584
899
 
585
- [`examples/react-app`](examples/react-app) is a small shop in English, German, a part-translated
586
- Turkish and the pseudo-locale.
900
+ | Example | |
901
+ | ---------------------------------------------------- | ------------------------------------------------------------------------------------------- |
902
+ | [`examples/vite`](examples/vite) | React with Vite, and a part-translated Turkish. The inspector is on. |
903
+ | [`examples/next`](examples/next) | Next.js: server components read a copy per request, client components the provider's. |
904
+ | [`examples/tanstack-start`](examples/tanstack-start) | TanStack Start, rendered on the server in the request's locale with the Vite plugin. |
905
+ | [`examples/bun`](examples/bun) | `Bun.serve` with a React page, a route that answers in the request's locale, the inspector. |
906
+ | [`examples/node`](examples/node) | No framework and no bundler: a Node script prints a receipt from the generated TypeScript. |
587
907
 
588
908
  ```bash
589
- bun run example
909
+ bun run build # the examples import prosebook from dist
910
+ bun run --cwd examples/next dev
590
911
  ```
591
912
 
592
913
  ## Further
593
914
 
594
915
  Ideas not built yet, roughly in order of value:
595
916
 
596
- 1. **Draft translations with an LLM**: `prosebook translate de --draft`, prompted with `context`,
597
- `kind`, `surface` and `args`, written to a draft file a person reviews before it ships.
598
- 2. **Exchange formats**: XLIFF 2 and JSON export and import for translation tools.
599
- 3. **Per-area splitting**: one chunk per area and locale, so a lazy route loads only its own messages.
600
- 4. **Server rendering helpers**: `Accept-Language` negotiation and a React Server Components reader.
601
- 5. **Native targets**: generate Rust, Swift or Kotlin from the same catalog, for the parts of an app
602
- outside the browser, such as a tray or native menus.
603
- 6. **Variable MF2 options**: `{$price :currency currency=$code}`, compiled to a formatter cached per
604
- value.
917
+ 1. **Exchange formats**: XLIFF 2 and JSON export and import for translation tools.
918
+ 2. **Per-area splitting**: one chunk per area and locale, so a lazy route loads only its own messages.
919
+ 3. **Dates in native targets**: date and time formats taken apart from `Intl` as number formats are,
920
+ so a Rust, Swift or Kotlin message can say when.
package/dist/bun.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { t as Prune } from "./bundle-CpUIWb9j.js";
2
+ import { i as ProsebookConfig } from "./config-CzaJJQ60.js";
3
+ import { BunPlugin } from "bun";
4
+ export interface ProsebookBunOptions {
5
+ /** The config, inline. Without it, `prosebook.config.*` at `root` is read. */
6
+ config?: ProsebookConfig;
7
+ /** The folder the config's paths resolve from, and where its file is found: the working directory when unset. */
8
+ root?: string;
9
+ /**
10
+ * In `Bun.build`, ship only the messages the code names. `strict`, the default, also fails the
11
+ * build on a read whose key it can't see, such as `t(item.key)`. `loose` prunes without that
12
+ * check; `false` ships every message.
13
+ */
14
+ prune?: Prune;
15
+ /** Keys a build always keeps, exactly or as `prefix.*`: for keys built at runtime from data. */
16
+ keep?: string[];
17
+ /** Under the dev server, Alt-click a string on the page to open its entry in the editor. On by default. */
18
+ inspect?: boolean;
19
+ }
20
+ export declare function prosebook(options?: ProsebookBunOptions): BunPlugin;
21
+ export interface InspectorRoutesOptions {
22
+ /** The config, inline. Without it, `prosebook.config.*` at `root` is read. */
23
+ config?: ProsebookConfig;
24
+ /** The folder the config's paths resolve from, and where its file is found: the working directory when unset. */
25
+ root?: string;
26
+ }
27
+ /**
28
+ * The inspector's endpoints as `Bun.serve` routes: `routes: { ...inspector(), '/': index }`. For dev
29
+ * only: they read and write the catalog.
30
+ */
31
+ export declare function inspector(options?: InspectorRoutesOptions): Record<string, (req: Request) => Promise<Response>>;
32
+ declare const _default: BunPlugin;
33
+ export { _default as default };
34
+ //# sourceMappingURL=bun.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bun.d.ts","names":[],"sources":["../src/bun.ts"],"mappings":";;;iBAqCiB;;EAEf,SAAS;;EAET;;;;;;EAMA,QAAQ;;EAER;;EAEA;;wBAQc,UAAU,UAAS,sBAA2B;iBA6C7C;;EAEf,SAAS;;EAET;;;;;;wBAOc,UAAU,UAAS,yBAA8B,gBAAgB,KAAK,YAAY,QAAQ"}
package/dist/bun.js ADDED
@@ -0,0 +1,98 @@
1
+ import { a as installInspector, c as prunedModule, f as watchCatalog, i as hotLocale, l as scanSources, n as HOT_SET, o as keeper, r as beside, t as HOT_INDEX, u as unseenMessage } from "./bundle-DboItAre.js";
2
+ import { n as generate, x as CatalogError } from "./generate-BW3N8EGb.js";
3
+ import { r as loadConfig } from "./config-nWZf9QmF.js";
4
+ import { t as inspectorEndpoints } from "./endpoints-BgG_5Vpm.js";
5
+ import { basename, resolve } from "node:path";
6
+ import { readFileSync } from "node:fs";
7
+ // src/bun.ts
8
+ /**
9
+ * `prosebook/bun`: the catalog as part of a Bun build, or of Bun's dev server.
10
+ *
11
+ * Generates when the plugin is set up. Given to `Bun.build`, it ships only the messages the code
12
+ * names: Bun loads a locale module before it has seen the code that reads it, so the keys come from
13
+ * the source under `scan.dirs` (`tools/bundle.ts`) and each locale loads already pruned. The `bun
14
+ * build` command reads no plugins, so a build that prunes is a script that calls `Bun.build`.
15
+ *
16
+ * Named in bunfig.toml's `[serve.static] plugins`, it serves `Bun.serve`'s bundler instead, which
17
+ * names no entry points: it regenerates whenever a catalog or translation file changes, and each
18
+ * locale module accepts its own update and hands the new messages to every copy, as under Vite. The
19
+ * index starts the inspector on the page too, whose endpoints are `inspector()`'s routes, spread into
20
+ * `Bun.serve`'s; an entry opens in the editor `Bun.openInEditor` finds.
21
+ */
22
+ const inspectorPath = beside(import.meta.url, "inspector");
23
+ const describe = (e) => e instanceof CatalogError ? e.message : e instanceof Error ? e.stack ?? e.message : String(e);
24
+ const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
25
+ function prosebook(options = {}) {
26
+ return {
27
+ name: "prosebook",
28
+ async setup(build) {
29
+ const config = await loadConfig(resolve(options.root ?? process.cwd()), options.config);
30
+ let generated = generate(config);
31
+ const folder = config.out.split(/[\\/]/).map(escape).join("[\\\\/]");
32
+ const filter = new RegExp(`^${folder}[\\\\/][^\\\\/]+\\.[jt]s$`);
33
+ const nameOf = (path) => basename(path).replace(/\.[jt]s$/, "");
34
+ if ("entrypoints" in build.config) {
35
+ const prune = options.prune ?? "strict";
36
+ if (prune === false) return;
37
+ const { refs, unseen } = scanSources(config, prune === "strict");
38
+ if (unseen.length > 0) throw new Error(unseenMessage(unseen, config.root));
39
+ const kept = keeper(refs, options.keep);
40
+ build.onLoad({ filter }, ({ path }) => {
41
+ const entries = generated.entries.get(nameOf(path));
42
+ return entries === void 0 ? void 0 : {
43
+ contents: prunedModule(entries, kept),
44
+ loader: "js"
45
+ };
46
+ });
47
+ return;
48
+ }
49
+ const dev = process.env.NODE_ENV !== "production";
50
+ const inspector = dev && options.inspect !== false ? installInspector(inspectorPath, "/") : "";
51
+ if (dev) watchCatalog(config, () => {
52
+ try {
53
+ generated = generate(config);
54
+ } catch (e) {
55
+ console.error(describe(e));
56
+ }
57
+ });
58
+ build.onLoad({ filter }, ({ path }) => {
59
+ const name = nameOf(path);
60
+ const loader = path.endsWith(".ts") ? "ts" : "js";
61
+ if (generated.entries.has(name)) return {
62
+ contents: readFileSync(path, "utf8") + hotLocale(name),
63
+ loader
64
+ };
65
+ return name === "index" ? {
66
+ contents: HOT_SET + inspector + readFileSync(path, "utf8") + HOT_INDEX,
67
+ loader
68
+ } : void 0;
69
+ });
70
+ }
71
+ };
72
+ }
73
+ /**
74
+ * The inspector's endpoints as `Bun.serve` routes: `routes: { ...inspector(), '/': index }`. For dev
75
+ * only: they read and write the catalog.
76
+ */
77
+ function inspector(options = {}) {
78
+ const handle = inspectorEndpoints(loadConfig(resolve(options.root ?? process.cwd()), options.config), { open: ({ file, line, column }) => {
79
+ try {
80
+ Bun.openInEditor(file, {
81
+ line,
82
+ column
83
+ });
84
+ return new Response(null, { status: 204 });
85
+ } catch (e) {
86
+ return new Response(`${e instanceof Error ? e.message : String(e)}: set VISUAL or EDITOR to the editor's command`, { status: 500 });
87
+ }
88
+ } });
89
+ const route = async (req) => await handle(req) ?? new Response(null, { status: 404 });
90
+ return {
91
+ "/__prosebook/open": route,
92
+ "/__prosebook/entry": route
93
+ };
94
+ }
95
+ var bun_default = prosebook();
96
+ export { bun_default as default, inspector, prosebook };
97
+
98
+ //# sourceMappingURL=bun.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bun.js","names":[],"sources":["../src/bun.ts"],"sourcesContent":["/**\n * `prosebook/bun`: the catalog as part of a Bun build, or of Bun's dev server.\n *\n * Generates when the plugin is set up. Given to `Bun.build`, it ships only the messages the code\n * names: Bun loads a locale module before it has seen the code that reads it, so the keys come from\n * the source under `scan.dirs` (`tools/bundle.ts`) and each locale loads already pruned. The `bun\n * build` command reads no plugins, so a build that prunes is a script that calls `Bun.build`.\n *\n * Named in bunfig.toml's `[serve.static] plugins`, it serves `Bun.serve`'s bundler instead, which\n * names no entry points: it regenerates whenever a catalog or translation file changes, and each\n * locale module accepts its own update and hands the new messages to every copy, as under Vite. The\n * index starts the inspector on the page too, whose endpoints are `inspector()`'s routes, spread into\n * `Bun.serve`'s; an entry opens in the editor `Bun.openInEditor` finds.\n */\nimport { readFileSync } from 'node:fs';\nimport { basename, resolve } from 'node:path';\n\nimport type { BunPlugin } from 'bun';\n\nimport {\n beside,\n HOT_INDEX,\n HOT_SET,\n hotLocale,\n installInspector,\n keeper,\n prunedModule,\n scanSources,\n unseenMessage,\n watchCatalog,\n type Prune,\n} from './tools/bundle.ts';\nimport { CatalogError } from './tools/catalog.ts';\nimport { loadConfig, type ProsebookConfig } from './tools/config.ts';\nimport { inspectorEndpoints } from './tools/endpoints.ts';\nimport { generate } from './tools/generate.ts';\n\nexport interface ProsebookBunOptions {\n /** The config, inline. Without it, `prosebook.config.*` at `root` is read. */\n config?: ProsebookConfig;\n /** The folder the config's paths resolve from, and where its file is found: the working directory when unset. */\n root?: string;\n /**\n * In `Bun.build`, ship only the messages the code names. `strict`, the default, also fails the\n * build on a read whose key it can't see, such as `t(item.key)`. `loose` prunes without that\n * check; `false` ships every message.\n */\n prune?: Prune;\n /** Keys a build always keeps, exactly or as `prefix.*`: for keys built at runtime from data. */\n keep?: string[];\n /** Under the dev server, Alt-click a string on the page to open its entry in the editor. On by default. */\n inspect?: boolean;\n}\n\nconst inspectorPath = beside(import.meta.url, 'inspector');\n\nconst describe = (e: unknown) => (e instanceof CatalogError ? e.message : e instanceof Error ? (e.stack ?? e.message) : String(e));\nconst escape = (s: string) => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\nexport function prosebook(options: ProsebookBunOptions = {}): BunPlugin {\n return {\n name: 'prosebook',\n async setup(build) {\n const config = await loadConfig(resolve(options.root ?? process.cwd()), options.config);\n let generated = generate(config);\n // The generated modules, by path as Bun gives it: either separator on Windows.\n const folder = config.out.split(/[\\\\/]/).map(escape).join('[\\\\\\\\/]');\n const filter = new RegExp(`^${folder}[\\\\\\\\/][^\\\\\\\\/]+\\\\.[jt]s$`);\n const nameOf = (path: string) => basename(path).replace(/\\.[jt]s$/, '');\n\n if ('entrypoints' in build.config) {\n const prune = options.prune ?? 'strict';\n if (prune === false) return;\n const { refs, unseen } = scanSources(config, prune === 'strict');\n if (unseen.length > 0) throw new Error(unseenMessage(unseen, config.root));\n const kept = keeper(refs, options.keep);\n build.onLoad({ filter }, ({ path }) => {\n const entries = generated.entries.get(nameOf(path));\n return entries === undefined ? undefined : { contents: prunedModule(entries, kept), loader: 'js' };\n });\n return;\n }\n\n const dev = process.env.NODE_ENV !== 'production';\n const inspector = dev && options.inspect !== false ? installInspector(inspectorPath, '/') : '';\n if (dev) {\n watchCatalog(config, () => {\n try {\n generated = generate(config);\n } catch (e) {\n console.error(describe(e));\n }\n });\n }\n build.onLoad({ filter }, ({ path }) => {\n const name = nameOf(path);\n const loader = path.endsWith('.ts') ? 'ts' : 'js';\n if (generated.entries.has(name)) return { contents: readFileSync(path, 'utf8') + hotLocale(name), loader };\n return name === 'index' ? { contents: HOT_SET + inspector + readFileSync(path, 'utf8') + HOT_INDEX, loader } : undefined;\n });\n },\n };\n}\n\nexport interface InspectorRoutesOptions {\n /** The config, inline. Without it, `prosebook.config.*` at `root` is read. */\n config?: ProsebookConfig;\n /** The folder the config's paths resolve from, and where its file is found: the working directory when unset. */\n root?: string;\n}\n\n/**\n * The inspector's endpoints as `Bun.serve` routes: `routes: { ...inspector(), '/': index }`. For dev\n * only: they read and write the catalog.\n */\nexport function inspector(options: InspectorRoutesOptions = {}): Record<string, (req: Request) => Promise<Response>> {\n const handle = inspectorEndpoints(loadConfig(resolve(options.root ?? process.cwd()), options.config), {\n open: ({ file, line, column }) => {\n try {\n Bun.openInEditor(file, { line, column });\n return new Response(null, { status: 204 });\n } catch (e) {\n return new Response(`${e instanceof Error ? e.message : String(e)}: set VISUAL or EDITOR to the editor's command`, { status: 500 });\n }\n },\n });\n const route = async (req: Request) => (await handle(req)) ?? new Response(null, { status: 404 });\n return { '/__prosebook/open': route, '/__prosebook/entry': route };\n}\n\n// bunfig.toml names the module, and Bun takes its default export as the plugin.\nexport default prosebook();\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAsDA,MAAM,gBAAgB,OAAO,YAAY,KAAK,WAAW;AAEzD,MAAM,YAAY,MAAgB,aAAa,eAAe,EAAE,UAAU,aAAa,QAAS,EAAE,SAAS,EAAE,UAAW,OAAO,CAAC;AAChI,MAAM,UAAU,MAAc,EAAE,QAAQ,uBAAuB,MAAM;AAErE,SAAgB,UAAU,UAA+B,CAAC,GAAc;CACtE,OAAO;EACL,MAAM;EACN,MAAM,MAAM,OAAO;GACjB,MAAM,SAAS,MAAM,WAAW,QAAQ,QAAQ,QAAQ,QAAQ,IAAI,CAAC,GAAG,QAAQ,MAAM;GACtF,IAAI,YAAY,SAAS,MAAM;GAE/B,MAAM,SAAS,OAAO,IAAI,MAAM,OAAO,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,KAAK,SAAS;GACnE,MAAM,SAAS,IAAI,OAAO,IAAI,OAAO,0BAA0B;GAC/D,MAAM,UAAU,SAAiB,SAAS,IAAI,CAAC,CAAC,QAAQ,YAAY,EAAE;GAEtE,IAAI,iBAAiB,MAAM,QAAQ;IACjC,MAAM,QAAQ,QAAQ,SAAS;IAC/B,IAAI,UAAU,OAAO;IACrB,MAAM,EAAE,MAAM,WAAW,YAAY,QAAQ,UAAU,QAAQ;IAC/D,IAAI,OAAO,SAAS,GAAG,MAAM,IAAI,MAAM,cAAc,QAAQ,OAAO,IAAI,CAAC;IACzE,MAAM,OAAO,OAAO,MAAM,QAAQ,IAAI;IACtC,MAAM,OAAO,EAAE,OAAO,IAAI,EAAE,WAAW;KACrC,MAAM,UAAU,UAAU,QAAQ,IAAI,OAAO,IAAI,CAAC;KAClD,OAAO,YAAY,KAAA,IAAY,KAAA,IAAY;MAAE,UAAU,aAAa,SAAS,IAAI;MAAG,QAAQ;KAAK;IACnG,CAAC;IACD;GACF;GAEA,MAAM,MAAM,QAAQ,IAAI,aAAa;GACrC,MAAM,YAAY,OAAO,QAAQ,YAAY,QAAQ,iBAAiB,eAAe,GAAG,IAAI;GAC5F,IAAI,KACF,aAAa,cAAc;IACzB,IAAI;KACF,YAAY,SAAS,MAAM;IAC7B,SAAS,GAAG;KACV,QAAQ,MAAM,SAAS,CAAC,CAAC;IAC3B;GACF,CAAC;GAEH,MAAM,OAAO,EAAE,OAAO,IAAI,EAAE,WAAW;IACrC,MAAM,OAAO,OAAO,IAAI;IACxB,MAAM,SAAS,KAAK,SAAS,KAAK,IAAI,OAAO;IAC7C,IAAI,UAAU,QAAQ,IAAI,IAAI,GAAG,OAAO;KAAE,UAAU,aAAa,MAAM,MAAM,IAAI,UAAU,IAAI;KAAG;IAAO;IACzG,OAAO,SAAS,UAAU;KAAE,UAAU,UAAU,YAAY,aAAa,MAAM,MAAM,IAAI;KAAW;IAAO,IAAI,KAAA;GACjH,CAAC;EACH;CACF;AACF;;;;;AAaA,SAAgB,UAAU,UAAkC,CAAC,GAAwD;CACnH,MAAM,SAAS,mBAAmB,WAAW,QAAQ,QAAQ,QAAQ,QAAQ,IAAI,CAAC,GAAG,QAAQ,MAAM,GAAG,EACpG,OAAO,EAAE,MAAM,MAAM,aAAa;EAChC,IAAI;GACF,IAAI,aAAa,MAAM;IAAE;IAAM;GAAO,CAAC;GACvC,OAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;EAC3C,SAAS,GAAG;GACV,OAAO,IAAI,SAAS,GAAG,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,EAAE,iDAAiD,EAAE,QAAQ,IAAI,CAAC;EACpI;CACF,EACF,CAAC;CACD,MAAM,QAAQ,OAAO,QAAkB,MAAM,OAAO,GAAG,KAAM,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;CAC/F,OAAO;EAAE,qBAAqB;EAAO,sBAAsB;CAAM;AACnE;AAGA,IAAA,cAAe,UAAU"}
@@ -0,0 +1,6 @@
1
+ import "./compile-ClK9htKr.js";
2
+ import "./config-CzaJJQ60.js";
3
+ /** How a build prunes: `strict` also fails on a read whose key it can't see. */
4
+ type Prune = 'strict' | 'loose' | false;
5
+ export { Prune as t };
6
+ //# sourceMappingURL=bundle-CpUIWb9j.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle-CpUIWb9j.d.ts","names":[],"sources":["../src/tools/bundle.ts"],"mappings":";;;KAiBY"}