@arkebcacy/beacon-cli-temp 0.1.6 → 0.1.8

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 (59) hide show
  1. package/dist/cs/labels/Label.d.ts +1 -1
  2. package/dist/cs/labels/Label.d.ts.map +1 -1
  3. package/dist/cs/labels/Label.js +1 -3
  4. package/dist/cs/labels/Label.js.map +1 -1
  5. package/dist/cs/labels/getAllLabels.js +1 -1
  6. package/dist/cs/labels/getAllLabels.js.map +1 -1
  7. package/dist/cs/locales/addLocale.d.ts +11 -0
  8. package/dist/cs/locales/addLocale.d.ts.map +1 -0
  9. package/dist/cs/locales/addLocale.js +36 -0
  10. package/dist/cs/locales/addLocale.js.map +1 -0
  11. package/dist/cs/locales/ensureLocaleExists.d.ts +11 -0
  12. package/dist/cs/locales/ensureLocaleExists.d.ts.map +1 -0
  13. package/dist/cs/locales/ensureLocaleExists.js +38 -0
  14. package/dist/cs/locales/ensureLocaleExists.js.map +1 -0
  15. package/dist/cs/locales/getLocales.d.ts +12 -0
  16. package/dist/cs/locales/getLocales.d.ts.map +1 -0
  17. package/dist/cs/locales/getLocales.js +23 -0
  18. package/dist/cs/locales/getLocales.js.map +1 -0
  19. package/dist/dto/labels/flatten.js +1 -1
  20. package/dist/dto/labels/flatten.js.map +1 -1
  21. package/dist/dto/labels/organize.d.ts.map +1 -1
  22. package/dist/dto/labels/organize.js +15 -14
  23. package/dist/dto/labels/organize.js.map +1 -1
  24. package/dist/schema/entries/toContentstack.d.ts.map +1 -1
  25. package/dist/schema/entries/toContentstack.js +30 -5
  26. package/dist/schema/entries/toContentstack.js.map +1 -1
  27. package/dist/schema/entries/toFilesystem.js +23 -10
  28. package/dist/schema/entries/toFilesystem.js.map +1 -1
  29. package/dist/schema/labels/lib/labelHelpers.d.ts +3 -0
  30. package/dist/schema/labels/lib/labelHelpers.d.ts.map +1 -0
  31. package/dist/schema/labels/lib/labelHelpers.js +34 -0
  32. package/dist/schema/labels/lib/labelHelpers.js.map +1 -0
  33. package/dist/schema/labels/lib/labelOperations.d.ts +6 -0
  34. package/dist/schema/labels/lib/labelOperations.d.ts.map +1 -0
  35. package/dist/schema/labels/lib/labelOperations.js +57 -0
  36. package/dist/schema/labels/lib/labelOperations.js.map +1 -0
  37. package/dist/schema/labels/toContentstack.d.ts.map +1 -1
  38. package/dist/schema/labels/toContentstack.js +142 -105
  39. package/dist/schema/labels/toContentstack.js.map +1 -1
  40. package/dist/schema/labels/toFilesystem.d.ts.map +1 -1
  41. package/dist/schema/labels/toFilesystem.js +6 -3
  42. package/dist/schema/labels/toFilesystem.js.map +1 -1
  43. package/dist/tsconfig.tsbuildinfo +1 -1
  44. package/package.json +1 -1
  45. package/src/cs/labels/Label.ts +2 -6
  46. package/src/cs/labels/getAllLabels.ts +1 -1
  47. package/src/cs/locales/addLocale.ts +59 -0
  48. package/src/cs/locales/ensureLocaleExists.ts +51 -0
  49. package/src/cs/locales/getLocales.ts +38 -0
  50. package/src/dto/labels/flatten.test.ts +16 -16
  51. package/src/dto/labels/flatten.ts +1 -1
  52. package/src/dto/labels/organize.test.ts +19 -19
  53. package/src/dto/labels/organize.ts +15 -14
  54. package/src/schema/entries/toContentstack.ts +75 -6
  55. package/src/schema/entries/toFilesystem.ts +29 -14
  56. package/src/schema/labels/lib/labelHelpers.ts +36 -0
  57. package/src/schema/labels/lib/labelOperations.ts +74 -0
  58. package/src/schema/labels/toContentstack.ts +204 -109
  59. package/src/schema/labels/toFilesystem.ts +10 -3
