@lingual/i18n-check 0.8.18 → 0.9.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -247,7 +247,7 @@ yarn i18n:check --locales translations/folderExamples -s en-US -i "some.path.to.
247
247
 
248
248
  ### --parser-component-functions
249
249
 
250
- When using the `--unused` option, there will be situations where the i18next-parser will not be able to find components that wrap a `Trans` component.The component names for i18next-parser to match should be provided via the `--parser-component-functions` option. This option should onlybe used to define additional names for matching, a by default `Trans` will always be matched.
250
+ When using the `--unused` option, there will be situations where the i18next-parser will not be able to find components that wrap a `Trans` component. The component names for i18next-parser to match should be provided via the `--parser-component-functions` option. This option should only be used to define additional names for matching, as by default `Trans` will always be matched.
251
251
 
252
252
  ```bash
253
253
  yarn i18n:check --locales translations/i18NextMessageExamples -s en-US -f i18next
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ const findMissingKeys_1 = require("./utils/findMissingKeys");
8
8
  const findInvalidTranslations_1 = require("./utils/findInvalidTranslations");
9
9
  const findInvalidI18NextTranslations_1 = require("./utils/findInvalidI18NextTranslations");
10
10
  const cli_lib_1 = require("@formatjs/cli-lib");
11
+ const i18NextSrcParser_1 = require("./utils/i18NextSrcParser");
11
12
  const nextIntlSrcParser_1 = require("./utils/nextIntlSrcParser");
12
13
  const fs_1 = __importDefault(require("fs"));
13
14
  const path_1 = __importDefault(require("path"));
@@ -270,25 +271,6 @@ const isRecord = (data) => {
270
271
  data !== undefined);
271
272
  };
272
273
  const getI18NextKeysInCode = async (filesToParse, componentFunctions = []) => {
273
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
274
- // @ts-ignore
275
- const { transform } = await import('i18next-parser');
276
- const i18nextParser = new transform({
277
- lexers: {
278
- jsx: [
279
- {
280
- lexer: 'JsxLexer',
281
- componentFunctions: componentFunctions.concat(['Trans']),
282
- },
283
- ],
284
- tsx: [
285
- {
286
- lexer: 'JsxLexer',
287
- componentFunctions: componentFunctions.concat(['Trans']),
288
- },
289
- ],
290
- },
291
- });
292
274
  // Skip any parsed keys that have the `returnObjects` property set to true
293
275
  // As these are used dynamically, they will be skipped to prevent
294
276
  // these keys from being marked as unused.
@@ -296,7 +278,9 @@ const getI18NextKeysInCode = async (filesToParse, componentFunctions = []) => {
296
278
  const skippableKeys = [];
297
279
  filesToParse.forEach((file) => {
298
280
  const rawContent = fs_1.default.readFileSync(file, 'utf-8');
299
- const entries = i18nextParser.parser.parse(rawContent, file);
281
+ const entries = (0, i18NextSrcParser_1.getKeys)(file, {
282
+ componentFunctions: componentFunctions.concat(['Trans']),
283
+ }, rawContent);
300
284
  // Intermediate solution to retrieve all keys from the parser.
301
285
  // This will be built out to also include the namespace and check
302
286
  // the key against the namespace corresponding file.
@@ -0,0 +1,35 @@
1
+ /**
2
+ *
3
+ * Based on the original [i18next-parser](https://github.com/i18next/i18next-parser)
4
+ *
5
+ */
6
+ type Options = {
7
+ attr: string;
8
+ componentFunctions: string[];
9
+ functions: string[];
10
+ namespaceFunctions: string[];
11
+ parseGenerics: boolean;
12
+ transSupportBasicHtmlNodes: boolean;
13
+ transIdentityFunctionsToIgnore: string[];
14
+ typeMap: Record<string, unknown>;
15
+ translationFunctionsWithArgs: Record<string, {
16
+ pos: number;
17
+ storeGlobally: boolean;
18
+ keyPrefix?: string;
19
+ ns?: string;
20
+ }>;
21
+ keyPrefix?: string;
22
+ defaultNamespace?: string;
23
+ transKeepBasicHtmlNodesFor: string[];
24
+ omitAttributes: string[];
25
+ };
26
+ type FoundKey = {
27
+ key: string;
28
+ ns?: string;
29
+ namespace?: string;
30
+ functionName?: string;
31
+ defaultValue?: string;
32
+ [key: string]: unknown;
33
+ };
34
+ export declare const getKeys: (path: string, options: Partial<Options>, content: string) => FoundKey[];
35
+ export {};
@@ -0,0 +1,718 @@
1
+ "use strict";
2
+ /**
3
+ *
4
+ * Based on the original [i18next-parser](https://github.com/i18next/i18next-parser)
5
+ *
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.getKeys = void 0;
42
+ const ts = __importStar(require("typescript"));
43
+ const USE_TRANSLATION = 'useTranslation';
44
+ const WITH_TRANSLATION = 'withTranslation';
45
+ const TRANSLATION_TYPE = 'TFunction';
46
+ const getKeys = (path, options, content) => {
47
+ const sourceFile = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true);
48
+ const defaultOptions = {
49
+ attr: 'i18nKey',
50
+ componentFunctions: ['Trans'],
51
+ functions: ['t'],
52
+ namespaceFunctions: [USE_TRANSLATION, WITH_TRANSLATION],
53
+ parseGenerics: false,
54
+ transSupportBasicHtmlNodes: false,
55
+ transIdentityFunctionsToIgnore: [],
56
+ typeMap: {},
57
+ translationFunctionsWithArgs: {},
58
+ transKeepBasicHtmlNodesFor: ['br', 'strong', 'i', 'p'],
59
+ omitAttributes: ['ns', 'defaults'],
60
+ };
61
+ const parserOptions = { ...defaultOptions, ...options };
62
+ parserOptions.omitAttributes = [
63
+ parserOptions.attr,
64
+ ...parserOptions.omitAttributes,
65
+ ];
66
+ let foundKeys = [];
67
+ const visit = (node) => {
68
+ if (ts.isVariableDeclaration(node)) {
69
+ extractFromVariableDeclaration(node, parserOptions);
70
+ }
71
+ if (ts.isCallExpression(node)) {
72
+ const keys = extractFromExpression(node, parserOptions);
73
+ if (keys) {
74
+ foundKeys = foundKeys.concat(keys);
75
+ }
76
+ }
77
+ if (ts.isTaggedTemplateExpression(node)) {
78
+ const key = extractFromTaggedTemplateExpression(node, parserOptions);
79
+ if (key) {
80
+ foundKeys.push(key);
81
+ }
82
+ }
83
+ if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
84
+ const key = extractFromJsx(node, content, parserOptions);
85
+ if (key) {
86
+ foundKeys.push(key);
87
+ }
88
+ }
89
+ if (ts.isArrowFunction(node) || ts.isFunctionDeclaration(node)) {
90
+ extractFromFunction(node, parserOptions);
91
+ }
92
+ // Collect all keys defined in comments
93
+ const commentKeys = [];
94
+ ts.forEachLeadingCommentRange(content, node.getFullStart(), (pos, end, kind) => {
95
+ if (kind === ts.SyntaxKind.MultiLineCommentTrivia ||
96
+ kind === ts.SyntaxKind.SingleLineCommentTrivia) {
97
+ const text = content.slice(pos, end);
98
+ const functionPattern = '(?:' + parserOptions.functions.join('|').replace('.', '\\.') + ')';
99
+ const callPattern = '(?<=^|\\s|\\.)' + functionPattern + '\\(.*\\)';
100
+ const regexp = new RegExp(callPattern, 'g');
101
+ const expressions = text.match(regexp);
102
+ if (!expressions) {
103
+ return null;
104
+ }
105
+ const foundKeys = [];
106
+ expressions.forEach((expression) => {
107
+ const expressionKeys = (0, exports.getKeys)('', {}, expression);
108
+ if (expressionKeys) {
109
+ commentKeys.push(...expressionKeys);
110
+ }
111
+ });
112
+ if (foundKeys) {
113
+ commentKeys.push(...foundKeys);
114
+ }
115
+ }
116
+ });
117
+ if (commentKeys) {
118
+ commentKeys.forEach((key) => {
119
+ if (!foundKeys.find((k) => k.key === key.key)) {
120
+ foundKeys.push(key);
121
+ }
122
+ });
123
+ }
124
+ ts.forEachChild(node, visit);
125
+ };
126
+ ts.forEachChild(sourceFile, visit);
127
+ return foundKeys;
128
+ };
129
+ exports.getKeys = getKeys;
130
+ // Taken from: https://github.com/i18next/i18next-parser/blob/6c10a2b66ebadb8250039d078ad2a53e52753a6e/src/lexers/jsx-lexer.js#L74
131
+ const extractFromJsx = (node, content, options) => {
132
+ const tagNode = ts.isJsxElement(node) ? node.openingElement : node;
133
+ const getKey = (node) => getPropertyValue(node, options.attr);
134
+ if (options.componentFunctions.includes(expressionToName(tagNode.tagName))) {
135
+ const entry = { key: getKey(tagNode) ?? '' };
136
+ const namespace = getPropertyValue(tagNode, 'ns');
137
+ if (namespace) {
138
+ entry.namespace = namespace;
139
+ }
140
+ tagNode.attributes.properties.forEach((property) => {
141
+ if (property.kind === ts.SyntaxKind.JsxSpreadAttribute) {
142
+ return;
143
+ }
144
+ const propertyName = property.name.getText();
145
+ if (options.omitAttributes.includes(propertyName)) {
146
+ return;
147
+ }
148
+ if (property.initializer) {
149
+ if (ts.isJsxExpression(property.initializer) &&
150
+ property.initializer.expression) {
151
+ if (property.initializer.expression.kind === ts.SyntaxKind.TrueKeyword) {
152
+ entry[propertyName] = true;
153
+ }
154
+ else if (property.initializer.expression.kind === ts.SyntaxKind.FalseKeyword) {
155
+ entry[propertyName] = false;
156
+ }
157
+ else {
158
+ entry[propertyName] = `{${cleanMultiLineCode(content.slice(property.initializer.expression.pos, property.initializer.expression.end))}}`;
159
+ }
160
+ }
161
+ else if (ts.isStringLiteral(property.initializer)) {
162
+ entry[propertyName] = property.initializer.text;
163
+ }
164
+ }
165
+ else {
166
+ entry[propertyName] = true;
167
+ }
168
+ });
169
+ const nodeAsString = nodeToString(node, content, options);
170
+ const defaultsProp = getPropertyValue(tagNode, 'defaults');
171
+ let defaultValue = defaultsProp || nodeAsString;
172
+ if (entry.shouldUnescape !== true) {
173
+ defaultValue = unescape(defaultValue);
174
+ }
175
+ if (defaultValue !== '') {
176
+ entry.defaultValue = defaultValue;
177
+ if (!entry.key) {
178
+ entry.key = unescape(nodeAsString) || entry.defaultValue || '';
179
+ }
180
+ }
181
+ return entry.key ? entry : null;
182
+ }
183
+ else if (tagNode.tagName.getText() === 'Interpolate') {
184
+ const entry = { key: getKey(tagNode) ?? '' };
185
+ return entry.key ? entry : null;
186
+ }
187
+ else if (tagNode.tagName.getText() === 'Translation') {
188
+ const namespace = getPropertyValue(tagNode, 'ns');
189
+ if (namespace) {
190
+ options.defaultNamespace = namespace;
191
+ }
192
+ }
193
+ };
194
+ // Taken from: https://github.com/i18next/i18next-parser/blob/6c10a2b66ebadb8250039d078ad2a53e52753a6e/src/lexers/jsx-lexer.js#L77
195
+ const getPropertyValue = (node, attr) => {
196
+ const attribute = node.attributes.properties.find((attribute) => {
197
+ return attribute.name !== undefined && attribute.name.getText() === attr;
198
+ });
199
+ if (!attribute) {
200
+ return undefined;
201
+ }
202
+ if (!ts.isJsxAttribute(attribute) ||
203
+ !attribute.initializer ||
204
+ (ts.isJsxExpression(attribute.initializer) &&
205
+ attribute.initializer?.expression &&
206
+ ts.isIdentifier(attribute.initializer?.expression))) {
207
+ return undefined;
208
+ }
209
+ if (ts.isStringLiteral(attribute.initializer)) {
210
+ return attribute.initializer.text;
211
+ }
212
+ if (ts.isJsxExpression(attribute.initializer) &&
213
+ attribute.initializer?.expression &&
214
+ (ts.isStringLiteral(attribute.initializer?.expression) ||
215
+ ts.isNoSubstitutionTemplateLiteral(attribute.initializer.expression))) {
216
+ return attribute.initializer.expression.text;
217
+ }
218
+ return undefined;
219
+ };
220
+ // Taken from: https://github.com/i18next/i18next-parser/blob/6c10a2b66ebadb8250039d078ad2a53e52753a6e/src/lexers/javascript-lexer.js#L165
221
+ const extractFromTaggedTemplateExpression = (node, options) => {
222
+ const { tag, template } = node;
223
+ if (!options.functions.includes(tag.getText()) &&
224
+ !(ts.isPropertyAccessExpression(tag) &&
225
+ options.functions.includes(tag.name.text))) {
226
+ return undefined;
227
+ }
228
+ if (ts.isNoSubstitutionTemplateLiteral(template)) {
229
+ return { key: template.text };
230
+ }
231
+ return undefined;
232
+ };
233
+ // Taken from: https://github.com/i18next/i18next-parser/blob/6c10a2b66ebadb8250039d078ad2a53e52753a6e/src/lexers/javascript-lexer.js#L75
234
+ const extractFromVariableDeclaration = (node, options) => {
235
+ if (ts.isIdentifier(node.name)) {
236
+ return undefined;
237
+ }
238
+ const [firstElement] = node.name.elements;
239
+ if (!ts.isBindingElement(firstElement)) {
240
+ return undefined;
241
+ }
242
+ const firstElementName = firstElement?.propertyName ?? firstElement.name;
243
+ if (hasEscapedText(firstElementName) &&
244
+ firstElementName?.escapedText === 't' &&
245
+ hasEscapedText(firstElement.name) &&
246
+ options.functions.includes(firstElement?.name?.escapedText.toString()) &&
247
+ node.initializer &&
248
+ ts.isCallExpression(node.initializer) &&
249
+ ts.isIdentifier(node.initializer.expression) &&
250
+ options.namespaceFunctions.includes(
251
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
252
+ // @ts-ignore
253
+ node.initializer?.expression?.escapedText)) {
254
+ options.translationFunctionsWithArgs[firstElement.name.escapedText.toString()] = {
255
+ pos: node.initializer.pos,
256
+ storeGlobally: !(firstElement.propertyName &&
257
+ hasEscapedText(firstElement.propertyName) &&
258
+ firstElement.propertyName?.escapedText),
259
+ };
260
+ }
261
+ };
262
+ // Taken from: https://github.com/i18next/i18next-parser/blob/6c10a2b66ebadb8250039d078ad2a53e52753a6e/src/lexers/javascript-lexer.js#L138
263
+ const extractFromFunction = (node, options) => {
264
+ const tFnParam = node.parameters &&
265
+ node.parameters.find((param) => param.name &&
266
+ ts.isIdentifier(param.name) &&
267
+ options.functions.includes(param.name.text));
268
+ if (tFnParam &&
269
+ tFnParam.type &&
270
+ ts.isTypeReferenceNode(tFnParam.type) &&
271
+ tFnParam.type.typeName &&
272
+ ts.isIdentifier(tFnParam.type.typeName) &&
273
+ tFnParam.type.typeName.text === TRANSLATION_TYPE) {
274
+ if (tFnParam.type.typeArguments && tFnParam.type.typeArguments.length > 0) {
275
+ const [firstArgument] = tFnParam.type.typeArguments;
276
+ if (ts.isLiteralTypeNode(firstArgument) &&
277
+ ts.isStringLiteral(firstArgument.literal)) {
278
+ options.defaultNamespace = firstArgument.literal.text;
279
+ }
280
+ }
281
+ }
282
+ };
283
+ // Taken from: https://github.com/i18next/i18next-parser/blob/6c10a2b66ebadb8250039d078ad2a53e52753a6e/src/lexers/javascript-lexer.js#L189
284
+ const extractFromExpression = (node, options) => {
285
+ const entries = [{ key: '' }];
286
+ const functionDefinition = Object.entries(options.translationFunctionsWithArgs).find(([_name, translationFunc]) => translationFunc?.pos === node.pos);
287
+ let storeGlobally = functionDefinition?.[1].storeGlobally ?? true;
288
+ const isNamespaceFunction = (ts.isIdentifier(node.expression) &&
289
+ node.expression.escapedText &&
290
+ options.namespaceFunctions.includes(node.expression.escapedText)) ||
291
+ options.namespaceFunctions.includes(expressionToName(node.expression));
292
+ if (isNamespaceFunction && node.arguments.length > 0) {
293
+ storeGlobally =
294
+ storeGlobally ||
295
+ (hasEscapedText(node.expression) &&
296
+ node.expression.escapedText === WITH_TRANSLATION);
297
+ const [namespaceArgument, optionsArgument] = node.arguments;
298
+ const namespaces = ts.isArrayLiteralExpression(namespaceArgument)
299
+ ? namespaceArgument.elements
300
+ : [namespaceArgument];
301
+ const namespace = namespaces.find((namespace) => ts.isStringLiteral(namespace) ||
302
+ (ts.isIdentifier(namespace) && namespace.text === 'undefined'));
303
+ if (!namespace) {
304
+ // No namespace found - technically a warning could be logged here
305
+ }
306
+ else if (ts.isStringLiteral(namespace)) {
307
+ if (storeGlobally) {
308
+ options.defaultNamespace = namespace.text;
309
+ }
310
+ entries[0].ns = namespace.text;
311
+ }
312
+ if (optionsArgument && ts.isObjectLiteralExpression(optionsArgument)) {
313
+ const keyPrefixNode = optionsArgument.properties.find((p) => p.name &&
314
+ hasEscapedText(p.name) &&
315
+ p.name?.escapedText === 'keyPrefix');
316
+ if (keyPrefixNode != null && ts.isPropertyAssignment(keyPrefixNode)) {
317
+ if (storeGlobally) {
318
+ options.keyPrefix = keyPrefixNode.initializer.getText();
319
+ }
320
+ entries[0].keyPrefix = keyPrefixNode.initializer.getText();
321
+ }
322
+ }
323
+ }
324
+ const isTranslationFunction = (ts.isStringLiteral(node.expression) &&
325
+ node.expression.text &&
326
+ options.functions.includes(node.expression.text)) ||
327
+ (hasName(node.expression) &&
328
+ node.expression.name &&
329
+ hasText(node.expression.name) &&
330
+ options.functions.includes(node.expression.name.text)) ||
331
+ options.functions.includes(expressionToName(node.expression));
332
+ if (isTranslationFunction) {
333
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
334
+ // @ts-ignore
335
+ const keyArgument = node.arguments.shift();
336
+ if (!keyArgument) {
337
+ return null;
338
+ }
339
+ if (ts.isStringLiteral(keyArgument) ||
340
+ ts.isNoSubstitutionTemplateLiteral(keyArgument)) {
341
+ entries[0].key = keyArgument.text;
342
+ }
343
+ else if (ts.isBinaryExpression(keyArgument)) {
344
+ const concatenatedString = concatenateString(keyArgument);
345
+ if (!concatenatedString) {
346
+ return null;
347
+ }
348
+ entries[0].key = concatenatedString;
349
+ }
350
+ else {
351
+ return null;
352
+ }
353
+ if (options.parseGenerics && node.typeArguments) {
354
+ const nodeTypeArguments = copyNodeArray(node.typeArguments);
355
+ const typeArgument = nodeTypeArguments.shift();
356
+ const parseTypeArgument = (typeNode) => {
357
+ if (!typeNode) {
358
+ return;
359
+ }
360
+ if (ts.isTypeLiteralNode(typeNode)) {
361
+ for (const member of typeNode.members) {
362
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
363
+ // @ts-ignore
364
+ entries[0][member.name?.text] = '';
365
+ }
366
+ }
367
+ else if (ts.isTypeReferenceNode(typeNode) &&
368
+ ts.isIdentifier(typeNode.typeName)) {
369
+ const typeName = typeNode.typeName.text;
370
+ if (typeName in options.typeMap) {
371
+ Object.assign(entries[0], options.typeMap[typeName]);
372
+ }
373
+ }
374
+ else if ((ts.isUnionTypeNode(typeNode) ||
375
+ ts.isIntersectionTypeNode(typeNode)) &&
376
+ Array.isArray(typeNode.types)) {
377
+ typeNode.types.forEach((type) => parseTypeArgument(type));
378
+ }
379
+ };
380
+ parseTypeArgument(typeArgument);
381
+ }
382
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
383
+ // @ts-ignore
384
+ let optionsArgument = node.arguments.shift();
385
+ if (optionsArgument &&
386
+ (ts.isStringLiteral(optionsArgument) ||
387
+ ts.isNoSubstitutionTemplateLiteral(optionsArgument))) {
388
+ entries[0].defaultValue = optionsArgument.text;
389
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
390
+ // @ts-ignore
391
+ optionsArgument = node.arguments.shift();
392
+ }
393
+ else if (optionsArgument && ts.isBinaryExpression(optionsArgument)) {
394
+ const concatenatedString = concatenateString(optionsArgument);
395
+ if (!concatenatedString) {
396
+ return null;
397
+ }
398
+ entries[0].defaultValue = concatenatedString;
399
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
400
+ // @ts-ignore
401
+ optionsArgument = node.arguments.shift();
402
+ }
403
+ if (optionsArgument && ts.isObjectLiteralExpression(optionsArgument)) {
404
+ for (const optionProperty of optionsArgument.properties) {
405
+ if (ts.isSpreadAssignment(optionProperty)) {
406
+ // Skip as this can't be processed. A warning could be logged at some point.
407
+ }
408
+ else if (ts.isPropertyAssignment(optionProperty)) {
409
+ const propertyName = getNodeText(optionProperty.name);
410
+ if (optionProperty.initializer.kind === ts.SyntaxKind.TrueKeyword) {
411
+ entries[0][propertyName] = true;
412
+ }
413
+ else if (optionProperty.initializer.kind === ts.SyntaxKind.FalseKeyword) {
414
+ entries[0][propertyName] = false;
415
+ }
416
+ else if (ts.isCallExpression(optionProperty.initializer)) {
417
+ const nestedEntries = extractFromExpression(optionProperty.initializer, options);
418
+ if (nestedEntries) {
419
+ entries.push(...nestedEntries);
420
+ }
421
+ else {
422
+ entries[0][propertyName] =
423
+ getNodeText(optionProperty.initializer) || '';
424
+ }
425
+ }
426
+ else {
427
+ entries[0][propertyName] =
428
+ getNodeText(optionProperty.initializer) || '';
429
+ }
430
+ }
431
+ else {
432
+ entries[0][getNodeText(optionProperty.name)] = '';
433
+ }
434
+ }
435
+ }
436
+ if (entries[0].ns) {
437
+ if (typeof entries[0].ns === 'string') {
438
+ entries[0].namespace = entries[0].ns;
439
+ // Need to double check if this is even needed
440
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
441
+ // @ts-ignore
442
+ }
443
+ else if (Array.isArray(entries[0].ns) && entries[0].ns.length) {
444
+ entries[0].namespace = entries[0].ns[0];
445
+ }
446
+ }
447
+ entries[0].functionName = hasEscapedText(node.expression)
448
+ ? node.expression.escapedText.toString()
449
+ : node.expression.getText();
450
+ return entries
451
+ .map((entry) => {
452
+ const namespace = entry.ns ??
453
+ options.translationFunctionsWithArgs?.[entry.functionName ?? '']
454
+ ?.ns ??
455
+ options.defaultNamespace;
456
+ return namespace
457
+ ? {
458
+ ...entry,
459
+ namespace,
460
+ }
461
+ : entry;
462
+ })
463
+ .map(({ functionName, ...key }) => {
464
+ const keyPrefix = options.translationFunctionsWithArgs?.[functionName ?? '']
465
+ ?.keyPrefix ?? options.keyPrefix;
466
+ return keyPrefix
467
+ ? {
468
+ ...key,
469
+ keyPrefix,
470
+ }
471
+ : key;
472
+ });
473
+ }
474
+ const isTranslationFunctionCreation = hasEscapedText(node.expression) &&
475
+ node.expression.escapedText &&
476
+ options.namespaceFunctions.includes(node.expression.escapedText);
477
+ if (isTranslationFunctionCreation) {
478
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
479
+ // @ts-ignore
480
+ options.translationFunctionsWithArgs[functionDefinition?.[0] ?? ''] =
481
+ entries[0];
482
+ }
483
+ return null;
484
+ };
485
+ // Taken from: https://github.com/i18next/i18next-parser/blob/6c10a2b66ebadb8250039d078ad2a53e52753a6e/src/lexers/javascript-lexer.js#L419
486
+ const concatenateString = (binaryExpression, string = '') => {
487
+ if (!ts.isPlusToken(binaryExpression.operatorToken)) {
488
+ return;
489
+ }
490
+ if (ts.isBinaryExpression(binaryExpression.left)) {
491
+ string += concatenateString(binaryExpression.left, string);
492
+ }
493
+ else if (ts.isStringLiteral(binaryExpression.left)) {
494
+ string += binaryExpression.left.text;
495
+ }
496
+ else {
497
+ return;
498
+ }
499
+ if (ts.isBinaryExpression(binaryExpression.right)) {
500
+ string += concatenateString(binaryExpression.right, string);
501
+ }
502
+ else if (ts.isStringLiteral(binaryExpression.right)) {
503
+ string += binaryExpression.right.text;
504
+ }
505
+ else {
506
+ return;
507
+ }
508
+ return string;
509
+ };
510
+ // Taken from: https://github.com/i18next/i18next-parser/blob/6c10a2b66ebadb8250039d078ad2a53e52753a6e/src/lexers/jsx-lexer.js#L186
511
+ const nodeToString = (node, content, options) => {
512
+ if (ts.isJsxSelfClosingElement(node)) {
513
+ return '';
514
+ }
515
+ const elements = parseElements(node.children, content, options);
516
+ const elementsToString = (elements) => elements
517
+ .map((element, index) => {
518
+ switch (element.type) {
519
+ case 'js':
520
+ case 'text':
521
+ return element.content;
522
+ case 'tag': {
523
+ const useTagName = element.isBasic &&
524
+ options.transSupportBasicHtmlNodes &&
525
+ options.transKeepBasicHtmlNodesFor.includes(element.name);
526
+ const elementName = useTagName ? element.name : index;
527
+ const childrenString = elementsToString(element.children);
528
+ return childrenString || !(useTagName && element.selfClosing)
529
+ ? `<${elementName}>${childrenString}</${elementName}>`
530
+ : `<${elementName} />`;
531
+ }
532
+ default:
533
+ // Do nothing
534
+ }
535
+ })
536
+ .join('');
537
+ return elementsToString(elements);
538
+ };
539
+ // Taken from: https://github.com/i18next/i18next-parser/blob/6c10a2b66ebadb8250039d078ad2a53e52753a6e/src/lexers/jsx-lexer.js#L226
540
+ const parseElements = (elements, content, options) => {
541
+ if (!elements) {
542
+ return [];
543
+ }
544
+ return elements
545
+ .map((elem) => {
546
+ if (ts.isJsxText(elem)) {
547
+ return {
548
+ type: 'text',
549
+ content: cleanMultiLineCode(elem.text),
550
+ };
551
+ }
552
+ else if (ts.isJsxElement(elem) || ts.isJsxSelfClosingElement(elem)) {
553
+ const element = ts.isJsxElement(elem) ? elem.openingElement : elem;
554
+ const name = hasEscapedText(element.tagName)
555
+ ? element.tagName.escapedText.toString()
556
+ : element.tagName.getText();
557
+ const isBasic = !element.attributes.properties.length;
558
+ const hasDynamicChildren = element.attributes.properties.find((prop) => ts.isJsxAttribute(prop) &&
559
+ hasEscapedText(prop.name) &&
560
+ prop.name.escapedText === 'i18nIsDynamicList');
561
+ return {
562
+ type: 'tag',
563
+ children: hasDynamicChildren || !ts.isJsxElement(elem)
564
+ ? []
565
+ : parseElements(elem.children, content, options),
566
+ name,
567
+ isBasic,
568
+ selfClosing: ts.isJsxSelfClosingElement(elem),
569
+ };
570
+ }
571
+ else if (ts.isJsxExpression(elem)) {
572
+ if (!elem.expression) {
573
+ return {
574
+ type: 'text',
575
+ content: '',
576
+ };
577
+ }
578
+ //Taken from: https://github.com/i18next/i18next-parser/blob/6c10a2b66ebadb8250039d078ad2a53e52753a6e/src/lexers/jsx-lexer.js#L265
579
+ while (hasExpression(elem) &&
580
+ elem.expression &&
581
+ ts.isAsExpression(elem.expression)) {
582
+ elem = elem.expression;
583
+ }
584
+ if (hasExpression(elem) &&
585
+ ts.isCallExpression(elem.expression) &&
586
+ ts.isIdentifier(elem.expression.expression) &&
587
+ options.transIdentityFunctionsToIgnore.includes(elem.expression.expression.escapedText.toString()) &&
588
+ elem.expression.arguments.length >= 1) {
589
+ // Taken from: https://github.com/i18next/i18next-parser/blob/6c10a2b66ebadb8250039d078ad2a53e52753a6e/src/lexers/jsx-lexer.js#L291
590
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
591
+ // @ts-ignore
592
+ elem = { expression: elem.expression.arguments[0] };
593
+ }
594
+ if (hasExpression(elem) && ts.isStringLiteral(elem.expression)) {
595
+ return {
596
+ type: 'text',
597
+ content: elem.expression.text,
598
+ };
599
+ }
600
+ else if (hasExpression(elem) &&
601
+ ts.isObjectLiteralExpression(elem.expression)) {
602
+ const nonFormatProperties = elem.expression.properties.filter((prop) => prop.name?.getText() !== 'format');
603
+ const formatProperty = elem.expression.properties.find((prop) => prop.name?.getText() === 'format');
604
+ if (nonFormatProperties.length > 1) {
605
+ return {
606
+ type: 'text',
607
+ content: '',
608
+ };
609
+ }
610
+ const nonFormatPropertyName = nonFormatProperties[0]?.name
611
+ ? getNodeText(nonFormatProperties[0].name)
612
+ : '';
613
+ const value = formatProperty && ts.isPropertyAssignment(formatProperty)
614
+ ? `${nonFormatPropertyName}, ${getNodeText(formatProperty.initializer)}`
615
+ : nonFormatPropertyName;
616
+ return {
617
+ type: 'js',
618
+ content: `{{${value}}}`,
619
+ };
620
+ }
621
+ if (hasExpression(elem)) {
622
+ const slicedExpression = content.slice(elem.expression.pos, elem.expression.end);
623
+ return {
624
+ type: 'js',
625
+ content: `{${slicedExpression}}`,
626
+ };
627
+ }
628
+ }
629
+ })
630
+ .filter((elem) => elem !== undefined)
631
+ .filter((elem) => elem.type !== 'text' || elem.content);
632
+ };
633
+ // Taken from: https://github.com/i18next/i18next-parser/blob/6c10a2b66ebadb8250039d078ad2a53e52753a6e/src/lexers/jsx-lexer.js#L220
634
+ const cleanMultiLineCode = (text) => {
635
+ return text
636
+ .replace(/(^(\n|\r)\s*)|((\n|\r)\s*$)/g, '')
637
+ .replace(/(\n|\r)\s*/g, ' ');
638
+ };
639
+ // Taken from: https://github.com/i18next/i18next-parser/blob/6c10a2b66ebadb8250039d078ad2a53e52753a6e/src/lexers/javascript-lexer.js#L447
640
+ const expressionToName = (expression) => {
641
+ if (!expression) {
642
+ return '';
643
+ }
644
+ if (ts.isStringLiteral(expression) || ts.isIdentifier(expression)) {
645
+ return expression.text;
646
+ }
647
+ else if (hasExpression(expression)) {
648
+ return [
649
+ expressionToName(expression.expression),
650
+ expressionToName(expression.name),
651
+ ]
652
+ .filter((s) => s && s.length > 0)
653
+ .join('.');
654
+ }
655
+ return '';
656
+ };
657
+ const hasExpression = (node) => {
658
+ return ('expression' in node &&
659
+ !!node.expression &&
660
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
661
+ ts.isExpression(node.expression));
662
+ };
663
+ const hasEscapedText = (node) => {
664
+ return ts.isIdentifier(node) || ts.isPrivateIdentifier(node);
665
+ };
666
+ const hasName = (node) => {
667
+ return 'name' in node && node.name !== undefined;
668
+ };
669
+ function hasText(node) {
670
+ const result = node &&
671
+ (ts.isStringLiteral(node) ||
672
+ ts.isNumericLiteral(node) ||
673
+ ts.isBigIntLiteral(node) ||
674
+ ts.isNoSubstitutionTemplateLiteral(node) ||
675
+ ts.isRegularExpressionLiteral(node) ||
676
+ ts.isIdentifier(node));
677
+ return result;
678
+ }
679
+ const getNodeText = (node) => {
680
+ if ('text' in node) {
681
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
682
+ return node.text;
683
+ }
684
+ return node.getText();
685
+ };
686
+ const copyNodeArray = (nodes) => {
687
+ return Array.from(nodes);
688
+ };
689
+ // Uses the original unescape code from i18next-parser
690
+ // to ensure that the found keys reflect the original parser implementation
691
+ // Taken from: https://github.com/i18next/i18next-parser/blob/6c10a2b66ebadb8250039d078ad2a53e52753a6e/src/helpers.js#L317
692
+ const unescape = (text) => {
693
+ const matchHtmlEntity = /&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34|nbsp|#160|copy|#169|reg|#174|hellip|#8230|#x2F|#47);/g;
694
+ const htmlEntities = {
695
+ '&amp;': '&',
696
+ '&#38;': '&',
697
+ '&lt;': '<',
698
+ '&#60;': '<',
699
+ '&gt;': '>',
700
+ '&#62;': '>',
701
+ '&apos;': "'",
702
+ '&#39;': "'",
703
+ '&quot;': '"',
704
+ '&#34;': '"',
705
+ '&nbsp;': ' ',
706
+ '&#160;': ' ',
707
+ '&copy;': '©',
708
+ '&#169;': '©',
709
+ '&reg;': '®',
710
+ '&#174;': '®',
711
+ '&hellip;': '…',
712
+ '&#8230;': '…',
713
+ '&#x2F;': '/',
714
+ '&#47;': '/',
715
+ };
716
+ const unescapeHtmlEntity = (m) => htmlEntities[m];
717
+ return text.replace(matchHtmlEntity, unescapeHtmlEntity);
718
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lingual/i18n-check",
3
- "version": "0.8.18",
3
+ "version": "0.9.0-beta.1",
4
4
  "description": "i18n translation messages check",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -28,7 +28,6 @@
28
28
  "chalk": "^4.1.2",
29
29
  "commander": "^12.1.0",
30
30
  "glob": "12.0.0",
31
- "i18next-parser": "^9.3.0",
32
31
  "js-yaml": "^4.1.1",
33
32
  "typescript": "^5.9.3"
34
33
  },
@@ -36,7 +35,6 @@
36
35
  "@eslint/js": "^9.39.1",
37
36
  "@types/js-yaml": "^4.0.9",
38
37
  "@types/node": "^22.19.1",
39
- "@types/vinyl": "^2.0.12",
40
38
  "braces": "^3.0.3",
41
39
  "eslint": "^9.39.1",
42
40
  "globals": "^16.5.0",