ai-localize-locale-engine 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # ai-localize-locale-engine
2
2
 
3
+ ## 2.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+ - ai-localize-shared@2.0.1
9
+
3
10
  ## 2.0.0
4
11
 
5
12
  ### Major Changes
package/dist/index.d.mts CHANGED
@@ -20,6 +20,20 @@ interface WriteOptions {
20
20
  localesDir: string;
21
21
  merge?: boolean;
22
22
  sort?: boolean;
23
+ /**
24
+ * Locale file layout.
25
+ *
26
+ * - `"nested"` (default) — one JSON file per language + namespace:
27
+ * `locales/en/common.json`, `locales/en/dashboard.json`
28
+ * For the "common" / default namespace the file is named `translation.json`.
29
+ *
30
+ * - `"flat"` — one JSON file per language, all namespaces merged:
31
+ * `locales/en.json`, `locales/fr.json`
32
+ * Namespace prefixes are prepended to keys to avoid collisions:
33
+ * `common.button.save`, `dashboard.header.title`
34
+ * (keys that already start with the namespace prefix are left as-is).
35
+ */
36
+ localeStructure?: "nested" | "flat";
23
37
  }
24
38
  declare class LocaleWriter {
25
39
  private options;
@@ -29,7 +43,23 @@ declare class LocaleWriter {
29
43
  merged: string[];
30
44
  created: string[];
31
45
  };
32
- private resolveFilePath;
46
+ private writeNested;
47
+ private resolveNestedFilePath;
48
+ /**
49
+ * In flat mode all LocaleFile entries for the same language are merged into
50
+ * a single `<localesDir>/<language>.json` file.
51
+ *
52
+ * To avoid key collisions between namespaces, each key is prefixed with its
53
+ * namespace name (e.g. `common.button.save`, `dashboard.header.title`).
54
+ * Keys that already start with the namespace prefix are left unchanged.
55
+ */
56
+ private writeFlat;
57
+ /**
58
+ * Produces a flat key with the namespace prepended.
59
+ * If the key already starts with `<namespace>.` it is returned as-is
60
+ * to avoid double-prefixing (e.g. when namespace splitting already embedded it).
61
+ */
62
+ private flattenKey;
33
63
  private mergeEntries;
34
64
  private sort;
35
65
  }
package/dist/index.d.ts CHANGED
@@ -20,6 +20,20 @@ interface WriteOptions {
20
20
  localesDir: string;
21
21
  merge?: boolean;
22
22
  sort?: boolean;
23
+ /**
24
+ * Locale file layout.
25
+ *
26
+ * - `"nested"` (default) — one JSON file per language + namespace:
27
+ * `locales/en/common.json`, `locales/en/dashboard.json`
28
+ * For the "common" / default namespace the file is named `translation.json`.
29
+ *
30
+ * - `"flat"` — one JSON file per language, all namespaces merged:
31
+ * `locales/en.json`, `locales/fr.json`
32
+ * Namespace prefixes are prepended to keys to avoid collisions:
33
+ * `common.button.save`, `dashboard.header.title`
34
+ * (keys that already start with the namespace prefix are left as-is).
35
+ */
36
+ localeStructure?: "nested" | "flat";
23
37
  }
24
38
  declare class LocaleWriter {
25
39
  private options;
@@ -29,7 +43,23 @@ declare class LocaleWriter {
29
43
  merged: string[];
30
44
  created: string[];
31
45
  };
32
- private resolveFilePath;
46
+ private writeNested;
47
+ private resolveNestedFilePath;
48
+ /**
49
+ * In flat mode all LocaleFile entries for the same language are merged into
50
+ * a single `<localesDir>/<language>.json` file.
51
+ *
52
+ * To avoid key collisions between namespaces, each key is prefixed with its
53
+ * namespace name (e.g. `common.button.save`, `dashboard.header.title`).
54
+ * Keys that already start with the namespace prefix are left unchanged.
55
+ */
56
+ private writeFlat;
57
+ /**
58
+ * Produces a flat key with the namespace prepended.
59
+ * If the key already starts with `<namespace>.` it is returned as-is
60
+ * to avoid double-prefixing (e.g. when namespace splitting already embedded it).
61
+ */
62
+ private flattenKey;
33
63
  private mergeEntries;
34
64
  private sort;
35
65
  }
