@tandem-language-exchange/content-store 1.2.0 → 1.2.2

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 (34) hide show
  1. package/README.md +81 -13
  2. package/dist/chunk-4DE47ZJD.js +19 -0
  3. package/dist/chunk-4DE47ZJD.js.map +1 -0
  4. package/dist/{chunk-YZSLCPN6.js → chunk-6FXNAJSI.js} +160 -14
  5. package/dist/chunk-6FXNAJSI.js.map +1 -0
  6. package/dist/{chunk-UCBZUEUP.js → chunk-BAJT7RMQ.js} +31 -69
  7. package/dist/chunk-BAJT7RMQ.js.map +1 -0
  8. package/dist/{chunk-XP3USUQC.js → chunk-EQ3DSPTJ.js} +6 -2
  9. package/dist/{chunk-XP3USUQC.js.map → chunk-EQ3DSPTJ.js.map} +1 -1
  10. package/dist/chunk-UPIQFNCR.js +17 -0
  11. package/dist/chunk-UPIQFNCR.js.map +1 -0
  12. package/dist/{chunk-VRWRAFDK.js → chunk-VKXN2SE6.js} +79 -24
  13. package/dist/chunk-VKXN2SE6.js.map +1 -0
  14. package/dist/client/fetch-content-bundles.js +7 -4
  15. package/dist/client/fetch-content-bundles.js.map +1 -1
  16. package/dist/client/fetch-merged-translation-bundles.js +46 -0
  17. package/dist/client/fetch-merged-translation-bundles.js.map +1 -0
  18. package/dist/client/fetch-translation-bundles.js +22 -11
  19. package/dist/client/fetch-translation-bundles.js.map +1 -1
  20. package/dist/client/query-cms.js +33 -0
  21. package/dist/client/query-cms.js.map +1 -0
  22. package/dist/{index-Db97SUTy.d.ts → index-PQ7XN47c.d.ts} +36 -7
  23. package/dist/index.d.ts +1 -1
  24. package/dist/node.browser.js +7 -0
  25. package/dist/node.browser.js.map +1 -1
  26. package/dist/node.d.ts +9 -3
  27. package/dist/node.js +237 -15
  28. package/dist/node.js.map +1 -1
  29. package/package.json +8 -5
  30. package/dist/chunk-UCBZUEUP.js.map +0 -1
  31. package/dist/chunk-VRWRAFDK.js.map +0 -1
  32. package/dist/chunk-YZSLCPN6.js.map +0 -1
  33. package/dist/client/cli.js +0 -82
  34. package/dist/client/cli.js.map +0 -1
package/dist/node.js CHANGED
@@ -28,6 +28,18 @@ var ContentStore = class {
28
28
  );
29
29
  return key;
30
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
+ }
31
43
  async download(key) {
32
44
  const response = await this.client.send(
33
45
  new GetObjectCommand({ Bucket: this.bucket, Key: key })
@@ -36,6 +48,15 @@ var ContentStore = class {
36
48
  if (!body) throw new Error(`Empty response for key: ${key}`);
37
49
  return JSON.parse(body);
38
50
  }
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 })
55
+ );
56
+ const body = await response.Body?.transformToString();
57
+ if (!body) throw new Error(`Empty response for key: ${key}`);
58
+ return body;
59
+ }
39
60
  };
