@tandem-language-exchange/content-store 1.2.7 → 1.2.9

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.
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ config,
4
+ summariseCmsEntries,
5
+ summariseCmsErrors,
6
+ summariseTranslationEntries,
7
+ syncCmsContent,
8
+ syncTranslations
9
+ } from "./chunk-PSTICAT4.js";
10
+ import {
11
+ allProjects
12
+ } from "./chunk-SF7FCBR2.js";
13
+
14
+ // src/server/slack.ts
15
+ import { App } from "@slack/bolt";
16
+ var VALID_CMS = ["contentful", "sanity"];
17
+ var app = null;
18
+ async function notifySlack(text) {
19
+ const { notifyChannel } = config.slack;
20
+ if (!app || !notifyChannel) {
21
+ console.log("[slack] Notification skipped (no app or channel configured)");
22
+ return;
23
+ }
24
+ try {
25
+ await app.client.chat.postMessage({ channel: notifyChannel, text: `[${config.environment}] ${text}` });
26
+ } catch (err) {
27
+ console.error("[slack] Failed to send notification:", err);
28
+ }
29
+ }
30
+ async function startSlackBot() {
31
+ const { slack } = config;
32
+ if (!slack.enabled) {
33
+ console.log("[slack] Disabled (SLACK_BOT_TOKEN not set)");
34
+ return;
35
+ }
36
+ app = new App({
37
+ token: slack.botToken,
38
+ signingSecret: slack.signingSecret,
39
+ appToken: slack.appToken,
40
+ socketMode: true
41
+ });
42
+ app.command(slack.cmdSyncContent, async ({ command, ack, respond }) => {
43
+ await ack();
44
+ const args = command.text.trim().split(/\s+/).filter(Boolean);
45
+ const cms = args[0] ?? "contentful";
46
+ const contentTypes = args.length > 1 ? args.slice(1) : void 0;
47
+ if (!VALID_CMS.includes(cms)) {
48
+ await respond({
49
+ response_type: "ephemeral",
50
+ text: `Invalid CMS provider \`${cms}\`. Use \`contentful\` or \`sanity\`.`
51
+ });
52
+ return;
53
+ }
54
+ const typesHint = contentTypes?.length ? ` (types: ${contentTypes.join(", ")})` : " (all types)";
55
+ await respond({
56
+ response_type: "in_channel",
57
+ text: `:hourglass_flowing_sand: Sync started for *${cms}*${typesHint}\u2026`
58
+ });
59
+ try {
60
+ const result = await syncCmsContent(cms, contentTypes);
61
+ const blocks = [
62
+ result.errors.length === 0 ? `:white_check_mark: *Sync complete* \u2014 ${result.entries.length} content types synced` : `:warning: *Sync complete with errors* \u2014 ${result.entries.length} succeeded, ${result.errors.length} failed`,
63
+ "",
64
+ ...summariseCmsEntries(result)
65
+ ];
66
+ if (result.errors.length > 0) {
67
+ blocks.push("", "*Errors:*", ...summariseCmsErrors(result));
68
+ }
69
+ await respond({ response_type: "in_channel", text: blocks.join("\n") });
70
+ } catch (err) {
71
+ const message = err instanceof Error ? err.message : String(err);
72
+ console.error("[slack] Sync failed:", message);
73
+ await respond({
74
+ response_type: "ephemeral",
75
+ text: `:x: Sync failed: ${message}`
76
+ });
77
+ }
78
+ });
79
+ app.command(slack.cmdSyncTranslations, async ({ command, ack, respond }) => {
80
+ await ack();
81
+ const raw = command.text.trim();
82
+ if (!raw) {
83
+ await respond({
84
+ response_type: "ephemeral",
85
+ text: "Usage: `/sync-translations <comma-separated projects> [<comma-separated locales>]`\nExample: `/sync-translations tandem,tandem-(website) en,de,fr`\nOmit locales to use the default locale list."
86
+ });
87
+ return;
88
+ }
89
+ const spaceIdx = raw.indexOf(" ");
90
+ const projectsPart = spaceIdx === -1 ? raw : raw.slice(0, spaceIdx).trim();
91
+ const localesPart = spaceIdx === -1 ? "" : raw.slice(spaceIdx + 1).trim();
92
+ const projects = projectsPart.split(",").map((s) => s.trim()).filter(Boolean);
93
+ if (projects.length === 0) {
94
+ await respond({
95
+ response_type: "ephemeral",
96
+ text: "No valid projects. Pass a comma-separated list as the first argument, e.g. `tandem,tandem-(website)`."
97
+ });
98
+ return;
99
+ }
100
+ let locales = localesPart.length ? localesPart.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
101
+ if (locales && locales.length === 0) {
102
+ locales = void 0;
103
+ }
104
+ const localesHint = locales?.length ? ` \u2014 locales: ${locales?.join(", ")}` : " \u2014 default locales";
105
+ await respond({
106
+ response_type: "in_channel",
107
+ text: `:hourglass_flowing_sand: Translation sync started for projects: *${projects.join(", ")}*${localesHint}\u2026`
108
+ });
109
+ try {
110
+ const result = await syncTranslations(projects, locales);
111
+ const lines = summariseTranslationEntries(result.entries);
112
+ const errorLines = result.errors.map(
113
+ (e) => `\u2022 \`${e.project}\` / \`${e.resource}\` / \`${e.locale}\` \u2014 ${e.error}`
114
+ );
115
+ const blocks = [
116
+ result.errors.length === 0 ? `:white_check_mark: *Translation sync complete* \u2014 ${result.entries.length} resource \xD7 locale uploads` : `:warning: *Translation sync complete with errors* \u2014 ${result.entries.length} succeeded, ${result.errors.length} failed`,
117
+ "",
118
+ ...lines
119
+ ];
120
+ if (errorLines.length > 0) {
121
+ blocks.push("", "*Errors:*", ...errorLines);
122
+ }
123
+ await respond({ response_type: "in_channel", text: blocks.join("\n") });
124
+ } catch (err) {
125
+ const message = err instanceof Error ? err.message : String(err);
126
+ console.error("[slack] Sync failed:", message);
127
+ await respond({
128
+ response_type: "ephemeral",
129
+ text: `:x: Sync failed: ${message}`
130
+ });
131
+ }
132
+ });
133
+ app.command("/list-projects", async ({ ack }) => {
134
+ const projectNames = Object.keys(allProjects);
135
+ const lines = projectNames.map((name, i) => `${i + 1}. ${name}`);
136
+ await ack({
137
+ response_type: "ephemeral",
138
+ text: `*Available Lingohub projects:*
139
+ ${lines.join("\n")}`
140
+ });
141
+ });
142
+ app.command("/list-resources", async ({ command, ack }) => {
143
+ const project = command.text.trim();
144
+ if (!project) {
145
+ const projectNames = Object.keys(allProjects);
146
+ const lines2 = projectNames.map((name, i) => `${i + 1}. ${name}`);
147
+ await ack({
148
+ response_type: "ephemeral",
149
+ text: `Usage: \`/list-resources <project-name>\`
150
+
151
+ *Available projects:*
152
+ ${lines2.join("\n")}`
153
+ });
154
+ return;
155
+ }
156
+ const resources = allProjects[project];
157
+ if (!resources) {
158
+ const available = Object.keys(allProjects).join(", ");
159
+ await ack({
160
+ response_type: "ephemeral",
161
+ text: `Unknown project \`${project}\`.
162
+ Available projects: ${available}`
163
+ });
164
+ return;
165
+ }
166
+ const lines = resources.map(
167
+ (r, i) => `${i + 1}. \`${r.resource}\` (${r.fileName})`
168
+ );
169
+ await ack({
170
+ response_type: "ephemeral",
171
+ text: `*Resources for "${project}":*
172
+ ${lines.join("\n")}`
173
+ });
174
+ });
175
+ await app.start();
176
+ console.log("[slack] Bot connected via Socket Mode");
177
+ }
178
+
179
+ export {
180
+ notifySlack,
181
+ startSlackBot
182
+ };
183
+ //# sourceMappingURL=chunk-SMGEK5VP.js.map
@@ -0,0 +1 @@
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"]}
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  config
4
- } from "./chunk-4DE47ZJD.js";
4
+ } from "./chunk-75UTJIZQ.js";
5
5
 
