├── .gitignore
├── src
├── migrate-test.ts
├── models
│ └── models.ts
├── utils
│ ├── resource.ts
│ ├── node-range.ts
│ ├── text-ast.ts
│ └── pipe-ast.ts
├── migrate.ts
└── replace-pipes.ts
├── assets
└── migrate.png
├── bin
└── migrate
├── tsconfig.json
├── README.md
├── LICENSE
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .idea
--------------------------------------------------------------------------------
/src/migrate-test.ts:
--------------------------------------------------------------------------------
1 | import { migrate } from './migrate';
2 | migrate();
--------------------------------------------------------------------------------
/assets/migrate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/irustm/ngx-translate-migrate/HEAD/assets/migrate.png
--------------------------------------------------------------------------------
/src/models/models.ts:
--------------------------------------------------------------------------------
1 | export interface CliConfig {
2 | projectPath: string;
3 | filePath: string;
4 | }
--------------------------------------------------------------------------------
/bin/migrate:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | let path = require('path')
4 |
5 | require(path.join(__dirname, '..', 'migrate')).migrate();
--------------------------------------------------------------------------------
/src/utils/resource.ts:
--------------------------------------------------------------------------------
1 | import { ResourceResolver } from 'ngast';
2 | import { readFile, readFileSync } from 'fs';
3 |
4 | export const resourceResolver: ResourceResolver = {
5 | get(url: string) {
6 | return new Promise((resolve, reject) => {
7 | readFile(url, 'utf-8', (err, content) => {
8 | if (err) {
9 | reject(err);
10 | } else {
11 | resolve(content);
12 | }
13 | });
14 | });
15 | },
16 | getSync(url: string) {
17 | return readFileSync(url).toString();
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "outDir": "./dist",
5 | "sourceMap": true,
6 | "declaration": false,
7 | "module": "commonjs",
8 | "moduleResolution": "node",
9 | "emitDecoratorMetadata": true,
10 | "experimentalDecorators": true,
11 | "importHelpers": true,
12 | "target": "es6",
13 | "typeRoots": [
14 | "node_modules/@types"
15 | ],
16 | "lib": [
17 | "es2018",
18 | "dom"
19 | ]
20 | },
21 | "exclude": [
22 | "node_modules"
23 | ],
24 | "include": [
25 | "src"
26 | ],
27 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # ngx-translate-migrate
6 |
7 | Automate migrate from `ngx-translate` to `Angular i18n`
8 |
9 | ## How to use
10 |
11 | `npx ngx-translate-migrate -f src/assets/i18n/main/ru.json`
12 |
13 | or to define a tsconfig
14 |
15 | `npx ngx-translate-migrate -p ./ngx-translate-all-test/tsconfig.json -f src/assets/i18n/main/ru.json`
16 |
17 | ## Related projects
18 |
19 | - [ngx-translate-all](https://github.com/irustm/ngx-translate-all) - Automate translate Angular project
20 |
21 |
22 | ## License
23 | MIT
24 |
--------------------------------------------------------------------------------
/src/utils/node-range.ts:
--------------------------------------------------------------------------------
1 | export function nodeToRange(node: any) {
2 | if (node.startSourceSpan) {
3 | if (node.endSourceSpan) {
4 | return [
5 | node.startSourceSpan.start.offset,
6 | node.endSourceSpan.end.offset,
7 | ];
8 | }
9 | return [
10 | node.startSourceSpan.start.offset,
11 | node.startSourceSpan.end.offset,
12 | ];
13 | }
14 | if (node.endSourceSpan) {
15 | if (node.sourceSpan) {
16 | return [
17 | node.sourceSpan.start.offset,
18 | node.endSourceSpan.end.offset,
19 | ];
20 | }
21 | }
22 | if (node.sourceSpan) {
23 | return [node.sourceSpan.start.offset, node.sourceSpan.end.offset];
24 | }
25 | if (node.span) {
26 | return [node.span.start, node.span.end];
27 | }
28 | }
29 |
30 | export function getTextFromRange(source, start, end) {
31 | if (start !== null && end !== null) {
32 | let res = '';
33 | for (let i = start; i < end; i++) {
34 | res += source[i];
35 | }
36 | return res;
37 | } else {
38 | return null;
39 | }
40 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 irustm
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/utils/text-ast.ts:
--------------------------------------------------------------------------------
1 | import { ElementAst, TextAst, ASTWithSource } from '@angular/compiler';
2 | export function getTextAst(element: ElementAst): string[] {
3 | const texts: string[] = [];
4 | if (element && element.children && element.children.length) {
5 | element.children.forEach((child: any) => {
6 | const name = child.constructor.name;
7 | const value: TextAst | ASTWithSource | string | any = (child as TextAst)
8 | .value;
9 | if (value) {
10 | if (name === 'TextAst' && value.trim() !== '') {
11 | texts.push(child.value);
12 | } else {
13 | const source: string = (value as ASTWithSource).source;
14 | // if (typeof value === 'object' && source && source.trim() !== '') {
15 | // texts.push(source);
16 | // }
17 | if(value.constructor.name === 'ASTWithSource'){
18 | texts.push(source);
19 | }
20 | }
21 | } else {
22 | const childTexts = getTextAst(child as ElementAst);
23 | childTexts.forEach((el: any) => {
24 | texts.push(el);
25 | });
26 | }
27 | });
28 | }
29 | return texts;
30 | }
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ngx-translate-migrate",
3 | "version": "0.0.1",
4 | "description": "Automate migrate from ngx-translate to i18n",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/irustm/ngx-translate-migrate.git"
8 | },
9 | "keywords": [
10 | "angular",
11 | "ngx-translate",
12 | "i18n",
13 | "translate",
14 | "migrate"
15 | ],
16 | "author": "irustm",
17 | "license": "MIT",
18 | "bin": "./bin/migrate",
19 | "bugs": {
20 | "url": "https://github.com/irustm/ngx-translate-migrate/issues"
21 | },
22 | "homepage": "https://github.com/irustm/ngx-translate-migrate#readme",
23 | "scripts": {
24 | "prebuild": "rimraf dist",
25 | "build": "tsc",
26 | "postbuild": "cp -r bin dist && cp -r package.json dist && cp -r README.md dist && cp -r LICENSE dist",
27 | "test": "ts-node src/migrate-test.ts -p ../ngx-translate-migrate-test/tsconfig.json -f ../ngx-translate-migrate-test/src/assets/i18n/en.json"
28 | },
29 | "dependencies": {
30 | "@angular/compiler": "~7.2.13",
31 | "@angular/compiler-cli": "~7.2.13",
32 | "@angular/core": "~7.2.13",
33 | "chalk": "^2.4.1",
34 | "minimist": "^1.2.0",
35 | "ngast": "^0.2.4",
36 | "rxjs": "~6.3.3",
37 | "typescript": "~3.1.6"
38 | },
39 | "devDependencies": {
40 | "@types/node": "^10.12.18",
41 | "copy": "^0.3.2",
42 | "rimraf": "^2.6.3"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/utils/pipe-ast.ts:
--------------------------------------------------------------------------------
1 | import { ElementAst, Interpolation } from '@angular/compiler';
2 | export interface PipeBoundText {
3 | ast: Interpolation;
4 | element: any;
5 | parentNode: ElementAst;
6 | source: string;
7 | pipeValues: PipeSourceAst[];
8 | }
9 | export interface PipeSourceAst {
10 | expression: any;
11 | value: string;
12 | }
13 | export function getPipeAst(element: ElementAst, pipeName = 'translate'): PipeBoundText[] {
14 | const pipeBounds: PipeBoundText[] = [];
15 | if (element && element.children && element.children.length) {
16 | element.children.forEach((child: any) => {
17 | const name = child.constructor.name;
18 | const value: any = child.value;
19 | if (value) {
20 | if (name === 'BoundTextAst') {
21 | const expressions = value.ast.expressions.filter(expression => expression.name === pipeName);
22 | if (expressions && expressions.length > 0) {
23 | const pipesValues: PipeSourceAst[] = [];
24 | expressions.forEach(expression => {
25 | pipesValues.push({
26 | expression,
27 | value: expression.exp.value,
28 | });
29 | });
30 | pipeBounds.push({
31 | ast: value.ast,
32 | element: child,
33 | parentNode: element,
34 | source: value.source,
35 | pipeValues: pipesValues
36 | });
37 | }
38 | }
39 | } else {
40 | return pipeBounds.push(...getPipeAst(child as ElementAst));
41 | }
42 | });
43 | }
44 | return pipeBounds;
45 | }
46 |
--------------------------------------------------------------------------------
/src/migrate.ts:
--------------------------------------------------------------------------------
1 | import * as minimist from "minimist";
2 | import * as chalk from "chalk";
3 | import { existsSync, writeFile, mkdirSync } from "fs";
4 | import { ProjectSymbols } from "ngast";
5 |
6 | import { resourceResolver } from "./utils/resource";
7 | import { replacePipes } from './replace-pipes';
8 | import { CliConfig } from './models/models';
9 |
10 | const error = message => {
11 | console.error(chalk.default.bgRed.white(message));
12 | };
13 | const info = (message, count1?, count2?) => {
14 | console.log(
15 | chalk.default.green(message) +
16 | ` ${count1 ? chalk.default.blue(count1) : ""}` +
17 | ` ${count2 ? "/ " + chalk.default.yellowBright(count2) : ""}`
18 | );
19 | };
20 |
21 | export function migrate() {
22 | const config = getCliConfig();
23 | console.log("Parsing...");
24 | let parseError: any = null;
25 | const projectSymbols = new ProjectSymbols(
26 | config.projectPath,
27 | resourceResolver,
28 | e => (parseError = e)
29 | );
30 | let allDirectives = projectSymbols.getDirectives();
31 | if (!parseError) {
32 | allDirectives = allDirectives.filter(
33 | el => el.symbol.filePath.indexOf("node_modules") === -1
34 | );
35 | replacePipes(allDirectives, config);
36 | } else {
37 | error(parseError);
38 | }
39 | }
40 |
41 |
42 | function validateArgs(args: any, attrs: string[], error: Function) {
43 | attrs.forEach(attr => {
44 | if (!args[attr] || args[attr].trim().length === 0) {
45 | error(`Connot find --${attr} argument`);
46 | process.exit(1);
47 | }
48 | });
49 | }
50 |
51 | function getCliConfig(): CliConfig {
52 | const args: any = minimist(process.argv.slice(2));
53 | validateArgs(args, ['f'], error);
54 | let projectPath = args.p;
55 | const filePath = args.f;
56 |
57 | if (!projectPath) {
58 | projectPath = "./tsconfig.json";
59 | }
60 | if (!existsSync(projectPath)) {
61 | error(`Cannot find tsconfig at ${projectPath}`);
62 | process.exit(1);
63 | }
64 | if (!existsSync(filePath)) {
65 | error(`Cannot find filePath at ${filePath}.`);
66 | process.exit(1);
67 | }
68 | return {
69 | projectPath,
70 | filePath
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/replace-pipes.ts:
--------------------------------------------------------------------------------
1 | import { ElementAst } from '@angular/compiler';
2 | import { DirectiveSymbol } from 'ngast';
3 | import { getPipeAst, PipeBoundText } from './utils/pipe-ast';
4 | import { readFileSync, writeFileSync } from 'fs';
5 | import { CliConfig } from './models/models';
6 | import { nodeToRange, getTextFromRange } from './utils/node-range';
7 |
8 |
9 | export function replacePipes(allDirectives: DirectiveSymbol[], config: CliConfig): void {
10 | const translates = getTranslatesSync(config.filePath);
11 | allDirectives.forEach(el => {
12 | let url = null;
13 | try {
14 | if (el.isComponent()) {
15 | let translatePipes: PipeBoundText[] = [];
16 | url = el.getResolvedMetadata().templateUrl || el.symbol.filePath;
17 | el.getTemplateAst().templateAst.forEach(element => {
18 | translatePipes.push(...getPipeAst(element as ElementAst, 'translate'));
19 | });
20 |
21 | let replaces: { from: string; to: string }[] = translatePipes.map(pipe => {
22 | // get all pipes in one textBound source
23 | let replaceResult = pipe.source;
24 | pipe.pipeValues.forEach(pipeValue => {
25 | const text = getParamWithString(translates, pipeValue.value);
26 | if(text){
27 | const replace = `{{[\\s*]?'${pipeValue.value}'[\\s+]?\\|?\\s?\\w*\\s?}}`;
28 | replaceResult = replaceResult.replace(new RegExp(replace, 'g'), text.trim());
29 | } else {
30 | console.warn(`translate for pipe: ${pipeValue.value} not found`);
31 | }
32 | });
33 | if(replaceResult !== pipe.source){
34 | const translateVars = `${pipe.pipeValues.map(el => el.value).join(',')}`;
35 | const range = nodeToRange(pipe.parentNode);
36 | const from = getTextFromRange(pipe.parentNode.sourceSpan.start.file.content, range[0], range[1]);
37 |
38 | // add i18n attrubute
39 | let to = from.replace(`<${pipe.parentNode.name}`, `<${pipe.parentNode.name} i18n="${translateVars}"`);
40 | // replace content
41 | to = to.replace(pipe.source, replaceResult);
42 |
43 | return {
44 | from,
45 | to
46 | };
47 | } else {
48 | return null;
49 | }
50 | });
51 | let sourceCode = readFileSync(url).toString();
52 | replaces = replaces.filter(Boolean);
53 | sourceCode = replacizeText(sourceCode, replaces);
54 |
55 | if (sourceCode !== null) {
56 | writeFileSync(url, sourceCode);
57 | }
58 | } else {
59 | // Directive
60 | }
61 | } catch (e) {
62 | // Component
63 | // exception only componentß
64 | // console.log(url);
65 | console.error(e);
66 | }
67 | });
68 | }
69 |
70 | function getTranslatesSync(path: string): JSON {
71 | return JSON.parse(readFileSync(path).toString());
72 | }
73 |
74 | function getParamWithString(obj: JSON, value: string): string {
75 | const result = null;
76 | if (!value) return;
77 | const params = value.split('.');
78 | if (params.length === 1) {
79 | return obj[params[0]];
80 | } else {
81 | if (obj[params[0]]) {
82 | return getParamWithString(obj[params[0]], params.slice(1).join('.'));
83 | }
84 | }
85 | return result;
86 | }
87 | function replacizeText(sourceCode: string, replaces: { from: string; to: string }[]): string {
88 | let result = sourceCode;
89 | replaces.forEach(replacer => {
90 | result = result.replace(replacer.from, replacer.to);
91 | });
92 | return result;
93 |
94 | }
--------------------------------------------------------------------------------