├── .eslintrc.json ├── assets └── icon.png ├── 1646677294311.zip ├── ScreenRecording.gif ├── excelContentSync.sketchplugin └── Contents │ ├── Resources │ └── icon.png │ └── Sketch │ ├── manifest.json │ ├── __open-url.js.map │ └── __open-url.js ├── src ├── open-url.js ├── constants.js ├── manifest.json └── plugin.js ├── .gitignore ├── LICENSE.md ├── package.json ├── README.md └── .appcast.xml /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "rules": {"quote-props": 2} 4 | } -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wousser/SketchExcelContentSync/HEAD/assets/icon.png -------------------------------------------------------------------------------- /1646677294311.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wousser/SketchExcelContentSync/HEAD/1646677294311.zip -------------------------------------------------------------------------------- /ScreenRecording.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wousser/SketchExcelContentSync/HEAD/ScreenRecording.gif -------------------------------------------------------------------------------- /excelContentSync.sketchplugin/Contents/Resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wousser/SketchExcelContentSync/HEAD/excelContentSync.sketchplugin/Contents/Resources/icon.png -------------------------------------------------------------------------------- /src/open-url.js: -------------------------------------------------------------------------------- 1 | export function openFeedback (context) { 2 | openUrl('https://github.com/wousser/SketchExcelContentSync') 3 | } 4 | 5 | function openUrl (url) { 6 | NSWorkspace.sharedWorkspace().openURL(NSURL.URLWithString(url)) 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build artifacts 2 | #excelContentSync.sketchplugin 3 | 4 | # npm 5 | node_modules 6 | package-lock.json 7 | .npm 8 | npm-debug.log 9 | 10 | # mac 11 | .DS_Store 12 | 13 | # WebStorm 14 | .idea 15 | 16 | # logs 17 | logs 18 | *.log 19 | npm-debug.log* 20 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | const excelDivider = " / "; 2 | const sketchSymbolDivider = "/"; 3 | const pagePrefix = " P#"; 4 | const artboardPrefix = "A#"; 5 | const noArtboardPrefix = "N#"; 6 | const translateLayerPrefix = "#"; 7 | const LayerIdPrefix = "+"; 8 | 9 | module.exports = { 10 | excelDivider, 11 | sketchSymbolDivider, 12 | pagePrefix, 13 | artboardPrefix, 14 | noArtboardPrefix, 15 | translateLayerPrefix, 16 | LayerIdPrefix, 17 | }; 18 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Wouter Bron 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. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "excel-content-sync", 3 | "version": "0.8.0", 4 | "engines": { 5 | "sketch": ">=3.0" 6 | }, 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/wousser/SketchExcelContentSync.git" 10 | }, 11 | "skpm": { 12 | "name": "Excel Content Sync", 13 | "manifest": "src/manifest.json", 14 | "main": "excelContentSync.sketchplugin", 15 | "assets": [ 16 | "assets/**/*" 17 | ] 18 | }, 19 | "scripts": { 20 | "build": "skpm-build", 21 | "watch": "skpm-build --watch", 22 | "start": "skpm-build --watch --run", 23 | "postinstall": "npm run build && skpm-link" 24 | }, 25 | "devDependencies": { 26 | "@skpm/babel-preset": "^0.2.3", 27 | "@skpm/builder": "^0.8.0", 28 | "@skpm/extract-loader": "^2.0.2", 29 | "css-loader": "^6.7.1", 30 | "eslint": "^8.10.0", 31 | "eslint-config-google": "^0.14.0", 32 | "eslint-config-sketch": "^0.2.4", 33 | "eslint-plugin-import": "^2.25.4", 34 | "eslint-plugin-node": "^11.1.0", 35 | "eslint-plugin-promise": "^6.0.0", 36 | "eslint-plugin-standard": "^5.0.0", 37 | "html-loader": "^3.1.0" 38 | }, 39 | "author": "Wouter Bron ", 40 | "dependencies": { 41 | "@skpm/dialog": "^0.4.2", 42 | "@skpm/fs": "^0.2.6", 43 | "lodash": "^4.17.21", 44 | "sketch-module-web-view": "^3.5.1", 45 | "xlsx": "^0.17.5" 46 | }, 47 | "resources": [ 48 | "resources/**/*.js" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sketchExcelTranslate 2 | 3 | Decouple and Sync multi-language content between Sketch and Excel. 4 | 5 | ![ScreenRecording](ScreenRecording.gif) 6 | 7 | ## Use cases 8 | - Decouple your content from Sketch so you can hand-off your content for review by a copy-writer 9 | - Switch easily between multiple languages 10 | - Switch easily between multiple versions of your content 11 | 12 | ## Usage 13 | 14 | - Backup your Sketch file (just in case) 15 | - Prefix your text layers and/or symbol layers that you want to include with '#' 16 | - Generate Excel file with all your content: Plugins -> Excel Content Sync -> Generate Content File... 17 | - Select the Content File: Plugins -> Excel Content Sync -> Select Content File... 18 | - Select your language 19 | - Wait till your Sketch file is updated. 20 | 21 | ## Installation 22 | 1. [Download the Zip](https://github.com/wousser/SketchExcelContentSync/archive/master.zip) and unzip the package 23 | 2. Double click on `excelContentSync.sketchplugin` for auto installation 24 | 25 | ## Compatibility 26 | 27 | The plugin is compatible with the latest version of Sketch 84 🎉 (And previous versions). 28 | 29 | ## How many languages are supported 30 | - As many as you need. Each columnn in your Excel file can be chosen as a language source. 31 | 32 | ## What about symbol overrides 33 | - Symbol overrides are supported. Prefix your symbols with '#' to include them. The will be listed as 'symbolName / overrideName' in the Excel file. 34 | - Same applies to symbols within symbols. 35 | 36 | ## Get involved 37 | - Accepting bug fixes, feature requests, PRs. 38 | - This is my first Sketch plugin, and I'm happy to review PRs regarding code improvements and best practices. 39 | - See SKPM on how to get started. 40 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Excel Content Sync", 3 | "description": "Decouple and Sync multi-language content between Excel and Sketch", 4 | "author": "Wouter Bron", 5 | "homepage": "https://github.com/wousser/SketchExcelContentSync", 6 | "compatibleVersion": 3, 7 | "bundleVersion": 1, 8 | "icon": "icon.png", 9 | "commands": [ 10 | { 11 | "name": "Select Content File...", 12 | "description": "Select the Excel or CSV file that contains the content.", 13 | "identifier": "select-content-file", 14 | "script": "./plugin.js", 15 | "handler": "selectContentFile" 16 | }, 17 | { 18 | "name": "Sync Current Page...", 19 | "description": "Sync content from Excel file to this page.", 20 | "identifier": "sync-current-page", 21 | "script": "./plugin.js", 22 | "handler": "syncCurrentPage" 23 | }, 24 | { 25 | "name": "Sync All Pages...", 26 | "description": "Sync content from Excel file to all pages (except Symbols).", 27 | "identifier": "sync-all-pages", 28 | "script": "./plugin.js", 29 | "handler": "syncAllPages" 30 | }, 31 | { 32 | "name": "Generate Content File...", 33 | "description": "Generate Excel file with keys and values from the current Sketch document.", 34 | "identifier": "generate-content-file", 35 | "script": "./plugin.js", 36 | "handler": "generateContentFile" 37 | }, 38 | { 39 | "name": "Report Issue or Feedback", 40 | "description": "Report Issue, Feedback or Feature on Github.", 41 | "identifier": "open-feedback", 42 | "script": "./open-url.js", 43 | "handler": "openFeedback" 44 | } 45 | ], 46 | "menu": { 47 | "title": "Excel Content Sync", 48 | "items": [ 49 | "sync-current-page", 50 | "sync-all-pages", 51 | "-", 52 | "select-content-file", 53 | "generate-content-file", 54 | "-", 55 | "open-feedback" 56 | ] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /excelContentSync.sketchplugin/Contents/Sketch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Excel Content Sync", 3 | "description": "Decouple and Sync multi-language content between Excel and Sketch", 4 | "author": "Wouter Bron", 5 | "homepage": "https://github.com/wousser/SketchExcelContentSync", 6 | "compatibleVersion": 3, 7 | "bundleVersion": 1, 8 | "icon": "icon.png", 9 | "commands": [ 10 | { 11 | "name": "Select Content File...", 12 | "description": "Select the Excel or CSV file that contains the content.", 13 | "identifier": "select-content-file", 14 | "script": "__plugin.js", 15 | "handler": "selectContentFile" 16 | }, 17 | { 18 | "name": "Sync Current Page...", 19 | "description": "Sync content from Excel file to this page.", 20 | "identifier": "sync-current-page", 21 | "script": "__plugin.js", 22 | "handler": "syncCurrentPage" 23 | }, 24 | { 25 | "name": "Sync All Pages...", 26 | "description": "Sync content from Excel file to all pages (except Symbols).", 27 | "identifier": "sync-all-pages", 28 | "script": "__plugin.js", 29 | "handler": "syncAllPages" 30 | }, 31 | { 32 | "name": "Generate Content File...", 33 | "description": "Generate Excel file with keys and values from the current Sketch document.", 34 | "identifier": "generate-content-file", 35 | "script": "__plugin.js", 36 | "handler": "generateContentFile" 37 | }, 38 | { 39 | "name": "Report Issue or Feedback", 40 | "description": "Report Issue, Feedback or Feature on Github.", 41 | "identifier": "open-feedback", 42 | "script": "__open-url.js", 43 | "handler": "openFeedback" 44 | } 45 | ], 46 | "menu": { 47 | "title": "Excel Content Sync", 48 | "items": [ 49 | "sync-current-page", 50 | "sync-all-pages", 51 | "-", 52 | "select-content-file", 53 | "generate-content-file", 54 | "-", 55 | "open-feedback" 56 | ] 57 | }, 58 | "version": "0.7.0", 59 | "identifier": "Excel Content Sync", 60 | "disableCocoaScriptPreprocessor": true, 61 | "appcast": "https://raw.githubusercontent.com/wousser/SketchExcelContentSync/master/.appcast.xml" 62 | } -------------------------------------------------------------------------------- /.appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /excelContentSync.sketchplugin/Contents/Sketch/__open-url.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack://exports/webpack/bootstrap","webpack://exports/./src/open-url.js"],"names":["openFeedback","context","openUrl","url","NSWorkspace","sharedWorkspace","openURL","NSURL","URLWithString"],"mappings":";;;;;;;;QAAA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;;QAEA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;;;QAGA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;QACA,0CAA0C,gCAAgC;QAC1E;QACA;;QAEA;QACA;QACA;QACA,wDAAwD,kBAAkB;QAC1E;QACA,iDAAiD,cAAc;QAC/D;;QAEA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA,yCAAyC,iCAAiC;QAC1E,gHAAgH,mBAAmB,EAAE;QACrI;QACA;;QAEA;QACA;QACA;QACA,2BAA2B,0BAA0B,EAAE;QACvD,iCAAiC,eAAe;QAChD;QACA;QACA;;QAEA;QACA,sDAAsD,+DAA+D;;QAErH;QACA;;;QAGA;QACA;;;;;;;;;;;;;AClFA;AAAA;AAAO,SAASA,YAAT,CAAuBC,OAAvB,EAAgC;AACrCC,SAAO,CAAC,mDAAD,CAAP;AACD;;AAED,SAASA,OAAT,CAAkBC,GAAlB,EAAuB;AACrBC,aAAW,CAACC,eAAZ,GAA8BC,OAA9B,CAAsCC,KAAK,CAACC,aAAN,CAAoBL,GAApB,CAAtC;AACD,C","file":"__open-url.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = \"./src/open-url.js\");\n","export function openFeedback (context) {\n openUrl('https://github.com/wousser/SketchExcelContentSync')\n}\n\nfunction openUrl (url) {\n NSWorkspace.sharedWorkspace().openURL(NSURL.URLWithString(url))\n}"],"sourceRoot":""} -------------------------------------------------------------------------------- /excelContentSync.sketchplugin/Contents/Sketch/__open-url.js: -------------------------------------------------------------------------------- 1 | var globalThis = this; 2 | var global = this; 3 | function __skpm_run (key, context) { 4 | globalThis.context = context; 5 | try { 6 | 7 | var exports = 8 | /******/ (function(modules) { // webpackBootstrap 9 | /******/ // The module cache 10 | /******/ var installedModules = {}; 11 | /******/ 12 | /******/ // The require function 13 | /******/ function __webpack_require__(moduleId) { 14 | /******/ 15 | /******/ // Check if module is in cache 16 | /******/ if(installedModules[moduleId]) { 17 | /******/ return installedModules[moduleId].exports; 18 | /******/ } 19 | /******/ // Create a new module (and put it into the cache) 20 | /******/ var module = installedModules[moduleId] = { 21 | /******/ i: moduleId, 22 | /******/ l: false, 23 | /******/ exports: {} 24 | /******/ }; 25 | /******/ 26 | /******/ // Execute the module function 27 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 28 | /******/ 29 | /******/ // Flag the module as loaded 30 | /******/ module.l = true; 31 | /******/ 32 | /******/ // Return the exports of the module 33 | /******/ return module.exports; 34 | /******/ } 35 | /******/ 36 | /******/ 37 | /******/ // expose the modules object (__webpack_modules__) 38 | /******/ __webpack_require__.m = modules; 39 | /******/ 40 | /******/ // expose the module cache 41 | /******/ __webpack_require__.c = installedModules; 42 | /******/ 43 | /******/ // define getter function for harmony exports 44 | /******/ __webpack_require__.d = function(exports, name, getter) { 45 | /******/ if(!__webpack_require__.o(exports, name)) { 46 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 47 | /******/ } 48 | /******/ }; 49 | /******/ 50 | /******/ // define __esModule on exports 51 | /******/ __webpack_require__.r = function(exports) { 52 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 53 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 54 | /******/ } 55 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 56 | /******/ }; 57 | /******/ 58 | /******/ // create a fake namespace object 59 | /******/ // mode & 1: value is a module id, require it 60 | /******/ // mode & 2: merge all properties of value into the ns 61 | /******/ // mode & 4: return value when already ns object 62 | /******/ // mode & 8|1: behave like require 63 | /******/ __webpack_require__.t = function(value, mode) { 64 | /******/ if(mode & 1) value = __webpack_require__(value); 65 | /******/ if(mode & 8) return value; 66 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 67 | /******/ var ns = Object.create(null); 68 | /******/ __webpack_require__.r(ns); 69 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 70 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 71 | /******/ return ns; 72 | /******/ }; 73 | /******/ 74 | /******/ // getDefaultExport function for compatibility with non-harmony modules 75 | /******/ __webpack_require__.n = function(module) { 76 | /******/ var getter = module && module.__esModule ? 77 | /******/ function getDefault() { return module['default']; } : 78 | /******/ function getModuleExports() { return module; }; 79 | /******/ __webpack_require__.d(getter, 'a', getter); 80 | /******/ return getter; 81 | /******/ }; 82 | /******/ 83 | /******/ // Object.prototype.hasOwnProperty.call 84 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 85 | /******/ 86 | /******/ // __webpack_public_path__ 87 | /******/ __webpack_require__.p = ""; 88 | /******/ 89 | /******/ 90 | /******/ // Load entry module and return exports 91 | /******/ return __webpack_require__(__webpack_require__.s = "./src/open-url.js"); 92 | /******/ }) 93 | /************************************************************************/ 94 | /******/ ({ 95 | 96 | /***/ "./src/open-url.js": 97 | /*!*************************!*\ 98 | !*** ./src/open-url.js ***! 99 | \*************************/ 100 | /*! exports provided: openFeedback */ 101 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 102 | 103 | "use strict"; 104 | __webpack_require__.r(__webpack_exports__); 105 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "openFeedback", function() { return openFeedback; }); 106 | function openFeedback(context) { 107 | openUrl('https://github.com/wousser/SketchExcelContentSync'); 108 | } 109 | 110 | function openUrl(url) { 111 | NSWorkspace.sharedWorkspace().openURL(NSURL.URLWithString(url)); 112 | } 113 | 114 | /***/ }) 115 | 116 | /******/ }); 117 | if (key === 'default' && typeof exports === 'function') { 118 | exports(context); 119 | } else if (typeof exports[key] !== 'function') { 120 | throw new Error('Missing export named "' + key + '". Your command should contain something like `export function " + key +"() {}`.'); 121 | } else { 122 | exports[key](context); 123 | } 124 | } catch (err) { 125 | if (typeof process !== 'undefined' && process.listenerCount && process.listenerCount('uncaughtException')) { 126 | process.emit("uncaughtException", err, "uncaughtException"); 127 | } else { 128 | throw err 129 | } 130 | } 131 | } 132 | globalThis['openFeedback'] = __skpm_run.bind(this, 'openFeedback'); 133 | globalThis['onRun'] = __skpm_run.bind(this, 'default') 134 | 135 | //# sourceMappingURL=__open-url.js.map -------------------------------------------------------------------------------- /src/plugin.js: -------------------------------------------------------------------------------- 1 | import fs from "@skpm/fs"; 2 | import dialog from "@skpm/dialog"; 3 | 4 | const XLSX = require("xlsx"); 5 | var _ = require("lodash"); 6 | 7 | var sketch = require("sketch/dom"); 8 | var constants = require("./constants"); 9 | var UI = require("sketch/ui"); 10 | var Page = require("sketch/dom").Page; 11 | var path = require("path"); 12 | var Settings = require("sketch/settings"); 13 | 14 | var document = sketch.getSelectedDocument(); 15 | 16 | var contentDictionary = []; 17 | var languageOptions = []; 18 | var selectedLanguage; 19 | var contentLanguage = "en-US"; // set en-US as standard 20 | var contentFileType = "Excel"; 21 | var renameTextLayersFlag = false; 22 | var generatedFileData = []; 23 | var duplicateData = []; 24 | var duplicateKeys = 0; 25 | var excelJson; 26 | 27 | class ExcelContent { 28 | constructor(key, value) { 29 | this.key = key; 30 | this.value = value; 31 | } 32 | } 33 | 34 | // 35 | // Generate Content 36 | // 37 | export function generateContentFile() { 38 | console.log("generateContentFile"); 39 | if (document.pages) { 40 | // ask for language 41 | if (askContentLanguage() && askRenameTextLayers() && askFileType()) { 42 | UI.alert( 43 | "Creating content document", 44 | "Press OK to start creating content document\n\nDepending on the number of pages, artboards and content this might take a while..." 45 | ); 46 | 47 | const symbolsPage = Page.getSymbolsPage(document); 48 | const symbolsPageName = symbolsPage ? symbolsPage.name : "Symbols"; 49 | console.log("symbolsPageName", symbolsPageName); 50 | 51 | for (let page of document.pages) { 52 | // Don't add Symbols page 53 | 54 | if (page.name !== symbolsPageName) { 55 | generateContentForPage(page); 56 | } 57 | } 58 | saveToFile(); 59 | } 60 | } else { 61 | console.log("Document contains no pages"); 62 | UI.message("Sketch document contains no pages"); 63 | } 64 | } 65 | 66 | function getGroupPath(layer) {} 67 | 68 | function generateContentForPage(page) { 69 | console.time("generateContent"); 70 | console.log("page: ", page.name); 71 | 72 | //add page to excel 73 | addToSheet("", ""); // add empty row 74 | addToSheet("", ""); // add empty row 75 | const pageDivider = `${constants.pagePrefix}${page.name}`; 76 | addToSheet(pageDivider, ""); 77 | 78 | // all artboards 79 | const artBoardLayers = sketch.find("Artboard", page); 80 | 81 | if (artBoardLayers) { 82 | console.log("ArtBoards: ", artBoardLayers.length); 83 | artBoardLayers.forEach((artBoard) => { 84 | const artBoardName = artBoard.name; 85 | console.log("ArtBoard: ", artBoardName); 86 | 87 | //add artBoard to excel 88 | const artboardDivider = `${constants.artboardPrefix}${artBoardName}`; 89 | addToSheet("", ""); // add empty row 90 | addToSheet(artboardDivider, ""); 91 | 92 | // Text 93 | const textLayers = sketch.find("Text", artBoard); 94 | 95 | if (textLayers) { 96 | console.log("Text layers: ", textLayers.length); 97 | textLayers.forEach((textLayer) => { 98 | console.log("Text layer: ", textLayer.name); 99 | console.log(textLayer); 100 | if (textLayer.parent) { 101 | console.log("Group:", textLayer.parent.name); 102 | } 103 | 104 | //duplicateCheck 105 | let isDuplicate = duplicateData.includes(textLayer.name); 106 | console.log( 107 | "isDuplicate:", 108 | isDuplicate, 109 | textLayer.name, 110 | duplicateData 111 | ); 112 | 113 | if (isDuplicate) { 114 | //add ID to layer name 115 | addToSheet( 116 | textLayer.name + constants.LayerIdPrefix + textLayer.id, 117 | textLayer.text 118 | ); 119 | duplicateData.push( 120 | textLayer.name + constants.LayerIdPrefix + textLayer.id 121 | ); 122 | duplicateKeys += 1; 123 | } else { 124 | //add to sheet buffer 125 | addToSheet(textLayer.name, textLayer.text); 126 | duplicateData.push(textLayer.name); 127 | } 128 | 129 | //rename with # 130 | renameLayer(textLayer, isDuplicate); 131 | }); 132 | } 133 | 134 | // Symbol 135 | const symbolLayers = sketch.find("SymbolInstance", artBoard); 136 | 137 | if (symbolLayers) { 138 | console.log("Symbols: ", symbolLayers.length); 139 | symbolLayers.forEach((symbolLayer) => { 140 | console.log("Symbol: ", symbolLayer.name); 141 | //rename with # 142 | renameLayer(symbolLayer); 143 | 144 | //add to sheet buffer 145 | let textOverrides = extractTextOverrides(symbolLayer); 146 | // console.log("textOverrides", textOverrides); 147 | if (textOverrides) { 148 | textOverrides.forEach((textOverride) => { 149 | addToSheet(textOverride.fullPath, textOverride.value); 150 | }); 151 | } 152 | }); 153 | } 154 | }); 155 | } 156 | console.timeEnd("generateContent"); 157 | } 158 | 159 | function renameLayer(layer, isDuplicate) { 160 | if (renameTextLayersFlag) { 161 | if (isDuplicate) { 162 | layer.name = layer.name + constants.LayerIdPrefix + layer.id; 163 | } 164 | 165 | if (layer.name.charAt(0) !== constants.translateLayerPrefix) { 166 | layer.name = constants.translateLayerPrefix + layer.name; 167 | } 168 | 169 | console.log("layer renamed", layer.name); 170 | } else { 171 | console.log("Rename Layers disabled"); 172 | } 173 | } 174 | 175 | function extractTextOverrides(symbol) { 176 | // console.log("extractTextOverrides"); 177 | 178 | const symbolName = `${symbol.name}`; 179 | 180 | var result = []; 181 | if (symbol.overrides) { 182 | symbol.overrides.forEach((override) => { 183 | if ( 184 | override.affectedLayer.type === sketch.Types.Text && 185 | override.property === "stringValue" 186 | ) { 187 | // console.log(symbol.name); 188 | const fullPath = `${symbolName}${ 189 | constants.excelDivider 190 | }${layerNamesFromPath(override.path, symbol)}`; 191 | // console.log("path", fullPath, override.value); 192 | // console.log(symbol); 193 | result.push({ 194 | fullPath: fullPath, 195 | value: override.value, 196 | }); 197 | } 198 | }); 199 | } 200 | 201 | return result; 202 | } 203 | 204 | function addToSheet(key, value) { 205 | // add to array 206 | let keyValue = new ExcelContent(key, value); 207 | generatedFileData.push(keyValue); 208 | } 209 | 210 | function askRenameTextLayers() { 211 | var returnValue = false; 212 | UI.getInputFromUser( 213 | `Prefix text and symbol layers with '${constants.translateLayerPrefix}', and rename duplicates?`, 214 | { 215 | type: UI.INPUT_TYPE.selection, 216 | possibleValues: ["yes", "no"], 217 | initialValue: "yes", 218 | }, 219 | (err, value) => { 220 | if (err) { 221 | console.log("pressed cancel"); 222 | // most likely the user canceled the input 223 | return; 224 | } 225 | if (value !== "null" && value.length > 1) { 226 | renameTextLayersFlag = value === "yes"; 227 | console.log("set renameTextLayersFlag", renameTextLayersFlag); 228 | returnValue = true; 229 | } 230 | } 231 | ); 232 | return returnValue; 233 | } 234 | 235 | function askContentLanguage() { 236 | var returnValue = false; 237 | UI.getInputFromUser( 238 | "Current document language?", 239 | { 240 | initialValue: "en-US", 241 | }, 242 | (err, value) => { 243 | if (err) { 244 | console.log("pressed cancel"); 245 | // most likely the user canceled the input 246 | return; 247 | } 248 | if (value !== "null" && value.length > 1) { 249 | contentLanguage = value; 250 | console.log("set contentLanguage", contentLanguage); 251 | returnValue = true; 252 | } 253 | } 254 | ); 255 | return returnValue; 256 | } 257 | 258 | function askFileType() { 259 | var returnValue = false; 260 | UI.getInputFromUser( 261 | `Filetype: Save as Excel or CSV file?`, 262 | { 263 | type: UI.INPUT_TYPE.selection, 264 | possibleValues: ["Excel", "CSV"], 265 | initialValue: "Excel", 266 | }, 267 | (err, value) => { 268 | if (err) { 269 | console.log("pressed cancel"); 270 | // most likely the user canceled the input 271 | return; 272 | } 273 | if (value !== "null" && value.length > 1) { 274 | contentFileType = value; 275 | console.log("set contentFileType", contentFileType); 276 | returnValue = true; 277 | } 278 | } 279 | ); 280 | return returnValue; 281 | } 282 | 283 | function saveToFile() { 284 | var date = new Date(); 285 | var dateFormat = `${date.getFullYear()}-${ 286 | date.getMonth() + 1 287 | }-${date.getDate()} ${date.getHours()}-${date.getMinutes()}`; 288 | 289 | var defaultPath = "/"; 290 | if (document.path) { 291 | let name = decodeURI(path.basename(document.path, ".sketch")); 292 | let contentFileName = name + "-content-" + dateFormat; 293 | defaultPath = path.join( 294 | path.dirname(document.path), 295 | contentFileName 296 | // + ".xlsx" 297 | ); 298 | switch (contentFileType) { 299 | case "Excel": 300 | defaultPath += ".xslx"; 301 | break; 302 | case "CSV": 303 | defaultPath += ".csv"; 304 | break; 305 | } 306 | } 307 | 308 | console.log(defaultPath); 309 | var filePath = dialog.showSaveDialogSync({ 310 | title: "Export as:", 311 | message: "Export Sketch content as Excel or CSV file.", 312 | filters: [ 313 | { name: "Excel", extensions: ["xlsx"] }, 314 | { name: "CSV", extensions: ["csv"] }, 315 | ], 316 | defaultPath: defaultPath, 317 | }); 318 | 319 | try { 320 | // check if user want to save the file 321 | if (filePath) { 322 | console.log("filePath", filePath); 323 | let fileType = path.extname(filePath); 324 | console.log("fileType", fileType); 325 | 326 | let book = XLSX.utils.book_new(); 327 | let sheet = XLSX.utils.json_to_sheet(generatedFileData); 328 | sheet["B1"].v = contentLanguage; 329 | XLSX.utils.book_append_sheet(book, sheet, "content"); 330 | var content; 331 | switch (fileType.toLowerCase()) { 332 | case ".csv": 333 | console.log("fileType csv"); 334 | content = CSVFile(book); 335 | break; 336 | case (".xls", ".xlsx"): 337 | console.log("fileType excel"); 338 | content = excelFile(book); 339 | break; 340 | } 341 | 342 | fs.writeFileSync(filePath, content, { encoding: "binary" }); 343 | console.log("File created as:", filePath); 344 | 345 | Settings.setDocumentSettingForKey( 346 | document, 347 | "excelTranslateContentFile", 348 | filePath 349 | ); 350 | 351 | // done 352 | console.log( 353 | "Completed. Duplicates: " + 354 | duplicateKeys + 355 | " File saved as " + 356 | path.basename(filePath) 357 | ); 358 | UI.message( 359 | "Content file generated. Found " + 360 | duplicateKeys + 361 | " duplicated keys. File saved as " + 362 | decodeURI(path.basename(filePath)) 363 | ); 364 | } 365 | } catch (error) { 366 | console.log(error); 367 | UI.message("error"); 368 | } 369 | } 370 | 371 | function excelFile(book) { 372 | // console.log(generatedFileData) 373 | 374 | return XLSX.write(book, { 375 | type: "buffer", 376 | bookType: "xlsx", 377 | bookSST: false, 378 | }); 379 | } 380 | 381 | function CSVFile(book) { 382 | return XLSX.write(book, { 383 | type: "buffer", 384 | bookType: "csv", 385 | bookSST: false, 386 | }); 387 | } 388 | 389 | // 390 | // Sync page 391 | // 392 | 393 | export function syncCurrentPage(context) { 394 | console.time("syncCurrentPage(context)"); 395 | console.log("syncCurrentPage"); 396 | if (contentDocumentExists()) { 397 | console.log("contentDocumentExists true"); 398 | var contentFile = Settings.documentSettingForKey( 399 | document, 400 | "excelTranslateContentFile" 401 | ); 402 | 403 | loadData(contentFile); 404 | 405 | //ask language / column 406 | let contentFileName = path.basename(contentFile); 407 | UI.getInputFromUser( 408 | `Select content column:\n\nContent file:\n${contentFileName}`, 409 | { 410 | type: UI.INPUT_TYPE.selection, 411 | possibleValues: languageOptions, 412 | }, 413 | (err, value) => { 414 | if (err) { 415 | // most likely the user canceled the input 416 | console.log("user cancelled input"); 417 | console.log(err, value); 418 | return; 419 | } 420 | console.log("selected language", value); 421 | 422 | selectedLanguage = value; // languageOptions[selection[1]] 423 | UI.alert( 424 | "Syncing content", 425 | "Press OK to start syncing content\n\nDepending on the number of pages, artboards and content this might take a while..." 426 | ); 427 | 428 | if (selectedLanguage) { 429 | syncContentForPage(document.selectedPage); 430 | 431 | context.document.reloadInspector(); 432 | } 433 | } 434 | ); 435 | //ask language / column 436 | } else { 437 | console.log("Content file not found."); 438 | UI.message("Content file not found."); 439 | } 440 | 441 | console.timeEnd("syncCurrentPage(context)"); 442 | } 443 | 444 | export function syncAllPages(context) { 445 | console.log("syncAllPages"); 446 | if (contentDocumentExists() && document.pages) { 447 | var contentFile = Settings.documentSettingForKey( 448 | document, 449 | "excelTranslateContentFile" 450 | ); 451 | 452 | loadData(contentFile); 453 | 454 | //ask language / column 455 | let contentFileName = path.basename(contentFile); 456 | UI.getInputFromUser( 457 | `Select content column:\n\nContent file:\n${contentFileName}`, 458 | { 459 | type: UI.INPUT_TYPE.selection, 460 | possibleValues: languageOptions, 461 | }, 462 | (err, value) => { 463 | if (err) { 464 | // most likely the user canceled the input 465 | console.log("user cancelled input"); 466 | console.log(err, value); 467 | return; 468 | } 469 | console.log("selected language", value); 470 | 471 | selectedLanguage = value; // languageOptions[selection[1]] 472 | UI.alert( 473 | "Syncing content", 474 | "Press OK to start syncing content\n\nDepending on the number of pages, artboards and content this might take a while..." 475 | ); 476 | 477 | if (selectedLanguage) { 478 | const symbolsPage = Page.getSymbolsPage(document); 479 | const symbolsPageName = symbolsPage ? symbolsPage.name : "Symbols"; 480 | for (let page of document.pages) { 481 | // Don't add symbols page 482 | if (page.name !== symbolsPageName) { 483 | syncContentForPage(page); 484 | } 485 | } 486 | context.document.reloadInspector(); 487 | } 488 | } 489 | ); 490 | //ask language / column 491 | } else { 492 | console.log("Document contains no pages, or content file not found."); 493 | UI.message("Document contains no pages, or content file not found."); 494 | } 495 | } 496 | 497 | function syncContentForPage(page) { 498 | console.time("syncContentForPage"); 499 | console.log("syncContentForPage"); 500 | UI.message("Syncing content:", page); 501 | 502 | //page 503 | const pageName = page.name; 504 | console.log("Page:", pageName); 505 | 506 | // all artboards 507 | const artBoardLayers = sketch.find("Artboard", page); 508 | 509 | if (artBoardLayers) { 510 | console.log("ArtBoards:", artBoardLayers.length); 511 | artBoardLayers.forEach((artBoard) => { 512 | const artBoardName = artBoard.name; 513 | console.log("ArtBoard:", artBoardName); 514 | UI.message("Syncing content:", page, artBoardName); 515 | 516 | // Text 517 | const textLayers = sketch.find("Text", artBoard); 518 | 519 | if (textLayers) { 520 | console.log("Text layers:", textLayers.length); 521 | 522 | console.log(contentDictionary); 523 | 524 | textLayers.forEach((textLayer, index) => { 525 | //sync content 526 | console.log("textLayer:", textLayer.name); 527 | UI.message( 528 | "Syncing content:", 529 | page, 530 | artBoardName, 531 | `${index + 1}/${textLayers.length}` 532 | ); 533 | 534 | //1 find Sketch Content 535 | console.log("finding", pageName, artBoardName, textLayer.name); 536 | 537 | let result = _.find(contentDictionary, { 538 | page: pageName, 539 | artboard: artBoardName, 540 | key: textLayer.name, 541 | }); 542 | // console.log("result", result); 543 | 544 | //2 replace text 545 | if (result) { 546 | const resultValue = result.content[`${selectedLanguage}`]; 547 | if (resultValue) { 548 | textLayer.text = resultValue; 549 | } 550 | } else { 551 | console.log("Skipped text layer:", textLayer.name); 552 | } 553 | }); 554 | } 555 | 556 | // Symbol 557 | const symbolLayers = sketch.find("SymbolInstance", artBoard); 558 | 559 | if (symbolLayers) { 560 | console.log("Symbols:", symbolLayers.length); 561 | symbolLayers.forEach((symbolLayer, index) => { 562 | console.log("Symbol:", symbolLayer.name); 563 | UI.message( 564 | "Syncing content:", 565 | page, 566 | artBoardName, 567 | `${index + 1}/${symbolLayers.length}` 568 | ); 569 | 570 | //for each override 571 | if (symbolLayer.overrides) { 572 | symbolLayer.overrides.forEach((override) => { 573 | if ( 574 | override.affectedLayer.type === sketch.Types.Text && 575 | override.property === "stringValue" 576 | ) { 577 | const textLayerPath = layerNamesFromPath( 578 | override.path, 579 | symbolLayer 580 | ); 581 | // console.log("textLayerPath", textLayerPath); 582 | 583 | //2 find Sketch Content 584 | let key = 585 | symbolLayer.name + constants.excelDivider + textLayerPath; 586 | // console.log("finding", pageName, artBoardName, key); 587 | 588 | let result = _.find(contentDictionary, { 589 | page: pageName, 590 | artboard: artBoardName, 591 | key: key, 592 | }); 593 | // console.log("result", result); 594 | 595 | //3 replace text 596 | if (result) { 597 | const resultValue = result.content[`${selectedLanguage}`]; 598 | if (resultValue) { 599 | override.value = resultValue; 600 | } 601 | } else { 602 | console.log( 603 | "Skipped symbol: ", 604 | symbolLayer.name, 605 | textLayerPath 606 | ); 607 | } 608 | 609 | //4 auto layout 610 | symbolLayer.resizeWithSmartLayout(); 611 | } 612 | }); 613 | } 614 | }); 615 | } 616 | }); 617 | } 618 | console.log("Page:", pageName, "Done 🎉"); 619 | UI.message("Syncing content:", page, "Done 🎉"); 620 | console.timeEnd("syncContentForPage"); 621 | } 622 | 623 | export function selectContentFile(context) { 624 | console.log("selectContentFile"); 625 | var filePaths = dialog.showOpenDialogSync({ 626 | properties: ["openFile"], 627 | defaultPath: "directory", 628 | filters: [ 629 | { 630 | name: "Excel or CSV", 631 | extensions: ["xlsx", "xls", "csv"], 632 | }, 633 | ], 634 | }); 635 | console.log("filePaths", filePaths, filePaths[0], filePaths.length); 636 | if (filePaths) { 637 | var contentFile = filePaths[0]; 638 | console.log("set contentFile: ", contentFile); 639 | // set as document key 640 | Settings.setDocumentSettingForKey( 641 | document, 642 | "excelTranslateContentFile", 643 | contentFile 644 | ); 645 | UI.message( 646 | "Content file set! Now you can translate the current page or all pages." 647 | ); 648 | } else { 649 | console.log("no file selected"); 650 | UI.message( 651 | "No file selected. Select Excel or CSV file to continue. Generate a file if you don't have one." 652 | ); 653 | } 654 | } 655 | 656 | function loadData(contentFile) { 657 | // check filetype 658 | let fileType = path.extname(contentFile); 659 | switch (fileType.toLowerCase()) { 660 | case ".csv": 661 | console.log("csv"); 662 | loadExcelData(contentFile); 663 | break; 664 | // eslint-disable-next-line no-sequences 665 | case (".xls", ".xlsx"): 666 | console.log("Excel"); 667 | loadExcelData(contentFile); 668 | break; 669 | default: 670 | console.log("File format not supported.", fileType.toLowerCase()); 671 | UI.message("File format not supported."); 672 | } 673 | } 674 | 675 | function contentDocumentExists() { 676 | var contentFile = Settings.documentSettingForKey( 677 | document, 678 | "excelTranslateContentFile" 679 | ); 680 | console.log("contentFile: ", contentFile); 681 | if (contentFile) { 682 | if (fs.existsSync(contentFile)) { 683 | // console.log("file exists: ", contentFile); 684 | return true; 685 | } 686 | } else { 687 | console.log("No content file."); 688 | UI.message("Select a content document first."); 689 | return false; 690 | } 691 | } 692 | 693 | function loadExcelData(contentFile) { 694 | console.time("loadExcelData"); 695 | console.log("loadExcelData"); 696 | 697 | let xlsData = fs.readFileSync(contentFile); 698 | 699 | var workbook = XLSX.read(xlsData, { 700 | type: "buffer", 701 | }); 702 | 703 | /* Get worksheet. Only support one sheet at the moment. */ 704 | 705 | var firstSheetName = workbook.SheetNames[0]; 706 | 707 | var worksheet = workbook.Sheets[firstSheetName]; 708 | 709 | excelJson = XLSX.utils.sheet_to_json(worksheet, { range: 0, defval: "" }); 710 | 711 | console.log("excelJson"); 712 | console.log(excelJson); 713 | 714 | // get language options 715 | 716 | var keyAndLanguageOptions = Object.keys(excelJson[0]); // get language options from first row 717 | if (!keyAndLanguageOptions) { 718 | console.log("Key not found"); 719 | UI.message( 720 | "Language options not found. Ensure the top row contains a 'key' and at least one data 'column'" 721 | ); 722 | return; 723 | } 724 | console.log(keyAndLanguageOptions); 725 | 726 | languageOptions = _.without(keyAndLanguageOptions, "key"); 727 | languageOptions = _.remove(languageOptions, function (languageOption) { 728 | return !languageOption.includes("__EMPTY"); 729 | }); 730 | // languageOptions = languageOptions.filter( 731 | // ({ languageOption }) => !languageOption.includes("__EMPTY") 732 | // ); 733 | 734 | console.timeEnd("loadExcelData"); 735 | 736 | // ask for language first so we don't load all language data into the object. 737 | // console.log("showLanguageSelectionPopup"); 738 | // console.log(languageOptions); 739 | 740 | processData(); 741 | } 742 | 743 | function processData() { 744 | console.time("processData"); 745 | console.log("processData"); 746 | 747 | var currentPage = ""; 748 | var currentArboard = ""; 749 | if (excelJson) { 750 | excelJson.forEach((contentObject) => { 751 | // console.log("contentObject", contentObject); 752 | // console.log("contentObject", contentObject.key); 753 | if (contentObject.key.includes(constants.pagePrefix)) { 754 | currentPage = contentObject.key.replace(constants.pagePrefix, ""); 755 | } 756 | 757 | if (contentObject.key.includes(constants.artboardPrefix)) { 758 | currentArboard = contentObject.key.replace( 759 | constants.artboardPrefix, 760 | "" 761 | ); 762 | } 763 | 764 | //all content 765 | var allContent = {}; 766 | languageOptions.forEach((languageOption) => { 767 | if (contentObject[languageOption]) { 768 | allContent[languageOption] = String(contentObject[languageOption]); 769 | } 770 | }); 771 | 772 | // console.log("contentObject", contentObject.key); 773 | 774 | contentDictionary.push({ 775 | page: currentPage, 776 | artboard: currentArboard, 777 | key: contentObject.key, 778 | content: allContent, 779 | }); 780 | }); 781 | } 782 | 783 | console.log("contentDictionary"); 784 | console.log(contentDictionary); 785 | 786 | console.timeEnd("processData"); 787 | } 788 | 789 | // ********************** 790 | // Helper methods 791 | // ********************** 792 | // TODO: function duplicated 793 | 794 | function layerNameFromLibrary(symbol, layerID) { 795 | // console.log("layerNameFromLibrary", symbol, layerID); 796 | var nameFromLibary = null; 797 | // const symbolName = `${symbol.name}`; 798 | 799 | if (symbol.overrides) { 800 | symbol.overrides.forEach((override) => { 801 | if ( 802 | override.affectedLayer.id === layerID && 803 | (override.property === "stringValue" || 804 | override.property === "symbolID") && 805 | override.editable === true 806 | ) { 807 | // console.log(symbol.name, override.affectedLayer.name); 808 | nameFromLibary = override.affectedLayer.name; 809 | } 810 | }); 811 | } 812 | 813 | // console.log("nameFromLibary", nameFromLibary); 814 | return nameFromLibary; 815 | } 816 | 817 | function layerNamesFromPath(path, symbol) { 818 | // console.log("layerNamesFromPath path:", path); 819 | var layerNames = []; 820 | let layerIDs = path.split(constants.sketchSymbolDivider); 821 | for (let layerID of layerIDs) { 822 | let layer = document.getLayerWithID(layerID); 823 | 824 | if (layer) { 825 | let layerName = layer.name; 826 | layerNames.push(layerName); 827 | } else { 828 | //library, try find in overrides 829 | layerNames.push(layerNameFromLibrary(symbol, layerID)); 830 | } 831 | } 832 | // filter empty string 833 | // console.log("layerNamesFromPath", layerNames); 834 | layerNames = layerNames.filter(Boolean); 835 | return layerNames.join(constants.excelDivider); 836 | } 837 | --------------------------------------------------------------------------------