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
|
@@ -1,169 +1,7 @@
|
|
|
1
|
+
import { d as staticLiteral, p as walk, t as TYPE } from "./icu-Dej0mNsF.js";
|
|
1
2
|
import { isAbsolute, join, resolve } from "node:path";
|
|
2
3
|
import { pathToFileURL } from "node:url";
|
|
3
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
-
import { TYPE as TYPE$1, isDateElement, isNumberElement, isTimeElement, parse } from "@formatjs/icu-messageformat-parser";
|
|
5
|
-
// src/tools/icu.ts
|
|
6
|
-
/**
|
|
7
|
-
* What an ICU message declares, read off its AST: argument types, tags, and whether each number and
|
|
8
|
-
* date style is one the formatter honors. Shared by the loader, the compiler and the lint.
|
|
9
|
-
*/
|
|
10
|
-
function parseMessage(text) {
|
|
11
|
-
return parse(text, {
|
|
12
|
-
shouldParseSkeletons: true,
|
|
13
|
-
requiresOtherClause: true
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* ICU argument types, as TypeScript. A bare `{x}` is a string: a number spliced in raw is printed by
|
|
18
|
-
* `String()`, which knows no locale, so a quantity says `{x, number}` and the type is what asks for it.
|
|
19
|
-
* A `select` narrows to the arms it names, and still takes any string, since `other` catches the rest.
|
|
20
|
-
*/
|
|
21
|
-
function argType(el) {
|
|
22
|
-
switch (el.type) {
|
|
23
|
-
case TYPE$1.argument: return "string";
|
|
24
|
-
case TYPE$1.number:
|
|
25
|
-
case TYPE$1.plural: return "number";
|
|
26
|
-
case TYPE$1.date:
|
|
27
|
-
case TYPE$1.time: return "Date | number";
|
|
28
|
-
case TYPE$1.select: {
|
|
29
|
-
const named = Object.keys(el.options).filter((o) => o !== "other");
|
|
30
|
-
return named.length > 0 ? `${named.map((o) => JSON.stringify(o)).join(" | ")} | (string & {})` : "string";
|
|
31
|
-
}
|
|
32
|
-
case TYPE$1.literal:
|
|
33
|
-
case TYPE$1.pound:
|
|
34
|
-
case TYPE$1.tag:
|
|
35
|
-
default: return;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
/** Calls `visit` on every element, inside plural and select arms and tags too. */
|
|
39
|
-
function walk(elements, visit) {
|
|
40
|
-
for (const el of elements) {
|
|
41
|
-
visit(el);
|
|
42
|
-
if (el.type === TYPE$1.select || el.type === TYPE$1.plural) for (const option of Object.values(el.options)) walk(option.value, visit);
|
|
43
|
-
if (el.type === TYPE$1.tag) walk(el.children, visit);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
function argsOf(elements, clash = () => {}) {
|
|
47
|
-
const args = /* @__PURE__ */ new Map();
|
|
48
|
-
walk(elements, (el) => {
|
|
49
|
-
const type = argType(el);
|
|
50
|
-
if (type === void 0 || !("value" in el) || typeof el.value !== "string") return;
|
|
51
|
-
const seen = args.get(el.value);
|
|
52
|
-
if (seen !== void 0 && seen !== type) clash(el.value);
|
|
53
|
-
else args.set(el.value, type);
|
|
54
|
-
});
|
|
55
|
-
return args;
|
|
56
|
-
}
|
|
57
|
-
function tagsOf(elements) {
|
|
58
|
-
const tags = /* @__PURE__ */ new Set();
|
|
59
|
-
walk(elements, (el) => {
|
|
60
|
-
if (el.type === TYPE$1.tag) tags.add(el.value);
|
|
61
|
-
});
|
|
62
|
-
return [...tags].sort();
|
|
63
|
-
}
|
|
64
|
-
/** A message with no placeholders and no tags renders to one literal, which ships as a plain string. */
|
|
65
|
-
function staticLiteral(ast) {
|
|
66
|
-
if (ast.length === 0) return "";
|
|
67
|
-
if (ast.length === 1 && ast[0].type === TYPE$1.literal) return ast[0].value;
|
|
68
|
-
}
|
|
69
|
-
const NUMBER_STYLES = {
|
|
70
|
-
integer: { maximumFractionDigits: 0 },
|
|
71
|
-
percent: { style: "percent" }
|
|
72
|
-
};
|
|
73
|
-
const DATE_STYLES = [
|
|
74
|
-
"short",
|
|
75
|
-
"medium",
|
|
76
|
-
"long",
|
|
77
|
-
"full"
|
|
78
|
-
];
|
|
79
|
-
/** The `Intl` options a number element formats with. Throws, with the fix, where the style is wrong. */
|
|
80
|
-
function numberOptions(el) {
|
|
81
|
-
const style = el.style;
|
|
82
|
-
if (style === null || style === void 0) return void 0;
|
|
83
|
-
if (typeof style === "string") {
|
|
84
|
-
if (style === "currency") throw new Error(`{${el.value}} needs its currency named: {${el.value}, number, ::currency/EUR}`);
|
|
85
|
-
const named = NUMBER_STYLES[style];
|
|
86
|
-
if (named === void 0) throw new Error(`{${el.value}} asks for number style "${style}": integer, percent, or a ::skeleton`);
|
|
87
|
-
return named;
|
|
88
|
-
}
|
|
89
|
-
const unknown = style.tokens.filter((t) => !stemIsKnown(t.stem, t.options)).map((t) => t.stem);
|
|
90
|
-
if (unknown.length > 0) throw new Error(`{${el.value}} has skeleton stems the formatter drops without a word: ${unknown.join(", ")}`);
|
|
91
|
-
const options = style.parsedOptions;
|
|
92
|
-
try {
|
|
93
|
-
new Intl.NumberFormat("en", options).resolvedOptions();
|
|
94
|
-
} catch (e) {
|
|
95
|
-
throw new Error(`{${el.value}} has a skeleton Intl refuses: ${e.message}`, { cause: e });
|
|
96
|
-
}
|
|
97
|
-
return options;
|
|
98
|
-
}
|
|
99
|
-
/** The `Intl` options a date or time element formats with. Throws, with the fix, where the style is wrong. */
|
|
100
|
-
function dateOptions(el) {
|
|
101
|
-
const style = el.style;
|
|
102
|
-
const field = el.type === TYPE$1.time ? "timeStyle" : "dateStyle";
|
|
103
|
-
if (style === null || style === void 0) return el.type === TYPE$1.time ? { timeStyle: "short" } : void 0;
|
|
104
|
-
if (typeof style === "string") {
|
|
105
|
-
if (!DATE_STYLES.includes(style)) {
|
|
106
|
-
const kind = el.type === TYPE$1.time ? "time" : "date";
|
|
107
|
-
throw new Error(`{${el.value}} asks for ${kind} style "${style}": ${DATE_STYLES.join(", ")}, or a ::skeleton`);
|
|
108
|
-
}
|
|
109
|
-
return { [field]: style };
|
|
110
|
-
}
|
|
111
|
-
if (Object.keys(style.parsedOptions).length === 0) throw new Error(`{${el.value}} has a date skeleton the formatter reads nothing from: "${style.pattern}"`);
|
|
112
|
-
try {
|
|
113
|
-
new Intl.DateTimeFormat("en", style.parsedOptions).resolvedOptions();
|
|
114
|
-
} catch (e) {
|
|
115
|
-
throw new Error(`{${el.value}} has a skeleton Intl refuses: ${e.message}`, { cause: e });
|
|
116
|
-
}
|
|
117
|
-
return style.parsedOptions;
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Whether the formatter understands a skeleton stem. One it does not is dropped without a word, so
|
|
121
|
-
* `::compact-shortt` reaches the screen as `5` where `5K` was written. Every stem it understands sets
|
|
122
|
-
* at least one option, so an empty result is the answer.
|
|
123
|
-
*/
|
|
124
|
-
function stemIsKnown(stem, options) {
|
|
125
|
-
try {
|
|
126
|
-
const el = parse(`{n, number, ::${[stem, ...options].join("/")}}`, { shouldParseSkeletons: true })[0];
|
|
127
|
-
if (el === void 0 || !isNumberElement(el) || typeof el.style !== "object" || el.style === null) return false;
|
|
128
|
-
return Object.keys(el.style.parsedOptions).length > 0;
|
|
129
|
-
} catch {
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
/** Every format problem in a message, as sentences. */
|
|
134
|
-
function formatProblems(ast) {
|
|
135
|
-
const found = [];
|
|
136
|
-
walk(ast, (el) => {
|
|
137
|
-
try {
|
|
138
|
-
if (isNumberElement(el)) numberOptions(el);
|
|
139
|
-
if (isDateElement(el) || isTimeElement(el)) dateOptions(el);
|
|
140
|
-
} catch (e) {
|
|
141
|
-
found.push(e.message);
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
return found;
|
|
145
|
-
}
|
|
146
|
-
/** How each number or date argument is formatted, for the generated doc comment: `date medium, time short`. */
|
|
147
|
-
function formatsOf(ast) {
|
|
148
|
-
const formats = /* @__PURE__ */ new Map();
|
|
149
|
-
walk(ast, (el) => {
|
|
150
|
-
if (!isNumberElement(el) && !isDateElement(el) && !isTimeElement(el)) return;
|
|
151
|
-
const style = el.style;
|
|
152
|
-
if (style === null || style === void 0) return;
|
|
153
|
-
const kind = el.type === TYPE$1.number ? "number" : el.type === TYPE$1.date ? "date" : "time";
|
|
154
|
-
let said;
|
|
155
|
-
if (typeof style === "string") said = `${kind} \`${style}\``;
|
|
156
|
-
else if ("mf2" in style) {
|
|
157
|
-
const { name, options } = style.mf2;
|
|
158
|
-
said = `\`${[`:${name}`, ...Object.entries(options).map(([k, v]) => `${k}=${v}`)].join(" ")}\``;
|
|
159
|
-
} else if ("tokens" in style) said = `${kind} \`::${style.tokens.map((t) => [t.stem, ...t.options].join("/")).join(" ")}\``;
|
|
160
|
-
else said = `${kind} \`::${style.pattern}\``;
|
|
161
|
-
const list = formats.get(el.value) ?? [];
|
|
162
|
-
if (!list.includes(said)) list.push(said);
|
|
163
|
-
formats.set(el.value, list);
|
|
164
|
-
});
|
|
165
|
-
return new Map([...formats].map(([name, list]) => [name, list.join(", ")]));
|
|
166
|
-
}
|
|
167
5
|
// src/tools/lint.ts
|
|
168
6
|
/** Typed plugin, for `lint.plugins`. */
|
|
169
7
|
function definePlugin(plugin) {
|
|
@@ -181,9 +19,9 @@ const VERBATIM_TAGS = /* @__PURE__ */ new Set([
|
|
|
181
19
|
]);
|
|
182
20
|
function renderAs(elements, arm) {
|
|
183
21
|
let out = "";
|
|
184
|
-
for (const el of elements) if (el.type === TYPE
|
|
185
|
-
else if (el.type === TYPE
|
|
186
|
-
else if (el.type === TYPE
|
|
22
|
+
for (const el of elements) if (el.type === TYPE.literal) out += el.value;
|
|
23
|
+
else if (el.type === TYPE.tag) out += VERBATIM_TAGS.has(el.value) ? "" : renderAs(el.children, arm);
|
|
24
|
+
else if (el.type === TYPE.plural || el.type === TYPE.select) {
|
|
187
25
|
const option = arm === null ? void 0 : el.options[arm] ?? el.options["other"];
|
|
188
26
|
out += option === void 0 ? "" : renderAs(option.value, arm);
|
|
189
27
|
} else out += "";
|
|
@@ -192,17 +30,17 @@ function renderAs(elements, arm) {
|
|
|
192
30
|
function readings(message) {
|
|
193
31
|
const literal = staticLiteral(message);
|
|
194
32
|
const ast = literal === void 0 ? message : [{
|
|
195
|
-
type: TYPE
|
|
33
|
+
type: TYPE.literal,
|
|
196
34
|
value: literal
|
|
197
35
|
}];
|
|
198
36
|
const arms = /* @__PURE__ */ new Set();
|
|
199
37
|
const collect = (els) => {
|
|
200
38
|
for (const el of els) {
|
|
201
|
-
if (el.type === TYPE
|
|
39
|
+
if (el.type === TYPE.plural || el.type === TYPE.select) for (const [name, option] of Object.entries(el.options)) {
|
|
202
40
|
arms.add(name);
|
|
203
41
|
collect(option.value);
|
|
204
42
|
}
|
|
205
|
-
if (el.type === TYPE
|
|
43
|
+
if (el.type === TYPE.tag) collect(el.children);
|
|
206
44
|
}
|
|
207
45
|
};
|
|
208
46
|
collect(ast);
|
|
@@ -264,10 +102,14 @@ const NOT_PLURAL = /* @__PURE__ */ new Set([
|
|
|
264
102
|
"news",
|
|
265
103
|
"series"
|
|
266
104
|
]);
|
|
267
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* The singular of an English plural noun, or nothing where it doesn't look like one. A word with no
|
|
107
|
+
* vowel is a unit or an abbreviation (`kbps`, `fps`, `hrs`), which reads the same after 1.
|
|
108
|
+
*/
|
|
268
109
|
function singularOf(word) {
|
|
269
110
|
const lower = word.toLowerCase();
|
|
270
111
|
if (word.length < 3 || !lower.endsWith("s") || lower.endsWith("ss") || NOT_PLURAL.has(lower)) return void 0;
|
|
112
|
+
if (!/[aeiouy]/.test(lower)) return void 0;
|
|
271
113
|
if (/[^aeiou]ies$/.test(lower)) return `${word.slice(0, -3)}y`;
|
|
272
114
|
if (/(?:s|x|z|ch|sh)es$/.test(lower)) return word.slice(0, -2);
|
|
273
115
|
return word.slice(0, -1);
|
|
@@ -292,24 +134,33 @@ const CONTRACTIONS = [
|
|
|
292
134
|
function eachRun(elements, visit, counted = /* @__PURE__ */ new Set()) {
|
|
293
135
|
visit(elements, counted);
|
|
294
136
|
for (const el of elements) {
|
|
295
|
-
if (el.type === TYPE
|
|
296
|
-
if (el.type === TYPE
|
|
297
|
-
if (el.type === TYPE
|
|
137
|
+
if (el.type === TYPE.plural) for (const o of Object.values(el.options)) eachRun(o.value, visit, /* @__PURE__ */ new Set([...counted, el.value]));
|
|
138
|
+
if (el.type === TYPE.select) for (const o of Object.values(el.options)) eachRun(o.value, visit, counted);
|
|
139
|
+
if (el.type === TYPE.tag) eachRun(el.children, visit, counted);
|
|
298
140
|
}
|
|
299
141
|
}
|
|
300
142
|
/** A plain placeholder named as a count. A number placeholder is one whatever its name. */
|
|
301
143
|
const COUNT_NAME = /^(?:n|num|count|total|amount|quantity)$|(?:count|total)$/i;
|
|
302
144
|
/**
|
|
303
|
-
* Whether an arm ends with a stop; nothing for an arm with no words
|
|
304
|
-
*
|
|
145
|
+
* Whether an arm ends with a stop; nothing for an arm with no words, or one that trails off in an
|
|
146
|
+
* ellipsis (`Saving…`, `Save as…`), which is neither. Which stop is the arm's own: a question in
|
|
147
|
+
* `one` and a second sentence after it in `other` both end well.
|
|
305
148
|
*/
|
|
306
149
|
function endsWithStop(arm) {
|
|
307
150
|
const text = prose(renderAs(arm, "other")).trim();
|
|
308
|
-
|
|
151
|
+
if (text.replaceAll("", "").trim() === "" || /(?:…|\.{3})["')”»」』]*$/u.test(text)) return void 0;
|
|
152
|
+
return hasStop(text);
|
|
309
153
|
}
|
|
310
154
|
/** Ends with a stop in any script (`。`, `؟`, `;` as Greek writes `?`), though not a comma. */
|
|
311
155
|
const hasStop = (text) => /(?![,,、،])[\p{Terminal_Punctuation}…]["')”»」』]*$/u.test(prose(text).trim());
|
|
312
156
|
const CURRENCY_BEFORE = /[$€£¥₹]\s?$/;
|
|
157
|
+
/**
|
|
158
|
+
* The ways a path through menus is written, the separator in a group: an arrow or `›` however
|
|
159
|
+
* spaced, and `>` or `/` only spaced between words or placeholders, with a capital after a slash,
|
|
160
|
+
* since "2 > 1" and "and / or" are no paths. The match takes the spaces around, so the fix spaces
|
|
161
|
+
* the separator.
|
|
162
|
+
*/
|
|
163
|
+
const PATH_SEPARATORS = new RegExp(String.raw`\s*(→|->|›)\s*|(?<=[\p{L}${""}])\s+(>)\s+(?=[\p{L}${""}])|(?<=\p{L})\s+(\/)\s+(?=\p{Lu})`, "gu");
|
|
313
164
|
const OFF_SCREEN = /* @__PURE__ */ new Set(["aria"]);
|
|
314
165
|
const an = (word = "string") => `${/^[aeiou]/i.test(word) ? "an" : "a"} ${word}`;
|
|
315
166
|
const CORE_RULES = {
|
|
@@ -412,6 +263,21 @@ const CORE_RULES = {
|
|
|
412
263
|
}];
|
|
413
264
|
}
|
|
414
265
|
},
|
|
266
|
+
path: {
|
|
267
|
+
description: "A path through menus or settings is written with `lint.pathSeparator`, spaced: Settings › General.",
|
|
268
|
+
check: ({ text, config }) => {
|
|
269
|
+
const use = config.lint.pathSeparator;
|
|
270
|
+
for (const v of text.variants) for (const m of v.matchAll(PATH_SEPARATORS)) {
|
|
271
|
+
if ((m[1] ?? m[2] ?? m[3]) === use) continue;
|
|
272
|
+
return [{
|
|
273
|
+
message: `a path is written with a spaced ${use}`,
|
|
274
|
+
match: m[0],
|
|
275
|
+
fix: ` ${use} `
|
|
276
|
+
}];
|
|
277
|
+
}
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
},
|
|
415
281
|
contractions: {
|
|
416
282
|
description: "`could not`, `is not` and `cannot` are written as contractions.",
|
|
417
283
|
check: ({ text }) => CONTRACTIONS.flatMap(([pattern, short]) => {
|
|
@@ -440,10 +306,10 @@ const CORE_RULES = {
|
|
|
440
306
|
check: ({ ast }) => {
|
|
441
307
|
const said = [];
|
|
442
308
|
eachRun(ast, (run, counted) => run.forEach((el, i) => {
|
|
443
|
-
if (el.type !== TYPE
|
|
444
|
-
if (counted.has(el.value) || el.type === TYPE
|
|
309
|
+
if (el.type !== TYPE.number && el.type !== TYPE.argument) return;
|
|
310
|
+
if (counted.has(el.value) || el.type === TYPE.argument && !COUNT_NAME.test(el.value)) return;
|
|
445
311
|
const next = run[i + 1];
|
|
446
|
-
const word = next?.type === TYPE
|
|
312
|
+
const word = next?.type === TYPE.literal ? /^\s+(\p{L}+)(?![\p{L}'])/u.exec(next.value)?.[1] : void 0;
|
|
447
313
|
const one = word === void 0 ? void 0 : singularOf(word);
|
|
448
314
|
if (word !== void 0 && one !== void 0) said.push({
|
|
449
315
|
message: `${el.value} is followed by "${word}", which reads "1 ${word}": make it a plural with a "# ${one}" arm`,
|
|
@@ -460,13 +326,13 @@ const CORE_RULES = {
|
|
|
460
326
|
check: ({ ast, locale, config }) => {
|
|
461
327
|
const said = [];
|
|
462
328
|
walk(ast, (el) => {
|
|
463
|
-
if (el.type !== TYPE
|
|
329
|
+
if (el.type !== TYPE.plural || el.pluralType !== "cardinal") return;
|
|
464
330
|
const { one, other } = el.options;
|
|
465
331
|
if (one === void 0) return;
|
|
466
332
|
if (locale === config.source && other !== void 0 && renderAs(one.value, "other") === renderAs(other.value, "other")) said.push({ message: `the one and other arms of ${el.value} say the same: write the singular in one` });
|
|
467
333
|
let literalOne = false;
|
|
468
334
|
walk(one.value, (inner) => {
|
|
469
|
-
if (inner.type === TYPE
|
|
335
|
+
if (inner.type === TYPE.literal && /(?<![\d.,])1(?![\d.,])/.test(inner.value)) literalOne = true;
|
|
470
336
|
});
|
|
471
337
|
if (!literalOne) return;
|
|
472
338
|
const message = `the one arm of ${el.value} writes 1, which one is not in every language: write #, or make the arm =1`;
|
|
@@ -485,7 +351,7 @@ const CORE_RULES = {
|
|
|
485
351
|
check: ({ ast }) => {
|
|
486
352
|
const said = [];
|
|
487
353
|
walk(ast, (el) => {
|
|
488
|
-
if (el.type !== TYPE
|
|
354
|
+
if (el.type !== TYPE.plural && el.type !== TYPE.select) return;
|
|
489
355
|
if (new Set(Object.values(el.options).flatMap((o) => endsWithStop(o.value) ?? [])).size > 1) said.push(`some arms of ${el.value} end with a stop and some without: end them alike`);
|
|
490
356
|
});
|
|
491
357
|
return said;
|
|
@@ -502,12 +368,12 @@ const CORE_RULES = {
|
|
|
502
368
|
match
|
|
503
369
|
});
|
|
504
370
|
eachRun(ast, (run) => run.forEach((el, i) => {
|
|
505
|
-
if (el.type !== TYPE
|
|
371
|
+
if (el.type !== TYPE.number && el.type !== TYPE.argument) return;
|
|
506
372
|
const before = run[i - 1];
|
|
507
373
|
const after = run[i + 1];
|
|
508
|
-
const symbol = before?.type === TYPE
|
|
374
|
+
const symbol = before?.type === TYPE.literal ? CURRENCY_BEFORE.exec(before.value)?.[0].trim() : void 0;
|
|
509
375
|
if (symbol !== void 0) say(`"${symbol}" is typed before ${el.value}: format it as a currency, which puts the symbol where the locale does`, symbol);
|
|
510
|
-
if (after?.type === TYPE
|
|
376
|
+
if (after?.type === TYPE.literal && /^\s?%/.test(after.value)) say(`"%" is typed after ${el.value}: format it as a percent, which takes the fraction (0.5 for 50%)`, "%");
|
|
511
377
|
}));
|
|
512
378
|
const grouped = text.variants.map((v) => /\d{1,3}(?:,\d{3})+(?!\d)/.exec(v)?.[0]).find((g) => g !== void 0);
|
|
513
379
|
if (grouped !== void 0) say(`"${grouped}" groups its digits the English way: make it a placeholder formatted as a number`, grouped);
|
|
@@ -661,9 +527,11 @@ function reasonProblem(why) {
|
|
|
661
527
|
/** A locale is one a rule reads when it names its language, or the locale and a region of it. */
|
|
662
528
|
const readsLocale = (locales, locale, sourceLocale) => locales === void 0 ? locale === sourceLocale : locales === "all" || locales.some((l) => locale.toLowerCase() === l.toLowerCase() || locale.toLowerCase().startsWith(`${l.toLowerCase()}-`));
|
|
663
529
|
function applies(rule, message, locale, sourceLocale) {
|
|
664
|
-
const { kind, surface, locale: locales } = rule.on ?? {};
|
|
665
|
-
return readsLocale(locales, locale, sourceLocale) && (kind === void 0 || message.kind !== void 0 && kind.includes(message.kind)) && (surface === void 0 || message.surface !== void 0 && surface.includes(message.surface));
|
|
530
|
+
const { kind, surface, area, locale: locales } = rule.on ?? {};
|
|
531
|
+
return readsLocale(locales, locale, sourceLocale) && (kind === void 0 || message.kind !== void 0 && kind.includes(message.kind)) && (surface === void 0 || message.surface !== void 0 && surface.includes(message.surface)) && (area === void 0 || area.includes(message.area));
|
|
666
532
|
}
|
|
533
|
+
/** A rule's severity for an entry: its area's, where `lint.areas` sets one. */
|
|
534
|
+
const severityOf = (config, name, message) => config.lint.areas[message.area]?.[name] ?? config.lint.rules[name]?.severity;
|
|
667
535
|
/**
|
|
668
536
|
* Where the message's literals are written in its text, each looked for after the one before. A
|
|
669
537
|
* literal escaped in the text (`''`, `\{`) isn't written as read, so it has no place, and nor does a
|
|
@@ -673,7 +541,7 @@ function literalsIn(ast, text) {
|
|
|
673
541
|
const found = [];
|
|
674
542
|
let from = 0;
|
|
675
543
|
walk(ast, (el) => {
|
|
676
|
-
if (el.type !== TYPE
|
|
544
|
+
if (el.type !== TYPE.literal || el.value === "") return;
|
|
677
545
|
const next = text.indexOf(el.value, from);
|
|
678
546
|
const start = next === -1 ? text.indexOf(el.value) : next;
|
|
679
547
|
if (start === -1) return;
|
|
@@ -740,7 +608,8 @@ function lint(messages, config) {
|
|
|
740
608
|
config
|
|
741
609
|
};
|
|
742
610
|
let literals;
|
|
743
|
-
for (const [name, { rule
|
|
611
|
+
for (const [name, { rule }] of rules) {
|
|
612
|
+
const severity = severityOf(config, name, message);
|
|
744
613
|
if (severity === "off" || !applies(rule, message, locale, config.source)) continue;
|
|
745
614
|
let said;
|
|
746
615
|
try {
|
|
@@ -782,12 +651,11 @@ function lint(messages, config) {
|
|
|
782
651
|
rule: "allow",
|
|
783
652
|
severity: "error"
|
|
784
653
|
};
|
|
785
|
-
|
|
786
|
-
if (rule === void 0) findings.push({
|
|
654
|
+
if (config.lint.rules[name] === void 0) findings.push({
|
|
787
655
|
...at,
|
|
788
656
|
message: `allow.${name} is not a rule: ${rules.map(([n]) => n).join(", ")}`
|
|
789
657
|
});
|
|
790
|
-
else if (
|
|
658
|
+
else if (severityOf(config, name, message) === "off") continue;
|
|
791
659
|
else if (!fired.has(name)) findings.push({
|
|
792
660
|
...at,
|
|
793
661
|
message: `allow.${name} exempts the entry from a rule it passes: remove it`
|
|
@@ -1046,6 +914,11 @@ const STRICT_LISTS = {
|
|
|
1046
914
|
* `prosebook/config`: where the catalog lives, what an entry may say about itself, and how strictly
|
|
1047
915
|
* its words are checked. Written as `prosebook.config.ts` (or `.js`, `.mjs`, `.json`) at the project root.
|
|
1048
916
|
*/
|
|
917
|
+
const NATIVE_NAMES = {
|
|
918
|
+
rust: "Locale",
|
|
919
|
+
swift: "Copy",
|
|
920
|
+
kotlin: "Copy"
|
|
921
|
+
};
|
|
1049
922
|
/** Typed config, for `prosebook.config.ts`. */
|
|
1050
923
|
function defineConfig(config) {
|
|
1051
924
|
return config;
|
|
@@ -1072,6 +945,8 @@ function resolveConfig(root, config) {
|
|
|
1072
945
|
const at = (path) => isAbsolute(path) ? path : resolve(root, path);
|
|
1073
946
|
const lint = config.lint ?? {};
|
|
1074
947
|
const strict = (lint.extends ?? "recommended") === "strict";
|
|
948
|
+
const rules = lintRules(lint);
|
|
949
|
+
for (const [area, severities] of Object.entries(lint.areas ?? {})) for (const name of Object.keys(severities)) if (!(name in rules)) throw new Error(`lint.areas.${area}.${name} is not a rule: name one of ${Object.keys(rules).join(", ")}`);
|
|
1075
950
|
return {
|
|
1076
951
|
root,
|
|
1077
952
|
catalog: at(config.catalog ?? "copy/catalog"),
|
|
@@ -1080,6 +955,7 @@ function resolveConfig(root, config) {
|
|
|
1080
955
|
lock: at(config.lock ?? "copy/lock.json"),
|
|
1081
956
|
source: config.source ?? "en",
|
|
1082
957
|
messageFormat: config.messageFormat ?? 1,
|
|
958
|
+
isolate: config.isolate ?? "rtl",
|
|
1083
959
|
bundled: config.bundled ?? [],
|
|
1084
960
|
pseudo: config.pseudo ?? false,
|
|
1085
961
|
stale: config.stale ?? "use",
|
|
@@ -1089,15 +965,25 @@ function resolveConfig(root, config) {
|
|
|
1089
965
|
kinds: config.kinds ?? DEFAULT_KINDS,
|
|
1090
966
|
surfaces: config.surfaces ?? DEFAULT_SURFACES,
|
|
1091
967
|
lint: {
|
|
1092
|
-
rules
|
|
968
|
+
rules,
|
|
969
|
+
areas: lint.areas ?? {},
|
|
1093
970
|
banned: [...strict ? STRICT_LISTS.banned : [], ...lint.banned ?? []],
|
|
1094
971
|
spelling: {
|
|
1095
972
|
...strict ? STRICT_LISTS.spelling : {},
|
|
1096
973
|
...lint.spelling
|
|
1097
974
|
},
|
|
1098
975
|
concepts: { ...lint.concepts },
|
|
1099
|
-
properNouns: lint.properNouns ?? []
|
|
976
|
+
properNouns: lint.properNouns ?? [],
|
|
977
|
+
pathSeparator: lint.pathSeparator ?? "›"
|
|
1100
978
|
},
|
|
979
|
+
native: (config.native ?? []).map((target, i) => {
|
|
980
|
+
if (!(target.language in NATIVE_NAMES)) throw new Error(`native[${i}].language is "${target.language}": name one of ${Object.keys(NATIVE_NAMES).join(", ")}`);
|
|
981
|
+
return {
|
|
982
|
+
...target,
|
|
983
|
+
out: at(target.out),
|
|
984
|
+
name: target.name ?? NATIVE_NAMES[target.language]
|
|
985
|
+
};
|
|
986
|
+
}),
|
|
1101
987
|
scan: {
|
|
1102
988
|
dirs: (config.scan?.dirs ?? ["src"]).map(at),
|
|
1103
989
|
extensions: config.scan?.extensions ?? [
|
|
@@ -1141,6 +1027,6 @@ function lintRules(lint) {
|
|
|
1141
1027
|
for (const name of Object.keys(lint.rules ?? {})) if (!(name in rules)) throw new Error(`lint.rules.${name} is not a rule: name one of ${Object.keys(rules).join(", ")}`);
|
|
1142
1028
|
return rules;
|
|
1143
1029
|
}
|
|
1144
|
-
export {
|
|
1030
|
+
export { words as _, DEFAULT_KINDS as a, CORE_RULES as c, compileRule as d, definePlugin as f, singularOf as g, sentences as h, resolveConfig as i, HOLE as l, phrase as m, defineConfig as n, DEFAULT_SURFACES as o, lint as p, loadConfig as r, STRICT_LISTS as s, CONFIG_FILES as t, applyEdits as u };
|
|
1145
1031
|
|
|
1146
|
-
//# sourceMappingURL=config-
|
|
1032
|
+
//# sourceMappingURL=config-BPSmoYel.js.map
|