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.
- package/README.md +345 -29
- package/dist/bun.d.ts +34 -0
- package/dist/bun.d.ts.map +1 -0
- package/dist/bun.js +98 -0
- package/dist/bun.js.map +1 -0
- package/dist/bundle-CpUIWb9j.d.ts +6 -0
- package/dist/bundle-CpUIWb9j.d.ts.map +1 -0
- package/dist/bundle-DboItAre.js +111 -0
- package/dist/bundle-DboItAre.js.map +1 -0
- package/dist/cli.js +51 -56
- package/dist/cli.js.map +1 -1
- package/dist/compile-ClK9htKr.d.ts +16 -0
- package/dist/compile-ClK9htKr.d.ts.map +1 -0
- package/dist/{config-_4HfnJQu.d.ts → config-CzaJJQ60.d.ts} +75 -2
- package/dist/config-CzaJJQ60.d.ts.map +1 -0
- package/dist/{config-D--RITk2.js → config-nWZf9QmF.js} +42 -212
- package/dist/config-nWZf9QmF.js.map +1 -0
- package/dist/config.d.ts +2 -2
- package/dist/config.js +1 -1
- package/dist/{copy-DXnzXsJa.d.ts → copy-B3V_QRkT.d.ts} +20 -7
- package/dist/copy-B3V_QRkT.d.ts.map +1 -0
- package/dist/copy-Bo_FIoLy.js +154 -0
- package/dist/copy-Bo_FIoLy.js.map +1 -0
- package/dist/endpoints-BgG_5Vpm.js +96 -0
- package/dist/endpoints-BgG_5Vpm.js.map +1 -0
- package/dist/generate-BW3N8EGb.js +5488 -0
- package/dist/generate-BW3N8EGb.js.map +1 -0
- package/dist/helpers-BWGcD0kS.js +96 -0
- package/dist/helpers-BWGcD0kS.js.map +1 -0
- package/dist/icu-Dej0mNsF.js +218 -0
- package/dist/icu-Dej0mNsF.js.map +1 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -188
- package/dist/inspector.d.ts +4 -1
- package/dist/inspector.d.ts.map +1 -1
- package/dist/inspector.js +7 -2
- package/dist/inspector.js.map +1 -1
- package/dist/locale-DpQA1pqH.js +70 -0
- package/dist/locale-DpQA1pqH.js.map +1 -0
- package/dist/next-loader.d.ts +11 -0
- package/dist/next-loader.d.ts.map +1 -0
- package/dist/next-loader.js +17 -0
- package/dist/next-loader.js.map +1 -0
- package/dist/next.d.ts +30 -0
- package/dist/next.d.ts.map +1 -0
- package/dist/next.js +160 -0
- package/dist/next.js.map +1 -0
- package/dist/react/index.d.ts +3 -3
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react/server.d.ts +20 -0
- package/dist/react/server.d.ts.map +1 -0
- package/dist/react/server.js +34 -0
- package/dist/react/server.js.map +1 -0
- package/dist/{sarif-CI8HOokm.js → sarif-B5nrMqrT.js} +103 -87
- package/dist/sarif-B5nrMqrT.js.map +1 -0
- package/dist/scan-Bx0_yDSi.js +387 -0
- package/dist/scan-Bx0_yDSi.js.map +1 -0
- package/dist/tools.d.ts +83 -90
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +5 -4
- package/dist/vite.d.ts +3 -2
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +29 -109
- package/dist/vite.js.map +1 -1
- package/package.json +37 -4
- package/src/bun.ts +132 -0
- package/src/cli.ts +60 -58
- package/src/index.ts +1 -1
- package/src/inspector.ts +8 -3
- package/src/next-loader.ts +16 -0
- package/src/next.ts +188 -0
- package/src/react/index.tsx +6 -6
- package/src/react/server.ts +37 -0
- package/src/runtime/copy.ts +22 -3
- package/src/runtime/helpers.ts +54 -18
- package/src/runtime/locale.ts +19 -0
- package/src/runtime/types.ts +21 -6
- package/src/tools/bundle.ts +131 -0
- package/src/tools/catalog.ts +2 -2
- package/src/tools/compile.ts +60 -23
- package/src/tools/config.ts +57 -0
- package/src/tools/endpoints.ts +121 -0
- package/src/tools/entry.ts +77 -0
- package/src/tools/generate.ts +2 -0
- package/src/tools/icu.ts +63 -7
- package/src/tools/index.ts +6 -3
- package/src/tools/lock.ts +7 -0
- package/src/tools/mf1.ts +21 -7
- package/src/tools/mf2.ts +249 -57
- package/src/tools/migrate.ts +3 -2
- package/src/tools/native/android.ts +201 -0
- package/src/tools/native/apple.ts +139 -0
- package/src/tools/native/emit.ts +87 -0
- package/src/tools/native/index.ts +48 -0
- package/src/tools/native/kotlin.ts +269 -0
- package/src/tools/native/model.ts +292 -0
- package/src/tools/native/number.ts +210 -0
- package/src/tools/native/plurals.ts +103 -0
- package/src/tools/native/printf.ts +95 -0
- package/src/tools/native/rust.ts +367 -0
- package/src/tools/native/swift.ts +326 -0
- package/src/tools/syntax.ts +3 -3
- package/src/tools/todo.ts +118 -0
- package/src/vite.ts +28 -114
- package/dist/config-D--RITk2.js.map +0 -1
- package/dist/config-_4HfnJQu.d.ts.map +0 -1
- package/dist/copy-DXnzXsJa.d.ts.map +0 -1
- package/dist/helpers-BPuPGjQP.js +0 -61
- package/dist/helpers-BPuPGjQP.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/sarif-CI8HOokm.js.map +0 -1
- package/dist/scan-Ch2BfIUD.js +0 -2561
- 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
|
|
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
|
|
112
|
-
|
|
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.
|
|
203
|
-
|
|
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.
|
|
208
|
-
|
|
209
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
545
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
586
|
-
|
|
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
|
|
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. **
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
package/dist/bun.js.map
ADDED
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"bundle-CpUIWb9j.d.ts","names":[],"sources":["../src/tools/bundle.ts"],"mappings":";;;KAiBY"}
|