prosebook 0.3.0 → 0.5.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 +357 -26
- package/dist/bind-BIZNv8nU.js +62 -0
- package/dist/bind-BIZNv8nU.js.map +1 -0
- package/dist/bind-ZS86jHXk.d.ts +31 -0
- package/dist/bind-ZS86jHXk.d.ts.map +1 -0
- package/dist/bun.d.ts +12 -3
- package/dist/bun.d.ts.map +1 -1
- package/dist/bun.js +77 -26
- package/dist/bun.js.map +1 -1
- package/dist/{bundle-KD-DiiFa.d.ts → bundle-CLHY_97D.d.ts} +3 -3
- package/dist/bundle-CLHY_97D.d.ts.map +1 -0
- package/dist/bundle-Ut6Y2X6N.js +217 -0
- package/dist/bundle-Ut6Y2X6N.js.map +1 -0
- package/dist/cli.js +55 -12
- package/dist/cli.js.map +1 -1
- package/dist/{config-BPSmoYel.js → config-7T4hX_f7.js} +13 -5
- package/dist/{config-BPSmoYel.js.map → config-7T4hX_f7.js.map} +1 -1
- package/dist/{config-DdMkKt_N.d.ts → config-CzaJJQ60.d.ts} +19 -5
- package/dist/{config-DdMkKt_N.d.ts.map → config-CzaJJQ60.d.ts.map} +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/{copy-B3V_QRkT.d.ts → copy-BXn0aYQn.d.ts} +37 -3
- package/dist/copy-BXn0aYQn.d.ts.map +1 -0
- package/dist/copy-Bv6S-Hr2.js +235 -0
- package/dist/copy-Bv6S-Hr2.js.map +1 -0
- package/dist/endpoints-Dhgowovz.js +217 -0
- package/dist/endpoints-Dhgowovz.js.map +1 -0
- package/dist/generate-1BeQAd1O.d.ts +72 -0
- package/dist/generate-1BeQAd1O.d.ts.map +1 -0
- package/dist/{generate-9pR3i5Bj.js → generate-CKv86Iki.js} +1724 -627
- package/dist/generate-CKv86Iki.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/next-loader.d.ts +9 -2
- package/dist/next-loader.d.ts.map +1 -1
- package/dist/next-loader.js +12 -3
- package/dist/next-loader.js.map +1 -1
- package/dist/next.d.ts +11 -3
- package/dist/next.d.ts.map +1 -1
- package/dist/next.js +94 -21
- package/dist/next.js.map +1 -1
- package/dist/preact/index.d.ts +34 -0
- package/dist/preact/index.d.ts.map +1 -0
- package/dist/preact/index.js +72 -0
- package/dist/preact/index.js.map +1 -0
- package/dist/react/index.d.ts +5 -14
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +9 -33
- package/dist/react/index.js.map +1 -1
- package/dist/react/server.d.ts +1 -1
- package/dist/react/server.js +1 -1
- package/dist/{sarif-CWyTAQ0y.js → sarif-BnwYC78_.js} +8 -98
- package/dist/sarif-BnwYC78_.js.map +1 -0
- package/dist/{scan-Bx0_yDSi.js → scan-C0ap4Mpp.js} +145 -20
- package/dist/scan-C0ap4Mpp.js.map +1 -0
- package/dist/solid/index.d.ts +29 -0
- package/dist/solid/index.d.ts.map +1 -0
- package/dist/solid/index.js +48 -0
- package/dist/solid/index.js.map +1 -0
- package/dist/svelte/Text.svelte +13 -0
- package/dist/svelte/Text.svelte.d.ts +20 -0
- package/dist/svelte/index.d.ts +31 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +35 -0
- package/dist/svelte/index.js.map +1 -0
- package/dist/tools.d.ts +44 -65
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +5 -5
- package/dist/vite.d.ts +13 -3
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +82 -25
- package/dist/vite.js.map +1 -1
- package/dist/vue/index.d.ts +43 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/dist/vue/index.js +76 -0
- package/dist/vue/index.js.map +1 -0
- package/package.json +63 -7
- package/src/bun.ts +77 -20
- package/src/cli.ts +62 -8
- package/src/index.ts +1 -1
- package/src/next-loader.ts +17 -5
- package/src/next.ts +120 -16
- package/src/preact/index.ts +91 -0
- package/src/react/index.tsx +12 -42
- package/src/runtime/bind.ts +96 -0
- package/src/runtime/copy.ts +148 -24
- package/src/solid/index.ts +67 -0
- package/src/svelte/Text.svelte +13 -0
- package/src/svelte/Text.svelte.d.ts +20 -0
- package/src/svelte/index.ts +65 -0
- package/src/tools/bundle.ts +143 -2
- package/src/tools/catalog.ts +10 -1
- package/src/tools/chunks.ts +142 -0
- package/src/tools/config.ts +25 -7
- package/src/tools/entry.ts +19 -20
- package/src/tools/exchange.ts +275 -0
- package/src/tools/extract.ts +2 -2
- package/src/tools/generate.ts +7 -1
- package/src/tools/index.ts +2 -1
- package/src/tools/native/android.ts +186 -0
- package/src/tools/native/apple.ts +201 -0
- package/src/tools/native/date.ts +219 -0
- package/src/tools/native/index.ts +17 -5
- package/src/tools/native/kotlin.ts +47 -6
- package/src/tools/native/model.ts +67 -29
- package/src/tools/native/printf.ts +137 -0
- package/src/tools/native/rust.ts +86 -5
- package/src/tools/native/swift.ts +62 -6
- package/src/tools/scan.ts +146 -25
- package/src/tools/todo.ts +6 -1
- package/src/vite.ts +121 -28
- package/src/vue/index.ts +117 -0
- package/dist/bundle-DboItAre.js +0 -111
- package/dist/bundle-DboItAre.js.map +0 -1
- package/dist/bundle-KD-DiiFa.d.ts.map +0 -1
- package/dist/compile-xF3F4oIp.d.ts +0 -16
- package/dist/compile-xF3F4oIp.d.ts.map +0 -1
- package/dist/copy-B3V_QRkT.d.ts.map +0 -1
- package/dist/copy-Bo_FIoLy.js +0 -154
- package/dist/copy-Bo_FIoLy.js.map +0 -1
- package/dist/endpoints-CnUfCkIk.js +0 -96
- package/dist/endpoints-CnUfCkIk.js.map +0 -1
- package/dist/generate-9pR3i5Bj.js.map +0 -1
- package/dist/sarif-CWyTAQ0y.js.map +0 -1
- package/dist/scan-Bx0_yDSi.js.map +0 -1
package/dist/bun.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { C as unseenMessage, S as splitIndex, T as watchCatalog, _ as membersOf, a as areaModule, c as beside, d as hotLocale, f as importingAreas, g as lazyAreas, h as keeper, m as installInspector, n as HOT_INDEX, o as areasNamed, p as inAreas, r as HOT_SET, t as AREA, x as scanSources, y as prunedModule } from "./bundle-Ut6Y2X6N.js";
|
|
2
|
+
import { T as CatalogError, n as generate } from "./generate-CKv86Iki.js";
|
|
3
|
+
import { i as areaGroups, t as inspectorEndpoints } from "./endpoints-Dhgowovz.js";
|
|
4
|
+
import { d as compileLocale, f as localeExpression } from "./scan-C0ap4Mpp.js";
|
|
5
|
+
import { r as loadConfig } from "./config-7T4hX_f7.js";
|
|
6
6
|
import { readFileSync } from "node:fs";
|
|
7
|
+
import { basename, resolve } from "node:path";
|
|
7
8
|
// src/bun.ts
|
|
8
9
|
/**
|
|
9
10
|
* `prosebook/bun`: the catalog as part of a Bun build, or of Bun's dev server.
|
|
@@ -18,35 +19,39 @@ import { readFileSync } from "node:fs";
|
|
|
18
19
|
* locale module accepts its own update and hands the new messages to every copy, as under Vite. The
|
|
19
20
|
* index starts the inspector on the page too, whose endpoints are `inspector()`'s routes, spread into
|
|
20
21
|
* `Bun.serve`'s; an entry opens in the editor `Bun.openInEditor` finds.
|
|
22
|
+
*
|
|
23
|
+
* With `split`, the page's catalog is split by area as the Vite plugin's is (`tools/bundle.ts`): each
|
|
24
|
+
* source file under `scan.dirs` naming an area's keys imports the area, a module of the plugin's own
|
|
25
|
+
* namespace. Bun has no transform hook, so those files are read here and handed back. A build knows
|
|
26
|
+
* the keys it keeps before it loads anything, so each area loads already pruned, and which areas
|
|
27
|
+
* share a chunk (`tools/chunks.ts`), so a lazy locale's part is a file per group of them.
|
|
21
28
|
*/
|
|
22
29
|
const inspectorPath = beside(import.meta.url, "inspector");
|
|
30
|
+
const NAMESPACE = "prosebook-area";
|
|
23
31
|
const describe = (e) => e instanceof CatalogError ? e.message : e instanceof Error ? e.stack ?? e.message : String(e);
|
|
24
32
|
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
33
|
+
/** A folder's path as a pattern, by either separator: Bun gives paths with either on Windows. */
|
|
34
|
+
const folderPattern = (path) => path.split(/[\\/]/).map(escape).join("[\\\\/]");
|
|
25
35
|
function prosebook(options = {}) {
|
|
26
36
|
return {
|
|
27
37
|
name: "prosebook",
|
|
28
38
|
async setup(build) {
|
|
29
39
|
const config = await loadConfig(resolve(options.root ?? process.cwd()), options.config);
|
|
30
40
|
let generated = generate(config);
|
|
31
|
-
const
|
|
32
|
-
const filter = new RegExp(`^${folder}[\\\\/][^\\\\/]+\\.[jt]s$`);
|
|
41
|
+
const filter = new RegExp(`^${folderPattern(config.out)}[\\\\/][^\\\\/]+\\.[jt]s$`);
|
|
33
42
|
const nameOf = (path) => basename(path).replace(/\.[jt]s$/, "");
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
const building = "entrypoints" in build.config;
|
|
44
|
+
const split = options.split === true;
|
|
45
|
+
if (building && split && build.config.splitting !== true) throw new Error("prosebook: `split` needs `splitting: true` in Bun.build, or every area ships in the entry chunk");
|
|
46
|
+
const client = (args) => "side" in args ? args.side === "client" : (build.config.target ?? "browser") === "browser";
|
|
47
|
+
const prune = options.prune ?? "strict";
|
|
48
|
+
let kept = () => true;
|
|
49
|
+
if (building && prune !== false) {
|
|
37
50
|
const { refs, unseen } = scanSources(config, prune === "strict");
|
|
38
51
|
if (unseen.length > 0) throw new Error(unseenMessage(unseen, config.root));
|
|
39
|
-
|
|
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;
|
|
52
|
+
kept = keeper(refs, options.keep);
|
|
48
53
|
}
|
|
49
|
-
const dev = process.env.NODE_ENV !== "production";
|
|
54
|
+
const dev = !building && process.env.NODE_ENV !== "production";
|
|
50
55
|
const inspector = dev && options.inspect !== false ? installInspector(inspectorPath, "/") : "";
|
|
51
56
|
if (dev) watchCatalog(config, () => {
|
|
52
57
|
try {
|
|
@@ -55,18 +60,64 @@ function prosebook(options = {}) {
|
|
|
55
60
|
console.error(describe(e));
|
|
56
61
|
}
|
|
57
62
|
});
|
|
58
|
-
build.onLoad({ filter }, (
|
|
59
|
-
const name = nameOf(path);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
build.onLoad({ filter }, (args) => {
|
|
64
|
+
const name = nameOf(args.path);
|
|
65
|
+
if (name === "index" && split && client(args)) {
|
|
66
|
+
const index = splitIndex(config, generated, options.keep);
|
|
67
|
+
return {
|
|
68
|
+
contents: building ? index : HOT_SET + inspector + index + HOT_INDEX,
|
|
69
|
+
loader: "js"
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const entries = generated.entries.get(name);
|
|
73
|
+
if (building) return entries === void 0 || prune === false ? void 0 : {
|
|
74
|
+
contents: prunedModule(entries, kept),
|
|
75
|
+
loader: "js"
|
|
76
|
+
};
|
|
77
|
+
const loader = args.path.endsWith(".ts") ? "ts" : "js";
|
|
78
|
+
if (entries !== void 0) return {
|
|
79
|
+
contents: readFileSync(args.path, "utf8") + hotLocale(name),
|
|
63
80
|
loader
|
|
64
81
|
};
|
|
65
82
|
return name === "index" ? {
|
|
66
|
-
contents: HOT_SET + inspector + readFileSync(path, "utf8") + HOT_INDEX,
|
|
83
|
+
contents: HOT_SET + inspector + readFileSync(args.path, "utf8") + HOT_INDEX,
|
|
67
84
|
loader
|
|
68
85
|
} : void 0;
|
|
69
86
|
});
|
|
87
|
+
if (!split) return;
|
|
88
|
+
const groups = building ? areaGroups(config, generated, options.keep) : /* @__PURE__ */ new Map();
|
|
89
|
+
build.onResolve({ filter: new RegExp(`^${escape(AREA)}`) }, ({ path }) => ({
|
|
90
|
+
path: path.slice(AREA.length),
|
|
91
|
+
namespace: NAMESPACE
|
|
92
|
+
}));
|
|
93
|
+
build.onLoad({
|
|
94
|
+
filter: /.*/,
|
|
95
|
+
namespace: NAMESPACE
|
|
96
|
+
}, ({ path }) => {
|
|
97
|
+
const [area = "", locale] = path.split("/");
|
|
98
|
+
if (locale !== void 0) {
|
|
99
|
+
const entries = inAreas(generated.entries.get(locale) ?? [], membersOf(groups, area));
|
|
100
|
+
return {
|
|
101
|
+
contents: prunedModule(entries, kept),
|
|
102
|
+
loader: "js"
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
contents: areaModule(config, generated, area, building ? (_locale, entries) => localeExpression(compileLocale(entries, kept)) : void 0, void 0, groups),
|
|
107
|
+
loader: "js"
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
const dirs = config.scan.dirs.map((dir) => folderPattern(resolve(config.root, dir))).join("|");
|
|
111
|
+
const out = new RegExp(`^${folderPattern(config.out)}[\\\\/]`);
|
|
112
|
+
build.onLoad({ filter: new RegExp(`^(?:${dirs})[\\\\/].*\\.[cm]?[jt]sx?$`) }, (args) => {
|
|
113
|
+
if (!client(args) || out.test(args.path) || /[\\/]node_modules[\\/]/.test(args.path)) return void 0;
|
|
114
|
+
const code = readFileSync(args.path, "utf8");
|
|
115
|
+
const areas = areasNamed(code, lazyAreas(generated, options.keep));
|
|
116
|
+
return areas.length === 0 ? void 0 : {
|
|
117
|
+
contents: importingAreas(code, args.path, areas),
|
|
118
|
+
loader: args.loader
|
|
119
|
+
};
|
|
120
|
+
});
|
|
70
121
|
}
|
|
71
122
|
};
|
|
72
123
|
}
|
package/dist/bun.js.map
CHANGED
|
@@ -1 +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"}
|
|
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 *\n * With `split`, the page's catalog is split by area as the Vite plugin's is (`tools/bundle.ts`): each\n * source file under `scan.dirs` naming an area's keys imports the area, a module of the plugin's own\n * namespace. Bun has no transform hook, so those files are read here and handed back. A build knows\n * the keys it keeps before it loads anything, so each area loads already pruned, and which areas\n * share a chunk (`tools/chunks.ts`), so a lazy locale's part is a file per group of them.\n */\nimport { readFileSync } from 'node:fs';\nimport { basename, resolve } from 'node:path';\n\nimport type { BunPlugin } from 'bun';\n\nimport {\n AREA,\n areaModule,\n areasNamed,\n lazyAreas,\n beside,\n HOT_INDEX,\n HOT_SET,\n hotLocale,\n importingAreas,\n inAreas,\n installInspector,\n keeper,\n membersOf,\n prunedModule,\n scanSources,\n splitIndex,\n unseenMessage,\n watchCatalog,\n type Prune,\n} from './tools/bundle.ts';\nimport { CatalogError } from './tools/catalog.ts';\nimport { areaGroups } from './tools/chunks.ts';\nimport { compileLocale, localeExpression, type Entry } from './tools/compile.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 /**\n * Keys a build always keeps, exactly or as `prefix.*`: for keys built at runtime from data. With\n * `split`, their areas load with the catalog.\n */\n keep?: string[];\n /**\n * Split each locale by area in the page, as the Vite plugin's `split` does, so a lazy route loads\n * only the messages it reads. `Bun.build` needs `splitting: true`. A server's own code loads whole\n * locales.\n */\n split?: boolean;\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');\nconst NAMESPACE = 'prosebook-area';\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/** A folder's path as a pattern, by either separator: Bun gives paths with either on Windows. */\nconst folderPattern = (path: string) => path.split(/[\\\\/]/).map(escape).join('[\\\\\\\\/]');\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.\n const filter = new RegExp(`^${folderPattern(config.out)}[\\\\\\\\/][^\\\\\\\\/]+\\\\.[jt]s$`);\n const nameOf = (path: string) => basename(path).replace(/\\.[jt]s$/, '');\n const building = 'entrypoints' in build.config;\n const split = options.split === true;\n if (building && split && build.config.splitting !== true)\n throw new Error('prosebook: `split` needs `splitting: true` in Bun.build, or every area ships in the entry chunk');\n // A full-stack build, one `Bun.build` of a server and the pages it imports, names the side it\n // loads for (undocumented, and not in Bun's types); any other build is for its target.\n const client = (args: object) => ('side' in args ? args.side === 'client' : (build.config.target ?? 'browser') === 'browser');\n\n const prune = options.prune ?? 'strict';\n let kept: (key: string) => boolean = () => true;\n if (building && prune !== false) {\n const { refs, unseen } = scanSources(config, prune === 'strict');\n if (unseen.length > 0) throw new Error(unseenMessage(unseen, config.root));\n kept = keeper(refs, options.keep);\n }\n const dev = !building && 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\n build.onLoad({ filter }, (args) => {\n const name = nameOf(args.path);\n if (name === 'index' && split && client(args)) {\n const index = splitIndex(config, generated, options.keep);\n return { contents: building ? index : HOT_SET + inspector + index + HOT_INDEX, loader: 'js' };\n }\n const entries = generated.entries.get(name);\n if (building) return entries === undefined || prune === false ? undefined : { contents: prunedModule(entries, kept), loader: 'js' };\n const loader = args.path.endsWith('.ts') ? 'ts' : 'js';\n if (entries !== undefined) return { contents: readFileSync(args.path, 'utf8') + hotLocale(name), loader };\n return name === 'index' ? { contents: HOT_SET + inspector + readFileSync(args.path, 'utf8') + HOT_INDEX, loader } : undefined;\n });\n if (!split) return;\n\n const groups = building ? areaGroups(config, generated, options.keep) : new Map<string, string>();\n // `cart`, or in a build `cart/de`, a lazy locale's part of the group `cart` leads.\n build.onResolve({ filter: new RegExp(`^${escape(AREA)}`) }, ({ path }) => ({ path: path.slice(AREA.length), namespace: NAMESPACE }));\n build.onLoad({ filter: /.*/, namespace: NAMESPACE }, ({ path }) => {\n const [area = '', locale] = path.split('/');\n if (locale !== undefined) {\n const entries = inAreas(generated.entries.get(locale) ?? [], membersOf(groups, area));\n return { contents: prunedModule(entries, kept), loader: 'js' };\n }\n const inline = building ? (_locale: string, entries: Entry[]) => localeExpression(compileLocale(entries, kept)) : undefined;\n return { contents: areaModule(config, generated, area, inline, undefined, groups), loader: 'js' };\n });\n const dirs = config.scan.dirs.map((dir) => folderPattern(resolve(config.root, dir))).join('|');\n const out = new RegExp(`^${folderPattern(config.out)}[\\\\\\\\/]`);\n build.onLoad({ filter: new RegExp(`^(?:${dirs})[\\\\\\\\/].*\\\\.[cm]?[jt]sx?$`) }, (args) => {\n if (!client(args) || out.test(args.path) || /[\\\\/]node_modules[\\\\/]/.test(args.path)) return undefined;\n const code = readFileSync(args.path, 'utf8');\n const areas = areasNamed(code, lazyAreas(generated, options.keep));\n return areas.length === 0 ? undefined : { contents: importingAreas(code, args.path, areas), loader: args.loader };\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":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import "./
|
|
2
|
-
import "./config-
|
|
1
|
+
import "./generate-1BeQAd1O.js";
|
|
2
|
+
import "./config-CzaJJQ60.js";
|
|
3
3
|
/** How a build prunes: `strict` also fails on a read whose key it can't see. */
|
|
4
4
|
type Prune = 'strict' | 'loose' | false;
|
|
5
5
|
export { Prune as t };
|
|
6
|
-
//# sourceMappingURL=bundle-
|
|
6
|
+
//# sourceMappingURL=bundle-CLHY_97D.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundle-CLHY_97D.d.ts","names":[],"sources":["../src/tools/bundle.ts"],"mappings":";;;KAkBY"}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { a as keyRefs, d as compileLocale, i as isReferenced, l as sourceFiles, n as SFC, p as localeModule, t as READERS, u as unseenReads } from "./scan-C0ap4Mpp.js";
|
|
2
|
+
import { existsSync, readFileSync, watch } from "node:fs";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
// src/tools/bundle.ts
|
|
6
|
+
/**
|
|
7
|
+
* What the bundler integrations share: which messages a build ships, the reads that would show a key
|
|
8
|
+
* once their message is pruned, and the code a dev server appends so an edit swaps messages in place.
|
|
9
|
+
*
|
|
10
|
+
* Vite sees every module before it writes a chunk, so it prunes after the fact (`vite.ts`). Next's
|
|
11
|
+
* loaders and Bun's `onLoad` hand over a locale module before the code that reads it has been seen,
|
|
12
|
+
* so they read the source under `scan.dirs` first (`scanSources`) and load the locale already pruned.
|
|
13
|
+
*/
|
|
14
|
+
/** Whether a build keeps a key: some module names it, or `keep` lists it, exactly or as `prefix.*`. */
|
|
15
|
+
function keeper(refs, keep = []) {
|
|
16
|
+
return (key) => isReferenced(key, refs) || keep.some((k) => k.endsWith(".*") ? key.startsWith(k.slice(0, -1)) : k === key);
|
|
17
|
+
}
|
|
18
|
+
/** The unseen reads that are a copy's: a name only counts once some module binds a reader to it. */
|
|
19
|
+
function unseenOf(reads) {
|
|
20
|
+
const all = [...reads];
|
|
21
|
+
const readers = /* @__PURE__ */ new Set([...READERS, ...all.flatMap((r) => [...r.aliases])]);
|
|
22
|
+
return all.flatMap((r) => r.unseen).filter((u) => u.name === "Text" || readers.has(u.name));
|
|
23
|
+
}
|
|
24
|
+
/** The error a strict build fails with, each read at its path from `root`. */
|
|
25
|
+
function unseenMessage(unseen, root) {
|
|
26
|
+
const at = (file) => relative(root, file).replaceAll("\\", "/");
|
|
27
|
+
return [
|
|
28
|
+
`${unseen.length} read(s) whose key the build can't see, so their messages would be pruned and the key shown instead:`,
|
|
29
|
+
...unseen.map((u) => ` ${at(u.file)}:${u.line}:${u.column} ${u.code}`),
|
|
30
|
+
"Write each key at the read, as a literal or `area.${id}`; or, where every key it reads is written elsewhere or listed in `keep`, mark the line `prosebook-ignore`. `prune: \"loose\"` turns this check off."
|
|
31
|
+
].join("\n");
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Every key the app's source names and, with `strict`, its unseen reads. The generated modules are
|
|
35
|
+
* left out, since they name every key.
|
|
36
|
+
*/
|
|
37
|
+
function scanSources(config, strict) {
|
|
38
|
+
const refs = {
|
|
39
|
+
exact: /* @__PURE__ */ new Set(),
|
|
40
|
+
prefixes: /* @__PURE__ */ new Set()
|
|
41
|
+
};
|
|
42
|
+
const reads = [];
|
|
43
|
+
for (const file of sourceFiles(config.scan.dirs, config.scan.extensions, [config.out])) {
|
|
44
|
+
const code = readFileSync(file, "utf8");
|
|
45
|
+
keyRefs(code, refs);
|
|
46
|
+
if (strict) reads.push(unseenReads(file, code));
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
refs,
|
|
50
|
+
unseen: unseenOf(reads)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** Calls `change` once a burst of edits to the catalog or its translations settles. Returns the stop. */
|
|
54
|
+
function watchCatalog(config, change) {
|
|
55
|
+
let timer;
|
|
56
|
+
const watchers = [config.catalog, config.locales].filter((dir) => existsSync(dir)).map((dir) => watch(dir, { recursive: true }, (_event, file) => {
|
|
57
|
+
if (file === null || !file.endsWith(".yaml")) return;
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
timer = setTimeout(change, 50);
|
|
60
|
+
}));
|
|
61
|
+
return () => {
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
for (const w of watchers) w.close();
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** `virtual:prosebook/area/cart`, and in a build `…/cart/de`, a lazy locale's part of the group `cart` leads. */
|
|
67
|
+
const AREA = "virtual:prosebook/area/";
|
|
68
|
+
const VIRTUAL = {
|
|
69
|
+
area: (area, locale) => AREA + area + (locale === void 0 ? "" : `/${locale}`),
|
|
70
|
+
generated: (path) => path
|
|
71
|
+
};
|
|
72
|
+
/** `path` imported from a module in the folder `from`, as a relative specifier. */
|
|
73
|
+
function relativeImport(from, path) {
|
|
74
|
+
const to = relative(from, path).replaceAll("\\", "/");
|
|
75
|
+
return to.startsWith(".") ? to : `./${to}`;
|
|
76
|
+
}
|
|
77
|
+
/** An area's file, or a lazy locale's part of a group, where a bundler without virtual modules finds them. */
|
|
78
|
+
const areaFile = (area, locale) => locale === void 0 ? `${area}.js` : `${area}.${locale}.js`;
|
|
79
|
+
/** The specifiers of area files in `dir`, from a module in the folder `from`. */
|
|
80
|
+
const fileSpecifiers = (from, dir) => ({
|
|
81
|
+
area: (area, locale) => relativeImport(from, join(dir, areaFile(area, locale))),
|
|
82
|
+
generated: (path) => relativeImport(from, path)
|
|
83
|
+
});
|
|
84
|
+
const areaOf = (key) => key.slice(0, key.indexOf("."));
|
|
85
|
+
/** The entries of some areas: their keys start with an area's name. */
|
|
86
|
+
const inAreas = (entries, areas) => entries.filter(([key]) => areas.includes(areaOf(key)));
|
|
87
|
+
/** The areas whose lazy locales load from the group `lead` names. */
|
|
88
|
+
const membersOf = (groups, lead) => [...groups].filter(([, g]) => g === lead).map(([area]) => area);
|
|
89
|
+
const areasOf = (generated) => new Set(generated.catalog.messages.map((m) => m.area));
|
|
90
|
+
/** Areas `keep` names, which load with the index of a split catalog, as no module may name their keys. */
|
|
91
|
+
const eagerAreas = (generated, keep = []) => {
|
|
92
|
+
const known = areasOf(generated);
|
|
93
|
+
return new Set(keep.map(areaOf).filter((a) => known.has(a)));
|
|
94
|
+
};
|
|
95
|
+
/** The areas that load with the code naming their keys: all but those `keep` loads with the index. */
|
|
96
|
+
const lazyAreas = (generated, keep) => {
|
|
97
|
+
const eager = eagerAreas(generated, keep);
|
|
98
|
+
return new Set([...areasOf(generated)].filter((a) => !eager.has(a)));
|
|
99
|
+
};
|
|
100
|
+
/** Which of `areas` the keys `code` names are in. */
|
|
101
|
+
function areasNamed(code, areas) {
|
|
102
|
+
const named = keyRefs(code);
|
|
103
|
+
return [...new Set([...named.exact, ...named.prefixes].map(areaOf))].filter((a) => areas.has(a)).toSorted();
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* `code` importing each of `areas`. At the end, as imports run first wherever they are, so no line
|
|
107
|
+
* moves; in a component file, at the end of its first script.
|
|
108
|
+
*/
|
|
109
|
+
function importingAreas(code, path, areas, specifier = VIRTUAL.area) {
|
|
110
|
+
const imports = areas.map((a) => `import ${JSON.stringify(specifier(a))};`).join("");
|
|
111
|
+
if (!SFC.test(path)) return `${code}\n${imports}\n`;
|
|
112
|
+
const close = code.indexOf("<\/script>");
|
|
113
|
+
return close === -1 ? `${code}\n<script>${imports}<\/script>\n` : `${code.slice(0, close)};${imports}${code.slice(close)}`;
|
|
114
|
+
}
|
|
115
|
+
/** What a split catalog's areas register under: the generated folder, from the config's. */
|
|
116
|
+
const splitId = (config) => relative(config.root, config.out).replaceAll("\\", "/");
|
|
117
|
+
/** A generated module's path, with forward slashes. */
|
|
118
|
+
const generatedPath = (config, name) => join(config.out, `${name}.${config.language === "ts" ? "ts" : "js"}`).replaceAll("\\", "/");
|
|
119
|
+
/** The index of a split catalog, in the browser: no messages of its own, only the areas `keep` names. */
|
|
120
|
+
function splitIndex(config, generated, keep, where = VIRTUAL) {
|
|
121
|
+
const locales = [...generated.entries.keys()];
|
|
122
|
+
const split = {
|
|
123
|
+
id: splitId(config),
|
|
124
|
+
areas: [...areasOf(generated)]
|
|
125
|
+
};
|
|
126
|
+
return [
|
|
127
|
+
"import { defineCatalog } from 'prosebook';",
|
|
128
|
+
...[...eagerAreas(generated, keep)].map((a) => `import ${JSON.stringify(where.area(a))};`),
|
|
129
|
+
`export const catalog = defineCatalog(${JSON.stringify({
|
|
130
|
+
source: config.source,
|
|
131
|
+
locales,
|
|
132
|
+
messages: {},
|
|
133
|
+
loaders: {},
|
|
134
|
+
split
|
|
135
|
+
})});`,
|
|
136
|
+
""
|
|
137
|
+
].join("\n");
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* An area's module: registers its messages in the locales that ship with it, and loaders for the
|
|
141
|
+
* rest, and waits until every copy holds it. In a build, `inline` writes the first, and each of the
|
|
142
|
+
* rest loads its part of the area's group, shared with the areas `groups` puts beside it, which the
|
|
143
|
+
* copy takes only this area's keys from; in dev, without it, each locale is its generated module
|
|
144
|
+
* whole, so an edit updates it as it would without splitting.
|
|
145
|
+
*/
|
|
146
|
+
function areaModule(config, generated, area, inline, where = VIRTUAL, groups = /* @__PURE__ */ new Map()) {
|
|
147
|
+
const lines = ["import { addArea } from 'prosebook';"];
|
|
148
|
+
const messages = [];
|
|
149
|
+
const loaders = [];
|
|
150
|
+
for (const [locale, entries] of generated.entries) {
|
|
151
|
+
const name = JSON.stringify(locale);
|
|
152
|
+
const own = inAreas(entries, [area]);
|
|
153
|
+
const whole = JSON.stringify(where.generated(generatedPath(config, locale)));
|
|
154
|
+
if (locale === config.source || config.bundled.includes(locale)) {
|
|
155
|
+
if (inline !== void 0) messages.push(`${name}: ${inline(locale, own)}`);
|
|
156
|
+
else {
|
|
157
|
+
lines.push(`import m${messages.length} from ${whole};`);
|
|
158
|
+
messages.push(`${name}: m${messages.length}`);
|
|
159
|
+
}
|
|
160
|
+
} else if (inline === void 0) loaders.push(`${name}: () => import(${whole})`);
|
|
161
|
+
else if (own.length > 0) loaders.push(`${name}: () => import(${JSON.stringify(where.area(groups.get(area) ?? area, locale))})`);
|
|
162
|
+
}
|
|
163
|
+
const args = [
|
|
164
|
+
JSON.stringify(splitId(config)),
|
|
165
|
+
JSON.stringify(area),
|
|
166
|
+
`{ ${messages.join(", ")} }`,
|
|
167
|
+
`{ ${loaders.join(", ")} }`
|
|
168
|
+
];
|
|
169
|
+
lines.push(`await addArea(${args.join(", ")});`, "");
|
|
170
|
+
return lines.join("\n");
|
|
171
|
+
}
|
|
172
|
+
/** A locale's module holding only the keys `kept`, as JavaScript, for a bundler to load in its place. */
|
|
173
|
+
function prunedModule(entries, kept) {
|
|
174
|
+
return localeModule(compileLocale(entries, kept), "// Pruned by prosebook: only the messages the code names.", false);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Appended to a locale module in dev. Accepts its own update and hands the new messages to every
|
|
178
|
+
* copy (`HotHook`); a page loaded before the hooks existed reloads instead.
|
|
179
|
+
*/
|
|
180
|
+
const hotLocale = (locale) => `
|
|
181
|
+
if (import.meta.hot) {
|
|
182
|
+
import.meta.hot.accept((next) => {
|
|
183
|
+
const hot = globalThis.__PROSEBOOK_HOT__;
|
|
184
|
+
if (next === undefined || hot === undefined) return import.meta.hot.invalidate();
|
|
185
|
+
for (const update of hot) update(${JSON.stringify(locale)}, next.default);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
`;
|
|
189
|
+
/** Appended to the index in dev: its doc comments change with every edit; only a new locale reloads. */
|
|
190
|
+
const HOT_INDEX = `
|
|
191
|
+
if (import.meta.hot) {
|
|
192
|
+
import.meta.hot.accept((next) => {
|
|
193
|
+
if (next === undefined || next.catalog.locales.join() !== catalog.locales.join()) import.meta.hot.invalidate();
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
`;
|
|
197
|
+
/**
|
|
198
|
+
* A locale module in dev under a bundler that runs it again when it changes, as Turbopack's Fast
|
|
199
|
+
* Refresh does, but has no `import.meta.hot`: each run hands its messages to every copy. The first
|
|
200
|
+
* run finds no copy holding the locale yet, so it changes nothing.
|
|
201
|
+
*/
|
|
202
|
+
const notifyingLocale = (code, locale) => `${code.replace(/^export default /m, "const messages = ")}
|
|
203
|
+
export default messages;
|
|
204
|
+
globalThis.__PROSEBOOK_HOT__?.forEach((update) => update(${JSON.stringify(locale)}, messages));
|
|
205
|
+
`;
|
|
206
|
+
/** Prepended to the index in dev, so the set every copy adds itself to exists before any copy does. */
|
|
207
|
+
const HOT_SET = "globalThis.__PROSEBOOK_HOT__ ??= new Set();\n";
|
|
208
|
+
/**
|
|
209
|
+
* A module of prosebook's own beside the one at `url`, which passes its `import.meta.url`: the source
|
|
210
|
+
* when run from source, the built file in dist.
|
|
211
|
+
*/
|
|
212
|
+
const beside = (url, name) => fileURLToPath(new URL(`./${name}${url.endsWith(".ts") ? ".ts" : ".js"}`, url)).replaceAll("\\", "/");
|
|
213
|
+
/** Code that starts the inspector (`inspector.ts`, imported from `path`) on the page, its endpoints under `base`. */
|
|
214
|
+
const installInspector = (path, base) => `import { install as installInspector } from ${JSON.stringify(path)};\ninstallInspector(${JSON.stringify({ base })});\n`;
|
|
215
|
+
export { unseenMessage as C, splitIndex as S, watchCatalog as T, membersOf as _, areaModule as a, relativeImport as b, beside as c, hotLocale as d, importingAreas as f, lazyAreas as g, keeper as h, areaFile as i, eagerAreas as l, installInspector as m, HOT_INDEX as n, areasNamed as o, inAreas as p, HOT_SET as r, areasOf as s, AREA as t, fileSpecifiers as u, notifyingLocale as v, unseenOf as w, scanSources as x, prunedModule as y };
|
|
216
|
+
|
|
217
|
+
//# sourceMappingURL=bundle-Ut6Y2X6N.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundle-Ut6Y2X6N.js","names":[],"sources":["../src/tools/bundle.ts"],"sourcesContent":["/**\n * What the bundler integrations share: which messages a build ships, the reads that would show a key\n * once their message is pruned, and the code a dev server appends so an edit swaps messages in place.\n *\n * Vite sees every module before it writes a chunk, so it prunes after the fact (`vite.ts`). Next's\n * loaders and Bun's `onLoad` hand over a locale module before the code that reads it has been seen,\n * so they read the source under `scan.dirs` first (`scanSources`) and load the locale already pruned.\n */\nimport { existsSync, readFileSync, watch } from 'node:fs';\nimport { join, relative } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport { compileLocale, localeModule, type Entry } from './compile.ts';\nimport type { ResolvedConfig } from './config.ts';\nimport type { Generated } from './generate.ts';\nimport { isReferenced, keyRefs, READERS, SFC, sourceFiles, unseenReads, type KeyRefs, type Reads, type UnseenRead } from './scan.ts';\n\n/** How a build prunes: `strict` also fails on a read whose key it can't see. */\nexport type Prune = 'strict' | 'loose' | false;\n\n/** Whether a build keeps a key: some module names it, or `keep` lists it, exactly or as `prefix.*`. */\nexport function keeper(refs: KeyRefs, keep: string[] = []): (key: string) => boolean {\n return (key) => isReferenced(key, refs) || keep.some((k) => (k.endsWith('.*') ? key.startsWith(k.slice(0, -1)) : k === key));\n}\n\n/** The unseen reads that are a copy's: a name only counts once some module binds a reader to it. */\nexport function unseenOf(reads: Iterable<Reads>): UnseenRead[] {\n const all = [...reads];\n const readers = new Set([...READERS, ...all.flatMap((r) => [...r.aliases])]);\n return all.flatMap((r) => r.unseen).filter((u) => u.name === 'Text' || readers.has(u.name));\n}\n\n/** The error a strict build fails with, each read at its path from `root`. */\nexport function unseenMessage(unseen: UnseenRead[], root: string): string {\n const at = (file: string) => relative(root, file).replaceAll('\\\\', '/');\n return [\n `${unseen.length} read(s) whose key the build can't see, so their messages would be pruned and the key shown instead:`,\n ...unseen.map((u) => ` ${at(u.file)}:${u.line}:${u.column} ${u.code}`),\n 'Write each key at the read, as a literal or `area.${id}`; or, where every key it reads is written elsewhere or listed in `keep`, mark the line `prosebook-ignore`. `prune: \"loose\"` turns this check off.',\n ].join('\\n');\n}\n\nexport interface SourceScan {\n refs: KeyRefs;\n unseen: UnseenRead[];\n}\n\n/**\n * Every key the app's source names and, with `strict`, its unseen reads. The generated modules are\n * left out, since they name every key.\n */\nexport function scanSources(config: ResolvedConfig, strict: boolean): SourceScan {\n const refs: KeyRefs = { exact: new Set(), prefixes: new Set() };\n const reads: Reads[] = [];\n for (const file of sourceFiles(config.scan.dirs, config.scan.extensions, [config.out])) {\n const code = readFileSync(file, 'utf8');\n keyRefs(code, refs);\n if (strict) reads.push(unseenReads(file, code));\n }\n return { refs, unseen: unseenOf(reads) };\n}\n\n/** Calls `change` once a burst of edits to the catalog or its translations settles. Returns the stop. */\nexport function watchCatalog(config: ResolvedConfig, change: () => void): () => void {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const watchers = [config.catalog, config.locales]\n .filter((dir) => existsSync(dir))\n .map((dir) =>\n watch(dir, { recursive: true }, (_event, file) => {\n if (file === null || !file.endsWith('.yaml')) return;\n clearTimeout(timer);\n timer = setTimeout(change, 50);\n }),\n );\n return () => {\n clearTimeout(timer);\n for (const w of watchers) w.close();\n };\n}\n\n/** `virtual:prosebook/area/cart`, and in a build `…/cart/de`, a lazy locale's part of the group `cart` leads. */\nexport const AREA = 'virtual:prosebook/area/';\n\n/**\n * How a split catalog's modules are imported: an area (`cart`), a lazy locale's part of a group of\n * them, by the group's first area (`cart`, `de`), and a generated module, from its path. Vite's and Bun's are virtual modules (`VIRTUAL`);\n * Turbopack has none, so Next's are files.\n */\nexport interface Specifiers {\n area: (area: string, locale?: string) => string;\n generated: (path: string) => string;\n}\n\nexport const VIRTUAL: Specifiers = {\n area: (area, locale) => AREA + area + (locale === undefined ? '' : `/${locale}`),\n generated: (path) => path,\n};\n\n/** `path` imported from a module in the folder `from`, as a relative specifier. */\nexport function relativeImport(from: string, path: string): string {\n const to = relative(from, path).replaceAll('\\\\', '/');\n return to.startsWith('.') ? to : `./${to}`;\n}\n\n/** An area's file, or a lazy locale's part of a group, where a bundler without virtual modules finds them. */\nexport const areaFile = (area: string, locale?: string) => (locale === undefined ? `${area}.js` : `${area}.${locale}.js`);\n\n/** The specifiers of area files in `dir`, from a module in the folder `from`. */\nexport const fileSpecifiers = (from: string, dir: string): Specifiers => ({\n area: (area, locale) => relativeImport(from, join(dir, areaFile(area, locale))),\n generated: (path) => relativeImport(from, path),\n});\n\nexport const areaOf = (key: string) => key.slice(0, key.indexOf('.'));\n\n/** The entries of some areas: their keys start with an area's name. */\nexport const inAreas = (entries: Entry[], areas: readonly string[]) => entries.filter(([key]) => areas.includes(areaOf(key)));\n\n/**\n * Each area to the group whose file its lazy locales load from, named by the group's first area: the\n * areas a bundler puts in one chunk (`areaGroups` in `chunks.ts`), so they load in one request.\n */\nexport type Groups = ReadonlyMap<string, string>;\n\n/** The areas whose lazy locales load from the group `lead` names. */\nexport const membersOf = (groups: Groups, lead: string) => [...groups].filter(([, g]) => g === lead).map(([area]) => area);\n\nexport const areasOf = (generated: Generated) => new Set(generated.catalog.messages.map((m) => m.area));\n\n/** Areas `keep` names, which load with the index of a split catalog, as no module may name their keys. */\nexport const eagerAreas = (generated: Generated, keep: string[] = []) => {\n const known = areasOf(generated);\n return new Set(keep.map(areaOf).filter((a) => known.has(a)));\n};\n\n/** The areas that load with the code naming their keys: all but those `keep` loads with the index. */\nexport const lazyAreas = (generated: Generated, keep?: string[]) => {\n const eager = eagerAreas(generated, keep);\n return new Set([...areasOf(generated)].filter((a) => !eager.has(a)));\n};\n\n/** Which of `areas` the keys `code` names are in. */\nexport function areasNamed(code: string, areas: ReadonlySet<string>): string[] {\n const named = keyRefs(code);\n return [...new Set([...named.exact, ...named.prefixes].map(areaOf))].filter((a) => areas.has(a)).toSorted();\n}\n\n/**\n * `code` importing each of `areas`. At the end, as imports run first wherever they are, so no line\n * moves; in a component file, at the end of its first script.\n */\nexport function importingAreas(code: string, path: string, areas: string[], specifier = VIRTUAL.area): string {\n const imports = areas.map((a) => `import ${JSON.stringify(specifier(a))};`).join('');\n if (!SFC.test(path)) return `${code}\\n${imports}\\n`;\n const close = code.indexOf('</script>');\n return close === -1 ? `${code}\\n<script>${imports}</script>\\n` : `${code.slice(0, close)};${imports}${code.slice(close)}`;\n}\n\n/** What a split catalog's areas register under: the generated folder, from the config's. */\nconst splitId = (config: ResolvedConfig) => relative(config.root, config.out).replaceAll('\\\\', '/');\n\n/** A generated module's path, with forward slashes. */\nconst generatedPath = (config: ResolvedConfig, name: string) =>\n join(config.out, `${name}.${config.language === 'ts' ? 'ts' : 'js'}`).replaceAll('\\\\', '/');\n\n/** The index of a split catalog, in the browser: no messages of its own, only the areas `keep` names. */\nexport function splitIndex(config: ResolvedConfig, generated: Generated, keep?: string[], where = VIRTUAL): string {\n const locales = [...generated.entries.keys()];\n const split = { id: splitId(config), areas: [...areasOf(generated)] };\n return [\n \"import { defineCatalog } from 'prosebook';\",\n ...[...eagerAreas(generated, keep)].map((a) => `import ${JSON.stringify(where.area(a))};`),\n `export const catalog = defineCatalog(${JSON.stringify({ source: config.source, locales, messages: {}, loaders: {}, split })});`,\n '',\n ].join('\\n');\n}\n\n/** How an area's module holds a locale that ships with it in a build: as an expression, from its entries. */\nexport type Inline = (locale: string, entries: Entry[]) => string;\n\n/**\n * An area's module: registers its messages in the locales that ship with it, and loaders for the\n * rest, and waits until every copy holds it. In a build, `inline` writes the first, and each of the\n * rest loads its part of the area's group, shared with the areas `groups` puts beside it, which the\n * copy takes only this area's keys from; in dev, without it, each locale is its generated module\n * whole, so an edit updates it as it would without splitting.\n */\nexport function areaModule(\n config: ResolvedConfig,\n generated: Generated,\n area: string,\n inline?: Inline,\n where = VIRTUAL,\n groups: Groups = new Map(),\n): string {\n const lines = [\"import { addArea } from 'prosebook';\"];\n const messages: string[] = [];\n const loaders: string[] = [];\n for (const [locale, entries] of generated.entries) {\n const name = JSON.stringify(locale);\n const own = inAreas(entries, [area]);\n const whole = JSON.stringify(where.generated(generatedPath(config, locale)));\n if (locale === config.source || config.bundled.includes(locale)) {\n if (inline !== undefined) {\n messages.push(`${name}: ${inline(locale, own)}`);\n } else {\n lines.push(`import m${messages.length} from ${whole};`);\n messages.push(`${name}: m${messages.length}`);\n }\n } else if (inline === undefined) {\n loaders.push(`${name}: () => import(${whole})`);\n } else if (own.length > 0) {\n loaders.push(`${name}: () => import(${JSON.stringify(where.area(groups.get(area) ?? area, locale))})`);\n }\n }\n const args = [JSON.stringify(splitId(config)), JSON.stringify(area), `{ ${messages.join(', ')} }`, `{ ${loaders.join(', ')} }`];\n lines.push(`await addArea(${args.join(', ')});`, '');\n return lines.join('\\n');\n}\n\n/** A locale's module holding only the keys `kept`, as JavaScript, for a bundler to load in its place. */\nexport function prunedModule(entries: Entry[], kept: (key: string) => boolean): string {\n return localeModule(compileLocale(entries, kept), '// Pruned by prosebook: only the messages the code names.', false);\n}\n\n/**\n * Appended to a locale module in dev. Accepts its own update and hands the new messages to every\n * copy (`HotHook`); a page loaded before the hooks existed reloads instead.\n */\nexport const hotLocale = (locale: string) => `\nif (import.meta.hot) {\n import.meta.hot.accept((next) => {\n const hot = globalThis.__PROSEBOOK_HOT__;\n if (next === undefined || hot === undefined) return import.meta.hot.invalidate();\n for (const update of hot) update(${JSON.stringify(locale)}, next.default);\n });\n}\n`;\n\n/** Appended to the index in dev: its doc comments change with every edit; only a new locale reloads. */\nexport const HOT_INDEX = `\nif (import.meta.hot) {\n import.meta.hot.accept((next) => {\n if (next === undefined || next.catalog.locales.join() !== catalog.locales.join()) import.meta.hot.invalidate();\n });\n}\n`;\n\n/**\n * A locale module in dev under a bundler that runs it again when it changes, as Turbopack's Fast\n * Refresh does, but has no `import.meta.hot`: each run hands its messages to every copy. The first\n * run finds no copy holding the locale yet, so it changes nothing.\n */\nexport const notifyingLocale = (code: string, locale: string) =>\n `${code.replace(/^export default /m, 'const messages = ')}\nexport default messages;\nglobalThis.__PROSEBOOK_HOT__?.forEach((update) => update(${JSON.stringify(locale)}, messages));\n`;\n\n/** Prepended to the index in dev, so the set every copy adds itself to exists before any copy does. */\nexport const HOT_SET = 'globalThis.__PROSEBOOK_HOT__ ??= new Set();\\n';\n\n/**\n * A module of prosebook's own beside the one at `url`, which passes its `import.meta.url`: the source\n * when run from source, the built file in dist.\n */\nexport const beside = (url: string, name: string) =>\n fileURLToPath(new URL(`./${name}${url.endsWith('.ts') ? '.ts' : '.js'}`, url)).replaceAll('\\\\', '/');\n\n/** Code that starts the inspector (`inspector.ts`, imported from `path`) on the page, its endpoints under `base`. */\nexport const installInspector = (path: string, base: string) =>\n `import { install as installInspector } from ${JSON.stringify(path)};\\ninstallInspector(${JSON.stringify({ base })});\\n`;\n"],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { D as whereOf,
|
|
4
|
-
import { p as lint, r as loadConfig, t as CONFIG_FILES } from "./config-
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { join, relative, resolve } from "node:path";
|
|
2
|
+
import { T as watchCatalog } from "./bundle-Ut6Y2X6N.js";
|
|
3
|
+
import { D as SCHEMA_LINE, M as whereOf, N as SYNTAX_NAME, T as CatalogError, _ as changeOf, a as render, d as importTranslations, g as todoOf, k as entryLocator, l as applyFixes, n as generate, p as exportOf } from "./generate-CKv86Iki.js";
|
|
4
|
+
import { p as lint, r as loadConfig, t as CONFIG_FILES } from "./config-7T4hX_f7.js";
|
|
5
|
+
import { i as applyExtract, n as applyMigrate, r as planMigrate, s as planExtract, t as SarifReport } from "./sarif-BnwYC78_.js";
|
|
6
|
+
import { i as isReferenced, l as sourceFiles, s as scan } from "./scan-C0ap4Mpp.js";
|
|
8
7
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
9
9
|
import { parseArgs } from "node:util";
|
|
10
10
|
// src/cli.ts
|
|
11
11
|
/**
|
|
@@ -25,8 +25,11 @@ Commands:
|
|
|
25
25
|
accept <locale> [keys] Mark stale translations as still right (--all for every stale one)
|
|
26
26
|
todo <locale> Print the missing and stale entries of a locale, ready to translate
|
|
27
27
|
(--prompt: as a prompt for a model, saying how to reply)
|
|
28
|
-
|
|
29
|
-
(
|
|
28
|
+
export <locale> [file] Write the entries todo lists as XLIFF 2.0 or JSON, for a translation tool
|
|
29
|
+
(--all: every entry; the format from the file's extension, or --format)
|
|
30
|
+
import <locale> [file] Write translations from todo's YAML filled in, a model's reply to --prompt,
|
|
31
|
+
or a tool's XLIFF or JSON (stdin without a file); each is checked first,
|
|
32
|
+
and one that fails is left out
|
|
30
33
|
scan Find hardcoded text in JSX and catalog keys no code reads (--strict)
|
|
31
34
|
extract [paths] Move hardcoded JSX text into the catalog and rewrite the code to read it
|
|
32
35
|
(dry run unless --write; --area <name> puts every entry in one area)
|
|
@@ -38,6 +41,7 @@ Options:
|
|
|
38
41
|
--root <dir> Project root (default: the current directory)
|
|
39
42
|
--format <text|sarif> With lint, scan, status and check: sarif writes a SARIF 2.1.0 log to stdout
|
|
40
43
|
for code scanning, and the usual text to stderr
|
|
44
|
+
--format <xliff|json> With export: the file's format (XLIFF by default on stdout)
|
|
41
45
|
-h, --help Show this help
|
|
42
46
|
`;
|
|
43
47
|
const SARIF_COMMANDS = [
|
|
@@ -46,6 +50,11 @@ const SARIF_COMMANDS = [
|
|
|
46
50
|
"status",
|
|
47
51
|
"check"
|
|
48
52
|
];
|
|
53
|
+
const EXCHANGE_FORMATS = {
|
|
54
|
+
xliff: "xliff",
|
|
55
|
+
xlf: "xliff",
|
|
56
|
+
json: "json"
|
|
57
|
+
};
|
|
49
58
|
/** What the checks that aren't lint rules hold the project to, for the rules in a SARIF log. */
|
|
50
59
|
const RULES = {
|
|
51
60
|
catalog: "The catalog and its translations parse, and each translation keeps the placeholders of its source.",
|
|
@@ -86,7 +95,12 @@ async function main(argv) {
|
|
|
86
95
|
out(HELP);
|
|
87
96
|
return 0;
|
|
88
97
|
}
|
|
89
|
-
if (
|
|
98
|
+
if (command === "export") {
|
|
99
|
+
if (values.format !== void 0 && EXCHANGE_FORMATS[values.format] === void 0) {
|
|
100
|
+
err(`prosebook: export writes xliff or json, not "${values.format}"`);
|
|
101
|
+
return 1;
|
|
102
|
+
}
|
|
103
|
+
} else if (values.format !== void 0 && values.format !== "text" && values.format !== "sarif") {
|
|
90
104
|
err(`prosebook: --format is text or sarif, not "${values.format}"`);
|
|
91
105
|
return 1;
|
|
92
106
|
}
|
|
@@ -116,6 +130,7 @@ async function main(argv) {
|
|
|
116
130
|
break;
|
|
117
131
|
case "accept": return runAccept(config, rest, values.all === true);
|
|
118
132
|
case "todo": return runTodo(config, rest[0], values.prompt === true);
|
|
133
|
+
case "export": return runExport(config, rest[0], rest[1], values.format, values.all === true);
|
|
119
134
|
case "import": return runImport(config, rest[0], rest[1]);
|
|
120
135
|
case "scan":
|
|
121
136
|
code = runScan(config, values.strict === true, report);
|
|
@@ -177,12 +192,13 @@ function runGenerate(config, check, report) {
|
|
|
177
192
|
out(`prosebook: generated files are up to date (${summary})`);
|
|
178
193
|
return 0;
|
|
179
194
|
}
|
|
180
|
-
|
|
195
|
+
const why = (p) => dirname(p) === config.catalog && p.endsWith(".yaml") ? `its first line isn't \`${SCHEMA_LINE}\`, so editors can't find the schema` : "out of date";
|
|
196
|
+
err(`prosebook: out of date, run \`prosebook generate\`:\n ${changed.map((p) => `${rel(config, p)}${why(p) === "out of date" ? "" : `: ${why(p)}`}`).join("\n ")}`);
|
|
181
197
|
report?.rule("out-of-date", RULES["out-of-date"]);
|
|
182
198
|
for (const p of changed) report?.add({
|
|
183
199
|
rule: "out-of-date",
|
|
184
200
|
level: "error",
|
|
185
|
-
message:
|
|
201
|
+
message: `${why(p)}: run \`prosebook generate\``,
|
|
186
202
|
file: rel(config, p)
|
|
187
203
|
});
|
|
188
204
|
return 1;
|
|
@@ -338,6 +354,33 @@ function runTodo(config, locale, prompt) {
|
|
|
338
354
|
if (prompt && todo.count > 0) err(`\nHand this to a model, save its reply, and \`prosebook import ${locale} <reply>\`.`);
|
|
339
355
|
return 0;
|
|
340
356
|
}
|
|
357
|
+
function runExport(config, locale, file, named, all) {
|
|
358
|
+
if (locale === void 0) {
|
|
359
|
+
err("Usage: prosebook export <locale> [file] [--all] [--format xliff|json]");
|
|
360
|
+
return 1;
|
|
361
|
+
}
|
|
362
|
+
const format = EXCHANGE_FORMATS[named ?? (file === void 0 ? "xliff" : extname(file).slice(1).toLowerCase())];
|
|
363
|
+
if (format === void 0) {
|
|
364
|
+
err(`prosebook: ${file} is neither .xlf nor .json: name the format with --format xliff or --format json`);
|
|
365
|
+
return 1;
|
|
366
|
+
}
|
|
367
|
+
const { catalog, stale } = render(config);
|
|
368
|
+
const exported = exportOf(config, catalog, stale.get(locale) ?? /* @__PURE__ */ new Map(), locale, format, all);
|
|
369
|
+
if (exported.count === 0) {
|
|
370
|
+
err(`prosebook: ${locale} is complete; --all exports every entry`);
|
|
371
|
+
return 0;
|
|
372
|
+
}
|
|
373
|
+
if (file === void 0) {
|
|
374
|
+
process.stdout.write(exported.text);
|
|
375
|
+
return 0;
|
|
376
|
+
}
|
|
377
|
+
const path = resolve(file);
|
|
378
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
379
|
+
writeFileSync(path, exported.text);
|
|
380
|
+
out(`prosebook: exported ${exported.count} entr${exported.count === 1 ? "y" : "ies"} for ${locale} to ${file}`);
|
|
381
|
+
if (locale !== config.source) out(`Translate them, then \`prosebook import ${locale} ${file}\`.`);
|
|
382
|
+
return 0;
|
|
383
|
+
}
|
|
341
384
|
function runImport(config, locale, file) {
|
|
342
385
|
if (locale === void 0 || (file === void 0 || file === "-") && process.stdin.isTTY) {
|
|
343
386
|
err("Usage: prosebook import <locale> <file>, or the reply on stdin");
|
|
@@ -464,7 +507,7 @@ export default defineConfig({
|
|
|
464
507
|
// messageFormat: 2,
|
|
465
508
|
});
|
|
466
509
|
`;
|
|
467
|
-
const SAMPLE_CATALOG =
|
|
510
|
+
const SAMPLE_CATALOG = `${SCHEMA_LINE}
|
|
468
511
|
app.title:
|
|
469
512
|
kind: name
|
|
470
513
|
surface: title
|