Skip to content

Commit

Permalink
feat: 🎸 add scope support for marker function
Browse files Browse the repository at this point in the history
✅ Closes: jsverse#178, jsverse#132
  • Loading branch information
kekel87 committed Mar 27, 2024
1 parent 9eb2a8b commit 90827a3
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 84 deletions.
83 changes: 50 additions & 33 deletions __tests__/buildTranslationFiles.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,11 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => {
describe('Template Extraction', () => {
describe('Pipe', () => {
const type: TranslationCategory = 'pipe';
const config = gConfig(type);

beforeEach(() => removeI18nFolder(type));
beforeEach(() => {
removeI18nFolder(type);
createTranslations(gConfig(type));
});

it('should work with pipe', () => {
const expected = {
Expand All @@ -150,16 +152,17 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => {
expected[nonNumericKey] = defaultValue;
});

createTranslations(config);
assertTranslation({ type, expected, fileFormat });
});
});

describe('Directive', () => {
const type: TranslationCategory = 'directive';
const config = gConfig(type);

beforeEach(() => removeI18nFolder(type));
beforeEach(() => {
removeI18nFolder(type);
createTranslations(gConfig(type));
});

it('should work with directive', () => {
const expected = generateKeys({ end: 24 });
Expand All @@ -168,16 +171,17 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => {
expected[nonNumericKey] = defaultValue;
},
);
createTranslations(config);
assertTranslation({ type, expected, fileFormat });
});
});

describe('ngContainer', () => {
const type: TranslationCategory = 'ngContainer';
const config = gConfig(type);

beforeEach(() => removeI18nFolder(type));
beforeEach(() => {
removeI18nFolder(type);
createTranslations(gConfig(type));
});

it('should work with ngContainer', () => {
let expected = generateKeys({ end: 46 });
Expand All @@ -186,7 +190,6 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => {
expected['another(test)'] =
expected['last "one"'] =
defaultValue;
createTranslations(config);
assertTranslation({ type, expected, fileFormat });
});

Expand All @@ -199,7 +202,6 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => {
'5': defaultValue,
};

createTranslations(config);
assertTranslation({
type,
expected,
Expand All @@ -211,13 +213,14 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => {

describe('ngTemplate', () => {
const type: TranslationCategory = 'ngTemplate';
const config = gConfig(type);

beforeEach(() => removeI18nFolder(type));
beforeEach(() => {
removeI18nFolder(type);
createTranslations(gConfig(type));
});

it('should work with ngTemplate', () => {
let expected = generateKeys({ end: 42 });
createTranslations(config);
assertTranslation({ type, expected, fileFormat });
});

Expand All @@ -230,7 +233,6 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => {
'5': defaultValue,
};

createTranslations(config);
assertTranslation({
type,
expected,
Expand All @@ -242,22 +244,25 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => {

describe('Control flow', () => {
const type: TranslationCategory = 'control-flow';
const config = gConfig(type);

beforeEach(() => removeI18nFolder(type));
beforeEach(() => {
removeI18nFolder(type);
createTranslations(gConfig(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);

beforeEach(() => removeI18nFolder(type));
beforeEach(() => {
removeI18nFolder(type);
createTranslations(gConfig(type));
});

it('should work with read', () => {
const expected = {
Expand Down Expand Up @@ -287,7 +292,6 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => {
},
};

createTranslations(config);
assertTranslation({ type, expected: expected.global, fileFormat });
assertTranslation({
type,
Expand All @@ -302,9 +306,11 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => {
describe('Typescript Extraction', () => {
describe('service', () => {
const type: TranslationCategory = 'service';
const config = gConfig(type);

beforeEach(() => removeI18nFolder(type));
beforeEach(() => {
removeI18nFolder(type);
createTranslations(gConfig(type));
});

it('should work with service', () => {
const expected = {
Expand All @@ -314,7 +320,6 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => {
'inject.test': defaultValue,
};

createTranslations(config);
assertTranslation({ type, expected, fileFormat });
});

Expand All @@ -334,7 +339,6 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => {
},
};

createTranslations(config);
assertTranslation({
type,
expected: expected.todos,
Expand All @@ -357,35 +361,49 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => {

it('should work when passing an array of keys', () => {
const expected = generateKeys({ start: 26, end: 33 });

createTranslations(config);
assertPartialTranslation({ type, expected, fileFormat });
});
});

describe('marker', () => {
const type: TranslationCategory = 'marker';

beforeEach(() => removeI18nFolder(type));
beforeEach(() => {
removeI18nFolder(type);
createTranslations(gConfig(type));
});

it('should work with marker', () => {
const config = gConfig(type);

let expected = {};
expected['username4'] = defaultValue;
expected['password4'] = defaultValue;
expected['username'] = defaultValue;
expected['password'] = defaultValue;
createTranslations(config);
assertTranslation({ type, expected, fileFormat });
});

it('should work with scopes', () => {
const expected = {
username: defaultValue,
password: defaultValue,
};

assertTranslation({
type,
expected: expected,
path: 'nested/scope/',
fileFormat,
});
});
});

describe('inline template', () => {
const type: TranslationCategory = 'inline-template';
const config = gConfig(type);

beforeEach(() => removeI18nFolder(type));
beforeEach(() => {
removeI18nFolder(type);
createTranslations(gConfig(type));
});

it('should work with inline templates', () => {
const expected = generateKeys({ end: 23 });
Expand All @@ -394,7 +412,6 @@ describe.each(formats)('buildTranslationFiles in %s', (fileFormat) => {
expected[nonNumericKey] = defaultValue;
},
);
createTranslations(config);
assertTranslation({ type, expected, fileFormat });
});
});
Expand Down
4 changes: 4 additions & 0 deletions __tests__/marker/nested-scope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const f = {
provide: TRANSLOCO_SCOPE,
useValue: 'nested/scope'
};
26 changes: 26 additions & 0 deletions __tests__/marker/with-scope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { marker } from '@jsverse/transloco-keys-manager/marker';

@Component({
selector: 'bla-bla',
template: `
<table>
<tr>
<th *ngFor="let column of displayedColumns">
{{ column | transloco }}
</th>
</tr>
<tr *ngFor="let row of data">
<td *ngFor="let column of displayedColumns">
{{ row[column] }}
</td>
</tr>
</table>
`
})
export class Basic {
data = [
{ username: 'alex', password: '12345678' },
{ username: 'bob', password: 'password' }
];
displayedColumns = [marker('username', 'nested/scope'), marker('password', 'nested/scope')];
}
112 changes: 82 additions & 30 deletions src/keys-builder/typescript/build-keys-from-ast-nodes.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,98 @@
import { Node, StringLiteral, NoSubstitutionTemplateLiteral } from 'typescript';
import { Node, StringLiteral, NoSubstitutionTemplateLiteral, CallExpression } from 'typescript';
import ts from 'typescript';

import { TSExtractorResult } from './types';

export function buildKeysFromASTNodes(
nodes: Node[],
allowedMethods = ['translate', 'selectTranslate'],
): TSExtractorResult {
/**
* It can be one of the following:
*
* translate('2', {}, 'some/nested');
* translate('3', {}, 'some/nested/en');
* translate(['2', '3'], {}, 'some/nested/en');
* translate('globalKey');
*
*
* selectTranslate('2', {}, 'some/nested');
* selectTranslate('3', {}, 'some/nested/en');
* selectTranslate(['2', '3'], {}, 'some/nested/en');
* selectTranslate('globalKey');
*/
export function buildTranslateKeysFromASTNodes(nodes: Node[]): TSExtractorResult {
const result: TSExtractorResult = [];

for (let node of nodes) {
if (ts.isCallExpression(node.parent)) {
const method = node.parent.expression;
let methodName = '';
if (ts.isIdentifier(method)) {
methodName = method.text;
} else if (ts.isPropertyAccessExpression(method)) {
methodName = method.name.text;
}
if (!allowedMethods.includes(methodName)) {
continue;
}

const [keyNode, _, langNode] = node.parent.arguments;
let lang = isStringNode(langNode) ? langNode.text : '';
let keys: string[] = [];

if (isStringNode(keyNode)) {
keys = [keyNode.text];
} else if (ts.isArrayLiteralExpression(keyNode)) {
keys = keyNode.elements.filter(isStringNode).map((node) => node.text);
}

for (const key of keys) {
result.push({ key, lang });
}
if (!ts.isCallExpression(node.parent)) {
continue;
}

const methodName = getMethodName(node.parent);
if (!['translate', 'selectTranslate'].includes(methodName)) {
continue;
}

const [keyNode, _, langNode] = node.parent.arguments;
let scope = isStringNode(langNode) ? langNode.text : '';
let keys: string[] = [];

if (isStringNode(keyNode)) {
keys = [keyNode.text];
} else if (ts.isArrayLiteralExpression(keyNode)) {
keys = keyNode.elements.filter(isStringNode).map((node) => node.text);
}

for (const key of keys) {
result.push({ key, scope });
}
}

return result;
}

/**
* It can be one of the following:
*
* marker('globalKey');
* marker('globalKey', 'some/nested/en');
*
* alias('globalKey');
* alias('globalKey', 'some/nested/en');
*/
export function buildMarkerKeysFromASTNodes(nodes: Node[], markerName: string): TSExtractorResult {
const result: TSExtractorResult = [];

for (let node of nodes) {
if (!ts.isCallExpression(node.parent)) {
continue;
}

if (markerName !== getMethodName(node.parent)) {
continue;
}

const [keyNode, scopeNode] = node.parent.arguments;
result.push({
key: isStringNode(keyNode) ? keyNode.text : '',
scope: isStringNode(scopeNode) ? scopeNode.text : ''
});
}

return result;
}

function getMethodName(node: CallExpression): string {
const method = node.expression;

let methodName = '';
if (ts.isIdentifier(method)) {
methodName = method.text;
} else if (ts.isPropertyAccessExpression(method)) {
methodName = method.name.text;
}

return methodName;
}


function isStringNode(
node: Node,
): node is StringLiteral | NoSubstitutionTemplateLiteral {
Expand Down
Loading

0 comments on commit 90827a3

Please sign in to comment.