6
6
  // src/client/config.ts
7
7
  import dotenv from "dotenv";
@@ -14,4 +14,4 @@ var config2 = {
14
14
  export {
15
15
  config2 as config
16
16
  };
17
- //# sourceMappingURL=chunk-UPIQFNCR.js.map
17
+ //# sourceMappingURL=chunk-UMWRIKPS.js.map
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  config
4
- } from "../chunk-UPIQFNCR.js";
4
+ } from "../chunk-UMWRIKPS.js";
5
5
  import {
6
6
  fetchCmsBundles
7
- } from "../chunk-R3E6ZZJE.js";
7
+ } from "../chunk-JVI4MEN5.js";
8
8
  import "../chunk-EQ3DSPTJ.js";
9
- import "../chunk-4DE47ZJD.js";
9
+ import "../chunk-75UTJIZQ.js";
10
10
  import {
11
11
  ContentStore
12
- } from "../chunk-5WQPK6GZ.js";
12
+ } from "../chunk-KNUP5JWR.js";
13
13
  import "../chunk-SF7FCBR2.js";
14
14
 
15
15
  // src/client/fetch-content-bundles.ts
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  config
4
- } from "../chunk-UPIQFNCR.js";
4
+ } from "../chunk-UMWRIKPS.js";
5
5
  import {
6
6
  fetchMergedTranslationBundles
7
- } from "../chunk-R3E6ZZJE.js";
7
+ } from "../chunk-JVI4MEN5.js";
8
8
  import "../chunk-EQ3DSPTJ.js";
