i18next-cli 1.42.4 → 1.42.6

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 (35) hide show
  1. package/dist/cjs/cli.js +7 -7
  2. package/dist/cjs/extractor/core/ast-visitors.js +0 -4
  3. package/dist/cjs/extractor/core/extractor.js +8 -4
  4. package/dist/cjs/extractor/core/translation-manager.js +12 -3
  5. package/dist/cjs/extractor/parsers/ast-utils.js +69 -2
  6. package/dist/cjs/extractor/parsers/call-expression-handler.js +12 -42
  7. package/dist/cjs/extractor/parsers/jsx-handler.js +5 -28
  8. package/dist/cjs/locize.js +3 -3
  9. package/dist/cjs/rename-key.js +1 -1
  10. package/dist/cjs/status.js +1 -1
  11. package/dist/cjs/utils/file-utils.js +38 -0
  12. package/dist/esm/cli.js +7 -7
  13. package/dist/esm/extractor/core/ast-visitors.js +0 -4
  14. package/dist/esm/extractor/core/extractor.js +9 -5
  15. package/dist/esm/extractor/core/translation-manager.js +14 -5
  16. package/dist/esm/extractor/parsers/ast-utils.js +68 -3
  17. package/dist/esm/extractor/parsers/call-expression-handler.js +13 -43
  18. package/dist/esm/extractor/parsers/jsx-handler.js +6 -29
  19. package/dist/esm/locize.js +3 -3
  20. package/dist/esm/rename-key.js +1 -1
  21. package/dist/esm/status.js +1 -1
  22. package/dist/esm/utils/file-utils.js +38 -1
  23. package/package.json +1 -1
  24. package/types/extractor/core/ast-visitors.d.ts +0 -1
  25. package/types/extractor/core/ast-visitors.d.ts.map +1 -1
  26. package/types/extractor/core/extractor.d.ts.map +1 -1
  27. package/types/extractor/core/translation-manager.d.ts.map +1 -1
  28. package/types/extractor/parsers/ast-utils.d.ts +28 -2
  29. package/types/extractor/parsers/ast-utils.d.ts.map +1 -1
  30. package/types/extractor/parsers/call-expression-handler.d.ts +4 -8
  31. package/types/extractor/parsers/call-expression-handler.d.ts.map +1 -1
  32. package/types/extractor/parsers/jsx-handler.d.ts +3 -6
  33. package/types/extractor/parsers/jsx-handler.d.ts.map +1 -1
  34. package/types/utils/file-utils.d.ts +16 -0
  35. package/types/utils/file-utils.d.ts.map +1 -1
package/dist/cjs/cli.js CHANGED
@@ -28,7 +28,7 @@ const program = new commander.Command();
28
28
  program
29
29
  .name('i18next-cli')
30
30
  .description('A unified, high-performance i18next CLI.')
31
- .version('1.42.4'); // This string is replaced with the actual version at build time by rollup
31
+ .version('1.42.6'); // This string is replaced with the actual version at build time by rollup
32
32
  // new: global config override option
33
33
  program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
34
34
  program
@@ -202,8 +202,8 @@ program
202
202
  });
203
203
  program
204
204
  .command('locize-sync')
205
- .description('Synchronize local translations with your locize project.')
206
- .option('--update-values', 'Update values of existing translations on locize.')
205
+ .description('Synchronize local translations with your Locize project.')
206
+ .option('--update-values', 'Update values of existing translations on Locize.')
207
207
  .option('--src-lng-only', 'Check for changes in source language only.')
208
208
  .option('--compare-mtime', 'Compare modification times when syncing.')
209
209
  .option('--dry-run', 'Run the command without making any changes.')
@@ -215,8 +215,8 @@ program
215
215
  });
216
216
  program
217
217
  .command('locize-download')
218
- .description('Download all translations from your locize project.')
219
- .option('--cdn-type <standard|pro>', 'Specify the cdn endpoint that should be used (depends on which cdn type you\'ve in your locize project)')
218
+ .description('Download all translations from your Locize project.')
219
+ .option('--cdn-type <standard|pro>', 'Specify the cdn endpoint that should be used (depends on which cdn type you\'ve in your Locize project)')
220
220
  .action(async (options) => {
221
221
  const cfgPath = program.opts().config;
222
222
  const config$1 = await config.ensureConfig(cfgPath);
@@ -224,8 +224,8 @@ program
224
224
  });
225
225
  program
226
226
  .command('locize-migrate')
227
- .description('Migrate local translation files to a new locize project.')
228
- .option('--cdn-type <standard|pro>', 'Specify the cdn endpoint that should be used (depends on which cdn type you\'ve in your locize project)')
227
+ .description('Migrate local translation files to a new Locize project.')
228
+ .option('--cdn-type <standard|pro>', 'Specify the cdn endpoint that should be used (depends on which cdn type you\'ve in your Locize project)')
229
229
  .action(async (options) => {
230
230
  const cfgPath = program.opts().config;
231
231
  const config$1 = await config.ensureConfig(cfgPath);
@@ -342,14 +342,10 @@ class ASTVisitors {
342
342
  }
343
343
  /**
344
344
  * Sets the current file path and code used by the extractor.
345
- * Also resets the search index for location tracking.
346
345
  */
347
346
  setCurrentFile(file, code) {
348
347
  this.currentFile = file;
349
348
  this.currentCode = code;
350
- // Reset search indexes when processing a new file
351
- this.callExpressionHandler.resetSearchIndex();
352
- this.jsxHandler.resetSearchIndex();
353
349
  }
354
350
  /**
355
351
  * Returns the currently set file path.
@@ -183,10 +183,14 @@ async function processFile(file, plugins, astVisitors, pluginContext, config, lo
183
183
  throw new validation.ExtractorError('Failed to process file', file, err);
184
184
  }
185
185
  }
186
- // Normalize SWC span offsets so every span is file-relative.
187
- // SWC accumulates byte offsets across successive parse() calls,
188
- // so without this, plugins would see positions beyond the file length.
189
- astUtils.normalizeASTSpans(ast, ast.span.start);
186
+ // Normalize SWC span offsets so every span is file-relative (0-based).
187
+ // SWC accumulates byte offsets across successive parse() calls and uses
188
+ // 1-based positions, so Module.span.start points to the first token,
189
+ // NOT to byte 0 of the source. We derive the true base by subtracting
190
+ // the 0-based index of that first token in the source string.
191
+ const firstTokenIdx = astUtils.findFirstTokenIndex(code);
192
+ const spanBase = ast.span.start - firstTokenIdx;
193
+ astUtils.normalizeASTSpans(ast, spanBase);
190
194
  // "Wire up" the visitor's scope method to the context.
191
195
  // This avoids a circular dependency while giving plugins access to the scope.
192
196
  pluginContext.getVarFromScope = astVisitors.getVarFromScope.bind(astVisitors);
@@ -815,14 +815,23 @@ async function getTranslations(keys, objectKeys, config, { syncPrimaryWithDefaul
815
815
  // LOGIC PATH 2: Separate Namespace Files
816
816
  }
817
817
  else {
818
- // Find all namespaces that exist on disk for this locale
818
+ // Find all namespaces that exist on disk for this locale.
819
+ // Use '**' so the glob crosses directory boundaries — namespaces
820
+ // can contain '/' and span multiple directory levels.
819
821
  const namespacesToProcess = new Set(keysByNS.keys());
820
- const existingNsPattern = fileUtils.getOutputPath(config.extract.output, locale, '*');
822
+ const existingNsPattern = fileUtils.getOutputPath(config.extract.output, locale, '**');
821
823
  // Ensure glob receives POSIX-style separators so pattern matching works cross-platform (Windows -> backslashes)
822
824
  const existingNsGlobPattern = existingNsPattern.replace(/\\/g, '/');
823
825
  const existingNsFiles = await glob.glob(existingNsGlobPattern, { ignore: userIgnore });
824
826
  for (const file of existingNsFiles) {
825
- namespacesToProcess.add(node_path.basename(file, node_path.extname(file)));
827
+ // Recover the full (possibly multi-segment) namespace from the file path
828
+ // by matching it against the output template.
829
+ const ns = typeof config.extract.output === 'string'
830
+ ? fileUtils.extractNamespaceFromPath(config.extract.output, locale, file)
831
+ : undefined;
832
+ if (ns) {
833
+ namespacesToProcess.add(ns);
834
+ }
826
835
  }
827
836
  // Process each namespace individually and create a result for each one
828
837
  for (const ns of namespacesToProcess) {
@@ -1,14 +1,62 @@
1
1
  'use strict';
2
2
 
3
+ /**
4
+ * Returns the 0-based index of the first real token in the source code,
5
+ * skipping leading whitespace, single-line comments (`//`), multi-line
6
+ * comments, and hashbang lines (`#!`).
7
+ *
8
+ * This is needed because SWC's `Module.span.start` points to the first
9
+ * token, not to byte 0 of the source. Knowing the first token's index
10
+ * lets us compute the true base offset for span normalisation:
11
+ * `base = ast.span.start - findFirstTokenIndex(code)`.
12
+ */
13
+ function findFirstTokenIndex(code) {
14
+ let i = 0;
15
+ while (i < code.length) {
16
+ const ch = code[i];
17
+ // Skip whitespace
18
+ if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
19
+ i++;
20
+ continue;
21
+ }
22
+ // Skip hashbang (only at very start of file)
23
+ if (i === 0 && ch === '#' && code[1] === '!') {
24
+ while (i < code.length && code[i] !== '\n')
25
+ i++;
26
+ continue;
27
+ }
28
+ // Skip single-line comment
29
+ if (ch === '/' && code[i + 1] === '/') {
30
+ i += 2;
31
+ while (i < code.length && code[i] !== '\n')
32
+ i++;
33
+ continue;
34
+ }
35
+ // Skip multi-line comment
36
+ if (ch === '/' && code[i + 1] === '*') {
37
+ i += 2;
38
+ while (i < code.length && !(code[i] === '*' && code[i + 1] === '/'))
39
+ i++;
40
+ i += 2;
41
+ continue;
42
+ }
43
+ return i;
44
+ }
45
+ return 0;
46
+ }
3
47
  /**
4
48
  * Recursively normalizes all SWC span offsets in an AST by subtracting a base
5
49
  * offset. SWC's `parse()` accumulates byte offsets across successive calls in
6
50
  * the same process, so `span.start`/`span.end` values can exceed the length of
7
51
  * the source file. Call this once on the root `Module` node right after parsing
8
- * to make every span file-relative.
52
+ * to make every span file-relative (0-based index into the source string).
53
+ *
54
+ * The correct base is `ast.span.start - findFirstTokenIndex(code)` because
55
+ * SWC uses 1-based byte positions and `Module.span.start` points to the first
56
+ * token, not to byte 0 of the source.
9
57
  *
10
58
  * @param node - Any AST node (or the root Module)
11
- * @param base - The base offset to subtract (`ast.span.start`)
59
+ * @param base - The base offset to subtract
12
60
  */
13
61
  function normalizeASTSpans(node, base) {
14
62
  if (!node || typeof node !== 'object' || base === 0)
@@ -38,6 +86,23 @@ function normalizeASTSpans(node, base) {
38
86
  }
39
87
  }
40
88
  }