40
61
  var buildCmsObjectKey = (cms, contentType) => {
41
62
  return `${cms}-${contentType}.json`;
@@ -46,7 +67,7 @@ var buildTranslationObjectKey = (project, fileName, locale) => {
46
67
 
47
68
  // src/shared/bundles.ts
48
69
  import fs from "fs/promises";
49
- import path from "path";
70
+ import path2 from "path";
50
71
 
51
72
  // src/shared/s3-retry.ts
52
73
  function computeDelay(attempt, baseDelayMs, maxDelayMs) {
@@ -122,6 +143,9 @@ async function withS3Retry(fn, { maxRetries, baseDelayMs, maxDelayMs }) {
122
143
  async function downloadWithRetry(store, key, cfg) {
123
144
  return withS3Retry(() => store.download(key), cfg);
124
145
  }
146
+ async function downloadRawWithRetry(store, key, cfg) {
147
+ return withS3Retry(() => store.downloadRaw(key), cfg);
148
+ }
125
149
 
126
150
  // src/shared/trimDepth.ts
127
151
  function trimDepth(value, remaining) {
@@ -169,41 +193,49 @@ var localeMapping = {
169
193
  var allProjects = {
170
194
  "tandem-(new-website)": [
171
195
  {
196
+ resource: "main",
172
197
  fileName: "Website.[locale].json",
173
198
  type: "json"
174
199
  }
175
200
  ],
176
201
  "tandem-(website)": [
177
202
  {
203
+ resource: "main",
178
204
  fileName: "[locale].json",
179
205
  type: "json"
180
206
  },
181
207
  {
208
+ resource: "ai",
182
209
  fileName: "AI.[locale].json",
183
210
  type: "json"
184
211
  },
185
212
  {
213
+ resource: "languages",
186
214
  fileName: "languages.[locale].json",
187
215
  type: "json"
188
216
  }
189
217
  ],
190
218
  "tandem": [
191
219
  {
220
+ resource: "infoplist",
192
221
  fileName: "InfoPlist.[locale].strings",
193
222
  type: "strings",
194
223
  localeMapping: localeMapping.ios
195
224
  },
196
225
  {
226
+ resource: "localizable",
197
227
  fileName: "Localizable.[locale].strings",
198
228
  type: "strings",
199
229
  localeMapping: localeMapping.ios
200
230
  },
201
231
  {
232
+ resource: "ipad",
202
233
  fileName: "MainiPad.[locale].strings",
203
234
  type: "strings",
204
235
  localeMapping: localeMapping.ios
205
236
  },
206
237
  {
238
+ resource: "main",
207
239
  fileName: "Main.[locale].strings",
208
240
  type: "strings",
209
241
  localeMapping: localeMapping.ios
@@ -211,136 +243,163 @@ var allProjects = {
211
243
  ],
212
244
  "tandem-(android)": [
213
245
  {
246
+ resource: "accessibility",
214
247
  fileName: "accessibility_localizable.[locale].xml",
215
248
  type: "xml",
216
249
  localeMapping: localeMapping.android
217
250
  },
218
251
  {
252
+ resource: "call",
219
253
  fileName: "call_localizable.[locale].xml",
220
254
  type: "xml",
221
255
  localeMapping: localeMapping.android
222
256
  },
223
257
  {
258
+ resource: "cert",
224
259
  fileName: "cert_localizable.[locale].xml",
225
260
  type: "xml",
226
261
  localeMapping: localeMapping.android
227
262
  },
228
263
  {
264
+ resource: "chat",
229
265
  fileName: "chat_localizable.[locale].xml",
230
266
  type: "xml",
231
267
  localeMapping: localeMapping.android
232
268
  },
233
269
  {
270
+ resource: "checklist",
234
271
  fileName: "checklist_localizable.[locale].xml",
235
272
  type: "xml",
236
273
  localeMapping: localeMapping.android
237
274
  },
238
275
  {
276
+ resource: "clubs",
239
277
  fileName: "clubs_localizable.[locale].xml",
240
278
  type: "xml",
241
279
  localeMapping: localeMapping.android
242
280
  },
243
281
  {
282
+ resource: "common",
244
283
  fileName: "common_localizable.[locale].xml",
245
284
  type: "xml",
246
285
  localeMapping: localeMapping.android
247
286
  },
248
287
  {
288
+ resource: "community",
249
289
  fileName: "community_localizable.[locale].xml",
250
290
  type: "xml",
251
291
  localeMapping: localeMapping.android
252
292
  },
253
293
  {
294
+ resource: "correction",
254
295
  fileName: "correction_localizable.[locale].xml",
255
296
  type: "xml",
256
297
  localeMapping: localeMapping.android
257
298
  },
258
299
  {
300
+ resource: "country_names",
259
301
  fileName: "country_names.[locale].xml",
260
302
  type: "xml",
261
303
  localeMapping: localeMapping.android
262
304
  },
263
305
  {
306
+ resource: "emoji",
264
307
  fileName: "emoji_localizable.[locale].xml",
265
308
  type: "xml",
266
309
  localeMapping: localeMapping.android
267
310
  },
268
311
  {
312
+ resource: "errors",
269
313
  fileName: "errors_localizable.[locale].xml",
270
314
  type: "xml",
271
315
  localeMapping: localeMapping.android
272
316
  },
273
317
  {
318
+ resource: "expressions",
274
319
  fileName: "expressions_localizable.[locale].xml",
275
320
  type: "xml",
276
321
  localeMapping: localeMapping.android
277
322
  },
278
323
  {
324
+ resource: "gif",
279
325
  fileName: "gif_localizable.[locale].xml",
280
326
  type: "xml",
281
327
  localeMapping: localeMapping.android
282
328
  },
283
329
  {
330
+ resource: "guidelines",
284
331
  fileName: "guidelines_localizable.[locale].xml",
285
332
  type: "xml",
286
333
  localeMapping: localeMapping.android
287
334
  },
288
335
  {
336
+ resource: "lanuguages",
289
337
  fileName: "languages_localizable.[locale].xml",
290
338
  type: "xml",
291
339
  localeMapping: localeMapping.android
292
340
  },
293
341
  {
342
+ resource: "localizable",
294
343
  fileName: "localizable.[locale].xml",
295
344
  type: "xml",
296
345
  localeMapping: localeMapping.android
297
346
  },
298
347
  {
348
+ resource: "localizable2",
299
349
  fileName: "localizable2.[locale].xml",
300
350
  type: "xml",
301
351
  localeMapping: localeMapping.android
302
352
  },
303
353
  {
354
+ resource: "login",
304
355
  fileName: "login_localizable.[locale].xml",
305
356
  type: "xml",
306
357
  localeMapping: localeMapping.android
307
358
  },
308
359
  {
360
+ resource: "myprofile",
309
361
  fileName: "myprofile_localizable.[locale].xml",
310
362
  type: "xml",
311
363
  localeMapping: localeMapping.android
312
364
  },
313
365
  {
366
+ resource: "onb",
314
367
  fileName: "onb_localizable.[locale].xml",
315
368
  type: "xml",
316
369
  localeMapping: localeMapping.android
317
370
  },
318
371
  {
372
+ resource: "parties",
319
373
  fileName: "parties_localizable.[locale].xml",
320
374
  type: "xml",
321
375
  localeMapping: localeMapping.android
322
376
  },
323
377
  {
378
+ resource: "pro",
324
379
  fileName: "pro_localizable.[locale].xml",
325
380
  type: "xml",
326
381
  localeMapping: localeMapping.android
327
382
  },
328
383
  {
384
+ resource: "pro_screen",
329
385
  fileName: "pro_screen_localizable.[locale].xml",
330
386
  type: "xml",
331
387
  localeMapping: localeMapping.android
332
388
  },
333
389
  {
390
+ resource: "push_notification",
334
391
  fileName: "push_notification_localizable.[locale].xml",
335
392
  type: "xml",
336
393
  localeMapping: localeMapping.android
337
394
  },
338
395
  {
396
+ resource: "reporting",
339
397
  fileName: "reporting_localizable.[locale].xml",
340
398
  type: "xml",
341
399
  localeMapping: localeMapping.android
342
400
  },
343
401
  {
402
+ resource: "translation",
344
403
  fileName: "translation_localizable.[locale].xml",
345
404
  type: "xml",
346
405
  localeMapping: localeMapping.android
@@ -348,12 +407,106 @@ var allProjects = {
348
407
  ],
349
408
  "tandem-(web-invites)": [
350
409
  {
410
+ resource: "main",
351
411
  fileName: "[locale].json",
352
412
  type: "json"
353
413
  }
354
414
  ]
355
415
  };
356
416
 
417
+ // src/shared/translationResource.ts
418
+ import path from "path";
419
+
420
+ // src/shared/utils.ts
421
+ import convert from "xml-js";
422
+ import set from "lodash.set";
423
+ import merge from "lodash.merge";
424
+ var transformObjectToFlat = (data) => {
425
+ const result = {};
426
+ const flatten = (obj, path3 = []) => {
427
+ Object.entries(obj).forEach(([key, value]) => {
428
+ if (typeof value === "object") {
429
+ flatten(value, path3.concat(key));
430
+ } else {
431
+ result[path3.concat(key).join(".")] = value;
432
+ }
433
+ });
434
+ };
435
+ flatten(data);
436
+ return result;
437
+ };
438
+ var convertXMLToJS = (xml) => {
439
+ const converted = convert.xml2js(xml, {
440
+ ignoreComment: true,
441
+ ignoreDeclaration: true,
442
+ ignoreInstruction: true,
443
+ compact: true
444
+ });
445
+ let mapped = {};
446
+ converted.resources.string.forEach((item) => {
447
+ mapped = {
448
+ ...mapped,
449
+ [item._attributes.name]: item._text
450
+ };
451
+ });
452
+ return mapped;
453
+ };
454
+ var parseIOSStrings = (strings) => {
455
+ const parsedObj = {};
456
+ strings.split("\n").filter((line) => line.startsWith('"') && line.endsWith(";")).map((line) => line.trim().slice(0, -1)).forEach((line) => {
457
+ let [key, value] = line.split(" = ");
458
+ if (!key || !value) return;
459
+ key = key.slice(1, -1);
460
+ value = value.slice(1, -1);
461
+ parsedObj[key] = value;
462
+ });
463
+ return parsedObj;
464
+ };
465
+
466
+ // src/shared/translationResource.ts
467
+ function parseTranslationResourceRaw(raw, resource) {
468
+ if (resource.type === "json") {
469
+ return JSON.parse(raw);
470
+ }
471
+ if (resource.type === "strings") {
472
+ return parseIOSStrings(raw);
473
+ }
474
+ if (resource.type === "xml") {
475
+ return convertXMLToJS(raw);
476
+ }
477
+ throw new Error(`Unsupported resource type: ${resource.type}`);
478
+ }
479
+ function toFlatStringMap(parsed) {
480
+ if (parsed === null || parsed === void 0) return {};
481
+ if (typeof parsed === "string") return { value: parsed };
482
+ if (typeof parsed !== "object") return { value: String(parsed) };
483
+ if (Array.isArray(parsed)) {
484
+ const out2 = {};
485
+ parsed.forEach((v, i) => {
486
+ out2[String(i)] = typeof v === "object" && v !== null ? JSON.stringify(v) : String(v);
487
+ });
488
+ return out2;
489
+ }
490
+ const flat = transformObjectToFlat(parsed);
491
+ const out = {};
492
+ for (const [k, v] of Object.entries(flat)) {
493
+ if (v === null || v === void 0) {
494
+ out[k] = "";
495
+ } else if (typeof v === "object") {
496
+ out[k] = JSON.stringify(v);
497
+ } else {
498
+ out[k] = String(v);
499
+ }
500
+ }
501
+ return out;
502
+ }
503
+ function translationJsonOutputPath(outputDir, objectKey) {
504
+ if (objectKey.endsWith(".json")) {
505
+ return path.resolve(outputDir, objectKey);
506
+ }
507
+ return path.resolve(outputDir, `${objectKey}.json`);
508
+ }
509
+
357
510
  // src/shared/bundles.ts
358
511
  function getAtPath(obj, dottedPath) {
359
512
  const parts = dottedPath.split(".").filter((p) => p.length > 0);
@@ -384,8 +537,8 @@ function isEmptyValue(value) {
384
537
  return typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
385
538
  }
386
539
  function matchesFieldFilter(item, key, expected) {
387
- const { path: path2, operator } = parseFieldKey(key);
388
- const at = getAtPath(item, path2);
540
+ const { path: path3, operator } = parseFieldKey(key);
541
+ const at = getAtPath(item, path3);
389
542
  if (operator === "exists") {
390
543
  if (expected !== true && expected !== false) return false;
391
544
  const empty = !at.found || isEmptyValue(at.value);
@@ -420,13 +573,17 @@ async function fetchCmsBundles(store, outputDir, options) {
420
573
  contentTypes.map(async (contentType) => {
421
574
  const key = buildCmsObjectKey(cms, contentType);
422
575
  const data = await downloadWithRetry(store, key, retry);
423
- const filePath = path.resolve(outputDir, key);
576
+ const filePath = path2.resolve(outputDir, key);
424
577
  await fs.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
425
578
  result[contentType] = filePath;
426
579
  })
427
580
  );
428
581
  return result;
429
582
  }
583
+ function filterResources(projectResources, resourceFilter) {
584
+ if (resourceFilter.length === 0) return projectResources;
585
+ return projectResources.filter((r) => resourceFilter.includes(r.resource));
586
+ }
430
587
  async function fetchTranslationBundles(store, outputDir, options) {
431
588
  const { projects, locales } = options;
432
589
  const retry = options.retry ?? getDefaultS3RetryConfig();
@@ -434,11 +591,11 @@ async function fetchTranslationBundles(store, outputDir, options) {
434
591
  const result = {};
435
592
  const localesToSync = locales && locales.length ? locales : defaultLocales;
436
593
  await Promise.all(
437
- projects.map(async (project) => {
438
- const resources = allProjects[project];
439
- if (!resources?.length) {
440
- return;
441
- }
594
+ Object.entries(projects).map(async ([project, resourceFilter]) => {
595
+ const allResources = allProjects[project];
596
+ if (!allResources?.length) return;
597
+ const resources = filterResources(allResources, resourceFilter);
598
+ if (!resources.length) return;
442
599
  const resourceTasks = [];
443
600
  for (const resource of resources) {
444
601
  for (const loc of localesToSync) {
@@ -448,14 +605,20 @@ async function fetchTranslationBundles(store, outputDir, options) {
448
605
  resource.fileName,
449
606
  locale
450
607
  );
451
- resourceTasks.push({ objectKey });
608
+ resourceTasks.push({ objectKey, resource });
452
609
  }
453
610
  }
454
611
  await Promise.all(
455
- resourceTasks.map(async ({ objectKey }) => {
456
- const data = await downloadWithRetry(store, objectKey, retry);
457
- const filePath = path.resolve(outputDir, objectKey);
458
- await fs.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
612
+ resourceTasks.map(async ({ objectKey, resource }) => {
613
+ const raw = await downloadRawWithRetry(store, objectKey, retry);
614
+ const parsed = parseTranslationResourceRaw(raw, resource);
615
+ const filePath = translationJsonOutputPath(outputDir, objectKey);
616
+ await fs.mkdir(path2.dirname(filePath), { recursive: true });
617
+ await fs.writeFile(
618
+ filePath,
619
+ JSON.stringify(parsed, null, 2),
620
+ "utf-8"
621
+ );
459
622
  if (!result[project]) {
460
623
  result[project] = {};
461
624
  }
@@ -466,8 +629,58 @@ async function fetchTranslationBundles(store, outputDir, options) {
466
629
  );
467
630
  return result;
468
631
  }
632
+ async function fetchMergedTranslationBundles(store, outputDir, options) {
633
+ const { projects, locales } = options;
634
+ const retry = options.retry ?? getDefaultS3RetryConfig();
635
+ await fs.mkdir(outputDir, { recursive: true });
636
+ const localesToSync = locales && locales.length ? locales : defaultLocales;
637
+ const tasks = [];
638
+ for (const [project, resourceFilter] of Object.entries(projects)) {
639
+ const allResources = allProjects[project];
640
+ if (!allResources?.length) continue;
641
+ const resources = filterResources(allResources, resourceFilter);
642
+ if (!resources.length) continue;
643
+ for (const resource of resources) {
644
+ for (const loc of localesToSync) {
645
+ const mappedLocale = resource.localeMapping && resource.localeMapping[loc] ? resource.localeMapping[loc] : loc;
646
+ const objectKey = buildTranslationObjectKey(
647
+ project,
648
+ resource.fileName,
649
+ mappedLocale
650
+ );
651
+ tasks.push({ catalogLocale: loc, objectKey, resource });
652
+ }
653
+ }
654
+ }
655
+ const results = await Promise.all(
656
+ tasks.map(async ({ catalogLocale, objectKey, resource }) => {
657
+ const raw = await downloadRawWithRetry(store, objectKey, retry);
658
+ const parsed = parseTranslationResourceRaw(raw, resource);
659
+ const stringMap = toFlatStringMap(parsed);
660
+ return { catalogLocale, stringMap };
661
+ })
662
+ );
663
+ const merged = {};
664
+ for (const loc of localesToSync) {
665
+ merged[loc] = {};
666
+ }
667
+ for (const { catalogLocale, stringMap } of results) {
668
+ Object.assign(merged[catalogLocale], stringMap);
669
+ }
670
+ const out = {};
671
+ for (const loc of localesToSync) {
672
+ const filePath = path2.resolve(outputDir, `${loc}.json`);
673
+ await fs.writeFile(
674
+ filePath,
675
+ JSON.stringify(merged[loc], null, 2),
676
+ "utf-8"
677
+ );
678
+ out[loc] = filePath;
679
+ }
680
+ return out;
681
+ }
469
682
  async function queryCmsBundle(outputDir, cms, contentType, options = {}) {
470
- const filePath = path.resolve(
683
+ const filePath = path2.resolve(
471
684
  outputDir,
472
685
  `${cms}-${contentType}.json`
473
686
  );
@@ -529,6 +742,14 @@ var ContentStoreSDK = class {
529
742
  async fetchTranslationBundles(options) {
530
743
  return fetchTranslationBundles(this.store, this.outputDir, options);
531
744
  }
745
+ /**
746
+ * Downloads all translation resources for the given projects/locales, parses them,
747
+ * and writes one merged `{locale}.json` per locale (string key/value map; duplicate keys:
748
+ * last wins).
749
+ */
750
+ async fetchMergedTranslationBundles(options) {
751
+ return fetchMergedTranslationBundles(this.store, this.outputDir, options);
752
+ }
532
753
  /**
533
754
  * Queries a previously fetched bundle from the local filesystem.
534
755
  */
@@ -540,6 +761,7 @@ export {
540
761
  ContentStore,
541
762
  ContentStoreSDK,
542
763
  fetchCmsBundles,
764
+ fetchMergedTranslationBundles,
543
765
  fetchTranslationBundles,
544
766
  getDefaultS3RetryConfig,
545
767
  queryCmsBundle,