9
- import "../chunk-4DE47ZJD.js";
9
+ import "../chunk-75UTJIZQ.js";
10
10
  import {
11
11
  ContentStore
12
- } from "../chunk-5WQPK6GZ.js";
12
+ } from "../chunk-KNUP5JWR.js";
13
13
  import {
14
14
  allProjects
15
15
  } from "../chunk-SF7FCBR2.js";
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  config
4
- } from "../chunk-UPIQFNCR.js";
4
+ } from "../chunk-UMWRIKPS.js";
5
5
  import {
6
6
  fetchTranslationBundles
7
- } from "../chunk-R3E6ZZJE.js";
7
+ } from "../chunk-JVI4MEN5.js";
8
8
  import "../chunk-EQ3DSPTJ.js";
9
- import "../chunk-4DE47ZJD.js";
9
+ import "../chunk-75UTJIZQ.js";
10
10
  import {
11
11
  ContentStore
12
- } from "../chunk-5WQPK6GZ.js";
12
+ } from "../chunk-KNUP5JWR.js";
13
13
  import {
14
14
  allProjects
15
15
  } from "../chunk-SF7FCBR2.js";
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  queryCmsBundle
4
- } from "../chunk-R3E6ZZJE.js";
4
+ } from "../chunk-JVI4MEN5.js";
5
5
  import "../chunk-EQ3DSPTJ.js";
6
- import "../chunk-5WQPK6GZ.js";
6
+ import "../chunk-KNUP5JWR.js";
7
7
  import "../chunk-SF7FCBR2.js";
8
8
 
9
9
  // src/client/query-cms.ts
