├── src ├── chrome │ ├── inject.js │ ├── background │ │ ├── index.js │ │ └── hmr.js │ └── manifest.json ├── assets │ ├── logo.png │ └── img │ │ └── bt.svg ├── config.js ├── content │ ├── components │ │ ├── 0aN8S7.png │ │ ├── lang-inspector │ │ │ ├── config.js │ │ │ ├── printer.js │ │ │ ├── langMap.js │ │ │ └── index.js │ │ ├── check.js │ │ ├── sheetToCode.js │ │ └── App.vue │ └── index.js ├── mixins │ └── store.js ├── popup │ ├── index.js │ ├── index.html │ └── components │ │ └── App.vue ├── store.js ├── scripts │ └── crx.js └── utils │ └── insert.js ├── .browserslistrc ├── babel.config.js ├── .gitattributes ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── .gitignore ├── .eslintrc.js ├── README.md ├── LICENSE ├── package.json └── vue.config.js /src/chrome/inject.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/app"] 3 | }; 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rhan2020/vue-chrome-extension/master/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rhan2020/vue-chrome-extension/master/src/assets/logo.png -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | /** 3 | * 基地址 4 | */ 5 | baseUrl: "https://*" // 线上 6 | }; 7 | -------------------------------------------------------------------------------- /src/chrome/background/index.js: -------------------------------------------------------------------------------- 1 | import installReload from "./hmr"; 2 | 3 | // 安装热刷新功能 4 | installReload(); 5 | -------------------------------------------------------------------------------- /src/content/components/0aN8S7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rhan2020/vue-chrome-extension/master/src/content/components/0aN8S7.png -------------------------------------------------------------------------------- /src/mixins/store.js: -------------------------------------------------------------------------------- 1 | import store from "@/store"; 2 | 3 | export default { 4 | beforeCreate() { 5 | this.$store = store; 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /src/popup/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./components/App.vue"; 3 | 4 | new Vue({ 5 | el: "#app", 6 | render: h => h(App) 7 | }); 8 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex from "vuex"; 3 | 4 | Vue.use(Vuex); 5 | 6 | export default new Vuex.Store({ 7 | state: {}, 8 | mutations: {}, 9 | actions: {} 10 | }); 11 | -------------------------------------------------------------------------------- /src/content/components/lang-inspector/config.js: -------------------------------------------------------------------------------- 1 | // lilejia@bigo.sg 2 | 3 | module.exports = { 4 | // UI以英文为准; 5 | uiLang: "en", 6 | // 短文本比较,标志过长短标题,前端处理短文本时可能没考虑多语言溢出; 7 | shortTextCompare: { 8 | which: [10, 20], // 短文本长度范围 9 | scale: 1.8 // 超过2的字符长度就提示 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | *.crx 23 | *.pem 24 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | globals: { 4 | chrome: true, 5 | }, 6 | env: { 7 | node: true 8 | }, 9 | extends: ["plugin:vue/essential", "@vue/prettier"], 10 | rules: { 11 | "no-console": process.env.NODE_ENV === "production" ? "error" : "off", 12 | "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off" 13 | }, 14 | parserOptions: { 15 | parser: "babel-eslint" 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-chrome-extension 2 | 3 | ## 模块安装 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### 运行调试 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### 打包编译 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### 打包编译crx文件 19 | ``` 20 | npm run build:crx 21 | ``` 22 | 23 | ### 调试插件 24 | - 使用`npm run serve`运行项目 25 | - 打开谷歌浏览器拓展程序页面 26 | - 打开开发者模式,选择`加载已解压的扩展程序` 27 | - 导入项目中的`dist`文件夹即可到目标页查看插件状态 28 | 29 | ### Customize configuration 30 | See [Configuration Reference](https://cli.vuejs.org/config/). 31 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-chrome-extension 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/popup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-chrome-extension 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/content/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./components/App.vue"; 3 | import insert from "@/utils/insert"; 4 | import stroe from "@/mixins/store"; 5 | 6 | // 注入js到页面 7 | injectJS(); 8 | 9 | Vue.mixin(stroe); 10 | 11 | // 插入组件到页面中 12 | insert(App); 13 | 14 | function injectJS() { 15 | document.addEventListener("readystatechange", () => { 16 | const injectPath = "inject.js"; 17 | const temp = document.createElement("script"); 18 | 19 | temp.setAttribute("type", "text/javascript"); 20 | // 获得的地址类似:chrome-extension://ihcokhadfjfchaeagdoclpnjdiokfakg/js/inject.js 21 | temp.src = chrome.extension.getURL(injectPath); 22 | document.body.appendChild(temp); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /src/scripts/crx.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const manifest = require(path.resolve(__dirname, "../chrome/manifest.json")); 4 | const ChromeExtension = require("crx"); 5 | const crxName = `${manifest.name}-v${manifest.version}.crx`; 6 | const crx = new ChromeExtension({ 7 | privateKey: fs.readFileSync(path.resolve(__dirname, "../../dist.pem")) 8 | }); 9 | 10 | crx 11 | .load(path.resolve(__dirname, "../../dist")) 12 | .then(crx => crx.pack()) 13 | .then(crxBuffer => { 14 | fs.writeFile(crxName, crxBuffer, err => 15 | err 16 | ? console.error(err) 17 | : console.log(`>>>>>>> ${crxName} <<<<<<< 已打包完成`) 18 | ); 19 | }) 20 | .catch(err => { 21 | console.error(err); 22 | }); 23 | -------------------------------------------------------------------------------- /src/utils/insert.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | 3 | /** 4 | * 将vue组件插入到页面上 5 | * @param {object} component 组件 6 | * @param {string} insertSelector 插入选择器 7 | */ 8 | function insert(component, insertSelector = "body") { 9 | insertDomFactory(component, insertSelector); 10 | } 11 | 12 | // 根据组件生成vue实例 13 | // 生成插入的dom 14 | function insertDomFactory(component, insertSelector) { 15 | const vm = generateVueInstance(component); 16 | 17 | generateInsertDom(insertSelector, vm); 18 | } 19 | 20 | // 遍历待插入的dom 21 | // 插入新创建的元素 22 | // 将vue实例挂载到新创建的元素上 23 | function generateInsertDom(insertSelector, vm) { 24 | const dom = document.querySelectorAll(insertSelector); 25 | dom.forEach(item => { 26 | const insert = document.createElement("div"); 27 | insert.id = "insert-item"; 28 | item.appendChild(insert); 29 | vm.$mount("#insert-item"); 30 | }); 31 | } 32 | 33 | function generateVueInstance(component) { 34 | const insertCon = Vue.extend(component); 35 | 36 | return new insertCon(); 37 | } 38 | 39 | export default insert; 40 | -------------------------------------------------------------------------------- /src/assets/img/bt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "chrome-sheetToCode", 4 | "description": "transform sheet to code", 5 | "version": "1.0.0", 6 | "browser_action": { 7 | "default_title": "chrome-sheetToCode", 8 | "default_icon": "assets/logo.png", 9 | "default_popup": "popup.html" 10 | }, 11 | "permissions": [ 12 | "webRequestBlocking", 13 | "notifications", 14 | "tabs", 15 | "webRequest", 16 | "http://*/", 17 | "https://*/", 18 | "", 19 | "storage", 20 | "activeTab" 21 | ], 22 | "background": { 23 | "scripts": ["js/background.js"] 24 | }, 25 | "icons": { 26 | "16": "assets/logo.png", 27 | "48": "assets/logo.png", 28 | "128": "assets/logo.png" 29 | }, 30 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", 31 | "content_scripts": [ 32 | { 33 | "matches": [ 34 | "https://docs.google.com/*" 35 | ], 36 | "css": [ 37 | "css/content.css" 38 | ], 39 | "js": [ 40 | "js/content.js" 41 | ], 42 | "run_at": "document_end" 43 | } 44 | ], 45 | "web_accessible_resources": ["fonts/*", "inject.js"] 46 | } -------------------------------------------------------------------------------- /src/popup/components/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 40 | 41 | 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jacano 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-chrome-extension", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service build --watch", 7 | "crx": "node ./src/scripts/crx.js", 8 | "build:crx": "npm run build && node src/scripts/crx.js", 9 | "build": "vue-cli-service build", 10 | "build:zip": "vue-cli-service build --zip", 11 | "lint": "vue-cli-service lint" 12 | }, 13 | "dependencies": { 14 | "axios": "^0.21.1", 15 | "core-js": "^2.6.5", 16 | "vue": "^2.6.10", 17 | "papaparse": "^5.1.0", 18 | "vuex": "^3.0.1" 19 | }, 20 | "devDependencies": { 21 | "@vue/cli-plugin-babel": "^3.12.0", 22 | "@vue/cli-plugin-eslint": "^3.12.0", 23 | "@vue/cli-service": "^3.12.0", 24 | "@vue/eslint-config-prettier": "^5.0.0", 25 | "babel-eslint": "^10.0.1", 26 | "copy-webpack-plugin": "^6.0.2", 27 | "crx": "^5.0.1", 28 | "eslint": "^5.16.0", 29 | "eslint-plugin-prettier": "^3.1.0", 30 | "eslint-plugin-vue": "^5.0.0", 31 | "jszip": "^3.7.1", 32 | "lint-staged": "^8.1.5", 33 | "node-sass": "^4.12.0", 34 | "prettier": "^1.18.2", 35 | "sass-loader": "^8.0.0", 36 | "vue-template-compiler": "^2.6.10", 37 | "zip-webpack-plugin": "^3.0.0" 38 | }, 39 | "gitHooks": { 40 | "pre-commit": "lint-staged" 41 | }, 42 | "lint-staged": { 43 | "*.{js,vue}": [ 44 | "vue-cli-service lint", 45 | "git add" 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/content/components/lang-inspector/printer.js: -------------------------------------------------------------------------------- 1 | // lilejia@bigo.sg 2 | 3 | require("console.table"); 4 | // 字色编号:30黑,31红,32绿,33黄,34蓝,35紫,36深绿,37白色 5 | // 背景编号:40黑,41红,42绿,43黄,44蓝,45紫,46深绿,47白色 6 | const chalk = { 7 | red(str) { 8 | return `\x1b[31m${str}\x1b[0m`; 9 | }, 10 | green(str) { 11 | return `\x1b[32m${str}\x1b[0m`; 12 | }, 13 | yellow(str) { 14 | return `\x1b[33m${str}\x1b[0m`; 15 | }, 16 | cyan(str) { 17 | return `\x1b[35m${str}\x1b[0m`; 18 | } 19 | }; 20 | 21 | module.exports = { 22 | printReport(report) { 23 | const { reportList, warnList, status } = report; 24 | reportList.forEach(it => { 25 | console.log(chalk.green(`【多语言】:${it.msg}\n`)); 26 | }); 27 | if (status) { 28 | console.log(chalk.green("【多语言】没有发现文档的任何错误!\n")); 29 | return; 30 | } 31 | console.log(chalk.green("【多语言】发现问题:\n")); 32 | const errorList = (warnList || []) 33 | .filter(it => it.level === "error") 34 | .map(it => ({ 35 | Type: chalk.red("Error"), 36 | Pos: chalk.yellow(it.pos), 37 | message: chalk.red(it.msg) 38 | })); 39 | const warnsList = (warnList || []) 40 | .filter(it => it.level === "warn") 41 | .map(it => ({ 42 | Type: chalk.green("Warn"), 43 | Pos: chalk.yellow(it.pos), 44 | message: chalk.cyan(it.msg) 45 | })); 46 | console.table([...errorList, ...warnsList]); 47 | }, 48 | printSheetList(sheets, choose) { 49 | sheets = sheets || []; 50 | sheets[choose] = chalk.yellow(`${sheets[choose]} (选中)`); 51 | console.log( 52 | chalk.green("【多语言】当前链接检测到[") + 53 | chalk.yellow(sheets.length) + 54 | chalk.green("]张表:\n") 55 | ); 56 | console.log(chalk.green(" [ ") + sheets.join("、") + chalk.green(" ]\n")); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /src/chrome/background/hmr.js: -------------------------------------------------------------------------------- 1 | // 加载文件 2 | 3 | const filesInDirectory = dir => 4 | new Promise(resolve => 5 | dir.createReader().readEntries(entries => { 6 | Promise.all( 7 | entries 8 | .filter(e => e.name[0] !== ".") 9 | .map(e => 10 | e.isDirectory 11 | ? filesInDirectory(e) 12 | : new Promise(resolve => e.file(resolve)) 13 | ) 14 | ) 15 | .then(files => [].concat(...files)) 16 | .then(resolve); 17 | }) 18 | ); 19 | 20 | // 遍历插件目录,读取文件信息,组合文件名称和修改时间成数据 21 | const timestampForFilesInDirectory = dir => 22 | filesInDirectory(dir).then(files => 23 | files.map(f => f.name + f.lastModifiedDate).join() 24 | ); 25 | 26 | // 刷新当前活动页 27 | const reload = () => { 28 | window.chrome.tabs.query( 29 | { 30 | active: true, 31 | currentWindow: true 32 | }, 33 | tabs => { 34 | // NB: see https://github.com/xpl/crx-hotreload/issues/5 35 | if (tabs[0]) { 36 | window.chrome.tabs.reload(tabs[0].id); 37 | } 38 | // 强制刷新页面 39 | window.chrome.runtime.reload(); 40 | } 41 | ); 42 | }; 43 | 44 | // 观察文件改动 45 | const watchChanges = (dir, lastTimestamp) => { 46 | timestampForFilesInDirectory(dir).then(timestamp => { 47 | // 文件没有改动则循环监听watchChanges方法 48 | if (!lastTimestamp || lastTimestamp === timestamp) { 49 | setTimeout(() => watchChanges(dir, timestamp), 1000); // retry after 1s 50 | } else { 51 | // 强制刷新页面 52 | reload(); 53 | } 54 | }); 55 | }; 56 | 57 | const installReload = () => { 58 | window.chrome.management.getSelf(self => { 59 | if (self.installType === "development") { 60 | // 获取插件目录,监听文件变化 61 | window.chrome.runtime.getPackageDirectoryEntry(dir => watchChanges(dir)); 62 | } 63 | }); 64 | }; 65 | 66 | export default installReload; 67 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 2 | const ZipWebpackPlugin = require("zip-webpack-plugin"); 3 | const path = require("path"); 4 | 5 | // 只需要复制的文件 6 | const copyFiles = [ 7 | { 8 | from: path.resolve("src/chrome/manifest.json"), 9 | to: `${path.resolve("dist")}/manifest.json` 10 | }, 11 | { 12 | from: path.resolve("src/assets"), 13 | to: path.resolve("dist/assets") 14 | }, 15 | { 16 | from: path.resolve("src/chrome/inject.js"), 17 | to: path.resolve("dist") 18 | } 19 | ]; 20 | 21 | const plugins = [ 22 | new CopyWebpackPlugin({ 23 | patterns: copyFiles 24 | }) 25 | ]; 26 | // 生产环境打包dist为zip 27 | if (process.argv.includes("--zip")) { 28 | plugins.push( 29 | new ZipWebpackPlugin({ 30 | path: path.resolve("./"), 31 | filename: "dist.zip" 32 | }) 33 | ); 34 | } 35 | 36 | // 配置页面 37 | const pages = {}; 38 | /** 39 | * popup 和 devtool 都需要html文件 40 | * 因此 chromeName 还可以添加devtool 41 | */ 42 | const chromeName = ["popup"]; 43 | 44 | chromeName.forEach(name => { 45 | pages[name] = { 46 | entry: `src/${name}/index.js`, 47 | template: `src/${name}/index.html`, 48 | filename: `${name}.html` 49 | }; 50 | }); 51 | 52 | module.exports = { 53 | pages, 54 | // 生产环境是否生成 sourceMap 文件 55 | productionSourceMap: false, 56 | 57 | configureWebpack: { 58 | // 多入口打包 59 | entry: { 60 | content: "./src/content/index.js", 61 | background: "./src/chrome/background/index.js" 62 | }, 63 | output: { 64 | filename: "js/[name].js" 65 | }, 66 | plugins 67 | }, 68 | css: { 69 | extract: { 70 | filename: "css/[name].css" 71 | } 72 | }, 73 | 74 | chainWebpack: config => { 75 | config.resolve.alias.set("@", path.resolve("src")); 76 | // 处理字体文件名,去除hash值 77 | const fontsRule = config.module.rule("fonts"); 78 | 79 | // 清除已有的所有 loader。 80 | // 如果你不这样做,接下来的 loader 会附加在该规则现有的 loader 之后。 81 | fontsRule.uses.clear(); 82 | fontsRule 83 | .test(/\.(woff2?|eot|ttf|otf)(\?.*)?$/i) 84 | .use("url") 85 | .loader("url-loader") 86 | .options({ 87 | limit: 1000, 88 | name: "fonts/[name].[ext]" 89 | }); 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /src/content/components/lang-inspector/langMap.js: -------------------------------------------------------------------------------- 1 | // lilejia@bigo.sg 2 | 3 | module.exports = { 4 | cn: "cn,zh,zh-hans,zh-cn,zh-hans-cn,zh-sg,zh-hans-sg", 5 | // 简体中文 6 | tw: "tw,zh-hant,zh-hk,zh-mo,zh-tw,zh-hant-hk,zh-hant-mo,zh-hant-tw", 7 | // 繁体中文 8 | en: 9 | "en,en-au,en-bz,en-ca,en-cb,en-ie,en-jm,en-nz,en-ph,en-za,en-tt,en-gb,en-us,en-zw,en-sg", 10 | // 英语 11 | th: "th,th-th", 12 | // 傣语 13 | vi: "vn,vi-vn,vi,vn-vn", 14 | // 越南语 15 | ru: "ru,ru-ru,ru-mo", 16 | // 俄语 17 | id: "id,id-id,in-id", 18 | // 印尼语 19 | ko: "ko,ko-kr", 20 | // 朝鲜语 21 | hi: "in,hi,hi-in", 22 | // 印地语 23 | ar: 24 | "ar,ar-er,ar-sa,ar-eg,ar-dz,ar-tn,ar-ye,ar-jo,ar-kw,ar-bh,ar-iq,ar-ly,ar-ma,ar-om,ar-sy,ar-lb,ar-ae,ar-qa,ar-ss,ar-il", 25 | // 阿拉伯语 26 | af: "af,af-za", 27 | // 南非语 28 | tr: "tr,tr-tr", 29 | // 土耳其语 30 | es: 31 | "es,es-ar,es-bo,es-cl,es-co,es-cr,es-do,es-ec,es-es,es-gt,es-hn,es-mx,es-ni,es-pa,es-pe,es-pr,es-py,es-sv,es-uy,es-ve,es-xl", 32 | // 西班牙语 33 | ms: "ms,ms-bn,ms-my,my", 34 | // 马来语 35 | pt: "pt,pt-pt,pt-br", 36 | // 葡萄牙语 37 | ja: "ja,ja-jp,ja-ja,jp,jp-jp", 38 | // 日语 39 | ur: "ur,ur-pk", 40 | // 乌尔都语 41 | de: "de,de-at,de-ch,de-de,de-li,de-lu", 42 | // 德语 43 | ne: "ne,ne-np", 44 | // 尼泊尔语 45 | bn: "bn,bn-bd,bn-in", 46 | // 孟加拉语 47 | gu: "gu,gu-in", 48 | // 古吉拉特语 49 | kn: "kn,kn-in", 50 | // 埃纳德语 51 | mr: "mr,mr-in", 52 | // 马拉地语 53 | pa: "pa,pa-in", 54 | // 旁遮普语 55 | ta: "ta,ta-in", 56 | // 泰米尔语 57 | te: "te,te-in", 58 | // 泰卢固语 59 | fil: "fil,fil-ph", 60 | // 菲律宾语 61 | it: "it,it-ch,it-it", 62 | // 意大利语 63 | uk: "uk,uk-ua", 64 | // 乌克兰语 65 | be: "be,be-by", 66 | // 白俄罗斯语 67 | kk: "kk,kk-kz", 68 | // 哈萨克语 69 | fa: "fa,fa-ir", 70 | // 波斯语 71 | da: "da,da-dk", 72 | // 丹麦语 73 | ml: "ml,ml-in", 74 | // 马拉雅拉姆语 75 | or: "or,or-in", 76 | // 奥里雅语 77 | as: "as,as-in", 78 | // 阿萨姆语 79 | ka: "ka,ka-ge", 80 | // 格鲁吉亚语 81 | sa: "sa,sa-in", 82 | // 梵语 83 | uz: "uz,uz-latn,uz-latn-uz,uz-cyrl,uz-cyrl-uz", 84 | // 乌兹别克语 85 | hne: "hne", 86 | // 恰蒂斯加尔语 87 | jv: "jv", 88 | // 爪哇语 89 | raj: "raj", 90 | // 拉贾斯坦语 91 | km: "km,kh,km-kh,khm", 92 | // 高棉语 93 | fr: "fr,fr-be,fr-ca,fr-fr,fr-lu,fr-mc,fr-ch,fra,fre", 94 | //法语 95 | si: "si,sin" //僧伽罗语 96 | /** 97 | * @description ISO 3166-1 国家码映射(facebook标准) 98 | * @see https://zh.wikipedia.org/wiki/ISO_3166-1?fbclid=IwAR1l_8YPkeiFg8S5-n1_iiBq0K4gIBJyBBKGOcCfn16qF5I_hdzDRXEH2nU 99 | */ 100 | }; 101 | -------------------------------------------------------------------------------- /src/content/components/check.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import inspect from './lang-inspector' 4 | import Papa from 'papaparse' 5 | 6 | /** 7 | * 解析google表格链接 8 | */ 9 | function parseGoogleDocUrl(url) { 10 | var urlParts = url.split('/') 11 | var hashPart = urlParts.pop() 12 | var docUrl = urlParts.join('/') 13 | var id = urlParts.pop() 14 | var hash = hashPart.split('#').pop() 15 | var downurl = docUrl + '/export?format=csv&id=' + id + '&' + hash 16 | return downurl 17 | } 18 | 19 | function Ajax(url, method, data) { 20 | let promise = new Promise((resolve, reject) => { 21 | var xhr = new XMLHttpRequest(); 22 | xhr.onreadystatechange = function() { 23 | if (xhr.readyState === 4) { 24 | if (xhr.status === 200) { 25 | resolve(xhr.response); 26 | } else { 27 | reject(new Error("error")); 28 | } 29 | } 30 | } 31 | if (method.toUpperCase() === "GET") { 32 | let paramsList = []; 33 | for (let key in data) { 34 | paramsList.push(key + "=" + data[key]); 35 | } 36 | let params = paramsList.join("&"); 37 | url = url + params; 38 | xhr.open("get", url, false); 39 | xhr.send(); 40 | } 41 | else if (method.toUpperCase() === "POST") { 42 | xhr.open("post", url, false); 43 | xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); 44 | xhr.send(data); 45 | } 46 | }) 47 | return promise; 48 | } 49 | 50 | export function check(url) { 51 | return Ajax(parseGoogleDocUrl(url), 'GET').then(res => { 52 | if (!res) { 53 | return res 54 | } 55 | const parseObj = Papa.parse(res) 56 | console.log(res, parseObj, 111); 57 | let arr = parseObj && parseObj.data || [] 58 | let deleteCount = 0 59 | for (let i = arr[0].length - 1; i >= 0; i--) { 60 | let empty = true 61 | for (let j = 0; j < arr.length; j++) { 62 | if (arr[j][i].trim()) { 63 | empty = false 64 | break 65 | } 66 | } 67 | if (empty) { 68 | deleteCount++ 69 | } else { 70 | break 71 | } 72 | } 73 | if (deleteCount) { 74 | arr = arr.map(it => it.slice(0, -deleteCount)) 75 | } 76 | console.log(arr) 77 | const toArr = [] 78 | const h = arr.length || 0 79 | const w = arr[0] && arr[0].length || 0 80 | 81 | for (let i = 0; i < h; i++) { 82 | for (let j = 0; j < w; j++) { 83 | if (!toArr[j]) { 84 | toArr[j] = [] 85 | } 86 | toArr[j][i] = arr[i][j] 87 | } 88 | } 89 | const report = inspect(toArr) 90 | return { 91 | status: 0, 92 | result: report 93 | }; 94 | }) 95 | } -------------------------------------------------------------------------------- /src/content/components/sheetToCode.js: -------------------------------------------------------------------------------- 1 | function htmlToArr(text) { 2 | let dom = document.createElement(`div`); 3 | dom.innerHTML = text; 4 | dom = dom.querySelector("table tbody"); 5 | if (!dom) { 6 | return []; 7 | } 8 | // raw arr 9 | const cellArr = Array.prototype.map.call(dom.children || [], it => { 10 | return Array.prototype.map.call(it.children || [], cell => { 11 | return { 12 | row: cell.getAttribute("rowspan") - 0 || 1, 13 | col: cell.getAttribute("colspan") - 0 || 1, 14 | val: cell.innerText 15 | }; 16 | }); 17 | }); 18 | 19 | // map arr 20 | for (let i = 0; i < cellArr.length; i++) { 21 | const row = cellArr[i]; 22 | for (let j = 0; j < row.length; j++) { 23 | const cell = row[j]; 24 | const id = cell.id || `${i}-${j}`; 25 | if (cell.col > 1) { 26 | row.splice(j + 1, 0, { 27 | ...cell, 28 | id, 29 | col: cell.col - 1 30 | }); 31 | } 32 | if (cell.row > 1) { 33 | cellArr[i + 1].splice(j, 0, { 34 | ...cell, 35 | id, 36 | row: cell.row - 1 37 | }); 38 | } 39 | row[j] = { 40 | id, 41 | val: cell.val 42 | }; 43 | } 44 | } 45 | return cellArr; 46 | } 47 | 48 | // 双键json 49 | // row0,col0不应该有重复键 todo 50 | function arrToJson_doublekey(arr, major = "col") { 51 | if (arr.length < 2 || arr[0].length < 2) { 52 | return {}; 53 | } 54 | if (major === "row") { 55 | const body = arr.slice(1); 56 | const obj = arr[0].slice(1).reduce((res, cur, idx) => { 57 | const colsObj = body.reduce((cols, it) => { 58 | cols[it[0].val] = it[idx + 1].val; 59 | return cols; 60 | }, {}); 61 | res[cur.val] = colsObj; 62 | return res; 63 | }, {}); 64 | return obj; 65 | } else { 66 | const subKey = arr[0].slice(1); 67 | const body = arr.slice(1); 68 | const obj = body.reduce((res, cur) => { 69 | const rowObj = cur.slice(1).reduce((rows, it, idx) => { 70 | rows[subKey[idx].val] = it.val; 71 | return rows; 72 | }, {}); 73 | res[cur[0].val] = rowObj; 74 | return res; 75 | }, {}); 76 | return obj; 77 | } 78 | } 79 | 80 | // 单键json 81 | function arrToJson(arr, major = "col") { 82 | if (arr.length < 2 || arr[0].length < 2) { 83 | return {}; 84 | } 85 | if (major === "col") { 86 | return arr.reduce((res, cur) => { 87 | res[cur[0].val] = cur.slice(1).map(it => it.val); 88 | return res; 89 | }, {}); 90 | } 91 | if (major === "row") { 92 | const body = arr.slice(1); 93 | return arr[0].reduce((res, cur, idx) => { 94 | res[cur.val] = body.map(it => it[idx].val); 95 | return res; 96 | }, {}); 97 | } 98 | } 99 | 100 | // 双键json转php 101 | function dbkeyJsonToPhpCode(json) { 102 | const keys = Object.keys(json); 103 | const subKeys = Object.keys(json[keys[0]]); 104 | return keys 105 | .map(it => { 106 | const item = json[it]; 107 | const val = subKeys 108 | .map( 109 | subKey => 110 | `\t"${(subKey || "").replace(/"/g, '\\"')}"=>"${( 111 | item[subKey] || "" 112 | ).replace(/"/g, '\\"')}"` 113 | ) 114 | .join(",\n"); 115 | return `"${(it || "").replace(/"/g, '\\"')}" => [\n${val}\n]`; 116 | }) 117 | .join(",\n"); 118 | } 119 | 120 | // 单键json转php 121 | function jsonToPhpCode(json) { 122 | const keys = Object.keys(json); 123 | const rows = keys 124 | .map(it => { 125 | const item = json[it]; 126 | return `\t"${(it || "").replace(/"/g, '\\"')}" => [${item 127 | .map(it => `"${(it || "").replace(/"/g, '\\"')}"`) 128 | .join(", ")}]`; 129 | }) 130 | .join(",\n"); 131 | return `[\n${rows}\n]`; 132 | } 133 | 134 | // 双键转xml文档 135 | function jsonToXmlObj(json) { 136 | return Object.keys(json).reduce((res, it) => { 137 | const data = json[it]; 138 | const list = Object.keys(data).map( 139 | item => `${data[item]}` 140 | ); 141 | res[it] = ` 142 | 143 | 144 | 145 | ${list.join("\n ")} 146 | `; 147 | return res; 148 | }, {}); 149 | } 150 | 151 | export default function htmlTransform(text) { 152 | const arr = htmlToArr(text); 153 | if (arr.length === 0) { 154 | alert( 155 | "操作可能失败!如果文档表格有背景色,请将删除背景色或者将该文档**剔除格式**拷贝到新文档" 156 | ); 157 | } 158 | const dbkeyJson_row = arrToJson_doublekey(arr, "row"); 159 | const dbkeyJson_col = arrToJson_doublekey(arr, "col"); 160 | const json_row = arrToJson(arr, "row"); 161 | const json_col = arrToJson(arr, "col"); 162 | const dbkeyPhpRow = dbkeyJsonToPhpCode(dbkeyJson_row); 163 | const dbkeyPhpCol = dbkeyJsonToPhpCode(dbkeyJson_col); 164 | const jsonPhpRow = jsonToPhpCode(json_row); 165 | const jsonPhpCol = jsonToPhpCode(json_col); 166 | const xmlObj = jsonToXmlObj(dbkeyJson_row); 167 | return { 168 | dbkeyJson_row, 169 | dbkeyJson_col, 170 | json_row, 171 | json_col, 172 | dbkeyPhpRow, 173 | dbkeyPhpCol, 174 | jsonPhpRow, 175 | jsonPhpCol, 176 | xmlObj 177 | }; 178 | } 179 | -------------------------------------------------------------------------------- /src/content/components/lang-inspector/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const _config = require("./config") 4 | const lm = require("./langMap") 5 | let config = {} 6 | // 语言别名映射 7 | const langMap = Object.keys(lm).reduce((res, cur) => { 8 | lm[cur].split(",").forEach((it) => { 9 | if (res[it]) { 10 | console.error("配置文件存在相同的语言别名!", res[it]) 11 | } 12 | res[it] = cur 13 | }) 14 | return res 15 | }, {}) 16 | 17 | // 读行 18 | function readRow(arr, idx) { 19 | if (!Array.isArray(arr)) { 20 | return [] 21 | } 22 | return arr.map( 23 | (it) => (Array.isArray(it) && it.length > idx && it[idx]) || "" 24 | ) 25 | } 26 | 27 | // 检查语言名 28 | let langCount = 0 29 | let langValid = 0 30 | function checkLangRow(langRow) { 31 | const errorList = [] 32 | langCount = langRow.length 33 | langRow.forEach((it, idx) => { 34 | let exist = lm[it] 35 | if (exist) { 36 | langValid++ 37 | return 38 | } 39 | // 语言名大写 40 | exist = lm[it.toLowerCase()] 41 | if (exist) { 42 | errorList.push({ 43 | pos: `1-${String.fromCharCode(65 + idx + 1)}`, 44 | level: "error", 45 | msg: `请使用小写的语言名,修改【${it}】=>【${it.toLowerCase()}】` 46 | }) 47 | return 48 | } 49 | // 写为别名 50 | exist = langMap[it.toLowerCase()] 51 | if (exist) { 52 | errorList.push({ 53 | pos: `1-${String.fromCharCode(65 + idx + 1)}`, 54 | level: "error", 55 | msg: `请修改语言别名,修改【${it}】=>【${exist}】` 56 | }) 57 | return 58 | } 59 | // 不存在 60 | errorList.push({ 61 | pos: `1-${String.fromCharCode(65 + idx + 1)}`, 62 | level: "error", 63 | msg: `不存在该语言别名【${it}】,请检查修改!` 64 | }) 65 | }) 66 | return errorList 67 | } 68 | 69 | // 检查一个多语言 70 | function checkMulLang(row, rowIdx, uilangIdx) { 71 | const result = [] 72 | const uilang = row[uilangIdx] 73 | const params = uilang.match(/\[\d+\]/g) || [] 74 | // 非数字变量,排除{001}、{abc}、[abc]、[ab_001]正则式排除中括号影响 75 | if (/\[[^\]\[]*[^\d\]]+[^\]\[]*\]/i.test(uilang)) { 76 | result.push({ 77 | pos: `${rowIdx + 1}-${String.fromCharCode(65 + uilangIdx + 1)}`, 78 | level: "error", 79 | msg: `变量命名不规范,不允许[abc]、[ab_001]的命名方式,将【${ 80 | /\[[^\]\[]*[^\d\]]+[^\]\[]*\]/i.exec(uilang)[0] 81 | }】修改为[001]格式命名` 82 | }) 83 | } 84 | // 检查变量序号 85 | const codes = params.map((it) => it.slice(1, -1)).sort() 86 | if (codes.length > 0 && codes[0] != 1) { 87 | result.push({ 88 | pos: `${rowIdx + 1}-${String.fromCharCode(65 + uilangIdx + 1)}`, 89 | level: "error", 90 | msg: `变量命名不规范,请使用从001开始的变量,例如:[001]` 91 | }) 92 | } 93 | for (let i = 1; i < codes.length; i++) { 94 | if (codes[i] - codes[i - 1] != 1) { 95 | result.push({ 96 | pos: `${rowIdx + 1}-${String.fromCharCode(65 + uilangIdx + 1)}`, 97 | level: "error", 98 | msg: `变量命名不规范,多个变量名需要使用递增的数字序号,例如:[001]、[002]、..` 99 | }) 100 | break 101 | } 102 | } 103 | // 提示[01]、[0001]格式错误 104 | params.some((it) => { 105 | if (it.length !== 5) { 106 | result.push({ 107 | pos: `${rowIdx + 1}-${String.fromCharCode(65 + uilangIdx + 1)}`, 108 | level: "error", 109 | msg: `变量命名不规范,序号不能是${it.length - 110 | 2}个数字,应改为3位数字,例如:[001]、[002]、..` 111 | }) 112 | return true 113 | } 114 | return false 115 | }) 116 | // 检查多语言命名规范 117 | if (/[(({【]\w+[))}】]/.test(uilang)) { 118 | result.push({ 119 | pos: `${rowIdx + 1}-${String.fromCharCode(65 + uilangIdx + 1)}`, 120 | level: "error", 121 | msg: `变量命名不规范,请使用中括号和数字序号。例如:[001]` 122 | }) 123 | } 124 | // 检查空 125 | row.forEach((item, index) => { 126 | if (!item.trim()) { 127 | result.push({ 128 | pos: `${rowIdx + 1}-${String.fromCharCode(65 + index + 1)}`, 129 | level: "error", 130 | msg: `不能为空!` 131 | }) 132 | } 133 | }) 134 | // 检查参数不匹配 135 | row.forEach((item, index) => { 136 | params.forEach((it) => { 137 | if (item.indexOf(it) === -1) { 138 | result.push({ 139 | pos: `${rowIdx + 1}-${String.fromCharCode(65 + index + 1)}`, 140 | level: "error", 141 | msg: `多语言缺少参数${it}` 142 | }) 143 | } 144 | }) 145 | }) 146 | // 检查短文本 147 | const short = config.shortTextCompare 148 | if (!short) { 149 | return result 150 | } 151 | const min = Math.min(short.which[0], short.which[1]) 152 | const max = Math.max(short.which[0], short.which[1]) 153 | const scale = short.scale || 1.6 154 | if (uilang.length >= min && uilang.length <= max) { 155 | let notExpect = null 156 | let lastScale = 1 157 | row.forEach((item, index) => { 158 | if (item.length > uilang.length && item.length / uilang.length > scale) { 159 | const scale = item.length / uilang.length 160 | if (scale > lastScale) { 161 | notExpect = { 162 | pos: `${rowIdx + 1}-${String.fromCharCode(65 + index + 1)}`, 163 | level: "warn", 164 | msg: `短文本(${uilang}), 出现${scale.toFixed(2)}倍长度的文本(${item})` 165 | } 166 | lastScale = scale 167 | } 168 | } 169 | }) 170 | if (notExpect) { 171 | result.push(notExpect) 172 | } 173 | } 174 | return result 175 | } 176 | 177 | // 处理 178 | export default function inspect(langData, conf) { 179 | langCount = 0 180 | langValid = 0 181 | config = Object.assign({}, _config, conf) 182 | const result = { 183 | warnList: [], 184 | reportList: [] 185 | } 186 | // 有效行,即需要处理的行 187 | const validRow = [] 188 | const keyCol = langData[0] 189 | const dataCols = langData.slice(1) 190 | let uilangIdx = 0 191 | 192 | // 检查文件命名 193 | if (keyCol[0] !== "lang") { 194 | result.warnList.push({ 195 | pos: "1-A", 196 | level: "error", 197 | msg: `首行第一格必须填写为【lang】, 当前值为【${keyCol[0]}】` 198 | }) 199 | } 200 | 201 | // 检查语言行 202 | const langRow = readRow(dataCols, 0) 203 | result.langList = langRow || [] 204 | result.warnList.push(...checkLangRow(langRow)) 205 | langRow.forEach((it, idx) => { 206 | if (it.toLowerCase() === config.uiLang.toLowerCase()) { 207 | uilangIdx = idx 208 | } 209 | }) 210 | const keySet = new Set() 211 | keyCol.slice(1).forEach((key, index) => { 212 | if (keySet.has(key.toLowerCase())) { 213 | result.warnList.push({ 214 | pos: `${2 + index}-A`, 215 | level: "error", 216 | msg: `存在重复键【${key}】, 请检查修改!` 217 | }) 218 | } else { 219 | keySet.add(key.toLowerCase()) 220 | } 221 | if (/^lang\d+$/.test(key)) { 222 | validRow.push(index + 1) 223 | } 224 | }) 225 | result.reportList.push({ 226 | msg: `检查到的多语言项${ 227 | validRow.length 228 | }个,共[${langValid}/${langCount}]种语言` 229 | }) 230 | // 依次检查单个多语言 231 | validRow.forEach((it) => { 232 | const langRow = readRow(dataCols, it) 233 | result.warnList.push(...checkMulLang(langRow, it, uilangIdx)) 234 | }) 235 | result.status = result.warnList.length === 0 236 | return result 237 | } -------------------------------------------------------------------------------- /src/content/components/App.vue: -------------------------------------------------------------------------------- 1 | 190 | 191 | 323 | 324 | 603 | --------------------------------------------------------------------------------