Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for Built-in control flow syntax (#26) #27

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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