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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +81 -13
  2. package/dist/chunk-4DE47ZJD.js +19 -0
  3. package/dist/chunk-4DE47ZJD.js.map +1 -0
  4. package/dist/{chunk-YZSLCPN6.js → chunk-6FXNAJSI.js} +160 -14
  5. package/dist/chunk-6FXNAJSI.js.map +1 -0
  6. package/dist/{chunk-UCBZUEUP.js → chunk-BAJT7RMQ.js} +31 -69
  7. package/dist/chunk-BAJT7RMQ.js.map +1 -0
  8. package/dist/{chunk-XP3USUQC.js → chunk-EQ3DSPTJ.js} +6 -2
  9. package/dist/{chunk-XP3USUQC.js.map → chunk-EQ3DSPTJ.js.map} +1 -1
  10. package/dist/chunk-UPIQFNCR.js +17 -0
  11. package/dist/chunk-UPIQFNCR.js.map +1 -0
  12. package/dist/{chunk-VRWRAFDK.js → chunk-VKXN2SE6.js} +79 -24
  13. package/dist/chunk-VKXN2SE6.js.map +1 -0
  14. package/dist/client/fetch-content-bundles.js +7 -4
  15. package/dist/client/fetch-content-bundles.js.map +1 -1
  16. package/dist/client/fetch-merged-translation-bundles.js +46 -0
  17. package/dist/client/fetch-merged-translation-bundles.js.map +1 -0
  18. package/dist/client/fetch-translation-bundles.js +22 -11
  19. package/dist/client/fetch-translation-bundles.js.map +1 -1
  20. package/dist/client/query-cms.js +33 -0
  21. package/dist/client/query-cms.js.map +1 -0
  22. package/dist/{index-Db97SUTy.d.ts → index-PQ7XN47c.d.ts} +36 -7
  23. package/dist/index.d.ts +1 -1
  24. package/dist/node.browser.js +7 -0
  25. package/dist/node.browser.js.map +1 -1
  26. package/dist/node.d.ts +9 -3
  27. package/dist/node.js +237 -15
  28. package/dist/node.js.map +1 -1
  29. package/package.json +8 -5
  30. package/dist/chunk-UCBZUEUP.js.map +0 -1
  31. package/dist/chunk-VRWRAFDK.js.map +0 -1
  32. package/dist/chunk-YZSLCPN6.js.map +0 -1
  33. package/dist/client/cli.js +0 -82
  34. package/dist/client/cli.js.map +0 -1
package/README.md CHANGED
@@ -7,7 +7,7 @@ For the Express API, server CLI, scheduling, and deployment, see the [Server & C
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`, `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.).
10
+ - **`@tandem-language-exchange/content-store/node`** — `ContentStoreSDK`, `fetchCmsBundles`, `fetchTranslationBundles`, `fetchMergedTranslationBundles`, `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
 
@@ -107,20 +107,23 @@ Files are written to `outputDir` with the naming pattern `{cms}-{contentType}.js
107
107
 
108
108
  ## `fetchTranslationBundles(options)`
109
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).
110
+ Downloads translation objects from S3. The **sync** stores each Lingohub file **verbatim** (raw UTF-8); this call **parses** each file (JSON / `.strings` / Android XML per `src/shared/lingohub.ts`) and writes **normalized JSON** under `outputDir` (see file naming below).
111
111
 
112
112
  ```typescript
113
113
  const files = await sdk.fetchTranslationBundles({
114
- projects: ['tandem', 'tandem-(website)'],
114
+ projects: {
115
+ 'tandem': [], // all resources
116
+ 'tandem-(website)': ['main', 'ai'], // only "main" and "ai" resources
117
+ },
115
118
  locales: ['en', 'de'], // omit or leave empty to use the package default locale list
116
119
  });
117
120
  ```
118
121
 
119
- **Parameters:**
122
+ **Parameters (extends `TranslationFilterConfig`):**
120
123
 
121
124
  | Field | Type | Description |
122
125
  | --- | --- | --- |
