@lang-tag/cli 0.13.1 → 0.15.0

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.
@@ -1,6 +1,76 @@
1
- import { sep } from "pathe";
1
+ import { T as TranslationsCollector, e as $LT_RemoveFile, c as $LT_EnsureDirectoryExists } from "../namespace-collector-DCruv_PK.js";
2
+ import { N } from "../namespace-collector-DCruv_PK.js";
3
+ import path, { resolve, sep } from "pathe";
4
+ import process__default from "node:process";
2
5
  import * as caseLib from "case";
3
- const TRIGGER_NAME$1 = "path-based-config-generator";
6
+ class DictionaryCollector extends TranslationsCollector {
7
+ constructor(options = {
8
+ appendNamespaceToPath: false
9
+ }) {
10
+ super();
11
+ this.options = options;
12
+ }
13
+ clean;
14
+ aggregateCollection(namespace) {
15
+ return this.config.baseLanguageCode;
16
+ }
17
+ transformTag(tag) {
18
+ const originalPath = tag.parameterConfig.path;
19
+ let path2 = originalPath;
20
+ if (this.options.appendNamespaceToPath) {
21
+ path2 = tag.parameterConfig.namespace;
22
+ if (originalPath) {
23
+ path2 += ".";
24
+ path2 += originalPath;
25
+ }
26
+ return {
27
+ ...tag,
28
+ parameterConfig: {
29
+ ...tag.parameterConfig,
30
+ namespace: void 0,
31
+ path: path2
32
+ }
33
+ };
34
+ }
35
+ return tag;
36
+ }
37
+ async preWrite(clean) {
38
+ this.clean = clean;
39
+ const baseDictionaryFile = path.join(this.config.localesDirectory, `${this.config.baseLanguageCode}.json`);
40
+ if (clean) {
41
+ this.logger.info("Removing {file}", { file: baseDictionaryFile });
42
+ await $LT_RemoveFile(baseDictionaryFile);
43
+ }
44
+ await $LT_EnsureDirectoryExists(this.config.localesDirectory);
45
+ }
46
+ async resolveCollectionFilePath(baseLanguageCode) {
47
+ return resolve(
48
+ process__default.cwd(),
49
+ this.config.localesDirectory,
50
+ baseLanguageCode + ".json"
51
+ );
52
+ }
53
+ async onMissingCollection(baseLanguageCode) {
54
+ if (!this.clean) {
55
+ this.logger.warn(`Original dictionary file "{namespace}.json" not found. A new one will be created.`, { namespace: baseLanguageCode });
56
+ }
57
+ }
58
+ async postWrite(changedCollections) {
59
+ if (!changedCollections?.length) {
60
+ this.logger.info("No changes were made based on the current configuration and files");
61
+ return;
62
+ }
63
+ if (changedCollections.length > 1) {
64
+ throw new Error("Should not write more than 1 collection! Only 1 base language dictionary expected!");
65
+ }
66
+ const dict = resolve(
67
+ this.config.localesDirectory,
68
+ this.config.baseLanguageCode + ".json"
69
+ );
70
+ this.logger.success("Updated dictionary {dict}", { dict });
71
+ }
72
+ }
73
+ const TRIGGER_NAME$2 = "path-based-config-generator";
4
74
  function pathBasedConfigGenerator(options = {}) {
5
75
  const {
6
76
  includeFileName = false,
@@ -56,11 +126,13 @@ function pathBasedConfigGenerator(options = {}) {
56
126
  }
57
127
  pathSegments = pathSegments.filter((seg) => !ignoreDirectories.includes(seg));
58
128
  let namespace;
59
- let path;
129
+ let path2;
60
130
  if (pathSegments.length >= 1) {
61
131
  namespace = pathSegments[0];
62
132
  if (pathSegments.length > 1) {
63
- path = pathSegments.slice(1).join(".");
133
+ path2 = pathSegments.slice(1).join(".");
134
+ } else {
135
+ path2 = "";
64
136
  }
65
137
  } else {
66
138
  namespace = actualFallbackNamespace;
@@ -73,22 +145,22 @@ function pathBasedConfigGenerator(options = {}) {
73
145
  namespace = applyCaseTransform(namespace, namespaceCase);
74
146
  }
75
147
  }
76
- if (path && pathCase) {
77
- const pathParts = path.split(".");
148
+ if (path2 && pathCase) {
149
+ const pathParts = path2.split(".");
78
150
  const transformedParts = pathParts.map((part) => applyCaseTransform(part, pathCase));
79
- path = transformedParts.join(".");
151
+ path2 = transformedParts.join(".");
80
152
  }
81
153
  const newConfig = event.config ? { ...event.config } : {};
82
154
  if (clearOnDefaultNamespace && namespace === actualFallbackNamespace) {
83
- if (path) {
84
- newConfig.path = path;
155
+ if (path2) {
156
+ newConfig.path = path2;
85
157
  delete newConfig.namespace;
86
158
  } else {
87
159
  const hasOtherProperties = event.config && Object.keys(event.config).some(
88
160
  (key) => key !== "namespace" && key !== "path"
89
161
  );
90
162
  if (!hasOtherProperties) {
91
- event.save(null, TRIGGER_NAME$1);
163
+ event.save(null, TRIGGER_NAME$2);
92
164
  return;
93
165
  } else {
94
166
  delete newConfig.namespace;
@@ -99,14 +171,14 @@ function pathBasedConfigGenerator(options = {}) {
99
171
  if (namespace) {
100
172
  newConfig.namespace = namespace;
101
173
  }
102
- if (path) {
103
- newConfig.path = path;
174
+ if (path2) {
175
+ newConfig.path = path2;
104
176
  } else {
105
177
  delete newConfig.path;
106
178
  }
107
179
  }
108
180
  if (Object.keys(newConfig).length > 0) {
109
- event.save(newConfig, TRIGGER_NAME$1);
181
+ event.save(newConfig, TRIGGER_NAME$2);
110
182
  }
111
183
  };
112
184
  }
@@ -144,11 +216,69 @@ function applyStructuredIgnore(segments, structure) {
144
216
  }
145
217
  return result;
146
218
  }
219
+ function addPathPrefixAndSegments(result, pathPrefix, remainingSegments) {
220
+ if (pathPrefix && remainingSegments.length > 0) {
221
+ const cleanPrefix = pathPrefix.endsWith(".") ? pathPrefix.slice(0, -1) : pathPrefix;
222
+ result.push(cleanPrefix, ...remainingSegments);
223
+ } else if (pathPrefix && remainingSegments.length === 0) {
224
+ const cleanPrefix = pathPrefix.endsWith(".") ? pathPrefix.slice(0, -1) : pathPrefix;
225
+ result.push(cleanPrefix);
226
+ } else if (remainingSegments.length > 0) {
227
+ result.push(...remainingSegments);
228
+ }
229
+ }
230
+ function processNamespaceRedirect(redirectRule, remainingSegments, options) {
231
+ const result = [];
232
+ if (redirectRule === null || redirectRule === void 0) {
233
+ if (options?.currentSegment !== void 0) {
234
+ if (!options.ignoreSelf) {
235
+ result.push(options.renameTo || options.currentSegment);
236
+ }
237
+ }
238
+ result.push(...remainingSegments);
239
+ } else if (typeof redirectRule === "string") {
240
+ if (redirectRule === "") {
241
+ if (options?.currentSegment !== void 0) {
242
+ if (!options.ignoreSelf) {
243
+ result.push(options.renameTo || options.currentSegment);
244
+ }
245
+ }
246
+ result.push(...remainingSegments);
247
+ } else {
248
+ result.push(redirectRule);
249
+ result.push(...remainingSegments);
250
+ }
251
+ } else if (typeof redirectRule === "object" && redirectRule !== null) {
252
+ const namespace = redirectRule.namespace;
253
+ const pathPrefix = redirectRule.pathPrefix || "";
254
+ if (namespace === void 0 || namespace === null || namespace === "") {
255
+ if (options?.currentSegment !== void 0) {
256
+ if (!options.ignoreSelf) {
257
+ result.push(options.renameTo || options.currentSegment);
258
+ }
259
+ }
260
+ addPathPrefixAndSegments(result, pathPrefix, remainingSegments);
261
+ } else {
262
+ result.push(namespace);
263
+ addPathPrefixAndSegments(result, pathPrefix, remainingSegments);
264
+ }
265
+ }
266
+ return result;
267
+ }
147
268
  function applyPathRules(segments, structure) {
148
269
  const result = [];
149
270
  let currentStructure = structure;
271
+ let deepestRedirect = null;
150
272
  for (let i = 0; i < segments.length; i++) {
151
273
  const segment = segments[i];
274
+ if (">>" in currentStructure && (!deepestRedirect || !deepestRedirect.context)) {
275
+ const redirectRule = currentStructure[">>"];
276
+ const remainingSegments = segments.slice(i);
277
+ deepestRedirect = {
278
+ rule: redirectRule,
279
+ remainingSegments
280
+ };
281
+ }
152
282
  if (segment in currentStructure) {
153
283
  const rule = currentStructure[segment];
154
284
  if (rule === true) {
@@ -171,6 +301,22 @@ function applyPathRules(segments, structure) {
171
301
  } else if (typeof rule === "object" && rule !== null) {
172
302
  const ignoreSelf = rule["_"] === false;
173
303
  const renameTo = rule[">"];
304
+ const redirectRule = rule[">>"];
305
+ if (">>" in rule) {
306
+ const remainingSegments = segments.slice(i + 1);
307
+ const ruleWithoutRedirect = { ...rule };
308
+ delete ruleWithoutRedirect[">>"];
309
+ const processedRemaining = applyPathRules(remainingSegments, ruleWithoutRedirect);
310
+ deepestRedirect = {
311
+ rule: redirectRule,
312
+ remainingSegments: processedRemaining,
313
+ context: {
314
+ currentSegment: segment,
315
+ renameTo,
316
+ ignoreSelf
317
+ }
318
+ };
319
+ }
174
320
  if (!ignoreSelf) {
175
321
  if (typeof renameTo === "string") {
176
322
  result.push(renameTo);
@@ -185,6 +331,13 @@ function applyPathRules(segments, structure) {
185
331
  result.push(segment);
186
332
  currentStructure = structure;
187
333
  }
334
+ if (deepestRedirect) {
335
+ return processNamespaceRedirect(
336
+ deepestRedirect.rule,
337
+ deepestRedirect.remainingSegments,
338
+ deepestRedirect.context
339
+ );
340
+ }
188
341
  return result;
189
342
  }
190
343
  function applyCaseTransform(str, caseType) {
@@ -211,7 +364,7 @@ function extractRootDirectoriesFromIncludes(includes) {
211
364
  }
212
365
  return Array.from(directories);
213
366
  }
214
- const TRIGGER_NAME = "config-keeper";
367
+ const TRIGGER_NAME$1 = "config-keeper";
215
368
  function configKeeper(options = {}) {
216
369
  const propertyName = options.propertyName ?? "keep";
217
370
  const keepPropertyAtEnd = options.keepPropertyAtEnd ?? true;
@@ -268,10 +421,35 @@ function configKeeper(options = {}) {
268
421
  delete finalConfig[propertyName];
269
422
  }
270
423
  finalConfig[propertyName] = keepMode;
271
- event.save(finalConfig, TRIGGER_NAME);
424
+ event.save(finalConfig, TRIGGER_NAME$1);
425
+ };
426
+ }
427
+ const TRIGGER_NAME = "prepend-namespace-to-path";
428
+ function prependNamespaceToPath(options = {}) {
429
+ return async (event) => {
430
+ const currentConfig = event.savedConfig;
431
+ const { namespace, path: path2 } = currentConfig || {};
432
+ const actualNamespace = namespace || event.langTagConfig.collect?.defaultNamespace;
433
+ if (!actualNamespace) {
434
+ return;
435
+ }
436
+ let newPath;
437
+ if (path2) {
438
+ newPath = `${actualNamespace}.${path2}`;
439
+ } else {
440
+ newPath = actualNamespace;
441
+ }
442
+ event.save({
443
+ ...currentConfig || {},
444
+ path: newPath,
445
+ namespace: void 0
446
+ }, TRIGGER_NAME);
272
447
  };
273
448
  }
274
449
  export {
450
+ DictionaryCollector,
451
+ N as NamespaceCollector,
275
452
  configKeeper,
276
- pathBasedConfigGenerator
453
+ pathBasedConfigGenerator,
454
+ prependNamespaceToPath
277
455
  };
package/config.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { LangTagTranslationsConfig } from 'lang-tag';
2
2
  import { LangTagCLILogger } from './logger.ts';
3
+ import { TranslationsCollector } from './algorithms/collector/type.ts';
3
4
  export interface LangTagCLIConfig {
4
5
  /**
5
6
  * Tag name used to mark translations in code.
@@ -17,11 +18,37 @@ export interface LangTagCLIConfig {
17
18
  */
18
19
  excludes: string[];
19
20
  /**
20
- * Output directory for generated translation namespace files (e.g., common.json, errors.json).
21
- * @default 'locales/en'
21
+ * Root directory for translation files.
22
+ * The actual file structure depends on the collector implementation used.
23
+ * @default 'locales'
24
+ * @example With baseLanguageCode='en' and localesDirectory='locales':
25
+ * - NamespaceCollector (default): locales/en/common.json, locales/en/errors.json
26
+ * - DictionaryCollector: locales/en.json (all translations in one file)
22
27
  */
23
- outputDir: string;
28
+ localesDirectory: string;
29
+ /**
30
+ * The language in which translation values/messages are written in the codebase.
31
+ * This determines the source language for your translations.
32
+ * @default 'en'
33
+ * @example 'en' - Translation values are in English: lang({ helloWorld: 'Hello World' })
34
+ * @example 'pl' - Translation values are in Polish: lang({ helloWorld: 'Witaj Świecie' })
35
+ */
36
+ baseLanguageCode: string;
37
+ /**
38
+ * Indicates whether this configuration is for a translation library.
39
+ * If true, generates an exports file (`.lang-tag.exports.json`) instead of locale files.
40
+ * @default false
41
+ */
42
+ isLibrary: boolean;
24
43
  collect?: {
44
+ /**
45
+ * Translation collector that defines how translation tags are organized into output files.
46
+ * If not specified, NamespaceCollector is used by default.
47
+ * @default NamespaceCollector
48
+ * @example DictionaryCollector - All translations in single file per language
49
+ * @example NamespaceCollector - Separate files per namespace within language directory
50
+ */
51
+ collector?: TranslationsCollector;
25
52
  /**
26
53
  * @default 'common'
27
54
  */
@@ -50,50 +77,6 @@ export interface LangTagCLIConfig {
50
77
  */
51
78
  onCollectFinish?: (event: LangTagCLICollectFinishEvent) => void;
52
79
  };
53
- import: {
54
- /**
55
- * Output directory for generated files containing imported library tags.
56
- * @default 'src/lang-libraries'
57
- */
58
- dir: string;
59
- /**
60
- * The import statement used in generated library files to import the project's `lang` tag function.
61
- * @default 'import { lang } from "@/my-lang-tag-path"'
62
- */
63
- tagImportPath: string;
64
- /**
65
- * A function to customize the generated file name and export name for imported library tags.
66
- * Allows controlling how imported tags are organized and named within the generated files.
67
- */
68
- onImport: (params: LangTagCLIOnImportParams, actions: LangTagCLIOnImportActions) => void;
69
- /**
70
- * A function called after all lang-tags were imported
71
- */
72
- onImportFinish?: () => void;
73
- };
74
- /**
75
- * Determines the position of the translation argument in the `lang()` function.
76
- * If `1`, translations are in the first argument (`lang(translations, options)`).
77
- * If `2`, translations are in the second argument (`lang(options, translations)`).
78
- * @default 1
79
- */
80
- translationArgPosition: 1 | 2;
81
- /**
82
- * Primary language used for the library's translations.
83
- * Affects default language settings when used in library mode.
84
- * @default 'en'
85
- */
86
- language: string;
87
- /**
88
- * Indicates whether this configuration is for a translation library.
89
- * If true, generates an exports file (`.lang-tag.exports.json`) instead of locale files.
90
- * @default false
91
- */
92
- isLibrary: boolean;
93
- /**
94
- * Whether to flatten the translation keys. (Currently unused)
95
- * @default false
96
- */
97
80
  /**
98
81
  * A function called for each found lang tag before processing.
99
82
  * Allows dynamic modification of the tag's configuration (namespace, path, etc.)
@@ -123,6 +106,34 @@ export interface LangTagCLIConfig {
123
106
  * ```
124
107
  */
125
108
  onConfigGeneration: (event: LangTagCLIConfigGenerationEvent) => Promise<void>;
109
+ import: {
110
+ /**
111
+ * Output directory for generated files containing imported library tags.
112
+ * @default 'src/lang-libraries'
113
+ */
114
+ dir: string;
115
+ /**
116
+ * The import statement used in generated library files to import the project's `lang` tag function.
117
+ * @default 'import { lang } from "@/my-lang-tag-path"'
118
+ */
119
+ tagImportPath: string;
120
+ /**
121
+ * A function to customize the generated file name and export name for imported library tags.
122
+ * Allows controlling how imported tags are organized and named within the generated files.
123
+ */
124
+ onImport: (params: LangTagCLIOnImportParams, actions: LangTagCLIOnImportActions) => void;
125
+ /**
126
+ * A function called after all lang-tags were imported
127
+ */
128
+ onImportFinish?: () => void;
129
+ };
130
+ /**
131
+ * Determines the position of the translation argument in the `lang()` function.
132
+ * If `1`, translations are in the first argument (`lang(translations, options)`).
133
+ * If `2`, translations are in the second argument (`lang(options, translations)`).
134
+ * @default 1
135
+ */
136
+ translationArgPosition: 1 | 2;
126
137
  debug?: boolean;
127
138
  }
128
139
  /**