89
+ /**
90
+ * Computes 1-based line and 0-based column numbers from a byte offset in source code.
91
+ *
92
+ * @param code - The full source code string
93
+ * @param offset - A character offset (e.g. from a normalised `node.span.start`)
94
+ * @returns `{ line, column }` or `undefined` when the offset is out of range
95
+ */
96
+ function lineColumnFromOffset(code, offset) {
97
+ if (offset < 0 || offset > code.length)
98
+ return undefined;
99
+ const upTo = code.substring(0, offset);
100
+ const lines = upTo.split('\n');
101
+ return {
102
+ line: lines.length,
103
+ column: lines[lines.length - 1].length
104
+ };
105
+ }
41
106
  /**
42
107
  * Finds and returns the full property node (KeyValueProperty) for the given
43
108
  * property name from an ObjectExpression.
@@ -117,8 +182,10 @@ function getObjectPropValue(object, propName) {
117
182
  return undefined;
118
183
  }
119
184
 
185
+ exports.findFirstTokenIndex = findFirstTokenIndex;
120
186
  exports.getObjectPropValue = getObjectPropValue;
121
187
  exports.getObjectPropValueExpression = getObjectPropValueExpression;
122
188
  exports.getObjectProperty = getObjectProperty;
123
189
  exports.isSimpleTemplateLiteral = isSimpleTemplateLiteral;
190
+ exports.lineColumnFromOffset = lineColumnFromOffset;
124
191
  exports.normalizeASTSpans = normalizeASTSpans;
@@ -12,7 +12,6 @@ class CallExpressionHandler {
12
12
  objectKeys = new Set();
13
13
  getCurrentFile;
14
14
  getCurrentCode;
15
- lastSearchIndex = 0;
16
15
  constructor(config, pluginContext, logger, expressionResolver, getCurrentFile, getCurrentCode) {
17
16
  this.config = config;
18
17
  this.pluginContext = pluginContext;
@@ -22,52 +21,23 @@ class CallExpressionHandler {
22
21
  this.getCurrentCode = getCurrentCode;
23
22
  }
24
23
  /**
25
- * Reset the search index when starting to process a new file.
26
- * This should be called before processing each file.
27
- */
28
- resetSearchIndex() {
29
- this.lastSearchIndex = 0;
30
- }
31
- /**
32
- * Helper method to calculate line and column from a position in the code.
33
- * Uses string searching instead of SWC span offsets to avoid accumulation bugs.
24
+ * Computes line and column from a node's normalised span.
25
+ * For call / new expressions the location points to the first argument
26
+ * (the translation key) rather than the callee, matching the previous
27
+ * string-search behaviour.
34
28
  */
