@lingual/i18n-check 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ export declare const extract: (filesPaths: string[]) => {
2
+ key: string;
3
+ meta: {
4
+ file: string;
5
+ namespace?: string;
6
+ dynamic?: boolean;
7
+ };
8
+ }[];
@@ -0,0 +1,432 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.extract = void 0;
40
+ const node_fs_1 = __importDefault(require("node:fs"));
41
+ const ts = __importStar(require("typescript"));
42
+ const USE_TRANSLATIONS = 'useTranslations';
43
+ const GET_TRANSLATIONS = 'getTranslations';
44
+ const COMMENT_CONTAINS_STATIC_KEY_REGEX = /i18n-check t\((["'])(.*?[^\\])(["'])\)/;
45
+ const extract = (filesPaths) => {
46
+ return filesPaths.flatMap(getKeys).sort((a, b) => {
47
+ return a.key > b.key ? 1 : -1;
48
+ });
49
+ };
50
+ exports.extract = extract;
51
+ const getKeys = (path) => {
52
+ const content = node_fs_1.default.readFileSync(path, 'utf-8');
53
+ const sourceFile = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true);
54
+ const foundKeys = [];
55
+ let namespaces = [];
56
+ const getCurrentNamespaces = (range = 1) => {
57
+ if (namespaces.length > 0) {
58
+ return namespaces.slice(namespaces.length - range);
59
+ }
60
+ return null;
61
+ };
62
+ const getCurrentNamespaceForIdentifier = (name) => {
63
+ return [...namespaces].reverse().find((namespace) => {
64
+ return namespace.variable === name;
65
+ });
66
+ };
67
+ const pushNamespace = (namespace) => {
68
+ namespaces.push(namespace);
69
+ };
70
+ const setNamespaceAsDynamic = (name) => {
71
+ namespaces = namespaces.map((namespace) => {
72
+ if (namespace.name === name) {
73
+ return { ...namespace, dynamic: true };
74
+ }
75
+ return namespace;
76
+ });
77
+ };
78
+ const removeNamespaces = (range = 1) => {
79
+ if (namespaces.length > 0) {
80
+ namespaces = namespaces.slice(0, namespaces.length - range);
81
+ }
82
+ };
83
+ const visit = (node) => {
84
+ let key = null;
85
+ const initialNamespacesLength = namespaces.length;
86
+ if (node === undefined) {
87
+ return;
88
+ }
89
+ if (ts.isVariableDeclaration(node)) {
90
+ if (node.initializer && ts.isCallExpression(node.initializer)) {
91
+ if (ts.isIdentifier(node.initializer.expression)) {
92
+ // Search for `useTranslations` calls and extract the namespace
93
+ // Additionally check for assigned variable name, as it might differ
94
+ // from the default `t`, i.e.: const other = useTranslations("namespace1");
95
+ if (node.initializer.expression.text === USE_TRANSLATIONS) {
96
+ const [argument] = node.initializer.arguments;
97
+ const variable = ts.isIdentifier(node.name) ? node.name.text : 't';
98
+ if (argument && ts.isStringLiteral(argument)) {
99
+ pushNamespace({ name: argument.text, variable });
100
+ }
101
+ else if (argument === undefined) {
102
+ pushNamespace({ name: '', variable });
103
+ }
104
+ }
105
+ }
106
+ }
107
+ // Search for `getTranslations` calls and extract the namespace
108
+ // There are two different ways `getTranslations` can be used:
109
+ //
110
+ // import {getTranslations} from 'next-intl/server';
111
+ // const t = await getTranslations(namespace?);
112
+ // const t = await getTranslations({locale, namespace});
113
+ //
114
+ // Additionally check for assigned variable name, as it might differ
115
+ // from the default `t`, i.e.: const other = getTranslations("namespace1");
116
+ // Simplified usage in async components
117
+ if (node.initializer && ts.isAwaitExpression(node.initializer)) {
118
+ if (ts.isCallExpression(node.initializer.expression) &&
119
+ ts.isIdentifier(node.initializer.expression.expression)) {
120
+ if (node.initializer.expression.expression.text === GET_TRANSLATIONS) {
121
+ const [argument] = node.initializer.expression.arguments;
122
+ const variable = ts.isIdentifier(node.name) ? node.name.text : 't';
123
+ if (argument && ts.isObjectLiteralExpression(argument)) {
124
+ argument.properties.forEach((property) => {
125
+ if (property &&
126
+ ts.isPropertyAssignment(property) &&
127
+ property.name &&
128
+ ts.isIdentifier(property.name) &&
129
+ property.name.text === 'namespace' &&
130
+ ts.isStringLiteral(property.initializer)) {
131
+ pushNamespace({ name: property.initializer.text, variable });
132
+ }
133
+ else {
134
+ pushNamespace({ name: '', variable });
135
+ }
136
+ });
137
+ }
138
+ else if (argument && ts.isStringLiteral(argument)) {
139
+ pushNamespace({ name: argument.text, variable });
140
+ }
141
+ else if (argument === undefined) {
142
+ pushNamespace({ name: '', variable });
143
+ }
144
+ }
145
+ }
146
+ // Check if getTranslations is called inside a promise.all
147
+ // Example:
148
+ // const [data, t] = await Promise.all([
149
+ // loadData(id),
150
+ // getTranslations('asyncPromiseAll'),
151
+ // ]);
152
+ if (ts.isCallExpression(node.initializer.expression) &&
153
+ node.initializer.expression.arguments.length > 0 &&
154
+ ts.isArrayLiteralExpression(node.initializer.expression.arguments[0])) {
155
+ const functionNameIndex = node.initializer.expression.arguments[0].elements.findIndex((argument) => {
156
+ return (ts.isCallExpression(argument) &&
157
+ ts.isIdentifier(argument.expression) &&
158
+ argument.expression.text === GET_TRANSLATIONS);
159
+ });
160
+ // Try to find the correct function name via the position in the variable declaration
161
+ if (functionNameIndex !== -1 &&
162
+ ts.isArrayBindingPattern(node.name) &&
163
+ ts.isBindingElement(node.name.elements[functionNameIndex]) &&
164
+ ts.isIdentifier(node.name.elements[functionNameIndex].name)) {
165
+ const variable = node.name.elements[functionNameIndex].name.text;
166
+ const [argument] = ts.isCallExpression(node.initializer.expression.arguments[0].elements[functionNameIndex])
167
+ ? node.initializer.expression.arguments[0].elements[functionNameIndex].arguments
168
+ : [];
169
+ if (argument && ts.isObjectLiteralExpression(argument)) {
170
+ argument.properties.forEach((property) => {
171
+ if (property &&
172
+ ts.isPropertyAssignment(property) &&
173
+ property.name &&
174
+ ts.isIdentifier(property.name) &&
175
+ property.name.text === 'namespace' &&
176
+ ts.isStringLiteral(property.initializer)) {
177
+ pushNamespace({ name: property.initializer.text, variable });
178
+ }
179
+ });
180
+ }
181
+ else if (argument && ts.isStringLiteral(argument)) {
182
+ pushNamespace({ name: argument.text, variable });
183
+ }
184
+ else if (argument === undefined) {
185
+ pushNamespace({ name: '', variable });
186
+ }
187
+ }
188
+ }
189
+ }
190
+ }
191
+ // Search for direct inline calls and extract namespace and key
192
+ //
193
+ // useTranslations("ns1")("one")
194
+ // useTranslations("ns1").rich("one");
195
+ // useTranslations("ns1").raw("one");
196
+ if (ts.isExpressionStatement(node)) {
197
+ let inlineNamespace = null;
198
+ if (node.expression && ts.isCallExpression(node.expression)) {
199
+ // Search: useTranslations("ns1")("one")
200
+ if (ts.isCallExpression(node.expression.expression) &&
201
+ ts.isIdentifier(node.expression.expression.expression)) {
202
+ if (node.expression.expression.expression.text === USE_TRANSLATIONS) {
203
+ const [argument] = node.expression.expression.arguments;
204
+ if (argument && ts.isStringLiteral(argument)) {
205
+ inlineNamespace = argument.text;
206
+ }
207
+ }
208
+ }
209
+ // Search: useTranslations("ns1").*("one")
210
+ if (ts.isPropertyAccessExpression(node.expression.expression) &&
211
+ ts.isCallExpression(node.expression.expression.expression) &&
212
+ ts.isIdentifier(node.expression.expression.expression.expression)) {
213
+ if (node.expression.expression.expression.expression.text ===
214
+ USE_TRANSLATIONS) {
215
+ const [argument] = node.expression.expression.expression.arguments;
216
+ if (argument && ts.isStringLiteral(argument)) {
217
+ inlineNamespace = argument.text;
218
+ }
219
+ const [callArgument] = node.expression.arguments;
220
+ if (callArgument && ts.isStringLiteral(callArgument)) {
221
+ const key = callArgument.text;
222
+ if (key) {
223
+ foundKeys.push({
224
+ key: inlineNamespace ? `${inlineNamespace}.${key}` : key,
225
+ meta: { file: path, namespace: inlineNamespace ?? undefined },
226
+ });
227
+ }
228
+ }
229
+ }
230
+ }
231
+ }
232
+ }
233
+ // Check if a function has the t function as a parameter
234
+ // and if a namespace can be extracted from it
235
+ if (ts.isFunctionLike(node)) {
236
+ // The first scenario is the t function is defined as:
237
+ // someFn(t: ReturnType<typeof useTranslations<'someNamespace'>>)
238
+ const tFunctionParam = node.parameters &&
239
+ node.parameters.find((param) => param.type &&
240
+ ts.isTypeReferenceNode(param.type) &&
241
+ param.type.typeName &&
242
+ ts.isIdentifier(param.type.typeName) &&
243
+ param.type.typeName.text === 'ReturnType' &&
244
+ param.type.typeArguments &&
245
+ param.type.typeArguments.length > 0 &&
246
+ ts.isTypeQueryNode(param.type.typeArguments[0]) &&
247
+ ts.isIdentifier(param.type.typeArguments[0].exprName) &&
248
+ param.type.typeArguments[0].exprName.text === USE_TRANSLATIONS);
249
+ if (tFunctionParam !== undefined &&
250
+ tFunctionParam.type &&
251
+ ts.isTypeReferenceNode(tFunctionParam.type) &&
252
+ tFunctionParam.type.typeArguments &&
253
+ tFunctionParam.type.typeArguments.length > 0 &&
254
+ ts.isTypeQueryNode(tFunctionParam.type.typeArguments[0]) &&
255
+ ts.isIdentifier(tFunctionParam.type.typeArguments[0].exprName) &&
256
+ tFunctionParam.type.typeArguments[0].exprName.text === USE_TRANSLATIONS) {
257
+ const [namespaceArgument] = tFunctionParam.type.typeArguments[0].typeArguments &&
258
+ tFunctionParam.type.typeArguments[0].typeArguments.length > 0
259
+ ? tFunctionParam.type.typeArguments[0].typeArguments
260
+ : [];
261
+ if (ts.isIdentifier(tFunctionParam.name)) {
262
+ const name = namespaceArgument &&
263
+ ts.isLiteralTypeNode(namespaceArgument) &&
264
+ ts.isStringLiteral(namespaceArgument.literal)
265
+ ? namespaceArgument.literal.text
266
+ : '';
267
+ pushNamespace({
268
+ name,
269
+ variable: tFunctionParam.name.text,
270
+ });
271
+ }
272
+ }
273
+ // The second scenario is the t function is defined as an object property:
274
+ // someFn({t}: {t: ReturnType<typeof useTranslations<'someNamespace'>>}>
275
+ const tFunctionParamAsProperty = node.parameters &&
276
+ node.parameters.find((param) => param.type &&
277
+ ts.isTypeLiteralNode(param.type) &&
278
+ param.type.members.find((member) => {
279
+ return (ts.isPropertySignature(member) &&
280
+ member.type &&
281
+ ts.isTypeReferenceNode(member.type) &&
282
+ member.type.typeName &&
283
+ ts.isIdentifier(member.type.typeName) &&
284
+ member.type.typeName.text === 'ReturnType' &&
285
+ member.type.typeArguments &&
286
+ member.type.typeArguments.length > 0 &&
287
+ ts.isTypeQueryNode(member.type.typeArguments[0]) &&
288
+ ts.isIdentifier(member.type.typeArguments[0].exprName) &&
289
+ member.type.typeArguments[0].exprName.text === USE_TRANSLATIONS);
290
+ }));
291
+ if (tFunctionParamAsProperty !== undefined &&
292
+ tFunctionParamAsProperty.type &&
293
+ ts.isTypeLiteralNode(tFunctionParamAsProperty.type)) {
294
+ const fnType = tFunctionParamAsProperty.type.members.find((member) => {
295
+ return (ts.isPropertySignature(member) &&
296
+ member.type &&
297
+ ts.isTypeReferenceNode(member.type) &&
298
+ member.type.typeName &&
299
+ ts.isIdentifier(member.type.typeName) &&
300
+ member.type.typeName.text === 'ReturnType' &&
301
+ member.type.typeArguments &&
302
+ member.type.typeArguments.length > 0 &&
303
+ ts.isTypeQueryNode(member.type.typeArguments[0]) &&
304
+ ts.isIdentifier(member.type.typeArguments[0].exprName) &&
305
+ member.type.typeArguments[0].exprName.text === USE_TRANSLATIONS);
306
+ });
307
+ if (fnType &&
308
+ ts.isPropertySignature(fnType) &&
309
+ fnType.type &&
310
+ ts.isTypeReferenceNode(fnType.type) &&
311
+ fnType.type.typeArguments &&
312
+ fnType.type.typeArguments.length > 0 &&
313
+ ts.isTypeQueryNode(fnType.type.typeArguments[0])) {
314
+ const [namespaceArgument] = fnType.type.typeArguments[0].typeArguments &&
315
+ fnType.type.typeArguments[0].typeArguments.length > 0
316
+ ? fnType.type.typeArguments[0].typeArguments
317
+ : [];
318
+ if (fnType.name && ts.isIdentifier(fnType.name)) {
319
+ const name = namespaceArgument &&
320
+ ts.isLiteralTypeNode(namespaceArgument) &&
321
+ ts.isStringLiteral(namespaceArgument.literal)
322
+ ? namespaceArgument.literal.text
323
+ : '';
324
+ pushNamespace({
325
+ name,
326
+ variable: fnType.name.text,
327
+ });
328
+ }
329
+ }
330
+ }
331
+ }
332
+ // Search for `t()` calls
333
+ if (getCurrentNamespaces() !== null &&
334
+ ts.isCallExpression(node) &&
335
+ ts.isIdentifier(node.expression)) {
336
+ const expressionName = node.expression.text;
337
+ const namespace = getCurrentNamespaceForIdentifier(expressionName);
338
+ if (namespace) {
339
+ const [argument] = node.arguments;
340
+ if (argument && ts.isStringLiteral(argument)) {
341
+ key = { name: argument.text, identifier: expressionName };
342
+ }
343
+ else if (argument && ts.isIdentifier(argument)) {
344
+ setNamespaceAsDynamic(namespace.name);
345
+ }
346
+ else if (argument && ts.isTemplateExpression(argument)) {
347
+ setNamespaceAsDynamic(namespace.name);
348
+ }
349
+ }
350
+ }
351
+ // Search for `t.*()` calls, i.e. t.html() or t.rich()
352
+ if (getCurrentNamespaces() !== null &&
353
+ ts.isCallExpression(node) &&
354
+ ts.isPropertyAccessExpression(node.expression) &&
355
+ ts.isIdentifier(node.expression.expression)) {
356
+ const expressionName = node.expression.expression.text;
357
+ const namespace = getCurrentNamespaceForIdentifier(expressionName);
358
+ if (namespace) {
359
+ const [argument] = node.arguments;
360
+ if (argument && ts.isStringLiteral(argument)) {
361
+ key = { name: argument.text, identifier: expressionName };
362
+ }
363
+ else if (argument && ts.isIdentifier(argument)) {
364
+ setNamespaceAsDynamic(namespace.name);
365
+ }
366
+ else if (argument && ts.isTemplateExpression(argument)) {
367
+ setNamespaceAsDynamic(namespace.name);
368
+ }
369
+ }
370
+ }
371
+ if (key) {
372
+ const namespace = getCurrentNamespaceForIdentifier(key.identifier);
373
+ const namespaceName = namespace ? namespace.name : '';
374
+ foundKeys.push({
375
+ key: namespaceName ? `${namespaceName}.${key.name}` : key.name,
376
+ meta: { file: path, namespace: namespaceName },
377
+ });
378
+ }
379
+ // Search for single-line comments that contain the static values of a dynamic key
380
+ // Example:
381
+ // const someKeys = messages[selectedOption];
382
+ // Define as a single-line comment all the possible static keys for that dynamic key
383
+ // i18n-check t('some.static.key.we.want.to.extract');
384
+ // i18n-check t('some.other.key.we.want.to.extract.without.semicolons')
385
+ const commentRanges = ts.getLeadingCommentRanges(sourceFile.getFullText(), node.getFullStart());
386
+ if (commentRanges?.length && commentRanges.length > 0) {
387
+ commentRanges.forEach((range) => {
388
+ const comment = sourceFile.getFullText().slice(range.pos, range.end);
389
+ // parse the string and check if it includes the following format:
390
+ // i18n-check t('someString')
391
+ const hasStaticKeyComment = COMMENT_CONTAINS_STATIC_KEY_REGEX.test(comment);
392
+ if (hasStaticKeyComment) {
393
+ // capture the string comment
394
+ const commentKey = COMMENT_CONTAINS_STATIC_KEY_REGEX.exec(comment)?.[2];
395
+ if (commentKey) {
396
+ const namespace = getCurrentNamespaces();
397
+ const namespaceName = namespace ? namespace[0]?.name : '';
398
+ const key = namespaceName
399
+ ? `${namespaceName}.${commentKey}`
400
+ : commentKey;
401
+ const keyExists = foundKeys.find((foundKey) => {
402
+ return foundKey.key === key;
403
+ });
404
+ if (!keyExists) {
405
+ foundKeys.push({
406
+ key,
407
+ meta: { file: path, namespace: namespaceName },
408
+ });
409
+ }
410
+ }
411
+ }
412
+ });
413
+ }
414
+ ts.forEachChild(node, visit);
415
+ if (ts.isFunctionLike(node) &&
416
+ namespaces.length > initialNamespacesLength) {
417
+ // check if the namespaces are dynamic and add a placeholder key
418
+ const currentNamespaces = getCurrentNamespaces(namespaces?.length - initialNamespacesLength);
419
+ currentNamespaces?.forEach((namespace) => {
420
+ if (namespace.dynamic) {
421
+ foundKeys.push({
422
+ key: namespace.name,
423
+ meta: { file: path, namespace: namespace.name, dynamic: true },
424
+ });
425
+ }
426
+ });
427
+ removeNamespaces(namespaces.length - initialNamespacesLength);
428
+ }
429
+ };
430
+ ts.forEachChild(sourceFile, visit);
431
+ return foundKeys;
432
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lingual/i18n-check",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "i18n translation messages check",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -16,7 +16,8 @@
16
16
  "lint:fix": "eslint src --fix ",
17
17
  "check-format": "prettier --check './{src,translations}/**/*.{js,jsx,ts,tsx,json,html,css}'",
18
18
  "test": "vitest run",
19
- "test:cli": "pnpm build && vitest --config vitest.bin.config.ts run src/bin/index.test.ts"
19
+ "test:cli": "pnpm build && vitest --config vitest.bin.config.ts run src/bin/index.test.ts",
20
+ "prepublishOnly": "pnpm install --frozen-lockfile && pnpm run build"
20
21
  },
21
22
  "files": [
22
23
  "dist/",