Skip to content

Commit 0e3ab54

Browse files
authored
feat: 🎸 Add support for self closing tag and control flow (#180)
1 parent ac283b7 commit 0e3ab54

File tree

10 files changed

+226
-6
lines changed

10 files changed

+226
-6
lines changed

‎__tests__/buildTranslationFiles.spec.ts‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ type TranslationCategory =
6666
| 'multi-input'
6767
| 'scope-mapping'
6868
| 'comments'
69-
| 'remove-extra-keys';
69+
| 'remove-extra-keys'
70+
| 'self-closing'
71+
| 'control-flow';
7072

7173
interface assertTranslationParams extends Pick<Config, 'fileFormat'> {
7274
type: TranslationCategory;
@@ -135,7 +137,7 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => {
135137
'49.50.51.52': defaultValue,
136138
...generateKeys({ start: 53, end: 62 }),
137139
'63.64.65': defaultValue,
138-
...generateKeys({ start: 66, end: 78 }),
140+
...generateKeys({ start: 66, end: 79 }),
139141
'{{count}} items': defaultValue,
140142
};
141143
[
@@ -160,7 +162,7 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => {
160162
beforeEach(() => removeI18nFolder(type));
161163

162164
it('should work with directive', () => {
163-
const expected = generateKeys({ end: 23 });
165+
const expected = generateKeys({ end: 24 });
164166
['Processing archive...', 'Restore Options'].forEach(
165167
(nonNumericKey) => {
166168
expected[nonNumericKey] = defaultValue;
@@ -214,7 +216,7 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => {
214216
beforeEach(() => removeI18nFolder(type));
215217

216218
it('should work with ngTemplate', () => {
217-
let expected = generateKeys({ end: 41 });
219+
let expected = generateKeys({ end: 42 });
218220
createTranslations(config);
219221
assertTranslation({ type, expected, fileFormat });
220222
});
@@ -238,6 +240,19 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => {
238240
});
239241
});
240242

243+
describe('Control flow', () => {
244+
const type: TranslationCategory = 'control-flow';
245+
const config = gConfig(type);
246+
247+
beforeEach(() => removeI18nFolder(type));
248+
249+
it('should work with control flow', () => {
250+
let expected = generateKeys({ end: 26 });
251+
createTranslations(config);
252+
assertTranslation({ type, expected, fileFormat });
253+
});
254+
});
255+
241256
describe('read', () => {
242257
const type: TranslationCategory = 'read';
243258
const config = gConfig(type);
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<span>{{ '1' | transloco }}</span>
2+
<span transloco="2"></span>
3+
<ng-container *transloco="let t">
4+
<span>{{ t('3') }}</span>
5+
</ng-container>
6+
<ng-template transloco let-t>
7+
<span>{{ t('4') }}</span>
8+
</ng-template>
9+
10+
11+
@if (a > b) {
12+
<span>{{ '5' | transloco }}</span>
13+
<span transloco="6">
14+
</span>
15+
<ng-container *transloco="let t">
16+
<span>{{ t('7') }}</span>
17+
</ng-container>
18+
<ng-template transloco let-t>
19+
<span>{{ t('8') }}</span>
20+
</ng-template>
21+
} @else if (b > a) {
22+
<span>{{ '9' | transloco }}</span>
23+
} @else {
24+
<span [innerHtml]="'10' | transloco"></span>
25+
}
26+
27+
@for (item of items; track item.id) {
28+
{{ '11' | transloco }}
29+
} @empty {
30+
<span>{{ '12' | transloco }}</span>
31+
}
32+
33+
@switch (condition) {
34+
@case (caseA) {
35+
<span>{{ '13' | transloco }}</span>
36+
}
37+
@default {
38+
<span>{{ '14' | transloco }}</span>
39+
}
40+
}
41+
42+
@defer {
43+
<span>{{ '15' | transloco }}</span>
44+
} @error {
45+
<span>{{ '16' | transloco }}</span>
46+
} @placeholder {
47+
<span>{{ '17' | transloco }}</span>
48+
} @loading {
49+
<span>{{ '18' | transloco }}</span>
50+
}
51+
52+
<ng-container *transloco="let t">
53+
@if (a > b) {
54+
<span>{{ t('19') }}</span>
55+
} @else if (b > a) {
56+
<span [innerHtml]="t('20')"></span>
57+
} @else {
58+
@for (item of items; track item.id) {
59+
<span>{{ t('21') }}</span>
60+
} @empty {
61+
@switch (condition) {
62+
@case (caseA) {
63+
<span>{{ t('22') }}</span>
64+
}
65+
@default {
66+
@defer {
67+
<span>{{ '23' | transloco }}</span>
68+
} @error {
69+
<span transloco="24"></span>
70+
} @placeholder {
71+
<span>{{ t('25') }}</span>
72+
} @loading {
73+
<span>{{ t('26') }}</span>
74+
}
75+
}
76+
}
77+
}
78+
}
79+
</ng-container>

‎__tests__/directive/2.html‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,5 @@
124124
<div class="my-agenda-header d-flex" transloco="{{'20'}}"></div>
125125
<div class="my-agenda-header d-flex" transloco="{{condition ? '21' : dontTake}}"></div>
126126
<div class="my-agenda-header d-flex" transloco="{{condition ? dontTake : (condition2 ? '22' : '23')}}"></div>
127+
<self-closing transloco="24" />
127128
</div>

‎__tests__/ngTemplate/5.html‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,7 @@ <h1>ddsds</h1>
1414
<my-comp [aa]="t('39') + 'a'">
1515
<comp a="{{t('40') + 'a'}}">{{t('41') + 'a'}}</comp>
1616
</my-comp>
17+
<ng-template>
18+
<self-closing [value]="t('42')" />
19+
</ng-template>
1720
</ng-container>

‎__tests__/pipe/6.html‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,5 @@
5151

5252
{{ '{{count}} items' | transloco:{ count: item.usageCount } }}
5353
</my-comp>
54+
55+
<self-closing [value]="'79' | transloco" />

‎package.json‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,14 @@
77
},
88
"type": "module",
99
"exports": {
10-
"types": "./public-api.d.ts",
11-
"default": "./public-api.js"
10+
".": {
11+
"import": "./public-api.js",
12+
"types": "./public-api.d.ts"
13+
},
14+
"./marker": {
15+
"import": "./marker.js",
16+
"types": "./marker.d.ts"
17+
}
1218
},
1319
"bin": {
1420
"transloco-keys-manager": "index.js"

‎src/keys-builder/template/directive.extractor.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { resolveAliasAndKey } from '../utils/resolvers.utils';
1212

1313
import { TemplateExtractorConfig } from './types';
1414
import {
15+
isBlockNode,
1516
isBoundAttribute,
1617
isConditionalExpression,
1718
isElement,
@@ -21,6 +22,7 @@ import {
2122
isTemplate,
2223
isTextAttribute,
2324
parseTemplate,
25+
resolveBlockChildNodes,
2426
} from './utils';
2527

2628
export function directiveExtractor(config: TemplateExtractorConfig) {
@@ -30,6 +32,11 @@ export function directiveExtractor(config: TemplateExtractorConfig) {
3032

3133
function traverse(nodes: TmplAstNode[], config: ExtractorConfig) {
3234
for (const node of nodes) {
35+
if (isBlockNode(node)) {
36+
traverse(resolveBlockChildNodes(node), config);
37+
continue;
38+
}
39+
3340
if (!isSupportedNode(node, [isTemplate, isElement])) {
3441
continue;
3542
}

‎src/keys-builder/template/pipe.extractor.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
isPropertyRead,
1919
isTemplate,
2020
parseTemplate,
21+
isBlockNode,
22+
resolveBlockChildNodes,
2123
} from './utils';
2224

2325
export function pipeExtractor(config: TemplateExtractorConfig) {
@@ -27,6 +29,11 @@ export function pipeExtractor(config: TemplateExtractorConfig) {
2729

2830
function traverse(nodes: TmplAstNode[], config: ExtractorConfig) {
2931
for (const node of nodes) {
32+
if (isBlockNode(node)) {
33+
traverse(resolveBlockChildNodes(node), config);
34+
continue;
35+
}
36+
3037
let astTrees: AST[] = [];
3138

3239
if (isElement(node) || isTemplate(node)) {

‎src/keys-builder/template/structural-directive.extractor.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import {
2828
isSupportedNode,
2929
isTemplate,
3030
parseTemplate,
31+
isBlockNode,
32+
resolveBlockChildNodes,
3133
} from './utils';
3234

3335
interface ContainerMetaData {
@@ -49,6 +51,11 @@ export function traverse(
4951
config: TemplateExtractorConfig,
5052
) {
5153
for (const node of nodes) {
54+
if (isBlockNode(node)) {
55+
traverse(resolveBlockChildNodes(node), containers, config);
56+
continue;
57+
}
58+
5259
let methodUsages: ContainerMetaData[] = [];
5360

5461
if (isBoundText(node)) {

‎src/keys-builder/template/utils.ts‎

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@ import {
1414
TmplAstElement,
1515
TmplAstTemplate,
1616
TmplAstTextAttribute,
17+
TmplAstNode,
18+
TmplAstDeferredBlock,
19+
TmplAstDeferredBlockError,
20+
TmplAstDeferredBlockLoading,
21+
TmplAstDeferredBlockPlaceholder,
22+
TmplAstForLoopBlock,
23+
TmplAstForLoopBlockEmpty,
24+
TmplAstIfBlockBranch,
25+
TmplAstSwitchBlockCase,
26+
TmplAstIfBlock,
27+
TmplAstSwitchBlock,
1728
} from '@angular/compiler';
1829

1930
import { readFile } from '../../utils/file.utils';
@@ -98,3 +109,85 @@ export function isSupportedNode<Predicates extends any[]>(
98109
): node is GuardedType<Predicates[number]> {
99110
return predicates.some((predicate) => predicate(node));
100111
}
112+
113+
type BlockNode =
114+
| TmplAstDeferredBlockError
115+
| TmplAstDeferredBlockLoading
116+
| TmplAstDeferredBlockPlaceholder
117+
| TmplAstForLoopBlockEmpty
118+
| TmplAstIfBlockBranch
119+
| TmplAstSwitchBlockCase
120+
| TmplAstForLoopBlock
121+
| TmplAstDeferredBlock
122+
| TmplAstIfBlock
123+
| TmplAstSwitchBlock;
124+
125+
export function isBlockWithChildren(
126+
node: unknown,
127+
): node is { children: TmplAstNode[] } {
128+
return (
129+
node instanceof TmplAstDeferredBlockError ||
130+
node instanceof TmplAstDeferredBlockLoading ||
131+
node instanceof TmplAstDeferredBlockPlaceholder ||
132+
node instanceof TmplAstForLoopBlockEmpty ||
133+
node instanceof TmplAstIfBlockBranch ||
134+
node instanceof TmplAstSwitchBlockCase
135+
);
136+
}
137+
138+
export function isTmplAstForLoopBlock(
139+
node: unknown,
140+
): node is TmplAstForLoopBlock {
141+
return node instanceof TmplAstForLoopBlock;
142+
}
143+
144+
export function isTmplAstDeferredBlock(
145+
node: unknown,
146+
): node is TmplAstDeferredBlock {
147+
return node instanceof TmplAstDeferredBlock;
148+
}
149+
150+
export function isTmplAstIfBlock(node: unknown): node is TmplAstIfBlock {
151+
return node instanceof TmplAstIfBlock;
152+
}
153+
154+
export function isTmplAstSwitchBlock(
155+
node: unknown,
156+
): node is TmplAstSwitchBlock {
157+
return node instanceof TmplAstSwitchBlock;
158+
}
159+
160+
export function isBlockNode(node: TmplAstNode): node is BlockNode {
161+
return (
162+
isTmplAstIfBlock(node) ||
163+
isTmplAstForLoopBlock(node) ||
164+
isTmplAstDeferredBlock(node) ||
165+
isTmplAstSwitchBlock(node) ||
166+
isBlockWithChildren(node)
167+
);
168+
}
169+
170+
export function resolveBlockChildNodes(node: BlockNode): TmplAstNode[] {
171+
if (isTmplAstIfBlock(node)) {
172+
return node.branches;
173+
}
174+
175+
if (isTmplAstForLoopBlock(node)) {
176+
return node.empty ? [...node.children, node.empty] : node.children;
177+
}
178+
179+
if (isTmplAstDeferredBlock(node)) {
180+
return [
181+
...node.children,
182+
...([node.loading, node.error, node.placeholder].filter(
183+
Boolean,
184+
) as TmplAstNode[]),
185+
];
186+
}
187+
188+
if (isTmplAstSwitchBlock(node)) {
189+
return node.cases;
190+
}
191+
192+
return node.children;
193+
}

0 commit comments

Comments
 (0)