i18next-cli 1.59.1 → 1.61.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.
- package/README.md +31 -0
- package/dist/cjs/cli.js +1 -1
- package/dist/cjs/extractor/parsers/ast-utils.js +169 -0
- package/dist/cjs/instrumenter/core/instrumenter.js +7 -96
- package/dist/cjs/linter.js +19 -30
- package/dist/cjs/status.js +49 -12
- package/dist/esm/cli.js +1 -1
- package/dist/esm/extractor/parsers/ast-utils.js +167 -1
- package/dist/esm/instrumenter/core/instrumenter.js +6 -95
- package/dist/esm/linter.js +19 -30
- package/dist/esm/status.js +49 -12
- package/package.json +1 -1
- package/types/extractor/parsers/ast-utils.d.ts +40 -0
- package/types/extractor/parsers/ast-utils.d.ts.map +1 -1
- package/types/instrumenter/core/instrumenter.d.ts.map +1 -1
- package/types/linter.d.ts.map +1 -1
- package/types/status.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -204,6 +204,8 @@ npx i18next-cli extract --watch --with-types
|
|
|
204
204
|
|
|
205
205
|
Displays a health check of your project's translation status. Can run without a config file. Exits with a non-zero status code when translations are missing.
|
|
206
206
|
|
|
207
|
+
The primary language is checked too: any key used in your code but absent from the primary language's translation files (a typo, or `extract` was never run) is reported and causes a non-zero exit code. Empty-string placeholders written by `extract` are considered present and do not fail the check. Running `npx i18next-cli status <primaryLanguage>` shows the absent keys in detail.
|
|
208
|
+
|
|
207
209
|
**Options:**
|
|
208
210
|
- `--namespace <ns>, -n <ns>`: Filter the report by a specific namespace.
|
|
209
211
|
- `--hide-translated`: Hide already translated keys in the detailed view, showing only missing translations.
|
|
@@ -280,6 +282,8 @@ Analyzes your source code for internationalization issues like hardcoded strings
|
|
|
280
282
|
npx i18next-cli lint
|
|
281
283
|
```
|
|
282
284
|
|
|
285
|
+
To suppress warnings for code you intentionally aren't translating yet, use the [`i18next-instrument-ignore` directive](#suppressing-detection-with-i18next-instrument-ignore) — the same comment recognized by the `instrument` command.
|
|
286
|
+
|
|
283
287
|
### `instrument`
|
|
284
288
|
|
|
285
289
|
Scans your source code for hardcoded user-facing strings and instruments them with i18next translation calls. This is useful for adding i18next instrumentation to an existing codebase that wasn't built with internationalization in mind. You can see this in action in [this video](https://youtu.be/aWZnZXwGg34) or in [this blog post](https://www.locize.com/blog/i18next-cli-instrument?utm_source=i18next_cli_readme&utm_medium=github&utm_campaign=readme).
|
|
@@ -437,6 +441,33 @@ The intended usage pattern is:
|
|
|
437
441
|
5. Manually fix any false positives or false negatives
|
|
438
442
|
6. Run `extract` to finalize translation files
|
|
439
443
|
|
|
444
|
+
#### Suppressing detection with `i18next-instrument-ignore`
|
|
445
|
+
|
|
446
|
+
Both the `lint` and `instrument` commands honor an ignore comment so you can skip placeholder or intentionally-untranslated content. It works as a line or block comment, including the JSX `{/* ... */}` form, and comes in two variants:
|
|
447
|
+
|
|
448
|
+
| Directive | Scope |
|
|
449
|
+
| --- | --- |
|
|
450
|
+
| `i18next-instrument-ignore` | The **entire JSX element** that begins on the next line — its opening tag, all nested children, and its closing tag. Falls back to a single line when the next line isn't a JSX element (e.g. a plain `t()` call). |
|
|
451
|
+
| `i18next-instrument-ignore-next-line` | Only the **single line** immediately after the directive. |
|
|
452
|
+
|
|
453
|
+
```jsx
|
|
454
|
+
// Suppress a whole element (including multi-line opening tags and nested children)
|
|
455
|
+
{/* i18next-instrument-ignore */}
|
|
456
|
+
<div
|
|
457
|
+
css={css`text-align: center;`}>
|
|
458
|
+
Hi, I'm Bob 👋
|
|
459
|
+
<p>This nested text is ignored too</p>
|
|
460
|
+
</div>
|
|
461
|
+
|
|
462
|
+
// Suppress just one line
|
|
463
|
+
{/* i18next-instrument-ignore-next-line */}
|
|
464
|
+
<p>Only this line is ignored</p>
|
|
465
|
+
|
|
466
|
+
// Also works for t() interpolation warnings in the linter
|
|
467
|
+
// i18next-instrument-ignore
|
|
468
|
+
const msg = t('Hello {{name}}!', { wrong: 'world' })
|
|
469
|
+
```
|
|
470
|
+
|
|
440
471
|
### `migrate-config`
|
|
441
472
|
Automatically migrates a legacy `i18next-parser.config.js` file to the new `i18next.config.ts` format.
|
|
442
473
|
|
package/dist/cjs/cli.js
CHANGED
|
@@ -32,7 +32,7 @@ const program = new commander.Command();
|
|
|
32
32
|
program
|
|
33
33
|
.name('i18next-cli')
|
|
34
34
|
.description('A unified, high-performance i18next CLI.')
|
|
35
|
-
.version('1.
|
|
35
|
+
.version('1.61.0'); // This string is replaced with the actual version at build time by rollup
|
|
36
36
|
// new: global config override option
|
|
37
37
|
program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
|
|
38
38
|
program
|
|
@@ -103,6 +103,172 @@ function lineColumnFromOffset(code, offset) {
|
|
|
103
103
|
column: lines[lines.length - 1].length
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
|
+
// ─── Byte → char offset helpers ────────────────────────────────────────────
|
|
107
|
+
/**
|
|
108
|
+
* Builds a lookup table from UTF-8 byte offsets to JavaScript string character
|
|
109
|
+
* indices (UTF-16 code-unit positions).
|
|
110
|
+
*
|
|
111
|
+
* SWC internally represents source as UTF-8 and reports AST spans as byte
|
|
112
|
+
* offsets into that representation. MagicString and all JavaScript String
|
|
113
|
+
* methods operate on UTF-16 code-unit indices. For files that contain only
|
|
114
|
+
* ASCII characters the two coincide, so this function returns `null` as a
|
|
115
|
+
* fast path. For files with multi-byte characters (emoji, accented letters,
|
|
116
|
+
* CJK, etc.) the returned array allows O(1) conversion of any byte offset.
|
|
117
|
+
*/
|
|
118
|
+
function buildByteToCharMap(content) {
|
|
119
|
+
// Fast path: pure ASCII means byte offset ≡ char index
|
|
120
|
+
// eslint-disable-next-line no-control-regex
|
|
121
|
+
if (!/[^\x00-\x7F]/.test(content))
|
|
122
|
+
return null;
|
|
123
|
+
const map = [];
|
|
124
|
+
let byteIdx = 0;
|
|
125
|
+
for (let charIdx = 0; charIdx < content.length;) {
|
|
126
|
+
const cp = content.codePointAt(charIdx);
|
|
127
|
+
const byteLen = cp <= 0x7F ? 1 : cp <= 0x7FF ? 2 : cp <= 0xFFFF ? 3 : 4;
|
|
128
|
+
const charLen = cp > 0xFFFF ? 2 : 1; // surrogate pair
|
|
129
|
+
// Every byte belonging to this character maps to the same char index
|
|
130
|
+
for (let b = 0; b < byteLen; b++) {
|
|
131
|
+
map[byteIdx + b] = charIdx;
|
|
132
|
+
}
|
|
133
|
+
byteIdx += byteLen;
|
|
134
|
+
charIdx += charLen;
|
|
135
|
+
}
|
|
136
|
+
// Sentinel so that span.end (one-past-the-last-byte) resolves correctly
|
|
137
|
+
map[byteIdx] = content.length;
|
|
138
|
+
return map;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Recursively converts every `span.start` / `span.end` in an SWC AST from
|
|
142
|
+
* UTF-8 byte offsets to JavaScript string character indices using the
|
|
143
|
+
* pre-built lookup table.
|
|
144
|
+
*/
|
|
145
|
+
function convertSpansToCharIndices(node, byteToChar) {
|
|
146
|
+
if (!node || typeof node !== 'object')
|
|
147
|
+
return;
|
|
148
|
+
if (node.span && typeof node.span.start === 'number') {
|
|
149
|
+
const charStart = byteToChar[node.span.start];
|
|
150
|
+
const charEnd = byteToChar[node.span.end];
|
|
151
|
+
if (charStart !== undefined && charEnd !== undefined) {
|
|
152
|
+
node.span = { ...node.span, start: charStart, end: charEnd };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (const key of Object.keys(node)) {
|
|
156
|
+
if (key === 'span')
|
|
157
|
+
continue;
|
|
158
|
+
const child = node[key];
|
|
159
|
+
if (Array.isArray(child)) {
|
|
160
|
+
for (const item of child) {
|
|
161
|
+
if (item && typeof item === 'object') {
|
|
162
|
+
convertSpansToCharIndices(item, byteToChar);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else if (child && typeof child === 'object') {
|
|
167
|
+
convertSpansToCharIndices(child, byteToChar);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// ─── Ignore-comment helpers ──────────────────────────────────────────────────
|
|
172
|
+
/**
|
|
173
|
+
* Matches the shared ignore directive used by both the instrumenter and the
|
|
174
|
+
* linter. The optional `-next-line` suffix is captured in group 1:
|
|
175
|
+
*
|
|
176
|
+
* i18next-instrument-ignore-next-line → suppress only the single next line
|
|
177
|
+
* i18next-instrument-ignore → suppress the whole next JSX element
|
|
178
|
+
*
|
|
179
|
+
* Works in line-comment (`// ...`) and block-comment form, including the JSX
|
|
180
|
+
* `{ /* ... * / }` form.
|
|
181
|
+
*/
|
|
182
|
+
const IGNORE_DIRECTIVE_RE = /i18next-instrument-ignore(-next-line)?/;
|
|
183
|
+
/**
|
|
184
|
+
* Scans `code` for ignore-directive comments and returns a Set of 1-based line
|
|
185
|
+
* numbers whose issues/strings should be suppressed.
|
|
186
|
+
*
|
|
187
|
+
* A directive on line `D` targets the following line `D + 1`. The behaviour
|
|
188
|
+
* depends on the directive variant:
|
|
189
|
+
*
|
|
190
|
+
* - `i18next-instrument-ignore-next-line` suppresses only line `D + 1`.
|
|
191
|
+
* - `i18next-instrument-ignore` suppresses the **entire JSX element** that
|
|
192
|
+
* begins on line `D + 1` — i.e. every line from its opening tag through its
|
|
193
|
+
* closing tag, including nested children. This makes a single directive
|
|
194
|
+
* cover multi-line elements (e.g. `<div … css={…}>…</div>`) and elements
|
|
195
|
+
* with nested children, instead of just the one physical line after it.
|
|
196
|
+
*
|
|
197
|
+
* When no AST node begins on the targeted line (e.g. the next line is a plain
|
|
198
|
+
* `t()` call rather than a JSX element), block scope falls back to suppressing
|
|
199
|
+
* the single targeted line, preserving the original line-based behaviour.
|
|
200
|
+
*
|
|
201
|
+
* `ast` spans must already be normalised to file-relative character indices
|
|
202
|
+
* (see {@link normalizeASTSpans} / {@link convertSpansToCharIndices}).
|
|
203
|
+
*/
|
|
204
|
+
function collectIgnoredLineRanges(ast, code) {
|
|
205
|
+
const ignored = new Set();
|
|
206
|
+
const lines = code.split('\n');
|
|
207
|
+
// 1-based target lines, split by directive variant.
|
|
208
|
+
const blockTargets = new Set();
|
|
209
|
+
const lineTargets = new Set();
|
|
210
|
+
for (let i = 0; i < lines.length; i++) {
|
|
211
|
+
const match = IGNORE_DIRECTIVE_RE.exec(lines[i]);
|
|
212
|
+
if (!match)
|
|
213
|
+
continue;
|
|
214
|
+
const target = i + 2; // directive on line i+1 (1-based) → target line i+2
|
|
215
|
+
if (match[1])
|
|
216
|
+
lineTargets.add(target); // `-next-line` variant
|
|
217
|
+
else
|
|
218
|
+
blockTargets.add(target);
|
|
219
|
+
}
|
|
220
|
+
if (blockTargets.size === 0 && lineTargets.size === 0)
|
|
221
|
+
return ignored;
|
|
222
|
+
// Always suppress the directly targeted line. For `-next-line` this is the
|
|
223
|
+
// whole effect; for block directives it is the fallback when no element is
|
|
224
|
+
// found, and otherwise the start of the suppressed range.
|
|
225
|
+
for (const target of lineTargets)
|
|
226
|
+
ignored.add(target);
|
|
227
|
+
for (const target of blockTargets)
|
|
228
|
+
ignored.add(target);
|
|
229
|
+
if (blockTargets.size === 0)
|
|
230
|
+
return ignored;
|
|
231
|
+
// For block directives, find the widest AST node that begins on the targeted
|
|
232
|
+
// line and expand suppression to cover its full line span. Multiple nodes can
|
|
233
|
+
// start on the same line (e.g. a JSXElement and its JSXOpeningElement); taking
|
|
234
|
+
// the largest end line picks the outermost element.
|
|
235
|
+
const widestEndLineByStartLine = new Map();
|
|
236
|
+
const visit = (node) => {
|
|
237
|
+
if (!node || typeof node !== 'object')
|
|
238
|
+
return;
|
|
239
|
+
if (node.span && typeof node.span.start === 'number') {
|
|
240
|
+
const start = lineColumnFromOffset(code, node.span.start);
|
|
241
|
+
if (start && blockTargets.has(start.line)) {
|
|
242
|
+
const end = lineColumnFromOffset(code, node.span.end);
|
|
243
|
+
if (end) {
|
|
244
|
+
const prev = widestEndLineByStartLine.get(start.line);
|
|
245
|
+
if (prev === undefined || end.line > prev) {
|
|
246
|
+
widestEndLineByStartLine.set(start.line, end.line);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
for (const key of Object.keys(node)) {
|
|
252
|
+
if (key === 'span')
|
|
253
|
+
continue;
|
|
254
|
+
const child = node[key];
|
|
255
|
+
if (Array.isArray(child)) {
|
|
256
|
+
for (const item of child)
|
|
257
|
+
visit(item);
|
|
258
|
+
}
|
|
259
|
+
else if (child && typeof child === 'object') {
|
|
260
|
+
visit(child);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
visit(ast);
|
|
265
|
+
for (const target of blockTargets) {
|
|
266
|
+
const endLine = widestEndLineByStartLine.get(target) ?? target;
|
|
267
|
+
for (let line = target; line <= endLine; line++)
|
|
268
|
+
ignored.add(line);
|
|
269
|
+
}
|
|
270
|
+
return ignored;
|
|
271
|
+
}
|
|
106
272
|
/**
|
|
107
273
|
* Finds and returns the full property node (KeyValueProperty) for the given
|
|
108
274
|
* property name from an ObjectExpression.
|
|
@@ -189,6 +355,9 @@ function getObjectPropValue(object, propName, identifierResolver) {
|
|
|
189
355
|
return undefined;
|
|
190
356
|
}
|
|
191
357
|
|
|
358
|
+
exports.buildByteToCharMap = buildByteToCharMap;
|
|
359
|
+
exports.collectIgnoredLineRanges = collectIgnoredLineRanges;
|
|
360
|
+
exports.convertSpansToCharIndices = convertSpansToCharIndices;
|
|
192
361
|
exports.findFirstTokenIndex = findFirstTokenIndex;
|
|
193
362
|
exports.getObjectPropValue = getObjectPropValue;
|
|
194
363
|
exports.getObjectPropValueExpression = getObjectPropValueExpression;
|
|
@@ -262,9 +262,9 @@ async function scanFileForCandidates(content, file, config) {
|
|
|
262
262
|
// SWC reports spans as UTF-8 byte offsets, but JavaScript strings and
|
|
263
263
|
// MagicString use UTF-16 code-unit indices. Without this conversion,
|
|
264
264
|
// every emoji / accented char / CJK char shifts all subsequent offsets.
|
|
265
|
-
const byteToChar = buildByteToCharMap(content);
|
|
265
|
+
const byteToChar = astUtils.buildByteToCharMap(content);
|
|
266
266
|
if (byteToChar) {
|
|
267
|
-
convertSpansToCharIndices(ast, byteToChar);
|
|
267
|
+
astUtils.convertSpansToCharIndices(ast, byteToChar);
|
|
268
268
|
}
|
|
269
269
|
// Detect React function component boundaries
|
|
270
270
|
detectComponentBoundaries(ast, content, components);
|
|
@@ -296,11 +296,13 @@ async function scanFileForCandidates(content, file, config) {
|
|
|
296
296
|
}
|
|
297
297
|
// Filter out candidates suppressed by ignore-comment directives.
|
|
298
298
|
// Supported comments (line or block):
|
|
299
|
-
// // i18next-instrument-ignore-next-line
|
|
300
|
-
// // i18next-instrument-ignore
|
|
299
|
+
// // i18next-instrument-ignore-next-line → suppress the single next line
|
|
300
|
+
// // i18next-instrument-ignore → suppress the whole next JSX element
|
|
301
301
|
// /* i18next-instrument-ignore-next-line */
|
|
302
302
|
// /* i18next-instrument-ignore */
|
|
303
|
-
|
|
303
|
+
// AST spans were normalised to char indices above, so the shared helper can
|
|
304
|
+
// resolve element line ranges directly.
|
|
305
|
+
const ignoredLines = astUtils.collectIgnoredLineRanges(ast, content);
|
|
304
306
|
if (ignoredLines.size > 0) {
|
|
305
307
|
const keep = [];
|
|
306
308
|
for (const c of candidates) {
|
|
@@ -318,32 +320,6 @@ async function scanFileForCandidates(content, file, config) {
|
|
|
318
320
|
}
|
|
319
321
|
return { candidates, components, languageChangeSites };
|
|
320
322
|
}
|
|
321
|
-
// ─── Ignore-comment helpers ──────────────────────────────────────────────────
|
|
322
|
-
/**
|
|
323
|
-
* Regex that matches a directive comment requesting the instrumenter to skip
|
|
324
|
-
* the **next** line. Works with both line comments (`// ...`) and block
|
|
325
|
-
* comments. The supported directives are:
|
|
326
|
-
*
|
|
327
|
-
* i18next-instrument-ignore-next-line
|
|
328
|
-
* i18next-instrument-ignore
|
|
329
|
-
*/
|
|
330
|
-
const IGNORE_RE = /i18next-instrument-ignore(?:-next-line)?/;
|
|
331
|
-
/**
|
|
332
|
-
* Scans `content` for ignore-directive comments and returns a Set of 1-based
|
|
333
|
-
* line numbers whose strings should be excluded from instrumentation.
|
|
334
|
-
*/
|
|
335
|
-
function collectIgnoredLines(content) {
|
|
336
|
-
const ignored = new Set();
|
|
337
|
-
const lines = content.split('\n');
|
|
338
|
-
for (let i = 0; i < lines.length; i++) {
|
|
339
|
-
if (IGNORE_RE.test(lines[i])) {
|
|
340
|
-
// Directive on line i → suppress the *following* line (i + 1)
|
|
341
|
-
// We store 1-based line numbers, so the suppressed line is i + 2
|
|
342
|
-
ignored.add(i + 2);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
return ignored;
|
|
346
|
-
}
|
|
347
323
|
/**
|
|
348
324
|
* Returns the 1-based line number for a character offset.
|
|
349
325
|
*/
|
|
@@ -1964,71 +1940,6 @@ async function runInstrumentOnResultPipeline(filePath, initialCandidates, plugin
|
|
|
1964
1940
|
}
|
|
1965
1941
|
return candidates;
|
|
1966
1942
|
}
|
|
1967
|
-
// ─── Byte → char offset helpers ────────────────────────────────────────────
|
|
1968
|
-
/**
|
|
1969
|
-
* Builds a lookup table from UTF-8 byte offsets to JavaScript string character
|
|
1970
|
-
* indices (UTF-16 code-unit positions).
|
|
1971
|
-
*
|
|
1972
|
-
* SWC internally represents source as UTF-8 and reports AST spans as byte
|
|
1973
|
-
* offsets into that representation. MagicString and all JavaScript String
|
|
1974
|
-
* methods operate on UTF-16 code-unit indices. For files that contain only
|
|
1975
|
-
* ASCII characters the two coincide, so this function returns `null` as a
|
|
1976
|
-
* fast path. For files with multi-byte characters (emoji, accented letters,
|
|
1977
|
-
* CJK, etc.) the returned array allows O(1) conversion of any byte offset.
|
|
1978
|
-
*/
|
|
1979
|
-
function buildByteToCharMap(content) {
|
|
1980
|
-
// Fast path: pure ASCII means byte offset ≡ char index
|
|
1981
|
-
// eslint-disable-next-line no-control-regex
|
|
1982
|
-
if (!/[^\x00-\x7F]/.test(content))
|
|
1983
|
-
return null;
|
|
1984
|
-
const map = [];
|
|
1985
|
-
let byteIdx = 0;
|
|
1986
|
-
for (let charIdx = 0; charIdx < content.length;) {
|
|
1987
|
-
const cp = content.codePointAt(charIdx);
|
|
1988
|
-
const byteLen = cp <= 0x7F ? 1 : cp <= 0x7FF ? 2 : cp <= 0xFFFF ? 3 : 4;
|
|
1989
|
-
const charLen = cp > 0xFFFF ? 2 : 1; // surrogate pair
|
|
1990
|
-
// Every byte belonging to this character maps to the same char index
|
|
1991
|
-
for (let b = 0; b < byteLen; b++) {
|
|
1992
|
-
map[byteIdx + b] = charIdx;
|
|
1993
|
-
}
|
|
1994
|
-
byteIdx += byteLen;
|
|
1995
|
-
charIdx += charLen;
|
|
1996
|
-
}
|
|
1997
|
-
// Sentinel so that span.end (one-past-the-last-byte) resolves correctly
|
|
1998
|
-
map[byteIdx] = content.length;
|
|
1999
|
-
return map;
|
|
2000
|
-
}
|
|
2001
|
-
/**
|
|
2002
|
-
* Recursively converts every `span.start` / `span.end` in an SWC AST from
|
|
2003
|
-
* UTF-8 byte offsets to JavaScript string character indices using the
|
|
2004
|
-
* pre-built lookup table.
|
|
2005
|
-
*/
|
|
2006
|
-
function convertSpansToCharIndices(node, byteToChar) {
|
|
2007
|
-
if (!node || typeof node !== 'object')
|
|
2008
|
-
return;
|
|
2009
|
-
if (node.span && typeof node.span.start === 'number') {
|
|
2010
|
-
const charStart = byteToChar[node.span.start];
|
|
2011
|
-
const charEnd = byteToChar[node.span.end];
|
|
2012
|
-
if (charStart !== undefined && charEnd !== undefined) {
|
|
2013
|
-
node.span = { ...node.span, start: charStart, end: charEnd };
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
2016
|
-
for (const key of Object.keys(node)) {
|
|
2017
|
-
if (key === 'span')
|
|
2018
|
-
continue;
|
|
2019
|
-
const child = node[key];
|
|
2020
|
-
if (Array.isArray(child)) {
|
|
2021
|
-
for (const item of child) {
|
|
2022
|
-
if (item && typeof item === 'object') {
|
|
2023
|
-
convertSpansToCharIndices(item, byteToChar);
|
|
2024
|
-
}
|
|
2025
|
-
}
|
|
2026
|
-
}
|
|
2027
|
-
else if (child && typeof child === 'object') {
|
|
2028
|
-
convertSpansToCharIndices(child, byteToChar);
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
1943
|
|
|
2033
1944
|
exports.runInstrumenter = runInstrumenter;
|
|
2034
1945
|
exports.writeExtractedKeys = writeExtractedKeys;
|
package/dist/cjs/linter.js
CHANGED
|
@@ -9,6 +9,7 @@ var node_util = require('node:util');
|
|
|
9
9
|
var logger = require('./utils/logger.js');
|
|
10
10
|
var wrapOra = require('./utils/wrap-ora.js');
|
|
11
11
|
var jsxAttributes = require('./utils/jsx-attributes.js');
|
|
12
|
+
var astUtils = require('./extractor/parsers/ast-utils.js');
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Loads all translation values from the primary locale's JSON files and returns
|
|
@@ -101,34 +102,6 @@ function isI18nextOptionKey(key) {
|
|
|
101
102
|
return true;
|
|
102
103
|
return false;
|
|
103
104
|
}
|
|
104
|
-
// ─── Ignore-comment helpers ──────────────────────────────────────────────────
|
|
105
|
-
/**
|
|
106
|
-
* Regex matching the shared ignore directive used by both the instrumenter and
|
|
107
|
-
* the linter. Supports both `-next-line` and inline variants, in line or block
|
|
108
|
-
* comment form.
|
|
109
|
-
*
|
|
110
|
-
* // i18next-instrument-ignore-next-line → suppresses the following line
|
|
111
|
-
* // i18next-instrument-ignore → suppresses the following line
|
|
112
|
-
* { /* i18next-instrument-ignore * / } → same, block-comment form
|
|
113
|
-
*/
|
|
114
|
-
const LINT_IGNORE_RE = /i18next-instrument-ignore(?:-next-line)?/;
|
|
115
|
-
/**
|
|
116
|
-
* Scans `code` for ignore-directive comments and returns a Set of 1-based
|
|
117
|
-
* line numbers whose issues should be suppressed.
|
|
118
|
-
*
|
|
119
|
-
* The directive always suppresses the **next** line (line N+1), matching the
|
|
120
|
-
* behaviour of the instrumenter's `collectIgnoredLines`.
|
|
121
|
-
*/
|
|
122
|
-
function collectLintIgnoredLines(code) {
|
|
123
|
-
const ignored = new Set();
|
|
124
|
-
const lines = code.split('\n');
|
|
125
|
-
for (let i = 0; i < lines.length; i++) {
|
|
126
|
-
if (LINT_IGNORE_RE.test(lines[i])) {
|
|
127
|
-
ignored.add(i + 2); // 1-based: directive is on line i+1, suppressed line is i+2
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return ignored;
|
|
131
|
-
}
|
|
132
105
|
// Helper to lint interpolation parameter errors in t() calls
|
|
133
106
|
function lintInterpolationParams(ast, code, config, translationValues) {
|
|
134
107
|
const issues = [];
|
|
@@ -403,14 +376,30 @@ class Linter extends node_events.EventEmitter {
|
|
|
403
376
|
continue;
|
|
404
377
|
}
|
|
405
378
|
}
|
|
379
|
+
// Normalise AST spans to file-relative character indices so the ignore
|
|
380
|
+
// directive can resolve the line range of the JSX element it precedes.
|
|
381
|
+
// (findHardcodedStrings/lintInterpolationParams locate issues via text
|
|
382
|
+
// search and don't read spans, so this only affects ignore handling.)
|
|
383
|
+
try {
|
|
384
|
+
const spanBase = ast.span.start - astUtils.findFirstTokenIndex(code);
|
|
385
|
+
astUtils.normalizeASTSpans(ast, spanBase);
|
|
386
|
+
const byteToChar = astUtils.buildByteToCharMap(code);
|
|
387
|
+
if (byteToChar)
|
|
388
|
+
astUtils.convertSpansToCharIndices(ast, byteToChar);
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
// If span normalisation fails for any reason, fall back to text-based
|
|
392
|
+
// issue detection without block-scoped ignore support.
|
|
393
|
+
}
|
|
406
394
|
// Collect hardcoded string issues
|
|
407
395
|
const hardcodedStrings = findHardcodedStrings(ast, code, config);
|
|
408
396
|
// Collect interpolation parameter issues
|
|
409
397
|
const interpolationIssues = lintInterpolationParams(ast, code, config, translationValues);
|
|
410
398
|
let allIssues = [...hardcodedStrings, ...interpolationIssues];
|
|
411
399
|
// Filter issues suppressed by ignore-directive comments.
|
|
412
|
-
//
|
|
413
|
-
|
|
400
|
+
// i18next-instrument-ignore-next-line → suppresses the single next line
|
|
401
|
+
// i18next-instrument-ignore → suppresses the whole next JSX element
|
|
402
|
+
const ignoredLines = astUtils.collectIgnoredLineRanges(ast, code);
|
|
414
403
|
if (ignoredLines.size > 0) {
|
|
415
404
|
allIssues = allIssues.filter(issue => !ignoredLines.has(issue.line));
|
|
416
405
|
}
|
package/dist/cjs/status.js
CHANGED
|
@@ -56,6 +56,11 @@ async function runStatus(config, options = {}) {
|
|
|
56
56
|
break;
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
// The primary language fails the check only on absent keys (used in code but
|
|
60
|
+
// missing from the translation file); empty placeholders are tolerated.
|
|
61
|
+
if (!hasMissing && report.primary && report.primary.totalAbsent > 0) {
|
|
62
|
+
hasMissing = true;
|
|
63
|
+
}
|
|
59
64
|
if (hasMissing) {
|
|
60
65
|
spinner.fail('Error: Incomplete translations detected.');
|
|
61
66
|
process.exit(1);
|
|
@@ -203,7 +208,13 @@ async function generateStatusReport(config) {
|
|
|
203
208
|
if (nestedRefKeys.length > 0)
|
|
204
209
|
nestedReferenceKeysByNs.set(ns, nestedRefKeys);
|
|
205
210
|
}
|
|
206
|
-
|
|
211
|
+
// The primary language is checked first so that keys used in code but absent
|
|
212
|
+
// from the primary translation file (e.g. a typo, or `extract` never run) are
|
|
213
|
+
// surfaced as well. For the primary, an empty value is a deliberate
|
|
214
|
+
// placeholder and counts as present — only truly absent keys are flagged.
|
|
215
|
+
const localesToCheck = [primaryLanguage, ...secondaryLanguages.filter((l) => l !== primaryLanguage)];
|
|
216
|
+
for (const locale of localesToCheck) {
|
|
217
|
+
const isPrimary = locale === primaryLanguage;
|
|
207
218
|
let totalTranslatedForLocale = 0;
|
|
208
219
|
let totalEmptyForLocale = 0;
|
|
209
220
|
let totalAbsentForLocale = 0;
|
|
@@ -264,11 +275,17 @@ async function generateStatusReport(config) {
|
|
|
264
275
|
const primaryState = classifyValue(primaryValue);
|
|
265
276
|
// Only fall back when the key is genuinely absent from the primary file.
|
|
266
277
|
// An empty string is intentional (placeholder from extract) — don't hide it.
|
|
278
|
+
let state = primaryState;
|
|
267
279
|
if (primaryState === 'absent' && fallbackTranslations) {
|
|
268
280
|
const fallbackValue = nestedObject.getNestedValue(fallbackTranslations, key, sep);
|
|
269
|
-
|
|
281
|
+
state = classifyValue(fallbackValue);
|
|
270
282
|
}
|
|
271
|
-
|
|
283
|
+
// For the primary language the file itself is the source of values, so an
|
|
284
|
+
// empty placeholder still means the key is present. Only a truly absent
|
|
285
|
+
// key (used in code, missing from the file) is a problem here.
|
|
286
|
+
if (isPrimary && state === 'empty')
|
|
287
|
+
return 'translated';
|
|
288
|
+
return state;
|
|
272
289
|
};
|
|
273
290
|
const processedKeys = new Set();
|
|
274
291
|
// Combine AST-extracted keys with nested-reference keys discovered in
|
|
@@ -365,13 +382,19 @@ async function generateStatusReport(config) {
|
|
|
365
382
|
totalAbsentForLocale += absentInNs;
|
|
366
383
|
totalKeysForLocale += totalInNs;
|
|
367
384
|
}
|
|
368
|
-
|
|
385
|
+
const localeStatus = {
|
|
369
386
|
totalKeys: totalKeysForLocale,
|
|
370
387
|
totalTranslated: totalTranslatedForLocale,
|
|
371
388
|
totalEmpty: totalEmptyForLocale,
|
|
372
389
|
totalAbsent: totalAbsentForLocale,
|
|
373
390
|
namespaces,
|
|
374
|
-
}
|
|
391
|
+
};
|
|
392
|
+
if (isPrimary) {
|
|
393
|
+
report.primary = localeStatus;
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
report.locales.set(locale, localeStatus);
|
|
397
|
+
}
|
|
375
398
|
}
|
|
376
399
|
return report;
|
|
377
400
|
}
|
|
@@ -419,15 +442,12 @@ async function displayStatusReport(report, config, options) {
|
|
|
419
442
|
* ✗ red — absent from file entirely (structural problem)
|
|
420
443
|
*/
|
|
421
444
|
async function displayDetailedLocaleReport(report, config, locale, namespaceFilter, hideTranslated) {
|
|
422
|
-
if (locale === config.extract.primaryLanguage) {
|
|
423
|
-
console.log(node_util.styleText('yellow', `Locale "${locale}" is the primary language. All keys are considered present.`));
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
445
|
if (!config.locales.includes(locale)) {
|
|
427
446
|
console.error(node_util.styleText('red', `Error: Locale "${locale}" is not defined in your configuration.`));
|
|
428
447
|
return;
|
|
429
448
|
}
|
|
430
|
-
const
|
|
449
|
+
const isPrimary = locale === config.extract.primaryLanguage;
|
|
450
|
+
const localeData = isPrimary ? report.primary : report.locales.get(locale);
|
|
431
451
|
if (!localeData) {
|
|
432
452
|
console.error(node_util.styleText('red', `Error: Locale "${locale}" is not a valid secondary language.`));
|
|
433
453
|
return;
|
|
@@ -465,8 +485,16 @@ async function displayDetailedLocaleReport(report, config, locale, namespaceFilt
|
|
|
465
485
|
}
|
|
466
486
|
const missingCount = totalKeysForLocale - localeData.totalTranslated;
|
|
467
487
|
if (missingCount > 0) {
|
|
468
|
-
|
|
469
|
-
|
|
488
|
+
if (isPrimary) {
|
|
489
|
+
console.log(node_util.styleText(['red', 'bold'], `\nSummary: Found ${missingCount} key(s) used in code but absent from the "${locale}" translation files. Run "i18next-cli extract" to add them.`));
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
const summaryBreakdown = buildBreakdown(localeData.totalEmpty, localeData.totalAbsent);
|
|
493
|
+
console.log(node_util.styleText(['yellow', 'bold'], `\nSummary: Found ${missingCount} incomplete translations for "${locale}" — ${summaryBreakdown}.`));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
else if (isPrimary) {
|
|
497
|
+
console.log(node_util.styleText(['green', 'bold'], `\nSummary: 🎉 All keys used in code are present in the "${locale}" translation files.`));
|
|
470
498
|
}
|
|
471
499
|
else {
|
|
472
500
|
console.log(node_util.styleText(['green', 'bold'], `\nSummary: 🎉 All keys are translated for "${locale}".`));
|
|
@@ -501,6 +529,11 @@ async function displayNamespaceSummaryReport(report, config, namespace) {
|
|
|
501
529
|
console.log(`- ${locale}: ${bar} ${percentage}% (${nsLocaleData.translatedKeys}/${nsLocaleData.totalKeys} keys)${suffix}`);
|
|
502
530
|
}
|
|
503
531
|
}
|
|
532
|
+
const primaryNsData = report.primary?.namespaces.get(namespace);
|
|
533
|
+
if (primaryNsData && primaryNsData.absentKeys > 0) {
|
|
534
|
+
const { primaryLanguage } = config.extract;
|
|
535
|
+
console.log(node_util.styleText(['red', 'bold'], `\n⚠ Primary language "${primaryLanguage}" is missing ${primaryNsData.absentKeys} key(s) that are used in code.`));
|
|
536
|
+
}
|
|
504
537
|
await printLocizeFunnel();
|
|
505
538
|
}
|
|
506
539
|
/**
|
|
@@ -531,6 +564,10 @@ async function displayOverallSummaryReport(report, config) {
|
|
|
531
564
|
const suffix = breakdown ? ` — ${breakdown}` : '';
|
|
532
565
|
console.log(`- ${locale}: ${bar} ${percentage}% (${localeData.totalTranslated}/${localeData.totalKeys} keys)${suffix}`);
|
|
533
566
|
}
|
|
567
|
+
if (report.primary && report.primary.totalAbsent > 0) {
|
|
568
|
+
console.log(node_util.styleText(['red', 'bold'], `\n⚠ Primary language "${primaryLanguage}" is missing ${report.primary.totalAbsent} key(s) that are used in code.`));
|
|
569
|
+
console.log(node_util.styleText('red', ` Run "i18next-cli status ${primaryLanguage}" for details, or "i18next-cli extract" to add them.`));
|
|
570
|
+
}
|
|
534
571
|
await printLocizeFunnel();
|
|
535
572
|
}
|
|
536
573
|
/**
|
package/dist/esm/cli.js
CHANGED
|
@@ -30,7 +30,7 @@ const program = new Command();
|
|
|
30
30
|
program
|
|
31
31
|
.name('i18next-cli')
|
|
32
32
|
.description('A unified, high-performance i18next CLI.')
|
|
33
|
-
.version('1.
|
|
33
|
+
.version('1.61.0'); // This string is replaced with the actual version at build time by rollup
|
|
34
34
|
// new: global config override option
|
|
35
35
|
program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
|
|
36
36
|
program
|
|
@@ -101,6 +101,172 @@ function lineColumnFromOffset(code, offset) {
|
|
|
101
101
|
column: lines[lines.length - 1].length
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
|
+
// ─── Byte → char offset helpers ────────────────────────────────────────────
|
|
105
|
+
/**
|
|
106
|
+
* Builds a lookup table from UTF-8 byte offsets to JavaScript string character
|
|
107
|
+
* indices (UTF-16 code-unit positions).
|
|
108
|
+
*
|
|
109
|
+
* SWC internally represents source as UTF-8 and reports AST spans as byte
|
|
110
|
+
* offsets into that representation. MagicString and all JavaScript String
|
|
111
|
+
* methods operate on UTF-16 code-unit indices. For files that contain only
|
|
112
|
+
* ASCII characters the two coincide, so this function returns `null` as a
|
|
113
|
+
* fast path. For files with multi-byte characters (emoji, accented letters,
|
|
114
|
+
* CJK, etc.) the returned array allows O(1) conversion of any byte offset.
|
|
115
|
+
*/
|
|
116
|
+
function buildByteToCharMap(content) {
|
|
117
|
+
// Fast path: pure ASCII means byte offset ≡ char index
|
|
118
|
+
// eslint-disable-next-line no-control-regex
|
|
119
|
+
if (!/[^\x00-\x7F]/.test(content))
|
|
120
|
+
return null;
|
|
121
|
+
const map = [];
|
|
122
|
+
let byteIdx = 0;
|
|
123
|
+
for (let charIdx = 0; charIdx < content.length;) {
|
|
124
|
+
const cp = content.codePointAt(charIdx);
|
|
125
|
+
const byteLen = cp <= 0x7F ? 1 : cp <= 0x7FF ? 2 : cp <= 0xFFFF ? 3 : 4;
|
|
126
|
+
const charLen = cp > 0xFFFF ? 2 : 1; // surrogate pair
|
|
127
|
+
// Every byte belonging to this character maps to the same char index
|
|
128
|
+
for (let b = 0; b < byteLen; b++) {
|
|
129
|
+
map[byteIdx + b] = charIdx;
|
|
130
|
+
}
|
|
131
|
+
byteIdx += byteLen;
|
|
132
|
+
charIdx += charLen;
|
|
133
|
+
}
|
|
134
|
+
// Sentinel so that span.end (one-past-the-last-byte) resolves correctly
|
|
135
|
+
map[byteIdx] = content.length;
|
|
136
|
+
return map;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Recursively converts every `span.start` / `span.end` in an SWC AST from
|
|
140
|
+
* UTF-8 byte offsets to JavaScript string character indices using the
|
|
141
|
+
* pre-built lookup table.
|
|
142
|
+
*/
|
|
143
|
+
function convertSpansToCharIndices(node, byteToChar) {
|
|
144
|
+
if (!node || typeof node !== 'object')
|
|
145
|
+
return;
|
|
146
|
+
if (node.span && typeof node.span.start === 'number') {
|
|
147
|
+
const charStart = byteToChar[node.span.start];
|
|
148
|
+
const charEnd = byteToChar[node.span.end];
|
|
149
|
+
if (charStart !== undefined && charEnd !== undefined) {
|
|
150
|
+
node.span = { ...node.span, start: charStart, end: charEnd };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const key of Object.keys(node)) {
|
|
154
|
+
if (key === 'span')
|
|
155
|
+
continue;
|
|
156
|
+
const child = node[key];
|
|
157
|
+
if (Array.isArray(child)) {
|
|
158
|
+
for (const item of child) {
|
|
159
|
+
if (item && typeof item === 'object') {
|
|
160
|
+
convertSpansToCharIndices(item, byteToChar);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else if (child && typeof child === 'object') {
|
|
165
|
+
convertSpansToCharIndices(child, byteToChar);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// ─── Ignore-comment helpers ──────────────────────────────────────────────────
|
|
170
|
+
/**
|
|
171
|
+
* Matches the shared ignore directive used by both the instrumenter and the
|
|
172
|
+
* linter. The optional `-next-line` suffix is captured in group 1:
|
|
173
|
+
*
|
|
174
|
+
* i18next-instrument-ignore-next-line → suppress only the single next line
|
|
175
|
+
* i18next-instrument-ignore → suppress the whole next JSX element
|
|
176
|
+
*
|
|
177
|
+
* Works in line-comment (`// ...`) and block-comment form, including the JSX
|
|
178
|
+
* `{ /* ... * / }` form.
|
|
179
|
+
*/
|
|
180
|
+
const IGNORE_DIRECTIVE_RE = /i18next-instrument-ignore(-next-line)?/;
|
|
181
|
+
/**
|
|
182
|
+
* Scans `code` for ignore-directive comments and returns a Set of 1-based line
|
|
183
|
+
* numbers whose issues/strings should be suppressed.
|
|
184
|
+
*
|
|
185
|
+
* A directive on line `D` targets the following line `D + 1`. The behaviour
|
|
186
|
+
* depends on the directive variant:
|
|
187
|
+
*
|
|
188
|
+
* - `i18next-instrument-ignore-next-line` suppresses only line `D + 1`.
|
|
189
|
+
* - `i18next-instrument-ignore` suppresses the **entire JSX element** that
|
|
190
|
+
* begins on line `D + 1` — i.e. every line from its opening tag through its
|
|
191
|
+
* closing tag, including nested children. This makes a single directive
|
|
192
|
+
* cover multi-line elements (e.g. `<div … css={…}>…</div>`) and elements
|
|
193
|
+
* with nested children, instead of just the one physical line after it.
|
|
194
|
+
*
|
|
195
|
+
* When no AST node begins on the targeted line (e.g. the next line is a plain
|
|
196
|
+
* `t()` call rather than a JSX element), block scope falls back to suppressing
|
|
197
|
+
* the single targeted line, preserving the original line-based behaviour.
|
|
198
|
+
*
|
|
199
|
+
* `ast` spans must already be normalised to file-relative character indices
|
|
200
|
+
* (see {@link normalizeASTSpans} / {@link convertSpansToCharIndices}).
|
|
201
|
+
*/
|
|
202
|
+
function collectIgnoredLineRanges(ast, code) {
|
|
203
|
+
const ignored = new Set();
|
|
204
|
+
const lines = code.split('\n');
|
|
205
|
+
// 1-based target lines, split by directive variant.
|
|
206
|
+
const blockTargets = new Set();
|
|
207
|
+
const lineTargets = new Set();
|
|
208
|
+
for (let i = 0; i < lines.length; i++) {
|
|
209
|
+
const match = IGNORE_DIRECTIVE_RE.exec(lines[i]);
|
|
210
|
+
if (!match)
|
|
211
|
+
continue;
|
|
212
|
+
const target = i + 2; // directive on line i+1 (1-based) → target line i+2
|
|
213
|
+
if (match[1])
|
|
214
|
+
lineTargets.add(target); // `-next-line` variant
|
|
215
|
+
else
|
|
216
|
+
blockTargets.add(target);
|
|
217
|
+
}
|
|
218
|
+
if (blockTargets.size === 0 && lineTargets.size === 0)
|
|
219
|
+
return ignored;
|
|
220
|
+
// Always suppress the directly targeted line. For `-next-line` this is the
|
|
221
|
+
// whole effect; for block directives it is the fallback when no element is
|
|
222
|
+
// found, and otherwise the start of the suppressed range.
|
|
223
|
+
for (const target of lineTargets)
|
|
224
|
+
ignored.add(target);
|
|
225
|
+
for (const target of blockTargets)
|
|
226
|
+
ignored.add(target);
|
|
227
|
+
if (blockTargets.size === 0)
|
|
228
|
+
return ignored;
|
|
229
|
+
// For block directives, find the widest AST node that begins on the targeted
|
|
230
|
+
// line and expand suppression to cover its full line span. Multiple nodes can
|
|
231
|
+
// start on the same line (e.g. a JSXElement and its JSXOpeningElement); taking
|
|
232
|
+
// the largest end line picks the outermost element.
|
|
233
|
+
const widestEndLineByStartLine = new Map();
|
|
234
|
+
const visit = (node) => {
|
|
235
|
+
if (!node || typeof node !== 'object')
|
|
236
|
+
return;
|
|
237
|
+
if (node.span && typeof node.span.start === 'number') {
|
|
238
|
+
const start = lineColumnFromOffset(code, node.span.start);
|
|
239
|
+
if (start && blockTargets.has(start.line)) {
|
|
240
|
+
const end = lineColumnFromOffset(code, node.span.end);
|
|
241
|
+
if (end) {
|
|
242
|
+
const prev = widestEndLineByStartLine.get(start.line);
|
|
243
|
+
if (prev === undefined || end.line > prev) {
|
|
244
|
+
widestEndLineByStartLine.set(start.line, end.line);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (const key of Object.keys(node)) {
|
|
250
|
+
if (key === 'span')
|
|
251
|
+
continue;
|
|
252
|
+
const child = node[key];
|
|
253
|
+
if (Array.isArray(child)) {
|
|
254
|
+
for (const item of child)
|
|
255
|
+
visit(item);
|
|
256
|
+
}
|
|
257
|
+
else if (child && typeof child === 'object') {
|
|
258
|
+
visit(child);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
visit(ast);
|
|
263
|
+
for (const target of blockTargets) {
|
|
264
|
+
const endLine = widestEndLineByStartLine.get(target) ?? target;
|
|
265
|
+
for (let line = target; line <= endLine; line++)
|
|
266
|
+
ignored.add(line);
|
|
267
|
+
}
|
|
268
|
+
return ignored;
|
|
269
|
+
}
|
|
104
270
|
/**
|
|
105
271
|
* Finds and returns the full property node (KeyValueProperty) for the given
|
|
106
272
|
* property name from an ObjectExpression.
|
|
@@ -187,4 +353,4 @@ function getObjectPropValue(object, propName, identifierResolver) {
|
|
|
187
353
|
return undefined;
|
|
188
354
|
}
|
|
189
355
|
|
|
190
|
-
export { findFirstTokenIndex, getObjectPropValue, getObjectPropValueExpression, getObjectProperty, isSimpleTemplateLiteral, lineColumnFromOffset, normalizeASTSpans };
|
|
356
|
+
export { buildByteToCharMap, collectIgnoredLineRanges, convertSpansToCharIndices, findFirstTokenIndex, getObjectPropValue, getObjectPropValueExpression, getObjectProperty, isSimpleTemplateLiteral, lineColumnFromOffset, normalizeASTSpans };
|
|
@@ -10,7 +10,7 @@ import { createKeyRegistry, generateKeyFromContent } from './key-generator.js';
|
|
|
10
10
|
import { createSpinnerLike } from '../../utils/wrap-ora.js';
|
|
11
11
|
import { ConsoleLogger } from '../../utils/logger.js';
|
|
12
12
|
import { ignoredAttributeSet } from '../../utils/jsx-attributes.js';
|
|
13
|
-
import { findFirstTokenIndex, normalizeASTSpans } from '../../extractor/parsers/ast-utils.js';
|
|
13
|
+
import { findFirstTokenIndex, normalizeASTSpans, buildByteToCharMap, convertSpansToCharIndices, collectIgnoredLineRanges } from '../../extractor/parsers/ast-utils.js';
|
|
14
14
|
import { getOutputPath } from '../../utils/file-utils.js';
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -294,11 +294,13 @@ async function scanFileForCandidates(content, file, config) {
|
|
|
294
294
|
}
|
|
295
295
|
// Filter out candidates suppressed by ignore-comment directives.
|
|
296
296
|
// Supported comments (line or block):
|
|
297
|
-
// // i18next-instrument-ignore-next-line
|
|
298
|
-
// // i18next-instrument-ignore
|
|
297
|
+
// // i18next-instrument-ignore-next-line → suppress the single next line
|
|
298
|
+
// // i18next-instrument-ignore → suppress the whole next JSX element
|
|
299
299
|
// /* i18next-instrument-ignore-next-line */
|
|
300
300
|
// /* i18next-instrument-ignore */
|
|
301
|
-
|
|
301
|
+
// AST spans were normalised to char indices above, so the shared helper can
|
|
302
|
+
// resolve element line ranges directly.
|
|
303
|
+
const ignoredLines = collectIgnoredLineRanges(ast, content);
|
|
302
304
|
if (ignoredLines.size > 0) {
|
|
303
305
|
const keep = [];
|
|
304
306
|
for (const c of candidates) {
|
|
@@ -316,32 +318,6 @@ async function scanFileForCandidates(content, file, config) {
|
|
|
316
318
|
}
|
|
317
319
|
return { candidates, components, languageChangeSites };
|
|
318
320
|
}
|
|
319
|
-
// ─── Ignore-comment helpers ──────────────────────────────────────────────────
|
|
320
|
-
/**
|
|
321
|
-
* Regex that matches a directive comment requesting the instrumenter to skip
|
|
322
|
-
* the **next** line. Works with both line comments (`// ...`) and block
|
|
323
|
-
* comments. The supported directives are:
|
|
324
|
-
*
|
|
325
|
-
* i18next-instrument-ignore-next-line
|
|
326
|
-
* i18next-instrument-ignore
|
|
327
|
-
*/
|
|
328
|
-
const IGNORE_RE = /i18next-instrument-ignore(?:-next-line)?/;
|
|
329
|
-
/**
|
|
330
|
-
* Scans `content` for ignore-directive comments and returns a Set of 1-based
|
|
331
|
-
* line numbers whose strings should be excluded from instrumentation.
|
|
332
|
-
*/
|
|
333
|
-
function collectIgnoredLines(content) {
|
|
334
|
-
const ignored = new Set();
|
|
335
|
-
const lines = content.split('\n');
|
|
336
|
-
for (let i = 0; i < lines.length; i++) {
|
|
337
|
-
if (IGNORE_RE.test(lines[i])) {
|
|
338
|
-
// Directive on line i → suppress the *following* line (i + 1)
|
|
339
|
-
// We store 1-based line numbers, so the suppressed line is i + 2
|
|
340
|
-
ignored.add(i + 2);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
return ignored;
|
|
344
|
-
}
|
|
345
321
|
/**
|
|
346
322
|
* Returns the 1-based line number for a character offset.
|
|
347
323
|
*/
|
|
@@ -1962,70 +1938,5 @@ async function runInstrumentOnResultPipeline(filePath, initialCandidates, plugin
|
|
|
1962
1938
|
}
|
|
1963
1939
|
return candidates;
|
|
1964
1940
|
}
|
|
1965
|
-
// ─── Byte → char offset helpers ────────────────────────────────────────────
|
|
1966
|
-
/**
|
|
1967
|
-
* Builds a lookup table from UTF-8 byte offsets to JavaScript string character
|
|
1968
|
-
* indices (UTF-16 code-unit positions).
|
|
1969
|
-
*
|
|
1970
|
-
* SWC internally represents source as UTF-8 and reports AST spans as byte
|
|
1971
|
-
* offsets into that representation. MagicString and all JavaScript String
|
|
1972
|
-
* methods operate on UTF-16 code-unit indices. For files that contain only
|
|
1973
|
-
* ASCII characters the two coincide, so this function returns `null` as a
|
|
1974
|
-
* fast path. For files with multi-byte characters (emoji, accented letters,
|
|
1975
|
-
* CJK, etc.) the returned array allows O(1) conversion of any byte offset.
|
|
1976
|
-
*/
|
|
1977
|
-
function buildByteToCharMap(content) {
|
|
1978
|
-
// Fast path: pure ASCII means byte offset ≡ char index
|
|
1979
|
-
// eslint-disable-next-line no-control-regex
|
|
1980
|
-
if (!/[^\x00-\x7F]/.test(content))
|
|
1981
|
-
return null;
|
|
1982
|
-
const map = [];
|
|
1983
|
-
let byteIdx = 0;
|
|
1984
|
-
for (let charIdx = 0; charIdx < content.length;) {
|
|
1985
|
-
const cp = content.codePointAt(charIdx);
|
|
1986
|
-
const byteLen = cp <= 0x7F ? 1 : cp <= 0x7FF ? 2 : cp <= 0xFFFF ? 3 : 4;
|
|
1987
|
-
const charLen = cp > 0xFFFF ? 2 : 1; // surrogate pair
|
|
1988
|
-
// Every byte belonging to this character maps to the same char index
|
|
1989
|
-
for (let b = 0; b < byteLen; b++) {
|
|
1990
|
-
map[byteIdx + b] = charIdx;
|
|
1991
|
-
}
|
|
1992
|
-
byteIdx += byteLen;
|
|
1993
|
-
charIdx += charLen;
|
|
1994
|
-
}
|
|
1995
|
-
// Sentinel so that span.end (one-past-the-last-byte) resolves correctly
|
|
1996
|
-
map[byteIdx] = content.length;
|
|
1997
|
-
return map;
|
|
1998
|
-
}
|
|
1999
|
-
/**
|
|
2000
|
-
* Recursively converts every `span.start` / `span.end` in an SWC AST from
|
|
2001
|
-
* UTF-8 byte offsets to JavaScript string character indices using the
|
|
2002
|
-
* pre-built lookup table.
|
|
2003
|
-
*/
|
|
2004
|
-
function convertSpansToCharIndices(node, byteToChar) {
|
|
2005
|
-
if (!node || typeof node !== 'object')
|
|
2006
|
-
return;
|
|
2007
|
-
if (node.span && typeof node.span.start === 'number') {
|
|
2008
|
-
const charStart = byteToChar[node.span.start];
|
|
2009
|
-
const charEnd = byteToChar[node.span.end];
|
|
2010
|
-
if (charStart !== undefined && charEnd !== undefined) {
|
|
2011
|
-
node.span = { ...node.span, start: charStart, end: charEnd };
|
|
2012
|
-
}
|
|
2013
|
-
}
|
|
2014
|
-
for (const key of Object.keys(node)) {
|
|
2015
|
-
if (key === 'span')
|
|
2016
|
-
continue;
|
|
2017
|
-
const child = node[key];
|
|
2018
|
-
if (Array.isArray(child)) {
|
|
2019
|
-
for (const item of child) {
|
|
2020
|
-
if (item && typeof item === 'object') {
|
|
2021
|
-
convertSpansToCharIndices(item, byteToChar);
|
|
2022
|
-
}
|
|
2023
|
-
}
|
|
2024
|
-
}
|
|
2025
|
-
else if (child && typeof child === 'object') {
|
|
2026
|
-
convertSpansToCharIndices(child, byteToChar);
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
1941
|
|
|
2031
1942
|
export { runInstrumenter, writeExtractedKeys };
|
package/dist/esm/linter.js
CHANGED
|
@@ -7,6 +7,7 @@ import { styleText } from 'node:util';
|
|
|
7
7
|
import { ConsoleLogger } from './utils/logger.js';
|
|
8
8
|
import { createSpinnerLike } from './utils/wrap-ora.js';
|
|
9
9
|
import { acceptedTags, translatableAttributes, ignoredTags, ignoredAttributeLowerSet } from './utils/jsx-attributes.js';
|
|
10
|
+
import { findFirstTokenIndex, normalizeASTSpans, buildByteToCharMap, convertSpansToCharIndices, collectIgnoredLineRanges } from './extractor/parsers/ast-utils.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Loads all translation values from the primary locale's JSON files and returns
|
|
@@ -99,34 +100,6 @@ function isI18nextOptionKey(key) {
|
|
|
99
100
|
return true;
|
|
100
101
|
return false;
|
|
101
102
|
}
|
|
102
|
-
// ─── Ignore-comment helpers ──────────────────────────────────────────────────
|
|
103
|
-
/**
|
|
104
|
-
* Regex matching the shared ignore directive used by both the instrumenter and
|
|
105
|
-
* the linter. Supports both `-next-line` and inline variants, in line or block
|
|
106
|
-
* comment form.
|
|
107
|
-
*
|
|
108
|
-
* // i18next-instrument-ignore-next-line → suppresses the following line
|
|
109
|
-
* // i18next-instrument-ignore → suppresses the following line
|
|
110
|
-
* { /* i18next-instrument-ignore * / } → same, block-comment form
|
|
111
|
-
*/
|
|
112
|
-
const LINT_IGNORE_RE = /i18next-instrument-ignore(?:-next-line)?/;
|
|
113
|
-
/**
|
|
114
|
-
* Scans `code` for ignore-directive comments and returns a Set of 1-based
|
|
115
|
-
* line numbers whose issues should be suppressed.
|
|
116
|
-
*
|
|
117
|
-
* The directive always suppresses the **next** line (line N+1), matching the
|
|
118
|
-
* behaviour of the instrumenter's `collectIgnoredLines`.
|
|
119
|
-
*/
|
|
120
|
-
function collectLintIgnoredLines(code) {
|
|
121
|
-
const ignored = new Set();
|
|
122
|
-
const lines = code.split('\n');
|
|
123
|
-
for (let i = 0; i < lines.length; i++) {
|
|
124
|
-
if (LINT_IGNORE_RE.test(lines[i])) {
|
|
125
|
-
ignored.add(i + 2); // 1-based: directive is on line i+1, suppressed line is i+2
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return ignored;
|
|
129
|
-
}
|
|
130
103
|
// Helper to lint interpolation parameter errors in t() calls
|
|
131
104
|
function lintInterpolationParams(ast, code, config, translationValues) {
|
|
132
105
|
const issues = [];
|
|
@@ -401,14 +374,30 @@ class Linter extends EventEmitter {
|
|
|
401
374
|
continue;
|
|
402
375
|
}
|
|
403
376
|
}
|
|
377
|
+
// Normalise AST spans to file-relative character indices so the ignore
|
|
378
|
+
// directive can resolve the line range of the JSX element it precedes.
|
|
379
|
+
// (findHardcodedStrings/lintInterpolationParams locate issues via text
|
|
380
|
+
// search and don't read spans, so this only affects ignore handling.)
|
|
381
|
+
try {
|
|
382
|
+
const spanBase = ast.span.start - findFirstTokenIndex(code);
|
|
383
|
+
normalizeASTSpans(ast, spanBase);
|
|
384
|
+
const byteToChar = buildByteToCharMap(code);
|
|
385
|
+
if (byteToChar)
|
|
386
|
+
convertSpansToCharIndices(ast, byteToChar);
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
// If span normalisation fails for any reason, fall back to text-based
|
|
390
|
+
// issue detection without block-scoped ignore support.
|
|
391
|
+
}
|
|
404
392
|
// Collect hardcoded string issues
|
|
405
393
|
const hardcodedStrings = findHardcodedStrings(ast, code, config);
|
|
406
394
|
// Collect interpolation parameter issues
|
|
407
395
|
const interpolationIssues = lintInterpolationParams(ast, code, config, translationValues);
|
|
408
396
|
let allIssues = [...hardcodedStrings, ...interpolationIssues];
|
|
409
397
|
// Filter issues suppressed by ignore-directive comments.
|
|
410
|
-
//
|
|
411
|
-
|
|
398
|
+
// i18next-instrument-ignore-next-line → suppresses the single next line
|
|
399
|
+
// i18next-instrument-ignore → suppresses the whole next JSX element
|
|
400
|
+
const ignoredLines = collectIgnoredLineRanges(ast, code);
|
|
412
401
|
if (ignoredLines.size > 0) {
|
|
413
402
|
allIssues = allIssues.filter(issue => !ignoredLines.has(issue.line));
|
|
414
403
|
}
|
package/dist/esm/status.js
CHANGED
|
@@ -54,6 +54,11 @@ async function runStatus(config, options = {}) {
|
|
|
54
54
|
break;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
// The primary language fails the check only on absent keys (used in code but
|
|
58
|
+
// missing from the translation file); empty placeholders are tolerated.
|
|
59
|
+
if (!hasMissing && report.primary && report.primary.totalAbsent > 0) {
|
|
60
|
+
hasMissing = true;
|
|
61
|
+
}
|
|
57
62
|
if (hasMissing) {
|
|
58
63
|
spinner.fail('Error: Incomplete translations detected.');
|
|
59
64
|
process.exit(1);
|
|
@@ -201,7 +206,13 @@ async function generateStatusReport(config) {
|
|
|
201
206
|
if (nestedRefKeys.length > 0)
|
|
202
207
|
nestedReferenceKeysByNs.set(ns, nestedRefKeys);
|
|
203
208
|
}
|
|
204
|
-
|
|
209
|
+
// The primary language is checked first so that keys used in code but absent
|
|
210
|
+
// from the primary translation file (e.g. a typo, or `extract` never run) are
|
|
211
|
+
// surfaced as well. For the primary, an empty value is a deliberate
|
|
212
|
+
// placeholder and counts as present — only truly absent keys are flagged.
|
|
213
|
+
const localesToCheck = [primaryLanguage, ...secondaryLanguages.filter((l) => l !== primaryLanguage)];
|
|
214
|
+
for (const locale of localesToCheck) {
|
|
215
|
+
const isPrimary = locale === primaryLanguage;
|
|
205
216
|
let totalTranslatedForLocale = 0;
|
|
206
217
|
let totalEmptyForLocale = 0;
|
|
207
218
|
let totalAbsentForLocale = 0;
|
|
@@ -262,11 +273,17 @@ async function generateStatusReport(config) {
|
|
|
262
273
|
const primaryState = classifyValue(primaryValue);
|
|
263
274
|
// Only fall back when the key is genuinely absent from the primary file.
|
|
264
275
|
// An empty string is intentional (placeholder from extract) — don't hide it.
|
|
276
|
+
let state = primaryState;
|
|
265
277
|
if (primaryState === 'absent' && fallbackTranslations) {
|
|
266
278
|
const fallbackValue = getNestedValue(fallbackTranslations, key, sep);
|
|
267
|
-
|
|
279
|
+
state = classifyValue(fallbackValue);
|
|
268
280
|
}
|
|
269
|
-
|
|
281
|
+
// For the primary language the file itself is the source of values, so an
|
|
282
|
+
// empty placeholder still means the key is present. Only a truly absent
|
|
283
|
+
// key (used in code, missing from the file) is a problem here.
|
|
284
|
+
if (isPrimary && state === 'empty')
|
|
285
|
+
return 'translated';
|
|
286
|
+
return state;
|
|
270
287
|
};
|
|
271
288
|
const processedKeys = new Set();
|
|
272
289
|
// Combine AST-extracted keys with nested-reference keys discovered in
|
|
@@ -363,13 +380,19 @@ async function generateStatusReport(config) {
|
|
|
363
380
|
totalAbsentForLocale += absentInNs;
|
|
364
381
|
totalKeysForLocale += totalInNs;
|
|
365
382
|
}
|
|
366
|
-
|
|
383
|
+
const localeStatus = {
|
|
367
384
|
totalKeys: totalKeysForLocale,
|
|
368
385
|
totalTranslated: totalTranslatedForLocale,
|
|
369
386
|
totalEmpty: totalEmptyForLocale,
|
|
370
387
|
totalAbsent: totalAbsentForLocale,
|
|
371
388
|
namespaces,
|
|
372
|
-
}
|
|
389
|
+
};
|
|
390
|
+
if (isPrimary) {
|
|
391
|
+
report.primary = localeStatus;
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
report.locales.set(locale, localeStatus);
|
|
395
|
+
}
|
|
373
396
|
}
|
|
374
397
|
return report;
|
|
375
398
|
}
|
|
@@ -417,15 +440,12 @@ async function displayStatusReport(report, config, options) {
|
|
|
417
440
|
* ✗ red — absent from file entirely (structural problem)
|
|
418
441
|
*/
|
|
419
442
|
async function displayDetailedLocaleReport(report, config, locale, namespaceFilter, hideTranslated) {
|
|
420
|
-
if (locale === config.extract.primaryLanguage) {
|
|
421
|
-
console.log(styleText('yellow', `Locale "${locale}" is the primary language. All keys are considered present.`));
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
443
|
if (!config.locales.includes(locale)) {
|
|
425
444
|
console.error(styleText('red', `Error: Locale "${locale}" is not defined in your configuration.`));
|
|
426
445
|
return;
|
|
427
446
|
}
|
|
428
|
-
const
|
|
447
|
+
const isPrimary = locale === config.extract.primaryLanguage;
|
|
448
|
+
const localeData = isPrimary ? report.primary : report.locales.get(locale);
|
|
429
449
|
if (!localeData) {
|
|
430
450
|
console.error(styleText('red', `Error: Locale "${locale}" is not a valid secondary language.`));
|
|
431
451
|
return;
|
|
@@ -463,8 +483,16 @@ async function displayDetailedLocaleReport(report, config, locale, namespaceFilt
|
|
|
463
483
|
}
|
|
464
484
|
const missingCount = totalKeysForLocale - localeData.totalTranslated;
|
|
465
485
|
if (missingCount > 0) {
|
|
466
|
-
|
|
467
|
-
|
|
486
|
+
if (isPrimary) {
|
|
487
|
+
console.log(styleText(['red', 'bold'], `\nSummary: Found ${missingCount} key(s) used in code but absent from the "${locale}" translation files. Run "i18next-cli extract" to add them.`));
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
const summaryBreakdown = buildBreakdown(localeData.totalEmpty, localeData.totalAbsent);
|
|
491
|
+
console.log(styleText(['yellow', 'bold'], `\nSummary: Found ${missingCount} incomplete translations for "${locale}" — ${summaryBreakdown}.`));
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
else if (isPrimary) {
|
|
495
|
+
console.log(styleText(['green', 'bold'], `\nSummary: 🎉 All keys used in code are present in the "${locale}" translation files.`));
|
|
468
496
|
}
|
|
469
497
|
else {
|
|
470
498
|
console.log(styleText(['green', 'bold'], `\nSummary: 🎉 All keys are translated for "${locale}".`));
|
|
@@ -499,6 +527,11 @@ async function displayNamespaceSummaryReport(report, config, namespace) {
|
|
|
499
527
|
console.log(`- ${locale}: ${bar} ${percentage}% (${nsLocaleData.translatedKeys}/${nsLocaleData.totalKeys} keys)${suffix}`);
|
|
500
528
|
}
|
|
501
529
|
}
|
|
530
|
+
const primaryNsData = report.primary?.namespaces.get(namespace);
|
|
531
|
+
if (primaryNsData && primaryNsData.absentKeys > 0) {
|
|
532
|
+
const { primaryLanguage } = config.extract;
|
|
533
|
+
console.log(styleText(['red', 'bold'], `\n⚠ Primary language "${primaryLanguage}" is missing ${primaryNsData.absentKeys} key(s) that are used in code.`));
|
|
534
|
+
}
|
|
502
535
|
await printLocizeFunnel();
|
|
503
536
|
}
|
|
504
537
|
/**
|
|
@@ -529,6 +562,10 @@ async function displayOverallSummaryReport(report, config) {
|
|
|
529
562
|
const suffix = breakdown ? ` — ${breakdown}` : '';
|
|
530
563
|
console.log(`- ${locale}: ${bar} ${percentage}% (${localeData.totalTranslated}/${localeData.totalKeys} keys)${suffix}`);
|
|
531
564
|
}
|
|
565
|
+
if (report.primary && report.primary.totalAbsent > 0) {
|
|
566
|
+
console.log(styleText(['red', 'bold'], `\n⚠ Primary language "${primaryLanguage}" is missing ${report.primary.totalAbsent} key(s) that are used in code.`));
|
|
567
|
+
console.log(styleText('red', ` Run "i18next-cli status ${primaryLanguage}" for details, or "i18next-cli extract" to add them.`));
|
|
568
|
+
}
|
|
532
569
|
await printLocizeFunnel();
|
|
533
570
|
}
|
|
534
571
|
/**
|
package/package.json
CHANGED
|
@@ -36,6 +36,46 @@ export declare function lineColumnFromOffset(code: string, offset: number): {
|
|
|
36
36
|
line: number;
|
|
37
37
|
column: number;
|
|
38
38
|
} | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Builds a lookup table from UTF-8 byte offsets to JavaScript string character
|
|
41
|
+
* indices (UTF-16 code-unit positions).
|
|
42
|
+
*
|
|
43
|
+
* SWC internally represents source as UTF-8 and reports AST spans as byte
|
|
44
|
+
* offsets into that representation. MagicString and all JavaScript String
|
|
45
|
+
* methods operate on UTF-16 code-unit indices. For files that contain only
|
|
46
|
+
* ASCII characters the two coincide, so this function returns `null` as a
|
|
47
|
+
* fast path. For files with multi-byte characters (emoji, accented letters,
|
|
48
|
+
* CJK, etc.) the returned array allows O(1) conversion of any byte offset.
|
|
49
|
+
*/
|
|
50
|
+
export declare function buildByteToCharMap(content: string): number[] | null;
|
|
51
|
+
/**
|
|
52
|
+
* Recursively converts every `span.start` / `span.end` in an SWC AST from
|
|
53
|
+
* UTF-8 byte offsets to JavaScript string character indices using the
|
|
54
|
+
* pre-built lookup table.
|
|
55
|
+
*/
|
|
56
|
+
export declare function convertSpansToCharIndices(node: any, byteToChar: number[]): void;
|
|
57
|
+
/**
|
|
58
|
+
* Scans `code` for ignore-directive comments and returns a Set of 1-based line
|
|
59
|
+
* numbers whose issues/strings should be suppressed.
|
|
60
|
+
*
|
|
61
|
+
* A directive on line `D` targets the following line `D + 1`. The behaviour
|
|
62
|
+
* depends on the directive variant:
|
|
63
|
+
*
|
|
64
|
+
* - `i18next-instrument-ignore-next-line` suppresses only line `D + 1`.
|
|
65
|
+
* - `i18next-instrument-ignore` suppresses the **entire JSX element** that
|
|
66
|
+
* begins on line `D + 1` — i.e. every line from its opening tag through its
|
|
67
|
+
* closing tag, including nested children. This makes a single directive
|
|
68
|
+
* cover multi-line elements (e.g. `<div … css={…}>…</div>`) and elements
|
|
69
|
+
* with nested children, instead of just the one physical line after it.
|
|
70
|
+
*
|
|
71
|
+
* When no AST node begins on the targeted line (e.g. the next line is a plain
|
|
72
|
+
* `t()` call rather than a JSX element), block scope falls back to suppressing
|
|
73
|
+
* the single targeted line, preserving the original line-based behaviour.
|
|
74
|
+
*
|
|
75
|
+
* `ast` spans must already be normalised to file-relative character indices
|
|
76
|
+
* (see {@link normalizeASTSpans} / {@link convertSpansToCharIndices}).
|
|
77
|
+
*/
|
|
78
|
+
export declare function collectIgnoredLineRanges(ast: any, code: string): Set<number>;
|
|
39
79
|
/**
|
|
40
80
|
* Finds and returns the full property node (KeyValueProperty) for the given
|
|
41
81
|
* 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,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,KAAK,kBAAkB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAA;AAEjF;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,EAAE,kBAAkB,CAAC,EAAE,kBAAkB,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAkB9J"}
|
|
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;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAE,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CA0BpE;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAE,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,IAAI,CAwBhF;AAgBD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,wBAAwB,CAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CA4D7E;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,KAAK,kBAAkB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAA;AAEjF;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,EAAE,kBAAkB,CAAC,EAAE,kBAAkB,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAkB9J"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instrumenter.d.ts","sourceRoot":"","sources":["../../../src/instrumenter/core/instrumenter.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,eAAe,EAA6B,sBAAsB,EAAiE,MAAM,gBAAgB,CAAA;AAU1N;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE,mBAAmB,EAC5B,MAAM,GAAE,MAA4B,GACnC,OAAO,CAAC,sBAAsB,CAAC,CAoNjC;
|
|
1
|
+
{"version":3,"file":"instrumenter.d.ts","sourceRoot":"","sources":["../../../src/instrumenter/core/instrumenter.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,eAAe,EAA6B,sBAAsB,EAAiE,MAAM,gBAAgB,CAAA;AAU1N;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE,mBAAmB,EAC5B,MAAM,GAAE,MAA4B,GACnC,OAAO,CAAC,sBAAsB,CAAC,CAoNjC;AAivDD;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,eAAe,EAAE,EAC7B,MAAM,EAAE,oBAAoB,EAC5B,SAAS,CAAC,EAAE,MAAM,EAClB,MAAM,GAAE,MAA4B,GACnC,OAAO,CAAC,IAAI,CAAC,CAoDf"}
|
package/types/linter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"linter.d.ts","sourceRoot":"","sources":["../src/linter.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"linter.d.ts","sourceRoot":"","sources":["../src/linter.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAM1C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,EAAE,SAAS,EAA6B,MAAM,YAAY,CAAA;AA+QpG,KAAK,cAAc,GAAG;IACpB,QAAQ,EAAE;QAAC;YACT,OAAO,EAAE,MAAM,CAAC;SACjB;KAAC,CAAC;IACH,IAAI,EAAE;QAAC;YACL,OAAO,EAAE,OAAO,CAAC;YACjB,OAAO,EAAE,MAAM,CAAC;YAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;SACpC;KAAC,CAAC;IACH,KAAK,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;CACvB,CAAA;AAED,eAAO,MAAM,uBAAuB,EAAE,MAAM,EAAiD,CAAA;AAC7F,eAAO,MAAM,6BAA6B,EAAE,MAAM,EAAqD,CAAA;AAKvG,qBAAa,MAAO,SAAQ,YAAY,CAAC,cAAc,CAAC;IACtD,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,MAAM,CAAQ;gBAET,MAAM,EAAE,oBAAoB,EAAE,MAAM,GAAE,MAA4B;IAM/E,SAAS,CAAE,KAAK,EAAE,OAAO;IAanB,GAAG;;;;;;;IAkIT,OAAO,CAAC,uBAAuB;YAOjB,qBAAqB;IAWnC,OAAO,CAAC,kBAAkB;IAM1B,OAAO,CAAC,0BAA0B;YAUpB,qBAAqB;YAgBrB,uBAAuB;CAgBtC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB;;;;;;GAE5D;AAED,wBAAsB,YAAY,CAChC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,iBAkCnD"}
|
package/types/status.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAgB,MAAM,YAAY,CAAA;AAOpE;;GAEG;AACH,UAAU,aAAa;IACrB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAgB,MAAM,YAAY,CAAA;AAOpE;;GAEG;AACH,UAAU,aAAa;IACrB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAiED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,GAAE,aAAkB,iBA4BzF"}
|