Skip to content

Commit

Permalink
feat: Add support for Built-in control flow syntax (#26) (#27)
Browse files Browse the repository at this point in the history
Closes #26 

BREAKING CHANGE
- minimum angular version required bumped to 17
- minimum node version required bumped to v18.13.0 to be aligned with the Angular 17 requirements
- minimum TypeScript version required bumped to v5.2 to be aligned with the Angular 17 requirements
  • Loading branch information
pmpak authored Nov 21, 2023
1 parent 50264ae commit 5cfe5dd
Show file tree
Hide file tree
Showing 9 changed files with 703 additions and 537 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
node-version: ['18.x', '20.x']
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ Choose the version corresponding to your Angular version:

| Angular | ngx-translate-extract |
| ---------- | ------------------------------------------------------------------------------------------ |
| 14 | 8.x.x+ |
| 13 | 8.x.x+ |
| 8.x – 12.x | [@biesbjerg/ngx-translate-extract](https://github.com/biesbjerg/ngx-translate-extract) 7.x |
| >=17 | 9.x |
| 13 – 16 | 8.x |
| 8 – 12 | [@biesbjerg/ngx-translate-extract](https://github.com/biesbjerg/ngx-translate-extract) 7.x |

Add a script to your project's `package.json`:

Expand Down
902 changes: 398 additions & 504 deletions package-lock.json

Large diffs are not rendered by default.

15 changes: 8 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"yargs": "^17.5.1"
},
"devDependencies": {
"@angular/compiler": "^17.0.3",
"@types/braces": "^3.0.1",
"@types/chai": "^4.3.3",
"@types/flat": "^5.0.2",
Expand All @@ -33,9 +34,9 @@
"@types/mocha": "^9.1.1",
"@types/node": "^16",
"@types/yargs": "^17.0.20",
"@typescript-eslint/eslint-plugin": "^5.48.2",
"@typescript-eslint/eslint-plugin-tslint": "^5.48.2",
"@typescript-eslint/parser": "^5.48.2",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/eslint-plugin-tslint": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"chai": "^4.3.6",
"cross-env": "^7.0.3",
"eslint": "^8.32.0",
Expand All @@ -46,11 +47,11 @@
"rimraf": "^3.0.2",
"ts-mocha": "^10.0.0",
"ts-node": "^10.4.0",
"typescript": "^4.5.2"
"typescript": "~5.2.2"
},
"peerDependencies": {
"@angular/compiler": ">=13.1.2",
"typescript": ">=4.4.0"
"@angular/compiler": ">=17.0.0",
"typescript": ">=5.2.0"
},
"main": "dist/index.js",
"typings": "dist/index.d.ts",
Expand Down Expand Up @@ -87,7 +88,7 @@
},
"homepage": "https://github.com/vendure-ecommerce/ngx-translate-extract",
"engines": {
"node": ">=16",
"node": ">=18.13.0",
"npm": ">=8"
},
"config": {},
Expand Down
62 changes: 61 additions & 1 deletion src/parsers/directive.parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,27 @@ import {
TmplAstNode as Node,
TmplAstTemplate as Template,
TmplAstText as Text,
TmplAstTextAttribute as TextAttribute
TmplAstTextAttribute as TextAttribute,
ParseSourceSpan,
TmplAstIfBlock,
TmplAstSwitchBlock,
TmplAstForLoopBlock,
TmplAstDeferredBlock
} from '@angular/compiler';

import { ParserInterface } from './parser.interface.js';
import { TranslationCollection } from '../utils/translation.collection.js';
import { extractComponentInlineTemplate, isPathAngularComponent } from '../utils/utils.js';

interface BlockNode {
nameSpan: ParseSourceSpan;
sourceSpan: ParseSourceSpan;
startSourceSpan: ParseSourceSpan;
endSourceSpan: ParseSourceSpan | null;
children: Node[] | undefined;
visit<Result>(visitor: unknown): Result;
}

export const TRANSLATE_ATTR_NAMES = ['translate', 'marker'];
type ElementLike = Element | Template;

Expand Down Expand Up @@ -76,9 +90,42 @@ export class DirectiveParser implements ParserInterface {
elements = [...elements, ...childElements];
}
});

