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.
Files changed (114) hide show
  1. package/README.md +311 -57
  2. package/dist/bun.d.ts +34 -0
  3. package/dist/bun.d.ts.map +1 -0
  4. package/dist/bun.js +98 -0
  5. package/dist/bun.js.map +1 -0
  6. package/dist/bundle-DboItAre.js +111 -0
  7. package/dist/bundle-DboItAre.js.map +1 -0
  8. package/dist/bundle-KD-DiiFa.d.ts +6 -0
  9. package/dist/bundle-KD-DiiFa.d.ts.map +1 -0
  10. package/dist/cli.js +51 -56
  11. package/dist/cli.js.map +1 -1
  12. package/dist/compile-xF3F4oIp.d.ts +16 -0
  13. package/dist/compile-xF3F4oIp.d.ts.map +1 -0
  14. package/dist/{config-CxVMn19f.js → config-BPSmoYel.js} +82 -196
  15. package/dist/config-BPSmoYel.js.map +1 -0
  16. package/dist/{config-CF7HziDa.d.ts → config-DdMkKt_N.d.ts} +85 -7
  17. package/dist/config-DdMkKt_N.d.ts.map +1 -0
  18. package/dist/config.d.ts +2 -2
  19. package/dist/config.js +1 -1
  20. package/dist/{copy-DXnzXsJa.d.ts → copy-B3V_QRkT.d.ts} +20 -7
  21. package/dist/copy-B3V_QRkT.d.ts.map +1 -0
  22. package/dist/copy-Bo_FIoLy.js +154 -0
  23. package/dist/copy-Bo_FIoLy.js.map +1 -0
  24. package/dist/endpoints-CnUfCkIk.js +96 -0
  25. package/dist/endpoints-CnUfCkIk.js.map +1 -0
  26. package/dist/generate-9pR3i5Bj.js +5173 -0
  27. package/dist/generate-9pR3i5Bj.js.map +1 -0
  28. package/dist/helpers-BWGcD0kS.js +96 -0
  29. package/dist/helpers-BWGcD0kS.js.map +1 -0
  30. package/dist/icu-Dej0mNsF.js +218 -0
  31. package/dist/icu-Dej0mNsF.js.map +1 -0
  32. package/dist/index.d.ts +12 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +4 -188
  35. package/dist/inspector.d.ts +4 -1
  36. package/dist/inspector.d.ts.map +1 -1
  37. package/dist/inspector.js +7 -2
  38. package/dist/inspector.js.map +1 -1
  39. package/dist/locale-DpQA1pqH.js +70 -0
  40. package/dist/locale-DpQA1pqH.js.map +1 -0
  41. package/dist/next-loader.d.ts +11 -0
  42. package/dist/next-loader.d.ts.map +1 -0
  43. package/dist/next-loader.js +17 -0
  44. package/dist/next-loader.js.map +1 -0
  45. package/dist/next.d.ts +30 -0
  46. package/dist/next.d.ts.map +1 -0
  47. package/dist/next.js +160 -0
  48. package/dist/next.js.map +1 -0
  49. package/dist/react/index.d.ts +3 -3
  50. package/dist/react/index.d.ts.map +1 -1
  51. package/dist/react/index.js.map +1 -1
  52. package/dist/react/server.d.ts +20 -0
  53. package/dist/react/server.d.ts.map +1 -0
  54. package/dist/react/server.js +34 -0
  55. package/dist/react/server.js.map +1 -0
  56. package/dist/{sarif-PkPvsB07.js → sarif-CWyTAQ0y.js} +103 -87
  57. package/dist/sarif-CWyTAQ0y.js.map +1 -0
  58. package/dist/scan-Bx0_yDSi.js +387 -0
  59. package/dist/scan-Bx0_yDSi.js.map +1 -0
  60. package/dist/tools.d.ts +83 -64
  61. package/dist/tools.d.ts.map +1 -1
  62. package/dist/tools.js +5 -4
  63. package/dist/vite.d.ts +14 -4
  64. package/dist/vite.d.ts.map +1 -1
  65. package/dist/vite.js +60 -114
  66. package/dist/vite.js.map +1 -1
  67. package/package.json +37 -4
  68. package/src/bun.ts +132 -0
  69. package/src/cli.ts +60 -58
  70. package/src/index.ts +1 -1
  71. package/src/inspector.ts +8 -3
  72. package/src/next-loader.ts +16 -0
  73. package/src/next.ts +188 -0
  74. package/src/react/index.tsx +6 -6
  75. package/src/react/server.ts +37 -0
  76. package/src/runtime/copy.ts +22 -3
  77. package/src/runtime/helpers.ts +54 -18
  78. package/src/runtime/locale.ts +19 -0
  79. package/src/runtime/types.ts +21 -6
  80. package/src/tools/bundle.ts +131 -0
  81. package/src/tools/catalog.ts +2 -2
  82. package/src/tools/compile.ts +61 -13
  83. package/src/tools/config.ts +58 -3
  84. package/src/tools/endpoints.ts +121 -0
  85. package/src/tools/entry.ts +77 -0
  86. package/src/tools/generate.ts +32 -32
  87. package/src/tools/icu.ts +93 -8
  88. package/src/tools/index.ts +20 -4
  89. package/src/tools/lint.ts +49 -12
  90. package/src/tools/lock.ts +7 -0
  91. package/src/tools/mf1.ts +21 -7
  92. package/src/tools/mf2.ts +249 -57
  93. package/src/tools/migrate.ts +3 -2
  94. package/src/tools/native/emit.ts +87 -0
  95. package/src/tools/native/index.ts +36 -0
  96. package/src/tools/native/kotlin.ts +269 -0
  97. package/src/tools/native/model.ts +280 -0
  98. package/src/tools/native/number.ts +210 -0
  99. package/src/tools/native/plurals.ts +103 -0
  100. package/src/tools/native/rust.ts +367 -0
  101. package/src/tools/native/swift.ts +326 -0
  102. package/src/tools/scan.ts +134 -4
  103. package/src/tools/syntax.ts +3 -3
  104. package/src/tools/todo.ts +118 -0
  105. package/src/vite.ts +74 -116
  106. package/dist/config-CF7HziDa.d.ts.map +0 -1
  107. package/dist/config-CxVMn19f.js.map +0 -1
  108. package/dist/copy-DXnzXsJa.d.ts.map +0 -1
  109. package/dist/helpers-BPuPGjQP.js +0 -61
  110. package/dist/helpers-BPuPGjQP.js.map +0 -1
  111. package/dist/index.js.map +0 -1
  112. package/dist/sarif-PkPvsB07.js.map +0 -1
  113. package/dist/scan-B0tZ7y-D.js +0 -2447
  114. 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$1.literal) out += el.value;
