@tandem-language-exchange/content-store 1.3.2 → 1.3.3

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.
@@ -1,472 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- config
4
- } from "./chunk-Y6HC4NYU.js";
5
- import {
6
- ContentStore,
7
- buildCmsObjectKey,
8
- buildTranslationObjectKey,
9
- contentTypeForTranslationKey
10
- } from "./chunk-LZHYKLAU.js";
11
- import {
12
- allProjects,
13
- defaultLocales
14
- } from "./chunk-SF7FCBR2.js";
15
-
16
- // src/server/config.ts
17
- import dotenv from "dotenv";
18
- dotenv.config({ path: ".env.local" });
19
- dotenv.config();
20
- function parseScheduledCmsJobConfig() {
21
- const intervalMinutes = parseInt(process.env.SCHEDULE_INTERVAL_MINUTES ?? "0", 10);
22
- const enabled = Number.isFinite(intervalMinutes) && intervalMinutes > 0;
23
- const runOnStart = (process.env.SCHEDULE_RUN_ON_START ?? "true").toLowerCase() !== "false";
24
- const task = (process.env.SCHEDULE_TASK ?? "sync").trim();
25
- const syncCms = (process.env.SCHEDULE_SYNC_CMS ?? "").trim();
26
- const rawTypes = process.env.SCHEDULE_SYNC_TYPES?.trim();
27
- const syncTypes = rawTypes ? rawTypes.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
28
- return {
29
- enabled,
30
- intervalMinutes,
31
- runOnStart,
32
- task,
33
- syncCms: syncCms || void 0,
34
- syncTypes
35
- };
36
- }
37
- function parseScheduledTranslationJobConfig() {
38
- const intervalMinutes = parseInt(process.env.SCHEDULE_INTERVAL_MINUTES ?? "0", 10);
39
- const enabled = Number.isFinite(intervalMinutes) && intervalMinutes > 0;
40
- const runOnStart = (process.env.SCHEDULE_RUN_ON_START ?? "true").toLowerCase() !== "false";
41
- const task = (process.env.SCHEDULE_TASK ?? "sync").trim();
42
- const rawProjects = process.env.SCHEDULE_SYNC_TRANSLATION_PROJECTS?.trim();
43
- const syncProjects = rawProjects ? rawProjects.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
44
- return {
45
- enabled,
46
- intervalMinutes,
47
- runOnStart,
48
- task,
49
- syncProjects
50
- };
51
- }
52
- var config2 = {
53
- ...config,
54
- contentful: {
55
- spaceId: process.env.CONTENTFUL_SPACE_ID ?? "",
56
- accessToken: process.env.CONTENTFUL_WEBSITE_TOKEN ?? "",
57
- host: process.env.CONTENTFUL_HOST ?? "preview.contentful.com",
58
- batchSize: 1e3,
59
- maxDepth: 4,
60
- contentTypes: [
61
- "asset",
62
- "page",
63
- "longtailPage",
64
- "customJson",
65
- "banner",
66
- "cookieBanner",
67
- "downloadPage"
68
- // Add Contentful content type IDs here to limit sync scope.
69
- ]
70
- },
71
- sanity: {
72
- projectId: process.env.SANITY_PROJECT_ID ?? "",
73
- dataset: process.env.SANITY_DATASET ?? "main",
74
- token: process.env.SANITY_API_TOKEN ?? "",
75
- apiVersion: "2024-01-01"
76
- },
77
- lingohub: {
78
- authToken: process.env.LINGOHUB_AUTH_TOKEN ?? "",
79
- workspace: process.env.LINGOHUB_WORKSPACE ?? ""
80
- },
81
- retry: {
82
- maxRetries: parseInt(process.env.RETRY_MAX_RETRIES ?? "5", 10),
83
- baseDelayMs: parseInt(process.env.RETRY_BASE_DELAY_MS ?? "1000", 10),
84
- maxDelayMs: parseInt(process.env.RETRY_MAX_DELAY_MS ?? "60000", 10)
85
- },
86
- api: {
87
- port: parseInt(process.env.PORT ?? "3030", 10),
88
- apiToken: process.env.CONTENT_STORE_API_TOKEN ?? ""
89
- },
90
- scheduledCmsJob: parseScheduledCmsJobConfig(),
91
- scheduledTranslationJob: parseScheduledTranslationJobConfig(),
92
- slack: {
93
- enabled: (process.env.SLACK_BOT_TOKEN ?? "").length > 0,
94
- botToken: process.env.SLACK_BOT_TOKEN ?? "",
95
- signingSecret: process.env.SLACK_SIGNING_SECRET ?? "",
96
- appToken: process.env.SLACK_APP_TOKEN ?? "",
97
- notifyChannel: process.env.SLACK_NOTIFY_CHANNEL ?? "",
98
- cmdSyncContent: process.env.SLACK_CMD_SYNC_CONTENT ?? "/sync-content",
99
- cmdSyncTranslations: process.env.SLACK_CMD_SYNC_TRANSLATIONS ?? "/sync-translations"
100
- }
101
- };
102
-
103
- // src/server/adapters/contentful.ts
104
- import {
105
- createClient
106
- } from "contentful";
107
-
108
- // src/server/sync/retry.ts
109
- function rateLimitDelayMs(err) {
110
- const e = err;
111
- if (e?.status === 429 || e?.statusCode === 429) {
112
- const reset = e?.headers?.["x-contentful-ratelimit-reset"];
113
- return reset ? parseFloat(reset) * 1e3 : 0;
114
- }
115
- const resp = e?.response;
116
- if (resp?.status === 429) {
117
- const headers = resp?.headers;
118
- const reset = headers?.["x-contentful-ratelimit-reset"];
119
- return reset ? parseFloat(reset) * 1e3 : 0;
120
- }
121
- return null;
122
- }
123
- function computeDelay(attempt, baseDelayMs, maxDelayMs) {
124
- const exponential = baseDelayMs * Math.pow(2, attempt);
125
- const jitter = Math.random() * baseDelayMs;
126
- return Math.min(exponential + jitter, maxDelayMs);
127
- }
128
- async function withRetry(fn, { maxRetries, baseDelayMs, maxDelayMs }) {
129
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
130
- try {
131
- return await fn();
132
- } catch (err) {
133
- if (attempt === maxRetries) throw err;
134
- const rlDelay = rateLimitDelayMs(err);
135
- let delay;
136
- if (rlDelay !== null) {
137
- delay = rlDelay > 0 ? rlDelay : computeDelay(attempt, baseDelayMs, maxDelayMs);
138
- console.warn(
139
- ` Rate limited (attempt ${attempt + 1}/${maxRetries}). Waiting ${Math.round(delay)}ms\u2026`
140
- );
141
- } else {
142
- delay = computeDelay(attempt, baseDelayMs, maxDelayMs);
143
- console.warn(
144
- ` Request failed (attempt ${attempt + 1}/${maxRetries}): ${err.message}. Retrying in ${Math.round(delay)}ms\u2026`
145
- );
146
- }
147
- await new Promise((resolve) => setTimeout(resolve, delay));
148
- }
149
- }
150
- throw new Error("withRetry: unreachable");
151
- }
152
-
153
- // src/server/adapters/contentful.ts
154
- function stripEnvelope(value, maxDepth, depth = 0, path = /* @__PURE__ */ new WeakSet()) {
155
- if (value === null || typeof value !== "object") return value;
156
- const obj = value;
157
- if (path.has(obj)) return void 0;
158
- path.add(obj);
159
- try {
160
- if (Array.isArray(value)) {
161
- return value.map((item) => stripEnvelope(item, maxDepth, depth, path));
162
- }
163
- const isEnvelope = "sys" in obj && "fields" in obj && typeof obj.fields === "object";
164
- if (isEnvelope) {
165
- if (depth >= maxDepth) return void 0;
166
- const fields = obj.fields;
167
- const result2 = {};
168
- for (const [k, v] of Object.entries(fields)) {
169
- result2[k] = stripEnvelope(v, maxDepth, depth + 1, path);
170
- }
171
- const sys = obj.sys;
172
- if (sys) {
173
- const existingMeta = result2.meta;
174
- const metaBase = typeof existingMeta === "object" && existingMeta !== null && !Array.isArray(existingMeta) ? existingMeta : {};
175
- const _contentType = sys.contentType ? sys.contentType.sys.id : "Asset";
176
- result2.meta = { ...metaBase, _id: sys.id, _contentType, _updatedAt: sys.updatedAt };
177
- }
178
- return result2;
179
- }
180
- const result = {};
181
- for (const [k, v] of Object.entries(obj)) {
182
- result[k] = stripEnvelope(v, maxDepth, depth, path);
183
- }
184
- return result;
185
- } finally {
186
- path.delete(obj);
187
- }
188
- }
189
- var ContentfulAdapter = class {
190
- name = "contentful";
191
- client;
192
- batchSize;
193
- maxDepth;
194
- allowedTypes;
195
- retryConfig;
196
- constructor(cfg2, retryConfig) {
197
- this.client = createClient({
198
- space: cfg2.spaceId,
199
- accessToken: cfg2.accessToken,
200
- host: cfg2.host
201
- });
202
- this.batchSize = cfg2.batchSize;
203
- this.maxDepth = cfg2.maxDepth;
204
- this.allowedTypes = cfg2.contentTypes;
205
- this.retryConfig = retryConfig;
206
- }
207
- async getContentTypes() {
208
- const response = await withRetry(
209
- () => this.client.getContentTypes(),
210
- this.retryConfig
211
- );
212
- const allTypes = response.items.map((ct) => ct.sys.id);
213
- if (this.allowedTypes.length > 0) {
214
- return allTypes.filter((t) => this.allowedTypes.includes(t));
215
- }
216
- return allTypes;
217
- }
218
- /**
219
- * Fetches every entry for a content type using batched pagination.
220
- * Contentful caps `getEntries` at 1 000 items per call, so we page through
221
- * with `skip` until all items are collected.
222
- *
223
- * The reserved content type `"asset"` fetches from `getAssets()` instead.
224
- */
225
- async fetchAll(contentType, includeLevels = 4) {
226
- if (contentType === "asset") {
227
- return this.fetchAllAssets();
228
- }
229
- const allItems = [];
230
- let skip = 0;
231
- let total = 0;
232
- do {
233
- const payload = {
234
- content_type: contentType,
235
- limit: this.batchSize,
236
- skip,
237
- include: includeLevels
238
- };
239
- const response = await this.client.getEntries(payload);
240
- total = response.total;
241
- allItems.push(...response.items);
242
- skip += response.items.length;
243
- if (total > this.batchSize) {
244
- console.log(
245
- ` [contentful] ${contentType}: fetched ${allItems.length}/${total}`
246
- );
247
- }
248
- } while (skip < total);
249
- return {
250
- contentType,
251
- items: allItems.map((item) => stripEnvelope(item, this.maxDepth)),
252
- total
253
- };
254
- }
255
- async fetchAllAssets() {
256
- const allItems = [];
257
- let skip = 0;
258
- let total = 0;
259
- do {
260
- const response = await withRetry(
261
- () => this.client.getAssets({ limit: this.batchSize, skip }),
262
- this.retryConfig
263
- );
264
- total = response.total;
265
- allItems.push(...response.items);
266
- skip += response.items.length;
267
- if (total > this.batchSize) {
268
- console.log(
269
- ` [contentful] asset: fetched ${allItems.length}/${total}`
270
- );
271
- }
272
- } while (skip < total);
273
- return {
274
- contentType: "asset",
275
- items: allItems.map((item) => stripEnvelope(item, this.maxDepth)),
276
- total
277
- };
278
- }
279
- };
280
-
281
- // src/server/adapters/sanity.ts
282
- import { createClient as createClient2 } from "@sanity/client";
283
- var SanityAdapter = class {
284
- name = "sanity";
285
- client;
286
- retryConfig;
287
- constructor(cfg2, retryConfig) {
288
- this.client = createClient2({
289
- projectId: cfg2.projectId,
290
- dataset: cfg2.dataset,
291
- token: cfg2.token,
292
- apiVersion: cfg2.apiVersion,
293
- useCdn: false
294
- });
295
- this.retryConfig = retryConfig;
296
- }
297
- async getContentTypes() {
298
- const types = await withRetry(
299
- () => this.client.fetch("array::unique(*[]._type)"),
300
- this.retryConfig
301
- );
302
- return types.filter(
303
- (t) => !t.startsWith("system.") && !t.startsWith("sanity.")
304
- );
305
- }
306
- async fetchAll(contentType) {
307
- const items = await withRetry(
308
- () => this.client.fetch("*[_type == $type]", { type: contentType }),
309
- this.retryConfig
310
- );
311
- console.log(` [sanity] ${contentType}: fetched ${items.length} items`);
312
- return { contentType, items, total: items.length };
313
- }
314
- };
315
-
316
- // src/server/adapters/index.ts
317
- function createAdapter(cms) {
318
- switch (cms) {
319
- case "contentful":
320
- return new ContentfulAdapter(config2.contentful, config2.retry);
321
- case "sanity":
322
- return new SanityAdapter(config2.sanity, config2.retry);
323
- default:
324
- throw new Error(`Unknown CMS provider: ${cms}`);
325
- }
326
- }
327
-
328
- // src/server/adapters/lingohub.ts
329
- var cfg = config2.lingohub;
330
- var apiUrl = "https://api.lingohub.com/v1/" + cfg.workspace + "/projects/";
331
- async function fetchLingohubResourceRaw(project, resource, locale) {
332
- const urlForResourceLocalised = `${apiUrl}${project}/resources/${resource.fileName}?auth_token=${cfg.authToken}`.replace(
333
- "[locale]",
334
- locale
335
- );
336
- const res = await fetch(urlForResourceLocalised, { method: "GET" });
337
- if (!res.ok) {
338
- throw new Error(`Failed to fetch resource \`${resource.fileName}\` (${locale}): ${res.status} - ${res.statusText}`);
339
- }
340
- return await res.text();
341
- }
342
-
343
- // src/server/sync/engine.ts
344
- function summariseCmsEntries(result) {
345
- return result.entries.map(
346
- (e) => `\u2022 \`${e.contentType}\` \u2014 ${e.itemCount} items`
347
- );
348
- }
349
- function summariseCmsErrors(result) {
350
- return result.errors.map(
351
- (e) => `\u2022 \`${e.contentType}\` \u2014 ${e.error}`
352
- );
353
- }
354
- function summariseTranslationEntries(entries) {
355
- const grouped = /* @__PURE__ */ new Map();
356
- for (const e of entries) {
357
- const key = `${e.project} / ${e.resource}`;
358
- const existing = grouped.get(key);
359
- if (existing) {
360
- existing.locales += 1;
361
- existing.bytes += e.itemCount;
362
- } else {
363
- grouped.set(key, { locales: 1, bytes: e.itemCount });
364
- }
365
- }
366
- return [...grouped.entries()].map(
367
- ([key, { locales, bytes }]) => `\u2022 \`${key}\` / [${locales} locale${locales === 1 ? "" : "s"}] \u2014 ${bytes} bytes`
368
- );
369
- }
370
- async function syncCmsContent(cms, contentTypes, includeLevels) {
371
- const adapter = createAdapter(cms);
372
- const store = new ContentStore(config2.s3);
373
- const timestamp = Math.floor(Date.now() / 1e3);
374
- console.log(`
375
- Starting sync from ${cms} at ${new Date(timestamp * 1e3).toISOString()}`);
376
- const typesToSync = contentTypes && contentTypes.length > 0 ? contentTypes : await adapter.getContentTypes();
377
- console.log(`Content types to sync: ${typesToSync.join(", ")}
378
- `);
379
- const entries = [];
380
- const errors = [];
381
- for (const contentType of typesToSync) {
382
- try {
383
- const result = await adapter.fetchAll(contentType, includeLevels);
384
- const objectKey = buildCmsObjectKey(cms, contentType);
385
- await store.upload(objectKey, result.items);
386
- entries.push({
387
- contentType,
388
- itemCount: result.total,
389
- objectKey
390
- });
391
- console.log(
392
- ` + ${contentType}: ${result.total} items -> ${objectKey}`
393
- );
394
- } catch (err) {
395
- const message = err instanceof Error ? err.message : String(err);
396
- errors.push({ contentType, error: message });
397
- console.error(` x ${contentType}: ${message}`);
398
- }
399
- }
400
- console.log(
401
- `
402
- Sync complete: ${entries.length} succeeded, ${errors.length} failed
403
- `
404
- );
405
- return { cms, timestamp, entries, errors };
406
- }
407
- async function syncTranslations(projects, locales) {
408
- const store = new ContentStore(config2.s3);
409
- const entries = [];
410
- const errors = [];
411
- const timestamp = Math.floor(Date.now() / 1e3);
412
- if (!locales) {
413
- locales = defaultLocales;
414
- }
415
- if (!projects) {
416
- projects = Object.keys(allProjects);
417
- }
418
- for (const project of projects) {
419
- const resources = allProjects[project];
420
- if (!resources) {
421
- console.error(`No resources found for ${project}`);
422
- continue;
423
- }
424
- for (const resource of resources) {
425
- for (const loc of locales) {
426
- const locale = resource.localeMapping && resource.localeMapping[loc] ? resource.localeMapping[loc] : loc;
427
- try {
428
- const raw = await withRetry(
429
- () => fetchLingohubResourceRaw(project, resource, locale),
430
- config2.retry
431
- );
432
- const objectKey = buildTranslationObjectKey(
433
- project,
434
- resource.fileName,
435
- locale
436
- );
437
- await store.uploadRaw(
438
- objectKey,
439
- raw,
440
- contentTypeForTranslationKey(objectKey)
441
- );
442
- const byteLength = Buffer.byteLength(raw, "utf8");
443
- entries.push({
444
- project,
445
- resource: resource.resource,
446
- locale,
447
- itemCount: byteLength,
448
- objectKey
449
- });
450
- console.log(
451
- ` + ${project} - ${locale}: ${byteLength} bytes -> ${objectKey}`
452
- );
453
- } catch (err) {
454
- const message = err instanceof Error ? err.message : String(err);
455
- errors.push({ project, locale, resource: resource.resource, error: message });
456
- console.error(` x ${project} / ${resource.resource} - ${locale}: ${message}`);
457
- }
458
- }
459
- }
460
- }
461
- return { timestamp, entries, errors };
462
- }
463
-
464
- export {
465
- config2 as config,
466
- summariseCmsEntries,
467
- summariseCmsErrors,
468
- summariseTranslationEntries,
469
- syncCmsContent,
470
- syncTranslations
471
- };
472
- //# sourceMappingURL=chunk-NQHWG4XM.js.map
@@ -1 +0,0 @@
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/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', 10);\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', 10);\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 SlackConfig {\n enabled: boolean;\n botToken: string;\n signingSecret: string;\n /** Socket Mode app-level token (xapp-…). Required when enabled. */\n appToken: string;\n /** Channel ID or name for async notifications (e.g. sync results). */\n notifyChannel: string;\n /** Slash command name for CMS sync (default `/sync-content`). */\n cmdSyncContent: string;\n /** Slash command name for translation sync (default `/sync-translations`). */\n cmdSyncTranslations: 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 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 ?? '3030', 10),\n apiToken: process.env.CONTENT_STORE_API_TOKEN ?? '',\n },\n\n scheduledCmsJob: parseScheduledCmsJobConfig(),\n scheduledTranslationJob: parseScheduledTranslationJobConfig(),\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 notifyChannel: process.env.SLACK_NOTIFY_CHANNEL ?? '',\n cmdSyncContent: process.env.SLACK_CMD_SYNC_CONTENT ?? '/sync-content',\n cmdSyncTranslations: process.env.SLACK_CMD_SYNC_TRANSLATIONS ?? '/sync-translations',\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 { config } from '../config';\nimport type { LingohubResource } from '../../shared/lingohub';\n\nconst cfg = config.lingohub;\nconst apiUrl = 'https://api.lingohub.com/v1/' + cfg.workspace + '/projects/';\n\n/**\n * Downloads the raw Lingohub resource body (exact bytes as UTF-8 text).\n * Sync uploads this unmodified to S3; conversion happens at fetch time.\n */\nexport async function fetchLingohubResourceRaw(\n project: string,\n resource: LingohubResource,\n locale: string,\n): Promise<string> {\n const urlForResourceLocalised =\n `${apiUrl}${project}/resources/${resource.fileName}?auth_token=${cfg.authToken}`.replace(\n '[locale]',\n locale,\n );\n const res = await fetch(urlForResourceLocalised, { method: 'GET' });\n if (!res.ok) {\n throw new Error(`Failed to fetch resource \\`${resource.fileName}\\` (${locale}): ${res.status} - ${res.statusText}`);\n }\n return await res.text();\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 { fetchLingohubResourceRaw } from '../adapters/lingohub';\nimport { contentTypeForTranslationKey } from '../../shared/translationResource';\nimport { withRetry } from './retry';\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 resource: string;\n locale: string;\n /** UTF-8 byte size of the raw Lingohub file uploaded to S3. */\n itemCount: number;\n objectKey: string;\n}\n\nexport interface TranslationSyncResult {\n timestamp: number;\n entries: TranslationSyncResultEntry[];\n errors: Array<{ locale: string; project: string; resource: string; error: string }>;\n}\n\nexport function summariseCmsEntries(result: CmsSyncResult): string[] {\n return result.entries.map(\n (e) => `• \\`${e.contentType}\\` — ${e.itemCount} items`,\n );\n}\n\nexport function summariseCmsErrors(result: CmsSyncResult): string[] {\n return result.errors.map(\n (e) => `• \\`${e.contentType}\\` — ${e.error}`,\n );\n}\n\n/** Group translation sync entries into one summary line per project/resource. */\nexport function summariseTranslationEntries(entries: TranslationSyncResultEntry[]): string[] {\n const grouped = new Map<string, { locales: number; bytes: number }>();\n for (const e of entries) {\n const key = `${e.project} / ${e.resource}`;\n const existing = grouped.get(key);\n if (existing) {\n existing.locales += 1;\n existing.bytes += e.itemCount;\n } else {\n grouped.set(key, { locales: 1, bytes: e.itemCount });\n }\n }\n return [...grouped.entries()].map(\n ([key, { locales, bytes }]) => `• \\`${key}\\` / [${locales} locale${locales === 1 ? '' : 's'}] — ${bytes} bytes`,\n );\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; resource: 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 raw = await withRetry(\n () => fetchLingohubResourceRaw(project, resource, locale),\n config.retry,\n );\n const objectKey = buildTranslationObjectKey(\n project,\n resource.fileName,\n locale,\n );\n await store.uploadRaw(\n objectKey,\n raw,\n contentTypeForTranslationKey(objectKey),\n );\n const byteLength = Buffer.byteLength(raw, 'utf8');\n entries.push({\n project,\n resource: resource.resource,\n locale,\n itemCount: byteLength,\n objectKey,\n });\n\n console.log(\n ` + ${project} - ${locale}: ${byteLength} bytes -> ${objectKey}`,\n );\n }catch(err){\n const message = err instanceof Error ? err.message : String(err);\n errors.push({ project, locale, resource: resource.resource, error: message });\n console.error(` x ${project} / ${resource.resource} - ${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"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/shared/lingohub.ts"],"sourcesContent":["export const defaultLocales = ['en', 'fr', 'de', 'es', 'it', 'pt-br', 'ru', 'zh-hans', 'zh-hant', 'ko', 'ja'];\n\nconst localeMapping = {\n ios: {\n 'pt-br': 'pt',\n 'zh-hans': 'zh-Hans',\n 'zh-hant': 'zh-Hant',\n },\n android: {\n 'pt-br': 'pt',\n 'zh-hans': 'zh-Hans-CN',\n 'zh-hant': 'zh-TW',\n },\n};\n\nexport type LingohubResource = {\n resource: string,\n fileName: string;\n type: 'json'|'strings'|'xml';\n localeMapping?: Record<string, string>;\n}\n\nexport const allProjects: Record<string, LingohubResource[]> = {\n 'tandem-(new-website)': [\n {\n resource: 'main',\n fileName: 'Website.[locale].json',\n type: 'json'\n }\n ],\n 'tandem-(website)': [\n {\n resource: 'main',\n fileName: '[locale].json',\n type: 'json'\n },\n {\n resource: 'ai',\n fileName: 'AI.[locale].json',\n type: 'json'\n },\n {\n resource: 'languages',\n fileName: 'languages.[locale].json',\n type: 'json'\n }\n ],\n 'tandem': [\n {\n resource: 'infoplist',\n fileName: 'InfoPlist.[locale].strings',\n type: 'strings',\n localeMapping: localeMapping.ios,\n },\n {\n resource: 'localizable',\n fileName: 'Localizable.[locale].strings',\n type: 'strings',\n localeMapping: localeMapping.ios,\n },\n {\n resource: 'ipad',\n fileName: 'MainiPad.[locale].strings',\n type: 'strings',\n localeMapping: localeMapping.ios,\n },\n {\n resource: 'main',\n fileName: 'Main.[locale].strings',\n type: 'strings',\n localeMapping: localeMapping.ios,\n }\n ],\n 'tandem-(android)': [\n {\n resource: 'accessibility',\n fileName: 'accessibility_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'call',\n fileName: 'call_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'cert',\n fileName: 'cert_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'chat',\n fileName: 'chat_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'checklist',\n fileName: 'checklist_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'clubs',\n fileName: 'clubs_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'common',\n fileName: 'common_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'community',\n fileName: 'community_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'correction',\n fileName: 'correction_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'country_names',\n fileName: 'country_names.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'emoji',\n fileName: 'emoji_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'errors',\n fileName: 'errors.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'expressions',\n fileName: 'expressions_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'gif',\n fileName: 'gif_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'guidelines',\n fileName: 'guidelines_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'lanuguages',\n fileName: 'languages_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'localizable',\n fileName: 'localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'localizable2',\n fileName: 'localizable2.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'login',\n fileName: 'login_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'myprofile',\n fileName: 'myprofile_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'onb',\n fileName: 'onb_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'parties',\n fileName: 'parties_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'pro',\n fileName: 'pro_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'pro_screen',\n fileName: 'pro_screen_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'push_notification',\n fileName: 'push_notification_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'reporting',\n fileName: 'reporting_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n resource: 'translation',\n fileName: 'translation_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n }\n\n ],\n 'tandem-(web-invites)': [\n {\n resource: 'main',\n fileName: '[locale].json',\n type: 'json'\n }\n ]\n};\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/shared/config.ts"],"sourcesContent":["import dotenv from 'dotenv';\nimport type { S3Config, CMSProvider } from './types';\n\ndotenv.config({ path: '.env.local' });\ndotenv.config();\n\nexport type { CMSProvider, S3Config };\n\nexport interface SharedConfig {\n environment: string;\n s3: S3Config;\n}\n\nexport const config: SharedConfig = {\n environment: process.env.ENVIRONMENT || 'development',\n s3: {\n bucket: process.env.CONTENT_STORE_S3_BUCKET ?? '',\n region: process.env.CONTENT_STORE_S3_REGION ?? 'eu-central-1',\n accessKeyId: process.env.CONTENT_STORE_AWS_ACCESS_KEY ?? '',\n secretAccessKey: process.env.CONTENT_STORE_AWS_SECRET_ACCESS_KEY ?? '',\n }\n};\n"],"mappings":";;;AAAA,OAAO,YAAY;AAGnB,OAAO,OAAO,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,OAAO;AASP,IAAM,SAAuB;AAAA,EAChC,aAAa,QAAQ,IAAI,eAAe;AAAA,EACxC,IAAI;AAAA,IACA,QAAQ,QAAQ,IAAI,2BAA2B;AAAA,IAC/C,QAAQ,QAAQ,IAAI,2BAA2B;AAAA,IAC/C,aAAa,QAAQ,IAAI,gCAAgC;AAAA,IACzD,iBAAiB,QAAQ,IAAI,uCAAuC;AAAA,EACxE;AACJ;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/server/slack.ts"],"sourcesContent":["import { App } from '@slack/bolt';\nimport { config, type CMSProvider } from './config';\nimport { syncCmsContent, syncTranslations, summariseTranslationEntries, summariseCmsEntries, summariseCmsErrors } from './sync/engine';\nimport { allProjects } from '../shared/lingohub';\n\nconst VALID_CMS: CMSProvider[] = ['contentful', 'sanity'];\n\nlet app: App | null = null;\n\n/**\n * Post a message to the configured Slack notify channel.\n * Silently logs and returns if Slack is not configured or no channel is set.\n */\nexport async function notifySlack(text: string): Promise<void> {\n const { notifyChannel } = config.slack;\n if (!app || !notifyChannel) {\n console.log('[slack] Notification skipped (no app or channel configured)');\n return;\n }\n try {\n await app.client.chat.postMessage({ channel: notifyChannel, text: `[${config.environment}] ${text}` });\n } catch (err) {\n console.error('[slack] Failed to send notification:', err);\n }\n}\n\nexport async function startSlackBot(): Promise<void> {\n const { slack } = config;\n if (!slack.enabled) {\n console.log('[slack] Disabled (SLACK_BOT_TOKEN not set)');\n return;\n }\n\n app = new App({\n token: slack.botToken,\n signingSecret: slack.signingSecret,\n appToken: slack.appToken,\n socketMode: true,\n });\n\n app.command(slack.cmdSyncContent, async ({ command, ack, respond }) => {\n await ack();\n\n const args = command.text.trim().split(/\\s+/).filter(Boolean);\n const cms = (args[0] ?? 'contentful') as CMSProvider;\n const contentTypes = args.length > 1 ? args.slice(1) : undefined;\n\n if (!VALID_CMS.includes(cms)) {\n await respond({\n response_type: 'ephemeral',\n text: `Invalid CMS provider \\`${cms}\\`. Use \\`contentful\\` or \\`sanity\\`.`,\n });\n return;\n }\n\n const typesHint = contentTypes?.length ? ` (types: ${contentTypes.join(', ')})` : ' (all types)';\n await respond({\n response_type: 'in_channel',\n text: `:hourglass_flowing_sand: Sync started for *${cms}*${typesHint}…`,\n });\n\n try {\n const result = await syncCmsContent(cms, contentTypes);\n\n const blocks: string[] = [\n result.errors.length === 0\n ? `:white_check_mark: *Sync complete* — ${result.entries.length} content types synced`\n : `:warning: *Sync complete with errors* — ${result.entries.length} succeeded, ${result.errors.length} failed`,\n '',\n ...summariseCmsEntries(result),\n ];\n\n if (result.errors.length > 0) {\n blocks.push('', '*Errors:*', ...summariseCmsErrors(result));\n }\n\n await respond({ response_type: 'in_channel', text: blocks.join('\\n') });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n console.error('[slack] Sync failed:', message);\n await respond({\n response_type: 'ephemeral',\n text: `:x: Sync failed: ${message}`,\n });\n }\n });\n\n app.command(slack.cmdSyncTranslations, async ({ command, ack, respond }) => {\n await ack();\n\n const raw = command.text.trim();\n if (!raw) {\n await respond({\n response_type: 'ephemeral',\n text:\n 'Usage: `/sync-translations <comma-separated projects> [<comma-separated locales>]`\\n' +\n 'Example: `/sync-translations tandem,tandem-(website) en,de,fr`\\n' +\n 'Omit locales to use the default locale list.',\n });\n return;\n }\n\n const spaceIdx = raw.indexOf(' ');\n const projectsPart = spaceIdx === -1 ? raw : raw.slice(0, spaceIdx).trim();\n const localesPart = spaceIdx === -1 ? '' : raw.slice(spaceIdx + 1).trim();\n\n const projects = projectsPart\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n if (projects.length === 0) {\n await respond({\n response_type: 'ephemeral',\n text:\n 'No valid projects. Pass a comma-separated list as the first argument, e.g. `tandem,tandem-(website)`.',\n });\n return;\n }\n\n let locales: string[] | undefined = localesPart.length\n ? localesPart\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n : undefined;\n if (locales && locales.length === 0) {\n locales = undefined;\n }\n\n const localesHint =\n locales?.length ? ` — locales: ${locales?.join(', ')}` : ' — default locales';\n await respond({\n response_type: 'in_channel',\n text: `:hourglass_flowing_sand: Translation sync started for projects: *${projects.join(', ')}*${localesHint}…`,\n });\n\n try {\n const result = await syncTranslations(projects, locales);\n\n const lines = summariseTranslationEntries(result.entries);\n const errorLines = result.errors.map(\n (e) =>\n `• \\`${e.project}\\` / \\`${e.resource}\\` / \\`${e.locale}\\` — ${e.error}`,\n );\n\n const blocks: string[] = [\n result.errors.length === 0\n ? `:white_check_mark: *Translation sync complete* — ${result.entries.length} resource × locale uploads`\n : `:warning: *Translation sync complete with errors* — ${result.entries.length} succeeded, ${result.errors.length} failed`,\n '',\n ...lines,\n ];\n\n if (errorLines.length > 0) {\n blocks.push('', '*Errors:*', ...errorLines);\n }\n\n await respond({ response_type: 'in_channel', text: blocks.join('\\n') });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n console.error('[slack] Sync failed:', message);\n await respond({\n response_type: 'ephemeral',\n text: `:x: Sync failed: ${message}`,\n });\n }\n });\n\n app.command('/list-projects', async ({ ack }) => {\n const projectNames = Object.keys(allProjects);\n const lines = projectNames.map((name, i) => `${i + 1}. ${name}`);\n await ack({\n response_type: 'ephemeral',\n text: `*Available Lingohub projects:*\\n${lines.join('\\n')}`,\n });\n });\n\n app.command('/list-resources', async ({ command, ack }) => {\n const project = command.text.trim();\n if (!project) {\n const projectNames = Object.keys(allProjects);\n const lines = projectNames.map((name, i) => `${i + 1}. ${name}`);\n await ack({\n response_type: 'ephemeral',\n text: `Usage: \\`/list-resources <project-name>\\`\\n\\n*Available projects:*\\n${lines.join('\\n')}`,\n });\n return;\n }\n\n const resources = allProjects[project];\n if (!resources) {\n const available = Object.keys(allProjects).join(', ');\n await ack({\n response_type: 'ephemeral',\n text: `Unknown project \\`${project}\\`.\\nAvailable projects: ${available}`,\n });\n return;\n }\n\n const lines = resources.map(\n (r, i) => `${i + 1}. \\`${r.resource}\\` (${r.fileName})`,\n );\n await ack({\n response_type: 'ephemeral',\n text: `*Resources for \"${project}\":*\\n${lines.join('\\n')}`,\n });\n });\n\n await app.start();\n console.log('[slack] Bot connected via Socket Mode');\n}\n"],"mappings":"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","names":["lines"]}