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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # Content Store
2
2
 
3
- SDK for fetching CMS content bundles from S3 and querying them locally from the filesystem. Supports content synced from **Contentful** and **Sanity**.
3
+ SDK for fetching **CMS** and **translation** bundles from Amazon S3 and, for CMS data, querying them locally from the filesystem. CMS bundles come from syncs of **Contentful** or **Sanity**; translation bundles come from **Lingohub** projects synced to S3 (see the [Server & CLI README](src/server/README.md) for the upload pipeline).
4
4
 
5
- For the server, CLI, and deployment documentation, see the [Server & CLI README](src/server/README.md).
5
+ For the Express API, server CLI, scheduling, and deployment, see the [Server & CLI README](src/server/README.md).
6
6
 
7
7
  ### Package entry points
8
8
 
9
9
  - **`@tandem-language-exchange/content-store`** (default) — **types only** at runtime. Safe to import from shared code that Next.js, Vite, or Turbopack may bundle for the browser.
10
- - **`@tandem-language-exchange/content-store/node`** — `ContentStoreSDK`, `fetchBundles`, `queryBundle`, `ContentStore`, and `trimDepth`. Real implementations use the filesystem and S3 and run only under the Node (`node`) export condition (Route Handlers, `getServerSideProps`, CLI, etc.).
10
+ - **`@tandem-language-exchange/content-store/node`** — `ContentStoreSDK`, `fetchCmsBundles`, `fetchTranslationBundles`, `queryCmsBundle`, `ContentStore`, `getDefaultS3RetryConfig`, and `trimDepth`. Real implementations use the filesystem and S3 and run only under the Node (`node`) export condition (Route Handlers, `getServerSideProps`, CLI, etc.).
11
11
 
12
12
  For **browser** bundles (including anything imported from `_app.tsx`, client components, or shared modules that reach the client graph), bundlers should resolve the `browser` / `edge-light` conditions to a **stub** that does not import `fs`. That stub throws if you call server-only APIs; **`trimDepth` is fully implemented** and safe on the client.
13
13
 
@@ -16,9 +16,11 @@ For the server, CLI, and deployment documentation, see the [Server & CLI README]
16
16
  ## Installation
17
17
 
18
18
  ```bash
19
- npm install content-store
19
+ npm install @tandem-language-exchange/content-store
20
20
  ```
21
21
 
22
+ Use the same scoped name in `package.json` dependencies and in `npx` / import paths.
23
+
22
24
  ## Initialisation
23
25
 
24
26
  ```typescript
@@ -70,12 +72,12 @@ const sdk = new ContentStoreSDK({
70
72
 
71
73
  ---
72
74
 
73
- ## `fetchBundles(options)`
75
+ ## `fetchCmsBundles(options)`
74
76
 
75
77
  Downloads the latest content bundles from S3 and saves them as JSON files to `outputDir`.
76
78
 
77
79
  ```typescript
