author doubledream
 *  desc import * as fs from 'fs-extra' ; import * as _ from 'lodash' ; import * as prettier from 'prettier' ; import * as ts from 'typescript' ; import { readFile , writeFile } from './file' ; import { getLangData } from './getLangData' ; import { getProjectConfig , getLangDir } from '../utils' ; const CONFIG = getProjectConfig ( ) ; const srcLangDir = getLangDir ( CONFIG . srcLang ) ; function updateLangFiles ( keyValue , text , validateDuplicate ) { if ( ! _ . startsWith ( keyValue , 'I18N.' ) ) { return ; } const [ , filename , ... restPath ] = keyValue . split ( '.' ) ; const fullKey = restPath . join ( '.' ) ; const targetFilename = `${ srcLangDir }/${ filename }.ts` ; if ( ! fs . existsSync ( targetFilename ) ) { fs . writeFileSync ( targetFilename , generateNewLangFile ( fullKey , text ) ) ; addImportToMainLangFile ( filename ) ; console . log ( `成功新建语言文件 ${ targetFilename }` ) ; } else { const mainContent = getLangData ( targetFilename ) ; const obj = mainContent ; if ( Object . keys ( obj ) . length === 0 ) { console . log ( `${ filename } 解析失败，该文件包含的文案无法自动补全` ) ; } if ( validateDuplicate && _ . get ( obj , fullKey ) !== undefined ) { console . log ( `${ targetFilename } 中已存在 key 为 \`${ fullKey }\` 的翻译，请重新命名变量` ) ; throw new Error ( 'duplicate' ) ; } text = text . replace ( /\\n/gm , '\n' ) ; _ . set ( obj , fullKey , text ) ; fs . writeFileSync ( targetFilename , prettierFile ( `export default ${ JSON . stringify ( obj , null , 2 ) }` ) ) ; } } param fileContent function prettierFile ( fileContent ) { try { return prettier . format ( fileContent , { parser : 'typescript' , trailingComma : 'all' , singleQuote : true } ) ; } catch ( e ) { console . error ( `代码格式化报错！${ e . toString ( ) }\n代码为：${ fileContent }` ) ; return fileContent ; } } function generateNewLangFile ( key , value ) { const obj = _ . set ( { } , key , value ) ; return prettierFile ( `export default ${ JSON . stringify ( obj , null , 2 ) }` ) ; } function addImportToMainLangFile ( newFilename ) { let mainContent = '' ; if ( fs . existsSync ( `${ srcLangDir }/index.ts` ) ) { mainContent = fs . readFileSync ( `${ srcLangDir }/index.ts` , 'utf8' ) ; mainContent = mainContent . replace ( /^(\s*import.*?;)$/m , `$1\nimport ${ newFilename } from './${ newFilename }';` ) ; if ( /(}\);)/ . test ( mainContent ) ) { if ( /\,\n(}\);)/ . test ( mainContent ) ) { /** 最后一行包含,号 */ mainContent = mainContent . replace ( /(}\);)/ , `  ${ newFilename },\n$1` ) ; } else { /** 最后一行不包含,号 */ mainContent = mainContent . replace ( /\n(}\);)/ , `,\n  ${ newFilename },\n$1` ) ; } } if ( /(};)/ . test ( mainContent ) ) { if ( /\,\n(};)/ . test ( mainContent ) ) { /** 最后一行包含,号 */ mainContent = mainContent . replace ( /(};)/ , `  ${ newFilename },\n$1` ) ; } else { /** 最后一行不包含,号 */ mainContent = mainContent . replace ( /\n(};)/ , `,\n  ${ newFilename },\n$1` ) ; } } } else { mainContent = `import ${ newFilename } from './${ newFilename }';\n\nexport default Object.assign({}, {\n  ${ newFilename },\n});` ; } fs . writeFileSync ( `${ srcLangDir }/index.ts` , mainContent ) ; } param filePath function hasImportI18N ( filePath ) { const code = readFile ( filePath ) ; const ast = ts . createSourceFile ( '' , code , ts . ScriptTarget . ES2015 , true , ts . ScriptKind . TSX ) ; let hasImportI18N = false ; function visit ( node ) { if ( node . kind === ts . SyntaxKind . ImportDeclaration ) { const importClause = node . importClause ; if ( _ . get ( importClause , 'kind' ) === ts . SyntaxKind . ImportClause ) { if ( importClause . name ) { if ( importClause . name . escapedText === 'I18N' ) { hasImportI18N = true ; } } else { const namedBindings = importClause . namedBindings ; if ( namedBindings . kind === ts . SyntaxKind . NamedImports ) { namedBindings . elements . forEach ( element => { if ( element . kind === ts . SyntaxKind . ImportSpecifier && _ . get ( element , 'name.escapedText' ) === 'I18N' ) { hasImportI18N = true ; } } ) ; } if ( namedBindings . kind === ts . SyntaxKind . NamespaceImport ) { if ( _ . get ( namedBindings , 'name.escapedText' ) === 'I18N' ) { hasImportI18N = true ; } } } } } } ts . forEachChild ( ast , visit ) ; return hasImportI18N ; } param filePath function createImportI18N ( filePath ) { const code = readFile ( filePath ) ; const ast = ts . createSourceFile ( '' , code , ts . ScriptTarget . ES2015 , true , ts . ScriptKind . TSX ) ; const isTsFile = _ . endsWith ( filePath , '.ts' ) ; const isTsxFile = _ . endsWith ( filePath , '.tsx' ) ; const isVueFile = _ . endsWith ( filePath , '.vue' ) ; if ( isTsFile || isTsxFile ) { const importStatement = `${ CONFIG . importI18N }\n` ; const pos = ast . getStart ( ast , false ) ; const updateCode = code . slice ( 0 , pos ) + importStatement + code . slice ( pos ) ; return updateCode ; } else if ( isVueFile ) { const importStatement = `${ CONFIG . importI18N }\n` ; const updateCode = code . replace ( /<script>/g , `<script>\n${ importStatement }` ) ; return updateCode ; } } param filePath param arg param val param validateDuplicate function replaceAndUpdate ( filePath , arg , val , validateDuplicate ) { const code = readFile ( filePath ) ; const isHtmlFile = _ . endsWith ( filePath , '.html' ) ; const isVueFile = _ . endsWith ( filePath , '.vue' ) ; let newCode = code ; let finalReplaceText = arg . text ; const { start , end } = arg . range ; if ( arg . isString ) { const preTextStart = start - 1 ; const [ last2Char , last1Char ] = code . slice ( preTextStart , start + 1 ) . split ( '' ) ; let finalReplaceVal = val ; if ( last2Char === '=' ) { if ( isHtmlFile ) { finalReplaceVal = '{{' + val + '}}' ; } else if ( isVueFile ) { finalReplaceVal = '{{' + val + '}}' ; } else { finalReplaceVal = '{' + val + '}' ; } } if ( last1Char === '`' ) { const varInStr = arg . text . match ( /(\$\{[^\}]+?\})/g ) ; if ( varInStr ) { const kvPair = varInStr . map ( ( str , index ) => { return `val${ index + 1 }: ${ str . replace ( /^\${([^\}]+)\}$/ , '$1' ) }` ; } ) ; finalReplaceVal = `I18N.template(${ val }, { ${ kvPair . join ( ',\n' ) } })` ; varInStr . forEach ( ( str , index ) => { finalReplaceText = finalReplaceText . replace ( str , `{val${ index + 1 }}` ) ; } ) ; } } newCode = `${ code . slice ( 0 , start ) }${ finalReplaceVal }${ code . slice ( end ) }` ; } else { if ( isHtmlFile || isVueFile ) { newCode = `${ code . slice ( 0 , start ) }{{${ val }}}${ code . slice ( end ) }` ; } else { newCode = `${ code . slice ( 0 , start ) }{${ val }}${ code . slice ( end ) }` ; } } try { updateLangFiles ( val , finalReplaceText , validateDuplicate ) ; return writeFile ( filePath , newCode ) ; } catch ( e ) { return Promise . reject ( e . message ) ; } } export { replaceAndUpdate , hasImportI18N , createImportI18N } ;