prosebook 0.1.0 → 0.3.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 +311 -57
- 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-DboItAre.js +111 -0
- package/dist/bundle-DboItAre.js.map +1 -0
- package/dist/bundle-KD-DiiFa.d.ts +6 -0
- package/dist/bundle-KD-DiiFa.d.ts.map +1 -0
- package/dist/cli.js +51 -56
- package/dist/cli.js.map +1 -1
- package/dist/compile-xF3F4oIp.d.ts +16 -0
- package/dist/compile-xF3F4oIp.d.ts.map +1 -0
- package/dist/{config-CxVMn19f.js → config-BPSmoYel.js} +82 -196
- package/dist/config-BPSmoYel.js.map +1 -0
- package/dist/{config-CF7HziDa.d.ts → config-DdMkKt_N.d.ts} +85 -7
- package/dist/config-DdMkKt_N.d.ts.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-CnUfCkIk.js +96 -0
- package/dist/endpoints-CnUfCkIk.js.map +1 -0
- package/dist/generate-9pR3i5Bj.js +5173 -0
- package/dist/generate-9pR3i5Bj.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-PkPvsB07.js → sarif-CWyTAQ0y.js} +103 -87
- package/dist/sarif-CWyTAQ0y.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 -64
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +5 -4
- package/dist/vite.d.ts +14 -4
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +60 -114
- 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 +61 -13
- package/src/tools/config.ts +58 -3
- package/src/tools/endpoints.ts +121 -0
- package/src/tools/entry.ts +77 -0
- package/src/tools/generate.ts +32 -32
- package/src/tools/icu.ts +93 -8
- package/src/tools/index.ts +20 -4
- package/src/tools/lint.ts +49 -12
- 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/emit.ts +87 -0
- package/src/tools/native/index.ts +36 -0
- package/src/tools/native/kotlin.ts +269 -0
- package/src/tools/native/model.ts +280 -0
- package/src/tools/native/number.ts +210 -0
- package/src/tools/native/plurals.ts +103 -0
- package/src/tools/native/rust.ts +367 -0
- package/src/tools/native/swift.ts +326 -0
- package/src/tools/scan.ts +134 -4
- package/src/tools/syntax.ts +3 -3
- package/src/tools/todo.ts +118 -0
- package/src/vite.ts +74 -116
- package/dist/config-CF7HziDa.d.ts.map +0 -1
- package/dist/config-CxVMn19f.js.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-PkPvsB07.js.map +0 -1
- package/dist/scan-B0tZ7y-D.js +0 -2447
- package/dist/scan-B0tZ7y-D.js.map +0 -1
package/README.md
CHANGED
|
@@ -32,15 +32,16 @@ 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
|
|
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.
|
|
36
37
|
- **Written well.** An optional lint holds the words to a writing guide: word budgets per kind of
|
|
37
38
|
string, periods, sentence case, banned filler, and `{count} files` outside a plural. Plugins add your
|
|
38
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.
|
|
39
42
|
|
|
40
43
|
## Quick start
|
|
41
44
|
|
|
42
|
-
Not on npm yet: build it here (`bun run build`) and add it by path.
|
|
43
|
-
|
|
44
45
|
```bash
|
|
45
46
|
bun add prosebook
|
|
46
47
|
```
|
|
@@ -66,6 +67,66 @@ import { prosebook } from 'prosebook/vite';
|
|
|
66
67
|
export default defineConfig({ plugins: [react(), prosebook()] });
|
|
67
68
|
```
|
|
68
69
|
|
|
70
|
+
Several apps that share one catalog each point the plugin at it: `prosebook({ root: '../../packages/copy' })`
|
|
71
|
+
reads the config there and resolves its paths from there, so every app generates the same files.
|
|
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
|
+
|
|
69
130
|
### In React
|
|
70
131
|
|
|
71
132
|
```tsx
|
|
@@ -106,8 +167,133 @@ document.title = copy.t('app.title');
|
|
|
106
167
|
copy.subscribe(() => render()); // after a locale switch, and in dev after a catalog edit
|
|
107
168
|
```
|
|
108
169
|
|
|
109
|
-
Set `language: 'js'` in the config to generate `.js` modules with a `.d.ts` beside the index
|
|
110
|
-
|
|
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 and Kotlin
|
|
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`.
|
|
111
297
|
|
|
112
298
|
## The catalog
|
|
113
299
|
|
|
@@ -126,7 +312,8 @@ One file per area, `copy/catalog/<area>.yaml`. A key's first segment names its f
|
|
|
126
312
|
| `comment` | A change asked of whoever edits the entry next. A warning until it is made and the comment deleted. |
|
|
127
313
|
| `remove` | `true` asks for the entry to go, with its translations and the code that reads it. A warning until then. |
|
|
128
314
|
|
|
129
|
-
`key: message` is the shorthand for an entry with nothing to say about itself (set `require: []
|
|
315
|
+
`key: message` is the shorthand for an entry with nothing to say about itself (set `require: []`;
|
|
316
|
+
the schema offers it only then).
|
|
130
317
|
Kinds and surfaces are configurable; `prosebook generate` writes `catalog.schema.json` beside the
|
|
131
318
|
catalog, so an editor with the YAML language server completes and explains every field:
|
|
132
319
|
|
|
@@ -146,12 +333,14 @@ en: '{mode, select, dark {Dark} other {Light}}' # mode: 'dark' | (string & {})
|
|
|
146
333
|
en: '{size, number, ::.0 unit/megabyte}' # 1.4 MB
|
|
147
334
|
en: '{total, number, ::currency/EUR}' # €9.00
|
|
148
335
|
en: '{when, date, medium} at {when, time, short}' # Oct 28, 2026 at 2:05 PM
|
|
336
|
+
en: '{when, date, ::yMMMdjmm}' # Oct 28, 2026, 2:05 PM; 28. Okt. 2026, 14:05 in German
|
|
149
337
|
en: 'By checking out you agree to our <link>terms</link>.' # a tag: an element inside the sentence
|
|
150
338
|
```
|
|
151
339
|
|
|
152
340
|
A placeholder holds a number, a name or something a person typed, never a translated fragment: a
|
|
153
|
-
word that changes with the sentence belongs in a `plural` or `select` arm. A skeleton the
|
|
154
|
-
|
|
341
|
+
word that changes with the sentence belongs in a `plural` or `select` arm. A skeleton's `j` is the
|
|
342
|
+
hour as each locale writes it, 12 or 24. A skeleton the formatter would silently drop
|
|
343
|
+
(`::compact-shortt`, a date's `J`) fails the build instead of reaching the screen unformatted.
|
|
155
344
|
|
|
156
345
|
**Tags** keep a sentence with a link or bold run inside it as one entry, so a translator can move the
|
|
157
346
|
element. `t()` renders a tag as its text; `rich()` and `<Text>` render it through a function. Tags
|
|
@@ -194,14 +383,40 @@ en: |-
|
|
|
194
383
|
|
|
195
384
|
A message with `.match` holds whole sentences per variant, so a translator never stitches a sentence
|
|
196
385
|
from an arm and the text around it. Several selectors pick the variant MF2's rules pick: the best
|
|
197
|
-
match on the first, then the next, an exact number beating its plural category.
|
|
198
|
-
|
|
386
|
+
match on the first, then the next, an exact number beating its plural category. A number is matched
|
|
387
|
+
as it's shown: under `.input {$n :number minimumFractionDigits=1}`, 1 shows as "1.0", which English
|
|
388
|
+
counts as `other`. `:integer` matches the value rounded, and `:percent` matches it times 100.
|
|
389
|
+
`:offset` covers MF1's plural offset.
|
|
199
390
|
|
|
200
391
|
The functions are MF2's defaults: `:string`, `:number`, `:integer`, `:percent`, `:currency`, `:unit`,
|
|
201
392
|
`:offset`, and the date and time functions `:date`, `:time` and `:datetime`, which are still a draft
|
|
202
|
-
in the spec and follow it as the `messageformat` package does.
|
|
203
|
-
|
|
204
|
-
|
|
393
|
+
in the spec and follow it as the `messageformat` package does. `.match` selects with `:number`,
|
|
394
|
+
`:integer`, `:percent` or `:string`.
|
|
395
|
+
|
|
396
|
+
**Text direction.** In a locale written right to left, each placeholder is wrapped in Unicode
|
|
397
|
+
isolation marks, so a value written the other way can't reorder the text around it. Without them, a
|
|
398
|
+
Latin name in a Hebrew sentence can pull the punctuation after it to the wrong side. `u:dir` on an
|
|
399
|
+
expression picks its direction (`{$name :string u:dir=ltr}`) and asks for isolation in any locale.
|
|
400
|
+
`isolate: 'always'` in the config also isolates every string placeholder in left-to-right locales,
|
|
401
|
+
as the spec's default does. `isolate: false` turns it off.
|
|
402
|
+
|
|
403
|
+
**Options from arguments.** An option can take its value from an argument, so one message formats
|
|
404
|
+
whatever currency the order is in:
|
|
405
|
+
|
|
406
|
+
```yaml
|
|
407
|
+
en: 'Total: {$price :currency currency=$code}' # t('cart.total', { price: 9, code: 'USD' }): $9.00
|
|
408
|
+
en: 'Due {$when :time timeZone=$tz}'
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
The argument is typed with the rest (`code: string`; `currencyDisplay=$d` takes only the values the
|
|
412
|
+
option does), and a translation must use it too. These options can come from an argument: `currency`,
|
|
413
|
+
`currencyDisplay`, `currencySign`, `unit`, `unitDisplay`, `signDisplay`, `fractionDigits` and the
|
|
414
|
+
other digit counts, `timeZone` and `calendar`. The rest, such as a date's `fields` or a number's
|
|
415
|
+
`select`, decide what the formatter is, so they're written in the message and the message says so.
|
|
416
|
+
Literal options are still settled at build time. The ones from arguments are merged in as the
|
|
417
|
+
message renders, and each locale keeps one formatter per distinct value. A value `Intl` refuses,
|
|
418
|
+
such as `code: 'euro'`, throws a `RangeError` there. MF1 has no way to say this, so `migrate 1`
|
|
419
|
+
stops at such a message.
|
|
205
420
|
|
|
206
421
|
**Switching.** `prosebook migrate 2` rewrites the catalog and every translation in MF2, and
|
|
207
422
|
`prosebook migrate 1` goes back. It's a dry run unless `--write`; then set `messageFormat` in the
|
|
@@ -231,7 +446,8 @@ still hold:
|
|
|
231
446
|
|
|
232
447
|
```bash
|
|
233
448
|
prosebook status # coverage and stale counts per locale
|
|
234
|
-
prosebook todo de
|
|
449
|
+
prosebook todo de > de.yaml # missing and stale entries, with context, ready to translate
|
|
450
|
+
prosebook import de de.yaml # each filled-in entry into its file, checked first
|
|
235
451
|
prosebook accept de cart.items
|
|
236
452
|
```
|
|
237
453
|
|
|
@@ -246,6 +462,7 @@ de 13/14 92% 1 2
|
|
|
246
462
|
|
|
247
463
|
$ prosebook todo de
|
|
248
464
|
# The button on a product that adds one to the cart.
|
|
465
|
+
# kind: action, surface: action
|
|
249
466
|
# en: Add one to cart
|
|
250
467
|
# stale, the source changed: Add {+one +}to cart
|
|
251
468
|
cart.add: In den Warenkorb
|
|
@@ -253,6 +470,23 @@ cart.add: In den Warenkorb
|
|
|
253
470
|
|
|
254
471
|
Stale translations still show by default; `stale: 'fallback'` shows the source instead.
|
|
255
472
|
|
|
473
|
+
**Translating with a model.** prosebook calls no model itself. `prosebook todo de --prompt` prints
|
|
474
|
+
the same entries as a prompt for whichever model you use: how to reply, the plural forms German
|
|
475
|
+
takes, what each kind of string and surface asks for, and how to treat a stale entry. Hand it to the
|
|
476
|
+
model and give its reply to `import`, as a file or on stdin:
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
prosebook todo de --prompt | pbcopy # paste into a chat, or hand it to an agent
|
|
480
|
+
prosebook import de reply.md # the YAML block is read out of the reply
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
`import` checks every entry as the whole catalog before writing it. One whose placeholders, tags
|
|
484
|
+
or syntax don't hold is left out with its fix, which you can give back to the model, and the rest
|
|
485
|
+
are written. A stale translation given back unchanged is accepted as still right. The imported
|
|
486
|
+
translations are then linted, as a person's would be. An agent working in the repository needs none
|
|
487
|
+
of this: it can read `prosebook todo de`, write the translation files itself, and run
|
|
488
|
+
`prosebook check`.
|
|
489
|
+
|
|
256
490
|
**Pseudo-locale.** `pseudo: 'en-XA'` generates `⟦Ýóóúúŕ çááŕţ⟧` from the source. Switch to it and
|
|
257
491
|
any plain text on screen is hardcoded, a missing bracket is clipped text, and brackets inside
|
|
258
492
|
brackets are a sentence spliced from fragments.
|
|
@@ -290,8 +524,9 @@ writes them, in the case of the words they replace, wherever in the message they
|
|
|
290
524
|
checked as the whole catalog first, and nothing is written if any would break it. Only the message
|
|
291
525
|
changes: a `key: message` entry stays one, and quoting and comments elsewhere stay as they were.
|
|
292
526
|
|
|
293
|
-
`strict` adds sentence case, contractions, em-dashes, `…`, American spelling
|
|
294
|
-
|
|
527
|
+
`strict` adds sentence case, contractions, em-dashes, `…`, American spelling, paths written with one
|
|
528
|
+
separator (`Settings › General`, not `→` or `>`; set `pathSeparator`) and a list of filler and
|
|
529
|
+
marketing words. Add your own `banned`, `spelling` and `concepts` (one word per concept:
|
|
295
530
|
`{ tablet: 'device' }`), and `properNouns` for sentence case. Where a rule is wrong about an
|
|
296
531
|
entry, say why:
|
|
297
532
|
|
|
@@ -307,10 +542,16 @@ legal.privacy:
|
|
|
307
542
|
A reason that is a shrug (`Intentional.`) fails, and so does an exemption the entry no longer needs.
|
|
308
543
|
An exemption covers the entry in every locale, and is needed while any of them needs it.
|
|
309
544
|
|
|
545
|
+
Where a rule is wrong about a whole area, set its severity there. A key's first segment is its area:
|
|
546
|
+
|
|
547
|
+
```ts
|
|
548
|
+
lint: { areas: { native: { 'sentence-case': 'off' } } }, // menu bars and trays write Title Case
|
|
549
|
+
```
|
|
550
|
+
|
|
310
551
|
**Lint plugins.** A plugin's rules are named `<plugin>/<rule>` in `lint.rules` and `allow`. A rule
|
|
311
552
|
is given the entry and its readings: `primary` takes every `other` arm, `variants` holds one reading
|
|
312
553
|
per plural or select arm, and `skeleton` only the words every arm carries. Placeholders and verbatim
|
|
313
|
-
tags (`code`, `kbd`, ...) read as `HOLE`. `on` limits a rule to kinds or
|
|
554
|
+
tags (`code`, `kbd`, ...) read as `HOLE`. `on` limits a rule to kinds, surfaces or areas, and says which
|
|
314
555
|
locales it reads: the source when unset, `'all'`, or a list of languages (`['de']` covers `de-CH`).
|
|
315
556
|
A rule is then given the `locale`, that locale's `ast` and `text`, and the `source` readings to
|
|
316
557
|
compare with. `recommended` is its severity under that preset (off when unset), `strict` under
|
|
@@ -484,13 +725,22 @@ the singular is the plural less its `s`, `es` or `ies`.
|
|
|
484
725
|
## Pruning
|
|
485
726
|
|
|
486
727
|
The Vite plugin replaces each locale module in a build with only the messages some module
|
|
487
|
-
references.
|
|
728
|
+
references. Next's Turbopack and Bun load a locale before the code that reads it, so their
|
|
729
|
+
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;
|
|
488
730
|
``t(`status.${state}`)`` and `t('status.' + state)` keep every key under `status.`. For keys built
|
|
489
731
|
some other way, list them: `prosebook({ keep: ['plan.*'] })`.
|
|
490
732
|
|
|
733
|
+
A message the build leaves out shows as its key, so by default a build fails on any read whose key
|
|
734
|
+
it can't see: `t(item.key)`, ``rich(`${area}.title`)``, `<Text id={id} />`. Write the key at the read,
|
|
735
|
+
or, where every key it reads is written elsewhere (in an array, say) or listed in `keep`, mark the
|
|
736
|
+
line with `prosebook-ignore`. Reads are known by name: `t` and `rich` alone or as methods, the names
|
|
737
|
+
they're destructured to (`const { rich: tRich } = copy`) in any module, and `<Text>` from
|
|
738
|
+
`prosebook/react`. `prune: 'loose'` prunes without the check, and `prune: false` ships everything.
|
|
739
|
+
`prune` and `keep` are options of each integration.
|
|
740
|
+
|
|
491
741
|
## Finding a string's entry
|
|
492
742
|
|
|
493
|
-
In dev, hold Alt (Option on a Mac) and point at text on the page: the
|
|
743
|
+
In dev, hold Alt (Option on a Mac) and point at text on the page: the inspector outlines the
|
|
494
744
|
string and names its key and the locale it came from. Alt-click opens its entry in your editor: the
|
|
495
745
|
translation for a translated string, the catalog entry for one that fell back. Alt-Shift-click opens
|
|
496
746
|
the catalog entry either way. Save, and the string changes on the page, which keeps its locale and
|
|
@@ -520,10 +770,17 @@ Nothing is added to the page to make this work. In dev the runtime reports each
|
|
|
520
770
|
and the inspector matches the text under the pointer back to it, so `t()`, `<Text>`, a sentence with
|
|
521
771
|
a link inside it and readable attributes (`placeholder`, `title`, `aria-label`, `alt`) all work. Text
|
|
522
772
|
the code changes after reading it (`t('…').toUpperCase()`) can't be traced. Where two keys read the
|
|
523
|
-
same text, the label says so and the click opens the one read last.
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
773
|
+
same text, the label says so and the click opens the one read last. Under Next.js, only text read in
|
|
774
|
+
the browser is found: a server component's reads happen where the inspector can't see them.
|
|
775
|
+
|
|
776
|
+
The inspector comes with each integration, in dev only, and opens the editor its dev server already
|
|
777
|
+
launches. Under Vite, and so in any framework built on it, TanStack Start among them, that is the one
|
|
778
|
+
`/__open-in-editor` launches: one it finds running, or the command in `LAUNCH_EDITOR`. Under Next.js
|
|
779
|
+
it is the one `/__nextjs_launch-editor` launches, from `REACT_EDITOR` or one it finds running; the
|
|
780
|
+
endpoints are served on a local port of their own, which a rewrite of `/__prosebook/*` sends to.
|
|
781
|
+
Under Bun it is the one `Bun.openInEditor` finds, from `VISUAL`, `EDITOR` or bunfig.toml's
|
|
782
|
+
`[debug] editor`, and the endpoints are the `inspector()` routes [above](#with-bun). Turn it off with
|
|
783
|
+
`inspect: false` in the options of the Vite plugin, `withProsebook` or the Bun plugin.
|
|
527
784
|
|
|
528
785
|
## Config
|
|
529
786
|
|
|
@@ -538,6 +795,7 @@ export default defineConfig({
|
|
|
538
795
|
lock: 'copy/lock.json',
|
|
539
796
|
source: 'en',
|
|
540
797
|
messageFormat: 1, // or 2, for Unicode MessageFormat 2
|
|
798
|
+
isolate: 'rtl', // MF2: isolate placeholders in right-to-left locales; 'always', or false
|
|
541
799
|
bundled: [], // locales shipped in the main bundle; the rest load on first use
|
|
542
800
|
pseudo: false, // or 'en-XA'
|
|
543
801
|
stale: 'use', // or 'fallback'
|
|
@@ -546,50 +804,46 @@ export default defineConfig({
|
|
|
546
804
|
require: ['context'],
|
|
547
805
|
kinds: {/* name, action, description, state */},
|
|
548
806
|
surfaces: {/* label, heading, tooltip, ... */},
|
|
549
|
-
lint: {
|
|
807
|
+
lint: {
|
|
808
|
+
extends: 'recommended',
|
|
809
|
+
plugins: [],
|
|
810
|
+
rules: {},
|
|
811
|
+
areas: {}, // severities per area: { native: { 'sentence-case': 'off' } }
|
|
812
|
+
banned: [],
|
|
813
|
+
spelling: {},
|
|
814
|
+
concepts: {},
|
|
815
|
+
properNouns: [],
|
|
816
|
+
pathSeparator: '›',
|
|
817
|
+
},
|
|
818
|
+
native: [], // Rust, Swift or Kotlin files: { language: 'rust', out: 'src/copy.rs', keys: ['tray'] }
|
|
550
819
|
scan: { dirs: ['src'] },
|
|
551
820
|
});
|
|
552
821
|
```
|
|
553
822
|
|
|
554
|
-
##
|
|
823
|
+
## Examples
|
|
824
|
+
|
|
825
|
+
Each is a small shop in English, German and the pseudo-locale, and uses prosebook as an app would,
|
|
826
|
+
from the package. CI generates, lints, typechecks and builds each one, then checks what its build
|
|
827
|
+
serves.
|
|
555
828
|
|
|
556
|
-
|
|
557
|
-
|
|
829
|
+
| Example | |
|
|
830
|
+
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
831
|
+
| [`examples/vite`](examples/vite) | React with Vite, and a part-translated Turkish. The inspector is on. |
|
|
832
|
+
| [`examples/next`](examples/next) | Next.js: server components read a copy per request, client components the provider's. |
|
|
833
|
+
| [`examples/tanstack-start`](examples/tanstack-start) | TanStack Start, rendered on the server in the request's locale with the Vite plugin. |
|
|
834
|
+
| [`examples/bun`](examples/bun) | `Bun.serve` with a React page, a route that answers in the request's locale, the inspector. |
|
|
835
|
+
| [`examples/node`](examples/node) | No framework and no bundler: a Node script prints a receipt from the generated TypeScript. |
|
|
558
836
|
|
|
559
837
|
```bash
|
|
560
|
-
bun run
|
|
838
|
+
bun run build # the examples import prosebook from dist
|
|
839
|
+
bun run --cwd examples/next dev
|
|
561
840
|
```
|
|
562
841
|
|
|
563
|
-
## Where it came from
|
|
564
|
-
|
|
565
|
-
prosebook generalizes `@drasyn/text`, the catalog behind Drasyn's UI. It keeps that design: YAML
|
|
566
|
-
entries with `kind`, `surface` and `context`, ICU checked at build time, generated types, tags for
|
|
567
|
-
elements inside a sentence, a lint for the writing guide with reasoned exemptions, build-time
|
|
568
|
-
pruning, and the YAML 1.1/1.2 divergence check. What changed:
|
|
569
|
-
|
|
570
|
-
| Drasyn's text package | prosebook |
|
|
571
|
-
| ----------------------------------------------------------- | --------------------------------------------------------------------------- |
|
|
572
|
-
| Parses ICU at runtime with `intl-messageformat` | Compiles messages to functions; no parser ships |
|
|
573
|
-
| One global language; components don't re-render on a switch | `createCopy` instances; `useCopy()` re-renders; one per request on a server |
|
|
574
|
-
| Falls back to English only | Negotiates the locale and falls back key by key through parents |
|
|
575
|
-
| No way to know a translation is out of date | The lock marks stale translations; `status`, `todo`, `accept` |
|
|
576
|
-
| Kinds, surfaces and word lists are Drasyn's | All configurable; `recommended` and `strict` presets |
|
|
577
|
-
| A rich message needs `rich: true` and a `RichText` wrapper | Tags are detected; React output is keyed; default tags in the provider |
|
|
578
|
-
| Pruning fails the build on a key it can't read | Pruning reads string literals and prefixes, so keys in data still work |
|
|
579
|
-
| Bun scripts inside one repo | A CLI, a Vite plugin and `prosebook/tools` for other bundlers |
|
|
580
|
-
| — | Hardcoded-text `scan` and `extract`, pseudo-locale, translator `todo` |
|
|
581
|
-
| — | Alt-click a string in dev to open its entry |
|
|
582
|
-
|
|
583
842
|
## Further
|
|
584
843
|
|
|
585
844
|
Ideas not built yet, roughly in order of value:
|
|
586
845
|
|
|
587
|
-
1. **
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
4. **Server rendering helpers**: `Accept-Language` negotiation and a React Server Components reader.
|
|
592
|
-
5. **Native targets**: generate Rust, Swift or Kotlin from the same catalog, as Drasyn generates
|
|
593
|
-
Rust for its tray and menus.
|
|
594
|
-
6. **Variable MF2 options**: `{$price :currency currency=$code}`, compiled to a formatter cached per
|
|
595
|
-
value.
|
|
846
|
+
1. **Exchange formats**: XLIFF 2 and JSON export and import for translation tools.
|
|
847
|
+
2. **Per-area splitting**: one chunk per area and locale, so a lazy route loads only its own messages.
|
|
848
|
+
3. **Dates in native targets**: date and time formats taken apart from `Intl` as number formats are,
|
|
849
|
+
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-KD-DiiFa.js";
|
|
2
|
+
import { i as ProsebookConfig } from "./config-DdMkKt_N.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-9pR3i5Bj.js";
|
|
3
|
+
import { r as loadConfig } from "./config-BPSmoYel.js";
|
|
4
|
+
import { t as inspectorEndpoints } from "./endpoints-CnUfCkIk.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"}
|