@lang-tag/cli 0.12.0 → 0.12.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.
@@ -10,6 +10,20 @@ export interface ConfigKeeperOptions {
10
10
  * ```
11
11
  */
12
12
  propertyName?: string;
13
+ /**
14
+ * When true, ensures the keep property is always placed at the end of the configuration object.
15
+ * This improves readability by keeping metadata properties separate from core config.
16
+ * @default true
17
+ * @example
18
+ * ```tsx
19
+ * // With keepPropertyAtEnd: true
20
+ * { namespace: 'common', path: 'button', keep: 'namespace' }
21
+ *
22
+ * // With keepPropertyAtEnd: false
23
+ * // Order is not guaranteed
24
+ * ```
25
+ */
26
+ keepPropertyAtEnd?: boolean;
13
27
  }
14
28
  /**
15
29
  * Creates a config keeper algorithm that preserves original configuration values
@@ -24,17 +24,24 @@ export interface PathBasedConfigGeneratorOptions {
24
24
  */
25
25
  ignoreDirectories?: string[];
26
26
  /**
27
- * When true, automatically extracts root directory names from the config.includes patterns
28
- * and adds them to the ignoreDirectories list.
27
+ * When true, automatically removes the root directory from the path if it matches
28
+ * one of the root directories extracted from config.includes patterns.
29
+ * Unlike ignoreDirectories, this only removes the FIRST occurrence if the path starts with it.
29
30
  *
30
31
  * @default false
31
32
  *
32
33
  * @example
33
34
  * // With includes: ['src/**\/*.{js,ts,jsx,tsx}']
34
- * // Automatically ignores: ['src']
35
+ * // Path: 'src/components/Button.tsx' → removes root 'src' → 'components/Button.tsx'
35
36
  *
36
- * // With includes: ['(src|app)/**\/*.{js,ts,jsx,tsx}', 'components/**\/*.{jsx,tsx}']
37
- * // Automatically ignores: ['src', 'app', 'components']
37
+ * // With includes: ['app/**\/*.{js,ts,jsx,tsx}', 'components/**\/*.{jsx,tsx}']
38
+ * // Path: 'app/dashboard/components/utils.tsx' → removes root 'app' 'dashboard/components/utils.tsx'
39
+ * // Note: 'components' in the middle is NOT removed (only root occurrences)
40
+ *
41
+ * // With includes: ['(src|app)/**\/*.{js,ts,jsx,tsx}']
42
+ * // Extracts root directories: ['src', 'app']
43
+ * // Path: 'src/features/auth.tsx' → removes root 'src' → 'features/auth.tsx'
44
+ * // Path: 'app/pages/home.tsx' → removes root 'app' → 'pages/home.tsx'
38
45
  */
39
46
  ignoreIncludesRootDirectories?: boolean;
40
47
  /**
@@ -36,11 +36,6 @@ function pathBasedConfigGenerator(options = {}) {
36
36
  return async (event) => {
37
37
  const { relativePath, langTagConfig } = event;
38
38
  const actualFallbackNamespace = fallbackNamespace ?? langTagConfig.collect?.defaultNamespace;
39
- let finalIgnoreDirectories = [...ignoreDirectories];
40
- if (ignoreIncludesRootDirectories && langTagConfig.includes) {
41
- const extractedDirectories = extractRootDirectoriesFromIncludes(langTagConfig.includes);
42
- finalIgnoreDirectories = [.../* @__PURE__ */ new Set([...finalIgnoreDirectories, ...extractedDirectories])];
43
- }
44
39
  let pathSegments = relativePath.split(path.sep).filter(Boolean);
45
40
  if (pathSegments.length === 0) {
46
41
  return;
@@ -59,8 +54,14 @@ function pathBasedConfigGenerator(options = {}) {
59
54
  }
60
55
  return segment;
61
56
  }).filter((seg) => seg !== null);
57
+ if (ignoreIncludesRootDirectories && langTagConfig.includes && pathSegments.length > 0) {
58
+ const extractedDirectories = extractRootDirectoriesFromIncludes(langTagConfig.includes);
59
+ if (extractedDirectories.includes(pathSegments[0])) {
60
+ pathSegments = pathSegments.slice(1);
61
+ }
62
+ }
62
63
  pathSegments = applyStructuredIgnore(pathSegments, ignoreStructured);
63
- pathSegments = pathSegments.filter((seg) => !finalIgnoreDirectories.includes(seg));
64
+ pathSegments = pathSegments.filter((seg) => !ignoreDirectories.includes(seg));
64
65
  let namespace;
65
66
  let path$1;
66
67
  if (pathSegments.length >= 1) {
@@ -84,13 +85,22 @@ function pathBasedConfigGenerator(options = {}) {
84
85
  const transformedParts = pathParts.map((part) => applyCaseTransform(part, pathCase));
85
86
  path$1 = transformedParts.join(".");
86
87
  }
87
- const newConfig = {};
88
+ const newConfig = event.config ? { ...event.config } : {};
88
89
  if (clearOnDefaultNamespace && namespace === actualFallbackNamespace) {
89
90
  if (path$1) {
90
91
  newConfig.path = path$1;
92
+ delete newConfig.namespace;
91
93
  } else {
92
- event.save(null, TRIGGER_NAME$1);
93
- return;
94
+ const hasOtherProperties = event.config && Object.keys(event.config).some(
95
+ (key) => key !== "namespace" && key !== "path"
96
+ );
97
+ if (!hasOtherProperties) {
98
+ event.save(null, TRIGGER_NAME$1);
99
+ return;
100
+ } else {
101
+ delete newConfig.namespace;
102
+ delete newConfig.path;
103
+ }
94
104
  }
95
105
  } else {
96
106
  if (namespace) {
@@ -98,6 +108,8 @@ function pathBasedConfigGenerator(options = {}) {
98
108
  }
99
109
  if (path$1) {
100
110
  newConfig.path = path$1;
111
+ } else {
112
+ delete newConfig.path;
101
113
  }
102
114
  }
103
115
  if (Object.keys(newConfig).length > 0) {
@@ -160,6 +172,7 @@ function extractRootDirectoriesFromIncludes(includes) {
160
172
  const TRIGGER_NAME = "config-keeper";
161
173
  function configKeeper(options = {}) {
162
174
  const propertyName = options.propertyName ?? "keep";
175
+ const keepPropertyAtEnd = options.keepPropertyAtEnd ?? true;
163
176
  return async (event) => {
164
177
  if (!event.isSaved) {
165
178
  return;
@@ -182,14 +195,38 @@ function configKeeper(options = {}) {
182
195
  } else {
183
196
  restoredConfig = { ...event.savedConfig };
184
197
  }
185
- if ((keepMode === "namespace" || keepMode === "both") && event.config.namespace !== void 0) {
186
- restoredConfig.namespace = event.config.namespace;
198
+ let needsSave = false;
199
+ const restorePropertyIfNeeded = (propertyKey) => {
200
+ if (!event.config) return;
201
+ const shouldRestore = (keepMode === propertyKey || keepMode === "both") && event.config[propertyKey] !== void 0;
202
+ if (shouldRestore && restoredConfig[propertyKey] !== event.config[propertyKey]) {
203
+ restoredConfig[propertyKey] = event.config[propertyKey];
204
+ needsSave = true;
205
+ }
206
+ };
207
+ restorePropertyIfNeeded("namespace");
208
+ restorePropertyIfNeeded("path");
209
+ const keepPropertyExistedBefore = event.savedConfig && event.savedConfig[propertyName] !== void 0;
210
+ if (!keepPropertyExistedBefore) {
211
+ needsSave = true;
212
+ }
213
+ if (keepPropertyAtEnd && event.savedConfig && !needsSave) {
214
+ const savedKeys = Object.keys(event.savedConfig);
215
+ const keepIndex = savedKeys.indexOf(propertyName);
216
+ const isKeepAtEnd = keepIndex === savedKeys.length - 1;
217
+ if (!isKeepAtEnd && keepIndex !== -1) {
218
+ needsSave = true;
219
+ }
220
+ }
221
+ if (!needsSave) {
222
+ return;
187
223
  }
188
- if ((keepMode === "path" || keepMode === "both") && event.config.path !== void 0) {
189
- restoredConfig.path = event.config.path;
224
+ const finalConfig = { ...restoredConfig };
225
+ if (keepPropertyAtEnd) {
226
+ delete finalConfig[propertyName];
190
227
  }
191
- restoredConfig[propertyName] = keepMode;
192
- event.save(restoredConfig, TRIGGER_NAME);
228
+ finalConfig[propertyName] = keepMode;
229
+ event.save(finalConfig, TRIGGER_NAME);
193
230
  };
194
231
  }
195
232
  exports.configKeeper = configKeeper;
@@ -17,11 +17,6 @@ function pathBasedConfigGenerator(options = {}) {
17
17
  return async (event) => {
18
18
  const { relativePath, langTagConfig } = event;
19
19
  const actualFallbackNamespace = fallbackNamespace ?? langTagConfig.collect?.defaultNamespace;
20
- let finalIgnoreDirectories = [...ignoreDirectories];
21
- if (ignoreIncludesRootDirectories && langTagConfig.includes) {
22
- const extractedDirectories = extractRootDirectoriesFromIncludes(langTagConfig.includes);
23
- finalIgnoreDirectories = [.../* @__PURE__ */ new Set([...finalIgnoreDirectories, ...extractedDirectories])];
24
- }
25
20
  let pathSegments = relativePath.split(sep).filter(Boolean);
26
21
  if (pathSegments.length === 0) {
27
22
  return;
@@ -40,8 +35,14 @@ function pathBasedConfigGenerator(options = {}) {
40
35
  }
41
36
  return segment;
42
37
  }).filter((seg) => seg !== null);
38
+ if (ignoreIncludesRootDirectories && langTagConfig.includes && pathSegments.length > 0) {
39
+ const extractedDirectories = extractRootDirectoriesFromIncludes(langTagConfig.includes);
40
+ if (extractedDirectories.includes(pathSegments[0])) {
41
+ pathSegments = pathSegments.slice(1);
42
+ }
43
+ }
43
44
  pathSegments = applyStructuredIgnore(pathSegments, ignoreStructured);
44
- pathSegments = pathSegments.filter((seg) => !finalIgnoreDirectories.includes(seg));
45
+ pathSegments = pathSegments.filter((seg) => !ignoreDirectories.includes(seg));
45
46
  let namespace;
46
47
  let path;
47
48
  if (pathSegments.length >= 1) {
@@ -65,13 +66,22 @@ function pathBasedConfigGenerator(options = {}) {
65
66
  const transformedParts = pathParts.map((part) => applyCaseTransform(part, pathCase));
66
67
  path = transformedParts.join(".");
67
68
  }
68
- const newConfig = {};
69
+ const newConfig = event.config ? { ...event.config } : {};
69
70
  if (clearOnDefaultNamespace && namespace === actualFallbackNamespace) {
70
71
  if (path) {
71
72
  newConfig.path = path;
73
+ delete newConfig.namespace;
72
74
  } else {
73
- event.save(null, TRIGGER_NAME$1);
74
- return;
75
+ const hasOtherProperties = event.config && Object.keys(event.config).some(
76
+ (key) => key !== "namespace" && key !== "path"
77
+ );
78
+ if (!hasOtherProperties) {
79
+ event.save(null, TRIGGER_NAME$1);
80
+ return;
81
+ } else {
82
+ delete newConfig.namespace;
83
+ delete newConfig.path;
84
+ }
75
85
  }
76
86
  } else {
77
87
  if (namespace) {
@@ -79,6 +89,8 @@ function pathBasedConfigGenerator(options = {}) {
79
89
  }
80
90
  if (path) {
81
91
  newConfig.path = path;
92
+ } else {
93
+ delete newConfig.path;
82
94
  }
83
95
  }
84
96
  if (Object.keys(newConfig).length > 0) {
@@ -141,6 +153,7 @@ function extractRootDirectoriesFromIncludes(includes) {
141
153
  const TRIGGER_NAME = "config-keeper";
142
154
  function configKeeper(options = {}) {
143
155
  const propertyName = options.propertyName ?? "keep";
156
+ const keepPropertyAtEnd = options.keepPropertyAtEnd ?? true;
144
157
  return async (event) => {
145
158
  if (!event.isSaved) {
146
159
  return;
@@ -163,14 +176,38 @@ function configKeeper(options = {}) {
163
176
  } else {
164
177
  restoredConfig = { ...event.savedConfig };
165
178
  }
166
- if ((keepMode === "namespace" || keepMode === "both") && event.config.namespace !== void 0) {
167
- restoredConfig.namespace = event.config.namespace;
179
+ let needsSave = false;
180
+ const restorePropertyIfNeeded = (propertyKey) => {
181
+ if (!event.config) return;
182
+ const shouldRestore = (keepMode === propertyKey || keepMode === "both") && event.config[propertyKey] !== void 0;
183
+ if (shouldRestore && restoredConfig[propertyKey] !== event.config[propertyKey]) {
184
+ restoredConfig[propertyKey] = event.config[propertyKey];
185
+ needsSave = true;
186
+ }
187
+ };
188
+ restorePropertyIfNeeded("namespace");
189
+ restorePropertyIfNeeded("path");
190
+ const keepPropertyExistedBefore = event.savedConfig && event.savedConfig[propertyName] !== void 0;
191
+ if (!keepPropertyExistedBefore) {
192
+ needsSave = true;
193
+ }
194
+ if (keepPropertyAtEnd && event.savedConfig && !needsSave) {
195
+ const savedKeys = Object.keys(event.savedConfig);
196
+ const keepIndex = savedKeys.indexOf(propertyName);
197
+ const isKeepAtEnd = keepIndex === savedKeys.length - 1;
198
+ if (!isKeepAtEnd && keepIndex !== -1) {
199
+ needsSave = true;
200
+ }
201
+ }
202
+ if (!needsSave) {
203
+ return;
168
204
  }
169
- if ((keepMode === "path" || keepMode === "both") && event.config.path !== void 0) {
170
- restoredConfig.path = event.config.path;
205
+ const finalConfig = { ...restoredConfig };
206
+ if (keepPropertyAtEnd) {
207
+ delete finalConfig[propertyName];
171
208
  }
172
- restoredConfig[propertyName] = keepMode;
173
- event.save(restoredConfig, TRIGGER_NAME);
209
+ finalConfig[propertyName] = keepMode;
210
+ event.save(finalConfig, TRIGGER_NAME);
174
211
  };
175
212
  }
176
213
  export {
package/index.cjs CHANGED
@@ -173,7 +173,7 @@ class $LT_TagProcessor {
173
173
  }
174
174
  const tag = R.tag;
175
175
  let newTranslationsString = R.translations;
176
- if (!newTranslationsString) newTranslationsString = this.config.translationArgPosition === 1 ? tag.parameter1Text : tag.parameter2Text;
176
+ if (!newTranslationsString) newTranslationsString = this.config.translationArgPosition === 1 ? tag.parameter1Text : tag.parameter2Text || "{}";
177
177
  else if (typeof newTranslationsString !== "string") newTranslationsString = JSON5.stringify(newTranslationsString);
178
178
  if (!newTranslationsString) throw new Error("Tag must have translations provided!");
179
179
  try {
@@ -191,6 +191,9 @@ class $LT_TagProcessor {
191
191
  throw new Error(`Tag config is invalid object! Config: ${newConfigString}`);
192
192
  }
193
193
  }
194
+ if (newConfigString === null && this.config.translationArgPosition === 2) {
195
+ newConfigString = "{}";
196
+ }
194
197
  const arg1 = this.config.translationArgPosition === 1 ? newTranslationsString : newConfigString;
195
198
  const arg2 = this.config.translationArgPosition === 1 ? newConfigString : newTranslationsString;
196
199
  let tagFunction = `${this.config.tagName}(${arg1}`;
@@ -512,7 +515,7 @@ const LANG_TAG_DEFAULT_CONFIG = {
512
515
  await event.logger.conflict(event.conflict, true);
513
516
  },
514
517
  onCollectFinish: (event) => {
515
- event.exit();
518
+ if (event.conflicts.length) event.exit();
516
519
  }
517
520
  },
518
521
  import: {
@@ -1215,18 +1218,18 @@ async function $LT_GroupTagsToNamespaces({ logger, files, config }) {
1215
1218
  }
1216
1219
  if (allConflicts.length > 0) {
1217
1220
  logger.warn(`Found ${allConflicts.length} conflicts.`);
1218
- if (config.collect?.onCollectFinish) {
1219
- let shouldContinue = true;
1220
- config.collect.onCollectFinish({
1221
- conflicts: allConflicts,
1222
- logger,
1223
- exit() {
1224
- shouldContinue = false;
1225
- }
1226
- });
1227
- if (!shouldContinue) {
1228
- throw new Error(`LangTagConflictResolution:Processing stopped due to collect finish handler`);
1221
+ }
1222
+ if (config.collect?.onCollectFinish) {
1223
+ let shouldContinue = true;
1224
+ config.collect.onCollectFinish({
1225
+ conflicts: allConflicts,
1226
+ logger,
1227
+ exit() {
1228
+ shouldContinue = false;
1229
1229
  }
1230
+ });
1231
+ if (!shouldContinue) {
1232
+ throw new Error(`LangTagConflictResolution:Processing stopped due to collect finish handler`);
1230
1233
  }
1231
1234
  }
1232
1235
  return namespaces;
@@ -1478,7 +1481,7 @@ const config = {
1478
1481
  // Call event.exit(); to terminate the process upon the first conflict
1479
1482
  },
1480
1483
  onCollectFinish: event => {
1481
- event.exit(); // Stop the process to avoid merging on conflict
1484
+ if (event.conflicts.length) event.exit(); // Stop the process to avoid merging on conflict
1482
1485
  }
1483
1486
  },
1484
1487
  translationArgPosition: 1,
package/index.js CHANGED
@@ -153,7 +153,7 @@ class $LT_TagProcessor {
153
153
  }
154
154
  const tag = R.tag;
155
155
  let newTranslationsString = R.translations;
156
- if (!newTranslationsString) newTranslationsString = this.config.translationArgPosition === 1 ? tag.parameter1Text : tag.parameter2Text;
156
+ if (!newTranslationsString) newTranslationsString = this.config.translationArgPosition === 1 ? tag.parameter1Text : tag.parameter2Text || "{}";
157
157
  else if (typeof newTranslationsString !== "string") newTranslationsString = JSON5.stringify(newTranslationsString);
158
158
  if (!newTranslationsString) throw new Error("Tag must have translations provided!");
159
159
  try {
@@ -171,6 +171,9 @@ class $LT_TagProcessor {
171
171
  throw new Error(`Tag config is invalid object! Config: ${newConfigString}`);
172
172
  }
173
173
  }
174
+ if (newConfigString === null && this.config.translationArgPosition === 2) {
175
+ newConfigString = "{}";
176
+ }
174
177
  const arg1 = this.config.translationArgPosition === 1 ? newTranslationsString : newConfigString;
175
178
  const arg2 = this.config.translationArgPosition === 1 ? newConfigString : newTranslationsString;
176
179
  let tagFunction = `${this.config.tagName}(${arg1}`;
@@ -492,7 +495,7 @@ const LANG_TAG_DEFAULT_CONFIG = {
492
495
  await event.logger.conflict(event.conflict, true);
493
496
  },
494
497
  onCollectFinish: (event) => {
495
- event.exit();
498
+ if (event.conflicts.length) event.exit();
496
499
  }
497
500
  },
498
501
  import: {
@@ -1195,18 +1198,18 @@ async function $LT_GroupTagsToNamespaces({ logger, files, config }) {
1195
1198
  }
1196
1199
  if (allConflicts.length > 0) {
1197
1200
  logger.warn(`Found ${allConflicts.length} conflicts.`);
1198
- if (config.collect?.onCollectFinish) {
1199
- let shouldContinue = true;
1200
- config.collect.onCollectFinish({
1201
- conflicts: allConflicts,
1202
- logger,
1203
- exit() {
1204
- shouldContinue = false;
1205
- }
1206
- });
1207
- if (!shouldContinue) {
1208
- throw new Error(`LangTagConflictResolution:Processing stopped due to collect finish handler`);
1201
+ }
1202
+ if (config.collect?.onCollectFinish) {
1203
+ let shouldContinue = true;
1204
+ config.collect.onCollectFinish({
1205
+ conflicts: allConflicts,
1206
+ logger,
1207
+ exit() {
1208
+ shouldContinue = false;
1209
1209
  }
1210
+ });
1211
+ if (!shouldContinue) {
1212
+ throw new Error(`LangTagConflictResolution:Processing stopped due to collect finish handler`);
1210
1213
  }
1211
1214
  }
1212
1215
  return namespaces;
@@ -1458,7 +1461,7 @@ const config = {
1458
1461
  // Call event.exit(); to terminate the process upon the first conflict
1459
1462
  },
1460
1463
  onCollectFinish: event => {
1461
- event.exit(); // Stop the process to avoid merging on conflict
1464
+ if (event.conflicts.length) event.exit(); // Stop the process to avoid merging on conflict
1462
1465
  }
1463
1466
  },
1464
1467
  translationArgPosition: 1,
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@lang-tag/cli",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "https://github.com/TheTonsOfCode/lang-tag"
10
+ "url": "https://github.com/TheTonsOfCode/lang-tag-cli"
11
11
  },
12
12
  "author": "TheTonsOfCode",
13
13
  "license": "MIT",
@@ -10,7 +10,7 @@ import {
10
10
  import { createCallableTranslations } from 'lang-tag';
11
11
  {{/isTypeScript}}
12
12
  {{#isReact}}
13
- import { ReactNode, useMemo } from 'react';
13
+ import React, { ReactNode, useMemo } from 'react';
14
14
  {{/isReact}}
15
15
 
16
16
  {{#isTypeScript}}
@@ -13,7 +13,7 @@ import {
13
13
  import { createCallableTranslations, normalizeTranslations, lookupTranslation } from 'lang-tag';
14
14
  {{/isTypeScript}}
15
15
  {{#isReact}}
16
- import {
16
+ import React, {
17
17
  createContext,
18
18
  useContext,
19
19
  useMemo,
@@ -49,7 +49,12 @@ export function processPlaceholders(
49
49
  return parts.join('');
50
50
  }
51
51
 
52
- return parts{{#isTypeScript}} as unknown as string{{/isTypeScript}};
52
+ return parts.map((part, index) => {
53
+ if (React.isValidElement(part)) {
54
+ return React.cloneElement(part, { key: index });
55
+ }
56
+ return React.createElement(React.Fragment, { key: index }, part);
57
+ }){{#isTypeScript}} as unknown as string{{/isTypeScript}};
53
58
  }
54
59
  {{/isReact}}
55
60