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

fix(migrate): 🐛 improve "translate" pipe regex #702

Merged
merged 6 commits into from
Sep 19, 2023
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
11 changes: 7 additions & 4 deletions libs/transloco-schematics/src/migrate/ngx-translate-migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import * as p from 'node:path';
import * as ora from 'ora';
import { replaceInFile } from 'replace-in-file';

const PIPE_CONTENT_REGEX = `\\s*([^}\\r\\n]*?\\|)\\s*(translate)[^\\r\\n]*?`;
export const PIPE_REGEX = `{{${PIPE_CONTENT_REGEX}}}`;
export const PIPE_IN_BINDING_REGEX = `\\]=('|")${PIPE_CONTENT_REGEX}\\1`;

// Example: `./src/ng2/**/*.html`;
export function run(path) {
console.log('\x1b[4m%s\x1b[0m', '\nStarting migration script');
Expand All @@ -11,15 +15,14 @@ export function run(path) {
path = p.join(dir, path, '/**/*');

const noSpecFiles = { ignore: `${path}spec.ts`, files: `${path}.ts` };
const pipeContent = `\\s*([^}\\r\\n]*?\\|)\\s*(translate)\\s*(?::\\s*{[^}\\r\\n]+})?\\s*(\\s*\\|[\\s\\r\\t\\n]*\\w*)*\\s*`;
const [directive, pipe, pipeInBinding] = [
/(translate|\[translate(?:Params)?\])=("|')[^"']*\2/gm,
new RegExp(`{{${pipeContent}}}`, 'gm'),
new RegExp(`\\]=('|")${pipeContent}\\1`, 'gm'),
new RegExp(PIPE_REGEX, 'gm'),
new RegExp(PIPE_IN_BINDING_REGEX, 'gm'),
].map((regex) => ({
files: `${path}.html`,
from: regex,
to: (match) => match.replace('translate', 'transloco'),
to: (match) => match.replace(/translate/g, 'transloco'),
}));

const moduleMultiImport = {
Expand Down
199 changes: 199 additions & 0 deletions libs/transloco-schematics/src/tests/ngx-translate-migration.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
// noinspection AngularUndefinedBinding

import * as nodePath from 'node:path';
import {readFile} from 'node:fs/promises';

import {replaceInFile, ReplaceInFileConfig} from 'replace-in-file';
import {glob} from 'glob';

import {PIPE_IN_BINDING_REGEX, PIPE_REGEX, run} from '../migrate/ngx-translate-migration';

import Mock = jest.Mock;

jest.mock('replace-in-file');

describe('ngx-translate migration', () => {

describe('Positive regex tests', () => {

describe('Pipe in binding', () => {
test.each([
{
testCase: `<component [header]="'hello.world' | translate">`,
match: [`]="'hello.world' | translate"`]
},
{
testCase: `<component [header]="'hello.world' | translate | anotherPipe">`,
match: [`]="'hello.world' | translate | anotherPipe"`]
},
{
testCase: `<component [header]="'hello' | translate:params | anotherPipe">`,
match: [`]="'hello' | translate:params | anotherPipe"`]
},
{
testCase: `<component [title]="titleMap[reportType] | translate">`,
match: [`]="titleMap[reportType] | translate"`]
},
{
testCase: `<component [matTooltip]="('foo.bar' | translate) + ': ' + (value | number: '1.0-2')">`,
match: [`]="('foo.bar' | translate) + ': ' + (value | number: '1.0-2')"`]
},
{
testCase: `<compnent [title]="'Hello, ' + ('mom' | translate) | fooBar">`,
match: [`]="'Hello, ' + ('mom' | translate) | fooBar"`]
},
{
testCase: `<edge-wizard-step [label]="'Restore Options' | translate" [validatingMessage]="'Processing archive...'|translate"`,
match: [`]="'Restore Options' | translate"`, `]="'Processing archive...'|translate"`]
}
])('Case: $testCase; Match: $match', ({testCase, match}) => {
const regex = new RegExp(PIPE_IN_BINDING_REGEX, 'gm');
const result = testCase.match(regex);

expect(result).toMatchObject(match);
});
});

describe('Pipe', () => {
test.each([
{
testCase: `<component>{{ "hello.world" | translate }}</component>`,
match: [`{{ "hello.world" | translate }}`]
},
{
testCase: `<component>{{ "hello.world" | translate | anotherPipe | oneMore }}</component>`,
match: [`{{ "hello.world" | translate | anotherPipe | oneMore }}`]
},
{
testCase: `<component>{{ "hello" | translate: { name: 'John' } }}</component>`,
match: [`{{ "hello" | translate: { name: 'John' } }}`]
},
{
testCase: `<component>{{ titleMap[reportType] | translate }}</component>`,
match: [`{{ titleMap[reportType] | translate }}`]
},
{
testCase: `<component>{{ ('foo.bar' | translate) + ': ' + (value | number: '1.0-2') }}</component>`,
match: [`{{ ('foo.bar' | translate) + ': ' + (value | number: '1.0-2') }}`]
},
{
testCase: `<compnent>{{ 'Hello, ' + ('mom' | translate) | fooBar }}</compnent>`,
match: [`{{ 'Hello, ' + ('mom' | translate) | fooBar }}`]
},
{
testCase: `{{"1" | translate}} {{errorCounter}} {{"2" | translate}}`,
match: [`{{"1" | translate}}`, `{{"2" | translate}}`]
}
])('Case: $testCase; Match: $match', ({testCase, match}) => {
const regex = new RegExp(PIPE_REGEX, 'gm');
const result = testCase.match(regex);

expect(result).toMatchObject(match);
});
});
});

describe('Negative regex tests', () => {

describe('Pipe in binding', () => {
test.each([
{
testCase: `<component [header]="'hello.world' | transloco">`
},
{
testCase: `<component [header]="'hello.world' | somePipe | anotherPipe">`
},
{
testCase: `<component [header]="'hello' | transloco:params | anotherPipe">`
},
{
testCase: `<component [title]="titleMap[reportType] | fooBar">`
},
{
testCase: `<component [matTooltip]="('foo.bar' | transloco) + ': ' + (value | number: '1.0-2')">`
},
{
testCase: `<compnent [title]="'Hello World ' + ('mom' | transloco) | fooBar">`
},
{
testCase: `<a [title]="'admin.1' | lowercase
| translate"
</a>`
}
])('Case: $testCase', ({testCase}) => {
const regex = new RegExp(PIPE_IN_BINDING_REGEX, 'gm');
const result = testCase.match(regex);

expect(result).toBeNull();
});
});

describe('Pipe', () => {
test.each([
{
testCase: `<component>{{ "hello.world" | transloco }}</component>`
},
{
testCase: `<component>{{ "hello.world" | transloco | anotherPipe | oneMore }}</component>`
},
{
testCase: `<component>{{ "hello" | transloco: { name: 'John' } }}</component>`
},
{
testCase: `<component>{{ titleMap[reportType] | somePipe }}</component>`
},
{
testCase: `<component>{{ ('foo.bar' | transloco) + ': ' + (value | number: '1.0-2') }}</component>`
},
{
testCase: `<compnent>{{ 'Hello, ' + ('mom' | transloco) | fooBar }}</compnent>`
}
])('Case: $testCase', ({testCase}) => {
const regex = new RegExp(PIPE_REGEX, 'gm');
const result = testCase.match(regex);

expect(result).toBeNull();
});
});

});

describe('HTML template', () => {

it('should replace html template content', async () => {
const replacements: Record<string, string> = {},
isWindows = process.platform === 'win32';

(replaceInFile as Mock).mockImplementation(
async (config: ReplaceInFileConfig): Promise<void> => {
const path = config.files as string,
regex = config.from as RegExp,
replacer = config.to as (match: string) => string;

const files = await glob(path, {windowsPathsNoEscape: isWindows});

for (const fullPath of files) {
const filename = nodePath.parse(fullPath).base,
content = replacements[filename] ?? await readFile(fullPath, {encoding: 'utf-8'});

replacements[filename] = content.replace(regex, replacer);
}
}
);

const ngxTranslateTemplatePath = './src/tests/templates/pipes/ngx-translate';

await run(ngxTranslateTemplatePath);

const filenames = Object.keys(replacements);

for(const filename of filenames) {
const resultPath = nodePath.join(__dirname, './templates/pipes/transloco', filename),
resultContent = await readFile(resultPath, {encoding: 'utf-8'});

expect(replacements[filename]).toBe(resultContent);
}
});

});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<div class="bot-setup-container width-100 d-flex">
<form class="da-form d-flex-column width-100" [formGroup]="bot" novalidate>
<!-- Bot name -->
<div class="bot-setup-header allow-full-line d-flex">
<da-editable-input-text [value]="botSetupStore.bot.controls.name.value" [entity]="'bot'"
(onChange)="onNameChange($event)"></da-editable-input-text>
</div>

<edge-wizard-step [label]="'Restore Options' | translate" [validatingMessage]="'Processing archive...'|translate"
[validate]="simulateRestore" [useFormCtrlValidation]="profile">
<ui-view></ui-view>

<!-- Footer error message -->
<div class="footer-error-message d-flex" *ngIf="errorCounter > 0">
{{"1" | translate}} {{errorCounter}} {{"2" | translate}}{{errorCounter > 1 ? 's'
: ''}}
</div>
<div class="allow-full-line footer-buttons d-flex align-end-center">
<div class="setup-buttons d-flex">
<button class="da-btn secondary link footer cancel-button" type="button" (click)="backToPrevPage(false)">
{{"3" | translate}}
</button>
<button class="da-btn secondary save-button" *ngIf="isEditState && !isDuplicate" (click)="onSubmit(false)">
{{"4" | translate}}
</button>
<button class="da-btn primary save-run-button" (click)="onSubmit(true)">{{"5" | translate}}
</button>
</div>
</div>
</edge-wizard-step>
</form>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<div class="d-flex-column my-agenda-container width-100" [formGroup]="agenda" *ngIf="agenda">


<!-- Measurement -->
<div class="d-flex">
<div style="width: 216px;">
<select daChoices
labelKey="name"
valueKey="metricName"
formControlName="measurement"
(change)="onMeasurementChange($event)"
[choicesConfig]="botSetupStore.metricsList">
</select>
</div>

<div class="d-flex align-start-center">
<label class="text-between-inputs inner-text"> {{"6" | translate}}</label>

<div style="width: 216px;">
<select daChoices
formControlName="metricDirection"
labelKey="name"
[daOptions]="{searchEnabled: false}"
valueKey="value"
[choicesConfig]="botSetupStore.metricDirection">
</select>
</div>


<label class="right-label inner-text">{{"7" | translate}}</label>
</div>
</div>
<div class="input-field">
<div class="input-error-abs"
*ngIf="agenda.get('measurement').hasError('required') && agenda.get('measurement').touched">
{{"8" | translate}}
</div>
</div>


<!-- Dimensions -->
<div class="d-flex my-agenda-header">{{"9" | translate}}</div>
<mat-radio-group class="d-flex-column" formControlName="isAllDimensions" (change)="onIsAllDimensionsChange($event)">
<mat-radio-button class="checkbox-input" [value]="botSetupStore.isAllDimensions[0].value">
<span class="input-line primary-text">{{"10" | translate}}</span>
</mat-radio-button>
<mat-radio-button class="checkbox-input" [value]="botSetupStore.isAllDimensions[1].value">
<span class="input-line primary-text">{{"11" | translate}}</span>
</mat-radio-button>
</mat-radio-group>

<div class="space-between-rows d-flex"
[hidden]="agenda.controls.isAllDimensions.value === botSetupStore.isAllDimensions[0].value">
<div style="width: 468px;">
<select daChoices
multiple
labelKey="name"
valueKey="systemName"
formControlName="dimensionsListSelection"
(removeItem)="onDimensionUnSelected($event)"
[choicesConfig]="botSetupStore.dimensions">
</select>
</div>
</div>

<!-- Dimensions list -->
<div [ngClass]="{'input-field': agenda.get('dimensionsListSelection').hasError('required') && agenda.get('dimensionsListSelection').touched }">
<div class="input-error-abs"
*ngIf="agenda.get('dimensionsListSelection').hasError('required') && agenda.get('dimensionsListSelection').touched">
{{"12" | translate}}
</div>
</div>

<div class="d-flex align-start-center">
<!-- Date range -->
<div class="d-flex-column">

<!-- translated in the component.ts and the value set according to enableCompareTo status (Within / Later Date) -->
<div data-auto-id="agenda_setup_date_range_label" class="top-line secondary-text d-flex">{{_dateRangeLabelText}}
</div>

<da-date-picker *ngIf="!!dateRange" class="long-select" [selectedRange]="dateRange"
[selectedDateType]="dateRange.dateRangeType"
(onChange)="onDateChange($event)"></da-date-picker>
</div>
<!-- compare to Date range -->
<div *ngIf="allowNewBotType()" class="d-flex align-center mt-30" [ngClass]="{'mt-30': !isCompareToEnabled}">
<dato-checkbox [formControl]="agenda.controls.enableCompareTo"
[ngClass]="{'mt-30': isCompareToEnabled}"
class="mx-10">{{"13" | translate}}
</dato-checkbox>
<div [ngClass]="{'d-flex-column': isCompareToEnabled}">

<div *ngIf="isCompareToEnabled; else isCompareToDisabled" class="top-line secondary-text d-flex">
{{"14" | translate}}
</div>
<ng-template #isCompareToDisabled>
<span class="primary-200-color primary-50-background-color d-flex align-start-center px-10 long-select">{{"15" | translate}}</span>
</ng-template>
<da-date-picker *ngIf="!!compareToDateRange && isCompareToEnabled" class="long-select"
[selectedRange]="compareToDateRange" [selectedDateType]="compareToDateRange.dateRangeType"
(onChange)="onCompareToDateChange($event)">
</da-date-picker>
</div>

</div>
</div>

<!-- Filters -->
<div class="my-agenda-header d-flex">{{"16" | translate}}</div>
<div class="d-flex">
<div class="add-filter-button" (click)="addFiltersModal($event)">
<dato-icon datoIcon="plus-round" class="icon"></dato-icon>
</div>
<span class="add-filter-text"
(click)="addFiltersModal($event)">{{"17" | translate}} {{"18" | translate}}</span>
</div>
<!-- Header -->
<div class="my-agenda-header d-flex">{{"19" | translate}}</div>
</div>
Loading
Loading