35
29
  getLocationFromNode(node) {
36
- const code = this.getCurrentCode();
37
- // Extract searchable text from the node
38
- // For CallExpression and NewExpression, we can search for the key argument
39
- let searchText;
40
- if ((node.type === 'CallExpression' || node.type === 'NewExpression') && node.arguments.length > 0) {
30
+ if (!node?.span || typeof node.span.start !== 'number')
31
+ return undefined;
32
+ // For call / new expressions, prefer the first argument's span
33
+ if ((node.type === 'CallExpression' || node.type === 'NewExpression') &&
34
+ node.arguments?.length > 0) {
41
35
  const firstArg = node.arguments[0].expression;
42
- if (firstArg.type === 'StringLiteral') {
43
- // Search for the string literal including quotes
44
- searchText = firstArg.raw ?? `'${firstArg.value}'`;
45
- }
46
- else if (firstArg.type === 'TemplateLiteral') {
47
- // For template literals, search for the backtick
48
- searchText = '`';
49
- }
50
- else if (firstArg.type === 'ArrowFunctionExpression') {
51
- searchText = '=>';
36
+ if (firstArg?.span && typeof firstArg.span.start === 'number') {
37
+ return astUtils.lineColumnFromOffset(this.getCurrentCode(), firstArg.span.start);
52
38
  }
53
39
  }
54
- if (!searchText)
55
- return undefined;
56
- // Search for the text starting from last known position
57
- const position = code.indexOf(searchText, this.lastSearchIndex);
58
- if (position === -1) {
59
- // Not found - might be a parsing issue, skip location tracking
60
- return undefined;
61
- }
62
- // Update last search position for next search
63
- this.lastSearchIndex = position + searchText.length;
64
- // Calculate line and column from the position
65
- const upToPosition = code.substring(0, position);
66
- const lines = upToPosition.split('\n');
67
- return {
68
- line: lines.length,
69
- column: lines[lines.length - 1].length
70
- };
40
+ return astUtils.lineColumnFromOffset(this.getCurrentCode(), node.span.start);
71
41
  }
72
42
  /**
73
43
  * Processes function call expressions and new expressions to extract translation keys.
@@ -9,7 +9,6 @@ class JSXHandler {
9
9
  expressionResolver;
10
10
  getCurrentFile;
11
11
  getCurrentCode;
12
- lastSearchIndex = 0;
13
12
  constructor(config, pluginContext, expressionResolver, getCurrentFile, getCurrentCode) {
14
13
  this.config = config;
15
14
  this.pluginContext = pluginContext;
@@ -18,36 +17,14 @@ class JSXHandler {
18
17
  this.getCurrentCode = getCurrentCode;
19
18
  }
20
19
  /**
21
- * Reset the search index when starting to process a new file.
22
- */
23
- resetSearchIndex() {
24
- this.lastSearchIndex = 0;
25
- }
26
- /**
27
- * Helper method to calculate line and column by searching for the JSX element in the code.
20
+ * Computes line and column from a node's normalised span.
21
+ * SWC spans are normalised to file-relative offsets after parsing,
22
+ * so we can use them directly.
28
23
  */
29
24
  getLocationFromNode(node) {
30
- const code = this.getCurrentCode();
31
- // For JSXElement, search for the opening tag
32
- let searchText;
33
- if (node.type === 'JSXElement' && node.opening) {
34
- const tagName = node.opening.name?.value;
35
- if (tagName) {
36
- searchText = `<${tagName}`;
37
- }
38
- }
39
- if (!searchText)
40
- return undefined;
41
- const position = code.indexOf(searchText, this.lastSearchIndex);
42
- if (position === -1)
25
+ if (!node?.span || typeof node.span.start !== 'number')
43
26
  return undefined;
44
- this.lastSearchIndex = position + searchText.length;
45
- const upToPosition = code.substring(0, position);
46
- const lines = upToPosition.split('\n');
47
- return {
48
- line: lines.length,
49
- column: lines[lines.length - 1].length
50
- };
27
+ return astUtils.lineColumnFromOffset(this.getCurrentCode(), node.span.start);
51
28
  }
52
29
  /**
53
30
  * Processes JSX elements to extract translation keys from Trans components.
@@ -25,7 +25,7 @@ async function checkLocizeCliExists() {
25
25
  catch (error) {
26
26
  if (error.code === 'ENOENT') {
27
27
  console.error(chalk.red('Error: `locize-cli` command not found.'));
28
- console.log(chalk.yellow('Please install it globally to use the locize integration:'));
28
+ console.log(chalk.yellow('Please install it globally to use the Locize integration:'));
29
29
  console.log(chalk.cyan('npm install -g locize-cli'));
30
30
  process.exit(1);
31
31
  }
@@ -59,13 +59,13 @@ async function interactiveCredentialSetup(config) {
59
59
  {
60
60
  type: 'input',
61
61
  name: 'projectId',
62
- message: 'What is your locize Project ID? (Find this in your project settings on www.locize.app)',
62
+ message: 'What is your Locize Project ID? (Find this in your project settings on www.locize.app)',
63
63
  validate: input => !!input || 'Project ID cannot be empty.',
64
64
  },
65
65
  {
66
66
  type: 'password',
67
67
  name: 'apiKey',
68
- message: 'What is your locize API key? (Create or use one in your project settings > "API Keys")',
68
+ message: 'What is your Locize API key? (Create or use one in your project settings > "API Keys")',
69
69
  validate: input => !!input || 'API Key cannot be empty.',
70
70
  },
71
71
  {
@@ -107,7 +107,7 @@ async function printLocizeFunnel() {
107
107
  if (!(await funnelMsgTracker.shouldShowFunnel('rename-key')))
108
108
  return;
109
109
  console.log(chalk.yellow.bold('\n💡 Tip: Managing translations across multiple projects?'));
110
- console.log(' With locize, you can rename, move, and copy translation keys directly');
110
+ console.log(' With Locize, you can rename, move, and copy translation keys directly');
111
111
  console.log(' in the web interface—no CLI needed. Perfect for collaboration with');
112
112
  console.log(' translators and managing complex refactoring across namespaces.');
113
113
  console.log(` Learn more: ${chalk.cyan('https://www.locize.com/docs/how-can-a-segment-key-be-copied-moved-or-renamed')}`);
@@ -361,7 +361,7 @@ async function printLocizeFunnel() {
361
361
  if (!(await funnelMsgTracker.shouldShowFunnel('status')))
362
362
  return;
363
363
  console.log(chalk.yellow.bold('\n✨ Take your localization to the next level!'));
364
- console.log('Manage translations with your team in the cloud with locize => https://www.locize.com/docs/getting-started');
364
+ console.log('Manage translations with your team in the cloud with Locize => https://www.locize.com/docs/getting-started');
365
365
  console.log(`Run ${chalk.cyan('npx i18next-cli locize-migrate')} to get started.`);
366
366
  return funnelMsgTracker.recordFunnelShown('status');
367
367
  }
@@ -45,6 +45,43 @@ function getOutputPath(outputTemplate, language, namespace) {
45
45
  out = out.replace(/\/\/+/g, '/');
46
46
  return node_path.normalize(out);
47
47
  }
48
+ /**
49
+ * Extracts the namespace value from a concrete file path by matching it against
50
+ * the output template.
51
+ *
52
+ * Given a template like `src/{{namespace}}/locales/{{language}}.json` and a file
53
+ * path like `src/widgets/component/locales/en.json`, this returns `widgets/component`.
54
+ *
55
+ * This handles multi-segment namespaces (namespaces containing `/`) which
56
+ * `basename()` cannot recover.
57
+ *
58
+ * @param outputTemplate - The output path template string (must contain `{{namespace}}`)
59
+ * @param language - The language value used when the file was generated
60
+ * @param filePath - The concrete file path to extract the namespace from
61
+ * @returns The namespace string, or `undefined` if the path doesn't match the template
62
+ */
63
+ function extractNamespaceFromPath(outputTemplate, language, filePath) {
64
+ // Build a regex from the template by escaping everything except the placeholders.
65
+ // Replace {{language}}/{{lng}} with the literal language value and
66
+ // {{namespace}} with a named capture group that matches one or more path segments.
67
+ const pattern = outputTemplate
68
+ // Normalise to forward slashes for matching
69
+ .replace(/\\/g, '/');
70
+ // Escape regex-special characters (but keep our placeholders intact first)
71
+ const nsPlaceholder = '{{namespace}}';
72
+ const parts = pattern.split(nsPlaceholder);
73
+ // Escape each part individually then rejoin with the capture group
74
+ const escaped = parts.map(p => p
75
+ .replace(/\{\{language\}\}|\{\{lng\}\}/g, () => escapeForRegex(language))
76
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
77
+ // Don't anchor the start — the glob may return absolute or prefixed paths.
78
+ // Anchor only the end so that the namespace capture is unambiguous.
79
+ const regexStr = escaped.join('(.+)') + '$';
80
+ const normalized = filePath.replace(/\\/g, '/');
81
+ const m = new RegExp(regexStr).exec(normalized);
82
+ return m?.[1];
83
+ }
84
+ const escapeForRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
48
85
  /**
49
86
  * Dynamically loads a translation file, supporting .json, .js, and .ts formats.
50
87
  * @param filePath - The path to the translation file.
@@ -156,6 +193,7 @@ function inferFormatFromPath(filePath, defaultFormat = 'json') {
156
193
  return defaultFormat || 'json';
157
194
  }
158
195
 
196
+ exports.extractNamespaceFromPath = extractNamespaceFromPath;
159
197
  exports.getOutputPath = getOutputPath;
160
198
  exports.inferFormatFromPath = inferFormatFromPath;
161
199
  exports.loadRawJson5Content = loadRawJson5Content;
package/dist/esm/cli.js CHANGED
@@ -26,7 +26,7 @@ const program = new Command();
26
26
  program
27
27
  .name('i18next-cli')
28
28
  .description('A unified, high-performance i18next CLI.')
29
- .version('1.42.4'); // This string is replaced with the actual version at build time by rollup
29
+ .version('1.42.6'); // This string is replaced with the actual version at build time by rollup
30
30
  // new: global config override option
31
31
  program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
32
32
  program
@@ -200,8 +200,8 @@ program
200
200
  });
201
201
  program
202
202
  .command('locize-sync')
203
- .description('Synchronize local translations with your locize project.')
204
- .option('--update-values', 'Update values of existing translations on locize.')
203
+ .description('Synchronize local translations with your Locize project.')
204
+ .option('--update-values', 'Update values of existing translations on Locize.')
205
205
  .option('--src-lng-only', 'Check for changes in source language only.')
206
206
  .option('--compare-mtime', 'Compare modification times when syncing.')
207
207
  .option('--dry-run', 'Run the command without making any changes.')
@@ -213,8 +213,8 @@ program
213
213
  });
214
214
  program
215
215
  .command('locize-download')
216
- .description('Download all translations from your locize project.')
217
- .option('--cdn-type <standard|pro>', 'Specify the cdn endpoint that should be used (depends on which cdn type you\'ve in your locize project)')
216
+ .description('Download all translations from your Locize project.')
217
+ .option('--cdn-type <standard|pro>', 'Specify the cdn endpoint that should be used (depends on which cdn type you\'ve in your Locize project)')
218
218
  .action(async (options) => {
219
219
  const cfgPath = program.opts().config;
220
220
  const config = await ensureConfig(cfgPath);
@@ -222,8 +222,8 @@ program
222
222
  });
223
223
  program
224
224
  .command('locize-migrate')
225
- .description('Migrate local translation files to a new locize project.')
226
- .option('--cdn-type <standard|pro>', 'Specify the cdn endpoint that should be used (depends on which cdn type you\'ve in your locize project)')
225
+ .description('Migrate local translation files to a new Locize project.')
226
+ .option('--cdn-type <standard|pro>', 'Specify the cdn endpoint that should be used (depends on which cdn type you\'ve in your Locize project)')
227
227
  .action(async (options) => {
228
228
  const cfgPath = program.opts().config;
229
229
  const config = await ensureConfig(cfgPath);
@@ -340,14 +340,10 @@ class ASTVisitors {
340
340
  }
341
341
  /**
342
342
  * Sets the current file path and code used by the extractor.
343
- * Also resets the search index for location tracking.
344
343
  */
345
344
  setCurrentFile(file, code) {
346
345
  this.currentFile = file;
347
346
  this.currentCode = code;
348
- // Reset search indexes when processing a new file
349
- this.callExpressionHandler.resetSearchIndex();
350
- this.jsxHandler.resetSearchIndex();
351
347
  }
352
348
  /**
353
349
  * Returns the currently set file path.
@@ -7,7 +7,7 @@ import { findKeys } from './key-finder.js';
7
7
  import { getTranslations } from './translation-manager.js';
8
8
  import { validateExtractorConfig, ExtractorError } from '../../utils/validation.js';
9
9
  import { extractKeysFromComments } from '../parsers/comment-parser.js';
10
- import { normalizeASTSpans } from '../parsers/ast-utils.js';
10
+ import { findFirstTokenIndex, normalizeASTSpans } from '../parsers/ast-utils.js';
11
11
  import { ConsoleLogger } from '../../utils/logger.js';
12
12
  import { inferFormatFromPath, loadRawJson5Content, serializeTranslationFile } from '../../utils/file-utils.js';
13
13
  import { shouldShowFunnel, recordFunnelShown } from '../../utils/funnel-msg-tracker.js';
@@ -181,10 +181,14 @@ async function processFile(file, plugins, astVisitors, pluginContext, config, lo
181
181
  throw new ExtractorError('Failed to process file', file, err);
182
182
  }
183
183
  }
184
- // Normalize SWC span offsets so every span is file-relative.
185
- // SWC accumulates byte offsets across successive parse() calls,
186
- // so without this, plugins would see positions beyond the file length.
187
- normalizeASTSpans(ast, ast.span.start);
184
+ // Normalize SWC span offsets so every span is file-relative (0-based).
185
+ // SWC accumulates byte offsets across successive parse() calls and uses
186
+ // 1-based positions, so Module.span.start points to the first token,
187
+ // NOT to byte 0 of the source. We derive the true base by subtracting
188
+ // the 0-based index of that first token in the source string.
189
+ const firstTokenIdx = findFirstTokenIndex(code);
190
+ const spanBase = ast.span.start - firstTokenIdx;
191
+ normalizeASTSpans(ast, spanBase);
188
192
  // "Wire up" the visitor's scope method to the context.
189
193
  // This avoids a circular dependency while giving plugins access to the scope.
190
194
  pluginContext.getVarFromScope = astVisitors.getVarFromScope.bind(astVisitors);
@@ -1,7 +1,7 @@
1
- import { resolve, basename, extname } from 'node:path';
1
+ import { resolve } from 'node:path';
2
2
  import { glob } from 'glob';
3
3
  import { getNestedKeys, getNestedValue, setNestedValue } from '../../utils/nested-object.js';
4
- import { getOutputPath, loadTranslationFile } from '../../utils/file-utils.js';
4
+ import { getOutputPath, loadTranslationFile, extractNamespaceFromPath } from '../../utils/file-utils.js';
5
5
  import { resolveDefaultValue } from '../../utils/default-value.js';
6
6
 
7
7
  // used for natural language check
@@ -813,14 +813,23 @@ async function getTranslations(keys, objectKeys, config, { syncPrimaryWithDefaul
813
813
  // LOGIC PATH 2: Separate Namespace Files
814
814
  }
815
815
  else {
816
- // Find all namespaces that exist on disk for this locale
816
+ // Find all namespaces that exist on disk for this locale.
817
+ // Use '**' so the glob crosses directory boundaries — namespaces
818
+ // can contain '/' and span multiple directory levels.
817
819
  const namespacesToProcess = new Set(keysByNS.keys());
818
- const existingNsPattern = getOutputPath(config.extract.output, locale, '*');
820
+ const existingNsPattern = getOutputPath(config.extract.output, locale, '**');
819
821
  // Ensure glob receives POSIX-style separators so pattern matching works cross-platform (Windows -> backslashes)
820
822
  const existingNsGlobPattern = existingNsPattern.replace(/\\/g, '/');
821
823
  const existingNsFiles = await glob(existingNsGlobPattern, { ignore: userIgnore });
822
824
  for (const file of existingNsFiles) {
823
- namespacesToProcess.add(basename(file, extname(file)));
825
+ // Recover the full (possibly multi-segment) namespace from the file path
826
+ // by matching it against the output template.
827
+ const ns = typeof config.extract.output === 'string'
828
+ ? extractNamespaceFromPath(config.extract.output, locale, file)
829
+ : undefined;
830
+ if (ns) {
831
+ namespacesToProcess.add(ns);
832
+ }
824
833
  }
825
834
  // Process each namespace individually and create a result for each one
826
835
  for (const ns of namespacesToProcess) {
@@ -1,12 +1,60 @@
1
+ /**
2
+ * Returns the 0-based index of the first real token in the source code,
3
+ * skipping leading whitespace, single-line comments (`//`), multi-line
4
+ * comments, and hashbang lines (`#!`).
5
+ *
6
+ * This is needed because SWC's `Module.span.start` points to the first
7
+ * token, not to byte 0 of the source. Knowing the first token's index
8
+ * lets us compute the true base offset for span normalisation:
9
+ * `base = ast.span.start - findFirstTokenIndex(code)`.
10
+ */
11
+ function findFirstTokenIndex(code) {
12
+ let i = 0;
13
+ while (i < code.length) {
14
+ const ch = code[i];
15
+ // Skip whitespace
16
+ if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
17
+ i++;
18
+ continue;
19
+ }
20
+ // Skip hashbang (only at very start of file)
21
+ if (i === 0 && ch === '#' && code[1] === '!') {
22
+ while (i < code.length && code[i] !== '\n')
23
+ i++;
24
+ continue;
25
+ }
26
+ // Skip single-line comment
27
+ if (ch === '/' && code[i + 1] === '/') {
28
+ i += 2;
29
+ while (i < code.length && code[i] !== '\n')
30
+ i++;
31
+ continue;
32
+ }
33
+ // Skip multi-line comment
34
+ if (ch === '/' && code[i + 1] === '*') {
35
+ i += 2;
36
+ while (i < code.length && !(code[i] === '*' && code[i + 1] === '/'))
37
+ i++;
38
+ i += 2;
39
+ continue;
40
+ }
41
+ return i;
42
+ }
43
+ return 0;
44
+ }
1
45
  /**
2
46
  * Recursively normalizes all SWC span offsets in an AST by subtracting a base
3
47
  * offset. SWC's `parse()` accumulates byte offsets across successive calls in
4
48
  * the same process, so `span.start`/`span.end` values can exceed the length of
5
49
  * the source file. Call this once on the root `Module` node right after parsing
6
- * to make every span file-relative.
50
+ * to make every span file-relative (0-based index into the source string).
51
+ *
52
+ * The correct base is `ast.span.start - findFirstTokenIndex(code)` because
53
+ * SWC uses 1-based byte positions and `Module.span.start` points to the first
54
+ * token, not to byte 0 of the source.
7
55
  *
8
56
  * @param node - Any AST node (or the root Module)
9
- * @param base - The base offset to subtract (`ast.span.start`)
57
+ * @param base - The base offset to subtract
10
58
  */
11
59
  function normalizeASTSpans(node, base) {
12
60
  if (!node || typeof node !== 'object' || base === 0)
@@ -36,6 +84,23 @@ function normalizeASTSpans(node, base) {
36
84
  }
37
85
  }
38
86
  }
87
+ /**
88
+ * Computes 1-based line and 0-based column numbers from a byte offset in source code.
89
+ *
90
+ * @param code - The full source code string
91
+ * @param offset - A character offset (e.g. from a normalised `node.span.start`)
92
+ * @returns `{ line, column }` or `undefined` when the offset is out of range
93
+ */
94
+ function lineColumnFromOffset(code, offset) {
95
+ if (offset < 0 || offset > code.length)
96
+ return undefined;
97
+ const upTo = code.substring(0, offset);
98
+ const lines = upTo.split('\n');
99
+ return {
100
+ line: lines.length,
101
+ column: lines[lines.length - 1].length
102
+ };
103
+ }
39
104
  /**
40
105
  * Finds and returns the full property node (KeyValueProperty) for the given
41
106
  * property name from an ObjectExpression.
@@ -115,4 +180,4 @@ function getObjectPropValue(object, propName) {
115
180
  return undefined;
116
181
  }
117
182
 
118
- export { getObjectPropValue, getObjectPropValueExpression, getObjectProperty, isSimpleTemplateLiteral, normalizeASTSpans };
183
+ export { findFirstTokenIndex, getObjectPropValue, getObjectPropValueExpression, getObjectProperty, isSimpleTemplateLiteral, lineColumnFromOffset, normalizeASTSpans };
@@ -1,4 +1,4 @@
1
- import { isSimpleTemplateLiteral, getObjectPropValue, getObjectPropValueExpression } from './ast-utils.js';
1
+ import { lineColumnFromOffset, isSimpleTemplateLiteral, getObjectPropValue, getObjectPropValueExpression } from './ast-utils.js';
2
2
 
3
3
  // Helper to escape regex characters
4
4
  const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -10,7 +10,6 @@ class CallExpressionHandler {
10
10
  objectKeys = new Set();
11
11
  getCurrentFile;
12
12
  getCurrentCode;
13
- lastSearchIndex = 0;
14
13
  constructor(config, pluginContext, logger, expressionResolver, getCurrentFile, getCurrentCode) {
15
14
  this.config = config;
16
15
  this.pluginContext = pluginContext;
@@ -20,52 +19,23 @@ class CallExpressionHandler {
20
19
  this.getCurrentCode = getCurrentCode;
21
20
  }
22
21
  /**
23
- * Reset the search index when starting to process a new file.
24
- * This should be called before processing each file.
25
- */
26
- resetSearchIndex() {
27
- this.lastSearchIndex = 0;
28
- }
29
- /**
30
- * Helper method to calculate line and column from a position in the code.
31
- * Uses string searching instead of SWC span offsets to avoid accumulation bugs.
22
+ * Computes line and column from a node's normalised span.
23
+ * For call / new expressions the location points to the first argument
24
+ * (the translation key) rather than the callee, matching the previous
25
+ * string-search behaviour.
32
26
  */
33
27
  getLocationFromNode(node) {
34
- const code = this.getCurrentCode();
35
- // Extract searchable text from the node
36
- // For CallExpression and NewExpression, we can search for the key argument
37
- let searchText;
38
- if ((node.type === 'CallExpression' || node.type === 'NewExpression') && node.arguments.length > 0) {
28
+ if (!node?.span || typeof node.span.start !== 'number')
29
+ return undefined;
30
+ // For call / new expressions, prefer the first argument's span
31
+ if ((node.type === 'CallExpression' || node.type === 'NewExpression') &&
32
+ node.arguments?.length > 0) {
39
33
  const firstArg = node.arguments[0].expression;
40
- if (firstArg.type === 'StringLiteral') {
41
- // Search for the string literal including quotes
42
- searchText = firstArg.raw ?? `'${firstArg.value}'`;
43
- }
44
- else if (firstArg.type === 'TemplateLiteral') {
45
- // For template literals, search for the backtick
46
- searchText = '`';
47
- }
48
- else if (firstArg.type === 'ArrowFunctionExpression') {
49
- searchText = '=>';
34
+ if (firstArg?.span && typeof firstArg.span.start === 'number') {
35
+ return lineColumnFromOffset(this.getCurrentCode(), firstArg.span.start);
50
36
  }
51
37
  }
52
- if (!searchText)
53
- return undefined;
54
- // Search for the text starting from last known position
55
- const position = code.indexOf(searchText, this.lastSearchIndex);
56
- if (position === -1) {
57
- // Not found - might be a parsing issue, skip location tracking
58
- return undefined;
59
- }
60
- // Update last search position for next search
61
- this.lastSearchIndex = position + searchText.length;
62
- // Calculate line and column from the position
63
- const upToPosition = code.substring(0, position);
64
- const lines = upToPosition.split('\n');
65
- return {
66
- line: lines.length,
67
- column: lines[lines.length - 1].length
68
- };
38
+ return lineColumnFromOffset(this.getCurrentCode(), node.span.start);
69
39
  }
70
40
  /**
71
41
  * Processes function call expressions and new expressions to extract translation keys.
@@ -1,5 +1,5 @@
1
1
  import { extractFromTransComponent } from './jsx-parser.js';
2
- import { getObjectPropValue } from './ast-utils.js';
2
+ import { lineColumnFromOffset, getObjectPropValue } from './ast-utils.js';
3
3
 
4
4
  class JSXHandler {
5
5
  config;
@@ -7,7 +7,6 @@ class JSXHandler {
7
7
  expressionResolver;
8
8
  getCurrentFile;
9
9
  getCurrentCode;
10
- lastSearchIndex = 0;
11
10
  constructor(config, pluginContext, expressionResolver, getCurrentFile, getCurrentCode) {
12
11
  this.config = config;
13
12
  this.pluginContext = pluginContext;
@@ -16,36 +15,14 @@ class JSXHandler {
16
15
  this.getCurrentCode = getCurrentCode;
17
16
  }
18
17
  /**
19
- * Reset the search index when starting to process a new file.
20
- */
21
- resetSearchIndex() {
22
- this.lastSearchIndex = 0;
23
- }
24
- /**
25
- * Helper method to calculate line and column by searching for the JSX element in the code.
18
+ * Computes line and column from a node's normalised span.
19
+ * SWC spans are normalised to file-relative offsets after parsing,
20
+ * so we can use them directly.
26
21
  */
27
22
  getLocationFromNode(node) {
28
- const code = this.getCurrentCode();
29
- // For JSXElement, search for the opening tag
30
- let searchText;
31
- if (node.type === 'JSXElement' && node.opening) {
32
- const tagName = node.opening.name?.value;
33
- if (tagName) {
34
- searchText = `<${tagName}`;
35
- }
36
- }
37
- if (!searchText)
38
- return undefined;
39
- const position = code.indexOf(searchText, this.lastSearchIndex);
40
- if (position === -1)
23
+ if (!node?.span || typeof node.span.start !== 'number')
41
24
  return undefined;
42
- this.lastSearchIndex = position + searchText.length;
43
- const upToPosition = code.substring(0, position);
44
- const lines = upToPosition.split('\n');
45
- return {
46
- line: lines.length,
47
- column: lines[lines.length - 1].length
48
- };
25
+ return lineColumnFromOffset(this.getCurrentCode(), node.span.start);
49
26
  }
50
27
  /**
51
28
  * Processes JSX elements to extract translation keys from Trans components.
@@ -23,7 +23,7 @@ async function checkLocizeCliExists() {
23
23
  catch (error) {
24
24
  if (error.code === 'ENOENT') {
25
25
  console.error(chalk.red('Error: `locize-cli` command not found.'));
26
- console.log(chalk.yellow('Please install it globally to use the locize integration:'));
26
+ console.log(chalk.yellow('Please install it globally to use the Locize integration:'));
27
27
  console.log(chalk.cyan('npm install -g locize-cli'));
28
28
  process.exit(1);
29
29
  }
@@ -57,13 +57,13 @@ async function interactiveCredentialSetup(config) {
57
57
  {
58
58
  type: 'input',
59
59
  name: 'projectId',
60
- message: 'What is your locize Project ID? (Find this in your project settings on www.locize.app)',
60
+ message: 'What is your Locize Project ID? (Find this in your project settings on www.locize.app)',
61
61
  validate: input => !!input || 'Project ID cannot be empty.',
62
62
  },
63
63
  {
64
64
  type: 'password',
65
65
  name: 'apiKey',
66
- message: 'What is your locize API key? (Create or use one in your project settings > "API Keys")',
66
+ message: 'What is your Locize API key? (Create or use one in your project settings > "API Keys")',
67
67
  validate: input => !!input || 'API Key cannot be empty.',
68
68
  },
69
69
  {
@@ -105,7 +105,7 @@ async function printLocizeFunnel() {
105
105
  if (!(await shouldShowFunnel('rename-key')))
106
106
  return;
107
107
  console.log(chalk.yellow.bold('\n💡 Tip: Managing translations across multiple projects?'));
108
- console.log(' With locize, you can rename, move, and copy translation keys directly');
108
+ console.log(' With Locize, you can rename, move, and copy translation keys directly');
109
109
  console.log(' in the web interface—no CLI needed. Perfect for collaboration with');
110
110
  console.log(' translators and managing complex refactoring across namespaces.');
111
111
  console.log(` Learn more: ${chalk.cyan('https://www.locize.com/docs/how-can-a-segment-key-be-copied-moved-or-renamed')}`);
@@ -359,7 +359,7 @@ async function printLocizeFunnel() {
359
359
  if (!(await shouldShowFunnel('status')))
360
360
  return;
361
361
  console.log(chalk.yellow.bold('\n✨ Take your localization to the next level!'));
362
- console.log('Manage translations with your team in the cloud with locize => https://www.locize.com/docs/getting-started');
362
+ console.log('Manage translations with your team in the cloud with Locize => https://www.locize.com/docs/getting-started');
363
363
  console.log(`Run ${chalk.cyan('npx i18next-cli locize-migrate')} to get started.`);
364
364
  return recordFunnelShown('status');
365
365
  }
@@ -43,6 +43,43 @@ function getOutputPath(outputTemplate, language, namespace) {
43
43
  out = out.replace(/\/\/+/g, '/');
44
44
  return normalize(out);
45
45
  }
46
+ /**
47
+ * Extracts the namespace value from a concrete file path by matching it against
48
+ * the output template.
49
+ *
50
+ * Given a template like `src/{{namespace}}/locales/{{language}}.json` and a file
51
+ * path like `src/widgets/component/locales/en.json`, this returns `widgets/component`.
52
+ *
53
+ * This handles multi-segment namespaces (namespaces containing `/`) which
54
+ * `basename()` cannot recover.
55
+ *
56
+ * @param outputTemplate - The output path template string (must contain `{{namespace}}`)
57
+ * @param language - The language value used when the file was generated
58
+ * @param filePath - The concrete file path to extract the namespace from
59
+ * @returns The namespace string, or `undefined` if the path doesn't match the template
60
+ */
61
+ function extractNamespaceFromPath(outputTemplate, language, filePath) {
62
+ // Build a regex from the template by escaping everything except the placeholders.
63
+ // Replace {{language}}/{{lng}} with the literal language value and
64
+ // {{namespace}} with a named capture group that matches one or more path segments.
65
+ const pattern = outputTemplate
66
+ // Normalise to forward slashes for matching
67
+ .replace(/\\/g, '/');
68
+ // Escape regex-special characters (but keep our placeholders intact first)
69
+ const nsPlaceholder = '{{namespace}}';
70
+ const parts = pattern.split(nsPlaceholder);
71
+ // Escape each part individually then rejoin with the capture group
72
+ const escaped = parts.map(p => p
73
+ .replace(/\{\{language\}\}|\{\{lng\}\}/g, () => escapeForRegex(language))
74
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
75
+ // Don't anchor the start — the glob may return absolute or prefixed paths.
76
+ // Anchor only the end so that the namespace capture is unambiguous.
77
+ const regexStr = escaped.join('(.+)') + '$';
78
+ const normalized = filePath.replace(/\\/g, '/');
79
+ const m = new RegExp(regexStr).exec(normalized);
80
+ return m?.[1];
81
+ }
82
+ const escapeForRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
46
83
  /**
47
84
  * Dynamically loads a translation file, supporting .json, .js, and .ts formats.
48
85
  * @param filePath - The path to the translation file.
@@ -154,4 +191,4 @@ function inferFormatFromPath(filePath, defaultFormat = 'json') {
154
191
  return defaultFormat || 'json';
155
192
  }
156
193
 
157
- export { getOutputPath, inferFormatFromPath, loadRawJson5Content, loadTranslationFile, serializeTranslationFile };
194
+ export { extractNamespaceFromPath, getOutputPath, inferFormatFromPath, loadRawJson5Content, loadTranslationFile, serializeTranslationFile };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "1.42.4",
3
+ "version": "1.42.6",
4
4
  "description": "A unified, high-performance i18next CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -76,7 +76,6 @@ export declare class ASTVisitors {
76
76
  getVarFromScope(name: string): ScopeInfo | undefined;
77
77
  /**
78
78
  * Sets the current file path and code used by the extractor.
79
- * Also resets the search index for location tracking.
80
79
  */
81
80
  setCurrentFile(file: string, code: string): void;
82
81
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"ast-visitors.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/ast-visitors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAQ,MAAM,WAAW,CAAA;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE1G,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAA;AAInE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuC;IAC9D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,KAAK,CAAiB;IAE9B,IAAW,UAAU,gBAEpB;IAED,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAc;IAC3C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAoB;IACvD,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAuB;IAC7D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAY;IACvC,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,WAAW,CAAa;IAEhC;;;;;;OAMG;gBAED,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,eAAe,EACvB,kBAAkB,CAAC,EAAE,kBAAkB;IAgCzC;;;;;OAKG;IACI,KAAK,CAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAUjC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,IAAI;IA8NZ;;;;;;;;OAQG;IACI,eAAe,CAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAI5D;;;OAGG;IACI,cAAc,CAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAQxD;;;;;;OAMG;IACI,cAAc,IAAK,MAAM;IAIhC;;OAEG;IACI,cAAc,IAAK,MAAM;CAGjC"}
1
+ {"version":3,"file":"ast-visitors.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/ast-visitors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAQ,MAAM,WAAW,CAAA;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE1G,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAA;AAInE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuC;IAC9D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,KAAK,CAAiB;IAE9B,IAAW,UAAU,gBAEpB;IAED,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAc;IAC3C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAoB;IACvD,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAuB;IAC7D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAY;IACvC,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,WAAW,CAAa;IAEhC;;;;;;OAMG;gBAED,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,eAAe,EACvB,kBAAkB,CAAC,EAAE,kBAAkB;IAgCzC;;;;;OAKG;IACI,KAAK,CAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAUjC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,IAAI;IA8NZ;;;;;;;;OAQG;IACI,eAAe,CAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAI5D;;OAEG;IACI,cAAc,CAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAKxD;;;;;;OAMG;IACI,cAAc,IAAK,MAAM;IAIhC;;OAEG;IACI,cAAc,IAAK,MAAM;CAGjC"}
@@ -1 +1 @@
1
- {"version":3,"file":"extractor.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/extractor.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAMtF,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAK5C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,GAAE;IACP,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;CACX,GACL,OAAO,CAAC,OAAO,CAAC,CAkElB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EAAE,EACjB,WAAW,EAAE,WAAW,EACxB,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,MAAM,GAAE,MAA4B,GACnC,OAAO,CAAC,IAAI,CAAC,CAsGf;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,OAAO,CAAE,MAAM,EAAE,oBAAoB,EAAE,EAAE,uBAA+B,EAAE,GAAE;IAAE,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAAO,sDAO3I"}
1
+ {"version":3,"file":"extractor.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/extractor.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAMtF,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAK5C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,GAAE;IACP,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;CACX,GACL,OAAO,CAAC,OAAO,CAAC,CAkElB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EAAE,EACjB,WAAW,EAAE,WAAW,EACxB,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,MAAM,GAAE,MAA4B,GACnC,OAAO,CAAC,IAAI,CAAC,CA0Gf;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,OAAO,CAAE,MAAM,EAAE,oBAAoB,EAAE,EAAE,uBAA+B,EAAE,GAAE;IAAE,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAAO,sDAO3I"}
@@ -1 +1 @@
1
- {"version":3,"file":"translation-manager.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/translation-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAgyBnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAC/B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,EACvB,MAAM,EAAE,oBAAoB,EAC5B,EACE,uBAA+B,EAC/B,OAAe,EAChB,GAAE;IACD,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAA;CACb,GACL,OAAO,CAAC,iBAAiB,EAAE,CAAC,CA0I9B"}
1
+ {"version":3,"file":"translation-manager.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/translation-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAgyBnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAC/B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,EACvB,MAAM,EAAE,oBAAoB,EAC5B,EACE,uBAA+B,EAC/B,OAAe,EAChB,GAAE;IACD,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAA;CACb,GACL,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAmJ9B"}
@@ -1,15 +1,41 @@
1
1
  import type { Expression, ObjectExpression, TemplateLiteral } from '@swc/core';
2
+ /**
3
+ * Returns the 0-based index of the first real token in the source code,
4
+ * skipping leading whitespace, single-line comments (`//`), multi-line
5
+ * comments, and hashbang lines (`#!`).
6
+ *
7
+ * This is needed because SWC's `Module.span.start` points to the first
8
+ * token, not to byte 0 of the source. Knowing the first token's index
9
+ * lets us compute the true base offset for span normalisation:
10
+ * `base = ast.span.start - findFirstTokenIndex(code)`.
11
+ */
12
+ export declare function findFirstTokenIndex(code: string): number;
2
13
  /**
3
14
  * Recursively normalizes all SWC span offsets in an AST by subtracting a base
4
15
  * offset. SWC's `parse()` accumulates byte offsets across successive calls in
5
16
  * the same process, so `span.start`/`span.end` values can exceed the length of
6
17
  * the source file. Call this once on the root `Module` node right after parsing
7
- * to make every span file-relative.
18
+ * to make every span file-relative (0-based index into the source string).
19
+ *
20
+ * The correct base is `ast.span.start - findFirstTokenIndex(code)` because
21
+ * SWC uses 1-based byte positions and `Module.span.start` points to the first
22
+ * token, not to byte 0 of the source.
8
23
  *
9
24
  * @param node - Any AST node (or the root Module)
10
- * @param base - The base offset to subtract (`ast.span.start`)
25
+ * @param base - The base offset to subtract
11
26
  */
12
27
  export declare function normalizeASTSpans(node: any, base: number): void;
28
+ /**
29
+ * Computes 1-based line and 0-based column numbers from a byte offset in source code.
30
+ *
31
+ * @param code - The full source code string
32
+ * @param offset - A character offset (e.g. from a normalised `node.span.start`)
33
+ * @returns `{ line, column }` or `undefined` when the offset is out of range
34
+ */
35
+ export declare function lineColumnFromOffset(code: string, offset: number): {
36
+ line: number;
37
+ column: number;
38
+ } | undefined;
13
39
  /**
14
40
  * Finds and returns the full property node (KeyValueProperty) for the given
15
41
  * property name from an ObjectExpression.
@@ -1 +1 @@
1
- {"version":3,"file":"ast-utils.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/ast-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAc,gBAAgB,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAE1F;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CA0BhE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,qDAU5E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,4BAA4B,CAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAKhH;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAE1E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAYrH"}
1
+ {"version":3,"file":"ast-utils.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/ast-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAc,gBAAgB,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAE1F;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CA2BzD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CA0BhE;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAQhH;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,qDAU5E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,4BAA4B,CAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAKhH;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAE1E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAYrH"}
@@ -9,16 +9,12 @@ export declare class CallExpressionHandler {
9
9
  objectKeys: Set<string>;
10
10
  private getCurrentFile;
11
11
  private getCurrentCode;
12
- private lastSearchIndex;
13
12
  constructor(config: Omit<I18nextToolkitConfig, 'plugins'>, pluginContext: PluginContext, logger: Logger, expressionResolver: ExpressionResolver, getCurrentFile: () => string, getCurrentCode: () => string);
14
13
  /**
15
- * Reset the search index when starting to process a new file.
16
- * This should be called before processing each file.
17
- */
18
- resetSearchIndex(): void;
19
- /**
20
- * Helper method to calculate line and column from a position in the code.
21
- * Uses string searching instead of SWC span offsets to avoid accumulation bugs.
14
+ * Computes line and column from a node's normalised span.
15
+ * For call / new expressions the location points to the first argument
16
+ * (the translation key) rather than the callee, matching the previous
17
+ * string-search behaviour.
22
18
  */
23
19
  private getLocationFromNode;
24
20
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"call-expression-handler.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/call-expression-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAA6C,MAAM,WAAW,CAAA;AAC1F,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,EAAgB,SAAS,EAAE,MAAM,aAAa,CAAA;AACvG,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAM1D,qBAAa,qBAAqB;IAChC,OAAO,CAAC,aAAa,CAAe;IACpC,OAAO,CAAC,MAAM,CAAuC;IACrD,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,kBAAkB,CAAoB;IACvC,UAAU,cAAoB;IACrC,OAAO,CAAC,cAAc,CAAc;IACpC,OAAO,CAAC,cAAc,CAAc;IACpC,OAAO,CAAC,eAAe,CAAY;gBAGjC,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,MAAM,EACd,kBAAkB,EAAE,kBAAkB,EACtC,cAAc,EAAE,MAAM,MAAM,EAC5B,cAAc,EAAE,MAAM,MAAM;IAU9B;;;OAGG;IACI,gBAAgB,IAAK,IAAI;IAIhC;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IA4C3B;;;;;;;;;;;;;;OAcG;IACH,oBAAoB,CAAE,IAAI,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,SAAS,GAAG,SAAS,GAAG,IAAI;IAgYxG;;OAEG;IACH,OAAO,CAAC,iBAAiB;IA4BzB,OAAO,CAAC,oBAAoB;IA6E5B,OAAO,CAAC,wBAAwB;IAyEhC;;;;;;OAMG;IACH,OAAO,CAAC,4BAA4B;IA8BpC;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,sBAAsB;IA2C9B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,gBAAgB;IA6LxB;;;;;;;;;OASG;IACH,OAAO,CAAC,eAAe;CA2BxB"}
1
+ {"version":3,"file":"call-expression-handler.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/call-expression-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAA6C,MAAM,WAAW,CAAA;AAC1F,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,EAAgB,SAAS,EAAE,MAAM,aAAa,CAAA;AACvG,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAM1D,qBAAa,qBAAqB;IAChC,OAAO,CAAC,aAAa,CAAe;IACpC,OAAO,CAAC,MAAM,CAAuC;IACrD,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,kBAAkB,CAAoB;IACvC,UAAU,cAAoB;IACrC,OAAO,CAAC,cAAc,CAAc;IACpC,OAAO,CAAC,cAAc,CAAc;gBAGlC,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,MAAM,EACd,kBAAkB,EAAE,kBAAkB,EACtC,cAAc,EAAE,MAAM,MAAM,EAC5B,cAAc,EAAE,MAAM,MAAM;IAU9B;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IAiB3B;;;;;;;;;;;;;;OAcG;IACH,oBAAoB,CAAE,IAAI,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,SAAS,GAAG,SAAS,GAAG,IAAI;IAgYxG;;OAEG;IACH,OAAO,CAAC,iBAAiB;IA4BzB,OAAO,CAAC,oBAAoB;IA6E5B,OAAO,CAAC,wBAAwB;IAyEhC;;;;;;OAMG;IACH,OAAO,CAAC,4BAA4B;IA8BpC;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,sBAAsB;IA2C9B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,gBAAgB;IA6LxB;;;;;;;;;OASG;IACH,OAAO,CAAC,eAAe;CA2BxB"}
@@ -7,14 +7,11 @@ export declare class JSXHandler {
7
7
  private expressionResolver;
8
8
  private getCurrentFile;
9
9
  private getCurrentCode;
10
- private lastSearchIndex;
11
10
  constructor(config: Omit<I18nextToolkitConfig, 'plugins'>, pluginContext: PluginContext, expressionResolver: ExpressionResolver, getCurrentFile: () => string, getCurrentCode: () => string);
12
11
  /**
13
- * Reset the search index when starting to process a new file.
14
- */
15
- resetSearchIndex(): void;
16
- /**
17
- * Helper method to calculate line and column by searching for the JSX element in the code.
12
+ * Computes line and column from a node's normalised span.
13
+ * SWC spans are normalised to file-relative offsets after parsing,
14
+ * so we can use them directly.
18
15
  */
19
16
  private getLocationFromNode;
20
17
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"jsx-handler.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/jsx-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAoB,MAAM,WAAW,CAAA;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAgB,MAAM,aAAa,CAAA;AACpF,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAI1D,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAuC;IACrD,OAAO,CAAC,aAAa,CAAe;IACpC,OAAO,CAAC,kBAAkB,CAAoB;IAC9C,OAAO,CAAC,cAAc,CAAc;IACpC,OAAO,CAAC,cAAc,CAAc;IACpC,OAAO,CAAC,eAAe,CAAY;gBAGjC,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,aAAa,EAAE,aAAa,EAC5B,kBAAkB,EAAE,kBAAkB,EACtC,cAAc,EAAE,MAAM,MAAM,EAC5B,cAAc,EAAE,MAAM,MAAM;IAS9B;;OAEG;IACI,gBAAgB,IAAK,IAAI;IAIhC;;OAEG;IACH,OAAO,CAAC,mBAAmB;IA8B3B;;;;;;;;OAQG;IACH,gBAAgB,CAAE,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,GAAG,IAAI;IAwUjI;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,0BAA0B;IAuHlC;;;;;;;;;OASG;IACH,OAAO,CAAC,cAAc;CAevB"}
1
+ {"version":3,"file":"jsx-handler.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/jsx-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAoB,MAAM,WAAW,CAAA;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAgB,MAAM,aAAa,CAAA;AACpF,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAI1D,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAuC;IACrD,OAAO,CAAC,aAAa,CAAe;IACpC,OAAO,CAAC,kBAAkB,CAAoB;IAC9C,OAAO,CAAC,cAAc,CAAc;IACpC,OAAO,CAAC,cAAc,CAAc;gBAGlC,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,aAAa,EAAE,aAAa,EAC5B,kBAAkB,EAAE,kBAAkB,EACtC,cAAc,EAAE,MAAM,MAAM,EAC5B,cAAc,EAAE,MAAM,MAAM;IAS9B;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IAK3B;;;;;;;;OAQG;IACH,gBAAgB,CAAE,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,GAAG,IAAI;IAwUjI;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,0BAA0B;IAuHlC;;;;;;;;;OASG;IACH,OAAO,CAAC,cAAc;CAevB"}
@@ -48,6 +48,22 @@ export declare function writeFileAsync(filePath: string, data: string): Promise<
48
48
  * - Normalizes duplicate slashes and returns a platform-correct path.
49
49
  */
50
50
  export declare function getOutputPath(outputTemplate: string | ((language: string, namespace?: string) => string) | undefined, language: string, namespace?: string): string;
51
+ /**
52
+ * Extracts the namespace value from a concrete file path by matching it against
53
+ * the output template.
54
+ *
55
+ * Given a template like `src/{{namespace}}/locales/{{language}}.json` and a file
56
+ * path like `src/widgets/component/locales/en.json`, this returns `widgets/component`.
57
+ *
58
+ * This handles multi-segment namespaces (namespaces containing `/`) which
59
+ * `basename()` cannot recover.
60
+ *
61
+ * @param outputTemplate - The output path template string (must contain `{{namespace}}`)
62
+ * @param language - The language value used when the file was generated
63
+ * @param filePath - The concrete file path to extract the namespace from
64
+ * @returns The namespace string, or `undefined` if the path doesn't match the template
65
+ */
66
+ export declare function extractNamespaceFromPath(outputTemplate: string, language: string, filePath: string): string | undefined;
51
67
  /**
52
68
  * Dynamically loads a translation file, supporting .json, .js, and .ts formats.
53
69
  * @param filePath - The path to the translation file.
@@ -1 +1 @@
1
- {"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/utils/file-utils.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAKpD;;;;;;;;;;;GAWG;AACH,wBAAsB,qBAAqB,CAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG5E;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEtE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEnF;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,cAAc,EAAE,MAAM,GAAG,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC,GAAG,SAAS,EACvF,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CA8BR;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,CAwChG;AAGD,wBAAsB,mBAAmB,CAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAQnF;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACzB,MAAM,GAAE,oBAAoB,CAAC,SAAS,CAAC,CAAC,cAAc,CAAU,EAChE,WAAW,GAAE,MAAM,GAAG,MAAU,EAChC,UAAU,CAAC,EAAE,MAAM,GAClB,MAAM,CA6BR;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,aAAa,GAAE,oBAAoB,CAAC,SAAS,CAAC,CAAC,cAAc,CAAU,GACtE,WAAW,CAAC,oBAAoB,CAAC,SAAS,CAAC,CAAC,cAAc,CAAC,CAAC,CAO9D"}
1
+ {"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/utils/file-utils.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAKpD;;;;;;;;;;;GAWG;AACH,wBAAsB,qBAAqB,CAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG5E;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEtE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEnF;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,cAAc,EAAE,MAAM,GAAG,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC,GAAG,SAAS,EACvF,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CA8BR;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,wBAAwB,CACtC,cAAc,EAAE,MAAM,EACtB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,MAAM,GAAG,SAAS,CAwBpB;AAID;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,CAwChG;AAGD,wBAAsB,mBAAmB,CAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAQnF;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACzB,MAAM,GAAE,oBAAoB,CAAC,SAAS,CAAC,CAAC,cAAc,CAAU,EAChE,WAAW,GAAE,MAAM,GAAG,MAAU,EAChC,UAAU,CAAC,EAAE,MAAM,GAClB,MAAM,CA6BR;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,aAAa,GAAE,oBAAoB,CAAC,SAAS,CAAC,CAAC,cAAc,CAAU,GACtE,WAAW,CAAC,oBAAoB,CAAC,SAAS,CAAC,CAAC,cAAc,CAAC,CAAC,CAO9D"}