chapterjs 0.1.2 → 0.2.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 (318) hide show
  1. package/dist/assets/asset.d.ts +46 -0
  2. package/dist/assets/asset.js +86 -0
  3. package/dist/assets/public.d.ts +19 -0
  4. package/dist/assets/public.js +120 -0
  5. package/dist/cache/cache.d.ts +62 -0
  6. package/dist/cache/cache.js +80 -0
  7. package/dist/cache/store.d.ts +45 -0
  8. package/dist/cache/store.js +53 -0
  9. package/dist/cli/build.d.ts +41 -0
  10. package/dist/cli/build.js +205 -0
  11. package/dist/cli/cli.js +108 -25
  12. package/dist/cli/cluster.d.ts +70 -0
  13. package/dist/cli/cluster.js +213 -0
  14. package/dist/cli/dev.d.ts +18 -1
  15. package/dist/cli/dev.js +253 -198
  16. package/dist/cli/env.d.ts +31 -0
  17. package/dist/cli/env.js +93 -0
  18. package/dist/cli/log.d.ts +13 -0
  19. package/dist/cli/log.js +17 -0
  20. package/dist/cli/node-version.d.ts +8 -0
  21. package/dist/cli/node-version.js +13 -0
  22. package/dist/cli/preflight.d.ts +36 -0
  23. package/dist/cli/preflight.js +122 -0
  24. package/dist/cli/project.d.ts +135 -0
  25. package/dist/cli/project.js +517 -0
  26. package/dist/cli/start.d.ts +25 -0
  27. package/dist/cli/start.js +225 -0
  28. package/dist/commands/autocomplete.d.ts +13 -0
  29. package/dist/commands/autocomplete.js +159 -0
  30. package/dist/commands/command.d.ts +360 -0
  31. package/dist/commands/command.js +28 -0
  32. package/dist/commands/declaration.d.ts +77 -0
  33. package/dist/commands/declaration.js +492 -0
  34. package/dist/commands/register.d.ts +18 -0
  35. package/dist/commands/register.js +42 -0
  36. package/dist/commands/router.d.ts +20 -0
  37. package/dist/commands/router.js +228 -0
  38. package/dist/commands/tree.d.ts +47 -0
  39. package/dist/commands/tree.js +216 -0
  40. package/dist/commands/twins.d.ts +15 -0
  41. package/dist/commands/twins.js +78 -0
  42. package/dist/components/button.d.ts +79 -0
  43. package/dist/components/button.js +106 -0
  44. package/dist/components/component.d.ts +140 -0
  45. package/dist/components/component.js +39 -0
  46. package/dist/components/custom-id.d.ts +72 -0
  47. package/dist/components/custom-id.js +128 -0
  48. package/dist/components/declaration.d.ts +42 -0
  49. package/dist/components/declaration.js +342 -0
  50. package/dist/components/declared.d.ts +29 -0
  51. package/dist/components/declared.js +49 -0
  52. package/dist/components/embed.d.ts +29 -0
  53. package/dist/components/embed.js +11 -0
  54. package/dist/components/instance.d.ts +100 -0
  55. package/dist/components/instance.js +86 -0
  56. package/dist/components/layout.d.ts +103 -0
  57. package/dist/components/layout.js +279 -0
  58. package/dist/components/modal.d.ts +245 -0
  59. package/dist/components/modal.js +398 -0
  60. package/dist/components/options.d.ts +41 -0
  61. package/dist/components/options.js +101 -0
  62. package/dist/components/render.d.ts +12 -0
  63. package/dist/components/render.js +58 -0
  64. package/dist/components/router.d.ts +19 -0
  65. package/dist/components/router.js +251 -0
  66. package/dist/components/select.d.ts +146 -0
  67. package/dist/components/select.js +179 -0
  68. package/dist/core/bot.d.ts +99 -0
  69. package/dist/core/bot.js +174 -0
  70. package/dist/core/memory.d.ts +66 -0
  71. package/dist/core/memory.js +115 -0
  72. package/dist/discord/api.d.ts +112 -0
  73. package/dist/discord/api.js +115 -0
  74. package/dist/discord/cdn.d.ts +67 -0
  75. package/dist/discord/cdn.js +100 -0
  76. package/dist/discord/codes.d.ts +171 -0
  77. package/dist/discord/codes.js +421 -0
  78. package/dist/discord/endpoint.d.ts +56 -0
  79. package/dist/discord/endpoint.js +53 -0
  80. package/dist/discord/endpoints.d.ts +1529 -0
  81. package/dist/discord/endpoints.js +1139 -0
  82. package/dist/discord/formatting.d.ts +67 -0
  83. package/dist/discord/formatting.js +87 -0
  84. package/dist/discord/index.d.ts +10 -0
  85. package/dist/discord/index.js +13 -0
  86. package/dist/discord/intents.d.ts +48 -0
  87. package/dist/discord/intents.js +128 -0
  88. package/dist/discord/permissions.d.ts +94 -0
  89. package/dist/discord/permissions.js +185 -0
  90. package/dist/discord/snowflake.d.ts +21 -0
  91. package/dist/discord/snowflake.js +36 -0
  92. package/dist/discord/types/application-command.d.ts +332 -0
  93. package/dist/discord/types/application-command.js +48 -0
  94. package/dist/discord/types/application-role-connection-metadata.d.ts +42 -0
  95. package/dist/discord/types/application-role-connection-metadata.js +22 -0
  96. package/dist/discord/types/application.d.ts +208 -0
  97. package/dist/discord/types/application.js +48 -0
  98. package/dist/discord/types/audit-log.d.ts +255 -0
  99. package/dist/discord/types/audit-log.js +144 -0
  100. package/dist/discord/types/auto-moderation.d.ts +167 -0
  101. package/dist/discord/types/auto-moderation.js +52 -0
  102. package/dist/discord/types/channel.d.ts +557 -0
  103. package/dist/discord/types/channel.js +88 -0
  104. package/dist/discord/types/common.d.ts +54 -0
  105. package/dist/discord/types/common.js +38 -0
  106. package/dist/discord/types/component.d.ts +775 -0
  107. package/dist/discord/types/component.js +76 -0
  108. package/dist/discord/types/emoji.d.ts +74 -0
  109. package/dist/discord/types/emoji.js +1 -0
  110. package/dist/discord/types/entitlement.d.ts +84 -0
  111. package/dist/discord/types/entitlement.js +22 -0
  112. package/dist/discord/types/gateway-events.d.ts +1148 -0
  113. package/dist/discord/types/gateway-events.js +54 -0
  114. package/dist/discord/types/gateway.d.ts +38 -0
  115. package/dist/discord/types/gateway.js +1 -0
  116. package/dist/discord/types/guild-scheduled-event.d.ts +257 -0
  117. package/dist/discord/types/guild-scheduled-event.js +68 -0
  118. package/dist/discord/types/guild-template.d.ts +51 -0
  119. package/dist/discord/types/guild-template.js +1 -0
  120. package/dist/discord/types/guild.d.ts +910 -0
  121. package/dist/discord/types/guild.js +153 -0
  122. package/dist/discord/types/index.d.ts +31 -0
  123. package/dist/discord/types/index.js +31 -0
  124. package/dist/discord/types/interaction.d.ts +321 -0
  125. package/dist/discord/types/interaction.js +47 -0
  126. package/dist/discord/types/invite.d.ts +158 -0
  127. package/dist/discord/types/invite.js +39 -0
  128. package/dist/discord/types/message.d.ts +834 -0
  129. package/dist/discord/types/message.js +159 -0
  130. package/dist/discord/types/oauth2.d.ts +22 -0
  131. package/dist/discord/types/oauth2.js +1 -0
  132. package/dist/discord/types/permissions.d.ts +183 -0
  133. package/dist/discord/types/permissions.js +118 -0
  134. package/dist/discord/types/poll.d.ts +106 -0
  135. package/dist/discord/types/poll.js +8 -0
  136. package/dist/discord/types/sku.d.ts +47 -0
  137. package/dist/discord/types/sku.js +26 -0
  138. package/dist/discord/types/soundboard.d.ts +71 -0
  139. package/dist/discord/types/soundboard.js +1 -0
  140. package/dist/discord/types/stage-instance.d.ts +58 -0
  141. package/dist/discord/types/stage-instance.js +10 -0
  142. package/dist/discord/types/sticker.d.ts +115 -0
  143. package/dist/discord/types/sticker.js +20 -0
  144. package/dist/discord/types/subscription.d.ts +54 -0
  145. package/dist/discord/types/subscription.js +12 -0
  146. package/dist/discord/types/team.d.ts +55 -0
  147. package/dist/discord/types/team.js +21 -0
  148. package/dist/discord/types/user.d.ts +273 -0
  149. package/dist/discord/types/user.js +83 -0
  150. package/dist/discord/types/voice.d.ts +72 -0
  151. package/dist/discord/types/voice.js +1 -0
  152. package/dist/discord/types/webhook.d.ts +184 -0
  153. package/dist/discord/types/webhook.js +12 -0
  154. package/dist/events/declaration.d.ts +16 -0
  155. package/dist/events/declaration.js +80 -0
  156. package/dist/events/event.d.ts +45 -0
  157. package/dist/events/event.js +34 -0
  158. package/dist/events/registry.d.ts +697 -0
  159. package/dist/events/registry.js +839 -0
  160. package/dist/events/router.d.ts +46 -0
  161. package/dist/events/router.js +133 -0
  162. package/dist/gateway/errors.d.ts +18 -0
  163. package/dist/gateway/errors.js +47 -0
  164. package/dist/gateway/gateway.d.ts +64 -0
  165. package/dist/gateway/gateway.js +117 -0
  166. package/dist/gateway/identify-queue.d.ts +25 -0
  167. package/dist/gateway/identify-queue.js +29 -0
  168. package/dist/gateway/shard.d.ts +94 -0
  169. package/dist/gateway/shard.js +414 -0
  170. package/dist/gateway/state.d.ts +11 -0
  171. package/dist/gateway/state.js +184 -0
  172. package/dist/index.d.ts +57 -18
  173. package/dist/index.js +40 -12
  174. package/dist/interactions/cooldown.d.ts +33 -0
  175. package/dist/interactions/cooldown.js +74 -0
  176. package/dist/interactions/dispatch.d.ts +91 -0
  177. package/dist/interactions/dispatch.js +110 -0
  178. package/dist/loader/generated.d.ts +10 -0
  179. package/dist/loader/generated.js +80 -0
  180. package/dist/loader/hot.d.ts +13 -0
  181. package/dist/loader/hot.js +85 -0
  182. package/dist/loader/loader.d.ts +85 -0
  183. package/dist/loader/loader.js +119 -0
  184. package/dist/loader/locate.d.ts +10 -0
  185. package/dist/loader/locate.js +36 -0
  186. package/dist/loader/watch.d.ts +17 -0
  187. package/dist/loader/watch.js +66 -0
  188. package/dist/main.d.ts +2 -0
  189. package/dist/main.js +5 -0
  190. package/dist/messages/commands.d.ts +23 -0
  191. package/dist/messages/commands.js +142 -0
  192. package/dist/messages/declaration.d.ts +89 -0
  193. package/dist/messages/declaration.js +455 -0
  194. package/dist/messages/messages.d.ts +241 -0
  195. package/dist/messages/messages.js +31 -0
  196. package/dist/messages/phrases.d.ts +23 -0
  197. package/dist/messages/phrases.js +44 -0
  198. package/dist/messages/translate.d.ts +100 -0
  199. package/dist/messages/translate.js +249 -0
  200. package/dist/presence/declaration.d.ts +19 -0
  201. package/dist/presence/declaration.js +118 -0
  202. package/dist/presence/presence.d.ts +57 -0
  203. package/dist/presence/presence.js +27 -0
  204. package/dist/rest/errors.d.ts +55 -0
  205. package/dist/rest/errors.js +92 -0
  206. package/dist/rest/rate-limiter.d.ts +46 -0
  207. package/dist/rest/rate-limiter.js +153 -0
  208. package/dist/rest/rest.d.ts +63 -0
  209. package/dist/rest/rest.js +194 -0
  210. package/dist/structures/base.d.ts +48 -0
  211. package/dist/structures/base.js +84 -0
  212. package/dist/structures/channel.d.ts +380 -0
  213. package/dist/structures/channel.js +627 -0
  214. package/dist/structures/context.d.ts +30 -0
  215. package/dist/structures/context.js +7 -0
  216. package/dist/structures/emoji.d.ts +46 -0
  217. package/dist/structures/emoji.js +65 -0
  218. package/dist/structures/entities.d.ts +62 -0
  219. package/dist/structures/entities.js +226 -0
  220. package/dist/structures/guild.d.ts +348 -0
  221. package/dist/structures/guild.js +466 -0
  222. package/dist/structures/interaction.d.ts +197 -0
  223. package/dist/structures/interaction.js +396 -0
  224. package/dist/structures/invite.d.ts +38 -0
  225. package/dist/structures/invite.js +57 -0
  226. package/dist/structures/known.d.ts +28 -0
  227. package/dist/structures/known.js +58 -0
  228. package/dist/structures/member.d.ts +140 -0
  229. package/dist/structures/member.js +260 -0
  230. package/dist/structures/message.d.ts +197 -0
  231. package/dist/structures/message.js +268 -0
  232. package/dist/structures/payload.d.ts +91 -0
  233. package/dist/structures/payload.js +184 -0
  234. package/dist/structures/role.d.ts +69 -0
  235. package/dist/structures/role.js +112 -0
  236. package/dist/structures/user.d.ts +59 -0
  237. package/dist/structures/user.js +94 -0
  238. package/dist/structures/webhook.d.ts +46 -0
  239. package/dist/structures/webhook.js +79 -0
  240. package/dist/tasks/declaration.d.ts +16 -0
  241. package/dist/tasks/declaration.js +56 -0
  242. package/dist/tasks/schedule.d.ts +54 -0
  243. package/dist/tasks/schedule.js +236 -0
  244. package/dist/tasks/scheduler.d.ts +35 -0
  245. package/dist/tasks/scheduler.js +107 -0
  246. package/dist/tasks/task.d.ts +75 -0
  247. package/dist/tasks/task.js +27 -0
  248. package/dist/util/case.d.ts +13 -0
  249. package/dist/util/case.js +54 -0
  250. package/dist/util/duration.d.ts +8 -0
  251. package/dist/util/duration.js +29 -0
  252. package/dist/util/websocket.d.ts +9 -0
  253. package/dist/util/websocket.js +31 -0
  254. package/dist/voice/connection.d.ts +90 -0
  255. package/dist/voice/connection.js +494 -0
  256. package/dist/voice/dave.d.ts +109 -0
  257. package/dist/voice/dave.js +245 -0
  258. package/dist/voice/opus.d.ts +19 -0
  259. package/dist/voice/opus.js +273 -0
  260. package/dist/voice/session.d.ts +55 -0
  261. package/dist/voice/session.js +405 -0
  262. package/dist/voice/source.d.ts +22 -0
  263. package/dist/voice/source.js +195 -0
  264. package/dist/voice/transport.d.ts +43 -0
  265. package/dist/voice/transport.js +140 -0
  266. package/dist/voice/usage.d.ts +4 -0
  267. package/dist/voice/usage.js +33 -0
  268. package/package.json +26 -7
  269. package/vendor/dave/LICENSE +231 -0
  270. package/vendor/dave/libdave.d.mts +99 -0
  271. package/vendor/dave/libdave.mjs +2 -0
  272. package/vendor/dave/libdave.wasm +0 -0
  273. package/dist/components.d.ts +0 -97
  274. package/dist/components.js +0 -260
  275. package/dist/context.d.ts +0 -72
  276. package/dist/context.js +0 -273
  277. package/dist/discord/client.d.ts +0 -35
  278. package/dist/discord/client.js +0 -66
  279. package/dist/discord/constants.d.ts +0 -49
  280. package/dist/discord/constants.js +0 -61
  281. package/dist/discord/events.d.ts +0 -129
  282. package/dist/discord/events.js +0 -102
  283. package/dist/discord/gateway.d.ts +0 -61
  284. package/dist/discord/gateway.js +0 -320
  285. package/dist/discord/payloads.d.ts +0 -275
  286. package/dist/discord/payloads.js +0 -3
  287. package/dist/discord/presence.d.ts +0 -42
  288. package/dist/discord/presence.js +0 -51
  289. package/dist/discord/rest.d.ts +0 -49
  290. package/dist/discord/rest.js +0 -271
  291. package/dist/discord/types.d.ts +0 -334
  292. package/dist/discord/types.js +0 -4
  293. package/dist/events.d.ts +0 -94
  294. package/dist/events.js +0 -255
  295. package/dist/presence.d.ts +0 -92
  296. package/dist/presence.js +0 -250
  297. package/dist/structures/Channel.d.ts +0 -110
  298. package/dist/structures/Channel.js +0 -171
  299. package/dist/structures/Guild.d.ts +0 -70
  300. package/dist/structures/Guild.js +0 -105
  301. package/dist/structures/Interaction.d.ts +0 -113
  302. package/dist/structures/Interaction.js +0 -274
  303. package/dist/structures/Member.d.ts +0 -70
  304. package/dist/structures/Member.js +0 -124
  305. package/dist/structures/Message.d.ts +0 -87
  306. package/dist/structures/Message.js +0 -182
  307. package/dist/structures/Reaction.d.ts +0 -38
  308. package/dist/structures/Reaction.js +0 -64
  309. package/dist/structures/Role.d.ts +0 -43
  310. package/dist/structures/Role.js +0 -77
  311. package/dist/structures/User.d.ts +0 -41
  312. package/dist/structures/User.js +0 -79
  313. package/dist/structures/utils.d.ts +0 -72
  314. package/dist/structures/utils.js +0 -59
  315. package/dist/utils/errors.d.ts +0 -5
  316. package/dist/utils/errors.js +0 -28
  317. package/dist/utils/modules.d.ts +0 -39
  318. package/dist/utils/modules.js +0 -156