185
- else if (el.type === TYPE$1.tag) out += VERBATIM_TAGS.has(el.value) ? "⁣" : renderAs(el.children, arm);
186
- else if (el.type === TYPE$1.plural || el.type === TYPE$1.select) {
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$1.literal,
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$1.plural || el.type === TYPE$1.select) for (const [name, option] of Object.entries(el.options)) {
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$1.tag) collect(el.children);
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
- /** The singular of an English plural noun, or nothing where it doesn't look like one. */
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$1.plural) for (const o of Object.values(el.options)) eachRun(o.value, visit, /* @__PURE__ */ new Set([...counted, el.value]));
296
- if (el.type === TYPE$1.select) for (const o of Object.values(el.options)) eachRun(o.value, visit, counted);
297
- if (el.type === TYPE$1.tag) eachRun(el.children, visit, counted);
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. Which stop is the arm's own: a
304
- * question in `one` and a second sentence after it in `other` both end well.
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
- return text.replaceAll("⁣", "").trim() === "" ? void 0 : hasStop(text);
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$1.number && el.type !== TYPE$1.argument) return;
444
- if (counted.has(el.value) || el.type === TYPE$1.argument && !COUNT_NAME.test(el.value)) return;
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$1.literal ? /^\s+(\p{L}+)(?![\p{L}'])/u.exec(next.value)?.[1] : void 0;
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$1.plural || el.pluralType !== "cardinal") return;
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$1.literal && /(?<![\d.,])1(?![\d.,])/.test(inner.value)) literalOne = true;
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$1.plural && el.type !== TYPE$1.select) return;
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$1.number && el.type !== TYPE$1.argument) return;
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$1.literal ? CURRENCY_BEFORE.exec(before.value)?.[0].trim() : void 0;
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$1.literal && /^\s?%/.test(after.value)) say(`"%" is typed after ${el.value}: format it as a percent, which takes the fraction (0.5 for 50%)`, "%");
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$1.literal || el.value === "") return;
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, severity }] of rules) {
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
- const rule = config.lint.rules[name];
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 (rule.severity === "off") continue;
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: lintRules(lint),
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 { numberOptions as C, walk as D, tagsOf as E, formatsOf as S, staticLiteral as T, words as _, DEFAULT_KINDS as a, dateOptions as b, 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, TYPE$1 as v, parseMessage as w, formatProblems as x, argsOf as y };
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-CxVMn19f.js.map
1032
+ //# sourceMappingURL=config-BPSmoYel.js.map