diff --git a/__tests__/buildTranslationFiles.spec.ts b/__tests__/buildTranslationFiles.spec.ts index 68086ea..18876f4 100644 --- a/__tests__/buildTranslationFiles.spec.ts +++ b/__tests__/buildTranslationFiles.spec.ts @@ -66,7 +66,9 @@ type TranslationCategory = | 'multi-input' | 'scope-mapping' | 'comments' - | 'remove-extra-keys'; + | 'remove-extra-keys' + | 'self-closing' + | 'control-flow'; interface assertTranslationParams extends Pick { type: TranslationCategory; @@ -135,7 +137,7 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => { '49.50.51.52': defaultValue, ...generateKeys({ start: 53, end: 62 }), '63.64.65': defaultValue, - ...generateKeys({ start: 66, end: 78 }), + ...generateKeys({ start: 66, end: 79 }), '{{count}} items': defaultValue, }; [ @@ -160,7 +162,7 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => { beforeEach(() => removeI18nFolder(type)); it('should work with directive', () => { - const expected = generateKeys({ end: 23 }); + const expected = generateKeys({ end: 24 }); ['Processing archive...', 'Restore Options'].forEach( (nonNumericKey) => { expected[nonNumericKey] = defaultValue; @@ -214,7 +216,7 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => { beforeEach(() => removeI18nFolder(type)); it('should work with ngTemplate', () => { - let expected = generateKeys({ end: 41 }); + let expected = generateKeys({ end: 42 }); createTranslations(config); assertTranslation({ type, expected, fileFormat }); }); @@ -238,6 +240,19 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => { }); }); + describe('Control flow', () => { + const type: TranslationCategory = 'control-flow'; + const config = gConfig(type); + + beforeEach(() => removeI18nFolder(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); diff --git a/__tests__/control-flow/1.html b/__tests__/control-flow/1.html new file mode 100644 index 0000000..5bf14e0 --- /dev/null +++ b/__tests__/control-flow/1.html @@ -0,0 +1,79 @@ +{{ '1' | transloco }} + + + {{ t('3') }} + + + {{ t('4') }} + + + +@if (a > b) { + {{ '5' | transloco }} + + + + {{ t('7') }} + + + {{ t('8') }} + +} @else if (b > a) { + {{ '9' | transloco }} +} @else { + +} + +@for (item of items; track item.id) { + {{ '11' | transloco }} +} @empty { + {{ '12' | transloco }} +} + +@switch (condition) { + @case (caseA) { + {{ '13' | transloco }} + } + @default { + {{ '14' | transloco }} + } +} + +@defer { + {{ '15' | transloco }} +} @error { + {{ '16' | transloco }} +} @placeholder { + {{ '17' | transloco }} +} @loading { + {{ '18' | transloco }} +} + + +@if (a > b) { + {{ t('19') }} +} @else if (b > a) { + +} @else { + @for (item of items; track item.id) { + {{ t('21') }} + } @empty { + @switch (condition) { + @case (caseA) { + {{ t('22') }} + } + @default { + @defer { + {{ '23' | transloco }} + } @error { + + } @placeholder { + {{ t('25') }} + } @loading { + {{ t('26') }} + } + } + } + } +} + diff --git a/__tests__/directive/2.html b/__tests__/directive/2.html index f1c91d9..a4edb3c 100644 --- a/__tests__/directive/2.html +++ b/__tests__/directive/2.html @@ -124,4 +124,5 @@
+ diff --git a/__tests__/ngTemplate/5.html b/__tests__/ngTemplate/5.html index 789900a..b5f2b77 100644 --- a/__tests__/ngTemplate/5.html +++ b/__tests__/ngTemplate/5.html @@ -14,4 +14,7 @@

ddsds

{{t('41') + 'a'}} + + + diff --git a/__tests__/pipe/6.html b/__tests__/pipe/6.html index 34abae1..a21bc96 100644 --- a/__tests__/pipe/6.html +++ b/__tests__/pipe/6.html @@ -51,3 +51,5 @@ {{ '{{count}} items' | transloco:{ count: item.usageCount } }} + + diff --git a/package.json b/package.json index 27657a5..ad0c652 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,14 @@ }, "type": "module", "exports": { - "types": "./public-api.d.ts", - "default": "./public-api.js" + ".": { + "import": "./public-api.js", + "types": "./public-api.d.ts" + }, + "./marker": { + "import": "./marker.js", + "types": "./marker.d.ts" + } }, "bin": { "transloco-keys-manager": "index.js" diff --git a/src/keys-builder/template/directive.extractor.ts b/src/keys-builder/template/directive.extractor.ts index abd9093..343576f 100644 --- a/src/keys-builder/template/directive.extractor.ts +++ b/src/keys-builder/template/directive.extractor.ts @@ -12,6 +12,7 @@ import { resolveAliasAndKey } from '../utils/resolvers.utils'; import { TemplateExtractorConfig } from './types'; import { + isBlockNode, isBoundAttribute, isConditionalExpression, isElement, @@ -21,6 +22,7 @@ import { isTemplate, isTextAttribute, parseTemplate, + resolveBlockChildNodes, } from './utils'; export function directiveExtractor(config: TemplateExtractorConfig) { @@ -30,6 +32,11 @@ export function directiveExtractor(config: TemplateExtractorConfig) { function traverse(nodes: TmplAstNode[], config: ExtractorConfig) { for (const node of nodes) { + if (isBlockNode(node)) { + traverse(resolveBlockChildNodes(node), config); + continue; + } + if (!isSupportedNode(node, [isTemplate, isElement])) { continue; } diff --git a/src/keys-builder/template/pipe.extractor.ts b/src/keys-builder/template/pipe.extractor.ts index 17c56d4..7ce9a3e 100644 --- a/src/keys-builder/template/pipe.extractor.ts +++ b/src/keys-builder/template/pipe.extractor.ts @@ -18,6 +18,8 @@ import { isPropertyRead, isTemplate, parseTemplate, + isBlockNode, + resolveBlockChildNodes, } from './utils'; export function pipeExtractor(config: TemplateExtractorConfig) { @@ -27,6 +29,11 @@ export function pipeExtractor(config: TemplateExtractorConfig) { function traverse(nodes: TmplAstNode[], config: ExtractorConfig) { for (const node of nodes) { + if (isBlockNode(node)) { + traverse(resolveBlockChildNodes(node), config); + continue; + } + let astTrees: AST[] = []; if (isElement(node) || isTemplate(node)) { diff --git a/src/keys-builder/template/structural-directive.extractor.ts b/src/keys-builder/template/structural-directive.extractor.ts index 0cb57a1..ff08f75 100644 --- a/src/keys-builder/template/structural-directive.extractor.ts +++ b/src/keys-builder/template/structural-directive.extractor.ts @@ -28,6 +28,8 @@ import { isSupportedNode, isTemplate, parseTemplate, + isBlockNode, + resolveBlockChildNodes, } from './utils'; interface ContainerMetaData { @@ -49,6 +51,11 @@ export function traverse( config: TemplateExtractorConfig, ) { for (const node of nodes) { + if (isBlockNode(node)) { + traverse(resolveBlockChildNodes(node), containers, config); + continue; + } + let methodUsages: ContainerMetaData[] = []; if (isBoundText(node)) { diff --git a/src/keys-builder/template/utils.ts b/src/keys-builder/template/utils.ts index 4baea1f..1aea7d6 100644 --- a/src/keys-builder/template/utils.ts +++ b/src/keys-builder/template/utils.ts @@ -14,6 +14,17 @@ import { TmplAstElement, TmplAstTemplate, TmplAstTextAttribute, + TmplAstNode, + TmplAstDeferredBlock, + TmplAstDeferredBlockError, + TmplAstDeferredBlockLoading, + TmplAstDeferredBlockPlaceholder, + TmplAstForLoopBlock, + TmplAstForLoopBlockEmpty, + TmplAstIfBlockBranch, + TmplAstSwitchBlockCase, + TmplAstIfBlock, + TmplAstSwitchBlock, } from '@angular/compiler'; import { readFile } from '../../utils/file.utils'; @@ -98,3 +109,85 @@ export function isSupportedNode( ): node is GuardedType { return predicates.some((predicate) => predicate(node)); } + +type BlockNode = + | TmplAstDeferredBlockError + | TmplAstDeferredBlockLoading + | TmplAstDeferredBlockPlaceholder + | TmplAstForLoopBlockEmpty + | TmplAstIfBlockBranch + | TmplAstSwitchBlockCase + | TmplAstForLoopBlock + | TmplAstDeferredBlock + | TmplAstIfBlock + | TmplAstSwitchBlock; + +export function isBlockWithChildren( + node: unknown, +): node is { children: TmplAstNode[] } { + return ( + node instanceof TmplAstDeferredBlockError || + node instanceof TmplAstDeferredBlockLoading || + node instanceof TmplAstDeferredBlockPlaceholder || + node instanceof TmplAstForLoopBlockEmpty || + node instanceof TmplAstIfBlockBranch || + node instanceof TmplAstSwitchBlockCase + ); +} + +export function isTmplAstForLoopBlock( + node: unknown, +): node is TmplAstForLoopBlock { + return node instanceof TmplAstForLoopBlock; +} + +export function isTmplAstDeferredBlock( + node: unknown, +): node is TmplAstDeferredBlock { + return node instanceof TmplAstDeferredBlock; +} + +export function isTmplAstIfBlock(node: unknown): node is TmplAstIfBlock { + return node instanceof TmplAstIfBlock; +} + +export function isTmplAstSwitchBlock( + node: unknown, +): node is TmplAstSwitchBlock { + return node instanceof TmplAstSwitchBlock; +} + +export function isBlockNode(node: TmplAstNode): node is BlockNode { + return ( + isTmplAstIfBlock(node) || + isTmplAstForLoopBlock(node) || + isTmplAstDeferredBlock(node) || + isTmplAstSwitchBlock(node) || + isBlockWithChildren(node) + ); +} + +export function resolveBlockChildNodes(node: BlockNode): TmplAstNode[] { + if (isTmplAstIfBlock(node)) { + return node.branches; + } + + if (isTmplAstForLoopBlock(node)) { + return node.empty ? [...node.children, node.empty] : node.children; + } + + if (isTmplAstDeferredBlock(node)) { + return [ + ...node.children, + ...([node.loading, node.error, node.placeholder].filter( + Boolean, + ) as TmplAstNode[]), + ]; + } + + if (isTmplAstSwitchBlock(node)) { + return node.cases; + } + + return node.children; +}