Hello everyone 👋🏻 , I've created a translation widget. It translates any file that matches the selected file type from a specified directory and its sub-directories (check env variables) from Greek letters to Greeklish. It can be helpful when we use apps that strangle to read files with greek characters on their filenames.
For example: Therefore, we need to have all our files converted to a Greeklish format using Latin characters.
I hope some of you find this useful 🤞🏻.
ℹ️ Hint: You can even replace the greekToGreeklishMap
to a custom translation map and make your own filename renames.
⚠️ Use this with caution. It may rename a large number of file names and make unwanted changes to your file names. ⚠️
https://user-images.githubusercontent.com/50381321/200197611-bde2c6d8-d7a0-468f-9c7e-d050a64aeb1b.mov
Environment variables:
ROOT_TRANSLATION_PATH=/Users/{{user}}TRANSLATION_VERBOSE_LOGS=yesREAD_FILES_RECURSIVELY_FOR_CHILDREN_DIRS=trueOVERRIDE_FILE_TRANSLATION_LIMIT=2500
import '@johnlindquist/kit';import fs from 'fs';// ====== APPLICATION CONFIGURATION START ======const greekToGreeklishMap = [{ find: '́', replace: '' },{ find: 'ΓΧ', replace: 'GX' },{ find: 'γχ', replace: 'gx' },{ find: 'ΤΘ', replace: 'T8' },{ find: 'τθ', replace: 't8' },{ find: '(θη|Θη)', replace: '8h' },{ find: 'ΘΗ', replace: '8H' },{ find: 'αυ', replace: 'au' },{ find: 'Αυ', replace: 'Au' },{ find: 'ΑΥ', replace: 'AY' },{ find: 'ευ', replace: 'eu' },{ find: 'εύ', replace: 'eu' },{ find: 'εϋ', replace: 'ey' },{ find: 'εΰ', replace: 'ey' },{ find: 'Ευ', replace: 'Eu' },{ find: 'Εύ', replace: 'Eu' },{ find: 'Εϋ', replace: 'Ey' },{ find: 'Εΰ', replace: 'Ey' },{ find: 'ΕΥ', replace: 'EY' },{ find: 'ου', replace: 'ou' },{ find: 'ού', replace: 'ou' },{ find: 'οϋ', replace: 'oy' },{ find: 'οΰ', replace: 'oy' },{ find: 'Ου', replace: 'Ou' },{ find: 'Ού', replace: 'Ou' },{ find: 'Οϋ', replace: 'Oy' },{ find: 'Οΰ', replace: 'Oy' },{ find: 'ΟΥ', replace: 'OY' },{ find: 'Α', replace: 'A' },{ find: 'α', replace: 'a' },{ find: 'ά', replace: 'a' },{ find: 'Ά', replace: 'A' },{ find: 'Β', replace: 'B' },{ find: 'β', replace: 'b' },{ find: 'Γ', replace: 'G' },{ find: 'γ', replace: 'g' },{ find: 'Δ', replace: 'D' },{ find: 'δ', replace: 'd' },{ find: 'Ε', replace: 'E' },{ find: 'ε', replace: 'e' },{ find: 'έ', replace: 'e' },{ find: 'Έ', replace: 'E' },{ find: 'Ζ', replace: 'Z' },{ find: 'ζ', replace: 'z' },{ find: 'Η', replace: 'H' },{ find: 'η', replace: 'h' },{ find: 'ή', replace: 'h' },{ find: 'Ή', replace: 'H' },{ find: 'Θ', replace: 'TH' },{ find: 'θ', replace: 'th' },{ find: 'Ι', replace: 'I' },{ find: 'Ϊ', replace: 'I' },{ find: 'ι', replace: 'i' },{ find: 'ί', replace: 'i' },{ find: 'ΐ', replace: 'i' },{ find: 'ϊ', replace: 'i' },{ find: 'Ί', replace: 'I' },{ find: 'Κ', replace: 'K' },{ find: 'κ', replace: 'k' },{ find: 'Λ', replace: 'L' },{ find: 'λ', replace: 'l' },{ find: 'Μ', replace: 'M' },{ find: 'μ', replace: 'm' },{ find: 'Ν', replace: 'N' },{ find: 'ν', replace: 'n' },{ find: 'Ξ', replace: 'KS' },{ find: 'ξ', replace: 'ks' },{ find: 'Ο', replace: 'O' },{ find: 'ο', replace: 'o' },{ find: 'Ό', replace: 'O' },{ find: 'ό', replace: 'o' },{ find: 'Π', replace: 'P' },{ find: 'π', replace: 'p' },{ find: 'Ρ', replace: 'R' },{ find: 'ρ', replace: 'r' },{ find: 'Σ', replace: 'S' },{ find: 'σ', replace: 's' },{ find: 'Τ', replace: 'T' },{ find: 'τ', replace: 't' },{ find: 'Υ', replace: 'Y' },{ find: 'Ύ', replace: 'Y' },{ find: 'Ϋ', replace: 'Y' },{ find: 'ΰ', replace: 'y' },{ find: 'ύ', replace: 'y' },{ find: 'ϋ', replace: 'y' },{ find: 'υ', replace: 'y' },{ find: 'Φ', replace: 'F' },{ find: 'φ', replace: 'f' },{ find: 'Χ', replace: 'X' },{ find: 'χ', replace: 'x' },{ find: 'Ψ', replace: 'Ps' },{ find: 'ψ', replace: 'ps' },{ find: 'Ω', replace: 'w' },{ find: 'ω', replace: 'w' },{ find: 'Ώ', replace: 'w' },{ find: 'ώ', replace: 'w' },{ find: 'ς', replace: 's' },{ find: ';', replace: '?' },];const extensions = {'1.ada': ['code'],'2.ada': ['code'],'3dm': ['image'],'3ds': ['image'],'3g2': ['video'],'3gp': ['video'],'7z': ['archive'],a: ['archive'],aac: ['audio'],aaf: ['video'],ada: ['code'],adb: ['code'],ads: ['code'],ai: ['image'],aiff: ['audio'],ape: ['audio'],apk: ['archive'],ar: ['archive'],asf: ['video'],asm: ['code'],au: ['audio'],avchd: ['video'],avi: ['video'],azw: ['book'],azw1: ['book'],azw3: ['book'],azw4: ['book'],azw6: ['book'],bas: ['code'],bash: ['code', 'exec'],bat: ['code', 'exec'],bin: ['exec'],bmp: ['image'],bz2: ['archive'],c: ['code'],'c++': ['code'],cab: ['archive'],cbl: ['code'],cbr: ['book'],cbz: ['book'],cc: ['code'],class: ['code'],clj: ['code'],cob: ['code'],command: ['exec'],cpio: ['archive'],cpp: ['code'],crx: ['exec'],cs: ['code'],csh: ['code', 'exec'],css: ['web'],csv: ['sheet'],cxx: ['code'],d: ['code'],dds: ['image'],deb: ['archive'],diff: ['code'],dmg: ['archive'],doc: ['text'],docx: ['text'],drc: ['video'],dwg: ['image'],dxf: ['image'],e: ['code'],ebook: ['text'],egg: ['archive'],el: ['code'],eot: ['font'],eps: ['image'],epub: ['book'],exe: ['exec'],f: ['code'],f77: ['code'],f90: ['code'],fish: ['code', 'exec'],flac: ['audio'],flv: ['video'],for: ['code'],fth: ['code'],ftn: ['code'],gif: ['image'],go: ['code'],gpx: ['image'],groovy: ['code'],gsm: ['audio'],gz: ['archive'],h: ['code'],hh: ['code'],hpp: ['code'],hs: ['code'],htm: ['code', 'web'],html: ['code', 'web'],hxx: ['code'],ics: ['sheet'],iso: ['archive'],it: ['audio'],jar: ['archive'],java: ['code'],jpeg: ['image'],jpg: ['image'],js: ['code', 'web'],jsp: ['code'],jsx: ['code', 'web'],kml: ['image'],kmz: ['image'],ksh: ['code', 'exec'],kt: ['code'],less: ['web'],lha: ['archive'],lhs: ['code'],lisp: ['code'],log: ['text'],lua: ['code'],m: ['code'],m2v: ['video'],m3u: ['audio'],m4: ['code'],m4a: ['audio'],m4p: ['video'],m4v: ['video'],mar: ['archive'],max: ['image'],md: ['text'],mid: ['audio'],mkv: ['video'],mng: ['video'],mobi: ['book'],mod: ['audio'],mov: ['video'],mp2: ['video'],mp3: ['audio'],mp4: ['video'],mpa: ['audio'],mpe: ['video'],mpeg: ['video'],mpg: ['video'],mpv: ['video'],msg: ['text'],msi: ['exec'],mxf: ['video'],nim: ['code'],nsv: ['video'],odp: ['slide'],ods: ['sheet'],odt: ['text'],ogg: ['video'],ogm: ['video'],ogv: ['video'],org: ['text'],otf: ['font'],pages: ['text'],pak: ['archive'],patch: ['code'],pdf: ['text'],pea: ['archive'],php: ['code', 'web'],pl: ['code'],pls: ['audio'],png: ['image'],po: ['code'],pp: ['code'],ppt: ['slide'],ps: ['image'],psd: ['image'],py: ['code'],qt: ['video'],r: ['code'],ra: ['audio'],rar: ['archive'],rb: ['code'],rm: ['video'],rmvb: ['video'],roq: ['video'],rpm: ['archive'],rs: ['code'],rst: ['text'],rtf: ['text'],s: ['code'],s3m: ['audio'],s7z: ['archive'],scala: ['code'],scss: ['web'],sh: ['code', 'exec'],shar: ['archive'],sid: ['audio'],srt: ['video'],svg: ['image'],svi: ['video'],swg: ['code'],swift: ['code'],tar: ['archive'],tbz2: ['archive'],tex: ['text'],tga: ['image'],tgz: ['archive'],thm: ['image'],tif: ['image'],tiff: ['image'],tlz: ['archive'],ttf: ['font'],txt: ['text'],v: ['code'],vb: ['code'],vcf: ['sheet'],vcxproj: ['code'],vob: ['video'],war: ['archive'],wasm: ['web'],wav: ['audio'],webm: ['video'],webp: ['image'],whl: ['archive'],wma: ['audio'],wmv: ['video'],woff: ['font'],woff2: ['font'],wpd: ['text'],wps: ['text'],xcf: ['image'],xcodeproj: ['code'],xls: ['sheet'],xlsx: ['sheet'],xm: ['audio'],xml: ['code'],xpi: ['archive'],xz: ['archive'],yuv: ['image', 'video'],zip: ['archive'],zipx: ['archive'],zsh: ['code', 'exec'],};// ====== APPLICATION CONFIGURATION END ======// ====== APPLICATION MAIN CODEBASE START =======const CHECKS = {YES: 'yes',NO: 'no',};const allExtensionsValues = Object.values(extensions).flatMap((type) => type);const allExtensionEntries = Object.entries(extensions);let limitErrorState = {hasLimitError: false,lastFilesCount: 0,};let fileNameConfigList = [];let shouldContinueTranslation = CHECKS.YES;let shouldContinueNextFolder = CHECKS.YES;const { translationPath, fileType } = await initTranslationApp();const fileTranslationLimit = await env('OVERRIDE_FILE_TRANSLATION_LIMIT');const shouldLogVerboseLogs = await env('TRANSLATION_VERBOSE_LOGS');const shouldReadFilesRecursively = await env('READ_FILES_RECURSIVELY_FOR_CHILDREN_DIRS');const fileTranslationLimitNumber = +fileTranslationLimit || 2500;const fileNames = await getFilesNames(translationPath,fileType,fileTranslationLimitNumber,shouldReadFilesRecursively === 'true');const width = 700;const height = 600;const initState = {errorMessage: null,successMessage: null,loading: false,hasFiles: !!fileNames.length,limitError: limitErrorState.hasLimitError,};const widgetConfig = {alwaysOnTop: true,title: 'Music files translation widget',width,height,center: true,state: initState,backgroundColor: '#0284C7',};const translationWidget = await widget(`<div v-if="!limitError" class="flex flex-col"><div v-if="!hasFiles"><h1>No files for translation found in the selected directory. 🙁</h1></div><div v-if="hasFiles" class="p-2"><h3 class="font-bold text-sky-900 text-center">Base folder: ${translationPath}</h3><div v-if="successMessage"><span class="text-emerald-500 font-bold text-lg">{{successMessage}}</span><button id="open-translated-dir" class="button bg-amber-100 text-amber-700 p-2 m-4 rounded">Open directory</button></div><div v-else><h1 class="text-sky-900 text-center">⚠️ ${fileNames.length} files will be renamed and translated to greeklish!</h1></div><div v-if="loading" class="loader">Loading ...</div><div v-if="errorMessage" class="text-rose-600 font-bold text-lg">{{errorMessage}}</div></div><hr/><div v-if="!successMessage && hasFiles" class="overflow-scroll max-h-80"><h3 class="text-sky-900">Files preview list</h3><ul role="list">${fileNames.map((f) => `<li class="list-none drop-shadow-md mb-5 border-b-2 border-neutral-900 border-solid"><div class="text-amber-100 italic">${f.oldName}</div><strong class="text-amber-800">⚠️ will be renamed to ⚠️</strong><div class="text-lime-200">${f.newName}</div></li>`).join(' ')}</ul></div><div class="overflow-scroll max-h-96" v-else><ul v-if="hasFiles">${fileNames.map((f) => `<li class="list-none drop-shadow-md mb-5 border-b-2 border-neutral-900 border-solid"><strong>⭐️ New file name ⭐️ </strong><div class="text-orange-200">${f.newName}</div><hr/></li>`).join(' ')}</ul></div><div v-if="!successMessage && !loading && hasFiles"><button id="rename-songs" class="button bg-amber-100 text-amber-700 p-2 m-4 rounded">Translate files</button></div></div><div v-else class="p-2"><h1 class="text-rose-500">MAX LIMIT ERROR: Too many files parsed (${limitErrorState.lastFilesCount})!</h1><p class="text-orange-200">Current limit set to ${fileTranslationLimitNumber}</p></div>`,widgetConfig);translationWidget.onClick(async (data) => {if (data.targetId === 'open-translated-dir') await $`open ${translationPath}`;if (data.targetId === 'rename-songs' && !limitErrorState.hasLimitError) {const hasFiles = !!fileNames.length;try {translationWidget.setState({ ...initState, loading: true, hasFiles });logVerboseInfo(shouldLogVerboseLogs);renameMp3Files(fileNames, async () => {const successMessage = `${fileNames.length} files have been renamed successfully!`;translationWidget.setState({...initState,hasFiles,successMessage,loading: false,});console.log(successMessage);});} catch (error) {const errorMessage = `An error has occurred during translation! Error: ${error}`;translationWidget.setState({...initState,errorMessage,hasFiles,loading: false,});console.error(errorMessage);}}});// ====== APPLICATION MAIN CODEBASE END =======// ====== APPLICATION UTIL FUNCTIONS START =======async function initTranslationApp() {const ROOT_TRANSLATION_PATH = await env('ROOT_TRANSLATION_PATH');const uniqueFileTypes = new Set(allExtensionsValues);const fileType = await arg('Select a file type to translate.', Array.from(uniqueFileTypes));const translationPath = await path({startPath: ROOT_TRANSLATION_PATH,hint: 'Please add the specific folder path of files you want to translate.',onlyDirs: true,});return { translationPath, fileType };}async function getFilesNames(dir, fileType, fileTranslationLimit, shouldReadFilesRecursively = true) {if (fileNameConfigList.length >= fileTranslationLimit) {limitErrorState = {...limitErrorState,hasLimitError: true,lastFilesCount: fileNameConfigList.length,};return [];}if (shouldContinueNextFolder === CHECKS.NO) {return fileNameConfigList;}const files = await readdir(dir);for (const fileName of files) {if (shouldContinueTranslation === CHECKS.NO) return [];const nextDir = getNextDir(dir, fileName);const isDirectory = await isDir(nextDir);if (isDirectory && shouldReadFilesRecursively) {fileNameConfigList = await getFilesNames(nextDir, fileType, fileTranslationLimit);} else {const isOsxFile = fileName.startsWith('._');const shouldAddFileToList = isValidFileExtension(fileName, fileType) && !isOsxFile;if (shouldAddFileToList) {const translatedMp3Name = translateToGreeklish(fileName);fileNameConfigList.push({oldName: fileName,newName: translatedMp3Name,oldSrc: path.join(dir, fileName),newSrc: path.join(dir, translatedMp3Name),});}}}return fileNameConfigList;}function getExtensionTypes(fileType) {const isValidExtensionType = allExtensionsValues.some((type) => type === fileType);if (isValidExtensionType)return allExtensionEntries.map(([ext, types]) => {if (types.includes(fileType)) return ext;return null;}).filter((item) => !!item);}function replaceText(text, replacementMap, isExactMatch = false, ignoreCharacters = '', regExOptions = 'g') {let regexString, regex;if (typeof text === 'string' && text.length > 0) {replacementMap.forEach(function (replacementItem) {if (isExactMatch) {regexString = replacementItem.find;} else {regexString = '[' + replacementItem.find + ']';}if (ignoreCharacters !== '') {regexString = '(?![' + ignoreCharacters + '])' + regexString;}regex = new RegExp(regexString, regExOptions);text = text.replace(regex, replacementItem.replace);});}return text;}function getNextDir(dirName, fileName) {return path.join(dirName, fileName);}function translateToGreeklish(mp3Text) {return replaceText(mp3Text, greekToGreeklishMap, true);}function isValidFileExtension(fileName, fileType = 'audio') {const splittedAudioFile = fileName.split('.');if (!splittedAudioFile.length) return false;const fileExtension = splittedAudioFile[splittedAudioFile.length - 1];return getExtensionTypes(fileType).includes(fileExtension);}function renameMp3Files(filesConfigList, onRenamingCompletion) {if (filesConfigList.length)filesConfigList.forEach((f, i) => {fs.rename(f.oldSrc, f.newSrc, (err) => {if (err) console.error(err);if (i === filesConfigList.length - 1) onRenamingCompletion();});});}function logVerboseInfo(shouldLogVerboseLogs) {if (shouldLogVerboseLogs === 'yes') {console.log(`RENAMING ${fileNames.length} FILES......`);fileNames.forEach((config) => {console.log(`${config.oldName} =====> ${config.newName}`);});}}// ====== APPLICATION UTIL FUNCTIONS END =======