pankosmia-lib 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # pankosmia-lib
2
+
3
+ A utility library for the Pankosmia ecosystem.
4
+
5
+ ## Modules
6
+
7
+ The library is divided into multiple modules:
8
+
9
+ - **`http`** — Functions for communicating with a Pankosmia server through HTTP requests (GET/POST).
10
+ - **`i18n`** — Internationalization utilities, mainly the `do18n` function.
11
+ - **`usfm`** — Utility functions for working with USFM.
12
+
13
+ Each module can be imported independently:
14
+
15
+ ```ts
16
+ import { ... } from "pankosmia-lib/http"
17
+ import { ... } from "pankosmia-lib/i18n"
18
+ import { ... } from "pankosmia-lib/usfm"
19
+ ```
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ pnpm add pankosmia-lib
25
+ ```
26
+
27
+ ## Testing
28
+
29
+ Tests require a running Pankosmia server.
30
+
31
+ Once the server is running, execute:
32
+
33
+ ```bash
34
+ pnpm run test
35
+ ```
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "name": "pankosmia-lib",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
+ "packageManager": "pnpm@11.20.0",
7
+ "engines": {
8
+ "node": ">=24",
9
+ "pnpm": "11.20.0"
10
+ },
6
11
  "files": [
7
12
  "src",
8
13
  "index.js",
@@ -1,88 +1,323 @@
1
- async function getJson(url, debug=false) {
2
- try {
3
- const response = await fetch(url);
4
- if (!response.ok) {
5
- const result = {
6
- url,
7
- ok: false,
8
- status: response.status,
9
- error: await response.text()
10
- };
11
- if (debug) {
12
- console.log("getJson", result);
13
- }
14
- return result;
15
- }
16
- const result = {
17
- url,
18
- ok: true,
19
- status: response.status,
20
- json: await response.json()
21
- };
22
- if (debug) {
23
- console.log("getJson", result);
24
- }
25
- return result;
26
- } catch (err) {
27
- const result = {
28
- url,
29
- ok: false,
30
- status: 0,
31
- error: err.message
32
- };
33
- if (debug) {
34
- console.log("getJson", result);
35
- }
36
- return result;
1
+ /**
2
+ * @typedef {Object} GetSuccess
3
+ * @property {string} url - The requested URL.
4
+ * @property {true} ok - Indicates that the request succeeded.
5
+ * @property {number} status - The HTTP response status code.
6
+ */
7
+
8
+ /**
9
+ * @typedef {GetSuccess & {
10
+ * json: any
11
+ * }} GetJsonResult
12
+ *
13
+ * @typedef {GetSuccess & {
14
+ * text: string
15
+ * }} GetTextResult
16
+ *
17
+ * @typedef {GetSuccess & {
18
+ * bytes: ArrayBuffer
19
+ * }} GetBytesResult
20
+ *
21
+ * @typedef {Object} GetError
22
+ * @property {string} url - The requested URL.
23
+ * @property {false} ok - Indicates that the request failed.
24
+ * @property {number} status - The HTTP status code, or 0 when the request
25
+ * failed before receiving an HTTP response.
26
+ * @property {any|string} error - The server error or JavaScript error message.
27
+ */
28
+
29
+ /**
30
+ * Performs an HTTP GET request and parses the response as JSON.
31
+ *
32
+ * Use this function when the API endpoint returns a JSON response.
33
+ *
34
+ * @param {string} url - The URL to request.
35
+ * @param {boolean} [debug=false] - If true, logs the result to the console.
36
+ *
37
+ * @returns {Promise<GetJsonResult|GetError>} The HTTP response result.
38
+ *
39
+ * @example
40
+ * const result = await getJson(
41
+ * "http://127.0.0.1:19119/api/settings/languages/en"
42
+ * );
43
+ *
44
+ * if (result.ok) {
45
+ * console.log(result.status);
46
+ * console.log(result.json);
47
+ * } else {
48
+ * console.error(result.error);
49
+ * }
50
+ *
51
+ * @example
52
+ * // Enable debug logging.
53
+ * const result = await getJson(
54
+ * "https://example.com/api/settings",
55
+ * true
56
+ * );
57
+ */
58
+ async function getJson(url, debug = false) {
59
+ try {
60
+ const response = await fetch(url);
61
+
62
+ if (!response.ok) {
63
+ const result = {
64
+ url,
65
+ ok: false,
66
+ status: response.status,
67
+ error: JSON.parse(await response.text()),
68
+ };
69
+
70
+ if (debug) {
71
+ console.log("getJson", result);
72
+ }
73
+
74
+ return result;
75
+ }
76
+
77
+ const result = {
78
+ url,
79
+ ok: true,
80
+ status: response.status,
81
+ json: await response.json(),
82
+ };
83
+
84
+ if (debug) {
85
+ console.log("getJson", result);
86
+ }
87
+
88
+ return result;
89
+ } catch (err) {
90
+ const result = {
91
+ url,
92
+ ok: false,
93
+ status: 0,
94
+ error: err.message,
95
+ };
96
+
97
+ if (debug) {
98
+ console.log("getJson", result);
37
99
  }
100
+
101
+ return result;
102
+ }
38
103
  }
39
- async function getText(url, debug=false) {
40
- try {
41
- const response = await fetch(url);
42
- if (!response.ok) {
43
- const result = {
44
- url,
45
- ok: false,
46
- status: response.status,
47
- error: await response.text()
48
- };
49
- if (debug) {
50
- console.log("getText", result);
51
- }
52
- return result;
53
- }
54
- const result = {
55
- url,
56
- ok: true,
57
- status: response.status,
58
- text: await response.text()
59
- };
60
- if (debug) {
61
- console.log("getText", result);
62
- }
63
- return result;
64
- } catch (err) {
65
- const result = {
66
- url,
67
- ok: false,
68
- status: 0,
69
- error: err.message
70
- };
71
- if (debug) {
72
- console.log("getText", result);
73
- }
74
- return result;
104
+
105
+ /**
106
+ * Performs an HTTP GET request and returns the response body as text.
107
+ *
108
+ * Use this function when the API endpoint returns plain text, source text,
109
+ * configuration text, USFM, XML, HTML, or another text-based response.
110
+ *
111
+ * @param {string} url - The URL to request.
112
+ * @param {boolean} [debug=false] - If true, logs the result to the console.
113
+ *
114
+ * @returns {Promise<GetTextResult|GetError>} The HTTP response result.
115
+ *
116
+ * @example
117
+ * const result = await getText(
118
+ * "http://127.0.0.1:19119/api/burrito/ingredient/raw/_local_/_local_/plt?ipath=test.usfm"
119
+ * );
120
+ *
121
+ * if (result.ok) {
122
+ * console.log(result.text);
123
+ * } else {
124
+ * console.error(result.error);
125
+ * }
126
+ *
127
+ * @example
128
+ * // Enable debug logging.
129
+ * const result = await getText(
130
+ * "https://example.com/api/content",
131
+ * true
132
+ * );
133
+ */
134
+ async function getText(url, debug = false) {
135
+ try {
136
+ const response = await fetch(url);
137
+
138
+ if (!response.ok) {
139
+ const result = {
140
+ url,
141
+ ok: false,
142
+ status: response.status,
143
+ error: JSON.parse(await response.text()),
144
+ };
145
+
146
+ if (debug) {
147
+ console.log("getText", result);
148
+ }
149
+
150
+ return result;
75
151
  }
152
+
153
+ const result = {
154
+ url,
155
+ ok: true,
156
+ status: response.status,
157
+ text: await response.text(),
158
+ };
159
+
160
+ if (debug) {
161
+ console.log("getText", result);
162
+ }
163
+
164
+ return result;
165
+ } catch (err) {
166
+ const result = {
167
+ url,
168
+ ok: false,
169
+ status: 0,
170
+ error: err.message,
171
+ };
172
+
173
+ if (debug) {
174
+ console.log("getText", result);
175
+ }
176
+
177
+ return result;
178
+ }
76
179
  }
77
180
 
78
- const getAndSetJson = async ({url, setter, debug=false}) => {
79
- const response = await getJson(url, debug);
80
- if (response.ok
81
- ) {
82
- setter(response.json);
83
- } else {
84
- return response;
181
+ /**
182
+ * Performs an HTTP GET request and returns the response body as binary data.
183
+ *
184
+ * The response is returned as an ArrayBuffer, which can be converted to
185
+ * a Uint8Array when byte-level access is required.
186
+ *
187
+ * @param {string} url - The URL to request.
188
+ * @param {boolean} [debug=false] - If true, logs the result to the console.
189
+ *
190
+ * @returns {Promise<GetBytesResult|GetError>} The HTTP response result.
191
+ *
192
+ * @example
193
+ * const result = await getBytes(
194
+ * "http://127.0.0.1:19119/api/burrito/ingredient/bytes/_local_/_local_/plt?ipath=test.usfm"
195
+ * );
196
+ *
197
+ * if (result.ok) {
198
+ * const bytes = new Uint8Array(result.bytes);
199
+ *
200
+ * console.log(bytes);
201
+ * } else {
202
+ * console.error(result.error);
203
+ * }
204
+ *
205
+ * @example
206
+ * // Enable debug logging.
207
+ * const result = await getBytes(
208
+ * "https://example.com/api/file",
209
+ * true
210
+ * );
211
+ */
212
+ async function getBytes(url, debug = false) {
213
+ try {
214
+ const response = await fetch(url);
215
+
216
+ if (!response.ok) {
217
+ const result = {
218
+ url,
219
+ ok: false,
220
+ status: response.status,
221
+ error: JSON.parse(await response.text()),
222
+ };
223
+
224
+ if (debug) {
225
+ console.log("getBytes", result);
226
+ }
227
+
228
+ return result;
85
229
  }
230
+
231
+ const result = {
232
+ url,
233
+ ok: true,
234
+ status: response.status,
235
+ bytes: await response.arrayBuffer(),
236
+ };
237
+
238
+ if (debug) {
239
+ console.log("getBytes", result);
240
+ }
241
+
242
+ return result;
243
+ } catch (err) {
244
+ const result = {
245
+ url,
246
+ ok: false,
247
+ status: 0,
248
+ error: err.message,
249
+ };
250
+
251
+ if (debug) {
252
+ console.log("getBytes", result);
253
+ }
254
+
255
+ return result;
256
+ }
86
257
  }
87
258
 
88
- export {getJson, getAndSetJson, getText};
259
+ /**
260
+ * Performs a GET request for JSON and passes the resulting JSON value
261
+ * to a setter function when the request succeeds.
262
+ *
263
+ * This is a convenience wrapper around {@link getJson}.
264
+ *
265
+ * If the request fails, the error response is returned instead of calling
266
+ * the setter.
267
+ *
268
+ * @param {Object} options - Function options.
269
+ * @param {string} options.url - The URL to request.
270
+ * @param {(json: any) => void} options.setter - Function that receives
271
+ * the parsed JSON response.
272
+ * @param {boolean} [options.debug=false] - If true, logs the request result.
273
+ *
274
+ * @returns {Promise<GetError|undefined>} The error response when the request
275
+ * fails, otherwise undefined.
276
+ *
277
+ * @example
278
+ * let languages;
279
+ *
280
+ * const result = await getAndSetJson({
281
+ * url: "http://127.0.0.1:19119/api/settings/languages/en",
282
+ * setter: (json) => {
283
+ * languages = json;
284
+ * },
285
+ * });
286
+ *
287
+ * if (result) {
288
+ * console.error(result.error);
289
+ * }
290
+ *
291
+ * @example
292
+ * // Using a framework state setter.
293
+ * await getAndSetJson({
294
+ * url: "https://example.com/api/settings",
295
+ * setter: setSettings,
296
+ * });
297
+ *
298
+ * @example
299
+ * // Enable debug logging.
300
+ * await getAndSetJson({
301
+ * url: "https://example.com/api/settings",
302
+ * setter: (json) => {
303
+ * console.log("Received:", json);
304
+ * },
305
+ * debug: true,
306
+ * });
307
+ */
308
+ const getAndSetJson = async ({ url, setter, debug = false }) => {
309
+ const response = await getJson(url, debug);
310
+
311
+ if (response.ok) {
312
+ setter(response.json);
313
+ } else {
314
+ return response;
315
+ }
316
+ };
317
+
318
+ export {
319
+ getJson,
320
+ getAndSetJson,
321
+ getText,
322
+ getBytes,
323
+ };
package/src/http/index.js CHANGED
@@ -1,12 +1,14 @@
1
1
 
2
- import {getJson, getAndSetJson, getText} from "./getLib.js";
3
- import {postEmptyJson, postJson, postText} from "./postLib.js";
2
+ import {getJson, getAndSetJson, getText,getBytes} from "./getLib.js";
3
+ import {postEmpty, postJson, postText,postBytes} from "./postLib.js";
4
4
 
5
5
  export {
6
6
  getJson,
7
7
  getText,
8
8
  getAndSetJson,
9
- postEmptyJson,
9
+ postEmpty,
10
10
  postJson,
11
- postText
11
+ postText,
12
+ postBytes,
13
+ getBytes
12
14
  };
@@ -1,128 +1,383 @@
1
- async function postEmptyJson(url, debug=false) {
2
- try {
3
- const response = await fetch(url, {method: "POST"});
4
- if (!response.ok) {
5
- const result = {
6
- url,
7
- ok: false,
8
- status: response.status,
9
- error: await response.text()
10
- };
11
- if (debug) {
12
- console.log("postEmptyJson", result);
13
- }
14
- return result;
15
- }
16
- const result = {
17
- url,
18
- ok: true,
19
- status: response.status,
20
- json: await response.json()
21
- };
22
- if (debug) {
23
- console.log("postEmptyJson", result);
24
- }
25
- return result;
26
- } catch (err) {
27
- const result = {
28
- url,
29
- ok: false,
30
- status: 0,
31
- error: err.message
32
- };
33
- if (debug) {
34
- console.log("postEmptyJson", result);
35
- }
36
- return result;
1
+ /**
2
+ * Sends an HTTP POST request without a request body.
3
+ *
4
+ * The server response is expected to contain JSON.
5
+ *
6
+ * @param {string} url - The URL to send the POST request to.
7
+ * @param {boolean} [debug=false] - If true, logs the request result to the console.
8
+ *
9
+ * @returns {Promise<PostEmptyResult>} The HTTP response result.
10
+ *
11
+ * @example
12
+ * // POST /api/settings/languages/en
13
+ * const result = await postEmpty(
14
+ * "http://127.0.0.1:19119/api/settings/languages/en"
15
+ * );
16
+ *
17
+ * if (result.ok) {
18
+ * console.log(result.status);
19
+ * console.log(result.json);
20
+ * } else {
21
+ * console.error(result.error);
22
+ * }
23
+ */
24
+ async function postEmpty(url, debug = false) {
25
+ try {
26
+ const response = await fetch(url, { method: "POST" });
27
+
28
+ if (!response.ok) {
29
+ const result = {
30
+ url,
31
+ ok: false,
32
+ status: response.status,
33
+ error: JSON.parse(await response.text()),
34
+ };
35
+
36
+ if (debug) {
37
+ console.log("postEmpty", result);
38
+ }
39
+
40
+ return result;
41
+ }
42
+
43
+ const result = {
44
+ url,
45
+ ok: true,
46
+ status: response.status,
47
+ json: await response.json(),
48
+ };
49
+
50
+ if (debug) {
51
+ console.log("postEmpty", result);
37
52
  }
53
+
54
+ return result;
55
+ } catch (err) {
56
+ const result = {
57
+ url,
58
+ ok: false,
59
+ status: 0,
60
+ error: err.message,
61
+ };
62
+
63
+ if (debug) {
64
+ console.log("postEmpty", result);
65
+ }
66
+
67
+ return result;
68
+ }
38
69
  }
39
70
 
40
- async function postJson(url, body, debug=false, contentType = "application/json") {
41
- try {
42
- const response = await fetch(
43
- url,
44
- {
45
- method: "POST",
46
- headers: {"Content-Type": contentType},
47
- body});
48
- if (!response.ok) {
49
- const result = {
50
- url,
51
- ok: false,
52
- status: response.status,
53
- error: await response.text()
54
- };
55
- if (debug) {
56
- console.log("postJson", result);
57
- }
58
- return result;
59
- }
60
- const result = {
61
- url,
62
- ok: true,
63
- status: response.status,
64
- json: await response.json()
65
- };
66
- if (debug) {
67
- console.log("postJson", result);
68
- }
69
- return result;
70
- } catch (err) {
71
- const result = {
72
- url,
73
- ok: false,
74
- status: 0,
75
- error: err.message
76
- };
77
- if (debug) {
78
- console.log("postJson", result);
79
- }
80
- return result;
71
+ /**
72
+ * Sends an HTTP POST request with a JSON request body.
73
+ *
74
+ * The request body is sent using the specified Content-Type.
75
+ * By default, the Content-Type is `application/json`.
76
+ *
77
+ * The server response is expected to contain JSON.
78
+ *
79
+ * @param {string} url - The URL to send the POST request to.
80
+ * @param {string|object} body - The request body. When using an object,
81
+ * convert it to JSON with JSON.stringify() before passing it.
82
+ * @param {boolean} [debug=false] - If true, logs the request result to the console.
83
+ * @param {string} [contentType="application/json"] - The Content-Type
84
+ * header sent with the request.
85
+ *
86
+ * @returns {Promise<PostJsonResult>} The HTTP response result.
87
+ *
88
+ * @example
89
+ * const body = JSON.stringify({
90
+ * payload: JSON.stringify({
91
+ * language: "en",
92
+ * }),
93
+ * });
94
+ *
95
+ * const result = await postJson(
96
+ * "http://127.0.0.1:19119/api/burrito/ingredient/raw/_local_/_local_/plt?ipath=test.json",
97
+ * body
98
+ * );
99
+ *
100
+ * if (result.ok) {
101
+ * console.log(result.json);
102
+ * } else {
103
+ * console.error(result.error);
104
+ * }
105
+ *
106
+ * @example
107
+ * // Custom Content-Type
108
+ * const result = await postJson(
109
+ * "https://example.com/api/data",
110
+ * JSON.stringify({ hello: "world" }),
111
+ * false,
112
+ * "application/custom+json"
113
+ * );
114
+ */
115
+ async function postJson(
116
+ url,
117
+ body,
118
+ debug = false,
119
+ contentType = "application/json",
120
+ ) {
121
+ try {
122
+ const response = await fetch(url, {
123
+ method: "POST",
124
+ headers: { "Content-Type": contentType },
125
+ body,
126
+ });
127
+
128
+ if (!response.ok) {
129
+ const result = {
130
+ url,
131
+ ok: false,
132
+ status: response.status,
133
+ error: JSON.parse(await response.text()),
134
+ };
135
+
136
+ if (debug) {
137
+ console.log("postJson", result);
138
+ }
139
+
140
+ return result;
141
+ }
142
+
143
+ const result = {
144
+ url,
145
+ ok: true,
146
+ status: response.status,
147
+ json: await response.json(),
148
+ };
149
+
150
+ if (debug) {
151
+ console.log("postJson", result);
152
+ }
153
+
154
+ return result;
155
+ } catch (err) {
156
+ const result = {
157
+ url,
158
+ ok: false,
159
+ status: 0,
160
+ error: err.message,
161
+ };
162
+
163
+ if (debug) {
164
+ console.log("postJson", result);
81
165
  }
166
+
167
+ return result;
168
+ }
82
169
  }
83
170
 
84
- async function postText(url, body, debug=false, contentType = "text/plain") {
85
- try {
86
- const response = await fetch(
87
- url,
88
- {
89
- method: "POST",
90
- headers: {"Content-Type": contentType},
91
- body});
92
- if (!response.ok) {
93
- const result = {
94
- url,
95
- ok: false,
96
- status: response.status,
97
- error: await response.text()
98
- };
99
- if (debug) {
100
- console.log("postText", result);
101
- }
102
- return result;
103
- }
104
- const result = {
105
- url,
106
- ok: true,
107
- status: response.status,
108
- text: await response.text()
109
- };
110
- if (debug) {
111
- console.log("postText", result);
112
- }
113
- return result;
114
- } catch (err) {
115
- const result = {
116
- url,
117
- ok: false,
118
- status: 0,
119
- error: err.message
120
- };
121
- if (debug) {
122
- console.log("postText", result);
123
- }
124
- return result;
171
+ /**
172
+ * Sends an HTTP POST request with a text request body.
173
+ *
174
+ * The request body is sent using the specified Content-Type.
175
+ * By default, the Content-Type is `application/json`.
176
+ *
177
+ * Unlike {@link postJson}, the response body is returned as text.
178
+ *
179
+ * @param {string} url - The URL to send the POST request to.
180
+ * @param {string} body - The text request body.
181
+ * @param {boolean} [debug=false] - If true, logs the request result to the console.
182
+ * @param {string} [contentType="application/json"] - The Content-Type
183
+ * header sent with the request.
184
+ *
185
+ * @returns {Promise<PostTextResult>} The HTTP response result.
186
+ *
187
+ * @example
188
+ * const body = JSON.stringify({
189
+ * payload: "this is a test",
190
+ * });
191
+ *
192
+ * const result = await postText(
193
+ * "http://127.0.0.1:19119/api/burrito/ingredient/raw/_local_/_local_/plt?ipath=textTest.usfm",
194
+ * body
195
+ * );
196
+ *
197
+ * if (result.ok) {
198
+ * console.log(result.text);
199
+ * } else {
200
+ * console.error(result.error);
201
+ * }
202
+ *
203
+ * @example
204
+ * // Sending plain text instead of JSON
205
+ * const result = await postText(
206
+ * "https://example.com/api/text",
207
+ * "this is a test",
208
+ * false,
209
+ * "text/plain"
210
+ * );
211
+ */
212
+ async function postText(
213
+ url,
214
+ body,
215
+ debug = false,
216
+ contentType = "application/json",
217
+ ) {
218
+ try {
219
+ const response = await fetch(url, {
220
+ method: "POST",
221
+ headers: { "Content-Type": contentType },
222
+ body,
223
+ });
224
+
225
+ if (!response.ok) {
226
+ const result = {
227
+ url,
228
+ ok: false,
229
+ status: response.status,
230
+ error: JSON.parse(await response.text()),
231
+ };
232
+
233
+ if (debug) {
234
+ console.log("postText", result);
235
+ }
236
+
237
+ return result;
125
238
  }
239
+
240
+ const result = {
241
+ url,
242
+ ok: true,
243
+ status: response.status,
244
+ text: await response.text(),
245
+ };
246
+
247
+ if (debug) {
248
+ console.log("postText", result);
249
+ }
250
+
251
+ return result;
252
+ } catch (err) {
253
+ const result = {
254
+ url,
255
+ ok: false,
256
+ status: 0,
257
+ error: err.message,
258
+ };
259
+
260
+ if (debug) {
261
+ console.log("postText", result);
262
+ }
263
+
264
+ return result;
265
+ }
126
266
  }
127
267
 
128
- export {postEmptyJson, postJson, postText};
268
+ /**
269
+ * Sends an HTTP POST request containing binary data or multipart FormData.
270
+ *
271
+ * When using FormData, the Content-Type header is intentionally not set
272
+ * manually. The browser/runtime automatically creates the required
273
+ * multipart boundary.
274
+ *
275
+ * This is useful for uploading files or binary content to an API.
276
+ *
277
+ * @param {string} url - The URL to send the POST request to.
278
+ * @param {BodyInit} body - The request body. This can be FormData,
279
+ * Uint8Array, ArrayBuffer, Blob, or another valid fetch BodyInit.
280
+ * @param {boolean} [debug=false] - If true, logs the request result to the console.
281
+ * @param {string|null} [contentType=null] - Optional Content-Type.
282
+ * This is ignored when `body` is a FormData instance.
283
+ *
284
+ * @returns {Promise<PostBytesResult>} The HTTP response result.
285
+ *
286
+ * @example
287
+ * // Upload a file using multipart/form-data.
288
+ * const form = new FormData();
289
+ *
290
+ * form.append(
291
+ * "file",
292
+ * new Blob(["this is a test"], {
293
+ * type: "application/octet-stream",
294
+ * }),
295
+ * "bytesTest.usfm"
296
+ * );
297
+ *
298
+ * const result = await postBytes(
299
+ * "http://127.0.0.1:19119/api/burrito/ingredient/bytes/_local_/_local_/plt?ipath=bytesTest.usfm",
300
+ * form
301
+ * );
302
+ *
303
+ * if (result.ok) {
304
+ * console.log(result.text);
305
+ * } else {
306
+ * console.error(result.error);
307
+ * }
308
+ *
309
+ * @example
310
+ * // Send raw binary data.
311
+ * const body = new TextEncoder().encode("this is a test");
312
+ *
313
+ * const result = await postBytes(
314
+ * "https://example.com/api/upload",
315
+ * body,
316
+ * true,
317
+ * "application/octet-stream"
318
+ * );
319
+ */
320
+ async function postBytes(url, body, debug = false, contentType = null) {
321
+ try {
322
+ const headers = {};
323
+
324
+ if (!(body instanceof FormData) && contentType) {
325
+ headers["Content-Type"] = contentType;
326
+ }
327
+
328
+ const response = await fetch(url, {
329
+ method: "POST",
330
+ headers,
331
+ body,
332
+ });
333
+
334
+ if (!response.ok) {
335
+ const result = {
336
+ url,
337
+ ok: false,
338
+ status: response.status,
339
+ error: JSON.parse(await response.text()),
340
+ };
341
+
342
+ if (debug) {
343
+ console.log("postBytes", result);
344
+ }
345
+
346
+ return result;
347
+ }
348
+
349
+ const result = {
350
+ url,
351
+ ok: true,
352
+ status: response.status,
353
+ text: await response.text(),
354
+ };
355
+
356
+ if (debug) {
357
+ console.log("postBytes", result);
358
+ }
359
+
360
+ return result;
361
+ } catch (err) {
362
+ const result = {
363
+ url,
364
+ ok: false,
365
+ status: 0,
366
+ error: err.message,
367
+ };
368
+
369
+ if (debug) {
370
+ console.log("postBytes", result);
371
+ }
372
+
373
+ return result;
374
+ }
375
+ }
376
+
377
+ export {
378
+ postEmpty,
379
+ postJson,
380
+ postText,
381
+ postBytes,
382
+ };
383
+
@@ -1,3 +1,44 @@
1
1
  const doI18n = (key, source) => source[key] || key;
2
2
 
3
- export {doI18n};
3
+ const doInterpolatedI18n = (key, values, source = {}) => {
4
+ if (!Array.isArray(values)) {
5
+ throw new TypeError("Interpolation values must be an array of strings");
6
+ }
7
+ for (const value of values) {
8
+ if (typeof value !== "string") {
9
+ throw new TypeError("Interpolation values must be strings");
10
+ }
11
+ }
12
+
13
+ const template = source && source[key];
14
+ if (!template) {
15
+ throw new Error(`No localized string found for key: ${key}`);
16
+ }
17
+
18
+ if (typeof template !== "string") {
19
+ throw new TypeError("The localized string template must be a string");
20
+ }
21
+
22
+ const invalidPlaceholder = template.match(/%([^%]*)%/g)?.find((placeholder) => {
23
+ const position = placeholder.slice(1, -1);
24
+ return position === "" || (/\S/.test(position) && !/^\d+$/.test(position));
25
+ });
26
+ if (invalidPlaceholder) {
27
+ throw new SyntaxError(`Invalid interpolation placeholder: ${invalidPlaceholder}`);
28
+ }
29
+
30
+ const placeholders = template.match(/%\d+%/g) || [];
31
+ if (placeholders.length !== values.length) {
32
+ throw new RangeError("The number of interpolation values must match the placeholders");
33
+ }
34
+
35
+ return template.replace(/%(\d+)%/g, (placeholder, position) => {
36
+ const index = Number(position) - 1;
37
+ if (index < 0 || index >= values.length) {
38
+ throw new RangeError(`Placeholder ${placeholder} has no matching interpolation value`);
39
+ }
40
+ return values[index];
41
+ });
42
+ };
43
+
44
+ export {doI18n, doInterpolatedI18n};
package/src/i18n/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { doI18n } from "./i18nLib.js";
1
+ import { doI18n, doInterpolatedI18n } from "./i18nLib.js";
2
2
 
3
- export { doI18n };
3
+ export { doI18n, doInterpolatedI18n };