package/dist/index.js CHANGED
@@ -77,7 +77,7 @@ var LocaleExtractor = class {
77
77
  const allLanguages = [this.options.defaultLanguage, ...this.options.targetLanguages];
78
78
  for (const lang of allLanguages) {
79
79
  for (const [namespace, entries] of namespaceMap) {
80
- const langEntries = lang === this.options.defaultLanguage ? { ...entries } : Object.fromEntries(Object.keys(entries).map((k) => [k, ""]));
80
+ const langEntries = { ...entries };
81
81
  localeFiles.push({ language: lang, namespace, entries: langEntries, filePath: "" });
82
82
  }
83
83
  }
@@ -91,14 +91,26 @@ var import_ai_localize_shared2 = require("ai-localize-shared");
91
91
  var LocaleWriter = class {
92
92
  options;
93
93
  constructor(options) {
94
- this.options = { localesDir: options.localesDir, merge: options.merge ?? true, sort: options.sort ?? true };
94
+ this.options = {
95
+ localesDir: options.localesDir,
96
+ merge: options.merge ?? true,
97
+ sort: options.sort ?? true,
98
+ localeStructure: options.localeStructure ?? "nested"
99
+ };
95
100
  }
96
101
  write(localeFiles) {
102
+ if (this.options.localeStructure === "flat") {
103
+ return this.writeFlat(localeFiles);
104
+ }
105
+ return this.writeNested(localeFiles);
106
+ }
107
+ // ─── Nested layout ───────────────────────────────────────────────────────────
108
+ writeNested(localeFiles) {
97
109
  const written = [];
98
110
  const merged = [];
99
111
  const created = [];
100
112
  for (const lf of localeFiles) {
101
- const filePath = this.resolveFilePath(lf);
113
+ const filePath = this.resolveNestedFilePath(lf);
102
114
  lf.filePath = filePath;
103
115
  (0, import_ai_localize_shared2.ensureDir)(path.dirname(filePath));
104
116
  const existing = (0, import_ai_localize_shared2.readJsonSafe)(filePath);
@@ -114,9 +126,67 @@ var LocaleWriter = class {
114
126
  }
115
127
  return { written, merged, created };
116
128
  }
117
- resolveFilePath(lf) {
129
+ resolveNestedFilePath(lf) {
118
130
  return lf.namespace === "common" ? path.join(this.options.localesDir, lf.language, "translation.json") : path.join(this.options.localesDir, lf.language, `${lf.namespace}.json`);
119
131
  }
132
+ // ─── Flat layout ─────────────────────────────────────────────────────────────
133
+ /**
134
+ * In flat mode all LocaleFile entries for the same language are merged into
135
+ * a single `<localesDir>/<language>.json` file.
136
+ *
137
+ * To avoid key collisions between namespaces, each key is prefixed with its
138
+ * namespace name (e.g. `common.button.save`, `dashboard.header.title`).
139
+ * Keys that already start with the namespace prefix are left unchanged.
140
+ */
141
+ writeFlat(localeFiles) {
142
+ const byLanguage = /* @__PURE__ */ new Map();
143
+ for (const lf of localeFiles) {
144
+ const list = byLanguage.get(lf.language) ?? [];
145
+ list.push(lf);
146
+ byLanguage.set(lf.language, list);
147
+ }
148
+ const written = [];
149
+ const merged = [];
150
+ const created = [];
151
+ for (const [language, files] of byLanguage) {
152
+ const filePath = path.join(this.options.localesDir, `${language}.json`);
153
+ (0, import_ai_localize_shared2.ensureDir)(path.dirname(filePath));
154
+ const incomingEntries = {};
155
+ for (const lf of files) {
156
+ for (const [key, value] of Object.entries(lf.entries)) {
157
+ const flatKey = this.flattenKey(lf.namespace, key);
158
+ incomingEntries[flatKey] = value;
159
+ }
160
+ }
161
+ const existing = (0, import_ai_localize_shared2.readJsonSafe)(filePath);
162
+ if (existing && this.options.merge) {
163
+ const mergedEntries = this.mergeEntries(existing, incomingEntries);
164
+ (0, import_ai_localize_shared2.writeJson)(filePath, this.options.sort ? this.sort(mergedEntries) : mergedEntries);
165
+ merged.push(filePath);
166
+ } else {
167
+ (0, import_ai_localize_shared2.writeJson)(filePath, this.options.sort ? this.sort(incomingEntries) : incomingEntries);
168
+ created.push(filePath);
169
+ }
170
+ for (const lf of files) {
171
+ lf.filePath = filePath;
172
+ }
173
+ written.push(filePath);
174
+ }
175
+ return { written, merged, created };
176
+ }
177
+ /**
178
+ * Produces a flat key with the namespace prepended.
179
+ * If the key already starts with `<namespace>.` it is returned as-is
180
+ * to avoid double-prefixing (e.g. when namespace splitting already embedded it).
181
+ */
182
+ flattenKey(namespace, key) {
183
+ if (namespace === "common" || namespace === "translation") {
184
+ return key;
185
+ }
186
+ const prefix = `${namespace}.`;
187
+ return key.startsWith(prefix) ? key : `${prefix}${key}`;
188
+ }
189
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
120
190
  mergeEntries(existing, incoming) {
121
191
  const result = { ...existing };
122
192
  for (const [key, value] of Object.entries(incoming)) {
package/dist/index.mjs CHANGED
@@ -40,7 +40,7 @@ var LocaleExtractor = class {
40
40
  const allLanguages = [this.options.defaultLanguage, ...this.options.targetLanguages];
41
41
  for (const lang of allLanguages) {
42
42
  for (const [namespace, entries] of namespaceMap) {
43
- const langEntries = lang === this.options.defaultLanguage ? { ...entries } : Object.fromEntries(Object.keys(entries).map((k) => [k, ""]));
43
+ const langEntries = { ...entries };
44
44
  localeFiles.push({ language: lang, namespace, entries: langEntries, filePath: "" });
45
45
  }
46
46
  }
@@ -54,14 +54,26 @@ import { writeJson, readJsonSafe, ensureDir } from "ai-localize-shared";
54
54
  var LocaleWriter = class {
55
55
  options;
56
56
  constructor(options) {
57
- this.options = { localesDir: options.localesDir, merge: options.merge ?? true, sort: options.sort ?? true };
57
+ this.options = {
58
+ localesDir: options.localesDir,
59
+ merge: options.merge ?? true,
60
+ sort: options.sort ?? true,
61
+ localeStructure: options.localeStructure ?? "nested"
62
+ };
58
63
  }
59
64
  write(localeFiles) {
65
+ if (this.options.localeStructure === "flat") {
66
+ return this.writeFlat(localeFiles);
67
+ }
68
+ return this.writeNested(localeFiles);
69
+ }
70
+ // ─── Nested layout ───────────────────────────────────────────────────────────
71
+ writeNested(localeFiles) {
60
72
  const written = [];
61
73
  const merged = [];
62
74
  const created = [];
63
75
  for (const lf of localeFiles) {
64
- const filePath = this.resolveFilePath(lf);
76
+ const filePath = this.resolveNestedFilePath(lf);
65
77
  lf.filePath = filePath;
66
78
  ensureDir(path.dirname(filePath));
67
79
  const existing = readJsonSafe(filePath);
@@ -77,9 +89,67 @@ var LocaleWriter = class {
77
89
  }
78
90
  return { written, merged, created };
79
91
  }
80
- resolveFilePath(lf) {
92
+ resolveNestedFilePath(lf) {
81
93
  return lf.namespace === "common" ? path.join(this.options.localesDir, lf.language, "translation.json") : path.join(this.options.localesDir, lf.language, `${lf.namespace}.json`);
82
94
  }
95
+ // ─── Flat layout ─────────────────────────────────────────────────────────────
96
+ /**
97
+ * In flat mode all LocaleFile entries for the same language are merged into
98
+ * a single `<localesDir>/<language>.json` file.
99
+ *
100
+ * To avoid key collisions between namespaces, each key is prefixed with its
101
+ * namespace name (e.g. `common.button.save`, `dashboard.header.title`).
102
+ * Keys that already start with the namespace prefix are left unchanged.
103
+ */
104
+ writeFlat(localeFiles) {
105
+ const byLanguage = /* @__PURE__ */ new Map();
106
+ for (const lf of localeFiles) {
107
+ const list = byLanguage.get(lf.language) ?? [];
108
+ list.push(lf);
109
+ byLanguage.set(lf.language, list);
110
+ }
111
+ const written = [];
112
+ const merged = [];
113
+ const created = [];
114
+ for (const [language, files] of byLanguage) {
115
+ const filePath = path.join(this.options.localesDir, `${language}.json`);
116
+ ensureDir(path.dirname(filePath));
117
+ const incomingEntries = {};
118
+ for (const lf of files) {
119
+ for (const [key, value] of Object.entries(lf.entries)) {
120
+ const flatKey = this.flattenKey(lf.namespace, key);
121
+ incomingEntries[flatKey] = value;
122
+ }
123
+ }
124
+ const existing = readJsonSafe(filePath);
125
+ if (existing && this.options.merge) {
126
+ const mergedEntries = this.mergeEntries(existing, incomingEntries);
127
+ writeJson(filePath, this.options.sort ? this.sort(mergedEntries) : mergedEntries);
128
+ merged.push(filePath);
129
+ } else {
130
+ writeJson(filePath, this.options.sort ? this.sort(incomingEntries) : incomingEntries);
131
+ created.push(filePath);
132
+ }
133
+ for (const lf of files) {
134
+ lf.filePath = filePath;
135
+ }
136
+ written.push(filePath);
137
+ }
138
+ return { written, merged, created };
139
+ }
140
+ /**
141
+ * Produces a flat key with the namespace prepended.
142
+ * If the key already starts with `<namespace>.` it is returned as-is
143
+ * to avoid double-prefixing (e.g. when namespace splitting already embedded it).
144
+ */
145
+ flattenKey(namespace, key) {
146
+ if (namespace === "common" || namespace === "translation") {
147
+ return key;
148
+ }
149
+ const prefix = `${namespace}.`;
150
+ return key.startsWith(prefix) ? key : `${prefix}${key}`;
151
+ }
152
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
83
153
  mergeEntries(existing, incoming) {
84
154
  const result = { ...existing };
85
155
  for (const [key, value] of Object.entries(incoming)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-localize-locale-engine",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Locale file generation, merging, deduplication and synchronization",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -13,7 +13,7 @@
13
13
  }
14
14
  },
15
15
  "dependencies": {
16
- "ai-localize-shared": "2.0.0"
16
+ "ai-localize-shared": "2.0.1"
17
17
  },
18
18
  "devDependencies": {
19
19
  "tsup": "^8.0.1",
package/src/extractor.ts CHANGED
@@ -38,36 +38,37 @@ export class LocaleExtractor {
38
38
  key = resolveKeyCollision(key, existingKeys);
39
39
  }
40
40
  existingKeys.add(key);
41
- keyValueMap.set(key, dt.text);
41
+ keyValueMap.set(key, dt.text);
42
42
  }
43
43
 
44
44
  const namespaceMap = new Map<string, Record<string, string>>();
45
45
  for (const [key, value] of keyValueMap) {
46
46
  const { namespace, localKey } = this.options.namespaceSplitting
47
47
  ? splitKeyNamespace(key)
48
- : { namespace: DEFAULT_NAMESPACE, localKey: key };
48
+ : { namespace: DEFAULT_NAMESPACE, localKey: key };
49
49
  if (!namespaceMap.has(namespace)) namespaceMap.set(namespace, {});
50
50
  namespaceMap.get(namespace)![localKey] = value;
51
51
  }
52
52
 
53
53
  for (const [ns, entries] of namespaceMap) {
54
54
  namespaceMap.set(
55
- ns,
55
+ ns,
56
56
  Object.fromEntries(Object.entries(entries).sort(([a], [b]) => a.localeCompare(b)))
57
- );
57
+ );
58
58
  }
59
59
 
60
60
  const localeFiles: LocaleFile[] = [];
61
61
  const allLanguages = [this.options.defaultLanguage, ...this.options.targetLanguages];
62
62
 
63
63
  for (const lang of allLanguages) {
64
- for (const [namespace, entries] of namespaceMap) {
65
- const langEntries =
66
- lang === this.options.defaultLanguage
67
- ? { ...entries }
68
- : Object.fromEntries(Object.keys(entries).map((k) => [k, ""]));
64
+ for (const [namespace, entries] of namespaceMap) {
65
+ // All languages get the source (English) value as the initial translation.
66
+ // This makes locale files immediately usable without blank values, and human
67
+ // translators can identify untranslated strings by comparing them to the
68
+ // default language file.
69
+ const langEntries = { ...entries };
69
70
  localeFiles.push({ language: lang, namespace, entries: langEntries, filePath: "" });
70
- }
71
+ }
71
72
  }
72
73
 
73
74
  return { localeFiles, keyCount: keyValueMap.size, namespaces: Array.from(namespaceMap.keys()) };
package/src/writer.ts CHANGED
@@ -6,49 +6,154 @@ export interface WriteOptions {
6
6
  localesDir: string;
7
7
  merge?: boolean;
8
8
  sort?: boolean;
9
+ /**
10
+ * Locale file layout.
11
+ *
12
+ * - `"nested"` (default) — one JSON file per language + namespace:
13
+ * `locales/en/common.json`, `locales/en/dashboard.json`
14
+ * For the "common" / default namespace the file is named `translation.json`.
15
+ *
16
+ * - `"flat"` — one JSON file per language, all namespaces merged:
17
+ * `locales/en.json`, `locales/fr.json`
18
+ * Namespace prefixes are prepended to keys to avoid collisions:
19
+ * `common.button.save`, `dashboard.header.title`
20
+ * (keys that already start with the namespace prefix are left as-is).
21
+ */
22
+ localeStructure?: "nested" | "flat";
9
23
  }
10
24
 
11
25
  export class LocaleWriter {
12
26
  private options: Required<WriteOptions>;
13
27
 
14
28
  constructor(options: WriteOptions) {
15
- this.options = { localesDir: options.localesDir, merge: options.merge ?? true, sort: options.sort ?? true };
29
+ this.options = {
30
+ localesDir: options.localesDir,
31
+ merge: options.merge ?? true,
32
+ sort: options.sort ?? true,
33
+ localeStructure: options.localeStructure ?? "nested",
34
+ };
16
35
  }
17
36
 
18
37
  write(localeFiles: LocaleFile[]): { written: string[]; merged: string[]; created: string[] } {
19
- const written: string[] = [];
38
+ if (this.options.localeStructure === "flat") {
39
+ return this.writeFlat(localeFiles);
40
+ }
41
+ return this.writeNested(localeFiles);
42
+ }
43
+
44
+ // ─── Nested layout ───────────────────────────────────────────────────────────
45
+
46
+ private writeNested(localeFiles: LocaleFile[]): { written: string[]; merged: string[]; created: string[] } {
47
+ const written: string[] = [];
20
48
  const merged: string[] = [];
21
49
  const created: string[] = [];
22
50
 
23
51
  for (const lf of localeFiles) {
24
- const filePath = this.resolveFilePath(lf);
25
- lf.filePath = filePath;
52
+ const filePath = this.resolveNestedFilePath(lf);
53
+ lf.filePath = filePath;
26
54
  ensureDir(path.dirname(filePath));
27
- const existing = readJsonSafe<Record<string, string>>(filePath);
55
+ const existing = readJsonSafe<Record<string, string>>(filePath);
28
56
 
29
57
  if (existing && this.options.merge) {
30
58
  const mergedEntries = this.mergeEntries(existing, lf.entries);
31
- writeJson(filePath, this.options.sort ? this.sort(mergedEntries) : mergedEntries);
32
- merged.push(filePath);
33
- } else {
59
+ writeJson(filePath, this.options.sort ? this.sort(mergedEntries) : mergedEntries);
60
+ merged.push(filePath);
61
+ } else {
34
62
  writeJson(filePath, this.options.sort ? this.sort(lf.entries) : lf.entries);
35
63
  created.push(filePath);
36
64
  }
37
65
  written.push(filePath);
38
66
  }
39
- return { written, merged, created };
67
+
68
+ return { written, merged, created };
40
69
  }
41
70
 
42
- private resolveFilePath(lf: LocaleFile): string {
71
+ private resolveNestedFilePath(lf: LocaleFile): string {
43
72
  return lf.namespace === "common"
44
- ? path.join(this.options.localesDir, lf.language, "translation.json")
73
+ ? path.join(this.options.localesDir, lf.language, "translation.json")
45
74
  : path.join(this.options.localesDir, lf.language, `${lf.namespace}.json`);
46
75
  }
47
76
 
48
- private mergeEntries(existing: Record<string, string>, incoming: Record<string, string>): Record<string, string> {
77
+ // ─── Flat layout ─────────────────────────────────────────────────────────────
78
+
79
+ /**
80
+ * In flat mode all LocaleFile entries for the same language are merged into
81
+ * a single `<localesDir>/<language>.json` file.
82
+ *
83
+ * To avoid key collisions between namespaces, each key is prefixed with its
84
+ * namespace name (e.g. `common.button.save`, `dashboard.header.title`).
85
+ * Keys that already start with the namespace prefix are left unchanged.
86
+ */
87
+ private writeFlat(localeFiles: LocaleFile[]): { written: string[]; merged: string[]; created: string[] } {
88
+ // Group locale files by language
89
+ const byLanguage = new Map<string, LocaleFile[]>();
90
+ for (const lf of localeFiles) {
91
+ const list = byLanguage.get(lf.language) ?? [];
92
+ list.push(lf);
93
+ byLanguage.set(lf.language, list);
94
+ }
95
+
96
+ const written: string[] = [];
97
+ const merged: string[] = [];
98
+ const created: string[] = [];
99
+
100
+ for (const [language, files] of byLanguage) {
101
+ const filePath = path.join(this.options.localesDir, `${language}.json`);
102
+ ensureDir(path.dirname(filePath));
103
+
104
+ // Merge all namespace entries into a single flat object
105
+ const incomingEntries: Record<string, string> = {};
106
+ for (const lf of files) {
107
+ for (const [key, value] of Object.entries(lf.entries)) {
108
+ const flatKey = this.flattenKey(lf.namespace, key);
109
+ incomingEntries[flatKey] = value;
110
+ }
111
+ }
112
+
113
+ const existing = readJsonSafe<Record<string, string>>(filePath);
114
+ if (existing && this.options.merge) {
115
+ const mergedEntries = this.mergeEntries(existing, incomingEntries);
116
+ writeJson(filePath, this.options.sort ? this.sort(mergedEntries) : mergedEntries);
117
+ merged.push(filePath);
118
+ } else {
119
+ writeJson(filePath, this.options.sort ? this.sort(incomingEntries) : incomingEntries);
120
+ created.push(filePath);
121
+ }
122
+
123
+ // Update filePath on each LocaleFile for reference
124
+ for (const lf of files) {
125
+ lf.filePath = filePath;
126
+ }
127
+
128
+ written.push(filePath);
129
+ }
130
+
131
+ return { written, merged, created };
132
+ }
133
+
134
+ /**
135
+ * Produces a flat key with the namespace prepended.
136
+ * If the key already starts with `<namespace>.` it is returned as-is
137
+ * to avoid double-prefixing (e.g. when namespace splitting already embedded it).
138
+ */
139
+ private flattenKey(namespace: string, key: string): string {
140
+ // "common" is the default namespace — don't prepend to avoid verbose keys
141
+ if (namespace === "common" || namespace === "translation") {
142
+ return key;
143
+ }
144
+ const prefix = `${namespace}.`;
145
+ return key.startsWith(prefix) ? key : `${prefix}${key}`;
146
+ }
147
+
148
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
149
+
150
+ private mergeEntries(
151
+ existing: Record<string, string>,
152
+ incoming: Record<string, string>
153
+ ): Record<string, string> {
49
154
  const result = { ...existing };
50
- for (const [key, value] of Object.entries(incoming)) {
51
- if (!(key in result) || result[key] === "") result[key] = value;
155
+ for (const [key, value] of Object.entries(incoming)) {
156
+ if (!(key in result) || result[key] === "") result[key] = value;
52
157
  }
53
158
  return result;
54
159
  }