chapterjs 0.1.1 → 0.1.4

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 (320) 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 +206 -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 +252 -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 +122 -0
  25. package/dist/cli/project.js +452 -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 +346 -0
  31. package/dist/commands/command.js +28 -0
  32. package/dist/commands/convention.d.ts +64 -0
  33. package/dist/commands/convention.js +464 -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 +43 -0
  39. package/dist/commands/tree.js +206 -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 +108 -0
  44. package/dist/components/component.d.ts +140 -0
  45. package/dist/components/component.js +39 -0
  46. package/dist/components/convention.d.ts +35 -0
  47. package/dist/components/convention.js +339 -0
  48. package/dist/components/custom-id.d.ts +72 -0
  49. package/dist/components/custom-id.js +128 -0
  50. package/dist/components/embed.d.ts +28 -0
  51. package/dist/components/embed.js +11 -0
  52. package/dist/components/file.d.ts +30 -0
  53. package/dist/components/file.js +52 -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 +244 -0
  59. package/dist/components/modal.js +399 -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 +18 -0
  65. package/dist/components/router.js +256 -0
  66. package/dist/components/select.d.ts +144 -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/convention.d.ts +25 -0
  155. package/dist/events/convention.js +97 -0
  156. package/dist/events/event.d.ts +8 -0
  157. package/dist/events/event.js +20 -0
  158. package/dist/events/registry.d.ts +706 -0
  159. package/dist/events/registry.js +839 -0
  160. package/dist/events/router.d.ts +45 -0
  161. package/dist/events/router.js +133 -0
  162. package/dist/events/types.d.ts +7 -0
  163. package/dist/events/types.js +48 -0
  164. package/dist/gateway/errors.d.ts +18 -0
  165. package/dist/gateway/errors.js +47 -0
  166. package/dist/gateway/gateway.d.ts +64 -0
  167. package/dist/gateway/gateway.js +117 -0
  168. package/dist/gateway/identify-queue.d.ts +25 -0
  169. package/dist/gateway/identify-queue.js +29 -0
  170. package/dist/gateway/shard.d.ts +94 -0
  171. package/dist/gateway/shard.js +414 -0
  172. package/dist/gateway/state.d.ts +11 -0
  173. package/dist/gateway/state.js +184 -0
  174. package/dist/index.d.ts +55 -18
  175. package/dist/index.js +38 -12
  176. package/dist/interactions/cooldown.d.ts +33 -0
  177. package/dist/interactions/cooldown.js +74 -0
  178. package/dist/interactions/dispatch.d.ts +91 -0
  179. package/dist/interactions/dispatch.js +110 -0
  180. package/dist/loader/generated.d.ts +37 -0
  181. package/dist/loader/generated.js +114 -0
  182. package/dist/loader/hot.d.ts +13 -0
  183. package/dist/loader/hot.js +85 -0
  184. package/dist/loader/loader.d.ts +72 -0
  185. package/dist/loader/loader.js +135 -0
  186. package/dist/loader/locate.d.ts +10 -0
  187. package/dist/loader/locate.js +36 -0
  188. package/dist/loader/watch.d.ts +17 -0
  189. package/dist/loader/watch.js +66 -0
  190. package/dist/main.d.ts +3 -0
  191. package/dist/main.js +6 -0
  192. package/dist/messages/commands.d.ts +23 -0
  193. package/dist/messages/commands.js +123 -0
  194. package/dist/messages/convention.d.ts +89 -0
  195. package/dist/messages/convention.js +347 -0
  196. package/dist/messages/messages.d.ts +219 -0
  197. package/dist/messages/messages.js +30 -0
  198. package/dist/messages/phrases.d.ts +23 -0
  199. package/dist/messages/phrases.js +44 -0
  200. package/dist/messages/translate.d.ts +100 -0
  201. package/dist/messages/translate.js +249 -0
  202. package/dist/presence/convention.d.ts +26 -0
  203. package/dist/presence/convention.js +133 -0
  204. package/dist/presence/presence.d.ts +57 -0
  205. package/dist/presence/presence.js +27 -0
  206. package/dist/rest/errors.d.ts +55 -0
  207. package/dist/rest/errors.js +92 -0
  208. package/dist/rest/rate-limiter.d.ts +46 -0
  209. package/dist/rest/rate-limiter.js +153 -0
  210. package/dist/rest/rest.d.ts +63 -0
  211. package/dist/rest/rest.js +194 -0
  212. package/dist/structures/base.d.ts +48 -0
  213. package/dist/structures/base.js +84 -0
  214. package/dist/structures/channel.d.ts +380 -0
  215. package/dist/structures/channel.js +627 -0
  216. package/dist/structures/context.d.ts +30 -0
  217. package/dist/structures/context.js +7 -0
  218. package/dist/structures/emoji.d.ts +46 -0
  219. package/dist/structures/emoji.js +65 -0
  220. package/dist/structures/entities.d.ts +62 -0
  221. package/dist/structures/entities.js +226 -0
  222. package/dist/structures/guild.d.ts +348 -0
  223. package/dist/structures/guild.js +466 -0
  224. package/dist/structures/interaction.d.ts +197 -0
  225. package/dist/structures/interaction.js +396 -0
  226. package/dist/structures/invite.d.ts +38 -0
  227. package/dist/structures/invite.js +57 -0
  228. package/dist/structures/known.d.ts +28 -0
  229. package/dist/structures/known.js +58 -0
  230. package/dist/structures/member.d.ts +140 -0
  231. package/dist/structures/member.js +260 -0
  232. package/dist/structures/message.d.ts +197 -0
  233. package/dist/structures/message.js +268 -0
  234. package/dist/structures/payload.d.ts +91 -0
  235. package/dist/structures/payload.js +184 -0
  236. package/dist/structures/role.d.ts +69 -0
  237. package/dist/structures/role.js +112 -0
  238. package/dist/structures/user.d.ts +59 -0
  239. package/dist/structures/user.js +94 -0
  240. package/dist/structures/webhook.d.ts +46 -0
  241. package/dist/structures/webhook.js +79 -0
  242. package/dist/tasks/convention.d.ts +26 -0
  243. package/dist/tasks/convention.js +78 -0
  244. package/dist/tasks/schedule.d.ts +54 -0
  245. package/dist/tasks/schedule.js +236 -0
  246. package/dist/tasks/scheduler.d.ts +33 -0
  247. package/dist/tasks/scheduler.js +107 -0
  248. package/dist/tasks/task.d.ts +76 -0
  249. package/dist/tasks/task.js +28 -0
  250. package/dist/util/case.d.ts +13 -0
  251. package/dist/util/case.js +54 -0
  252. package/dist/util/duration.d.ts +8 -0
  253. package/dist/util/duration.js +29 -0
  254. package/dist/util/websocket.d.ts +9 -0
  255. package/dist/util/websocket.js +31 -0
  256. package/dist/voice/connection.d.ts +90 -0
  257. package/dist/voice/connection.js +494 -0
  258. package/dist/voice/dave.d.ts +109 -0
  259. package/dist/voice/dave.js +245 -0
  260. package/dist/voice/opus.d.ts +19 -0
  261. package/dist/voice/opus.js +273 -0
  262. package/dist/voice/session.d.ts +55 -0
  263. package/dist/voice/session.js +405 -0
  264. package/dist/voice/source.d.ts +22 -0
  265. package/dist/voice/source.js +195 -0
  266. package/dist/voice/transport.d.ts +43 -0
  267. package/dist/voice/transport.js +140 -0
  268. package/dist/voice/usage.d.ts +4 -0
  269. package/dist/voice/usage.js +33 -0
  270. package/package.json +26 -7
  271. package/vendor/dave/LICENSE +231 -0
  272. package/vendor/dave/libdave.d.mts +99 -0
  273. package/vendor/dave/libdave.mjs +2 -0
  274. package/vendor/dave/libdave.wasm +0 -0
  275. package/dist/components.d.ts +0 -97
  276. package/dist/components.js +0 -260
  277. package/dist/context.d.ts +0 -72
  278. package/dist/context.js +0 -273
  279. package/dist/discord/client.d.ts +0 -35
  280. package/dist/discord/client.js +0 -66
  281. package/dist/discord/constants.d.ts +0 -49
  282. package/dist/discord/constants.js +0 -61
  283. package/dist/discord/events.d.ts +0 -129
  284. package/dist/discord/events.js +0 -102
  285. package/dist/discord/gateway.d.ts +0 -61
  286. package/dist/discord/gateway.js +0 -320
  287. package/dist/discord/payloads.d.ts +0 -275
  288. package/dist/discord/payloads.js +0 -3
  289. package/dist/discord/presence.d.ts +0 -42
  290. package/dist/discord/presence.js +0 -51
  291. package/dist/discord/rest.d.ts +0 -49
  292. package/dist/discord/rest.js +0 -271
  293. package/dist/discord/types.d.ts +0 -334
  294. package/dist/discord/types.js +0 -4
  295. package/dist/events.d.ts +0 -94
  296. package/dist/events.js +0 -255
  297. package/dist/presence.d.ts +0 -92
  298. package/dist/presence.js +0 -250
  299. package/dist/structures/Channel.d.ts +0 -110
  300. package/dist/structures/Channel.js +0 -171
  301. package/dist/structures/Guild.d.ts +0 -70
  302. package/dist/structures/Guild.js +0 -105
  303. package/dist/structures/Interaction.d.ts +0 -113
  304. package/dist/structures/Interaction.js +0 -274
  305. package/dist/structures/Member.d.ts +0 -70
  306. package/dist/structures/Member.js +0 -124
  307. package/dist/structures/Message.d.ts +0 -87
  308. package/dist/structures/Message.js +0 -182
  309. package/dist/structures/Reaction.d.ts +0 -38
  310. package/dist/structures/Reaction.js +0 -64
  311. package/dist/structures/Role.d.ts +0 -43
  312. package/dist/structures/Role.js +0 -77
  313. package/dist/structures/User.d.ts +0 -41
  314. package/dist/structures/User.js +0 -79
  315. package/dist/structures/utils.d.ts +0 -72
  316. package/dist/structures/utils.js +0 -59
  317. package/dist/utils/errors.d.ts +0 -5
  318. package/dist/utils/errors.js +0 -28
  319. package/dist/utils/modules.d.ts +0 -39
  320. package/dist/utils/modules.js +0 -156