package/package.json CHANGED
@@ -50,5 +50,5 @@
50
50
  "prepack": "node ./build/prepack.js"
51
51
  },
52
52
  "type": "module",
53
- "version": "0.1.6"
53
+ "version": "0.1.8"
54
54
  }
@@ -4,13 +4,9 @@ import { isItem } from '../Types.js';
4
4
 
5
5
  export default interface Label extends OmitIndex<Item> {
6
6
  readonly name: string;
7
- readonly parent_uid: string | null;
7
+ readonly parent: readonly string[];
8
8
  }
9
9
 
10
10
  export function isLabel(o: unknown): o is Label {
11
- return (
12
- isItem(o) &&
13
- typeof o.name === 'string' &&
14
- (o.parent_uid === null || typeof o.parent_uid === 'string')
15
- );
11
+ return isItem(o) && typeof o.name === 'string' && Array.isArray(o.parent);
16
12
  }
@@ -30,7 +30,7 @@ export default async function getAllLabels(client: Client): Promise<Label[]> {
30
30
  const label: Label = {
31
31
  ...raw,
32
32
  name: raw.name,
33
- parent_uid: typeof raw.parent_uid === 'string' ? raw.parent_uid : null,
33
+ parent: Array.isArray(raw.parent) ? raw.parent : [],
34
34
  uid: raw.uid,
35
35
  };
36
36
  if (isLabel(label)) {
@@ -0,0 +1,59 @@
1
+ import type Client from '../api/Client.js';
2
+ import ContentstackError from '../api/ContentstackError.js';
3
+ import isRecord from '#cli/util/isRecord.js';
4
+ import type { Locale } from './getLocales.js';
5
+
6
+ interface AddLocaleRequest {
7
+ readonly locale: {
8
+ readonly code: string;
9
+ readonly fallback_locale?: string;
10
+ readonly name?: string;
11
+ };
12
+ }
13
+
14
+ /**
15
+ * Adds a new locale to the stack.
16
+ * @param client - Contentstack API client
17
+ * @param code - Locale code (e.g., 'zh-cn', 'zh-chs')
18
+ * @param name - Display name for the locale (e.g., 'Chinese - China')
19
+ * @param fallbackLocale - Optional fallback locale code
20
+ */
21
+ export async function addLocale(
22
+ client: Client,
23
+ code: string,
24
+ name?: string,
25
+ fallbackLocale?: string,
26
+ ): Promise<Locale> {
27
+ const requestBody: AddLocaleRequest = {
28
+ locale: {
29
+ code,
30
+ ...(fallbackLocale !== undefined && { fallback_locale: fallbackLocale }),
31
+ ...(name !== undefined && { name }),
32
+ },
33
+ };
34
+
35
+ const response = await client.POST('/v3/locales', {
36
+ body: requestBody as never,
37
+ });
38
+
39
+ const msg = `Failed to add locale '${code}'`;
40
+ ContentstackError.throwIfError(response.error, msg);
41
+
42
+ if (!response.response.ok) {
43
+ throw new Error(msg);
44
+ }
45
+
46
+ const { data } = response;
47
+
48
+ if (!isRecord(data)) {
49
+ throw new TypeError(msg);
50
+ }
51
+
52
+ const { locale } = data;
53
+
54
+ if (!isRecord(locale)) {
55
+ throw new TypeError(msg);
56
+ }
57
+
58
+ return locale as Locale;
59
+ }
@@ -0,0 +1,51 @@
1
+ import type Client from '../api/Client.js';
2
+ import { addLocale } from './addLocale.js';
3
+ import { getLocales } from './getLocales.js';
4
+ import type { Locale } from './getLocales.js';
5
+
6
+ /**
7
+ * Ensures a locale exists in the target stack. If it doesn't exist, creates it.
8
+ * @param client - Contentstack API client
9
+ * @param localeCode - Locale code to ensure exists (e.g., 'zh-cn')
10
+ * @param fallbackLocale - Optional fallback locale code (defaults to 'en-us')
11
+ * @returns The locale object (either existing or newly created)
12
+ */
13
+ export async function ensureLocaleExists(
14
+ client: Client,
15
+ localeCode: string,
16
+ fallbackLocale = 'en-us',
17
+ ): Promise<Locale> {
18
+ const existingLocales = await getLocales(client);
19
+ const existing = existingLocales.find((loc) => loc.code === localeCode);
20
+
21
+ if (existing !== undefined) {
22
+ return existing;
23
+ }
24
+
25
+ // Locale doesn't exist - create it
26
+
27
+ // Derive a reasonable display name from the locale code
28
+ const name = deriveLocaleName(localeCode);
29
+
30
+ const newLocale = await addLocale(client, localeCode, name, fallbackLocale);
31
+
32
+ return newLocale;
33
+ }
34
+
35
+ /**
36
+ * Derives a human-readable name from a locale code.
37
+ * This is a basic implementation that can be expanded as needed.
38
+ */
39
+ function deriveLocaleName(code: string): string {
40
+ const nameMap: Record<string, string> = {
41
+ 'en-us': 'English - United States',
42
+ 'zh-chs': 'Chinese (simplified)',
43
+ 'zh-cht': 'Chinese (traditional)',
44
+ 'zh-cn': 'Chinese - China',
45
+ 'zh-hans': 'Chinese (simplified)',
46
+ 'zh-hant': 'Chinese (traditional)',
47
+ 'zh-tw': 'Chinese - Taiwan',
48
+ };
49
+
50
+ return nameMap[code.toLowerCase()] ?? code.toUpperCase();
51
+ }
@@ -0,0 +1,38 @@
1
+ import type Client from '../api/Client.js';
2
+ import ContentstackError from '../api/ContentstackError.js';
3
+ import isRecord from '#cli/util/isRecord.js';
4
+
5
+ export interface Locale {
6
+ readonly code: string;
7
+ readonly fallback_locale?: string;
8
+ readonly name: string;
9
+ readonly uid: string;
10
+ }
11
+
12
+ /**
13
+ * Fetches all locales configured for a stack.
14
+ */
15
+ export async function getLocales(client: Client): Promise<readonly Locale[]> {
16
+ const response = await client.GET('/v3/locales', {});
17
+
18
+ const msg = 'Failed to fetch locales';
19
+ ContentstackError.throwIfError(response.error, msg);
20
+
21
+ if (!response.response.ok) {
22
+ throw new Error(msg);
23
+ }
24
+
25
+ const { data } = response;
26
+
27
+ if (!isRecord(data)) {
28
+ throw new TypeError(msg);
29
+ }
30
+
31
+ const { locales } = data;
32
+
33
+ if (!Array.isArray(locales)) {
34
+ throw new TypeError(msg);
35
+ }
36
+
37
+ return locales as Locale[];
38
+ }
@@ -18,9 +18,9 @@ describe('flatten', () => {
18
18
  const result = flatten(tree);
19
19
 
20
20
  expect(result).toEqual([
21
- { name: 'Parent', parent_uid: null, uid: 'parent' },
22
- { name: 'Child 1', parent_uid: 'parent', uid: 'child1' },
23
- { name: 'Child 2', parent_uid: 'parent', uid: 'child2' },
21
+ { name: 'Parent', parent: [], uid: 'parent' },
22
+ { name: 'Child 1', parent: ['parent'], uid: 'child1' },
23
+ { name: 'Child 2', parent: ['parent'], uid: 'child2' },
24
24
  ]);
25
25
  });
26
26
 
@@ -40,10 +40,10 @@ describe('flatten', () => {
40
40
  const result = flatten(tree);
41
41
 
42
42
  expect(result).toEqual([
43
- { name: 'Component', parent_uid: null, uid: 'component' },
44
- { name: 'Calculator', parent_uid: 'component', uid: 'calculator' },
45
- { name: 'Data', parent_uid: 'component', uid: 'data' },
46
- { name: 'Page', parent_uid: 'component', uid: 'page' },
43
+ { name: 'Component', parent: [], uid: 'component' },
44
+ { name: 'Calculator', parent: ['component'], uid: 'calculator' },
45
+ { name: 'Data', parent: ['component'], uid: 'data' },
46
+ { name: 'Page', parent: ['component'], uid: 'page' },
47
47
  ]);
48
48
  });
49
49
 
@@ -71,10 +71,10 @@ describe('flatten', () => {
71
71
  const result = flatten(tree);
72
72
 
73
73
  expect(result).toEqual([
74
- { name: 'Root', parent_uid: null, uid: 'root' },
75
- { name: 'Level 1', parent_uid: 'root', uid: 'level1' },
76
- { name: 'Level 2', parent_uid: 'level1', uid: 'level2' },
77
- { name: 'Level 3', parent_uid: 'level2', uid: 'level3' },
74
+ { name: 'Root', parent: [], uid: 'root' },
75
+ { name: 'Level 1', parent: ['root'], uid: 'level1' },
76
+ { name: 'Level 2', parent: ['level1'], uid: 'level2' },
77
+ { name: 'Level 3', parent: ['level2'], uid: 'level3' },
78
78
  ]);
79
79
  });
80
80
 
@@ -91,9 +91,9 @@ describe('flatten', () => {
91
91
  const result = flatten(tree);
92
92
 
93
93
  expect(result).toEqual([
94
- { name: 'Label 1', parent_uid: null, uid: 'label1' },
95
- { name: 'Label 2', parent_uid: null, uid: 'label2' },
96
- { name: 'Child', parent_uid: 'label2', uid: 'child' },
94
+ { name: 'Label 1', parent: [], uid: 'label1' },
95
+ { name: 'Label 2', parent: [], uid: 'label2' },
96
+ { name: 'Child', parent: ['label2'], uid: 'child' },
97
97
  ]);
98
98
  });
99
99
 
@@ -111,8 +111,8 @@ describe('flatten', () => {
111
111
  const result = flatten(tree);
112
112
 
113
113
  expect(result).toEqual([
114
- { name: 'Label 1', parent_uid: null, uid: 'label1' },
115
- { name: 'Label 2', parent_uid: null, uid: 'label2' },
114
+ { name: 'Label 1', parent: [], uid: 'label1' },
115
+ { name: 'Label 2', parent: [], uid: 'label2' },
116
116
  ]);
117
117
  });
118
118
  });
