├── .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 | screen migrate ngx-translate to Angular i18n 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 | } --------------------------------------------------------------------------------