Skip to content
Draft
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ If you want to use spaces instead, you can do the following:
### Marker function
If you want to extract strings that are not passed directly to `TranslateService`'s `get()`/`instant()`/`stream()` methods, you can wrap them in a marker function to let `ngx-translate-extract` know you want to extract them.

> Do not use the marker function with the `NamespaceTranslateService`! The tool will extract the marker without adding the set namespace and the `NamespaceTranslateService` will search for the key, prefixed with the namespace!

Install marker function:
`npm install @biesbjerg/ngx-translate-extract-marker`

Expand All @@ -66,6 +68,8 @@ _('Extract me');

_Note: `ngx-translate-extract` will automatically detect the import name_



### Commandline arguments
```
Usage:
Expand Down
17 changes: 16 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,15 @@
"@types/glob": "^7.1.3",
"@types/mkdirp": "^1.0.1",
"@types/mocha": "^8.0.3",
"@types/mock-fs": "^4.13.0",
"@types/node": "^14.11.2",
"@types/yargs": "^15.0.7",
"braces": "^3.0.2",
"chai": "^4.2.0",
"husky": "^4.3.0",
"lint-staged": "^10.4.0",
"mocha": "^8.1.3",
"mock-fs": "^4.13.0",
"prettier": "^2.1.2",
"rimraf": "^3.0.2",
"ts-node": "^9.0.0",
Expand Down
13 changes: 12 additions & 1 deletion src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import { CompilerInterface } from '../compilers/compiler.interface';
import { CompilerFactory } from '../compilers/compiler.factory';
import { normalizePaths } from '../utils/fs-helpers';
import { donateMessage } from '../utils/donate';
import { NamespacePipeParser } from '../parsers/namespace-pipe.parser';
import { NamespaceDirectiveParser } from '../parsers/namespace-directive.parser';
import { NamespaceServiceParser } from '../parsers/namespace-service.parser';

// First parsing pass to be able to access pattern argument for use input/output arguments
const y = yargs.option('patterns', {
Expand Down Expand Up @@ -122,7 +125,15 @@ const extractTask = new ExtractTask(cli.input, cli.output, {
});

// Parsers
const parsers: ParserInterface[] = [new PipeParser(), new DirectiveParser(), new ServiceParser(), new MarkerParser()];
const parsers: ParserInterface[] = [
new PipeParser(),
new DirectiveParser(),
new ServiceParser(),
new MarkerParser(),
new NamespacePipeParser(),
new NamespaceDirectiveParser(),
new NamespaceServiceParser()
];
extractTask.setParsers(parsers);

// Post processors
Expand Down
11 changes: 6 additions & 5 deletions src/parsers/directive.parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ import { ParserInterface } from './parser.interface';
import { TranslationCollection } from '../utils/translation.collection';
import { isPathAngularComponent, extractComponentInlineTemplate } from '../utils/utils';

const TRANSLATE_ATTR_NAME = 'translate';
type ElementLike = Element | Template;

export class DirectiveParser implements ParserInterface {
protected TRANSLATE_ATTR_NAME = 'translate';

public extract(source: string, filePath: string): TranslationCollection | null {
let collection: TranslationCollection = new TranslationCollection();

Expand All @@ -35,13 +36,13 @@ export class DirectiveParser implements ParserInterface {
const elements: ElementLike[] = this.getElementsWithTranslateAttribute(nodes);

elements.forEach((element) => {
const attribute = this.getAttribute(element, TRANSLATE_ATTR_NAME);
const attribute = this.getAttribute(element, this.TRANSLATE_ATTR_NAME);
if (attribute?.value) {
collection = collection.add(attribute.value);
return;
}

const boundAttribute = this.getBoundAttribute(element, TRANSLATE_ATTR_NAME);
const boundAttribute = this.getBoundAttribute(element, this.TRANSLATE_ATTR_NAME);
if (boundAttribute?.value) {
this.getLiteralPrimitives(boundAttribute.value).forEach((literalPrimitive) => {
collection = collection.add(literalPrimitive.value);
Expand All @@ -64,10 +65,10 @@ export class DirectiveParser implements ParserInterface {
protected getElementsWithTranslateAttribute(nodes: Node[]): ElementLike[] {
let elements: ElementLike[] = [];
nodes.filter(this.isElementLike).forEach((element) => {
if (this.hasAttribute(element, TRANSLATE_ATTR_NAME)) {
if (this.hasAttribute(element, this.TRANSLATE_ATTR_NAME)) {
elements = [...elements, element];
}
if (this.hasBoundAttribute(element, TRANSLATE_ATTR_NAME)) {
if (this.hasBoundAttribute(element, this.TRANSLATE_ATTR_NAME)) {
elements = [...elements, element];
}
const childElements = this.getElementsWithTranslateAttribute(element.children);
Expand Down
18 changes: 18 additions & 0 deletions src/parsers/namespace-directive.parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { TranslationCollection } from '../utils/translation.collection';
import { extractNamespace } from '../utils/utils';
import { DirectiveParser } from './directive.parser';

export class NamespaceDirectiveParser extends DirectiveParser {
protected TRANSLATE_ATTR_NAME = 'namespace-translate';

public extract(source: string, filePath: string): TranslationCollection | null {
const keys = super.extract(source, filePath);
// only try to extract namespace if the namespaceTranslate pipe has been used.
if (!keys || keys.isEmpty()) {
return keys;
}
const namespace = extractNamespace(source, filePath);
keys.prefixKeys(namespace);
return keys;
}
}
18 changes: 18 additions & 0 deletions src/parsers/namespace-pipe.parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { file } from 'mock-fs/lib/filesystem';
import { PipeParser } from './pipe.parser';
import { extractNamespace } from '../utils/utils';

export class NamespacePipeParser extends PipeParser {
protected TRANSLATE_PIPE_NAME = 'namespaceTranslate';

public extract(source: string, filePath: string) {
const keys = super.extract(source, filePath);
// only try to extract namespace if the namespaceTranslate pipe has been used.
if (!keys || keys.isEmpty()) {
return keys;
}
const namespace = extractNamespace(source, filePath);
keys.prefixKeys(namespace);
return keys;
}
}
19 changes: 19 additions & 0 deletions src/parsers/namespace-service.parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { TranslationCollection } from '../utils/translation.collection';
import { ServiceParser } from './service.parser';
import { extractNamespace } from '../utils/utils';

export class NamespaceServiceParser extends ServiceParser {
protected TRANSLATE_SERVICE_TYPE_REFERENCE = 'NamespaceTranslateService';
protected TRANSLATE_SERVICE_METHOD_NAMES = ['get', 'instant', 'stream'];

public extract(source: string, filePath: string): TranslationCollection | null {
const keys = super.extract(source, filePath);
// only try to extract namespace if the namespaceTranslate pipe has been used.
if (!keys || keys.isEmpty()) {
return keys;
}
const namespace = extractNamespace(source, filePath);
keys.prefixKeys(namespace);
return keys;
}
}
8 changes: 4 additions & 4 deletions src/parsers/pipe.parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ import { ParserInterface } from './parser.interface';
import { TranslationCollection } from '../utils/translation.collection';
import { isPathAngularComponent, extractComponentInlineTemplate } from '../utils/utils';

const TRANSLATE_PIPE_NAME = 'translate';

export class PipeParser implements ParserInterface {
protected TRANSLATE_PIPE_NAME = 'translate';

public extract(source: string, filePath: string): TranslationCollection | null {
if (filePath && isPathAngularComponent(filePath)) {
source = extractComponentInlineTemplate(source);
Expand Down Expand Up @@ -54,7 +54,7 @@ export class PipeParser implements ParserInterface {

if (node?.attributes) {
const translateableAttributes = node.attributes.filter((attr: TmplAstTextAttribute) => {
return attr.name === TRANSLATE_PIPE_NAME;
return attr.name === this.TRANSLATE_PIPE_NAME;
});
ret = [...ret, ...translateableAttributes];
}
Expand Down Expand Up @@ -143,7 +143,7 @@ export class PipeParser implements ParserInterface {
}

protected expressionIsOrHasBindingPipe(exp: any): exp is BindingPipe {
if (exp.name && exp.name === TRANSLATE_PIPE_NAME) {
if (exp.name && exp.name === this.TRANSLATE_PIPE_NAME) {
return true;
}
if (exp.exp && exp.exp instanceof BindingPipe) {
Expand Down
14 changes: 7 additions & 7 deletions src/parsers/service.parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import {
findConstructorDeclaration
} from '../utils/ast-helpers';

const TRANSLATE_SERVICE_TYPE_REFERENCE = 'TranslateService';
const TRANSLATE_SERVICE_METHOD_NAMES = ['get', 'instant', 'stream'];

export class ServiceParser implements ParserInterface {
protected TRANSLATE_SERVICE_TYPE_REFERENCE = 'TranslateService';
protected TRANSLATE_SERVICE_METHOD_NAMES = ['get', 'instant', 'stream'];

public extract(source: string, filePath: string): TranslationCollection | null {
const sourceFile = tsquery.ast(source, filePath);

Expand Down Expand Up @@ -50,15 +50,15 @@ export class ServiceParser implements ParserInterface {
if (!constructorDeclaration) {
return [];
}
const paramName = findMethodParameterByType(constructorDeclaration, TRANSLATE_SERVICE_TYPE_REFERENCE);
return findMethodCallExpressions(constructorDeclaration, paramName, TRANSLATE_SERVICE_METHOD_NAMES);
const paramName = findMethodParameterByType(constructorDeclaration, this.TRANSLATE_SERVICE_TYPE_REFERENCE);
return findMethodCallExpressions(constructorDeclaration, paramName, this.TRANSLATE_SERVICE_METHOD_NAMES);
}

protected findPropertyCallExpressions(classDeclaration: ClassDeclaration): CallExpression[] {
const propName: string = findClassPropertyByType(classDeclaration, TRANSLATE_SERVICE_TYPE_REFERENCE);
const propName: string = findClassPropertyByType(classDeclaration, this.TRANSLATE_SERVICE_TYPE_REFERENCE);
if (!propName) {
return [];
}
return findPropertyCallExpressions(classDeclaration, propName, TRANSLATE_SERVICE_METHOD_NAMES);
return findPropertyCallExpressions(classDeclaration, propName, this.TRANSLATE_SERVICE_METHOD_NAMES);
}
}
11 changes: 10 additions & 1 deletion src/utils/ast-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
Expression,
isBinaryExpression,
isConditionalExpression,
PropertyAccessExpression
PropertyAccessExpression,
StringLiteral
} from 'typescript';

export function getNamedImports(node: Node, moduleName: string): NamedImports[] {
Expand Down Expand Up @@ -155,3 +156,11 @@ export function getStringsFromExpression(expression: Expression): string[] {
}
return [];
}

export function getTranslationNamespace(node: Node, translationNamespaceImportName: string): string | undefined {
const query = `ClassDeclaration > Decorator > CallExpression ObjectLiteralExpression PropertyAssignment:has(Identifier[name='providers']) ArrayLiteralExpression ObjectLiteralExpression:has(PropertyAssignment:has(Identifier[name="${translationNamespaceImportName}"])) PropertyAssignment:has(Identifier[name="useValue"]) StringLiteral`;

const [namespace] = tsquery<StringLiteral>(node, query);

return namespace?.text ?? undefined;
}
6 changes: 6 additions & 0 deletions src/utils/translation.collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,10 @@ export class TranslationCollection {

return new TranslationCollection(values);
}

public prefixKeys(prefix: string): void {
const values: TranslationType = {};
this.forEach((key, value) => (values[prefix + '.' + key] = value));
this.values = values;
}
}
41 changes: 41 additions & 0 deletions src/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,44 @@
import { tsquery } from '@phenomnomnominal/tsquery';
import * as fs from 'fs';
import ts from 'typescript';
import { getNamedImportAlias, getTranslationNamespace } from './ast-helpers';

const NAMESPACE_MODULE_NAME = '@ngx-translate/core';
const TRANSLATION_NAMESPACE_NAME = 'TRANSLATION_NAMESPACE';

export function extractNamespace(source: string, filePath: string): string {
let sourceFile: ts.SourceFile;
// if html tries to get ts file where the namespace should be set
if (filePath.endsWith('.html')) {
try {
const tsFilePath = filePath.replace('.html', '.ts');
const tsFile = fs.readFileSync(tsFilePath, 'utf-8');
sourceFile = tsquery.ast(tsFile, filePath);
} catch (err) {
throw new Error(`unable to load corresponding ts file for ${filePath} where namespace should be defined!`);
}
} else {
sourceFile = tsquery.ast(source, filePath);
}

const translationNamespaceImportName = getNamedImportAlias(sourceFile, NAMESPACE_MODULE_NAME, TRANSLATION_NAMESPACE_NAME);
if (!translationNamespaceImportName) {
throw new Error(
`"${TRANSLATION_NAMESPACE_NAME}" not imported in "${filePath}". Namespace has to be provided to component when using namespace translation service, pipe or directive from "${NAMESPACE_MODULE_NAME}"!!`
);
}

const namespace = getTranslationNamespace(sourceFile, translationNamespaceImportName);

if (!namespace || namespace === '') {
throw new Error(
`No ${TRANSLATION_NAMESPACE_NAME} provided in "${filePath}". Namespace has to be provided to component when using namespace translation service, pipe or directive from "${NAMESPACE_MODULE_NAME}"!!`
);
}

return namespace;
}

/**
* Assumes file is an Angular component if type is javascript/typescript
*/
Expand Down
Loading