@@ -12,7 +12,7 @@ export default function flatten(
12
12
 
13
13
  result.push({
14
14
  ...label,
15
- parent_uid: parentUid,
15
+ parent: parentUid ? [parentUid] : [],
16
16
  } as Label);
17
17
 
18
18
  if (children) {
@@ -5,9 +5,9 @@ import type Label from '#cli/cs/labels/Label.js';
5
5
  describe('organize', () => {
6
6
  it('organizes flat labels into hierarchy', () => {
7
7
  const labels: Label[] = [
8
- { name: 'Parent', parent_uid: null, uid: 'parent' },
9
- { name: 'Child 1', parent_uid: 'parent', uid: 'child1' },
10
- { name: 'Child 2', parent_uid: 'parent', uid: 'child2' },
8
+ { name: 'Parent', parent: [], uid: 'parent' },
9
+ { name: 'Child 1', parent: ['parent'], uid: 'child1' },
10
+ { name: 'Child 2', parent: ['parent'], uid: 'child2' },
11
11
  ];
12
12
 
13
13
  const result = organize(labels);
@@ -26,10 +26,10 @@ describe('organize', () => {
26
26
 
27
27
  it('organizes component label structure from screenshot', () => {
28
28
  const labels: Label[] = [
29
- { name: 'Component', parent_uid: null, uid: 'component' },
30
- { name: 'Calculator', parent_uid: 'component', uid: 'calculator' },
31
- { name: 'Data', parent_uid: 'component', uid: 'data' },
32
- { name: 'Page', parent_uid: 'component', uid: 'page' },
29
+ { name: 'Component', parent: [], uid: 'component' },
30
+ { name: 'Calculator', parent: ['component'], uid: 'calculator' },
31
+ { name: 'Data', parent: ['component'], uid: 'data' },
32
+ { name: 'Page', parent: ['component'], uid: 'page' },
33
33
  ];
34
34
 
35
35
  const result = organize(labels);
@@ -49,10 +49,10 @@ describe('organize', () => {
49
49
 
50
50
  it('handles deeply nested labels', () => {
51
51
  const labels: Label[] = [
52
- { name: 'Root', parent_uid: null, uid: 'root' },
53
- { name: 'Level 1', parent_uid: 'root', uid: 'level1' },
54
- { name: 'Level 2', parent_uid: 'level1', uid: 'level2' },
55
- { name: 'Level 3', parent_uid: 'level2', uid: 'level3' },
52
+ { name: 'Root', parent: [], uid: 'root' },
53
+ { name: 'Level 1', parent: ['root'], uid: 'level1' },
54
+ { name: 'Level 2', parent: ['level1'], uid: 'level2' },
55
+ { name: 'Level 3', parent: ['level2'], uid: 'level3' },
56
56
  ];
57
57
 
58
58
  const result = organize(labels);
@@ -80,9 +80,9 @@ describe('organize', () => {
80
80
 
81
81
  it('handles multiple top-level labels', () => {
82
82
  const labels: Label[] = [
83
- { name: 'Label 1', parent_uid: null, uid: 'label1' },
84
- { name: 'Label 2', parent_uid: null, uid: 'label2' },
85
- { name: 'Child', parent_uid: 'label2', uid: 'child' },
83
+ { name: 'Label 1', parent: [], uid: 'label1' },
84
+ { name: 'Label 2', parent: [], uid: 'label2' },
85
+ { name: 'Child', parent: ['label2'], uid: 'child' },
86
86
  ];
87
87
 
88
88
  const result = organize(labels);
@@ -104,7 +104,7 @@ describe('organize', () => {
104
104
 
105
105
  it('throws error for orphaned label', () => {
106
106
  const labels: Label[] = [
107
- { name: 'Child', parent_uid: 'nonexistent', uid: 'child' },
107
+ { name: 'Child', parent: ['nonexistent'], uid: 'child' },
108
108
  ];
109
109
 
110
110
  expect(() => organize(labels)).toThrow(
@@ -114,10 +114,10 @@ describe('organize', () => {
114
114
 
115
115
  it('preserves order of siblings', () => {
116
116
  const labels: Label[] = [
117
- { name: 'Parent', parent_uid: null, uid: 'parent' },
118
- { name: 'Child 3', parent_uid: 'parent', uid: 'child3' },
119
- { name: 'Child 1', parent_uid: 'parent', uid: 'child1' },
120
- { name: 'Child 2', parent_uid: 'parent', uid: 'child2' },
117
+ { name: 'Parent', parent: [], uid: 'parent' },
118
+ { name: 'Child 3', parent: ['parent'], uid: 'child3' },
119
+ { name: 'Child 1', parent: ['parent'], uid: 'child1' },
120
+ { name: 'Child 2', parent: ['parent'], uid: 'child2' },
121
121
  ];
122
122
 
123
123
  const result = organize(labels);
@@ -1,10 +1,10 @@
1
1
  import type Label from '#cli/cs/labels/Label.js';
2
2
  import type { LabelTreeNode } from './NormalizedLabels.js';
3
3
 
4
- // Labels have a tree structure defined by uid/parent_uid.
4
+ // Labels have a tree structure defined by uid/parent array.
5
5
  //
6
- // The API returns a flat array that includes uid/parent_uid, but
7
- // leaves it up to the client to reconstruct the tree.
6
+ // The API returns a flat array that includes uid/parent, where parent is an array
7
+ // containing the parent UID (or empty array for top-level labels).
8
8
  //
9
9
  // We organize labels into a tree structure for serialization.
10
10
  //
@@ -17,8 +17,8 @@ export default function organize(
17
17
  const topLevel: MutableNode[] = [];
18
18
 
19
19
  for (const label of labels) {
20
- const { parent_uid, ...labelWithoutParent } = label;
21
- // Preserve all fields except parent_uid (which is represented by the tree structure)
20
+ const { parent, ...labelWithoutParent } = label;
21
+ // Preserve all fields except parent (which is represented by the tree structure)
22
22
  byUid.set(label.uid, {
23
23
  ...labelWithoutParent,
24
24
  name: label.name,
@@ -26,24 +26,25 @@ export default function organize(
26
26
  });
27
27
  }
28
28
 
29
- for (const { uid, parent_uid } of labels) {
30
- const label = byUid.get(uid);
31
- if (!label) {
32
- throw new Error(`Label ${uid} not found`);
29
+ for (const label of labels) {
30
+ const node = byUid.get(label.uid);
31
+ if (!node) {
32
+ throw new Error(`Label ${label.uid} not found`);
33
33
  }
34
34
 
35
- if (!parent_uid) {
36
- topLevel.push(label);
35
+ const parentUid = label.parent.length > 0 ? label.parent[0] : null;
36
+ if (!parentUid) {
37
+ topLevel.push(node);
37
38
  continue;
38
39
  }
39
40
 
40
- const parent = byUid.get(parent_uid);
41
+ const parent = byUid.get(parentUid);
41
42
  if (!parent) {
42
- const msg = `Orphaned label ${uid} with parent ${parent_uid}`;
43
+ const msg = `Orphaned label ${label.uid} with parent ${parentUid}`;
43
44
  throw new Error(msg);
44
45
  }
45
46
 
46
- (parent.children ??= []).push(label);
47
+ (parent.children ??= []).push(node);
47
48
  }
48
49
 
49
50
  return topLevel;
@@ -3,6 +3,7 @@ import deleteEntry from '#cli/cs/entries/delete.js';
3
3
  import getEntryLocales from '#cli/cs/entries/getEntryLocales.js';
4
4
  import importEntry from '#cli/cs/entries/import.js';
5
5
  import type { Entry } from '#cli/cs/entries/Types.js';
6
+ import { ensureLocaleExists } from '#cli/cs/locales/ensureLocaleExists.js';
6
7
  import BeaconReplacer from '#cli/dto/entry/BeaconReplacer.js';
7
8
  import type ProgressBar from '#cli/ui/progress/ProgressBar.js';
8
9
  import type Ctx from '../ctx/Ctx.js';
@@ -45,7 +46,45 @@ export default async function toContentstack(
45
46
  update,
46
47
  });
47
48
 
48
- for (const title of result.unmodified) {
49
+ // Process unmodified entries to ensure all locale versions are synced
50
+ await processUnmodifiedEntries(
51
+ result.unmodified,
52
+ ctx,
53
+ contentType,
54
+ csEntriesByTitle,
55
+ fsEntriesByTitle,
56
+ transformer,
57
+ filenamesByTitle,
58
+ );
59
+
60
+ return result;
61
+ }
62
+
63
+ function shouldSyncLocales(
64
+ fsLocaleVersions: Awaited<ReturnType<typeof loadFsLocaleVersions>>,
65
+ csLocaleSet: Set<string>,
66
+ ): boolean {
67
+ // Check if we have new locale versions that don't exist in Contentstack
68
+ const fsLocaleSet = new Set(
69
+ fsLocaleVersions.map((lv) => (lv.locale === 'default' ? null : lv.locale)),
70
+ );
71
+ return Array.from(fsLocaleSet).some(
72
+ (locale) => locale !== null && !csLocaleSet.has(locale),
73
+ );
74
+ }
75
+
76
+ async function processUnmodifiedEntries(
77
+ unmodified: Iterable<string>,
78
+ ctx: Ctx,
79
+ contentType: ContentType,
80
+ csEntriesByTitle: ReadonlyMap<string, Entry>,
81
+ fsEntriesByTitle: ReadonlyMap<string, Entry>,
82
+ transformer: BeaconReplacer,
83
+ filenamesByTitle: ReadonlyMap<Entry['uid'], string>,
84
+ ) {
85
+ const ui = getUi();
86
+
87
+ for (const title of unmodified) {
49
88
  const cs = csEntriesByTitle.get(title);
50
89
  const fs = fsEntriesByTitle.get(title);
51
90
 
@@ -60,11 +99,33 @@ export default async function toContentstack(
60
99
  throw new Error(`No matching entry found for ${title}.`);
61
100
  }
62
101
 
102
+ // Load all locale versions from filesystem
103
+ const fsLocaleVersions = await loadFsLocaleVersions(
104
+ fs,
105
+ contentType.uid,
106
+ filenamesByTitle,
107
+ );
108
+
109
+ const csLocaleSet = await getExistingLocales(ctx, contentType, cs.uid);
110
+
111
+ // Only push if there are new locale versions to sync
112
+ if (!shouldSyncLocales(fsLocaleVersions, csLocaleSet)) {
113
+ continue;
114
+ }
115
+
116
+ // Push all locale versions (including new locales like zh-cn)
117
+ await updateAllLocales(
118
+ ctx,
119
+ transformer,
120
+ contentType,
121
+ fsLocaleVersions,
122
+ cs.uid,
123
+ csLocaleSet,
124
+ );
125
+
63
126
  const entry = { ...fs, uid: cs.uid };
64
127
  ctx.references.recordEntryForReferences(contentType.uid, entry);
65
128
  }
66
-
67
- return result;
68
129
  }
69
130
 
70
131
  function buildUpdateFn(
@@ -141,6 +202,13 @@ async function updateAllLocales(
141
202
  entryUid: string,
142
203
  csLocaleSet: Set<string>,
143
204
  ) {
205
+ // Ensure all required locales exist in the target stack before pushing entries
206
+ const localeEnsurePromises = fsLocaleVersions
207
+ .filter((lv) => lv.locale !== 'default')
208
+ .map(async (lv) => ensureLocaleExists(ctx.cs.client, lv.locale));
209
+
210
+ await Promise.all(localeEnsurePromises);
211
+
144
212
  // Import all locale versions in parallel for better performance
145
213
  const importPromises = fsLocaleVersions.map(async (localeVersion) => {
146
214
  const transformed = transformer.process(localeVersion.entry);
@@ -148,9 +216,10 @@ async function updateAllLocales(
148
216
  // Pass undefined for 'default' locale (single-locale backward compat)
149
217
  const locale =
150
218
  localeVersion.locale === 'default' ? undefined : localeVersion.locale;
151
- const overwrite = locale
152
- ? csLocaleSet.has(localeVersion.locale)
153
- : csLocaleSet.size > 0;
219
+
220
+ // Always use overwrite=true for locale-specific versions since the entry exists.
221
+ // For 'default' locale (single-locale backward compat), only overwrite if entry has locales.
222
+ const overwrite = locale ? true : csLocaleSet.size > 0;
154
223
 
155
224
  return importEntry(
156
225
  ctx.cs.client,
@@ -80,28 +80,24 @@ function createWriteFn(
80
80
  }
81
81
 
82
82
  // If only one locale, save without locale suffix for backward compatibility.
83
- // When multiple locales exist prefer writing a single base file when an
84
- // English locale is present (fixtures expect base filenames like
85
- // `Autumn Feast and Social.yaml`). Only use locale suffix when no
86
- // English locale is available.
87
- const hasEnglish = locales.some((l) => /^en(?:[-_]|$)/iu.test(l.code));
88
- const useLocaleSuffix = locales.length > 1 && !hasEnglish;
89
-
90
- // Log locale details for debugging why specific locales (e.g. Chinese)
91
- // may not be written to the filesystem.
92
- // Debug logging removed to avoid unsafe-call lint errors.
83
+ // When multiple locales exist, write English as base file and other locales
84
+ // with locale suffixes (e.g., Entry.yaml for English, Entry.zh-chs.yaml for Chinese)
93
85
 
94
86
  // Write all locale versions in parallel for better performance
95
- const writePromises = locales.map(async (locale) =>
96
- writeLocaleVersion(
87
+ const writePromises = locales.map(async (locale) => {
88
+ const isEnglish = /^en(?:[-_]|$)/iu.test(locale.code);
89
+ // Use locale suffix for non-English locales when multiple locales exist
90
+ const useLocaleSuffix = locales.length > 1 && !isEnglish;
91
+
92
+ return writeLocaleVersion(
97
93
  ctx,
98
94
  contentType,
99
95
  entry,
100
96
  locale.code,
101
97
  getBasePath,
102
98
  useLocaleSuffix,
103
- ),
104
- );
99
+ );
100
+ });
105
101
 
106
102
  await Promise.all(writePromises);
107
103
  };
@@ -122,8 +118,27 @@ async function writeLocaleVersion(
122
118
  localeCode,
123
119
  );
124
120
 
121
+ // Skip writing this locale version if it's a fallback (locale doesn't match requested)
122
+ // This happens when Contentstack returns the default locale content because
123
+ // no localized version exists for the requested locale
124
+ if (
125
+ useLocaleSuffix &&
126
+ exported.locale &&
127
+ typeof exported.locale === 'string' &&
128
+ exported.locale !== localeCode
129
+ ) {
130
+ // This is a fallback locale, skip writing it
131
+ return;
132
+ }
133
+
125
134
  const { uid, ...transformed } = transformEntry(ctx, contentType, exported);
126
135
 
136
+ // Preserve the actual locale code from the source stack
137
+ // This ensures we maintain the exact language-region configuration
138
+ if ('locale' in transformed) {
139
+ transformed.locale = localeCode;
140
+ }
141
+
127
142
  const basePath = getBasePath(entry);
128
143
  const filePath = useLocaleSuffix
129
144
  ? basePath.replace(/\.yaml$/u, `.${localeCode}.yaml`)
@@ -0,0 +1,36 @@
1
+ import isRecord from '#cli/util/isRecord.js';
2
+
3
+ export function canonicalize(value: unknown): unknown {
4
+ if (Array.isArray(value)) {
5
+ const result: unknown[] = [];
6
+ for (const item of value) {
7
+ result.push(canonicalize(item));
8
+ }
9
+ return result;
10
+ }
11
+ if (isRecord(value)) {
12
+ const obj: Record<string, unknown> = value;
13
+ const out: Record<string, unknown> = {};
14
+ const objKeys: string[] = Object.keys(obj);
15
+ objKeys.sort();
16
+ for (const key of objKeys) {
17
+ if (key === 'uid' || key === 'created_at' || key === 'updated_at')
18
+ continue;
19
+ // Normalize parent: skip if empty array
20
+ const val: unknown = obj[key];
21
+ if (key === 'parent' && Array.isArray(val) && val.length === 0) continue;
22
+ const canonicalizedVal: unknown = canonicalize(val);
23
+ out[key] = canonicalizedVal;
24
+ }
25
+ return out;
26
+ }
27
+ return value;
28
+ }
29
+
30
+ export function prepareLabel(
31
+ label: Record<string, unknown>,
32
+ ): Record<string, unknown> {
33
+ // Labels are sent to the API with all fields intact
34
+ // The API expects the parent field as an array
35
+ return label;
36
+ }