78
- const files = await sdk.fetchBundles({
80
+ const files = await sdk.fetchCmsBundles({
79
81
  cms: 'contentful',
80
82
  contentTypes: ['gridLayout', 'iconWithText', 'page'],
81
83
  });
@@ -87,6 +89,7 @@ const files = await sdk.fetchBundles({
87
89
  | --- | --- | --- |
88
90
  | `cms` | `'contentful' \| 'sanity'` | Which CMS the bundles were synced from |
89
91
  | `contentTypes` | `string[]` | Content types to download |
92
+ | `retry` | `S3RetryConfig` | Optional. Overrides [S3 download retries](#s3-download-retries). |
90
93
 
91
94
  **Returns:** `Record<string, string>` — a map of content type to absolute file path.
92
95
 
@@ -100,12 +103,66 @@ const files = await sdk.fetchBundles({
100
103
 
101
104
  Files are written to `outputDir` with the naming pattern `{cms}-{contentType}.json`.
102
105
 
103
- ## `queryBundle(cms, contentType, options?)`
106
+ ---
107
+
108
+ ## `fetchTranslationBundles(options)`
109
+
110
+ Downloads translation bundles that were previously uploaded to S3 (after a Lingohub sync). Files are written under `outputDir` using the same **S3 object key** as the path (see below).
111
+
112
+ ```typescript
113
+ const files = await sdk.fetchTranslationBundles({
114
+ projects: ['tandem', 'tandem-(website)'],
115
+ locales: ['en', 'de'], // omit or leave empty to use the package default locale list
116
+ });
117
+ ```
118
+
119
+ **Parameters:**
120
+
121
+ | Field | Type | Description |
122
+ | --- | --- | --- |
123
+ | `projects` | `string[]` | Lingohub project ids to fetch (must match keys configured in the package, e.g. `tandem`, `tandem-(android)`). |
124
+ | `locales` | `string[]` | Optional. Locale codes to fetch (e.g. `pt-br`, `zh-hans`). If omitted or empty, a built-in default list is used. |
125
+ | `retry` | `S3RetryConfig` | Optional. Overrides [S3 download retries](#s3-download-retries). |
126
+
127
+ **Returns:** `TranslationBundleInfo` — nested map **`project → S3 object key → absolute file path`** on disk.
128
+
129
+ ```typescript
130
+ {
131
+ 'tandem-(website)': {
132
+ 'lingohub-tandem-(website).en.json': '/abs/path/to/content-cache/lingohub-tandem-(website).en.json',
133
+ 'lingohub-tandem-(website).AI.en.json': '/abs/path/to/...'
134
+ }
135
+ }
136
+ ```
137
+
138
+ **S3 object keys** follow `lingohub-{project}.{fileName}` where `{fileName}` is the Lingohub resource template with `[locale]` replaced by the **mapped** locale when a resource defines `localeMapping` (same rules as the server sync). Consumers should use the returned keys or list objects by project as shown above.
139
+
140
+ Downloads are run **in parallel** (per project); [S3 download retries](#s3-download-retries) apply by default.
141
+
142
+ ---
143
+
144
+ ## S3 download retries
145
+
146
+ `fetchCmsBundles` and `fetchTranslationBundles` use retry + exponential backoff on **transient** S3/network failures (for example HTTP 503 “Slow Down”, throttling, timeouts). They do **not** retry clear client errors such as **404** (missing key).
147
+
148
+ Default limits are read from the environment (highest precedence first):
149
+
150
+ | Variable | Fallback | Purpose |
151
+ | --- | --- | --- |
152
+ | `S3_RETRY_MAX_RETRIES` | `RETRY_MAX_RETRIES` (default `5`) | Maximum retry attempts after the first try |
153
+ | `S3_RETRY_BASE_DELAY_MS` | `RETRY_BASE_DELAY_MS` (default `1000`) | Base delay for exponential backoff |
154
+ | `S3_RETRY_MAX_DELAY_MS` | `RETRY_MAX_DELAY_MS` (default `60000`) | Cap on backoff delay |
155
+
156
+ Override per call with **`retry: { maxRetries, baseDelayMs, maxDelayMs }`**, or import **`getDefaultS3RetryConfig()`** to merge with your own defaults.
157
+
158
+ ---
159
+
160
+ ## `queryCmsBundle(cms, contentType, options?)`
104
161
 
105
162
  Reads a previously fetched bundle from the local filesystem and returns a filtered, shaped result set.
106
163
 
107
164
  ```typescript
108
- const results = await sdk.queryBundle('contentful', 'gridLayout', {
165
+ const results = await sdk.queryCmsBundle('contentful', 'gridLayout', {
109
166
  fields: { columns: '2' },
110
167
  select: ['title', 'bodyBefore'],
111
168
  limit: 10,
@@ -231,10 +288,16 @@ Query options are applied in this order:
231
288
 
232
289
  ## Standalone functions
233
290
 
234
- The core `fetchBundles` and `queryBundle` functions are also available as standalone imports for use outside the SDK class:
291
+ The same operations are available as standalone imports (no `ContentStoreSDK` wrapper):
235
292
 
236
293
  ```typescript
237
- import { fetchBundles, queryBundle, ContentStore } from '@tandem-language-exchange/content-store/node';
294
+ import {
295
+ fetchCmsBundles,
296
+ fetchTranslationBundles,
297
+ queryCmsBundle,
298
+ getDefaultS3RetryConfig,
299
+ ContentStore,
300
+ } from '@tandem-language-exchange/content-store/node';
238
301
 
239
302
  const store = new ContentStore({
240
303
  bucket: 'beta-content-store',
@@ -243,12 +306,18 @@ const store = new ContentStore({
243
306
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
244
307
  });
245
308
 
246
- await fetchBundles(store, './content-cache', {
309
+ await fetchCmsBundles(store, './content-cache', {
247
310
  cms: 'contentful',
248
311
  contentTypes: ['gridLayout'],
249
312
  });
250
313
 
251
- const results = await queryBundle('./content-cache', 'contentful', 'gridLayout', {
314
+ await fetchTranslationBundles(store, './content-cache', {
315
+ projects: ['tandem-(website)'],
316
+ locales: ['en', 'fr'],
317
+ retry: getDefaultS3RetryConfig(),
318
+ });
319
+
320
+ const results = await queryCmsBundle('./content-cache', 'contentful', 'gridLayout', {
252
321
  fields: { columns: '2' },
253
322
  limit: 5,
254
323
  });
@@ -269,14 +338,19 @@ const sdk = new ContentStoreSDK({
269
338
  outputDir: './.content-cache',
270
339
  });
271
340
 
272
- // 1. Pull latest bundles from S3 to disk
273
- await sdk.fetchBundles({
341
+ // 1. Pull latest CMS bundles from S3 to disk
342
+ await sdk.fetchCmsBundles({
274
343
  cms: 'contentful',
275
344
  contentTypes: ['page', 'gridLayout'],
276
345
  });
277
346
 
278
- // 2. Query locally no further network calls
279
- const grids = await sdk.queryBundle('contentful', 'gridLayout', {
347
+ // Optional: pull translation bundles written by the Lingohub → S3 sync
348
+ await sdk.fetchTranslationBundles({
349
+ projects: ['tandem-(website)'],
350
+ });
351
+
352
+ // 2. Query CMS bundle locally — no further network calls
353
+ const grids = await sdk.queryCmsBundle('contentful', 'gridLayout', {
280
354
  fields: { columns: '2' },
281
355
  select: ['title', 'refs'],
282
356
  include: 2,
@@ -287,7 +361,7 @@ console.log(grids);
287
361
  ```
288
362
  ## CLI
289
363
 
290
- The package ships two CLI entry points that can be called from npm scripts in a consuming application.
364
+ The published package exposes one **global binary** **`fetch-content-bundles`** (CMS downloads). Other commands live in **`dist/client/cli.js`** as subcommands; call them with **`node node_modules/@tandem-language-exchange/content-store/dist/client/cli.js <command>`** after install, or wrap them in your app’s **`package.json` `scripts`** (see examples below).
291
365
 
292
366
  ### `fetch-content-bundles` — download bundles from S3
293
367
 
@@ -313,12 +387,14 @@ Files are written as `{cms}-{contentType}.json` inside the output directory.
313
387
  }
314
388
  ```
315
389
 
316
- ### `content-store query` query a local bundle
390
+ The published **`bin`** only registers **`fetch-content-bundles`** (CMS). To **download translation bundles** from a script without embedding credentials in the shell, use a small Node script that calls **`fetchTranslationBundles`** (or `ContentStoreSDK`) with the same env vars as in [S3 config via environment variables](#s3-config-via-environment-variables), or call the server’s **`POST /getTranslationBundles`** API if you run the content-store service (see [Server & CLI README](src/server/README.md)).
317
391
 
318
- Reads a previously fetched bundle from disk and prints filtered results as JSON. Invoke via `npx` or as a local script using `node`:
392
+ ### `query-cms` query a local bundle
393
+
394
+ Reads a previously fetched bundle from disk and prints JSON to stdout. This command is **not** a separate `bin`; run the built client CLI (after `npm install` of this package):
319
395
 
320
396
  ```bash
321
- npx @tandem-language-exchange/content-store query \
397
+ node node_modules/@tandem-language-exchange/content-store/dist/client/cli.js query-cms \
322
398
  --cms contentful --type gridLayout \
323
399
  --fields '{"columns":"2"}' \
324
400
  --select title,bodyBefore \
@@ -326,6 +402,16 @@ npx @tandem-language-exchange/content-store query \
326
402
  --include 2
327
403
  ```
328
404
 
405
+ **Typical `package.json` shortcut:**
406
+
407
+ ```json
408
+ "scripts": {
409
+ "query:cms": "node ./node_modules/@tandem-language-exchange/content-store/dist/client/cli.js query-cms"
410
+ }
411
+ ```
412
+
413
+ Then: `npm run query:cms -- --cms contentful --type gridLayout …`
414
+
329
415
  | Flag | Required | Default | Description |
330
416
  | --- | --- | --- | --- |
331
417
  | `--cms <provider>` | Yes | | `contentful` or `sanity` |
@@ -1,13 +1,49 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ContentStore,
4
- config
5
- } from "./chunk-UWGOF36L.js";
4
+ allProjects,
5
+ buildCmsObjectKey,
6
+ buildTranslationObjectKey,
7
+ config,
8
+ defaultLocales
9
+ } from "./chunk-YZSLCPN6.js";
6
10
 
7
11
  // src/server/config.ts
8
12
  import dotenv from "dotenv";
9
13
  dotenv.config({ path: ".env.local" });
10
14
  dotenv.config();
15
+ function parseScheduledCmsJobConfig() {
16
+ const intervalMinutes = parseInt(process.env.SCHEDULE_INTERVAL_MINUTES ?? "0", 60);
17
+ const enabled = Number.isFinite(intervalMinutes) && intervalMinutes > 0;
18
+ const runOnStart = (process.env.SCHEDULE_RUN_ON_START ?? "true").toLowerCase() !== "false";
19
+ const task = (process.env.SCHEDULE_TASK ?? "sync").trim();
20
+ const syncCms = (process.env.SCHEDULE_SYNC_CMS ?? "").trim();
21
+ const rawTypes = process.env.SCHEDULE_SYNC_TYPES?.trim();
22
+ const syncTypes = rawTypes ? rawTypes.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
23
+ return {
24
+ enabled,
25
+ intervalMinutes,
26
+ runOnStart,
27
+ task,
28
+ syncCms: syncCms || void 0,
29
+ syncTypes
30
+ };
31
+ }
32
+ function parseScheduledTranslationJobConfig() {
33
+ const intervalMinutes = parseInt(process.env.SCHEDULE_INTERVAL_MINUTES ?? "0", 60);
34
+ const enabled = Number.isFinite(intervalMinutes) && intervalMinutes > 0;
35
+ const runOnStart = (process.env.SCHEDULE_RUN_ON_START ?? "true").toLowerCase() !== "false";
36
+ const task = (process.env.SCHEDULE_TASK ?? "sync").trim();
37
+ const rawProjects = process.env.SCHEDULE_SYNC_TRANSLATION_PROJECTS?.trim();
38
+ const syncProjects = rawProjects ? rawProjects.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
39
+ return {
40
+ enabled,
41
+ intervalMinutes,
42
+ runOnStart,
43
+ task,
44
+ syncProjects
45
+ };
46
+ }
11
47
  var config2 = {
12
48
  ...config,
13
49
  contentful: {
@@ -17,11 +53,14 @@ var config2 = {
17
53
  batchSize: 1e3,
18
54
  maxDepth: 4,
19
55
  contentTypes: [
20
- "gridLayout",
21
- "iconWithText",
22
- "page"
56
+ "asset",
57
+ "page",
58
+ "longtailPage",
59
+ "customJson",
60
+ "banner",
61
+ "cookieBanner",
62
+ "downloadPage"
23
63
  // Add Contentful content type IDs here to limit sync scope.
24
- // Leave empty to sync all content types in the space.
25
64
  ]
26
65
  },
27
66
  sanity: {
@@ -30,6 +69,10 @@ var config2 = {
30
69
  token: process.env.SANITY_API_TOKEN ?? "",
31
70
  apiVersion: "2024-01-01"
32
71
  },
72
+ lingohub: {
73
+ authToken: process.env.LINGOHUB_AUTH_TOKEN ?? "",
74
+ workspace: process.env.LINGOHUB_WORKSPACE ?? ""
75
+ },
33
76
  retry: {
34
77
  maxRetries: parseInt(process.env.RETRY_MAX_RETRIES ?? "5", 10),
35
78
  baseDelayMs: parseInt(process.env.RETRY_BASE_DELAY_MS ?? "1000", 10),
@@ -38,6 +81,18 @@ var config2 = {
38
81
  api: {
39
82
  port: parseInt(process.env.PORT ?? "3010"),
40
83
  apiToken: process.env.CONTENT_STORE_API_TOKEN ?? ""
84
+ },
85
+ scheduledCmsJob: parseScheduledCmsJobConfig(),
86
+ scheduledTranslationJob: parseScheduledTranslationJobConfig(),
87
+ cleanup: {
88
+ enabled: (process.env.CLEANUP_ENABLED ?? "true").toLowerCase() !== "false",
89
+ retentionDays: parseInt(process.env.CLEANUP_RETENTION_DAYS ?? "30", 10)
90
+ },
91
+ slack: {
92
+ enabled: (process.env.SLACK_BOT_TOKEN ?? "").length > 0,
93
+ botToken: process.env.SLACK_BOT_TOKEN ?? "",
94
+ signingSecret: process.env.SLACK_SIGNING_SECRET ?? "",
95
+ appToken: process.env.SLACK_APP_TOKEN ?? ""
41
96
  }
42
97
  };
43
98
 
@@ -134,15 +189,15 @@ var ContentfulAdapter = class {
134
189
  maxDepth;
135
190
  allowedTypes;
136
191
  retryConfig;
137
- constructor(cfg, retryConfig) {
192
+ constructor(cfg2, retryConfig) {
138
193
  this.client = createClient({
139
- space: cfg.spaceId,
140
- accessToken: cfg.accessToken,
141
- host: cfg.host
194
+ space: cfg2.spaceId,
195
+ accessToken: cfg2.accessToken,
196
+ host: cfg2.host
142
197
  });
143
- this.batchSize = cfg.batchSize;
144
- this.maxDepth = cfg.maxDepth;
145
- this.allowedTypes = cfg.contentTypes;
198
+ this.batchSize = cfg2.batchSize;
199
+ this.maxDepth = cfg2.maxDepth;
200
+ this.allowedTypes = cfg2.contentTypes;
146
201
  this.retryConfig = retryConfig;
147
202
  }
148
203
  async getContentTypes() {
@@ -160,8 +215,13 @@ var ContentfulAdapter = class {
160
215
  * Fetches every entry for a content type using batched pagination.
161
216
  * Contentful caps `getEntries` at 1 000 items per call, so we page through
162
217
  * with `skip` until all items are collected.
218
+ *
219
+ * The reserved content type `"asset"` fetches from `getAssets()` instead.
163
220
  */
164
221
  async fetchAll(contentType, includeLevels = 4) {
222
+ if (contentType === "asset") {
223
+ return this.fetchAllAssets();
224
+ }
165
225
  const allItems = [];
166
226
  let skip = 0;
167
227
  let total = 0;
@@ -188,6 +248,30 @@ var ContentfulAdapter = class {
188
248
  total
189
249
  };
190
250
  }
251
+ async fetchAllAssets() {
252
+ const allItems = [];
253
+ let skip = 0;
254
+ let total = 0;
255
+ do {
256
+ const response = await withRetry(
257
+ () => this.client.getAssets({ limit: this.batchSize, skip }),
258
+ this.retryConfig
259
+ );
260
+ total = response.total;
261
+ allItems.push(...response.items);
262
+ skip += response.items.length;
263
+ if (total > this.batchSize) {
264
+ console.log(
265
+ ` [contentful] asset: fetched ${allItems.length}/${total}`
266
+ );
267
+ }
268
+ } while (skip < total);
269
+ return {
270
+ contentType: "asset",
271
+ items: allItems.map((item) => stripEnvelope(item, this.maxDepth)),
272
+ total
273
+ };
274
+ }
191
275
  };
192
276
 
193
277
  // src/server/adapters/sanity.ts
@@ -196,19 +280,19 @@ var SanityAdapter = class {
196
280
  name = "sanity";
197
281
  client;
198
282
  retryConfig;
199
- constructor(cfg, retryConfig) {
283
+ constructor(cfg2, retryConfig) {
200
284
  this.client = createClient2({
201
- projectId: cfg.projectId,
202
- dataset: cfg.dataset,
203
- token: cfg.token,
204
- apiVersion: cfg.apiVersion,
285
+ projectId: cfg2.projectId,
286
+ dataset: cfg2.dataset,
287
+ token: cfg2.token,
288
+ apiVersion: cfg2.apiVersion,
205
289
  useCdn: false
206
290
  });
207
291
  this.retryConfig = retryConfig;
208
292
  }
209
293
  async getContentTypes() {
210
294
  const types = await withRetry(
211
- () => this.client.fetch(`array::unique(*[]._type)`),
295
+ () => this.client.fetch("array::unique(*[]._type)"),
212
296
  this.retryConfig
213
297
  );
214
298
  return types.filter(
@@ -217,7 +301,7 @@ var SanityAdapter = class {
217
301
  }
218
302
  async fetchAll(contentType) {
219
303
  const items = await withRetry(
220
- () => this.client.fetch(`*[_type == $type]`, { type: contentType }),
304
+ () => this.client.fetch("*[_type == $type]", { type: contentType }),
221
305
  this.retryConfig
222
306
  );
223
307
  console.log(` [sanity] ${contentType}: fetched ${items.length} items`);
@@ -237,8 +321,72 @@ function createAdapter(cms) {
237
321
  }
238
322
  }
239
323
 
324
+ // src/shared/utils.ts
325
+ import convert from "xml-js";
326
+ import set from "lodash.set";
327
+ import merge from "lodash.merge";
328
+ var convertXMLToJS = (xml) => {
329
+ const converted = convert.xml2js(xml, {
330
+ ignoreComment: true,
331
+ ignoreDeclaration: true,
332
+ ignoreInstruction: true,
333
+ compact: true
334
+ });
335
+ let mapped = {};
336
+ converted.resources.string.forEach((item) => {
337
+ mapped = {
338
+ ...mapped,
339
+ [item._attributes.name]: item._text
340
+ };
341
+ });
342
+ return mapped;
343
+ };
344
+ var parseIOSStrings = (strings) => {
345
+ const parsedObj = {};
346
+ strings.split("\n").filter((line) => line.startsWith('"') && line.endsWith(";")).map((line) => line.trim().slice(0, -1)).forEach((line) => {
347
+ let [key, value] = line.split(" = ");
348
+ if (!key || !value) return;
349
+ key = key.slice(1, -1);
350
+ value = value.slice(1, -1);
351
+ parsedObj[key] = value;
352
+ });
353
+ return parsedObj;
354
+ };
355
+
356
+ // src/server/adapters/lingohub.ts
357
+ var cfg = config2.lingohub;
358
+ var apiUrl = "https://api.lingohub.com/v1/" + cfg.workspace + "/projects/";
359
+ var getResourceForLocale = async (project, resource, locale) => {
360
+ const urlForResourceLocalised = `${apiUrl}${project}/resources/${resource.fileName}?auth_token=${cfg.authToken}`.replace("[locale]", locale);
361
+ const result = await fetch(
362
+ urlForResourceLocalised,
363
+ {
364
+ method: "GET"
365
+ }
366
+ ).then(async (res) => {
367
+ if (!res.ok) {
368
+ throw new Error(`Failed to fetch resource: ${res.status} - ${res.statusText}`);
369
+ }
370
+ if (resource.type === "json") {
371
+ return res.json();
372
+ }
373
+ if (resource.type === "xml") {
374
+ const xml = await res.text();
375
+ return convertXMLToJS(xml);
376
+ }
377
+ if (resource.type === "strings") {
378
+ const strings = await res.text();
379
+ return parseIOSStrings(strings);
380
+ }
381
+ throw new Error("Invalid resource type");
382
+ }).catch((err) => {
383
+ throw err;
384
+ });
385
+ return result;
386
+ };
387
+
240
388
  // src/server/sync/engine.ts
241
- async function runSync(cms, contentTypes, includeLevels) {
389
+ async function syncCmsContent(cms, contentTypes, includeLevels) {
242
390
  const adapter = createAdapter(cms);
243
391
  const store = new ContentStore(config2.s3);
244
392
  const timestamp = Math.floor(Date.now() / 1e3);
@@ -252,17 +400,15 @@ Starting sync from ${cms} at ${new Date(timestamp * 1e3).toISOString()}`);
252
400
  for (const contentType of typesToSync) {
253
401
  try {
254
402
  const result = await adapter.fetchAll(contentType, includeLevels);
255
- const versionedKey = store.buildVersionedKey(cms, contentType, timestamp);
256
- await store.upload(versionedKey, result.items);
257
- const latestKey = await store.copyToLatest(versionedKey, cms, contentType);
403
+ const objectKey = buildCmsObjectKey(cms, contentType);
404
+ await store.upload(objectKey, result.items);
258
405
  entries.push({
259
406
  contentType,
260
407
  itemCount: result.total,
261
- versionedKey,
262
- latestKey
408
+ objectKey
263
409
  });
264
410
  console.log(
265
- ` + ${contentType}: ${result.total} items -> ${versionedKey}`
411
+ ` + ${contentType}: ${result.total} items -> ${objectKey}`
266
412
  );
267
413
  } catch (err) {
268
414
  const message = err instanceof Error ? err.message : String(err);
@@ -277,9 +423,54 @@ Sync complete: ${entries.length} succeeded, ${errors.length} failed
277
423
  );
278
424
  return { cms, timestamp, entries, errors };
279
425
  }
426
+ async function syncTranslations(projects, locales) {
427
+ const store = new ContentStore(config2.s3);
428
+ const entries = [];
429
+ const errors = [];
430
+ const timestamp = Math.floor(Date.now() / 1e3);
431
+ if (!locales) {
432
+ locales = defaultLocales;
433
+ }
434
+ if (!projects) {
435
+ projects = Object.keys(allProjects);
436
+ }
437
+ for (const project of projects) {
438
+ const resources = allProjects[project];
439
+ if (!resources) {
440
+ console.error(`No resources found for ${project}`);
441
+ continue;
442
+ }
443
+ for (const resource of resources) {
444
+ for (const loc of locales) {
445
+ const locale = resource.localeMapping && resource.localeMapping[loc] ? resource.localeMapping[loc] : loc;
446
+ try {
447
+ const result = await getResourceForLocale(project, resource, locale);
448
+ const objectKey = buildTranslationObjectKey(project, resource.fileName, locale);
449
+ await store.upload(objectKey, result);
450
+ const itemCount = Object.keys(result).length;
451
+ entries.push({
452
+ project,
453
+ locale,
454
+ itemCount,
455
+ objectKey
456
+ });
457
+ console.log(
458
+ ` + ${project} - ${locale}: ${itemCount} items -> ${objectKey}`
459
+ );
460
+ } catch (err) {
461
+ const message = err instanceof Error ? err.message : String(err);
462
+ errors.push({ project, locale, error: message });
463
+ console.error(` x ${project} - ${locale}: ${message}`);
464
+ }
465
+ }
466
+ }
467
+ }
468
+ return { timestamp, entries, errors };
469
+ }
280
470
 
281
471
  export {
282
472
  config2 as config,
283
- runSync
473
+ syncCmsContent,
474
+ syncTranslations
284
475
  };
285
- //# sourceMappingURL=chunk-YOREZCXB.js.map
476
+ //# sourceMappingURL=chunk-UCBZUEUP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/config.ts","../src/server/adapters/contentful.ts","../src/server/sync/retry.ts","../src/server/adapters/sanity.ts","../src/server/adapters/index.ts","../src/shared/utils.ts","../src/server/adapters/lingohub.ts","../src/server/sync/engine.ts"],"sourcesContent":["import dotenv from 'dotenv';\nimport type { S3Config, CMSProvider } from '../shared/types';\nimport {SharedConfig, config as sharedConfig} from '../shared/config';\n\ndotenv.config({ path: '.env.local' });\ndotenv.config();\n\nexport type { CMSProvider, S3Config };\n\nexport interface ContentfulConfig {\n spaceId: string;\n accessToken: string;\n host: string;\n batchSize: number;\n /** Content types to sync. When empty, all content types in the space are synced. */\n contentTypes: string[];\n /** Max nesting depth when unwrapping resolved entries. Deeper references are dropped. */\n maxDepth: number;\n}\n\nexport interface SanityConfig {\n projectId: string;\n dataset: string;\n token: string;\n apiVersion: string;\n}\n\nexport interface LingohubConfig {\n authToken: string;\n workspace: string;\n}\n\nexport interface RetryConfig {\n maxRetries: number;\n baseDelayMs: number;\n maxDelayMs: number;\n}\n\nexport interface RestApiConfig {\n port: number;\n apiToken: string;\n}\n\n/** Background schedule (same behaviour as `content-store sync` in the CLI). */\nexport interface ScheduledCmsJobConfig {\n enabled: boolean;\n intervalMinutes: number;\n /** When true, run once when the server starts, then on every interval. */\n runOnStart: boolean;\n /** Job kind; only `sync` is implemented. */\n task: string;\n syncCms?: CMSProvider;\n syncTypes?: string[];\n}\n\nexport interface ScheduledTranslationJobConfig {\n enabled: boolean;\n intervalMinutes: number;\n /** When true, run once when the server starts, then on every interval. */\n runOnStart: boolean;\n /** Job kind; only `sync` is implemented. */\n task: string;\n syncProjects?: string[];\n}\n\nfunction parseScheduledCmsJobConfig(): ScheduledCmsJobConfig {\n const intervalMinutes = parseInt(process.env.SCHEDULE_INTERVAL_MINUTES ?? '0', 60);\n const enabled = Number.isFinite(intervalMinutes) && intervalMinutes > 0;\n const runOnStart =\n (process.env.SCHEDULE_RUN_ON_START ?? 'true').toLowerCase() !== 'false';\n const task = (process.env.SCHEDULE_TASK ?? 'sync').trim();\n const syncCms = (process.env.SCHEDULE_SYNC_CMS ?? '').trim() as CMSProvider;\n const rawTypes = process.env.SCHEDULE_SYNC_TYPES?.trim();\n const syncTypes = rawTypes\n ? rawTypes.split(',').map((s) => s.trim()).filter(Boolean)\n : undefined;\n\n return {\n enabled,\n intervalMinutes,\n runOnStart,\n task,\n syncCms: syncCms || undefined,\n syncTypes,\n };\n}\n\nfunction parseScheduledTranslationJobConfig(): ScheduledTranslationJobConfig {\n const intervalMinutes = parseInt(process.env.SCHEDULE_INTERVAL_MINUTES ?? '0', 60);\n const enabled = Number.isFinite(intervalMinutes) && intervalMinutes > 0;\n const runOnStart =\n (process.env.SCHEDULE_RUN_ON_START ?? 'true').toLowerCase() !== 'false';\n const task = (process.env.SCHEDULE_TASK ?? 'sync').trim();\n const rawProjects = process.env.SCHEDULE_SYNC_TRANSLATION_PROJECTS?.trim();\n const syncProjects = rawProjects\n ? rawProjects.split(',').map((s) => s.trim()).filter(Boolean)\n : undefined;\n\n return {\n enabled,\n intervalMinutes,\n runOnStart,\n task,\n syncProjects,\n };\n}\n\nexport interface CleanupConfig {\n enabled: boolean;\n /** Delete versioned S3 objects older than this many days. */\n retentionDays: number;\n}\n\nexport interface SlackConfig {\n enabled: boolean;\n botToken: string;\n signingSecret: string;\n /** Socket Mode app-level token (xapp-…). Required when enabled. */\n appToken: string;\n}\n\nexport interface ServerConfig {\n contentful: ContentfulConfig;\n sanity: SanityConfig;\n lingohub: LingohubConfig;\n retry: RetryConfig;\n api: RestApiConfig;\n scheduledCmsJob: ScheduledCmsJobConfig;\n scheduledTranslationJob: ScheduledTranslationJobConfig;\n cleanup: CleanupConfig;\n slack: SlackConfig;\n}\n\nexport const config: ServerConfig & SharedConfig = {\n ...sharedConfig,\n contentful: {\n spaceId: process.env.CONTENTFUL_SPACE_ID ?? '',\n accessToken: process.env.CONTENTFUL_WEBSITE_TOKEN ?? '',\n host: process.env.CONTENTFUL_HOST ?? 'preview.contentful.com',\n batchSize: 1000,\n maxDepth: 4,\n contentTypes: [\n 'asset','page','longtailPage','customJson','banner','cookieBanner','downloadPage'\n // Add Contentful content type IDs here to limit sync scope.\n ],\n },\n\n sanity: {\n projectId: process.env.SANITY_PROJECT_ID ?? '',\n dataset: process.env.SANITY_DATASET ?? 'main',\n token: process.env.SANITY_API_TOKEN ?? '',\n apiVersion: '2024-01-01',\n },\n\n lingohub: {\n authToken : process.env.LINGOHUB_AUTH_TOKEN ?? '',\n workspace: process.env.LINGOHUB_WORKSPACE ?? '',\n },\n\n retry: {\n maxRetries: parseInt(process.env.RETRY_MAX_RETRIES ?? '5', 10),\n baseDelayMs: parseInt(process.env.RETRY_BASE_DELAY_MS ?? '1000', 10),\n maxDelayMs: parseInt(process.env.RETRY_MAX_DELAY_MS ?? '60000', 10),\n },\n\n api: {\n port: parseInt(process.env.PORT ?? '3010'),\n apiToken: process.env.CONTENT_STORE_API_TOKEN ?? '',\n },\n\n scheduledCmsJob: parseScheduledCmsJobConfig(),\n scheduledTranslationJob: parseScheduledTranslationJobConfig(),\n\n cleanup: {\n enabled: (process.env.CLEANUP_ENABLED ?? 'true').toLowerCase() !== 'false',\n retentionDays: parseInt(process.env.CLEANUP_RETENTION_DAYS ?? '30', 10),\n },\n\n slack: {\n enabled: (process.env.SLACK_BOT_TOKEN ?? '').length > 0,\n botToken: process.env.SLACK_BOT_TOKEN ?? '',\n signingSecret: process.env.SLACK_SIGNING_SECRET ?? '',\n appToken: process.env.SLACK_APP_TOKEN ?? '',\n },\n};\n","import {\n createClient,\n type ContentfulClientApi,\n type ContentTypeCollection,\n type AssetCollection,\n} from 'contentful';\nimport type { ContentfulConfig, RetryConfig } from '../config';\nimport type { CMSAdapter, FetchResult } from './types';\nimport { withRetry } from '../sync/retry';\n\ntype CfCollection = {\n items: CfItem[],\n total: number\n}\n\ntype CfItem = {\n metadata:{\n tags: string[],\n concepts: string[]\n }\n sys:{\n type: string,\n id: string\n space: {\n sys: {\n type: string\n linkType: string\n id: string\n }\n },\n environment: {\n sys: {\n id: string\n type: 'Link',\n linkType: 'Environment'\n }\n },\n contentType: {\n sys: {\n type: 'Link',\n linkType: 'ContentType',\n id: string\n }\n },\n createdBy: {\n sys: {\n type: 'Link',\n linkType: 'User',\n id: string,\n }\n },\n updatedBy: {\n sys: {\n type: 'Link',\n linkType: 'User',\n id: string\n }\n },\n 'revision': number,\n 'createdAt': string,\n 'updatedAt': string,\n 'publishedVersion': string\n },\n fields:{\n [key:string]: unknown\n }\n}\n\n\n/**\n * Recursively unwraps Contentful's { metadata, sys, fields } envelope.\n * `depth` tracks how many entry/asset envelopes deep we are — anything\n * beyond `maxDepth` is dropped to avoid blowing the call stack on\n * circular or extremely deep reference chains.\n *\n * `path` holds objects on the current recursion branch only. That way a\n * shared reference (e.g. the same asset on `image` and `mobileImage`) is\n * unwrapped for each sibling; only true cycles (an object recurring as a\n * descendant of itself) yield `undefined`.\n */\nfunction stripEnvelope(\n value: unknown,\n maxDepth: number,\n depth = 0,\n path = new WeakSet<object>(),\n): unknown {\n if (value === null || typeof value !== 'object') return value;\n\n const obj = value as CfItem;\n\n if (path.has(obj)) return undefined;\n path.add(obj);\n\n try {\n if (Array.isArray(value)) {\n return value.map((item) => stripEnvelope(item, maxDepth, depth, path));\n }\n\n const isEnvelope = 'sys' in obj && 'fields' in obj && typeof obj.fields === 'object';\n\n if (isEnvelope) {\n if (depth >= maxDepth) return undefined;\n\n const fields = obj.fields as Record<string, unknown>;\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(fields)) {\n result[k] = stripEnvelope(v, maxDepth, depth + 1, path);\n }\n\n const sys = obj.sys;\n if (sys) {\n const existingMeta = result.meta;\n const metaBase =\n typeof existingMeta === 'object' &&\n existingMeta !== null &&\n !Array.isArray(existingMeta)\n ? (existingMeta as Record<string, unknown>)\n : {};\n const _contentType = sys.contentType ? sys.contentType.sys.id : 'Asset'\n result.meta = { ...metaBase, _id: sys.id, _contentType, _updatedAt: sys.updatedAt };\n }\n\n return result;\n }\n\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) {\n result[k] = stripEnvelope(v, maxDepth, depth, path);\n }\n return result;\n } finally {\n path.delete(obj);\n }\n}\n\nexport class ContentfulAdapter implements CMSAdapter {\n readonly name = 'contentful';\n private client: ContentfulClientApi<undefined>;\n private batchSize: number;\n private maxDepth: number;\n private allowedTypes: string[];\n private retryConfig: RetryConfig;\n\n constructor(cfg: ContentfulConfig, retryConfig: RetryConfig) {\n this.client = createClient({\n space: cfg.spaceId,\n accessToken: cfg.accessToken,\n host: cfg.host,\n });\n this.batchSize = cfg.batchSize;\n this.maxDepth = cfg.maxDepth;\n this.allowedTypes = cfg.contentTypes;\n this.retryConfig = retryConfig;\n }\n\n async getContentTypes(): Promise<string[]> {\n const response = await withRetry<ContentTypeCollection>(\n () => this.client.getContentTypes(),\n this.retryConfig,\n );\n\n const allTypes = response.items.map((ct) => ct.sys.id);\n\n if (this.allowedTypes.length > 0) {\n return allTypes.filter((t) => this.allowedTypes.includes(t));\n }\n return allTypes;\n }\n\n /**\n * Fetches every entry for a content type using batched pagination.\n * Contentful caps `getEntries` at 1 000 items per call, so we page through\n * with `skip` until all items are collected.\n *\n * The reserved content type `\"asset\"` fetches from `getAssets()` instead.\n */\n async fetchAll(contentType: string, includeLevels: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 = 4): Promise<FetchResult> {\n if (contentType === 'asset') {\n return this.fetchAllAssets();\n }\n\n const allItems: unknown[] = [];\n let skip = 0;\n let total = 0;\n\n do {\n const payload = {\n content_type: contentType,\n limit: this.batchSize,\n skip,\n include: includeLevels,\n };\n const response = await this.client.getEntries(payload) as unknown as CfCollection;\n total = response.total;\n allItems.push(...response.items);\n skip += response.items.length;\n\n if (total > this.batchSize) {\n console.log(\n ` [contentful] ${contentType}: fetched ${allItems.length}/${total}`,\n );\n }\n } while (skip < total);\n\n return {\n contentType,\n items: allItems.map((item) => stripEnvelope(item, this.maxDepth)),\n total,\n };\n }\n\n private async fetchAllAssets(): Promise<FetchResult> {\n const allItems: unknown[] = [];\n let skip = 0;\n let total = 0;\n\n do {\n const response = await withRetry<AssetCollection>(\n () => this.client.getAssets({ limit: this.batchSize, skip }),\n this.retryConfig,\n );\n total = response.total;\n allItems.push(...response.items);\n skip += response.items.length;\n\n if (total > this.batchSize) {\n console.log(\n ` [contentful] asset: fetched ${allItems.length}/${total}`,\n );\n }\n } while (skip < total);\n\n return {\n contentType: 'asset',\n items: allItems.map((item) => stripEnvelope(item, this.maxDepth)),\n total,\n };\n }\n}\n","import type { RetryConfig } from '../config';\n\n/**\n * Inspects an error to determine if it represents an API rate-limit (HTTP 429).\n * Returns the suggested wait time in ms when available, otherwise `0` to signal\n * that the caller should fall back to computed backoff. Returns `null` when the\n * error is *not* a rate-limit error.\n */\nfunction rateLimitDelayMs(err: unknown): number | null {\n const e = err as Record<string, unknown>;\n\n if (e?.status === 429 || e?.statusCode === 429) {\n const reset = (e?.headers as Record<string, string> | undefined)?.[\n 'x-contentful-ratelimit-reset'\n ];\n return reset ? parseFloat(reset) * 1000 : 0;\n }\n\n const resp = e?.response as Record<string, unknown> | undefined;\n if (resp?.status === 429) {\n const headers = resp?.headers as Record<string, string> | undefined;\n const reset = headers?.['x-contentful-ratelimit-reset'];\n return reset ? parseFloat(reset) * 1000 : 0;\n }\n\n return null;\n}\n\nfunction computeDelay(\n attempt: number,\n baseDelayMs: number,\n maxDelayMs: number,\n): number {\n const exponential = baseDelayMs * Math.pow(2, attempt);\n const jitter = Math.random() * baseDelayMs;\n return Math.min(exponential + jitter, maxDelayMs);\n}\n\n/**\n * Executes `fn` with automatic retry + exponential backoff.\n * Rate-limit (429) responses are handled specially: if the API provides a\n * Retry-After / reset header, that value is respected instead of computed backoff.\n */\nexport async function withRetry<T>(\n fn: () => Promise<T>,\n { maxRetries, baseDelayMs, maxDelayMs }: RetryConfig,\n): Promise<T> {\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await fn();\n } catch (err) {\n if (attempt === maxRetries) throw err;\n\n const rlDelay = rateLimitDelayMs(err);\n let delay: number;\n\n if (rlDelay !== null) {\n delay =\n rlDelay > 0 ? rlDelay : computeDelay(attempt, baseDelayMs, maxDelayMs);\n console.warn(\n ` Rate limited (attempt ${attempt + 1}/${maxRetries}). ` +\n `Waiting ${Math.round(delay)}ms…`,\n );\n } else {\n delay = computeDelay(attempt, baseDelayMs, maxDelayMs);\n console.warn(\n ` Request failed (attempt ${attempt + 1}/${maxRetries}): ` +\n `${(err as Error).message}. Retrying in ${Math.round(delay)}ms…`,\n );\n }\n\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n\n throw new Error('withRetry: unreachable');\n}\n","import { createClient, type SanityClient } from '@sanity/client';\nimport type { SanityConfig, RetryConfig } from '../config';\nimport type { CMSAdapter, FetchResult } from './types';\nimport { withRetry } from '../sync/retry';\n\nexport class SanityAdapter implements CMSAdapter {\n readonly name = 'sanity';\n private client: SanityClient;\n private retryConfig: RetryConfig;\n\n constructor(cfg: SanityConfig, retryConfig: RetryConfig) {\n this.client = createClient({\n projectId: cfg.projectId,\n dataset: cfg.dataset,\n token: cfg.token,\n apiVersion: cfg.apiVersion,\n useCdn: false,\n });\n this.retryConfig = retryConfig;\n }\n\n async getContentTypes(): Promise<string[]> {\n const types: string[] = await withRetry(\n () => this.client.fetch('array::unique(*[]._type)'),\n this.retryConfig,\n );\n return types.filter(\n (t) => !t.startsWith('system.') && !t.startsWith('sanity.'),\n );\n }\n\n async fetchAll(contentType: string): Promise<FetchResult> {\n const items: unknown[] = await withRetry(\n () => this.client.fetch('*[_type == $type]', { type: contentType }),\n this.retryConfig,\n );\n\n console.log(` [sanity] ${contentType}: fetched ${items.length} items`);\n return { contentType, items, total: items.length };\n }\n}\n","import { config, type CMSProvider } from '../config';\nimport type { CMSAdapter } from './types';\nimport { ContentfulAdapter } from './contentful';\nimport { SanityAdapter } from './sanity';\n\nexport function createAdapter(cms: CMSProvider): CMSAdapter {\n switch (cms) {\n case 'contentful':\n return new ContentfulAdapter(config.contentful, config.retry);\n case 'sanity':\n return new SanityAdapter(config.sanity, config.retry);\n default:\n throw new Error(`Unknown CMS provider: ${cms as string}`);\n }\n}\n\nexport type { CMSAdapter, FetchResult } from './types';\n","import convert from 'xml-js';\nimport set from 'lodash.set';\nimport merge from 'lodash.merge';\n\nexport const transformObjectToNested = (data: Record<string, unknown>): Record<string, unknown> => {\n const result: Record<string, unknown> = {};\n\n Object.entries(data).forEach(([key, value]) => {\n const tempObject = {};\n set(tempObject, key, value);\n merge(result, tempObject);\n });\n\n return result;\n};\n\nexport const transformObjectToFlat = (data: Record<string, any>): Record<string, any> => { // eslint-disable-line @typescript-eslint/no-explicit-any\n const result: Record<string, unknown> = {};\n\n const flatten = (obj: Record<string, any>, path: string[] = []) => { // eslint-disable-line @typescript-eslint/no-explicit-any\n Object.entries(obj).forEach(([key, value]) => {\n if (typeof value === 'object') {\n flatten(value, path.concat(key));\n } else {\n result[path.concat(key).join('.')] = value;\n }\n });\n };\n\n flatten(data);\n\n return result;\n}\n\nexport const convertXMLToJS = (xml: string): Record<string, string> => {\n const converted = convert.xml2js(xml, {\n ignoreComment: true,\n ignoreDeclaration: true,\n ignoreInstruction: true,\n compact: true,\n }) as {\n resources: {\n string: {\n _attributes: { name: string },\n _text: 'User does not exist'\n }[];\n }\n };\n\n let mapped = {};\n converted.resources.string.forEach((item) => {\n mapped = {\n ...mapped,\n [item._attributes.name]: item._text,\n };\n });\n\n return mapped;\n};\n\nexport const parseIOSStrings = (strings: string): Record<string, string> => {\n const parsedObj: Record<string, string> = {};\n strings\n .split('\\n')\n .filter((line) => line.startsWith('\"') && line.endsWith(';'))\n .map((line) => line.trim().slice(0, -1))\n .forEach((line) => {\n let [key, value] = line.split(' = ');\n if (!key || !value) return;\n key = key.slice(1, -1);\n value = value.slice(1, -1);\n parsedObj[key] = value;\n });\n\n return parsedObj;\n};","import { config } from '../config';\nimport { LingohubResource} from '../../shared/lingohub';\nimport {convertXMLToJS, parseIOSStrings} from '../../shared/utils';\n\nconst cfg = config.lingohub\nconst apiUrl = 'https://api.lingohub.com/v1/' + cfg.workspace + '/projects/';\n\nexport const getResourceForLocale = async (project: string, resource: LingohubResource, locale: string) => {\n const urlForResourceLocalised = `${apiUrl}${project}/resources/${resource.fileName}?auth_token=${cfg.authToken}`.replace('[locale]', locale);\n const result = await fetch(urlForResourceLocalised,\n {\n method: 'GET',\n }).then(async (res) => {\n\n if (!res.ok) {\n throw new Error(`Failed to fetch resource: ${res.status} - ${res.statusText}`);\n }\n\n if (resource.type === 'json') {\n return res.json();\n }\n\n if (resource.type === 'xml') {\n const xml = await res.text();\n return convertXMLToJS(xml);\n }\n\n if (resource.type === 'strings') {\n const strings = await res.text();\n return parseIOSStrings(strings);\n }\n\n throw new Error('Invalid resource type');\n })\n .catch((err) => {\n throw err;\n });\n return result;\n}\n","import type { CMSProvider } from '../config';\nimport { config } from '../config';\nimport { createAdapter } from '../adapters';\nimport {buildCmsObjectKey, buildTranslationObjectKey, ContentStore} from '../../shared/s3';\nimport { allProjects, defaultLocales } from '../../shared/lingohub';\nimport { getResourceForLocale} from '../adapters/lingohub';\n\nexport interface CmsSyncResultEntry {\n contentType: string;\n itemCount: number;\n objectKey: string;\n}\n\nexport interface CmsSyncResult {\n cms: CMSProvider;\n timestamp: number;\n entries: CmsSyncResultEntry[];\n errors: Array<{ contentType: string; error: string }>;\n}\n\nexport interface TranslationSyncResultEntry {\n project: string;\n locale: string;\n itemCount: number;\n objectKey: string;\n}\n\nexport interface TranslationSyncResult {\n timestamp: number;\n entries: TranslationSyncResultEntry[];\n errors: Array<{ locale: string; project: string, error: string }>;\n}\n\nexport async function runSync(cms: CMSProvider, contentTypes?: string[], includeLevels?: number){\n await syncCmsContent(cms, contentTypes, includeLevels )\n}\n\nexport async function syncCmsContent(\n cms: CMSProvider,\n contentTypes?: string[],\n includeLevels?: number\n): Promise<CmsSyncResult> {\n const adapter = createAdapter(cms);\n const store = new ContentStore(config.s3);\n const timestamp = Math.floor(Date.now() / 1000);\n\n console.log(`\\nStarting sync from ${cms} at ${new Date(timestamp * 1000).toISOString()}`);\n\n const typesToSync =\n contentTypes && contentTypes.length > 0\n ? contentTypes\n : await adapter.getContentTypes();\n\n console.log(`Content types to sync: ${typesToSync.join(', ')}\\n`);\n\n const entries: CmsSyncResultEntry[] = [];\n const errors: Array<{ contentType: string; error: string }> = [];\n\n for (const contentType of typesToSync) {\n try {\n const result = await adapter.fetchAll(contentType, includeLevels);\n const objectKey = buildCmsObjectKey(cms, contentType);\n await store.upload(objectKey, result.items);\n\n entries.push({\n contentType,\n itemCount: result.total,\n objectKey,\n });\n\n console.log(\n ` + ${contentType}: ${result.total} items -> ${objectKey}`,\n );\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n errors.push({ contentType, error: message });\n console.error(` x ${contentType}: ${message}`);\n }\n }\n\n console.log(\n `\\nSync complete: ${entries.length} succeeded, ${errors.length} failed\\n`,\n );\n\n return { cms, timestamp, entries, errors };\n}\n\nexport async function syncTranslations(projects?: string[], locales?:string[]):Promise<TranslationSyncResult> {\n\n const store = new ContentStore(config.s3);\n const entries: TranslationSyncResultEntry[] = [];\n const errors: Array<{ project: string; locale: string, error: string }> = [];\n const timestamp = Math.floor(Date.now() / 1000);\n if(!locales){\n locales = defaultLocales;\n }\n if(!projects){\n projects = Object.keys(allProjects);\n }\n\n for(const project of projects) {\n const resources = allProjects[project];\n if(!resources){\n console.error(`No resources found for ${project}`);\n continue;\n }\n for(const resource of resources) {\n for(const loc of locales){\n const locale = (resource.localeMapping && resource.localeMapping[loc]) ? resource.localeMapping[loc] : loc;\n try{\n const result = await getResourceForLocale(project, resource, locale);\n const objectKey = buildTranslationObjectKey(project, resource.fileName, locale);\n await store.upload(objectKey, result);\n const itemCount = Object.keys(result).length\n entries.push({\n project,\n locale,\n itemCount,\n objectKey,\n });\n\n console.log(\n ` + ${project} - ${locale}: ${itemCount} items -> ${objectKey}`,\n );\n\n // await new Promise(resolve => setTimeout(resolve, 1000)); // Rate limiting\n\n }catch(err){\n const message = err instanceof Error ? err.message : String(err);\n errors.push({ project,locale, error: message });\n console.error(` x ${project} - ${locale}: ${message}`);\n }\n }\n }\n }\n\n return { timestamp, entries, errors };\n}\n"],"mappings":"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","names":["config","result","cfg","createClient","cfg","createClient","config","config","config"]}