npm插件-i18n-autoinsert

本文介绍了一款自定义的npm插件i18n-autoinsert,用于简化AngularJS项目中的国际化流程。该插件能自动从HTML文件中提取翻译键和值,并批量更新至翻译配置文件,提高开发效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.自定义插件的初衷

最近在开发angularjs项目中使用到了国际化,例如html里有段是这样的

<h1 class="page-header" [translate]="'company.labelCompanyProfile'">Company Profile</h1>

translate会去en.json里找company.labelCompanyProfile的翻译,如果没有就直接显示Company Profile。
开发过程需要在en.json的配置文件里先定义好如下

{
  "company": {
    "labelCompanyProfile": "Company Profile"  
  }
}

每次碰到翻译的地方都要重复这个步骤,很烦人。我的想法是只需要关心html的编写,因为编写过程翻译和key都有了,是否有个命令批量将key和对应翻译给加入到en.json里,找了下没有想关的插件,于是就想自己定义一个。本篇记录开发过程,以及遇到的问题和解决方案。

2. i18n-autoinsert的开发过程记录
2.1 npm 插件开发很简单,可以参看自定义一个npm插件
2.2 安装commander插件
npm install commander
2.3 主要逻辑

index.js

#!/usr/bin/env node
var program = require('commander');
var fs = require("fs");
var path = require('path');
program
    .version('1.0.6')
    .option('-t, --translationFile [value]', 'tranFile')
    .option('-h, --htmlFile [value]', 'htmlFile')
    .option('-p, --htmlPath [value]', 'htmlPath')
    .option('-s, --showlog')
    .parse(process.argv);

var projectPath=path.join(__dirname,'../../');
if (!program.translationFile) {
    console.log('need specify -t for translationFile');
    process.exit(1);
}
if (!program.htmlFile && !program.htmlPath) {
    console.log('need specify -h for htmlFile or -p for htmlPath');
    process.exit(1);
}
//找到翻译文件
var enFile=path.join(projectPath,program.translationFile);
var htmlFiles=[];
//找出所有的html文件放到数组htmlFiles里
if(program.htmlFile) {
    var htmlFile = path.join(projectPath, program.htmlFile);
    htmlFiles.push(htmlFile);
}
if(program.htmlPath){
    var htmlPath=path.join(projectPath, program.htmlPath);
    addTargetFile(htmlPath,htmlFiles);
}
//循环htmlFiles加入翻译
htmlFiles.forEach(function(f){
    autoInsertTranslation(enFile,f);
});
console.info("done!")

function addTargetFile(htmlPath,htmlFiles){
    var files =fs.readdirSync(htmlPath);
    files.forEach( function (file){
            var f = path.join(htmlPath, file);
            if(file.indexOf(".html")!=-1||file.indexOf(".HTML")!=-1){
                htmlFiles.push(f);
            }else{
                var stats=fs.statSync(f);
                if(stats.isDirectory()){
                    addTargetFile(f,htmlFiles);
                }
            }
        });
}