@@ -65,6 +65,8 @@ interface FetchCmsBundlesOptions {
65
65
  interface TranslationFilterConfig {
66
66
  projects: Record<string, string[]>;
67
67
  locales?: string[];
68
+ /** Output key structure. `"flat"` (default) keeps dotted keys as-is; `"nested"` expands dots into nested objects. */
69
+ structure?: 'flat' | 'nested';
68
70
  }
69
71
  interface FetchTranslationBundlesOptions extends TranslationFilterConfig {
70
72
  /** S3 GET retry; defaults via {@link getDefaultS3RetryConfig}. */
package/dist/index.d.ts CHANGED
@@ -1 +1 @@
1
- export { B as BundleItem, C as CMSProvider, c as CmsBundleInfo, F as FetchCmsBundlesOptions, b as FetchMergedTranslationBundlesOptions, a as FetchTranslationBundlesOptions, Q as QueryOptions, e as S3Config, f as S3RetryConfig, S as SDKConfig, T as TranslationBundleInfo, g as TranslationFilterConfig } from './index-PQ7XN47c.js';
1
+ export { B as BundleItem, C as CMSProvider, c as CmsBundleInfo, F as FetchCmsBundlesOptions, b as FetchMergedTranslationBundlesOptions, a as FetchTranslationBundlesOptions, Q as QueryOptions, e as S3Config, f as S3RetryConfig, S as SDKConfig, T as TranslationBundleInfo, g as TranslationFilterConfig } from './index-DqDNlXSE.js';
package/dist/node.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as SDKConfig, F as FetchCmsBundlesOptions, a as FetchTranslationBundlesOptions, T as TranslationBundleInfo, b as FetchMergedTranslationBundlesOptions, C as CMSProvider, Q as QueryOptions } from './index-PQ7XN47c.js';
2
- export { B as BundleItem, c as CmsBundleInfo, d as ContentStore, e as S3Config, f as S3RetryConfig, g as TranslationFilterConfig, h as fetchCmsBundles, i as fetchMergedTranslationBundles, j as fetchTranslationBundles, k as getDefaultS3RetryConfig, q as queryCmsBundle } from './index-PQ7XN47c.js';
1
+ import { S as SDKConfig, F as FetchCmsBundlesOptions, a as FetchTranslationBundlesOptions, T as TranslationBundleInfo, b as FetchMergedTranslationBundlesOptions, C as CMSProvider, Q as QueryOptions } from './index-DqDNlXSE.js';
2
+ export { B as BundleItem, c as CmsBundleInfo, d as ContentStore, e as S3Config, f as S3RetryConfig, g as TranslationFilterConfig, h as fetchCmsBundles, i as fetchMergedTranslationBundles, j as fetchTranslationBundles, k as getDefaultS3RetryConfig, q as queryCmsBundle } from './index-DqDNlXSE.js';
3
3
 
4
4
  /**
5
5
  * Trims nested object depth.
package/dist/node.js CHANGED
@@ -61,8 +61,9 @@ var ContentStore = class {
61
61
  var buildCmsObjectKey = (cms, contentType) => {
62
62
  return `${cms}-${contentType}.json`;
63
63
  };
64
+ var sanitiseKeySegment = (s) => s.replace(/[()]/g, "");
64
65
  var buildTranslationObjectKey = (project, fileName, locale) => {
65
- return `lingohub-${project}.${fileName.replaceAll("[locale]", locale)}`;
66
+ return `lingohub-${sanitiseKeySegment(project)}.${fileName.replaceAll("[locale]", locale)}`;
66
67
  };
67
68
 
68
69
  // src/shared/bundles.ts
@@ -425,7 +426,7 @@ var transformObjectToFlat = (data) => {
425
426
  const result = {};
426
427
  const flatten = (obj, path3 = []) => {
427
428
  Object.entries(obj).forEach(([key, value]) => {
428
- if (typeof value === "object") {
429
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
429
430
  flatten(value, path3.concat(key));
430
431
  } else {
431
432
  result[path3.concat(key).join(".")] = value;
@@ -443,7 +444,10 @@ var convertXMLToJS = (xml) => {
443
444
  compact: true
444
445
  });
445
446
  let mapped = {};
446
- converted.resources.string.forEach((item) => {
447
+ const strings = converted.resources.string;
448
+ const items = Array.isArray(strings) ? strings : [strings];
449
+ items.forEach((item) => {
450
+ if (!item?._attributes?.name) return;
447
451
  mapped = {
448
452
  ...mapped,
449
453
  [item._attributes.name]: item._text
@@ -454,10 +458,11 @@ var convertXMLToJS = (xml) => {
454
458
  var parseIOSStrings = (strings) => {
455
459
  const parsedObj = {};
456
460
  strings.split("\n").filter((line) => line.startsWith('"') && line.endsWith(";")).map((line) => line.trim().slice(0, -1)).forEach((line) => {
457
- let [key, value] = line.split(" = ");
458
- if (!key || !value) return;
459
- key = key.slice(1, -1);
460
- value = value.slice(1, -1);
461
+ const eqIdx = line.indexOf(" = ");
462
+ if (eqIdx === -1) return;
463
+ const key = line.slice(1, eqIdx - 1);
464
+ const value = line.slice(eqIdx + 3 + 1, -1);
465
+ if (!key) return;
461
466
  parsedObj[key] = value;
462
467
  });
463
468
  return parsedObj;
@@ -508,6 +513,24 @@ function translationJsonOutputPath(outputDir, objectKey) {
508
513
  }
509
514
 
510
515
  // src/shared/bundles.ts
516
+ function nestKeys(flat) {
517
+ if (typeof flat !== "object" || flat === null || Array.isArray(flat)) return flat;
518
+ const src = flat;
519
+ const result = {};
520
+ for (const [key, value] of Object.entries(src)) {
521
+ const parts = key.split(".");
522
+ let cur = result;
523
+ for (let i = 0; i < parts.length - 1; i++) {
524
+ const seg = parts[i];
525
+ if (!(seg in cur) || typeof cur[seg] !== "object" || cur[seg] === null) {
526
+ cur[seg] = {};
527
+ }
528
+ cur = cur[seg];
529
+ }
530
+ cur[parts[parts.length - 1]] = value;
531
+ }
532
+ return result;
533
+ }
511
534
  function getAtPath(obj, dottedPath) {
512
535
  const parts = dottedPath.split(".").filter((p) => p.length > 0);
513
536
  if (parts.length === 0) return { found: false };
@@ -585,7 +608,8 @@ function filterResources(projectResources, resourceFilter) {
585
608
  return projectResources.filter((r) => resourceFilter.includes(r.resource));
586
609
  }
587
610
  async function fetchTranslationBundles(store, outputDir, options) {
588
- const { projects, locales } = options;
611
+ const { projects, locales, structure } = options;
612
+ const nested = structure === "nested";
589
613
  const retry = options.retry ?? getDefaultS3RetryConfig();
590
614
  await fs.mkdir(outputDir, { recursive: true });
591
615
  const result = {};
@@ -612,11 +636,12 @@ async function fetchTranslationBundles(store, outputDir, options) {
612
636
  resourceTasks.map(async ({ objectKey, resource }) => {
613
637
  const raw = await downloadRawWithRetry(store, objectKey, retry);
614
638
  const parsed = parseTranslationResourceRaw(raw, resource);
639
+ const output = nested ? nestKeys(parsed) : parsed;
615
640
  const filePath = translationJsonOutputPath(outputDir, objectKey);
616
641
  await fs.mkdir(path2.dirname(filePath), { recursive: true });
617
642
  await fs.writeFile(
618
643
  filePath,
619
- JSON.stringify(parsed, null, 2),
644
+ JSON.stringify(output, null, 2),
620
645
  "utf-8"
621
646
  );
622
647
  if (!result[project]) {
@@ -630,7 +655,8 @@ async function fetchTranslationBundles(store, outputDir, options) {
630
655
  return result;
631
656
  }
632
657
  async function fetchMergedTranslationBundles(store, outputDir, options) {
633
- const { projects, locales } = options;
658
+ const { projects, locales, structure } = options;
659
+ const nested = structure === "nested";
634
660
  const retry = options.retry ?? getDefaultS3RetryConfig();
635
661
  await fs.mkdir(outputDir, { recursive: true });
636
662
  const localesToSync = locales && locales.length ? locales : defaultLocales;
@@ -670,9 +696,10 @@ async function fetchMergedTranslationBundles(store, outputDir, options) {
670
696
  const out = {};
671
697
  for (const loc of localesToSync) {
672
698
  const filePath = path2.resolve(outputDir, `${loc}.json`);
699
+ const output = nested ? nestKeys(merged[loc]) : merged[loc];
673
700
  await fs.writeFile(
674
701
  filePath,
675
- JSON.stringify(merged[loc], null, 2),
702
+ JSON.stringify(output, null, 2),
676
703
  "utf-8"
677
704
  );
678
705
  out[loc] = filePath;