123
- | `projects` | `string[]` | Lingohub project ids to fetch (must match keys configured in the package, e.g. `tandem`, `tandem-(android)`). |
126
+ | `projects` | `Record<string, string[]>` | Map of Lingohub project id to resource keys. An empty array fetches **all** resources for that project; a populated array fetches only the listed resources (matched against the `resource` field in `src/shared/lingohub.ts`). |
124
127
  | `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
128
  | `retry` | `S3RetryConfig` | Optional. Overrides [S3 download retries](#s3-download-retries). |
126
129
 
@@ -135,15 +138,34 @@ const files = await sdk.fetchTranslationBundles({
135
138
  }
136
139
  ```
137
140
 
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.
141
+ **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). On disk, non-`.json` keys gain a trailing `.json` (e.g. `…en.strings` `…en.strings.json`) containing the parsed structure as JSON.
139
142
 
140
143
  Downloads are run **in parallel** (per project); [S3 download retries](#s3-download-retries) apply by default.
141
144
 
142
145
  ---
143
146
 
147
+ ## `fetchMergedTranslationBundles(options)`
148
+
149
+ Same **`projects`**, **`locales`**, and **`retry`** as `fetchTranslationBundles`. Downloads and parses every matching resource, flattens each file to string key/value pairs, then **merges** all pairs per **catalog locale** into a single file **`{locale}.json`** in `outputDir` (e.g. `en.json`). Duplicate keys across resources or projects: **last wins** (order: projects → resources → locales loop).
150
+
151
+ **Returns:** `Record<string, string>` — locale code → absolute path of the merged file.
152
+
153
+ ```typescript
154
+ const mergedPaths = await sdk.fetchMergedTranslationBundles({
155
+ projects: {
156
+ 'tandem-(new-website)': [],
157
+ 'tandem-(website)': ['main'],
158
+ },
159
+ locales: ['en'],
160
+ });
161
+ // mergedPaths.en → path to one big en.json
162
+ ```
163
+
164
+ ---
165
+
144
166
  ## S3 download retries
145
167
 
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).
168
+ `fetchCmsBundles`, `fetchTranslationBundles`, and `fetchMergedTranslationBundles` 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
169
 
148
170
  Default limits are read from the environment (highest precedence first):
149
171
 