function autoInsertTranslation(enFile,htmlFile) {
    if(program.showlog) {
        console.log("translation file : "+ enFile);
        console.log("html file : "+ htmlFile);
    }
    var data = fs.readFileSync(enFile);
    var tranJson = JSON.parse('{}');
    if(data.toString().trim().length!=0){
         tranJson = JSON.parse(data);
    }
    var data = fs.readFileSync(htmlFile);
   
   //正则表达式的匹配,例如
   //<input class="xxx" [translate]="'abc'">helloworld<input>
   var matches = data.toString().match(/\[translate\]=\"\'.*\'\"[\s\S]*?</gmi);
    for (var i in matches) {
        var str = matches[i];
       if((/\'(.*)\'\"[\s\S]*>([\s\S]*)</).test(str)) {
           if(RegExp.$1)
            addToJson(tranJson, RegExp.$1.trim(), RegExp.$2.trim());
       }
    }
    fs.writeFileSync(enFile, JSON.stringify(tranJson, null, '  '));
}
function addToJson(json,key,value){
    var keys=key.split("\.");
    var tmpJson=json;
    for( var j=0;j<keys.length-1;j++){
        if(!tmpJson[keys[j]]) {
            tmpJson[keys[j]] = {};
        }
        tmpJson=tmpJson[keys[j]];
    }
    //tmpJson must be a json,not a string
    if(typeof tmpJson==="object"){
        if(!tmpJson[keys[keys.length-1]])
            tmpJson[keys[keys.length-1]]=value;
    }else{
        console.error("ERROR:can't auto insert ["+key+"], because ["+key.substr(0,key.lastIndexOf("\."))+"] is not a json object in translation file," +
            "please check!!!")
    }

}
2.4 使用

插件发布好后

  1. npm install -save-dev i18n-autoinsert
  2. 在 package.json 里 script 加入
"scripts": {
...
"i18n-autoinsert": "i18n-autoinsert -t /src/assets/i18n/en.json -p /src/app/page/company/approval-groups/ --showlog"
...
}

-t 指定翻译文件,必须指定

-p 指定html的路径,程序会递归所有的子文件夹并找出html文件,-p和-h必须指定一个

-h 指定单个html路径,-p和-h必须指定一个

–showlog 可有可无,打印日志

  1. 运行
npm run i18n-autoinsert

不用头疼在en.json里一个个去写翻译

github代码

3.问题记录

npm install i i8n-autoinsert
生成出来的npm_module/.bin/i18n-autoinsert.cmd

@"%~dp0\..\i18n-autoinsert\index.js"   %*

而不是

@IF EXIST "%~dp0\node.exe" (
  "%~dp0\node.exe"  "%~dp0\..\i18n-autoinsert\index.js" %*
) ELSE (
  @SETLOCAL
  @SET PATHEXT=%PATHEXT:;.JS;=;%
  node  "%~dp0\..\i18n-autoinsert\index.js" %*
)

解决方式,需要在index.js加入

#!/usr/bin/env node
  1. 正则表达式匹配问题

碰到html是

<ng-template dbsTabTitle><span [translate]="'company.labelApprovalGroups'">Approval Groups</span></ng-template>

使用

var matches=data.toString().match(/\[translate\]=\"\'.*\'\">.*</gmi)

匹配到的是

[translate]="'company.labelApprovalGroups'">Approval Groups</span><

而不是想要的结果

[translate]="'company.labelApprovalGroups'">Approval Groups<

解决方式 : 使用非贪婪匹配模式,如下

/\[translate\]=\"\'.*\'\">.*?</gmi

而不是

/\[translate\]=\"\'.*\'\">.*<?/gmi
  1. 相对路径的获取
    在代码里要获取到index.js所在的路径可以使用
    __dirname

4.使用过程中发现下面的匹配不到

<h2 class="form-section-header" style="margin-top: 0px" [translate]="'company-group.create.step1.group.detail'">
        Step 1: Approval group details</h2>

原来的是

  var matches = data.toString().match(/\[translate\]=\"\'.*\'\">.*?</gmi);

解决方式

  var matches = data.toString().match(/\[translate\]=\"\'.*\'\">[\s\S]*?</gmi);

.不包含换行,[\s\S]则包含所有的字符

5.使用过程中发现下面的匹配不到,
>前多了个空格

<button (click)="createNewGroup()" [translate]="'labelCreateAnotherGroup'" >Create another Group</button>

<button  [translate]="'labelCreateAnotherGroup'" (click)="createNewGroup()" >Create another Group</button>

解决方式

 var matches = data.toString().match(/\[translate\]=\"\'.*\'\"[\s\S]*?</gmi);
    for (var i in matches) {
        var str = matches[i];
       if((/\'(.*)\'\"[\s\S]*>([\s\S]*)</).test(str)) {
           if(RegExp.$1)
            addToJson(tranJson, RegExp.$1.trim(), RegExp.$2.trim());
       }
    }
<script setup> // import { h } from 'snabbdom' import { onBeforeUnmount, ref, shallowRef } from 'vue' // import { IButtonMenu } from '@wangeditor/core' import { Boot } from '@wangeditor/editor' import { Editor, Toolbar } from '@wangeditor/editor-for-vue' // 测试:第三方插件 // import withCtrlEnter from '@wangeditor/plugin-ctrl-enter' // Boot.registerPlugin(withCtrlEnter) // // 测试:多语言 // i18nChangeLanguage('en') // // 测试:注册 renderElem // function renderElemFn(elem, children) { // const vnode = h('div', {}, children) // type: 'paragraph' 节点,即渲染为 <p> 标签 // return vnode // } // const renderElemConf = { // type: 'paragraph', // 节点 type ,重要!!! // renderElem: renderElemFn, // } // Boot.registerRenderElem(renderElemConf) // // 测试:注册插件 // function withCtrlEnter(editor) { // const { insertBreak } = editor // setTimeout(() => { // // beforeInput 事件不能识别 ctrl+enter ,所以自己绑定 DOM 事件 // const { $textArea } = DomEditor.getTextarea(newEditor) // $textArea.on('keydown', e => { // const isCtrl = e.ctrlKey || e.metaKey // if (e.keyCode === 13 && isCtrl) { // // ctrl+enter 触发换行 // editor.insertBreak() // } // }) // }) // const newEditor = editor // newEditor.insertBreak = () => { // const e = window.event // const isCtrl = e.ctrlKey || e.metaKey // // 只有 ctrl 才能换行 // if (isCtrl) { // insertBreak() // } // } // return newEditor // } // Boot.registerPlugin(withCtrlEnter) // 测试:注册 button menu // class MyButtonMenu { // constructor() { // // this.title = '', // this.tag = 'button' // } // getValue() { return '' } // isActive() { return false } // isDisabled() { return false } // exec(editor) { // console.log(editor) // // alert('menu1 exec') // } // } // const menuConf = { // key: 'my-menu-1', // menu key ,唯一。注册之后,需通过 toolbarKeys 配置到工具栏 // factory() { // return new MyButtonMenu() // }, // } // // 检查菜单是否已注册,避免重复注册 // if (!Boot.getMenuConfig('my-menu-1')) { // Boot.registerMenu(menuConf); // console.log('菜单注册成功'); // } else { // console.log('菜单已存在,跳过注册'); // } // console.log(1111111111) // 移到组件最外层,确保模块加载时只执行一次 class MyButtonMenu { constructor() { this.title = ''; // 必须有标题,否则按钮不显示 this.tag = 'button'; } getValue() { return ''; } isActive() { return false; } isDisabled() { return false; } exec(editor) { // console.log('自定义菜单点击', editor); // editor.insertText('这是自定义菜单插入的内容'); // 示例功能 } } const menuConf = { key: 'my-menu-1', factory() { return new MyButtonMenu(); }, }; // 核心:用try-catch捕获重复注册错误 try { Boot.registerMenu(menuConf); console.log('菜单注册成功'); } catch (err) { if (err.message.includes('Duplicated key')) { console.log('菜单已注册,跳过重复注册'); } else { console.error('菜单注册失败:', err); } } // 编辑器实例,必须用 shallowRef ,重要! const editorRef = shallowRef() // 内容 HTML const valueHtml = ref('<p>hello world</p>') // 编辑器配置 const editorConfig = { placeholder: '请输入内容...', MENU_CONF: { insertImage: { checkImage(src) { console.log('image src', src) if (src.indexOf("http") !== 0) { return "图片网址必须以 http/https 开头"; } return true; }, }, } } // 工具栏配置 const toolbarConfig = { // toolbarKeys: ['headerSelect', 'bold', 'my-menu-1'], // excludeKeys: [], insertKeys: { index: 0, keys: 'my-menu-1' } } // 编辑器回调函数 const handleCreated = (editor) => { console.log("created", editor); editorRef.value = editor // 记录 editor 实例,重要! // window.editor = editor // 临时测试使用,用完删除 } const handleChange = (editor) => { console.log("change:", editor.children); } const handleDestroyed = (editor) => { console.log('destroyed', editor) } const handleFocus = (editor) => { console.log('focus', editor) } const handleBlur = (editor) => { console.log('blur', editor) } const customAlert = (info, type) => { alert(`【自定义提示】${type} - ${info}`) } const customPaste = (editor, event, callback) => { console.log('ClipboardEvent 粘贴事件对象', event) // 自定义插入内容 editor.insertText('xxx') // 返回值(注意,vue 事件的返回值,不能用 return) callback(false) // 返回 false ,阻止默认粘贴行为 // callback(true) // 返回 true ,继续默认的粘贴行为 } // 及时销毁编辑器 onBeforeUnmount(() => { const editor = editorRef.value if (editor == null) return // 销毁,并移除 editor editor.destroy() }) const getHtml = () => { const editor = editorRef.value if (editor == null) return console.log(editor.getHtml()) } </script> <template> <!-- <div>--> <!-- <button @click="getHtml">获取 html</button>--> <!-- </div>--> <!-- <div style="border: 1px solid #ccc">--> <!-- <!– 工具栏 –>--> <!-- <Toolbar--> <!-- :editor="editorRef"--> <!-- :defaultConfig="toolbarConfig"--> <!-- style="border-bottom: 1px solid #ccc"--> <!-- />--> <!-- <!– 编辑器 –>--> <!-- <Editor--> <!-- v-model="valueHtml"--> <!-- :defaultConfig="editorConfig"--> <!-- @onChange="handleChange"--> <!-- @onCreated="handleCreated"--> <!-- @onDestroyed="handleDestroyed"--> <!-- @onFocus="handleFocus"--> <!-- @onBlur="handleBlur"--> <!-- @customAlert="customAlert"--> <!-- @customPaste="customPaste"--> <!-- style="height: 500px"--> <!-- />--> <!-- </div>--> <div class="editor-container"> <!-- 添加容器类名 --> <!-- 工具栏 --> <Toolbar :editor="editorRef" :defaultConfig="toolbarConfig" style="border-bottom: 1px solid #e5e7eb" /> <!-- 编辑器 --> <Editor v-model="valueHtml" :defaultConfig="editorConfig" @onChange="handleChange" @onCreated="handleCreated" @onDestroyed="handleDestroyed" @onFocus="handleFocus" @onBlur="handleBlur" @customAlert="customAlert" @customPaste="customPaste" /> </div> </template> <style src="@wangeditor/editor/dist/css/style.css"> /* 编辑器整体容器 */ .editor-container { border: 1px solid #e5e7eb; /* 浅灰色边框 */ border-radius: 8px; /* 圆角 */ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); /* 轻微阴影 */ overflow: hidden; /* 防止内部元素溢出 */ transition: box-shadow 0.2s ease; /* 阴影过渡效果 */ } .editor-container:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); /* hover时增强阴影 */ } /* 工具栏样式 */ .w-e-toolbar { background-color: #f9fafb !important; /* 工具栏背景色 */ border-bottom: 1px solid #e5e7eb !important; /* 底部边框 */ padding: 8px 12px !important; /* 内边距 */ } /* 工具栏按钮通用样式 */ .w-e-toolbar .w-e-menu { margin: 0 3px !important; /* 按钮间距 */ border-radius: 4px !important; /* 按钮圆角 */ transition: all 0.2s ease !important; /* 过渡效果 */ } .w-e-toolbar .w-e-menu:hover { background-color: #eef2f5 !important; /* 悬停背景色 */ transform: translateY(-1px) !important; /* 轻微上浮效果 */ } /* 自定义菜单按钮样式(my-menu-1) */ .w-e-toolbar [data-key="my-menu-1"] { background-color: #4f46e5 !important; /* 紫色背景(突出自定义按钮) */ color: white !important; /* 白色文字 */ border: none !important; } .w-e-toolbar [data-key="my-menu-1"]:hover { background-color: #4338ca !important; /* 深色hover效果 */ } /* 编辑区域样式 */ .w-e-text-container { height: 500px !important; /* 固定高度 */ padding: 16px !important; /* 内边距 */ background-color: white !important; /* 白色背景 */ } /* 编辑区域内容样式 */ .w-e-text-container p { margin: 0 0 12px 0 !important; /* 段落间距 */ line-height: 1.8 !important; /* 行高(提升可读性) */ font-size: 14px !important; /* 基础字体大小 */ color: #1f2937 !important; /* 文字颜色 */ } /* 编辑区域聚焦样式 */ .w-e-text-container:focus-within { outline: 2px solid rgba(79, 70, 229, 0.2) !important; /* 聚焦时边框高亮 */ } /* 工具栏分割线样式 */ .w-e-toolbar .w-e-separator { background-color: #e5e7eb !important; /* 分割线颜色 */ margin: 0 6px !important; /* 分割线间距 */ } /* 响应式调整(小屏幕适配) */ @media (max-width: 768px) { .editor-container { border-radius: 4px; /* 小屏幕减小圆角 */ } .w-e-toolbar { padding: 4px 8px !important; /* 小屏幕减小内边距 */ } .w-e-text-container { height: 400px !important; /* 小屏幕减小编辑区高度 */ padding: 12px !important; } } </style>改一改让工具栏悬浮在上面,不会随着页面移动,不论页面滚轮怎么移动都保持在页面上方,右侧添加滚轮使页面可以移动,工具栏上面再搞一个头部,最左边是返回键,中间是AI笔记的标题,最右边是一个头像,点击头像会跳转到个人用户管理界面,头像左边有一个历史记录按钮,点击后会显示一个侧边栏,里面有AI笔记历史,历史记录先留一个按钮就行,头像也先留一个位置就行
最新发布
07-26
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值