nodes.filter(this.isBlockNode).forEach((node) => elements.push(...this.getElementsWithTranslateAttributeFromBlockNodes(node)));

return elements;
}

/**
* Get the child elements that are inside a block node (e.g. @if, @deferred)
*/
protected getElementsWithTranslateAttributeFromBlockNodes(blockNode: BlockNode) {
let blockChildren = blockNode.children;

if (blockNode instanceof TmplAstIfBlock) {
blockChildren = blockNode.branches.map((branch) => branch.children).flat();
}

if (blockNode instanceof TmplAstSwitchBlock) {
blockChildren = blockNode.cases.map((branch) => branch.children).flat();
}

if (blockNode instanceof TmplAstForLoopBlock) {
const emptyBlockChildren = blockNode.empty?.children ?? [];
blockChildren.push(...emptyBlockChildren);
}

if (blockNode instanceof TmplAstDeferredBlock) {
const placeholderBlockChildren = blockNode.placeholder?.children ?? [];
const loadingBlockChildren = blockNode.loading?.children ?? [];
const errorBlockChildren = blockNode.error?.children ?? [];

blockChildren.push(...placeholderBlockChildren, ...loadingBlockChildren, ...errorBlockChildren);
}

return this.getElementsWithTranslateAttribute(blockChildren);
}

/**
* Get direct child nodes of type Text
* @param element
Expand Down Expand Up @@ -164,6 +211,19 @@ export class DirectiveParser implements ParserInterface {
return node instanceof Element || node instanceof Template;
}

/**
* Check if node type is BlockNode
* @param node
*/
protected isBlockNode(node: Node): node is BlockNode {
return (
node.hasOwnProperty('nameSpan') &&
node.hasOwnProperty('sourceSpan') &&
node.hasOwnProperty('startSourceSpan') &&
node.hasOwnProperty('endSourceSpan')
);
}

