Skip to content
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
23 changes: 19 additions & 4 deletions __tests__/buildTranslationFiles.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Config, 'fileFormat'> {
type: TranslationCategory;
Expand Down Expand Up @@ -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,
};
[
Expand All @@ -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;
Expand Down Expand Up @@ -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 });
});
Expand All @@ -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);
Expand Down
79 changes: 79 additions & 0 deletions __tests__/control-flow/1.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<span>{{ '1' | transloco }}</span>
<span transloco="2"></span>
<ng-container *transloco="let t">
<span>{{ t('3') }}</span>
</ng-container>
<ng-template transloco let-t>
<span>{{ t('4') }}</span>
</ng-template>


@if (a > b) {
<span>{{ '5' | transloco }}</span>
<span transloco="6">
</span>
<ng-container *transloco="let t">
<span>{{ t('7') }}</span>
</ng-container>
<ng-template transloco let-t>
<span>{{ t('8') }}</span>
</ng-template>
} @else if (b > a) {
<span>{{ '9' | transloco }}</span>
} @else {
<span [innerHtml]="'10' | transloco"></span>
}

@for (item of items; track item.id) {
{{ '11' | transloco }}
} @empty {
<span>{{ '12' | transloco }}</span>
}

@switch (condition) {
@case (caseA) {
<span>{{ '13' | transloco }}</span>
}
@default {
<span>{{ '14' | transloco }}</span>
}
}

@defer {
<span>{{ '15' | transloco }}</span>
} @error {
<span>{{ '16' | transloco }}</span>
} @placeholder {
<span>{{ '17' | transloco }}</span>
} @loading {
<span>{{ '18' | transloco }}</span>
}

<ng-container *transloco="let t">
@if (a > b) {
<span>{{ t('19') }}</span>
} @else if (b > a) {
<span [innerHtml]="t('20')"></span>
} @else {
@for (item of items; track item.id) {
<span>{{ t('21') }}</span>
} @empty {
@switch (condition) {
@case (caseA) {
<span>{{ t('22') }}</span>
}
@default {
@defer {
<span>{{ '23' | transloco }}</span>
} @error {
<span transloco="24"></span>
} @placeholder {
<span>{{ t('25') }}</span>
} @loading {
<span>{{ t('26') }}</span>
}
}
}
}
}
</ng-container>
1 change: 1 addition & 0 deletions __tests__/directive/2.html
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,5 @@
<div class="my-agenda-header d-flex" transloco="{{'20'}}"></div>
<div class="my-agenda-header d-flex" transloco="{{condition ? '21' : dontTake}}"></div>
<div class="my-agenda-header d-flex" transloco="{{condition ? dontTake : (condition2 ? '22' : '23')}}"></div>
<self-closing transloco="24" />
</div>
3 changes: 3 additions & 0 deletions __tests__/ngTemplate/5.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ <h1>ddsds</h1>
<my-comp [aa]="t('39') + 'a'">
<comp a="{{t('40') + 'a'}}">{{t('41') + 'a'}}</comp>
</my-comp>
<ng-template>
<self-closing [value]="t('42')" />
</ng-template>
</ng-container>
2 changes: 2 additions & 0 deletions __tests__/pipe/6.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,5 @@

{{ '{{count}} items' | transloco:{ count: item.usageCount } }}
</my-comp>

<self-closing [value]="'79' | transloco" />
10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 7 additions & 0 deletions src/keys-builder/template/directive.extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { resolveAliasAndKey } from '../utils/resolvers.utils';

import { TemplateExtractorConfig } from './types';
import {
isBlockNode,
isBoundAttribute,
isConditionalExpression,
isElement,
Expand All @@ -21,6 +22,7 @@ import {
isTemplate,
isTextAttribute,
parseTemplate,
resolveBlockChildNodes,
} from './utils';

export function directiveExtractor(config: TemplateExtractorConfig) {
Expand All @@ -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;
}
Expand Down
7 changes: 7 additions & 0 deletions src/keys-builder/template/pipe.extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
isPropertyRead,
isTemplate,
parseTemplate,
isBlockNode,
resolveBlockChildNodes,
} from './utils';

export function pipeExtractor(config: TemplateExtractorConfig) {
Expand All @@ -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)) {
Expand Down
7 changes: 7 additions & 0 deletions src/keys-builder/template/structural-directive.extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import {
isSupportedNode,
isTemplate,
parseTemplate,
isBlockNode,
resolveBlockChildNodes,
} from './utils';

interface ContainerMetaData {
Expand All @@ -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)) {
Expand Down
93 changes: 93 additions & 0 deletions src/keys-builder/template/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -98,3 +109,85 @@ export function isSupportedNode<Predicates extends any[]>(
): node is GuardedType<Predicates[number]> {
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;
}