@@ -0,0 +1,347 @@
1
+ // `src/messages/`: one file per language, named after it (`fr.ts` is the
2
+ // French of the bot). What a file declares is checked when it loads, and
3
+ // the languages are checked against each other once loaded: every one has
4
+ // the texts of the default language, with the same placeholders, so a
5
+ // missing text is explained before anyone asks for it.
6
+ import { commandName } from '../commands/tree.js';
7
+ import { logicalPath, } from '../loader/loader.js';
8
+ import { isLanguageFile } from './messages.js';
9
+ import { FRAMEWORK, FRAMEWORK_KEYS } from './phrases.js';
10
+ import { compile, compilePlural, LOCALES, PLURAL_FORMS, } from './translate.js';
11
+ const fail = message => {
12
+ throw new TypeError(message);
13
+ };
14
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
15
+ const NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
16
+ const KEYS = ['default', 'texts', 'commands', 'framework'];
17
+ const EXAMPLE = `language({ texts: { pong: 'Pong!' } })`;
18
+ /** Placeholders, for a message: `{name}, {count}`, or `none`. */
19
+ const showParams = (names) => names.length === 0 ? 'none' : names.map(name => `{${name}}`).join(', ');
20
+ /** Whether two templates take the same placeholders. */
21
+ const samePlaceholders = (a, b) => [...a.params].sort().join('\0') === [...b.params].sort().join('\0');
22
+ /** One form of a text, cut around its placeholders, each one checked. */
23
+ function readForm(what, text, example) {
24
+ if (typeof text !== 'string' || text === '') {
25
+ fail(`${what} must be a text: ${example}`);
26
+ }
27
+ const template = compile(text);
28
+ for (const name of template.params) {
29
+ if (!NAME.test(name)) {
30
+ fail(`${what} has a placeholder "{${name}}": a placeholder is a name made of letters, digits and _, like {name}.`);
31
+ }
32
+ }
33
+ return template;
34
+ }
35
+ /**
36
+ * A text of a language file: a string, or a plural (one form per
37
+ * quantity, `other` at least), with an error saying what to write.
38
+ */
39
+ function readText(key, text, kind = 'message') {
40
+ const what = `The ${kind} "${key}"`;
41
+ if (isRecord(text)) {
42
+ const forms = new Map();
43
+ for (const [form, one] of Object.entries(text)) {
44
+ if (!PLURAL_FORMS.includes(form)) {
45
+ 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' }`);
46
+ }
47
+ forms.set(form, readForm(`The form "${form}" of the plural "${key}"`, one, `${form}: '{count} members'`));
48
+ }
49
+ if (!forms.has('other')) {
50
+ fail(`The plural "${key}" needs an "other" form, used when no other form fits: ${key}: { one: '{count} member', other: '{count} members' }`);
51
+ }
52
+ return compilePlural(forms);
53
+ }
54
+ return readForm(what, text, `${key}: 'Hello {name}!'${kind === 'message' ? `, or a plural: ${key}: { one: '{count} member', other: '{count} members' }` : ''}`);
55
+ }
56
+ /** The language a file is named after, or an error saying which ones exist. */
57
+ function localeOf(path) {
58
+ const name = path.replace(/\.[^./]+$/, '');
59
+ if (name.includes('/')) {
60
+ fail(`A language is a file directly in src/messages/, named after the language: src/messages/${name.split('/').pop()}.ts. It can't be in a subfolder.`);
61
+ }
62
+ if (!LOCALES.has(name)) {
63
+ fail(`"${name}" is not a language Discord knows: the name of a file of src/messages/ is the language it holds. It can be: ${[...LOCALES].join(', ')}.`);
64
+ }
65
+ return name;
66
+ }
67
+ /**
68
+ * `src/messages/`: one file per language. Each file exports by default
69
+ * what `language()` returns:
70
+ *
71
+ * ```ts
72
+ * // src/messages/fr.ts
73
+ * import { language } from 'chapterjs';
74
+ *
75
+ * export default language({ texts: { pong: 'Pong !' } });
76
+ * ```
77
+ */
78
+ export const languagesConvention = {
79
+ folder: 'messages',
80
+ one: 'language',
81
+ many: 'languages',
82
+ check(path) {
83
+ localeOf(path);
84
+ },
85
+ read(exports, path) {
86
+ const locale = localeOf(path);
87
+ const file = exports.default;
88
+ const example = `import { language } from 'chapterjs'; export default ${EXAMPLE}`;
89
+ if (!isLanguageFile(file)) {
90
+ return fail('default' in exports
91
+ ? `The default export of this file must be what language() returns: ${example}`
92
+ : `This file has no default export. It should look like: ${example}`);
93
+ }
94
+ const config = file.config;
95
+ if (!isRecord(config)) {
96
+ return fail(`language() needs an object: ${EXAMPLE}`);
97
+ }
98
+ for (const key of Object.keys(config)) {
99
+ if (!KEYS.includes(key)) {
100
+ fail(`"${key}" is not something a language has. It can have: ${KEYS.join(', ')}.`);
101
+ }
102
+ }
103
+ if (config.default !== undefined && typeof config.default !== 'boolean') {
104
+ fail(`"default" is true or false, got ${JSON.stringify(config.default)}.`);
105
+ }
106
+ if (!isRecord(config.texts)) {
107
+ fail(`"texts" is an object, one text per key: texts: { pong: 'Pong!', welcome: 'Welcome {name}!' }`);
108
+ }
109
+ const texts = new Map();
110
+ for (const [key, text] of Object.entries(config.texts)) {
111
+ texts.set(key, readText(key, text));
112
+ }
113
+ const framework = new Map();
114
+ if (config.framework !== undefined) {
115
+ if (!isRecord(config.framework)) {
116
+ fail(`"framework" is an object, one phrase per key: framework: { guildOnly: '...' }. It can have: ${FRAMEWORK_KEYS.join(', ')}.`);
117
+ }
118
+ for (const [key, text] of Object.entries(config.framework)) {
119
+ const expected = FRAMEWORK.get(key);
120
+ if (!expected) {
121
+ fail(`"${key}" is not a phrase of the framework. It can be: ${FRAMEWORK_KEYS.join(', ')}.`);
122
+ }
123
+ const template = readText(key, text, 'phrase');
124
+ if (!samePlaceholders(template, expected)) {
125
+ fail(`The phrase "${key}" does not have the placeholders of the framework: it has ${showParams(template.params)}, the framework has ${showParams(expected.params)}.`);
126
+ }
127
+ framework.set(key, template);
128
+ }
129
+ }
130
+ if (config.commands !== undefined) {
131
+ if (!isRecord(config.commands)) {
132
+ fail(`"commands" is an object whose keys are the paths of your command files: commands: { ping: { description: '...' }, 'mod/ban': { name: '...' } }`);
133
+ }
134
+ for (const [path, translation] of Object.entries(config.commands)) {
135
+ if (!/^[a-z0-9_-]+(?:\/[a-z0-9_-]+){0,2}$/.test(path)) {
136
+ fail(`"${path}" in "commands" is not the path of a command file: write it as the file is named, like 'ping' or 'mod/ban'.`);
137
+ }
138
+ if (!isRecord(translation)) {
139
+ fail(`The texts of the command ${commandName(path.split('/'))} must be an object like { name: '...', description: '...' }.`);
140
+ }
141
+ }
142
+ }
143
+ return {
144
+ locale,
145
+ isDefault: config.default === true,
146
+ texts,
147
+ framework,
148
+ commands: config.commands,
149
+ };
150
+ },
151
+ };
152
+ /**
153
+ * Puts the languages together: one default, which `t` is typed from and
154
+ * gives a text to the languages that do not have it, and every other
155
+ * language with texts the default has, the same placeholders included. A
156
+ * language that does not match is left out and reported on its file: the
157
+ * others still work. `null` when there is no language at all.
158
+ */
159
+ export function assembleMessages(entries) {
160
+ if (entries.length === 0)
161
+ return { messages: null, failed: [] };
162
+ const failed = [];
163
+ const defaults = entries.filter(({ language }) => language.isDefault);
164
+ let reference;
165
+ if (entries.length === 1) {
166
+ reference = entries[0];
167
+ }
168
+ else if (defaults.length === 1) {
169
+ reference = defaults[0];
170
+ }
171
+ else if (defaults.length === 0) {
172
+ // No default: nothing can be assembled.
173
+ return {
174
+ messages: null,
175
+ failed: entries.map(({ file }) => ({
176
+ file,
177
+ error: new TypeError(`None of the ${entries.length} languages of src/messages/ 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.`),
178
+ })),
179
+ };
180
+ }
181
+ else {
182
+ return {
183
+ messages: null,
184
+ failed: defaults.map(({ file }) => ({
185
+ file,
186
+ error: new TypeError(`${defaults.length} languages say default: true (${defaults.map(({ file }) => file).join(', ')}): only one can be the default.`),
187
+ })),
188
+ };
189
+ }
190
+ const locales = new Map();
191
+ const framework = new Map();
192
+ const commands = new Map();
193
+ const files = new Map();
194
+ const name = reference.file.split('/').pop();
195
+ for (const entry of entries) {
196
+ const { file, language } = entry;
197
+ try {
198
+ // A text a language does not have is taken from the default one;
199
+ // one the default does not have is dead: `t` offers the default's.
200
+ if (entry !== reference) {
201
+ for (const [key, template] of language.texts) {
202
+ const expected = reference.language.texts.get(key);
203
+ if (!expected) {
204
+ fail(`The message "${key}" is not in ${name}: add it there, or remove it here.`);
205
+ }
206
+ if (!samePlaceholders(template, expected)) {
207
+ fail(`The message "${key}" does not have the placeholders of ${name}: it has ${showParams([...template.params].sort())}, ${name} has ${showParams([...expected.params].sort())}.`);
208
+ }
209
+ }
210
+ }
211
+ locales.set(language.locale, language.texts);
212
+ if (language.framework.size > 0) {
213
+ framework.set(language.locale, language.framework);
214
+ }
215
+ files.set(language.locale, file);
216
+ if (language.commands)
217
+ commands.set(language.locale, language.commands);
218
+ }
219
+ catch (error) {
220
+ failed.push({ file, error });
221
+ }
222
+ }
223
+ return {
224
+ messages: {
225
+ default: reference.language.locale,
226
+ locales,
227
+ framework,
228
+ commands,
229
+ files,
230
+ },
231
+ failed,
232
+ };
233
+ }
234
+ /**
235
+ * The augmentation that types `t` from the files themselves: the editor
236
+ * reads the default language of `src/messages/` directly, so the keys
237
+ * and placeholders follow every keystroke; the other languages take the
238
+ * texts they do not have from it. Which file is the default is known
239
+ * once the files ran (`defaultFile`, from the loaded messages): until
240
+ * then (`sync`), the first file is taken. Nothing when the folder has no
241
+ * language.
242
+ */
243
+ export function messagesDeclarations(files, defaultFile) {
244
+ const named = namedFiles(files);
245
+ const first = named.find(({ file }) => file === defaultFile) ?? named[0];
246
+ if (!first)
247
+ return '';
248
+ const path = first.file.replace(/^src\//, '').replace(/\.[^./]+$/, '');
249
+ const locales = named
250
+ .map(({ locale }) => ` ${JSON.stringify(locale)}: true;`)
251
+ .sort()
252
+ .join('\n');
253
+ return `import type { MessagesOf } from '#chapterjs';
254
+
255
+ declare module 'chapterjs' {
256
+ /** The texts of src/messages/, as ${first.file} writes them: what \`t\` accepts. */
257
+ export interface ProjectMessages
258
+ extends MessagesOf<typeof import('../../src/${path}').default> {}
259
+ /** The languages of src/messages/: what \`t.in()\` offers. */
260
+ export interface ProjectLocales {
261
+ ${locales}
262
+ }
263
+ }
264
+ `;
265
+ }
266
+ /** The files of `src/messages/` named after a language, with it. */
267
+ function namedFiles(files) {
268
+ return files.flatMap(({ file }) => {
269
+ try {
270
+ return [{ file, locale: localeOf(file.slice('src/messages/'.length)) }];
271
+ }
272
+ catch {
273
+ return [];
274
+ }
275
+ });
276
+ }
277
+ /**
278
+ * One typed folder per language file: in `src/messages/fr.ts`, the
279
+ * `language()` imported from 'chapterjs' knows the other files. When
280
+ * another one says `default: true` (`defaults`, the files that do, known
281
+ * once they ran: `dev` and `build` give them, `sync` runs nothing), it
282
+ * refuses `default: true`, naming that file: a second default language is
283
+ * underlined in the editor, and no longer only reported in the console.
284
+ * Each project takes `main` (the commands a language file may translate)
285
+ * and not `shared`, which types `t` from the default file: a language file
286
+ * checked with the `language()` of another would be a false error.
287
+ */
288
+ export function languageTypedFolders(files, defaults = []) {
289
+ return namedFiles(files).map(({ file, locale }) => {
290
+ const others = defaults.filter(one => one !== file).sort();
291
+ const refusal = others.length === 0
292
+ ? ''
293
+ : ` & {
294
+ /** ${others.join(' and ')} already ${others.length === 1 ? 'says' : 'say'} \`default: true\`: only one file of src/messages/ is the default language. Remove it there to make this one the default. */
295
+ default?: false;
296
+ }`;
297
+ return {
298
+ id: `messages.${locale}`,
299
+ folder: file,
300
+ includes: ['main'],
301
+ declarations: `import type { LanguageConfig, LanguageFile, MessageTexts } from '#chapterjs';
302
+
303
+ /**
304
+ * Declares one language of the bot: the one this file is named after.
305
+ * Every handler then receives \`t\`, which gives a text in the language of
306
+ * who will read the message, with its \`{placeholders}\` filled in.
307
+ */
308
+ export declare function language<const T extends MessageTexts>(
309
+ config: LanguageConfig<T>${refusal}
310
+ ): LanguageFile<T>;
311
+ `,
312
+ };
313
+ });
314
+ }
315
+ /**
316
+ * The augmentation that offers the commands of the project in the
317
+ * `commands` of a language file: the path of each command file, `true`
318
+ * when the language files describe it and `false` when the file has its
319
+ * own `description`, so that the editor only offers the first ones. The
320
+ * path comes from the file name; whether it is described comes from
321
+ * running the file (`dev` and `build` know it, `sync` does not). The
322
+ * command files themselves are never referred to: their `t` is typed from
323
+ * the languages, which would be a circle. Nothing when the project has no
324
+ * command.
325
+ */
326
+ export function commandsDeclarations(commands) {
327
+ const entries = commands
328
+ .map(({ file, described }) => {
329
+ const inside = file.slice('src/commands/'.length);
330
+ const path = logicalPath(inside).replace(/\.[^./]+$/, '');
331
+ return ` ${JSON.stringify(path)}: ${!described};`;
332
+ })
333
+ .sort();
334
+ if (entries.length === 0)
335
+ return '';
336
+ // A module (`export {}`), so that this augments 'chapterjs' instead of
337
+ // declaring it anew.
338
+ return `export {};
339
+
340
+ declare module 'chapterjs' {
341
+ /** The commands of src/commands/, by the path of their file: true when the language files describe it, false when the file has its own description. */
342
+ export interface ProjectCommands {
343
+ ${entries.join('\n')}
344
+ }
345
+ }
346
+ `;
347
+ }
@@ -0,0 +1,219 @@
1
+ import type { CommandTranslation } from '../commands/command.js';
2
+ import type { Locale } from '../discord/types/common.js';
3
+ /**
4
+ * A text with several forms, one per quantity: `t('members', { count })`
5
+ * picks the form of the language for that number (`one` for 1 in
6
+ * English, `other` for the rest; French, Russian or Arabic have others).
7
+ * `other` is always there; the forms a language does not use are left
8
+ * out. `{count}` in a form is the number itself.
9
+ */
10
+ export interface PluralText {
11
+ zero?: string;
12
+ one?: string;
13
+ two?: string;
14
+ few?: string;
15
+ many?: string;
16
+ other: string;
17
+ }
18
+ /**
19
+ * One text of a language: a string, with its `{placeholders}`, or a text
20
+ * with several forms, picked by `count`.
21
+ */
22
+ export type MessageText = string | PluralText;
23
+ /** The texts of one language, by key. */
24
+ export type MessageTexts = Record<string, MessageText>;
25
+ /**
26
+ * What the bot answers by itself, when it can't run what was asked: where
27
+ * a command or a component can't be used, a button of an old message, a
28
+ * `run` that failed... Every phrase has an English version; a language
29
+ * file gives its own in `framework`, the ones it wants. `{what}` is the
30
+ * word for what was used: `command`, `button`, `menu` or `form`, given
31
+ * here too.
32
+ */
33
+ export interface FrameworkTexts {
34
+ /** The word for a command: "command". */
35
+ command: string;
36
+ /** The word for a button: "button". */
37
+ button: string;
38
+ /** The word for a select menu: "menu". */
39
+ menu: string;
40
+ /** The word for a form: "form". */
41
+ form: string;
42
+ /** "This {what} can only be used in a server." */
43
+ guildOnly: string;
44
+ /** "This {what} can only be used in a private message with me." */
45
+ dmOnly: string;
46
+ /** "This {what} can not be used here." */
47
+ notHere: string;
48
+ /** "This command is not available right now." (its file is gone or broken) */
49
+ unavailable: string;
50
+ /** "This {what} is not available any more." (an old message, its file is gone) */
51
+ gone: string;
52
+ /** "This {what} is out of date." (an old message, its `data` changed) */
53
+ outdated: string;
54
+ /** "Only the person who used the command can use this {what}." */
55
+ authorOnly: string;
56
+ /** "Something went wrong while running this {what}." */
57
+ failed: string;
58
+ /** "I don't have the permission to do that here." */
59
+ missingPermission: string;
60
+ /**
61
+ * "You can use this {what} again {when}." (a `cooldown` not over yet);
62
+ * `{when}` is a Discord timestamp, shown as "in 5 seconds" (or a date
63
+ * and time past 15 minutes) in the language of the person.
64
+ */
65
+ cooldown: string;
66
+ /**
67
+ * "You need the {permissions} permission to use this command." A plural
68
+ * of `count`, how many permissions are missing; `{permissions}` is their
69
+ * names, separated by commas.
70
+ */
71
+ needsPermissions: MessageText;
72
+ }
73
+ /**
74
+ * The commands of the project, as the editor sees them: the path of each
75
+ * command file, `true` when the language files describe it, `false` when
76
+ * the file has its own `description`. Filled in by the `.chapterjs/`
77
+ * folder of the project, so that the `commands` of a language file offers
78
+ * the ones it may translate. The options and choices are checked when the
79
+ * files load.
80
+ */
81
+ export interface ProjectCommands {
82
+ }
83
+ /** The paths of the commands the language files describe. */
84
+ type TranslatableCommand = {
85
+ [Path in keyof ProjectCommands]: ProjectCommands[Path] extends true ? Path : never;
86
+ }[keyof ProjectCommands];
87
+ /**
88
+ * What Discord shows of the commands in one language: by the path of the
89
+ * command file (`'ping'`, `'mod/ban'`), its name, description, options and
90
+ * choices in that language, as the `locales` of a command file take them.
91
+ * The editor offers the commands of the project that have no `description`
92
+ * in their file (one that has owns its texts, `locales` included); their
93
+ * options and choices are checked when the files load.
94
+ */
95
+ export type CommandsTranslations = [keyof ProjectCommands] extends [never] ? Readonly<Record<string, CommandTranslation>> : [TranslatableCommand] extends [never] ? Readonly<Record<string, never>> : {
96
+ readonly [Path in TranslatableCommand]?: CommandTranslation;
97
+ };
98
+ /** What a file of `src/messages/` gives to `language()`. */
99
+ export interface LanguageConfig<T extends MessageTexts = MessageTexts> {
100
+ /**
101
+ * Whether this is the language used when the one of a person or of a
102
+ * server is not in `src/messages/`. Exactly one file says so; with one
103
+ * file, it goes without saying.
104
+ */
105
+ default?: boolean;
106
+ /** The texts of the bot in this language, by key. */
107
+ texts: T;
108
+ /** What Discord shows of the commands in this language. */
109
+ commands?: CommandsTranslations;
110
+ /**
111
+ * What the bot answers by itself in this language, when it can't run
112
+ * what was asked: the phrases of `FrameworkTexts`, the ones this file
113
+ * wants to translate. The others stay in English.
114
+ */
115
+ framework?: Partial<FrameworkTexts>;
116
+ }
117
+ /** The text of a key, when it is one. */
118
+ type TextOf<M, K extends keyof M> = M[K] extends MessageText ? M[K] : string;
119
+ /**
120
+ * The placeholders found in a string, read from left to right like the
121
+ * bot does: `{{` is a `{` written as is, and a `{` with another `{` before
122
+ * its `}` is one too.
123
+ */
124
+ type Found<S extends string> = S extends `${string}{${infer After}` ? After extends `{${infer Rest}` ? Found<Rest> : After extends `${infer Name}}${infer Rest}` ? Name extends `${string}{${string}` ? Found<After> : Name | Found<Rest> : never : never;
125
+ /**
126
+ * The placeholders of a text: `'Hello {name}'` has `'name'`. Written
127
+ * between braces, a placeholder is a name made of letters, digits and `_`;
128
+ * `{{` and `}}` are braces written as is. A plural has `count`, and the
129
+ * placeholders of its forms.
130
+ */
131
+ export type Placeholders<T extends MessageText> = T extends string ? Found<T> : T extends PluralText ? 'count' | Found<Extract<T[keyof T], string>> : never;
132
+ /**
133
+ * What `t` takes for a text: one value per placeholder, or nothing when
134
+ * the text has none. A number is written the way the language writes
135
+ * numbers (`1,234` in English, `1 234` in French): give a text to keep it
136
+ * as is. The `count` of a plural is a number.
137
+ */
138
+ export type ParamsOf<T extends MessageText> = {
139
+ [Name in Placeholders<T>]: Name extends 'count' ? T extends PluralText ? number : string | number : string | number;
140
+ };
141
+ /** The arguments of `t` after the key: the params, only when there are some. */
142
+ export type ParamsArgs<T extends MessageText> = string extends T ? [params?: Record<string, string | number>] : [Placeholders<T>] extends [never] ? [] : [params: ParamsOf<T>];
143
+ /**
144
+ * The languages of `src/messages/`, as the editor sees them: one key per
145
+ * file. Filled in by the `.chapterjs/` folder of the project, empty when
146
+ * the folder has no language.
147
+ */
148
+ export interface ProjectLocales {
149
+ }
150
+ /** A language of `src/messages/`: `'en-US'`, `'fr'`... */
151
+ export type ProjectLocale = keyof ProjectLocales & string;
152
+ /**
153
+ * A language given to `t.in()`: the editor offers the ones of
154
+ * `src/messages/`, and any language of Discord is accepted (the one of a
155
+ * person, of a server), `t` then taking the closest one declared. A word
156
+ * that is no language of Discord is underlined, with the ones of the
157
+ * project.
158
+ */
159
+ export type LocaleGiven<L extends string> = L extends Locale ? L : ProjectLocale;
160
+ /**
161
+ * `t`, as handlers receive it: gives a text of `src/messages/` in the
162
+ * language of who will read the message, with its placeholders filled in.
163
+ * `M` is the texts of the default language, which types the keys and the
164
+ * params: a text another language does not have is taken from it.
165
+ */
166
+ export interface Translator<M extends object = ProjectMessages> {
167
+ <K extends keyof M & string>(key: K, ...params: ParamsArgs<TextOf<M, K>>): string;
168
+ /** The language the texts come from: the one asked, or the closest one declared. */
169
+ readonly locale: Locale;
170
+ /**
171
+ * A text in another language: `t.in('fr', 'welcome', { name })`, or
172
+ * `t.in(owner.locale, 'pong')`. The language of the person who will read
173
+ * it, when it is not the one `t` speaks.
174
+ */
175
+ in<L extends ProjectLocale | (string & {}), K extends keyof M & string>(locale: LocaleGiven<L>, key: K, ...params: ParamsArgs<TextOf<M, K>>): string;
176
+ /** The same `t` for another language, to write several texts: `t.in('fr')('welcome', { name })`. */
177
+ in<L extends ProjectLocale | (string & {})>(locale: LocaleGiven<L>): Translator<M>;
178
+ }
179
+ /**
180
+ * The texts of `src/messages/`, as the editor sees them: the keys `t`
181
+ * accepts and the placeholders of each. Filled in by the `.chapterjs/`
182
+ * folder of the project when the folder has a language, empty otherwise.
183
+ */
184
+ export interface ProjectMessages {
185
+ }
186
+ /** What `t` is typed from, for a language file: its texts. */
187
+ export type MessagesOf<F> = F extends LanguageFile<infer T> ? T : never;
188
+ /**
189
+ * What every handler receives when the project has a language in
190
+ * `src/messages/`: `t`. Nothing otherwise.
191
+ */
192
+ export type TranslationContext = [keyof ProjectMessages] extends [never] ? {} : {
193
+ /** The texts of `src/messages/`, in the right language. */ t: Translator;
194
+ };
195
+ /** What `language()` returns: the default export of a file of `src/messages/`. */
196
+ export interface LanguageFile<T extends MessageTexts = MessageTexts> {
197
+ /** What the file gave to `language()`, not checked yet. */
198
+ readonly config: LanguageConfig<T>;
199
+ }
200
+ /**
201
+ * Declares one language of the bot. Export the result as the default
202
+ * export of a file of `src/messages/` named after the language
203
+ * (`en-US.ts`, `fr.ts`...): every handler then receives `t`, which gives a
204
+ * text in the language of who will read the message, with its
205
+ * `{placeholders}` filled in.
206
+ *
207
+ * ```ts
208
+ * // src/messages/fr.ts
209
+ * import { language } from 'chapterjs';
210
+ *
211
+ * export default language({
212
+ * texts: { pong: 'Pong !', welcome: 'Bienvenue {name} !' },
213
+ * });
214
+ * ```
215
+ */
216
+ export declare function language<const T extends MessageTexts>(config: LanguageConfig<T>): LanguageFile<T>;
217
+ /** Whether a value was made by `language()`. */
218
+ export declare function isLanguageFile(value: unknown): value is LanguageFile;
219
+ export {};
@@ -0,0 +1,30 @@
1
+ // `language()`, as the files of `src/messages/` import it from 'chapterjs':
2
+ // every text the bot answers with in one language, and `t`, which handlers
3
+ // receive to pick the right one.
4
+ // https://docs.discord.com/developers/reference#locales
5
+ const BRAND = Symbol.for('chapterjs.language');
6
+ /**
7
+ * Declares one language of the bot. Export the result as the default
8
+ * export of a file of `src/messages/` named after the language
9
+ * (`en-US.ts`, `fr.ts`...): every handler then receives `t`, which gives a
10
+ * text in the language of who will read the message, with its
11
+ * `{placeholders}` filled in.
12
+ *
13
+ * ```ts
14
+ * // src/messages/fr.ts
15
+ * import { language } from 'chapterjs';
16
+ *
17
+ * export default language({
18
+ * texts: { pong: 'Pong !', welcome: 'Bienvenue {name} !' },
19
+ * });
20
+ * ```
21
+ */
22
+ export function language(config) {
23
+ return Object.freeze({ [BRAND]: true, config });
24
+ }
25
+ /** Whether a value was made by `language()`. */
26
+ export function isLanguageFile(value) {
27
+ return (typeof value === 'object' &&
28
+ value !== null &&
29
+ value[BRAND] === true);
30
+ }
@@ -0,0 +1,23 @@
1
+ import type { Context } from '../structures/context.js';
2
+ import type { FrameworkTexts } from './messages.js';
3
+ import { type Template } from './translate.js';
4
+ /** The phrases of the framework, in English. */
5
+ export declare const FRAMEWORK_TEXTS: Readonly<FrameworkTexts>;
6
+ /** A phrase of the framework. */
7
+ export type FrameworkKey = keyof FrameworkTexts;
8
+ /** The word for what was used, in `{what}`. */
9
+ export type What = 'command' | 'button' | 'menu' | 'form';
10
+ /** The phrases of the framework, compiled, by key. */
11
+ export declare const FRAMEWORK: ReadonlyMap<FrameworkKey, Template>;
12
+ /** The keys a language file may translate, in the order of the docs. */
13
+ export declare const FRAMEWORK_KEYS: (keyof FrameworkTexts)[];
14
+ /**
15
+ * A phrase of the framework in the language of who will read it: the one
16
+ * of the language file that translates it, else the English one.
17
+ */
18
+ export declare function phrase(ctx: Pick<Context, 'messages'>, locale: string | null, key: FrameworkKey, params?: {
19
+ what?: What;
20
+ permissions?: string;
21
+ count?: number;
22
+ when?: string;
23
+ }): string;
@@ -0,0 +1,44 @@
1
+ // What the bot answers by itself, when it can't run what was asked: in
2
+ // English here, and in the languages of `src/messages/` that translate
3
+ // them (the `framework` of a language file).
4
+ import { fill, pickLocale, templateOf } from './translate.js';
5
+ /** The phrases of the framework, in English. */
6
+ export const FRAMEWORK_TEXTS = Object.freeze({
7
+ command: 'command',
8
+ button: 'button',
9
+ menu: 'menu',
10
+ form: 'form',
11
+ guildOnly: 'This {what} can only be used in a server.',
12
+ dmOnly: 'This {what} can only be used in a private message with me.',
13
+ notHere: 'This {what} can not be used here.',
14
+ unavailable: 'This command is not available right now.',
15
+ gone: 'This {what} is not available any more.',
16
+ outdated: 'This {what} is out of date.',
17
+ authorOnly: 'Only the person who used the command can use this {what}.',
18
+ failed: 'Something went wrong while running this {what}.',
19
+ missingPermission: "I don't have the permission to do that here.",
20
+ cooldown: 'You can use this {what} again {when}.',
21
+ needsPermissions: {
22
+ one: 'You need the {permissions} permission to use this command.',
23
+ other: 'You need the {permissions} permissions to use this command.',
24
+ },
25
+ });
26
+ /** The phrases of the framework, compiled, by key. */
27
+ export const FRAMEWORK = new Map(Object.entries(FRAMEWORK_TEXTS).map(([key, text]) => [key, templateOf(text)]));
28
+ /** The keys a language file may translate, in the order of the docs. */
29
+ export const FRAMEWORK_KEYS = [...FRAMEWORK.keys()];
30
+ /**
31
+ * A phrase of the framework in the language of who will read it: the one
32
+ * of the language file that translates it, else the English one.
33
+ */
34
+ export function phrase(ctx, locale, key, params = {}) {
35
+ const { messages } = ctx;
36
+ const picked = messages ? pickLocale(messages, locale) : null;
37
+ const own = picked ? messages.framework.get(picked)?.get(key) : undefined;
38
+ const template = own ?? FRAMEWORK.get(key);
39
+ const spoken = own ? picked : 'en-US';
40
+ return fill(key, template, {
41
+ ...params,
42
+ ...(params.what ? { what: phrase(ctx, locale, params.what) } : {}),
43
+ }, spoken);
44
+ }