From 90827a3755d9014069d3c99d8866328c3f2abc92 Mon Sep 17 00:00:00 2001 From: Michael Parry Date: Mon, 25 Mar 2024 16:30:37 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20scope=20support=20?= =?UTF-8?q?for=20marker=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ Closes: #178, #132 --- __tests__/buildTranslationFiles.spec.ts | 83 +++++++------ __tests__/marker/nested-scope.ts | 4 + __tests__/marker/with-scope.ts | 26 ++++ .../typescript/build-keys-from-ast-nodes.ts | 112 +++++++++++++----- src/keys-builder/typescript/index.ts | 17 +-- .../typescript/marker.extractor.ts | 4 +- .../typescript/pure-function.extractor.ts | 4 +- .../typescript/service.extractor.ts | 4 +- src/keys-builder/typescript/types.ts | 2 +- src/marker.ts | 2 +- 10 files changed, 174 insertions(+), 84 deletions(-) create mode 100644 __tests__/marker/nested-scope.ts create mode 100644 __tests__/marker/with-scope.ts diff --git a/__tests__/buildTranslationFiles.spec.ts b/__tests__/buildTranslationFiles.spec.ts index 18876f4..cdf3897 100644 --- a/__tests__/buildTranslationFiles.spec.ts +++ b/__tests__/buildTranslationFiles.spec.ts @@ -127,9 +127,11 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => { describe('Template Extraction', () => { describe('Pipe', () => { const type: TranslationCategory = 'pipe'; - const config = gConfig(type); - beforeEach(() => removeI18nFolder(type)); + beforeEach(() => { + removeI18nFolder(type); + createTranslations(gConfig(type)); + }); it('should work with pipe', () => { const expected = { @@ -150,16 +152,17 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => { expected[nonNumericKey] = defaultValue; }); - createTranslations(config); assertTranslation({ type, expected, fileFormat }); }); }); describe('Directive', () => { const type: TranslationCategory = 'directive'; - const config = gConfig(type); - beforeEach(() => removeI18nFolder(type)); + beforeEach(() => { + removeI18nFolder(type); + createTranslations(gConfig(type)); + }); it('should work with directive', () => { const expected = generateKeys({ end: 24 }); @@ -168,16 +171,17 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => { expected[nonNumericKey] = defaultValue; }, ); - createTranslations(config); assertTranslation({ type, expected, fileFormat }); }); }); describe('ngContainer', () => { const type: TranslationCategory = 'ngContainer'; - const config = gConfig(type); - beforeEach(() => removeI18nFolder(type)); + beforeEach(() => { + removeI18nFolder(type); + createTranslations(gConfig(type)); + }); it('should work with ngContainer', () => { let expected = generateKeys({ end: 46 }); @@ -186,7 +190,6 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => { expected['another(test)'] = expected['last "one"'] = defaultValue; - createTranslations(config); assertTranslation({ type, expected, fileFormat }); }); @@ -199,7 +202,6 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => { '5': defaultValue, }; - createTranslations(config); assertTranslation({ type, expected, @@ -211,13 +213,14 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => { describe('ngTemplate', () => { const type: TranslationCategory = 'ngTemplate'; - const config = gConfig(type); - beforeEach(() => removeI18nFolder(type)); + beforeEach(() => { + removeI18nFolder(type); + createTranslations(gConfig(type)); + }); it('should work with ngTemplate', () => { let expected = generateKeys({ end: 42 }); - createTranslations(config); assertTranslation({ type, expected, fileFormat }); }); @@ -230,7 +233,6 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => { '5': defaultValue, }; - createTranslations(config); assertTranslation({ type, expected, @@ -242,22 +244,25 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => { describe('Control flow', () => { const type: TranslationCategory = 'control-flow'; - const config = gConfig(type); - beforeEach(() => removeI18nFolder(type)); + beforeEach(() => { + removeI18nFolder(type); + createTranslations(gConfig(type)); + }); it('should work with control flow', () => { let expected = generateKeys({ end: 26 }); - createTranslations(config); assertTranslation({ type, expected, fileFormat }); }); }); describe('read', () => { const type: TranslationCategory = 'read'; - const config = gConfig(type); - beforeEach(() => removeI18nFolder(type)); + beforeEach(() => { + removeI18nFolder(type); + createTranslations(gConfig(type)); + }); it('should work with read', () => { const expected = { @@ -287,7 +292,6 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => { }, }; - createTranslations(config); assertTranslation({ type, expected: expected.global, fileFormat }); assertTranslation({ type, @@ -302,9 +306,11 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => { describe('Typescript Extraction', () => { describe('service', () => { const type: TranslationCategory = 'service'; - const config = gConfig(type); - beforeEach(() => removeI18nFolder(type)); + beforeEach(() => { + removeI18nFolder(type); + createTranslations(gConfig(type)); + }); it('should work with service', () => { const expected = { @@ -314,7 +320,6 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => { 'inject.test': defaultValue, }; - createTranslations(config); assertTranslation({ type, expected, fileFormat }); }); @@ -334,7 +339,6 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => { }, }; - createTranslations(config); assertTranslation({ type, expected: expected.todos, @@ -357,8 +361,6 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => { it('should work when passing an array of keys', () => { const expected = generateKeys({ start: 26, end: 33 }); - - createTranslations(config); assertPartialTranslation({ type, expected, fileFormat }); }); }); @@ -366,26 +368,42 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => { describe('marker', () => { const type: TranslationCategory = 'marker'; - beforeEach(() => removeI18nFolder(type)); + beforeEach(() => { + removeI18nFolder(type); + createTranslations(gConfig(type)); + }); it('should work with marker', () => { - const config = gConfig(type); - let expected = {}; expected['username4'] = defaultValue; expected['password4'] = defaultValue; expected['username'] = defaultValue; expected['password'] = defaultValue; - createTranslations(config); assertTranslation({ type, expected, fileFormat }); }); + + it('should work with scopes', () => { + const expected = { + username: defaultValue, + password: defaultValue, + }; + + assertTranslation({ + type, + expected: expected, + path: 'nested/scope/', + fileFormat, + }); + }); }); describe('inline template', () => { const type: TranslationCategory = 'inline-template'; - const config = gConfig(type); - beforeEach(() => removeI18nFolder(type)); + beforeEach(() => { + removeI18nFolder(type); + createTranslations(gConfig(type)); + }); it('should work with inline templates', () => { const expected = generateKeys({ end: 23 }); @@ -394,7 +412,6 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => { expected[nonNumericKey] = defaultValue; }, ); - createTranslations(config); assertTranslation({ type, expected, fileFormat }); }); }); diff --git a/__tests__/marker/nested-scope.ts b/__tests__/marker/nested-scope.ts new file mode 100644 index 0000000..47c785b --- /dev/null +++ b/__tests__/marker/nested-scope.ts @@ -0,0 +1,4 @@ +const f = { + provide: TRANSLOCO_SCOPE, + useValue: 'nested/scope' +}; diff --git a/__tests__/marker/with-scope.ts b/__tests__/marker/with-scope.ts new file mode 100644 index 0000000..157f84a --- /dev/null +++ b/__tests__/marker/with-scope.ts @@ -0,0 +1,26 @@ +import { marker } from '@jsverse/transloco-keys-manager/marker'; + +@Component({ + selector: 'bla-bla', + template: ` + + + + + + + +
+ {{ column | transloco }} +
+ {{ row[column] }} +
+ ` +}) +export class Basic { + data = [ + { username: 'alex', password: '12345678' }, + { username: 'bob', password: 'password' } + ]; + displayedColumns = [marker('username', 'nested/scope'), marker('password', 'nested/scope')]; +} diff --git a/src/keys-builder/typescript/build-keys-from-ast-nodes.ts b/src/keys-builder/typescript/build-keys-from-ast-nodes.ts index 8648b7f..b9ef426 100644 --- a/src/keys-builder/typescript/build-keys-from-ast-nodes.ts +++ b/src/keys-builder/typescript/build-keys-from-ast-nodes.ts @@ -1,46 +1,98 @@ -import { Node, StringLiteral, NoSubstitutionTemplateLiteral } from 'typescript'; +import { Node, StringLiteral, NoSubstitutionTemplateLiteral, CallExpression } from 'typescript'; import ts from 'typescript'; import { TSExtractorResult } from './types'; -export function buildKeysFromASTNodes( - nodes: Node[], - allowedMethods = ['translate', 'selectTranslate'], -): TSExtractorResult { +/** + * It can be one of the following: + * + * translate('2', {}, 'some/nested'); + * translate('3', {}, 'some/nested/en'); + * translate(['2', '3'], {}, 'some/nested/en'); + * translate('globalKey'); + * + * + * selectTranslate('2', {}, 'some/nested'); + * selectTranslate('3', {}, 'some/nested/en'); + * selectTranslate(['2', '3'], {}, 'some/nested/en'); + * selectTranslate('globalKey'); + */ +export function buildTranslateKeysFromASTNodes(nodes: Node[]): TSExtractorResult { const result: TSExtractorResult = []; for (let node of nodes) { - if (ts.isCallExpression(node.parent)) { - const method = node.parent.expression; - let methodName = ''; - if (ts.isIdentifier(method)) { - methodName = method.text; - } else if (ts.isPropertyAccessExpression(method)) { - methodName = method.name.text; - } - if (!allowedMethods.includes(methodName)) { - continue; - } - - const [keyNode, _, langNode] = node.parent.arguments; - let lang = isStringNode(langNode) ? langNode.text : ''; - let keys: string[] = []; - - if (isStringNode(keyNode)) { - keys = [keyNode.text]; - } else if (ts.isArrayLiteralExpression(keyNode)) { - keys = keyNode.elements.filter(isStringNode).map((node) => node.text); - } - - for (const key of keys) { - result.push({ key, lang }); - } + if (!ts.isCallExpression(node.parent)) { + continue; + } + + const methodName = getMethodName(node.parent); + if (!['translate', 'selectTranslate'].includes(methodName)) { + continue; + } + + const [keyNode, _, langNode] = node.parent.arguments; + let scope = isStringNode(langNode) ? langNode.text : ''; + let keys: string[] = []; + + if (isStringNode(keyNode)) { + keys = [keyNode.text]; + } else if (ts.isArrayLiteralExpression(keyNode)) { + keys = keyNode.elements.filter(isStringNode).map((node) => node.text); + } + + for (const key of keys) { + result.push({ key, scope }); } } return result; } +/** + * It can be one of the following: + * + * marker('globalKey'); + * marker('globalKey', 'some/nested/en'); + * + * alias('globalKey'); + * alias('globalKey', 'some/nested/en'); + */ +export function buildMarkerKeysFromASTNodes(nodes: Node[], markerName: string): TSExtractorResult { + const result: TSExtractorResult = []; + + for (let node of nodes) { + if (!ts.isCallExpression(node.parent)) { + continue; + } + + if (markerName !== getMethodName(node.parent)) { + continue; + } + + const [keyNode, scopeNode] = node.parent.arguments; + result.push({ + key: isStringNode(keyNode) ? keyNode.text : '', + scope: isStringNode(scopeNode) ? scopeNode.text : '' + }); + } + + return result; +} + +function getMethodName(node: CallExpression): string { + const method = node.expression; + + let methodName = ''; + if (ts.isIdentifier(method)) { + methodName = method.text; + } else if (ts.isPropertyAccessExpression(method)) { + methodName = method.name.text; + } + + return methodName; +} + + function isStringNode( node: Node, ): node is StringLiteral | NoSubstitutionTemplateLiteral { diff --git a/src/keys-builder/typescript/index.ts b/src/keys-builder/typescript/index.ts index 14e5723..3b522d9 100644 --- a/src/keys-builder/typescript/index.ts +++ b/src/keys-builder/typescript/index.ts @@ -48,10 +48,10 @@ function TSExtractor(config: ExtractorConfig): ScopeMap { extractors .map((ex) => ex(ast)) .flat() - .forEach(({ key, lang }) => { - const [keyWithoutScope, scopeAlias] = resolveAliasAndKeyFromService( + .forEach(({ key, scope }) => { + const [keyWithoutScope, scopeAlias] = resolveKeyAndScopeAliasFunctions( key, - lang, + scope, scopes, ); addKey({ @@ -73,16 +73,7 @@ function TSExtractor(config: ExtractorConfig): ScopeMap { return scopeToKeys; } -/** - * - * It can be one of the following: - * - * translate('2', {}, 'some/nested'); - * translate('3', {}, 'some/nested/en'); - * translate('globalKey'); - * - */ -function resolveAliasAndKeyFromService( +function resolveKeyAndScopeAliasFunctions( key: string, scopePath: string, scopes: Scopes, diff --git a/src/keys-builder/typescript/marker.extractor.ts b/src/keys-builder/typescript/marker.extractor.ts index a0c87f6..44656f1 100644 --- a/src/keys-builder/typescript/marker.extractor.ts +++ b/src/keys-builder/typescript/marker.extractor.ts @@ -1,7 +1,7 @@ import { SourceFile, Node } from 'typescript'; import { tsquery } from '@phenomnomnominal/tsquery'; -import { buildKeysFromASTNodes } from './build-keys-from-ast-nodes'; +import { buildMarkerKeysFromASTNodes, buildTranslateKeysFromASTNodes } from './build-keys-from-ast-nodes'; import { TSExtractorResult } from './types'; export function markerExtractor(ast: SourceFile): TSExtractorResult { @@ -16,7 +16,7 @@ export function markerExtractor(ast: SourceFile): TSExtractorResult { const markerName = getMarkerName(importNode); const fns = tsquery(ast, `CallExpression Identifier[text=${markerName}]`); - return buildKeysFromASTNodes(fns, [markerName]); + return buildMarkerKeysFromASTNodes(fns, markerName); } function getMarkerName(importNode: Node) { diff --git a/src/keys-builder/typescript/pure-function.extractor.ts b/src/keys-builder/typescript/pure-function.extractor.ts index 3f38613..1d0782d 100644 --- a/src/keys-builder/typescript/pure-function.extractor.ts +++ b/src/keys-builder/typescript/pure-function.extractor.ts @@ -1,11 +1,11 @@ import { SourceFile } from 'typescript'; import { tsquery } from '@phenomnomnominal/tsquery'; -import { buildKeysFromASTNodes } from './build-keys-from-ast-nodes'; +import { buildTranslateKeysFromASTNodes } from './build-keys-from-ast-nodes'; import { TSExtractorResult } from './types'; export function pureFunctionExtractor(ast: SourceFile): TSExtractorResult { const fns = tsquery(ast, `CallExpression Identifier[text=translate]`); - return buildKeysFromASTNodes(fns); + return buildTranslateKeysFromASTNodes(fns); } diff --git a/src/keys-builder/typescript/service.extractor.ts b/src/keys-builder/typescript/service.extractor.ts index efbd46d..0e798d2 100644 --- a/src/keys-builder/typescript/service.extractor.ts +++ b/src/keys-builder/typescript/service.extractor.ts @@ -2,7 +2,7 @@ import { tsquery } from '@phenomnomnominal/tsquery'; import { SourceFile } from 'typescript'; import ts from 'typescript'; -import { buildKeysFromASTNodes } from './build-keys-from-ast-nodes'; +import { buildTranslateKeysFromASTNodes } from './build-keys-from-ast-nodes'; import { TSExtractorResult } from './types'; export function serviceExtractor(ast: SourceFile): TSExtractorResult { @@ -26,7 +26,7 @@ export function serviceExtractor(ast: SourceFile): TSExtractorResult { `PropertyAccessExpression:has([name=${propName}])`, ); - result = result.concat(buildKeysFromASTNodes(methodNodes)); + result = result.concat(buildTranslateKeysFromASTNodes(methodNodes)); } } diff --git a/src/keys-builder/typescript/types.ts b/src/keys-builder/typescript/types.ts index 26bac15..37c6d61 100644 --- a/src/keys-builder/typescript/types.ts +++ b/src/keys-builder/typescript/types.ts @@ -1 +1 @@ -export type TSExtractorResult = { key: string; lang: string }[]; +export type TSExtractorResult = { key: string; scope: string }[]; diff --git a/src/marker.ts b/src/marker.ts index 4a8ad4d..6481d97 100644 --- a/src/marker.ts +++ b/src/marker.ts @@ -1,3 +1,3 @@ -export function marker(key: T): T { +export function marker(key: T, scope?: string): T { return key; }