@@ -294,6 +316,7 @@ The same operations are available as standalone imports (no `ContentStoreSDK` wr
294
316
  import {
295
317
  fetchCmsBundles,
296
318
  fetchTranslationBundles,
319
+ fetchMergedTranslationBundles,
297
320
  queryCmsBundle,
298
321
  getDefaultS3RetryConfig,
299
322
  ContentStore,
@@ -312,11 +335,16 @@ await fetchCmsBundles(store, './content-cache', {
312
335
  });
313
336
 
314
337
  await fetchTranslationBundles(store, './content-cache', {
315
- projects: ['tandem-(website)'],
338
+ projects: { 'tandem-(website)': [] },
316
339
  locales: ['en', 'fr'],
317
340
  retry: getDefaultS3RetryConfig(),
318
341
  });
319
342
 
343
+ await fetchMergedTranslationBundles(store, './content-cache/merged', {
344
+ projects: { 'tandem-(new-website)': [], 'tandem-(website)': ['main'] },
345
+ locales: ['en'],
346
+ });
347
+
320
348
  const results = await queryCmsBundle('./content-cache', 'contentful', 'gridLayout', {
321
349
  fields: { columns: '2' },
322
350
  limit: 5,
@@ -346,7 +374,7 @@ await sdk.fetchCmsBundles({
346
374
 
347
375
  // Optional: pull translation bundles written by the Lingohub → S3 sync
348
376
  await sdk.fetchTranslationBundles({
349
- projects: ['tandem-(website)'],
377
+ projects: { 'tandem-(website)': [] },
350
378
  });
351
379
 
352
380
  // 2. Query CMS bundle locally — no further network calls
@@ -361,7 +389,7 @@ console.log(grids);
361
389
  ```
362
390
  ## CLI
363
391
 
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).
392
+ The package ships **`fetch-content-bundles`**, **`fetch-translation-bundles`**, and **`fetch-merged-translation-bundles`** as **`bin`** commands. The **`query-cms`** command lives at **`dist/client/query-cms.js`**; run with **`node node_modules/@tandem-language-exchange/content-store/dist/client/query-cms.js`** or via npm scripts.
365
393
 
366
394
  ### `fetch-content-bundles` — download bundles from S3
367
395
 
@@ -387,14 +415,54 @@ Files are written as `{cms}-{contentType}.json` inside the output directory.
387
415
  }
388
416
  ```
389
417
 
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)).
418
+ All **`bin`** tools read S3 settings from [S3 config via environment variables](#s3-config-via-environment-variables).
419
+
420
+ ### `fetch-translation-bundles`
421
+
422
+ Both translation CLI commands accept a **`--config`** flag pointing to a JSON config file that defines which projects, resources, and locales to fetch.
423
+
424
+ **Config file format (`TranslationFilterConfig`):**
425
+
426
+ ```json
427
+ {
428
+ "projects": {
429
+ "tandem": [],
430
+ "tandem-(website)": ["main", "ai"]
431
+ },
432
+ "locales": ["en", "de", "it"]
433
+ }
434
+ ```
435
+
436
+ - **`projects`** — map of Lingohub project id to resource keys. An empty array (`[]`) fetches **all** resources for that project. A populated array fetches only the listed resources (matched against the `resource` field in `src/shared/lingohub.ts`).
437
+ - **`locales`** — optional. Omit or leave empty to use the built-in default locale list.
438
+
439
+ ```bash
440
+ npx fetch-translation-bundles --config ./translation-config.json --output ./content-cache
441
+ ```
442
+
443
+ | Flag | Required | Default | Description |
444
+ | --- | --- | --- | --- |
445
+ | `--config <path>` | Yes | | Path to a JSON config file (`TranslationFilterConfig`) |
446
+ | `--output <dir>` | No | `./content-cache` | Output directory |
447
+
448
+ ### `fetch-merged-translation-bundles`
449
+
450
+ Writes merged **`{locale}.json`** files (string key/value map; duplicate keys: last wins).
451
+
452
+ Uses the same config file format as `fetch-translation-bundles`.
453
+
454
+ ```bash
455
+ npx fetch-merged-translation-bundles --config ./translation-config.json --output ./content-cache/merged
456
+ ```
457
+
458
+ Alternatively call **`fetchTranslationBundles`** / **`fetchMergedTranslationBundles`** from a Node script, or use the server’s **`POST /getTranslationBundles`** API (see [Server & CLI README](src/server/README.md)).
391
459
 
392
460
  ### `query-cms` — query a local bundle
393
461
 
394
462
  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):
395
463
 
396
464
  ```bash
397
- node node_modules/@tandem-language-exchange/content-store/dist/client/cli.js query-cms \
465
+ node node_modules/@tandem-language-exchange/content-store/dist/client/query-cms.js \
398
466
  --cms contentful --type gridLayout \
399
467
  --fields '{"columns":"2"}' \
400
468
  --select title,bodyBefore \
@@ -406,7 +474,7 @@ node node_modules/@tandem-language-exchange/content-store/dist/client/cli.js que
406
474
 
407
475
  ```json
408
476
  "scripts": {
409
- "query:cms": "node ./node_modules/@tandem-language-exchange/content-store/dist/client/cli.js query-cms"
477
+ "query:cms": "node ./node_modules/@tandem-language-exchange/content-store/dist/client/query-cms.js"
410
478
  }
411
479
  ```
412
480
 
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/shared/config.ts
4
+ import dotenv from "dotenv";
5
+ dotenv.config({ path: ".env.local" });
6
+ dotenv.config();
7
+ var config = {
8
+ s3: {
9
+ bucket: process.env.CONTENT_STORE_S3_BUCKET ?? "",
10
+ region: process.env.CONTENT_STORE_S3_REGION ?? "eu-central-1",
11
+ accessKeyId: process.env.AWS_ACCESS_KEY ?? "",
12
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? ""
13
+ }
14
+ };
15
+
16
+ export {
17
+ config
18
+ };
19
+ //# sourceMappingURL=chunk-4DE47ZJD.js.map
@@ -0,0 +1 @@
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 s3: S3Config;\n}\n\nexport const config: SharedConfig = {\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.AWS_ACCESS_KEY ?? '',\n secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? '',\n }\n};\n"],"mappings":";;;AAAA,OAAO,YAAY;AAGnB,OAAO,OAAO,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,OAAO;AAQP,IAAM,SAAuB;AAAA,EAChC,IAAI;AAAA,IACA,QAAQ,QAAQ,IAAI,2BAA2B;AAAA,IAC/C,QAAQ,QAAQ,IAAI,2BAA2B;AAAA,IAC/C,aAAa,QAAQ,IAAI,kBAAkB;AAAA,IAC3C,iBAAiB,QAAQ,IAAI,yBAAyB;AAAA,EAC1D;AACJ;","names":[]}
@@ -30,6 +30,18 @@ var ContentStore = class {
30
30
  );
31
31
  return key;
32
32
  }
33
+ /** Raw UTF-8 body (e.g. Lingohub file bytes as text). */
34
+ async uploadRaw(key, body, contentType) {
35
+ await this.client.send(
36
+ new PutObjectCommand({
37
+ Bucket: this.bucket,
38
+ Key: key,
39
+ Body: body,
40
+ ContentType: contentType
41
+ })
42
+ );
43
+ return key;
44
+ }
33
45
  async download(key) {
34
46
  const response = await this.client.send(
35
47
  new GetObjectCommand({ Bucket: this.bucket, Key: key })
@@ -38,6 +50,15 @@ var ContentStore = class {
38
50
  if (!body) throw new Error(`Empty response for key: ${key}`);
39
51
  return JSON.parse(body);
40
52
  }
53
+ /** Raw UTF-8 body from S3 (no JSON.parse). */
54
+ async downloadRaw(key) {
55
+ const response = await this.client.send(
56
+ new GetObjectCommand({ Bucket: this.bucket, Key: key })
57
+ );
58
+ const body = await response.Body?.transformToString();
59
+ if (!body) throw new Error(`Empty response for key: ${key}`);
60
+ return body;
61
+ }
41
62
  };
42
63
  var buildCmsObjectKey = (cms, contentType) => {
43
64
  return `${cms}-${contentType}.json`;
@@ -63,41 +84,49 @@ var localeMapping = {
63
84
  var allProjects = {
64
85
  "tandem-(new-website)": [
65
86
  {
87
+ resource: "main",
66
88
  fileName: "Website.[locale].json",
67
89
  type: "json"
68
90
  }
69
91
  ],
70
92
  "tandem-(website)": [
71
93
  {
94
+ resource: "main",
72
95
  fileName: "[locale].json",
73
96
  type: "json"
74
97
  },
75
98
  {
99
+ resource: "ai",
76
100
  fileName: "AI.[locale].json",
77
101
  type: "json"
78
102
  },
79
103
  {
104
+ resource: "languages",
80
105
  fileName: "languages.[locale].json",
81
106
  type: "json"
82
107
  }
83
108
  ],
84
109
  "tandem": [
85
110
  {
111
+ resource: "infoplist",
86
112
  fileName: "InfoPlist.[locale].strings",
87
113
  type: "strings",
88
114
  localeMapping: localeMapping.ios
89
115
  },
90
116
  {
117
+ resource: "localizable",
91
118
  fileName: "Localizable.[locale].strings",
92
119
  type: "strings",
93
120
  localeMapping: localeMapping.ios
94
121
  },
95
122
  {
123
+ resource: "ipad",
96
124
  fileName: "MainiPad.[locale].strings",
97
125
  type: "strings",
98
126
  localeMapping: localeMapping.ios
99
127
  },
100
128
  {
129
+ resource: "main",
101
130
  fileName: "Main.[locale].strings",
102
131
  type: "strings",
103
132
  localeMapping: localeMapping.ios
@@ -105,136 +134,163 @@ var allProjects = {
105
134
  ],
106
135
  "tandem-(android)": [
107
136
  {
137
+ resource: "accessibility",
108
138
  fileName: "accessibility_localizable.[locale].xml",
109
139
  type: "xml",
110
140
  localeMapping: localeMapping.android
111
141
  },
112
142
  {
143
+ resource: "call",
113
144
  fileName: "call_localizable.[locale].xml",
114
145
  type: "xml",
115
146
  localeMapping: localeMapping.android
116
147
  },
117
148
  {
149
+ resource: "cert",
118
150
  fileName: "cert_localizable.[locale].xml",
119
151
  type: "xml",
120
152
  localeMapping: localeMapping.android
121
153
  },
122
154
  {
155
+ resource: "chat",
123
156
  fileName: "chat_localizable.[locale].xml",
124
157
  type: "xml",
125
158
  localeMapping: localeMapping.android
126
159
  },
127
160
  {
161
+ resource: "checklist",
128
162
  fileName: "checklist_localizable.[locale].xml",
129
163
  type: "xml",
130
164
  localeMapping: localeMapping.android
131
165
  },
132
166
  {
167
+ resource: "clubs",
133
168
  fileName: "clubs_localizable.[locale].xml",
134
169
  type: "xml",
135
170
  localeMapping: localeMapping.android
136
171
  },
137
172
  {
173
+ resource: "common",
138
174
  fileName: "common_localizable.[locale].xml",
139
175
  type: "xml",
140
176
  localeMapping: localeMapping.android
141
177
  },
142
178
  {
179
+ resource: "community",
143
180
  fileName: "community_localizable.[locale].xml",
144
181
  type: "xml",
145
182
  localeMapping: localeMapping.android
146
183
  },
147
184
  {
185
+ resource: "correction",
148
186
  fileName: "correction_localizable.[locale].xml",
149
187
  type: "xml",
150
188
  localeMapping: localeMapping.android
151
189
  },
152
190
  {
191
+ resource: "country_names",
153
192
  fileName: "country_names.[locale].xml",
154
193
  type: "xml",
155
194
  localeMapping: localeMapping.android
156
195
  },
157
196
  {
197
+ resource: "emoji",
158
198
  fileName: "emoji_localizable.[locale].xml",
159
199
  type: "xml",
160
200
  localeMapping: localeMapping.android
161
201
  },
162
202
  {
203
+ resource: "errors",
163
204
  fileName: "errors_localizable.[locale].xml",
164
205
  type: "xml",
165
206
  localeMapping: localeMapping.android
166
207
  },
167
208
  {
209
+ resource: "expressions",
168
210
  fileName: "expressions_localizable.[locale].xml",
169
211
  type: "xml",
170
212
  localeMapping: localeMapping.android
171
213
  },
172
214
  {
215
+ resource: "gif",
173
216
  fileName: "gif_localizable.[locale].xml",
174
217
  type: "xml",
175
218
  localeMapping: localeMapping.android
176
219
  },
177
220
  {
221
+ resource: "guidelines",
178
222
  fileName: "guidelines_localizable.[locale].xml",
179
223
  type: "xml",
180
224
  localeMapping: localeMapping.android
181
225
  },
182
226
  {
227
+ resource: "lanuguages",
183
228
  fileName: "languages_localizable.[locale].xml",
184
229
  type: "xml",
185
230
  localeMapping: localeMapping.android
186
231
  },
187
232
  {
233
+ resource: "localizable",
188
234
  fileName: "localizable.[locale].xml",
189
235
  type: "xml",
190
236
  localeMapping: localeMapping.android
191
237
  },
192
238
  {
239
+ resource: "localizable2",
193
240
  fileName: "localizable2.[locale].xml",
194
241
  type: "xml",
195
242
  localeMapping: localeMapping.android
196
243
  },
197
244
  {
245
+ resource: "login",
198
246
  fileName: "login_localizable.[locale].xml",
199
247
  type: "xml",
200
248
  localeMapping: localeMapping.android
201
249
  },
202
250
  {
251
+ resource: "myprofile",
203
252
  fileName: "myprofile_localizable.[locale].xml",
204
253
  type: "xml",
205
254
  localeMapping: localeMapping.android
206
255
  },
207
256
  {
257
+ resource: "onb",
208
258
  fileName: "onb_localizable.[locale].xml",
209
259
  type: "xml",
210
260
  localeMapping: localeMapping.android
211
261
  },
212
262
  {
263
+ resource: "parties",
213
264
  fileName: "parties_localizable.[locale].xml",
214
265
  type: "xml",
215
266
  localeMapping: localeMapping.android
216
267
  },
217
268
  {
269
+ resource: "pro",
218
270
  fileName: "pro_localizable.[locale].xml",
219
271
  type: "xml",
220
272
  localeMapping: localeMapping.android
221
273
  },
222
274
  {
275
+ resource: "pro_screen",
223
276
  fileName: "pro_screen_localizable.[locale].xml",
224
277
  type: "xml",
225
278
  localeMapping: localeMapping.android
226
279
  },
227
280
  {
281
+ resource: "push_notification",
228
282
  fileName: "push_notification_localizable.[locale].xml",
229
283
  type: "xml",
230
284
  localeMapping: localeMapping.android
231
285
  },
232
286
  {
287
+ resource: "reporting",
233
288
  fileName: "reporting_localizable.[locale].xml",
234
289
  type: "xml",
235
290
  localeMapping: localeMapping.android
236
291
  },
237
292
  {
293
+ resource: "translation",
238
294
  fileName: "translation_localizable.[locale].xml",
239
295
  type: "xml",
240
296
  localeMapping: localeMapping.android
@@ -242,31 +298,121 @@ var allProjects = {
242
298
  ],
243
299
  "tandem-(web-invites)": [
244
300
  {
301
+ resource: "main",
245
302
  fileName: "[locale].json",
246
303
  type: "json"
247
304
  }
248
305
  ]
249
306
  };
250
307
 
251
- // src/shared/config.ts
252
- import dotenv from "dotenv";
253
- dotenv.config({ path: ".env.local" });
254
- dotenv.config();
255
- var config = {
256
- s3: {
257
- bucket: process.env.CONTENT_STORE_S3_BUCKET ?? "",
258
- region: process.env.CONTENT_STORE_S3_REGION ?? "eu-central-1",
259
- accessKeyId: process.env.AWS_ACCESS_KEY ?? "",
260
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? ""
261
- }
308
+ // src/shared/translationResource.ts
309
+ import path from "path";
310
+
311
+ // src/shared/utils.ts
312
+ import convert from "xml-js";
313
+ import set from "lodash.set";
314
+ import merge from "lodash.merge";
315
+ var transformObjectToFlat = (data) => {
316
+ const result = {};
317
+ const flatten = (obj, path2 = []) => {
318
+ Object.entries(obj).forEach(([key, value]) => {
319
+ if (typeof value === "object") {
320
+ flatten(value, path2.concat(key));
321
+ } else {
322
+ result[path2.concat(key).join(".")] = value;
323
+ }
324
+ });
325
+ };
326
+ flatten(data);
327
+ return result;
328
+ };
329
+ var convertXMLToJS = (xml) => {
330
+ const converted = convert.xml2js(xml, {
331
+ ignoreComment: true,
332
+ ignoreDeclaration: true,
333
+ ignoreInstruction: true,
334
+ compact: true
335
+ });
336
+ let mapped = {};
337
+ converted.resources.string.forEach((item) => {
338
+ mapped = {
339
+ ...mapped,
340
+ [item._attributes.name]: item._text
341
+ };
342
+ });
343
+ return mapped;
262
344
  };
345
+ var parseIOSStrings = (strings) => {
346
+ const parsedObj = {};
347
+ strings.split("\n").filter((line) => line.startsWith('"') && line.endsWith(";")).map((line) => line.trim().slice(0, -1)).forEach((line) => {
348
+ let [key, value] = line.split(" = ");
349
+ if (!key || !value) return;
350
+ key = key.slice(1, -1);
351
+ value = value.slice(1, -1);
352
+ parsedObj[key] = value;
353
+ });
354
+ return parsedObj;
355
+ };
356
+
357
+ // src/shared/translationResource.ts
358
+ function contentTypeForTranslationKey(objectKey) {
359
+ if (objectKey.endsWith(".json")) return "application/json; charset=utf-8";
360
+ if (objectKey.endsWith(".xml")) return "application/xml; charset=utf-8";
361
+ if (objectKey.endsWith(".strings")) return "text/plain; charset=utf-8";
362
+ return "application/octet-stream";
363
+ }
364
+ function parseTranslationResourceRaw(raw, resource) {
365
+ if (resource.type === "json") {
366
+ return JSON.parse(raw);
367
+ }
368
+ if (resource.type === "strings") {
369
+ return parseIOSStrings(raw);
370
+ }
371
+ if (resource.type === "xml") {
372
+ return convertXMLToJS(raw);
373
+ }
374
+ throw new Error(`Unsupported resource type: ${resource.type}`);
375
+ }
376
+ function toFlatStringMap(parsed) {
377
+ if (parsed === null || parsed === void 0) return {};
378
+ if (typeof parsed === "string") return { value: parsed };
379
+ if (typeof parsed !== "object") return { value: String(parsed) };
380
+ if (Array.isArray(parsed)) {
381
+ const out2 = {};
382
+ parsed.forEach((v, i) => {
383
+ out2[String(i)] = typeof v === "object" && v !== null ? JSON.stringify(v) : String(v);
384
+ });
385
+ return out2;
386
+ }
387
+ const flat = transformObjectToFlat(parsed);
388
+ const out = {};
389
+ for (const [k, v] of Object.entries(flat)) {
390
+ if (v === null || v === void 0) {
391
+ out[k] = "";
392
+ } else if (typeof v === "object") {
393
+ out[k] = JSON.stringify(v);
394
+ } else {
395
+ out[k] = String(v);
396
+ }
397
+ }
398
+ return out;
399
+ }
400
+ function translationJsonOutputPath(outputDir, objectKey) {
401
+ if (objectKey.endsWith(".json")) {
402
+ return path.resolve(outputDir, objectKey);
403
+ }
404
+ return path.resolve(outputDir, `${objectKey}.json`);
405
+ }
263
406
 
264
407
  export {
265
- config,
266
408
  ContentStore,
267
409
  buildCmsObjectKey,
268
410
  buildTranslationObjectKey,
269
411
  defaultLocales,
270
- allProjects
412
+ allProjects,
413
+ contentTypeForTranslationKey,
414
+ parseTranslationResourceRaw,
415
+ toFlatStringMap,
416
+ translationJsonOutputPath
271
417
  };
272
- //# sourceMappingURL=chunk-YZSLCPN6.js.map
418
+ //# sourceMappingURL=chunk-6FXNAJSI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/s3.ts","../src/shared/lingohub.ts","../src/shared/translationResource.ts","../src/shared/utils.ts"],"sourcesContent":["import {\n S3Client,\n PutObjectCommand,\n GetObjectCommand,\n} from '@aws-sdk/client-s3';\nimport type { S3Config } from './types';\n\nexport class ContentStore {\n private client: S3Client;\n private bucket: string;\n\n constructor(cfg: S3Config) {\n this.client = new S3Client({\n region: cfg.region,\n credentials: {\n accessKeyId: cfg.accessKeyId,\n secretAccessKey: cfg.secretAccessKey,\n },\n });\n this.bucket = cfg.bucket;\n }\n\n async upload(key: string, data: unknown): Promise<string> {\n await this.client.send(\n new PutObjectCommand({\n Bucket: this.bucket,\n Key: key,\n Body: JSON.stringify(data, null, 2),\n ContentType: 'application/json',\n }),\n );\n return key;\n }\n\n /** Raw UTF-8 body (e.g. Lingohub file bytes as text). */\n async uploadRaw(\n key: string,\n body: string,\n contentType: string,\n ): Promise<string> {\n await this.client.send(\n new PutObjectCommand({\n Bucket: this.bucket,\n Key: key,\n Body: body,\n ContentType: contentType,\n }),\n );\n return key;\n }\n\n async download(key: string): Promise<unknown> {\n const response = await this.client.send(\n new GetObjectCommand({ Bucket: this.bucket, Key: key }),\n );\n const body = await response.Body?.transformToString();\n if (!body) throw new Error(`Empty response for key: ${key}`);\n return JSON.parse(body);\n }\n\n /** Raw UTF-8 body from S3 (no JSON.parse). */\n async downloadRaw(key: string): Promise<string> {\n const response = await this.client.send(\n new GetObjectCommand({ Bucket: this.bucket, Key: key }),\n );\n const body = await response.Body?.transformToString();\n if (!body) throw new Error(`Empty response for key: ${key}`);\n return body;\n }\n}\n\n/** {cms}-{contentType}.json (always points at the latest version) */\nexport const buildCmsObjectKey = (cms: string, contentType: string): string => {\n return `${cms}-${contentType}.json`;\n}\n\nexport const buildTranslationObjectKey = (project:string, fileName: string, locale: string): string => {\n return `lingohub-${project}.${fileName.replaceAll('[locale]', locale)}`;\n}\n","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_localizable.[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","import path from 'node:path';\nimport { convertXMLToJS, parseIOSStrings, transformObjectToFlat } from './utils';\nimport type { LingohubResource } from './lingohub';\n\n/** Content-Type for raw Lingohub bodies stored in S3. */\nexport function contentTypeForTranslationKey(objectKey: string): string {\n if (objectKey.endsWith('.json')) return 'application/json; charset=utf-8';\n if (objectKey.endsWith('.xml')) return 'application/xml; charset=utf-8';\n if (objectKey.endsWith('.strings')) return 'text/plain; charset=utf-8';\n return 'application/octet-stream';\n}\n\n/**\n * Parses a raw Lingohub file body from S3 into structured data.\n */\nexport function parseTranslationResourceRaw(\n raw: string,\n resource: LingohubResource,\n): unknown {\n if (resource.type === 'json') {\n return JSON.parse(raw) as unknown;\n }\n\n if (resource.type === 'strings') {\n return parseIOSStrings(raw);\n }\n\n if (resource.type === 'xml') {\n return convertXMLToJS(raw);\n }\n\n throw new Error(`Unsupported resource type: ${resource.type}`);\n}\n\n/**\n * Normalizes parsed translation data to a flat string map for merging (duplicate keys: last wins).\n */\nexport function toFlatStringMap(parsed: unknown): Record<string, string> {\n if (parsed === null || parsed === undefined) return {};\n if (typeof parsed === 'string') return { value: parsed };\n if (typeof parsed !== 'object') return { value: String(parsed) };\n if (Array.isArray(parsed)) {\n const out: Record<string, string> = {};\n parsed.forEach((v, i) => {\n out[String(i)] =\n typeof v === 'object' && v !== null ? JSON.stringify(v) : String(v);\n });\n return out;\n }\n\n const flat = transformObjectToFlat(parsed as Record<string, unknown>);\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(flat)) {\n if (v === null || v === undefined) {\n out[k] = '';\n } else if (typeof v === 'object') {\n out[k] = JSON.stringify(v);\n } else {\n out[k] = String(v);\n }\n }\n return out;\n}\n\n/** Where to write normalized JSON for one translation object key (avoids `.json.json`). */\nexport function translationJsonOutputPath(\n outputDir: string,\n objectKey: string,\n): string {\n if (objectKey.endsWith('.json')) {\n return path.resolve(outputDir, objectKey);\n }\n return path.resolve(outputDir, `${objectKey}.json`);\n}\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};"],"mappings":"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","names":["path","out"]}