/**
* Check if node type is Text
* @param node
Expand Down
45 changes: 32 additions & 13 deletions src/parsers/pipe.parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import {
LiteralMap,
LiteralArray,
Interpolation,
Call
Call,
TmplAstIfBlockBranch,
TmplAstSwitchBlockCase
} from '@angular/compiler';

import { ParserInterface } from './parser.interface.js';
Expand All @@ -20,7 +22,7 @@ import { isPathAngularComponent, extractComponentInlineTemplate } from '../utils
export const TRANSLATE_PIPE_NAMES = ['translate', 'marker'];

export class PipeParser implements ParserInterface {
public extract(source: string, filePath: string): TranslationCollection | null {
public extract(source: string, filePath: string): TranslationCollection {
if (filePath && isPathAngularComponent(filePath)) {
source = extractComponentInlineTemplate(source);
}
Expand All @@ -37,25 +39,38 @@ export class PipeParser implements ParserInterface {
}

protected findPipesInNode(node: any): BindingPipe[] {
let ret: BindingPipe[] = [];

if (node?.children) {
ret = node.children.reduce(
(result: BindingPipe[], childNode: TmplAstNode) => {
const children = this.findPipesInNode(childNode);
return result.concat(children);
},
[ret]
);
const ret: BindingPipe[] = [];

const nodeChildren = node?.children ?? [];

// @if and @switch blocks
const nodeBranchesOrCases: TmplAstIfBlockBranch[] | TmplAstSwitchBlockCase[] = node?.branches ?? node?.cases ?? [];

// @for blocks
const emptyBlockChildren = node?.empty?.children ?? [];

// @deferred blocks
const errorBlockChildren = node?.error?.children ?? [];
const loadingBlockChildren = node?.loading?.children ?? [];
const placeholderBlockChildren = node?.placeholder?.children ?? [];

nodeChildren.push(...emptyBlockChildren, ...errorBlockChildren, ...loadingBlockChildren, ...placeholderBlockChildren);

if (nodeChildren.length > 0) {
ret.push(...this.extractPipesFromChildNodes(nodeChildren));
}

nodeBranchesOrCases.forEach((branch) => {
ret.push(...this.extractPipesFromChildNodes(branch.children));
});

if (node?.value?.ast) {
ret.push(...this.getTranslatablesFromAst(node.value.ast));
}

if (node?.attributes) {
const translateableAttributes = node.attributes.filter((attr: TmplAstTextAttribute) => TRANSLATE_PIPE_NAMES.includes(attr.name));
ret = [...ret, ...translateableAttributes];
ret.push(...ret, ...translateableAttributes);
}

if (node?.inputs) {
Expand All @@ -78,6 +93,10 @@ export class PipeParser implements ParserInterface {
return ret;
}

protected extractPipesFromChildNodes(nodeChildren: TmplAstNode[]) {
return nodeChildren.map((childNode) => this.findPipesInNode(childNode)).flat();
}

protected parseTranslationKeysFromPipe(pipeContent: BindingPipe | LiteralPrimitive | Conditional): string[] {
const ret: string[] = [];
if (pipeContent instanceof LiteralPrimitive) {
Expand Down
97 changes: 97 additions & 0 deletions tests/parsers/directive.parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,103 @@ describe('DirectiveParser', () => {
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['this is an example']);
});

describe('Built-in control flow', () => {
it('should extract keys from elements inside an @if/@else block', () => {
const contents = `
@if (loggedIn) {
<p ${translateAttrName}>if.block</p>
} @else if (condition) {
<p ${translateAttrName}>elseif.block</p>
} @else {
<p ${translateAttrName}>else.block</p>
}
`;

const keys = parser.extract(contents, templateFilename)?.keys();
expect(keys).to.deep.equal(['if.block', 'elseif.block', 'else.block']);
});

it('should extract keys from elements inside a @for/@empty block', () => {
const contents = `
@for (user of users; track user.id) {
<p ${translateAttrName}>for.block</p>
} @empty {
<p ${translateAttrName}>for.empty.block</p>
}
`;

const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['for.block', 'for.empty.block']);
});

it('should extract keys from elements inside an @switch/@case block', () => {
const contents = `
@switch (condition) {
@case (caseA) {
<p ${translateAttrName}>switch.caseA</p>
}
@case (caseB) {
<p ${translateAttrName}>switch.caseB</p>
}
@default {
<p ${translateAttrName}>switch.default</p>
}
}
`;

const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['switch.caseA', 'switch.caseB', 'switch.default']);
});

it('should extract keys from elements inside an @deferred/@error/@loading/@placeholder block', () => {
const contents = `
@defer (on viewport) {
<p ${translateAttrName}>defer</p>
} @loading {
<p ${translateAttrName}>defer.loading</p>
} @error {
<p ${translateAttrName}>defer.error</p>
} @placeholder {
<p ${translateAttrName}>defer.placeholder</p>
}
`;

const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['defer', 'defer.placeholder', 'defer.loading', 'defer.error']);
});

it('should extract keys from nested blocks', () => {
const contents = `
@if (loggedIn) {
<p ${translateAttrName}>if.block</p>
@if (nestedCondition) {
@if (nestedCondition) {
<p ${translateAttrName}>nested.if.block</p>
} @else {
<p ${translateAttrName}>nested.else.block</p>
}
} @else if (nestedElseIfCondition) {
<p ${translateAttrName}>nested.elseif.block</p>
}
} @else if (condition) {
<p ${translateAttrName}>elseif.block</p>
} @else {
<p ${translateAttrName}>else.block</p>
}
`;

const keys = parser.extract(contents, templateFilename)?.keys();
expect(keys).to.deep.equal([
'if.block',
'elseif.block',
'else.block',
'nested.elseif.block',
'nested.if.block',
'nested.else.block'
]);
});
});
});
});
});
Loading

0 comments on commit 5cfe5dd

Please sign in to comment.