@tandem-language-exchange/content-store 1.1.2 → 1.2.1
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 +163 -20
- package/dist/chunk-EQ3DSPTJ.js +86 -0
- package/dist/chunk-EQ3DSPTJ.js.map +1 -0
- package/dist/{chunk-JBFJU4JA.js → chunk-LOCC2BXB.js} +118 -13
- package/dist/chunk-LOCC2BXB.js.map +1 -0
- package/dist/chunk-OTZLCMZ6.js +396 -0
- package/dist/chunk-OTZLCMZ6.js.map +1 -0
- package/dist/{chunk-YOREZCXB.js → chunk-PQJ2MGH7.js} +180 -29
- package/dist/chunk-PQJ2MGH7.js.map +1 -0
- package/dist/client/cli.js +31 -8
- package/dist/client/cli.js.map +1 -1
- package/dist/client/{fetch-bundles.js → fetch-content-bundles.js} +8 -7
- package/dist/client/fetch-content-bundles.js.map +1 -0
- package/dist/client/fetch-merged-translation-bundles.js +40 -0
- package/dist/client/fetch-merged-translation-bundles.js.map +1 -0
- package/dist/client/fetch-translation-bundles.js +43 -0
- package/dist/client/fetch-translation-bundles.js.map +1 -0
- package/dist/index-kfqHGgMO.d.ts +118 -0
- package/dist/index.d.ts +1 -1
- package/dist/node.browser.js +20 -6
- package/dist/node.browser.js.map +1 -1
- package/dist/node.d.ts +18 -5
- package/dist/node.js +527 -41
- package/dist/node.js.map +1 -1
- package/package.json +25 -11
- package/dist/chunk-JBFJU4JA.js.map +0 -1
- package/dist/chunk-UWGOF36L.js +0 -85
- package/dist/chunk-UWGOF36L.js.map +0 -1
- package/dist/chunk-YOREZCXB.js.map +0 -1
- package/dist/client/fetch-bundles.js.map +0 -1
- package/dist/index-DJXkO17k.d.ts +0 -83
package/dist/node.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import {
|
|
3
3
|
S3Client,
|
|
4
4
|
PutObjectCommand,
|
|
5
|
-
CopyObjectCommand,
|
|
6
5
|
GetObjectCommand
|
|
7
6
|
} from "@aws-sdk/client-s3";
|
|
8
7
|
var ContentStore = class {
|
|
@@ -18,14 +17,6 @@ var ContentStore = class {
|
|
|
18
17
|
});
|
|
19
18
|
this.bucket = cfg.bucket;
|
|
20
19
|
}
|
|
21
|
-
/** {cms}-{contentType}-{timestamp}.json */
|
|
22
|
-
buildVersionedKey(cms, contentType, timestamp) {
|
|
23
|
-
return `${cms}-${contentType}-${timestamp}.json`;
|
|
24
|
-
}
|
|
25
|
-
/** {cms}-{contentType}.json (always points at the latest version) */
|
|
26
|
-
buildLatestKey(cms, contentType) {
|
|
27
|
-
return `${cms}-${contentType}.json`;
|
|
28
|
-
}
|
|
29
20
|
async upload(key, data) {
|
|
30
21
|
await this.client.send(
|
|
31
22
|
new PutObjectCommand({
|
|
@@ -37,6 +28,18 @@ var ContentStore = class {
|
|
|
37
28
|
);
|
|
38
29
|
return key;
|
|
39
30
|
}
|
|
31
|
+
/** Raw UTF-8 body (e.g. Lingohub file bytes as text). */
|
|
32
|
+
async uploadRaw(key, body, contentType) {
|
|
33
|
+
await this.client.send(
|
|
34
|
+
new PutObjectCommand({
|
|
35
|
+
Bucket: this.bucket,
|
|
36
|
+
Key: key,
|
|
37
|
+
Body: body,
|
|
38
|
+
ContentType: contentType
|
|
39
|
+
})
|
|
40
|
+
);
|
|
41
|
+
return key;
|
|
42
|
+
}
|
|
40
43
|
async download(key) {
|
|
41
44
|
const response = await this.client.send(
|
|
42
45
|
new GetObjectCommand({ Bucket: this.bucket, Key: key })
|
|
@@ -45,27 +48,104 @@ var ContentStore = class {
|
|
|
45
48
|
if (!body) throw new Error(`Empty response for key: ${key}`);
|
|
46
49
|
return JSON.parse(body);
|
|
47
50
|
}
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
async copyToLatest(sourceKey, cms, contentType) {
|
|
53
|
-
const latestKey = this.buildLatestKey(cms, contentType);
|
|
54
|
-
await this.client.send(
|
|
55
|
-
new CopyObjectCommand({
|
|
56
|
-
Bucket: this.bucket,
|
|
57
|
-
CopySource: `${this.bucket}/${sourceKey}`,
|
|
58
|
-
Key: latestKey,
|
|
59
|
-
ContentType: "application/json"
|
|
60
|
-
})
|
|
51
|
+
/** Raw UTF-8 body from S3 (no JSON.parse). */
|
|
52
|
+
async downloadRaw(key) {
|
|
53
|
+
const response = await this.client.send(
|
|
54
|
+
new GetObjectCommand({ Bucket: this.bucket, Key: key })
|
|
61
55
|
);
|
|
62
|
-
|
|
56
|
+
const body = await response.Body?.transformToString();
|
|
57
|
+
if (!body) throw new Error(`Empty response for key: ${key}`);
|
|
58
|
+
return body;
|
|
63
59
|
}
|
|
64
60
|
};
|
|
61
|
+
var buildCmsObjectKey = (cms, contentType) => {
|
|
62
|
+
return `${cms}-${contentType}.json`;
|
|
63
|
+
};
|
|
64
|
+
var buildTranslationObjectKey = (project, fileName, locale) => {
|
|
65
|
+
return `lingohub-${project}.${fileName.replaceAll("[locale]", locale)}`;
|
|
66
|
+
};
|
|
65
67
|
|
|
66
68
|
// src/shared/bundles.ts
|
|
67
69
|
import fs from "fs/promises";
|
|
68
|
-
import
|
|
70
|
+
import path2 from "path";
|
|
71
|
+
|
|
72
|
+
// src/shared/s3-retry.ts
|
|
73
|
+
function computeDelay(attempt, baseDelayMs, maxDelayMs) {
|
|
74
|
+
const exponential = baseDelayMs * Math.pow(2, attempt);
|
|
75
|
+
const jitter = Math.random() * baseDelayMs;
|
|
76
|
+
return Math.min(exponential + jitter, maxDelayMs);
|
|
77
|
+
}
|
|
78
|
+
function getDefaultS3RetryConfig() {
|
|
79
|
+
return {
|
|
80
|
+
maxRetries: parseInt(
|
|
81
|
+
process.env.S3_RETRY_MAX_RETRIES ?? process.env.RETRY_MAX_RETRIES ?? "5",
|
|
82
|
+
10
|
|
83
|
+
),
|
|
84
|
+
baseDelayMs: parseInt(
|
|
85
|
+
process.env.S3_RETRY_BASE_DELAY_MS ?? process.env.RETRY_BASE_DELAY_MS ?? "1000",
|
|
86
|
+
10
|
|
87
|
+
),
|
|
88
|
+
maxDelayMs: parseInt(
|
|
89
|
+
process.env.S3_RETRY_MAX_DELAY_MS ?? process.env.RETRY_MAX_DELAY_MS ?? "60000",
|
|
90
|
+
10
|
|
91
|
+
)
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function isRetryableS3DownloadError(err) {
|
|
95
|
+
if (err === null || err === void 0) return false;
|
|
96
|
+
if (typeof err === "object") {
|
|
97
|
+
const e = err;
|
|
98
|
+
const status = e.$metadata?.httpStatusCode;
|
|
99
|
+
if (status === 404 || status === 403 || status === 401 || status === 400) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
if (status !== void 0 && (status === 408 || status === 429 || status === 500 || status === 502 || status === 503 || status === 504)) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
const name = e.name ?? "";
|
|
106
|
+
const code = e.Code ?? "";
|
|
107
|
+
if (/SlowDown|Throttl|Timeout|TooManyRequests|ServiceUnavailable|InternalError/i.test(
|
|
108
|
+
name
|
|
109
|
+
) || /SlowDown|Throttl/i.test(code)) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (err instanceof Error) {
|
|
114
|
+
const m = err.message;
|
|
115
|
+
if (/ECONNRESET|ETIMEDOUT|EPIPE|ECONNREFUSED|socket hang up|getaddrinfo/i.test(
|
|
116
|
+
m
|
|
117
|
+
)) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
async function withS3Retry(fn, { maxRetries, baseDelayMs, maxDelayMs }) {
|
|
124
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
125
|
+
try {
|
|
126
|
+
return await fn();
|
|
127
|
+
} catch (err) {
|
|
128
|
+
if (!isRetryableS3DownloadError(err)) {
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
if (attempt === maxRetries) {
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
const delay = computeDelay(attempt, baseDelayMs, maxDelayMs);
|
|
135
|
+
console.warn(
|
|
136
|
+
` S3 request failed (attempt ${attempt + 1}/${maxRetries + 1}): ${err instanceof Error ? err.message : String(err)}. Retrying in ${Math.round(delay)}ms\u2026`
|
|
137
|
+
);
|
|
138
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
throw new Error("withS3Retry: unreachable");
|
|
142
|
+
}
|
|
143
|
+
async function downloadWithRetry(store, key, cfg) {
|
|
144
|
+
return withS3Retry(() => store.download(key), cfg);
|
|
145
|
+
}
|
|
146
|
+
async function downloadRawWithRetry(store, key, cfg) {
|
|
147
|
+
return withS3Retry(() => store.downloadRaw(key), cfg);
|
|
148
|
+
}
|
|
69
149
|
|
|
70
150
|
// src/shared/trimDepth.ts
|
|
71
151
|
function trimDepth(value, remaining) {
|
|
@@ -96,6 +176,301 @@ function trimDepth(value, remaining) {
|
|
|
96
176
|
return result;
|
|
97
177
|
}
|
|
98
178
|
|
|
179
|
+
// src/shared/lingohub.ts
|
|
180
|
+
var defaultLocales = ["en", "fr", "de", "es", "it", "pt-br", "ru", "zh-hans", "zh-hant", "ko", "ja"];
|
|
181
|
+
var localeMapping = {
|
|
182
|
+
ios: {
|
|
183
|
+
"pt-br": "pt",
|
|
184
|
+
"zh-hans": "zh-Hans",
|
|
185
|
+
"zh-hant": "zh-Hant"
|
|
186
|
+
},
|
|
187
|
+
android: {
|
|
188
|
+
"pt-br": "pt",
|
|
189
|
+
"zh-hans": "zh-Hans-CN",
|
|
190
|
+
"zh-hant": "zh-TW"
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
var allProjects = {
|
|
194
|
+
"tandem-(new-website)": [
|
|
195
|
+
{
|
|
196
|
+
fileName: "Website.[locale].json",
|
|
197
|
+
type: "json"
|
|
198
|
+
}
|
|
199
|
+
],
|
|
200
|
+
"tandem-(website)": [
|
|
201
|
+
{
|
|
202
|
+
fileName: "[locale].json",
|
|
203
|
+
type: "json"
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
fileName: "AI.[locale].json",
|
|
207
|
+
type: "json"
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
fileName: "languages.[locale].json",
|
|
211
|
+
type: "json"
|
|
212
|
+
}
|
|
213
|
+
],
|
|
214
|
+
"tandem": [
|
|
215
|
+
{
|
|
216
|
+
fileName: "InfoPlist.[locale].strings",
|
|
217
|
+
type: "strings",
|
|
218
|
+
localeMapping: localeMapping.ios
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
fileName: "Localizable.[locale].strings",
|
|
222
|
+
type: "strings",
|
|
223
|
+
localeMapping: localeMapping.ios
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
fileName: "MainiPad.[locale].strings",
|
|
227
|
+
type: "strings",
|
|
228
|
+
localeMapping: localeMapping.ios
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
fileName: "Main.[locale].strings",
|
|
232
|
+
type: "strings",
|
|
233
|
+
localeMapping: localeMapping.ios
|
|
234
|
+
}
|
|
235
|
+
],
|
|
236
|
+
"tandem-(android)": [
|
|
237
|
+
{
|
|
238
|
+
fileName: "accessibility_localizable.[locale].xml",
|
|
239
|
+
type: "xml",
|
|
240
|
+
localeMapping: localeMapping.android
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
fileName: "call_localizable.[locale].xml",
|
|
244
|
+
type: "xml",
|
|
245
|
+
localeMapping: localeMapping.android
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
fileName: "cert_localizable.[locale].xml",
|
|
249
|
+
type: "xml",
|
|
250
|
+
localeMapping: localeMapping.android
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
fileName: "chat_localizable.[locale].xml",
|
|
254
|
+
type: "xml",
|
|
255
|
+
localeMapping: localeMapping.android
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
fileName: "checklist_localizable.[locale].xml",
|
|
259
|
+
type: "xml",
|
|
260
|
+
localeMapping: localeMapping.android
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
fileName: "clubs_localizable.[locale].xml",
|
|
264
|
+
type: "xml",
|
|
265
|
+
localeMapping: localeMapping.android
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
fileName: "common_localizable.[locale].xml",
|
|
269
|
+
type: "xml",
|
|
270
|
+
localeMapping: localeMapping.android
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
fileName: "community_localizable.[locale].xml",
|
|
274
|
+
type: "xml",
|
|
275
|
+
localeMapping: localeMapping.android
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
fileName: "correction_localizable.[locale].xml",
|
|
279
|
+
type: "xml",
|
|
280
|
+
localeMapping: localeMapping.android
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
fileName: "country_names.[locale].xml",
|
|
284
|
+
type: "xml",
|
|
285
|
+
localeMapping: localeMapping.android
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
fileName: "emoji_localizable.[locale].xml",
|
|
289
|
+
type: "xml",
|
|
290
|
+
localeMapping: localeMapping.android
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
fileName: "errors_localizable.[locale].xml",
|
|
294
|
+
type: "xml",
|
|
295
|
+
localeMapping: localeMapping.android
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
fileName: "expressions_localizable.[locale].xml",
|
|
299
|
+
type: "xml",
|
|
300
|
+
localeMapping: localeMapping.android
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
fileName: "gif_localizable.[locale].xml",
|
|
304
|
+
type: "xml",
|
|
305
|
+
localeMapping: localeMapping.android
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
fileName: "guidelines_localizable.[locale].xml",
|
|
309
|
+
type: "xml",
|
|
310
|
+
localeMapping: localeMapping.android
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
fileName: "languages_localizable.[locale].xml",
|
|
314
|
+
type: "xml",
|
|
315
|
+
localeMapping: localeMapping.android
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
fileName: "localizable.[locale].xml",
|
|
319
|
+
type: "xml",
|
|
320
|
+
localeMapping: localeMapping.android
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
fileName: "localizable2.[locale].xml",
|
|
324
|
+
type: "xml",
|
|
325
|
+
localeMapping: localeMapping.android
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
fileName: "login_localizable.[locale].xml",
|
|
329
|
+
type: "xml",
|
|
330
|
+
localeMapping: localeMapping.android
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
fileName: "myprofile_localizable.[locale].xml",
|
|
334
|
+
type: "xml",
|
|
335
|
+
localeMapping: localeMapping.android
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
fileName: "onb_localizable.[locale].xml",
|
|
339
|
+
type: "xml",
|
|
340
|
+
localeMapping: localeMapping.android
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
fileName: "parties_localizable.[locale].xml",
|
|
344
|
+
type: "xml",
|
|
345
|
+
localeMapping: localeMapping.android
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
fileName: "pro_localizable.[locale].xml",
|
|
349
|
+
type: "xml",
|
|
350
|
+
localeMapping: localeMapping.android
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
fileName: "pro_screen_localizable.[locale].xml",
|
|
354
|
+
type: "xml",
|
|
355
|
+
localeMapping: localeMapping.android
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
fileName: "push_notification_localizable.[locale].xml",
|
|
359
|
+
type: "xml",
|
|
360
|
+
localeMapping: localeMapping.android
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
fileName: "reporting_localizable.[locale].xml",
|
|
364
|
+
type: "xml",
|
|
365
|
+
localeMapping: localeMapping.android
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
fileName: "translation_localizable.[locale].xml",
|
|
369
|
+
type: "xml",
|
|
370
|
+
localeMapping: localeMapping.android
|
|
371
|
+
}
|
|
372
|
+
],
|
|
373
|
+
"tandem-(web-invites)": [
|
|
374
|
+
{
|
|
375
|
+
fileName: "[locale].json",
|
|
376
|
+
type: "json"
|
|
377
|
+
}
|
|
378
|
+
]
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// src/shared/translationResource.ts
|
|
382
|
+
import path from "path";
|
|
383
|
+
|
|
384
|
+
// src/shared/utils.ts
|
|
385
|
+
import convert from "xml-js";
|
|
386
|
+
import set from "lodash.set";
|
|
387
|
+
import merge from "lodash.merge";
|
|
388
|
+
var transformObjectToFlat = (data) => {
|
|
389
|
+
const result = {};
|
|
390
|
+
const flatten = (obj, path3 = []) => {
|
|
391
|
+
Object.entries(obj).forEach(([key, value]) => {
|
|
392
|
+
if (typeof value === "object") {
|
|
393
|
+
flatten(value, path3.concat(key));
|
|
394
|
+
} else {
|
|
395
|
+
result[path3.concat(key).join(".")] = value;
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
};
|
|
399
|
+
flatten(data);
|
|
400
|
+
return result;
|
|
401
|
+
};
|
|
402
|
+
var convertXMLToJS = (xml) => {
|
|
403
|
+
const converted = convert.xml2js(xml, {
|
|
404
|
+
ignoreComment: true,
|
|
405
|
+
ignoreDeclaration: true,
|
|
406
|
+
ignoreInstruction: true,
|
|
407
|
+
compact: true
|
|
408
|
+
});
|
|
409
|
+
let mapped = {};
|
|
410
|
+
converted.resources.string.forEach((item) => {
|
|
411
|
+
mapped = {
|
|
412
|
+
...mapped,
|
|
413
|
+
[item._attributes.name]: item._text
|
|
414
|
+
};
|
|
415
|
+
});
|
|
416
|
+
return mapped;
|
|
417
|
+
};
|
|
418
|
+
var parseIOSStrings = (strings) => {
|
|
419
|
+
const parsedObj = {};
|
|
420
|
+
strings.split("\n").filter((line) => line.startsWith('"') && line.endsWith(";")).map((line) => line.trim().slice(0, -1)).forEach((line) => {
|
|
421
|
+
let [key, value] = line.split(" = ");
|
|
422
|
+
if (!key || !value) return;
|
|
423
|
+
key = key.slice(1, -1);
|
|
424
|
+
value = value.slice(1, -1);
|
|
425
|
+
parsedObj[key] = value;
|
|
426
|
+
});
|
|
427
|
+
return parsedObj;
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// src/shared/translationResource.ts
|
|
431
|
+
function parseTranslationResourceRaw(raw, resource) {
|
|
432
|
+
if (resource.type === "json") {
|
|
433
|
+
return JSON.parse(raw);
|
|
434
|
+
}
|
|
435
|
+
if (resource.type === "strings") {
|
|
436
|
+
return parseIOSStrings(raw);
|
|
437
|
+
}
|
|
438
|
+
if (resource.type === "xml") {
|
|
439
|
+
return convertXMLToJS(raw);
|
|
440
|
+
}
|
|
441
|
+
throw new Error(`Unsupported resource type: ${resource.type}`);
|
|
442
|
+
}
|
|
443
|
+
function toFlatStringMap(parsed) {
|
|
444
|
+
if (parsed === null || parsed === void 0) return {};
|
|
445
|
+
if (typeof parsed === "string") return { value: parsed };
|
|
446
|
+
if (typeof parsed !== "object") return { value: String(parsed) };
|
|
447
|
+
if (Array.isArray(parsed)) {
|
|
448
|
+
const out2 = {};
|
|
449
|
+
parsed.forEach((v, i) => {
|
|
450
|
+
out2[String(i)] = typeof v === "object" && v !== null ? JSON.stringify(v) : String(v);
|
|
451
|
+
});
|
|
452
|
+
return out2;
|
|
453
|
+
}
|
|
454
|
+
const flat = transformObjectToFlat(parsed);
|
|
455
|
+
const out = {};
|
|
456
|
+
for (const [k, v] of Object.entries(flat)) {
|
|
457
|
+
if (v === null || v === void 0) {
|
|
458
|
+
out[k] = "";
|
|
459
|
+
} else if (typeof v === "object") {
|
|
460
|
+
out[k] = JSON.stringify(v);
|
|
461
|
+
} else {
|
|
462
|
+
out[k] = String(v);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return out;
|
|
466
|
+
}
|
|
467
|
+
function translationJsonOutputPath(outputDir, objectKey) {
|
|
468
|
+
if (objectKey.endsWith(".json")) {
|
|
469
|
+
return path.resolve(outputDir, objectKey);
|
|
470
|
+
}
|
|
471
|
+
return path.resolve(outputDir, `${objectKey}.json`);
|
|
472
|
+
}
|
|
473
|
+
|
|
99
474
|
// src/shared/bundles.ts
|
|
100
475
|
function getAtPath(obj, dottedPath) {
|
|
101
476
|
const parts = dottedPath.split(".").filter((p) => p.length > 0);
|
|
@@ -126,8 +501,8 @@ function isEmptyValue(value) {
|
|
|
126
501
|
return typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
|
|
127
502
|
}
|
|
128
503
|
function matchesFieldFilter(item, key, expected) {
|
|
129
|
-
const { path:
|
|
130
|
-
const at = getAtPath(item,
|
|
504
|
+
const { path: path3, operator } = parseFieldKey(key);
|
|
505
|
+
const at = getAtPath(item, path3);
|
|
131
506
|
if (operator === "exists") {
|
|
132
507
|
if (expected !== true && expected !== false) return false;
|
|
133
508
|
const empty = !at.found || isEmptyValue(at.value);
|
|
@@ -153,26 +528,117 @@ function setNestedAt(target, dottedPath, value) {
|
|
|
153
528
|
}
|
|
154
529
|
setNestedAt(nested, rest, value);
|
|
155
530
|
}
|
|
156
|
-
async function
|
|
531
|
+
async function fetchCmsBundles(store, outputDir, options) {
|
|
157
532
|
const { cms, contentTypes } = options;
|
|
533
|
+
const retry = options.retry ?? getDefaultS3RetryConfig();
|
|
158
534
|
await fs.mkdir(outputDir, { recursive: true });
|
|
159
535
|
const result = {};
|
|
160
536
|
await Promise.all(
|
|
161
537
|
contentTypes.map(async (contentType) => {
|
|
162
|
-
const key =
|
|
163
|
-
const data = await store
|
|
164
|
-
const filePath =
|
|
165
|
-
outputDir,
|
|
166
|
-
`${cms}-${contentType}.json`
|
|
167
|
-
);
|
|
538
|
+
const key = buildCmsObjectKey(cms, contentType);
|
|
539
|
+
const data = await downloadWithRetry(store, key, retry);
|
|
540
|
+
const filePath = path2.resolve(outputDir, key);
|
|
168
541
|
await fs.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
169
542
|
result[contentType] = filePath;
|
|
170
543
|
})
|
|
171
544
|
);
|
|
172
545
|
return result;
|
|
173
546
|
}
|
|
174
|
-
async function
|
|
175
|
-
const
|
|
547
|
+
async function fetchTranslationBundles(store, outputDir, options) {
|
|
548
|
+
const { projects, locales } = options;
|
|
549
|
+
const retry = options.retry ?? getDefaultS3RetryConfig();
|
|
550
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
551
|
+
const result = {};
|
|
552
|
+
const localesToSync = locales && locales.length ? locales : defaultLocales;
|
|
553
|
+
await Promise.all(
|
|
554
|
+
projects.map(async (project) => {
|
|
555
|
+
const resources = allProjects[project];
|
|
556
|
+
if (!resources?.length) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const resourceTasks = [];
|
|
560
|
+
for (const resource of resources) {
|
|
561
|
+
for (const loc of localesToSync) {
|
|
562
|
+
const locale = resource.localeMapping && resource.localeMapping[loc] ? resource.localeMapping[loc] : loc;
|
|
563
|
+
const objectKey = buildTranslationObjectKey(
|
|
564
|
+
project,
|
|
565
|
+
resource.fileName,
|
|
566
|
+
locale
|
|
567
|
+
);
|
|
568
|
+
resourceTasks.push({ objectKey, resource });
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
await Promise.all(
|
|
572
|
+
resourceTasks.map(async ({ objectKey, resource }) => {
|
|
573
|
+
const raw = await downloadRawWithRetry(store, objectKey, retry);
|
|
574
|
+
const parsed = parseTranslationResourceRaw(raw, resource);
|
|
575
|
+
const filePath = translationJsonOutputPath(outputDir, objectKey);
|
|
576
|
+
await fs.mkdir(path2.dirname(filePath), { recursive: true });
|
|
577
|
+
await fs.writeFile(
|
|
578
|
+
filePath,
|
|
579
|
+
JSON.stringify(parsed, null, 2),
|
|
580
|
+
"utf-8"
|
|
581
|
+
);
|
|
582
|
+
if (!result[project]) {
|
|
583
|
+
result[project] = {};
|
|
584
|
+
}
|
|
585
|
+
result[project][objectKey] = filePath;
|
|
586
|
+
})
|
|
587
|
+
);
|
|
588
|
+
})
|
|
589
|
+
);
|
|
590
|
+
return result;
|
|
591
|
+
}
|
|
592
|
+
async function fetchMergedTranslationBundles(store, outputDir, options) {
|
|
593
|
+
const { projects, locales } = options;
|
|
594
|
+
const retry = options.retry ?? getDefaultS3RetryConfig();
|
|
595
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
596
|
+
const localesToSync = locales && locales.length ? locales : defaultLocales;
|
|
597
|
+
const tasks = [];
|
|
598
|
+
for (const project of projects) {
|
|
599
|
+
const resources = allProjects[project];
|
|
600
|
+
if (!resources?.length) continue;
|
|
601
|
+
for (const resource of resources) {
|
|
602
|
+
for (const loc of localesToSync) {
|
|
603
|
+
const mappedLocale = resource.localeMapping && resource.localeMapping[loc] ? resource.localeMapping[loc] : loc;
|
|
604
|
+
const objectKey = buildTranslationObjectKey(
|
|
605
|
+
project,
|
|
606
|
+
resource.fileName,
|
|
607
|
+
mappedLocale
|
|
608
|
+
);
|
|
609
|
+
tasks.push({ catalogLocale: loc, objectKey, resource });
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
const results = await Promise.all(
|
|
614
|
+
tasks.map(async ({ catalogLocale, objectKey, resource }) => {
|
|
615
|
+
const raw = await downloadRawWithRetry(store, objectKey, retry);
|
|
616
|
+
const parsed = parseTranslationResourceRaw(raw, resource);
|
|
617
|
+
const stringMap = toFlatStringMap(parsed);
|
|
618
|
+
return { catalogLocale, stringMap };
|
|
619
|
+
})
|
|
620
|
+
);
|
|
621
|
+
const merged = {};
|
|
622
|
+
for (const loc of localesToSync) {
|
|
623
|
+
merged[loc] = {};
|
|
624
|
+
}
|
|
625
|
+
for (const { catalogLocale, stringMap } of results) {
|
|
626
|
+
Object.assign(merged[catalogLocale], stringMap);
|
|
627
|
+
}
|
|
628
|
+
const out = {};
|
|
629
|
+
for (const loc of localesToSync) {
|
|
630
|
+
const filePath = path2.resolve(outputDir, `${loc}.json`);
|
|
631
|
+
await fs.writeFile(
|
|
632
|
+
filePath,
|
|
633
|
+
JSON.stringify(merged[loc], null, 2),
|
|
634
|
+
"utf-8"
|
|
635
|
+
);
|
|
636
|
+
out[loc] = filePath;
|
|
637
|
+
}
|
|
638
|
+
return out;
|
|
639
|
+
}
|
|
640
|
+
async function queryCmsBundle(outputDir, cms, contentType, options = {}) {
|
|
641
|
+
const filePath = path2.resolve(
|
|
176
642
|
outputDir,
|
|
177
643
|
`${cms}-${contentType}.json`
|
|
178
644
|
);
|
|
@@ -222,21 +688,41 @@ var ContentStoreSDK = class {
|
|
|
222
688
|
*
|
|
223
689
|
* @returns A map of contentType to absolute file path.
|
|
224
690
|
*/
|
|
225
|
-
async
|
|
226
|
-
return
|
|
691
|
+
async fetchCmsBundles(options) {
|
|
692
|
+
return fetchCmsBundles(this.store, this.outputDir, options);
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Downloads translation bundles from S3 and writes them as JSON files
|
|
696
|
+
* to `outputDir`.
|
|
697
|
+
*
|
|
698
|
+
* @returns Per project, a map of S3 object key to absolute file path.
|
|
699
|
+
*/
|
|
700
|
+
async fetchTranslationBundles(options) {
|
|
701
|
+
return fetchTranslationBundles(this.store, this.outputDir, options);
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Downloads all translation resources for the given projects/locales, parses them,
|
|
705
|
+
* and writes one merged `{locale}.json` per locale (string key/value map; duplicate keys:
|
|
706
|
+
* last wins).
|
|
707
|
+
*/
|
|
708
|
+
async fetchMergedTranslationBundles(options) {
|
|
709
|
+
return fetchMergedTranslationBundles(this.store, this.outputDir, options);
|
|
227
710
|
}
|
|
228
711
|
/**
|
|
229
712
|
* Queries a previously fetched bundle from the local filesystem.
|
|
230
713
|
*/
|
|
231
|
-
async
|
|
232
|
-
return
|
|
714
|
+
async queryCmsBundle(cms, contentType, options = {}) {
|
|
715
|
+
return queryCmsBundle(this.outputDir, cms, contentType, options);
|
|
233
716
|
}
|
|
234
717
|
};
|
|
235
718
|
export {
|
|
236
719
|
ContentStore,
|
|
237
720
|
ContentStoreSDK,
|
|
238
|
-
|
|
239
|
-
|
|
721
|
+
fetchCmsBundles,
|
|
722
|
+
fetchMergedTranslationBundles,
|
|
723
|
+
fetchTranslationBundles,
|
|
724
|
+
getDefaultS3RetryConfig,
|
|
725
|
+
queryCmsBundle,
|
|
240
726
|
trimDepth
|
|
241
727
|
};
|
|
242
728
|
//# sourceMappingURL=node.js.map
|