@@ -0,0 +1,455 @@
1
+ // What a file declares with `language()`: the texts of the bot in one
2
+ // language. What a file declares is checked when it loads, and the
3
+ // languages are checked against each other once loaded: every one has the
4
+ // texts of the default language, with the same placeholders, so a missing
5
+ // text is explained before anyone asks for it.
6
+ import { readFile } from 'node:fs/promises';
7
+ import { commandName } from '../commands/tree.js';
8
+ import { listSources, siteName, } from '../loader/loader.js';
9
+ import { isLanguageDeclaration } from './messages.js';
10
+ import { FRAMEWORK, FRAMEWORK_KEYS } from './phrases.js';
11
+ import { compile, compilePlural, LOCALES, PLURAL_FORMS, } from './translate.js';
12
+ const fail = message => {
13
+ throw new TypeError(message);
14
+ };
15
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
16
+ const NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
17
+ const KEYS = ['locale', 'default', 'texts', 'commands', 'framework'];
18
+ const EXAMPLE = `language({ locale: 'en-US', texts: { pong: 'Pong!' } })`;
19
+ /** Placeholders, for a message: `{name}, {count}`, or `none`. */
20
+ const showParams = (names) => names.length === 0 ? 'none' : names.map(name => `{${name}}`).join(', ');
21
+ /** Whether two templates take the same placeholders. */
22
+ const samePlaceholders = (a, b) => [...a.params].sort().join('\0') === [...b.params].sort().join('\0');
23
+ /** One form of a text, cut around its placeholders, each one checked. */
24
+ function readForm(what, text, example) {
25
+ if (typeof text !== 'string' || text === '') {
26
+ fail(`${what} must be a text: ${example}`);
27
+ }
28
+ const template = compile(text);
29
+ for (const name of template.params) {
30
+ if (!NAME.test(name)) {
31
+ fail(`${what} has a placeholder "{${name}}": a placeholder is a name made of letters, digits and _, like {name}.`);
32
+ }
33
+ }
34
+ return template;
35
+ }
36
+ /**
37
+ * A text of a language file: a string, or a plural (one form per
38
+ * quantity, `other` at least), with an error saying what to write.
39
+ */
40
+ function readText(key, text, kind = 'message') {
41
+ const what = `The ${kind} "${key}"`;
42
+ if (isRecord(text)) {
43
+ const forms = new Map();
44
+ for (const [form, one] of Object.entries(text)) {
45
+ if (!PLURAL_FORMS.includes(form)) {
46
+ fail(`"${form}" is not a form of the plural "${key}". A plural has: ${PLURAL_FORMS.join(', ')} (other at least): ${key}: { one: '{count} member', other: '{count} members' }`);
47
+ }
48
+ forms.set(form, readForm(`The form "${form}" of the plural "${key}"`, one, `${form}: '{count} members'`));
49
+ }
50
+ if (!forms.has('other')) {
51
+ fail(`The plural "${key}" needs an "other" form, used when no other form fits: ${key}: { one: '{count} member', other: '{count} members' }`);
52
+ }
53
+ return compilePlural(forms);
54
+ }
55
+ return readForm(what, text, `${key}: 'Hello {name}!'${kind === 'message' ? `, or a plural: ${key}: { one: '{count} member', other: '{count} members' }` : ''}`);
56
+ }
57
+ /** The language a declaration says it is, or an error saying which ones exist. */
58
+ function readLocale(locale) {
59
+ if (locale === undefined) {
60
+ fail(`This language has no "locale": the language its texts are in, like ${EXAMPLE}. It can be: ${[...LOCALES].join(', ')}.`);
61
+ }
62
+ if (typeof locale !== 'string' || !LOCALES.has(locale)) {
63
+ fail(`${JSON.stringify(locale) ?? typeof locale} is not a language Discord knows. "locale" can be: ${[...LOCALES].join(', ')}.`);
64
+ }
65
+ return locale;
66
+ }
67
+ /**
68
+ * A language, declared anywhere in `src/` with `language()`: `locale` says
69
+ * which one, `texts` what the bot says in it.
70
+ */
71
+ export const languageDeclaration = {
72
+ one: 'language',
73
+ many: 'languages',
74
+ is: isLanguageDeclaration,
75
+ list: true,
76
+ read(value) {
77
+ const config = value.config;
78
+ if (!isRecord(config)) {
79
+ return fail(`language() needs an object: ${EXAMPLE}`);
80
+ }
81
+ const locale = readLocale(config.locale);
82
+ for (const key of Object.keys(config)) {
83
+ if (!KEYS.includes(key)) {
84
+ fail(`"${key}" is not something a language has. It can have: ${KEYS.join(', ')}.`);
85
+ }
86
+ }
87
+ if (config.default !== undefined && typeof config.default !== 'boolean') {
88
+ fail(`"default" is true or false, got ${JSON.stringify(config.default)}.`);
89
+ }
90
+ if (!isRecord(config.texts)) {
91
+ fail(`"texts" is an object, one text per key: texts: { pong: 'Pong!', welcome: 'Welcome {name}!' }`);
92
+ }
93
+ const texts = new Map();
94
+ for (const [key, text] of Object.entries(config.texts)) {
95
+ texts.set(key, readText(key, text));
96
+ }
97
+ const framework = new Map();
98
+ if (config.framework !== undefined) {
99
+ if (!isRecord(config.framework)) {
100
+ fail(`"framework" is an object, one phrase per key: framework: { guildOnly: '...' }. It can have: ${FRAMEWORK_KEYS.join(', ')}.`);
101
+ }
102
+ for (const [key, text] of Object.entries(config.framework)) {
103
+ const expected = FRAMEWORK.get(key);
104
+ if (!expected) {
105
+ fail(`"${key}" is not a phrase of the framework. It can be: ${FRAMEWORK_KEYS.join(', ')}.`);
106
+ }
107
+ const template = readText(key, text, 'phrase');
108
+ if (!samePlaceholders(template, expected)) {
109
+ fail(`The phrase "${key}" does not have the placeholders of the framework: it has ${showParams(template.params)}, the framework has ${showParams(expected.params)}.`);
110
+ }
111
+ framework.set(key, template);
112
+ }
113
+ }
114
+ if (config.commands !== undefined) {
115
+ if (!isRecord(config.commands)) {
116
+ fail(`"commands" is an object whose keys are the paths of your command files: commands: { ping: { description: '...' }, 'mod/ban': { name: '...' } }`);
117
+ }
118
+ for (const [name, translation] of Object.entries(config.commands)) {
119
+ if (!/^[^\s]+(?: [^\s]+){0,2}$/.test(name)) {
120
+ fail(`"${name}" in "commands" is not the name of a command: write it as the command declares it, like 'ping' or 'mod ban'.`);
121
+ }
122
+ if (!isRecord(translation)) {
123
+ fail(`The texts of the command ${commandName(name.split(' '))} must be an object like { name: '...', description: '...' }.`);
124
+ }
125
+ }
126
+ }
127
+ return {
128
+ locale,
129
+ isDefault: config.default === true,
130
+ texts,
131
+ framework,
132
+ commands: config.commands,
133
+ };
134
+ },
135
+ };
136
+ /**
137
+ * Puts the languages together: one default, which `t` is typed from and
138
+ * gives a text to the languages that do not have it, and every other
139
+ * language with texts the default has, the same placeholders included. A
140
+ * language that does not match is left out and reported on its file: the
141
+ * others still work. `null` when there is no language at all.
142
+ */
143
+ export function assembleMessages(entries) {
144
+ if (entries.length === 0)
145
+ return { messages: null, failed: [] };
146
+ const failed = [];
147
+ // A language may be declared in several places (the texts of one feature
148
+ // next to it): what says default: true decides for the whole language.
149
+ const byLocale = new Map();
150
+ for (const entry of entries) {
151
+ const list = byLocale.get(entry.language.locale) ?? [];
152
+ list.push(entry);
153
+ byLocale.set(entry.language.locale, list);
154
+ }
155
+ const defaults = [...byLocale.keys()].filter(locale => byLocale.get(locale).some(({ language }) => language.isDefault));
156
+ let defaultLocale;
157
+ if (byLocale.size === 1) {
158
+ defaultLocale = entries[0].language.locale;
159
+ }
160
+ else if (defaults.length === 1) {
161
+ defaultLocale = defaults[0];
162
+ }
163
+ else if (defaults.length === 0) {
164
+ // No default: nothing can be assembled.
165
+ return {
166
+ messages: null,
167
+ failed: entries.map(({ file, export: exported }) => ({
168
+ file,
169
+ export: exported,
170
+ error: new TypeError(`None of the ${byLocale.size} languages is the default one, used when the language of a person or of a server is not there. Add default: true in one of them.`),
171
+ })),
172
+ };
173
+ }
174
+ else {
175
+ const saying = entries.filter(({ language }) => language.isDefault);
176
+ return {
177
+ messages: null,
178
+ failed: saying.map(entry => ({
179
+ file: entry.file,
180
+ export: entry.export,
181
+ error: new TypeError(`${defaults.length} languages say default: true (${saying.map(one => `${one.language.locale} in ${siteName(one)}`).join(', ')}): only one can be the default.`),
182
+ })),
183
+ };
184
+ }
185
+ /** The texts of one language, from every place it is declared. */
186
+ const merge = (locale, what, kind, onConflict) => {
187
+ const merged = new Map();
188
+ const from = new Map();
189
+ for (const entry of byLocale.get(locale)) {
190
+ for (const [key, template] of entry.language[what]) {
191
+ const first = from.get(key);
192
+ if (first) {
193
+ onConflict(entry, new TypeError(`The ${kind} "${key}" is already in ${siteName(first)}: the ${locale} language has it twice.`));
194
+ continue;
195
+ }
196
+ from.set(key, entry);
197
+ merged.set(key, template);
198
+ }
199
+ }
200
+ return merged;
201
+ };
202
+ const report = (entry, error) => {
203
+ failed.push({ file: entry.file, export: entry.export, error });
204
+ };
205
+ const referenceTexts = merge(defaultLocale, 'texts', 'message', report);
206
+ const referenceSites = byLocale.get(defaultLocale);
207
+ const name = `the default language (${referenceSites.map(siteName).join(', ')})`;
208
+ // A text the default language does not have is taken from the language
209
+ // that has it (en-US first, then the others in order): the texts of a
210
+ // module written in other languages than the project's default still
211
+ // work, and `t` offers them. Where each such text comes from decides
212
+ // the placeholders every language must then follow.
213
+ const fallbackOrder = [...byLocale.keys()]
214
+ .filter(locale => locale !== defaultLocale)
215
+ .sort((a, b) => (a === 'en-US' ? -1 : b === 'en-US' ? 1 : a < b ? -1 : 1));
216
+ const fallbackFrom = new Map();
217
+ for (const locale of fallbackOrder) {
218
+ for (const entry of byLocale.get(locale)) {
219
+ for (const [key, template] of entry.language.texts) {
220
+ if (referenceTexts.has(key))
221
+ continue;
222
+ referenceTexts.set(key, template);
223
+ fallbackFrom.set(key, entry);
224
+ }
225
+ }
226
+ }
227
+ const referenceName = (key) => {
228
+ const from = fallbackFrom.get(key);
229
+ return from
230
+ ? `${from.language.locale} (${siteName(from)}), the language this message comes from`
231
+ : name;
232
+ };
233
+ const locales = new Map();
234
+ const framework = new Map();
235
+ const commands = new Map();
236
+ const files = new Map();
237
+ for (const [locale, group] of byLocale) {
238
+ // A text a language does not have is taken from the default one (or
239
+ // from the language the text comes from): every language that has it
240
+ // must take the same placeholders.
241
+ const kept = [];
242
+ for (const entry of group) {
243
+ try {
244
+ if (locale !== defaultLocale) {
245
+ for (const [key, template] of entry.language.texts) {
246
+ const expected = referenceTexts.get(key);
247
+ if (expected !== template &&
248
+ !samePlaceholders(template, expected)) {
249
+ fail(`The message "${key}" does not have the placeholders of ${referenceName(key)}: it has ${showParams([...template.params].sort())}, that one has ${showParams([...expected.params].sort())}.`);
250
+ }
251
+ }
252
+ }
253
+ kept.push(entry);
254
+ }
255
+ catch (error) {
256
+ report(entry, error);
257
+ }
258
+ }
259
+ if (kept.length === 0)
260
+ continue;
261
+ const texts = locale === defaultLocale
262
+ ? referenceTexts
263
+ : new Map(kept.flatMap(({ language }) => [...language.texts]));
264
+ if (locale !== defaultLocale) {
265
+ // The same message twice in a language that is not the default.
266
+ const from = new Map();
267
+ for (const entry of kept) {
268
+ for (const key of entry.language.texts.keys()) {
269
+ const first = from.get(key);
270
+ if (first) {
271
+ report(entry, new TypeError(`The message "${key}" is already in ${siteName(first)}: the ${locale} language has it twice.`));
272
+ }
273
+ else
274
+ from.set(key, entry);
275
+ }
276
+ }
277
+ }
278
+ locales.set(locale, texts);
279
+ const phrases = merge(locale, 'framework', 'phrase', report);
280
+ if (phrases.size > 0)
281
+ framework.set(locale, phrases);
282
+ files.set(locale, kept[0].file);
283
+ const sections = kept.filter(({ language }) => language.commands);
284
+ if (sections.length > 0) {
285
+ const translated = {};
286
+ const from = new Map();
287
+ for (const entry of sections) {
288
+ for (const [command, section] of Object.entries(entry.language.commands)) {
289
+ const first = from.get(command);
290
+ if (first) {
291
+ report(entry, new TypeError(`The command "${command}" is already translated in ${siteName(first)}: the ${locale} language translates it twice.`));
292
+ continue;
293
+ }
294
+ from.set(command, entry);
295
+ translated[command] = section;
296
+ }
297
+ }
298
+ commands.set(locale, translated);
299
+ }
300
+ }
301
+ return {
302
+ messages: { default: defaultLocale, locales, framework, commands, files },
303
+ failed,
304
+ };
305
+ }
306
+ /**
307
+ * The augmentation that types `t` from the files themselves: the editor
308
+ * reads every declaration of the default language directly, so the keys
309
+ * and placeholders follow every keystroke; the other languages take the
310
+ * texts they do not have from it. `languages` is every language declared,
311
+ * `defaultLocale` the one `t` is typed from (the first one when it is not
312
+ * known yet: `sync`, which runs nothing). Nothing when the project has no
313
+ * language.
314
+ */
315
+ export function messagesDeclarations(languages, defaultLocale) {
316
+ const bySite = (a, b) => a.file === b.file
317
+ ? a.export < b.export
318
+ ? -1
319
+ : 1
320
+ : a.file < b.file
321
+ ? -1
322
+ : 1;
323
+ const sorted = [...languages].sort(bySite);
324
+ const first = sorted.find(({ locale }) => locale === defaultLocale) ?? sorted[0];
325
+ if (!first)
326
+ return '';
327
+ // Every declaration, the default language first, then en-US, then the
328
+ // others: `t` knows the texts of each, the first one winning for a key
329
+ // several have, as the bot does.
330
+ const rank = (locale) => locale === first.locale ? 0 : locale === 'en-US' ? 1 : 2;
331
+ const ordered = [...sorted].sort((a, b) => rank(a.locale) - rank(b.locale) ||
332
+ (a.locale < b.locale ? -1 : a.locale > b.locale ? 1 : 0) ||
333
+ bySite(a, b));
334
+ const parts = ordered.map(one => {
335
+ const path = one.file.replace(/^src\//, '').replace(/\.[^./]+$/, '');
336
+ const member = one.export === 'default'
337
+ ? '.default'
338
+ : /^[A-Za-z_$][\w$]*$/.test(one.export)
339
+ ? `.${one.export}`
340
+ : `[${JSON.stringify(one.export)}]`;
341
+ return `MessagesOf<typeof import('../../src/${path}')${member}>`;
342
+ });
343
+ const where = ordered
344
+ .filter(({ locale }) => locale === first.locale)
345
+ .map(siteName)
346
+ .join(', ');
347
+ const locales = [...new Set(sorted.map(({ locale }) => locale))]
348
+ .map(locale => ` ${JSON.stringify(locale)}: true;`)
349
+ .sort()
350
+ .join('\n');
351
+ return `import type { MergedMessages, MessagesOf } from '#chapterjs';
352
+
353
+ declare module 'chapterjs' {
354
+ /** The texts of the bot, as ${where} ${where.includes(',') ? 'write' : 'writes'} them (the ${first.locale} language), then what the other languages add: what \`t\` accepts. */
355
+ export interface ProjectMessages
356
+ extends MergedMessages<
357
+ [
358
+ ${parts.join(',\n ')},
359
+ ]
360
+ > {}
361
+ /** The languages of the project: what \`t.in()\` offers. */
362
+ export interface ProjectLocales {
363
+ ${locales}
364
+ }
365
+ }
366
+ `;
367
+ }
368
+ /**
369
+ * The augmentation that underlines a second default language in the
370
+ * editor: when more than one language says `default: true` (`defaults`,
371
+ * the ones that do, known once the files ran), `language()` refuses
372
+ * `default: true` for each of them, so they are both underlined and not
373
+ * only reported in the console. Nothing when at most one says it.
374
+ */
375
+ export function languageDefaultsDeclaration(defaults) {
376
+ const unique = [...new Set(defaults)].sort();
377
+ if (unique.length < 2)
378
+ return '';
379
+ return `export {};
380
+
381
+ declare module 'chapterjs' {
382
+ /** The languages that say \`default: true\`: only one can be the default, so \`default: true\` is refused in each of them until one gives it up. */
383
+ export interface ProjectLanguageDefaults {
384
+ ${unique.map(locale => ` ${JSON.stringify(locale)}: true;`).join('\n')}
385
+ }
386
+ }
387
+ `;
388
+ }
389
+ /** `export default language(` or `export const fr = language(`, with the config that follows. */
390
+ const DECLARED = /export\s+(?:default|const\s+([A-Za-z_$][\w$]*)\s*=)\s*language\s*\(\s*\{([^]*?)\}\s*\)/g;
391
+ /**
392
+ * The languages of a project, found without running its files: a text
393
+ * search for `language({ locale: '...' })` in the exports of each file.
394
+ * Approximate on purpose, for `sync`, which runs nothing: `dev` and
395
+ * `build` then write the exact ones. Returns the languages with the one
396
+ * that says `default: true`, when one does.
397
+ */
398
+ export async function scanLanguages(cwd) {
399
+ const languages = [];
400
+ let defaultLocale;
401
+ for (const { file, path } of await listSources(cwd)) {
402
+ const code = await readFile(path, 'utf8').catch(() => '');
403
+ if (!code.includes('language('))
404
+ continue;
405
+ for (const match of code.matchAll(DECLARED)) {
406
+ const config = match[2] ?? '';
407
+ const locale = /\blocale\s*:\s*['"`]([\w-]+)['"`]/.exec(config)?.[1];
408
+ if (!locale || !LOCALES.has(locale))
409
+ continue;
410
+ const exported = match[1] ?? 'default';
411
+ languages.push({
412
+ file,
413
+ export: exported,
414
+ name: exported === 'default' ? baseName(file) : exported,
415
+ locale: locale,
416
+ });
417
+ if (/\bdefault\s*:\s*true\b/.test(config)) {
418
+ defaultLocale ??= locale;
419
+ }
420
+ }
421
+ }
422
+ return defaultLocale === undefined
423
+ ? { languages }
424
+ : { languages, defaultLocale };
425
+ }
426
+ const baseName = (file) => file
427
+ .split('/')
428
+ .pop()
429
+ .replace(/\.[^.]+$/, '');
430
+ /**
431
+ * The augmentation that offers the commands of the project in the
432
+ * `commands` of a language: the name of each command, `true` when the
433
+ * language files describe it and `false` when the command has its own
434
+ * `description`, so that the editor only offers the first ones. Both come
435
+ * from running the files (`dev` and `build` know them, `sync` does not and
436
+ * offers any name). Nothing when the project has no command.
437
+ */
438
+ export function commandsDeclarations(commands) {
439
+ const entries = commands
440
+ .map(({ name, described }) => ` ${JSON.stringify(name)}: ${!described};`)
441
+ .sort();
442
+ if (entries.length === 0)
443
+ return '';
444
+ // A module (`export {}`), so that this augments 'chapterjs' instead of
445
+ // declaring it anew.
446
+ return `export {};
447
+
448
+ declare module 'chapterjs' {
449
+ /** The commands of the project, by name: true when the language files describe it, false when the command has its own description. */
450
+ export interface ProjectCommands {
451
+ ${entries.join('\n')}
452
+ }
453
+ }
454
+ `;
455
+ }