diff --git a/README.md b/README.md index 97c5fd2e..0a7bc008 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,11 @@ Any type of contributions are welcome. * json - [.json, .arb (Flutter Internationalization)] * yaml - [.yaml, .yml] +#### Import / export translations + +* Import translations from XLSX [.xlsx, .xls] files +* Export translations to XLSX or Comma-separated [.xlsx, .csv] files + **Feature requests and/or pull requests with new plugins are welcomed 🙂** **If you want to test the features, you can open the testData folder!** diff --git a/common/ipcMessages.ts b/common/ipcMessages.ts index 96eb439c..c3d074e7 100644 --- a/common/ipcMessages.ts +++ b/common/ipcMessages.ts @@ -13,3 +13,6 @@ export const saveSettings = 'saveSettings'; export const recentFolders = 'recentFolders'; export const closeFolder = 'closeFolder'; export const refreshFolder = 'refreshFolder'; +export const showExport = 'showExport'; +export const createXls = 'createXls'; +export const exportComplete = 'exportComplete'; diff --git a/main/events.ts b/main/events.ts index 81772c4e..ffca6df4 100644 --- a/main/events.ts +++ b/main/events.ts @@ -5,6 +5,7 @@ import { ParsedFile } from '../common/types'; import * as fileManager from './fileManager'; import * as settings from './Settings'; import * as windowManager from './windowManager'; +import { getCurrentWindow } from './windowManager' const onSave = async (e: any, data: any) => { const window = BrowserWindow.fromWebContents(e.sender); @@ -38,6 +39,23 @@ const onSave = async (e: any, data: any) => { } }; +const createXLS = async (e: any, data: Object) => { + const window = getCurrentWindow(); + if (!window) { + return false; + } + try { + const res = await fileManager.saveXls(data, window); + if (res) { + dialog.showMessageBox(window, { message: 'Export complete' }); + } + } catch (e) { + dialog.showErrorBox('Failed export', 'Failed to create xls or csv file'); + } finally { + windowManager.sendExportComplete(window) + } +} + const onOpen = (e: any, data: string) => { const window = BrowserWindow.fromWebContents(e.sender); if (!window) return; @@ -81,6 +99,7 @@ const registerAppEvents = () => { ipcMain.on(ipcMessages.saveSettings, onSaveSettings); ipcMain.on(ipcMessages.settings, onGetSettings); ipcMain.on(ipcMessages.recentFolders, onRecentFolders); + ipcMain.on(ipcMessages.createXls, createXLS); app.on('open-file', onOpenFile); app.on('will-finish-launching', () => { diff --git a/main/fileManager.ts b/main/fileManager.ts index a5668193..46015254 100644 --- a/main/fileManager.ts +++ b/main/fileManager.ts @@ -3,26 +3,62 @@ import { exists } from 'fs'; import { promisify } from 'util'; import nodeWatch from 'node-watch'; import * as _ from 'lodash/fp'; +import * as xlsx from 'xlsx'; import { LoadedFolder, LoadedGroup, LoadedPath, ParsedFile } from '../common/types'; -import { loadFolder, saveFile } from './pluginManager'; +import { loadFolder, saveFile, parseXlsx } from './pluginManager'; import * as settings from './Settings'; import { createWindow, - getAvailableWindow, + getAvailableWindow, getCurrentWindow, sendClose, sendOpen, + sendSave, sendRecentFolders, - sendRefreshFolder, -} from './windowManager'; + sendRefreshFolder +} from './windowManager' const existsAsync = promisify(exists); +let watcher: any; export const openFolder = async (folderPath: string) => { const window = getAvailableWindow() || createWindow(); await openFolderInWindow(folderPath, window); }; +export const openFile = async (filePath: string) => { + const window = getAvailableWindow() || createWindow(); + const isValidPath = await existsAsync(filePath); + if (!isValidPath) { + dialog.showMessageBox(window, { + type: 'error', + message: `File not found in the given path "${filePath}"`, + }); + } else { + const { canceled, filePaths } = await dialog.showOpenDialog({ + title: 'Select save directory', + properties: ['openDirectory'], + }); + if (canceled) { return; } + const savePath = filePaths[0]; + const parsedData = await parseXlsx(filePath, savePath); + if (parsedData.length) { + await sendOpen(window, savePath, parsedData); + sendSave(window); + watchFolder(window, savePath); + + app.addRecentDocument(savePath); + const recentFolders = settings.addRecentFolder(savePath); + sendRecentFolders(window, recentFolders); + } else { + dialog.showMessageBox(window, { + type: 'error', + message: `Data not found in the given file "${filePath}"`, + }); + } + } +}; + export const openFolderInWindow = async (folderPath: string, window: Electron.BrowserWindow) => { let recentFolders: string[]; @@ -60,6 +96,46 @@ export const saveFolder = async (data: LoadedPath[]): Promise => { ); }; +export const saveXls = async (data: any, window: Electron.BrowserWindow) => { + if (!window) { + return false + } + const file = await dialog.showSaveDialog(window, { + filters: [ + { name: 'Microsoft Excel (xlsx)', extensions: ['xlsx', 'xls'] }, + { name: 'Comma-separated values (csv)', extensions: ['csv'] }, + ], + }); + if (file.canceled) { + return false; + } + const { selectedLanguages, sheetData } = data; + const wb = xlsx.utils.book_new(); + const ws = xlsx.utils.aoa_to_sheet([['filename', 'label', ...selectedLanguages]]); + for (const fileName in sheetData) { + for (const label in sheetData[fileName]) { + let row = [fileName, label]; + selectedLanguages.map((lang: string) => { + const val = sheetData[fileName][label][lang] || ''; + row.push(val); + }); + xlsx.utils.sheet_add_aoa(ws, [ + [...row] + ], { + origin: -1, + }); + } + } + const filePath = file.filePath || '' + xlsx.utils.book_append_sheet(wb, ws); + try { + await xlsx.writeFile(wb, filePath); + return true; + } catch (e) { + throw e; + } +}; + const getParsedFiles = (data: LoadedPath[]): ParsedFile[] => data .map((it) => @@ -67,11 +143,20 @@ const getParsedFiles = (data: LoadedPath[]): ParsedFile[] => ) .flat(); +export const closeFolderWatcher = function () { + if (typeof watcher !== undefined) { + if (watcher && typeof watcher.close !== undefined) { + watcher.close(); + } + } +} + const watchFolder = (window: Electron.BrowserWindow, folderPath: string) => { const handleFileUpdate = _.debounce(1000, async () => { const parsedFiles = await loadFolder(folderPath); sendRefreshFolder(window, parsedFiles); }); - nodeWatch(folderPath, { recursive: true }, handleFileUpdate); + closeFolderWatcher(); + watcher = nodeWatch(folderPath, { recursive: true }, handleFileUpdate); }; diff --git a/main/menu/import.ts b/main/menu/import.ts new file mode 100644 index 00000000..a90a6cf3 --- /dev/null +++ b/main/menu/import.ts @@ -0,0 +1,41 @@ +import { dialog } from 'electron'; +import { openFile } from '../fileManager'; +import * as windowManager from '../windowManager'; + +const openDirectory = async () => { + const { canceled, filePaths } = await dialog.showOpenDialog({ + filters: [ + { name: 'Supported files xls / xlsx', extensions: ['xls', 'xlsx'] }, + { name: 'All Files', extensions: ['*'] }, + ], + properties: ['openFile'], + }); + if (!canceled) { + openFile(filePaths[0]); + } +}; + +const openExport = async () => { + const window = windowManager.getCurrentWindow(); + if (!window) { + return; + } + + windowManager.sendShowExport(window); +}; + +const importMenu: Electron.MenuItemConstructorOptions = { + label: 'Import / Export', + submenu: [ + { + label: 'Import From XLSX', + click: openDirectory, + }, + { + label: 'Export To XLSX or CSV', + click: openExport, + }, + ], +}; + +export default importMenu; diff --git a/main/menu/index.ts b/main/menu/index.ts index 729bd852..2543b8a3 100644 --- a/main/menu/index.ts +++ b/main/menu/index.ts @@ -6,6 +6,7 @@ import fileMenu from './file'; import helpMenu from './help'; import viewMenu from './view'; import windowMenu from './window'; +import importMenu from './import' import MenuItem = Electron.MenuItem; @@ -18,6 +19,7 @@ if (Object.keys(appMenu).length > 0) { } menuTemplate.push(fileMenu); menuTemplate.push(editMenu); +menuTemplate.push(importMenu); menuTemplate.push(viewMenu); menuTemplate.push(windowMenu); menuTemplate.push(helpMenu); diff --git a/main/pluginManager.ts b/main/pluginManager.ts index 67a8860e..fc1d95ff 100644 --- a/main/pluginManager.ts +++ b/main/pluginManager.ts @@ -2,10 +2,12 @@ import * as fs from 'fs'; import * as _ from 'lodash'; import * as path from 'path'; import * as util from 'util'; +import * as xlsx from 'xlsx'; import { getLocale } from '../common/language'; import { LoadedFolder, LoadedGroup, LoadedPath, ParsedFile } from '../common/types'; import getPlugins, { IPlugin } from './plugins'; +import { setWith, omit } from 'lodash'; const readdirAsync = util.promisify(fs.readdir); const readFileAsync = util.promisify(fs.readFile); @@ -27,6 +29,59 @@ export const loadFolder = async (folderPath: string): Promise => { return groupedFiles.concat(groupedLanguageFolders).concat(subFolders); }; +export const parseXlsx = async (filePath: string, savePath: string): Promise => { + try { + const workbook = await xlsx.readFile(filePath, { + cellHTML: false, + }); + const sheet = workbook.Sheets[workbook.SheetNames[0]]; + const sheetJson = xlsx.utils.sheet_to_json(sheet); + const sheetObject: { [key: string]: { [key: string]: ParsedFile } } = {}; + await sheetJson.map((row: any) => { + let filename: any = row.filename; + let label: any = row.label; + const languages: any = omit(row, ['filename', 'label']); + for (const lang in languages) { + if (!sheetObject[filename]) { + sheetObject[filename] = {}; + } + if (!sheetObject[filename][lang]) { + sheetObject[filename][lang] = { + fileName: filename + '_' + lang, + filePath: savePath + '\\' + filename + '_' + lang + '.json', + prefix: filename, + language: lang, + extension: '.json', + data: {}, + } as ParsedFile; + + try { + writeFileAsync(savePath + '\\' + filename + '_' + lang + '.json', '{}'); + } catch (e) {} + + } + setWith(sheetObject[filename][lang].data, label, languages[lang], Object); + } + }); + const parsedFiles: LoadedGroup[] = []; + const files = Object.keys(sheetObject); + await files.map((filename) => { + const items: ParsedFile[] = []; + for (const k in sheetObject[filename]) { + items.push(sheetObject[filename][k]); + } + parsedFiles.push({ + type: 'file', + name: filename, + items: items, + } as LoadedGroup); + }); + return parsedFiles; + } catch (e) { + return []; + } +}; + export const parseFile = async (filePath: string): Promise => { try { const fileContent = await readFileAsync(filePath); @@ -53,7 +108,6 @@ export const saveFile = async (parsedFile: ParsedFile): Promise => { const data = await plugin.parse(fileContent.toString()); const updatedData = mergeDrop(data, parsedFile.data); - const serializedContent = await plugin.serialize(updatedData); if (serializedContent === null) { return false; diff --git a/main/plugins/json/index.ts b/main/plugins/json/index.ts index e5cf6598..a9ddd543 100644 --- a/main/plugins/json/index.ts +++ b/main/plugins/json/index.ts @@ -10,7 +10,7 @@ export const parse = (content: string): Promise => { export const serialize = async (data: object): Promise => { try { - return JSON.stringify(data, Object.keys(data).sort(), 2); + return JSON.stringify(data, undefined, 2); } catch (e) { return undefined; } diff --git a/main/windowManager.ts b/main/windowManager.ts index c46a8b08..58d6f251 100644 --- a/main/windowManager.ts +++ b/main/windowManager.ts @@ -6,6 +6,7 @@ import * as ipcMessages from '../common/ipcMessages'; import { LoadedPath } from '../common/types'; import { getFormattedFoldersPaths } from './pathUtils'; import * as settings from './Settings'; +import { closeFolderWatcher } from './fileManager' export const hasWindows = (): boolean => BrowserWindow.getAllWindows().length > 0; @@ -73,6 +74,7 @@ export const sendRecentFolders = (window: BrowserWindow, data: string[]) => { }; export const sendClose = (window: BrowserWindow) => { + closeFolderWatcher(); sendToIpc(window, ipcMessages.closeFolder, {}); }; @@ -86,6 +88,14 @@ const sendToIpc = (window: BrowserWindow, message: string, data?: any) => { } }; +export const sendShowExport = (window: BrowserWindow) => { + sendToIpc(window, ipcMessages.showExport); +}; + +export const sendExportComplete = (window: BrowserWindow) => { + sendToIpc(window, ipcMessages.exportComplete) +}; + export enum SaveResponse { Save, Cancel, diff --git a/package.json b/package.json index a52e4278..3f88614a 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "node-watch": "^0.6.3", "rimraf": "^3.0.2", "roboto-fontface": "*", + "snyk": "^1.316.1", "vue": "^2.6.11", "vue-class-component": "^7.2.3", "vue-router": "^3.1.6", @@ -81,7 +82,7 @@ "vuex": "^3.1.3", "vuex-class": "^0.3.2", "vuex-module-decorators": "^0.16.1", - "snyk": "^1.316.1" + "xlsx": "^0.16.7" }, "browserslist": [ "last 2 Chrome versions" diff --git a/src/App.vue b/src/App.vue index 8bce9fd9..d13ec255 100644 --- a/src/App.vue +++ b/src/App.vue @@ -5,6 +5,7 @@ + @@ -12,11 +13,13 @@ import { provideStore } from '@/store/utils'; import { defineComponent } from '@vue/composition-api'; import Settings from '@/settings/views/Settings.vue'; + import Export from '@/export/Export.vue'; export default defineComponent({ name: 'App', components: { Settings, + Export }, setup(props, ctx) { provideStore(ctx.root.$store); diff --git a/src/export/Export.vue b/src/export/Export.vue new file mode 100644 index 00000000..fca9d605 --- /dev/null +++ b/src/export/Export.vue @@ -0,0 +1,170 @@ + + + + + diff --git a/src/export/store.ts b/src/export/store.ts new file mode 100644 index 00000000..ee001453 --- /dev/null +++ b/src/export/store.ts @@ -0,0 +1,37 @@ +import { Action, Module, Mutation, VuexModule } from 'vuex-module-decorators'; + +import { sendIpc } from '@/store/plugins/ipc'; +import * as ipcMessages from '@common/ipcMessages'; + +@Module({ + namespaced: true, +}) +export default class ExportModule extends VuexModule { + isExportVisible = false; + isLoading = false; + + @Action + createXLS(data: Object) { + sendIpc(ipcMessages.createXls, data); + } + + @Mutation + showExport() { + this.isExportVisible = true; + } + + @Mutation + hideExport() { + this.isExportVisible = false; + } + + @Mutation + showLoading() { + this.isLoading = true; + } + + @Mutation + hideLoading() { + this.isLoading = false; + } +} diff --git a/src/folder/store.ts b/src/folder/store.ts index 9680c5b0..3ea4a3ff 100644 --- a/src/folder/store.ts +++ b/src/folder/store.ts @@ -68,6 +68,7 @@ export default class FolderModule extends VuexModule { commit('setSelectedItem', null); commit('setModifiedContent', false); commit('setClipboard', { item: null, action: null }); + await dispatch('createLanguageList'); await dispatch('sendModifiedContent'); } diff --git a/src/store/index.ts b/src/store/index.ts index e8d53fb4..60444c61 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -7,6 +7,7 @@ import global from './modules/global'; import home from '@/home/store'; import folder from '@/folder/store'; import settings from '@/settings/store'; +import exportModule from '@/export/store' Vue.use(Vuex); @@ -20,6 +21,7 @@ export default new Vuex.Store({ home, folder, settings, + export: exportModule }, plugins: [ipcPlugin], }); diff --git a/src/store/plugins/ipc.ts b/src/store/plugins/ipc.ts index 2481a954..cd9125e0 100644 --- a/src/store/plugins/ipc.ts +++ b/src/store/plugins/ipc.ts @@ -85,10 +85,21 @@ export class IpcModule extends VuexModule { @Action closeFolder() { this.context.dispatch('folder/closeFolder', undefined, { root: true }); + this.context.commit('export/hideExport', undefined, { root: true }); } @Action refreshFolder(data: LoadedPath[]) { this.context.dispatch('folder/refreshFolder', data, { root: true }); } + + @Action + showExport() { + this.context.commit('export/showExport', undefined, { root: true }); + } + + @Action + exportComplete() { + this.context.commit('export/hideLoading', undefined, { root: true }); + } } diff --git a/testData/import/import_example.xlsx b/testData/import/import_example.xlsx new file mode 100644 index 00000000..18fbc177 Binary files /dev/null and b/testData/import/import_example.xlsx differ diff --git a/yarn.lock b/yarn.lock index 81c143aa..3f262d59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1181,6 +1181,14 @@ address@^1.1.2: resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6" integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA== +adler-32@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/adler-32/-/adler-32-1.2.0.tgz#6a3e6bf0a63900ba15652808cb15c6813d1a5f25" + integrity sha1-aj5r8KY5ALoVZSgIyxXGgT0aXyU= + dependencies: + exit-on-epipe "~1.0.1" + printj "~1.1.0" + agent-base@4, agent-base@^4.2.0, agent-base@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" @@ -2117,6 +2125,15 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= +cfb@^1.1.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/cfb/-/cfb-1.2.0.tgz#6a4d0872b525ed60349e1ef51fb4b0bf73eca9a8" + integrity sha512-sXMvHsKCICVR3Naq+J556K+ExBo9n50iKl6LGarlnvuA2035uMlGA/qVrc0wQtow5P1vJEw9UyrKLCbtIKz+TQ== + dependencies: + adler-32 "~1.2.0" + crc-32 "~1.2.0" + printj "~1.1.2" + chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -2402,6 +2419,14 @@ code-point-at@^1.0.0: resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= +codepage@~1.14.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/codepage/-/codepage-1.14.0.tgz#8cbe25481323559d7d307571b0fff91e7a1d2f99" + integrity sha1-jL4lSBMjVZ19MHVxsP/5HnodL5k= + dependencies: + commander "~2.14.1" + exit-on-epipe "~1.0.1" + collection-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" @@ -2462,7 +2487,7 @@ commander@2.15.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag== -commander@2.17.x: +commander@2.17.x, commander@~2.17.1: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== @@ -2472,6 +2497,11 @@ commander@^2.12.1, commander@^2.18.0, commander@^2.20.0, commander@^2.x: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commander@~2.14.1: + version "2.14.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.14.1.tgz#2235123e37af8ca3c65df45b026dbd357b01b9aa" + integrity sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw== + commander@~2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" @@ -2661,6 +2691,14 @@ cosmiconfig@^5.0.0: js-yaml "^3.13.1" parse-json "^4.0.0" +crc-32@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.0.tgz#cb2db6e29b88508e32d9dd0ec1693e7b41a18208" + integrity sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA== + dependencies: + exit-on-epipe "~1.0.1" + printj "~1.1.0" + create-ecdh@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff" @@ -3903,6 +3941,11 @@ execa@^3.3.0: signal-exit "^3.0.2" strip-final-newline "^2.0.0" +exit-on-epipe@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692" + integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw== + expand-brackets@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" @@ -4294,6 +4337,11 @@ forwarded@~0.1.2: resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= +frac@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/frac/-/frac-1.1.2.tgz#3d74f7f6478c88a1b5020306d747dc6313c74d0b" + integrity sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA== + fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -7953,6 +8001,11 @@ pretty-error@^2.0.2: renderkid "^2.0.1" utila "~0.4" +printj@~1.1.0, printj@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222" + integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ== + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -9463,6 +9516,13 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= +ssf@~0.11.2: + version "0.11.2" + resolved "https://registry.yarnpkg.com/ssf/-/ssf-0.11.2.tgz#0b99698b237548d088fc43cdf2b70c1a7512c06c" + integrity sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g== + dependencies: + frac "~1.1.2" + ssh2-streams@~0.4.10: version "0.4.10" resolved "https://registry.yarnpkg.com/ssh2-streams/-/ssh2-streams-0.4.10.tgz#48ef7e8a0e39d8f2921c30521d56dacb31d23a34" @@ -10850,11 +10910,21 @@ windows-release@^3.1.0: dependencies: execa "^1.0.0" +wmf@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wmf/-/wmf-1.0.2.tgz#7d19d621071a08c2bdc6b7e688a9c435298cc2da" + integrity sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw== + word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +word@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/word/-/word-0.3.0.tgz#8542157e4f8e849f4a363a288992d47612db9961" + integrity sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA== + worker-farm@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" @@ -10929,6 +10999,21 @@ xdg-basedir@^4.0.0: resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== +xlsx@^0.16.7: + version "0.16.7" + resolved "https://registry.yarnpkg.com/xlsx/-/xlsx-0.16.7.tgz#62fd6590addac7c4419daaaa2b0c5388015d5f69" + integrity sha512-Xc4NRjci2Grbh9NDk/XoaWycJurxEug1wwn0aJCmB0NvIMyQuHYq2muWLWGidYNZPf94aUbqm6K8Fbjd7gKTZg== + dependencies: + adler-32 "~1.2.0" + cfb "^1.1.4" + codepage "~1.14.0" + commander "~2.17.1" + crc-32 "~1.2.0" + exit-on-epipe "~1.0.1" + ssf "~0.11.2" + wmf "~1.0.1" + word "~0.3.0" + xml2js@0.4.23, xml2js@^0.4.17, xml2js@^0.4.9: version "0.4.23" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66"